@flamingo-stack/openframe-frontend-core 0.0.428 → 0.0.429-snapshot.20260715222543

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 (131) hide show
  1. package/dist/{chunk-43KGFHI3.js → chunk-2ELINEWQ.js} +2 -2
  2. package/dist/{chunk-OZTPC6VW.cjs → chunk-374KFDET.cjs} +31 -31
  3. package/dist/{chunk-OZTPC6VW.cjs.map → chunk-374KFDET.cjs.map} +1 -1
  4. package/dist/{chunk-55EIHSOQ.js → chunk-3AWPRVFM.js} +2 -2
  5. package/dist/{chunk-SLCANQN3.js → chunk-3OKVH6FW.js} +4 -4
  6. package/dist/{chunk-UXXCNZXC.js → chunk-3XWBE3SM.js} +4 -4
  7. package/dist/{chunk-7JUUQAAH.js → chunk-67LZDLIO.js} +2 -2
  8. package/dist/{chunk-YP3DO4BM.js → chunk-A6JKKKIF.js} +4 -4
  9. package/dist/{chunk-RR77Z27A.js → chunk-AY5TBDWZ.js} +2 -2
  10. package/dist/{chunk-LZYKGEN3.cjs → chunk-CJOFXDTK.cjs} +26 -26
  11. package/dist/{chunk-LZYKGEN3.cjs.map → chunk-CJOFXDTK.cjs.map} +1 -1
  12. package/dist/{chunk-I7YFFSDS.cjs → chunk-CTY37UUO.cjs} +59 -59
  13. package/dist/{chunk-I7YFFSDS.cjs.map → chunk-CTY37UUO.cjs.map} +1 -1
  14. package/dist/{chunk-NXCFIPEY.cjs → chunk-D4K53CES.cjs} +26 -26
  15. package/dist/{chunk-NXCFIPEY.cjs.map → chunk-D4K53CES.cjs.map} +1 -1
  16. package/dist/{chunk-FFDGTMZX.cjs → chunk-FL4IOCYH.cjs} +5 -5
  17. package/dist/{chunk-FFDGTMZX.cjs.map → chunk-FL4IOCYH.cjs.map} +1 -1
  18. package/dist/{chunk-7ELFQWYF.cjs → chunk-FYMD7CCA.cjs} +7 -7
  19. package/dist/{chunk-7ELFQWYF.cjs.map → chunk-FYMD7CCA.cjs.map} +1 -1
  20. package/dist/{chunk-CVYVCBMM.js → chunk-IMOJWQDR.js} +4 -4
  21. package/dist/{chunk-N2CTZ3Q2.cjs → chunk-KDVGC6EG.cjs} +37 -37
  22. package/dist/{chunk-N2CTZ3Q2.cjs.map → chunk-KDVGC6EG.cjs.map} +1 -1
  23. package/dist/{chunk-DA2VSDAX.js → chunk-NZBQGODM.js} +2 -2
  24. package/dist/{chunk-SJTNVETH.cjs → chunk-PP5KNHMK.cjs} +408 -233
  25. package/dist/chunk-PP5KNHMK.cjs.map +1 -0
  26. package/dist/{chunk-MJH4VS4L.cjs → chunk-Q7SYPXUX.cjs} +11 -11
  27. package/dist/{chunk-MJH4VS4L.cjs.map → chunk-Q7SYPXUX.cjs.map} +1 -1
  28. package/dist/{chunk-DQ5NRLCG.js → chunk-S7UXICHB.js} +2 -2
  29. package/dist/{chunk-KMXHSL6M.js → chunk-TDITU4KW.js} +2 -2
  30. package/dist/{chunk-OBDDE4WV.js → chunk-TYANQCZA.js} +3034 -2859
  31. package/dist/chunk-TYANQCZA.js.map +1 -0
  32. package/dist/{chunk-F4WTCWOY.cjs → chunk-WQN4B6CM.cjs} +15 -15
  33. package/dist/{chunk-F4WTCWOY.cjs.map → chunk-WQN4B6CM.cjs.map} +1 -1
  34. package/dist/{chunk-533XR4JK.cjs → chunk-WQR7H56P.cjs} +62 -62
  35. package/dist/{chunk-533XR4JK.cjs.map → chunk-WQR7H56P.cjs.map} +1 -1
  36. package/dist/{chunk-MO4QIPT7.cjs → chunk-ZUP2ZKS6.cjs} +9 -9
  37. package/dist/{chunk-MO4QIPT7.cjs.map → chunk-ZUP2ZKS6.cjs.map} +1 -1
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/chat-message-skeleton.d.ts +17 -2
  41. package/dist/components/chat/chat-message-skeleton.d.ts.map +1 -1
  42. package/dist/components/chat/chat-panel-header.d.ts +6 -1
  43. package/dist/components/chat/chat-panel-header.d.ts.map +1 -1
  44. package/dist/components/chat/chat-quick-action-row.d.ts +12 -2
  45. package/dist/components/chat/chat-quick-action-row.d.ts.map +1 -1
  46. package/dist/components/chat/hooks/index.d.ts +1 -0
  47. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  48. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +31 -2
  49. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  50. package/dist/components/chat/hooks/use-scripted-stream.d.ts +3 -0
  51. package/dist/components/chat/hooks/use-scripted-stream.d.ts.map +1 -0
  52. package/dist/components/chat/index.cjs +14 -2
  53. package/dist/components/chat/index.cjs.map +1 -1
  54. package/dist/components/chat/index.js +13 -1
  55. package/dist/components/chat/quick-action-chip.d.ts +5 -1
  56. package/dist/components/chat/quick-action-chip.d.ts.map +1 -1
  57. package/dist/components/chat/utils/index.d.ts +1 -0
  58. package/dist/components/chat/utils/index.d.ts.map +1 -1
  59. package/dist/components/chat/utils/scripted-stream.d.ts +38 -0
  60. package/dist/components/chat/utils/scripted-stream.d.ts.map +1 -0
  61. package/dist/components/contact/index.cjs +3 -3
  62. package/dist/components/contact/index.js +2 -2
  63. package/dist/components/docs/index.cjs +5 -5
  64. package/dist/components/docs/index.js +4 -4
  65. package/dist/components/embeds/index.cjs +3 -3
  66. package/dist/components/embeds/index.js +2 -2
  67. package/dist/components/faq/index.cjs +3 -3
  68. package/dist/components/faq/index.js +2 -2
  69. package/dist/components/features/board/index.d.ts +2 -2
  70. package/dist/components/features/board/index.d.ts.map +1 -1
  71. package/dist/components/features/board/ticket-card.d.ts +22 -2
  72. package/dist/components/features/board/ticket-card.d.ts.map +1 -1
  73. package/dist/components/features/index.cjs +6 -2
  74. package/dist/components/features/index.cjs.map +1 -1
  75. package/dist/components/features/index.js +5 -1
  76. package/dist/components/help-center-pages/index.cjs +20 -20
  77. package/dist/components/help-center-pages/index.js +11 -11
  78. package/dist/components/index.cjs +146 -128
  79. package/dist/components/index.cjs.map +1 -1
  80. package/dist/components/index.js +27 -9
  81. package/dist/components/index.js.map +1 -1
  82. package/dist/components/navigation/index.cjs +2 -2
  83. package/dist/components/navigation/index.js +1 -1
  84. package/dist/components/onboarding-guides/index.cjs +5 -5
  85. package/dist/components/onboarding-guides/index.js +4 -4
  86. package/dist/components/related-content/index.cjs +3 -3
  87. package/dist/components/related-content/index.js +2 -2
  88. package/dist/components/tickets/index.cjs +5 -5
  89. package/dist/components/tickets/index.js +4 -4
  90. package/dist/components/ui/index.cjs +16 -2
  91. package/dist/components/ui/index.cjs.map +1 -1
  92. package/dist/components/ui/index.d.ts +1 -0
  93. package/dist/components/ui/index.d.ts.map +1 -1
  94. package/dist/components/ui/index.js +15 -1
  95. package/dist/components/ui/info-card-row.d.ts +13 -0
  96. package/dist/components/ui/info-card-row.d.ts.map +1 -0
  97. package/dist/components/ui/tag.d.ts +1 -1
  98. package/dist/components/ui/tag.d.ts.map +1 -1
  99. package/dist/index.cjs +20 -2
  100. package/dist/index.cjs.map +1 -1
  101. package/dist/index.js +19 -1
  102. package/package.json +1 -1
  103. package/src/components/chat/chat-message-skeleton.tsx +40 -9
  104. package/src/components/chat/chat-panel-header.tsx +28 -16
  105. package/src/components/chat/chat-quick-action-row.tsx +33 -0
  106. package/src/components/chat/embeddable-chat.tsx +11 -1
  107. package/src/components/chat/hooks/index.ts +1 -0
  108. package/src/components/chat/hooks/use-nats-chat-adapter.ts +30 -1
  109. package/src/components/chat/hooks/use-scripted-stream.ts +69 -0
  110. package/src/components/chat/quick-action-chip.tsx +6 -1
  111. package/src/components/chat/utils/index.ts +2 -0
  112. package/src/components/chat/utils/scripted-stream.ts +111 -0
  113. package/src/components/features/board/index.ts +2 -2
  114. package/src/components/features/board/ticket-card.tsx +108 -64
  115. package/src/components/ui/index.ts +1 -0
  116. package/src/components/ui/info-card-row.tsx +70 -0
  117. package/src/components/ui/tag.tsx +7 -0
  118. package/src/stories/InfoCardRow.stories.tsx +68 -0
  119. package/dist/chunk-OBDDE4WV.js.map +0 -1
  120. package/dist/chunk-SJTNVETH.cjs.map +0 -1
  121. /package/dist/{chunk-43KGFHI3.js.map → chunk-2ELINEWQ.js.map} +0 -0
  122. /package/dist/{chunk-55EIHSOQ.js.map → chunk-3AWPRVFM.js.map} +0 -0
  123. /package/dist/{chunk-SLCANQN3.js.map → chunk-3OKVH6FW.js.map} +0 -0
  124. /package/dist/{chunk-UXXCNZXC.js.map → chunk-3XWBE3SM.js.map} +0 -0
  125. /package/dist/{chunk-7JUUQAAH.js.map → chunk-67LZDLIO.js.map} +0 -0
  126. /package/dist/{chunk-YP3DO4BM.js.map → chunk-A6JKKKIF.js.map} +0 -0
  127. /package/dist/{chunk-RR77Z27A.js.map → chunk-AY5TBDWZ.js.map} +0 -0
  128. /package/dist/{chunk-CVYVCBMM.js.map → chunk-IMOJWQDR.js.map} +0 -0
  129. /package/dist/{chunk-DA2VSDAX.js.map → chunk-NZBQGODM.js.map} +0 -0
  130. /package/dist/{chunk-DQ5NRLCG.js.map → chunk-S7UXICHB.js.map} +0 -0
  131. /package/dist/{chunk-KMXHSL6M.js.map → chunk-TDITU4KW.js.map} +0 -0
@@ -6,8 +6,18 @@ interface ChatMessageSkeletonProps {
6
6
  className?: string
7
7
  showAvatar?: boolean
8
8
  isUser?: boolean
9
+ /** How many body lines to draw under the name row. Default 3 (paragraph-ish
10
+ * reply). Pass 1 for surfaces whose real messages are single-line (e.g. the
11
+ * short scripted hero-demo turns), so the skeleton isn't ~3x too tall. */
12
+ bodyLines?: number
9
13
  }
10
14
 
15
+ // Body-line width palette: full-width lines then a short trailing line, so any
16
+ // `bodyLines` count ends on a short line like a real wrapped message.
17
+ const BODY_LINE_WIDTHS = ["w-full", "w-11/12", "w-4/5", "w-5/6"]
18
+ const bodyLineWidth = (index: number, total: number) =>
19
+ index === total - 1 ? "w-3/5" : BODY_LINE_WIDTHS[index % BODY_LINE_WIDTHS.length]
20
+
11
21
  /**
12
22
  * One message-row skeleton. Mirrors `ChatMessageEnhanced`'s real layout:
13
23
  * full panel width, no card background, and an INLINE avatar in the name row
@@ -19,7 +29,9 @@ export function ChatMessageSkeleton({
19
29
  className,
20
30
  showAvatar = true,
21
31
  isUser = false,
32
+ bodyLines = 3,
22
33
  }: ChatMessageSkeletonProps) {
34
+ const lines = Math.max(1, bodyLines)
23
35
  return (
24
36
  <div className={cn("relative py-[var(--spacing-system-s)]", className)}>
25
37
  <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
@@ -37,9 +49,12 @@ export function ChatMessageSkeleton({
37
49
 
38
50
  {/* Body lines — percentage widths so they reflow on panel resize. */}
39
51
  <div className="flex flex-col gap-2 pt-[var(--spacing-system-xxs)]">
40
- <div className="h-4 w-full rounded bg-ods-border animate-pulse" />
41
- <div className="h-4 w-11/12 rounded bg-ods-border animate-pulse" />
42
- <div className="h-4 w-3/5 rounded bg-ods-border animate-pulse" />
52
+ {Array.from({ length: lines }, (_, i) => (
53
+ <div
54
+ key={i}
55
+ className={cn("h-4 rounded bg-ods-border animate-pulse", bodyLineWidth(i, lines))}
56
+ />
57
+ ))}
43
58
  </div>
44
59
  </div>
45
60
  </div>
@@ -61,6 +76,17 @@ interface ChatMessageListSkeletonProps {
61
76
  * host wrapper is expected to own the panel padding; this stays empty by
62
77
  * default so the skeleton sits flush. */
63
78
  contentClassName?: string
79
+ /** Body lines per message row (forwarded to `ChatMessageSkeleton`). Default 3;
80
+ * pass 1 for single-line message surfaces. */
81
+ bodyLines?: number
82
+ /**
83
+ * FILL mode: top-anchor the rows and CLIP overflow (instead of bottom-anchoring
84
+ * + inner scroll). Combined with a generous `messageCount`, the rows cover the
85
+ * container top-to-bottom at ANY height — no per-container measuring, no empty
86
+ * band at the top. Use for loading surfaces that should read as "the whole
87
+ * panel is loading" rather than "a short thread pinned to the bottom".
88
+ */
89
+ fill?: boolean
64
90
  }
65
91
 
66
92
  export function ChatMessageListSkeleton({
@@ -69,6 +95,8 @@ export function ChatMessageListSkeleton({
69
95
  showAvatars = true,
70
96
  fullWidth = false,
71
97
  contentClassName,
98
+ bodyLines,
99
+ fill = false,
72
100
  }: ChatMessageListSkeletonProps) {
73
101
  const messages = Array.from({ length: messageCount }, (_, index) => ({
74
102
  id: index,
@@ -79,9 +107,10 @@ export function ChatMessageListSkeleton({
79
107
  <div className="relative flex-1 min-h-0 flex flex-col">
80
108
  <div
81
109
  className={cn(
82
- "flex h-full w-full flex-col overflow-y-auto overflow-x-hidden flex-1",
83
- "[scroll-behavior:smooth]",
84
- "scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
110
+ "flex h-full w-full flex-col flex-1",
111
+ fill
112
+ ? "overflow-hidden"
113
+ : "overflow-y-auto overflow-x-hidden [scroll-behavior:smooth] scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
85
114
  className,
86
115
  )}
87
116
  >
@@ -92,15 +121,17 @@ export function ChatMessageListSkeleton({
92
121
  : "mx-auto flex w-full max-w-ods-content-narrow flex-col min-w-0",
93
122
  contentClassName,
94
123
  )}
95
- style={{ minHeight: '100%' }}
124
+ style={fill ? undefined : { minHeight: '100%' }}
96
125
  >
97
- {/* Bottom-anchor the rows like the real list. */}
98
- <div className="flex-1" />
126
+ {/* Bottom-anchor the rows like the real list — unless FILL mode wants
127
+ them top-anchored + clipped to cover the whole panel. */}
128
+ {!fill && <div className="flex-1" />}
99
129
  {messages.map((message) => (
100
130
  <ChatMessageSkeleton
101
131
  key={message.id}
102
132
  showAvatar={showAvatars}
103
133
  isUser={message.isUser}
134
+ bodyLines={bodyLines}
104
135
  />
105
136
  ))}
106
137
  </div>
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
+ import { cn } from '../../utils/cn'
4
5
  import { Chevron01LeftIcon } from '../icons-v2-generated/arrows/chevron-01-left-icon'
5
6
  import {
6
7
  Ellipsis01Icon,
@@ -42,6 +43,11 @@ export interface ChatPanelHeaderProps {
42
43
  onArchive?: () => void
43
44
  /** Open the Chat Archive page — renders the clock button (list view only). */
44
45
  onOpenArchive?: () => void
46
+ /** Force the COMPACT (`h-14` desktop-style) bar at every width, skipping the
47
+ * full-screen mobile header. For EMBEDDED surfaces (e.g. a small preview
48
+ * panel) where the phone-sized full-bleed header (large `text-h2` title) is
49
+ * wrong regardless of viewport. Default false = viewport-responsive. */
50
+ compact?: boolean
45
51
  }
46
52
 
47
53
  /**
@@ -61,6 +67,7 @@ export function ChatPanelHeader({
61
67
  onRename,
62
68
  onArchive,
63
69
  onOpenArchive,
70
+ compact = false,
64
71
  }: ChatPanelHeaderProps) {
65
72
  // Desktop ⋯ menu (active, non-archived conversation only) — rename / archive.
66
73
  const menuItems = [
@@ -70,23 +77,28 @@ export function ChatPanelHeader({
70
77
 
71
78
  return (
72
79
  <>
73
- {/* Mobile (<md): a distinct layout (Figma node 7363:85532). */}
74
- <ChatPanelHeaderMobile
75
- className="flex md:hidden"
76
- showBack={showBack}
77
- title={title}
78
- backAriaLabel={backAriaLabel}
79
- isArchivedView={isArchivedView}
80
- onBack={onBack}
81
- onClose={onClose}
82
- onRestore={onRestore}
83
- onRename={onRename}
84
- onArchive={onArchive}
85
- onOpenArchive={onOpenArchive}
86
- />
80
+ {/* Mobile (<md): a distinct full-screen layout (Figma node 7363:85532).
81
+ Skipped in `compact` mode — an embedded preview keeps the small bar
82
+ at every width. */}
83
+ {!compact && (
84
+ <ChatPanelHeaderMobile
85
+ className="flex md:hidden"
86
+ showBack={showBack}
87
+ title={title}
88
+ backAriaLabel={backAriaLabel}
89
+ isArchivedView={isArchivedView}
90
+ onBack={onBack}
91
+ onClose={onClose}
92
+ onRestore={onRestore}
93
+ onRename={onRename}
94
+ onArchive={onArchive}
95
+ onOpenArchive={onOpenArchive}
96
+ />
97
+ )}
87
98
 
88
- {/* Desktop (md+): fixed-height bar with full-height divider action cells. */}
89
- <div className="hidden md:flex flex-shrink-0 h-14 w-full overflow-hidden border-b border-ods-border bg-ods-card">
99
+ {/* Compact bar: `md+` normally, or ALL widths when `compact`. Fixed-height
100
+ bar with full-height divider action cells. */}
101
+ <div className={cn('flex-shrink-0 h-14 w-full overflow-hidden border-b border-ods-border bg-ods-card', compact ? 'flex' : 'hidden md:flex')}>
90
102
  <div className="flex flex-1 min-w-0 items-center gap-2 px-4 py-3">
91
103
  {showBack ? (
92
104
  <>
@@ -9,8 +9,10 @@ import { Ellipsis01Icon } from '../icons-v2-generated'
9
9
  import { useAutoLimitTags } from '../../hooks/ui/use-auto-limit-tags'
10
10
  import {
11
11
  QuickActionChipButton,
12
+ QuickActionChipSkeleton,
12
13
  renderQuickActionIcon,
13
14
  type QuickActionIconSpec,
15
+ type QuickActionAccent,
14
16
  } from './quick-action-chip'
15
17
 
16
18
  // =============================================================================
@@ -27,6 +29,11 @@ export interface QuickActionChip {
27
29
  icon?: React.ReactNode | QuickActionIconSpec
28
30
  /** `'primary'` = accent (yellow) chip, `'outline'` = bordered chip (default). */
29
31
  variant?: 'primary' | 'outline'
32
+ /** Active single-select state — renders the accented `selected` skin
33
+ * (overrides `variant`). */
34
+ selected?: boolean
35
+ /** Accent for the `selected` skin (`'cyan'` = cyan twin, else pink). */
36
+ selectedAccent?: QuickActionAccent
30
37
  onSelect?: () => void
31
38
  /** Pointer/keyboard focus enters the chip — e.g. preview the full prompt in
32
39
  * the composer. */
@@ -47,9 +54,18 @@ export interface ChatQuickActionRowProps {
47
54
  * for roomy contexts (e.g. the Guide empty state) where overflow-hiding chips
48
55
  * would defeat per-chip hover/preview. */
49
56
  wrap?: boolean
57
+ /** Render `skeletonCount` loading chips (the real {@link QuickActionChipSkeleton},
58
+ * so geometry is 1:1 with the loaded chips) instead of `chips`. Wrap-aware. */
59
+ loading?: boolean
60
+ /** How many skeleton chips to draw when `loading`. Default 8. */
61
+ skeletonCount?: number
50
62
  className?: string
51
63
  }
52
64
 
65
+ // Deterministic label widths (in `ch`) so a loading wall reads like a populated
66
+ // one — a varied spread, not a uniform block.
67
+ const SKELETON_LABEL_CHS = [16, 9, 13, 7, 18, 8, 20, 11, 15, 10, 19, 12, 17, 8, 21, 14]
68
+
53
69
  // =============================================================================
54
70
  // Chip button
55
71
  // =============================================================================
@@ -62,6 +78,8 @@ function ChipButton({ chip }: { chip: QuickActionChip }) {
62
78
  label={chip.label}
63
79
  icon={chip.icon}
64
80
  variant={chip.variant ?? 'outline'}
81
+ selected={chip.selected}
82
+ selectedAccent={chip.selectedAccent}
65
83
  onSelect={chip.onSelect}
66
84
  onHoverStart={chip.onHoverStart}
67
85
  onHoverEnd={chip.onHoverEnd}
@@ -86,6 +104,8 @@ export function ChatQuickActionRow({
86
104
  leading,
87
105
  chips,
88
106
  wrap = false,
107
+ loading = false,
108
+ skeletonCount = 8,
89
109
  className,
90
110
  }: ChatQuickActionRowProps) {
91
111
  const auto = useAutoLimitTags({
@@ -96,6 +116,19 @@ export function ChatQuickActionRow({
96
116
  const visibleChips = chips.slice(0, auto.visibleCount)
97
117
  const hiddenChips = chips.slice(auto.visibleCount)
98
118
 
119
+ // Loading: draw skeleton chips (real `QuickActionChipSkeleton`, 1:1 geometry)
120
+ // in the same wrap/inline layout — one shared skeleton for every consumer, no
121
+ // hand-rolled bars.
122
+ if (loading) {
123
+ return (
124
+ <div className={cn('flex shrink-0 items-center gap-1', wrap && 'flex-wrap', className)}>
125
+ {Array.from({ length: skeletonCount }, (_, i) => (
126
+ <QuickActionChipSkeleton key={i} labelCh={SKELETON_LABEL_CHS[i % SKELETON_LABEL_CHS.length]} />
127
+ ))}
128
+ </div>
129
+ )
130
+ }
131
+
99
132
  if (!leading && chips.length === 0) return null
100
133
 
101
134
  // Wrap mode: no width-measurement / "⋯" overflow — every chip renders inline
@@ -1557,7 +1557,13 @@ function EmbeddableChatInner({
1557
1557
  // history has loaded (`hasMessages`). Driving the surface + content branch
1558
1558
  // off this makes the normal-chat open animate identically to the archived
1559
1559
  // one instead of lagging behind the message fetch.
1560
- const hasConversation = hasMessages || isOpeningDialog || isViewingArchived
1560
+ // In `previewMode`, the host drives the whole state (`mingoStateOverride`) and
1561
+ // there is no dialog manager to set `isOpeningDialog`. So an embedded preview
1562
+ // that wants a message-list skeleton (real header + composer, skeleton bubbles)
1563
+ // signals it via `isMessagesLoading` — treat that as an open conversation so
1564
+ // the content branch shows the skeleton instead of the new-user welcome.
1565
+ const hasConversation =
1566
+ hasMessages || isOpeningDialog || isViewingArchived || (previewMode && isMessagesLoading)
1561
1567
  // Opening a dialog whose history hasn't arrived yet — show a message-list
1562
1568
  // skeleton instead of an empty thread so the open reads as "loading" rather
1563
1569
  // than a blank flash before the bubbles stream in.
@@ -1680,6 +1686,10 @@ function EmbeddableChatInner({
1680
1686
  className="flex flex-1 min-h-0 flex-col animate-in fade-in-0 duration-200"
1681
1687
  >
1682
1688
  <ChatPanelHeader
1689
+ // Embedded previews (the hero demo tabs) keep the small bar at
1690
+ // every width — the phone-sized full-screen header (large title)
1691
+ // is wrong in a small embedded panel.
1692
+ compact={previewMode}
1683
1693
  // Guide-mode empty state shows a back-chevron + "Mingo Guide"
1684
1694
  // (back returns to the default Mingo welcome); an open
1685
1695
  // conversation shows back + the dialog title; the Mingo list
@@ -2,6 +2,7 @@
2
2
 
3
3
  export * from './use-chunk-catchup'
4
4
  export * from './use-collapsible'
5
+ export * from './use-scripted-stream'
5
6
  export * from './use-jetstream-dialog-subscription'
6
7
  export * from './use-nats-dialog-subscription'
7
8
  export * from './use-realtime-chunk-processor'
@@ -74,6 +74,7 @@ import type {
74
74
  ChunkData,
75
75
  FetchChunksFunction,
76
76
  HistoricalMessage,
77
+ MessageProcessingOptions,
77
78
  MessageSegment,
78
79
  NatsMessageType,
79
80
  StreamingPhase,
@@ -374,8 +375,14 @@ function updateTrailingAssistant(
374
375
  * here because the unified contract surfaces errors as banners, not
375
376
  * inline messages. (Hosts that need inline error bubbles can extend
376
377
  * the contract later.)
378
+ *
379
+ * Exported for host reuse (e.g. scripted marketing demos that rehydrate a
380
+ * stored `HistoricalMessage[]` via `processHistoricalMessagesWithErrors` and
381
+ * feed a `previewMode` EmbeddableChat). This mapper carries the segment/content
382
+ * shape only; author identity (name/avatar/authorType/timestamp) that a host
383
+ * needs for the demo is re-attached by the host from the same processed rows.
377
384
  */
378
- function mapProcessedToUnified(
385
+ export function mapProcessedToUnified(
379
386
  processed: Array<{
380
387
  id: string
381
388
  role: 'user' | 'assistant' | 'error'
@@ -404,6 +411,28 @@ function mapProcessedToUnified(
404
411
  return out
405
412
  }
406
413
 
414
+ /**
415
+ * ONE-CALL `HistoricalMessage[]` -> `UnifiedChatMessage[]` for hosts replaying a
416
+ * stored conversation outside the live adapter (scripted marketing demos, a
417
+ * `previewMode` EmbeddableChat). Does the full pipeline the adapter does inline:
418
+ * `processHistoricalMessagesWithErrors` -> `mapProcessedToUnified` -> re-attach
419
+ * the author identity (name / avatar / authorType / timestamp) the segment
420
+ * mapper drops. Hosts no longer hand-roll the two-step + re-attach.
421
+ */
422
+ export function historicalToUnified(
423
+ messages: HistoricalMessage[],
424
+ options: MessageProcessingOptions = {},
425
+ ): UnifiedChatMessage[] {
426
+ const { messages: processed } = processHistoricalMessagesWithErrors(messages, options)
427
+ const byId = new Map(processed.map((p) => [p.id, p]))
428
+ return mapProcessedToUnified(processed).map((u) => {
429
+ const p = byId.get(u.id)
430
+ return p
431
+ ? { ...u, name: p.name, avatar: p.avatar ?? null, authorType: p.authorType, timestamp: p.timestamp }
432
+ : u
433
+ })
434
+ }
435
+
407
436
  // =============================================================================
408
437
  // Hook
409
438
  // =============================================================================
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Playback clock for {@link buildStreamFrames}: advance through pre-built
5
+ * scripted frames like a live chat stream — each frame holds for its own
6
+ * `delayMs`, then the next reveals, parking on the last. Surface-agnostic: the
7
+ * caller maps `frame.messages` to its own chat shape and reads `frame.phase` /
8
+ * `frame.typing`.
9
+ *
10
+ * `resetKey` restarts playback from the top when a NEW conversation is selected
11
+ * (identity change) — NOT when the SAME conversation grows (e.g. an Approve
12
+ * appends its continuation), which must resume from where it paused. Because the
13
+ * frame list's shared prefix is deterministic, keeping the index across a grow
14
+ * resumes seamlessly into the appended frames.
15
+ *
16
+ * `prefers-reduced-motion` (or `enabled === false`) jumps straight to the final
17
+ * frame (everything revealed, idle).
18
+ */
19
+
20
+ import { useEffect, useState } from 'react'
21
+ import type { StreamFrame } from '../utils/scripted-stream'
22
+
23
+ const EMPTY_FRAME: StreamFrame = { messages: [], phase: 'idle', typing: false, delayMs: 0 }
24
+
25
+ function prefersReducedMotion(): boolean {
26
+ return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true
27
+ }
28
+
29
+ export function useScriptedStream(frames: StreamFrame[], resetKey: unknown, enabled = true): StreamFrame {
30
+ const [index, setIndex] = useState(0)
31
+ const [reducedMotion, setReducedMotion] = useState(false)
32
+
33
+ // Restart from the top on a new conversation, synchronously during render
34
+ // (React's "adjust state on prop change" pattern) so the previous stream's
35
+ // last frame never flashes before the reset effect runs.
36
+ const [prevKey, setPrevKey] = useState(resetKey)
37
+ if (resetKey !== prevKey) {
38
+ setPrevKey(resetKey)
39
+ setIndex(0)
40
+ }
41
+
42
+ useEffect(() => {
43
+ setReducedMotion(prefersReducedMotion())
44
+ const mq = window.matchMedia?.('(prefers-reduced-motion: reduce)')
45
+ if (!mq) return
46
+ const onChange = () => setReducedMotion(mq.matches)
47
+ mq.addEventListener('change', onChange)
48
+ return () => mq.removeEventListener('change', onChange)
49
+ }, [])
50
+
51
+ const playing = enabled && !reducedMotion && frames.length > 0
52
+ const lastIndex = Math.max(0, frames.length - 1)
53
+
54
+ // Advance one frame after the current frame's hold. When `frames` grows (an
55
+ // Approve appends more), this effect re-runs and, if we were parked on the old
56
+ // last frame, schedules the next — resuming the stream. `frames[index]` is
57
+ // clamped because a shrink (a shorter new conversation before the reset lands)
58
+ // could momentarily leave `index` past the end.
59
+ useEffect(() => {
60
+ if (!playing || index >= lastIndex) return
61
+ const current = frames[Math.min(index, lastIndex)] ?? EMPTY_FRAME
62
+ const t = setTimeout(() => setIndex((i) => Math.min(i + 1, lastIndex)), current.delayMs)
63
+ return () => clearTimeout(t)
64
+ }, [index, frames, playing, lastIndex])
65
+
66
+ if (frames.length === 0) return EMPTY_FRAME
67
+ if (!playing) return frames[lastIndex]
68
+ return frames[Math.min(index, lastIndex)] ?? EMPTY_FRAME
69
+ }
@@ -145,6 +145,10 @@ export interface QuickActionChipButtonProps {
145
145
  * Tag's `selected` variant (pink border + pink-secondary fill), overriding
146
146
  * `variant`. Used by chip groups acting as tabs (OpenFrame categories). */
147
147
  selected?: boolean
148
+ /** Accent for the `selected` skin: `'cyan'` uses the cyan twin, anything else
149
+ * (default) uses pink. Lets an agent's chip group match its own theme accent
150
+ * (fae pink / mingo cyan) instead of the fixed pink. */
151
+ selectedAccent?: QuickActionAccent
148
152
  /** Chip scale, forwarded to `Tag` — `'large'` is the Figma "Feature Item"
149
153
  * 48px chip (h3 bold label, 24px icon box). Default `'default'` (32px). */
150
154
  size?: 'default' | 'large'
@@ -226,6 +230,7 @@ export function QuickActionChipButton({
226
230
  lozenge,
227
231
  variant = 'outline',
228
232
  selected = false,
233
+ selectedAccent,
229
234
  size = 'default',
230
235
  onSelect,
231
236
  onHoverStart,
@@ -235,7 +240,7 @@ export function QuickActionChipButton({
235
240
  }: QuickActionChipButtonProps) {
236
241
  const resolvedIcon = renderQuickActionIcon(icon)
237
242
  const resolvedLabel = composeChipLabel(label, lozenge)
238
- const tagVariant = selected ? 'selected' : variant
243
+ const tagVariant = selected ? (selectedAccent === 'cyan' ? 'selectedCyan' : 'selected') : variant
239
244
  if (!interactive) {
240
245
  return <Tag variant={tagVariant} size={size} icon={resolvedIcon} label={resolvedLabel} className={className} />
241
246
  }
@@ -198,3 +198,5 @@ export {
198
198
  resolveSourceRowCTA,
199
199
  resolveSourceIcon,
200
200
  } from './source-row-cta'
201
+
202
+ export * from './scripted-stream'
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Scripted-conversation replay — turn a stored `HistoricalMessage[]` into ordered
3
+ * "mock SSE" playback frames that animate a chat surface exactly like the live
4
+ * NATS/SSE stream (a `thinking` pause → assistant text token-by-token → tool /
5
+ * approval segments popping in → `idle`), just replayed from a script instead of
6
+ * a network stream.
7
+ *
8
+ * Lives in the lib (not a host) so any consumer — marketing hero demos, docs,
9
+ * onboarding tours — can drive a `previewMode` `EmbeddableChat` or a
10
+ * `ChatMessageList` from a canned conversation without re-implementing the
11
+ * streaming cadence. Pair with {@link useScriptedStream} (the playback clock).
12
+ */
13
+
14
+ import type { HistoricalMessage, MessageData, StreamingPhase } from '../types'
15
+
16
+ /** Normalize the single-or-array `messageData` to an array (empty when absent). */
17
+ export function normalizeHistoricalMessageData(md: HistoricalMessage['messageData']): MessageData[] {
18
+ return Array.isArray(md) ? md : md ? [md] : []
19
+ }
20
+
21
+ /** A message the user authored carries an `owner`; assistant turns have none —
22
+ * drives whether a turn streams (assistant) or lands at once (user). */
23
+ export function isHistoricalUserTurn(m: HistoricalMessage): boolean {
24
+ return !!m.owner
25
+ }
26
+
27
+ const isTextSegment = (d: MessageData): d is MessageData & { type: 'TEXT'; text: string } =>
28
+ (d as { type?: string })?.type === 'TEXT' && typeof (d as { text?: unknown }).text === 'string'
29
+
30
+ /** One rendered step of the mocked stream. */
31
+ export interface StreamFrame {
32
+ /** The partial message list to show at this step. */
33
+ messages: HistoricalMessage[]
34
+ phase: StreamingPhase
35
+ /** The chat "typing" indicator (up during the `thinking` pause). */
36
+ typing: boolean
37
+ /** Hold time (ms) before advancing to the next frame. */
38
+ delayMs: number
39
+ }
40
+
41
+ // Playback cadence. Assistant turns "think" then stream token-by-token; user
42
+ // turns land at once. Tuned to read like a real chat without dragging.
43
+ const THINK_MS = 550
44
+ const TOKEN_MS = 55 // per word revealed while streaming text
45
+ const USER_MS = 480 // dwell on a user turn before the reply starts thinking
46
+ const SEGMENT_MS = 340 // a tool / approval card pops in
47
+ const POST_MS = 240 // dwell after a completed assistant turn
48
+
49
+ /** Split text into tokens that KEEP surrounding whitespace, so re-joining a
50
+ * prefix reproduces the original spacing exactly (`"a b"` → `["a ", "b"]`). */
51
+ function tokenize(text: string): string[] {
52
+ return text.match(/\s*\S+\s*/g) ?? (text ? [text] : [])
53
+ }
54
+
55
+ /**
56
+ * Expand a message list into ordered playback frames reproducing the live
57
+ * stream: each user turn lands whole; each assistant turn first shows a
58
+ * `thinking` indicator, then streams its text token-by-token, then pops any
59
+ * tool / approval segments. Frame COUNT for a given prefix is deterministic, so
60
+ * a host that appends a continuation (e.g. after an Approve) resumes seamlessly
61
+ * from where it paused — the shared prefix's frames are unchanged.
62
+ */
63
+ export function buildStreamFrames(messages: HistoricalMessage[]): StreamFrame[] {
64
+ const frames: StreamFrame[] = []
65
+ const revealed: HistoricalMessage[] = []
66
+ const push = (msgs: HistoricalMessage[], phase: StreamingPhase, typing: boolean, delayMs: number) =>
67
+ frames.push({ messages: msgs.slice(), phase, typing, delayMs })
68
+
69
+ messages.forEach((msg) => {
70
+ if (isHistoricalUserTurn(msg)) {
71
+ revealed.push(msg)
72
+ push(revealed, 'idle', false, USER_MS)
73
+ return
74
+ }
75
+
76
+ // Assistant turn: a `thinking` pause, then stream this turn's segments in
77
+ // order. The pause is emitted ONLY when there is already content to show
78
+ // (`revealed.length > 0`) — an empty first frame would hand the surface a
79
+ // zero-message list, flashing its empty state before the opening line.
80
+ if (revealed.length > 0) push(revealed, 'thinking', true, THINK_MS)
81
+
82
+ const segs = normalizeHistoricalMessageData(msg.messageData)
83
+ const built: MessageData[] = []
84
+ const emit = () => push([...revealed, { ...msg, messageData: [...built] }], 'streaming', false, 0)
85
+
86
+ segs.forEach((seg) => {
87
+ if (isTextSegment(seg)) {
88
+ const tokens = tokenize(seg.text)
89
+ let acc = ''
90
+ built.push({ ...seg, text: '' })
91
+ tokens.forEach((tok) => {
92
+ acc += tok
93
+ built[built.length - 1] = { ...seg, text: acc }
94
+ push([...revealed, { ...msg, messageData: [...built] }], 'streaming', false, TOKEN_MS)
95
+ })
96
+ built[built.length - 1] = { ...seg }
97
+ } else {
98
+ // Tool / approval segment: pops in fully.
99
+ built.push(seg)
100
+ emit()
101
+ frames[frames.length - 1].delayMs = SEGMENT_MS
102
+ }
103
+ })
104
+
105
+ revealed.push(msg)
106
+ push(revealed, 'idle', false, POST_MS)
107
+ })
108
+
109
+ if (frames.length === 0) push(revealed, 'idle', false, 0)
110
+ return frames
111
+ }
@@ -6,8 +6,8 @@ export { BoardColumn } from './board-column'
6
6
  export type { BoardColumnProps } from './board-column'
7
7
  export { BoardColumnHeader } from './board-column-header'
8
8
  export type { BoardColumnHeaderProps } from './board-column-header'
9
- export { TicketCard } from './ticket-card'
10
- export type { TicketCardProps } from './ticket-card'
9
+ export { TicketCard, TicketCardView, TicketCardBody } from './ticket-card'
10
+ export type { TicketCardProps, TicketCardViewProps, TicketCardBodyProps } from './ticket-card'
11
11
  export { BoardTicketApproval } from './board-ticket-approval'
12
12
  export type { BoardTicketApprovalProps } from './board-ticket-approval'
13
13
  export { TicketCardSkeleton } from './ticket-card-skeleton'