@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.
Files changed (59) hide show
  1. package/dist/{chunk-WYTJR2YU.js → chunk-5CPBSNWQ.js} +6 -1
  2. package/dist/chunk-BRXI6EDO.js +84 -0
  3. package/dist/{chunk-54FQQZZJ.js → chunk-EQYB4Q77.js} +1 -1
  4. package/dist/chunk-QRSIJC4X.js +156 -0
  5. package/dist/components/concept-compare-rail.js +2 -2
  6. package/dist/components/concept-compare.js +2 -2
  7. package/dist/components/event-card.d.ts +22 -4
  8. package/dist/components/event-card.js +95 -50
  9. package/dist/components/event-day-header.d.ts +5 -3
  10. package/dist/components/event-day-header.js +12 -4
  11. package/dist/components/event-hub-hero.d.ts +12 -2
  12. package/dist/components/event-hub-hero.js +11 -5
  13. package/dist/components/event-tonight-strip.d.ts +50 -0
  14. package/dist/components/event-tonight-strip.js +67 -0
  15. package/dist/components/menu-family-block.js +4 -3
  16. package/dist/components/menu-hub-hero.d.ts +7 -1
  17. package/dist/components/menu-hub-hero.js +2 -1
  18. package/dist/components/poster-stack.d.ts +36 -3
  19. package/dist/components/poster-stack.js +87 -48
  20. package/dist/components/snap-rail.d.ts +2 -2
  21. package/dist/components/snap-rail.js +1 -1
  22. package/dist/components/timeline-day.d.ts +54 -0
  23. package/dist/components/timeline-day.js +65 -0
  24. package/dist/components/timeline-hour-puck.d.ts +47 -0
  25. package/dist/components/timeline-hour-puck.js +3 -0
  26. package/dist/components/timeline-hour.d.ts +96 -0
  27. package/dist/components/{timeline-band.js → timeline-hour.js} +2 -1
  28. package/dist/components/timeline-playhead.js +1 -3
  29. package/package.json +1 -1
  30. package/src/components/event-card.tsx +70 -13
  31. package/src/components/event-day-header.tsx +19 -7
  32. package/src/components/event-hub-hero.tsx +25 -3
  33. package/src/components/event-tonight-strip.stories.tsx +78 -0
  34. package/src/components/event-tonight-strip.test.tsx +49 -0
  35. package/src/components/event-tonight-strip.tsx +134 -0
  36. package/src/components/menu-family-block.tsx +13 -6
  37. package/src/components/menu-hub-hero.test.tsx +13 -0
  38. package/src/components/menu-hub-hero.tsx +9 -2
  39. package/src/components/poster-stack.stories.tsx +43 -1
  40. package/src/components/poster-stack.tsx +104 -19
  41. package/src/components/snap-rail.tsx +6 -1
  42. package/src/components/timeline-day.stories.tsx +201 -0
  43. package/src/components/timeline-day.test.tsx +78 -0
  44. package/src/components/timeline-day.tsx +114 -0
  45. package/src/components/timeline-hour-puck.tsx +142 -0
  46. package/src/components/timeline-hour.tsx +266 -0
  47. package/src/components/timeline-playhead.tsx +1 -1
  48. package/dist/chunk-NSNEANOQ.js +0 -104
  49. package/dist/components/hour-band.d.ts +0 -66
  50. package/dist/components/hour-band.js +0 -81
  51. package/dist/components/hour-group.d.ts +0 -67
  52. package/dist/components/hour-group.js +0 -59
  53. package/dist/components/timeline-band.d.ts +0 -64
  54. package/src/components/hour-band.stories.tsx +0 -182
  55. package/src/components/hour-band.tsx +0 -146
  56. package/src/components/hour-group.stories.tsx +0 -100
  57. package/src/components/hour-group.tsx +0 -124
  58. package/src/components/timeline-band.stories.tsx +0 -206
  59. 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
- "grid grid-cols-1 gap-8 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-10",
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-8 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-10",
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.32)_0%,oklch(0.16_0.012_55_/_0.62)_34%,oklch(0.16_0.012_55_/_0.9)_52%,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.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
- wide && "lg:min-h-[320px] lg:p-5",
74
- TEXT_SCRIM
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("h1", { 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
+ /* @__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 a neutral placeholder block instead of an image. */
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-[6px] top-[34px] w-[120px] md:left-0 md:top-[52px] md:w-[196px]",
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-[110px] top-0 w-[140px] md:left-[168px] md:w-[232px]",
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-[238px] top-[58px] w-[120px] md:left-[378px] md:top-24 md:w-[196px]",
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({ posters, className }) {
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("div", { ref: containerRef, className: cn("relative h-[250px] md:h-[400px]", className), children: [
60
- /* @__PURE__ */ jsx("style", { children: POSTER_STACK_ANIMATION_STYLE }),
61
- visiblePosters.map((poster, index) => {
62
- const slot = POSTER_SLOTS[index];
63
- const showPoster = !!poster.src && !failedIndices.has(index);
64
- return /* @__PURE__ */ jsx(
65
- "div",
66
- {
67
- ref: (node) => {
68
- posterRefs.current[index] = node;
69
- },
70
- "data-ds-poster-stack-item": true,
71
- className: cn(
72
- "absolute aspect-[3/4] overflow-hidden rounded-xl bg-muted",
73
- slot.positionClassName,
74
- slot.zIndexClassName
75
- ),
76
- style: {
77
- transform: slot.restTransform,
78
- boxShadow: POSTER_SHADOW,
79
- animationName: slot.animationName,
80
- animationDuration: `${slot.animationDurationMs}ms`,
81
- animationTimingFunction: "ease-in-out",
82
- animationIterationCount: "infinite",
83
- animationDirection: "alternate"
84
- },
85
- children: showPoster && // eslint-disable-next-line @next/next/no-img-element
86
- /* @__PURE__ */ jsx(
87
- "img",
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
- src: poster.src,
90
- alt: poster.alt,
91
- loading: "lazy",
92
- decoding: "async",
93
- className: "h-full w-full object-cover",
94
- onError: () => setFailedIndices((current) => new Set(current).add(index))
95
- }
96
- )
97
- },
98
- poster.alt || index
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> & {
@@ -1,3 +1,3 @@
1
- export { SnapRail, SnapRailDots } from '../chunk-WYTJR2YU.js';
1
+ export { SnapRail, SnapRailDots } from '../chunk-5CPBSNWQ.js';
2
2
  import '../chunk-FEK5XGZW.js';
3
3
  import '../chunk-MMUBH76A.js';
@@ -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 };