@flamingo-stack/openframe-frontend-core 0.0.219 → 0.0.220-snapshot.20260602172647

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 (146) hide show
  1. package/dist/{chunk-F3FO2ZZZ.cjs → chunk-4PBV66HQ.cjs} +7 -7
  2. package/dist/{chunk-F3FO2ZZZ.cjs.map → chunk-4PBV66HQ.cjs.map} +1 -1
  3. package/dist/{chunk-EDW2NVRV.js → chunk-4WZOFD46.js} +37 -37
  4. package/dist/{chunk-EDW2NVRV.js.map → chunk-4WZOFD46.js.map} +1 -1
  5. package/dist/{chunk-YX3YQNC4.cjs → chunk-73YDB6AT.cjs} +13 -13
  6. package/dist/{chunk-YX3YQNC4.cjs.map → chunk-73YDB6AT.cjs.map} +1 -1
  7. package/dist/{chunk-MPHDM2VZ.cjs → chunk-7TQNW2AM.cjs} +30 -30
  8. package/dist/{chunk-MPHDM2VZ.cjs.map → chunk-7TQNW2AM.cjs.map} +1 -1
  9. package/dist/{chunk-65CPJ4SX.cjs → chunk-C6ASEPZL.cjs} +30 -30
  10. package/dist/{chunk-65CPJ4SX.cjs.map → chunk-C6ASEPZL.cjs.map} +1 -1
  11. package/dist/{chunk-DRPECAXO.js → chunk-CPIX5AAR.js} +2 -2
  12. package/dist/{chunk-SRA2QYK6.js → chunk-E2AWBQEU.js} +4 -4
  13. package/dist/{chunk-SZXKKEUH.cjs → chunk-E6B4B7GM.cjs} +46 -30
  14. package/dist/chunk-E6B4B7GM.cjs.map +1 -0
  15. package/dist/{chunk-XG7DFRJL.js → chunk-FOOQFRJR.js} +3 -3
  16. package/dist/{chunk-A3PL6ZCF.js → chunk-IS4IZC7N.js} +6388 -5128
  17. package/dist/chunk-IS4IZC7N.js.map +1 -0
  18. package/dist/{chunk-ZGBXHK26.cjs → chunk-JMGSJHFP.cjs} +12 -12
  19. package/dist/{chunk-ZGBXHK26.cjs.map → chunk-JMGSJHFP.cjs.map} +1 -1
  20. package/dist/{chunk-SL3RGBPX.cjs → chunk-QNYH3WUU.cjs} +9 -9
  21. package/dist/{chunk-SL3RGBPX.cjs.map → chunk-QNYH3WUU.cjs.map} +1 -1
  22. package/dist/{chunk-24Q2WLIU.js → chunk-QYRV6MKX.js} +2 -2
  23. package/dist/{chunk-ZII7TNVA.js → chunk-SRCEVQYA.js} +3 -3
  24. package/dist/{chunk-Y3MXGCOW.js → chunk-YZDUOUMB.js} +46 -30
  25. package/dist/chunk-YZDUOUMB.js.map +1 -0
  26. package/dist/{chunk-7UZLRI7W.cjs → chunk-ZAGQXSAP.cjs} +3292 -2032
  27. package/dist/chunk-ZAGQXSAP.cjs.map +1 -0
  28. package/dist/components/chat/chat-archive-page.d.ts +25 -0
  29. package/dist/components/chat/chat-archive-page.d.ts.map +1 -0
  30. package/dist/components/chat/chat-composer.d.ts +29 -0
  31. package/dist/components/chat/chat-composer.d.ts.map +1 -0
  32. package/dist/components/chat/chat-header-icon-button.d.ts +14 -0
  33. package/dist/components/chat/chat-header-icon-button.d.ts.map +1 -0
  34. package/dist/components/chat/chat-panel-header.d.ts +32 -0
  35. package/dist/components/chat/chat-panel-header.d.ts.map +1 -0
  36. package/dist/components/chat/embeddable-chat.d.ts +18 -0
  37. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  38. package/dist/components/chat/guide-mode-banner.d.ts +16 -0
  39. package/dist/components/chat/guide-mode-banner.d.ts.map +1 -0
  40. package/dist/components/chat/guide-welcome.d.ts +42 -0
  41. package/dist/components/chat/guide-welcome.d.ts.map +1 -0
  42. package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts +58 -0
  43. package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts.map +1 -0
  44. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +26 -1
  45. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  46. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  47. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
  48. package/dist/components/chat/index.cjs +29 -5
  49. package/dist/components/chat/index.cjs.map +1 -1
  50. package/dist/components/chat/index.d.ts +9 -0
  51. package/dist/components/chat/index.d.ts.map +1 -1
  52. package/dist/components/chat/index.js +28 -4
  53. package/dist/components/chat/mingo-chat-history.d.ts +37 -0
  54. package/dist/components/chat/mingo-chat-history.d.ts.map +1 -0
  55. package/dist/components/chat/mingo-chat-modals.d.ts +50 -0
  56. package/dist/components/chat/mingo-chat-modals.d.ts.map +1 -0
  57. package/dist/components/chat/mingo-onboarding-card.d.ts.map +1 -1
  58. package/dist/components/chat/mingo-welcome.d.ts +78 -0
  59. package/dist/components/chat/mingo-welcome.d.ts.map +1 -0
  60. package/dist/components/chat/types/unified-chat-state.types.d.ts +6 -0
  61. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  62. package/dist/components/contact/index.cjs +6 -6
  63. package/dist/components/contact/index.js +5 -5
  64. package/dist/components/features/index.cjs +5 -5
  65. package/dist/components/features/index.js +4 -4
  66. package/dist/components/icons-v2-generated/brand-logos/fleet-mdm-logo-grey-icon.d.ts.map +1 -1
  67. package/dist/components/icons-v2-generated/brand-logos/fleet-mdm-logo-icon.d.ts.map +1 -1
  68. package/dist/components/icons-v2-generated/index.cjs +2 -2
  69. package/dist/components/icons-v2-generated/index.js +1 -1
  70. package/dist/components/index.cjs +128 -104
  71. package/dist/components/index.cjs.map +1 -1
  72. package/dist/components/index.js +31 -7
  73. package/dist/components/index.js.map +1 -1
  74. package/dist/components/layout/page-heading.d.ts +7 -6
  75. package/dist/components/layout/page-heading.d.ts.map +1 -1
  76. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  77. package/dist/components/navigation/header-mingo-button.d.ts +4 -2
  78. package/dist/components/navigation/header-mingo-button.d.ts.map +1 -1
  79. package/dist/components/navigation/index.cjs +5 -5
  80. package/dist/components/navigation/index.js +4 -4
  81. package/dist/components/onboarding-guides/index.cjs +28 -28
  82. package/dist/components/onboarding-guides/index.js +6 -6
  83. package/dist/components/tickets/index.cjs +88 -88
  84. package/dist/components/tickets/index.js +7 -7
  85. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  86. package/dist/components/ui/file-manager/index.cjs +50 -50
  87. package/dist/components/ui/file-manager/index.js +1 -1
  88. package/dist/components/ui/index.cjs +29 -5
  89. package/dist/components/ui/index.cjs.map +1 -1
  90. package/dist/components/ui/index.js +28 -4
  91. package/dist/components/ui/modal-v2.d.ts.map +1 -1
  92. package/dist/components/ui/more-actions-menu.d.ts +8 -1
  93. package/dist/components/ui/more-actions-menu.d.ts.map +1 -1
  94. package/dist/components/ui/portal-container.d.ts +21 -0
  95. package/dist/components/ui/portal-container.d.ts.map +1 -0
  96. package/dist/components/ui/tooltip.d.ts.map +1 -1
  97. package/dist/hooks/index.cjs +3 -3
  98. package/dist/hooks/index.js +2 -2
  99. package/dist/index.cjs +29 -5
  100. package/dist/index.cjs.map +1 -1
  101. package/dist/index.js +28 -4
  102. package/package.json +1 -1
  103. package/src/components/chat/chat-archive-page.tsx +93 -0
  104. package/src/components/chat/chat-composer.tsx +99 -0
  105. package/src/components/chat/chat-header-icon-button.tsx +36 -0
  106. package/src/components/chat/chat-panel-header.tsx +114 -0
  107. package/src/components/chat/embeddable-chat.tsx +388 -311
  108. package/src/components/chat/guide-mode-banner.tsx +75 -0
  109. package/src/components/chat/guide-welcome.tsx +199 -0
  110. package/src/components/chat/hooks/use-chat-dialog-manager.ts +227 -0
  111. package/src/components/chat/hooks/use-nats-chat-adapter.ts +85 -0
  112. package/src/components/chat/hooks/use-sse-chat-adapter.ts +8 -0
  113. package/src/components/chat/hooks/use-unified-chat.ts +12 -0
  114. package/src/components/chat/index.ts +9 -0
  115. package/src/components/chat/mingo-chat-history.tsx +308 -0
  116. package/src/components/chat/mingo-chat-modals.tsx +223 -0
  117. package/src/components/chat/mingo-onboarding-card.tsx +5 -8
  118. package/src/components/chat/mingo-welcome.tsx +396 -0
  119. package/src/components/chat/types/unified-chat-state.types.ts +8 -0
  120. package/src/components/icons-v2/brand-logos/fleet-mdm-logo-grey.svg +6 -6
  121. package/src/components/icons-v2/brand-logos/fleet-mdm-logo.svg +6 -6
  122. package/src/components/icons-v2-generated/brand-logos/fleet-mdm-logo-grey-icon.tsx +2 -22
  123. package/src/components/icons-v2-generated/brand-logos/fleet-mdm-logo-icon.tsx +22 -2
  124. package/src/components/layout/page-heading.tsx +13 -7
  125. package/src/components/navigation/app-header.tsx +12 -12
  126. package/src/components/navigation/app-layout-drawer.tsx +25 -15
  127. package/src/components/navigation/header-mingo-button.tsx +22 -7
  128. package/src/components/ui/dropdown-menu.tsx +9 -3
  129. package/src/components/ui/modal-v2.tsx +33 -3
  130. package/src/components/ui/more-actions-menu.tsx +15 -2
  131. package/src/components/ui/portal-container.tsx +28 -0
  132. package/src/components/ui/tooltip.tsx +9 -3
  133. package/src/stories/AppLayoutSidebar.stories.tsx +184 -0
  134. package/src/stories/EmbeddableChat.stories.tsx +114 -0
  135. package/src/stories/GuideWelcome.stories.tsx +124 -0
  136. package/src/stories/MingoChatModals.stories.tsx +82 -0
  137. package/src/stories/MingoWelcome.stories.tsx +119 -0
  138. package/dist/chunk-7UZLRI7W.cjs.map +0 -1
  139. package/dist/chunk-A3PL6ZCF.js.map +0 -1
  140. package/dist/chunk-SZXKKEUH.cjs.map +0 -1
  141. package/dist/chunk-Y3MXGCOW.js.map +0 -1
  142. /package/dist/{chunk-DRPECAXO.js.map → chunk-CPIX5AAR.js.map} +0 -0
  143. /package/dist/{chunk-SRA2QYK6.js.map → chunk-E2AWBQEU.js.map} +0 -0
  144. /package/dist/{chunk-XG7DFRJL.js.map → chunk-FOOQFRJR.js.map} +0 -0
  145. /package/dist/{chunk-24Q2WLIU.js.map → chunk-QYRV6MKX.js.map} +0 -0
  146. /package/dist/{chunk-ZII7TNVA.js.map → chunk-SRCEVQYA.js.map} +0 -0
@@ -21,6 +21,15 @@ export * from './tool-execution-display'
21
21
  export * from './tool-call-blocks'
22
22
  export * from './mingo-onboarding-card'
23
23
  export * from './mingo-onboarding-card-skeleton'
24
+ export * from './mingo-welcome'
25
+ export * from './guide-welcome'
26
+ export * from './guide-mode-banner'
27
+ export * from './mingo-chat-history'
28
+ export * from './mingo-chat-modals'
29
+ export * from './chat-header-icon-button'
30
+ export * from './chat-panel-header'
31
+ export * from './chat-composer'
32
+ export * from './chat-archive-page'
24
33
  export * from './model-display'
25
34
  export * from './chat-sidebar'
26
35
  export type { ChatRef } from './chat-ref.types'
@@ -0,0 +1,308 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../utils/cn'
5
+ import { MoreActionsMenu } from '../ui/more-actions-menu'
6
+ import { Ellipsis01Icon } from '../icons-v2-generated'
7
+ import type { DialogItem } from './types/component.types'
8
+
9
+ // =============================================================================
10
+ // Types
11
+ // =============================================================================
12
+
13
+ export interface MingoChatHistoryProps {
14
+ /** Dialogs to list, assumed already sorted newest-first by the host. */
15
+ dialogs: ReadonlyArray<DialogItem>
16
+ /** Currently-open dialog id (highlighted). */
17
+ activeDialogId?: string
18
+ /** Open a dialog. */
19
+ onSelectDialog?: (id: string) => void
20
+ /** Request rename — enables the row "Rename chat" action. The host opens
21
+ * the Rename modal; the list does no inline editing. */
22
+ onRequestRename?: (dialog: DialogItem) => void
23
+ /** Request archive — enables the row "Archive chat" action. The host opens
24
+ * the Archive confirmation modal. */
25
+ onRequestArchive?: (dialog: DialogItem) => void
26
+ /** Whether more dialogs remain (cursor pagination). */
27
+ hasMore?: boolean
28
+ /** True while the next page is loading. */
29
+ isLoadingMore?: boolean
30
+ /** Fetch the next page — fired when the bottom sentinel scrolls into view. */
31
+ onLoadMore?: () => void
32
+ /** Appended to the root element. */
33
+ className?: string
34
+ }
35
+
36
+ interface DialogGroup {
37
+ key: string
38
+ label: string
39
+ items: DialogItem[]
40
+ }
41
+
42
+ // =============================================================================
43
+ // Date grouping — Today / Yesterday / Older
44
+ // =============================================================================
45
+
46
+ function dialogTime(d: DialogItem): number | null {
47
+ if (!d.timestamp) return null
48
+ const t =
49
+ typeof d.timestamp === 'string'
50
+ ? Date.parse(d.timestamp)
51
+ : d.timestamp.getTime()
52
+ return Number.isNaN(t) ? null : t
53
+ }
54
+
55
+ function groupDialogs(dialogs: ReadonlyArray<DialogItem>): DialogGroup[] {
56
+ const now = new Date()
57
+ const startOfToday = new Date(
58
+ now.getFullYear(),
59
+ now.getMonth(),
60
+ now.getDate(),
61
+ ).getTime()
62
+ const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000
63
+
64
+ const today: DialogItem[] = []
65
+ const yesterday: DialogItem[] = []
66
+ const older: DialogItem[] = []
67
+ for (const d of dialogs) {
68
+ const t = dialogTime(d)
69
+ if (t !== null && t >= startOfToday) today.push(d)
70
+ else if (t !== null && t >= startOfYesterday) yesterday.push(d)
71
+ else older.push(d) // includes timestamp-less dialogs
72
+ }
73
+
74
+ return [
75
+ { key: 'today', label: 'Today', items: today },
76
+ { key: 'yesterday', label: 'Yesterday', items: yesterday },
77
+ { key: 'older', label: 'Older', items: older },
78
+ ].filter((g) => g.items.length > 0)
79
+ }
80
+
81
+ // =============================================================================
82
+ // Row
83
+ // =============================================================================
84
+
85
+ interface RowProps {
86
+ dialog: DialogItem
87
+ isActive: boolean
88
+ onSelect?: (id: string) => void
89
+ onRequestRename?: (dialog: DialogItem) => void
90
+ onRequestArchive?: (dialog: DialogItem) => void
91
+ }
92
+
93
+ function MingoChatHistoryRow({
94
+ dialog,
95
+ isActive,
96
+ onSelect,
97
+ onRequestRename,
98
+ onRequestArchive,
99
+ }: RowProps) {
100
+ const title = dialog.title || 'Untitled Chat'
101
+ const unread = dialog.unreadMessagesCount ?? 0
102
+ const hasMenu = !!onRequestRename || !!onRequestArchive
103
+ // Keep the `⋯` visible while its menu is open — once Radix moves focus into
104
+ // the portalled content the row loses hover/focus-within.
105
+ const [menuOpen, setMenuOpen] = React.useState(false)
106
+
107
+ const menuItems = [
108
+ onRequestRename && {
109
+ label: 'Rename chat',
110
+ onClick: () => onRequestRename(dialog),
111
+ },
112
+ onRequestArchive && {
113
+ label: 'Archive chat',
114
+ onClick: () => onRequestArchive(dialog),
115
+ },
116
+ ].filter(Boolean) as { label: string; onClick: () => void }[]
117
+
118
+ return (
119
+ <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
+ 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',
133
+ isActive && 'bg-ods-bg-hover',
134
+ )}
135
+ >
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}
144
+ >
145
+ {title}
146
+ </span>
147
+ {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).
150
+ <span
151
+ onClick={(e) => e.stopPropagation()}
152
+ className={cn(
153
+ 'shrink-0 transition-opacity',
154
+ menuOpen
155
+ ? 'opacity-100'
156
+ : 'opacity-0 group-hover/row:opacity-100 focus-within:opacity-100',
157
+ )}
158
+ >
159
+ <MoreActionsMenu
160
+ ariaLabel="Chat actions"
161
+ items={menuItems}
162
+ open={menuOpen}
163
+ onOpenChange={setMenuOpen}
164
+ // Don't return focus (and its ring) to the `⋯` trigger on close.
165
+ onCloseAutoFocus={(e) => e.preventDefault()}
166
+ trigger={
167
+ <button
168
+ type="button"
169
+ 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"
171
+ >
172
+ <Ellipsis01Icon size={24} />
173
+ </button>
174
+ }
175
+ />
176
+ </span>
177
+ ) : null}
178
+ </div>
179
+ )
180
+ }
181
+
182
+ // =============================================================================
183
+ // Component
184
+ // =============================================================================
185
+
186
+ /**
187
+ * MingoChatHistory — Figma node `7532:223950`.
188
+ *
189
+ * The returning-user variation of the Mingo empty state: the dialog history
190
+ * rendered inline in the main panel, grouped into Today / Yesterday / Older
191
+ * sections. Each row shows an optional unread badge, the title, and a `⋯`
192
+ * menu (Rename / Archive — each gated on the matching handler and surfaced as
193
+ * a request the host fulfils via a modal). Owns its own scroll + bottom/top
194
+ * fade and an infinite-scroll
195
+ * sentinel that fires `onLoadMore`.
196
+ */
197
+ export function MingoChatHistory({
198
+ dialogs,
199
+ activeDialogId,
200
+ onSelectDialog,
201
+ onRequestRename,
202
+ onRequestArchive,
203
+ hasMore = false,
204
+ isLoadingMore = false,
205
+ onLoadMore,
206
+ className,
207
+ }: MingoChatHistoryProps) {
208
+ const groups = React.useMemo(() => groupDialogs(dialogs), [dialogs])
209
+
210
+ // Scroll-fade affordances (same pattern as MingoWelcome's greeting region).
211
+ const scrollRef = React.useRef<HTMLDivElement>(null)
212
+ const [fade, setFade] = React.useState({ top: false, bottom: false })
213
+ const updateFade = React.useCallback(() => {
214
+ const el = scrollRef.current
215
+ if (!el) return
216
+ const top = el.scrollTop > 1
217
+ const bottom = el.scrollTop + el.clientHeight < el.scrollHeight - 1
218
+ setFade((p) => (p.top === top && p.bottom === bottom ? p : { top, bottom }))
219
+ }, [])
220
+ React.useEffect(() => {
221
+ updateFade()
222
+ const el = scrollRef.current
223
+ if (!el || typeof ResizeObserver === 'undefined') return
224
+ const ro = new ResizeObserver(updateFade)
225
+ ro.observe(el)
226
+ return () => ro.disconnect()
227
+ }, [updateFade, dialogs])
228
+
229
+ // Infinite scroll — load the next page when the sentinel enters the
230
+ // scroll viewport.
231
+ const sentinelRef = React.useRef<HTMLDivElement>(null)
232
+ const onLoadMoreRef = React.useRef(onLoadMore)
233
+ onLoadMoreRef.current = onLoadMore
234
+ const isLoadingMoreRef = React.useRef(isLoadingMore)
235
+ isLoadingMoreRef.current = isLoadingMore
236
+ React.useEffect(() => {
237
+ const root = scrollRef.current
238
+ const sentinel = sentinelRef.current
239
+ if (!root || !sentinel || !hasMore || typeof IntersectionObserver === 'undefined')
240
+ return
241
+ const io = new IntersectionObserver(
242
+ ([entry]) => {
243
+ if (entry.isIntersecting && !isLoadingMoreRef.current) {
244
+ onLoadMoreRef.current?.()
245
+ }
246
+ },
247
+ { root, rootMargin: '120px', threshold: 0.1 },
248
+ )
249
+ io.observe(sentinel)
250
+ return () => io.disconnect()
251
+ }, [hasMore])
252
+
253
+ return (
254
+ <div className={cn('relative flex flex-1 min-h-0 flex-col', className)}>
255
+ <div
256
+ ref={scrollRef}
257
+ onScroll={updateFade}
258
+ className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto"
259
+ >
260
+ {groups.map((group) => (
261
+ <div
262
+ key={group.key}
263
+ className="flex flex-col gap-[var(--spacing-system-xxs)]"
264
+ >
265
+ <p className="text-h5 text-ods-text-secondary">{group.label}</p>
266
+ <div className="overflow-hidden rounded-md border border-ods-border">
267
+ {group.items.map((dialog) => (
268
+ <MingoChatHistoryRow
269
+ key={dialog.id}
270
+ dialog={dialog}
271
+ isActive={dialog.id === activeDialogId}
272
+ onSelect={onSelectDialog}
273
+ onRequestRename={onRequestRename}
274
+ onRequestArchive={onRequestArchive}
275
+ />
276
+ ))}
277
+ </div>
278
+ </div>
279
+ ))}
280
+ {hasMore ? <div ref={sentinelRef} className="h-px shrink-0" /> : null}
281
+ </div>
282
+
283
+ {/* Scroll-fade — only while content is hidden in that direction. */}
284
+ <div
285
+ aria-hidden
286
+ className={cn(
287
+ 'pointer-events-none absolute inset-x-0 top-0 h-12 transition-opacity duration-150',
288
+ fade.top ? 'opacity-100' : 'opacity-0',
289
+ )}
290
+ style={{
291
+ background:
292
+ 'linear-gradient(0deg, transparent 0%, var(--color-bg-card) 100%)',
293
+ }}
294
+ />
295
+ <div
296
+ aria-hidden
297
+ className={cn(
298
+ 'pointer-events-none absolute inset-x-0 bottom-0 h-12 transition-opacity duration-150',
299
+ fade.bottom ? 'opacity-100' : 'opacity-0',
300
+ )}
301
+ style={{
302
+ background:
303
+ 'linear-gradient(180deg, transparent 0%, var(--color-bg-card) 100%)',
304
+ }}
305
+ />
306
+ </div>
307
+ )
308
+ }
@@ -0,0 +1,223 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../utils/cn'
5
+ import {
6
+ ModalV2,
7
+ ModalV2Header,
8
+ ModalV2Title,
9
+ ModalV2Footer,
10
+ } from '../ui/modal-v2'
11
+ import type { DialogItem } from './types/component.types'
12
+
13
+ // Shared button styling for the modal footers (Figma `button-full`):
14
+ // full-width, `text-h3` (DM Sans Bold 18px), 12/16 padding, rounded-md.
15
+ const footerBtn =
16
+ 'flex-1 min-w-0 rounded-md px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)] text-h3 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent disabled:opacity-50 disabled:cursor-not-allowed'
17
+ const cancelBtn = cn(
18
+ footerBtn,
19
+ 'border border-ods-border bg-transparent text-ods-text-primary hover:bg-ods-bg-hover',
20
+ )
21
+ const saveBtn = cn(
22
+ footerBtn,
23
+ 'bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover',
24
+ )
25
+ const dangerBtn = cn(
26
+ footerBtn,
27
+ 'bg-ods-error text-ods-text-on-accent hover:opacity-90',
28
+ )
29
+
30
+ // =============================================================================
31
+ // Rename
32
+ // =============================================================================
33
+
34
+ export interface RenameChatModalProps {
35
+ isOpen: boolean
36
+ /** Current chat name, used to seed the input each time the modal opens. */
37
+ initialName?: string
38
+ onClose: () => void
39
+ /** Fired with the trimmed new name when Save is pressed. */
40
+ onSave: (name: string) => void
41
+ }
42
+
43
+ /** Rename Chat modal — Figma node `7592:225962`. */
44
+ export function RenameChatModal({
45
+ isOpen,
46
+ initialName = '',
47
+ onClose,
48
+ onSave,
49
+ }: RenameChatModalProps) {
50
+ const [name, setName] = React.useState(initialName)
51
+ // Reseed whenever the modal (re)opens — possibly for a different chat.
52
+ React.useEffect(() => {
53
+ if (isOpen) setName(initialName)
54
+ }, [isOpen, initialName])
55
+
56
+ const canSave = name.trim().length > 0
57
+ const save = () => {
58
+ if (canSave) onSave(name.trim())
59
+ }
60
+
61
+ return (
62
+ <ModalV2 isOpen={isOpen} onClose={onClose} className="md:max-w-[600px]">
63
+ <ModalV2Header>
64
+ <ModalV2Title>Rename Chat</ModalV2Title>
65
+ </ModalV2Header>
66
+ <div className="flex w-full flex-col gap-[var(--spacing-system-xxs)]">
67
+ <label htmlFor="rename-chat-input" className="text-h4 text-ods-text-primary">
68
+ Chat Name
69
+ </label>
70
+ <input
71
+ id="rename-chat-input"
72
+ autoFocus
73
+ value={name}
74
+ onChange={(e) => setName(e.target.value)}
75
+ onKeyDown={(e) => {
76
+ if (e.key === 'Enter') save()
77
+ }}
78
+ className="w-full rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-sf)] text-h4 text-ods-text-primary focus:outline-none focus-visible:border-ods-accent"
79
+ />
80
+ </div>
81
+ <ModalV2Footer>
82
+ <button type="button" onClick={onClose} className={cancelBtn}>
83
+ Cancel
84
+ </button>
85
+ <button type="button" onClick={save} disabled={!canSave} className={saveBtn}>
86
+ Save
87
+ </button>
88
+ </ModalV2Footer>
89
+ </ModalV2>
90
+ )
91
+ }
92
+
93
+ // =============================================================================
94
+ // Archive
95
+ // =============================================================================
96
+
97
+ export interface ArchiveChatModalProps {
98
+ isOpen: boolean
99
+ onClose: () => void
100
+ /** Fired when the destructive "Archive Chat" button is pressed. */
101
+ onConfirm: () => void
102
+ }
103
+
104
+ /** Archive Chat confirmation modal — Figma node `7592:226181`. */
105
+ export function ArchiveChatModal({
106
+ isOpen,
107
+ onClose,
108
+ onConfirm,
109
+ }: ArchiveChatModalProps) {
110
+ return (
111
+ <ModalV2 isOpen={isOpen} onClose={onClose} className="md:max-w-[600px]">
112
+ <ModalV2Header>
113
+ <ModalV2Title>Archive Chat</ModalV2Title>
114
+ </ModalV2Header>
115
+ <p className="w-full text-h4 text-ods-text-primary">
116
+ This chat will be hidden from your current chats.
117
+ </p>
118
+ <ModalV2Footer>
119
+ <button type="button" onClick={onClose} className={cancelBtn}>
120
+ Cancel
121
+ </button>
122
+ <button type="button" onClick={onConfirm} className={dangerBtn}>
123
+ Archive Chat
124
+ </button>
125
+ </ModalV2Footer>
126
+ </ModalV2>
127
+ )
128
+ }
129
+
130
+ // =============================================================================
131
+ // Unarchive
132
+ // =============================================================================
133
+
134
+ export interface UnarchiveChatModalProps {
135
+ isOpen: boolean
136
+ onClose: () => void
137
+ /** Fired when the "Unarchive Chat" button is pressed. */
138
+ onConfirm: () => void
139
+ }
140
+
141
+ /** Unarchive (restore) Chat confirmation modal — restores an archived chat
142
+ * back to the current chats so the user can continue it. */
143
+ export function UnarchiveChatModal({
144
+ isOpen,
145
+ onClose,
146
+ onConfirm,
147
+ }: UnarchiveChatModalProps) {
148
+ return (
149
+ <ModalV2 isOpen={isOpen} onClose={onClose} className="md:max-w-[600px]">
150
+ <ModalV2Header>
151
+ <ModalV2Title>Unarchive Chat</ModalV2Title>
152
+ </ModalV2Header>
153
+ <p className="w-full text-h4 text-ods-text-primary">
154
+ This chat will be moved back to your current chats.
155
+ </p>
156
+ <ModalV2Footer>
157
+ <button type="button" onClick={onClose} className={cancelBtn}>
158
+ Cancel
159
+ </button>
160
+ <button type="button" onClick={onConfirm} className={saveBtn}>
161
+ Unarchive Chat
162
+ </button>
163
+ </ModalV2Footer>
164
+ </ModalV2>
165
+ )
166
+ }
167
+
168
+ // =============================================================================
169
+ // Composite — all three dialog-action modals
170
+ // =============================================================================
171
+
172
+ export interface ChatDialogModalsProps {
173
+ /** Dialog pending rename (`null` = closed). */
174
+ renameTarget: DialogItem | null
175
+ setRenameTarget: (dialog: DialogItem | null) => void
176
+ onConfirmRename: (name: string) => void
177
+ /** Dialog pending archive (`null` = closed). */
178
+ archiveTarget: DialogItem | null
179
+ setArchiveTarget: (dialog: DialogItem | null) => void
180
+ onConfirmArchive: () => void
181
+ /** Dialog pending restore/unarchive (`null` = closed). */
182
+ restoreTarget: DialogItem | null
183
+ setRestoreTarget: (dialog: DialogItem | null) => void
184
+ onConfirmRestore: () => void
185
+ }
186
+
187
+ /**
188
+ * Renders the Rename / Archive / Unarchive modals together, each driven by its
189
+ * target dialog. Pair with `useChatDialogManager`, which produces exactly this
190
+ * prop shape.
191
+ */
192
+ export function ChatDialogModals({
193
+ renameTarget,
194
+ setRenameTarget,
195
+ onConfirmRename,
196
+ archiveTarget,
197
+ setArchiveTarget,
198
+ onConfirmArchive,
199
+ restoreTarget,
200
+ setRestoreTarget,
201
+ onConfirmRestore,
202
+ }: ChatDialogModalsProps) {
203
+ return (
204
+ <>
205
+ <RenameChatModal
206
+ isOpen={renameTarget != null}
207
+ initialName={renameTarget?.title ?? ''}
208
+ onClose={() => setRenameTarget(null)}
209
+ onSave={onConfirmRename}
210
+ />
211
+ <ArchiveChatModal
212
+ isOpen={archiveTarget != null}
213
+ onClose={() => setArchiveTarget(null)}
214
+ onConfirm={onConfirmArchive}
215
+ />
216
+ <UnarchiveChatModal
217
+ isOpen={restoreTarget != null}
218
+ onClose={() => setRestoreTarget(null)}
219
+ onConfirm={onConfirmRestore}
220
+ />
221
+ </>
222
+ )
223
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
+ import { Button } from '../ui/button'
5
6
 
6
7
  export interface MingoOnboardingCardAction {
7
8
  /** Stable React key. */
@@ -91,22 +92,18 @@ export function MingoOnboardingCard({
91
92
  {hasActions ? (
92
93
  <div className="flex flex-wrap items-center gap-[var(--spacing-system-xxs)] mt-[var(--spacing-system-xs)]">
93
94
  {actions!.map((action) => (
94
- <button
95
+ <Button
95
96
  key={action.id}
96
97
  type="button"
98
+ variant="outline"
99
+ size="small"
97
100
  onClick={(e) => {
98
101
  e.stopPropagation()
99
102
  action.onClick(e)
100
103
  }}
101
- className={cn(
102
- 'inline-flex h-7 items-center justify-center px-[var(--spacing-system-xs)] rounded-md',
103
- 'border border-ods-border bg-transparent text-h6 text-ods-text-primary',
104
- 'transition-colors hover:bg-ods-bg-hover hover:border-ods-text-secondary',
105
- 'focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',
106
- )}
107
104
  >
108
105
  {action.label}
109
- </button>
106
+ </Button>
110
107
  ))}
111
108
  </div>
112
109
  ) : null}