@flamingo-stack/openframe-frontend-core 0.0.449 → 0.0.450

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 (142) hide show
  1. package/dist/{chunk-DJP5HYFG.cjs → chunk-26JSJMMZ.cjs} +7 -7
  2. package/dist/{chunk-DJP5HYFG.cjs.map → chunk-26JSJMMZ.cjs.map} +1 -1
  3. package/dist/{chunk-NPBTNWRA.js → chunk-3ITY2LBM.js} +4 -4
  4. package/dist/{chunk-MWRFPLNB.js → chunk-4SZDO3TE.js} +3 -3
  5. package/dist/{chunk-LWT6G75Q.js → chunk-4VSTLJDO.js} +2 -2
  6. package/dist/{chunk-DRKQNXSZ.cjs → chunk-5HW5HMNF.cjs} +5 -5
  7. package/dist/{chunk-DRKQNXSZ.cjs.map → chunk-5HW5HMNF.cjs.map} +1 -1
  8. package/dist/{chunk-KKJRBMG3.cjs → chunk-5OY7HYII.cjs} +73 -73
  9. package/dist/{chunk-KKJRBMG3.cjs.map → chunk-5OY7HYII.cjs.map} +1 -1
  10. package/dist/{chunk-WYPROXL4.cjs → chunk-6GJKSYM2.cjs} +73 -73
  11. package/dist/{chunk-WYPROXL4.cjs.map → chunk-6GJKSYM2.cjs.map} +1 -1
  12. package/dist/{chunk-6TS4AIDX.cjs → chunk-6RA4NRBA.cjs} +15 -15
  13. package/dist/{chunk-6TS4AIDX.cjs.map → chunk-6RA4NRBA.cjs.map} +1 -1
  14. package/dist/{chunk-X754WDWO.cjs → chunk-76EN3GEH.cjs} +40 -40
  15. package/dist/{chunk-X754WDWO.cjs.map → chunk-76EN3GEH.cjs.map} +1 -1
  16. package/dist/{chunk-YSNMV53O.js → chunk-CM5XKHHS.js} +6 -6
  17. package/dist/{chunk-47AMTR2S.cjs → chunk-DQ5U6DCR.cjs} +18 -18
  18. package/dist/{chunk-47AMTR2S.cjs.map → chunk-DQ5U6DCR.cjs.map} +1 -1
  19. package/dist/{chunk-YHNBJ3F3.js → chunk-EQWE64RJ.js} +2 -2
  20. package/dist/{chunk-5SJGFLL2.js → chunk-HXCQHMS5.js} +6386 -6032
  21. package/dist/chunk-HXCQHMS5.js.map +1 -0
  22. package/dist/{chunk-VSWFYFIG.js → chunk-HYIHXXF3.js} +5 -5
  23. package/dist/{chunk-AZKVNRZP.cjs → chunk-J5BYYZVM.cjs} +32 -32
  24. package/dist/{chunk-AZKVNRZP.cjs.map → chunk-J5BYYZVM.cjs.map} +1 -1
  25. package/dist/{chunk-JAASDJOF.js → chunk-K3GXIGN6.js} +5 -5
  26. package/dist/{chunk-UNQVTGVG.js → chunk-M3AKWFAO.js} +2 -2
  27. package/dist/{chunk-TKGCTDQY.js → chunk-MLFFBQ5B.js} +180 -87
  28. package/dist/chunk-MLFFBQ5B.js.map +1 -0
  29. package/dist/{chunk-XLFAFH2B.js → chunk-N53FC732.js} +2 -2
  30. package/dist/{chunk-H56AEHFC.cjs → chunk-N5M72LJX.cjs} +1620 -1266
  31. package/dist/chunk-N5M72LJX.cjs.map +1 -0
  32. package/dist/{chunk-4IMXFDRI.js → chunk-PCW2TRZ4.js} +3 -3
  33. package/dist/{chunk-AN7MCJTV.cjs → chunk-QF3NQW7C.cjs} +122 -29
  34. package/dist/chunk-QF3NQW7C.cjs.map +1 -0
  35. package/dist/{chunk-EOFENGSZ.cjs → chunk-ROBGJH52.cjs} +12 -12
  36. package/dist/{chunk-EOFENGSZ.cjs.map → chunk-ROBGJH52.cjs.map} +1 -1
  37. package/dist/{chunk-HQ7S6JG5.js → chunk-RQHHACP2.js} +3 -3
  38. package/dist/{chunk-ROCJMSJI.cjs → chunk-UPRY4I63.cjs} +3 -3
  39. package/dist/{chunk-ROCJMSJI.cjs.map → chunk-UPRY4I63.cjs.map} +1 -1
  40. package/dist/{chunk-LQTX7EJS.cjs → chunk-VLDZ4363.cjs} +29 -29
  41. package/dist/{chunk-LQTX7EJS.cjs.map → chunk-VLDZ4363.cjs.map} +1 -1
  42. package/dist/{chunk-BSBWBQ7M.js → chunk-VUDZOCPW.js} +6 -6
  43. package/dist/{chunk-4S4RR7XC.cjs → chunk-XITG5ICQ.cjs} +40 -40
  44. package/dist/{chunk-4S4RR7XC.cjs.map → chunk-XITG5ICQ.cjs.map} +1 -1
  45. package/dist/components/case-studies/index.cjs +9 -9
  46. package/dist/components/case-studies/index.js +3 -3
  47. package/dist/components/chat/chat-quick-action-row.d.ts +26 -1
  48. package/dist/components/chat/chat-quick-action-row.d.ts.map +1 -1
  49. package/dist/components/chat/index.cjs +15 -3
  50. package/dist/components/chat/index.cjs.map +1 -1
  51. package/dist/components/chat/index.d.ts +1 -0
  52. package/dist/components/chat/index.d.ts.map +1 -1
  53. package/dist/components/chat/index.js +14 -2
  54. package/dist/components/chat/quick-action-chip.d.ts +45 -4
  55. package/dist/components/chat/quick-action-chip.d.ts.map +1 -1
  56. package/dist/components/chat/quick-action-marquee.d.ts +24 -9
  57. package/dist/components/chat/quick-action-marquee.d.ts.map +1 -1
  58. package/dist/components/chat/quick-action-wall.d.ts +96 -0
  59. package/dist/components/chat/quick-action-wall.d.ts.map +1 -0
  60. package/dist/components/contact/index.cjs +4 -4
  61. package/dist/components/contact/index.js +3 -3
  62. package/dist/components/content-loading-container.d.ts +1 -1
  63. package/dist/components/docs/index.cjs +6 -6
  64. package/dist/components/docs/index.js +5 -5
  65. package/dist/components/embeds/index.cjs +4 -4
  66. package/dist/components/embeds/index.js +3 -3
  67. package/dist/components/faq/index.cjs +5 -5
  68. package/dist/components/faq/index.js +4 -4
  69. package/dist/components/features/cards-strip.d.ts.map +1 -1
  70. package/dist/components/features/index.cjs +3 -3
  71. package/dist/components/features/index.js +2 -2
  72. package/dist/components/form.d.ts +1 -1
  73. package/dist/components/form.d.ts.map +1 -1
  74. package/dist/components/help-center-pages/index.cjs +22 -22
  75. package/dist/components/help-center-pages/index.js +13 -13
  76. package/dist/components/index.cjs +159 -135
  77. package/dist/components/index.cjs.map +1 -1
  78. package/dist/components/index.js +35 -11
  79. package/dist/components/index.js.map +1 -1
  80. package/dist/components/navigation/index.cjs +3 -3
  81. package/dist/components/navigation/index.js +2 -2
  82. package/dist/components/onboarding-guides/index.cjs +7 -7
  83. package/dist/components/onboarding-guides/index.js +6 -6
  84. package/dist/components/related-content/index.cjs +5 -5
  85. package/dist/components/related-content/index.js +4 -4
  86. package/dist/components/tickets/index.cjs +6 -6
  87. package/dist/components/tickets/index.js +5 -5
  88. package/dist/components/ui/index.cjs +27 -3
  89. package/dist/components/ui/index.cjs.map +1 -1
  90. package/dist/components/ui/index.d.ts +1 -0
  91. package/dist/components/ui/index.d.ts.map +1 -1
  92. package/dist/components/ui/index.js +26 -2
  93. package/dist/components/ui/marquee-wall.d.ts +159 -0
  94. package/dist/components/ui/marquee-wall.d.ts.map +1 -0
  95. package/dist/hooks/index.cjs +6 -2
  96. package/dist/hooks/index.cjs.map +1 -1
  97. package/dist/hooks/index.js +5 -1
  98. package/dist/hooks/ui/index.d.ts +2 -0
  99. package/dist/hooks/ui/index.d.ts.map +1 -1
  100. package/dist/hooks/ui/use-marquee-engine.d.ts +73 -0
  101. package/dist/hooks/ui/use-marquee-engine.d.ts.map +1 -0
  102. package/dist/hooks/ui/use-suppress-clone-focus.d.ts +24 -0
  103. package/dist/hooks/ui/use-suppress-clone-focus.d.ts.map +1 -0
  104. package/dist/index.cjs +31 -3
  105. package/dist/index.cjs.map +1 -1
  106. package/dist/index.js +30 -2
  107. package/dist/tailwind.config.cjs +1 -8
  108. package/dist/tailwind.config.cjs.map +1 -1
  109. package/dist/tailwind.config.js +1 -8
  110. package/dist/tailwind.config.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/components/chat/chat-quick-action-row.tsx +47 -3
  113. package/src/components/chat/index.ts +1 -0
  114. package/src/components/chat/quick-action-chip.tsx +81 -13
  115. package/src/components/chat/quick-action-marquee.tsx +54 -47
  116. package/src/components/chat/quick-action-wall.tsx +247 -0
  117. package/src/components/features/cards-strip.tsx +88 -158
  118. package/src/components/ui/index.ts +1 -0
  119. package/src/components/ui/marquee-wall.tsx +522 -0
  120. package/src/hooks/ui/index.ts +2 -0
  121. package/src/hooks/ui/use-marquee-engine.ts +158 -0
  122. package/src/hooks/ui/use-suppress-clone-focus.ts +61 -0
  123. package/src/stories/MarqueeWall.stories.tsx +132 -0
  124. package/src/stories/QuickActionMarquee.stories.tsx +8 -7
  125. package/src/stories/QuickActionWall.stories.tsx +116 -0
  126. package/tailwind.config.ts +0 -7
  127. package/dist/chunk-5SJGFLL2.js.map +0 -1
  128. package/dist/chunk-AN7MCJTV.cjs.map +0 -1
  129. package/dist/chunk-H56AEHFC.cjs.map +0 -1
  130. package/dist/chunk-TKGCTDQY.js.map +0 -1
  131. /package/dist/{chunk-NPBTNWRA.js.map → chunk-3ITY2LBM.js.map} +0 -0
  132. /package/dist/{chunk-MWRFPLNB.js.map → chunk-4SZDO3TE.js.map} +0 -0
  133. /package/dist/{chunk-LWT6G75Q.js.map → chunk-4VSTLJDO.js.map} +0 -0
  134. /package/dist/{chunk-YSNMV53O.js.map → chunk-CM5XKHHS.js.map} +0 -0
  135. /package/dist/{chunk-YHNBJ3F3.js.map → chunk-EQWE64RJ.js.map} +0 -0
  136. /package/dist/{chunk-VSWFYFIG.js.map → chunk-HYIHXXF3.js.map} +0 -0
  137. /package/dist/{chunk-JAASDJOF.js.map → chunk-K3GXIGN6.js.map} +0 -0
  138. /package/dist/{chunk-UNQVTGVG.js.map → chunk-M3AKWFAO.js.map} +0 -0
  139. /package/dist/{chunk-XLFAFH2B.js.map → chunk-N53FC732.js.map} +0 -0
  140. /package/dist/{chunk-4IMXFDRI.js.map → chunk-PCW2TRZ4.js.map} +0 -0
  141. /package/dist/{chunk-HQ7S6JG5.js.map → chunk-RQHHACP2.js.map} +0 -0
  142. /package/dist/{chunk-BSBWBQ7M.js.map → chunk-VUDZOCPW.js.map} +0 -0
@@ -14,6 +14,10 @@ import { EntityIcon, type EntityIconValue } from '../icon-display'
14
14
  * (agent/persona `icon_props.color`). */
15
15
  export type QuickActionAccent = 'pink' | 'cyan' | (string & {})
16
16
 
17
+ /** THE agent→accent pairs (one spelling — {@link getAgentAccent} and
18
+ * {@link QUICK_ACTION_THEMES} both derive from it). */
19
+ const AGENT_ACCENTS = { fae: 'pink', mingo: 'cyan' } as const
20
+
17
21
  /**
18
22
  * FALLBACK-ONLY agent→accent mapping (fae→pink, mingo→cyan) for the built-in
19
23
  * agents when nothing is configured. A color configured on the entity itself —
@@ -23,9 +27,7 @@ export type QuickActionAccent = 'pink' | 'cyan' | (string & {})
23
27
  * comparison elsewhere.
24
28
  */
25
29
  export function getAgentAccent(slug: string | null | undefined): QuickActionAccent | undefined {
26
- if (slug === 'fae') return 'pink'
27
- if (slug === 'mingo') return 'cyan'
28
- return undefined
30
+ return slug === 'fae' || slug === 'mingo' ? AGENT_ACCENTS[slug] : undefined
29
31
  }
30
32
 
31
33
  /** Admin-configured identity color (`icon_props.color` on the agent/persona
@@ -63,18 +65,24 @@ const ACCENT_CLASS: Record<string, string> = {
63
65
  cyan: 'text-ods-flamingo-cyan',
64
66
  }
65
67
 
68
+ /** A spec is a plain data object, never a React element — so any non-element,
69
+ * non-array object is a spec; strings, elements, fragments, and ReactNode
70
+ * ARRAYS stay ReactNode. */
71
+ export function isQuickActionIconSpec(
72
+ icon: React.ReactNode | QuickActionIconSpec | undefined,
73
+ ): icon is QuickActionIconSpec {
74
+ return typeof icon === 'object' && icon !== null && !Array.isArray(icon) && !React.isValidElement(icon)
75
+ }
76
+
66
77
  /** Resolve a chip `icon` prop to a renderable node. Specs go through
67
78
  * `<EntityIcon>`; ReactNodes pass through untouched. */
68
79
  export function renderQuickActionIcon(
69
80
  icon: React.ReactNode | QuickActionIconSpec | undefined,
70
81
  ): 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)) {
82
+ if (!isQuickActionIconSpec(icon)) {
75
83
  return icon
76
84
  }
77
- const spec = icon as QuickActionIconSpec
85
+ const spec = icon
78
86
  if (!spec.name && !spec.url) return undefined
79
87
  // Named brand tokens tint via a text class on `currentColor`; any other
80
88
  // accent value is an admin-configured CSS color, delivered as the glyph's
@@ -104,6 +112,48 @@ export interface QuickActionChipLozenge {
104
112
  className?: string
105
113
  }
106
114
 
115
+ // =============================================================================
116
+ // Themes
117
+ // =============================================================================
118
+
119
+ /**
120
+ * THE quick-action wall/chip theme set — one spelling of "who does this work"
121
+ * across every surface (homepage hero tabs, marketing marquees, company-hub
122
+ * deck panels, chat empty states):
123
+ * - `fae` / `mingo` — the agent identities (pink / cyan), same values
124
+ * {@link getAgentAccent} resolves for the built-in agents.
125
+ * - `it` / `sec` — work-category themes on the ODS attention pair
126
+ * (IT = yellow routine work, security = red defense), deliberately NOT the
127
+ * agent identities so a category never reads as agent attribution. These two
128
+ * carry the classification lozenge (leading-edge affix) as part of the theme.
129
+ */
130
+ export type QuickActionTheme = 'fae' | 'mingo' | 'it' | 'sec'
131
+
132
+ export interface QuickActionThemeSpec {
133
+ /** Chip icon tint ({@link QuickActionAccent}: brand token or CSS color). */
134
+ accent: QuickActionAccent
135
+ /** Classification affix rendered inside the chip (category themes only). */
136
+ lozenge?: QuickActionChipLozenge
137
+ }
138
+
139
+ /** Caller-injected per-theme accent overrides — how SERVER-CONFIGURED agent
140
+ * colors reach the chips. Consumers resolve the configured value (e.g.
141
+ * {@link accentFromIdentityIcon} on the agent row's `icon_props.color`) and
142
+ * inject it here; {@link QUICK_ACTION_THEMES} below is fallback-only. */
143
+ export type QuickActionThemeAccents = Partial<Record<QuickActionTheme, QuickActionAccent>>
144
+
145
+ /** FALLBACK-ONLY theme specs (same contract as {@link getAgentAccent}): the
146
+ * built-in accents apply when the caller injects nothing. The fae/mingo
147
+ * accents in particular are server-configured per agent — walls/chips accept
148
+ * a {@link QuickActionThemeAccents} override (`themeAccents`/`themeAccent`)
149
+ * that always wins over this map. */
150
+ export const QUICK_ACTION_THEMES: Record<QuickActionTheme, QuickActionThemeSpec> = {
151
+ fae: { accent: AGENT_ACCENTS.fae },
152
+ mingo: { accent: AGENT_ACCENTS.mingo },
153
+ it: { accent: 'var(--color-warning)', lozenge: { label: 'IT', className: 'text-ods-warning' } },
154
+ sec: { accent: 'var(--color-error)', lozenge: { label: 'SEC', className: 'text-ods-error' } },
155
+ }
156
+
107
157
  /** Resolve a chip label + optional lozenge to the Tag label node. */
108
158
  function composeChipLabel(
109
159
  label: React.ReactNode,
@@ -136,9 +186,17 @@ export interface QuickActionChipButtonProps {
136
186
  /** Icon: a declarative {@link QuickActionIconSpec} (preferred — unified
137
187
  * EntityIcon resolution) or a pre-rendered ReactNode. */
138
188
  icon?: React.ReactNode | QuickActionIconSpec
139
- /** Optional {@link QuickActionChipLozenge} at the label's leading edge
140
- * (e.g. an IT/SEC classification affix). */
141
- lozenge?: QuickActionChipLozenge
189
+ /** Chip theme ({@link QuickActionTheme}): supplies the icon accent when the
190
+ * icon spec doesn't carry its own, and the lozenge when `lozenge` is
191
+ * `true`. Explicit values always win over the theme. */
192
+ theme?: QuickActionTheme
193
+ /** Caller-injected accent for `theme` — the server-configured agent color
194
+ * (resolve via {@link accentFromIdentityIcon}). Overrides the theme's
195
+ * built-in fallback accent; the icon spec's own `accent` still wins. */
196
+ themeAccent?: QuickActionAccent
197
+ /** {@link QuickActionChipLozenge} at the label's leading edge (e.g. an
198
+ * IT/SEC classification affix). `true` renders the `theme`'s lozenge. */
199
+ lozenge?: QuickActionChipLozenge | boolean
142
200
  /** `'primary'` = accent (yellow) chip, `'outline'` = bordered chip (default). */
143
201
  variant?: 'primary' | 'outline'
144
202
  /** Active single-select state (Figma "Feature Item" active): renders the
@@ -227,6 +285,8 @@ export function QuickActionChipSkeleton({ labelCh = 16, icon = true, lozenge = f
227
285
  export function QuickActionChipButton({
228
286
  label,
229
287
  icon,
288
+ theme,
289
+ themeAccent,
230
290
  lozenge,
231
291
  variant = 'outline',
232
292
  selected = false,
@@ -238,8 +298,16 @@ export function QuickActionChipButton({
238
298
  interactive = true,
239
299
  className,
240
300
  }: QuickActionChipButtonProps) {
241
- const resolvedIcon = renderQuickActionIcon(icon)
242
- const resolvedLabel = composeChipLabel(label, lozenge)
301
+ const themeSpec = theme ? QUICK_ACTION_THEMES[theme] : undefined
302
+ // Accent precedence: the icon spec's own accent (admin-configured color,
303
+ // agent identity) → caller-injected `themeAccent` (server-configured agent
304
+ // color) → the theme's built-in fallback.
305
+ const accent = themeAccent ?? themeSpec?.accent
306
+ const themedIcon =
307
+ accent && isQuickActionIconSpec(icon) && !icon.accent ? { ...icon, accent } : icon
308
+ const resolvedLozenge = lozenge === true ? themeSpec?.lozenge : lozenge === false ? undefined : lozenge
309
+ const resolvedIcon = renderQuickActionIcon(themedIcon)
310
+ const resolvedLabel = composeChipLabel(label, resolvedLozenge)
243
311
  const tagVariant = selected ? (selectedAccent === 'cyan' ? 'selectedCyan' : 'selected') : variant
244
312
  if (!interactive) {
245
313
  return <Tag variant={tagVariant} size={size} icon={resolvedIcon} label={resolvedLabel} className={className} />
@@ -1,8 +1,10 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { cn } from '../../utils/cn'
5
- import { QuickActionChipButton, type QuickActionIconSpec } from './quick-action-chip'
4
+ import type { MarqueeSyncController } from '../ui/marquee-wall'
5
+ import type { QuickActionIconSpec } from './quick-action-chip'
6
+ import type { QuickActionChip } from './chat-quick-action-row'
7
+ import { QuickActionWall } from './quick-action-wall'
6
8
 
7
9
  // =============================================================================
8
10
  // Types
@@ -17,76 +19,81 @@ export interface QuickActionMarqueeItem {
17
19
 
18
20
  export interface QuickActionMarqueeProps {
19
21
  items: ReadonlyArray<QuickActionMarqueeItem>
20
- /** Scroll direction. `'right'` reverses the animation. Default `'left'`. */
22
+ /** `'animated'` (default): endless scroll. `'plain'`: a static chip row —
23
+ * the consumer's opt-out of the marquee (no motion, no track padding
24
+ * effects beyond the plain layout). */
25
+ mode?: 'animated' | 'plain'
26
+ /** Scroll direction. `'right'` reverses the travel. Default `'left'`. */
21
27
  direction?: 'left' | 'right'
22
- /** Seconds per full loop. Default 40. */
23
- duration?: number
28
+ /** Scroll speed in px/s the shared marquee unit (CardsStrip cruises at
29
+ * 60). Default 40: on-screen speed stays constant no matter how many quick
30
+ * actions an agent has (the engine is position-based, not loop-based). */
31
+ speed?: number
24
32
  /** Pause the scroll while hovered. Default true. */
25
33
  pauseOnHover?: boolean
26
34
  /** When provided chips are interactive buttons; omitted → decorative tags. */
27
35
  onSelect?: (item: QuickActionMarqueeItem) => void
36
+ /** Repeat-pad the track to at least this many chips so one copy overflows
37
+ * the widest container this strip can get (the loop only engages on
38
+ * overflow). Default 12 ≈ 2600px+, enough for a full-bleed max-w-[1920px]
39
+ * section. */
40
+ minChips?: number
41
+ /** Pair with a sibling marquee under ONE position driver (the resolve
42
+ * strips' pending/resolved copies stay pixel-locked). */
43
+ sync?: MarqueeSyncController
28
44
  className?: string
29
45
  }
30
46
 
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
47
  // =============================================================================
36
48
  // Component
37
49
  // =============================================================================
38
50
 
39
51
  /**
40
52
  * 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`.
53
+ * marquee rows) the single-row preset of {@link QuickActionWall} (same
54
+ * chips, same engine, `flex-nowrap` layout). Clone-copy chips stay
55
+ * interactive when `onSelect` is set (clone wrapper a11y applies). Pauses on
56
+ * hover (optional) and always under `prefers-reduced-motion`.
45
57
  */
46
58
  export function QuickActionMarquee({
47
59
  items,
60
+ mode = 'animated',
48
61
  direction = 'left',
49
- duration = 40,
62
+ speed = 40,
50
63
  pauseOnHover = true,
51
64
  onSelect,
65
+ minChips = 12,
66
+ sync,
52
67
  className,
53
68
  }: 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])
69
+ const chips = React.useMemo<QuickActionChip[]>(
70
+ () =>
71
+ items.map(item => ({
72
+ id: item.id,
73
+ label: item.label,
74
+ icon: item.icon,
75
+ onSelect: onSelect ? () => onSelect(item) : undefined,
76
+ })),
77
+ [items, onSelect],
78
+ )
59
79
 
60
80
  if (items.length === 0) return null
61
81
 
62
82
  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>
83
+ <QuickActionWall
84
+ chips={chips}
85
+ mode={mode}
86
+ axis="x"
87
+ reverse={direction === 'right'}
88
+ speed={speed}
89
+ pauseOnHover={pauseOnHover}
90
+ // Plain mode shows the items once, untruncated; padding only serves
91
+ // the endless loop.
92
+ minChips={mode === 'plain' ? undefined : minChips}
93
+ copyGap="var(--spacing-system-xsf)"
94
+ className={className}
95
+ contentClassName="flex-nowrap items-center"
96
+ sync={sync}
97
+ />
91
98
  )
92
99
  }
@@ -0,0 +1,247 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../utils/cn'
5
+ import {
6
+ MarqueeWall,
7
+ MarqueeWallFades,
8
+ type MarqueeSyncController,
9
+ type MarqueeWallFadeEdge,
10
+ type MarqueeWallProps,
11
+ } from '../ui/marquee-wall'
12
+ import { QuickActionChipSkeleton } from './quick-action-chip'
13
+ import type { QuickActionTheme, QuickActionThemeAccents } from './quick-action-chip'
14
+ import { QuickActionChipFromData } from './chat-quick-action-row'
15
+ import type { QuickActionChip } from './chat-quick-action-row'
16
+
17
+ // =============================================================================
18
+ // Shared wall constants
19
+ // =============================================================================
20
+
21
+ /** Skeleton label widths (ch) — realistic spread, cycled. THE wall-skeleton
22
+ * spread, shared with the company-hub deck's ChipWall: sized so a 24-item
23
+ * wall overflows typical caps at any viewport width (same fill + fade as
24
+ * the loaded state — zero height jump when data lands). */
25
+ export const WALL_SKELETON_LABEL_CH = [17, 24, 14, 20, 15, 25, 12, 21, 16, 19, 13, 23]
26
+
27
+ /** Deterministic index-alternating merge — the mixed "one stream" pile
28
+ * (deck slide 3's IT+SEC blend and its Storybook mirror). Input order
29
+ * stable → the blend never reshuffles across renders or consumers. */
30
+ export function interleave<T>(a: ReadonlyArray<T>, b: ReadonlyArray<T>): T[] {
31
+ const out: T[] = []
32
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
33
+ if (a[i] !== undefined) out.push(a[i])
34
+ if (b[i] !== undefined) out.push(b[i])
35
+ }
36
+ return out
37
+ }
38
+
39
+ // Brick-mode padding default: ~14 columns of chips per row (~3200px) — one
40
+ // row copy overflows any sensible container, so the loop always engages.
41
+ const DEFAULT_MIN_CHIPS_PER_ROW = 14
42
+
43
+ // =============================================================================
44
+ // Types
45
+ // =============================================================================
46
+
47
+ export interface QuickActionWallProps {
48
+ /** The wall's chips — the shared {@link QuickActionChip} shape (same as the
49
+ * chat rows), including per-chip `theme`/`lozenge` for mixed IT/SEC
50
+ * streams. Chips with `onSelect` render as buttons; without, as tags. */
51
+ chips: ReadonlyArray<QuickActionChip>
52
+ /** Wall-default theme (fae/mingo/it/sec) for chips without their own. */
53
+ theme?: QuickActionTheme
54
+ /** Server-configured per-theme accent overrides
55
+ * ({@link QuickActionThemeAccents}) — inject the agents' configured colors
56
+ * (resolve via `accentFromIdentityIcon`); the built-in theme accents are
57
+ * fallback-only. Per-chip `themeAccent` still wins. */
58
+ themeAccents?: QuickActionThemeAccents
59
+ /** `'animated'` (default): marquee + overflow fades. `'plain'`: no motion,
60
+ * no blur — the consumer's per-surface opt-out (forwarded to
61
+ * {@link MarqueeWall}; in brick mode the rows render static and the
62
+ * stack-level fades are skipped). */
63
+ mode?: 'animated' | 'plain'
64
+ /** Render every chip's theme lozenge (classification walls). Per-chip
65
+ * `lozenge` values still win. */
66
+ lozenges?: boolean
67
+ /** Marquee axis — defaults to where `fade` sits (see {@link MarqueeWall}). */
68
+ axis?: MarqueeWallProps['axis']
69
+ reverse?: boolean
70
+ /** px/s. Default 40 (MarqueeWall default). */
71
+ speed?: number
72
+ /** Marquee on/off (auto-off when content fits / reduced motion / loading). */
73
+ animate?: boolean
74
+ pauseOnHover?: boolean
75
+ fade?: MarqueeWallFadeEdge | ReadonlyArray<MarqueeWallFadeEdge>
76
+ /** Color behind the wall — the fades blend into it. */
77
+ fadeColor?: string
78
+ fadeSize?: MarqueeWallProps['fadeSize']
79
+ /** THE wall pitch — one value drives the chip gap, the clone-seam gap, and
80
+ * (in brick mode) the row-stack gap, so the courses read as a uniform wall
81
+ * and the seam is invisible by construction. Default
82
+ * `var(--spacing-system-xsf)`. */
83
+ copyGap?: number | string
84
+ /** Pad the track by REPEATING the chips up to this count (suffixed keys) so
85
+ * one copy always overflows the container and the loop engages — without
86
+ * it a wall whose content roughly fits its box goes static (the marquee
87
+ * only loops what overflows). Defaults to `rows × 14` in brick mode;
88
+ * unset otherwise (capped walls that always overflow, and FLIP-tracked
89
+ * walls where repeats would duplicate flip ids, need no padding). */
90
+ minChips?: number
91
+ /**
92
+ * BRICK-WALL mode: render this many stacked independent single-row
93
+ * marquees (chips distributed round-robin) instead of one track. Rows pack
94
+ * their chips edge-to-edge like bricks — a grid would column-align chips
95
+ * of different widths and leave holes — and each row loops on its own
96
+ * wrap, so the courses stagger organically. Horizontal only; the fades
97
+ * draw ONCE over the whole stack.
98
+ */
99
+ rows?: number
100
+ /** Draw skeleton chips instead of `chips` (the shared
101
+ * {@link QuickActionChipSkeleton} — 1:1 geometry with loaded chips).
102
+ * Callers own the policy (`loading` flag vs skeleton-when-empty). */
103
+ loading?: boolean
104
+ skeletonCount?: number
105
+ /** Outer container (sizing: height cap for vertical walls). */
106
+ className?: string
107
+ /** Per-copy layout — default a wrapping chip wall. Override for single-row
108
+ * marquee strips (`flex-nowrap`). */
109
+ contentClassName?: string
110
+ /** Pair with sibling walls under one position driver (resolve strips). */
111
+ sync?: MarqueeSyncController
112
+ }
113
+
114
+ // =============================================================================
115
+ // Component
116
+ // =============================================================================
117
+
118
+ /**
119
+ * <QuickActionWall> — THE quick-action chip wall: a themed pile of the shared
120
+ * chip on the shared {@link MarqueeWall} engine. One component for every
121
+ * surface that shows "the work the agents do" — homepage hero tabs, deck
122
+ * panels, marketing strips — whether static-with-fade or endlessly scrolling
123
+ * (axis follows the fade), interactive or decorative.
124
+ *
125
+ * Clone-copy chips stay INTERACTIVE (pointer cursor + working click) — the
126
+ * loop paints clones inside the viewport (reverse walls even START on the
127
+ * clone copy), and a visible chip that ignores clicks reads as broken. A11y
128
+ * holds via the clone wrapper: aria-hidden + focus suppression (the standard
129
+ * Swiper/Splide loop-clone treatment, same as CardsStrip). `pauseOnHover`
130
+ * (default on) freezes the track under a pointing cursor so chips never
131
+ * dodge a click.
132
+ */
133
+ export function QuickActionWall({
134
+ chips,
135
+ theme,
136
+ themeAccents,
137
+ mode = 'animated',
138
+ lozenges = false,
139
+ axis,
140
+ reverse,
141
+ speed,
142
+ animate = true,
143
+ pauseOnHover,
144
+ fade,
145
+ fadeColor,
146
+ fadeSize,
147
+ copyGap,
148
+ minChips,
149
+ rows,
150
+ loading = false,
151
+ skeletonCount = 24,
152
+ className,
153
+ contentClassName,
154
+ sync,
155
+ }: QuickActionWallProps) {
156
+ // Repeat-pad the track (stable suffixed keys; repeats keep the full chip —
157
+ // identical selected/interactive state, so every visible instance behaves
158
+ // the same and the clone copy stays geometry-identical by construction).
159
+ const padTarget = minChips ?? (rows ? rows * DEFAULT_MIN_CHIPS_PER_ROW : undefined)
160
+ const track = React.useMemo(() => {
161
+ if (!padTarget || chips.length === 0 || chips.length >= padTarget) return chips
162
+ const repeats = Math.ceil(padTarget / chips.length)
163
+ return Array.from({ length: repeats }, (_, r) =>
164
+ r === 0 ? chips : chips.map(chip => ({ ...chip, id: `${chip.id}__r${r}` })),
165
+ ).flat()
166
+ }, [chips, padTarget])
167
+
168
+ const renderSkeletons = (count: number, offset = 0) =>
169
+ Array.from({ length: count }, (_, i) => (
170
+ <QuickActionChipSkeleton
171
+ key={i}
172
+ lozenge={lozenges}
173
+ labelCh={WALL_SKELETON_LABEL_CH[(offset + i) % WALL_SKELETON_LABEL_CH.length]}
174
+ />
175
+ ))
176
+
177
+ const renderChipNodes = (list: ReadonlyArray<QuickActionChip>) =>
178
+ list.map(chip => (
179
+ <QuickActionChipFromData
180
+ key={chip.id}
181
+ chip={chip}
182
+ defaultTheme={theme}
183
+ themeAccents={themeAccents}
184
+ defaultLozenge={lozenges}
185
+ interactive={!!chip.onSelect}
186
+ className="max-w-full"
187
+ />
188
+ ))
189
+
190
+ const renderChips = () => (loading ? renderSkeletons(skeletonCount) : renderChipNodes(track))
191
+
192
+ // ONE pitch for chips, clone seams, and (brick mode) the row stack — a
193
+ // custom `copyGap` moves all three together, so no seam can ever disagree
194
+ // with the chip gap.
195
+ const gap = copyGap ?? 'var(--spacing-system-xsf)'
196
+
197
+ // ---- BRICK-WALL mode: stacked independent row marquees ---------------------
198
+ if (rows && rows > 0) {
199
+ const rowLists = Array.from({ length: rows }, (_, r) => track.filter((_, i) => i % rows === r))
200
+ const skelPerRow = Math.ceil(skeletonCount / rows)
201
+ return (
202
+ <div className={cn('relative flex flex-col overflow-hidden', className)} style={{ gap }}>
203
+ {rowLists.map((list, r) => (
204
+ <MarqueeWall
205
+ key={r}
206
+ mode={mode}
207
+ axis="x"
208
+ reverse={reverse}
209
+ speed={speed}
210
+ animate={animate && !loading}
211
+ pauseOnHover={pauseOnHover}
212
+ copyGap={gap}
213
+ contentClassName={cn('flex items-center', contentClassName)}
214
+ contentStyle={{ gap }}
215
+ >
216
+ {loading ? renderSkeletons(skelPerRow, r * 3) : renderChipNodes(list)}
217
+ </MarqueeWall>
218
+ ))}
219
+ {mode === 'animated' && (
220
+ <MarqueeWallFades fade={fade} fadeColor={fadeColor} fadeSize={fadeSize} />
221
+ )}
222
+ </div>
223
+ )
224
+ }
225
+
226
+ return (
227
+ <MarqueeWall
228
+ mode={mode}
229
+ axis={axis}
230
+ reverse={reverse}
231
+ speed={speed}
232
+ // Skeleton walls stay put — the marquee starts when real chips land.
233
+ animate={animate && !loading}
234
+ pauseOnHover={pauseOnHover}
235
+ fade={fade}
236
+ fadeColor={fadeColor}
237
+ fadeSize={fadeSize}
238
+ copyGap={gap}
239
+ className={className}
240
+ contentClassName={cn('flex flex-wrap content-start items-start', contentClassName)}
241
+ contentStyle={{ gap }}
242
+ sync={sync}
243
+ >
244
+ {renderChips()}
245
+ </MarqueeWall>
246
+ )
247
+ }