@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.272",
3
+ "version": "0.0.273",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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-[#FFC008]' : 'border-ods-border hover:border-[#FFC008]'} rounded-lg flex flex-col items-center justify-center cursor-pointer bg-[#1A1A1A]`}
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-[#666666] font-['DM_Sans'] text-[12px]">PNG, JPEG, WebP, GIF up to 5MB</span>
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>
@@ -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 { ImageGalleryModal } from '../../ui/image-gallery-modal';
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 Gallery - Horizontal Scrolling */}
311
- {releaseMedia && releaseMedia.length > 0 && (
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
  }