@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.
Files changed (112) hide show
  1. package/dist/chunk-4XLJWX2N.js +12 -0
  2. package/dist/chunk-4XLJWX2N.js.map +1 -0
  3. package/dist/{chunk-OLTGB32E.js → chunk-6WMMLMKM.js} +2857 -2045
  4. package/dist/chunk-6WMMLMKM.js.map +1 -0
  5. package/dist/chunk-VFKQMAUF.cjs +12 -0
  6. package/dist/chunk-VFKQMAUF.cjs.map +1 -0
  7. package/dist/{chunk-YGOJIDL5.cjs → chunk-WYLNTZZ7.cjs} +1343 -531
  8. package/dist/chunk-WYLNTZZ7.cjs.map +1 -0
  9. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
  10. package/dist/components/chat/index.cjs +3 -2
  11. package/dist/components/chat/index.cjs.map +1 -1
  12. package/dist/components/chat/index.js +2 -1
  13. package/dist/components/chat/types/api.types.d.ts +17 -1
  14. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  15. package/dist/components/features/index.cjs +3 -2
  16. package/dist/components/features/index.cjs.map +1 -1
  17. package/dist/components/features/index.js +2 -1
  18. package/dist/components/index.cjs +21 -2
  19. package/dist/components/index.cjs.map +1 -1
  20. package/dist/components/index.d.ts +4 -0
  21. package/dist/components/index.d.ts.map +1 -1
  22. package/dist/components/index.js +20 -1
  23. package/dist/components/navigation/index.cjs +3 -2
  24. package/dist/components/navigation/index.cjs.map +1 -1
  25. package/dist/components/navigation/index.js +2 -1
  26. package/dist/components/shared/delivery/delivery-lists.d.ts +16 -0
  27. package/dist/components/shared/delivery/delivery-lists.d.ts.map +1 -0
  28. package/dist/components/shared/delivery/delivery-table.d.ts +12 -0
  29. package/dist/components/shared/delivery/delivery-table.d.ts.map +1 -0
  30. package/dist/components/shared/delivery/index.d.ts +3 -0
  31. package/dist/components/shared/delivery/index.d.ts.map +1 -0
  32. package/dist/components/shared/dev-section/dev-section-page.d.ts +31 -0
  33. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -0
  34. package/dist/components/shared/dev-section/dev-section-view.d.ts +34 -0
  35. package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -0
  36. package/dist/components/shared/dev-section/index.d.ts +3 -0
  37. package/dist/components/shared/dev-section/index.d.ts.map +1 -0
  38. package/dist/components/shared/legal-document/index.d.ts +10 -0
  39. package/dist/components/shared/legal-document/index.d.ts.map +1 -0
  40. package/dist/components/shared/legal-document/legal-document-page.d.ts +66 -0
  41. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -0
  42. package/dist/components/shared/legal-document/use-legal-docs.d.ts +40 -0
  43. package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -0
  44. package/dist/components/shared/product-release/index.d.ts +2 -1
  45. package/dist/components/shared/product-release/index.d.ts.map +1 -1
  46. package/dist/components/shared/product-release/release-detail-page.d.ts +11 -7
  47. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  48. package/dist/components/shared/roadmap/index.d.ts +18 -0
  49. package/dist/components/shared/roadmap/index.d.ts.map +1 -0
  50. package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts +24 -0
  51. package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts.map +1 -0
  52. package/dist/components/shared/roadmap/roadmap-grid.d.ts +18 -0
  53. package/dist/components/shared/roadmap/roadmap-grid.d.ts.map +1 -0
  54. package/dist/components/shared/roadmap/use-roadmap-voting.d.ts +25 -0
  55. package/dist/components/shared/roadmap/use-roadmap-voting.d.ts.map +1 -0
  56. package/dist/components/ui/index.cjs +3 -2
  57. package/dist/components/ui/index.cjs.map +1 -1
  58. package/dist/components/ui/index.js +2 -1
  59. package/dist/components/ui/release-changelog-section.d.ts +13 -2
  60. package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
  61. package/dist/embed-shims/index.cjs +1 -6
  62. package/dist/embed-shims/index.cjs.map +1 -1
  63. package/dist/embed-shims/index.js +1 -6
  64. package/dist/embed-shims/index.js.map +1 -1
  65. package/dist/index.cjs +13 -2
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.js +12 -1
  68. package/dist/types/delivery.d.ts +49 -0
  69. package/dist/types/delivery.d.ts.map +1 -0
  70. package/dist/types/index.cjs +13 -0
  71. package/dist/types/index.cjs.map +1 -1
  72. package/dist/types/index.d.ts +1 -0
  73. package/dist/types/index.d.ts.map +1 -1
  74. package/dist/types/index.js +12 -1
  75. package/dist/types/index.js.map +1 -1
  76. package/dist/utils/dev-sections/index.d.ts +11 -0
  77. package/dist/utils/dev-sections/index.d.ts.map +1 -0
  78. package/dist/utils/dev-sections/openframe-dev-sections.d.ts +209 -0
  79. package/dist/utils/dev-sections/openframe-dev-sections.d.ts.map +1 -0
  80. package/dist/utils/index.cjs +82 -0
  81. package/dist/utils/index.cjs.map +1 -1
  82. package/dist/utils/index.d.ts +1 -0
  83. package/dist/utils/index.d.ts.map +1 -1
  84. package/dist/utils/index.js +81 -2
  85. package/dist/utils/index.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +53 -6
  88. package/src/components/chat/types/api.types.ts +23 -1
  89. package/src/components/index.ts +8 -0
  90. package/src/components/shared/delivery/delivery-lists.tsx +199 -0
  91. package/src/components/shared/delivery/delivery-table.tsx +174 -0
  92. package/src/components/shared/delivery/index.ts +9 -0
  93. package/src/components/shared/dev-section/dev-section-page.tsx +72 -0
  94. package/src/components/shared/dev-section/dev-section-view.tsx +129 -0
  95. package/src/components/shared/dev-section/index.ts +2 -0
  96. package/src/components/shared/legal-document/index.ts +19 -0
  97. package/src/components/shared/legal-document/legal-document-page.tsx +178 -0
  98. package/src/components/shared/legal-document/use-legal-docs.ts +123 -0
  99. package/src/components/shared/product-release/index.ts +14 -3
  100. package/src/components/shared/product-release/release-detail-page.tsx +45 -7
  101. package/src/components/shared/roadmap/index.ts +23 -0
  102. package/src/components/shared/roadmap/roadmap-grid-skeleton.tsx +74 -0
  103. package/src/components/shared/roadmap/roadmap-grid.tsx +106 -0
  104. package/src/components/shared/roadmap/use-roadmap-voting.ts +163 -0
  105. package/src/components/ui/release-changelog-section.tsx +113 -32
  106. package/src/types/delivery.ts +54 -0
  107. package/src/types/index.ts +1 -0
  108. package/src/utils/dev-sections/index.ts +17 -0
  109. package/src/utils/dev-sections/openframe-dev-sections.ts +148 -0
  110. package/src/utils/index.ts +6 -1
  111. package/dist/chunk-OLTGB32E.js.map +0 -1
  112. 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,2 @@
1
+ export { DevSectionView, type DevSectionViewProps } from './dev-section-view';
2
+ export { DevSectionPage, type DevSectionPageProps } from './dev-section-page';
@@ -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 interface DeliveryResponse {
36
- completed: unknown[];
37
- inProgress: unknown[];
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';