@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.
- package/dist/build/export-map.json +1 -1
- package/dist/{chunk-F7STMHT7.cjs → chunk-4FJZ7RJD.cjs} +66 -66
- package/dist/{chunk-SZRZBCWC.cjs → chunk-4ISP3XP4.cjs} +75 -69
- package/dist/{chunk-EQ7OOK4B.cjs → chunk-4RMLAHU2.cjs} +24 -16
- package/dist/{chunk-XKB7YGQM.cjs → chunk-77SD7RXY.cjs} +18 -6
- package/dist/{chunk-NVCSVGNI.cjs → chunk-CM35DLKS.cjs} +33 -33
- package/dist/{chunk-T323E2Y6.cjs → chunk-DA4VS6TS.cjs} +4 -4
- package/dist/{chunk-36CCUJOE.cjs → chunk-E2JCZU36.cjs} +313 -313
- package/dist/{chunk-RH3AKULV.cjs → chunk-EJOXXJJB.cjs} +56 -4
- package/dist/{chunk-SHQZN5LF.cjs → chunk-EX2GYIGW.cjs} +6 -6
- package/dist/{chunk-EQH7BUNS.cjs → chunk-FA2LATDM.cjs} +4 -4
- package/dist/{chunk-3JP2OVH3.cjs → chunk-KVGBDWYV.cjs} +30 -30
- package/dist/{chunk-HBUUCLQM.cjs → chunk-L535OD7V.cjs} +2 -2
- package/dist/{chunk-3X4N3IT7.cjs → chunk-LW3QSP47.cjs} +16 -16
- package/dist/{chunk-OXJ57643.cjs → chunk-MGW5DO6P.cjs} +76 -17
- package/dist/{chunk-4F7V2MAC.cjs → chunk-N6YOOGCD.cjs} +14 -14
- package/dist/chunk-O4MG7FXW.cjs +57 -0
- package/dist/{chunk-TUMUT7CV.cjs → chunk-SGVQO5J6.cjs} +11 -11
- package/dist/{chunk-MRVZ56GT.cjs → chunk-TJQYX3QC.cjs} +81 -81
- package/dist/{chunk-7HT3VHLJ.cjs → chunk-URIJ6YUG.cjs} +43 -69
- package/dist/{chunk-JDXQ6T5P.cjs → chunk-VL4JKXU2.cjs} +253 -290
- package/dist/{chunk-HLBFTDUL.cjs → chunk-VRNNIV5Y.cjs} +2 -2
- package/dist/{chunk-W6E7XCBI.cjs → chunk-VYODTHMR.cjs} +31 -31
- package/dist/{chunk-3FZZPR76.cjs → chunk-XLSVQJBA.cjs} +44 -44
- package/dist/{chunk-6TTAILSV.cjs → chunk-XPP7VCNY.cjs} +3 -3
- package/dist/{cloud-tenants-H5D57P4U.cjs → cloud-tenants-IWQ5FFLY.cjs} +7 -7
- package/dist/components/case-studies/index.cjs +19 -19
- package/dist/components/chat/entity-cards/index.d.ts +1 -0
- package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/index.js +2 -0
- package/dist/components/chat/hooks/use-ask-prompts.d.ts +1 -1
- package/dist/components/chat/hooks/use-ask-prompts.js +1 -1
- package/dist/components/chat/index.cjs +10 -8
- package/dist/components/chat/mingo-info-card.d.ts +17 -1
- package/dist/components/chat/mingo-info-card.d.ts.map +1 -1
- package/dist/components/chat/mingo-info-card.js +15 -3
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/docs/index.cjs +11 -11
- package/dist/components/embeds/index.cjs +8 -8
- package/dist/components/faq/faq-ask-card.d.ts +5 -4
- package/dist/components/faq/faq-ask-card.d.ts.map +1 -1
- package/dist/components/faq/faq-document-page.d.ts +3 -1
- package/dist/components/faq/faq-document-page.d.ts.map +1 -1
- package/dist/components/faq/faq-document-page.js +2 -0
- package/dist/components/faq/faq-section.d.ts +37 -5
- package/dist/components/faq/faq-section.d.ts.map +1 -1
- package/dist/components/faq/faq-section.js +31 -9
- package/dist/components/faq/index.cjs +14 -8
- package/dist/components/faq/index.d.ts +1 -1
- package/dist/components/faq/index.d.ts.map +1 -1
- package/dist/components/faq/index.js +4 -2
- package/dist/components/features/index.cjs +7 -7
- package/dist/components/help-center-pages/delivery-page.d.ts +8 -1
- package/dist/components/help-center-pages/delivery-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/delivery-page.js +16 -4
- package/dist/components/help-center-pages/download-apps-page.d.ts +8 -1
- package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/download-apps-page.js +11 -6
- package/dist/components/help-center-pages/index.cjs +195 -140
- package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts +8 -1
- package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/onboarding-guides-catalog-page.js +26 -14
- package/dist/components/help-center-pages/product-releases-list-page.d.ts +8 -1
- package/dist/components/help-center-pages/product-releases-list-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/product-releases-list-page.js +24 -12
- package/dist/components/help-center-pages/roadmap-page.d.ts +8 -1
- package/dist/components/help-center-pages/roadmap-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/roadmap-page.js +25 -13
- package/dist/components/index.cjs +67 -59
- package/dist/components/meeting-scheduler/index.cjs +29 -29
- package/dist/components/navigation/index.cjs +10 -10
- package/dist/components/onboarding-guides/index.cjs +10 -10
- package/dist/components/pagination.d.ts +9 -1
- package/dist/components/pagination.d.ts.map +1 -1
- package/dist/components/pagination.js +72 -17
- package/dist/components/product-demo/index.cjs +6 -6
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/shared/dev-section/dev-section-page.d.ts +8 -1
- package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
- package/dist/components/shared/dev-section/dev-section-page.js +8 -3
- package/dist/components/shared/legal-document/legal-document-page.d.ts +8 -1
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
- package/dist/components/shared/legal-document/legal-document-page.js +4 -1
- package/dist/components/tickets/index.cjs +13 -12
- package/dist/components/ui/index.cjs +12 -10
- package/dist/components/unified-pagination.d.ts.map +1 -1
- package/dist/components/unified-pagination.js +16 -4
- package/dist/contexts/assistant-runtime-context.d.ts +5 -5
- package/dist/{devices-SQTEXY2H.cjs → devices-OUHGWA6Y.cjs} +8 -8
- package/dist/{embeddable-chat-S5ODWE3L.cjs → embeddable-chat-U3C4WWB7.cjs} +72 -72
- package/dist/index.cjs +20 -14
- package/dist/{knowledge-FO3TPLFO.cjs → knowledge-3ZASPWMF.cjs} +8 -8
- package/dist/{logs-Y4WQB2YR.cjs → logs-E6Y54P6K.cjs} +15 -15
- package/dist/{policies-D6VY2ODF.cjs → policies-LAV75B5B.cjs} +11 -11
- package/dist/{software-update-7XN6U7HC.cjs → software-update-2CAL3AMJ.cjs} +10 -10
- package/dist/types/faq.d.ts +17 -0
- package/dist/types/faq.d.ts.map +1 -1
- package/dist/types/faq.js +4 -0
- package/dist/types/index.cjs +4 -0
- package/package.json +1 -1
- package/src/components/chat/entity-cards/index.ts +1 -0
- package/src/components/chat/hooks/use-ask-prompts.ts +2 -2
- package/src/components/chat/mingo-info-card.tsx +39 -4
- package/src/components/faq/faq-ask-card.tsx +5 -4
- package/src/components/faq/faq-document-page.tsx +4 -0
- package/src/components/faq/faq-section.tsx +83 -14
- package/src/components/faq/index.ts +1 -1
- package/src/components/help-center-pages/delivery-page.tsx +16 -1
- package/src/components/help-center-pages/download-apps-page.tsx +9 -0
- package/src/components/help-center-pages/onboarding-guides-catalog-page.tsx +16 -1
- package/src/components/help-center-pages/product-releases-list-page.tsx +16 -1
- package/src/components/help-center-pages/roadmap-page.tsx +16 -1
- package/src/components/pagination.tsx +117 -21
- package/src/components/shared/dev-section/dev-section-page.tsx +9 -0
- package/src/components/shared/legal-document/legal-document-page.tsx +9 -0
- package/src/components/unified-pagination.tsx +23 -7
- package/src/contexts/assistant-runtime-context.tsx +5 -5
- 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: `
|
|
64
|
-
*
|
|
65
|
-
* No runtime or no topic: the FAQ is the single column it
|
|
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(
|
|
79
|
-
|
|
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
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
162
|
+
<PaginationControl
|
|
163
|
+
page={currentPage - 1}
|
|
164
|
+
label="Go to previous page"
|
|
72
165
|
variant="outline"
|
|
73
|
-
|
|
74
|
-
onClick={() => onPageChange(currentPage - 1)}
|
|
166
|
+
className={NEIGHBOUR_CLASS}
|
|
75
167
|
disabled={currentPage === 1}
|
|
76
|
-
|
|
77
|
-
|
|
168
|
+
rel="prev"
|
|
169
|
+
onPageChange={onPageChange}
|
|
170
|
+
getPageHref={getPageHref}
|
|
78
171
|
>
|
|
79
172
|
<ChevronLeft className="h-4 w-4" />
|
|
80
|
-
</
|
|
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
|
-
<
|
|
191
|
+
<PaginationControl
|
|
192
|
+
page={page}
|
|
193
|
+
label={`Page ${page}`}
|
|
99
194
|
variant={currentPage === page ? 'accent' : 'outline'}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
</
|
|
201
|
+
</PaginationControl>
|
|
108
202
|
</li>
|
|
109
203
|
);
|
|
110
204
|
})}
|
|
111
205
|
|
|
112
206
|
<li>
|
|
113
|
-
<
|
|
207
|
+
<PaginationControl
|
|
208
|
+
page={currentPage + 1}
|
|
209
|
+
label="Go to next page"
|
|
114
210
|
variant="outline"
|
|
115
|
-
|
|
116
|
-
onClick={() => onPageChange(currentPage + 1)}
|
|
211
|
+
className={NEIGHBOUR_CLASS}
|
|
117
212
|
disabled={currentPage === totalPages}
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
</
|
|
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
|
|