@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
package/package.json
CHANGED
|
@@ -55,11 +55,22 @@ export function useRealtimeChunkProcessor(
|
|
|
55
55
|
})
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// Resumed dialog: a MESSAGE_START already fired server-side. Treat
|
|
59
|
+
// subsequent continuation chunks (after the next MESSAGE_END) as
|
|
60
|
+
// post-stream so they append into the existing bubble instead of
|
|
61
|
+
// replacing its content via the cold-start cumulative path.
|
|
62
|
+
hasEverStreamedRef.current = true
|
|
58
63
|
hasInitializedWithData.current = true
|
|
59
64
|
}
|
|
60
65
|
}, [initialState, callbacks])
|
|
61
66
|
|
|
62
67
|
const isInStreamRef = useRef(false)
|
|
68
|
+
// Distinguishes post-MESSAGE_END continuation (append into prior bubble)
|
|
69
|
+
// from cold-start before any MESSAGE_START (cumulative; otherwise
|
|
70
|
+
// appendSegmentsToLastAssistant silently drops the chunk when no
|
|
71
|
+
// assistant bubble exists yet). Flipped true on MESSAGE_START and on
|
|
72
|
+
// resumed-dialog initializeWithState.
|
|
73
|
+
const hasEverStreamedRef = useRef(false)
|
|
63
74
|
|
|
64
75
|
// Track pending escalated approvals (single or batch)
|
|
65
76
|
const pendingEscalatedRef = useRef<
|
|
@@ -85,6 +96,7 @@ export function useRealtimeChunkProcessor(
|
|
|
85
96
|
switch (action.action) {
|
|
86
97
|
case 'message_start':
|
|
87
98
|
isInStreamRef.current = true
|
|
99
|
+
hasEverStreamedRef.current = true
|
|
88
100
|
callbacks.onStreamStart?.()
|
|
89
101
|
accumulator.resetSegments()
|
|
90
102
|
break
|
|
@@ -101,17 +113,43 @@ export function useRealtimeChunkProcessor(
|
|
|
101
113
|
|
|
102
114
|
case 'text': {
|
|
103
115
|
const segments = accumulator.appendText(action.text)
|
|
104
|
-
|
|
116
|
+
// Append-mode only for *true* post-stream continuation (after a
|
|
117
|
+
// MESSAGE_END we actually saw). Cold-start chunks (no prior
|
|
118
|
+
// MESSAGE_START) emit cumulative segments so the consumer can
|
|
119
|
+
// spawn the first assistant bubble — otherwise appendSegmentsToLastAssistant
|
|
120
|
+
// silently drops the chunk when no last assistant exists.
|
|
121
|
+
if (isInStreamRef.current || !hasEverStreamedRef.current) {
|
|
122
|
+
callbacks.onSegmentsUpdate?.(segments)
|
|
123
|
+
} else {
|
|
124
|
+
callbacks.onSegmentsUpdate?.([{ type: 'text', text: action.text }], { append: true })
|
|
125
|
+
}
|
|
105
126
|
break
|
|
106
127
|
}
|
|
107
128
|
|
|
108
129
|
case 'thinking': {
|
|
109
130
|
const segments = accumulator.appendThinking(action.text)
|
|
110
|
-
|
|
131
|
+
if (isInStreamRef.current || !hasEverStreamedRef.current) {
|
|
132
|
+
callbacks.onSegmentsUpdate?.(segments)
|
|
133
|
+
} else {
|
|
134
|
+
callbacks.onSegmentsUpdate?.([{ type: 'thinking', text: action.text }], { append: true })
|
|
135
|
+
}
|
|
111
136
|
break
|
|
112
137
|
}
|
|
113
138
|
|
|
114
139
|
case 'tool_execution': {
|
|
140
|
+
// Post-MESSAGE_END tool chunks (cancellations / async batch
|
|
141
|
+
// results for a batch in a prior bubble) flow only through the
|
|
142
|
+
// cross-message updater. Skipping the accumulator avoids
|
|
143
|
+
// pushing a standalone segment that the next text chunk would
|
|
144
|
+
// replay into a new bubble.
|
|
145
|
+
if (!isInStreamRef.current && callbacks.onToolExecuted) {
|
|
146
|
+
callbacks.onToolExecuted(action.segment)
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
// In-stream: accumulator-driven update of the streaming bubble
|
|
150
|
+
// is the source of truth. Don't fire onToolExecuted here — its
|
|
151
|
+
// cross-message scan is first-match-wins and could touch a
|
|
152
|
+
// same-execId segment in a prior bubble (agent retry case).
|
|
115
153
|
const segments = accumulator.addToolExecution(action.segment)
|
|
116
154
|
callbacks.onSegmentsUpdate?.(segments)
|
|
117
155
|
break
|
|
@@ -234,11 +272,20 @@ export function useRealtimeChunkProcessor(
|
|
|
234
272
|
callbacks.onSegmentsUpdate?.(segments)
|
|
235
273
|
}
|
|
236
274
|
} else {
|
|
237
|
-
|
|
238
|
-
|
|
275
|
+
// Always keep the in-memory accumulator in sync so a following
|
|
276
|
+
// text/tool chunk replays the resolved status into the message.
|
|
277
|
+
accumulator.updateApprovalStatus(requestId, status)
|
|
278
|
+
// When the consumer wires cross-message resolution via
|
|
279
|
+
// `onApprovalResolved`, skip `onSegmentsUpdate` here: this path
|
|
280
|
+
// routes through `ensureAssistantMessage` + `updateStreamingMessageSegments`,
|
|
281
|
+
// which adopts/creates an assistant bubble and replays the
|
|
282
|
+
// accumulator's segments into it — turning a status flip into a
|
|
283
|
+
// bubble overwrite that wipes the original card.
|
|
284
|
+
if (!callbacks.onApprovalResolved) {
|
|
285
|
+
callbacks.onSegmentsUpdate?.(accumulator.getSegments())
|
|
286
|
+
}
|
|
239
287
|
}
|
|
240
|
-
|
|
241
|
-
void approvalType
|
|
288
|
+
callbacks.onApprovalResolved?.(requestId, status, approvalType)
|
|
242
289
|
break
|
|
243
290
|
}
|
|
244
291
|
|
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ChunkData, NatsMessageType, FetchChunksFunction } from './network.types'
|
|
7
7
|
import type { ChatType, ChatApprovalStatus } from './chat.types'
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
MessageSegment,
|
|
10
|
+
PendingToolCallData,
|
|
11
|
+
TokenUsageData,
|
|
12
|
+
ExecutingToolState,
|
|
13
|
+
ToolExecutionSegment,
|
|
14
|
+
} from './message.types'
|
|
9
15
|
|
|
10
16
|
// ========== Hook Options ==========
|
|
11
17
|
|
|
@@ -169,6 +175,22 @@ export interface RealtimeChunkCallbacks {
|
|
|
169
175
|
onEscalatedApproval?: (requestId: string, data: { command: string; explanation?: string; approvalType: string }) => void
|
|
170
176
|
/** Called when an escalated approval result is received */
|
|
171
177
|
onEscalatedApprovalResult?: (requestId: string, approved: boolean, data: { command: string; explanation?: string; approvalType: string }) => void
|
|
178
|
+
/**
|
|
179
|
+
* Called whenever an `APPROVAL_RESULT` chunk is processed. Fires in addition
|
|
180
|
+
* to the accumulator's in-message status flip so consumers can find the
|
|
181
|
+
* matching `approval_request` / `approval_batch` segment in an *earlier*
|
|
182
|
+
* message bubble (e.g. when a user-interrupted approval is resolved while
|
|
183
|
+
* a new assistant message is streaming). Idempotent — safe to no-op if no
|
|
184
|
+
* matching segment is found dialog-wide.
|
|
185
|
+
*/
|
|
186
|
+
onApprovalResolved?: (requestId: string, status: ChatApprovalStatus, approvalType: string) => void
|
|
187
|
+
/**
|
|
188
|
+
* Called whenever an `EXECUTED_TOOL` chunk is processed. Lets consumers
|
|
189
|
+
* merge the result into the originating `EXECUTING_TOOL` (or batch
|
|
190
|
+
* `executions[execId]`) segment in an earlier message bubble when the tool
|
|
191
|
+
* outlived its message scope. Idempotent.
|
|
192
|
+
*/
|
|
193
|
+
onToolExecuted?: (segment: ToolExecutionSegment) => void
|
|
172
194
|
/** Called when a DIALOG_CLOSED chunk is received */
|
|
173
195
|
onDialogClosed?: () => void
|
|
174
196
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -65,6 +65,14 @@ export * from './shared/onboarding'
|
|
|
65
65
|
// Product Release components
|
|
66
66
|
export * from './shared/product-release'
|
|
67
67
|
|
|
68
|
+
// Dev-center shared components (Roadmap / Delivery / DevSectionView chrome)
|
|
69
|
+
export * from './shared/dev-section'
|
|
70
|
+
export * from './shared/roadmap'
|
|
71
|
+
export * from './shared/delivery'
|
|
72
|
+
|
|
73
|
+
// Legal-document shared component (privacy policy, terms of service)
|
|
74
|
+
export * from './shared/legal-document'
|
|
75
|
+
|
|
68
76
|
// Detail Page Skeleton
|
|
69
77
|
export { DetailPageSkeleton, type DetailPageSkeletonProps } from './shared/detail-page-skeleton'
|
|
70
78
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DeliveryLists — the delivery section body (two tables: recently
|
|
5
|
+
* completed + active). Reads `search` and `task_type` URL params
|
|
6
|
+
* written by the shared `<DevSectionView>` chrome and refetches on
|
|
7
|
+
* change.
|
|
8
|
+
*
|
|
9
|
+
* Endpoint configuration:
|
|
10
|
+
* - `completedApiEndpoint` / `inProgressApiEndpoint` are the two
|
|
11
|
+
* per-bucket GET endpoints. Defaults match the hub's pre-migration
|
|
12
|
+
* routes (`/api/delivery/completed`, `/api/delivery/in-progress`).
|
|
13
|
+
*
|
|
14
|
+
* Coupling constraint — `searchParamKey` / `taskTypeParamKey`:
|
|
15
|
+
* These props serve TWO purposes:
|
|
16
|
+
* 1. URL READS — keys this component reads via `useSearchParams()`.
|
|
17
|
+
* MUST match the consuming chrome's `section.search.paramKey` /
|
|
18
|
+
* `section.filter.paramKey` (the chrome WRITES the URL params).
|
|
19
|
+
* 2. API WRITES — keys this component sends as query params on the
|
|
20
|
+
* outbound fetch to `{completedApiEndpoint,inProgressApiEndpoint}`.
|
|
21
|
+
* The hub API contract uses `'search'` / `'task_type'`; embedders
|
|
22
|
+
* reverse-proxying those routes must preserve the same names OR
|
|
23
|
+
* rewrite the inbound query string on the proxy side.
|
|
24
|
+
*
|
|
25
|
+
* Defaults align with `OPENFRAME_DEV_SECTIONS.delivery.{search.paramKey,filter.paramKey}`
|
|
26
|
+
* AND the hub API contract, so the OpenFrame zero-config case "just
|
|
27
|
+
* works". Custom chrome overriding the param keys must override BOTH
|
|
28
|
+
* ends consistently AND ensure the backend reads the same names.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { useEffect, useState } from 'react';
|
|
32
|
+
import { useSearchParams, useRouter, usePathname } from '../../../embed-shims';
|
|
33
|
+
import type { DeliveryResponse } from '../../../types/delivery';
|
|
34
|
+
import { DeliveryTable } from './delivery-table';
|
|
35
|
+
import { EmptyState } from '../../empty-state';
|
|
36
|
+
import { LoadError } from '../../ui/error-state';
|
|
37
|
+
|
|
38
|
+
const DEFAULT_COMPLETED_ENDPOINT = '/api/delivery/completed';
|
|
39
|
+
const DEFAULT_IN_PROGRESS_ENDPOINT = '/api/delivery/in-progress';
|
|
40
|
+
const DEFAULT_SEARCH_PARAM_KEY = 'search';
|
|
41
|
+
const DEFAULT_TASK_TYPE_PARAM_KEY = 'task_type';
|
|
42
|
+
|
|
43
|
+
export interface DeliveryListsProps {
|
|
44
|
+
/** GET endpoint for the "Recently Completed" bucket. Default
|
|
45
|
+
* `/api/delivery/completed`. */
|
|
46
|
+
completedApiEndpoint?: string;
|
|
47
|
+
/** GET endpoint for the "Active Tasks" bucket. Default
|
|
48
|
+
* `/api/delivery/in-progress`. */
|
|
49
|
+
inProgressApiEndpoint?: string;
|
|
50
|
+
/** URL param key for the search input. MUST match the consuming
|
|
51
|
+
* chrome's `section.search.paramKey`. Default `'search'`. */
|
|
52
|
+
searchParamKey?: string;
|
|
53
|
+
/** URL param key for the task-type filter. MUST match the consuming
|
|
54
|
+
* chrome's `section.filter.paramKey`. Default `'task_type'`. */
|
|
55
|
+
taskTypeParamKey?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function DeliveryLists({
|
|
59
|
+
completedApiEndpoint = DEFAULT_COMPLETED_ENDPOINT,
|
|
60
|
+
inProgressApiEndpoint = DEFAULT_IN_PROGRESS_ENDPOINT,
|
|
61
|
+
searchParamKey = DEFAULT_SEARCH_PARAM_KEY,
|
|
62
|
+
taskTypeParamKey = DEFAULT_TASK_TYPE_PARAM_KEY,
|
|
63
|
+
}: DeliveryListsProps = {}) {
|
|
64
|
+
const searchParams = useSearchParams();
|
|
65
|
+
const router = useRouter();
|
|
66
|
+
const pathname = usePathname();
|
|
67
|
+
|
|
68
|
+
const [data, setData] = useState<DeliveryResponse | null>(null);
|
|
69
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
70
|
+
const [error, setError] = useState<string | null>(null);
|
|
71
|
+
|
|
72
|
+
// Get filter state from URL
|
|
73
|
+
const searchQuery = searchParams.get(searchParamKey) || '';
|
|
74
|
+
const taskTypeFilter = searchParams.get(taskTypeParamKey) || 'all';
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
async function fetchDeliveryData() {
|
|
78
|
+
try {
|
|
79
|
+
setIsLoading(true);
|
|
80
|
+
setError(null);
|
|
81
|
+
|
|
82
|
+
// Build query parameters for filtering. The outbound key names
|
|
83
|
+
// mirror the inbound URL-param keys — see "Coupling constraint"
|
|
84
|
+
// in the file docblock for why.
|
|
85
|
+
const params = new URLSearchParams();
|
|
86
|
+
if (searchQuery) {
|
|
87
|
+
params.set(searchParamKey, searchQuery);
|
|
88
|
+
}
|
|
89
|
+
if (taskTypeFilter && taskTypeFilter !== 'all') {
|
|
90
|
+
params.set(taskTypeParamKey, taskTypeFilter);
|
|
91
|
+
}
|
|
92
|
+
const queryString = params.toString();
|
|
93
|
+
const queryParam = queryString ? `?${queryString}` : '';
|
|
94
|
+
|
|
95
|
+
// Fetch completed and in-progress tasks separately with filters
|
|
96
|
+
const [completedResponse, inProgressResponse] = await Promise.all([
|
|
97
|
+
fetch(`${completedApiEndpoint}${queryParam}`),
|
|
98
|
+
fetch(`${inProgressApiEndpoint}${queryParam}`),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
if (!completedResponse.ok || !inProgressResponse.ok) {
|
|
102
|
+
throw new Error('Failed to fetch delivery items');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const [completedResult, inProgressResult] = await Promise.all([
|
|
106
|
+
completedResponse.json(),
|
|
107
|
+
inProgressResponse.json(),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
setData({
|
|
111
|
+
completed: completedResult.items || [],
|
|
112
|
+
inProgress: inProgressResult.items || [],
|
|
113
|
+
});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error('Error fetching delivery items:', err);
|
|
116
|
+
setError('Failed to load delivery items. Please try again later.');
|
|
117
|
+
} finally {
|
|
118
|
+
setIsLoading(false);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fetchDeliveryData();
|
|
123
|
+
}, [searchQuery, taskTypeFilter, completedApiEndpoint, inProgressApiEndpoint, searchParamKey, taskTypeParamKey]);
|
|
124
|
+
|
|
125
|
+
const filteredCompleted = data?.completed || [];
|
|
126
|
+
const filteredInProgress = data?.inProgress || [];
|
|
127
|
+
|
|
128
|
+
const showCompleted = true;
|
|
129
|
+
const showInProgress = true;
|
|
130
|
+
|
|
131
|
+
const hasActiveFilters = searchQuery !== '' || taskTypeFilter !== 'all';
|
|
132
|
+
const hasResults = (showCompleted && filteredCompleted.length > 0) || (showInProgress && filteredInProgress.length > 0);
|
|
133
|
+
|
|
134
|
+
// Error state — consume lib's canonical LoadError so ODS tokens +
|
|
135
|
+
// retry affordance stay in lockstep with every other surface.
|
|
136
|
+
if (error) {
|
|
137
|
+
return (
|
|
138
|
+
<div className="w-full">
|
|
139
|
+
<LoadError message={error} onRetry={() => window.location.reload()} />
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className="w-full flex flex-col gap-[40px]">
|
|
146
|
+
{/* Empty state if no results after filtering */}
|
|
147
|
+
{!isLoading && !hasResults && (
|
|
148
|
+
hasActiveFilters ? (
|
|
149
|
+
<EmptyState
|
|
150
|
+
type="search"
|
|
151
|
+
title="No tasks found"
|
|
152
|
+
description="No tasks match your current filters. Try adjusting your search or status filter."
|
|
153
|
+
showCTA={true}
|
|
154
|
+
ctaText="Reset Filters"
|
|
155
|
+
onCtaClick={() => {
|
|
156
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
157
|
+
params.delete(searchParamKey);
|
|
158
|
+
params.delete(taskTypeParamKey);
|
|
159
|
+
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
|
160
|
+
}}
|
|
161
|
+
/>
|
|
162
|
+
) : (
|
|
163
|
+
<EmptyState
|
|
164
|
+
type="generic"
|
|
165
|
+
title="No tasks available"
|
|
166
|
+
description="Check back soon for upcoming tasks!"
|
|
167
|
+
showCTA={false}
|
|
168
|
+
/>
|
|
169
|
+
)
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{/* Completed Tasks Table */}
|
|
173
|
+
{showCompleted && (hasResults || isLoading) && (
|
|
174
|
+
<div className="w-full">
|
|
175
|
+
<h3 className="text-h2 text-ods-text-primary tracking-[-0.48px] md:tracking-[-0.56px] lg:tracking-[-0.64px] mb-4">
|
|
176
|
+
Recently Completed<span className="text-ods-accent">:</span>
|
|
177
|
+
</h3>
|
|
178
|
+
<DeliveryTable
|
|
179
|
+
items={filteredCompleted}
|
|
180
|
+
isLoading={isLoading}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{/* In Progress Tasks Table */}
|
|
186
|
+
{showInProgress && (hasResults || isLoading) && (
|
|
187
|
+
<div className="w-full">
|
|
188
|
+
<h3 className="text-h2 text-ods-text-primary tracking-[-0.48px] md:tracking-[-0.56px] lg:tracking-[-0.64px] mb-4">
|
|
189
|
+
Active Tasks<span className="text-ods-accent">:</span>
|
|
190
|
+
</h3>
|
|
191
|
+
<DeliveryTable
|
|
192
|
+
items={filteredInProgress}
|
|
193
|
+
isLoading={isLoading}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -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
|
+
}
|