@flamingo-stack/openframe-frontend-core 0.0.735 → 0.0.737

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 (118) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{chunk-F7STMHT7.cjs → chunk-4FJZ7RJD.cjs} +66 -66
  3. package/dist/{chunk-SZRZBCWC.cjs → chunk-4ISP3XP4.cjs} +75 -69
  4. package/dist/{chunk-EQ7OOK4B.cjs → chunk-4RMLAHU2.cjs} +24 -16
  5. package/dist/{chunk-XKB7YGQM.cjs → chunk-77SD7RXY.cjs} +18 -6
  6. package/dist/{chunk-NVCSVGNI.cjs → chunk-CM35DLKS.cjs} +33 -33
  7. package/dist/{chunk-T323E2Y6.cjs → chunk-DA4VS6TS.cjs} +4 -4
  8. package/dist/{chunk-36CCUJOE.cjs → chunk-E2JCZU36.cjs} +313 -313
  9. package/dist/{chunk-RH3AKULV.cjs → chunk-EJOXXJJB.cjs} +56 -4
  10. package/dist/{chunk-SHQZN5LF.cjs → chunk-EX2GYIGW.cjs} +6 -6
  11. package/dist/{chunk-EQH7BUNS.cjs → chunk-FA2LATDM.cjs} +4 -4
  12. package/dist/{chunk-3JP2OVH3.cjs → chunk-KVGBDWYV.cjs} +30 -30
  13. package/dist/{chunk-HBUUCLQM.cjs → chunk-L535OD7V.cjs} +2 -2
  14. package/dist/{chunk-3X4N3IT7.cjs → chunk-LW3QSP47.cjs} +16 -16
  15. package/dist/{chunk-OXJ57643.cjs → chunk-MGW5DO6P.cjs} +76 -17
  16. package/dist/{chunk-4F7V2MAC.cjs → chunk-N6YOOGCD.cjs} +14 -14
  17. package/dist/chunk-O4MG7FXW.cjs +57 -0
  18. package/dist/{chunk-TUMUT7CV.cjs → chunk-SGVQO5J6.cjs} +11 -11
  19. package/dist/{chunk-MRVZ56GT.cjs → chunk-TJQYX3QC.cjs} +81 -81
  20. package/dist/{chunk-7HT3VHLJ.cjs → chunk-URIJ6YUG.cjs} +43 -69
  21. package/dist/{chunk-JDXQ6T5P.cjs → chunk-VL4JKXU2.cjs} +253 -290
  22. package/dist/{chunk-HLBFTDUL.cjs → chunk-VRNNIV5Y.cjs} +2 -2
  23. package/dist/{chunk-W6E7XCBI.cjs → chunk-VYODTHMR.cjs} +31 -31
  24. package/dist/{chunk-3FZZPR76.cjs → chunk-XLSVQJBA.cjs} +44 -44
  25. package/dist/{chunk-6TTAILSV.cjs → chunk-XPP7VCNY.cjs} +3 -3
  26. package/dist/{cloud-tenants-H5D57P4U.cjs → cloud-tenants-IWQ5FFLY.cjs} +7 -7
  27. package/dist/components/case-studies/index.cjs +19 -19
  28. package/dist/components/chat/entity-cards/index.d.ts +1 -0
  29. package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
  30. package/dist/components/chat/entity-cards/index.js +2 -0
  31. package/dist/components/chat/hooks/use-ask-prompts.d.ts +1 -1
  32. package/dist/components/chat/hooks/use-ask-prompts.js +1 -1
  33. package/dist/components/chat/index.cjs +10 -8
  34. package/dist/components/chat/mingo-info-card.d.ts +17 -1
  35. package/dist/components/chat/mingo-info-card.d.ts.map +1 -1
  36. package/dist/components/chat/mingo-info-card.js +15 -3
  37. package/dist/components/contact/index.cjs +8 -8
  38. package/dist/components/docs/index.cjs +11 -11
  39. package/dist/components/embeds/index.cjs +8 -8
  40. package/dist/components/faq/faq-ask-card.d.ts +5 -4
  41. package/dist/components/faq/faq-ask-card.d.ts.map +1 -1
  42. package/dist/components/faq/faq-document-page.d.ts +3 -1
  43. package/dist/components/faq/faq-document-page.d.ts.map +1 -1
  44. package/dist/components/faq/faq-document-page.js +2 -0
  45. package/dist/components/faq/faq-section.d.ts +37 -5
  46. package/dist/components/faq/faq-section.d.ts.map +1 -1
  47. package/dist/components/faq/faq-section.js +31 -9
  48. package/dist/components/faq/index.cjs +14 -8
  49. package/dist/components/faq/index.d.ts +1 -1
  50. package/dist/components/faq/index.d.ts.map +1 -1
  51. package/dist/components/faq/index.js +4 -2
  52. package/dist/components/features/index.cjs +7 -7
  53. package/dist/components/help-center-pages/delivery-page.d.ts +8 -1
  54. package/dist/components/help-center-pages/delivery-page.d.ts.map +1 -1
  55. package/dist/components/help-center-pages/delivery-page.js +16 -4
  56. package/dist/components/help-center-pages/download-apps-page.d.ts +8 -1
  57. package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -1
  58. package/dist/components/help-center-pages/download-apps-page.js +11 -6
  59. package/dist/components/help-center-pages/index.cjs +195 -140
  60. package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts +8 -1
  61. package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts.map +1 -1
  62. package/dist/components/help-center-pages/onboarding-guides-catalog-page.js +26 -14
  63. package/dist/components/help-center-pages/product-releases-list-page.d.ts +8 -1
  64. package/dist/components/help-center-pages/product-releases-list-page.d.ts.map +1 -1
  65. package/dist/components/help-center-pages/product-releases-list-page.js +24 -12
  66. package/dist/components/help-center-pages/roadmap-page.d.ts +8 -1
  67. package/dist/components/help-center-pages/roadmap-page.d.ts.map +1 -1
  68. package/dist/components/help-center-pages/roadmap-page.js +25 -13
  69. package/dist/components/index.cjs +67 -59
  70. package/dist/components/meeting-scheduler/index.cjs +29 -29
  71. package/dist/components/navigation/index.cjs +10 -10
  72. package/dist/components/onboarding-guides/index.cjs +10 -10
  73. package/dist/components/pagination.d.ts +9 -1
  74. package/dist/components/pagination.d.ts.map +1 -1
  75. package/dist/components/pagination.js +72 -17
  76. package/dist/components/product-demo/index.cjs +6 -6
  77. package/dist/components/related-content/index.cjs +5 -5
  78. package/dist/components/shared/dev-section/dev-section-page.d.ts +8 -1
  79. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
  80. package/dist/components/shared/dev-section/dev-section-page.js +8 -3
  81. package/dist/components/shared/legal-document/legal-document-page.d.ts +8 -1
  82. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
  83. package/dist/components/shared/legal-document/legal-document-page.js +4 -1
  84. package/dist/components/tickets/index.cjs +13 -12
  85. package/dist/components/ui/index.cjs +12 -10
  86. package/dist/components/unified-pagination.d.ts.map +1 -1
  87. package/dist/components/unified-pagination.js +16 -4
  88. package/dist/contexts/assistant-runtime-context.d.ts +5 -5
  89. package/dist/{devices-SQTEXY2H.cjs → devices-OUHGWA6Y.cjs} +8 -8
  90. package/dist/{embeddable-chat-S5ODWE3L.cjs → embeddable-chat-U3C4WWB7.cjs} +72 -72
  91. package/dist/index.cjs +20 -14
  92. package/dist/{knowledge-FO3TPLFO.cjs → knowledge-3ZASPWMF.cjs} +8 -8
  93. package/dist/{logs-Y4WQB2YR.cjs → logs-E6Y54P6K.cjs} +15 -15
  94. package/dist/{policies-D6VY2ODF.cjs → policies-LAV75B5B.cjs} +11 -11
  95. package/dist/{software-update-7XN6U7HC.cjs → software-update-2CAL3AMJ.cjs} +10 -10
  96. package/dist/types/faq.d.ts +17 -0
  97. package/dist/types/faq.d.ts.map +1 -1
  98. package/dist/types/faq.js +4 -0
  99. package/dist/types/index.cjs +4 -0
  100. package/package.json +1 -1
  101. package/src/components/chat/entity-cards/index.ts +1 -0
  102. package/src/components/chat/hooks/use-ask-prompts.ts +2 -2
  103. package/src/components/chat/mingo-info-card.tsx +39 -4
  104. package/src/components/faq/faq-ask-card.tsx +5 -4
  105. package/src/components/faq/faq-document-page.tsx +4 -0
  106. package/src/components/faq/faq-section.tsx +83 -14
  107. package/src/components/faq/index.ts +1 -1
  108. package/src/components/help-center-pages/delivery-page.tsx +16 -1
  109. package/src/components/help-center-pages/download-apps-page.tsx +9 -0
  110. package/src/components/help-center-pages/onboarding-guides-catalog-page.tsx +16 -1
  111. package/src/components/help-center-pages/product-releases-list-page.tsx +16 -1
  112. package/src/components/help-center-pages/roadmap-page.tsx +16 -1
  113. package/src/components/pagination.tsx +117 -21
  114. package/src/components/shared/dev-section/dev-section-page.tsx +9 -0
  115. package/src/components/shared/legal-document/legal-document-page.tsx +9 -0
  116. package/src/components/unified-pagination.tsx +23 -7
  117. package/src/contexts/assistant-runtime-context.tsx +5 -5
  118. package/src/types/faq.ts +18 -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,41 @@ 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
+ <div className="space-y-1">
502
+ {titleNode}
503
+ <p className="text-ods-text-muted text-h6">{topic}</p>
504
+ </div>
505
+ ) : (
506
+ titleNode
507
+ );
439
508
 
440
509
  // Degrade silently — never show an error banner or an empty section shell
441
510
  // where FAQs would be (host pages and the standalone surface both rely on it).
@@ -453,7 +522,7 @@ export function FaqSection({
453
522
 
454
523
  return (
455
524
  <>
456
- <section className={className ?? 'space-y-10'}>
525
+ <section id={id} data-faq-topic={topic} className={className ?? 'space-y-10'}>
457
526
  {!aside && headingNode}
458
527
  <GroupedFaqList
459
528
  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';
@@ -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}
@@ -1,8 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
4
- import { type ButtonHTMLAttributes, type HTMLAttributes, forwardRef } from 'react';
4
+ import { type ButtonHTMLAttributes, type HTMLAttributes, type MouseEvent, type ReactNode, forwardRef } from 'react';
5
+ import Link from '../embed-shims/next-link';
5
6
  import { cn } from '../utils/cn';
7
+ import { isModifierClick } from './chat/utils/chat-nav-resolution';
6
8
  import { Button } from './ui/button';
7
9
 
8
10
  interface PaginationProps {
@@ -10,9 +12,98 @@ interface PaginationProps {
10
12
  totalPages: number;
11
13
  onPageChange: (page: number) => void;
12
14
  className?: string;
15
+ /**
16
+ * The address of a page of this list. With it every enabled control is a real
17
+ * link (`<a href>`), so a crawler that runs no script can reach every page of
18
+ * the list, and a person can open a page in a new tab. A plain click still
19
+ * changes the page in place through `onPageChange`. Without it the controls
20
+ * are buttons.
21
+ */
22
+ getPageHref?: (page: number) => string;
13
23
  }
14
24
 
15
- const Pagination = ({ currentPage, totalPages, onPageChange, className }: PaginationProps) => {
25
+ interface PaginationControlProps {
26
+ /** The page this control goes to. */
27
+ page: number;
28
+ /** The control's accessible name ("Page 3", "Go to next page"). */
29
+ label: string;
30
+ variant: 'accent' | 'outline';
31
+ className: string;
32
+ /** The control marks the page being shown. */
33
+ current?: boolean;
34
+ /** There is no such page (previous on the first page, next on the last). */
35
+ disabled?: boolean;
36
+ /** Which neighbour the control leads to, stated to crawlers and assistive technology as `rel`. */
37
+ rel?: 'prev' | 'next';
38
+ onPageChange: (page: number) => void;
39
+ getPageHref?: (page: number) => string;
40
+ children: ReactNode;
41
+ }
42
+
43
+ /**
44
+ * One control of the pager. With a page address it is a link: the href is the
45
+ * page's real URL, so it works with no script, in a new tab and for a crawler,
46
+ * and a plain click is taken over to change the page in place (the progressive
47
+ * enhancement every in-page link in this library uses). With no address, or
48
+ * when there is no such page, it is a button: a link must always lead somewhere.
49
+ */
50
+ function PaginationControl({
51
+ page,
52
+ label,
53
+ variant,
54
+ className,
55
+ current,
56
+ disabled,
57
+ rel,
58
+ onPageChange,
59
+ getPageHref,
60
+ children,
61
+ }: PaginationControlProps) {
62
+ const ariaCurrent = current ? ('page' as const) : undefined;
63
+
64
+ if (!getPageHref || disabled) {
65
+ return (
66
+ <Button
67
+ variant={variant}
68
+ size="icon"
69
+ className={className}
70
+ disabled={disabled}
71
+ aria-label={label}
72
+ aria-current={ariaCurrent}
73
+ onClick={() => onPageChange(page)}
74
+ >
75
+ {children}
76
+ </Button>
77
+ );
78
+ }
79
+
80
+ const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
81
+ // A new tab or window is the browser's to open, at the link's own address.
82
+ if (isModifierClick(event)) return;
83
+ event.preventDefault();
84
+ onPageChange(page);
85
+ };
86
+
87
+ return (
88
+ <Button variant={variant} size="icon" className={className} asChild>
89
+ <Link
90
+ href={getPageHref(page)}
91
+ prefetch={false}
92
+ rel={rel}
93
+ aria-label={label}
94
+ aria-current={ariaCurrent}
95
+ onClick={handleClick}
96
+ >
97
+ {children}
98
+ </Link>
99
+ </Button>
100
+ );
101
+ }
102
+
103
+ const NEIGHBOUR_CLASS = '!h-10 !w-10 content-md:!h-10 content-md:!w-10';
104
+ const PAGE_CLASS = '!h-9 !w-9 content-md:!h-9 content-md:!w-9';
105
+
106
+ const Pagination = ({ currentPage, totalPages, onPageChange, className, getPageHref }: PaginationProps) => {
16
107
  // Generate page numbers to display
17
108
  const getPageNumbers = () => {
18
109
  const pages = [];
@@ -68,16 +159,18 @@ const Pagination = ({ currentPage, totalPages, onPageChange, className }: Pagina
68
159
  <nav role="navigation" aria-label="pagination" className={cn('mx-auto flex w-full justify-center', className)}>
69
160
  <ul className="flex flex-row items-center gap-1">
70
161
  <li>
71
- <Button
162
+ <PaginationControl
163
+ page={currentPage - 1}
164
+ label="Go to previous page"
72
165
  variant="outline"
73
- size="icon"
74
- onClick={() => onPageChange(currentPage - 1)}
166
+ className={NEIGHBOUR_CLASS}
75
167
  disabled={currentPage === 1}
76
- aria-label="Go to previous page"
77
- className="!h-10 !w-10 content-md:!h-10 content-md:!w-10"
168
+ rel="prev"
169
+ onPageChange={onPageChange}
170
+ getPageHref={getPageHref}
78
171
  >
79
172
  <ChevronLeft className="h-4 w-4" />
80
- </Button>
173
+ </PaginationControl>
81
174
  </li>
82
175
 
83
176
  {pageNumbers.map((page, index) => {
@@ -95,32 +188,35 @@ const Pagination = ({ currentPage, totalPages, onPageChange, className }: Pagina
95
188
 
96
189
  return (
97
190
  <li key={page}>
98
- <Button
191
+ <PaginationControl
192
+ page={page}
193
+ label={`Page ${page}`}
99
194
  variant={currentPage === page ? 'accent' : 'outline'}
100
- size="icon"
101
- onClick={() => onPageChange(page)}
102
- className="!h-9 !w-9 content-md:!h-9 content-md:!w-9"
103
- aria-label={`Page ${page}`}
104
- aria-current={currentPage === page ? 'page' : undefined}
195
+ className={PAGE_CLASS}
196
+ current={currentPage === page}
197
+ onPageChange={onPageChange}
198
+ getPageHref={getPageHref}
105
199
  >
106
200
  {page}
107
- </Button>
201
+ </PaginationControl>
108
202
  </li>
109
203
  );
110
204
  })}
111
205
 
112
206
  <li>
113
- <Button
207
+ <PaginationControl
208
+ page={currentPage + 1}
209
+ label="Go to next page"
114
210
  variant="outline"
115
- size="icon"
116
- onClick={() => onPageChange(currentPage + 1)}
211
+ className={NEIGHBOUR_CLASS}
117
212
  disabled={currentPage === totalPages}
118
- aria-label="Go to next page"
119
- className="!h-10 !w-10 content-md:!h-10 content-md:!w-10"
213
+ rel="next"
214
+ onPageChange={onPageChange}
215
+ getPageHref={getPageHref}
120
216
  >
121
217
  <ChevronRight className="h-4 w-4" />
122
218
  <span className="sr-only">Next page</span>
123
- </Button>
219
+ </PaginationControl>
124
220
  </li>
125
221
  </ul>
126
222
  </nav>
@@ -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
 
@@ -21,6 +21,7 @@ import type { ComponentType } from 'react';
21
21
  import Link from '../../../embed-shims/next-link';
22
22
  import { useRouter } from '../../../embed-shims/next-navigation';
23
23
  import { formatLegalDate } from '../../../utils/format';
24
+ import { FaqSection, pageFaqProps, type PageFaqSlot } from '../../faq/faq-section';
24
25
  import { PageShell, PageLayout } from '../../ui';
25
26
  import { RichMarkdownRenderer } from '../../ui/markdown';
26
27
  import { useLegalDocs, type LegalDocument } from './use-legal-docs';
@@ -64,6 +65,12 @@ export interface LegalDocumentPageProps {
64
65
  /** Back-button config — same pattern as `DevSectionPage`. Pass `false`
65
66
  * to hide. Default `{ label: 'Back to home', href: '/' }`. */
66
67
  backButton?: { label?: string; href?: string } | false;
68
+ /**
69
+ * The page's own FAQ block: the topic the page is about (stated by the host,
70
+ * never read from its address) and where the host serves the FAQs. Rendered
71
+ * after the page's content, inside its container. Absent: no FAQ block.
72
+ */
73
+ faq?: PageFaqSlot;
67
74
  /** Render the standalone `<PageShell>`. Default true. Pass false when the host
68
75
  * layout already provides the page container — only the padding box renders,
69
76
  * avoiding a nested `<main>`. */
@@ -83,6 +90,7 @@ export function LegalDocumentPage({
83
90
  apiEndpoint,
84
91
  MarkdownRenderer = RichMarkdownRenderer,
85
92
  backButton,
93
+ faq,
86
94
  shell = true,
87
95
  }: LegalDocumentPageProps) {
88
96
  const router = useRouter();
@@ -161,6 +169,7 @@ export function LegalDocumentPage({
161
169
  </div>
162
170
  </div>
163
171
  </div>
172
+ {faq && <FaqSection {...pageFaqProps(faq)} />}
164
173
  </PageLayout>
165
174
  );
166
175