@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.
- package/dist/{chunk-DJP5HYFG.cjs → chunk-26JSJMMZ.cjs} +7 -7
- package/dist/{chunk-DJP5HYFG.cjs.map → chunk-26JSJMMZ.cjs.map} +1 -1
- package/dist/{chunk-NPBTNWRA.js → chunk-3ITY2LBM.js} +4 -4
- package/dist/{chunk-MWRFPLNB.js → chunk-4SZDO3TE.js} +3 -3
- package/dist/{chunk-LWT6G75Q.js → chunk-4VSTLJDO.js} +2 -2
- package/dist/{chunk-DRKQNXSZ.cjs → chunk-5HW5HMNF.cjs} +5 -5
- package/dist/{chunk-DRKQNXSZ.cjs.map → chunk-5HW5HMNF.cjs.map} +1 -1
- package/dist/{chunk-KKJRBMG3.cjs → chunk-5OY7HYII.cjs} +73 -73
- package/dist/{chunk-KKJRBMG3.cjs.map → chunk-5OY7HYII.cjs.map} +1 -1
- package/dist/{chunk-WYPROXL4.cjs → chunk-6GJKSYM2.cjs} +73 -73
- package/dist/{chunk-WYPROXL4.cjs.map → chunk-6GJKSYM2.cjs.map} +1 -1
- package/dist/{chunk-6TS4AIDX.cjs → chunk-6RA4NRBA.cjs} +15 -15
- package/dist/{chunk-6TS4AIDX.cjs.map → chunk-6RA4NRBA.cjs.map} +1 -1
- package/dist/{chunk-X754WDWO.cjs → chunk-76EN3GEH.cjs} +40 -40
- package/dist/{chunk-X754WDWO.cjs.map → chunk-76EN3GEH.cjs.map} +1 -1
- package/dist/{chunk-YSNMV53O.js → chunk-CM5XKHHS.js} +6 -6
- package/dist/{chunk-47AMTR2S.cjs → chunk-DQ5U6DCR.cjs} +18 -18
- package/dist/{chunk-47AMTR2S.cjs.map → chunk-DQ5U6DCR.cjs.map} +1 -1
- package/dist/{chunk-YHNBJ3F3.js → chunk-EQWE64RJ.js} +2 -2
- package/dist/{chunk-5SJGFLL2.js → chunk-HXCQHMS5.js} +6386 -6032
- package/dist/chunk-HXCQHMS5.js.map +1 -0
- package/dist/{chunk-VSWFYFIG.js → chunk-HYIHXXF3.js} +5 -5
- package/dist/{chunk-AZKVNRZP.cjs → chunk-J5BYYZVM.cjs} +32 -32
- package/dist/{chunk-AZKVNRZP.cjs.map → chunk-J5BYYZVM.cjs.map} +1 -1
- package/dist/{chunk-JAASDJOF.js → chunk-K3GXIGN6.js} +5 -5
- package/dist/{chunk-UNQVTGVG.js → chunk-M3AKWFAO.js} +2 -2
- package/dist/{chunk-TKGCTDQY.js → chunk-MLFFBQ5B.js} +180 -87
- package/dist/chunk-MLFFBQ5B.js.map +1 -0
- package/dist/{chunk-XLFAFH2B.js → chunk-N53FC732.js} +2 -2
- package/dist/{chunk-H56AEHFC.cjs → chunk-N5M72LJX.cjs} +1620 -1266
- package/dist/chunk-N5M72LJX.cjs.map +1 -0
- package/dist/{chunk-4IMXFDRI.js → chunk-PCW2TRZ4.js} +3 -3
- package/dist/{chunk-AN7MCJTV.cjs → chunk-QF3NQW7C.cjs} +122 -29
- package/dist/chunk-QF3NQW7C.cjs.map +1 -0
- package/dist/{chunk-EOFENGSZ.cjs → chunk-ROBGJH52.cjs} +12 -12
- package/dist/{chunk-EOFENGSZ.cjs.map → chunk-ROBGJH52.cjs.map} +1 -1
- package/dist/{chunk-HQ7S6JG5.js → chunk-RQHHACP2.js} +3 -3
- package/dist/{chunk-ROCJMSJI.cjs → chunk-UPRY4I63.cjs} +3 -3
- package/dist/{chunk-ROCJMSJI.cjs.map → chunk-UPRY4I63.cjs.map} +1 -1
- package/dist/{chunk-LQTX7EJS.cjs → chunk-VLDZ4363.cjs} +29 -29
- package/dist/{chunk-LQTX7EJS.cjs.map → chunk-VLDZ4363.cjs.map} +1 -1
- package/dist/{chunk-BSBWBQ7M.js → chunk-VUDZOCPW.js} +6 -6
- package/dist/{chunk-4S4RR7XC.cjs → chunk-XITG5ICQ.cjs} +40 -40
- package/dist/{chunk-4S4RR7XC.cjs.map → chunk-XITG5ICQ.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/chat-quick-action-row.d.ts +26 -1
- package/dist/components/chat/chat-quick-action-row.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +15 -3
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +14 -2
- package/dist/components/chat/quick-action-chip.d.ts +45 -4
- package/dist/components/chat/quick-action-chip.d.ts.map +1 -1
- package/dist/components/chat/quick-action-marquee.d.ts +24 -9
- package/dist/components/chat/quick-action-marquee.d.ts.map +1 -1
- package/dist/components/chat/quick-action-wall.d.ts +96 -0
- package/dist/components/chat/quick-action-wall.d.ts.map +1 -0
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/content-loading-container.d.ts +1 -1
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/cards-strip.d.ts.map +1 -1
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/form.d.ts +1 -1
- package/dist/components/form.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +159 -135
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +35 -11
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +27 -3
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +26 -2
- package/dist/components/ui/marquee-wall.d.ts +159 -0
- package/dist/components/ui/marquee-wall.d.ts.map +1 -0
- package/dist/hooks/index.cjs +6 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +5 -1
- package/dist/hooks/ui/index.d.ts +2 -0
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/ui/use-marquee-engine.d.ts +73 -0
- package/dist/hooks/ui/use-marquee-engine.d.ts.map +1 -0
- package/dist/hooks/ui/use-suppress-clone-focus.d.ts +24 -0
- package/dist/hooks/ui/use-suppress-clone-focus.d.ts.map +1 -0
- package/dist/index.cjs +31 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +30 -2
- package/dist/tailwind.config.cjs +1 -8
- package/dist/tailwind.config.cjs.map +1 -1
- package/dist/tailwind.config.js +1 -8
- package/dist/tailwind.config.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/chat-quick-action-row.tsx +47 -3
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/quick-action-chip.tsx +81 -13
- package/src/components/chat/quick-action-marquee.tsx +54 -47
- package/src/components/chat/quick-action-wall.tsx +247 -0
- package/src/components/features/cards-strip.tsx +88 -158
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/marquee-wall.tsx +522 -0
- package/src/hooks/ui/index.ts +2 -0
- package/src/hooks/ui/use-marquee-engine.ts +158 -0
- package/src/hooks/ui/use-suppress-clone-focus.ts +61 -0
- package/src/stories/MarqueeWall.stories.tsx +132 -0
- package/src/stories/QuickActionMarquee.stories.tsx +8 -7
- package/src/stories/QuickActionWall.stories.tsx +116 -0
- package/tailwind.config.ts +0 -7
- package/dist/chunk-5SJGFLL2.js.map +0 -1
- package/dist/chunk-AN7MCJTV.cjs.map +0 -1
- package/dist/chunk-H56AEHFC.cjs.map +0 -1
- package/dist/chunk-TKGCTDQY.js.map +0 -1
- /package/dist/{chunk-NPBTNWRA.js.map → chunk-3ITY2LBM.js.map} +0 -0
- /package/dist/{chunk-MWRFPLNB.js.map → chunk-4SZDO3TE.js.map} +0 -0
- /package/dist/{chunk-LWT6G75Q.js.map → chunk-4VSTLJDO.js.map} +0 -0
- /package/dist/{chunk-YSNMV53O.js.map → chunk-CM5XKHHS.js.map} +0 -0
- /package/dist/{chunk-YHNBJ3F3.js.map → chunk-EQWE64RJ.js.map} +0 -0
- /package/dist/{chunk-VSWFYFIG.js.map → chunk-HYIHXXF3.js.map} +0 -0
- /package/dist/{chunk-JAASDJOF.js.map → chunk-K3GXIGN6.js.map} +0 -0
- /package/dist/{chunk-UNQVTGVG.js.map → chunk-M3AKWFAO.js.map} +0 -0
- /package/dist/{chunk-XLFAFH2B.js.map → chunk-N53FC732.js.map} +0 -0
- /package/dist/{chunk-4IMXFDRI.js.map → chunk-PCW2TRZ4.js.map} +0 -0
- /package/dist/{chunk-HQ7S6JG5.js.map → chunk-RQHHACP2.js.map} +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
140
|
-
*
|
|
141
|
-
|
|
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
|
|
242
|
-
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
23
|
-
|
|
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)
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
-
|
|
62
|
+
speed = 40,
|
|
50
63
|
pauseOnHover = true,
|
|
51
64
|
onSelect,
|
|
65
|
+
minChips = 12,
|
|
66
|
+
sync,
|
|
52
67
|
className,
|
|
53
68
|
}: QuickActionMarqueeProps) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
}
|