@bearmenu/ui 0.8.4 → 0.8.7

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 (150) hide show
  1. package/dist/chunk-637L7XQ3.js +85 -0
  2. package/dist/chunk-6XMYEZVR.js +26 -0
  3. package/dist/chunk-DNSXMHYX.js +8 -0
  4. package/dist/chunk-FDITZ2VM.js +16 -0
  5. package/dist/chunk-GPH6L4DX.js +55 -0
  6. package/dist/{chunk-N2KNBPAQ.js → chunk-QXHMPJ35.js} +1 -0
  7. package/dist/chunk-TT3WFS6S.js +379 -0
  8. package/dist/chunk-UXWAOISG.js +170 -0
  9. package/dist/{chunk-UZ3PCIE5.js → chunk-XWMRABZW.js} +2 -2
  10. package/dist/{chunk-VBA45GLP.js → chunk-ZDAHKI3A.js} +12 -2
  11. package/dist/components/card.d.ts +2 -2
  12. package/dist/components/card.js +4 -127
  13. package/dist/components/category-deck.d.ts +50 -0
  14. package/dist/components/category-deck.js +128 -0
  15. package/dist/components/concept-card.d.ts +43 -0
  16. package/dist/components/concept-card.js +81 -0
  17. package/dist/components/coverflow.d.ts +123 -0
  18. package/dist/components/coverflow.js +300 -0
  19. package/dist/components/day-rail-chip.d.ts +82 -0
  20. package/dist/components/day-rail-chip.js +105 -0
  21. package/dist/components/drift-rail.d.ts +28 -0
  22. package/dist/components/drift-rail.js +3 -0
  23. package/dist/components/event-card.d.ts +97 -0
  24. package/dist/components/event-card.js +164 -0
  25. package/dist/components/event-compact-row.d.ts +73 -0
  26. package/dist/components/event-compact-row.js +134 -0
  27. package/dist/components/event-day-header.d.ts +65 -0
  28. package/dist/components/event-day-header.js +95 -0
  29. package/dist/components/event-day-separator.d.ts +46 -0
  30. package/dist/components/event-day-separator.js +50 -0
  31. package/dist/components/event-detail-panel.d.ts +80 -0
  32. package/dist/components/event-detail-panel.js +251 -0
  33. package/dist/components/event-hub-hero.d.ts +34 -0
  34. package/dist/components/event-hub-hero.js +57 -0
  35. package/dist/components/event-index-row.d.ts +68 -0
  36. package/dist/components/event-index-row.js +84 -0
  37. package/dist/components/event-list-row-wide.d.ts +101 -0
  38. package/dist/components/event-list-row-wide.js +218 -0
  39. package/dist/components/event-list-row.d.ts +82 -0
  40. package/dist/components/event-list-row.js +173 -0
  41. package/dist/components/event-poster-fallback.d.ts +20 -0
  42. package/dist/components/event-poster-fallback.js +3 -0
  43. package/dist/components/event-types.d.ts +122 -0
  44. package/dist/components/event-types.js +2 -0
  45. package/dist/components/events-empty-state.d.ts +44 -0
  46. package/dist/components/events-empty-state.js +73 -0
  47. package/dist/components/facet-group.d.ts +93 -0
  48. package/dist/components/facet-group.js +222 -0
  49. package/dist/components/featured-rail-item.d.ts +42 -0
  50. package/dist/components/featured-rail-item.js +88 -0
  51. package/dist/components/hour-band.d.ts +68 -0
  52. package/dist/components/hour-band.js +53 -0
  53. package/dist/components/hour-group.d.ts +67 -0
  54. package/dist/components/hour-group.js +59 -0
  55. package/dist/components/motion-icon-controls.js +3 -379
  56. package/dist/components/multi-select-combobox.js +1 -1
  57. package/dist/components/place-about-tab.js +3 -3
  58. package/dist/components/place-details-mobile.js +3 -3
  59. package/dist/components/poster-stack.d.ts +30 -0
  60. package/dist/components/poster-stack.js +104 -0
  61. package/dist/components/poster-tape.d.ts +36 -0
  62. package/dist/components/poster-tape.js +64 -0
  63. package/dist/components/poster-wall.d.ts +25 -0
  64. package/dist/components/poster-wall.js +183 -0
  65. package/dist/components/quiet-stretch.d.ts +43 -0
  66. package/dist/components/quiet-stretch.js +54 -0
  67. package/dist/components/section-header.d.ts +53 -0
  68. package/dist/components/section-header.js +63 -0
  69. package/dist/components/segmented-nav.d.ts +62 -0
  70. package/dist/components/segmented-nav.js +54 -0
  71. package/dist/components/skeleton-card.js +1 -1
  72. package/dist/components/skeleton.d.ts +17 -0
  73. package/dist/components/skeleton.js +1 -1
  74. package/dist/components/tag-group.js +3 -26
  75. package/dist/components/tag.d.ts +2 -2
  76. package/dist/components/tag.js +1 -1
  77. package/dist/components/timeline-band.d.ts +46 -0
  78. package/dist/components/timeline-band.js +5 -0
  79. package/dist/components/venue-events-block.d.ts +143 -0
  80. package/dist/components/venue-events-block.js +222 -0
  81. package/dist/components/weekend-panel.d.ts +44 -0
  82. package/dist/components/weekend-panel.js +231 -0
  83. package/package.json +1 -1
  84. package/src/components/card.tsx +44 -1
  85. package/src/components/category-deck.stories.tsx +96 -0
  86. package/src/components/category-deck.tsx +227 -0
  87. package/src/components/concept-card.stories.tsx +80 -0
  88. package/src/components/concept-card.tsx +143 -0
  89. package/src/components/coverflow.stories.tsx +240 -0
  90. package/src/components/coverflow.test.tsx +193 -0
  91. package/src/components/coverflow.tsx +550 -0
  92. package/src/components/day-rail-chip.stories.tsx +145 -0
  93. package/src/components/day-rail-chip.tsx +201 -0
  94. package/src/components/drift-rail.stories.tsx +92 -0
  95. package/src/components/drift-rail.tsx +86 -0
  96. package/src/components/event-card.stories.tsx +248 -0
  97. package/src/components/event-card.test.tsx +188 -0
  98. package/src/components/event-card.tsx +329 -0
  99. package/src/components/event-compact-row.stories.tsx +221 -0
  100. package/src/components/event-compact-row.test.tsx +51 -0
  101. package/src/components/event-compact-row.tsx +270 -0
  102. package/src/components/event-day-header.stories.tsx +124 -0
  103. package/src/components/event-day-header.tsx +180 -0
  104. package/src/components/event-day-separator.stories.tsx +70 -0
  105. package/src/components/event-day-separator.tsx +95 -0
  106. package/src/components/event-detail-panel.stories.tsx +191 -0
  107. package/src/components/event-detail-panel.tsx +360 -0
  108. package/src/components/event-hub-hero.stories.tsx +66 -0
  109. package/src/components/event-hub-hero.tsx +110 -0
  110. package/src/components/event-index-row.stories.tsx +116 -0
  111. package/src/components/event-index-row.tsx +181 -0
  112. package/src/components/event-list-row-wide.stories.tsx +204 -0
  113. package/src/components/event-list-row-wide.tsx +386 -0
  114. package/src/components/event-list-row.stories.tsx +159 -0
  115. package/src/components/event-list-row.tsx +335 -0
  116. package/src/components/event-poster-fallback.stories.tsx +52 -0
  117. package/src/components/event-poster-fallback.tsx +31 -0
  118. package/src/components/event-types.ts +137 -0
  119. package/src/components/events-empty-state.stories.tsx +59 -0
  120. package/src/components/events-empty-state.tsx +131 -0
  121. package/src/components/facet-group.stories.tsx +247 -0
  122. package/src/components/facet-group.test.tsx +225 -0
  123. package/src/components/facet-group.tsx +352 -0
  124. package/src/components/featured-rail-item.stories.tsx +86 -0
  125. package/src/components/featured-rail-item.tsx +153 -0
  126. package/src/components/hour-band.stories.tsx +159 -0
  127. package/src/components/hour-band.tsx +126 -0
  128. package/src/components/hour-group.stories.tsx +100 -0
  129. package/src/components/hour-group.tsx +124 -0
  130. package/src/components/poster-stack.stories.tsx +68 -0
  131. package/src/components/poster-stack.tsx +155 -0
  132. package/src/components/poster-tape.stories.tsx +70 -0
  133. package/src/components/poster-tape.tsx +122 -0
  134. package/src/components/poster-wall.stories.tsx +95 -0
  135. package/src/components/poster-wall.tsx +220 -0
  136. package/src/components/quiet-stretch.stories.tsx +62 -0
  137. package/src/components/quiet-stretch.tsx +115 -0
  138. package/src/components/section-header.stories.tsx +70 -0
  139. package/src/components/section-header.tsx +134 -0
  140. package/src/components/segmented-nav.stories.tsx +118 -0
  141. package/src/components/segmented-nav.tsx +113 -0
  142. package/src/components/skeleton.tsx +18 -0
  143. package/src/components/tag.tsx +11 -1
  144. package/src/components/timeline-band.stories.tsx +206 -0
  145. package/src/components/timeline-band.tsx +207 -0
  146. package/src/components/venue-events-block.stories.tsx +216 -0
  147. package/src/components/venue-events-block.tsx +408 -0
  148. package/src/components/weekend-panel.stories.tsx +92 -0
  149. package/src/components/weekend-panel.tsx +322 -0
  150. package/src/globals.css +19 -0
@@ -0,0 +1,123 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * Coverflow — the busiest-events browser: a 3D perspective lane with a large
5
+ * lead poster flanked by up to three shrinking, desaturating posters on each
6
+ * side. The lane itself drifts a few pixels back and forth
7
+ * (`prefers-reduced-motion` stops this outright; an `IntersectionObserver`
8
+ * pauses it off-screen).
9
+ *
10
+ * IT IS A RING, NOT A LANE. A card's slot comes from `ringOffset` — the
11
+ * SIGNED SHORTEST WAY ROUND the list, not the raw `index - activeIndex`. With
12
+ * seven items and `activeIndex` at 0, item 6 renders at offset −1 (just left
13
+ * of the lead) rather than +6, so both flanks are full at every index instead
14
+ * of the stage emptying out at the two ends, and prev/next keep going past
15
+ * either end. `goTo` already reported a wrapped index; the visuals now agree
16
+ * with it.
17
+ *
18
+ * IDENTITY IS THE ANIMATION: a card renders keyed by `item.id`, and its
19
+ * appearance (position, rotation, scale, depth filter) is a pure function of
20
+ * that offset. Advancing `activeIndex` shifts every offset by one, and
21
+ * because the DOM node stays put the browser tweens it from its old slot to
22
+ * its new one instead of popping straight there.
23
+ *
24
+ * THE WRAP MUST NOT TWEEN. Exactly one card per step crosses the ring's seam
25
+ * — with seven items it goes from offset −3 to +3 — and animating that would
26
+ * drag a poster right across the face of the coverflow. The mechanism chosen:
27
+ * `Coverflow` keeps the previous and current active index in one piece of
28
+ * state (`step`), so a render can tell, per card, whether its offset moved by
29
+ * the same amount the active index did. A card that did not (the only way
30
+ * that happens is crossing the seam) renders WITHOUT its transition classes
31
+ * for that one commit, so the transform applies instantly; the next commit
32
+ * restores them and it tweens normally from its new slot. Two alternatives
33
+ * were rejected: re-keying the card to force a remount throws away the
34
+ * decoded `<img>` and flashes the poster, and an effect-based "clear the
35
+ * transition after paint" lands a frame too late — the slide has already
36
+ * started. The seam sits at the deepest tier, which is also the lowest
37
+ * `z-index`, so the teleport happens behind the stack and off-stage.
38
+ *
39
+ * ONLY THE VISIBLE TIERS ARE MOUNTED. Every item now resolves to some ring
40
+ * offset, so the component renders only `|offset| <= 3` and drops the rest
41
+ * entirely — a fifty-item list paints seven posters, not fifty. Cards enter
42
+ * and leave at tier 3, parked well outside the stage's horizontal clip, so the
43
+ * mount/unmount is never seen. The stage clips on the x axis ONLY; see the
44
+ * note on the container element for why the y axis must stay visible.
45
+ *
46
+ * DEGENERATE LENGTHS: with one item there is no ring, so the arrows are not
47
+ * rendered at all rather than shown inert — see the note below on controls
48
+ * that look operable and do nothing. With two items the shortest path ties at
49
+ * `n / 2`; `ringOffset` resolves the tie to the POSITIVE side, so the other
50
+ * card is always on the right flank instead of flipping between them.
51
+ *
52
+ * `prefers-reduced-motion` already removes the position transition outright
53
+ * (`motion-reduce:transition-none`), so under it every step is a jump and the
54
+ * wrap is not a special case — it cannot read as a flash because nothing else
55
+ * is animating either.
56
+ *
57
+ * ACCESSIBILITY: every item carries a `title`, so every card is a real,
58
+ * focusable control (the injected link when it carries an `href`, otherwise a
59
+ * `<button>`) named after the event it depicts — a keyboard or screen-reader
60
+ * user can jump straight to any visible card the way a mouse user clicks one
61
+ * directly, not just step with `prevLabel`/`nextLabel`. Selecting one calls
62
+ * `onActiveIndexChange` with its index. Cards beyond the visible tiers are
63
+ * not rendered at all, so a screen-reader/keyboard user never lands on an
64
+ * invisible control — there is nothing there to land on. The depth
65
+ * styling (scale/grayscale/opacity) stays purely decorative, applied to an
66
+ * inner media element rather than the focusable wrapper — `filter` creates a
67
+ * new stacking context that would otherwise dim a `box-shadow`-based focus
68
+ * ring along with the poster. The active item's title/meta stays
69
+ * `aria-live="polite"` so a screen-reader user is told what changed after any
70
+ * kind of jump.
71
+ */
72
+ type CoverflowItem = {
73
+ /** Stable identity — this is what makes the position change animate rather than pop. */
74
+ id: string;
75
+ title: string;
76
+ /** Caller-composed, e.g. "Fri 4 · 90 going". */
77
+ meta: string;
78
+ posterUrl?: string;
79
+ href?: string;
80
+ /**
81
+ * Top-left overlay slot, rendered exactly as given — bring your own pill,
82
+ * icon and colour. Deliberately unstyled: a wrapper here would cap every
83
+ * caller at one look, and the first real consumer needed a colour-ramped
84
+ * badge the old black pill could not express.
85
+ */
86
+ badge?: React.ReactNode;
87
+ isFree?: boolean;
88
+ };
89
+ type CoverflowLinkProps = {
90
+ href: string;
91
+ className?: string;
92
+ style?: React.CSSProperties;
93
+ children: React.ReactNode;
94
+ onClick?: (mouseEvent: React.MouseEvent) => void;
95
+ "aria-label"?: string;
96
+ "aria-hidden"?: boolean;
97
+ tabIndex?: number;
98
+ };
99
+ type CoverflowProps = {
100
+ items: CoverflowItem[];
101
+ activeIndex: number;
102
+ onActiveIndexChange?: (index: number) => void;
103
+ /**
104
+ * Optional centred caption under the stage, e.g. a pluralized item count
105
+ * ("7 events"). Omit it when `positionLabel` already says everything there
106
+ * is to say — passing a position string here as well renders it twice, with
107
+ * a separator dot between the two identical halves.
108
+ */
109
+ caption?: string;
110
+ /** Required to render the free pill on an item — no label, no pill. */
111
+ freeLabel: string;
112
+ /** Accessible name for the previous-item control. */
113
+ prevLabel: string;
114
+ /** Accessible name for the next-item control. */
115
+ nextLabel: string;
116
+ /** e.g. "3 of 7" — caller-composed, locale-dependent. Shown next to `caption`. */
117
+ positionLabel?: string;
118
+ linkComponent?: React.ComponentType<CoverflowLinkProps>;
119
+ className?: string;
120
+ };
121
+ declare function Coverflow({ items, activeIndex, onActiveIndexChange, caption, freeLabel, prevLabel, nextLabel, positionLabel, linkComponent: LinkComponent, className, }: CoverflowProps): react_jsx_runtime.JSX.Element | null;
122
+
123
+ export { Coverflow, type CoverflowItem, type CoverflowLinkProps, type CoverflowProps };
@@ -0,0 +1,300 @@
1
+ import { EventPosterFallback } from '../chunk-FDITZ2VM.js';
2
+ import { cn } from '../chunk-FEK5XGZW.js';
3
+ import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
4
+ import { useId, useRef, useState, useEffect } from 'react';
5
+ import { ChevronLeft, ChevronRight, Gift } 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
+ var COVERFLOW_MAX_VISIBLE_TIER = 3;
13
+ var COVERFLOW_TIER_ROTATE_DEG = [0, 20, 30, 38];
14
+ var COVERFLOW_TIER_SCALE = [1, 0.92, 0.82, 0.72];
15
+ var COVERFLOW_TIER_GRAYSCALE = [0, 0, 0.35, 0.5];
16
+ var COVERFLOW_TIER_OPACITY = [1, 0.88, 0.7, 0.5];
17
+ var COVERFLOW_CARD_WIDTH_CLASS = "w-[149px] sm:w-[210px]";
18
+ var COVERFLOW_CARD_TRANSFORM_TRANSITION_CLASS = "transition-[transform,box-shadow] duration-[420ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none motion-reduce:duration-0";
19
+ var COVERFLOW_MEDIA_FILTER_TRANSITION_CLASS = "transition-[filter,opacity] duration-[420ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none motion-reduce:duration-0";
20
+ var COVERFLOW_CARD_FOCUS_CLASS = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2";
21
+ var COVERFLOW_SIDE_SHADOW_CLASS = "shadow-[0_11px_27px_oklch(0.3_0.02_60/0.22)]";
22
+ var COVERFLOW_LEAD_SHADOW_CLASS = "shadow-[0_24px_56px_oklch(0.28_0.03_45/0.32)]";
23
+ var COVERFLOW_PILL_CLASS = "inline-flex h-6 max-w-[85%] items-center gap-1 rounded-full px-2 text-[11px] font-semibold";
24
+ var COVERFLOW_DRIFT_DURATION_SECONDS = 26;
25
+ var COVERFLOW_DRIFT_PIXELS = 40;
26
+ var COVERFLOW_LEAD_Z_INDEX = 6;
27
+ var COVERFLOW_ARROW_CLASS = "absolute top-1/2 z-20 flex h-11 w-11 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full bg-background/85 text-foreground shadow-[var(--shadow-overlay)] backdrop-blur-sm transition-colors duration-150 hover:bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 motion-reduce:transition-none";
28
+ function wrapIndex(index, length) {
29
+ return (index % length + length) % length;
30
+ }
31
+ function ringOffset(delta, length) {
32
+ const raw = wrapIndex(delta, length);
33
+ return raw > length / 2 ? raw - length : raw;
34
+ }
35
+ function CoverflowCard({
36
+ item,
37
+ index,
38
+ offset,
39
+ suppressTransition,
40
+ freeLabel,
41
+ onActivate,
42
+ linkComponent: LinkComponent
43
+ }) {
44
+ var _a;
45
+ const isLead = offset === 0;
46
+ const magnitude = Math.abs(offset);
47
+ const tier = Math.min(magnitude, COVERFLOW_MAX_VISIBLE_TIER);
48
+ const sign = Math.sign(offset);
49
+ const wrapperStyle = {
50
+ // The lead sits at `--cf-scale-lead`, which is now 1 — see the width class.
51
+ // Side tiers multiply their depth ratio by `--cf-tier-base`, the side/lead
52
+ // width ratio, because the shared box is the LEAD's size: without it every
53
+ // side card would render at nearly lead width. It is a CSS var rather than
54
+ // a constant here because the ratio differs per breakpoint, and calc()
55
+ // cannot divide two lengths to derive it live.
56
+ transform: isLead ? "translate(-50%, -50%) scale(var(--cf-scale-lead))" : `translate(-50%, -50%) translateX(calc(${sign} * var(--cf-x${tier}))) rotateY(${-sign * COVERFLOW_TIER_ROTATE_DEG[tier]}deg) scale(calc(var(--cf-tier-base) * ${COVERFLOW_TIER_SCALE[tier]}))`,
57
+ zIndex: Math.max(COVERFLOW_LEAD_Z_INDEX - magnitude, 0)
58
+ };
59
+ const mediaStyle = {
60
+ opacity: COVERFLOW_TIER_OPACITY[tier],
61
+ filter: COVERFLOW_TIER_GRAYSCALE[tier] > 0 ? `grayscale(${COVERFLOW_TIER_GRAYSCALE[tier] * 100}%)` : "none"
62
+ };
63
+ const media = /* @__PURE__ */ jsxs(
64
+ "div",
65
+ {
66
+ className: cn(
67
+ "relative aspect-[4/5] overflow-hidden rounded-[10px] bg-muted",
68
+ !suppressTransition && COVERFLOW_MEDIA_FILTER_TRANSITION_CLASS
69
+ ),
70
+ style: mediaStyle,
71
+ children: [
72
+ item.posterUrl ? (
73
+ // eslint-disable-next-line @next/next/no-img-element
74
+ /* @__PURE__ */ jsx(
75
+ "img",
76
+ {
77
+ src: item.posterUrl,
78
+ alt: "",
79
+ loading: isLead ? "eager" : "lazy",
80
+ decoding: "async",
81
+ className: "h-full w-full object-cover"
82
+ }
83
+ )
84
+ ) : /* @__PURE__ */ jsx(EventPosterFallback, {}),
85
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-x-2 top-2 flex items-start justify-between gap-2", children: [
86
+ (_a = item.badge) != null ? _a : /* @__PURE__ */ jsx("span", {}),
87
+ item.isFree && freeLabel && /* @__PURE__ */ jsxs("span", { className: cn(COVERFLOW_PILL_CLASS, "bg-success font-bold text-success-foreground"), children: [
88
+ /* @__PURE__ */ jsx(Gift, { className: "h-3 w-3", "aria-hidden": true }),
89
+ freeLabel
90
+ ] })
91
+ ] })
92
+ ]
93
+ }
94
+ );
95
+ const wrapperClassName = cn(
96
+ "absolute left-1/2 top-1/2 block bg-transparent p-0 text-left",
97
+ COVERFLOW_CARD_WIDTH_CLASS,
98
+ isLead ? "cursor-default" : "cursor-pointer",
99
+ isLead ? COVERFLOW_LEAD_SHADOW_CLASS : COVERFLOW_SIDE_SHADOW_CLASS,
100
+ "rounded-[10px]",
101
+ !suppressTransition && COVERFLOW_CARD_TRANSFORM_TRANSITION_CLASS,
102
+ COVERFLOW_CARD_FOCUS_CLASS
103
+ );
104
+ const handleActivate = isLead ? void 0 : () => onActivate(index);
105
+ if (item.href) {
106
+ return /* @__PURE__ */ jsx(
107
+ LinkComponent,
108
+ {
109
+ href: item.href,
110
+ className: wrapperClassName,
111
+ style: wrapperStyle,
112
+ "aria-label": item.title,
113
+ onClick: handleActivate,
114
+ children: media
115
+ }
116
+ );
117
+ }
118
+ return /* @__PURE__ */ jsx(
119
+ "button",
120
+ {
121
+ type: "button",
122
+ className: wrapperClassName,
123
+ style: wrapperStyle,
124
+ "aria-label": item.title,
125
+ onClick: handleActivate,
126
+ children: media
127
+ }
128
+ );
129
+ }
130
+ function Coverflow({
131
+ items,
132
+ activeIndex,
133
+ onActiveIndexChange,
134
+ caption,
135
+ freeLabel,
136
+ prevLabel,
137
+ nextLabel,
138
+ positionLabel,
139
+ linkComponent: LinkComponent = DefaultLink,
140
+ className
141
+ }) {
142
+ const scopeId = useId().replace(/[^a-zA-Z0-9]/g, "");
143
+ const scopeClass = `coverflow-${scopeId}`;
144
+ const containerRef = useRef(null);
145
+ const length = items.length;
146
+ const active = length > 0 ? wrapIndex(activeIndex, length) : 0;
147
+ const [step, setStep] = useState({ from: active, to: active, length });
148
+ if (step.to !== active || step.length !== length) {
149
+ setStep(
150
+ step.length === length ? { from: step.to, to: active, length } : { from: active, to: active, length }
151
+ );
152
+ }
153
+ useEffect(() => {
154
+ const node = containerRef.current;
155
+ if (!node || typeof IntersectionObserver === "undefined") return void 0;
156
+ const observer = new IntersectionObserver(
157
+ ([entry]) => {
158
+ node.dataset.offscreen = entry.isIntersecting ? "false" : "true";
159
+ },
160
+ { threshold: 0 }
161
+ );
162
+ observer.observe(node);
163
+ return () => observer.disconnect();
164
+ }, []);
165
+ if (length === 0) return null;
166
+ const activeItem = items[active];
167
+ const goTo = (index) => onActiveIndexChange == null ? void 0 : onActiveIndexChange(wrapIndex(index, length));
168
+ const activeDelta = ringOffset(step.to - step.from, length);
169
+ const visibleCards = items.map((item, index) => {
170
+ const offset = ringOffset(index - active, length);
171
+ const suppressTransition = offset !== ringOffset(index - step.from, length) - activeDelta;
172
+ return { item, index, offset, suppressTransition };
173
+ }).filter(({ offset }) => Math.abs(offset) <= COVERFLOW_MAX_VISIBLE_TIER);
174
+ const hasRing = length > 1;
175
+ return /* @__PURE__ */ jsxs("div", { className: cn(scopeClass, "w-full", className), children: [
176
+ /* @__PURE__ */ jsx("style", { children: `
177
+ .${scopeClass} {
178
+ /*
179
+ * Every card shares one DOM box (COVERFLOW_CARD_WIDTH_CLASS above),
180
+ * and that box is now the LEAD's width. These are the visual
181
+ * footprints the JS transform math targets.
182
+ *
183
+ * --cf-scale-lead is therefore 1: the lead paints at its own size and
184
+ * is never resampled upward, which is what fixed it reading soft.
185
+ * --cf-tier-base is --cf-side-w / --cf-lead-w, the factor every side
186
+ * tier multiplies its depth ratio by; it must be kept in sync by hand
187
+ * if either width changes, because calc() cannot divide two lengths.
188
+ * 77 / 149 = 0.5168 120 / 210 = 0.5714
189
+ *
190
+ * Both footprints are 20% down from where they started (186/96 and
191
+ * 262/150): the lead was large enough that a poster meant for a feed
192
+ * thumbnail was being asked to carry a hero, and it showed.
193
+ */
194
+ --cf-lead-w: 149px;
195
+ --cf-side-w: 77px;
196
+ --cf-gap: 21px;
197
+ --cf-scale-lead: 1;
198
+ --cf-tier-base: 0.5168;
199
+ --cf-x1: calc(var(--cf-lead-w) / 2 + var(--cf-gap) + var(--cf-side-w) / 2);
200
+ --cf-x2: calc(var(--cf-x1) + var(--cf-side-w) + var(--cf-gap));
201
+ --cf-x3: calc(var(--cf-x2) + var(--cf-side-w) + var(--cf-gap));
202
+ }
203
+ @media (min-width: 640px) {
204
+ .${scopeClass} {
205
+ --cf-lead-w: 210px;
206
+ --cf-side-w: 120px;
207
+ --cf-gap: 26px;
208
+ --cf-tier-base: 0.5714;
209
+ }
210
+ }
211
+ .${scopeClass} .coverflow-lane {
212
+ animation: ${scopeClass}-drift ${COVERFLOW_DRIFT_DURATION_SECONDS}s ease-in-out infinite alternate;
213
+ }
214
+ .${scopeClass}[data-offscreen="true"] .coverflow-lane {
215
+ animation-play-state: paused;
216
+ }
217
+ @keyframes ${scopeClass}-drift {
218
+ from { transform: translateX(-${COVERFLOW_DRIFT_PIXELS}px); }
219
+ to { transform: translateX(${COVERFLOW_DRIFT_PIXELS}px); }
220
+ }
221
+ @media (prefers-reduced-motion: reduce) {
222
+ .${scopeClass} .coverflow-lane {
223
+ animation: none;
224
+ transform: none;
225
+ }
226
+ }
227
+ ` }),
228
+ /* @__PURE__ */ jsxs(
229
+ "div",
230
+ {
231
+ ref: containerRef,
232
+ className: "relative flex h-[300px] items-center justify-center overflow-x-clip overflow-y-visible [perspective:900px] sm:h-[380px] sm:[perspective:1400px]",
233
+ children: [
234
+ /* @__PURE__ */ jsx(
235
+ "div",
236
+ {
237
+ "aria-hidden": "true",
238
+ className: "pointer-events-none absolute inset-y-0 left-0 z-10 w-[120px] bg-gradient-to-r from-background to-transparent"
239
+ }
240
+ ),
241
+ /* @__PURE__ */ jsx(
242
+ "div",
243
+ {
244
+ "aria-hidden": "true",
245
+ className: "pointer-events-none absolute inset-y-0 right-0 z-10 w-[120px] bg-gradient-to-l from-background to-transparent"
246
+ }
247
+ ),
248
+ hasRing && /* @__PURE__ */ jsxs(Fragment, { children: [
249
+ /* @__PURE__ */ jsx(
250
+ "button",
251
+ {
252
+ type: "button",
253
+ "aria-label": prevLabel,
254
+ onClick: () => goTo(active - 1),
255
+ className: cn(COVERFLOW_ARROW_CLASS, "left-2 sm:left-4"),
256
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-5 w-5", "aria-hidden": true })
257
+ }
258
+ ),
259
+ /* @__PURE__ */ jsx(
260
+ "button",
261
+ {
262
+ type: "button",
263
+ "aria-label": nextLabel,
264
+ onClick: () => goTo(active + 1),
265
+ className: cn(COVERFLOW_ARROW_CLASS, "right-2 sm:right-4"),
266
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-5 w-5", "aria-hidden": true })
267
+ }
268
+ )
269
+ ] }),
270
+ /* @__PURE__ */ jsx("div", { className: "coverflow-lane relative h-full w-full [transform-style:preserve-3d] motion-reduce:[animation:none]", children: visibleCards.map(({ item, index, offset, suppressTransition }) => /* @__PURE__ */ jsx(
271
+ CoverflowCard,
272
+ {
273
+ item,
274
+ index,
275
+ offset,
276
+ suppressTransition,
277
+ freeLabel,
278
+ onActivate: goTo,
279
+ linkComponent: LinkComponent
280
+ },
281
+ item.id
282
+ )) })
283
+ ]
284
+ }
285
+ ),
286
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-4 text-center", children: [
287
+ /* @__PURE__ */ jsxs("div", { "aria-live": "polite", children: [
288
+ /* @__PURE__ */ jsx("h3", { className: "text-[17px] font-extrabold", children: activeItem.title }),
289
+ /* @__PURE__ */ jsx("p", { className: "mt-[3px] text-[12.5px] text-muted-foreground", children: activeItem.meta })
290
+ ] }),
291
+ (caption || positionLabel) && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 text-[12.5px] text-muted-foreground", children: [
292
+ caption && /* @__PURE__ */ jsx("span", { children: caption }),
293
+ caption && positionLabel && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
294
+ positionLabel && /* @__PURE__ */ jsx("span", { children: positionLabel })
295
+ ] })
296
+ ] })
297
+ ] });
298
+ }
299
+
300
+ export { Coverflow };
@@ -0,0 +1,82 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as React from 'react';
3
+ import { DayRailChipLabels } from './event-types.js';
4
+
5
+ /**
6
+ * DayRailChip — one day in the horizontally scrolling day rail.
7
+ *
8
+ * Three states the design distinguishes and that are easy to collapse by
9
+ * accident:
10
+ * - ACTIVE: FILLED — accent ground, accent-foreground label, count and dot.
11
+ * - HAS EVENTS: card ground, normal foreground, an accent density dot.
12
+ * - EMPTY: muted text, NO dot, an em dash instead of "0". A day with nothing
13
+ * on stays in the rail — ten of fourteen days routinely read empty, and
14
+ * hiding them makes the rail lie about how far it reaches — but the caller
15
+ * decides whether it is interactive, and a rail that NAVIGATES has nothing
16
+ * to send an empty day to.
17
+ *
18
+ * ── Why active is filled ──────────────────────────────────────────────────
19
+ * It was an accent border and accent text. Measured against the real tokens
20
+ * that is a 1px hairline plus a label at 1.16:1 against its inactive
21
+ * neighbours in light mode — and in dark, where `--primary` (0.66) is DIMMER
22
+ * than `--foreground` (0.96), the active chip read as the recessed one at
23
+ * 2.86:1. Survivable while a large pinned day header also announced the day;
24
+ * not survivable once the rail became the only indicator.
25
+ *
26
+ * An eye scanning fourteen near-identical 62px boxes horizontally, in
27
+ * peripheral vision, while the page moves, resolves AREA and LUMINANCE STEP
28
+ * long before it resolves the hue of a hairline or the weight of 11.5px type.
29
+ * The fill presents ~3,100px² at 5.97:1 (light) / 6.09:1 (dark). A tinted
30
+ * ground would not do it: `bg-accent/10` and `--accent-subtle` both measure
31
+ * ~1.15:1 against the chip's own card, which is no signal at all.
32
+ *
33
+ * `--accent`, deliberately, not `--gradient-primary`: white on that gradient's
34
+ * top stop is 4.45:1, under AA for text, and the gradient has no `.dark`
35
+ * override so the chip's ink direction would invert against the rest of the
36
+ * dark theme. `--accent` also already means "current" here — the bottom nav's
37
+ * active tab uses it.
38
+ *
39
+ * The border stays 1px in BOTH states. Thickening it on active would change
40
+ * the box model, and these chips sit in an equal-width grid that would reflow
41
+ * on every scroll-spy tick.
42
+ *
43
+ * Renders a link when `href` is given, a button when only `onClick` is, and a
44
+ * plain div otherwise. The button announces `aria-current`, not
45
+ * `aria-pressed`: this is the current item in a set of days, not a toggle.
46
+ */
47
+ type DayRailChipLinkProps = {
48
+ href: string;
49
+ className?: string;
50
+ children: React.ReactNode;
51
+ "aria-current"?: "page";
52
+ onClick?: () => void;
53
+ };
54
+ type DayRailChipProps = {
55
+ /** Short weekday label, already localised — "Today", "Fri", "Sâm". */
56
+ label: string;
57
+ /** Day of month, as displayed. */
58
+ dayNumber: string;
59
+ /** Events on this day, after any active filter. */
60
+ count: number;
61
+ isActive?: boolean;
62
+ /** Keeps the bold weight when inactive. */
63
+ isWeekend?: boolean;
64
+ labels: DayRailChipLabels;
65
+ href?: string;
66
+ linkComponent?: React.ComponentType<DayRailChipLinkProps>;
67
+ onClick?: () => void;
68
+ className?: string;
69
+ };
70
+ declare function DayRailChip({ label, dayNumber, count, isActive, isWeekend, labels, href, linkComponent: LinkComponent, onClick, className, }: DayRailChipProps): react_jsx_runtime.JSX.Element;
71
+ type DayRailChipSkeletonProps = {
72
+ className?: string;
73
+ };
74
+ /**
75
+ * Mirrors one `<DayRailChip>`'s footprint (62×50, 12px radius — the source
76
+ * design's own day-rail skeleton geometry, which also happens to match this
77
+ * chip's rendered size: label + count rows inside `py-2` padding). The caller
78
+ * repeats it with the rail's own `gap-2` to build the loading rail.
79
+ */
80
+ declare function DayRailChipSkeleton({ className }: DayRailChipSkeletonProps): react_jsx_runtime.JSX.Element;
81
+
82
+ export { DayRailChip, type DayRailChipLinkProps, type DayRailChipProps, DayRailChipSkeleton, type DayRailChipSkeletonProps };
@@ -0,0 +1,105 @@
1
+ import { Skeleton } from '../chunk-QXHMPJ35.js';
2
+ import { cn } from '../chunk-FEK5XGZW.js';
3
+ import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
4
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
5
+
6
+ function DefaultLink(_a) {
7
+ var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
8
+ return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
9
+ }
10
+ function DayRailChip({
11
+ label,
12
+ dayNumber,
13
+ count,
14
+ isActive,
15
+ isWeekend,
16
+ labels,
17
+ href,
18
+ linkComponent: LinkComponent = DefaultLink,
19
+ onClick,
20
+ className
21
+ }) {
22
+ const hasEvents = count > 0;
23
+ const rootClasses = cn(
24
+ "w-[62px] shrink-0 rounded-xl border px-1 py-2 text-center",
25
+ "transition-colors duration-150 motion-reduce:transition-none",
26
+ isActive ? "border-accent bg-accent" : "border-border bg-card",
27
+ (href || onClick) && // `ring-offset-background` is not decoration: Tailwind's default offset
28
+ // colour is white, which painted a white halo around a focused chip on
29
+ // the dark rail.
30
+ "cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
31
+ className
32
+ );
33
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
34
+ /* @__PURE__ */ jsxs(
35
+ "div",
36
+ {
37
+ className: cn(
38
+ "text-[11.5px]",
39
+ isActive || isWeekend ? "font-bold" : "font-semibold",
40
+ isActive ? "text-accent-foreground" : hasEvents ? "text-foreground" : "text-muted-foreground"
41
+ ),
42
+ children: [
43
+ label,
44
+ " ",
45
+ dayNumber
46
+ ]
47
+ }
48
+ ),
49
+ /* @__PURE__ */ jsxs("div", { className: "mt-[3px] flex items-center justify-center gap-1", children: [
50
+ hasEvents && /* @__PURE__ */ jsx(
51
+ "span",
52
+ {
53
+ className: cn(
54
+ "h-1 w-1 rounded-full",
55
+ isActive ? "bg-accent-foreground" : "bg-accent"
56
+ ),
57
+ "aria-hidden": true
58
+ }
59
+ ),
60
+ /* @__PURE__ */ jsx(
61
+ "span",
62
+ {
63
+ className: cn(
64
+ // Full opacity on active, never dimmed: `/80` lands at 4.48:1, an
65
+ // AA fail by 0.02. Hierarchy inside a 50px box comes from the
66
+ // label's weight and the type-size step, which is enough.
67
+ "font-mono text-[10.5px] tabular-nums",
68
+ isActive ? "text-accent-foreground" : "text-muted-foreground"
69
+ ),
70
+ children: hasEvents ? count : /* @__PURE__ */ jsx("span", { "aria-label": labels.noEvents, children: "\u2014" })
71
+ }
72
+ )
73
+ ] })
74
+ ] });
75
+ if (href) {
76
+ return /* @__PURE__ */ jsx(
77
+ LinkComponent,
78
+ {
79
+ href,
80
+ className: rootClasses,
81
+ "aria-current": isActive ? "page" : void 0,
82
+ onClick,
83
+ children: body
84
+ }
85
+ );
86
+ }
87
+ if (onClick) {
88
+ return /* @__PURE__ */ jsx(
89
+ "button",
90
+ {
91
+ type: "button",
92
+ className: rootClasses,
93
+ "aria-current": isActive ? "true" : void 0,
94
+ onClick,
95
+ children: body
96
+ }
97
+ );
98
+ }
99
+ return /* @__PURE__ */ jsx("div", { className: rootClasses, children: body });
100
+ }
101
+ function DayRailChipSkeleton({ className }) {
102
+ return /* @__PURE__ */ jsx(Skeleton, { "aria-hidden": "true", className: cn("h-[50px] w-[62px] shrink-0", className) });
103
+ }
104
+
105
+ export { DayRailChip, DayRailChipSkeleton };
@@ -0,0 +1,28 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * DriftRail — an infinite horizontal marquee for logos, tag chips or small
5
+ * poster tiles. The caller passes one set of children; the component renders
6
+ * a second, `aria-hidden` copy right after it so a `translateX(-50%)` loop is
7
+ * seamless. Because the loop depends on the duplicate being pixel-identical
8
+ * to the first pass, keep `children` a fixed, ordered list — no per-render
9
+ * randomisation.
10
+ *
11
+ * Motion is scoped to this instance via a generated id (no shared stylesheet,
12
+ * no class collisions between multiple rails on one page) and is paused two
13
+ * ways, per the source design: an `IntersectionObserver` stops the loop the
14
+ * moment the rail scrolls off-screen, and `prefers-reduced-motion: reduce`
15
+ * removes the animation outright rather than merely slowing it.
16
+ */
17
+ type DriftRailProps = {
18
+ children: React.ReactNode;
19
+ /** Runs the loop right-to-left instead of left-to-right. */
20
+ reverse?: boolean;
21
+ durationSeconds?: number;
22
+ /** Pauses the loop while the pointer hovers the rail. */
23
+ pauseOnHover?: boolean;
24
+ className?: string;
25
+ };
26
+ declare function DriftRail({ children, reverse, durationSeconds, pauseOnHover, className, }: DriftRailProps): react_jsx_runtime.JSX.Element;
27
+
28
+ export { DriftRail, type DriftRailProps };
@@ -0,0 +1,3 @@
1
+ export { DriftRail } from '../chunk-GPH6L4DX.js';
2
+ import '../chunk-FEK5XGZW.js';
3
+ import '../chunk-3GWWZKX7.js';