@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
|
@@ -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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
package/src/types/index.ts
CHANGED
|
@@ -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'
|