@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.206",
3
+ "version": "0.0.207-snapshot.20260526023528",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
- callbacks.onSegmentsUpdate?.(segments)
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
- callbacks.onSegmentsUpdate?.(segments)
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
- const segments = accumulator.updateApprovalStatus(requestId, status)
238
- callbacks.onSegmentsUpdate?.(segments)
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
- // approvalType from the result is informational; not consumed downstream yet.
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 { MessageSegment, PendingToolCallData, TokenUsageData, ExecutingToolState } from './message.types'
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
  }
@@ -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
+ }