@bearmenu/ui 0.8.17 → 0.8.19
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-WYTJR2YU.js → chunk-5CPBSNWQ.js} +6 -1
- package/dist/chunk-BRXI6EDO.js +84 -0
- package/dist/{chunk-54FQQZZJ.js → chunk-EQYB4Q77.js} +1 -1
- package/dist/chunk-QRSIJC4X.js +156 -0
- package/dist/components/concept-compare-rail.js +2 -2
- package/dist/components/concept-compare.js +2 -2
- package/dist/components/event-card.d.ts +22 -4
- package/dist/components/event-card.js +95 -50
- package/dist/components/event-day-header.d.ts +5 -3
- package/dist/components/event-day-header.js +12 -4
- package/dist/components/event-hub-hero.d.ts +12 -2
- package/dist/components/event-hub-hero.js +11 -5
- package/dist/components/event-tonight-strip.d.ts +50 -0
- package/dist/components/event-tonight-strip.js +67 -0
- package/dist/components/menu-family-block.js +4 -3
- package/dist/components/menu-hub-hero.d.ts +7 -1
- package/dist/components/menu-hub-hero.js +2 -1
- package/dist/components/poster-stack.d.ts +36 -3
- package/dist/components/poster-stack.js +87 -48
- package/dist/components/snap-rail.d.ts +2 -2
- package/dist/components/snap-rail.js +1 -1
- package/dist/components/timeline-day.d.ts +54 -0
- package/dist/components/timeline-day.js +65 -0
- package/dist/components/timeline-hour-puck.d.ts +47 -0
- package/dist/components/timeline-hour-puck.js +3 -0
- package/dist/components/timeline-hour.d.ts +96 -0
- package/dist/components/{timeline-band.js → timeline-hour.js} +2 -1
- package/dist/components/timeline-playhead.js +1 -3
- package/package.json +1 -1
- package/src/components/event-card.tsx +70 -13
- package/src/components/event-day-header.tsx +19 -7
- package/src/components/event-hub-hero.tsx +25 -3
- package/src/components/event-tonight-strip.stories.tsx +78 -0
- package/src/components/event-tonight-strip.test.tsx +49 -0
- package/src/components/event-tonight-strip.tsx +134 -0
- package/src/components/menu-family-block.tsx +13 -6
- package/src/components/menu-hub-hero.test.tsx +13 -0
- package/src/components/menu-hub-hero.tsx +9 -2
- package/src/components/poster-stack.stories.tsx +43 -1
- package/src/components/poster-stack.tsx +104 -19
- package/src/components/snap-rail.tsx +6 -1
- package/src/components/timeline-day.stories.tsx +201 -0
- package/src/components/timeline-day.test.tsx +78 -0
- package/src/components/timeline-day.tsx +114 -0
- package/src/components/timeline-hour-puck.tsx +142 -0
- package/src/components/timeline-hour.tsx +266 -0
- package/src/components/timeline-playhead.tsx +1 -1
- package/dist/chunk-NSNEANOQ.js +0 -104
- package/dist/components/hour-band.d.ts +0 -66
- package/dist/components/hour-band.js +0 -81
- package/dist/components/hour-group.d.ts +0 -67
- package/dist/components/hour-group.js +0 -59
- package/dist/components/timeline-band.d.ts +0 -64
- package/src/components/hour-band.stories.tsx +0 -182
- package/src/components/hour-band.tsx +0 -146
- package/src/components/hour-group.stories.tsx +0 -100
- package/src/components/hour-group.tsx +0 -124
- package/src/components/timeline-band.stories.tsx +0 -206
- package/src/components/timeline-band.tsx +0 -247
|
@@ -10,13 +10,17 @@ function EventHubHero({
|
|
|
10
10
|
stats,
|
|
11
11
|
statsLabel,
|
|
12
12
|
aside,
|
|
13
|
+
footer,
|
|
13
14
|
className
|
|
14
15
|
}) {
|
|
15
16
|
return /* @__PURE__ */ jsxs(
|
|
16
17
|
"div",
|
|
17
18
|
{
|
|
18
19
|
className: cn(
|
|
19
|
-
|
|
20
|
+
// 12px between the phone's blocks — the page's rhythm, the same as
|
|
21
|
+
// between the sections below: with a tappable poster stack the aside
|
|
22
|
+
// is content, not decoration, and gets no section gap of its own.
|
|
23
|
+
"grid grid-cols-1 gap-3 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-x-10 lg:gap-y-6",
|
|
20
24
|
className
|
|
21
25
|
),
|
|
22
26
|
children: [
|
|
@@ -84,18 +88,19 @@ function EventHubHero({
|
|
|
84
88
|
}
|
|
85
89
|
)
|
|
86
90
|
] }),
|
|
87
|
-
aside && /* @__PURE__ */ jsx("div", { className: "min-w-0", children: aside })
|
|
91
|
+
aside && /* @__PURE__ */ jsx("div", { className: "min-w-0", children: aside }),
|
|
92
|
+
footer && /* @__PURE__ */ jsx("div", { className: "lg:col-span-2 lg:w-fit lg:min-w-[480px]", children: footer })
|
|
88
93
|
]
|
|
89
94
|
}
|
|
90
95
|
);
|
|
91
96
|
}
|
|
92
|
-
function EventHubHeroSkeleton({ aside, className }) {
|
|
97
|
+
function EventHubHeroSkeleton({ aside, footer, className }) {
|
|
93
98
|
return /* @__PURE__ */ jsxs(
|
|
94
99
|
"div",
|
|
95
100
|
{
|
|
96
101
|
"aria-hidden": "true",
|
|
97
102
|
className: cn(
|
|
98
|
-
"grid grid-cols-1 gap-
|
|
103
|
+
"grid grid-cols-1 gap-3 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-x-10 lg:gap-y-6",
|
|
99
104
|
className
|
|
100
105
|
),
|
|
101
106
|
children: [
|
|
@@ -118,7 +123,8 @@ function EventHubHeroSkeleton({ aside, className }) {
|
|
|
118
123
|
index
|
|
119
124
|
)) })
|
|
120
125
|
] }),
|
|
121
|
-
aside && /* @__PURE__ */ jsx("div", { className: "min-w-0", children: aside })
|
|
126
|
+
aside && /* @__PURE__ */ jsx("div", { className: "min-w-0", children: aside }),
|
|
127
|
+
footer && /* @__PURE__ */ jsx("div", { className: "lg:col-span-2 lg:w-fit lg:min-w-[480px]", children: /* @__PURE__ */ jsx(Skeleton, { className: "h-[52px] w-full rounded-[14px]" }) })
|
|
122
128
|
]
|
|
123
129
|
}
|
|
124
130
|
);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EventTonightStrip — the hub's state line: what is on right now, what is
|
|
6
|
+
* later tonight, and one tap into the timeline.
|
|
7
|
+
*
|
|
8
|
+
* The hero's figures say SCALE ("52 events · 14 days"); this says STATE ("3
|
|
9
|
+
* live now · 12 more tonight · next at 20:00"). It is the one thing on the
|
|
10
|
+
* hub's first screen a reader can tap into today's programme, so it is a real
|
|
11
|
+
* anchor whenever there is a day to open — never a sheet trigger.
|
|
12
|
+
*
|
|
13
|
+
* Copy is caller-composed and carries only absolute facts: counts and
|
|
14
|
+
* wall-clock start times. Never a countdown or "in 12 minutes" — the hub is
|
|
15
|
+
* ISR-cached, and a ten-minute-old snapshot that says "next at 20:00" is at
|
|
16
|
+
* worst one boundary off, while "in 12 minutes" is simply wrong. The DS keeps
|
|
17
|
+
* no clock, no Intl and no i18n: the app derives `tone`, `lead` and `details`
|
|
18
|
+
* from its own clock and re-derives them after hydration.
|
|
19
|
+
*
|
|
20
|
+
* The lead stays `text-foreground`, not `--success`: 13px bold success on a
|
|
21
|
+
* card computes to roughly 4.5:1 in both modes, too close to the floor for
|
|
22
|
+
* the strip's one claim. The dot and the border carry the colour.
|
|
23
|
+
*/
|
|
24
|
+
type EventTonightStripTone = "live" | "tonight" | "quiet" | "none";
|
|
25
|
+
type EventTonightStripLinkProps = {
|
|
26
|
+
href: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
onClick?: (mouseEvent: React.MouseEvent) => void;
|
|
30
|
+
};
|
|
31
|
+
type EventTonightStripProps = {
|
|
32
|
+
tone: EventTonightStripTone;
|
|
33
|
+
/** The lead segment, bold — "3 live now". */
|
|
34
|
+
lead: string;
|
|
35
|
+
/** Trailing segments, muted, joined with " · " — ["12 more tonight", "next at 20:00"]. */
|
|
36
|
+
details?: string[];
|
|
37
|
+
/** Screen-reader suffix for the anchor — "Open tonight in the schedule". */
|
|
38
|
+
actionLabel?: string;
|
|
39
|
+
/** Absent (tone `none`) renders a non-interactive `role="status"` line. */
|
|
40
|
+
href?: string;
|
|
41
|
+
/** Defaults to a plain `<a>`. Pass `next/link` in a Next app. */
|
|
42
|
+
linkComponent?: React.ComponentType<EventTonightStripLinkProps>;
|
|
43
|
+
className?: string;
|
|
44
|
+
};
|
|
45
|
+
declare function EventTonightStrip({ tone, lead, details, actionLabel, href, linkComponent: LinkComponent, className, }: EventTonightStripProps): react_jsx_runtime.JSX.Element;
|
|
46
|
+
declare function EventTonightStripSkeleton({ className }: {
|
|
47
|
+
className?: string;
|
|
48
|
+
}): react_jsx_runtime.JSX.Element;
|
|
49
|
+
|
|
50
|
+
export { EventTonightStrip, type EventTonightStripLinkProps, type EventTonightStripProps, EventTonightStripSkeleton, type EventTonightStripTone };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
2
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
3
|
+
import { __objRest, __spreadValues } from '../chunk-MMUBH76A.js';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { Clock, CalendarDays, ChevronRight } from 'lucide-react';
|
|
6
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
function DefaultLink(_a) {
|
|
9
|
+
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
10
|
+
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
11
|
+
}
|
|
12
|
+
function EventTonightStrip({
|
|
13
|
+
tone,
|
|
14
|
+
lead,
|
|
15
|
+
details = [],
|
|
16
|
+
actionLabel,
|
|
17
|
+
href,
|
|
18
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
19
|
+
className
|
|
20
|
+
}) {
|
|
21
|
+
const interactive = !!href;
|
|
22
|
+
const mark = tone === "live" ? /* @__PURE__ */ jsxs("span", { className: "relative flex h-2.5 w-2.5", "aria-hidden": true, children: [
|
|
23
|
+
/* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full rounded-full bg-success/40 motion-live-pulse motion-reduce:animate-none" }),
|
|
24
|
+
/* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2.5 w-2.5 rounded-full bg-success" })
|
|
25
|
+
] }) : tone === "tonight" ? /* @__PURE__ */ jsx(Clock, { className: "h-4 w-4 text-muted-foreground", "aria-hidden": true }) : /* @__PURE__ */ jsx(
|
|
26
|
+
CalendarDays,
|
|
27
|
+
{
|
|
28
|
+
className: cn("h-4 w-4", tone === "none" ? "text-muted-foreground/70" : "text-muted-foreground"),
|
|
29
|
+
"aria-hidden": true
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
33
|
+
/* @__PURE__ */ jsx("span", { className: "flex h-4 w-4 shrink-0 items-center justify-center", children: mark }),
|
|
34
|
+
/* @__PURE__ */ jsxs("span", { className: "line-clamp-2 min-w-0 flex-1 text-[13px] leading-[1.3] tabular-nums lg:line-clamp-1 lg:text-sm", children: [
|
|
35
|
+
/* @__PURE__ */ jsx("span", { className: "font-bold text-foreground", children: lead }),
|
|
36
|
+
details.map((detail) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
37
|
+
" ",
|
|
38
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", "aria-hidden": true, children: "\xB7" }),
|
|
39
|
+
" ",
|
|
40
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: detail })
|
|
41
|
+
] }, detail)),
|
|
42
|
+
interactive && actionLabel && /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
|
|
43
|
+
". ",
|
|
44
|
+
actionLabel
|
|
45
|
+
] })
|
|
46
|
+
] }),
|
|
47
|
+
interactive && /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true })
|
|
48
|
+
] });
|
|
49
|
+
const rootClasses = cn(
|
|
50
|
+
"flex min-h-[52px] w-full items-center gap-2.5 rounded-[14px] border bg-card px-3.5 py-2 text-left",
|
|
51
|
+
tone === "live" ? "border-success/40" : "border-border",
|
|
52
|
+
interactive && cn(
|
|
53
|
+
"cursor-pointer transition-colors duration-150 hover:bg-accent-subtle active:scale-[0.99] motion-reduce:transition-none",
|
|
54
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
55
|
+
),
|
|
56
|
+
className
|
|
57
|
+
);
|
|
58
|
+
if (href) {
|
|
59
|
+
return /* @__PURE__ */ jsx(LinkComponent, { href, className: rootClasses, children: body });
|
|
60
|
+
}
|
|
61
|
+
return /* @__PURE__ */ jsx("div", { role: "status", className: rootClasses, children: body });
|
|
62
|
+
}
|
|
63
|
+
function EventTonightStripSkeleton({ className }) {
|
|
64
|
+
return /* @__PURE__ */ jsx(Skeleton, { className: cn("h-[52px] w-full rounded-[14px]", className) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { EventTonightStrip, EventTonightStripSkeleton };
|
|
@@ -17,7 +17,7 @@ function DefaultLink(_a) {
|
|
|
17
17
|
}
|
|
18
18
|
var GROUND = PHOTO_GROUND;
|
|
19
19
|
var INK_PHONE = "bg-[linear-gradient(to_bottom,oklch(0.16_0.012_55_/_0.12)_0%,oklch(0.16_0.012_55_/_0.55)_38%,oklch(0.16_0.012_55_/_0.92)_62%,oklch(0.16_0.012_55_/_0.96)_100%)]";
|
|
20
|
-
var INK_WIDE = "lg:bg-[linear-gradient(to_right,oklch(0.16_0.012_55_/_0.
|
|
20
|
+
var INK_WIDE = "lg:bg-[linear-gradient(to_right,oklch(0.16_0.012_55_/_0.5)_0%,oklch(0.16_0.012_55_/_0.66)_34%,oklch(0.16_0.012_55_/_0.9)_52%,oklch(0.16_0.012_55_/_0.96)_100%)]";
|
|
21
21
|
var TEXT_SCRIM = (
|
|
22
22
|
/* `isolate` + `before:-z-10`: a positioned pseudo-element paints ABOVE the
|
|
23
23
|
column's in-flow text unless the column is its own stacking context and
|
|
@@ -70,8 +70,9 @@ function MenuFamilyBlock({
|
|
|
70
70
|
{
|
|
71
71
|
className: cn(
|
|
72
72
|
"relative flex min-h-[220px] flex-col justify-end p-3.5",
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
TEXT_SCRIM,
|
|
74
|
+
// The two-column card has ONE gradient from `lg` — see `INK_WIDE`.
|
|
75
|
+
wide && "lg:min-h-[320px] lg:p-5 lg:before:hidden"
|
|
75
76
|
),
|
|
76
77
|
children: [
|
|
77
78
|
pill && /* @__PURE__ */ jsx(
|
|
@@ -58,8 +58,14 @@ type MenuHubHeroProps = {
|
|
|
58
58
|
* a 1440px hero.
|
|
59
59
|
*/
|
|
60
60
|
tileUrls?: string[];
|
|
61
|
+
/**
|
|
62
|
+
* The element the headline renders as. `h1` by default; a page that
|
|
63
|
+
* already owns its `<h1>` (an sr-only SEO heading, as on `/menu`) passes
|
|
64
|
+
* `h2` so the document has one h1, not two.
|
|
65
|
+
*/
|
|
66
|
+
headingLevel?: "h1" | "h2";
|
|
61
67
|
className?: string;
|
|
62
68
|
};
|
|
63
|
-
declare function MenuHubHero({ kicker, kickerIcon, headline, body, figures, proof, proofIcon, action, backgroundUrl, tileUrls, className, }: MenuHubHeroProps): react_jsx_runtime.JSX.Element;
|
|
69
|
+
declare function MenuHubHero({ kicker, kickerIcon, headline, body, figures, proof, proofIcon, action, backgroundUrl, tileUrls, headingLevel: Heading, className, }: MenuHubHeroProps): react_jsx_runtime.JSX.Element;
|
|
64
70
|
|
|
65
71
|
export { MenuHubHero, type MenuHubHeroFigure, type MenuHubHeroProps };
|
|
@@ -15,6 +15,7 @@ function MenuHubHero({
|
|
|
15
15
|
action,
|
|
16
16
|
backgroundUrl,
|
|
17
17
|
tileUrls,
|
|
18
|
+
headingLevel: Heading = "h1",
|
|
18
19
|
className
|
|
19
20
|
}) {
|
|
20
21
|
return /* @__PURE__ */ jsxs(
|
|
@@ -85,7 +86,7 @@ function MenuHubHero({
|
|
|
85
86
|
kickerIcon,
|
|
86
87
|
kicker
|
|
87
88
|
] }),
|
|
88
|
-
/* @__PURE__ */ jsx(
|
|
89
|
+
/* @__PURE__ */ jsx(Heading, { className: "max-w-[640px] font-serif text-[28px] font-semibold leading-[1.06] tracking-[-0.025em] lg:text-[50px] lg:leading-[1.04]", children: headline }),
|
|
89
90
|
body && /* @__PURE__ */ jsx("p", { className: "hidden max-w-[540px] text-sm leading-[1.6] opacity-[0.84] lg:block lg:text-[15.5px]", children: body }),
|
|
90
91
|
figures && figures.length > 0 && // One row of four on the phone. It was two by two, which was the
|
|
91
92
|
// taller half of a 500px hero; at 20px the widest figure ("19,090")
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* PosterStack — three overlapping, gently rotated posters that float, the
|
|
5
6
|
* hub hero's aside slot in the source design.
|
|
6
7
|
*
|
|
8
|
+
* TAPPABLE SINCE 0.8.18. The stack was three decorative images: the loudest
|
|
9
|
+
* thing on the hub's first screen, and the one thing on it a reader could not
|
|
10
|
+
* tap. A poster with `href` is an anchor now — the event's detail page for
|
|
11
|
+
* the crawler and a modified click, whatever the app's `linkComponent` does
|
|
12
|
+
* for a plain tap (the hub opens its preview drawer). `caption` prints the
|
|
13
|
+
* title at the poster's foot: it gives the tap a visible reason on touch,
|
|
14
|
+
* where there is no hover, and it is the anchor's accessible name, so the
|
|
15
|
+
* `<img>` goes `alt=""` whenever a caption is set. Without `href` a poster is
|
|
16
|
+
* decorative, as before.
|
|
17
|
+
*
|
|
18
|
+
* The float and the press live on DIFFERENT elements. The wrapper carries
|
|
19
|
+
* the keyframe `transform` (rotation + drift); the anchor inside it carries
|
|
20
|
+
* the press scale and the focus ring. One element cannot animate one
|
|
21
|
+
* `transform` and transition another. The pointed poster comes to the front
|
|
22
|
+
* (`hover:z-[4]`, `focus-within:z-[4]`) — z-index is not animated, it is the
|
|
23
|
+
* "pick this one" cue.
|
|
24
|
+
*
|
|
7
25
|
* Two motion rules, both mandatory:
|
|
8
26
|
* 1. Stops entirely under `prefers-reduced-motion: reduce` — enforced via
|
|
9
27
|
* an injected `<style>` media query rather than relying on Tailwind's
|
|
@@ -15,16 +33,31 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
15
33
|
* spends a paint budget while it isn't visible. Skipped entirely when
|
|
16
34
|
* reduced motion is on, since nothing is animating to pause.
|
|
17
35
|
*/
|
|
36
|
+
type PosterStackLinkProps = {
|
|
37
|
+
href: string;
|
|
38
|
+
className?: string;
|
|
39
|
+
children: React.ReactNode;
|
|
40
|
+
onClick?: (mouseEvent: React.MouseEvent) => void;
|
|
41
|
+
};
|
|
18
42
|
type PosterStackPoster = {
|
|
19
|
-
/** Absent renders
|
|
43
|
+
/** Absent renders `fallback` instead of an image. */
|
|
20
44
|
src?: string;
|
|
21
45
|
alt: string;
|
|
46
|
+
/** The event's detail page. With it the poster is an anchor; without it, decorative. */
|
|
47
|
+
href?: string;
|
|
48
|
+
/** Caption at the poster's foot — the event title. Becomes the anchor's
|
|
49
|
+
* accessible name; the `<img>` then gets `alt=""`. */
|
|
50
|
+
caption?: string;
|
|
51
|
+
/** Rendered when `src` is absent or fails. Defaults to `<EventPosterFallback />`. */
|
|
52
|
+
fallback?: React.ReactNode;
|
|
22
53
|
};
|
|
23
54
|
type PosterStackProps = {
|
|
24
55
|
/** Rendered in order; only the first three are shown. */
|
|
25
56
|
posters: PosterStackPoster[];
|
|
57
|
+
/** Defaults to a plain `<a>`. Pass `next/link`, or an anchor that opens the app's preview. */
|
|
58
|
+
linkComponent?: React.ComponentType<PosterStackLinkProps>;
|
|
26
59
|
className?: string;
|
|
27
60
|
};
|
|
28
|
-
declare function PosterStack({ posters, className }: PosterStackProps): react_jsx_runtime.JSX.Element;
|
|
61
|
+
declare function PosterStack({ posters, linkComponent: LinkComponent, className, }: PosterStackProps): react_jsx_runtime.JSX.Element;
|
|
29
62
|
|
|
30
|
-
export { PosterStack, type PosterStackPoster, type PosterStackProps };
|
|
63
|
+
export { PosterStack, type PosterStackLinkProps, type PosterStackPoster, type PosterStackProps };
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { EventPosterFallback } from '../chunk-FDITZ2VM.js';
|
|
1
2
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
-
import '../chunk-MMUBH76A.js';
|
|
3
|
+
import { __objRest, __spreadValues } from '../chunk-MMUBH76A.js';
|
|
3
4
|
import { useRef, useState, useEffect } from 'react';
|
|
4
|
-
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
5
6
|
|
|
7
|
+
function DefaultLink(_a) {
|
|
8
|
+
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
9
|
+
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
10
|
+
}
|
|
6
11
|
var POSTER_STACK_OBSERVER_ROOT_MARGIN = "200px";
|
|
7
12
|
var POSTER_STACK_ANIMATION_STYLE = `
|
|
8
13
|
@keyframes ds-poster-stack-float-1 { to { transform: rotate(-5deg) translateY(-16px); } }
|
|
@@ -12,29 +17,34 @@ var POSTER_STACK_ANIMATION_STYLE = `
|
|
|
12
17
|
[data-ds-poster-stack-item] { animation: none !important; }
|
|
13
18
|
}
|
|
14
19
|
`;
|
|
20
|
+
var PHONE_STAGE_CLASS = "mx-auto w-[336px] max-w-full";
|
|
15
21
|
var POSTER_SLOTS = [
|
|
16
22
|
{
|
|
17
|
-
positionClassName: "left-
|
|
23
|
+
positionClassName: "left-0 top-[28px] w-[112px] md:left-0 md:top-[52px] md:w-[196px]",
|
|
18
24
|
restTransform: "rotate(-5deg)",
|
|
19
25
|
animationName: "ds-poster-stack-float-1",
|
|
20
26
|
animationDurationMs: 9e3
|
|
21
27
|
},
|
|
22
28
|
{
|
|
23
|
-
positionClassName: "left-[
|
|
29
|
+
positionClassName: "left-[104px] top-0 w-[128px] md:left-[168px] md:w-[232px]",
|
|
24
30
|
restTransform: "rotate(3deg)",
|
|
25
31
|
animationName: "ds-poster-stack-float-2",
|
|
26
32
|
animationDurationMs: 11e3,
|
|
27
33
|
zIndexClassName: "z-[3]"
|
|
28
34
|
},
|
|
29
35
|
{
|
|
30
|
-
positionClassName: "left-[
|
|
36
|
+
positionClassName: "left-[224px] top-[44px] w-[112px] md:left-[378px] md:top-24 md:w-[196px]",
|
|
31
37
|
restTransform: "rotate(6deg)",
|
|
32
38
|
animationName: "ds-poster-stack-float-3",
|
|
33
39
|
animationDurationMs: 1e4
|
|
34
40
|
}
|
|
35
41
|
];
|
|
36
42
|
var POSTER_SHADOW = "0 22px 50px oklch(0.3 0.02 60 / 0.24)";
|
|
37
|
-
function PosterStack({
|
|
43
|
+
function PosterStack({
|
|
44
|
+
posters,
|
|
45
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
46
|
+
className
|
|
47
|
+
}) {
|
|
38
48
|
const containerRef = useRef(null);
|
|
39
49
|
const posterRefs = useRef([]);
|
|
40
50
|
const [failedIndices, setFailedIndices] = useState(/* @__PURE__ */ new Set());
|
|
@@ -56,49 +66,78 @@ function PosterStack({ posters, className }) {
|
|
|
56
66
|
return () => observer.disconnect();
|
|
57
67
|
}, []);
|
|
58
68
|
const visiblePosters = posters.slice(0, POSTER_SLOTS.length);
|
|
59
|
-
return /* @__PURE__ */ jsxs(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
69
|
+
return /* @__PURE__ */ jsxs(
|
|
70
|
+
"div",
|
|
71
|
+
{
|
|
72
|
+
ref: containerRef,
|
|
73
|
+
className: cn("relative h-[196px] md:h-[400px] md:w-auto md:mx-0", PHONE_STAGE_CLASS, className),
|
|
74
|
+
children: [
|
|
75
|
+
/* @__PURE__ */ jsx("style", { children: POSTER_STACK_ANIMATION_STYLE }),
|
|
76
|
+
visiblePosters.map((poster, index) => {
|
|
77
|
+
var _a;
|
|
78
|
+
const slot = POSTER_SLOTS[index];
|
|
79
|
+
const showPoster = !!poster.src && !failedIndices.has(index);
|
|
80
|
+
const face = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
81
|
+
showPoster ? (
|
|
82
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
83
|
+
/* @__PURE__ */ jsx(
|
|
84
|
+
"img",
|
|
85
|
+
{
|
|
86
|
+
src: poster.src,
|
|
87
|
+
alt: poster.caption ? "" : poster.alt,
|
|
88
|
+
loading: "lazy",
|
|
89
|
+
decoding: "async",
|
|
90
|
+
className: cn(
|
|
91
|
+
"h-full w-full object-cover",
|
|
92
|
+
poster.href && "transition-transform duration-300 group-hover/poster:scale-[1.03] motion-reduce:transition-none"
|
|
93
|
+
),
|
|
94
|
+
onError: () => setFailedIndices((current) => new Set(current).add(index))
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
) : (_a = poster.fallback) != null ? _a : /* @__PURE__ */ jsx(EventPosterFallback, {}),
|
|
98
|
+
poster.caption && /* @__PURE__ */ jsx("span", { className: "absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 to-transparent px-2 pb-2 pt-6", children: /* @__PURE__ */ jsx("span", { className: "line-clamp-2 text-[11px] font-semibold leading-[1.25] text-white md:text-[12.5px]", children: poster.caption }) })
|
|
99
|
+
] });
|
|
100
|
+
return /* @__PURE__ */ jsx(
|
|
101
|
+
"div",
|
|
88
102
|
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
className:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
ref: (node) => {
|
|
104
|
+
posterRefs.current[index] = node;
|
|
105
|
+
},
|
|
106
|
+
"data-ds-poster-stack-item": true,
|
|
107
|
+
className: cn(
|
|
108
|
+
"absolute aspect-[3/4] overflow-hidden rounded-xl bg-muted",
|
|
109
|
+
slot.positionClassName,
|
|
110
|
+
slot.zIndexClassName,
|
|
111
|
+
poster.href && "hover:z-[4] focus-within:z-[4]"
|
|
112
|
+
),
|
|
113
|
+
style: {
|
|
114
|
+
transform: slot.restTransform,
|
|
115
|
+
boxShadow: POSTER_SHADOW,
|
|
116
|
+
animationName: slot.animationName,
|
|
117
|
+
animationDuration: `${slot.animationDurationMs}ms`,
|
|
118
|
+
animationTimingFunction: "ease-in-out",
|
|
119
|
+
animationIterationCount: "infinite",
|
|
120
|
+
animationDirection: "alternate"
|
|
121
|
+
},
|
|
122
|
+
children: poster.href ? /* @__PURE__ */ jsx(
|
|
123
|
+
LinkComponent,
|
|
124
|
+
{
|
|
125
|
+
href: poster.href,
|
|
126
|
+
className: cn(
|
|
127
|
+
"group/poster relative block h-full w-full overflow-hidden rounded-xl",
|
|
128
|
+
"transition-transform duration-150 active:scale-[0.98] motion-reduce:transition-none",
|
|
129
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
130
|
+
),
|
|
131
|
+
children: face
|
|
132
|
+
}
|
|
133
|
+
) : /* @__PURE__ */ jsx("div", { className: "relative h-full w-full", children: face })
|
|
134
|
+
},
|
|
135
|
+
poster.alt || index
|
|
136
|
+
);
|
|
137
|
+
})
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
);
|
|
102
141
|
}
|
|
103
142
|
|
|
104
143
|
export { PosterStack };
|
|
@@ -21,7 +21,7 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
21
21
|
* `card` is the 14px inset of a hub card; `page` is the 16px page gutter.
|
|
22
22
|
*/
|
|
23
23
|
declare const snapRailVariants: (props?: ({
|
|
24
|
-
itemWidth?: "sm" | "lg" | "full" | "md" | null | undefined;
|
|
24
|
+
itemWidth?: "xs" | "sm" | "lg" | "full" | "md" | null | undefined;
|
|
25
25
|
gap?: "default" | "sm" | null | undefined;
|
|
26
26
|
bleed?: "none" | "page" | "card" | null | undefined;
|
|
27
27
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
@@ -43,7 +43,7 @@ type SnapRailProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof
|
|
|
43
43
|
onActiveChange?: (index: number) => void;
|
|
44
44
|
};
|
|
45
45
|
declare const SnapRail: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
|
|
46
|
-
itemWidth?: "sm" | "lg" | "full" | "md" | null | undefined;
|
|
46
|
+
itemWidth?: "xs" | "sm" | "lg" | "full" | "md" | null | undefined;
|
|
47
47
|
gap?: "default" | "sm" | null | undefined;
|
|
48
48
|
bleed?: "none" | "page" | "card" | null | undefined;
|
|
49
49
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TimelineDay — one day's programme on the timeline: a column of
|
|
6
|
+
* `TimelineHour`s that becomes, from `lg`, the three-column row
|
|
7
|
+
* lane │ spine │ content that `TimelineBand` used to draw.
|
|
8
|
+
*
|
|
9
|
+
* ONE DOM FOR BOTH VIEWPORTS. `TimelineBand` took the lane as a prop — a
|
|
10
|
+
* second parent — which forced the consumer to render two trees and pick one
|
|
11
|
+
* in JavaScript, and a server render cannot pick. Here the lane is grid
|
|
12
|
+
* column 1, and the day's hero is an ordinary child in its chronological
|
|
13
|
+
* slot that the grid PLACES in that column from `lg` (see `TimelineHour`
|
|
14
|
+
* `role="hero"`). Source order is the phone's visual order, which is the
|
|
15
|
+
* primary surface and the one a screen reader gets on both.
|
|
16
|
+
*
|
|
17
|
+
* `rows` is the number of children that are NOT the hero — the column-3
|
|
18
|
+
* items. It sets `--timeline-rows`, which both the lane and the spine span,
|
|
19
|
+
* and the explicit row track count. It has to be a number the caller
|
|
20
|
+
* supplies: the grid needs the lane to cover every row so auto-placement
|
|
21
|
+
* never drops a later hour into the lane column, and this component cannot
|
|
22
|
+
* tell a hero child from an hour child without inspecting props.
|
|
23
|
+
*
|
|
24
|
+
* `laneTop` is the lane's sticky `top` as a CSS value. `position: sticky` on
|
|
25
|
+
* a grid item is bounded by its grid area — column 1, rows 1…N, the day's
|
|
26
|
+
* full height — so the poster holds while column 3 scrolls and releases when
|
|
27
|
+
* the day runs out, with no scroll listener and no measured height.
|
|
28
|
+
*
|
|
29
|
+
* The spine is a vertical `Separator` in column 2 spanning every row, drawn
|
|
30
|
+
* here so consumers never forget it; the pucks that sit on it are
|
|
31
|
+
* `TimelineHour`'s.
|
|
32
|
+
*/
|
|
33
|
+
type TimelineDayProps = {
|
|
34
|
+
/** The column-3 children count — every child except the hero. */
|
|
35
|
+
rows: number;
|
|
36
|
+
/** Sticky `top` for the lane, as a CSS value. Omitted, the lane scrolls. */
|
|
37
|
+
laneTop?: string;
|
|
38
|
+
/** `TimelineHour`s, in chronological order, the hero among them. */
|
|
39
|
+
children: React.ReactNode;
|
|
40
|
+
className?: string;
|
|
41
|
+
};
|
|
42
|
+
declare function TimelineDay({ rows, laneTop, children, className }: TimelineDayProps): react_jsx_runtime.JSX.Element;
|
|
43
|
+
type TimelineDaySkeletonProps = {
|
|
44
|
+
/** Hours to reserve space for. */
|
|
45
|
+
hourCount?: number;
|
|
46
|
+
className?: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Holds the day's geometry at both widths so nothing jumps when data lands:
|
|
50
|
+
* the phone's stacked hours, the desktop's lane / spine / grid.
|
|
51
|
+
*/
|
|
52
|
+
declare function TimelineDaySkeleton({ hourCount, className }: TimelineDaySkeletonProps): react_jsx_runtime.JSX.Element;
|
|
53
|
+
|
|
54
|
+
export { TimelineDay, type TimelineDayProps, TimelineDaySkeleton, type TimelineDaySkeletonProps };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { TimelineHourSkeleton } from '../chunk-QRSIJC4X.js';
|
|
2
|
+
import '../chunk-BRXI6EDO.js';
|
|
3
|
+
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
4
|
+
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
5
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
6
|
+
import { __spreadValues } from '../chunk-MMUBH76A.js';
|
|
7
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
8
|
+
|
|
9
|
+
function TimelineDay({ rows, laneTop, children, className }) {
|
|
10
|
+
const style = __spreadValues({
|
|
11
|
+
"--timeline-rows": String(Math.max(rows, 1))
|
|
12
|
+
}, laneTop !== void 0 ? { "--timeline-lane-top": laneTop } : {});
|
|
13
|
+
return /* @__PURE__ */ jsxs(
|
|
14
|
+
"div",
|
|
15
|
+
{
|
|
16
|
+
className: cn(
|
|
17
|
+
"flex flex-col",
|
|
18
|
+
"lg:grid lg:grid-cols-[var(--timeline-lane)_var(--timeline-spine)_minmax(0,1fr)]",
|
|
19
|
+
"lg:[grid-template-rows:repeat(var(--timeline-rows),auto)]",
|
|
20
|
+
className
|
|
21
|
+
),
|
|
22
|
+
style,
|
|
23
|
+
children: [
|
|
24
|
+
/* @__PURE__ */ jsx(
|
|
25
|
+
"div",
|
|
26
|
+
{
|
|
27
|
+
"aria-hidden": true,
|
|
28
|
+
className: cn(
|
|
29
|
+
"hidden lg:col-start-2 lg:row-start-1 lg:flex lg:justify-center lg:self-stretch",
|
|
30
|
+
"lg:[grid-row-end:span_var(--timeline-rows)]"
|
|
31
|
+
),
|
|
32
|
+
children: /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "h-full" })
|
|
33
|
+
}
|
|
34
|
+
),
|
|
35
|
+
children
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function TimelineDaySkeleton({ hourCount = 2, className }) {
|
|
41
|
+
return /* @__PURE__ */ jsxs(
|
|
42
|
+
"div",
|
|
43
|
+
{
|
|
44
|
+
"aria-hidden": "true",
|
|
45
|
+
className: cn(
|
|
46
|
+
"flex flex-col",
|
|
47
|
+
"lg:grid lg:grid-cols-[var(--timeline-lane)_var(--timeline-spine)_minmax(0,1fr)]",
|
|
48
|
+
"lg:[grid-template-rows:repeat(var(--timeline-rows),auto)]",
|
|
49
|
+
className
|
|
50
|
+
),
|
|
51
|
+
style: { "--timeline-rows": String(hourCount) },
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ jsxs("div", { className: "hidden lg:col-start-1 lg:row-start-1 lg:flex lg:flex-col lg:gap-3 lg:pt-3.5 lg:[grid-row-end:span_var(--timeline-rows)]", children: [
|
|
54
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "aspect-[3/4] w-full rounded-xl" }),
|
|
55
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-[18px] w-[70%]" }),
|
|
56
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-[13px] w-[45%]" })
|
|
57
|
+
] }),
|
|
58
|
+
/* @__PURE__ */ jsx("div", { className: "hidden lg:col-start-2 lg:row-start-1 lg:flex lg:justify-center lg:self-stretch lg:[grid-row-end:span_var(--timeline-rows)]", children: /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "h-full" }) }),
|
|
59
|
+
Array.from({ length: hourCount }, (_, index) => /* @__PURE__ */ jsx(TimelineHourSkeleton, { cardCount: index === 0 ? 3 : 2 }, index))
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { TimelineDay, TimelineDaySkeleton };
|