@flamingo-stack/openframe-frontend-core 0.0.466 → 0.0.467-snapshot.20260723125044

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 (111) hide show
  1. package/dist/{chunk-5H3HACZZ.cjs → chunk-2WZAR2MB.cjs} +37 -37
  2. package/dist/{chunk-5H3HACZZ.cjs.map → chunk-2WZAR2MB.cjs.map} +1 -1
  3. package/dist/{chunk-FGL4ZNR7.cjs → chunk-3I34MUEG.cjs} +62 -62
  4. package/dist/{chunk-FGL4ZNR7.cjs.map → chunk-3I34MUEG.cjs.map} +1 -1
  5. package/dist/{chunk-VLLFRPNP.cjs → chunk-3PLQWO4H.cjs} +913 -742
  6. package/dist/chunk-3PLQWO4H.cjs.map +1 -0
  7. package/dist/{chunk-GRB7LPJV.js → chunk-3TU6QURP.js} +2 -2
  8. package/dist/{chunk-KX2TBXKN.js → chunk-AEM7ADDG.js} +2 -2
  9. package/dist/{chunk-RQZOCLTI.js → chunk-CLW2S55N.js} +4 -4
  10. package/dist/{chunk-YN77BNI7.cjs → chunk-ELIY4AO2.cjs} +26 -26
  11. package/dist/{chunk-YN77BNI7.cjs.map → chunk-ELIY4AO2.cjs.map} +1 -1
  12. package/dist/{chunk-FHLJQI4F.js → chunk-EZESVVIM.js} +4 -4
  13. package/dist/{chunk-7DULN25L.js → chunk-HTSWLPMU.js} +2 -2
  14. package/dist/{chunk-KLBBOLDK.cjs → chunk-HVLMAT4T.cjs} +5 -5
  15. package/dist/{chunk-KLBBOLDK.cjs.map → chunk-HVLMAT4T.cjs.map} +1 -1
  16. package/dist/{chunk-CC73CSLI.js → chunk-IUMPVRSH.js} +2 -2
  17. package/dist/{chunk-3WF7INYU.cjs → chunk-J6CE6HZS.cjs} +11 -11
  18. package/dist/{chunk-3WF7INYU.cjs.map → chunk-J6CE6HZS.cjs.map} +1 -1
  19. package/dist/{chunk-72HPXAMZ.js → chunk-JW3UNHGW.js} +2 -2
  20. package/dist/{chunk-BVNAYWLM.cjs → chunk-L3BWGFHT.cjs} +15 -15
  21. package/dist/{chunk-BVNAYWLM.cjs.map → chunk-L3BWGFHT.cjs.map} +1 -1
  22. package/dist/{chunk-NHNKMR22.cjs → chunk-MULVIGIN.cjs} +59 -59
  23. package/dist/{chunk-NHNKMR22.cjs.map → chunk-MULVIGIN.cjs.map} +1 -1
  24. package/dist/{chunk-OO7DO4DI.js → chunk-O2QOC5FA.js} +4 -4
  25. package/dist/{chunk-IZCOYFCN.js → chunk-P5SLNM3C.js} +5276 -5105
  26. package/dist/chunk-P5SLNM3C.js.map +1 -0
  27. package/dist/{chunk-WAO34PFO.js → chunk-QTGYQ2U7.js} +2 -2
  28. package/dist/{chunk-GHZMNDPN.js → chunk-QZJN5OHK.js} +4 -4
  29. package/dist/{chunk-HDP5VRPL.cjs → chunk-TISXEAGH.cjs} +7 -7
  30. package/dist/{chunk-HDP5VRPL.cjs.map → chunk-TISXEAGH.cjs.map} +1 -1
  31. package/dist/{chunk-2VCSDMPV.cjs → chunk-VHFH3BG6.cjs} +31 -31
  32. package/dist/{chunk-2VCSDMPV.cjs.map → chunk-VHFH3BG6.cjs.map} +1 -1
  33. package/dist/{chunk-MQQ7VIG6.cjs → chunk-VRJXY6TM.cjs} +26 -26
  34. package/dist/{chunk-MQQ7VIG6.cjs.map → chunk-VRJXY6TM.cjs.map} +1 -1
  35. package/dist/{chunk-XJH5ZT5Y.js → chunk-VYUEOKI6.js} +2 -2
  36. package/dist/{chunk-YHCLDRMU.cjs → chunk-X77X3RKH.cjs} +9 -9
  37. package/dist/{chunk-YHCLDRMU.cjs.map → chunk-X77X3RKH.cjs.map} +1 -1
  38. package/dist/components/announcement-bar.d.ts +1 -1
  39. package/dist/components/announcement-bar.d.ts.map +1 -1
  40. package/dist/components/case-studies/index.cjs +8 -8
  41. package/dist/components/case-studies/index.js +2 -2
  42. package/dist/components/chat/chat-composer.d.ts +8 -1
  43. package/dist/components/chat/chat-composer.d.ts.map +1 -1
  44. package/dist/components/chat/chat-context-memory-bar.d.ts +37 -0
  45. package/dist/components/chat/chat-context-memory-bar.d.ts.map +1 -0
  46. package/dist/components/chat/embeddable-chat.d.ts +14 -6
  47. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  48. package/dist/components/chat/index.cjs +4 -2
  49. package/dist/components/chat/index.cjs.map +1 -1
  50. package/dist/components/chat/index.d.ts +1 -0
  51. package/dist/components/chat/index.d.ts.map +1 -1
  52. package/dist/components/chat/index.js +3 -1
  53. package/dist/components/contact/index.cjs +3 -3
  54. package/dist/components/contact/index.js +2 -2
  55. package/dist/components/docs/index.cjs +5 -5
  56. package/dist/components/docs/index.js +4 -4
  57. package/dist/components/embeds/index.cjs +3 -3
  58. package/dist/components/embeds/index.js +2 -2
  59. package/dist/components/faq/index.cjs +3 -3
  60. package/dist/components/faq/index.js +2 -2
  61. package/dist/components/features/index.cjs +2 -2
  62. package/dist/components/features/index.js +1 -1
  63. package/dist/components/form.d.ts +1 -0
  64. package/dist/components/form.d.ts.map +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 +234 -206
  68. package/dist/components/index.cjs.map +1 -1
  69. package/dist/components/index.js +119 -91
  70. package/dist/components/index.js.map +1 -1
  71. package/dist/components/navigation/index.cjs +2 -2
  72. package/dist/components/navigation/index.js +1 -1
  73. package/dist/components/onboarding-guides/index.cjs +5 -5
  74. package/dist/components/onboarding-guides/index.js +4 -4
  75. package/dist/components/related-content/index.cjs +3 -3
  76. package/dist/components/related-content/index.js +2 -2
  77. package/dist/components/tickets/index.cjs +5 -5
  78. package/dist/components/tickets/index.js +4 -4
  79. package/dist/components/ui/announcement-bar-view.d.ts +49 -0
  80. package/dist/components/ui/announcement-bar-view.d.ts.map +1 -0
  81. package/dist/components/ui/index.cjs +6 -2
  82. package/dist/components/ui/index.cjs.map +1 -1
  83. package/dist/components/ui/index.d.ts +1 -0
  84. package/dist/components/ui/index.d.ts.map +1 -1
  85. package/dist/components/ui/index.js +5 -1
  86. package/dist/index.cjs +6 -2
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.js +5 -1
  89. package/package.json +1 -1
  90. package/src/components/announcement-bar.tsx +102 -110
  91. package/src/components/chat/chat-composer.tsx +32 -5
  92. package/src/components/chat/chat-context-memory-bar.tsx +159 -0
  93. package/src/components/chat/embeddable-chat.tsx +17 -15
  94. package/src/components/chat/index.ts +1 -0
  95. package/src/components/ui/announcement-bar-view.tsx +94 -0
  96. package/src/components/ui/index.ts +1 -0
  97. package/src/components/ui/tab-selector.tsx +8 -8
  98. package/src/styles/ods-colors.css +16 -0
  99. package/dist/chunk-IZCOYFCN.js.map +0 -1
  100. package/dist/chunk-VLLFRPNP.cjs.map +0 -1
  101. /package/dist/{chunk-GRB7LPJV.js.map → chunk-3TU6QURP.js.map} +0 -0
  102. /package/dist/{chunk-KX2TBXKN.js.map → chunk-AEM7ADDG.js.map} +0 -0
  103. /package/dist/{chunk-RQZOCLTI.js.map → chunk-CLW2S55N.js.map} +0 -0
  104. /package/dist/{chunk-FHLJQI4F.js.map → chunk-EZESVVIM.js.map} +0 -0
  105. /package/dist/{chunk-7DULN25L.js.map → chunk-HTSWLPMU.js.map} +0 -0
  106. /package/dist/{chunk-CC73CSLI.js.map → chunk-IUMPVRSH.js.map} +0 -0
  107. /package/dist/{chunk-72HPXAMZ.js.map → chunk-JW3UNHGW.js.map} +0 -0
  108. /package/dist/{chunk-OO7DO4DI.js.map → chunk-O2QOC5FA.js.map} +0 -0
  109. /package/dist/{chunk-WAO34PFO.js.map → chunk-QTGYQ2U7.js.map} +0 -0
  110. /package/dist/{chunk-GHZMNDPN.js.map → chunk-QZJN5OHK.js.map} +0 -0
  111. /package/dist/{chunk-XJH5ZT5Y.js.map → chunk-VYUEOKI6.js.map} +0 -0
@@ -1,20 +1,21 @@
1
- "use client";
1
+ 'use client';
2
2
 
3
- import { useEffect, useMemo, useRef, useState } from 'react';
4
3
  import { X } from 'lucide-react';
5
- import { Button } from './ui/button';
6
- import { EntityIcon } from './icon-display';
4
+ import { useEffect, useMemo, useRef, useState } from 'react';
5
+ import { useEndpointsRuntime } from '../contexts/endpoints-runtime-context';
6
+ import { useSelfFetch } from '../hooks/use-self-fetch';
7
+ import type { Announcement, AnnouncementBarProps, AnnouncementResponse } from '../types/announcement';
8
+ import { ANNOUNCEMENT_CTA_DEFAULTS } from '../types/announcement';
7
9
  import {
10
+ clearLegacyAnnouncementCache,
8
11
  dismissAnnouncement,
9
12
  isAnnouncementDismissed,
10
- clearLegacyAnnouncementCache,
11
13
  } from '../utils/announcement-storage';
12
- import { ANNOUNCEMENT_CTA_DEFAULTS } from '../types/announcement';
13
- import type { Announcement, AnnouncementBarProps, AnnouncementResponse } from '../types/announcement';
14
14
  import { getAppType } from '../utils/app-config';
15
- import { useEndpointsRuntime } from '../contexts/endpoints-runtime-context';
16
- import { useSelfFetch } from '../hooks/use-self-fetch';
17
15
  import { pickReadableTextColor } from '../utils/color-analysis';
16
+ import { EntityIcon } from './icon-display';
17
+ import { AnnouncementBarView } from './ui/announcement-bar-view';
18
+ import { Button } from './ui/button';
18
19
 
19
20
  /**
20
21
  * Platform announcement bar — DUAL MODE, works with or without SSR.
@@ -41,11 +42,7 @@ import { pickReadableTextColor } from '../utils/color-analysis';
41
42
  * storage reads happen ONLY in effects (a render-time read would desync
42
43
  * hydration in SSR mode).
43
44
  */
44
- export function AnnouncementBar({
45
- initialAnnouncement,
46
- previewMode = false,
47
- className,
48
- }: AnnouncementBarProps = {}) {
45
+ export function AnnouncementBar({ initialAnnouncement, previewMode = false, className }: AnnouncementBarProps = {}) {
49
46
  // Namespace for the dismissal cookie/legacy keys. Next hosts inline
50
47
  // NEXT_PUBLIC_APP_TYPE (matching the server's currentPlatform(), which the
51
48
  // hub layout uses for the SSR cookie read); platform-agnostic embeds get
@@ -55,7 +52,7 @@ export function AnnouncementBar({
55
52
 
56
53
  // Optional endpoint runtime: no provider → no URL → fetching disabled.
57
54
  const endpoints = useEndpointsRuntime();
58
- const url = previewMode ? null : endpoints?.announcementsUrl ?? null;
55
+ const url = previewMode ? null : (endpoints?.announcementsUrl ?? null);
59
56
 
60
57
  // MUST be memoized (the hook re-syncs on [initialData] identity — an inline
61
58
  // literal per render would setData-loop) and strict-undefined-mapped:
@@ -157,18 +154,20 @@ export function AnnouncementBar({
157
154
  // dark-toned needs the dark theme's). `.theme-light` / `.theme-dark` are
158
155
  // the design system's own scoping classes (ods-colors.css) and flip every
159
156
  // Tier-1 `--ods-*` primitive for descendants.
160
- const themeScope = pickReadableTextColor(displayAnnouncement.background_color) === 'dark' ? 'theme-light' : 'theme-dark';
157
+ const themeScope =
158
+ pickReadableTextColor(displayAnnouncement.background_color) === 'dark' ? 'theme-light' : 'theme-dark';
161
159
 
162
- // Inside the scope, colors reference Tier-1 theme primitives directly
163
- // (Tier-2 `--color-*` aliases resolve once at :root and do NOT re-resolve
164
- // in a nested theme scope — verified). `--ods-system-greys-white` is the
165
- // theme's primary-text primitive; `--ods-system-greys-black-hover/-action`
166
- // are its quiet hover/active surfaces. Every value comes from the active
167
- // ODS theme; nothing is hardcoded.
160
+ // Inside the scope, colors reference Tier-2 semantic aliases. Aliases
161
+ // declared only at :root would NOT re-resolve in a nested theme scope
162
+ // (they compute at :root), so ods-colors.css re-anchors these three inside
163
+ // `.theme-light` / `.theme-dark` specifically for theme islands like this
164
+ // bar. Every value comes from the active ODS theme; nothing is hardcoded.
168
165
  const barButtonClasses =
169
- 'text-[color:var(--ods-system-greys-white)] hover:bg-[var(--ods-system-greys-black-hover)] active:bg-[var(--ods-system-greys-black-action)]';
166
+ 'text-[color:var(--color-text-primary)] hover:bg-[var(--color-bg-hover)] active:bg-[var(--color-bg-active)]';
170
167
 
171
- const hasCta = Boolean(displayAnnouncement.cta_enabled && displayAnnouncement.cta_url && displayAnnouncement.cta_text);
168
+ const hasCta = Boolean(
169
+ displayAnnouncement.cta_enabled && displayAnnouncement.cta_url && displayAnnouncement.cta_text,
170
+ );
172
171
 
173
172
  return (
174
173
  <div
@@ -184,32 +183,23 @@ export function AnnouncementBar({
184
183
  style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
185
184
  >
186
185
  {/*
187
- Bar anatomy follows the announcement-bar industry standard: ONE line of
188
- text at 13-14px inside a 44px-tall strip (guides converge on 40-60px
189
- with a single sentence; two stacked 18px rows blew past that), title +
190
- description merged inline (description hidden on small screens), ONE
191
- compact CTA on the right, and a ghost-icon dismiss (design-system
192
- icon-sm: 32px target, >= the 24px WCAG 2.2 SC 2.5.8 AA floor).
186
+ Markup is the shared pure view (AnnouncementBarView, Figma
187
+ 9364-40603 / 9418-43969 / 9418-44006): one row from `md` (800px) up,
188
+ stacked below it with the CTA stretched full-width next to the
189
+ content-width dismiss. The CTA Button is now rendered on EVERY
190
+ breakpoint (it replaced the old whole-bar mobile tap target, which
191
+ also retires the 768px window.innerWidth check that disagreed with
192
+ the md:800px breakpoint).
193
193
  */}
194
- <div className={`min-h-0 overflow-hidden ${themeScope}`} style={{ backgroundColor: displayAnnouncement.background_color }}>
195
- <div className="flex items-center w-full max-w-full min-h-11 text-[color:var(--ods-system-greys-white)]">
196
- {/* Mobile: whole-bar tap target (touch-first by design; the CTA
197
- Button below is the keyboard/AT path and is CSS-hidden < md —
198
- known tradeoff carried from the original bar). */}
199
- <div
200
- className={`flex flex-row gap-2 md:gap-3 items-center pl-4 md:pl-6 py-1.5 flex-1 min-w-0 ${
201
- hasCta ? 'md:cursor-default cursor-pointer' : ''
202
- }`}
203
- onClick={(e) => {
204
- // Only handle click on mobile (< 768px) and if CTA is enabled
205
- if (window.innerWidth < 768 && hasCta) {
206
- e.preventDefault();
207
- handleCtaClick();
208
- }
209
- }}
210
- >
211
- {/* ONE unified icon path (shared with the chat): uploaded image URL
212
- wins, else a library glyph by name (+ props), via <EntityIcon>. */}
194
+ <div
195
+ className={`min-h-0 overflow-hidden ${themeScope}`}
196
+ style={{ backgroundColor: displayAnnouncement.background_color }}
197
+ >
198
+ <AnnouncementBarView
199
+ className="text-[color:var(--color-text-primary)]"
200
+ startAdornment={
201
+ /* ONE unified icon path (shared with the chat): uploaded image URL
202
+ wins, else a library glyph by name (+ props), via <EntityIcon>. */
213
203
  <EntityIcon
214
204
  icon={{
215
205
  name: displayAnnouncement.icon_name || 'openframe-logo',
@@ -217,72 +207,74 @@ export function AnnouncementBar({
217
207
  props: displayAnnouncement.icon_props,
218
208
  }}
219
209
  size={24}
220
- className="relative shrink-0 w-5 h-5 md:w-6 md:h-6"
210
+ // `!` is required: logo glyphs (LogoOpenframeIcon / sizedLogo in
211
+ // icon-library) drive their size via an inline style, which beats
212
+ // a plain class — author !important beats the inline style, so
213
+ // the responsive token (16px < md, 24px from md) wins everywhere.
214
+ className="relative !size-[var(--icon-size-icon-size)] shrink-0"
221
215
  />
222
-
223
- {/* Single-line message: bold title + regular description inline,
224
- truncating as one unit. Separator is a middot (house rule: no
225
- en/em dashes in copy). */}
226
- <p className="flex-1 min-w-0 max-w-full text-h6 truncate mb-0">
227
- <span className="font-semibold">{displayAnnouncement.title}</span>
216
+ }
217
+ title={
218
+ /* Single-line message: title + description inline, truncating as
219
+ one unit. Type per the ODS mockup (2862-8391): the "h3 body @
220
+ 500" Figma style maps to the code's `.text-h4` utility (the
221
+ `.text-h3` utility is 700; h3/h4 body share the same size
222
+ tokens) — the same treatment string titles get from the view.
223
+ Description keeps opacity-80 for hierarchy. Separator is a
224
+ middot (house rule: no en/em dashes in copy). */
225
+ <p className="min-w-0 max-w-full text-h4 truncate mb-0">
226
+ <span>{displayAnnouncement.title}</span>
228
227
  {displayAnnouncement.description && (
229
228
  <span className="hidden sm:inline opacity-80"> · {displayAnnouncement.description}</span>
230
229
  )}
231
230
  </p>
231
+ }
232
+ actionBlock={
233
+ /* CTA - the common Button carrying the ADMIN-CONFIGURED colors
234
+ (cta_button_background_color / cta_button_text_color are an
235
+ admin FEATURE, defaults from ANNOUNCEMENT_CTA_DEFAULTS —
236
+ announcement colors are data, not token surfaces). Inline
237
+ styles win over the variant's hover classes on every state,
238
+ so hover feedback is opacity (the bar's original treatment);
239
+ nothing can render dark-on-dark.
232
240
 
233
- {/* CTA - the common Button carrying the ADMIN-CONFIGURED colors
234
- (cta_button_background_color / cta_button_text_color are an
235
- admin FEATURE, defaults from ANNOUNCEMENT_CTA_DEFAULTS —
236
- announcement colors are data, not token surfaces). Inline
237
- styles win over the variant's hover classes on every state,
238
- so hover feedback is opacity (the bar's original treatment);
239
- nothing can render dark-on-dark. Hidden on mobile, where the
240
- whole bar is the tap target.
241
-
242
- Geometry + type come from the design system's size="compact"
243
- (24px caption-scale pill for slim strips — rationale documented
244
- on the variant in button.tsx). */}
245
- {hasCta && displayAnnouncement.cta_text && (
246
- <div className="hidden md:flex flex-shrink-0 ml-2 md:ml-4">
247
- <Button
248
- onClick={handleCtaClick}
249
- variant="outline"
250
- size="compact"
251
- className="transition-opacity hover:opacity-90"
252
- style={{
253
- backgroundColor: displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,
254
- color: displayAnnouncement.cta_button_text_color || ANNOUNCEMENT_CTA_DEFAULTS.text,
255
- borderColor: displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,
256
- }}
257
- tabIndex={expanded ? 0 : -1}
258
- leftIcon={
259
- displayAnnouncement.cta_show_icon && displayAnnouncement.cta_icon_name
260
- ? (
261
- <EntityIcon
262
- icon={{ name: displayAnnouncement.cta_icon_name, props: displayAnnouncement.cta_icon_props }}
263
- size={14}
264
- className="w-3.5 h-3.5"
265
- />
266
- )
267
- : undefined
268
- }
269
- >
270
- {displayAnnouncement.cta_text}
271
- </Button>
272
- </div>
273
- )}
274
- </div>
275
-
276
- {/* Dismiss - the common Button in its ghost-icon treatment
277
- (size="icon-sm": 32px target, >= the 24px WCAG 2.5.8 AA floor,
278
- 16px glyph) with the bar's quiet tint hover. Inert in
279
- previewMode. */}
280
- <div className="flex-shrink-0 ml-1 md:ml-2 mr-2 md:mr-4">
241
+ Geometry + type come from the design system's size="compact"
242
+ (24px caption-scale pill for slim strips — rationale documented
243
+ on the variant in button.tsx). */
244
+ hasCta && displayAnnouncement.cta_text ? (
245
+ <Button
246
+ onClick={handleCtaClick}
247
+ variant="outline"
248
+ size="compact"
249
+ className="transition-opacity hover:opacity-90"
250
+ style={{
251
+ backgroundColor:
252
+ displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,
253
+ color: displayAnnouncement.cta_button_text_color || ANNOUNCEMENT_CTA_DEFAULTS.text,
254
+ borderColor: displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,
255
+ }}
256
+ tabIndex={expanded ? 0 : -1}
257
+ leftIcon={
258
+ displayAnnouncement.cta_show_icon && displayAnnouncement.cta_icon_name ? (
259
+ <EntityIcon
260
+ icon={{ name: displayAnnouncement.cta_icon_name, props: displayAnnouncement.cta_icon_props }}
261
+ size={14}
262
+ className="w-3.5 h-3.5"
263
+ />
264
+ ) : undefined
265
+ }
266
+ >
267
+ {displayAnnouncement.cta_text}
268
+ </Button>
269
+ ) : undefined
270
+ }
271
+ endAdornment={
272
+ /* Dismiss - the common Button in its ghost-icon treatment
273
+ (size="icon-sm": 32px target, >= the 24px WCAG 2.5.8 AA floor,
274
+ 16px glyph) with the bar's quiet tint hover. Inert in
275
+ previewMode. */
281
276
  <Button
282
- onClick={(e) => {
283
- e.stopPropagation(); // Prevent triggering the mobile CTA click
284
- handleDismiss();
285
- }}
277
+ onClick={handleDismiss}
286
278
  variant="transparent"
287
279
  size="icon-sm"
288
280
  className={barButtonClasses}
@@ -292,8 +284,8 @@ export function AnnouncementBar({
292
284
  >
293
285
  <X strokeWidth={2} />
294
286
  </Button>
295
- </div>
296
- </div>
287
+ }
288
+ />
297
289
  </div>
298
290
  </div>
299
291
  );
@@ -4,6 +4,7 @@ import * as React from 'react'
4
4
  import { ChatFooter } from './chat-container'
5
5
  import { ChatInput } from './chat-input'
6
6
  import { ChatAttachmentAddButton } from './chat-attachment-bar'
7
+ import { ChatContextMemoryBar } from './chat-context-memory-bar'
7
8
  import {
8
9
  ChatComposerPlusMenu,
9
10
  ChatContextChipStrip,
@@ -11,6 +12,7 @@ import {
11
12
  } from './chat-context-picker'
12
13
  import { ModelDisplay } from './model-display'
13
14
  import { BoxArchiveIcon } from '../icons-v2-generated'
15
+ import { cn } from '../../utils/cn'
14
16
  import type { ChatInputRef, ModelDisplayProps } from './types/component.types'
15
17
  import type {
16
18
  ChatContextItem,
@@ -49,6 +51,13 @@ export interface ChatComposerProps {
49
51
  onToggleContextItem?: (item: ChatContextItem) => void
50
52
  /** Remove an item (chip ×). */
51
53
  onRemoveContextItem?: (item: ChatContextItem) => void
54
+ /** Ambient CONTEXT MEMORY (Figma 271:38656) — the entities the host collected
55
+ * from the user's navigation history and sends with every message. Rendered
56
+ * as a summary strip at the top of the composer card, above the assigned-item
57
+ * chips. Empty/omitted → no strip. */
58
+ contextMemoryItems?: ChatContextItem[]
59
+ /** Drop one entity from the context memory (dropdown row ×). */
60
+ onRemoveContextMemoryItem?: (item: ChatContextItem) => void
52
61
  /** Controlled picker open state. */
53
62
  contextPickerOpen?: boolean
54
63
  /** Open the picker (the `+` menu's "Assign Item"). */
@@ -94,6 +103,8 @@ export function ChatComposer({
94
103
  selectedContextItems,
95
104
  onToggleContextItem,
96
105
  onRemoveContextItem,
106
+ contextMemoryItems,
107
+ onRemoveContextMemoryItem,
97
108
  contextPickerOpen = false,
98
109
  onOpenContextPicker,
99
110
  onCloseContextPicker,
@@ -104,6 +115,7 @@ export function ChatComposer({
104
115
  }: ChatComposerProps) {
105
116
  const contextEnabled = !!contextPicker && !archived
106
117
  const selected = selectedContextItems ?? []
118
+ const memory = contextMemoryItems ?? []
107
119
 
108
120
  // type → icon lookup so the chips (composer + bubble) lead with their
109
121
  // entity-type glyph without the parent threading an extra resolver. Keyed on
@@ -184,23 +196,38 @@ export function ChatComposer({
184
196
  </p>
185
197
  </div>
186
198
  ) : contextEnabled ? (
187
- // Unified card (Figma 1:6073): a context-chip header strip
188
- // (`bg-ods-bg`, `border-b`) sits flush above the input; the card
189
- // owns the border / radius / focus ring. No `overflow-hidden` so the
190
- // picker popover can float above the input (chips get `rounded-t`).
199
+ // Unified card (Figma 1:6073 / 271:38656): the context-memory
200
+ // summary strip and the assigned-chip strip (both `bg-ods-bg`,
201
+ // `border-b`) stack flush above the input; the card owns the border
202
+ // / radius / focus ring. No `overflow-hidden` so the picker and the
203
+ // memory popover can float above the input (whichever strip comes
204
+ // first gets `rounded-t`).
191
205
  // `has-[[data-editor]:focus]` (not `focus-within`) so the accent
192
206
  // border reacts ONLY to the composer editor — not the picker's search
193
207
  // input or item buttons, which also live inside this card. The editor
194
208
  // is a `contentEditable` div (`data-editor`), not a `<textarea>`, so we
195
209
  // target its data attribute rather than the element name.
196
210
  <div className="rounded-md border border-ods-border bg-ods-card transition-colors has-[[data-editor]:focus]:border-ods-accent">
211
+ {/* Ambient navigation-history context — always the same items, so
212
+ it sits furthest from the textarea. Self-hides when empty. */}
213
+ <ChatContextMemoryBar
214
+ items={memory}
215
+ onRemove={onRemoveContextMemoryItem}
216
+ resolveIcon={resolveContextIcon}
217
+ disabled={sending}
218
+ className="rounded-t-md"
219
+ />
197
220
  {selected.length > 0 && (
198
221
  <ChatContextChipStrip
199
222
  items={selected}
200
223
  onRemove={onRemoveContextItem}
201
224
  resolveIcon={resolveContextIcon}
202
225
  disabled={sending}
203
- className="rounded-t-md border-b border-ods-border bg-ods-bg p-2"
226
+ className={cn(
227
+ 'border-b border-ods-border bg-ods-bg p-2',
228
+ // Only the TOP strip rounds into the card corner.
229
+ memory.length === 0 && 'rounded-t-md',
230
+ )}
204
231
  />
205
232
  )}
206
233
  {/* Picker is rendered inside the `+` button (its `dropdown` prop)
@@ -0,0 +1,159 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * `<ChatContextMemoryBar>` — the composer's CONTEXT MEMORY strip
5
+ * (Figma node 271:38656).
6
+ *
7
+ * A one-line summary row pinned to the top of the composer card ("5 recently
8
+ * viewed items in context") with a `⋯` trigger that opens a dropdown listing
9
+ * every remembered entity, each removable via its `×`.
10
+ *
11
+ * This is the AMBIENT half of the composer's context UI — the entities the host
12
+ * accumulated from the user's navigation history and sends along with every
13
+ * message — as opposed to `<ChatContextChipStrip>`, which shows the items the
14
+ * user ASSIGNED to the message they're typing. Both sit above the input; memory
15
+ * on top (passive, always the same items), assigned chips closest to the
16
+ * textarea (active selection for this message).
17
+ *
18
+ * The lib owns only the chrome: items and removal are host-fed
19
+ * (`EmbeddableChat`'s `contextMemory` prop), and lead glyphs come from the
20
+ * picker's entity-type icon map via `resolveIcon`. Renders `null` when the host
21
+ * has nothing remembered, so it has no permanent footprint.
22
+ */
23
+
24
+ import type * as React from 'react'
25
+ import { useEffect, useRef, useState } from 'react'
26
+ import { Ellipsis01Icon } from '../icons-v2-generated/interface/ellipsis-01-icon'
27
+ import { XmarkCircleIcon } from '../icons-v2-generated/signs-and-symbols/xmark-circle-icon'
28
+ import { cn } from '../../utils/cn'
29
+ import { CONTEXT_ICON_CLASS, CONTEXT_LABEL_CLASS, CONTEXT_ROW_CLASS } from './context-items-list'
30
+ import type { ChatContextItem } from './types/context-item.types'
31
+
32
+ const itemKey = (item: { type: string; id: string }) => `${item.type}:${item.id}`
33
+
34
+ export interface ChatContextMemoryBarProps {
35
+ /** Remembered entities, most-recent-first. Empty → the bar renders nothing. */
36
+ items: ChatContextItem[]
37
+ /** Drop one entity from the host's memory (dropdown row `×`). When omitted the
38
+ * list is read-only. */
39
+ onRemove?: (item: ChatContextItem) => void
40
+ /** Lead-glyph resolver — the composer passes its entity-type icon map, so the
41
+ * rows match the picker and the assigned chips. */
42
+ resolveIcon?: (item: ChatContextItem) => React.ReactNode
43
+ /** External disable (e.g. while a message is streaming). */
44
+ disabled?: boolean
45
+ className?: string
46
+ }
47
+
48
+ export function ChatContextMemoryBar({
49
+ items,
50
+ onRemove,
51
+ resolveIcon,
52
+ disabled = false,
53
+ className,
54
+ }: ChatContextMemoryBarProps) {
55
+ const [open, setOpen] = useState(false)
56
+ const rootRef = useRef<HTMLDivElement>(null)
57
+
58
+ // Removing the last remembered entity unmounts the bar; close first so a
59
+ // stale popover can't outlive its trigger.
60
+ useEffect(() => {
61
+ if (items.length === 0) setOpen(false)
62
+ }, [items.length])
63
+
64
+ // Outside-press close. Registered only while open. The root wraps BOTH the
65
+ // trigger and the popover, so a press on the `⋯` (inside root) is ignored here
66
+ // and handled by the button's own toggle.
67
+ //
68
+ // CAPTURE phase (the `true` 3rd arg): the chat lives inside a resizable drawer
69
+ // whose panel captures pointer events and whose composer sits on a
70
+ // `contentEditable`; a bubble-phase `document` listener can be pre-empted by
71
+ // an ancestor's `stopPropagation`, which is why a press outside the popover
72
+ // wasn't closing it. Capture runs top-down before any of that, so the handler
73
+ // always fires. `touchstart` covers tap-to-dismiss on touch devices.
74
+ useEffect(() => {
75
+ if (!open) return
76
+ const onOutside = (e: Event) => {
77
+ if (rootRef.current?.contains(e.target as Node)) return
78
+ setOpen(false)
79
+ }
80
+ document.addEventListener('mousedown', onOutside, true)
81
+ document.addEventListener('touchstart', onOutside, true)
82
+ return () => {
83
+ document.removeEventListener('mousedown', onOutside, true)
84
+ document.removeEventListener('touchstart', onOutside, true)
85
+ }
86
+ }, [open])
87
+
88
+ if (items.length === 0) return null
89
+
90
+ return (
91
+ // `relative` anchors the popover to this strip; `bottom-full` floats it
92
+ // above, matching the context picker's placement over the input.
93
+ <div
94
+ ref={rootRef}
95
+ className={cn(
96
+ 'relative flex items-center gap-[var(--spacing-system-xs)] border-b border-ods-border bg-ods-bg p-[var(--spacing-system-xs)]',
97
+ className,
98
+ )}
99
+ onKeyDown={e => {
100
+ if (e.key === 'Escape' && open) {
101
+ e.preventDefault()
102
+ setOpen(false)
103
+ }
104
+ }}
105
+ >
106
+ <p className="min-w-0 flex-1 truncate text-h6 text-ods-text-secondary">
107
+ {items.length} recently viewed {items.length === 1 ? 'item' : 'items'} in context
108
+ </p>
109
+
110
+ <button
111
+ type="button"
112
+ aria-label="Show remembered context"
113
+ aria-expanded={open}
114
+ title="Show remembered context"
115
+ onClick={() => setOpen(o => !o)}
116
+ className={cn(
117
+ 'flex size-4 shrink-0 items-center justify-center rounded outline-none transition-colors [&_svg]:size-4',
118
+ open ? 'text-ods-text-primary' : 'text-ods-text-secondary hover:text-ods-text-primary',
119
+ 'focus-visible:text-ods-text-primary',
120
+ )}
121
+ >
122
+ <Ellipsis01Icon size={16} />
123
+ </button>
124
+
125
+ {open && (
126
+ <div
127
+ role="list"
128
+ aria-label="Remembered context"
129
+ // Mirrors `ChatContextPicker`'s popover chrome (border / card bg /
130
+ // shadow-md) and its scroll cap, so both dropdowns over the composer
131
+ // read as one component family.
132
+ className="absolute bottom-full right-0 z-50 mb-1 max-h-[min(340px,45vh)] w-[320px] max-w-[90vw] overflow-y-auto overscroll-contain rounded-md border border-ods-border bg-ods-card text-ods-text-primary shadow-md"
133
+ >
134
+ {items.map(item => (
135
+ // Not a `<button>`: the row itself is inert (only the `×` acts), so
136
+ // the remove control isn't nested inside another button.
137
+ <div key={itemKey(item)} role="listitem" className={CONTEXT_ROW_CLASS}>
138
+ {resolveIcon && <span className={CONTEXT_ICON_CLASS}>{resolveIcon(item)}</span>}
139
+ <span className={CONTEXT_LABEL_CLASS} title={item.label}>
140
+ {item.label}
141
+ </span>
142
+ {onRemove && (
143
+ <button
144
+ type="button"
145
+ onClick={() => onRemove(item)}
146
+ disabled={disabled}
147
+ aria-label={`Remove ${item.label} from context`}
148
+ className="flex size-4 shrink-0 items-center justify-center text-ods-text-secondary outline-none transition-colors hover:text-ods-text-primary focus-visible:text-ods-text-primary disabled:cursor-not-allowed disabled:opacity-40 md:size-6"
149
+ >
150
+ <XmarkCircleIcon className="size-4 shrink-0 md:size-6" />
151
+ </button>
152
+ )}
153
+ </div>
154
+ ))}
155
+ </div>
156
+ )}
157
+ </div>
158
+ )
159
+ }
@@ -379,13 +379,21 @@ export interface EmbeddableChatProps {
379
379
  */
380
380
  onGuidePromptConsumed?: () => void
381
381
  /**
382
- * Host-rendered banner shown as a full-bleed row directly under the panel
383
- * header in MINGO mode only (Figma 192:51006) — e.g. a "current page context"
384
- * tag naming the entity whose detail page the user is viewing. The host owns
385
- * the data + click; pass `null`/return `null` to render nothing. Mirrors the
386
- * built-in `GuideModeBanner`, which fills the same slot in Guide mode.
382
+ * CONTEXT MEMORY (Figma 271:38656) — the entities the host collected from the
383
+ * user's navigation history and rides out on every message. Rendered as a
384
+ * summary strip at the top of the composer card ("N recently viewed items in
385
+ * context") with a `⋯` dropdown listing them all, each removable via
386
+ * `onRemove`. Replaces the old under-the-header "current page context" banner.
387
+ *
388
+ * Host-owned data (it lives in the host's navigation store); the lib only
389
+ * places and styles it, resolving lead glyphs from `contextPicker.entityTypes`.
390
+ * Shown only alongside an active `contextPicker` (i.e. Mingo mode); an empty
391
+ * `items` array renders nothing. Keep the object identity stable (`useMemo`).
387
392
  */
388
- mingoContextBanner?: React.ReactNode
393
+ contextMemory?: {
394
+ items: ChatContextItem[]
395
+ onRemove?: (item: ChatContextItem) => void
396
+ }
389
397
  }
390
398
 
391
399
  // =============================================================================
@@ -918,7 +926,7 @@ function EmbeddableChatInner({
918
926
  renderContextItem,
919
927
  guidePendingPrompt,
920
928
  onGuidePromptConsumed,
921
- mingoContextBanner,
929
+ contextMemory,
922
930
  }: EmbeddableChatProps) {
923
931
  // `shell === 'none'` means the consumer hosts us inside their own panel
924
932
  // (e.g. AppLayoutDrawer in openframe-frontend). Several drawer-shell
@@ -2197,14 +2205,6 @@ function EmbeddableChatInner({
2197
2205
  <GuideModeBanner className="animate-in fade-in-0 duration-200" />
2198
2206
  )}
2199
2207
 
2200
- {/* Mingo-mode current-page context banner (Figma 192:51006) —
2201
- full-bleed row under the header naming the entity whose detail
2202
- page the user is currently viewing, so they can ask Mingo to
2203
- recall it. Host-rendered (it reads the host's navigation-context
2204
- store) and host-gated to "has an open view"; this slot just
2205
- places it, mirroring `GuideModeBanner` above. */}
2206
- {activeMode === 'mingo' && mingoContextBanner}
2207
-
2208
2208
  {/* Chat-panel row. In the wide Mingo layout (`splitActive`) the
2209
2209
  dialog history is hoisted into a fixed 320px "Current Chats"
2210
2210
  rail on the left; the stacked layout keeps it inline in the
@@ -2532,6 +2532,8 @@ function EmbeddableChatInner({
2532
2532
  selectedContextItems={contextItems}
2533
2533
  onToggleContextItem={toggleContextItem}
2534
2534
  onRemoveContextItem={removeContextItem}
2535
+ contextMemoryItems={contextMemory?.items}
2536
+ onRemoveContextMemoryItem={contextMemory?.onRemove}
2535
2537
  contextPickerOpen={contextPickerOpen}
2536
2538
  onOpenContextPicker={openContextPicker}
2537
2539
  onCloseContextPicker={closeContextPicker}
@@ -37,6 +37,7 @@ export * from './chat-header-search-field'
37
37
  export * from './chat-panel-header'
38
38
  export * from './chat-composer'
39
39
  export * from './chat-context-picker'
40
+ export * from './chat-context-memory-bar'
40
41
  export * from './context-items-list'
41
42
  export * from './chat-archive-page'
42
43
  export * from './model-display'