eidotter 0.37.4 → 0.38.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.
- package/README.md +1 -1
- package/dist/components/TimelineContainer/components/TimelineEntryCard.d.ts +5 -3
- package/dist/components/TimelineContainer/components/TimelineEntryCard.js +8 -3
- package/dist/components/TimelineContainer/components/types.d.ts +23 -0
- package/dist/components/TimelineContainer/components/variants/GalleryGrid.d.ts +20 -0
- package/dist/components/TimelineContainer/components/variants/GalleryGrid.js +69 -0
- package/dist/components/TimelineContainer/components/variants/TimelineEntryCardArticle.d.ts +26 -0
- package/dist/components/TimelineContainer/components/variants/TimelineEntryCardArticle.js +32 -0
- package/dist/components/TimelineContainer/components/variants/TimelineEntryCardGallery.d.ts +3 -3
- package/dist/components/TimelineContainer/components/variants/TimelineEntryCardGallery.js +5 -54
- package/dist/components/TimelineContainer/components/views/HourView.js +1 -1
- package/dist/eidotter.css +1 -1
- package/dist/index.es.js +347 -263
- package/dist/index.umd.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ for client apps and `'use client'` files.
|
|
|
186
186
|
|
|
187
187
|
| Component | Purpose |
|
|
188
188
|
|---|---|
|
|
189
|
-
| `TimelineContainer` | Multi-zoom timeline (year / month / day / hour views) |
|
|
189
|
+
| `TimelineContainer` | Multi-zoom timeline (year / month / day / hour views); text / image / gallery / article entry kinds |
|
|
190
190
|
| `TimelineNode` | Axis markers with shape variants and glow |
|
|
191
191
|
| `TextScramble` | DOS text decode animation |
|
|
192
192
|
| `RetroEffects` | CRT scanlines, noise, phosphor glow |
|
|
@@ -12,9 +12,11 @@ export interface TimelineEntryCardProps {
|
|
|
12
12
|
* Dispatcher for content-aware timeline entry rendering.
|
|
13
13
|
*
|
|
14
14
|
* Branches on `entry.kind`:
|
|
15
|
-
* - `text` —
|
|
16
|
-
* - `image` —
|
|
17
|
-
* - `gallery` —
|
|
15
|
+
* - `text` — trigger + expandable panel with `entry.content`.
|
|
16
|
+
* - `image` — single-image thumbnail → lightbox.
|
|
17
|
+
* - `gallery` — thumbnail grid → focused → lightbox.
|
|
18
|
+
* - `article` — devlog card: collapsed thumb-strip + summary; expanded
|
|
19
|
+
* gallery + body + read-more link.
|
|
18
20
|
*
|
|
19
21
|
* The card chrome (border, hover slide+glow, selected/expanded states)
|
|
20
22
|
* lives on the outer `.eidotter-timeline-card` element and applies to all
|
|
@@ -5,13 +5,16 @@ import { Button as AriaButton } from 'react-aria-components';
|
|
|
5
5
|
import { cn } from '../../../utils/cn.js';
|
|
6
6
|
import { TimelineEntryCardImage } from './variants/TimelineEntryCardImage.js';
|
|
7
7
|
import { TimelineEntryCardGallery } from './variants/TimelineEntryCardGallery.js';
|
|
8
|
+
import { TimelineEntryCardArticle } from './variants/TimelineEntryCardArticle.js';
|
|
8
9
|
/**
|
|
9
10
|
* Dispatcher for content-aware timeline entry rendering.
|
|
10
11
|
*
|
|
11
12
|
* Branches on `entry.kind`:
|
|
12
|
-
* - `text` —
|
|
13
|
-
* - `image` —
|
|
14
|
-
* - `gallery` —
|
|
13
|
+
* - `text` — trigger + expandable panel with `entry.content`.
|
|
14
|
+
* - `image` — single-image thumbnail → lightbox.
|
|
15
|
+
* - `gallery` — thumbnail grid → focused → lightbox.
|
|
16
|
+
* - `article` — devlog card: collapsed thumb-strip + summary; expanded
|
|
17
|
+
* gallery + body + read-more link.
|
|
15
18
|
*
|
|
16
19
|
* The card chrome (border, hover slide+glow, selected/expanded states)
|
|
17
20
|
* lives on the outer `.eidotter-timeline-card` element and applies to all
|
|
@@ -33,6 +36,8 @@ function renderBranch(entry, isExpanded, onSelect, children) {
|
|
|
33
36
|
return _jsx(TimelineEntryCardImage, { entry: entry, isExpanded: isExpanded, onSelect: onSelect });
|
|
34
37
|
case 'gallery':
|
|
35
38
|
return _jsx(TimelineEntryCardGallery, { entry: entry, isExpanded: isExpanded, onSelect: onSelect });
|
|
39
|
+
case 'article':
|
|
40
|
+
return (_jsx(TimelineEntryCardArticle, { entry: entry, isExpanded: isExpanded, onSelect: onSelect, children: children }));
|
|
36
41
|
default:
|
|
37
42
|
return assertNever(entry);
|
|
38
43
|
}
|
|
@@ -56,6 +56,29 @@ export type TimelineEntryData = (TimelineEntryBase & {
|
|
|
56
56
|
}) | (TimelineEntryBase & {
|
|
57
57
|
kind: 'gallery';
|
|
58
58
|
images: TimelineImage[];
|
|
59
|
+
}) | (TimelineEntryBase & {
|
|
60
|
+
kind: 'article';
|
|
61
|
+
/** Short plain-text preview shown while collapsed (80-char truncated, like the `text` kind). */
|
|
62
|
+
summary?: string;
|
|
63
|
+
/** Full body rendered in the expandable panel. */
|
|
64
|
+
content?: ReactNode;
|
|
65
|
+
/**
|
|
66
|
+
* Hero images: a decorative thumbnail strip while collapsed; an
|
|
67
|
+
* interactive gallery + Lightbox when expanded.
|
|
68
|
+
*/
|
|
69
|
+
images?: TimelineImage[];
|
|
70
|
+
/**
|
|
71
|
+
* "Read more" URL. Rendered as a plain `<a>` inside the expanded panel;
|
|
72
|
+
* it never hijacks expand/collapse. Sanitized via `isSafeHref`.
|
|
73
|
+
*
|
|
74
|
+
* Contrast with {@link TimelineImage.link}: an image `link` *replaces*
|
|
75
|
+
* the image interaction (navigates out, skips the lightbox), whereas an
|
|
76
|
+
* article `href` is *supplementary* — a separate anchor that never
|
|
77
|
+
* interferes with expand/collapse or the gallery.
|
|
78
|
+
*/
|
|
79
|
+
href?: string;
|
|
80
|
+
/** Read-more link label. @default 'READ MORE' */
|
|
81
|
+
hrefLabel?: string;
|
|
59
82
|
});
|
|
60
83
|
/**
|
|
61
84
|
* @deprecated Use `TimelineEntryData` instead. Existing entries must add a
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { TimelineImage } from '../types.js';
|
|
3
|
+
export interface GalleryGridProps {
|
|
4
|
+
/** Images to render as a grid. */
|
|
5
|
+
images: TimelineImage[];
|
|
6
|
+
/** When false (parent collapsed) the interaction state resets to the grid. */
|
|
7
|
+
isExpanded: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Shared iOS-Photos-style gallery: grid → focused (grow-in-place) → lightbox.
|
|
11
|
+
*
|
|
12
|
+
* Owns the full interaction state machine **and** the Lightbox mount, so both
|
|
13
|
+
* the `gallery` entry variant and the `article` variant's expanded body can
|
|
14
|
+
* drop it in with only `{ images, isExpanded }`. Resets to grid on parent
|
|
15
|
+
* collapse via a `useEffect` on `isExpanded`.
|
|
16
|
+
*
|
|
17
|
+
* Class names retain the `eidotter-timeline-card-gallery__*` prefix so existing
|
|
18
|
+
* gallery styling and consumer hooks keep working after the extraction.
|
|
19
|
+
*/
|
|
20
|
+
export declare const GalleryGrid: React.FC<GalleryGridProps>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { Button as AriaButton } from 'react-aria-components';
|
|
5
|
+
import { Lightbox } from '../../../Lightbox/index.js';
|
|
6
|
+
import { cn } from '../../../../utils/cn.js';
|
|
7
|
+
import { isSafeHref } from '../../../../utils/isSafeHref.js';
|
|
8
|
+
/**
|
|
9
|
+
* Shared iOS-Photos-style gallery: grid → focused (grow-in-place) → lightbox.
|
|
10
|
+
*
|
|
11
|
+
* Owns the full interaction state machine **and** the Lightbox mount, so both
|
|
12
|
+
* the `gallery` entry variant and the `article` variant's expanded body can
|
|
13
|
+
* drop it in with only `{ images, isExpanded }`. Resets to grid on parent
|
|
14
|
+
* collapse via a `useEffect` on `isExpanded`.
|
|
15
|
+
*
|
|
16
|
+
* Class names retain the `eidotter-timeline-card-gallery__*` prefix so existing
|
|
17
|
+
* gallery styling and consumer hooks keep working after the extraction.
|
|
18
|
+
*/
|
|
19
|
+
export const GalleryGrid = ({ images, isExpanded }) => {
|
|
20
|
+
const [state, setState] = useState({ phase: 'grid' });
|
|
21
|
+
// Reset when the parent collapses, so re-expanding starts at grid.
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!isExpanded)
|
|
24
|
+
setState({ phase: 'grid' });
|
|
25
|
+
}, [isExpanded]);
|
|
26
|
+
// Re-validate state.index when images shrinks — otherwise a stale index can
|
|
27
|
+
// feed an out-of-range value into the Lightbox below.
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
setState((prev) => {
|
|
30
|
+
if (prev.phase === 'grid')
|
|
31
|
+
return prev;
|
|
32
|
+
if (prev.index >= images.length)
|
|
33
|
+
return { phase: 'grid' };
|
|
34
|
+
return prev;
|
|
35
|
+
});
|
|
36
|
+
}, [images.length]);
|
|
37
|
+
if (images.length === 0)
|
|
38
|
+
return null;
|
|
39
|
+
const handleThumbClick = (index, image) => {
|
|
40
|
+
// Only short-circuit when the link will actually render as a navigable
|
|
41
|
+
// anchor — unsafe schemes are stripped by GalleryThumb, in which case the
|
|
42
|
+
// thumb falls back to the focus/lightbox interaction.
|
|
43
|
+
if (image.link && isSafeHref(image.link))
|
|
44
|
+
return;
|
|
45
|
+
setState((prev) => {
|
|
46
|
+
if (prev.phase === 'focused' && prev.index === index) {
|
|
47
|
+
return { phase: 'lightbox', index };
|
|
48
|
+
}
|
|
49
|
+
return { phase: 'focused', index };
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const focusedIndex = state.phase === 'focused' || state.phase === 'lightbox' ? state.index : null;
|
|
53
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "eidotter-timeline-card-gallery__grid", role: "list", children: images.map((img, i) => (_jsx(GalleryThumb, { image: img, isFocused: focusedIndex === i, onClick: () => handleThumbClick(i, img) }, i))) }), _jsx(Lightbox, { images: images, isOpen: state.phase === 'lightbox', initialIndex: state.phase === 'lightbox' ? state.index : 0, onClose: () => {
|
|
54
|
+
setState((prev) => prev.phase === 'lightbox' ? { phase: 'focused', index: prev.index } : prev);
|
|
55
|
+
}, onIndexChange: (idx) => {
|
|
56
|
+
setState((prev) => prev.phase === 'lightbox' ? { phase: 'lightbox', index: idx } : prev);
|
|
57
|
+
} })] }));
|
|
58
|
+
};
|
|
59
|
+
const GalleryThumb = ({ image, isFocused, onClick }) => {
|
|
60
|
+
const src = image.thumbnail || image.src;
|
|
61
|
+
const linkHref = image.link && isSafeHref(image.link) ? image.link : undefined;
|
|
62
|
+
// Outer listitem wraps either an anchor (link mode) or a button (interactive
|
|
63
|
+
// focus/lightbox mode). The listitem semantics live on the outer wrapper so
|
|
64
|
+
// both modes participate in the parent role="list" consistently.
|
|
65
|
+
if (linkHref) {
|
|
66
|
+
return (_jsx("div", { role: "listitem", className: "eidotter-timeline-card-gallery__cell eidotter-timeline-card-gallery__cell--link", children: _jsx("a", { className: "eidotter-timeline-card-gallery__link", href: linkHref, rel: "noopener noreferrer", children: _jsx("img", { className: "eidotter-timeline-card-gallery__img", src: src, alt: image.alt, loading: "lazy", width: image.width, height: image.height }) }) }));
|
|
67
|
+
}
|
|
68
|
+
return (_jsx("div", { role: "listitem", className: cn('eidotter-timeline-card-gallery__cell', isFocused && 'eidotter-timeline-card-gallery__cell--focused'), children: _jsx(AriaButton, { className: "eidotter-timeline-card-gallery__button", onPress: onClick, "aria-label": image.alt || 'Image', "aria-pressed": isFocused, children: _jsx("img", { className: "eidotter-timeline-card-gallery__img", src: src, alt: image.alt, width: image.width, height: image.height }) }) }));
|
|
69
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { TimelineEntryData } from '../types.js';
|
|
3
|
+
type ArticleEntry = Extract<TimelineEntryData, {
|
|
4
|
+
kind: 'article';
|
|
5
|
+
}>;
|
|
6
|
+
export interface TimelineEntryCardArticleProps {
|
|
7
|
+
entry: ArticleEntry;
|
|
8
|
+
isExpanded: boolean;
|
|
9
|
+
onSelect?: (id: string) => void;
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Article variant of TimelineEntryCard — a devlog-style entry.
|
|
14
|
+
*
|
|
15
|
+
* Collapsed: type/tags header, title, an 80-char `summary` preview, then a
|
|
16
|
+
* decorative thumbnail strip. Everything sits inside the single trigger button
|
|
17
|
+
* (`aria-hidden` thumbs, no nested interactive elements) so a click anywhere
|
|
18
|
+
* toggles expand/collapse.
|
|
19
|
+
*
|
|
20
|
+
* Expanded: uses the shared always-in-DOM `__body`/`__body-inner` + `inert` +
|
|
21
|
+
* 0fr→1fr grid pattern (like the text branch), so the interactive gallery,
|
|
22
|
+
* body content, and sanitized "READ MORE →" link are unfocusable/unclickable
|
|
23
|
+
* while collapsed — correct a11y for free.
|
|
24
|
+
*/
|
|
25
|
+
export declare const TimelineEntryCardArticle: React.FC<TimelineEntryCardArticleProps>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Button as AriaButton } from 'react-aria-components';
|
|
4
|
+
import { isSafeHref } from '../../../../utils/isSafeHref.js';
|
|
5
|
+
import { GalleryGrid } from './GalleryGrid.js';
|
|
6
|
+
/** Max thumbnails shown in the collapsed decorative strip before a `+N` cell. */
|
|
7
|
+
const MAX_THUMBS = 4;
|
|
8
|
+
/**
|
|
9
|
+
* Article variant of TimelineEntryCard — a devlog-style entry.
|
|
10
|
+
*
|
|
11
|
+
* Collapsed: type/tags header, title, an 80-char `summary` preview, then a
|
|
12
|
+
* decorative thumbnail strip. Everything sits inside the single trigger button
|
|
13
|
+
* (`aria-hidden` thumbs, no nested interactive elements) so a click anywhere
|
|
14
|
+
* toggles expand/collapse.
|
|
15
|
+
*
|
|
16
|
+
* Expanded: uses the shared always-in-DOM `__body`/`__body-inner` + `inert` +
|
|
17
|
+
* 0fr→1fr grid pattern (like the text branch), so the interactive gallery,
|
|
18
|
+
* body content, and sanitized "READ MORE →" link are unfocusable/unclickable
|
|
19
|
+
* while collapsed — correct a11y for free.
|
|
20
|
+
*/
|
|
21
|
+
export const TimelineEntryCardArticle = ({ entry, isExpanded, onSelect, children, }) => {
|
|
22
|
+
const { summary, content, href, hrefLabel } = entry;
|
|
23
|
+
const imageList = entry.images ?? [];
|
|
24
|
+
const hasImages = imageList.length > 0;
|
|
25
|
+
const readMoreHref = href && isSafeHref(href) ? href : undefined;
|
|
26
|
+
// The expanded body is always mounted (inert when collapsed) so the grid can
|
|
27
|
+
// animate. Render it whenever there's a gallery, body content, or read-more.
|
|
28
|
+
const hasBody = hasImages || content != null || readMoreHref !== undefined;
|
|
29
|
+
const thumbs = imageList.slice(0, MAX_THUMBS);
|
|
30
|
+
const overflow = imageList.length - thumbs.length;
|
|
31
|
+
return (_jsxs(_Fragment, { children: [_jsxs(AriaButton, { className: "eidotter-timeline-card__trigger", onPress: () => onSelect?.(entry.id), "aria-expanded": isExpanded, children: [_jsxs("div", { className: "eidotter-timeline-card__header", children: [entry.type && (_jsx("span", { className: "eidotter-timeline-card__type", children: entry.type.toUpperCase() })), entry.tags && entry.tags.length > 0 && (_jsx("span", { className: "eidotter-timeline-card__tags", children: entry.tags.map((t) => `#${t}`).join(' ') }))] }), _jsx("p", { className: "eidotter-timeline-card__title", children: entry.title }), !isExpanded && typeof summary === 'string' && summary.length > 0 && (_jsxs("p", { className: "eidotter-timeline-card__preview", children: [summary.slice(0, 80), summary.length > 80 ? '...' : ''] })), children, !isExpanded && hasImages && (_jsxs("div", { className: "eidotter-timeline-card-article__thumbstrip", "aria-hidden": "true", children: [thumbs.map((img, i) => (_jsx("span", { className: "eidotter-timeline-card-article__thumb", children: _jsx("img", { className: "eidotter-timeline-card-article__thumb-img", src: img.thumbnail || img.src, alt: "", loading: "lazy", width: img.width, height: img.height }) }, i))), overflow > 0 && (_jsxs("span", { className: "eidotter-timeline-card-article__thumb eidotter-timeline-card-article__thumb--more", children: ["+", overflow] }))] }))] }), hasBody && (_jsx("div", { className: "eidotter-timeline-card__body", children: _jsxs("div", { className: "eidotter-timeline-card__body-inner", inert: !isExpanded ? true : undefined, children: [hasImages && _jsx(GalleryGrid, { images: imageList, isExpanded: isExpanded }), content != null && (_jsx("div", { className: "eidotter-timeline-card-article__content", children: content })), readMoreHref && (_jsx("div", { className: "eidotter-timeline-card-article__footer", children: _jsxs("a", { className: "eidotter-timeline-card-article__readmore", href: readMoreHref, rel: "noopener noreferrer", children: [hrefLabel ?? 'READ MORE', " \u2192"] }) }))] }) }))] }));
|
|
32
|
+
};
|
|
@@ -9,9 +9,9 @@ export interface TimelineEntryCardGalleryProps {
|
|
|
9
9
|
onSelect?: (id: string) => void;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Gallery variant of TimelineEntryCard.
|
|
13
|
-
* grid → focused
|
|
14
|
-
*
|
|
12
|
+
* Gallery variant of TimelineEntryCard. Thin wrapper: renders the trigger
|
|
13
|
+
* (title) + the shared {@link GalleryGrid} (grid → focused → lightbox). Empty
|
|
14
|
+
* galleries render a title + placeholder instead of a trigger.
|
|
15
15
|
*/
|
|
16
16
|
export declare const TimelineEntryCardGallery: React.FC<TimelineEntryCardGalleryProps>;
|
|
17
17
|
export {};
|
|
@@ -1,67 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
3
|
import { Button as AriaButton } from 'react-aria-components';
|
|
5
|
-
import {
|
|
6
|
-
import { cn } from '../../../../utils/cn.js';
|
|
7
|
-
import { isSafeHref } from '../../../../utils/isSafeHref.js';
|
|
4
|
+
import { GalleryGrid } from './GalleryGrid.js';
|
|
8
5
|
/**
|
|
9
|
-
* Gallery variant of TimelineEntryCard.
|
|
10
|
-
* grid → focused
|
|
11
|
-
*
|
|
6
|
+
* Gallery variant of TimelineEntryCard. Thin wrapper: renders the trigger
|
|
7
|
+
* (title) + the shared {@link GalleryGrid} (grid → focused → lightbox). Empty
|
|
8
|
+
* galleries render a title + placeholder instead of a trigger.
|
|
12
9
|
*/
|
|
13
10
|
export const TimelineEntryCardGallery = ({ entry, isExpanded, onSelect, }) => {
|
|
14
|
-
const [state, setState] = useState({ phase: 'grid' });
|
|
15
|
-
// Reset when the parent card collapses, so re-expanding starts at grid.
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (!isExpanded)
|
|
18
|
-
setState({ phase: 'grid' });
|
|
19
|
-
}, [isExpanded]);
|
|
20
|
-
// Re-validate state.index when entry.images shrinks — otherwise a stale
|
|
21
|
-
// index can feed an out-of-range value into the Lightbox below.
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
setState((prev) => {
|
|
24
|
-
if (prev.phase === 'grid')
|
|
25
|
-
return prev;
|
|
26
|
-
if (prev.index >= entry.images.length)
|
|
27
|
-
return { phase: 'grid' };
|
|
28
|
-
return prev;
|
|
29
|
-
});
|
|
30
|
-
}, [entry.images.length]);
|
|
31
11
|
if (entry.images.length === 0) {
|
|
32
12
|
if (process.env.NODE_ENV !== 'production') {
|
|
33
13
|
console.error(`[eidotter] TimelineEntryCard kind="gallery" entry "${entry.id}" has no images.`);
|
|
34
14
|
}
|
|
35
15
|
return (_jsxs(_Fragment, { children: [_jsx("p", { className: "eidotter-timeline-card__title", children: entry.title }), _jsx("p", { className: "eidotter-timeline-card-gallery__empty", children: "No images." })] }));
|
|
36
16
|
}
|
|
37
|
-
|
|
38
|
-
// Only short-circuit when the link will actually render as a navigable
|
|
39
|
-
// anchor — unsafe schemes are stripped by GalleryThumb, in which case
|
|
40
|
-
// the thumb falls back to the focus/lightbox interaction.
|
|
41
|
-
if (image.link && isSafeHref(image.link))
|
|
42
|
-
return;
|
|
43
|
-
setState((prev) => {
|
|
44
|
-
if (prev.phase === 'focused' && prev.index === index) {
|
|
45
|
-
return { phase: 'lightbox', index };
|
|
46
|
-
}
|
|
47
|
-
return { phase: 'focused', index };
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
const focusedIndex = state.phase === 'focused' || state.phase === 'lightbox' ? state.index : null;
|
|
51
|
-
return (_jsxs(_Fragment, { children: [_jsx(AriaButton, { className: "eidotter-timeline-card__trigger", onPress: () => onSelect?.(entry.id), "aria-expanded": isExpanded, children: _jsx("p", { className: "eidotter-timeline-card__title", children: entry.title }) }), _jsx("div", { className: "eidotter-timeline-card-gallery__grid", role: "list", children: entry.images.map((img, i) => (_jsx(GalleryThumb, { image: img, isFocused: focusedIndex === i, onClick: () => handleThumbClick(i, img) }, i))) }), _jsx(Lightbox, { images: entry.images, isOpen: state.phase === 'lightbox', initialIndex: state.phase === 'lightbox' ? state.index : 0, onClose: () => {
|
|
52
|
-
setState((prev) => prev.phase === 'lightbox' ? { phase: 'focused', index: prev.index } : prev);
|
|
53
|
-
}, onIndexChange: (idx) => {
|
|
54
|
-
setState((prev) => prev.phase === 'lightbox' ? { phase: 'lightbox', index: idx } : prev);
|
|
55
|
-
} })] }));
|
|
56
|
-
};
|
|
57
|
-
const GalleryThumb = ({ image, isFocused, onClick }) => {
|
|
58
|
-
const src = image.thumbnail || image.src;
|
|
59
|
-
const linkHref = image.link && isSafeHref(image.link) ? image.link : undefined;
|
|
60
|
-
// Outer listitem wraps either an anchor (link mode) or a button (interactive
|
|
61
|
-
// focus/lightbox mode). The listitem semantics live on the outer wrapper so
|
|
62
|
-
// both modes participate in the parent role="list" consistently.
|
|
63
|
-
if (linkHref) {
|
|
64
|
-
return (_jsx("div", { role: "listitem", className: "eidotter-timeline-card-gallery__cell eidotter-timeline-card-gallery__cell--link", children: _jsx("a", { className: "eidotter-timeline-card-gallery__link", href: linkHref, rel: "noopener noreferrer", children: _jsx("img", { className: "eidotter-timeline-card-gallery__img", src: src, alt: image.alt, width: image.width, height: image.height }) }) }));
|
|
65
|
-
}
|
|
66
|
-
return (_jsx("div", { role: "listitem", className: cn('eidotter-timeline-card-gallery__cell', isFocused && 'eidotter-timeline-card-gallery__cell--focused'), children: _jsx(AriaButton, { className: "eidotter-timeline-card-gallery__button", onPress: onClick, "aria-label": image.alt || 'Image', "aria-pressed": isFocused, children: _jsx("img", { className: "eidotter-timeline-card-gallery__img", src: src, alt: image.alt, width: image.width, height: image.height }) }) }));
|
|
17
|
+
return (_jsxs(_Fragment, { children: [_jsx(AriaButton, { className: "eidotter-timeline-card__trigger", onPress: () => onSelect?.(entry.id), "aria-expanded": isExpanded, children: _jsx("p", { className: "eidotter-timeline-card__title", children: entry.title }) }), _jsx(GalleryGrid, { images: entry.images, isExpanded: isExpanded })] }));
|
|
67
18
|
};
|
|
@@ -13,7 +13,7 @@ import { formatTimestamp } from '../timelineUtils.js';
|
|
|
13
13
|
export const HourView = React.memo(({ buckets, selectedEntryId, onEntrySelect, renderEntry, }) => {
|
|
14
14
|
return (_jsx("div", { className: "timeline-view timeline-view--hour", role: "list", children: buckets.map((bucket) => (_jsxs("div", { className: "timeline-view__bucket timeline-view__bucket--hour", role: "listitem", children: [_jsx("div", { className: "timeline-view__node", children: _jsx(TimelineNode, { shape: "circle", size: "medium", variant: "default", label: bucket.label, labelPosition: "right" }) }), _jsx("div", { className: "timeline-view__content", children: bucket.entries.map((entry) => {
|
|
15
15
|
const isSelected = selectedEntryId === entry.id;
|
|
16
|
-
const defaultRender = () => (_jsxs(TimelineEntryCard, { entry: entry, isSelected: isSelected, isExpanded: true, onSelect: onEntrySelect, footer: _jsxs("div", { className: "timeline-view__entry-footer", children: [entry.tags && entry.tags.length > 0 && (_jsx(TagGroup, { gap: "tight", "aria-label": `Tags for ${entry.title}`, children: entry.tags.map((tag) => (_jsx(Tag, { size: "small", variant: "outlined", children: tag }, tag))) })), entry.type && (_jsx(Badge, { variant: "default", size: "small", children: entry.type }))] }), children: [_jsx("time", { className: "timeline-view__timestamp", dateTime: entry.date, children: formatTimestamp(entry.date) }), entry.kind === 'text' && entry.content !== undefined && (_jsx("div", { className: "timeline-view__entry-content", children: entry.content }))] }));
|
|
16
|
+
const defaultRender = () => (_jsxs(TimelineEntryCard, { entry: entry, isSelected: isSelected, isExpanded: true, onSelect: onEntrySelect, footer: _jsxs("div", { className: "timeline-view__entry-footer", children: [entry.tags && entry.tags.length > 0 && (_jsx(TagGroup, { gap: "tight", "aria-label": `Tags for ${entry.title}`, children: entry.tags.map((tag) => (_jsx(Tag, { size: "small", variant: "outlined", children: tag }, tag))) })), entry.type && (_jsx(Badge, { variant: "default", size: "small", children: entry.type }))] }), children: [_jsx("time", { className: "timeline-view__timestamp", dateTime: entry.date, children: formatTimestamp(entry.date) }), entry.kind === 'text' && entry.content !== undefined && (_jsx("div", { className: "timeline-view__entry-content", children: entry.content })), entry.kind === 'article' && entry.summary !== undefined && (_jsx("div", { className: "timeline-view__entry-content", children: entry.summary }))] }));
|
|
17
17
|
return (_jsx(React.Fragment, { children: renderEntry
|
|
18
18
|
? renderEntry(entry, { isExpanded: true, isSelected, defaultRender })
|
|
19
19
|
: defaultRender() }, entry.id));
|