@flamingo-stack/openframe-frontend-core 0.0.272 → 0.0.273
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/components/index.cjs +53 -29
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +252 -228
- package/dist/components/index.js.map +1 -1
- package/dist/components/shared/media-gallery-strip.d.ts +26 -0
- package/dist/components/shared/media-gallery-strip.d.ts.map +1 -0
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/hero-image-uploader.tsx +2 -2
- package/src/components/index.ts +5 -0
- package/src/components/shared/media-gallery-strip.tsx +93 -0
- package/src/components/shared/product-release/release-detail-page.tsx +3 -36
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface MediaGalleryStripItem {
|
|
3
|
+
media_type: string;
|
|
4
|
+
media_url: string;
|
|
5
|
+
title?: string | null;
|
|
6
|
+
/** Stable key when present (e.g. release_media rows); falls back to index. */
|
|
7
|
+
id?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MediaGalleryStripProps {
|
|
10
|
+
items: MediaGalleryStripItem[];
|
|
11
|
+
/** Optional cap on tiles shown (e.g. product-release shows 5). Default: all. */
|
|
12
|
+
maxDisplay?: number;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Read-only media gallery — a horizontal-scroll strip of 240×200 tiles where
|
|
17
|
+
* still-images open an {@link ImageGalleryModal} lightbox and video clips play
|
|
18
|
+
* inline via {@link Video}. The lightbox index is the tile's rank among the
|
|
19
|
+
* IMAGE-only items (clips are skipped), so the modal opens on the correct image.
|
|
20
|
+
*
|
|
21
|
+
* Single source of truth for the detail-page media strip — replaces the markup
|
|
22
|
+
* that was hand-rolled inline in the product-release and "What I Shipped" detail
|
|
23
|
+
* pages (the release copy also had a raw-index lightbox bug this component fixes).
|
|
24
|
+
*/
|
|
25
|
+
export declare function MediaGalleryStrip({ items, maxDisplay, className }: MediaGalleryStripProps): React.JSX.Element | null;
|
|
26
|
+
//# sourceMappingURL=media-gallery-strip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-gallery-strip.d.ts","sourceRoot":"","sources":["../../../src/components/shared/media-gallery-strip.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAIxC,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8EAA8E;IAC9E,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAOD,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,qBAAqB,EAAE,CAAC;IAC/B,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,sBAAsB,4BAwDzF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"release-detail-page.d.ts","sourceRoot":"","sources":["../../../../src/components/shared/product-release/release-detail-page.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"release-detail-page.d.ts","sourceRoot":"","sources":["../../../../src/components/shared/product-release/release-detail-page.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -149,7 +149,7 @@ export function HeroImageUploader({ imageUrl, onChange, uploadEndpoint, height =
|
|
|
149
149
|
</div>
|
|
150
150
|
) : (
|
|
151
151
|
<div
|
|
152
|
-
className={`w-full h-full border-2 border-dashed ${uploading ? 'border-
|
|
152
|
+
className={`w-full h-full border-2 border-dashed ${uploading ? 'border-ods-accent' : 'border-ods-border hover:border-ods-accent'} rounded-lg flex flex-col items-center justify-center cursor-pointer bg-ods-bg`}
|
|
153
153
|
style={{ height: heightStyle }}
|
|
154
154
|
onClick={openDialog}
|
|
155
155
|
>
|
|
@@ -160,7 +160,7 @@ export function HeroImageUploader({ imageUrl, onChange, uploadEndpoint, height =
|
|
|
160
160
|
<ImageIcon className="h-12 w-12 text-ods-text-secondary" />
|
|
161
161
|
<span className="text-ods-text-primary font-['DM_Sans'] text-[16px] font-medium mt-2">Upload cover image</span>
|
|
162
162
|
<span className="text-ods-text-secondary font-['DM_Sans'] text-[14px] mt-1">Click to upload or drag and drop</span>
|
|
163
|
-
<span className="text-
|
|
163
|
+
<span className="text-ods-text-secondary font-['DM_Sans'] text-[12px]">PNG, JPEG, WebP, GIF up to 5MB</span>
|
|
164
164
|
</>
|
|
165
165
|
)}
|
|
166
166
|
</div>
|
package/src/components/index.ts
CHANGED
|
@@ -95,6 +95,11 @@ export { DetailPageSkeleton, type DetailPageSkeletonProps } from './shared/detai
|
|
|
95
95
|
// fallbackBio copy (the hub uses defaultAuthorFallbackBio()).
|
|
96
96
|
export { ArticleAuthorByline, type ArticleAuthorBylineProps } from './shared/article-author-byline'
|
|
97
97
|
|
|
98
|
+
// Read-only media gallery strip (horizontal scroll; images → lightbox, clips →
|
|
99
|
+
// inline Video). Single source of truth for the detail-page media gallery —
|
|
100
|
+
// used by product-release + What I Shipped detail pages.
|
|
101
|
+
export { MediaGalleryStrip, type MediaGalleryStripItem, type MediaGalleryStripProps } from './shared/media-gallery-strip'
|
|
102
|
+
|
|
98
103
|
// Author detail-page body (identity + socials + bio + expertise, rail as
|
|
99
104
|
// children) — the one implementation behind /authors/[slug] and embedded
|
|
100
105
|
// author pages.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Video } from '../features/video';
|
|
5
|
+
import { ImageGalleryModal } from '../ui/image-gallery-modal';
|
|
6
|
+
|
|
7
|
+
export interface MediaGalleryStripItem {
|
|
8
|
+
media_type: string;
|
|
9
|
+
media_url: string;
|
|
10
|
+
title?: string | null;
|
|
11
|
+
/** Stable key when present (e.g. release_media rows); falls back to index. */
|
|
12
|
+
id?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Still-images open the lightbox; clips ('video'/'demo') play inline. */
|
|
16
|
+
function isGalleryImage(mediaType: string): boolean {
|
|
17
|
+
return mediaType !== 'video' && mediaType !== 'demo';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MediaGalleryStripProps {
|
|
21
|
+
items: MediaGalleryStripItem[];
|
|
22
|
+
/** Optional cap on tiles shown (e.g. product-release shows 5). Default: all. */
|
|
23
|
+
maxDisplay?: number;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Read-only media gallery — a horizontal-scroll strip of 240×200 tiles where
|
|
29
|
+
* still-images open an {@link ImageGalleryModal} lightbox and video clips play
|
|
30
|
+
* inline via {@link Video}. The lightbox index is the tile's rank among the
|
|
31
|
+
* IMAGE-only items (clips are skipped), so the modal opens on the correct image.
|
|
32
|
+
*
|
|
33
|
+
* Single source of truth for the detail-page media strip — replaces the markup
|
|
34
|
+
* that was hand-rolled inline in the product-release and "What I Shipped" detail
|
|
35
|
+
* pages (the release copy also had a raw-index lightbox bug this component fixes).
|
|
36
|
+
*/
|
|
37
|
+
export function MediaGalleryStrip({ items, maxDisplay, className }: MediaGalleryStripProps) {
|
|
38
|
+
const [galleryOpen, setGalleryOpen] = useState(false);
|
|
39
|
+
const [galleryIndex, setGalleryIndex] = useState(0);
|
|
40
|
+
|
|
41
|
+
if (!items || items.length === 0) return null;
|
|
42
|
+
|
|
43
|
+
// Explicit numeric check (clamped): `maxDisplay={0}` means "show none", which a
|
|
44
|
+
// truthy check would wrongly treat as "no cap → show all".
|
|
45
|
+
const display = typeof maxDisplay === 'number' ? items.slice(0, Math.max(0, maxDisplay)) : items;
|
|
46
|
+
const galleryImages = display.filter((m) => isGalleryImage(m.media_type)).map((m) => m.media_url);
|
|
47
|
+
|
|
48
|
+
const tileClass =
|
|
49
|
+
'shrink-0 w-[240px] h-[200px] rounded-md overflow-hidden border border-ods-border bg-black transition-opacity';
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
<div className={`flex gap-6 overflow-x-auto w-full ${className ?? ''}`}>
|
|
54
|
+
{display.map((mediaItem, index) => {
|
|
55
|
+
// Image tiles open the lightbox, so they're real <button>s — keyboard
|
|
56
|
+
// focusable + Enter/Space activatable. Clips render in <Video>, which
|
|
57
|
+
// owns its own controls, so their tile stays a plain container.
|
|
58
|
+
if (isGalleryImage(mediaItem.media_type)) {
|
|
59
|
+
return (
|
|
60
|
+
<button
|
|
61
|
+
key={mediaItem.id || index}
|
|
62
|
+
type="button"
|
|
63
|
+
className={`${tileClass} cursor-pointer hover:opacity-80`}
|
|
64
|
+
aria-label={`Open ${mediaItem.title || `media ${index + 1}`} in gallery`}
|
|
65
|
+
onClick={() => {
|
|
66
|
+
// Lightbox position = rank among image-only items (clips skipped).
|
|
67
|
+
setGalleryIndex(display.slice(0, index).filter((m) => isGalleryImage(m.media_type)).length);
|
|
68
|
+
setGalleryOpen(true);
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<img src={mediaItem.media_url} alt={mediaItem.title || `Media ${index + 1}`} className="w-full h-full object-cover" />
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return (
|
|
76
|
+
<div key={mediaItem.id || index} className={tileClass}>
|
|
77
|
+
<Video url={mediaItem.media_url} layout="native" />
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{galleryImages.length > 0 && (
|
|
84
|
+
<ImageGalleryModal
|
|
85
|
+
images={galleryImages}
|
|
86
|
+
isOpen={galleryOpen}
|
|
87
|
+
onClose={() => setGalleryOpen(false)}
|
|
88
|
+
initialIndex={galleryIndex}
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -13,7 +13,7 @@ import { ReleaseChangelogSection } from '../../ui/release-changelog-section';
|
|
|
13
13
|
import { EntityTagBadges } from '../../features/entity-tag-badges';
|
|
14
14
|
import { EntityMetadataAuthorCell } from '../../chat/entity-cards/entity-author-card';
|
|
15
15
|
import type { EntityAuthor } from '../../../types/entity-author';
|
|
16
|
-
import {
|
|
16
|
+
import { MediaGalleryStrip } from '../media-gallery-strip';
|
|
17
17
|
import { GitHubIcon } from '../../icons/github-icon';
|
|
18
18
|
import { AlertTriangle, ExternalLink, BookMarked, Sparkles, TrendingUp, Wrench } from 'lucide-react';
|
|
19
19
|
import { formatReleaseDate } from '../../../utils/date-formatters';
|
|
@@ -131,8 +131,6 @@ export function ReleaseDetailPage({
|
|
|
131
131
|
// Use pre-fetched data if provided (admin preview), otherwise fetch via hook (public)
|
|
132
132
|
const { data: fetchedRelease, error, isLoading } = useRelease(initialData ? undefined : slug);
|
|
133
133
|
const release = (initialData || fetchedRelease) as Record<string, unknown> | undefined;
|
|
134
|
-
const [galleryOpen, setGalleryOpen] = useState(false);
|
|
135
|
-
const [galleryIndex, setGalleryIndex] = useState(0);
|
|
136
134
|
|
|
137
135
|
// Back-button config — mirrors DevSectionPage / LegalDocumentPage.
|
|
138
136
|
// Default: { label: 'Back to home', href: '/' }. Pass `false` to hide
|
|
@@ -307,29 +305,8 @@ export function ReleaseDetailPage({
|
|
|
307
305
|
/>
|
|
308
306
|
</div>
|
|
309
307
|
|
|
310
|
-
{/* Image
|
|
311
|
-
{releaseMedia
|
|
312
|
-
<div className="flex gap-6 overflow-x-auto w-full">
|
|
313
|
-
{releaseMedia.slice(0, 5).map((mediaItem, index) => (
|
|
314
|
-
<div
|
|
315
|
-
key={mediaItem.id || index}
|
|
316
|
-
className="shrink-0 w-[240px] h-[200px] rounded-md overflow-hidden border border-ods-border bg-black cursor-pointer hover:opacity-80 transition-opacity"
|
|
317
|
-
onClick={() => {
|
|
318
|
-
if (mediaItem.media_type !== 'video' && mediaItem.media_type !== 'demo') {
|
|
319
|
-
setGalleryIndex(index);
|
|
320
|
-
setGalleryOpen(true);
|
|
321
|
-
}
|
|
322
|
-
}}
|
|
323
|
-
>
|
|
324
|
-
{mediaItem.media_type === 'video' || mediaItem.media_type === 'demo' ? (
|
|
325
|
-
<Video url={mediaItem.media_url} layout="native" />
|
|
326
|
-
) : (
|
|
327
|
-
<img src={mediaItem.media_url} alt={mediaItem.title || `Media ${index + 1}`} className="w-full h-full object-cover" />
|
|
328
|
-
)}
|
|
329
|
-
</div>
|
|
330
|
-
))}
|
|
331
|
-
</div>
|
|
332
|
-
)}
|
|
308
|
+
{/* Image gallery — shared strip + lightbox (images) / inline clips. */}
|
|
309
|
+
<MediaGalleryStrip items={releaseMedia ?? []} maxDisplay={5} />
|
|
333
310
|
|
|
334
311
|
{/* Summary */}
|
|
335
312
|
{releaseSummary && (
|
|
@@ -580,16 +557,6 @@ export function ReleaseDetailPage({
|
|
|
580
557
|
</div>
|
|
581
558
|
)}
|
|
582
559
|
</div>
|
|
583
|
-
|
|
584
|
-
{/* Gallery Modal */}
|
|
585
|
-
{releaseMedia && releaseMedia.filter((m) => m.media_type !== 'video' && m.media_type !== 'demo').length > 0 && (
|
|
586
|
-
<ImageGalleryModal
|
|
587
|
-
images={releaseMedia.filter((m) => m.media_type !== 'video' && m.media_type !== 'demo').map((m) => m.media_url)}
|
|
588
|
-
isOpen={galleryOpen}
|
|
589
|
-
onClose={() => setGalleryOpen(false)}
|
|
590
|
-
initialIndex={galleryIndex}
|
|
591
|
-
/>
|
|
592
|
-
)}
|
|
593
560
|
</PageShell>
|
|
594
561
|
);
|
|
595
562
|
}
|