@flamingo-stack/openframe-frontend-core 0.0.207 → 0.0.208
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-Z3GQGR5E.js → chunk-2HMZSCJY.js} +3158 -2074
- package/dist/chunk-2HMZSCJY.js.map +1 -0
- package/dist/chunk-4XLJWX2N.js +12 -0
- package/dist/chunk-4XLJWX2N.js.map +1 -0
- package/dist/{chunk-APM6KBPU.cjs → chunk-C5EC5AZM.cjs} +1644 -560
- package/dist/chunk-C5EC5AZM.cjs.map +1 -0
- package/dist/chunk-VFKQMAUF.cjs +12 -0
- package/dist/chunk-VFKQMAUF.cjs.map +1 -0
- package/dist/components/chat/embeddable-chat.d.ts +35 -2
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/index.d.ts +3 -0
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-embedded-chat.d.ts +10 -169
- package/dist/components/chat/hooks/use-embedded-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +85 -0
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +124 -0
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-unified-chat.d.ts +33 -0
- package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -0
- package/dist/components/chat/index.cjs +8 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +11 -5
- package/dist/components/chat/types/index.d.ts +1 -0
- package/dist/components/chat/types/index.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +185 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -0
- 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 +26 -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 +27 -3
- 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 +8 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +11 -5
- 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 +18 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +19 -3
- 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/embeddable-chat.tsx +123 -8
- package/src/components/chat/hooks/index.ts +9 -2
- package/src/components/chat/hooks/use-embedded-chat.ts +18 -1016
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +372 -0
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +1058 -0
- package/src/components/chat/hooks/use-unified-chat.ts +171 -0
- package/src/components/chat/types/index.ts +1 -0
- package/src/components/chat/types/unified-chat-state.types.ts +215 -0
- 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/stories/EmbeddableChat.stories.tsx +186 -0
- 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-APM6KBPU.cjs.map +0 -1
- package/dist/chunk-Z3GQGR5E.js.map +0 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { StatusBadge } from '../../ui';
|
|
4
|
+
import { getStatusColorScheme } from '../../../utils';
|
|
5
|
+
import {
|
|
6
|
+
type DeliveryItem,
|
|
7
|
+
TASK_TYPE_LABELS,
|
|
8
|
+
TASK_TYPE_TEXT_COLORS,
|
|
9
|
+
} from '../../../types/delivery';
|
|
10
|
+
|
|
11
|
+
interface DeliveryTableProps {
|
|
12
|
+
items: DeliveryItem[];
|
|
13
|
+
isLoading?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Format relative time for display
|
|
18
|
+
*/
|
|
19
|
+
function getRelativeTime(timestamp: number): string {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const diff = now - timestamp;
|
|
22
|
+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
23
|
+
const weeks = Math.floor(days / 7);
|
|
24
|
+
const months = Math.floor(days / 30);
|
|
25
|
+
|
|
26
|
+
if (months > 0) {
|
|
27
|
+
return months === 1 ? 'last month' : `${months} months ago`;
|
|
28
|
+
}
|
|
29
|
+
if (weeks > 0) {
|
|
30
|
+
return weeks === 1 ? 'last week' : `${weeks} weeks ago`;
|
|
31
|
+
}
|
|
32
|
+
if (days > 0) {
|
|
33
|
+
return days === 1 ? 'yesterday' : `${days} days ago`;
|
|
34
|
+
}
|
|
35
|
+
return 'today';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Skeleton loader for rows - matching responsive structure
|
|
40
|
+
*/
|
|
41
|
+
function SkeletonRow() {
|
|
42
|
+
return (
|
|
43
|
+
<div className="border-b border-ods-border last:border-b-0 p-[12px] md:p-[16px]">
|
|
44
|
+
<div className="flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full">
|
|
45
|
+
{/* Left: Title, subtitle, and description skeleton */}
|
|
46
|
+
<div className="flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]">
|
|
47
|
+
{/* Title skeleton - responsive */}
|
|
48
|
+
<div className="min-h-[24px] flex items-center">
|
|
49
|
+
<div className="h-[20px] bg-ods-border rounded animate-pulse w-full"></div>
|
|
50
|
+
</div>
|
|
51
|
+
{/* Subtitle skeleton - 1 line */}
|
|
52
|
+
<div className="min-h-[20px] flex items-center">
|
|
53
|
+
<div className="h-[20px] bg-ods-border rounded animate-pulse w-1/2"></div>
|
|
54
|
+
</div>
|
|
55
|
+
{/* Description skeleton - 3 lines */}
|
|
56
|
+
<div className="min-h-[72px] flex items-center">
|
|
57
|
+
<div className="flex-1 space-y-1">
|
|
58
|
+
<div className="h-[20px] bg-ods-border rounded animate-pulse w-full"></div>
|
|
59
|
+
<div className="h-[20px] bg-ods-border rounded animate-pulse w-full"></div>
|
|
60
|
+
<div className="h-[20px] bg-ods-border rounded animate-pulse w-2/3"></div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Right: Badge skeleton - two stacked badges */}
|
|
66
|
+
<div className="flex-shrink-0 self-start flex flex-col gap-2">
|
|
67
|
+
<div className="h-[32px] w-[100px] bg-ods-border rounded animate-pulse"></div>
|
|
68
|
+
<div className="h-[32px] w-[120px] bg-ods-border rounded animate-pulse"></div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* DeliveryTable Component
|
|
77
|
+
* Displays bug fixes and enhancements with fixed-height rows
|
|
78
|
+
*/
|
|
79
|
+
export function DeliveryTable({ items, isLoading = false }: DeliveryTableProps) {
|
|
80
|
+
// Show skeletons while loading
|
|
81
|
+
if (isLoading) {
|
|
82
|
+
return (
|
|
83
|
+
<div className="bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full">
|
|
84
|
+
<div className="w-full">
|
|
85
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
86
|
+
<SkeletonRow key={i} />
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Empty state
|
|
94
|
+
if (items.length === 0) {
|
|
95
|
+
return (
|
|
96
|
+
<div className="bg-ods-card border border-ods-border rounded-[6px] p-[40px] text-center w-full">
|
|
97
|
+
<p className="text-ods-text-secondary text-[14px] font-['DM_Sans'] font-medium">
|
|
98
|
+
No tasks available
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full">
|
|
106
|
+
<div className="w-full">
|
|
107
|
+
{items.map((item, index) => {
|
|
108
|
+
// Get task type badge label and text color
|
|
109
|
+
const taskType = item.taskType as keyof typeof TASK_TYPE_LABELS;
|
|
110
|
+
const typeBadgeLabel = TASK_TYPE_LABELS[taskType] || 'TASK';
|
|
111
|
+
const typeBadgeTextColor = TASK_TYPE_TEXT_COLORS[taskType] || '';
|
|
112
|
+
|
|
113
|
+
// Get status badge color scheme using centralized utility
|
|
114
|
+
const statusBadgeScheme = getStatusColorScheme(item.status);
|
|
115
|
+
|
|
116
|
+
// Calculate relative time from last activity (dateUpdated)
|
|
117
|
+
const relativeTime = getRelativeTime(item.dateUpdated);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
key={item.id}
|
|
122
|
+
className={`border-b border-ods-border last:border-b-0 p-[12px] md:p-[16px] ${index === 0 ? '' : ''}`}
|
|
123
|
+
>
|
|
124
|
+
<div className="flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full">
|
|
125
|
+
{/* Left: Title, subtitle, and description - matching roadmap-card.tsx structure */}
|
|
126
|
+
<div className="flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]">
|
|
127
|
+
{/* Title: 2 lines on mobile, 1 line on desktop */}
|
|
128
|
+
<div className="min-h-[24px] md:min-h-[24px] flex items-center">
|
|
129
|
+
<h3 className="text-h3 text-ods-text-primary tracking-[-0.36px] flex-1 line-clamp-2 md:truncate break-words">
|
|
130
|
+
{item.title}
|
|
131
|
+
</h3>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Subtitle: 1 line with last activity date, list name(s), task ID - Azeret Mono.
|
|
135
|
+
A task can live in multiple ClickUp lists ("Tasks in Multiple Lists" feature) —
|
|
136
|
+
we render every list joined by ", ". */}
|
|
137
|
+
<div className="min-h-[20px] flex items-center">
|
|
138
|
+
<p className="text-h5 text-ods-text-secondary uppercase tracking-[-0.28px] truncate">
|
|
139
|
+
ACTIVE {relativeTime}{item.listNames.length > 0 ? `, ${item.listNames.join(', ')}` : ''}, {item.id}
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Description: 3 lines max, 72px height with vertical centering - matching roadmap */}
|
|
144
|
+
<div className="min-h-[72px] flex items-center">
|
|
145
|
+
<p className="text-h4 text-ods-text-secondary line-clamp-3 break-words">
|
|
146
|
+
{item.description || 'No description provided'}
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Right: Status and Type badges stacked vertically */}
|
|
152
|
+
<div className="flex-shrink-0 self-start flex flex-col gap-2">
|
|
153
|
+
{/* Status Badge - matching roadmap cards */}
|
|
154
|
+
<StatusBadge
|
|
155
|
+
text={item.status.toUpperCase()}
|
|
156
|
+
colorScheme={statusBadgeScheme}
|
|
157
|
+
variant="card"
|
|
158
|
+
className="border border-ods-border"
|
|
159
|
+
/>
|
|
160
|
+
{/* Task Type Badge - same style as Version badge in roadmap */}
|
|
161
|
+
<StatusBadge
|
|
162
|
+
text={typeBadgeLabel}
|
|
163
|
+
variant="card"
|
|
164
|
+
className={`border border-ods-border ${typeBadgeTextColor}`}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
})}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// NOTE: `DeliverySection` (standalone unfiltered fetch) was deleted —
|
|
2
|
+
// it had zero consumers (hub's release-detail wraps `<DeliveryTable>`
|
|
3
|
+
// directly, the public bug-fixes-and-enhancements page uses
|
|
4
|
+
// `<DeliveryLists>` with URL-driven filters) and duplicated the
|
|
5
|
+
// fetch/loading state machine. If an embedder ever needs a no-filter
|
|
6
|
+
// variant, build it as `<DeliveryLists ignoreUrlFilters />` rather
|
|
7
|
+
// than reviving a separate component.
|
|
8
|
+
export { DeliveryLists, type DeliveryListsProps } from './delivery-lists';
|
|
9
|
+
export { DeliveryTable } from './delivery-table';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DevSectionPage — full-page wrapper for a dev-center section
|
|
5
|
+
* (`/roadmap`, `/bug-fixes-and-enhancements`, `/releases`).
|
|
6
|
+
*
|
|
7
|
+
* Mounts the lib's canonical `PageLayout` directly (no in-app wrapper)
|
|
8
|
+
* so the back-button affordance stays in lockstep with whatever the
|
|
9
|
+
* design system ships — any future lib change to BackButton / TitleBlock
|
|
10
|
+
* propagates automatically.
|
|
11
|
+
*
|
|
12
|
+
* Composition: `PageShell` → `PageLayout` (back-to-home wired) →
|
|
13
|
+
* `DevSectionView` (icon hero + search + filter pills) → list body.
|
|
14
|
+
*
|
|
15
|
+
* Adding a new section is one entry in `OPENFRAME_DEV_SECTIONS` plus a
|
|
16
|
+
* single-line page file mounting this factory with the new key.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ReactNode } from 'react';
|
|
20
|
+
import { useRouter } from '../../../embed-shims/next-navigation';
|
|
21
|
+
import { PageShell, PageLayout } from '../../ui';
|
|
22
|
+
import { DevSectionView } from './dev-section-view';
|
|
23
|
+
import {
|
|
24
|
+
OPENFRAME_DEV_SECTIONS,
|
|
25
|
+
type OpenframeDevSectionKey,
|
|
26
|
+
} from '../../../utils/dev-sections/openframe-dev-sections';
|
|
27
|
+
|
|
28
|
+
const SECTION_HERO_ICON_CLASS = 'h-10 w-10 text-ods-accent';
|
|
29
|
+
|
|
30
|
+
export interface DevSectionPageProps {
|
|
31
|
+
sectionKey: OpenframeDevSectionKey;
|
|
32
|
+
/** The page-specific list body (e.g. `<RoadmapList />`). */
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
/** Back-button config — same shape as `LegalDocumentPage` /
|
|
35
|
+
* `ReleaseDetailPage`. Pass `false` to hide. Default
|
|
36
|
+
* `{ label: 'Back to home', href: '/' }`. */
|
|
37
|
+
backButton?: { label?: string; href?: string } | false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function DevSectionPage({ sectionKey, children, backButton }: DevSectionPageProps) {
|
|
41
|
+
const router = useRouter();
|
|
42
|
+
const section = OPENFRAME_DEV_SECTIONS[sectionKey];
|
|
43
|
+
const Icon = section.icon;
|
|
44
|
+
|
|
45
|
+
// Back-button config — mirrors LegalDocumentPage / ReleaseDetailPage.
|
|
46
|
+
// Default: { label: 'Back to home', href: '/' }. Pass `false` to hide.
|
|
47
|
+
// After `backButton &&` narrowing, inner type is `{ label?, href? } |
|
|
48
|
+
// undefined`; don't re-compare to `false` (TS2367).
|
|
49
|
+
const backCfg =
|
|
50
|
+
backButton === false
|
|
51
|
+
? undefined
|
|
52
|
+
: {
|
|
53
|
+
label: (backButton ? backButton.label : undefined) ?? 'Back to home',
|
|
54
|
+
onClick: () => router.push((backButton ? backButton.href : undefined) ?? '/'),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<PageShell>
|
|
59
|
+
<PageLayout backButton={backCfg}>
|
|
60
|
+
<DevSectionView
|
|
61
|
+
sectionKey={sectionKey}
|
|
62
|
+
hero={{
|
|
63
|
+
icon: <Icon className={SECTION_HERO_ICON_CLASS} />,
|
|
64
|
+
description: section.hero.description,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</DevSectionView>
|
|
69
|
+
</PageLayout>
|
|
70
|
+
</PageShell>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -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
|
+
}
|