@flamingo-stack/openframe-frontend-core 0.0.360 → 0.0.361-snapshot.20260702223540

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/{chunk-2LTQEQWH.js → chunk-25VKZ2QM.js} +2 -2
  2. package/dist/{chunk-LUEVAFJP.js → chunk-33CU7BKK.js} +4 -4
  3. package/dist/{chunk-DNJCIVD6.js → chunk-3L34VSKP.js} +2 -2
  4. package/dist/{chunk-KMYPDZEO.js → chunk-4ZHXIY5J.js} +3982 -3858
  5. package/dist/chunk-4ZHXIY5J.js.map +1 -0
  6. package/dist/{chunk-ZPY53IWK.cjs → chunk-57ZLSUWZ.cjs} +56 -56
  7. package/dist/{chunk-ZPY53IWK.cjs.map → chunk-57ZLSUWZ.cjs.map} +1 -1
  8. package/dist/{chunk-LTALFWHU.cjs → chunk-5EFJJBU5.cjs} +31 -31
  9. package/dist/{chunk-LTALFWHU.cjs.map → chunk-5EFJJBU5.cjs.map} +1 -1
  10. package/dist/{chunk-VS4EZRU3.cjs → chunk-5PO6CQVE.cjs} +62 -62
  11. package/dist/{chunk-VS4EZRU3.cjs.map → chunk-5PO6CQVE.cjs.map} +1 -1
  12. package/dist/{chunk-YA2LLBWQ.js → chunk-ACAVASHV.js} +2 -2
  13. package/dist/{chunk-QPGENYQV.js → chunk-BLUIJE4A.js} +4 -4
  14. package/dist/{chunk-E66A6UJC.js → chunk-CDQY7PJI.js} +4 -4
  15. package/dist/{chunk-5NHJC3GQ.js → chunk-ECK3O7S7.js} +2 -2
  16. package/dist/{chunk-QZ3KUDFG.cjs → chunk-IU3ADZVS.cjs} +7 -7
  17. package/dist/{chunk-QZ3KUDFG.cjs.map → chunk-IU3ADZVS.cjs.map} +1 -1
  18. package/dist/{chunk-YEB3R52B.js → chunk-KCJS6HWH.js} +4 -4
  19. package/dist/{chunk-2GLO2HEJ.cjs → chunk-KKZMWFCK.cjs} +25 -25
  20. package/dist/{chunk-2GLO2HEJ.cjs.map → chunk-KKZMWFCK.cjs.map} +1 -1
  21. package/dist/{chunk-XULFRLBZ.js → chunk-LI4WU5DO.js} +2 -2
  22. package/dist/{chunk-TRUNSPFD.cjs → chunk-MLWAQ3QJ.cjs} +37 -37
  23. package/dist/{chunk-TRUNSPFD.cjs.map → chunk-MLWAQ3QJ.cjs.map} +1 -1
  24. package/dist/{chunk-EJSVHG4X.cjs → chunk-NOJCRTFP.cjs} +5 -5
  25. package/dist/{chunk-EJSVHG4X.cjs.map → chunk-NOJCRTFP.cjs.map} +1 -1
  26. package/dist/{chunk-XP7M5FPW.js → chunk-O4J55Q6W.js} +2 -2
  27. package/dist/{chunk-4NGPMJK6.cjs → chunk-PREABAGF.cjs} +12 -12
  28. package/dist/{chunk-4NGPMJK6.cjs.map → chunk-PREABAGF.cjs.map} +1 -1
  29. package/dist/{chunk-YUZ25NHF.cjs → chunk-PU7UCMSL.cjs} +15 -15
  30. package/dist/{chunk-YUZ25NHF.cjs.map → chunk-PU7UCMSL.cjs.map} +1 -1
  31. package/dist/{chunk-MGQVVGQV.cjs → chunk-R27F2P55.cjs} +1188 -1064
  32. package/dist/chunk-R27F2P55.cjs.map +1 -0
  33. package/dist/{chunk-LGQ4L3SX.cjs → chunk-SXM5YHP2.cjs} +26 -26
  34. package/dist/{chunk-LGQ4L3SX.cjs.map → chunk-SXM5YHP2.cjs.map} +1 -1
  35. package/dist/{chunk-35E52Q3L.cjs → chunk-TSNXAJOJ.cjs} +9 -9
  36. package/dist/{chunk-35E52Q3L.cjs.map → chunk-TSNXAJOJ.cjs.map} +1 -1
  37. package/dist/{chunk-CBVU3DFH.js → chunk-U3LE33OX.js} +2 -2
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/chat-input.d.ts.map +1 -1
  41. package/dist/components/chat/chat-quick-action-row.d.ts +4 -1
  42. package/dist/components/chat/chat-quick-action-row.d.ts.map +1 -1
  43. package/dist/components/chat/chat-quick-action.d.ts +6 -0
  44. package/dist/components/chat/chat-quick-action.d.ts.map +1 -1
  45. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  46. package/dist/components/chat/guide-welcome.d.ts +3 -0
  47. package/dist/components/chat/guide-welcome.d.ts.map +1 -1
  48. package/dist/components/chat/index.cjs +12 -2
  49. package/dist/components/chat/index.cjs.map +1 -1
  50. package/dist/components/chat/index.d.ts +2 -0
  51. package/dist/components/chat/index.d.ts.map +1 -1
  52. package/dist/components/chat/index.js +11 -1
  53. package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
  54. package/dist/components/chat/quick-action-chip.d.ts +70 -0
  55. package/dist/components/chat/quick-action-chip.d.ts.map +1 -0
  56. package/dist/components/chat/quick-action-marquee.d.ts +29 -0
  57. package/dist/components/chat/quick-action-marquee.d.ts.map +1 -0
  58. package/dist/components/contact/index.cjs +3 -3
  59. package/dist/components/contact/index.js +2 -2
  60. package/dist/components/docs/index.cjs +5 -5
  61. package/dist/components/docs/index.js +4 -4
  62. package/dist/components/embeds/index.cjs +3 -3
  63. package/dist/components/embeds/index.js +2 -2
  64. package/dist/components/faq/index.cjs +3 -3
  65. package/dist/components/faq/index.js +2 -2
  66. package/dist/components/features/index.cjs +2 -2
  67. package/dist/components/features/index.js +1 -1
  68. package/dist/components/help-center-pages/index.cjs +20 -20
  69. package/dist/components/help-center-pages/index.js +11 -11
  70. package/dist/components/index.cjs +130 -120
  71. package/dist/components/index.cjs.map +1 -1
  72. package/dist/components/index.js +19 -9
  73. package/dist/components/index.js.map +1 -1
  74. package/dist/components/navigation/index.cjs +2 -2
  75. package/dist/components/navigation/index.js +1 -1
  76. package/dist/components/onboarding-guides/index.cjs +5 -5
  77. package/dist/components/onboarding-guides/index.js +4 -4
  78. package/dist/components/related-content/index.cjs +3 -3
  79. package/dist/components/related-content/index.js +2 -2
  80. package/dist/components/tickets/index.cjs +5 -5
  81. package/dist/components/tickets/index.js +4 -4
  82. package/dist/components/ui/index.cjs +12 -2
  83. package/dist/components/ui/index.cjs.map +1 -1
  84. package/dist/components/ui/index.js +11 -1
  85. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  86. package/dist/index.cjs +12 -2
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.js +11 -1
  89. package/dist/tailwind.config.cjs +8 -1
  90. package/dist/tailwind.config.cjs.map +1 -1
  91. package/dist/tailwind.config.js +8 -1
  92. package/dist/tailwind.config.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/components/chat/chat-input.tsx +16 -9
  95. package/src/components/chat/chat-quick-action-row.tsx +20 -14
  96. package/src/components/chat/chat-quick-action.tsx +6 -0
  97. package/src/components/chat/embeddable-chat.tsx +19 -6
  98. package/src/components/chat/guide-welcome.tsx +15 -9
  99. package/src/components/chat/index.ts +2 -0
  100. package/src/components/chat/mingo-welcome.tsx +7 -12
  101. package/src/components/chat/quick-action-chip.tsx +155 -0
  102. package/src/components/chat/quick-action-marquee.tsx +92 -0
  103. package/src/components/ui/tab-navigation.tsx +8 -2
  104. package/src/stories/EmbeddableChat.stories.tsx +75 -0
  105. package/src/stories/QuickActionChip.stories.tsx +149 -0
  106. package/src/stories/QuickActionMarquee.stories.tsx +114 -0
  107. package/tailwind.config.ts +7 -0
  108. package/dist/chunk-KMYPDZEO.js.map +0 -1
  109. package/dist/chunk-MGQVVGQV.cjs.map +0 -1
  110. /package/dist/{chunk-2LTQEQWH.js.map → chunk-25VKZ2QM.js.map} +0 -0
  111. /package/dist/{chunk-LUEVAFJP.js.map → chunk-33CU7BKK.js.map} +0 -0
  112. /package/dist/{chunk-DNJCIVD6.js.map → chunk-3L34VSKP.js.map} +0 -0
  113. /package/dist/{chunk-YA2LLBWQ.js.map → chunk-ACAVASHV.js.map} +0 -0
  114. /package/dist/{chunk-QPGENYQV.js.map → chunk-BLUIJE4A.js.map} +0 -0
  115. /package/dist/{chunk-E66A6UJC.js.map → chunk-CDQY7PJI.js.map} +0 -0
  116. /package/dist/{chunk-5NHJC3GQ.js.map → chunk-ECK3O7S7.js.map} +0 -0
  117. /package/dist/{chunk-YEB3R52B.js.map → chunk-KCJS6HWH.js.map} +0 -0
  118. /package/dist/{chunk-XULFRLBZ.js.map → chunk-LI4WU5DO.js.map} +0 -0
  119. /package/dist/{chunk-XP7M5FPW.js.map → chunk-O4J55Q6W.js.map} +0 -0
  120. /package/dist/{chunk-CBVU3DFH.js.map → chunk-U3LE33OX.js.map} +0 -0
@@ -0,0 +1,155 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../utils/cn'
5
+ import { Tag } from '../ui/tag'
6
+ import { EntityIcon, type EntityIconValue } from '../icon-display'
7
+
8
+ // =============================================================================
9
+ // Types
10
+ // =============================================================================
11
+
12
+ /** Accent tint for quick-action chip icons: a named brand token
13
+ * (`'pink'`/`'cyan'`) or ANY CSS color value coming from admin config
14
+ * (agent/persona `icon_props.color`). */
15
+ export type QuickActionAccent = 'pink' | 'cyan' | (string & {})
16
+
17
+ /**
18
+ * FALLBACK-ONLY agent→accent mapping (fae→pink, mingo→cyan) for the built-in
19
+ * agents when nothing is configured. A color configured on the entity itself —
20
+ * the quick action's `iconProps.color`, or the agent/persona identity
21
+ * `icon_props.color` — always wins over this map; callers must resolve the
22
+ * configured value first and only fall back here. Never re-inline the slug
23
+ * comparison elsewhere.
24
+ */
25
+ export function getAgentAccent(slug: string | null | undefined): QuickActionAccent | undefined {
26
+ if (slug === 'fae') return 'pink'
27
+ if (slug === 'mingo') return 'cyan'
28
+ return undefined
29
+ }
30
+
31
+ /** Admin-configured identity color (`icon_props.color` on the agent/persona
32
+ * row) → chip accent. Undefined when the identity doesn't set one. */
33
+ export function accentFromIdentityIcon(
34
+ icon: { props?: Record<string, unknown> | null } | null | undefined,
35
+ ): QuickActionAccent | undefined {
36
+ const color = icon?.props?.color
37
+ return typeof color === 'string' && color.length > 0 ? color : undefined
38
+ }
39
+
40
+ /**
41
+ * Declarative icon for a quick-action chip, resolved 100% by `<EntityIcon>`
42
+ * (url → fae/mingo `AgentMark` → icons-v2 glyph → file fallback):
43
+ * - "agent format" — `{ name: 'fae' }` / `{ name: 'mingo' }` renders the
44
+ * packaged agent mark (fills the chip's icon box; `accent` is ignored — the
45
+ * marks carry their own colors).
46
+ * - "config format" — `{ name, url, props, accent }` renders the
47
+ * admin-configured icon; `accent` tints registry glyphs via a
48
+ * `text-ods-flamingo-*` class on `currentColor`.
49
+ *
50
+ * If `props.color` is set it wins over `accent` — the accent is a class tint
51
+ * on `currentColor`, the prop is an explicit fill (EntityIcon spreads `props`
52
+ * onto the glyph last).
53
+ */
54
+ export interface QuickActionIconSpec extends EntityIconValue {
55
+ accent?: QuickActionAccent
56
+ /** Glyph size in px. Defaults to 16 (the chip design's config-icon size);
57
+ * agent marks size via the icon box instead. */
58
+ size?: number
59
+ }
60
+
61
+ const ACCENT_CLASS: Record<string, string> = {
62
+ pink: 'text-ods-flamingo-pink',
63
+ cyan: 'text-ods-flamingo-cyan',
64
+ }
65
+
66
+ /** Resolve a chip `icon` prop to a renderable node. Specs go through
67
+ * `<EntityIcon>`; ReactNodes pass through untouched. */
68
+ export function renderQuickActionIcon(
69
+ icon: React.ReactNode | QuickActionIconSpec | undefined,
70
+ ): React.ReactNode {
71
+ // A spec is a plain data object, never a React element — so any non-element,
72
+ // non-array object is a spec; strings, elements, fragments, and ReactNode
73
+ // ARRAYS stay ReactNode.
74
+ if (typeof icon !== 'object' || icon === null || Array.isArray(icon) || React.isValidElement(icon)) {
75
+ return icon
76
+ }
77
+ const spec = icon as QuickActionIconSpec
78
+ if (!spec.name && !spec.url) return undefined
79
+ // Named brand tokens tint via a text class on `currentColor`; any other
80
+ // accent value is an admin-configured CSS color, delivered as the glyph's
81
+ // DEFAULT `color` prop — an explicit per-action `props.color` (spread after)
82
+ // still wins.
83
+ const accentClass = spec.accent ? ACCENT_CLASS[spec.accent] : undefined
84
+ const props =
85
+ spec.accent && !accentClass ? { color: spec.accent, ...(spec.props ?? {}) } : spec.props
86
+ return (
87
+ <EntityIcon
88
+ icon={{ name: spec.name, url: spec.url, props }}
89
+ size={spec.size ?? 16}
90
+ className={accentClass}
91
+ />
92
+ )
93
+ }
94
+
95
+ export interface QuickActionChipButtonProps {
96
+ label: React.ReactNode
97
+ /** Icon: a declarative {@link QuickActionIconSpec} (preferred — unified
98
+ * EntityIcon resolution) or a pre-rendered ReactNode. */
99
+ icon?: React.ReactNode | QuickActionIconSpec
100
+ /** `'primary'` = accent (yellow) chip, `'outline'` = bordered chip (default). */
101
+ variant?: 'primary' | 'outline'
102
+ onSelect?: () => void
103
+ /** Pointer/keyboard focus enters the chip — e.g. preview the full prompt. */
104
+ onHoverStart?: () => void
105
+ /** Pointer/keyboard focus leaves the chip — e.g. restore the composer. */
106
+ onHoverEnd?: () => void
107
+ /** `false` renders a plain non-focusable `<Tag>` (decorative use: marquee
108
+ * strips, table cells). Default `true`. */
109
+ interactive?: boolean
110
+ className?: string
111
+ }
112
+
113
+ // =============================================================================
114
+ // Component
115
+ // =============================================================================
116
+
117
+ /**
118
+ * The unified quick-action chip — Figma chip anatomy (card bg, border, mono
119
+ * uppercase label) via the `Tag` outline/primary variants, with icon
120
+ * resolution owned by `<EntityIcon>` in two formats (config icon w/ agent
121
+ * accent, or fae/mingo agent mark). Used by every chat empty state
122
+ * (guide / mingo / ai-agent), the marketing marquee strips, and the ROI
123
+ * table task cells.
124
+ */
125
+ export function QuickActionChipButton({
126
+ label,
127
+ icon,
128
+ variant = 'outline',
129
+ onSelect,
130
+ onHoverStart,
131
+ onHoverEnd,
132
+ interactive = true,
133
+ className,
134
+ }: QuickActionChipButtonProps) {
135
+ const resolvedIcon = renderQuickActionIcon(icon)
136
+ if (!interactive) {
137
+ return <Tag variant={variant} icon={resolvedIcon} label={label} className={className} />
138
+ }
139
+ return (
140
+ <button
141
+ type="button"
142
+ onClick={onSelect}
143
+ onMouseEnter={onHoverStart}
144
+ onMouseLeave={onHoverEnd}
145
+ onFocus={onHoverStart}
146
+ onBlur={onHoverEnd}
147
+ className={cn(
148
+ 'shrink-0 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',
149
+ className,
150
+ )}
151
+ >
152
+ <Tag variant={variant} icon={resolvedIcon} label={label} />
153
+ </button>
154
+ )
155
+ }
@@ -0,0 +1,92 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../utils/cn'
5
+ import { QuickActionChipButton, type QuickActionIconSpec } from './quick-action-chip'
6
+
7
+ // =============================================================================
8
+ // Types
9
+ // =============================================================================
10
+
11
+ export interface QuickActionMarqueeItem {
12
+ /** Stable key (suffixed per track copy internally). */
13
+ id: string
14
+ label: string
15
+ icon?: React.ReactNode | QuickActionIconSpec
16
+ }
17
+
18
+ export interface QuickActionMarqueeProps {
19
+ items: ReadonlyArray<QuickActionMarqueeItem>
20
+ /** Scroll direction. `'right'` reverses the animation. Default `'left'`. */
21
+ direction?: 'left' | 'right'
22
+ /** Seconds per full loop. Default 40. */
23
+ duration?: number
24
+ /** Pause the scroll while hovered. Default true. */
25
+ pauseOnHover?: boolean
26
+ /** When provided chips are interactive buttons; omitted → decorative tags. */
27
+ onSelect?: (item: QuickActionMarqueeItem) => void
28
+ className?: string
29
+ }
30
+
31
+ // Heuristic: ~6 chips ≈ one 1280px viewport width, so the halved track always
32
+ // fills the screen; bump if chips shrink.
33
+ const MIN_TRACK_ITEMS = 6
34
+
35
+ // =============================================================================
36
+ // Component
37
+ // =============================================================================
38
+
39
+ /**
40
+ * Endless horizontally-scrolling strip of quick-action chips (Figma fae/mingo
41
+ * marquee rows). The item list is padded to at least {@link MIN_TRACK_ITEMS}
42
+ * and rendered twice so the `qa-marquee` keyframe's `translateX(-50%)` loops
43
+ * seamlessly; the second half is `aria-hidden`. Pauses on hover (optional)
44
+ * and always under `prefers-reduced-motion`.
45
+ */
46
+ export function QuickActionMarquee({
47
+ items,
48
+ direction = 'left',
49
+ duration = 40,
50
+ pauseOnHover = true,
51
+ onSelect,
52
+ className,
53
+ }: QuickActionMarqueeProps) {
54
+ const { track, half } = React.useMemo(() => {
55
+ const repeats = Math.max(1, Math.ceil(MIN_TRACK_ITEMS / Math.max(1, items.length)))
56
+ const padded = Array.from({ length: repeats }, () => items).flat()
57
+ return { track: [...padded, ...padded], half: padded.length }
58
+ }, [items])
59
+
60
+ if (items.length === 0) return null
61
+
62
+ return (
63
+ <div className={cn('group overflow-hidden', className)}>
64
+ <div
65
+ className={cn(
66
+ 'flex w-max items-center gap-2 animate-qa-marquee motion-reduce:[animation-play-state:paused]',
67
+ direction === 'right' && '[animation-direction:reverse]',
68
+ pauseOnHover && 'group-hover:[animation-play-state:paused]',
69
+ )}
70
+ style={{ '--qa-marquee-duration': `${duration}s` } as React.CSSProperties}
71
+ >
72
+ {track.map((item, index) => {
73
+ const isHiddenCopy = index >= half
74
+ return (
75
+ <span key={`${item.id}__${index}`} aria-hidden={isHiddenCopy || undefined}>
76
+ {/* The aria-hidden loop copy must contain NO focusable elements
77
+ (WCAG: no focusable descendants under aria-hidden) — its
78
+ chips render as plain tags even when the strip is
79
+ interactive. */}
80
+ <QuickActionChipButton
81
+ label={item.label}
82
+ icon={item.icon}
83
+ interactive={!!onSelect && !isHiddenCopy}
84
+ onSelect={onSelect && !isHiddenCopy ? () => onSelect(item) : undefined}
85
+ />
86
+ </span>
87
+ )
88
+ })}
89
+ </div>
90
+ </div>
91
+ )
92
+ }
@@ -116,6 +116,7 @@ export function TabNavigation({
116
116
 
117
117
  const scrollRef = useRef<HTMLDivElement>(null)
118
118
  const activeTabRef = useRef<HTMLButtonElement>(null)
119
+ const isFirstActiveScrollRef = useRef(true)
119
120
  const [canScrollLeft, setCanScrollLeft] = useState(false)
120
121
  const [canScrollRight, setCanScrollRight] = useState(false)
121
122
 
@@ -174,12 +175,17 @@ export function TabNavigation({
174
175
  const el = scrollRef.current
175
176
  const active = activeTabRef.current
176
177
  if (!el || !active) return
178
+ // On mount (e.g. a deep link landing on an off-screen tab, such as
179
+ // `?tab=software` as the last of many), snap instantly instead of visibly
180
+ // sliding right after first paint — smooth is reserved for later tab changes.
181
+ const behavior = isFirstActiveScrollRef.current ? 'auto' : 'smooth'
182
+ isFirstActiveScrollRef.current = false
177
183
  const elRect = el.getBoundingClientRect()
178
184
  const aRect = active.getBoundingClientRect()
179
185
  if (aRect.left < elRect.left) {
180
- el.scrollBy({ left: aRect.left - elRect.left, behavior: 'smooth' })
186
+ el.scrollBy({ left: aRect.left - elRect.left, behavior })
181
187
  } else if (aRect.right > elRect.right) {
182
- el.scrollBy({ left: aRect.right - elRect.right, behavior: 'smooth' })
188
+ el.scrollBy({ left: aRect.right - elRect.right, behavior })
183
189
  }
184
190
  }, [activeTab])
185
191
 
@@ -28,6 +28,29 @@ import {
28
28
 
29
29
  const COMMANDS_URL = '/__story__/commands'
30
30
 
31
+ // Agent-config endpoint. EmbeddableChat's `DEFAULT_AI_AGENT_CONFIG_URL` resolves
32
+ // `activeAgentSlug` → `/api/ai-agents/<slug>`; the fetch shim below answers any
33
+ // href containing this prefix with a mocked `EmptyStateConfig` so agent-mode
34
+ // quick-action chips render from the fetch (not the host prop).
35
+ const AI_AGENT_CONFIG_PREFIX = '/api/ai-agents/'
36
+
37
+ // Mocked `/api/ai-agents/fae` response — the `EmptyStateConfig` shape
38
+ // (see use-empty-state-config.ts). `icon: { name: 'fae' }` is the agent
39
+ // identity mark; the chip `iconName`s resolve via the icon library and get the
40
+ // fae→pink accent auto-derived from the slug (no `iconProps.color` set).
41
+ const FAE_AGENT_CONFIG = {
42
+ name: 'Fae',
43
+ icon: { name: 'fae' },
44
+ greeting: "Hi, I'm Fae — ask me about incidents, scripts, or device health.",
45
+ quickActions: [
46
+ { id: 'fae-triage', label: 'Triage an alert', prompt: 'Help me triage the latest critical alert.', iconName: 'search' },
47
+ { id: 'fae-script', label: 'Write a script', prompt: 'Write a PowerShell script to audit local admins.', iconName: 'bracket-curly' },
48
+ { id: 'fae-health', label: 'Device health', prompt: 'Which devices are unhealthy right now?', iconName: 'rocket' },
49
+ { id: 'fae-logs', label: 'Summarize logs', prompt: 'Summarize the last 24h of error logs.', iconName: 'newspaper' },
50
+ ],
51
+ suggestedQueries: [],
52
+ }
53
+
31
54
  // =============================================================================
32
55
  // Shared mocks
33
56
  // =============================================================================
@@ -417,6 +440,14 @@ if (
417
440
  }),
418
441
  )
419
442
  }
443
+ if (href.includes(AI_AGENT_CONFIG_PREFIX)) {
444
+ return Promise.resolve(
445
+ new Response(JSON.stringify(FAE_AGENT_CONFIG), {
446
+ status: 200,
447
+ headers: { 'Content-Type': 'application/json' },
448
+ }),
449
+ )
450
+ }
420
451
  return originalFetch(input as RequestInfo, init)
421
452
  }) as typeof window.fetch
422
453
  }
@@ -700,3 +731,47 @@ export const ArchiveEmpty: Story = {
700
731
  await userEvent.click(archiveButton)
701
732
  },
702
733
  }
734
+
735
+ // =============================================================================
736
+ // 9. Agent-mode quick actions (fetched chips — PR #1342)
737
+ // =============================================================================
738
+
739
+ /**
740
+ * Agent-mode empty state. `activeAgentSlug="fae"` makes EmbeddableChat resolve
741
+ * `/api/ai-agents/fae` and fetch that agent's `EmptyStateConfig` (mocked by the
742
+ * fetch shim → `FAE_AGENT_CONFIG`). The Guide-mode chip row renders the agent's
743
+ * fetched `quickActions` — pink-tinted because the fae→pink accent is
744
+ * auto-derived from the slug — with Fae's identity (name + mark) on the
745
+ * greeting. No host `guideWelcome.quickActions` here, so the fetched chips are
746
+ * the only source.
747
+ */
748
+ export const AgentModeQuickActions: Story = {
749
+ args: {
750
+ defaultOpen: true,
751
+ showInternalTrigger: false,
752
+ activeAgentSlug: 'fae',
753
+ },
754
+ }
755
+
756
+ /**
757
+ * The PR #1342 precedence fix. BOTH sources are configured: the host passes its
758
+ * own `guideWelcome.quickActions` (platform defaults) AND an agent is active
759
+ * with fetched chips. The agent's chips WIN — previously the host array always
760
+ * won, so agent chips never rendered. You should see Fae's "Triage an alert /
761
+ * Write a script / Device health / Summarize logs" chips, NOT the host's
762
+ * "Host default …" chips.
763
+ */
764
+ export const AgentChipsWinOverHost: Story = {
765
+ args: {
766
+ defaultOpen: true,
767
+ showInternalTrigger: false,
768
+ activeAgentSlug: 'fae',
769
+ guideWelcome: {
770
+ quickActions: [
771
+ { id: 'host-a', label: 'Host default A', prompt: 'host prompt A' },
772
+ { id: 'host-b', label: 'Host default B', prompt: 'host prompt B' },
773
+ { id: 'host-c', label: 'Host default C', prompt: 'host prompt C' },
774
+ ],
775
+ },
776
+ },
777
+ }
@@ -0,0 +1,149 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
2
+ import {
3
+ QuickActionChipButton,
4
+ type QuickActionIconSpec,
5
+ } from '../components/chat/quick-action-chip'
6
+
7
+ const meta: Meta<typeof QuickActionChipButton> = {
8
+ title: 'Chat/QuickActionChip',
9
+ component: QuickActionChipButton,
10
+ parameters: {
11
+ layout: 'centered',
12
+ docs: {
13
+ description: {
14
+ component:
15
+ 'The unified quick-action chip (`QuickActionChipButton`) used by every chat empty state (guide / mingo / ai-agent), the marketing marquee strips, and the ROI table task cells. Icon resolution is owned entirely by `<EntityIcon>` in two `QuickActionIconSpec` formats: the **agent format** (`{ name: \'fae\' | \'mingo\' }`) renders the packaged agent mark, and the **config format** (`{ name, url, props, accent }`) renders an admin-configured glyph tinted via `accent` (`pink` → flamingo-pink, `cyan` → flamingo-cyan, or any CSS color). An explicit `props.color` wins over `accent`.',
16
+ },
17
+ },
18
+ },
19
+ argTypes: {
20
+ label: { control: 'text' },
21
+ variant: { control: 'inline-radio', options: ['outline', 'primary'] },
22
+ interactive: { control: 'boolean' },
23
+ icon: { control: false },
24
+ onSelect: { control: false },
25
+ onHoverStart: { control: false },
26
+ onHoverEnd: { control: false },
27
+ },
28
+ decorators: [
29
+ (Story) => (
30
+ <div className="flex items-center justify-center bg-ods-bg p-8">
31
+ <Story />
32
+ </div>
33
+ ),
34
+ ],
35
+ }
36
+
37
+ export default meta
38
+ type Story = StoryObj<typeof QuickActionChipButton>
39
+
40
+ // Config-format icon specs — resolved by <EntityIcon> against the icons-v2
41
+ // library, tinted by `accent`.
42
+ const SEARCH_ICON: QuickActionIconSpec = { name: 'search', accent: 'pink' }
43
+ const BUG_ICON: QuickActionIconSpec = { name: 'bug', accent: 'cyan' }
44
+
45
+ /** Default bordered (`outline`) chip with a config glyph tinted flamingo-pink. */
46
+ export const Default: Story = {
47
+ args: {
48
+ label: 'How to start',
49
+ icon: SEARCH_ICON,
50
+ variant: 'outline',
51
+ onSelect: () => console.log('select'),
52
+ },
53
+ }
54
+
55
+ /** Accent (`primary`, yellow) chip variant. */
56
+ export const Primary: Story = {
57
+ args: {
58
+ label: 'Run scripts',
59
+ icon: { name: 'rocket' },
60
+ variant: 'primary',
61
+ onSelect: () => console.log('select'),
62
+ },
63
+ }
64
+
65
+ /** Config glyphs tinted via `accent` — `pink` and `cyan` brand tokens. */
66
+ export const AccentTints: Story = {
67
+ render: () => (
68
+ <div className="flex flex-wrap items-center gap-2">
69
+ <QuickActionChipButton label="Search docs" icon={SEARCH_ICON} onSelect={() => {}} />
70
+ <QuickActionChipButton label="Report a bug" icon={BUG_ICON} onSelect={() => {}} />
71
+ <QuickActionChipButton
72
+ label="Custom color"
73
+ icon={{ name: 'compass', accent: '#8b5cf6' }}
74
+ onSelect={() => {}}
75
+ />
76
+ </div>
77
+ ),
78
+ }
79
+
80
+ /**
81
+ * Agent-format icons — `{ name: 'fae' }` / `{ name: 'mingo' }` render the
82
+ * packaged agent marks (they carry their own colors; `accent` is ignored).
83
+ */
84
+ export const AgentMarks: Story = {
85
+ render: () => (
86
+ <div className="flex flex-wrap items-center gap-2">
87
+ <QuickActionChipButton label="Ask fae" icon={{ name: 'fae' }} onSelect={() => {}} />
88
+ <QuickActionChipButton label="Ask mingo" icon={{ name: 'mingo' }} onSelect={() => {}} />
89
+ </div>
90
+ ),
91
+ }
92
+
93
+ /** No icon — label only. */
94
+ export const NoIcon: Story = {
95
+ args: {
96
+ label: 'Find device',
97
+ onSelect: () => console.log('select'),
98
+ },
99
+ }
100
+
101
+ /**
102
+ * `interactive={false}` renders a plain, non-focusable `<Tag>` (decorative use:
103
+ * marquee strips, ROI table cells) — no button, no hover callbacks.
104
+ */
105
+ export const NonInteractive: Story = {
106
+ args: {
107
+ label: 'Decorative',
108
+ icon: SEARCH_ICON,
109
+ interactive: false,
110
+ },
111
+ }
112
+
113
+ /**
114
+ * `onHoverStart` / `onHoverEnd` fire on pointer AND keyboard focus — used to
115
+ * preview the full prompt in the composer, then restore it. Hover or tab the
116
+ * chip and watch the Actions/console panel.
117
+ */
118
+ export const HoverCallbacks: Story = {
119
+ args: {
120
+ label: 'Preview prompt',
121
+ icon: { name: 'fae' },
122
+ onSelect: () => console.log('select'),
123
+ onHoverStart: () => console.log('hover start → preview prompt'),
124
+ onHoverEnd: () => console.log('hover end → restore composer'),
125
+ },
126
+ }
127
+
128
+ /** All variants side by side. */
129
+ export const Gallery: Story = {
130
+ render: () => (
131
+ <div className="flex flex-col gap-4">
132
+ <div className="flex flex-wrap items-center gap-2">
133
+ <QuickActionChipButton label="Outline" icon={SEARCH_ICON} onSelect={() => {}} />
134
+ <QuickActionChipButton
135
+ label="Primary"
136
+ icon={{ name: 'rocket' }}
137
+ variant="primary"
138
+ onSelect={() => {}}
139
+ />
140
+ <QuickActionChipButton label="No icon" onSelect={() => {}} />
141
+ </div>
142
+ <div className="flex flex-wrap items-center gap-2">
143
+ <QuickActionChipButton label="fae" icon={{ name: 'fae' }} onSelect={() => {}} />
144
+ <QuickActionChipButton label="mingo" icon={{ name: 'mingo' }} onSelect={() => {}} />
145
+ <QuickActionChipButton label="Decorative" icon={BUG_ICON} interactive={false} />
146
+ </div>
147
+ </div>
148
+ ),
149
+ }
@@ -0,0 +1,114 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
2
+ import {
3
+ QuickActionMarquee,
4
+ type QuickActionMarqueeItem,
5
+ } from '../components/chat/quick-action-marquee'
6
+
7
+ // fae-row items (config glyphs tinted pink) and mingo-row items (tinted cyan) —
8
+ // mirrors the Figma fae/mingo marquee rows.
9
+ const FAE_ITEMS: ReadonlyArray<QuickActionMarqueeItem> = [
10
+ { id: 'how-to-start', label: 'How to start', icon: { name: 'fae' } },
11
+ { id: 'connect-device', label: 'Connect device', icon: { name: 'search', accent: 'pink' } },
12
+ { id: 'find-device', label: 'Find device', icon: { name: 'compass', accent: 'pink' } },
13
+ { id: 'remote-connection', label: 'Remote connection', icon: { name: 'rocket', accent: 'pink' } },
14
+ { id: 'run-scripts', label: 'Run scripts', icon: { name: 'bracket-curly', accent: 'pink' } },
15
+ { id: 'device-software', label: 'Device software', icon: { name: 'package', accent: 'pink' } },
16
+ ]
17
+
18
+ const MINGO_ITEMS: ReadonlyArray<QuickActionMarqueeItem> = [
19
+ { id: 'roi', label: 'Calculate ROI', icon: { name: 'mingo' } },
20
+ { id: 'ticket', label: 'Open ticket', icon: { name: 'ticket', accent: 'cyan' } },
21
+ { id: 'report-bug', label: 'Report a bug', icon: { name: 'bug', accent: 'cyan' } },
22
+ { id: 'docs', label: 'Read docs', icon: { name: 'book', accent: 'cyan' } },
23
+ { id: 'chat', label: 'Start a chat', icon: { name: 'chats', accent: 'cyan' } },
24
+ { id: 'news', label: "What's new", icon: { name: 'newspaper', accent: 'cyan' } },
25
+ ]
26
+
27
+ const meta: Meta<typeof QuickActionMarquee> = {
28
+ title: 'Chat/QuickActionMarquee',
29
+ component: QuickActionMarquee,
30
+ parameters: {
31
+ layout: 'fullscreen',
32
+ docs: {
33
+ description: {
34
+ component:
35
+ 'Endless horizontally-scrolling strip of quick-action chips (Figma fae/mingo marquee rows). The item list is padded to ≥6 chips and rendered twice so the `qa-marquee` keyframe’s `translateX(-50%)` loops seamlessly; the duplicate half is `aria-hidden` and never focusable. Pauses on hover (`pauseOnHover`, default on) and always under `prefers-reduced-motion`. Pass `onSelect` to make chips interactive buttons; omit it for a purely decorative strip.',
36
+ },
37
+ },
38
+ },
39
+ argTypes: {
40
+ items: { control: false },
41
+ direction: { control: 'inline-radio', options: ['left', 'right'] },
42
+ duration: { control: { type: 'number', min: 5, max: 120, step: 5 } },
43
+ pauseOnHover: { control: 'boolean' },
44
+ onSelect: { control: false },
45
+ },
46
+ decorators: [
47
+ (Story) => (
48
+ <div className="bg-ods-bg py-8">
49
+ <Story />
50
+ </div>
51
+ ),
52
+ ],
53
+ }
54
+
55
+ export default meta
56
+ type Story = StoryObj<typeof QuickActionMarquee>
57
+
58
+ /** Single fae row scrolling left, interactive chips. */
59
+ export const Default: Story = {
60
+ args: {
61
+ items: FAE_ITEMS,
62
+ onSelect: (item) => console.log('select', item.id),
63
+ },
64
+ }
65
+
66
+ /** The two Figma rows: fae (left) over mingo (right, reversed). */
67
+ export const TwoRows: Story = {
68
+ render: () => (
69
+ <div className="flex flex-col gap-4">
70
+ <QuickActionMarquee items={FAE_ITEMS} direction="left" onSelect={(i) => console.log(i.id)} />
71
+ <QuickActionMarquee items={MINGO_ITEMS} direction="right" onSelect={(i) => console.log(i.id)} />
72
+ </div>
73
+ ),
74
+ }
75
+
76
+ /** Reversed scroll direction. */
77
+ export const ScrollRight: Story = {
78
+ args: {
79
+ items: MINGO_ITEMS,
80
+ direction: 'right',
81
+ onSelect: (item) => console.log('select', item.id),
82
+ },
83
+ }
84
+
85
+ /**
86
+ * No `onSelect` → chips render as plain, non-focusable tags (decorative
87
+ * marketing strip). Nothing is clickable or tabbable.
88
+ */
89
+ export const Decorative: Story = {
90
+ args: {
91
+ items: FAE_ITEMS,
92
+ },
93
+ }
94
+
95
+ /**
96
+ * Fewer than 6 items — the track is auto-repeated up to the `MIN_TRACK_ITEMS`
97
+ * heuristic so the halved loop still fills the viewport with no visible gap.
98
+ */
99
+ export const FewItems: Story = {
100
+ args: {
101
+ items: FAE_ITEMS.slice(0, 2),
102
+ onSelect: (item) => console.log('select', item.id),
103
+ },
104
+ }
105
+
106
+ /** Faster loop (`duration={15}`) and hover-pause disabled. */
107
+ export const FastNoPause: Story = {
108
+ args: {
109
+ items: FAE_ITEMS,
110
+ duration: 15,
111
+ pauseOnHover: false,
112
+ onSelect: (item) => console.log('select', item.id),
113
+ },
114
+ }
@@ -302,11 +302,18 @@ const config: Config = {
302
302
  from: { opacity: "0" },
303
303
  to: { opacity: "1" },
304
304
  },
305
+ // Endless chip strip (QuickActionMarquee): the track renders its items
306
+ // twice, so -50% lands exactly one copy over for a seamless loop.
307
+ "qa-marquee": {
308
+ from: { transform: "translateX(0)" },
309
+ to: { transform: "translateX(-50%)" },
310
+ },
305
311
  },
306
312
  animation: {
307
313
  "accordion-down": "accordion-down 0.2s ease-out",
308
314
  "accordion-up": "accordion-up 0.2s ease-out",
309
315
  "fade-in": "fade-in 0.3s ease-out",
316
+ "qa-marquee": "qa-marquee var(--qa-marquee-duration,40s) linear infinite",
310
317
  },
311
318
  },
312
319
  },