@flamingo-stack/openframe-frontend-core 0.0.461 → 0.0.462-snapshot.20260721190919

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 (105) hide show
  1. package/dist/{chunk-KUVFE5WC.js → chunk-2ZDVWIAX.js} +3735 -3603
  2. package/dist/chunk-2ZDVWIAX.js.map +1 -0
  3. package/dist/{chunk-7NUTHK3A.js → chunk-4RQZ5X6Y.js} +4 -4
  4. package/dist/{chunk-FQKLIGV3.cjs → chunk-52NND23E.cjs} +62 -62
  5. package/dist/{chunk-FQKLIGV3.cjs.map → chunk-52NND23E.cjs.map} +1 -1
  6. package/dist/{chunk-5D3UPZIM.cjs → chunk-5ME74JNR.cjs} +59 -59
  7. package/dist/{chunk-5D3UPZIM.cjs.map → chunk-5ME74JNR.cjs.map} +1 -1
  8. package/dist/{chunk-S2KZZVJF.js → chunk-CIWVOQKW.js} +2 -2
  9. package/dist/{chunk-6IA5TD7O.cjs → chunk-E6OKPI77.cjs} +15 -15
  10. package/dist/{chunk-6IA5TD7O.cjs.map → chunk-E6OKPI77.cjs.map} +1 -1
  11. package/dist/{chunk-HWJWRQGV.cjs → chunk-G2GSHNSJ.cjs} +5 -5
  12. package/dist/{chunk-HWJWRQGV.cjs.map → chunk-G2GSHNSJ.cjs.map} +1 -1
  13. package/dist/{chunk-RTLYVFFQ.js → chunk-GHSSMGZL.js} +2 -2
  14. package/dist/{chunk-ZYHO7WAW.js → chunk-IDMH7CZO.js} +2 -2
  15. package/dist/{chunk-TFVBIOSB.cjs → chunk-IRVMOG4M.cjs} +956 -824
  16. package/dist/chunk-IRVMOG4M.cjs.map +1 -0
  17. package/dist/{chunk-BFEMQ2XC.cjs → chunk-KFND7IRU.cjs} +26 -26
  18. package/dist/{chunk-BFEMQ2XC.cjs.map → chunk-KFND7IRU.cjs.map} +1 -1
  19. package/dist/{chunk-2T5MXXTK.cjs → chunk-ME6PX3N3.cjs} +31 -31
  20. package/dist/{chunk-2T5MXXTK.cjs.map → chunk-ME6PX3N3.cjs.map} +1 -1
  21. package/dist/{chunk-HHKLSJ5Y.js → chunk-NXCDJV2N.js} +2 -2
  22. package/dist/{chunk-EXSLPPEY.js → chunk-OURYLIMA.js} +4 -4
  23. package/dist/{chunk-NFILBHXY.cjs → chunk-Q5QILCAZ.cjs} +9 -9
  24. package/dist/{chunk-NFILBHXY.cjs.map → chunk-Q5QILCAZ.cjs.map} +1 -1
  25. package/dist/{chunk-4TKGFC7I.js → chunk-QMJALAKE.js} +2 -2
  26. package/dist/{chunk-CDARBFPL.js → chunk-RQG32QZS.js} +2 -2
  27. package/dist/{chunk-QLLXI554.cjs → chunk-VHR5BOCQ.cjs} +26 -26
  28. package/dist/{chunk-QLLXI554.cjs.map → chunk-VHR5BOCQ.cjs.map} +1 -1
  29. package/dist/{chunk-WHXYDEAM.cjs → chunk-VUDXSYY7.cjs} +37 -37
  30. package/dist/{chunk-WHXYDEAM.cjs.map → chunk-VUDXSYY7.cjs.map} +1 -1
  31. package/dist/{chunk-3H5XD5UD.cjs → chunk-WDN6RDXY.cjs} +7 -7
  32. package/dist/{chunk-3H5XD5UD.cjs.map → chunk-WDN6RDXY.cjs.map} +1 -1
  33. package/dist/{chunk-FYIX7RTZ.js → chunk-XEQCEYTI.js} +4 -4
  34. package/dist/{chunk-UNGRKZ65.cjs → chunk-YMRAEKRR.cjs} +11 -11
  35. package/dist/{chunk-UNGRKZ65.cjs.map → chunk-YMRAEKRR.cjs.map} +1 -1
  36. package/dist/{chunk-PLJSN3SR.js → chunk-YQI5NNA2.js} +4 -4
  37. package/dist/{chunk-IQU6ZIZR.js → chunk-YROSJGPY.js} +2 -2
  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-panel-header.d.ts +9 -1
  41. package/dist/components/chat/chat-panel-header.d.ts.map +1 -1
  42. package/dist/components/chat/embeddable-chat.d.ts +8 -0
  43. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  44. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
  45. package/dist/components/chat/index.cjs +2 -2
  46. package/dist/components/chat/index.js +1 -1
  47. package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
  48. package/dist/components/chat/mingo-history-rail.d.ts +11 -1
  49. package/dist/components/chat/mingo-history-rail.d.ts.map +1 -1
  50. package/dist/components/chat/quick-action-wall.d.ts.map +1 -1
  51. package/dist/components/chat/types/component.types.d.ts +8 -0
  52. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  53. package/dist/components/chat/types/unified-chat-state.types.d.ts +7 -0
  54. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  55. package/dist/components/contact/index.cjs +3 -3
  56. package/dist/components/contact/index.js +2 -2
  57. package/dist/components/docs/index.cjs +5 -5
  58. package/dist/components/docs/index.js +4 -4
  59. package/dist/components/embeds/index.cjs +3 -3
  60. package/dist/components/embeds/index.js +2 -2
  61. package/dist/components/faq/index.cjs +3 -3
  62. package/dist/components/faq/index.js +2 -2
  63. package/dist/components/features/index.cjs +2 -2
  64. package/dist/components/features/index.js +1 -1
  65. package/dist/components/help-center-pages/index.cjs +20 -20
  66. package/dist/components/help-center-pages/index.js +11 -11
  67. package/dist/components/index.cjs +129 -129
  68. package/dist/components/index.js +9 -9
  69. package/dist/components/navigation/index.cjs +2 -2
  70. package/dist/components/navigation/index.js +1 -1
  71. package/dist/components/onboarding-guides/index.cjs +5 -5
  72. package/dist/components/onboarding-guides/index.js +4 -4
  73. package/dist/components/related-content/index.cjs +3 -3
  74. package/dist/components/related-content/index.js +2 -2
  75. package/dist/components/tickets/index.cjs +5 -5
  76. package/dist/components/tickets/index.js +4 -4
  77. package/dist/components/ui/index.cjs +2 -2
  78. package/dist/components/ui/index.js +1 -1
  79. package/dist/components/ui/tab-selector.d.ts.map +1 -1
  80. package/dist/index.cjs +2 -2
  81. package/dist/index.js +1 -1
  82. package/package.json +1 -1
  83. package/src/components/chat/chat-panel-header.tsx +21 -1
  84. package/src/components/chat/embeddable-chat.tsx +51 -4
  85. package/src/components/chat/hooks/use-unified-chat.ts +9 -0
  86. package/src/components/chat/mingo-chat-history.tsx +85 -37
  87. package/src/components/chat/mingo-history-rail.tsx +31 -0
  88. package/src/components/chat/quick-action-wall.tsx +47 -27
  89. package/src/components/chat/types/component.types.ts +8 -0
  90. package/src/components/chat/types/unified-chat-state.types.ts +9 -0
  91. package/src/components/ui/tab-selector.tsx +7 -4
  92. package/src/stories/QuickActionWall.stories.tsx +23 -0
  93. package/dist/chunk-KUVFE5WC.js.map +0 -1
  94. package/dist/chunk-TFVBIOSB.cjs.map +0 -1
  95. /package/dist/{chunk-7NUTHK3A.js.map → chunk-4RQZ5X6Y.js.map} +0 -0
  96. /package/dist/{chunk-S2KZZVJF.js.map → chunk-CIWVOQKW.js.map} +0 -0
  97. /package/dist/{chunk-RTLYVFFQ.js.map → chunk-GHSSMGZL.js.map} +0 -0
  98. /package/dist/{chunk-ZYHO7WAW.js.map → chunk-IDMH7CZO.js.map} +0 -0
  99. /package/dist/{chunk-HHKLSJ5Y.js.map → chunk-NXCDJV2N.js.map} +0 -0
  100. /package/dist/{chunk-EXSLPPEY.js.map → chunk-OURYLIMA.js.map} +0 -0
  101. /package/dist/{chunk-4TKGFC7I.js.map → chunk-QMJALAKE.js.map} +0 -0
  102. /package/dist/{chunk-CDARBFPL.js.map → chunk-RQG32QZS.js.map} +0 -0
  103. /package/dist/{chunk-FYIX7RTZ.js.map → chunk-XEQCEYTI.js.map} +0 -0
  104. /package/dist/{chunk-PLJSN3SR.js.map → chunk-YQI5NNA2.js.map} +0 -0
  105. /package/dist/{chunk-IQU6ZIZR.js.map → chunk-YROSJGPY.js.map} +0 -0
@@ -5,6 +5,7 @@ import { cn } from '../../utils/cn'
5
5
  import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
6
6
  import { Button } from '../ui/button'
7
7
  import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
8
+ import { SquareAvatar } from '../ui/square-avatar'
8
9
  import { Ellipsis01Icon, SearchXmarkIcon } from '../icons-v2-generated'
9
10
  import { ChatListEmptyState } from './chat-list-empty-state'
10
11
  import type { DialogItem } from './types/component.types'
@@ -107,6 +108,8 @@ function MingoChatHistoryRow({
107
108
  const title = dialog.title || 'Untitled Chat'
108
109
  const unread = dialog.unreadMessagesCount ?? 0
109
110
  const hasMenu = !!onRequestRename || !!onRequestArchive
111
+ const owner = dialog.owner
112
+ const hasAvatar = !!(owner?.name || owner?.avatarUrl)
110
113
  // Keep the `⋯` visible while its menu is open — once Radix moves focus into
111
114
  // the portalled content the row loses hover/focus-within.
112
115
  const [menuOpen, setMenuOpen] = React.useState(false)
@@ -173,46 +176,82 @@ function MingoChatHistoryRow({
173
176
  {title}
174
177
  </span>
175
178
  </div>
176
- {hasMenu ? (
177
- // Sibling of the padded content (not inside it): sits at the item's
178
- // right edge, free of the content's left padding. Revealed on row
179
- // hover/focus; kept visible while its menu is open. Hidden on mobile —
180
- // it's a hover affordance (no hover on touch).
179
+ {hasMenu || hasAvatar ? (
180
+ // Trailing 48px cell (Figma 113:63224): the owner avatar sits centred
181
+ // by default; hovering the row (or opening the menu) swaps it for the
182
+ // `⋯` actions button IN PLACE, so the title never reflows. Without an
183
+ // avatar the cell exists only for the hover menu, so it collapses on
184
+ // mobile exactly like the old hover-only gutter (no hover on touch).
181
185
  <span
182
- onClick={(e) => e.stopPropagation()}
183
186
  className={cn(
184
- // `self-stretch` so the wrapper fills the row height — gives the
185
- // button's `h-full` a definite parent to resolve against (under the
186
- // row's `items-center` the wrapper would otherwise collapse to its
187
- // content and `h-full` on the button would no-op).
188
- 'self-stretch shrink-0 transition-opacity max-md:hidden',
189
- menuOpen
190
- ? 'opacity-100'
191
- : 'opacity-0 group-hover/row:opacity-100 focus-within:opacity-100',
187
+ 'relative w-12 self-stretch shrink-0',
188
+ !hasAvatar && 'max-md:hidden',
192
189
  )}
193
190
  >
194
- <ActionsMenuDropdown
195
- triggerAriaLabel="Chat actions"
196
- groups={[{ items: menuItems }]}
197
- open={menuOpen}
198
- onOpenChange={setMenuOpen}
199
- // Don't return focus (and its ring) to the `⋯` trigger on close.
200
- onCloseAutoFocus={(e) => e.preventDefault()}
201
- customTrigger={
202
- <Button
203
- variant="transparent"
204
- size="icon"
205
- aria-label="Chat actions"
206
- // Square, grey icon → white on hover. `h-full` matches the row
207
- // height exactly (no breakpoint poke from `size="icon"`'s
208
- // responsive `md:h-12`); `w-9` keeps it compact; `p-0` drops the
209
- // icon-size padding. tailwind-merge so these win over defaults.
210
- className="h-full md:h-full rounded-none p-0 text-ods-text-secondary hover:text-ods-text-primary"
211
- >
212
- <Ellipsis01Icon />
213
- </Button>
214
- }
215
- />
191
+ {hasMenu ? (
192
+ // Revealed on row hover/its own focus; kept visible while the menu
193
+ // is open. Hidden on mobile — it's a hover affordance. `peer/menu`
194
+ // so the avatar layer (a later sibling) can fade when the trigger
195
+ // holds keyboard focus.
196
+ <span
197
+ onClick={(e) => e.stopPropagation()}
198
+ className={cn(
199
+ 'peer/menu absolute inset-0 transition-opacity max-md:hidden',
200
+ menuOpen
201
+ ? 'opacity-100'
202
+ : 'opacity-0 group-hover/row:opacity-100 focus-within:opacity-100',
203
+ )}
204
+ >
205
+ <ActionsMenuDropdown
206
+ triggerAriaLabel="Chat actions"
207
+ groups={[{ items: menuItems }]}
208
+ open={menuOpen}
209
+ onOpenChange={setMenuOpen}
210
+ // Don't return focus (and its ring) to the `⋯` trigger on close.
211
+ onCloseAutoFocus={(e) => e.preventDefault()}
212
+ customTrigger={
213
+ <Button
214
+ variant="transparent"
215
+ size="icon"
216
+ aria-label="Chat actions"
217
+ // Square, grey icon → white on hover. `h-full`/`w-full` fill
218
+ // the 48px cell exactly (no breakpoint poke from
219
+ // `size="icon"`'s responsive `md:h-12`); `p-0` drops the
220
+ // icon-size padding. tailwind-merge so these win over
221
+ // defaults.
222
+ className="h-full md:h-full w-full rounded-none p-0 text-ods-text-secondary hover:text-ods-text-primary"
223
+ >
224
+ <Ellipsis01Icon />
225
+ </Button>
226
+ }
227
+ />
228
+ </span>
229
+ ) : null}
230
+ {hasAvatar ? (
231
+ // Avatar layer — clicks fall through to row selection (it covers
232
+ // the cell whenever the menu trigger is faded out). On desktop it
233
+ // fades for the menu swap; on mobile it's always shown.
234
+ <span
235
+ onClick={() => onSelect?.(dialog.id)}
236
+ title={owner?.name || undefined}
237
+ className={cn(
238
+ 'absolute inset-0 flex cursor-pointer items-center justify-center transition-opacity',
239
+ hasMenu &&
240
+ (menuOpen
241
+ ? 'opacity-0 pointer-events-none'
242
+ : 'md:group-hover/row:opacity-0 md:group-hover/row:pointer-events-none md:peer-[:focus-within]/menu:opacity-0 md:peer-[:focus-within]/menu:pointer-events-none'),
243
+ )}
244
+ >
245
+ <SquareAvatar
246
+ variant="round"
247
+ sizePx={24}
248
+ src={owner?.avatarUrl || undefined}
249
+ alt={owner?.name || undefined}
250
+ fallback={owner?.name || undefined}
251
+ initialsClassName="text-[10px] text-ods-text-secondary"
252
+ />
253
+ </span>
254
+ ) : null}
216
255
  </span>
217
256
  ) : null}
218
257
  </div>
@@ -316,6 +355,7 @@ export function MingoChatHistory({
316
355
  onLoadMoreRef.current = onLoadMore
317
356
  const isLoadingMoreRef = React.useRef(isLoadingMore)
318
357
  isLoadingMoreRef.current = isLoadingMore
358
+ const dialogCount = dialogs.length
319
359
  React.useEffect(() => {
320
360
  const root = scrollRef.current
321
361
  const sentinel = sentinelRef.current
@@ -331,7 +371,15 @@ export function MingoChatHistory({
331
371
  )
332
372
  io.observe(sentinel)
333
373
  return () => io.disconnect()
334
- }, [hasMore])
374
+ // `dialogCount`/`isLoadingMore` deps re-arm the observer after every page
375
+ // lands: `observe()` re-reports the CURRENT intersection, so a sentinel
376
+ // that stays in view (short host-filtered list — e.g. the "My Chats"
377
+ // scope keeps a handful of rows out of a 20-row page) keeps chaining
378
+ // loads until the viewport fills or `hasMore` flips. A continuously
379
+ // visible sentinel never re-crosses the threshold, so the previous
380
+ // [hasMore]-only observer fired exactly once per mount and the list
381
+ // grew by a single page per drawer open.
382
+ }, [hasMore, isLoadingMore, dialogCount])
335
383
 
336
384
  return (
337
385
  <div className={cn('relative flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)]', className)}>
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
5
  import { Button } from '../ui/button'
6
+ import { TabSelector } from '../ui/tab-selector'
6
7
  import { ChatsIcon } from '../icons-v2-generated/communication/chats-icon'
7
8
  import { PlusCircleIcon } from '../icons-v2-generated/signs-and-symbols/plus-circle-icon'
8
9
  import { AlertCircleIcon, Refresh01RightIcon } from '../icons-v2-generated'
@@ -14,6 +15,10 @@ import type { DialogItem } from './types/component.types'
14
15
  // Types
15
16
  // =============================================================================
16
17
 
18
+ /** Ownership scope of the rail's dialog list — the current user's chats only,
19
+ * or every admin's chats in the tenant. */
20
+ export type MingoHistoryScope = 'my' | 'all'
21
+
17
22
  export interface MingoHistoryRailProps {
18
23
  /** Dialogs to list, assumed already sorted newest-first by the host. */
19
24
  dialogs: ReadonlyArray<DialogItem>
@@ -32,6 +37,13 @@ export interface MingoHistoryRailProps {
32
37
  onRequestRename?: (dialog: DialogItem) => void
33
38
  /** Request archive — enables the row "Archive chat" action. */
34
39
  onRequestArchive?: (dialog: DialogItem) => void
40
+ /** Ownership scope shown as a two-state "My Chats / All Chats" selector
41
+ * between "Start New Chat" and the list. Rendered only when BOTH `scope`
42
+ * and `onScopeChange` are provided; the host owns the state and refilters
43
+ * the `dialogs` it passes in. */
44
+ scope?: MingoHistoryScope
45
+ /** Scope selector change handler — see `scope`. */
46
+ onScopeChange?: (scope: MingoHistoryScope) => void
35
47
  /** Current server-side search term. Drives the list's "No chats found"
36
48
  * empty state; the search INPUT itself lives in the panel header now, not
37
49
  * in the rail body. */
@@ -77,6 +89,8 @@ export function MingoHistoryRail({
77
89
  newChatAlways = false,
78
90
  onRequestRename,
79
91
  onRequestArchive,
92
+ scope,
93
+ onScopeChange,
80
94
  searchQuery,
81
95
  hasMore = false,
82
96
  isLoadingMore = false,
@@ -114,6 +128,23 @@ export function MingoHistoryRail({
114
128
  </Button>
115
129
  )}
116
130
 
131
+ {/* Two-state ownership scope — "My Chats" / "All Chats". Stays visible in
132
+ the empty state too: an empty "My Chats" list must still offer the
133
+ switch to "All Chats" (host filters, so the empty state can be
134
+ scope-induced). Hidden while the load failed — retry is the only
135
+ useful action there. */}
136
+ {scope && onScopeChange && !loadError ? (
137
+ <TabSelector
138
+ className="shrink-0"
139
+ value={scope}
140
+ onValueChange={(value) => onScopeChange(value as MingoHistoryScope)}
141
+ items={[
142
+ { id: 'my', label: 'My Chats' },
143
+ { id: 'all', label: 'All Chats' },
144
+ ]}
145
+ />
146
+ ) : null}
147
+
117
148
  {loadError ? (
118
149
  <div className="flex flex-1 min-h-0 flex-col items-center justify-center gap-[var(--spacing-system-m)] text-center">
119
150
  <AlertCircleIcon className="h-8 w-8 text-ods-text-secondary shrink-0" />
@@ -38,24 +38,27 @@ export function interleave<T>(a: ReadonlyArray<T>, b: ReadonlyArray<T>): T[] {
38
38
  return out
39
39
  }
40
40
 
41
- // Brick-mode padding fallback: ~14 columns of chips per row (~3200px) — one
42
- // row copy overflows any sensible container, so the loop always engages. Used
43
- // as the row-count reference and until the container width has been measured;
44
- // once measured, the per-row pad target adapts to that width (see MIN_CHIP_PX).
41
+ // Brick-mode padding fallback: ~14 columns of chips per row — one row copy
42
+ // overflows any sensible container, so the loop always engages. Used as the
43
+ // row-count reference, and as the pad target until the container and chip
44
+ // widths have been measured (then the per-row target adapts — see chipWidth).
45
45
  const DEFAULT_MIN_CHIPS_PER_ROW = 14
46
46
 
47
- // Conservative floor for one chip's on-track footprint in px (icon + a short
48
- // mono label + Tag padding + the copy gap). The adaptive pad target divides the
49
- // measured container width by this, so it must stay AT OR BELOW the narrowest
50
- // real chip — an over-estimate would under-pad and the row could stop scrolling
51
- // on a wide surface. Realistic quick-action labels are far wider, so the padded
52
- // copy overflows comfortably; the `+ADAPTIVE_PAD_MARGIN` covers the worst case.
53
- const MIN_CHIP_PX = 72
47
+ // The adaptive pad target is derived from the MEASURED narrowest chip, not a
48
+ // guessed width: repeats = ceil(containerWidth / measuredChipWidth) + margin.
49
+ // Measuring (rather than assuming ~72px) is what makes it correct for a tiny
50
+ // one-word chip AND a wide multi-word one — a guess that overshoots the real
51
+ // chip under-pads and the row silently stops scrolling.
54
52
  const ADAPTIVE_PAD_MARGIN = 2
55
- // The adaptive target is clamped to [MIN_ADAPTIVE_PAD, DEFAULT_MIN_CHIPS_PER_ROW]:
56
- // it only ever REDUCES padding below the fixed default (for narrow composers) —
57
- // never above it, so no wide surface pads more than it does today.
53
+ // Divide-by-near-zero guard ONLY: floors a glitched sub-pixel measurement, but
54
+ // stays BELOW the narrowest real chip (padding alone is wider), so a genuinely
55
+ // tiny one-glyph chip keeps its true measured width and still gets enough
56
+ // repeats to overflow. MAX_ADAPTIVE_PAD bounds the count if the measurement is
57
+ // ever degenerate.
58
+ const MIN_MEASURED_CHIP_PX = 12
59
+ // Sanity bounds on the resulting count.
58
60
  const MIN_ADAPTIVE_PAD = 4
61
+ const MAX_ADAPTIVE_PAD = 60
59
62
 
60
63
  // Chat-agent walls (fae/mingo) cap the brick stack at 2 rows so the quick
61
64
  // actions never crowd the composer; every other surface keeps the caller's
@@ -187,22 +190,39 @@ export function QuickActionWall({
187
190
  contentClassName,
188
191
  sync,
189
192
  }: QuickActionWallProps) {
190
- // Measure the brick container so the per-row pad target can adapt to its
191
- // width (see MIN_CHIP_PX): a narrow chat composer needs only a handful of
192
- // repeats to overflow, a wide wall needs many. Pre-measure / SSR falls back
193
- // to the fixed default, which always overflows. Only used in brick mode.
193
+ // Adapt the per-row pad target to the actual geometry (brick mode only): a
194
+ // narrow chat composer needs only a handful of repeats to overflow, a wide
195
+ // wall needs many, and a tiny chip needs more repeats than a wide one. We
196
+ // measure BOTH the container width and the narrowest rendered chip, then pad
197
+ // to ceil(width / chip) + margin. Pre-measure / SSR falls back to the fixed
198
+ // default, which overflows any width.
194
199
  const brickRef = React.useRef<HTMLDivElement>(null)
195
200
  const [brickWidth, setBrickWidth] = React.useState(0)
201
+ const [chipWidth, setChipWidth] = React.useState(0)
196
202
  React.useEffect(() => {
197
203
  const el = brickRef.current
198
- if (!el || typeof ResizeObserver === 'undefined') return
199
- const ro = new ResizeObserver(entries => {
200
- const w = entries[0]?.contentRect.width ?? 0
201
- if (w > 0) setBrickWidth(w)
202
- })
204
+ if (!el) return
205
+ // Read both widths straight off the laid-out DOM (offsetWidth is synchronous
206
+ // and reliable — a ResizeObserver initial callback can be throttled or never
207
+ // fire in a backgrounded/offscreen tab, which would strand the target on its
208
+ // fallback). Chips render as buttons on interactive walls — the only case
209
+ // that pads short sets; decorative walls keep the fallback. Converges in one
210
+ // pass: the widths don't depend on how many repeats we then draw.
211
+ const measure = () => {
212
+ const w = el.offsetWidth
213
+ if (w > 0) setBrickWidth(prev => (prev === w ? prev : w))
214
+ const chips = [...el.querySelectorAll('button')].map(b => (b as HTMLElement).offsetWidth).filter(x => x > 0)
215
+ if (chips.length > 0) {
216
+ const min = Math.max(Math.min(...chips), MIN_MEASURED_CHIP_PX)
217
+ setChipWidth(prev => (prev === min ? prev : min))
218
+ }
219
+ }
220
+ measure()
221
+ if (typeof ResizeObserver === 'undefined') return
222
+ const ro = new ResizeObserver(measure)
203
223
  ro.observe(el)
204
224
  return () => ro.disconnect()
205
- }, [])
225
+ }, [chips])
206
226
 
207
227
  // Repeat-pad the track (stable suffixed keys; repeats keep the full chip —
208
228
  // identical selected/interactive state, so every visible instance behaves
@@ -266,10 +286,10 @@ export function QuickActionWall({
266
286
  // duplicates); before the first measure (or SSR) fall back to the fixed
267
287
  // default, which overflows any width.
268
288
  const adaptivePerRow =
269
- brickWidth > 0
289
+ brickWidth > 0 && chipWidth > 0
270
290
  ? Math.min(
271
- Math.max(Math.ceil(brickWidth / MIN_CHIP_PX) + ADAPTIVE_PAD_MARGIN, MIN_ADAPTIVE_PAD),
272
- DEFAULT_MIN_CHIPS_PER_ROW,
291
+ Math.max(Math.ceil(brickWidth / chipWidth) + ADAPTIVE_PAD_MARGIN, MIN_ADAPTIVE_PAD),
292
+ MAX_ADAPTIVE_PAD,
273
293
  )
274
294
  : DEFAULT_MIN_CHIPS_PER_ROW
275
295
  const perRowTarget = minChips ? Math.ceil(minChips / stackRows) : adaptivePerRow
@@ -608,6 +608,14 @@ export interface DialogItem {
608
608
  timestamp?: Date | string
609
609
  isActive?: boolean
610
610
  unreadMessagesCount?: number
611
+ /** Dialog owner (creator) — drives the trailing avatar in the chat-history
612
+ * rows (Figma 113:63224). Omit to render the row without an avatar. */
613
+ owner?: {
614
+ /** Full display name — initials fallback + `title` tooltip. */
615
+ name?: string | null
616
+ /** Absolute avatar URL; falls back to initials when absent or failing. */
617
+ avatarUrl?: string | null
618
+ }
611
619
  }
612
620
 
613
621
  // ========== Chat Sidebar Props ==========
@@ -369,6 +369,15 @@ export interface UnifiedChatState {
369
369
  /** Fetch the next page of dialogs. No-op when `hasMoreDialogs` is false. */
370
370
  loadMoreDialogs: () => Promise<void>
371
371
 
372
+ /** Ownership scope of `dialogs` — `'my'` (current user's chats) or `'all'`
373
+ * (every admin's). Optional: adapters without ownership data omit it and
374
+ * the "My Chats / All Chats" selector stays hidden. The HOST owns the
375
+ * filtering — `dialogs` must already reflect this scope. */
376
+ dialogScope?: 'my' | 'all'
377
+
378
+ /** Switch the ownership scope — see `dialogScope`. */
379
+ setDialogScope?: (scope: 'my' | 'all') => void
380
+
372
381
  /** Whether more historical messages remain in the active dialog. */
373
382
  hasMoreMessages: boolean
374
383
 
@@ -67,15 +67,18 @@ export function TabSelector({
67
67
  onClick={() => onValueChange(item.id)}
68
68
  aria-label={!item.label ? item.ariaLabel : undefined}
69
69
  className={cn(
70
- 'flex flex-1 items-center justify-center gap-2 rounded-xs p-2 text-h4 transition-colors duration-200 whitespace-nowrap',
70
+ // Bold on BOTH states — a medium↔bold flip between the active
71
+ // and inactive tabs read as inconsistent weight (and nudged
72
+ // label widths on every switch).
73
+ 'flex flex-1 items-center justify-center gap-2 rounded-xs p-2 text-h4 font-bold transition-colors duration-200 whitespace-nowrap',
71
74
  isDisabled
72
75
  ? isActive
73
- ? 'cursor-not-allowed bg-ods-bg-surface text-ods-text-secondary [font-weight:700]'
76
+ ? 'cursor-not-allowed bg-ods-bg-surface text-ods-text-secondary'
74
77
  : 'cursor-not-allowed bg-transparent text-ods-text-secondary'
75
78
  : isActive
76
79
  ? variant === 'primary'
77
- ? 'bg-ods-accent text-ods-text-on-accent cursor-default [font-weight:700]'
78
- : 'bg-ods-bg-surface text-ods-text-primary cursor-default [font-weight:700]'
80
+ ? 'bg-ods-accent text-ods-text-on-accent cursor-default'
81
+ : 'bg-ods-bg-surface text-ods-text-primary cursor-default'
79
82
  : 'bg-transparent text-ods-text-primary hover:bg-ods-bg-hover cursor-pointer',
80
83
  )}
81
84
  >
@@ -200,6 +200,29 @@ export const BrickNarrowComposer: Story = {
200
200
  ),
201
201
  }
202
202
 
203
+ /** Brick, single short chip: one tiny one-word action ("ONE") in a ~412px
204
+ * composer. The pad target is measured from the ACTUAL chip width, so a narrow
205
+ * chip is repeated enough times to overflow and scroll — a width guess that
206
+ * overshot the real chip would under-pad and the row would sit static. */
207
+ export const BrickSingleShortChip: Story = {
208
+ render: () => (
209
+ <div className="w-[412px] max-w-full rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-mf)]">
210
+ <QuickActionWall
211
+ chips={[{ id: 'one', label: 'ONE', theme: FAE_THEME, onSelect: () => console.log('one') }]}
212
+ agentSlug="mingo"
213
+ rows={4}
214
+ pauseOnHover
215
+ dragScroll
216
+ fade={['left', 'right']}
217
+ fadeSize={{ left: 32 }}
218
+ fadeColor="var(--color-bg-card)"
219
+ copyGap="var(--spacing-system-xxs)"
220
+ className="max-h-44"
221
+ />
222
+ </div>
223
+ ),
224
+ }
225
+
203
226
  /** Brick, non-agent (no `agentSlug`): keeps exactly `rows` rows (marketing /
204
227
  * onboarding walls are sized for their design), still getting the even split
205
228
  * so a row is never filled with only one repeated chip. */