@flamingo-stack/openframe-frontend-core 0.0.736 → 0.0.738

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 (189) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{chunk-AZL332TK.cjs → chunk-2QMBXGMQ.cjs} +15 -2
  3. package/dist/{chunk-7V2KP2TB.cjs → chunk-2SBHPHLS.cjs} +4 -4
  4. package/dist/{chunk-LIH5OQLY.cjs → chunk-2UZS3ZH7.cjs} +8 -8
  5. package/dist/{chunk-MGW5DO6P.cjs → chunk-3BBOAMWJ.cjs} +7 -7
  6. package/dist/{chunk-HZJRERJW.cjs → chunk-3RFQPQ7Z.cjs} +3 -3
  7. package/dist/{chunk-T27CVQBO.cjs → chunk-4T6ETBL4.cjs} +151 -83
  8. package/dist/{chunk-JM54CNZZ.cjs → chunk-4YQNJJWM.cjs} +95 -95
  9. package/dist/{chunk-UBQSOB3X.cjs → chunk-5BKAUXLR.cjs} +4 -4
  10. package/dist/{chunk-YADG5Q7C.cjs → chunk-63N4FN5V.cjs} +3 -3
  11. package/dist/{chunk-URX2YTCL.cjs → chunk-66R4NGGU.cjs} +77 -71
  12. package/dist/{chunk-EJOXXJJB.cjs → chunk-7F3V45AH.cjs} +29 -29
  13. package/dist/{chunk-BDF3FNOC.cjs → chunk-7GETNFZ5.cjs} +49 -49
  14. package/dist/{chunk-L4ZNR5V7.cjs → chunk-7LTG6CJ3.cjs} +67 -67
  15. package/dist/{chunk-MJGX357L.cjs → chunk-B27QEHYR.cjs} +2 -2
  16. package/dist/{chunk-OV6BGMEL.cjs → chunk-BRKTN6UR.cjs} +340 -328
  17. package/dist/{chunk-VRNNIV5Y.cjs → chunk-EG545WGV.cjs} +4 -4
  18. package/dist/{chunk-ZJNYWNCB.cjs → chunk-FG7HTGT3.cjs} +24 -24
  19. package/dist/{chunk-77SD7RXY.cjs → chunk-GAF4L3JW.cjs} +7 -7
  20. package/dist/{chunk-FTGVG6FW.cjs → chunk-GQZOUVXC.cjs} +34 -34
  21. package/dist/{chunk-ZEX5WU35.cjs → chunk-HNYLTEIS.cjs} +48 -69
  22. package/dist/{chunk-FCGLSWKR.cjs → chunk-IFS4AH77.cjs} +19 -1
  23. package/dist/{chunk-L535OD7V.cjs → chunk-JZNESLJX.cjs} +2 -2
  24. package/dist/{chunk-SZFA2LQR.cjs → chunk-KQ4IKALJ.cjs} +35 -35
  25. package/dist/{chunk-EX2GYIGW.cjs → chunk-LY5VOKV7.cjs} +6 -6
  26. package/dist/{chunk-XLSVQJBA.cjs → chunk-MLKL2YVE.cjs} +44 -44
  27. package/dist/{chunk-BL5GOCHP.cjs → chunk-MTNNYSP3.cjs} +26 -18
  28. package/dist/{chunk-ZRYKDN2J.cjs → chunk-N2CISBJJ.cjs} +15 -15
  29. package/dist/chunk-N5NDPULP.cjs +57 -0
  30. package/dist/{chunk-P6Z3EBPJ.cjs → chunk-R36NZTUJ.cjs} +26 -26
  31. package/dist/{chunk-OJUMVOMP.cjs → chunk-U5BRSRXA.cjs} +923 -759
  32. package/dist/{chunk-DUXLZPMJ.cjs → chunk-UMO55S55.cjs} +2 -2
  33. package/dist/{chunk-JGGSRCRD.cjs → chunk-XGHZIYQN.cjs} +24 -24
  34. package/dist/{chunk-NONQUGRP.cjs → chunk-XMNLPQ75.cjs} +12 -12
  35. package/dist/{cloud-tenants-M55HMW6F.cjs → cloud-tenants-FTQGWXHS.cjs} +16 -16
  36. package/dist/components/agent-identity.d.ts +8 -1
  37. package/dist/components/agent-identity.d.ts.map +1 -1
  38. package/dist/components/agent-identity.js +6 -1
  39. package/dist/components/calendar.cjs +4 -4
  40. package/dist/components/case-studies/index.cjs +28 -28
  41. package/dist/components/chat/chat-turn.d.ts +23 -0
  42. package/dist/components/chat/chat-turn.d.ts.map +1 -0
  43. package/dist/components/chat/chat-turn.js +30 -0
  44. package/dist/components/chat/entity-cards/index.d.ts +1 -0
  45. package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
  46. package/dist/components/chat/entity-cards/index.js +2 -0
  47. package/dist/components/chat/hooks/use-ask-prompts.d.ts +1 -1
  48. package/dist/components/chat/hooks/use-ask-prompts.js +1 -1
  49. package/dist/components/chat/index.cjs +16 -12
  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 +1 -0
  53. package/dist/components/chat/mingo-info-card.d.ts +17 -1
  54. package/dist/components/chat/mingo-info-card.d.ts.map +1 -1
  55. package/dist/components/chat/mingo-info-card.js +15 -3
  56. package/dist/components/contact/index.cjs +17 -17
  57. package/dist/components/docs/index.cjs +20 -20
  58. package/dist/components/embeds/index.cjs +17 -17
  59. package/dist/components/faq/faq-ask-card.d.ts +5 -4
  60. package/dist/components/faq/faq-ask-card.d.ts.map +1 -1
  61. package/dist/components/faq/faq-document-page.d.ts +3 -1
  62. package/dist/components/faq/faq-document-page.d.ts.map +1 -1
  63. package/dist/components/faq/faq-document-page.js +2 -0
  64. package/dist/components/faq/faq-section.d.ts +37 -5
  65. package/dist/components/faq/faq-section.d.ts.map +1 -1
  66. package/dist/components/faq/faq-section.js +36 -9
  67. package/dist/components/faq/index.cjs +23 -17
  68. package/dist/components/faq/index.d.ts +1 -1
  69. package/dist/components/faq/index.d.ts.map +1 -1
  70. package/dist/components/faq/index.js +4 -2
  71. package/dist/components/features/auth/auth-benefits-panel.d.ts +3 -5
  72. package/dist/components/features/auth/auth-benefits-panel.d.ts.map +1 -1
  73. package/dist/components/features/auth/auth-benefits-panel.js +3 -19
  74. package/dist/components/features/conversation-card.d.ts +12 -4
  75. package/dist/components/features/conversation-card.d.ts.map +1 -1
  76. package/dist/components/features/conversation-card.js +30 -38
  77. package/dist/components/features/index.cjs +16 -16
  78. package/dist/components/help-center-pages/delivery-page.d.ts +8 -1
  79. package/dist/components/help-center-pages/delivery-page.d.ts.map +1 -1
  80. package/dist/components/help-center-pages/delivery-page.js +16 -4
  81. package/dist/components/help-center-pages/download-apps-page.d.ts +8 -1
  82. package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -1
  83. package/dist/components/help-center-pages/download-apps-page.js +11 -6
  84. package/dist/components/help-center-pages/index.cjs +216 -161
  85. package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts +8 -1
  86. package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts.map +1 -1
  87. package/dist/components/help-center-pages/onboarding-guides-catalog-page.js +26 -14
  88. package/dist/components/help-center-pages/product-releases-list-page.d.ts +8 -1
  89. package/dist/components/help-center-pages/product-releases-list-page.d.ts.map +1 -1
  90. package/dist/components/help-center-pages/product-releases-list-page.js +24 -12
  91. package/dist/components/help-center-pages/roadmap-page.d.ts +8 -1
  92. package/dist/components/help-center-pages/roadmap-page.d.ts.map +1 -1
  93. package/dist/components/help-center-pages/roadmap-page.js +25 -13
  94. package/dist/components/index.cjs +103 -79
  95. package/dist/components/meeting-scheduler/index.cjs +49 -49
  96. package/dist/components/navigation/index.cjs +19 -19
  97. package/dist/components/onboarding-guides/index.cjs +19 -19
  98. package/dist/components/platform/ToolBadge.d.ts.map +1 -1
  99. package/dist/components/platform/ToolBadge.js +4 -3
  100. package/dist/components/product-demo/index.cjs +8 -8
  101. package/dist/components/related-content/index.cjs +8 -8
  102. package/dist/components/shared/dev-section/dev-section-page.d.ts +8 -1
  103. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
  104. package/dist/components/shared/dev-section/dev-section-page.js +8 -3
  105. package/dist/components/shared/legal-document/legal-document-page.d.ts +8 -1
  106. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
  107. package/dist/components/shared/legal-document/legal-document-page.js +4 -1
  108. package/dist/components/tickets/index.cjs +22 -21
  109. package/dist/components/ui/app-window-frame.d.ts +7 -1
  110. package/dist/components/ui/app-window-frame.d.ts.map +1 -1
  111. package/dist/components/ui/app-window-frame.js +10 -2
  112. package/dist/components/ui/benefit-list.d.ts +42 -0
  113. package/dist/components/ui/benefit-list.d.ts.map +1 -0
  114. package/dist/components/ui/benefit-list.js +62 -0
  115. package/dist/components/ui/button/button.d.ts +1 -0
  116. package/dist/components/ui/button/button.d.ts.map +1 -1
  117. package/dist/components/ui/button/button.js +15 -2
  118. package/dist/components/ui/dropdown-button.d.ts +20 -2
  119. package/dist/components/ui/dropdown-button.d.ts.map +1 -1
  120. package/dist/components/ui/dropdown-button.js +57 -1
  121. package/dist/components/ui/file-manager/index.cjs +11 -11
  122. package/dist/components/ui/index.cjs +32 -16
  123. package/dist/components/ui/index.d.ts +3 -0
  124. package/dist/components/ui/index.d.ts.map +1 -1
  125. package/dist/components/ui/index.js +3 -0
  126. package/dist/components/ui/leader-row.d.ts +17 -0
  127. package/dist/components/ui/leader-row.d.ts.map +1 -0
  128. package/dist/components/ui/leader-row.js +14 -0
  129. package/dist/components/ui/strip-card.d.ts +23 -0
  130. package/dist/components/ui/strip-card.d.ts.map +1 -0
  131. package/dist/components/ui/strip-card.js +56 -0
  132. package/dist/contexts/assistant-runtime-context.d.ts +5 -5
  133. package/dist/{devices-WWJZFG6E.cjs → devices-MPI5HUQS.cjs} +17 -17
  134. package/dist/{embeddable-chat-76IJ6TQZ.cjs → embeddable-chat-7WLUDBGK.cjs} +84 -84
  135. package/dist/index.cjs +46 -20
  136. package/dist/{knowledge-F4YFQT3G.cjs → knowledge-BS663XXL.cjs} +17 -17
  137. package/dist/{logs-GAYEGEZM.cjs → logs-KS2A2VN5.cjs} +25 -25
  138. package/dist/{policies-ZOVJCJWX.cjs → policies-H3OK6H4Q.cjs} +20 -20
  139. package/dist/{remote-session-Z4YZFG5E.cjs → remote-session-EHKXU4HI.cjs} +7 -7
  140. package/dist/{software-update-H4YE5GMJ.cjs → software-update-HMMMWIL3.cjs} +20 -20
  141. package/dist/{tickets-board-JOSYLOS7.cjs → tickets-board-3YJKOOZE.cjs} +10 -10
  142. package/dist/types/faq.d.ts +17 -0
  143. package/dist/types/faq.d.ts.map +1 -1
  144. package/dist/types/faq.js +4 -0
  145. package/dist/types/index.cjs +4 -0
  146. package/dist/utils/demo-clock.d.ts +22 -0
  147. package/dist/utils/demo-clock.d.ts.map +1 -0
  148. package/dist/utils/demo-clock.js +40 -0
  149. package/dist/utils/identity-tint.d.ts +13 -0
  150. package/dist/utils/identity-tint.d.ts.map +1 -0
  151. package/dist/utils/identity-tint.js +14 -0
  152. package/dist/utils/index.cjs +54 -0
  153. package/dist/utils/index.d.ts +2 -0
  154. package/dist/utils/index.d.ts.map +1 -1
  155. package/dist/utils/index.js +2 -0
  156. package/dist/{video-file-player-6NWGXZ3W.cjs → video-file-player-ZH2ILAPO.cjs} +12 -12
  157. package/package.json +1 -1
  158. package/src/components/agent-identity.tsx +12 -1
  159. package/src/components/chat/chat-turn.tsx +52 -0
  160. package/src/components/chat/entity-cards/index.ts +1 -0
  161. package/src/components/chat/hooks/use-ask-prompts.ts +2 -2
  162. package/src/components/chat/index.ts +1 -0
  163. package/src/components/chat/mingo-info-card.tsx +39 -4
  164. package/src/components/faq/faq-ask-card.tsx +5 -4
  165. package/src/components/faq/faq-document-page.tsx +4 -0
  166. package/src/components/faq/faq-section.tsx +86 -14
  167. package/src/components/faq/index.ts +1 -1
  168. package/src/components/features/auth/auth-benefits-panel.tsx +5 -32
  169. package/src/components/features/conversation-card.tsx +48 -46
  170. package/src/components/help-center-pages/delivery-page.tsx +16 -1
  171. package/src/components/help-center-pages/download-apps-page.tsx +9 -0
  172. package/src/components/help-center-pages/onboarding-guides-catalog-page.tsx +16 -1
  173. package/src/components/help-center-pages/product-releases-list-page.tsx +16 -1
  174. package/src/components/help-center-pages/roadmap-page.tsx +16 -1
  175. package/src/components/platform/ToolBadge.tsx +4 -3
  176. package/src/components/shared/dev-section/dev-section-page.tsx +9 -0
  177. package/src/components/shared/legal-document/legal-document-page.tsx +9 -0
  178. package/src/components/ui/app-window-frame.tsx +18 -2
  179. package/src/components/ui/benefit-list.tsx +97 -0
  180. package/src/components/ui/button/button.tsx +14 -1
  181. package/src/components/ui/dropdown-button.tsx +73 -1
  182. package/src/components/ui/index.ts +3 -0
  183. package/src/components/ui/leader-row.tsx +28 -0
  184. package/src/components/ui/strip-card.tsx +68 -0
  185. package/src/contexts/assistant-runtime-context.tsx +5 -5
  186. package/src/types/faq.ts +18 -0
  187. package/src/utils/demo-clock.ts +62 -0
  188. package/src/utils/identity-tint.ts +23 -0
  189. package/src/utils/index.ts +2 -0
@@ -27,6 +27,15 @@ export interface FaqSectionProps {
27
27
  /** Both required together for entity-attached FAQs; partial → bare /api/faqs. */
28
28
  entityType?: string;
29
29
  entityId?: number | string;
30
+ /**
31
+ * What the page this FAQ sits on is about: ANY string the host's FAQ
32
+ * endpoint knows. ONE topic for the whole block: the questions are the FAQs
33
+ * written for it (`/api/faqs?topic=`; `initialFaqs` are then the host's read
34
+ * of the same topic) and the "ask the assistant" card offers the questions
35
+ * written for it. Whoever renders the FAQ states it; the lib names none.
36
+ * Ignored for the questions when the FAQ is attached to an entity.
37
+ */
38
+ topic?: string;
30
39
  /**
31
40
  * Heading node above the grouped list. `undefined` → default
32
41
  * `<h2>`"Frequently Asked Questions". `null` → no heading (the host page
@@ -43,6 +52,14 @@ export interface FaqSectionProps {
43
52
  /** Overrides for the JSON-LD's name/description/url. */
44
53
  jsonLd?: FaqSchemaOptions;
45
54
  className?: string;
55
+ /** The `id` of the rendered section: what an in-page link to the FAQ points at (`PAGE_FAQ_SECTION_ID` for a page's own block). */
56
+ id?: string;
57
+ /**
58
+ * Print the topic's bare name in small muted text under the heading
59
+ * (`pricing`), so whoever edits the FAQs can see which topic a page asks
60
+ * for. Default off. The section always carries it as `data-faq-topic` too.
61
+ */
62
+ showTopic?: boolean;
46
63
  /** Maps to /api/faqs `?count=` (the 5-tier fill target). Absent → param
47
64
  * not sent (server default applies). */
48
65
  minResults?: number;
@@ -60,10 +77,10 @@ export interface FaqSectionProps {
60
77
  * The "ask the assistant" card beside the questions. Shown wherever the
61
78
  * host's assistant runtime says a chat is there to open
62
79
  * (`AssistantRuntimeContext`: its name, glyph, questions endpoint and
63
- * opener) AND the FAQ has a topic: `ask.topic` (any string the host's
64
- * questions endpoint knows), else the `entityType` the FAQ is attached to.
65
- * No runtime or no topic: the FAQ is the single column it always was; the
66
- * lib names no topic of its own. `false` switches the card off; the object
80
+ * opener) AND the FAQ has a topic: the block's `topic`, else the
81
+ * `entityType` the FAQ is attached to (`ask.topic` states another one for
82
+ * the card alone). No runtime or no topic: the FAQ is the single column it
83
+ * always was; the lib names no topic of its own. `false` switches the card off; the object
67
84
  * also states the questions to leave out, the wording and the chat it opens
68
85
  * (`onOpen`). An `aside` takes the card's place.
69
86
  */
@@ -72,11 +89,47 @@ export interface FaqSectionProps {
72
89
 
73
90
  const DEFAULT_HEADING_TEXT = 'Frequently Asked Questions';
74
91
 
92
+ /** The id of a page's own FAQ block: what a page's "FAQ" link or section nav points at. */
93
+ export const PAGE_FAQ_SECTION_ID = 'faq';
94
+
95
+ /** A page's own FAQ block inside a page that already provides the container: vertical rhythm only. */
96
+ const PAGE_FAQ_IN_PAGE_CLASS = 'space-y-10 pt-12 md:pt-20';
97
+
98
+ /**
99
+ * What a shared page of the lib takes as its `faq` prop: the topic the page is
100
+ * about (REQUIRED, stated by the host, never read from the page's address) and
101
+ * where the host serves the FAQs. The page renders `FaqSection` with it, inside
102
+ * its own container, after its content (`pageFaqProps`).
103
+ */
104
+ export type PageFaqSlot = Pick<
105
+ FaqSectionProps,
106
+ 'apiBaseUrl' | 'initialFaqs' | 'heading' | 'ask' | 'emitJsonLd' | 'jsonLd' | 'showTopic'
107
+ > & { topic: string };
108
+
109
+ /** The `FaqSection` props of a shared page's own FAQ block: the host's slot, as the page's `#faq` section. */
110
+ export function pageFaqProps(faq: PageFaqSlot): FaqSectionProps {
111
+ return { ...faq, id: PAGE_FAQ_SECTION_ID, className: PAGE_FAQ_IN_PAGE_CLASS };
112
+ }
113
+
75
114
  /** URL composition shared with RelatedContentSection (`buildSuggestionUrl`)
76
115
  * — byte-identical to the historical `buildFaqsUrl` output when
77
116
  * `minResults`/`apiBaseUrl` are absent. */
78
- function buildFaqsUrl(entityType?: string, entityId?: number | string, minResults?: number, apiBaseUrl = ''): string {
79
- return buildSuggestionUrl('/api/faqs', { apiBaseUrl, entityType, entityId, count: minResults });
117
+ function buildFaqsUrl(
118
+ entityType?: string,
119
+ entityId?: number | string,
120
+ minResults?: number,
121
+ apiBaseUrl = '',
122
+ topic?: string,
123
+ ): string {
124
+ // An entity's FAQs are the entity's: a topic narrows only the platform's own list.
125
+ const entityScoped = !!entityType && entityId !== undefined && entityId !== null && entityId !== '';
126
+ return buildSuggestionUrl('/api/faqs', {
127
+ apiBaseUrl,
128
+ entityType,
129
+ entityId,
130
+ count: minResults,
131
+ extraParams: entityScoped ? undefined : { topic },
132
+ });
80
133
  }
81
134
 
82
135
  interface FaqGroup {
@@ -390,6 +443,8 @@ interface FaqsResponse {
390
443
  * page owns the <h1>) + `emitJsonLd` with `jsonLd` overrides for SEO.
391
444
  * - Per-entity embed: pass `entityType` + `entityId` (no `initialFaqs`); the
392
445
  * hook self-fetches `GET /api/faqs`, and `heading` is this block's own <h2>.
446
+ * - A page's own FAQ: pass `topic` (what the page is about); the hook
447
+ * self-fetches `GET /api/faqs?topic=`, and the ask card takes the same topic.
393
448
  *
394
449
  * CONTRACT: the consuming app MUST implement `GET /api/faqs`. On a fetch error
395
450
  * (or zero FAQs) the component renders nothing so the host page isn't
@@ -400,10 +455,13 @@ export function FaqSection({
400
455
  initialFaqs,
401
456
  entityType,
402
457
  entityId,
458
+ topic,
403
459
  heading,
404
460
  emitJsonLd = false,
405
461
  jsonLd,
406
462
  className,
463
+ id,
464
+ showTopic = false,
407
465
  minResults,
408
466
  apiBaseUrl = '',
409
467
  aside: asideProp,
@@ -412,30 +470,44 @@ export function FaqSection({
412
470
  const assistant = useAssistantRuntime();
413
471
  // The host's own block wins; else the card, where a chat is there to open and the FAQ has a topic
414
472
  // (an empty topic is no topic, as the questions request reads it).
415
- const askTopic = ask === false ? undefined : ask?.topic || entityType;
473
+ const askTopic = ask === false ? undefined : ask?.topic || topic || entityType;
416
474
  const aside =
417
475
  asideProp ??
418
476
  (ask !== false && askTopic && faqAskCardShown(assistant) ? <FaqAskCard {...ask} topic={askTopic} /> : undefined);
419
- const url = buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl);
477
+ const url = buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl, topic);
420
478
  // Memoized — useSelfFetch re-syncs on [initialData]; a fresh per-render
421
479
  // wrapper object would setState-loop under re-rendering parents.
422
480
  const initialData = useMemo<FaqsResponse | undefined>(
423
481
  () => (initialFaqs ? { faqs: initialFaqs } : undefined),
424
482
  [initialFaqs],
425
483
  );
426
- const { data, isLoading, error } = useSelfFetch<FaqsResponse>(url, { initialData });
484
+ const { data, dataUrl, isLoading, error } = useSelfFetch<FaqsResponse>(url, { initialData });
427
485
 
428
- // Memoised because the `??` fallback minted a NEW array on every
429
- // render, which made the memo below re-run every time — i.e. do nothing.
430
- const faqs = useMemo(() => data?.faqs ?? [], [data?.faqs]);
486
+ // Only the answer for THIS url: while another topic's questions load, the
487
+ // previous topic's are not this page's. Memoised because the `??` fallback
488
+ // minted a NEW array on every render, which made the memo below re-run
489
+ // every time — i.e. do nothing.
490
+ const held = dataUrl === url ? data?.faqs : undefined;
491
+ const faqs = useMemo(() => held ?? [], [held]);
431
492
  // Grouped before the early returns so the hook order stays stable.
432
493
  const groups = useMemo(() => (faqs.length > 0 ? groupFaqsBySection(faqs) : []), [faqs]);
433
494
 
434
495
  // `undefined` → default <h2> title; `null` → the host page owns the <h1>, so
435
496
  // no title renders here. `heading === null` also makes the category headings
436
497
  // <h2> (directly under the page <h1>); otherwise they nest as <h3>.
498
+ const titleNode = heading === undefined ? <h2 className={SECTION_HEADING_CLASS}>{DEFAULT_HEADING_TEXT}</h2> : heading;
437
499
  const headingNode =
438
- heading === undefined ? <h2 className={SECTION_HEADING_CLASS}>{DEFAULT_HEADING_TEXT}</h2> : heading;
500
+ showTopic && topic ? (
501
+ // The host's heading may arrive from a server component, still streaming: React cannot check such a
502
+ // node when it sits in a list of siblings and warns about a missing key. So it is the ONLY child of its
503
+ // own wrapper, and the two siblings here are this component's own elements.
504
+ <div className="space-y-1">
505
+ <div>{titleNode}</div>
506
+ <p className="text-ods-text-muted text-h6">{topic}</p>
507
+ </div>
508
+ ) : (
509
+ titleNode
510
+ );
439
511
 
440
512
  // Degrade silently — never show an error banner or an empty section shell
441
513
  // where FAQs would be (host pages and the standalone surface both rely on it).
@@ -453,7 +525,7 @@ export function FaqSection({
453
525
 
454
526
  return (
455
527
  <>
456
- <section className={className ?? 'space-y-10'}>
528
+ <section id={id} data-faq-topic={topic} className={className ?? 'space-y-10'}>
457
529
  {!aside && headingNode}
458
530
  <GroupedFaqList
459
531
  groups={groups}
@@ -9,6 +9,6 @@
9
9
  //
10
10
  // (Server-safe subpath is wired in tsup.config.ts under the server/universal
11
11
  // block and exposed via "./components/faq/json-ld" in package.json#exports.)
12
- export { FaqSection, type FaqSectionProps } from './faq-section';
12
+ export { FaqSection, PAGE_FAQ_SECTION_ID, pageFaqProps, type FaqSectionProps, type PageFaqSlot } from './faq-section';
13
13
  export { FaqDocumentPage, type FaqDocumentPageProps } from './faq-document-page';
14
14
  export { FaqAskCard, type FaqAskOptions } from './faq-ask-card';
@@ -1,19 +1,12 @@
1
1
  'use client';
2
2
 
3
- import type { ReactNode } from 'react';
4
3
  import { cn } from '../../../utils/cn';
5
- import { trialLabels } from '../../../utils/trial';
6
- import { FileOffIcon } from '../../icons-v2-generated/documents/file-off-icon';
7
- import { CreditCardXmarkIcon } from '../../icons-v2-generated/finance/credit-card-xmark-icon';
8
4
  import { OpenframeLogoIcon } from '../../icons-v2-generated/logos/openframe-logo-icon';
9
- import { FlaskVialIcon } from '../../icons-v2-generated/school/flask-vial-icon';
10
- import { XmarkAltIcon } from '../../icons-v2-generated/signs-and-symbols/xmark-alt-icon';
5
+ import { BenefitList, trialBenefits, type BenefitItem } from '../../ui/benefit-list';
11
6
  import { Button } from '../../ui/button';
12
7
 
13
- export interface AuthBenefit {
14
- icon: ReactNode;
15
- label: string;
16
- }
8
+ /** One benefit of the panel: the lib's benefit item. */
9
+ export type AuthBenefit = BenefitItem;
17
10
 
18
11
  export interface AuthBenefitsPanelProps {
19
12
  title?: string;
@@ -31,17 +24,6 @@ const DEFAULT_TITLE = 'The All-in-One Open Platform for MSPs';
31
24
  const DEFAULT_DESCRIPTION =
32
25
  'All your core ops in one place - built for MSPs who are done duct-taping tools together. Unified stack, AI-ready, no vendor tax. Just solid software that lets you run lean and fast.';
33
26
 
34
- /** The default chips: the trial's terms, and its length when the host knows it. */
35
- function defaultBenefits(trialDays: number | null | undefined): AuthBenefit[] {
36
- const labels = trialLabels(trialDays);
37
- return [
38
- { icon: <CreditCardXmarkIcon />, label: labels.noCard },
39
- { icon: <XmarkAltIcon />, label: labels.cancel },
40
- ...(labels.length ? [{ icon: <FlaskVialIcon />, label: labels.length }] : []),
41
- { icon: <FileOffIcon />, label: labels.noContract },
42
- ];
43
- }
44
-
45
27
  /**
46
28
  * Marketing panel shown alongside the auth forms — title, blurb, benefit chips
47
29
  * and a "learn more" link. Presentational; content is overridable via props.
@@ -50,7 +32,7 @@ export function AuthBenefitsPanel({
50
32
  title = DEFAULT_TITLE,
51
33
  description = DEFAULT_DESCRIPTION,
52
34
  trialDays,
53
- benefits = defaultBenefits(trialDays),
35
+ benefits = trialBenefits(trialDays),
54
36
  learnMoreLabel = 'Learn More About OpenFrame',
55
37
  learnMoreUrl = 'https://www.flamingo.run/',
56
38
  className,
@@ -65,16 +47,7 @@ export function AuthBenefitsPanel({
65
47
  <h2 className="tracking-[-0.64px] text-ods-text-primary text-h2">{title}</h2>
66
48
  <p className="text-ods-text-primary text-h4">{description}</p>
67
49
 
68
- <div className="flex flex-wrap content-center items-center gap-[var(--spacing-system-m)]">
69
- {benefits.map(benefit => (
70
- <div key={benefit.label} className="flex items-center gap-[var(--spacing-system-xs)]">
71
- <span className="flex size-4 shrink-0 items-center justify-center text-ods-text-secondary [&_svg]:size-4">
72
- {benefit.icon}
73
- </span>
74
- <span className="whitespace-nowrap text-ods-text-primary text-h4">{benefit.label}</span>
75
- </div>
76
- ))}
77
- </div>
50
+ <BenefitList items={benefits} />
78
51
 
79
52
  <Button
80
53
  variant="outline"
@@ -1,6 +1,11 @@
1
+ 'use client';
2
+
1
3
  import type { ReactNode } from 'react';
2
4
  import { cn } from '../../utils/cn';
5
+ import { useAgentTint } from '../agent-identity';
6
+ import { ChatTurn } from '../chat/chat-turn';
3
7
  import { CheckCircleIcon } from '../icons-v2-generated/signs-and-symbols/check-circle-icon';
8
+ import { Tag } from '../ui/tag';
4
9
 
5
10
  export interface ConversationCardSpeaker {
6
11
  /** Rendered as given (an avatar, an agent mark). */
@@ -8,11 +13,13 @@ export interface ConversationCardSpeaker {
8
13
  name: string;
9
14
  /** A quieter word after the name ("on her laptop", "this computer"). */
10
15
  meta?: string;
16
+ /** When they spoke, as shown ("2:47 PM"). */
17
+ time?: string;
11
18
  }
12
19
 
13
20
  export interface ConversationCardProps {
14
21
  requester: ConversationCardSpeaker;
15
- /** What they said, shown in quotation marks. */
22
+ /** What they said. */
16
23
  request: string;
17
24
  responder: ConversationCardSpeaker;
18
25
  /** What was done about it. */
@@ -25,22 +32,19 @@ export interface ConversationCardProps {
25
32
  footerValue: string;
26
33
  /** `attention` paints the answer in the warning colour: it needs a person. */
27
34
  footerTone?: 'default' | 'attention';
35
+ /**
36
+ * The slug of the agent that answered. The card takes that agent's tint from
37
+ * its identity, so it reads as that agent's at a glance. A card names its
38
+ * agent, never a colour.
39
+ */
40
+ agent?: string | null;
28
41
  className?: string;
29
42
  }
30
43
 
31
- function Who({ speaker }: { speaker: ConversationCardSpeaker }) {
32
- return (
33
- <div className="flex items-baseline gap-2 text-ods-text-primary text-h4">
34
- {speaker.name}
35
- {speaker.meta && <span className="text-ods-text-secondary text-h6">{speaker.meta}</span>}
36
- </div>
37
- );
38
- }
39
-
40
44
  /**
41
45
  * A request and what was done about it, as two turns and a footer: who asked
42
- * and what they said, who answered and the result, then one line that sums up
43
- * the reader's own part in it.
46
+ * and what they said, who answered and the result (a command block with its
47
+ * check), then one line that sums up the reader's own part in it.
44
48
  */
45
49
  export function ConversationCard({
46
50
  requester,
@@ -51,47 +55,45 @@ export function ConversationCard({
51
55
  footerLabel,
52
56
  footerValue,
53
57
  footerTone = 'default',
58
+ agent,
54
59
  className,
55
60
  }: ConversationCardProps) {
61
+ const tint = useAgentTint(agent);
56
62
  return (
57
63
  <div
58
- className={cn('flex h-full flex-col gap-4 rounded-xl border border-ods-border bg-ods-card p-5 md:p-7', className)}
64
+ className={cn(
65
+ 'flex h-full flex-col gap-[var(--spacing-system-mf)] overflow-hidden rounded-md border border-ods-border bg-ods-bg p-[var(--spacing-system-mf)]',
66
+ className,
67
+ )}
68
+ style={tint}
59
69
  >
60
- <div className="flex items-start gap-3">
61
- <span className="shrink-0">{requester.avatar}</span>
62
- <div className="min-w-0 flex-1">
63
- <Who speaker={requester} />
64
- {/* Every part keeps a fixed number of lines, so cards in a row or a carousel line up part by part. */}
65
- <p className="m-0 mt-1 line-clamp-2 min-h-[2lh] text-ods-text-primary text-h3" title={request}>
66
- &ldquo;{request}&rdquo;
67
- </p>
68
- </div>
69
- </div>
70
- <div className="flex items-start gap-3 rounded-md border border-ods-border bg-ods-bg p-3.5">
71
- <span className="shrink-0">{responder.avatar}</span>
72
- <div className="min-w-0 flex-1">
73
- <Who speaker={responder} />
74
- <div className="mt-1 flex items-start gap-2 text-ods-text-secondary text-h4">
75
- <CheckCircleIcon size={16} className="mt-1 shrink-0 text-ods-success" aria-hidden />
76
- <span className="line-clamp-3 min-h-[3lh] min-w-0 flex-1" title={outcome}>
77
- {outcome}
78
- </span>
79
- {outcomeAside}
80
- </div>
70
+ {/* Every part keeps a fixed number of lines, so cards in a row or a carousel line up part by part. */}
71
+ <ChatTurn avatar={requester.avatar} name={requester.name} meta={requester.meta} time={requester.time}>
72
+ <p className="m-0 line-clamp-2 min-h-[2lh] text-ods-text-primary text-h3" title={request}>
73
+ &ldquo;{request}&rdquo;
74
+ </p>
75
+ </ChatTurn>
76
+ {/* The answer is a command block: who did it, what was done, and its check. */}
77
+ <ChatTurn
78
+ avatar={responder.avatar}
79
+ name={responder.name}
80
+ meta={responder.meta}
81
+ time={responder.time}
82
+ className="rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-sf)]"
83
+ >
84
+ <div className="flex items-start gap-[var(--spacing-system-xsf)] text-ods-text-secondary text-h4">
85
+ <span className="line-clamp-3 min-h-[3lh] min-w-0 flex-1" title={outcome}>
86
+ {outcome}
87
+ </span>
88
+ {outcomeAside}
89
+ <span className="flex h-[1lh] shrink-0 items-center">
90
+ <CheckCircleIcon size={16} className="text-ods-success" aria-hidden />
91
+ </span>
81
92
  </div>
82
- </div>
83
- <div className="mt-auto flex items-center justify-between gap-3 border-t border-ods-border pt-3.5 text-ods-text-secondary text-h6">
93
+ </ChatTurn>
94
+ <div className="mt-auto flex items-center justify-between gap-[var(--spacing-system-sf)] text-ods-text-secondary text-h6">
84
95
  <span>{footerLabel}</span>
85
- <span
86
- className={cn(
87
- 'inline-flex whitespace-nowrap rounded-full border px-3 py-1',
88
- footerTone === 'attention'
89
- ? 'border-ods-warning text-ods-warning'
90
- : 'border-ods-border text-ods-text-secondary',
91
- )}
92
- >
93
- {footerValue}
94
- </span>
96
+ <Tag as="span" label={footerValue} variant={footerTone === 'attention' ? 'warning' : 'badge'} />
95
97
  </div>
96
98
  </div>
97
99
  );
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { ReactNode } from 'react';
11
+ import type { PageFaqSlot } from '../faq/faq-section';
11
12
  import { DeliveryLists } from '../shared/delivery';
12
13
  import { DevSectionPage } from '../shared/dev-section';
13
14
 
@@ -22,6 +23,12 @@ export interface DeliveryPageProps {
22
23
  subtitle?: string;
23
24
  /** Optional slot rendered below the lists, inside the page chrome. */
24
25
  belowContent?: ReactNode;
26
+ /**
27
+ * The page's own FAQ block: the topic the page is about (stated by the host,
28
+ * never read from its address) and where the host serves the FAQs. Rendered
29
+ * after the page's content, inside its container. Absent: no FAQ block.
30
+ */
31
+ faq?: PageFaqSlot;
25
32
  /** Render the standalone `<PageShell>`. Default true. Pass false when the host
26
33
  * layout already provides the page container (forwarded to `DevSectionPage`). */
27
34
  shell?: boolean;
@@ -34,10 +41,18 @@ export function DeliveryPage({
34
41
  title,
35
42
  subtitle,
36
43
  belowContent,
44
+ faq,
37
45
  shell,
38
46
  }: DeliveryPageProps) {
39
47
  return (
40
- <DevSectionPage sectionKey="delivery" backButton={backButton} title={title} subtitle={subtitle} shell={shell}>
48
+ <DevSectionPage
49
+ sectionKey="delivery"
50
+ backButton={backButton}
51
+ title={title}
52
+ subtitle={subtitle}
53
+ faq={faq}
54
+ shell={shell}
55
+ >
41
56
  <DeliveryLists completedApiEndpoint={completedEndpoint} inProgressApiEndpoint={inProgressEndpoint} />
42
57
  {belowContent}
43
58
  </DevSectionPage>
@@ -52,6 +52,7 @@ import {
52
52
  import { cn } from '../../utils/cn';
53
53
  import { formatUrlForDisplay } from '../../utils/format';
54
54
  import { DESKTOP_OSES, type DesktopOs } from '../../utils/visitor-os';
55
+ import { FaqSection, pageFaqProps, type PageFaqSlot } from '../faq/faq-section';
55
56
  import { CommandBox } from '../features/command-box';
56
57
  import { AppleLogoIcon } from '../icons-v2-generated/brand-logos/apple-logo-icon';
57
58
  import { WindowsLogoGreyIcon } from '../icons-v2-generated/brand-logos/windows-logo-grey-icon';
@@ -86,6 +87,12 @@ export interface DownloadAppsPageProps {
86
87
  onDownloadAction?: (event: DownloadActionEvent) => void;
87
88
  /** The closing card, under the mobile app: using OpenFrame without installing anything. Default: none. */
88
89
  web?: DownloadAppsWebOption;
90
+ /**
91
+ * The page's own FAQ block: the topic the page is about (stated by the host,
92
+ * never read from its address) and where the host serves the FAQs. Rendered
93
+ * after the page's content, inside its container. Absent: no FAQ block.
94
+ */
95
+ faq?: PageFaqSlot;
89
96
  }
90
97
 
91
98
  /** The "no install" card: the host's words and its own button (the page holds no signup link). */
@@ -281,6 +288,7 @@ export function DownloadAppsPage({
281
288
  openStoresInNewTab = true,
282
289
  onDownloadAction,
283
290
  web,
291
+ faq,
284
292
  }: DownloadAppsPageProps) {
285
293
  const { data, isLoading, error, reload } = useDownloads({ endpoint, initialData });
286
294
  // The page adapts to the server's answer: a section exists only for what it names.
@@ -362,6 +370,7 @@ export function DownloadAppsPage({
362
370
  {body}
363
371
  {web && <WebCard web={web} />}
364
372
  </div>
373
+ {faq && <FaqSection {...pageFaqProps(faq)} />}
365
374
  </DownloadAppsChrome>
366
375
  );
367
376
  }
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { ReactNode } from 'react';
12
12
  import type { OnboardingGuide } from '../chat/types/entities/onboarding-guide';
13
+ import type { PageFaqSlot } from '../faq/faq-section';
13
14
  import { OnboardingGuidesCatalogView } from '../onboarding-guides';
14
15
  import { DevSectionPage } from '../shared/dev-section';
15
16
 
@@ -32,6 +33,12 @@ export interface OnboardingGuidesCatalogPageProps {
32
33
  subtitle?: string;
33
34
  /** Optional slot rendered below the catalog, inside the page chrome. */
34
35
  belowContent?: ReactNode;
36
+ /**
37
+ * The page's own FAQ block: the topic the page is about (stated by the host,
38
+ * never read from its address) and where the host serves the FAQs. Rendered
39
+ * after the page's content, inside its container. Absent: no FAQ block.
40
+ */
41
+ faq?: PageFaqSlot;
35
42
  /** Render the standalone `<PageShell>`. Default true. Pass false when the host
36
43
  * layout already provides the page container (forwarded to `DevSectionPage`). */
37
44
  shell?: boolean;
@@ -48,10 +55,18 @@ export function OnboardingGuidesCatalogPage({
48
55
  title,
49
56
  subtitle,
50
57
  belowContent,
58
+ faq,
51
59
  shell,
52
60
  }: OnboardingGuidesCatalogPageProps) {
53
61
  return (
54
- <DevSectionPage sectionKey="onboarding" backButton={backButton} title={title} subtitle={subtitle} shell={shell}>
62
+ <DevSectionPage
63
+ sectionKey="onboarding"
64
+ backButton={backButton}
65
+ title={title}
66
+ subtitle={subtitle}
67
+ faq={faq}
68
+ shell={shell}
69
+ >
55
70
  <OnboardingGuidesCatalogView
56
71
  guidesEndpoint={guidesEndpoint}
57
72
  sectionsEndpoint={sectionsEndpoint}
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { ReactNode } from 'react';
12
12
  import type { ProductRelease, ProductReleaseListResponse } from '../../types/product-release';
13
+ import type { PageFaqSlot } from '../faq/faq-section';
13
14
  import { DevSectionPage } from '../shared/dev-section';
14
15
  import { ProductReleasesView, type ProductReleaseCardExtras } from '../shared/product-release';
15
16
 
@@ -28,6 +29,12 @@ export interface ProductReleasesListPageProps {
28
29
  subtitle?: string;
29
30
  /** Optional slot rendered below the list, inside the page chrome. */
30
31
  belowContent?: ReactNode;
32
+ /**
33
+ * The page's own FAQ block: the topic the page is about (stated by the host,
34
+ * never read from its address) and where the host serves the FAQs. Rendered
35
+ * after the page's content, inside its container. Absent: no FAQ block.
36
+ */
37
+ faq?: PageFaqSlot;
31
38
  /** Render the standalone `<PageShell>`. Default true. Pass false when the host
32
39
  * layout already provides the page container (forwarded to `DevSectionPage`). */
33
40
  shell?: boolean;
@@ -42,10 +49,18 @@ export function ProductReleasesListPage({
42
49
  title,
43
50
  subtitle,
44
51
  belowContent,
52
+ faq,
45
53
  shell,
46
54
  }: ProductReleasesListPageProps) {
47
55
  return (
48
- <DevSectionPage sectionKey="releases" backButton={backButton} title={title} subtitle={subtitle} shell={shell}>
56
+ <DevSectionPage
57
+ sectionKey="releases"
58
+ backButton={backButton}
59
+ title={title}
60
+ subtitle={subtitle}
61
+ faq={faq}
62
+ shell={shell}
63
+ >
49
64
  <ProductReleasesView
50
65
  endpoint={releasesEndpoint}
51
66
  basePath={basePath}
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { ReactNode } from 'react';
12
12
  import type { RoadmapItem } from '../chat/types/entities/roadmap-item';
13
+ import type { PageFaqSlot } from '../faq/faq-section';
13
14
  import { DevSectionPage } from '../shared/dev-section';
14
15
  import { RoadmapView } from '../shared/roadmap';
15
16
  import type { UseRoadmapVotingOptions } from '../shared/roadmap';
@@ -35,6 +36,12 @@ export interface RoadmapPageProps {
35
36
  /** Optional slot rendered below the list, inside the page chrome — e.g. the
36
37
  * hub's related-content rail. */
37
38
  belowContent?: ReactNode;
39
+ /**
40
+ * The page's own FAQ block: the topic the page is about (stated by the host,
41
+ * never read from its address) and where the host serves the FAQs. Rendered
42
+ * after the page's content, inside its container. Absent: no FAQ block.
43
+ */
44
+ faq?: PageFaqSlot;
38
45
  /** Render the standalone `<PageShell>`. Default true. Pass false when the host
39
46
  * layout already provides the page container (forwarded to `DevSectionPage`). */
40
47
  shell?: boolean;
@@ -51,10 +58,18 @@ export function RoadmapPage({
51
58
  title,
52
59
  subtitle,
53
60
  belowContent,
61
+ faq,
54
62
  shell,
55
63
  }: RoadmapPageProps) {
56
64
  return (
57
- <DevSectionPage sectionKey="roadmap" backButton={backButton} title={title} subtitle={subtitle} shell={shell}>
65
+ <DevSectionPage
66
+ sectionKey="roadmap"
67
+ backButton={backButton}
68
+ title={title}
69
+ subtitle={subtitle}
70
+ faq={faq}
71
+ shell={shell}
72
+ >
58
73
  <RoadmapView
59
74
  endpoint={roadmapEndpoint}
60
75
  initialItems={initialItems}
@@ -85,13 +85,14 @@ export const ToolBadge: React.FC<ToolBadgeProps> = ({
85
85
  return linked(
86
86
  <span
87
87
  className={cn(
88
- 'inline-flex h-7 items-center gap-1.5 rounded-md border border-ods-border px-2 text-ods-text-secondary text-h6',
89
- href && 'transition-colors hover:border-ods-border-hover hover:text-ods-text-primary',
88
+ // The house tag (Figma `tag`): 32px, on the card surface, the caption face.
89
+ 'inline-flex h-8 items-center gap-[var(--spacing-system-xs)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xsf)] text-ods-text-primary text-h5',
90
+ href && 'transition-colors hover:border-ods-border-hover',
90
91
  className,
91
92
  )}
92
93
  >
93
94
  {mark && (
94
- <span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center [&_img]:h-full [&_img]:w-full [&_img]:object-contain [&_svg]:h-full [&_svg]:w-full">
95
+ <span className="flex h-4 w-4 shrink-0 items-center justify-center [&_img]:h-full [&_img]:w-full [&_img]:rounded-sm [&_img]:object-contain [&_svg]:h-full [&_svg]:w-full">
95
96
  {mark}
96
97
  </span>
97
98
  )}
@@ -22,6 +22,7 @@ import {
22
22
  OPENFRAME_DEV_SECTIONS,
23
23
  type OpenframeDevSectionKey,
24
24
  } from '../../../utils/dev-sections/openframe-dev-sections';
25
+ import { FaqSection, pageFaqProps, type PageFaqSlot } from '../../faq/faq-section';
25
26
  import { PageShell, PageLayout } from '../../ui';
26
27
  import { DevSectionView } from './dev-section-view';
27
28
 
@@ -51,6 +52,12 @@ export interface DevSectionPageProps {
51
52
  * `false` when the host layout already provides the page container (e.g.
52
53
  * openframe-frontend's `AppLayout` `<main>`): then only the
53
54
  * `page-shell-content` padding box is rendered, avoiding a nested `<main>`. */
55
+ /**
56
+ * The page's own FAQ block: the topic the page is about (stated by the host,
57
+ * never read from its address) and where the host serves the FAQs. Rendered
58
+ * after the page's content, inside its container. Absent: no FAQ block.
59
+ */
60
+ faq?: PageFaqSlot;
54
61
  shell?: boolean;
55
62
  }
56
63
 
@@ -61,6 +68,7 @@ export function DevSectionPage({
61
68
  backButton,
62
69
  title,
63
70
  subtitle,
71
+ faq,
64
72
  shell = true,
65
73
  }: DevSectionPageProps) {
66
74
  const router = useRouter();
@@ -95,6 +103,7 @@ export function DevSectionPage({
95
103
  <DevSectionView sectionKey={sectionKey} showHeading={false} preControls={preControls}>
96
104
  {children}
97
105
  </DevSectionView>
106
+ {faq && <FaqSection {...pageFaqProps(faq)} />}
98
107
  </PageLayout>
99
108
  );
100
109