@flamingo-stack/openframe-frontend-core 0.0.206 → 0.0.207-snapshot.20260526023528
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/chunk-4XLJWX2N.js +12 -0
- package/dist/chunk-4XLJWX2N.js.map +1 -0
- package/dist/{chunk-OLTGB32E.js → chunk-6WMMLMKM.js} +2857 -2045
- package/dist/chunk-6WMMLMKM.js.map +1 -0
- package/dist/chunk-VFKQMAUF.cjs +12 -0
- package/dist/chunk-VFKQMAUF.cjs.map +1 -0
- package/dist/{chunk-YGOJIDL5.cjs → chunk-WYLNTZZ7.cjs} +1343 -531
- package/dist/chunk-WYLNTZZ7.cjs.map +1 -0
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +3 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +2 -1
- package/dist/components/chat/types/api.types.d.ts +17 -1
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/features/index.cjs +3 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +2 -1
- package/dist/components/index.cjs +21 -2
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +20 -1
- package/dist/components/navigation/index.cjs +3 -2
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.js +2 -1
- package/dist/components/shared/delivery/delivery-lists.d.ts +16 -0
- package/dist/components/shared/delivery/delivery-lists.d.ts.map +1 -0
- package/dist/components/shared/delivery/delivery-table.d.ts +12 -0
- package/dist/components/shared/delivery/delivery-table.d.ts.map +1 -0
- package/dist/components/shared/delivery/index.d.ts +3 -0
- package/dist/components/shared/delivery/index.d.ts.map +1 -0
- package/dist/components/shared/dev-section/dev-section-page.d.ts +31 -0
- package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -0
- package/dist/components/shared/dev-section/dev-section-view.d.ts +34 -0
- package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -0
- package/dist/components/shared/dev-section/index.d.ts +3 -0
- package/dist/components/shared/dev-section/index.d.ts.map +1 -0
- package/dist/components/shared/legal-document/index.d.ts +10 -0
- package/dist/components/shared/legal-document/index.d.ts.map +1 -0
- package/dist/components/shared/legal-document/legal-document-page.d.ts +66 -0
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -0
- package/dist/components/shared/legal-document/use-legal-docs.d.ts +40 -0
- package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -0
- package/dist/components/shared/product-release/index.d.ts +2 -1
- package/dist/components/shared/product-release/index.d.ts.map +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts +11 -7
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/shared/roadmap/index.d.ts +18 -0
- package/dist/components/shared/roadmap/index.d.ts.map +1 -0
- package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts +24 -0
- package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts.map +1 -0
- package/dist/components/shared/roadmap/roadmap-grid.d.ts +18 -0
- package/dist/components/shared/roadmap/roadmap-grid.d.ts.map +1 -0
- package/dist/components/shared/roadmap/use-roadmap-voting.d.ts +25 -0
- package/dist/components/shared/roadmap/use-roadmap-voting.d.ts.map +1 -0
- package/dist/components/ui/index.cjs +3 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +2 -1
- package/dist/components/ui/release-changelog-section.d.ts +13 -2
- package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
- package/dist/embed-shims/index.cjs +1 -6
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +1 -6
- package/dist/embed-shims/index.js.map +1 -1
- package/dist/index.cjs +13 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +12 -1
- package/dist/types/delivery.d.ts +49 -0
- package/dist/types/delivery.d.ts.map +1 -0
- package/dist/types/index.cjs +13 -0
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +12 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/dev-sections/index.d.ts +11 -0
- package/dist/utils/dev-sections/index.d.ts.map +1 -0
- package/dist/utils/dev-sections/openframe-dev-sections.d.ts +209 -0
- package/dist/utils/dev-sections/openframe-dev-sections.d.ts.map +1 -0
- package/dist/utils/index.cjs +82 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +81 -2
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +53 -6
- package/src/components/chat/types/api.types.ts +23 -1
- package/src/components/index.ts +8 -0
- package/src/components/shared/delivery/delivery-lists.tsx +199 -0
- package/src/components/shared/delivery/delivery-table.tsx +174 -0
- package/src/components/shared/delivery/index.ts +9 -0
- package/src/components/shared/dev-section/dev-section-page.tsx +72 -0
- package/src/components/shared/dev-section/dev-section-view.tsx +129 -0
- package/src/components/shared/dev-section/index.ts +2 -0
- package/src/components/shared/legal-document/index.ts +19 -0
- package/src/components/shared/legal-document/legal-document-page.tsx +178 -0
- package/src/components/shared/legal-document/use-legal-docs.ts +123 -0
- package/src/components/shared/product-release/index.ts +14 -3
- package/src/components/shared/product-release/release-detail-page.tsx +45 -7
- package/src/components/shared/roadmap/index.ts +23 -0
- package/src/components/shared/roadmap/roadmap-grid-skeleton.tsx +74 -0
- package/src/components/shared/roadmap/roadmap-grid.tsx +106 -0
- package/src/components/shared/roadmap/use-roadmap-voting.ts +163 -0
- package/src/components/ui/release-changelog-section.tsx +113 -32
- package/src/types/delivery.ts +54 -0
- package/src/types/index.ts +1 -0
- package/src/utils/dev-sections/index.ts +17 -0
- package/src/utils/dev-sections/openframe-dev-sections.ts +148 -0
- package/src/utils/index.ts +6 -1
- package/dist/chunk-OLTGB32E.js.map +0 -1
- package/dist/chunk-YGOJIDL5.cjs.map +0 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DevSectionView — the canonical chrome for ANY dev-center section
|
|
5
|
+
* (Roadmap / Delivery / Releases). One component, used in BOTH:
|
|
6
|
+
*
|
|
7
|
+
* - tabbed `/roadmap-and-releases` (compact title mode, no `hero`)
|
|
8
|
+
* - full-page `/roadmap`, `/bug-fixes-and-enhancements`, `/releases`
|
|
9
|
+
* (hero mode with icon + description + back link)
|
|
10
|
+
*
|
|
11
|
+
* Owns: title rendering, the inline search input, the filter pill row,
|
|
12
|
+
* and the URL-param wiring that connects both. The list `children`
|
|
13
|
+
* receive a clean URL contract — they read `?<paramKey>=...` via
|
|
14
|
+
* `useSearchParams()` and refetch on change. No duplicated controls.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ReactNode } from 'react';
|
|
18
|
+
import { useState, useEffect } from 'react';
|
|
19
|
+
import { useRouter, useSearchParams, usePathname } from '../../../embed-shims';
|
|
20
|
+
import { SearchInput } from '../../ui';
|
|
21
|
+
import { StatusFilterComponent } from '../../features';
|
|
22
|
+
import {
|
|
23
|
+
OPENFRAME_DEV_SECTIONS,
|
|
24
|
+
type OpenframeDevSectionKey,
|
|
25
|
+
} from '../../../utils/dev-sections/openframe-dev-sections';
|
|
26
|
+
|
|
27
|
+
export interface DevSectionViewProps {
|
|
28
|
+
/** Which section to render — drives title, search, and filter
|
|
29
|
+
* config via the `OPENFRAME_DEV_SECTIONS` registry. */
|
|
30
|
+
sectionKey: OpenframeDevSectionKey;
|
|
31
|
+
/** When set, renders the rich page-level hero (icon + h1 + description).
|
|
32
|
+
* Omit for the compact tab-context heading. */
|
|
33
|
+
hero?: {
|
|
34
|
+
/** Pre-rendered icon JSX. Server components render the icon themselves
|
|
35
|
+
* and pass the element here — function references can't cross the
|
|
36
|
+
* server→client boundary, but React elements can. */
|
|
37
|
+
icon: ReactNode;
|
|
38
|
+
description: string;
|
|
39
|
+
};
|
|
40
|
+
/** The page-specific list body. Reads URL params written by this
|
|
41
|
+
* component (search input + filter pills). */
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function DevSectionView({ sectionKey, hero, children }: DevSectionViewProps) {
|
|
46
|
+
const section = OPENFRAME_DEV_SECTIONS[sectionKey];
|
|
47
|
+
const router = useRouter();
|
|
48
|
+
const pathname = usePathname();
|
|
49
|
+
const searchParams = useSearchParams();
|
|
50
|
+
|
|
51
|
+
const search = section.search;
|
|
52
|
+
const filter = section.filter;
|
|
53
|
+
|
|
54
|
+
const currentSearch = search ? searchParams.get(search.paramKey) || '' : '';
|
|
55
|
+
const currentFilterValue = filter
|
|
56
|
+
? searchParams.get(filter.paramKey) || filter.defaultValue
|
|
57
|
+
: '';
|
|
58
|
+
|
|
59
|
+
// Controlled search-input state — input commits to the URL only on
|
|
60
|
+
// Enter (not on every keystroke), preserving the legacy behavior.
|
|
61
|
+
// Lazy init from URL avoids a brief flash of stale value on first
|
|
62
|
+
// paint after URL-driven re-render (e.g. tab switch).
|
|
63
|
+
const [searchValue, setSearchValue] = useState(() => currentSearch);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
setSearchValue(currentSearch);
|
|
66
|
+
}, [currentSearch]);
|
|
67
|
+
|
|
68
|
+
const handleSearchSubmit = (value: string) => {
|
|
69
|
+
if (!search) return;
|
|
70
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
71
|
+
if (value.trim()) params.set(search.paramKey, value.trim());
|
|
72
|
+
else params.delete(search.paramKey);
|
|
73
|
+
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleFilterChange = (value: string) => {
|
|
77
|
+
if (!filter) return;
|
|
78
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
79
|
+
if (value === filter.defaultValue) params.delete(filter.paramKey);
|
|
80
|
+
else params.set(filter.paramKey, value);
|
|
81
|
+
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="w-full flex flex-col gap-10">
|
|
86
|
+
{hero ? (
|
|
87
|
+
<div className="space-y-4">
|
|
88
|
+
<h1 className="text-h1 tracking-[-1.12px] text-ods-text-primary flex items-center gap-3">
|
|
89
|
+
{hero.icon}
|
|
90
|
+
{section.hero.title}
|
|
91
|
+
</h1>
|
|
92
|
+
<p className="font-['DM_Sans'] font-medium text-[18px] leading-[28px] text-ods-text-secondary max-w-3xl">
|
|
93
|
+
{hero.description}
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
) : (
|
|
97
|
+
<div className="flex items-center justify-between w-full">
|
|
98
|
+
<h2 className="font-['Azeret_Mono'] font-semibold text-[32px] md:text-[40px] lg:text-[48px] leading-[40px] md:leading-[48px] lg:leading-[56px] text-ods-text-primary tracking-[-0.64px] md:tracking-[-0.8px] lg:tracking-[-0.96px]">
|
|
99
|
+
{section.hero.title}
|
|
100
|
+
<span className="text-ods-accent">:</span>
|
|
101
|
+
</h2>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{(search || filter) && (
|
|
106
|
+
<div className="space-y-4">
|
|
107
|
+
{search && (
|
|
108
|
+
<SearchInput
|
|
109
|
+
showDropdown={false}
|
|
110
|
+
placeholder={search.placeholder}
|
|
111
|
+
value={searchValue}
|
|
112
|
+
onChange={setSearchValue}
|
|
113
|
+
onSubmit={handleSearchSubmit}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
{filter && (
|
|
117
|
+
<StatusFilterComponent
|
|
118
|
+
selectedStatus={currentFilterValue}
|
|
119
|
+
onStatusChange={handleFilterChange}
|
|
120
|
+
statusOptions={[...filter.options]}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{children}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared legal-document surface barrel.
|
|
3
|
+
*
|
|
4
|
+
* Exports one parameterized `<LegalDocumentPage>` that replaces hub's
|
|
5
|
+
* formerly-duplicated `PrivacyPolicyPage` + `TermsOfServicePage`
|
|
6
|
+
* (95% identical, differing only in copy strings).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
LegalDocumentPage,
|
|
11
|
+
type LegalDocumentPageProps,
|
|
12
|
+
type LegalDocumentMarkdownRendererProps,
|
|
13
|
+
} from './legal-document-page';
|
|
14
|
+
export {
|
|
15
|
+
useLegalDocs,
|
|
16
|
+
type LegalDocument,
|
|
17
|
+
type UseLegalDocsOptions,
|
|
18
|
+
type UseLegalDocsReturn,
|
|
19
|
+
} from './use-legal-docs';
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LegalDocumentPage — unified UI for privacy-policy, terms-of-service,
|
|
5
|
+
* and any other markdown-backed legal document.
|
|
6
|
+
*
|
|
7
|
+
* Replaces two near-identical hub components (`PrivacyPolicyPage` +
|
|
8
|
+
* `TermsOfServicePage`) that differed only in title, contact email,
|
|
9
|
+
* and copy strings. Caller passes those as props.
|
|
10
|
+
*
|
|
11
|
+
* Markdown rendering: defaults to lib's `SimpleMarkdownRenderer`
|
|
12
|
+
* (sufficient for plain-markdown legal docs). Embedders that need
|
|
13
|
+
* richer markdown (embeds, video, OG previews) pass their own via
|
|
14
|
+
* the `MarkdownRenderer` prop — same injection pattern as
|
|
15
|
+
* `ReleaseDetailPage`.
|
|
16
|
+
*
|
|
17
|
+
* Endpoint configuration: forwarded to `useLegalDocs(docType, { apiEndpoint })`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { ComponentType } from 'react';
|
|
21
|
+
import { PageShell, PageLayout } from '../../ui';
|
|
22
|
+
import { SimpleMarkdownRenderer } from '../../ui/simple-markdown-renderer';
|
|
23
|
+
import { useRouter } from '../../../embed-shims/next-navigation';
|
|
24
|
+
import { useLegalDocs, type LegalDocument } from './use-legal-docs';
|
|
25
|
+
import { formatLegalDate } from '../../../utils/format';
|
|
26
|
+
|
|
27
|
+
export interface LegalDocumentMarkdownRendererProps {
|
|
28
|
+
content: string;
|
|
29
|
+
sectionIds?: Array<{ id: string; title: string; level: number }>;
|
|
30
|
+
demoteMarkdownH1ToH2?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LegalDocumentPageProps {
|
|
34
|
+
/** Document type identifier — drives the default API endpoint
|
|
35
|
+
* `/api/legal/<docType>` AND the error-log prefix. Common values:
|
|
36
|
+
* `'privacy'`, `'terms'`. Embedders may use any string. */
|
|
37
|
+
docType: string;
|
|
38
|
+
/** Heading text (e.g. "Privacy Policy", "Terms of Service"). */
|
|
39
|
+
title: string;
|
|
40
|
+
/** Fallback subtitle shown when no `lastUpdated` date is available
|
|
41
|
+
* (e.g. "Our privacy policy and data protection practices"). */
|
|
42
|
+
fallbackDescription: string;
|
|
43
|
+
/** Email shown in the error + empty-state copy
|
|
44
|
+
* (e.g. `'privacy@openframe.io'`, `'legal@openframe.io'`). */
|
|
45
|
+
contactEmail: string;
|
|
46
|
+
/** Prompt shown above the contact link in the error state
|
|
47
|
+
* (e.g. "For privacy-related questions, please contact:"). */
|
|
48
|
+
errorContactPrompt: string;
|
|
49
|
+
/** Title for the error block (e.g. "Unable to load privacy policy"). */
|
|
50
|
+
errorTitle: string;
|
|
51
|
+
/** Sentence shown when the API returns no document
|
|
52
|
+
* (e.g. "Privacy policy content is not available at this time."). */
|
|
53
|
+
emptyStateMessage: string;
|
|
54
|
+
/** SSR-prepared document, if available. */
|
|
55
|
+
initialData?: LegalDocument | null;
|
|
56
|
+
/** SSR-prepared formatted "Last Updated" label. Stable across hydration. */
|
|
57
|
+
initialLastUpdatedLabel?: string | null;
|
|
58
|
+
/** Override the default `/api/legal/<docType>` endpoint
|
|
59
|
+
* (reverse-proxy embedders, alternate API paths). */
|
|
60
|
+
apiEndpoint?: string;
|
|
61
|
+
/** Override the default markdown renderer. */
|
|
62
|
+
MarkdownRenderer?: ComponentType<LegalDocumentMarkdownRendererProps>;
|
|
63
|
+
/** Back-button config — same pattern as `DevSectionPage`. Pass `false`
|
|
64
|
+
* to hide. Default `{ label: 'Back to home', href: '/' }`. */
|
|
65
|
+
backButton?: { label?: string; href?: string } | false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function LegalDocumentPage({
|
|
69
|
+
docType,
|
|
70
|
+
title,
|
|
71
|
+
fallbackDescription,
|
|
72
|
+
contactEmail,
|
|
73
|
+
errorContactPrompt,
|
|
74
|
+
errorTitle,
|
|
75
|
+
emptyStateMessage,
|
|
76
|
+
initialData = null,
|
|
77
|
+
initialLastUpdatedLabel = null,
|
|
78
|
+
apiEndpoint,
|
|
79
|
+
MarkdownRenderer = SimpleMarkdownRenderer,
|
|
80
|
+
backButton,
|
|
81
|
+
}: LegalDocumentPageProps) {
|
|
82
|
+
const router = useRouter();
|
|
83
|
+
const { data, isLoading, error } = useLegalDocs(docType, { initialData, apiEndpoint });
|
|
84
|
+
|
|
85
|
+
// Back-button config — mirrors DevSectionPage's `{ label: 'Back to home',
|
|
86
|
+
// onClick: () => router.push('/') }`. Hide entirely when caller passes
|
|
87
|
+
// `false` (e.g. embed-mode where the host owns navigation chrome).
|
|
88
|
+
const backCfg =
|
|
89
|
+
backButton === false
|
|
90
|
+
? undefined
|
|
91
|
+
: {
|
|
92
|
+
label: backButton?.label ?? 'Back to home',
|
|
93
|
+
onClick: () => router.push(backButton?.href ?? '/'),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const fallbackLastUpdatedLabel =
|
|
97
|
+
data?.lastSynced != null ? formatLegalDate(data.lastSynced) : null;
|
|
98
|
+
const effectiveLastUpdatedLabel = initialLastUpdatedLabel ?? fallbackLastUpdatedLabel;
|
|
99
|
+
|
|
100
|
+
// Title with accent-colon trailing dot — matches knowledge-hub typography
|
|
101
|
+
const customTitle = (
|
|
102
|
+
<div className="flex flex-col gap-4">
|
|
103
|
+
<h1 className="font-['Azeret_Mono'] text-[32px] md:text-[40px] lg:text-[48px] font-semibold leading-[1em] tracking-[-0.02em] text-ods-text-primary">
|
|
104
|
+
<span>{title}</span>
|
|
105
|
+
<span className="text-ods-accent">.</span>
|
|
106
|
+
</h1>
|
|
107
|
+
<p className="font-['DM_Sans'] text-base md:text-lg text-ods-text-secondary max-w-2xl">
|
|
108
|
+
{effectiveLastUpdatedLabel ? `Last Updated: ${effectiveLastUpdatedLabel}` : fallbackDescription}
|
|
109
|
+
{data?.sourceFile && (
|
|
110
|
+
<span className="block text-sm mt-1 opacity-75">Source: {data.sourceFile}</span>
|
|
111
|
+
)}
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<PageShell>
|
|
118
|
+
<PageLayout backButton={backCfg}>
|
|
119
|
+
<div className="flex flex-col gap-4">{customTitle}</div>
|
|
120
|
+
|
|
121
|
+
<div className="flex flex-col lg:flex-row gap-6 lg:gap-10 items-start flex-1">
|
|
122
|
+
<div className="flex-1">
|
|
123
|
+
<div className="w-full">
|
|
124
|
+
<article className="space-y-2">
|
|
125
|
+
{isLoading ? (
|
|
126
|
+
// Loading skeleton matching Knowledge Hub pattern
|
|
127
|
+
<div className="space-y-6">
|
|
128
|
+
<div className="h-10 bg-ods-skeleton rounded-lg w-3/4 animate-pulse"></div>
|
|
129
|
+
<div className="space-y-4">
|
|
130
|
+
<div className="h-4 bg-ods-skeleton rounded w-full animate-pulse"></div>
|
|
131
|
+
<div className="h-4 bg-ods-skeleton rounded w-full animate-pulse"></div>
|
|
132
|
+
<div className="h-4 bg-ods-skeleton rounded w-5/6 animate-pulse"></div>
|
|
133
|
+
</div>
|
|
134
|
+
<div className="h-32 bg-ods-card border border-ods-border rounded-lg animate-pulse"></div>
|
|
135
|
+
<div className="space-y-4">
|
|
136
|
+
<div className="h-4 bg-ods-skeleton rounded w-full animate-pulse"></div>
|
|
137
|
+
<div className="h-4 bg-ods-skeleton rounded w-4/5 animate-pulse"></div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
) : error ? (
|
|
141
|
+
<div className="text-center space-y-4">
|
|
142
|
+
<div className="bg-red-900/20 border border-red-700 rounded-lg p-6">
|
|
143
|
+
<p className="text-red-400 mb-2">{errorTitle}</p>
|
|
144
|
+
<p className="text-red-300 text-sm">{error}</p>
|
|
145
|
+
</div>
|
|
146
|
+
<div className="text-ods-text-secondary">
|
|
147
|
+
<p>{errorContactPrompt}</p>
|
|
148
|
+
<a href={`mailto:${contactEmail}`} className="text-ods-accent hover:underline">
|
|
149
|
+
{contactEmail}
|
|
150
|
+
</a>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
) : data ? (
|
|
154
|
+
<MarkdownRenderer
|
|
155
|
+
content={data.content}
|
|
156
|
+
sectionIds={data.sections || []}
|
|
157
|
+
demoteMarkdownH1ToH2
|
|
158
|
+
/>
|
|
159
|
+
) : (
|
|
160
|
+
<div className="text-center text-ods-text-secondary py-16">
|
|
161
|
+
<p className="text-xl">{emptyStateMessage}</p>
|
|
162
|
+
<p className="mt-2">
|
|
163
|
+
Please contact{' '}
|
|
164
|
+
<a href={`mailto:${contactEmail}`} className="text-ods-accent hover:underline">
|
|
165
|
+
{contactEmail}
|
|
166
|
+
</a>{' '}
|
|
167
|
+
for more information.
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</article>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</PageLayout>
|
|
176
|
+
</PageShell>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useLegalDocs — fetches a legal document (privacy policy, terms of
|
|
5
|
+
* service, or any other markdown-backed legal page) from a hub API.
|
|
6
|
+
*
|
|
7
|
+
* Endpoint configuration — `apiEndpoint`:
|
|
8
|
+
* Default `/api/legal/<docType>`. Reverse-proxy embedders override
|
|
9
|
+
* with their proxied path (e.g. `/proxy/legal/privacy`).
|
|
10
|
+
*
|
|
11
|
+
* Data shape mirrors the hub's `lib/data/legal-utils.ts:LegalDocument`
|
|
12
|
+
* server type. The hook intentionally re-declares the type here so
|
|
13
|
+
* lib consumers don't need to import a server-side type.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
17
|
+
|
|
18
|
+
export interface LegalDocument {
|
|
19
|
+
title: string;
|
|
20
|
+
content: string;
|
|
21
|
+
sourceFile: string;
|
|
22
|
+
lastSynced: string | null;
|
|
23
|
+
githubSha: string | null;
|
|
24
|
+
sections: Array<{ id: string; title: string; level: number }>;
|
|
25
|
+
docType: string;
|
|
26
|
+
meta: {
|
|
27
|
+
sectionsCount: number;
|
|
28
|
+
contentLength: number;
|
|
29
|
+
lastSyncedAgo: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UseLegalDocsReturn {
|
|
34
|
+
data: LegalDocument | null;
|
|
35
|
+
isLoading: boolean;
|
|
36
|
+
error: string | null;
|
|
37
|
+
refetch: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface UseLegalDocsOptions {
|
|
41
|
+
/** Optional pre-fetched payload from server (SSR / RSC). When set,
|
|
42
|
+
* the hook skips the initial client fetch. */
|
|
43
|
+
initialData?: LegalDocument | null;
|
|
44
|
+
/** Full GET endpoint URL. Default `/api/legal/<docType>`. */
|
|
45
|
+
apiEndpoint?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Hook to fetch a legal document.
|
|
50
|
+
* @param docType — short identifier for the document (drives the
|
|
51
|
+
* default endpoint path AND the error-log prefix). Common values:
|
|
52
|
+
* `'privacy'` (SECURITY.md), `'terms'` (LICENSE). Embedders may use
|
|
53
|
+
* any string — the hook treats it as opaque.
|
|
54
|
+
*/
|
|
55
|
+
export function useLegalDocs(
|
|
56
|
+
docType: string,
|
|
57
|
+
options: UseLegalDocsOptions = {}
|
|
58
|
+
): UseLegalDocsReturn {
|
|
59
|
+
const { initialData = null, apiEndpoint } = options;
|
|
60
|
+
const effectiveEndpoint = apiEndpoint ?? `/api/legal/${docType}`;
|
|
61
|
+
|
|
62
|
+
const [data, setData] = useState<LegalDocument | null>(initialData ?? null);
|
|
63
|
+
const [isLoading, setIsLoading] = useState(!initialData);
|
|
64
|
+
const [error, setError] = useState<string | null>(null);
|
|
65
|
+
|
|
66
|
+
const fetchDocument = useCallback(async () => {
|
|
67
|
+
try {
|
|
68
|
+
setIsLoading(true);
|
|
69
|
+
setError(null);
|
|
70
|
+
|
|
71
|
+
const response = await fetch(effectiveEndpoint);
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Failed to fetch ${docType} document: ${response.status} ${response.statusText}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = await response.json();
|
|
80
|
+
|
|
81
|
+
// Validate the response has required fields
|
|
82
|
+
if (!result.content) {
|
|
83
|
+
throw new Error(`${docType} document content is empty`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setData(result);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
89
|
+
console.error(`Error fetching ${docType} document:`, err);
|
|
90
|
+
setError(errorMessage);
|
|
91
|
+
} finally {
|
|
92
|
+
setIsLoading(false);
|
|
93
|
+
}
|
|
94
|
+
}, [docType, effectiveEndpoint]);
|
|
95
|
+
|
|
96
|
+
// Reset cached data when docType changes — otherwise an embedder using
|
|
97
|
+
// the same hook instance for sequential docTypes (privacy → terms)
|
|
98
|
+
// would briefly render the OLD doc's content while the new fetch is
|
|
99
|
+
// in-flight. Not currently triggered by hub's per-route SSR (each
|
|
100
|
+
// docType mounts in a fresh component), but enforces the contract.
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
setData(initialData ?? null);
|
|
103
|
+
setError(null);
|
|
104
|
+
setIsLoading(!initialData);
|
|
105
|
+
}, [docType, initialData]);
|
|
106
|
+
|
|
107
|
+
// Fetch on mount (only if we don't already have server-provided initialData)
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (initialData) return;
|
|
110
|
+
fetchDocument();
|
|
111
|
+
}, [fetchDocument, initialData]);
|
|
112
|
+
|
|
113
|
+
const refetch = () => {
|
|
114
|
+
fetchDocument();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
data,
|
|
119
|
+
isLoading,
|
|
120
|
+
error,
|
|
121
|
+
refetch,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -8,8 +8,19 @@ export {
|
|
|
8
8
|
type VideoDisplaySectionProps,
|
|
9
9
|
type MarkdownRendererProps,
|
|
10
10
|
type RoadmapItem,
|
|
11
|
-
type DeliveryResponse,
|
|
12
|
-
type RoadmapSectionProps,
|
|
13
|
-
type DeliverySectionProps
|
|
14
11
|
} from './release-detail-page'
|
|
12
|
+
// NOTE: `RoadmapSectionProps` / `DeliverySectionProps` (the injectable-
|
|
13
|
+
// component slot types for ReleaseDetailPage) are intentionally NOT
|
|
14
|
+
// re-exported from this barrel — they collide with the prop types of
|
|
15
|
+
// the concrete `<RoadmapGrid>` / `<DeliverySection>` components in
|
|
16
|
+
// `./shared/{roadmap,delivery}` (TS2308 ambiguous re-export at the
|
|
17
|
+
// top-level `components/index.ts` barrel). The slot types remain
|
|
18
|
+
// internal to `release-detail-page.tsx`; consumers needing them can
|
|
19
|
+
// import directly from
|
|
20
|
+
// `@flamingo-stack/openframe-frontend-core/components/shared/product-release/release-detail-page`.
|
|
21
|
+
// DeliveryResponse re-sourced from the canonical types module so the
|
|
22
|
+
// public deep-import path `@flamingo-stack/openframe-frontend-core/components`
|
|
23
|
+
// keeps resolving (hub's components/releases/release-detail-page.tsx
|
|
24
|
+
// imports it through this barrel).
|
|
25
|
+
export type { DeliveryResponse } from '../../../types/delivery'
|
|
15
26
|
export { ReleaseDetailSkeleton } from './release-detail-skeleton'
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, ComponentType } from 'react';
|
|
4
4
|
import Link from '../../../embed-shims/next-link';
|
|
5
|
+
import { useRouter } from '../../../embed-shims/next-navigation';
|
|
5
6
|
import { Card, CardContent } from '../../ui/card';
|
|
6
7
|
import { ArticleDetailLayout } from '../../layout/article-detail-layout';
|
|
8
|
+
import { BackButton } from '../../layout/back-button';
|
|
7
9
|
import { ReleaseChangelogSection } from '../../ui/release-changelog-section';
|
|
8
10
|
import { StatusBadge } from '../../ui/status-badge';
|
|
9
11
|
import { SquareAvatar } from '../../ui/square-avatar';
|
|
@@ -29,13 +31,15 @@ export interface MarkdownRendererProps {
|
|
|
29
31
|
// shape once the entities barrel was added; re-exporting the canonical
|
|
30
32
|
// type fixes the collision while keeping the same import path for
|
|
31
33
|
// downstream consumers of `./release-detail-page`.
|
|
32
|
-
export type { RoadmapItem } from '../../chat/types/entities/roadmap-item';
|
|
33
34
|
import type { RoadmapItem } from '../../chat/types/entities/roadmap-item';
|
|
34
|
-
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
import type { DeliveryResponse } from '../../../types/delivery';
|
|
36
|
+
// Re-export both types for source-compat with consumers importing
|
|
37
|
+
// through this module. Canonical sources:
|
|
38
|
+
// - RoadmapItem → `../../chat/types/entities/roadmap-item`
|
|
39
|
+
// - DeliveryResponse → `../../../types/delivery` (single source of
|
|
40
|
+
// truth, shared with the lib `<DeliveryLists>` / `<DeliveryTable>`
|
|
41
|
+
// components and the new types barrel).
|
|
42
|
+
export type { RoadmapItem, DeliveryResponse };
|
|
39
43
|
|
|
40
44
|
export interface RoadmapSectionProps {
|
|
41
45
|
items: RoadmapItem[];
|
|
@@ -91,6 +95,10 @@ export interface ReleaseDetailPageProps {
|
|
|
91
95
|
// API endpoints for fetching linked tasks
|
|
92
96
|
roadmapApiEndpoint?: string;
|
|
93
97
|
deliveryApiEndpoint?: string;
|
|
98
|
+
/** Back-button config — same pattern as `DevSectionPage` /
|
|
99
|
+
* `LegalDocumentPage`. Pass `false` to hide. Default
|
|
100
|
+
* `{ label: 'Back to home', href: '/' }`. */
|
|
101
|
+
backButton?: { label?: string; href?: string } | false;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
// Default simple markdown renderer (just renders as text)
|
|
@@ -108,14 +116,26 @@ export function ReleaseDetailPage({
|
|
|
108
116
|
VideoSection,
|
|
109
117
|
VideoDisplaySection,
|
|
110
118
|
roadmapApiEndpoint = '/api/roadmap',
|
|
111
|
-
deliveryApiEndpoint = '/api/delivery'
|
|
119
|
+
deliveryApiEndpoint = '/api/delivery',
|
|
120
|
+
backButton
|
|
112
121
|
}: ReleaseDetailPageProps) {
|
|
122
|
+
const router = useRouter();
|
|
113
123
|
// Use pre-fetched data if provided (admin preview), otherwise fetch via hook (public)
|
|
114
124
|
const { data: fetchedRelease, error, isLoading } = useRelease(initialData ? undefined : slug);
|
|
115
125
|
const release = (initialData || fetchedRelease) as Record<string, unknown> | undefined;
|
|
116
126
|
const [galleryOpen, setGalleryOpen] = useState(false);
|
|
117
127
|
const [galleryIndex, setGalleryIndex] = useState(0);
|
|
118
128
|
|
|
129
|
+
// Back-button config — mirrors DevSectionPage / LegalDocumentPage.
|
|
130
|
+
// Default: { label: 'Back to home', href: '/' }. Pass `false` to hide
|
|
131
|
+
// (e.g. embed-mode where the host owns navigation chrome).
|
|
132
|
+
// Narrowing note: `backButton &&` already eliminates the `false` branch,
|
|
133
|
+
// so the inner expressions are typed as `{ label?, href? } | undefined`.
|
|
134
|
+
// Don't re-compare to `false` here — tsc TS2367s on the dead branch.
|
|
135
|
+
const showBackButton = backButton !== false;
|
|
136
|
+
const backLabel = (backButton ? backButton.label : undefined) ?? 'Back to home';
|
|
137
|
+
const backHref = (backButton ? backButton.href : undefined) ?? '/';
|
|
138
|
+
|
|
119
139
|
// Fetch roadmap and delivery tasks if linked to this release
|
|
120
140
|
const [roadmapTasks, setRoadmapTasks] = useState<RoadmapItem[]>([]);
|
|
121
141
|
const [deliveryData, setDeliveryData] = useState<DeliveryResponse | null>(null);
|
|
@@ -201,6 +221,15 @@ export function ReleaseDetailPage({
|
|
|
201
221
|
|
|
202
222
|
return (
|
|
203
223
|
<ArticleDetailLayout>
|
|
224
|
+
{/* Back button — desktop-only, matches DevSectionPage / LegalDocumentPage
|
|
225
|
+
(TitleBlock renders the same `hidden md:inline-flex` BackButton). */}
|
|
226
|
+
{showBackButton && (
|
|
227
|
+
<BackButton
|
|
228
|
+
label={backLabel}
|
|
229
|
+
onClick={() => router.push(backHref)}
|
|
230
|
+
className="hidden md:inline-flex mb-4"
|
|
231
|
+
/>
|
|
232
|
+
)}
|
|
204
233
|
<div className="space-y-6 md:space-y-8">
|
|
205
234
|
{/* Title Block */}
|
|
206
235
|
<div className="flex flex-col md:flex-row md:items-end gap-4 w-full">
|
|
@@ -399,22 +428,31 @@ export function ReleaseDetailPage({
|
|
|
399
428
|
icon={<AlertTriangle className="h-6 w-6" />}
|
|
400
429
|
SimpleMarkdownRenderer={MarkdownRenderer}
|
|
401
430
|
/>
|
|
431
|
+
{/* Features / Bugs / Improvements use `previewFirst` — same
|
|
432
|
+
progressive-disclosure pattern as the investor-update detail
|
|
433
|
+
page's Key Highlights / Financial Notes sections. Shows the
|
|
434
|
+
first entry in full + fade-masks the rest, with a "Show N
|
|
435
|
+
more / Show less" toggle. Breaking Changes (above) stays
|
|
436
|
+
fully expanded — it's critical info, not skim-friendly. */}
|
|
402
437
|
<ReleaseChangelogSection
|
|
403
438
|
title="Features Added"
|
|
404
439
|
entries={featuresAdded || []}
|
|
405
440
|
icon={<Sparkles className="h-6 w-6" />}
|
|
441
|
+
previewFirst
|
|
406
442
|
SimpleMarkdownRenderer={MarkdownRenderer}
|
|
407
443
|
/>
|
|
408
444
|
<ReleaseChangelogSection
|
|
409
445
|
title="Bugs Fixed"
|
|
410
446
|
entries={bugFixed || []}
|
|
411
447
|
icon={<Wrench className="h-6 w-6" />}
|
|
448
|
+
previewFirst
|
|
412
449
|
SimpleMarkdownRenderer={MarkdownRenderer}
|
|
413
450
|
/>
|
|
414
451
|
<ReleaseChangelogSection
|
|
415
452
|
title="Improvements"
|
|
416
453
|
entries={improvements || []}
|
|
417
454
|
icon={<TrendingUp className="h-6 w-6" />}
|
|
455
|
+
previewFirst
|
|
418
456
|
SimpleMarkdownRenderer={MarkdownRenderer}
|
|
419
457
|
/>
|
|
420
458
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared roadmap surface barrel.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: This barrel MUST NOT re-export `RoadmapItem`. The canonical
|
|
5
|
+
* type is exported via two paths already:
|
|
6
|
+
* - `@flamingo-stack/openframe-frontend-core/components/chat`
|
|
7
|
+
* (the source of truth at `chat/types/entities/roadmap-item.ts`)
|
|
8
|
+
* - `@flamingo-stack/openframe-frontend-core/components` (via
|
|
9
|
+
* `./shared/product-release` re-export)
|
|
10
|
+
*
|
|
11
|
+
* Adding a third re-export path here would trigger TypeScript's TS2308
|
|
12
|
+
* ambiguous re-export warning. Consumers needing the type should import
|
|
13
|
+
* it from one of the two existing paths.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export { RoadmapGrid, type RoadmapGridProps } from './roadmap-grid';
|
|
17
|
+
export { RoadmapGridSkeleton, type RoadmapGridSkeletonProps } from './roadmap-grid-skeleton';
|
|
18
|
+
// `VoteType` deliberately NOT re-exported — `./chat` already exports
|
|
19
|
+
// the same-shape `VoteType` from `roadmap-card.tsx`; a duplicate path
|
|
20
|
+
// triggers TS2308 ambiguous re-export at the top-level
|
|
21
|
+
// `components/index.ts` barrel. Consumers can import the canonical
|
|
22
|
+
// `VoteType` from `@flamingo-stack/openframe-frontend-core/components/chat`.
|
|
23
|
+
export { useRoadmapVoting, type VoteState, type UseRoadmapVotingOptions } from './use-roadmap-voting';
|