@flamingo-stack/openframe-frontend-core 0.0.206 → 0.0.207-snapshot.20260526023528

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/chunk-4XLJWX2N.js +12 -0
  2. package/dist/chunk-4XLJWX2N.js.map +1 -0
  3. package/dist/{chunk-OLTGB32E.js → chunk-6WMMLMKM.js} +2857 -2045
  4. package/dist/chunk-6WMMLMKM.js.map +1 -0
  5. package/dist/chunk-VFKQMAUF.cjs +12 -0
  6. package/dist/chunk-VFKQMAUF.cjs.map +1 -0
  7. package/dist/{chunk-YGOJIDL5.cjs → chunk-WYLNTZZ7.cjs} +1343 -531
  8. package/dist/chunk-WYLNTZZ7.cjs.map +1 -0
  9. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
  10. package/dist/components/chat/index.cjs +3 -2
  11. package/dist/components/chat/index.cjs.map +1 -1
  12. package/dist/components/chat/index.js +2 -1
  13. package/dist/components/chat/types/api.types.d.ts +17 -1
  14. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  15. package/dist/components/features/index.cjs +3 -2
  16. package/dist/components/features/index.cjs.map +1 -1
  17. package/dist/components/features/index.js +2 -1
  18. package/dist/components/index.cjs +21 -2
  19. package/dist/components/index.cjs.map +1 -1
  20. package/dist/components/index.d.ts +4 -0
  21. package/dist/components/index.d.ts.map +1 -1
  22. package/dist/components/index.js +20 -1
  23. package/dist/components/navigation/index.cjs +3 -2
  24. package/dist/components/navigation/index.cjs.map +1 -1
  25. package/dist/components/navigation/index.js +2 -1
  26. package/dist/components/shared/delivery/delivery-lists.d.ts +16 -0
  27. package/dist/components/shared/delivery/delivery-lists.d.ts.map +1 -0
  28. package/dist/components/shared/delivery/delivery-table.d.ts +12 -0
  29. package/dist/components/shared/delivery/delivery-table.d.ts.map +1 -0
  30. package/dist/components/shared/delivery/index.d.ts +3 -0
  31. package/dist/components/shared/delivery/index.d.ts.map +1 -0
  32. package/dist/components/shared/dev-section/dev-section-page.d.ts +31 -0
  33. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -0
  34. package/dist/components/shared/dev-section/dev-section-view.d.ts +34 -0
  35. package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -0
  36. package/dist/components/shared/dev-section/index.d.ts +3 -0
  37. package/dist/components/shared/dev-section/index.d.ts.map +1 -0
  38. package/dist/components/shared/legal-document/index.d.ts +10 -0
  39. package/dist/components/shared/legal-document/index.d.ts.map +1 -0
  40. package/dist/components/shared/legal-document/legal-document-page.d.ts +66 -0
  41. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -0
  42. package/dist/components/shared/legal-document/use-legal-docs.d.ts +40 -0
  43. package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -0
  44. package/dist/components/shared/product-release/index.d.ts +2 -1
  45. package/dist/components/shared/product-release/index.d.ts.map +1 -1
  46. package/dist/components/shared/product-release/release-detail-page.d.ts +11 -7
  47. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  48. package/dist/components/shared/roadmap/index.d.ts +18 -0
  49. package/dist/components/shared/roadmap/index.d.ts.map +1 -0
  50. package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts +24 -0
  51. package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts.map +1 -0
  52. package/dist/components/shared/roadmap/roadmap-grid.d.ts +18 -0
  53. package/dist/components/shared/roadmap/roadmap-grid.d.ts.map +1 -0
  54. package/dist/components/shared/roadmap/use-roadmap-voting.d.ts +25 -0
  55. package/dist/components/shared/roadmap/use-roadmap-voting.d.ts.map +1 -0
  56. package/dist/components/ui/index.cjs +3 -2
  57. package/dist/components/ui/index.cjs.map +1 -1
  58. package/dist/components/ui/index.js +2 -1
  59. package/dist/components/ui/release-changelog-section.d.ts +13 -2
  60. package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
  61. package/dist/embed-shims/index.cjs +1 -6
  62. package/dist/embed-shims/index.cjs.map +1 -1
  63. package/dist/embed-shims/index.js +1 -6
  64. package/dist/embed-shims/index.js.map +1 -1
  65. package/dist/index.cjs +13 -2
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.js +12 -1
  68. package/dist/types/delivery.d.ts +49 -0
  69. package/dist/types/delivery.d.ts.map +1 -0
  70. package/dist/types/index.cjs +13 -0
  71. package/dist/types/index.cjs.map +1 -1
  72. package/dist/types/index.d.ts +1 -0
  73. package/dist/types/index.d.ts.map +1 -1
  74. package/dist/types/index.js +12 -1
  75. package/dist/types/index.js.map +1 -1
  76. package/dist/utils/dev-sections/index.d.ts +11 -0
  77. package/dist/utils/dev-sections/index.d.ts.map +1 -0
  78. package/dist/utils/dev-sections/openframe-dev-sections.d.ts +209 -0
  79. package/dist/utils/dev-sections/openframe-dev-sections.d.ts.map +1 -0
  80. package/dist/utils/index.cjs +82 -0
  81. package/dist/utils/index.cjs.map +1 -1
  82. package/dist/utils/index.d.ts +1 -0
  83. package/dist/utils/index.d.ts.map +1 -1
  84. package/dist/utils/index.js +81 -2
  85. package/dist/utils/index.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +53 -6
  88. package/src/components/chat/types/api.types.ts +23 -1
  89. package/src/components/index.ts +8 -0
  90. package/src/components/shared/delivery/delivery-lists.tsx +199 -0
  91. package/src/components/shared/delivery/delivery-table.tsx +174 -0
  92. package/src/components/shared/delivery/index.ts +9 -0
  93. package/src/components/shared/dev-section/dev-section-page.tsx +72 -0
  94. package/src/components/shared/dev-section/dev-section-view.tsx +129 -0
  95. package/src/components/shared/dev-section/index.ts +2 -0
  96. package/src/components/shared/legal-document/index.ts +19 -0
  97. package/src/components/shared/legal-document/legal-document-page.tsx +178 -0
  98. package/src/components/shared/legal-document/use-legal-docs.ts +123 -0
  99. package/src/components/shared/product-release/index.ts +14 -3
  100. package/src/components/shared/product-release/release-detail-page.tsx +45 -7
  101. package/src/components/shared/roadmap/index.ts +23 -0
  102. package/src/components/shared/roadmap/roadmap-grid-skeleton.tsx +74 -0
  103. package/src/components/shared/roadmap/roadmap-grid.tsx +106 -0
  104. package/src/components/shared/roadmap/use-roadmap-voting.ts +163 -0
  105. package/src/components/ui/release-changelog-section.tsx +113 -32
  106. package/src/types/delivery.ts +54 -0
  107. package/src/types/index.ts +1 -0
  108. package/src/utils/dev-sections/index.ts +17 -0
  109. package/src/utils/dev-sections/openframe-dev-sections.ts +148 -0
  110. package/src/utils/index.ts +6 -1
  111. package/dist/chunk-OLTGB32E.js.map +0 -1
  112. package/dist/chunk-YGOJIDL5.cjs.map +0 -1
@@ -0,0 +1,74 @@
1
+ /**
2
+ * RoadmapGridSkeleton — loading state for the `/roadmap` grid view.
3
+ *
4
+ * Pure JSX (no hooks, no events) — `'use client'` not strictly required
5
+ * here; tsup's client-entry banner injects it automatically when this
6
+ * file is bundled into the client output. We match the playbook's
7
+ * skeleton-file convention (no directive when no hooks).
8
+ *
9
+ * NOTE: lib's `chat/entity-cards/roadmap-card.tsx` also exports a
10
+ * `RoadmapCardSkeleton` — that one is the COMPACT 56px chat-card
11
+ * variant. This file's internal card-skeleton (340px grid card)
12
+ * intentionally stays file-internal to avoid the naming collision;
13
+ * only `RoadmapGridSkeleton` is exported.
14
+ */
15
+
16
+ function RoadmapCardSkeleton() {
17
+ return (
18
+ <div className="bg-ods-card border border-ods-border rounded-[6px] p-[24px] flex flex-col gap-[16px] min-h-[340px] relative">
19
+ {/* Status Badge Skeleton - Top Right */}
20
+ <div className="absolute top-[24px] right-[24px]">
21
+ <div className="h-[20px] w-[80px] bg-ods-border rounded animate-pulse"></div>
22
+ </div>
23
+
24
+ {/* Icon and title skeleton */}
25
+ <div className="flex items-center gap-[16px] pr-[120px]">
26
+ <div className="w-[80px] h-[80px] bg-ods-border rounded-lg flex-shrink-0 animate-pulse"></div>
27
+ <div className="flex-1 min-w-0 flex flex-col gap-1">
28
+ <div className="min-h-[48px] flex items-center">
29
+ <div className="h-[24px] w-full bg-ods-border rounded animate-pulse"></div>
30
+ </div>
31
+ <div className="min-h-[20px] flex items-center">
32
+ <div className="h-[14px] w-1/2 bg-ods-border rounded animate-pulse"></div>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ {/* Description skeleton - exactly 3 lines */}
38
+ <div className="min-h-[72px] flex items-center">
39
+ <div className="w-full space-y-2">
40
+ <div className="h-[24px] bg-ods-border rounded animate-pulse"></div>
41
+ <div className="h-[24px] bg-ods-border rounded animate-pulse"></div>
42
+ <div className="h-[24px] w-4/5 bg-ods-border rounded animate-pulse"></div>
43
+ </div>
44
+ </div>
45
+
46
+ <div className="flex-1"></div>
47
+
48
+ {/* Bottom skeleton */}
49
+ <div className="flex items-center justify-between">
50
+ <div className="h-[48px] w-[120px] bg-ods-border rounded animate-pulse"></div>
51
+ <div className="h-[32px] w-[100px] bg-ods-border rounded animate-pulse"></div>
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export interface RoadmapGridSkeletonProps {
58
+ /** Number of skeleton cards to show. Default 4. */
59
+ count?: number;
60
+ /** Show the desktop left margin (~120px) that aligns the grid with
61
+ * the page hero's title block. Default `true`. Related-content rails
62
+ * inside narrower surfaces (e.g. the release detail page) pass `false`. */
63
+ showLeftMargin?: boolean;
64
+ }
65
+
66
+ export function RoadmapGridSkeleton({ count = 4, showLeftMargin = true }: RoadmapGridSkeletonProps) {
67
+ return (
68
+ <div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${showLeftMargin ? 'md:ml-[120px]' : ''}`}>
69
+ {Array.from({ length: count }).map((_, i) => (
70
+ <RoadmapCardSkeleton key={i} />
71
+ ))}
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,106 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * RoadmapGrid — full-page roadmap surface.
5
+ *
6
+ * Renders a responsive 2-col grid of `<RoadmapCard>`s (lib's chat-entity
7
+ * card, default density), threads voting state through `useRoadmapVoting`,
8
+ * and follows up a successful vote with a single-task refresh fetch so
9
+ * the displayed counts stay live.
10
+ *
11
+ * Endpoint configuration — `buildRefreshUrl`:
12
+ * The single-task refresh hits a PATH-based endpoint
13
+ * (`/api/roadmap/<taskId>` by default). A string-concat `refreshEndpoint`
14
+ * would silently break embedders whose by-id route is shaped
15
+ * differently (e.g. `/api/roadmap?id=…`), so this prop is a function
16
+ * builder. The default matches the hub's pre-migration shape.
17
+ *
18
+ * Empty state — uses lib's `<EmptyState>` directly (identical API to
19
+ * hub's, lives in `src/components/empty-state.tsx`).
20
+ */
21
+
22
+ import { useState } from 'react';
23
+ import { RoadmapCard } from '../../chat/entity-cards/roadmap-card';
24
+ import { useRoadmapVoting, type UseRoadmapVotingOptions } from './use-roadmap-voting';
25
+ import { EmptyState } from '../../empty-state';
26
+ import type { RoadmapItem } from '../../chat/types/entities/roadmap-item';
27
+
28
+ const DEFAULT_BUILD_REFRESH_URL = (taskId: string) => `/api/roadmap/${taskId}`;
29
+
30
+ export interface RoadmapGridProps {
31
+ items: RoadmapItem[];
32
+ onItemUpdate?: (updatedItem: RoadmapItem) => void;
33
+ /** Show the desktop left margin (~120px) that aligns the grid with
34
+ * the page hero. Default `true`. Related-content rails pass `false`. */
35
+ showLeftMargin?: boolean;
36
+ /** URL builder for the per-task refresh call after a successful vote.
37
+ * Function shape because the taskId sits in the URL path, not a
38
+ * query param. Default `(t) => \`/api/roadmap/${t}\``. */
39
+ buildRefreshUrl?: (taskId: string) => string;
40
+ /** Voting hook options (vote endpoint + storage key) — see
41
+ * `useRoadmapVoting`. */
42
+ votingOptions?: UseRoadmapVotingOptions;
43
+ }
44
+
45
+ export function RoadmapGrid({
46
+ items,
47
+ onItemUpdate,
48
+ showLeftMargin = true,
49
+ buildRefreshUrl = DEFAULT_BUILD_REFRESH_URL,
50
+ votingOptions,
51
+ }: RoadmapGridProps) {
52
+ const { getVote, toggleVote } = useRoadmapVoting(votingOptions);
53
+ const [votingTasks, setVotingTasks] = useState<Set<string>>(new Set());
54
+
55
+ const handleVote = async (taskId: string, voteType: 'up' | 'down') => {
56
+ // Prevent double-clicking
57
+ if (votingTasks.has(taskId)) return;
58
+
59
+ setVotingTasks(prev => new Set(prev).add(taskId));
60
+
61
+ try {
62
+ const result = await toggleVote(taskId, voteType);
63
+
64
+ if (result.success) {
65
+ // Refresh the specific task from server
66
+ const response = await fetch(buildRefreshUrl(taskId));
67
+ if (response.ok) {
68
+ const data = await response.json();
69
+ if (data.item && onItemUpdate) {
70
+ onItemUpdate(data.item);
71
+ }
72
+ }
73
+ }
74
+ } finally {
75
+ setVotingTasks(prev => {
76
+ const next = new Set(prev);
77
+ next.delete(taskId);
78
+ return next;
79
+ });
80
+ }
81
+ };
82
+
83
+ if (items.length === 0) {
84
+ return (
85
+ <EmptyState
86
+ type="generic"
87
+ title="No roadmap items"
88
+ description="Check back soon for upcoming features and improvements!"
89
+ />
90
+ );
91
+ }
92
+
93
+ return (
94
+ <div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${showLeftMargin ? 'md:ml-[120px]' : ''}`}>
95
+ {items.map((item) => (
96
+ <RoadmapCard
97
+ key={item.id}
98
+ item={item}
99
+ userVote={getVote(item.id)}
100
+ onVote={(voteType) => handleVote(item.id, voteType)}
101
+ isVoting={votingTasks.has(item.id)}
102
+ />
103
+ ))}
104
+ </div>
105
+ );
106
+ }
@@ -0,0 +1,163 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useRoadmapVoting — localStorage-backed optimistic voting for roadmap cards.
5
+ *
6
+ * One vote per task per user (storage key scoped per `storageKey` option,
7
+ * default `'roadmap_votes_v1'`). Toggling the same vote removes it;
8
+ * switching directions sends a remove + add pair so the server's running
9
+ * totals stay correct.
10
+ *
11
+ * Endpoint configuration — `voteApiEndpoint`:
12
+ * The hook posts to ONE endpoint (default `/api/roadmap/vote`) for
13
+ * BOTH the optimistic add AND the opposite-vote remove. Reverse-proxy
14
+ * embedders override this with their proxied path; lib otherwise
15
+ * matches the hub's pre-migration call shape.
16
+ */
17
+
18
+ import { useState, useEffect, useCallback } from 'react';
19
+
20
+ export type VoteType = 'up' | 'down' | null;
21
+
22
+ export interface VoteState {
23
+ [taskId: string]: VoteType;
24
+ }
25
+
26
+ export interface UseRoadmapVotingOptions {
27
+ /** Vote endpoint URL. Default `/api/roadmap/vote`. */
28
+ voteApiEndpoint?: string;
29
+ /** localStorage key. Default `'roadmap_votes_v1'`. Embedders mounting
30
+ * multiple roadmap surfaces in the same origin can scope per-surface
31
+ * (e.g. `'roadmap_votes_v1_main'` vs `'roadmap_votes_v1_admin'`) so
32
+ * votes don't cross-contaminate. */
33
+ storageKey?: string;
34
+ }
35
+
36
+ const DEFAULT_VOTE_ENDPOINT = '/api/roadmap/vote';
37
+ const DEFAULT_STORAGE_KEY = 'roadmap_votes_v1';
38
+
39
+ export function useRoadmapVoting(options: UseRoadmapVotingOptions = {}) {
40
+ const voteApiEndpoint = options.voteApiEndpoint ?? DEFAULT_VOTE_ENDPOINT;
41
+ const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
42
+
43
+ const [votes, setVotes] = useState<VoteState>({});
44
+ const [isLoading, setIsLoading] = useState(true);
45
+
46
+ // Load votes from localStorage. Runs on mount AND whenever `storageKey`
47
+ // changes — when the key changes mid-lifecycle (e.g. an embedder
48
+ // remounts with a new namespace), we MUST reset state first so the
49
+ // save-effect below doesn't write the old key's data into the new
50
+ // key. We also re-enter the loading phase so the load completes
51
+ // before any save runs.
52
+ useEffect(() => {
53
+ setIsLoading(true);
54
+ setVotes({});
55
+ try {
56
+ const stored = localStorage.getItem(storageKey);
57
+ if (stored) {
58
+ setVotes(JSON.parse(stored));
59
+ }
60
+ } catch (error) {
61
+ console.error('[Voting] Error loading votes from localStorage:', error);
62
+ } finally {
63
+ setIsLoading(false);
64
+ }
65
+ }, [storageKey]);
66
+
67
+ // Save votes to localStorage whenever they change
68
+ useEffect(() => {
69
+ if (!isLoading) {
70
+ try {
71
+ localStorage.setItem(storageKey, JSON.stringify(votes));
72
+ } catch (error) {
73
+ console.error('[Voting] Error saving votes to localStorage:', error);
74
+ }
75
+ }
76
+ }, [votes, isLoading, storageKey]);
77
+
78
+ const getVote = useCallback(
79
+ (taskId: string): VoteType => {
80
+ return votes[taskId] || null;
81
+ },
82
+ [votes]
83
+ );
84
+
85
+ const toggleVote = useCallback(
86
+ async (
87
+ taskId: string,
88
+ voteType: 'up' | 'down'
89
+ ): Promise<{ success: boolean; newVote: VoteType; action: 'add' | 'remove' }> => {
90
+ const currentVote = votes[taskId];
91
+
92
+ let newVote: VoteType = null;
93
+ let action: 'add' | 'remove' = 'add';
94
+
95
+ if (currentVote === voteType) {
96
+ // User clicked same vote - remove it
97
+ newVote = null;
98
+ action = 'remove';
99
+ } else {
100
+ // User clicked different vote - set it. If they had an opposite
101
+ // vote, remove that first so the server totals stay consistent.
102
+ if (currentVote) {
103
+ await fetch(voteApiEndpoint, {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({
107
+ taskId,
108
+ voteType: currentVote,
109
+ action: 'remove',
110
+ }),
111
+ }).catch(err => console.error('[Voting] Error removing opposite vote:', err));
112
+ }
113
+
114
+ newVote = voteType;
115
+ action = 'add';
116
+ }
117
+
118
+ // Optimistic update
119
+ setVotes(prev => ({
120
+ ...prev,
121
+ [taskId]: newVote,
122
+ }));
123
+
124
+ try {
125
+ const response = await fetch(voteApiEndpoint, {
126
+ method: 'POST',
127
+ headers: { 'Content-Type': 'application/json' },
128
+ body: JSON.stringify({ taskId, voteType, action }),
129
+ });
130
+
131
+ if (!response.ok) {
132
+ throw new Error('Vote API request failed');
133
+ }
134
+
135
+ return { success: true, newVote, action };
136
+ } catch (error) {
137
+ console.error('[Voting] API error:', error);
138
+
139
+ // Revert optimistic update on error
140
+ setVotes(prev => ({
141
+ ...prev,
142
+ [taskId]: currentVote,
143
+ }));
144
+
145
+ return { success: false, newVote: currentVote, action };
146
+ }
147
+ },
148
+ [votes, voteApiEndpoint]
149
+ );
150
+
151
+ const clearVotes = useCallback(() => {
152
+ setVotes({});
153
+ localStorage.removeItem(storageKey);
154
+ }, [storageKey]);
155
+
156
+ return {
157
+ votes,
158
+ isLoading,
159
+ getVote,
160
+ toggleVote,
161
+ clearVotes,
162
+ };
163
+ }
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import React, { useState } from 'react';
3
+ import React, { useState, useRef, useEffect } from 'react';
4
4
  import { Badge } from './badge';
5
5
  import { ChevronDown } from 'lucide-react';
6
6
  import type { ChangelogEntry } from '../../types/product-release';
@@ -10,10 +10,21 @@ interface ReleaseChangelogSectionProps {
10
10
  entries: ChangelogEntry[];
11
11
  isBreaking?: boolean;
12
12
  hideTitle?: boolean;
13
- /** When true, section starts collapsed and can be toggled open/closed */
13
+ /** When true, section starts collapsed and can be toggled open/closed
14
+ * via a button on the title row. Mutually exclusive with `previewFirst`. */
14
15
  collapsible?: boolean;
15
16
  /** Initial collapsed state (only used when collapsible=true). Defaults to true (collapsed). */
16
17
  defaultCollapsed?: boolean;
18
+ /** When true, render the first entry in full and fade-mask the rest
19
+ * with a "Show N more / Show less" toggle below the list. Hides the
20
+ * fade + toggle when there's only one entry. Mutually exclusive with
21
+ * `collapsible` — when both are passed, `collapsible` wins.
22
+ *
23
+ * This is the same progressive-disclosure pattern used on the
24
+ * investor-update detail page's Key Highlights / Financial Notes
25
+ * sections (formerly a duplicated `FadedHighlightSection` component
26
+ * in the hub — unified here). */
27
+ previewFirst?: boolean;
17
28
  /** Optional lucide icon rendered inline before the title text. Matches the
18
29
  * catalog card's changelog-strip icons (Sparkles for Features, Wrench for
19
30
  * Fixes, TrendingUp for Improvements, AlertTriangle for Breaking) — same
@@ -23,6 +34,10 @@ interface ReleaseChangelogSectionProps {
23
34
  SimpleMarkdownRenderer: React.ComponentType<{ content: string }>;
24
35
  }
25
36
 
37
+ // Collapsed height for the preview-first mode. ~120px shows the first
38
+ // entry's title + the start of its description before the mask kicks in.
39
+ const PREVIEW_COLLAPSED_HEIGHT = 120;
40
+
26
41
  export function ReleaseChangelogSection({
27
42
  title,
28
43
  entries,
@@ -30,13 +45,29 @@ export function ReleaseChangelogSection({
30
45
  hideTitle = false,
31
46
  collapsible = false,
32
47
  defaultCollapsed = true,
48
+ previewFirst = false,
33
49
  icon,
34
50
  SimpleMarkdownRenderer
35
51
  }: ReleaseChangelogSectionProps) {
36
52
  const [collapsed, setCollapsed] = useState(collapsible ? defaultCollapsed : false);
53
+ const [previewExpanded, setPreviewExpanded] = useState(false);
54
+ const previewContentRef = useRef<HTMLDivElement>(null);
55
+
56
+ // Reset preview-expanded state when the entries set changes — otherwise
57
+ // a parent that refetches and shrinks entries from N → 1 would leave
58
+ // the user with a stale "expanded" state and a momentarily-wrong
59
+ // "Show 0 more" button before the `previewNeedsFade` gate hides it.
60
+ // Keyed on `entries.length` (not identity) so re-renders with the same
61
+ // length don't churn state unnecessarily.
62
+ useEffect(() => {
63
+ setPreviewExpanded(false);
64
+ }, [entries.length]);
37
65
 
38
66
  if (!entries || entries.length === 0) return null;
39
67
 
68
+ // collapsible wins when both flags are passed (documented in JSDoc).
69
+ const inPreviewMode = previewFirst && !collapsible;
70
+ const previewNeedsFade = inPreviewMode && entries.length > 1;
40
71
  const showEntries = !collapsible || !collapsed;
41
72
 
42
73
  return (
@@ -68,37 +99,87 @@ export function ReleaseChangelogSection({
68
99
  )
69
100
  )}
70
101
  {showEntries && (
71
- <ul className="space-y-6">
72
- {entries.map((entry, index) => (
73
- <li key={index} className="border-l-2 border-ods-border pl-4 ml-0">
74
- {/* Entry title `text-h3` is body family + BOLD weight (per
75
- ODS tokens: `--font-h3-weight: var(--font-weight-bold)`)
76
- at 14/18px responsive. Same body size as the description
77
- below, distinguished by weight clean visual hierarchy
78
- without inflating the body scale. */}
79
- <p className="text-h3 text-ods-text-primary mb-2">{entry.title}</p>
80
- {entry.description && (
81
- /* Entry description — body text matches the main release
82
- summary (release-detail-page.tsx:321) at the SAME 14/18px
83
- responsive `text-h4` scale. The `SimpleMarkdownRenderer`
84
- forces its own `<p>` typography
85
- (`text-[16px] md:text-[18px] lg:text-[20px]`) which
86
- overrides the wrapper's `text-h4` on `lg+` viewports and
87
- inflates the changelog body to 20px — larger than the
88
- main summary AND larger than the entry title.
89
- The `[&_p]:!` overrides pin every descendant `<p>` back
90
- to the h4 responsive tokens (`var(--font-size-h4-body)`
91
- + `var(--font-line-space-h4-body)`) — same variables
92
- `text-h4` itself uses, so the responsive breakpoints
93
- stay aligned with the rest of the page. */
94
- <div className="text-h4 text-ods-text-primary [&_p]:!text-[length:var(--font-size-h4-body)] [&_p]:!leading-[var(--font-line-space-h4-body)] [&_p]:!font-medium">
95
- <SimpleMarkdownRenderer content={entry.description} />
96
- </div>
97
- )}
98
- </li>
99
- ))}
100
- </ul>
102
+ inPreviewMode ? (
103
+ /* Preview-first mode: render the list in a height-clamped +
104
+ mask-faded wrapper. The CSS mask creates the soft fade-out
105
+ at the bottom of the collapsed region; the inline maxHeight
106
+ + transition animate the open/close. When `previewExpanded`
107
+ flips, the wrapper falls back to its natural scrollHeight
108
+ (or 2000px on first render before the ref measures). */
109
+ <div className="relative">
110
+ <div
111
+ ref={previewContentRef}
112
+ className="overflow-hidden transition-[max-height] duration-500"
113
+ style={{
114
+ transitionTimingFunction: 'cubic-bezier(0.33, 1, 0.68, 1)',
115
+ maxHeight: previewExpanded || !previewNeedsFade
116
+ ? previewContentRef.current?.scrollHeight ?? 2000
117
+ : PREVIEW_COLLAPSED_HEIGHT,
118
+ ...(previewNeedsFade && !previewExpanded ? {
119
+ maskImage: 'linear-gradient(to bottom, black 30%, transparent 100%)',
120
+ WebkitMaskImage: 'linear-gradient(to bottom, black 30%, transparent 100%)',
121
+ } : {}),
122
+ }}
123
+ >
124
+ <ChangelogEntryList entries={entries} SimpleMarkdownRenderer={SimpleMarkdownRenderer} />
125
+ </div>
126
+ {previewNeedsFade && (
127
+ <button
128
+ type="button"
129
+ onClick={() => setPreviewExpanded(!previewExpanded)}
130
+ className="mt-4 flex items-center gap-1.5 text-sm text-ods-text-secondary hover:text-ods-accent transition-colors duration-200"
131
+ >
132
+ <span>{previewExpanded ? 'Show less' : `Show ${entries.length - 1} more`}</span>
133
+ <ChevronDown
134
+ className={`w-3.5 h-3.5 transition-transform duration-300 ${previewExpanded ? 'rotate-180' : ''}`}
135
+ />
136
+ </button>
137
+ )}
138
+ </div>
139
+ ) : (
140
+ <ChangelogEntryList entries={entries} SimpleMarkdownRenderer={SimpleMarkdownRenderer} />
141
+ )
101
142
  )}
102
143
  </div>
103
144
  );
104
145
  }
146
+
147
+ /**
148
+ * Internal list renderer — shared by the default and preview-first
149
+ * branches. Each entry is a bordered-left list item with bold title
150
+ * + markdown-rendered description body.
151
+ */
152
+ function ChangelogEntryList({
153
+ entries,
154
+ SimpleMarkdownRenderer,
155
+ }: {
156
+ entries: ChangelogEntry[];
157
+ SimpleMarkdownRenderer: React.ComponentType<{ content: string }>;
158
+ }) {
159
+ return (
160
+ <ul className="space-y-6">
161
+ {entries.map((entry, index) => (
162
+ <li key={index} className="border-l-2 border-ods-border pl-4 ml-0">
163
+ {/* Entry title — `text-h3` is body family + BOLD weight (per
164
+ ODS tokens: `--font-h3-weight: var(--font-weight-bold)`)
165
+ at 14/18px responsive. Same body size as the description
166
+ below, distinguished by weight — clean visual hierarchy
167
+ without inflating the body scale. */}
168
+ <p className="text-h3 text-ods-text-primary mb-2">{entry.title}</p>
169
+ {entry.description && (
170
+ /* Entry description — body text matches the main release
171
+ summary at the SAME 14/18px responsive `text-h4` scale.
172
+ The `SimpleMarkdownRenderer` forces its own `<p>` typography
173
+ which would override `text-h4` on `lg+` viewports and
174
+ inflate the changelog body to 20px. The `[&_p]:!` overrides
175
+ pin every descendant `<p>` back to the h4 responsive tokens
176
+ so the breakpoints stay aligned with the rest of the page. */
177
+ <div className="text-h4 text-ods-text-primary [&_p]:!text-[length:var(--font-size-h4-body)] [&_p]:!leading-[var(--font-line-space-h4-body)] [&_p]:!font-medium">
178
+ <SimpleMarkdownRenderer content={entry.description} />
179
+ </div>
180
+ )}
181
+ </li>
182
+ ))}
183
+ </ul>
184
+ );
185
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Delivery (ClickUp Bug-fixes & Enhancements) wire types — shared between the
3
+ * hub's `/api/delivery*` route shapes and the lib's `DeliveryTable`/`DeliveryLists`/
4
+ * `DeliverySection` components.
5
+ *
6
+ * Lifted from hub `types/delivery.ts` so embedders consuming the lib's
7
+ * delivery surfaces and the lib's own `ReleaseDetailPage` (which renders
8
+ * related delivery items) share one canonical shape.
9
+ */
10
+
11
+ export interface DeliveryItem {
12
+ id: string;
13
+ title: string;
14
+ description: string;
15
+ status: string;
16
+ statusColor: string; // ClickUp status color
17
+ taskType: 'Request' | 'Bug' | string; // ClickUp task type
18
+ /**
19
+ * Canonical ClickUp custom_item_id (1008 = Bug, 1009 = Request, …).
20
+ * Surfaced here so the chat's compact card can render a type-specific
21
+ * lucide icon via `TaskTypeIcon` instead of the two-letter initials
22
+ * fallback. Single source of truth lives in
23
+ * `lib/utils/clickup-task-type-utils.ts` (hub-side).
24
+ */
25
+ customItemId: number | null;
26
+ /**
27
+ * Every ClickUp list the task is associated with (home list + ClickUp's
28
+ * "Tasks in Multiple Lists" locations). UI joins these for display.
29
+ * Falls back to a single-element array containing the home list when
30
+ * there are no additional locations.
31
+ */
32
+ listNames: string[];
33
+ dateOpened: number; // Unix timestamp
34
+ dateUpdated: number; // Unix timestamp
35
+ dateClosed: number | null; // Unix timestamp or null if not closed
36
+ clickupUrl: string;
37
+ }
38
+
39
+ export interface DeliveryResponse {
40
+ completed: DeliveryItem[];
41
+ inProgress: DeliveryItem[];
42
+ }
43
+
44
+ // Task type to badge label mapping
45
+ export const TASK_TYPE_LABELS = {
46
+ Request: 'ENHANCEMENT',
47
+ Bug: 'BUG-FIX',
48
+ } as const;
49
+
50
+ // Task type to badge text-color mapping (ODS attention-red for Bug; default for others)
51
+ export const TASK_TYPE_TEXT_COLORS = {
52
+ Request: '', // Default white/grey
53
+ Bug: 'text-[var(--ods-attention-red-error)]',
54
+ } as const;
@@ -21,6 +21,7 @@ export * from './customer-interview'
21
21
  export * from './customer-interview-ai.types'
22
22
  export * from './luma'
23
23
  export * from './product-release'
24
+ export * from './delivery'
24
25
  export * from './vendor'
25
26
  export * from './vendor-links'
26
27
  export * from './video-processing'
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Dev-sections registry barrel.
3
+ *
4
+ * `RELEASE_STATUS_OPTIONS` (the hub's old alias) is deliberately NOT
5
+ * re-exported — embedders that need the release-status options should
6
+ * import `releaseStatusOptions` from `@flamingo-stack/openframe-frontend-core/types`
7
+ * directly. That matches lib's existing canonical export name and
8
+ * avoids one-way alias drift.
9
+ */
10
+
11
+ export {
12
+ OPENFRAME_DEV_SECTIONS,
13
+ ROADMAP_STATUS_OPTIONS,
14
+ DELIVERY_TASK_TYPE_OPTIONS,
15
+ type OpenframeDevSection,
16
+ type OpenframeDevSectionKey,
17
+ } from './openframe-dev-sections'