@gramps-connect/story 0.2.0 → 0.3.0

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.
@@ -114,15 +114,49 @@ function SlidePhoto({ mediaKey, item, caption, credit, resolveMedia, colors }: {
114
114
  );
115
115
  }
116
116
 
117
- function SlideContent({ slide, story, resolveMedia, colors }: {
117
+ /** A small button showing one of a slide's photos. */
118
+ function PhotoThumb({ mediaKey, item, label, selected, onSelect, resolveMedia, colors }: {
119
+ mediaKey: string;
120
+ item: MediaItem;
121
+ label: string;
122
+ selected: boolean;
123
+ onSelect: () => void;
124
+ resolveMedia: StoryPlayerProps["resolveMedia"];
125
+ colors: PlayerColors;
126
+ }) {
127
+ const [src, setSrc] = useState<string | null>(null);
128
+ useEffect(() => {
129
+ let cancelled = false;
130
+ resolveMedia(mediaKey, item).then((url) => { if (!cancelled) setSrc(url); }, () => {});
131
+ return () => { cancelled = true; };
132
+ }, [mediaKey, item, resolveMedia]);
133
+ return (
134
+ <button
135
+ type="button" aria-label={label} title={label} aria-pressed={selected} onClick={onSelect}
136
+ style={{
137
+ width: 44, height: 44, padding: 0, borderRadius: 4, overflow: "hidden", cursor: "pointer",
138
+ border: `2px solid ${selected ? colors.mark : "transparent"}`, background: colors.border,
139
+ opacity: selected ? 1 : 0.7,
140
+ }}
141
+ >
142
+ {src && <img src={src} alt="" style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />}
143
+ </button>
144
+ );
145
+ }
146
+
147
+ function SlideContent({ slide, story, resolveMedia, colors, strings }: {
118
148
  slide: Slide | undefined;
119
149
  story: StoryDoc;
120
150
  resolveMedia: StoryPlayerProps["resolveMedia"];
121
151
  colors: PlayerColors;
152
+ strings: PlayerStrings;
122
153
  }) {
123
- // The format allows several photos per slide; the player shows the
124
- // first one that exists.
125
- const photo = slide?.media?.find((m) => Object.prototype.hasOwnProperty.call(story.media, m.ref));
154
+ // Every photo whose media exists; with several, thumbnails pick which
155
+ // one is shown. Keyed by slide in the caller, so each slide starts on
156
+ // its first photo.
157
+ const photos = (slide?.media ?? []).filter((m) => Object.prototype.hasOwnProperty.call(story.media, m.ref));
158
+ const [shown, setShown] = useState(0);
159
+ const photo = photos[Math.min(shown, photos.length - 1)];
126
160
  const eyebrow = [slide?.date?.text, slide?.place?.name].filter(Boolean).join(" · ");
127
161
  return (
128
162
  <>
@@ -136,6 +170,22 @@ function SlideContent({ slide, story, resolveMedia, colors }: {
136
170
  colors={colors}
137
171
  />
138
172
  )}
173
+ {photos.length > 1 && (
174
+ <div style={{ display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "center" }}>
175
+ {photos.map((m, i) => (
176
+ <PhotoThumb
177
+ key={`${i}:${m.ref}`}
178
+ mediaKey={m.ref}
179
+ item={story.media[m.ref]}
180
+ label={strings.photoOf(i + 1, photos.length)}
181
+ selected={photo === m}
182
+ onSelect={() => setShown(i)}
183
+ resolveMedia={resolveMedia}
184
+ colors={colors}
185
+ />
186
+ ))}
187
+ </div>
188
+ )}
139
189
  <div style={{ display: "flex", flexDirection: "column", gap: 4, alignItems: "center", maxWidth: 640, textAlign: "center" }}>
140
190
  {eyebrow && <div dir="auto" style={{ fontSize: 14, color: colors.muted }}>{eyebrow}</div>}
141
191
  {slide?.title && <div dir="auto" style={{ fontSize: 20, fontWeight: 700, color: colors.text }}>{slide.title}</div>}
@@ -326,7 +376,9 @@ export function StoryPlayer({
326
376
  return item ? resolveMedia(key, item) : Promise.resolve(null);
327
377
  }, [story, resolveMedia]);
328
378
 
329
- const content = <SlideContent slide={slide} story={story} resolveMedia={resolveMedia} colors={colors} />;
379
+ const content = (
380
+ <SlideContent key={slide?.id ?? index} slide={slide} story={story} resolveMedia={resolveMedia} colors={colors} strings={strings} />
381
+ );
330
382
  const timeline = showTimeline && (
331
383
  <TimelineStrip points={datedSlides} currentIndex={index} onSelect={setIndex} colors={colors} />
332
384
  );
@@ -15,6 +15,8 @@ export interface PlayerStrings {
15
15
  historicalMapYear: (year: number) => string;
16
16
  noDateStandardMap: string;
17
17
  historicalYear: string;
18
+ /** A photo's thumbnail button, on a slide with several. */
19
+ photoOf: (n: number, total: number) => string;
18
20
  }
19
21
 
20
22
  export const ENGLISH: PlayerStrings = {
@@ -28,6 +30,7 @@ export const ENGLISH: PlayerStrings = {
28
30
  historicalMapYear: (year) => `Historical map, ${year}`,
29
31
  noDateStandardMap: "No date here — standard map",
30
32
  historicalYear: "Historical map year",
33
+ photoOf: (n, total) => `Photo ${n} of ${total}`,
31
34
  };
32
35
 
33
36
  const TABLE: Record<string, PlayerStrings> = {
@@ -43,6 +46,7 @@ const TABLE: Record<string, PlayerStrings> = {
43
46
  historicalMapYear: (year) => `Historische Karte, ${year}`,
44
47
  noDateStandardMap: "Kein Datum hier — Standardkarte",
45
48
  historicalYear: "Jahr der historischen Karte",
49
+ photoOf: (n, total) => `Foto ${n} von ${total}`,
46
50
  },
47
51
  es: {
48
52
  previous: "Anterior",
@@ -55,6 +59,7 @@ const TABLE: Record<string, PlayerStrings> = {
55
59
  historicalMapYear: (year) => `Mapa histórico, ${year}`,
56
60
  noDateStandardMap: "Sin fecha aquí — mapa estándar",
57
61
  historicalYear: "Año del mapa histórico",
62
+ photoOf: (n, total) => `Foto ${n} de ${total}`,
58
63
  },
59
64
  fr: {
60
65
  previous: "Précédent",
@@ -67,6 +72,7 @@ const TABLE: Record<string, PlayerStrings> = {
67
72
  historicalMapYear: (year) => `Carte historique, ${year}`,
68
73
  noDateStandardMap: "Pas de date ici — carte standard",
69
74
  historicalYear: "Année de la carte historique",
75
+ photoOf: (n, total) => `Photo ${n} sur ${total}`,
70
76
  },
71
77
  nl: {
72
78
  previous: "Vorige",
@@ -79,6 +85,7 @@ const TABLE: Record<string, PlayerStrings> = {
79
85
  historicalMapYear: (year) => `Historische kaart, ${year}`,
80
86
  noDateStandardMap: "Geen datum hier — standaardkaart",
81
87
  historicalYear: "Jaar van de historische kaart",
88
+ photoOf: (n, total) => `Foto ${n} van ${total}`,
82
89
  },
83
90
  };
84
91