@flamingo-stack/openframe-frontend-core 0.0.222 → 0.0.223

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 (183) hide show
  1. package/dist/{chunk-AIKPR6BE.js → chunk-4VHWSAQR.js} +6069 -5235
  2. package/dist/chunk-4VHWSAQR.js.map +1 -0
  3. package/dist/{chunk-GMNE7P7V.cjs → chunk-4WBVMIKQ.cjs} +26 -26
  4. package/dist/{chunk-GMNE7P7V.cjs.map → chunk-4WBVMIKQ.cjs.map} +1 -1
  5. package/dist/{chunk-QYRV6MKX.js → chunk-53EA4QPZ.js} +13 -7
  6. package/dist/chunk-53EA4QPZ.js.map +1 -0
  7. package/dist/{chunk-4JOWZLEL.cjs → chunk-7TKYFZ3L.cjs} +7 -7
  8. package/dist/{chunk-4JOWZLEL.cjs.map → chunk-7TKYFZ3L.cjs.map} +1 -1
  9. package/dist/{chunk-IZA5WF44.cjs → chunk-ABBU7QOM.cjs} +12 -12
  10. package/dist/{chunk-IZA5WF44.cjs.map → chunk-ABBU7QOM.cjs.map} +1 -1
  11. package/dist/{chunk-KM4KK2A6.cjs → chunk-ENDUMC7Z.cjs} +2558 -1724
  12. package/dist/chunk-ENDUMC7Z.cjs.map +1 -0
  13. package/dist/{chunk-CCSEXVI4.js → chunk-GYYGA24C.js} +2 -2
  14. package/dist/{chunk-RMOHKAHA.js → chunk-ISWWP2BH.js} +3 -3
  15. package/dist/{chunk-QNYH3WUU.cjs → chunk-JUWPBMQM.cjs} +13 -7
  16. package/dist/chunk-JUWPBMQM.cjs.map +1 -0
  17. package/dist/{chunk-A2SFHYVF.cjs → chunk-KXLTWANQ.cjs} +10 -10
  18. package/dist/{chunk-A2SFHYVF.cjs.map → chunk-KXLTWANQ.cjs.map} +1 -1
  19. package/dist/{chunk-ND24XSVT.js → chunk-TA6Z2AKD.js} +3 -3
  20. package/dist/chunk-TA6Z2AKD.js.map +1 -0
  21. package/dist/{chunk-HRIAZQ5D.js → chunk-WOKB6RJ7.js} +3 -3
  22. package/dist/components/chat/chat-archive-page.d.ts.map +1 -1
  23. package/dist/components/chat/chat-composer.d.ts.map +1 -1
  24. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  25. package/dist/components/chat/chat-message-skeleton.d.ts +19 -4
  26. package/dist/components/chat/chat-message-skeleton.d.ts.map +1 -1
  27. package/dist/components/chat/chat-panel-header-mobile.d.ts +24 -0
  28. package/dist/components/chat/chat-panel-header-mobile.d.ts.map +1 -0
  29. package/dist/components/chat/chat-panel-header.d.ts +6 -0
  30. package/dist/components/chat/chat-panel-header.d.ts.map +1 -1
  31. package/dist/components/chat/chat-quick-action-row.d.ts +31 -0
  32. package/dist/components/chat/chat-quick-action-row.d.ts.map +1 -0
  33. package/dist/components/chat/embeddable-chat.d.ts +27 -0
  34. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  35. package/dist/components/chat/entity-cards/dispatch.d.ts +14 -0
  36. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  37. package/dist/components/chat/entity-cards/github-activity-card.d.ts +10 -1
  38. package/dist/components/chat/entity-cards/github-activity-card.d.ts.map +1 -1
  39. package/dist/components/chat/guide-welcome.d.ts.map +1 -1
  40. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +9 -0
  41. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  42. package/dist/components/chat/hooks/use-slash-commands.d.ts +26 -0
  43. package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
  44. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +12 -1
  45. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  46. package/dist/components/chat/hooks/use-unified-chat.d.ts +12 -0
  47. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
  48. package/dist/components/chat/index.cjs +9 -3
  49. package/dist/components/chat/index.cjs.map +1 -1
  50. package/dist/components/chat/index.js +8 -2
  51. package/dist/components/chat/mingo-chat-history.d.ts +10 -0
  52. package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
  53. package/dist/components/chat/mingo-info-card.d.ts +63 -0
  54. package/dist/components/chat/mingo-info-card.d.ts.map +1 -0
  55. package/dist/components/chat/mingo-welcome.d.ts +14 -1
  56. package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
  57. package/dist/components/chat/types/chat.types.d.ts +6 -0
  58. package/dist/components/chat/types/chat.types.d.ts.map +1 -1
  59. package/dist/components/chat/types/unified-chat-state.types.d.ts +30 -0
  60. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  61. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  62. package/dist/components/contact/index.cjs +4 -4
  63. package/dist/components/contact/index.js +3 -3
  64. package/dist/components/features/index.cjs +3 -3
  65. package/dist/components/features/index.js +2 -2
  66. package/dist/components/index.cjs +110 -104
  67. package/dist/components/index.cjs.map +1 -1
  68. package/dist/components/index.js +36 -30
  69. package/dist/components/index.js.map +1 -1
  70. package/dist/components/navigation/app-layout-drawer.d.ts +7 -0
  71. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  72. package/dist/components/navigation/index.cjs +3 -3
  73. package/dist/components/navigation/index.js +2 -2
  74. package/dist/components/onboarding-guides/index.cjs +26 -26
  75. package/dist/components/onboarding-guides/index.js +4 -4
  76. package/dist/components/tickets/index.cjs +67 -67
  77. package/dist/components/tickets/index.js +5 -5
  78. package/dist/components/ui/actions-menu.d.ts +9 -0
  79. package/dist/components/ui/actions-menu.d.ts.map +1 -1
  80. package/dist/components/ui/dropdown-button.d.ts +2 -2
  81. package/dist/components/ui/dropdown-button.d.ts.map +1 -1
  82. package/dist/components/ui/index.cjs +9 -3
  83. package/dist/components/ui/index.cjs.map +1 -1
  84. package/dist/components/ui/index.js +8 -2
  85. package/dist/components/ui/modal-v2.d.ts.map +1 -1
  86. package/dist/components/ui/more-actions-menu.d.ts +13 -0
  87. package/dist/components/ui/more-actions-menu.d.ts.map +1 -1
  88. package/dist/components/ui/square-avatar.d.ts +4 -0
  89. package/dist/components/ui/square-avatar.d.ts.map +1 -1
  90. package/dist/components/ui/textarea.d.ts.map +1 -1
  91. package/dist/hooks/index.cjs +2 -2
  92. package/dist/hooks/index.js +1 -1
  93. package/dist/hooks/ui/use-auto-limit-tags.d.ts +7 -1
  94. package/dist/hooks/ui/use-auto-limit-tags.d.ts.map +1 -1
  95. package/dist/index.cjs +9 -3
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/index.js +8 -2
  98. package/package.json +1 -1
  99. package/src/components/chat/chat-archive-page.tsx +32 -7
  100. package/src/components/chat/chat-composer.tsx +6 -2
  101. package/src/components/chat/chat-message-enhanced.tsx +24 -7
  102. package/src/components/chat/chat-message-list.tsx +1 -1
  103. package/src/components/chat/chat-message-skeleton.tsx +60 -60
  104. package/src/components/chat/chat-panel-header-mobile.tsx +94 -0
  105. package/src/components/chat/chat-panel-header.tsx +78 -50
  106. package/src/components/chat/chat-quick-action-row.tsx +146 -0
  107. package/src/components/chat/embeddable-chat.tsx +140 -45
  108. package/src/components/chat/entity-cards/dispatch.tsx +770 -232
  109. package/src/components/chat/entity-cards/github-activity-card.tsx +9 -3
  110. package/src/components/chat/guide-welcome.tsx +11 -8
  111. package/src/components/chat/hooks/use-nats-chat-adapter.ts +80 -7
  112. package/src/components/chat/hooks/use-slash-commands.ts +39 -0
  113. package/src/components/chat/hooks/use-sse-chat-adapter.ts +49 -45
  114. package/src/components/chat/hooks/use-unified-chat.ts +31 -8
  115. package/src/components/chat/mingo-chat-history.tsx +118 -36
  116. package/src/components/chat/mingo-info-card.tsx +214 -0
  117. package/src/components/chat/mingo-welcome.tsx +89 -36
  118. package/src/components/chat/types/chat.types.ts +4 -0
  119. package/src/components/chat/types/unified-chat-state.types.ts +35 -0
  120. package/src/components/chat/utils/process-historical-messages.ts +8 -0
  121. package/src/components/content-loading-container.tsx +1 -1
  122. package/src/components/empty-state.tsx +3 -3
  123. package/src/components/faq-accordion.tsx +2 -2
  124. package/src/components/features/changelog-manager.tsx +2 -2
  125. package/src/components/features/highlight-config-section.tsx +2 -2
  126. package/src/components/features/og-editor-preview.tsx +3 -3
  127. package/src/components/features/release-media-manager.tsx +2 -2
  128. package/src/components/loading/card-skeleton.tsx +3 -3
  129. package/src/components/loading/device-card-skeleton.tsx +1 -1
  130. package/src/components/loading/margin-report-skeleton.tsx +2 -2
  131. package/src/components/loading/organization-card-skeleton.tsx +1 -1
  132. package/src/components/loading/organization-icon-skeleton.tsx +1 -1
  133. package/src/components/loading/page-layout-skeleton.tsx +9 -9
  134. package/src/components/loading/users-grid-skeleton.tsx +2 -2
  135. package/src/components/logs-list.tsx +1 -1
  136. package/src/components/media-carousel.tsx +1 -1
  137. package/src/components/navigation/app-layout-drawer.tsx +70 -6
  138. package/src/components/open-source-features.tsx +2 -2
  139. package/src/components/ui/actions-menu.tsx +36 -13
  140. package/src/components/ui/allowed-domains-input.tsx +3 -3
  141. package/src/components/ui/cursor-pagination.tsx +1 -1
  142. package/src/components/ui/dropdown-button.tsx +6 -6
  143. package/src/components/ui/info-row.tsx +2 -2
  144. package/src/components/ui/modal-v2.tsx +8 -1
  145. package/src/components/ui/more-actions-menu.tsx +13 -0
  146. package/src/components/ui/square-avatar.tsx +6 -1
  147. package/src/components/ui/tags-input.tsx +1 -1
  148. package/src/components/ui/textarea.tsx +18 -4
  149. package/src/components/vendor-icon.tsx +1 -1
  150. package/src/components/vendor-tag.tsx +1 -1
  151. package/src/components/why-it-matters.tsx +1 -1
  152. package/src/hooks/ui/use-auto-limit-tags.ts +22 -8
  153. package/src/stories/ActionsMenu.stories.tsx +256 -0
  154. package/src/stories/AppLayoutSidebar.stories.tsx +6 -0
  155. package/src/stories/Board.stories.tsx +1 -1
  156. package/src/stories/ChatMingoInfoCards.stories.tsx +748 -0
  157. package/src/stories/ChatTicketList.stories.tsx +1 -1
  158. package/src/stories/DataTable.stories.tsx +17 -13
  159. package/src/stories/EmbeddableChat.stories.tsx +233 -1
  160. package/src/stories/MingoInfoCard.stories.tsx +193 -0
  161. package/src/stories/MingoOnboardingCard.stories.tsx +1 -1
  162. package/src/stories/Modal.stories.tsx +1 -0
  163. package/src/stories/NotificationDrawer.stories.tsx +14 -3
  164. package/src/stories/PageActions.stories.tsx +4 -4
  165. package/src/stories/PageContainer.stories.tsx +5 -5
  166. package/src/stories/PageLayout.stories.tsx +1 -1
  167. package/src/stories/TabSelector.stories.tsx +2 -2
  168. package/src/stories/Tag.stories.tsx +10 -5
  169. package/src/stories/TicketInfoSection.stories.tsx +23 -11
  170. package/src/stories/Toaster.stories.tsx +5 -5
  171. package/src/stories/__fixtures__/chat-card-decorator.tsx +44 -0
  172. package/dist/chunk-AIKPR6BE.js.map +0 -1
  173. package/dist/chunk-KM4KK2A6.cjs.map +0 -1
  174. package/dist/chunk-ND24XSVT.js.map +0 -1
  175. package/dist/chunk-QNYH3WUU.cjs.map +0 -1
  176. package/dist/chunk-QYRV6MKX.js.map +0 -1
  177. package/src/stories/.TicketDescriptionViewer.stories.md +0 -39
  178. package/src/stories/.TicketKnowledgeBaseList.stories.md +0 -36
  179. package/src/stories/TicketDescriptionViewer.stories.tsx +0 -70
  180. package/src/stories/TicketKnowledgeBaseList.stories.tsx +0 -51
  181. /package/dist/{chunk-CCSEXVI4.js.map → chunk-GYYGA24C.js.map} +0 -0
  182. /package/dist/{chunk-RMOHKAHA.js.map → chunk-ISWWP2BH.js.map} +0 -0
  183. /package/dist/{chunk-HRIAZQ5D.js.map → chunk-WOKB6RJ7.js.map} +0 -0
@@ -2,7 +2,8 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
- import { MoreActionsMenu } from '../ui/more-actions-menu'
5
+ import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
6
+ import { Button } from '../ui/button'
6
7
  import { Ellipsis01Icon } from '../icons-v2-generated'
7
8
  import type { DialogItem } from './types/component.types'
8
9
 
@@ -106,71 +107,91 @@ function MingoChatHistoryRow({
106
107
 
107
108
  const menuItems = [
108
109
  onRequestRename && {
110
+ id: 'rename',
109
111
  label: 'Rename chat',
110
112
  onClick: () => onRequestRename(dialog),
111
113
  },
112
114
  onRequestArchive && {
115
+ id: 'archive',
113
116
  label: 'Archive chat',
114
117
  onClick: () => onRequestArchive(dialog),
115
118
  },
116
- ].filter(Boolean) as { label: string; onClick: () => void }[]
119
+ ].filter(Boolean) as ActionsMenuItem[]
117
120
 
121
+ // The item is the full-width row (inside the group's bordered box). Padding
122
+ // lives on the inner content button (badge + title) only — the `⋯` is a
123
+ // SIBLING of that padded content, so it sits at the item's right edge, inside
124
+ // the item but outside its padded content area.
118
125
  return (
119
126
  <div
120
- role="button"
121
- tabIndex={0}
122
- onClick={() => onSelect?.(dialog.id)}
123
- onKeyDown={(e) => {
124
- if (e.key === 'Enter' || e.key === ' ') {
125
- e.preventDefault()
126
- onSelect?.(dialog.id)
127
- }
128
- }}
129
127
  className={cn(
130
- 'group/row flex h-12 items-center gap-[var(--spacing-system-xsf)] px-[var(--spacing-system-s)]',
131
- 'bg-ods-bg border-b border-ods-border last:border-b-0',
132
- 'cursor-pointer transition-colors hover:bg-ods-bg-hover focus:outline-none focus-visible:bg-ods-bg-hover',
128
+ 'group/row flex h-12 items-center bg-ods-bg border-b border-ods-border last:border-b-0',
129
+ 'transition-colors hover:bg-ods-bg-hover',
133
130
  isActive && 'bg-ods-bg-hover',
134
131
  )}
135
132
  >
136
- {unread > 0 ? (
137
- <span className="flex h-8 min-w-8 shrink-0 items-center justify-center rounded-md bg-ods-accent px-[var(--spacing-system-xsf)] text-h5 text-ods-text-on-accent">
138
- {unread > 99 ? '99+' : unread}
139
- </span>
140
- ) : null}
141
- <span
142
- className="min-w-0 flex-1 truncate text-h4 text-ods-text-primary"
143
- title={title}
133
+ <div
134
+ role="button"
135
+ tabIndex={0}
136
+ onClick={() => onSelect?.(dialog.id)}
137
+ onKeyDown={(e) => {
138
+ if (e.key === 'Enter' || e.key === ' ') {
139
+ e.preventDefault()
140
+ onSelect?.(dialog.id)
141
+ }
142
+ }}
143
+ className="flex min-w-0 flex-1 items-center gap-[var(--spacing-system-xsf)] p-[var(--spacing-system-s)] cursor-pointer focus:outline-none focus-visible:bg-ods-bg-hover"
144
144
  >
145
- {title}
146
- </span>
145
+ {unread > 0 ? (
146
+ <span className="flex h-8 min-w-8 shrink-0 items-center justify-center rounded-md bg-ods-accent px-[var(--spacing-system-xsf)] text-h5 text-ods-text-on-accent">
147
+ {unread > 99 ? '99+' : unread}
148
+ </span>
149
+ ) : null}
150
+ <span
151
+ className="min-w-0 flex-1 truncate text-h4 text-ods-text-primary"
152
+ title={title}
153
+ >
154
+ {title}
155
+ </span>
156
+ </div>
147
157
  {hasMenu ? (
148
- // Stop propagation so opening the menu doesn't also select the dialog.
149
- // Hidden until the row is hovered/focused (or its menu is open).
158
+ // Sibling of the padded content (not inside it): sits at the item's
159
+ // right edge, free of the content's left padding. Revealed on row
160
+ // hover/focus; kept visible while its menu is open. Hidden on mobile —
161
+ // it's a hover affordance (no hover on touch).
150
162
  <span
151
163
  onClick={(e) => e.stopPropagation()}
152
164
  className={cn(
153
- 'shrink-0 transition-opacity',
165
+ // `self-stretch` so the wrapper fills the row height — gives the
166
+ // button's `h-full` a definite parent to resolve against (under the
167
+ // row's `items-center` the wrapper would otherwise collapse to its
168
+ // content and `h-full` on the button would no-op).
169
+ 'self-stretch shrink-0 transition-opacity max-md:hidden',
154
170
  menuOpen
155
171
  ? 'opacity-100'
156
172
  : 'opacity-0 group-hover/row:opacity-100 focus-within:opacity-100',
157
173
  )}
158
174
  >
159
- <MoreActionsMenu
160
- ariaLabel="Chat actions"
161
- items={menuItems}
175
+ <ActionsMenuDropdown
176
+ triggerAriaLabel="Chat actions"
177
+ groups={[{ items: menuItems }]}
162
178
  open={menuOpen}
163
179
  onOpenChange={setMenuOpen}
164
180
  // Don't return focus (and its ring) to the `⋯` trigger on close.
165
181
  onCloseAutoFocus={(e) => e.preventDefault()}
166
- trigger={
167
- <button
168
- type="button"
182
+ customTrigger={
183
+ <Button
184
+ variant="transparent"
185
+ size="icon"
169
186
  aria-label="Chat actions"
170
- className="flex size-6 items-center justify-center rounded-md text-ods-text-secondary transition-colors hover:text-ods-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
187
+ // Square, grey icon white on hover. `h-full` matches the row
188
+ // height exactly (no breakpoint poke from `size="icon"`'s
189
+ // responsive `md:h-12`); `w-9` keeps it compact; `p-0` drops the
190
+ // icon-size padding. tailwind-merge so these win over defaults.
191
+ className="h-full md:h-full rounded-none p-0 text-ods-text-secondary hover:text-ods-text-primary"
171
192
  >
172
- <Ellipsis01Icon size={24} />
173
- </button>
193
+ <Ellipsis01Icon />
194
+ </Button>
174
195
  }
175
196
  />
176
197
  </span>
@@ -179,6 +200,67 @@ function MingoChatHistoryRow({
179
200
  )
180
201
  }
181
202
 
203
+ // =============================================================================
204
+ // Skeleton
205
+ // =============================================================================
206
+
207
+ /**
208
+ * Loading placeholder for the dialog history, shaped like the grouped row list
209
+ * above. Shown while the FIRST page of dialogs is in flight so the Mingo empty
210
+ * state doesn't flash the new-user greeting+grid before the list arrives (the
211
+ * two layouts are mutually exclusive — swapping between them mid-load reads as
212
+ * a flicker). Pure ODS tokens, `animate-pulse`.
213
+ */
214
+ export function MingoChatHistorySkeleton({ className }: { className?: string }) {
215
+ // Mirror the real grouped list: a group label, then a bordered container of
216
+ // `h-12` rows. `withBadge` adds the leading unread-badge square on the first
217
+ // N rows; `widths` varies the title-bar length so the placeholder reads like
218
+ // a list of differing-length chat titles, not identical bars. `bg-ods-border`
219
+ // (not `bg-ods-bg-secondary`) so the placeholders are actually visible on the
220
+ // dark surface.
221
+ const groups: ReadonlyArray<{ withBadge: number; widths: ReadonlyArray<string> }> = [
222
+ { withBadge: 2, widths: ['w-3/5', 'w-4/5', 'w-2/5', 'w-3/4'] },
223
+ { withBadge: 0, widths: ['w-1/2', 'w-3/5', 'w-2/3'] },
224
+ ]
225
+ return (
226
+ <div
227
+ aria-hidden
228
+ // `overflow-hidden` clips the placeholder rows to the flex-1 box (the real
229
+ // list scrolls; the skeleton just needs to stay within bounds). Without it
230
+ // the rows bleed past their allotment and render under the pinned composer
231
+ // — which has no surface of its own — so the input appears to overlap them.
232
+ className={cn(
233
+ 'flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-hidden',
234
+ className,
235
+ )}
236
+ >
237
+ {groups.map((group, g) => (
238
+ <div key={g} className="flex shrink-0 flex-col gap-[var(--spacing-system-xxs)]">
239
+ {/* Group label (Today / Yesterday). */}
240
+ <div className="h-3 w-16 animate-pulse rounded bg-ods-border" />
241
+ <div className="overflow-hidden rounded-md border border-ods-border">
242
+ {group.widths.map((w, i) => (
243
+ <div
244
+ key={i}
245
+ // Match the real row surface: `bg-ods-bg` (#161616) sits darker
246
+ // than the `bg-ods-card` (#212121) panel, which is what makes
247
+ // the `bg-ods-border` (#3a3a3a) placeholders read clearly. On
248
+ // the bare card they'd be muddy (too close in value).
249
+ className="flex h-12 items-center gap-[var(--spacing-system-xsf)] bg-ods-bg border-b border-ods-border px-[var(--spacing-system-s)] last:border-b-0"
250
+ >
251
+ {i < group.withBadge && (
252
+ <div className="h-8 w-8 shrink-0 animate-pulse rounded-md bg-ods-border" />
253
+ )}
254
+ <div className={cn('h-4 animate-pulse rounded bg-ods-border', w)} />
255
+ </div>
256
+ ))}
257
+ </div>
258
+ </div>
259
+ ))}
260
+ </div>
261
+ )
262
+ }
263
+
182
264
  // =============================================================================
183
265
  // Component
184
266
  // =============================================================================
@@ -0,0 +1,214 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../utils/cn'
5
+ import { tagVariants, type TagProps } from '../ui/tag'
6
+ import { ActionsMenuDropdown, type ActionsMenuGroup } from '../ui/actions-menu'
7
+ import { Ellipsis01Icon } from '../icons-v2-generated'
8
+ import { getFirstLastInitials } from '../../utils/format'
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ /** A coloured status pill rendered to the right of the title/description. */
15
+ export interface MingoInfoCardStatus {
16
+ /** Pill text — rendered uppercase via the `success`/`warning`/… Tag palette. */
17
+ label: React.ReactNode
18
+ /** Tag colour. Defaults to `success` (green) to match the Figma sample. */
19
+ variant?: TagProps['variant']
20
+ }
21
+
22
+ export interface MingoInfoCardProps {
23
+ /** Card title — DM Sans Medium 18px (`text-h4`), white. Truncates on overflow. */
24
+ title: React.ReactNode
25
+ /** Optional secondary line below the title — 14px (`text-h6`), grey. Truncates. */
26
+ description?: React.ReactNode
27
+ /**
28
+ * Leading 40px avatar image (`img="image"` variant in Figma). Takes
29
+ * precedence over `icon` when both are supplied. Falls back to initials
30
+ * derived from `imageAlt` if the image fails to load.
31
+ */
32
+ imageSrc?: string
33
+ /** Alt text / initials fallback for `imageSrc`. */
34
+ imageAlt?: string
35
+ /**
36
+ * Leading 24px glyph rendered inside a 40px box (`img="icon"` variant).
37
+ * Ignored when `imageSrc` is set. Pass an `icons-v2-generated` icon — it
38
+ * inherits `currentColor` from the box's `text-ods-text-primary`.
39
+ */
40
+ icon?: React.ReactNode
41
+ /** Optional status pill (e.g. `{ label: 'Online' }`). */
42
+ status?: MingoInfoCardStatus
43
+ /**
44
+ * Overflow-menu groups. When non-empty, a trailing "⋯" button opens an
45
+ * `ActionsMenuDropdown`. Omit to hide the button entirely.
46
+ */
47
+ menuGroups?: ReadonlyArray<ActionsMenuGroup>
48
+ /** Aria-label for the "⋯" trigger. Defaults to `"More actions"`. */
49
+ menuAriaLabel?: string
50
+ /**
51
+ * Renders the main content region as an `<a>` (for navigation) instead of a
52
+ * `<button>`. Takes precedence over `onClick`. The "⋯" button stays a sibling
53
+ * of the anchor — not nested inside it — so the markup stays valid.
54
+ */
55
+ anchorProps?: MingoInfoCardAnchorProps
56
+ /**
57
+ * Click handler for the main content region. When set (and `anchorProps` is
58
+ * not), the title/description region becomes a `<button>` with hover/focus
59
+ * affordances. The "⋯" button keeps its own independent click target.
60
+ */
61
+ onClick?: () => void
62
+ /** Appended to the root element. */
63
+ className?: string
64
+ }
65
+
66
+ /** Anchor-prop bundle for the content region — mirrors the `*AnchorProps`
67
+ * shape the chat entity-cards pass (`{ href, target?, rel?, onClick? }`). */
68
+ export interface MingoInfoCardAnchorProps {
69
+ href: string
70
+ target?: '_blank'
71
+ rel?: string
72
+ onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void
73
+ }
74
+
75
+ // =============================================================================
76
+ // Span-only markup
77
+ //
78
+ // This card renders inline inside chat message paragraphs (the `[card://]`
79
+ // marker is replaced in-place, landing the card inside a `<p>`). To stay
80
+ // valid there, EVERY element is phrasing content — `<span>` / `<a>` / `<img>`
81
+ // / `<button>` only, never `<div>`. Layout comes from `display` utilities, so
82
+ // the visual is identical to a block card.
83
+ // =============================================================================
84
+
85
+ /** 40px leading media — avatar image (with initials fallback) or boxed glyph. */
86
+ function CardMedia({
87
+ imageSrc,
88
+ imageAlt,
89
+ icon,
90
+ }: Pick<MingoInfoCardProps, 'imageSrc' | 'imageAlt' | 'icon'>) {
91
+ if (imageSrc) {
92
+ return (
93
+ <span className="relative block size-10 shrink-0">
94
+ {/* Initials fallback sits BEHIND the image. The image is the framed
95
+ element itself — `rounded-sm` + `border` live on the `<img>` so the
96
+ picture and its 1px frame share one radius (no corner gap from
97
+ clipping a separate absolutely-positioned child). On a 404 the
98
+ image's `onError` hides it and the initials show through. */}
99
+ <span className="absolute inset-0 flex items-center justify-center rounded-sm border border-ods-border bg-ods-bg text-xs font-medium text-ods-text-primary">
100
+ {getFirstLastInitials(imageAlt) || '?'}
101
+ </span>
102
+ <img
103
+ src={imageSrc}
104
+ alt={imageAlt || ''}
105
+ className="relative block size-10 rounded-sm border border-ods-border object-cover"
106
+ onError={(e) => {
107
+ e.currentTarget.style.display = 'none'
108
+ }}
109
+ />
110
+ </span>
111
+ )
112
+ }
113
+ if (icon) {
114
+ return (
115
+ <span className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-md border border-ods-border bg-ods-bg text-ods-text-secondary">
116
+ <span className="flex size-6 items-center justify-center">{icon}</span>
117
+ </span>
118
+ )
119
+ }
120
+ return null
121
+ }
122
+
123
+ export function MingoInfoCard({
124
+ title,
125
+ description,
126
+ imageSrc,
127
+ imageAlt,
128
+ icon,
129
+ status,
130
+ menuGroups,
131
+ menuAriaLabel = 'More actions',
132
+ anchorProps,
133
+ onClick,
134
+ className,
135
+ }: MingoInfoCardProps) {
136
+ const hasMedia = !!imageSrc || !!icon
137
+ const hasMenu = !!menuGroups && menuGroups.length > 0
138
+ const isAnchor = !!anchorProps
139
+ const isInteractive = isAnchor || !!onClick
140
+
141
+ // The content region: leading media, text column, status pill. Owns the
142
+ // `border-r` divider that sits before the "⋯" button.
143
+ const content = (
144
+ <>
145
+ {hasMedia && (
146
+ <CardMedia imageSrc={imageSrc} imageAlt={imageAlt} icon={icon} />
147
+ )}
148
+ <span className="flex min-w-0 flex-1 flex-col justify-center">
149
+ <span className="truncate text-h4 font-medium text-ods-text-primary">
150
+ {title}
151
+ </span>
152
+ {description ? (
153
+ <span className="truncate text-h6 text-ods-text-secondary">
154
+ {description}
155
+ </span>
156
+ ) : null}
157
+ </span>
158
+ {status ? (
159
+ // Reuse the shared Tag palette (single source of truth for colours)
160
+ // without rendering Tag's `<div>` — span-only for `<p>` validity.
161
+ <span className={cn(tagVariants({ variant: status.variant ?? 'success' }), 'shrink-0')}>
162
+ <span className="truncate uppercase tracking-[-0.28px]">{status.label}</span>
163
+ </span>
164
+ ) : null}
165
+ </>
166
+ )
167
+
168
+ const contentClass = cn(
169
+ 'flex min-w-0 flex-1 items-center gap-[var(--spacing-system-s)] overflow-hidden p-[var(--spacing-system-s)] text-left',
170
+ hasMenu && 'border-r border-ods-border',
171
+ isInteractive &&
172
+ 'transition-colors hover:bg-ods-bg-hover active:bg-ods-bg-active focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-accent cursor-pointer',
173
+ )
174
+
175
+ return (
176
+ <span
177
+ className={cn(
178
+ 'flex w-full items-center overflow-hidden rounded-md border border-ods-border bg-ods-card',
179
+ className,
180
+ )}
181
+ >
182
+ {isAnchor ? (
183
+ <a {...anchorProps} className={cn(contentClass, 'no-underline')}>
184
+ {content}
185
+ </a>
186
+ ) : onClick ? (
187
+ <button type="button" onClick={onClick} className={contentClass}>
188
+ {content}
189
+ </button>
190
+ ) : (
191
+ <span className={contentClass}>{content}</span>
192
+ )}
193
+
194
+ {hasMenu && (
195
+ <ActionsMenuDropdown
196
+ groups={menuGroups as ActionsMenuGroup[]}
197
+ triggerAriaLabel={menuAriaLabel}
198
+ onCloseAutoFocus={(e) => e.preventDefault()}
199
+ customTrigger={
200
+ <button
201
+ type="button"
202
+ aria-label={menuAriaLabel}
203
+ // Press feedback mirrors `Button` (transparent variant):
204
+ // `hover:bg-ods-bg-hover` → `active:bg-ods-bg-active`.
205
+ className="flex w-[56px] md:w-[68px] shrink-0 items-center justify-center self-stretch text-ods-text-secondary transition-colors hover:bg-ods-bg-hover hover:text-ods-text-primary active:bg-ods-bg-active active:text-ods-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-accent"
206
+ >
207
+ <Ellipsis01Icon size={24} />
208
+ </button>
209
+ }
210
+ />
211
+ )}
212
+ </span>
213
+ )
214
+ }
@@ -3,7 +3,10 @@
3
3
  import * as React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
5
  import { MingoIcon } from '../icons'
6
+ import { MingoChatHistorySkeleton } from './mingo-chat-history'
7
+ import { ChatQuickActionRow } from './chat-quick-action-row'
6
8
  import { Tag } from '../ui/tag'
9
+ import { Button } from '../ui/button'
7
10
  import { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'
8
11
  import {
9
12
  Rocket01Icon,
@@ -12,6 +15,8 @@ import {
12
15
  LayersIcon,
13
16
  CompassIcon,
14
17
  Arrow01DownIcon,
18
+ AlertCircleIcon,
19
+ Refresh01RightIcon,
15
20
  } from '../icons-v2-generated'
16
21
 
17
22
  // =============================================================================
@@ -74,6 +79,19 @@ export interface MingoWelcomeProps {
74
79
  * `<MingoChatHistory>`), it replaces the greeting + feature grid and owns
75
80
  * its own scroll region. The chips below stay pinned. */
76
81
  dialogHistory?: React.ReactNode
82
+ /** True while the FIRST page of dialogs is still loading and we don't yet
83
+ * know if the user is new or returning. Renders a history skeleton in place
84
+ * of both the greeting+grid and the history, so the empty state doesn't
85
+ * flash the new-user layout before the list arrives. Ignored once
86
+ * `dialogHistory` is provided. */
87
+ isLoadingHistory?: boolean
88
+ /** The dialog-list load FAILED (e.g. backend down) and there's nothing
89
+ * cached. Renders an error + retry block in place of the new-user empty
90
+ * state (which would otherwise misleadingly advertise Guide). Takes
91
+ * priority over `isLoadingHistory`. Ignored once `dialogHistory` is set. */
92
+ loadError?: boolean
93
+ /** Retry handler for the `loadError` state. */
94
+ onRetry?: () => void
77
95
  /** When provided, renders the "Start Guide Chat" chip (the only wired
78
96
  * action — switches the host chat to Guide mode) and enables the default
79
97
  * promo notification. When omitted, both are suppressed. */
@@ -149,6 +167,9 @@ export function MingoWelcome({
149
167
  quickActions,
150
168
  hasExistingChats = false,
151
169
  dialogHistory,
170
+ isLoadingHistory = false,
171
+ loadError = false,
172
+ onRetry,
152
173
  onStartGuideChat,
153
174
  className,
154
175
  }: MingoWelcomeProps) {
@@ -222,6 +243,14 @@ export function MingoWelcome({
222
243
  // Last row's first index — used to drop the bottom divider on the final row.
223
244
  const lastRowStart = cellCount - ((cellCount % 2) || 2)
224
245
 
246
+ // While we don't yet know whether the user is new or returning (first page
247
+ // loading, or it errored with nothing cached), suppress the pinned region —
248
+ // the "New to OpenFrame?" promo + quick-action chips. Showing them over the
249
+ // history skeleton both clutters the loading frame and pre-judges the user as
250
+ // new. They return once a resolved state (history, or the new-user greeting)
251
+ // renders. `dialogHistory` always wins — a returning user keeps the chips.
252
+ const showPinnedRegion = !!dialogHistory || (!isLoadingHistory && !loadError)
253
+
225
254
  return (
226
255
  <div
227
256
  className={cn(
@@ -233,8 +262,33 @@ export function MingoWelcome({
233
262
  )}
234
263
  >
235
264
  {/* Returning users see their dialog history (its own scroll region) in
236
- place of the greeting + grid. */}
237
- {dialogHistory ?? (
265
+ place of the greeting + grid. Precedence: load error (retry) → first-
266
+ page loading (skeleton) → new-user greeting+grid. The error branch
267
+ stops a failed fetch from masquerading as "no chats" (which would
268
+ otherwise show the Guide promo). */}
269
+ {dialogHistory ?? (loadError ? (
270
+ <div className="flex flex-1 min-h-0 flex-col items-center justify-center gap-[var(--spacing-system-m)] text-center">
271
+ <AlertCircleIcon className="h-8 w-8 text-ods-text-secondary shrink-0" />
272
+ <div className="flex flex-col gap-[var(--spacing-system-xxs)]">
273
+ <p className="text-h4 text-ods-text-primary">Couldn’t load your chats</p>
274
+ <p className="text-body-sm text-ods-text-secondary">
275
+ Something went wrong reaching the server. Check your connection and try again.
276
+ </p>
277
+ </div>
278
+ {onRetry && (
279
+ <Button
280
+ variant="outline"
281
+ size="small"
282
+ leftIcon={<Refresh01RightIcon />}
283
+ onClick={onRetry}
284
+ >
285
+ Try again
286
+ </Button>
287
+ )}
288
+ </div>
289
+ ) : isLoadingHistory ? (
290
+ <MingoChatHistorySkeleton />
291
+ ) : (
238
292
  <>
239
293
  {/* Scrollable region — only the greeting + grid scroll; the notification
240
294
  and chips below stay pinned so they're always visible above the
@@ -321,9 +375,11 @@ export function MingoWelcome({
321
375
  />
322
376
  </div>
323
377
  </>
324
- )}
378
+ ))}
325
379
 
326
- {/* Pinned region — always visible above the input. */}
380
+ {/* Pinned region — visible above the input once the new/returning-user
381
+ state is known (hidden during first-page load / error). */}
382
+ {showPinnedRegion && (
327
383
  <div className="flex shrink-0 flex-col gap-[var(--spacing-system-m)]">
328
384
  {/* "New to OpenFrame?" — a one-time informational notification (no
329
385
  action). The leading down-arrow (Figma node 7532:317130) only shows
@@ -356,41 +412,38 @@ export function MingoWelcome({
356
412
  </div>
357
413
  )}
358
414
 
359
- {/* Quick-action chips. Only "Start Guide Chat" is wired by default; the
360
- chip and any `quickActions` use the design-system <Tag>, wrapped in a
361
- real <button> so each chip gets keyboard focus + a focus ring. */}
415
+ {/* Quick-action chips. "Start Guide Chat" stays pinned (the `leading`
416
+ slot never collapses); any caller `quickActions` fit inline and the
417
+ rest collapse under a "⋯" overflow menu, width-measured like the
418
+ Autocomplete tag row. */}
362
419
  {(onStartGuideChat || (quickActions && quickActions.length > 0)) && (
363
- <div className="flex shrink-0 flex-wrap items-center gap-1">
364
- {onStartGuideChat && (
365
- <button
366
- type="button"
367
- onClick={onStartGuideChat}
368
- className="rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
369
- >
370
- <Tag
371
- variant={hasExistingChats ? 'outline' : 'primary'}
372
- icon={<CompassIcon size={16} />}
373
- label="Start Guide Chat"
374
- />
375
- </button>
376
- )}
377
- {quickActions?.map((action) => (
378
- <button
379
- key={action.id}
380
- type="button"
381
- onClick={action.onClick}
382
- className="rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
383
- >
384
- <Tag
385
- variant={action.variant ?? 'outline'}
386
- icon={action.icon}
387
- label={action.label}
388
- />
389
- </button>
390
- ))}
391
- </div>
420
+ <ChatQuickActionRow
421
+ leading={
422
+ onStartGuideChat && (
423
+ <button
424
+ type="button"
425
+ onClick={onStartGuideChat}
426
+ className="shrink-0 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
427
+ >
428
+ <Tag
429
+ variant={hasExistingChats ? 'outline' : 'primary'}
430
+ icon={<CompassIcon size={16} />}
431
+ label="Start Guide Chat"
432
+ />
433
+ </button>
434
+ )
435
+ }
436
+ chips={(quickActions ?? []).map((action) => ({
437
+ id: action.id,
438
+ label: action.label,
439
+ icon: action.icon,
440
+ variant: action.variant,
441
+ onSelect: action.onClick,
442
+ }))}
443
+ />
392
444
  )}
393
445
  </div>
446
+ )}
394
447
  </div>
395
448
  )
396
449
  }
@@ -87,6 +87,10 @@ export interface MessageOwner {
87
87
  id: string
88
88
  firstName?: string
89
89
  lastName?: string
90
+ /** Optional author avatar. Host queries it on the message owner (e.g.
91
+ * GraphQL `User.image`); `imageUrl` may be relative — the host is
92
+ * responsible for resolving it to an absolute/proxied URL. */
93
+ image?: { imageUrl?: string | null } | null
90
94
  }
91
95
  }
92
96
 
@@ -18,6 +18,7 @@
18
18
 
19
19
  import type { MessageSegment } from './message.types'
20
20
  import type { ScrollAnchor } from './message.types'
21
+ import type { AuthorType } from './chat.types'
21
22
  import type { ChatRef } from '../chat-ref.types'
22
23
  import type { ChatSource } from '../hooks/use-sse-chat-adapter'
23
24
  import type { ChatAttachment } from '../utils/chat-attachment-markdown'
@@ -108,6 +109,32 @@ export interface UnifiedChatMessage {
108
109
  id: string
109
110
  role: 'user' | 'assistant'
110
111
 
112
+ /**
113
+ * Optional host-supplied display name for the message author. When set, the
114
+ * chat renders it verbatim (e.g. the signed-in user's full name for a
115
+ * `user` bubble, or a per-message admin name from history); when omitted the
116
+ * UI falls back to the role default (`'Mingo'` / `'You'`).
117
+ */
118
+ name?: string
119
+
120
+ /**
121
+ * Optional host-supplied avatar URL for the message author. Used by the
122
+ * panel to render an inline avatar on the author row (proxied via
123
+ * `useProxiedImageUrl`). Omit/`null` to fall back to initials. For `user`
124
+ * bubbles the avatar only renders when this is set, so consumers that don't
125
+ * supply one keep the name-only layout.
126
+ */
127
+ avatar?: string | null
128
+
129
+ /**
130
+ * Optional host-supplied author type — drives the name-row accent color in
131
+ * `<ChatMessageEnhanced>` (e.g. `'admin'` renders the name in the accent
132
+ * color, matching the standalone `/mingo` page). When omitted the panel
133
+ * falls back to the role default (`'user'` for user bubbles, the assistant
134
+ * type otherwise).
135
+ */
136
+ authorType?: AuthorType
137
+
111
138
  /**
112
139
  * Flat string form for legacy/simple callers. The structured `segments`
113
140
  * form is preferred — it carries thinking blocks, tool calls, approval
@@ -296,6 +323,14 @@ export interface UnifiedChatState {
296
323
  /** True while the dialog list is being fetched for the first time. */
297
324
  isDialogsLoading: boolean
298
325
 
326
+ /** True when the initial dialog-list load FAILED (e.g. backend down).
327
+ * Lets the UI distinguish a load error from a genuinely empty list and
328
+ * show a retry affordance instead of the new-user empty state. */
329
+ dialogsError: boolean
330
+
331
+ /** Re-run the initial dialog-list load — retry after `dialogsError`. */
332
+ reloadDialogs: () => void
333
+
299
334
  /** True while message history for the active dialog is being fetched. */
300
335
  isMessagesLoading: boolean
301
336