@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.
- package/dist/{chunk-2LTQEQWH.js → chunk-25VKZ2QM.js} +2 -2
- package/dist/{chunk-LUEVAFJP.js → chunk-33CU7BKK.js} +4 -4
- package/dist/{chunk-DNJCIVD6.js → chunk-3L34VSKP.js} +2 -2
- package/dist/{chunk-KMYPDZEO.js → chunk-4ZHXIY5J.js} +3982 -3858
- package/dist/chunk-4ZHXIY5J.js.map +1 -0
- package/dist/{chunk-ZPY53IWK.cjs → chunk-57ZLSUWZ.cjs} +56 -56
- package/dist/{chunk-ZPY53IWK.cjs.map → chunk-57ZLSUWZ.cjs.map} +1 -1
- package/dist/{chunk-LTALFWHU.cjs → chunk-5EFJJBU5.cjs} +31 -31
- package/dist/{chunk-LTALFWHU.cjs.map → chunk-5EFJJBU5.cjs.map} +1 -1
- package/dist/{chunk-VS4EZRU3.cjs → chunk-5PO6CQVE.cjs} +62 -62
- package/dist/{chunk-VS4EZRU3.cjs.map → chunk-5PO6CQVE.cjs.map} +1 -1
- package/dist/{chunk-YA2LLBWQ.js → chunk-ACAVASHV.js} +2 -2
- package/dist/{chunk-QPGENYQV.js → chunk-BLUIJE4A.js} +4 -4
- package/dist/{chunk-E66A6UJC.js → chunk-CDQY7PJI.js} +4 -4
- package/dist/{chunk-5NHJC3GQ.js → chunk-ECK3O7S7.js} +2 -2
- package/dist/{chunk-QZ3KUDFG.cjs → chunk-IU3ADZVS.cjs} +7 -7
- package/dist/{chunk-QZ3KUDFG.cjs.map → chunk-IU3ADZVS.cjs.map} +1 -1
- package/dist/{chunk-YEB3R52B.js → chunk-KCJS6HWH.js} +4 -4
- package/dist/{chunk-2GLO2HEJ.cjs → chunk-KKZMWFCK.cjs} +25 -25
- package/dist/{chunk-2GLO2HEJ.cjs.map → chunk-KKZMWFCK.cjs.map} +1 -1
- package/dist/{chunk-XULFRLBZ.js → chunk-LI4WU5DO.js} +2 -2
- package/dist/{chunk-TRUNSPFD.cjs → chunk-MLWAQ3QJ.cjs} +37 -37
- package/dist/{chunk-TRUNSPFD.cjs.map → chunk-MLWAQ3QJ.cjs.map} +1 -1
- package/dist/{chunk-EJSVHG4X.cjs → chunk-NOJCRTFP.cjs} +5 -5
- package/dist/{chunk-EJSVHG4X.cjs.map → chunk-NOJCRTFP.cjs.map} +1 -1
- package/dist/{chunk-XP7M5FPW.js → chunk-O4J55Q6W.js} +2 -2
- package/dist/{chunk-4NGPMJK6.cjs → chunk-PREABAGF.cjs} +12 -12
- package/dist/{chunk-4NGPMJK6.cjs.map → chunk-PREABAGF.cjs.map} +1 -1
- package/dist/{chunk-YUZ25NHF.cjs → chunk-PU7UCMSL.cjs} +15 -15
- package/dist/{chunk-YUZ25NHF.cjs.map → chunk-PU7UCMSL.cjs.map} +1 -1
- package/dist/{chunk-MGQVVGQV.cjs → chunk-R27F2P55.cjs} +1188 -1064
- package/dist/chunk-R27F2P55.cjs.map +1 -0
- package/dist/{chunk-LGQ4L3SX.cjs → chunk-SXM5YHP2.cjs} +26 -26
- package/dist/{chunk-LGQ4L3SX.cjs.map → chunk-SXM5YHP2.cjs.map} +1 -1
- package/dist/{chunk-35E52Q3L.cjs → chunk-TSNXAJOJ.cjs} +9 -9
- package/dist/{chunk-35E52Q3L.cjs.map → chunk-TSNXAJOJ.cjs.map} +1 -1
- package/dist/{chunk-CBVU3DFH.js → chunk-U3LE33OX.js} +2 -2
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-quick-action-row.d.ts +4 -1
- package/dist/components/chat/chat-quick-action-row.d.ts.map +1 -1
- package/dist/components/chat/chat-quick-action.d.ts +6 -0
- package/dist/components/chat/chat-quick-action.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/guide-welcome.d.ts +3 -0
- package/dist/components/chat/guide-welcome.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +12 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +2 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +11 -1
- package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
- package/dist/components/chat/quick-action-chip.d.ts +70 -0
- package/dist/components/chat/quick-action-chip.d.ts.map +1 -0
- package/dist/components/chat/quick-action-marquee.d.ts +29 -0
- package/dist/components/chat/quick-action-marquee.d.ts.map +1 -0
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +130 -120
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +19 -9
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +12 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +11 -1
- package/dist/components/ui/tab-navigation.d.ts.map +1 -1
- package/dist/index.cjs +12 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +11 -1
- package/dist/tailwind.config.cjs +8 -1
- package/dist/tailwind.config.cjs.map +1 -1
- package/dist/tailwind.config.js +8 -1
- package/dist/tailwind.config.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/chat-input.tsx +16 -9
- package/src/components/chat/chat-quick-action-row.tsx +20 -14
- package/src/components/chat/chat-quick-action.tsx +6 -0
- package/src/components/chat/embeddable-chat.tsx +19 -6
- package/src/components/chat/guide-welcome.tsx +15 -9
- package/src/components/chat/index.ts +2 -0
- package/src/components/chat/mingo-welcome.tsx +7 -12
- package/src/components/chat/quick-action-chip.tsx +155 -0
- package/src/components/chat/quick-action-marquee.tsx +92 -0
- package/src/components/ui/tab-navigation.tsx +8 -2
- package/src/stories/EmbeddableChat.stories.tsx +75 -0
- package/src/stories/QuickActionChip.stories.tsx +149 -0
- package/src/stories/QuickActionMarquee.stories.tsx +114 -0
- package/tailwind.config.ts +7 -0
- package/dist/chunk-KMYPDZEO.js.map +0 -1
- package/dist/chunk-MGQVVGQV.cjs.map +0 -1
- /package/dist/{chunk-2LTQEQWH.js.map → chunk-25VKZ2QM.js.map} +0 -0
- /package/dist/{chunk-LUEVAFJP.js.map → chunk-33CU7BKK.js.map} +0 -0
- /package/dist/{chunk-DNJCIVD6.js.map → chunk-3L34VSKP.js.map} +0 -0
- /package/dist/{chunk-YA2LLBWQ.js.map → chunk-ACAVASHV.js.map} +0 -0
- /package/dist/{chunk-QPGENYQV.js.map → chunk-BLUIJE4A.js.map} +0 -0
- /package/dist/{chunk-E66A6UJC.js.map → chunk-CDQY7PJI.js.map} +0 -0
- /package/dist/{chunk-5NHJC3GQ.js.map → chunk-ECK3O7S7.js.map} +0 -0
- /package/dist/{chunk-YEB3R52B.js.map → chunk-KCJS6HWH.js.map} +0 -0
- /package/dist/{chunk-XULFRLBZ.js.map → chunk-LI4WU5DO.js.map} +0 -0
- /package/dist/{chunk-XP7M5FPW.js.map → chunk-O4J55Q6W.js.map} +0 -0
- /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
|
|
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
|
|
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
|
+
}
|
package/tailwind.config.ts
CHANGED
|
@@ -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
|
},
|