Skip to content

Commit 72e5d13

Browse files
committed
Added nav arrows
1 parent 6cf4ed7 commit 72e5d13

9 files changed

Lines changed: 134 additions & 7 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ A component appearing here doesn't mean it's actively used in the default repo
136136
| `IconButton` | Accessible icon-only button: consistent reset, hover state, required `aria-label`. | `label`, `active?`, `className?`, `children`, plus any native `<button>` props | Use this instead of hand-rolling a `<button>` with an SVG — callers still add their own className for size/color. |
137137
| `PrivateDisclaimer` | Lock icon + "Only you can see this conversation" strip. | `text?` (override the default copy) | Used inside private message bubbles and above the agents-rail chat thread. |
138138
| `TypingIndicator` | Canonical "X is typing" UI — avatar + 3 bouncing dots. **Whenever a task says "typing indicator", it means this.** | `contact`, `avatarSize = 32`, `dotSize = 5`, `className?` | Component owns only the avatar + dots visual; positioning is the caller's job. Main canvas: floats above `Compose` with the avatar's bottom ~10% tucked behind the compose box top edge (see `.chat-compose-area` / `.chat-compose-typing` in `ChatView.css`). Agents rail: in-flow above the rail compose. Add new surface-specific CSS rather than forking the component. |
139+
| `DemoArrow` | Red pulsing arrow that cues the viewer to the next click target during a prototype walkthrough. **Whenever a task says "red arrow" / "demo arrow" / "hint arrow" to guide the viewer, it means this.** | `direction: 'left' \| 'right' \| 'up' \| 'down'`, `size = 24`, `className?` | Owns only the visual + pulse animation (uses the `translate` CSS property so caller `transform` doesn't collide). Caller handles absolute positioning near the target and should set `pointer-events: none` on the wrapper so clicks still land on the target. **Ephemeral**: track show/hide state wherever the target's click handler lives and unmount once the target is clicked. See `DESIGN_GUIDE.md` → "Demo Hint Arrows". |
139140
| `Icon` library | Shared SVG icons exported from `components/common/Icon.jsx`: `Close`, `Plus`, `ChevronDown`, `ChevronLeft`, `Send`, `Clock`, `Search`, `Dots`, `EmojiAdd`, `Edit`, `Lock`. | Each takes `size`; stroked icons (`Clock`, `EmojiAdd`, `Edit`) also take `stroke`. | When adding a new icon, start at [fluenticons.co](https://fluenticons.co/) (Fluent UI — matches Teams). Add it here instead of inlining SVG in a feature component. |
140141

141142
## Key Conventions

DESIGN_GUIDE.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,17 @@ Channels appear in the chat list grouped under their parent team:
187187

188188
## Demo Hint Arrows
189189

190-
Red pulsing arrows (`#E8173A`) cue the viewer where to click next during the demo. They animate with a ~1.5s opacity + translate nudge pointing *toward* the target:
190+
Red pulsing arrows (`#E8173A`) cue the viewer to the next click target during a prototype walkthrough. Use the reusable **`DemoArrow`** component in `components/common/` — do not inline a new SVG arrow.
191191

192-
- Main compose send button (initial `/Jira` draft)
193-
- Main compose (after Jira flow completes, prefilled recap reply)
192+
**Visual spec:**
193+
- Color: `#E8173A`
194+
- Animation: ~1.5s opacity (`1` → `0.4`) + `translate` nudge (3px) toward the target
195+
- Directions: `'left' | 'right' | 'up' | 'down'`
196+
- Size: 24px default, caller-tunable
194197

195-
These are gated by `JIRA_FLOW_ENABLED` in `ChatView.jsx` — the CSS is retained so re-enabling the flow doesn't require restoring styles.
198+
**Usage rules:**
199+
- **Positioning is the caller's job.** Typically `position: absolute` anchored near the target, inside a wrapper with `pointer-events: none` so clicks pass through to the target underneath. Wrap `DemoArrow` in a positioning `<span>` / `<div>` rather than relying on the arrow's own layout.
200+
- **Arrows are ephemeral.** Track a `show/hide` state wherever the target's click handler lives and unmount the arrow once the target is clicked so it doesn't linger after the action is taken. Clearing persistently (rather than toggling) matches demo/tutorial conventions.
201+
- One arrow per target at a time — don't stack hints.
202+
203+
The disabled scripted Jira flow in `ChatView.jsx` retains pre-`DemoArrow` inline arrow styles (`.main-compose-hint-arrow`, `.demo-hint-arrow-send` in `Compose.css`; `.agent-chat-hint-arrow` in `AgentsRail.css`) as a reference pattern only. Any new walkthrough should use `DemoArrow` instead.
Lines changed: 11 additions & 0 deletions
Loading
Lines changed: 14 additions & 0 deletions
Loading

src/components/ChatList.jsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import './ChatList.css'
66

77
// Small rounded-square team icon used in the Teams & channels section. Mirrors
88
// the channel avatar treatment but is standalone (no status dot, no agent
9-
// logo) so we don't need to add a synthetic contact for each team.
9+
// logo) so we don't need to add a synthetic contact for each team. Renders
10+
// `team.avatar` if set, otherwise falls back to initials over `team.color`.
1011
function TeamIcon({ team, size = 20 }) {
1112
const radius = Math.max(3, Math.round(size * 0.18))
1213
return (
@@ -15,12 +16,20 @@ function TeamIcon({ team, size = 20 }) {
1516
style={{
1617
width: size,
1718
height: size,
18-
background: team.color,
19+
background: team.avatar ? 'transparent' : team.color,
1920
borderRadius: radius,
2021
fontSize: size * 0.4,
2122
}}
2223
>
23-
{team.initials}
24+
{team.avatar ? (
25+
<img
26+
src={team.avatar}
27+
alt=""
28+
style={{ width: size, height: size, borderRadius: radius, display: 'block' }}
29+
/>
30+
) : (
31+
team.initials
32+
)}
2433
</div>
2534
)
2635
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.demo-arrow {
2+
color: #E8173A;
3+
display: inline-flex;
4+
align-items: center;
5+
pointer-events: none;
6+
}
7+
8+
/* Pulse nudges the arrow toward its target. Uses the `translate`
9+
property (not `transform`) so callers can apply their own transforms
10+
for positioning without clobbering the animation. */
11+
.demo-arrow-left { animation: demo-arrow-pulse-left 1.5s ease-in-out infinite; }
12+
.demo-arrow-right { animation: demo-arrow-pulse-right 1.5s ease-in-out infinite; }
13+
.demo-arrow-up { animation: demo-arrow-pulse-up 1.5s ease-in-out infinite; }
14+
.demo-arrow-down { animation: demo-arrow-pulse-down 1.5s ease-in-out infinite; }
15+
16+
@keyframes demo-arrow-pulse-left {
17+
0%, 100% { translate: 0 0; opacity: 1; }
18+
50% { translate: -3px 0; opacity: 0.4; }
19+
}
20+
21+
@keyframes demo-arrow-pulse-right {
22+
0%, 100% { translate: 0 0; opacity: 1; }
23+
50% { translate: 3px 0; opacity: 0.4; }
24+
}
25+
26+
@keyframes demo-arrow-pulse-up {
27+
0%, 100% { translate: 0 0; opacity: 1; }
28+
50% { translate: 0 -3px; opacity: 0.4; }
29+
}
30+
31+
@keyframes demo-arrow-pulse-down {
32+
0%, 100% { translate: 0 0; opacity: 1; }
33+
50% { translate: 0 3px; opacity: 0.4; }
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import './DemoArrow.css'
2+
3+
// Red pulsing arrow used to guide a viewer's eye to the next click
4+
// target during a prototype walkthrough. The component only owns the
5+
// visual + pulse animation; positioning (absolute, near the target) is
6+
// the caller's job. Meant to be ephemeral — mount it when the hint
7+
// should appear and unmount (or re-render without it) once the target
8+
// is clicked. See DESIGN_GUIDE.md → "Demo Hint Arrows".
9+
export default function DemoArrow({ direction = 'left', size = 24, className = '' }) {
10+
const rotations = { right: 0, down: 90, left: 180, up: 270 }
11+
const rotate = rotations[direction] ?? 0
12+
return (
13+
<span
14+
className={`demo-arrow demo-arrow-${direction} ${className}`.trim()}
15+
aria-hidden="true"
16+
>
17+
<svg
18+
width={size}
19+
height={size}
20+
viewBox="0 0 24 24"
21+
fill="none"
22+
stroke="currentColor"
23+
strokeWidth="2.5"
24+
strokeLinecap="round"
25+
strokeLinejoin="round"
26+
>
27+
<g transform={`rotate(${rotate} 12 12)`}>
28+
<path d="M4 12h16" />
29+
<path d="M14 6l6 6-6 6" />
30+
</g>
31+
</svg>
32+
</span>
33+
)
34+
}

src/components/common/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export { default as LinkCard } from './LinkCard'
55
export { default as IconButton } from './IconButton'
66
export { default as PrivateDisclaimer } from './PrivateDisclaimer'
77
export { default as TypingIndicator } from './TypingIndicator'
8+
export { default as DemoArrow } from './DemoArrow'
89
export * from './Icon'

src/data/contacts.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const teams = [
6161
name: 'Northwind Traders',
6262
initials: 'NT',
6363
color: '#0078D4',
64+
avatar: `${base}avatars/northwind-traders.svg`,
6465
channels: [
6566
{ id: 25, bold: true },
6667
{ id: 26, bold: true },
@@ -72,6 +73,7 @@ export const teams = [
7273
name: 'Morgan Collective',
7374
initials: 'MC',
7475
color: '#6264A7',
76+
avatar: `${base}avatars/morgan-collective.svg`,
7577
channels: [
7678
{ id: 28, bold: true },
7779
{ id: 29 },
@@ -114,3 +116,16 @@ export const chatList = [
114116
{ contactId: 19 },
115117
{ contactId: 20 },
116118
]
119+
120+
// Channels inherit their parent team's avatar — a channel in the chat list
121+
// should visually read as "part of Team X", not get its own generic tile.
122+
// Backfills any channel contact that doesn't already specify its own avatar.
123+
for (const team of teams) {
124+
if (!team.avatar) continue
125+
for (const entry of team.channels) {
126+
const channel = contacts.find((c) => c.id === entry.id)
127+
if (channel && channel.isChannel && !channel.avatar) {
128+
channel.avatar = team.avatar
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)