@bearmenu/ui 0.8.3 → 0.8.6
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-2WDJR6K7.js +76 -0
- package/dist/chunk-637L7XQ3.js +85 -0
- package/dist/chunk-6XMYEZVR.js +26 -0
- package/dist/chunk-DNSXMHYX.js +8 -0
- package/dist/chunk-FDITZ2VM.js +16 -0
- package/dist/chunk-GPH6L4DX.js +55 -0
- package/dist/{chunk-N2KNBPAQ.js → chunk-QXHMPJ35.js} +1 -0
- package/dist/chunk-TT3WFS6S.js +379 -0
- package/dist/chunk-UXWAOISG.js +170 -0
- package/dist/{chunk-UZ3PCIE5.js → chunk-XWMRABZW.js} +2 -2
- package/dist/{chunk-VBA45GLP.js → chunk-ZDAHKI3A.js} +12 -2
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.js +4 -127
- package/dist/components/category-deck.d.ts +50 -0
- package/dist/components/category-deck.js +128 -0
- package/dist/components/command.d.ts +15 -15
- package/dist/components/concept-card.d.ts +43 -0
- package/dist/components/concept-card.js +81 -0
- package/dist/components/coverflow.d.ts +123 -0
- package/dist/components/coverflow.js +300 -0
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/day-rail-chip.d.ts +82 -0
- package/dist/components/day-rail-chip.js +105 -0
- package/dist/components/drift-rail.d.ts +28 -0
- package/dist/components/drift-rail.js +3 -0
- package/dist/components/event-card.d.ts +97 -0
- package/dist/components/event-card.js +161 -0
- package/dist/components/event-compact-row.d.ts +72 -0
- package/dist/components/event-compact-row.js +133 -0
- package/dist/components/event-day-header.d.ts +65 -0
- package/dist/components/event-day-header.js +95 -0
- package/dist/components/event-day-separator.d.ts +46 -0
- package/dist/components/event-day-separator.js +50 -0
- package/dist/components/event-detail-panel.d.ts +80 -0
- package/dist/components/event-detail-panel.js +251 -0
- package/dist/components/event-hub-hero.d.ts +34 -0
- package/dist/components/event-hub-hero.js +57 -0
- package/dist/components/event-index-row.d.ts +68 -0
- package/dist/components/event-index-row.js +84 -0
- package/dist/components/event-list-row-wide.d.ts +101 -0
- package/dist/components/event-list-row-wide.js +218 -0
- package/dist/components/event-list-row.d.ts +82 -0
- package/dist/components/event-list-row.js +173 -0
- package/dist/components/event-poster-fallback.d.ts +20 -0
- package/dist/components/event-poster-fallback.js +3 -0
- package/dist/components/event-types.d.ts +118 -0
- package/dist/components/event-types.js +2 -0
- package/dist/components/events-empty-state.d.ts +44 -0
- package/dist/components/events-empty-state.js +73 -0
- package/dist/components/facet-group.d.ts +93 -0
- package/dist/components/facet-group.js +222 -0
- package/dist/components/featured-rail-item.d.ts +42 -0
- package/dist/components/featured-rail-item.js +88 -0
- package/dist/components/hour-band.d.ts +68 -0
- package/dist/components/hour-band.js +53 -0
- package/dist/components/hour-group.d.ts +67 -0
- package/dist/components/hour-group.js +59 -0
- package/dist/components/icon.d.ts +18 -0
- package/dist/components/icon.js +31 -0
- package/dist/components/item.d.ts +2 -2
- package/dist/components/motion-icon-controls.d.ts +26 -0
- package/dist/components/motion-icon-controls.js +4 -0
- package/dist/components/motion-icon-set.d.ts +27 -0
- package/dist/components/motion-icon-set.js +139 -0
- package/dist/components/motion-icon.d.ts +44 -0
- package/dist/components/motion-icon.js +3 -0
- package/dist/components/multi-select-combobox.js +1 -1
- package/dist/components/place-about-tab.js +3 -3
- package/dist/components/place-details-mobile.js +3 -3
- package/dist/components/poster-stack.d.ts +30 -0
- package/dist/components/poster-stack.js +104 -0
- package/dist/components/poster-tape.d.ts +36 -0
- package/dist/components/poster-tape.js +64 -0
- package/dist/components/poster-wall.d.ts +25 -0
- package/dist/components/poster-wall.js +183 -0
- package/dist/components/quiet-stretch.d.ts +43 -0
- package/dist/components/quiet-stretch.js +54 -0
- package/dist/components/section-header.d.ts +53 -0
- package/dist/components/section-header.js +63 -0
- package/dist/components/segmented-nav.d.ts +62 -0
- package/dist/components/segmented-nav.js +54 -0
- package/dist/components/skeleton-card.js +1 -1
- package/dist/components/skeleton.d.ts +17 -0
- package/dist/components/skeleton.js +1 -1
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/tag-group.js +3 -26
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/tag.js +1 -1
- package/dist/components/text.d.ts +3 -3
- package/dist/components/timeline-band.d.ts +46 -0
- package/dist/components/timeline-band.js +5 -0
- package/dist/components/venue-events-block.d.ts +143 -0
- package/dist/components/venue-events-block.js +222 -0
- package/dist/components/weekend-panel.d.ts +44 -0
- package/dist/components/weekend-panel.js +231 -0
- package/dist/lib/icon-registry.d.ts +58 -0
- package/dist/lib/icon-registry.js +39 -0
- package/package.json +2 -1
- package/src/components/accordion.stories.tsx +1 -1
- package/src/components/card.tsx +44 -1
- package/src/components/category-deck.stories.tsx +96 -0
- package/src/components/category-deck.tsx +227 -0
- package/src/components/combobox.stories.tsx +1 -1
- package/src/components/concept-card.stories.tsx +80 -0
- package/src/components/concept-card.tsx +143 -0
- package/src/components/coverflow.stories.tsx +240 -0
- package/src/components/coverflow.test.tsx +193 -0
- package/src/components/coverflow.tsx +550 -0
- package/src/components/currency-input.stories.tsx +3 -3
- package/src/components/day-rail-chip.stories.tsx +145 -0
- package/src/components/day-rail-chip.tsx +201 -0
- package/src/components/drift-rail.stories.tsx +92 -0
- package/src/components/drift-rail.tsx +86 -0
- package/src/components/empty-state.stories.tsx +3 -3
- package/src/components/event-card.stories.tsx +248 -0
- package/src/components/event-card.test.tsx +177 -0
- package/src/components/event-card.tsx +321 -0
- package/src/components/event-compact-row.stories.tsx +221 -0
- package/src/components/event-compact-row.tsx +268 -0
- package/src/components/event-day-header.stories.tsx +124 -0
- package/src/components/event-day-header.tsx +180 -0
- package/src/components/event-day-separator.stories.tsx +70 -0
- package/src/components/event-day-separator.tsx +95 -0
- package/src/components/event-detail-panel.stories.tsx +191 -0
- package/src/components/event-detail-panel.tsx +360 -0
- package/src/components/event-hub-hero.stories.tsx +66 -0
- package/src/components/event-hub-hero.tsx +110 -0
- package/src/components/event-index-row.stories.tsx +116 -0
- package/src/components/event-index-row.tsx +181 -0
- package/src/components/event-list-row-wide.stories.tsx +204 -0
- package/src/components/event-list-row-wide.tsx +386 -0
- package/src/components/event-list-row.stories.tsx +159 -0
- package/src/components/event-list-row.tsx +335 -0
- package/src/components/event-poster-fallback.stories.tsx +52 -0
- package/src/components/event-poster-fallback.tsx +31 -0
- package/src/components/event-types.ts +133 -0
- package/src/components/events-empty-state.stories.tsx +59 -0
- package/src/components/events-empty-state.tsx +131 -0
- package/src/components/facet-group.stories.tsx +247 -0
- package/src/components/facet-group.test.tsx +225 -0
- package/src/components/facet-group.tsx +352 -0
- package/src/components/featured-rail-item.stories.tsx +86 -0
- package/src/components/featured-rail-item.tsx +153 -0
- package/src/components/form.stories.tsx +1 -1
- package/src/components/hour-band.stories.tsx +159 -0
- package/src/components/hour-band.tsx +126 -0
- package/src/components/hour-group.stories.tsx +100 -0
- package/src/components/hour-group.tsx +124 -0
- package/src/components/icon.tsx +57 -0
- package/src/components/input-otp.stories.tsx +1 -1
- package/src/components/markdown-renderer.stories.tsx +1 -1
- package/src/components/motion-icon-controls.test.tsx +78 -0
- package/src/components/motion-icon-controls.tsx +552 -0
- package/src/components/motion-icon-set.stories.tsx +185 -0
- package/src/components/motion-icon-set.tsx +219 -0
- package/src/components/motion-icon.test.tsx +159 -0
- package/src/components/motion-icon.tsx +163 -0
- package/src/components/multi-select-combobox.stories.tsx +1 -1
- package/src/components/phone-frame.stories.tsx +1 -1
- package/src/components/poster-stack.stories.tsx +68 -0
- package/src/components/poster-stack.tsx +155 -0
- package/src/components/poster-tape.stories.tsx +70 -0
- package/src/components/poster-tape.tsx +122 -0
- package/src/components/poster-wall.stories.tsx +95 -0
- package/src/components/poster-wall.tsx +220 -0
- package/src/components/quiet-stretch.stories.tsx +62 -0
- package/src/components/quiet-stretch.tsx +115 -0
- package/src/components/rating.stories.tsx +3 -3
- package/src/components/searchable-select.stories.tsx +1 -1
- package/src/components/section-header.stories.tsx +70 -0
- package/src/components/section-header.tsx +134 -0
- package/src/components/segmented-nav.stories.tsx +118 -0
- package/src/components/segmented-nav.tsx +113 -0
- package/src/components/selection-bar.stories.tsx +1 -1
- package/src/components/skeleton-card.stories.tsx +3 -3
- package/src/components/skeleton.tsx +18 -0
- package/src/components/sliding-panels.stories.tsx +1 -1
- package/src/components/stepper.stories.tsx +1 -1
- package/src/components/sticky-container.stories.tsx +3 -3
- package/src/components/tag.tsx +11 -1
- package/src/components/timeline-band.stories.tsx +206 -0
- package/src/components/timeline-band.tsx +207 -0
- package/src/components/toggle-group.stories.tsx +1 -1
- package/src/components/tooltip.stories.tsx +2 -2
- package/src/components/venue-events-block.stories.tsx +216 -0
- package/src/components/venue-events-block.tsx +408 -0
- package/src/components/weekend-panel.stories.tsx +92 -0
- package/src/components/weekend-panel.tsx +322 -0
- package/src/globals.css +19 -0
- package/src/lib/icon-registry.ts +139 -0
- package/src/test/setup.ts +5 -1
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronRight } from "lucide-react";
|
|
5
|
+
import { MotionPlayPause } from "./motion-icon-controls";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
import { Skeleton } from "./skeleton";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* WeekendPanel — one day panel in the weekend section: a header row over an
|
|
11
|
+
* auto-advancing two-up carousel of the day's event cards.
|
|
12
|
+
*
|
|
13
|
+
* The carousel assumes exactly two "pages" of two cards (four `children` in
|
|
14
|
+
* total) — that's what the design's slide keyframes measure against
|
|
15
|
+
* (`translateX(calc(-100% - gap))` moves the track exactly one page). Pass a
|
|
16
|
+
* different count and the maths still animate, but the pause points won't
|
|
17
|
+
* line up with a full page.
|
|
18
|
+
*
|
|
19
|
+
* ACCESSIBILITY: satisfies WCAG 2.2.2 (Pause, Stop, Hide) with a visible
|
|
20
|
+
* pause/play toggle next to the progress dots. Hover/focus/off-screen pausing
|
|
21
|
+
* remain as convenience behaviour, but the button is the actual mechanism —
|
|
22
|
+
* it is a latch (`isManuallyPaused`), not a hold, so a touch or low-vision
|
|
23
|
+
* user can stop the carousel and keep it stopped while they read. Toggling it
|
|
24
|
+
* flips `animationPlayState` for the track AND both dot keyframes together.
|
|
25
|
+
* Under `prefers-reduced-motion` the panel already renders a static grid with
|
|
26
|
+
* nothing animating, so the control would be meaningless and is not rendered.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const WEEKEND_PANEL_TRACK_GAP_PX = 14;
|
|
30
|
+
const WEEKEND_PANEL_DEFAULT_DURATION_SECONDS = 7;
|
|
31
|
+
const WEEKEND_PANEL_SLIDE_KEYFRAMES_ID = "ds-weekend-panel-slide";
|
|
32
|
+
const WEEKEND_PANEL_DOT_A_KEYFRAMES_ID = "ds-weekend-panel-dot-a";
|
|
33
|
+
const WEEKEND_PANEL_DOT_B_KEYFRAMES_ID = "ds-weekend-panel-dot-b";
|
|
34
|
+
|
|
35
|
+
function usePrefersReducedMotion(): boolean {
|
|
36
|
+
const [reduced, setReduced] = React.useState(false);
|
|
37
|
+
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
40
|
+
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
41
|
+
setReduced(query.matches);
|
|
42
|
+
const onChange = () => setReduced(query.matches);
|
|
43
|
+
query.addEventListener("change", onChange);
|
|
44
|
+
return () => query.removeEventListener("change", onChange);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return reduced;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function useInViewport<T extends Element>(): [React.RefObject<T | null>, boolean] {
|
|
51
|
+
const ref = React.useRef<T | null>(null);
|
|
52
|
+
const [inView, setInView] = React.useState(true);
|
|
53
|
+
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
const node = ref.current;
|
|
56
|
+
if (!node || typeof IntersectionObserver === "undefined") return;
|
|
57
|
+
const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting), {
|
|
58
|
+
threshold: 0,
|
|
59
|
+
});
|
|
60
|
+
observer.observe(node);
|
|
61
|
+
return () => observer.disconnect();
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
return [ref, inView];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type WeekendPanelLinkProps = {
|
|
68
|
+
href: string;
|
|
69
|
+
className?: string;
|
|
70
|
+
children: React.ReactNode;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function DefaultLink({ href, ...props }: WeekendPanelLinkProps) {
|
|
74
|
+
return <a href={href} {...props} />;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Accessible names for the pause/play toggle, injected so the DS owns no copy. */
|
|
78
|
+
export type WeekendPanelLabels = {
|
|
79
|
+
/** Name announced while the carousel is running (the button pauses it). */
|
|
80
|
+
pause: string;
|
|
81
|
+
/** Name announced while the carousel is paused (the button resumes it). */
|
|
82
|
+
play: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type WeekendPanelProps = {
|
|
86
|
+
dayLabel: string;
|
|
87
|
+
/** Caller-composed, e.g. "23 events". */
|
|
88
|
+
countLabel: string;
|
|
89
|
+
actionLabel: string;
|
|
90
|
+
actionHref?: string;
|
|
91
|
+
linkComponent?: React.ComponentType<WeekendPanelLinkProps>;
|
|
92
|
+
onActionClick?: () => void;
|
|
93
|
+
isHighlighted?: boolean;
|
|
94
|
+
/** Loop duration for the slide/dots animation. Default 7 — sibling panels typically stagger at 8.5 and 10. */
|
|
95
|
+
durationSeconds?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Accessible names for the pause/play toggle. Required — the control is
|
|
98
|
+
* only omitted under `prefers-reduced-motion`, where the labels go unused.
|
|
99
|
+
*/
|
|
100
|
+
labels: WeekendPanelLabels;
|
|
101
|
+
/** The event cards. The animation is built around exactly four (two pages of two). */
|
|
102
|
+
children?: React.ReactNode;
|
|
103
|
+
className?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function WeekendPanel({
|
|
107
|
+
dayLabel,
|
|
108
|
+
countLabel,
|
|
109
|
+
actionLabel,
|
|
110
|
+
actionHref,
|
|
111
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
112
|
+
onActionClick,
|
|
113
|
+
isHighlighted,
|
|
114
|
+
durationSeconds = WEEKEND_PANEL_DEFAULT_DURATION_SECONDS,
|
|
115
|
+
labels,
|
|
116
|
+
children,
|
|
117
|
+
className,
|
|
118
|
+
}: WeekendPanelProps) {
|
|
119
|
+
const prefersReducedMotion = usePrefersReducedMotion();
|
|
120
|
+
const [panelRef, inView] = useInViewport<HTMLDivElement>();
|
|
121
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
122
|
+
const [isFocused, setIsFocused] = React.useState(false);
|
|
123
|
+
const [isManuallyPaused, setIsManuallyPaused] = React.useState(false);
|
|
124
|
+
const [playPauseReplayToken, setPlayPauseReplayToken] = React.useState(0);
|
|
125
|
+
const isPaused = isManuallyPaused || isHovered || isFocused || !inView;
|
|
126
|
+
|
|
127
|
+
const action = (
|
|
128
|
+
<span className="ml-auto flex items-center gap-0.5 text-[12.5px] text-muted-foreground">
|
|
129
|
+
{actionLabel}
|
|
130
|
+
<ChevronRight className="h-3.5 w-3.5" aria-hidden />
|
|
131
|
+
</span>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
ref={panelRef}
|
|
137
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
138
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
139
|
+
onFocus={() => setIsFocused(true)}
|
|
140
|
+
onBlur={() => setIsFocused(false)}
|
|
141
|
+
className={cn(
|
|
142
|
+
"relative overflow-hidden rounded-2xl border bg-card py-4",
|
|
143
|
+
isHighlighted ? "border-primary/40" : "border-border",
|
|
144
|
+
className
|
|
145
|
+
)}
|
|
146
|
+
>
|
|
147
|
+
<style>{`
|
|
148
|
+
@keyframes ${WEEKEND_PANEL_SLIDE_KEYFRAMES_ID} {
|
|
149
|
+
0%, 45% { transform: translateX(0); }
|
|
150
|
+
50%, 95% { transform: translateX(calc(-100% - ${WEEKEND_PANEL_TRACK_GAP_PX}px)); }
|
|
151
|
+
100% { transform: translateX(0); }
|
|
152
|
+
}
|
|
153
|
+
@keyframes ${WEEKEND_PANEL_DOT_A_KEYFRAMES_ID} {
|
|
154
|
+
0% { background: var(--primary); width: 18px; }
|
|
155
|
+
45% { background: var(--primary); width: 18px; }
|
|
156
|
+
50% { background: color-mix(in oklch, var(--muted-foreground) 35%, transparent); width: 6px; }
|
|
157
|
+
100% { background: color-mix(in oklch, var(--muted-foreground) 35%, transparent); width: 6px; }
|
|
158
|
+
}
|
|
159
|
+
@keyframes ${WEEKEND_PANEL_DOT_B_KEYFRAMES_ID} {
|
|
160
|
+
0% { background: color-mix(in oklch, var(--muted-foreground) 35%, transparent); width: 6px; }
|
|
161
|
+
50% { background: color-mix(in oklch, var(--muted-foreground) 35%, transparent); width: 6px; }
|
|
162
|
+
95% { background: var(--primary); width: 18px; }
|
|
163
|
+
100% { background: var(--primary); width: 18px; }
|
|
164
|
+
}
|
|
165
|
+
`}</style>
|
|
166
|
+
|
|
167
|
+
<div className="flex items-baseline gap-[9px] px-4 pb-3">
|
|
168
|
+
<span
|
|
169
|
+
className={cn(
|
|
170
|
+
"text-[15.5px] font-extrabold tracking-[-0.02em] sm:text-[17px]",
|
|
171
|
+
isHighlighted && "text-primary"
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
{dayLabel}
|
|
175
|
+
</span>
|
|
176
|
+
<span
|
|
177
|
+
className={cn(
|
|
178
|
+
"font-mono text-xs",
|
|
179
|
+
isHighlighted ? "text-primary" : "text-muted-foreground"
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
182
|
+
{countLabel}
|
|
183
|
+
</span>
|
|
184
|
+
|
|
185
|
+
{actionHref ? (
|
|
186
|
+
<LinkComponent href={actionHref} className="ml-auto">
|
|
187
|
+
{action}
|
|
188
|
+
</LinkComponent>
|
|
189
|
+
) : onActionClick ? (
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={onActionClick}
|
|
193
|
+
className="ml-auto cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md"
|
|
194
|
+
>
|
|
195
|
+
{action}
|
|
196
|
+
</button>
|
|
197
|
+
) : (
|
|
198
|
+
action
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{prefersReducedMotion ? (
|
|
203
|
+
<div className="grid grid-cols-2 gap-3.5 px-4">{children}</div>
|
|
204
|
+
) : (
|
|
205
|
+
<div className="overflow-hidden px-4">
|
|
206
|
+
<div
|
|
207
|
+
className="flex gap-3.5"
|
|
208
|
+
style={{
|
|
209
|
+
animationName: WEEKEND_PANEL_SLIDE_KEYFRAMES_ID,
|
|
210
|
+
animationDuration: `${durationSeconds}s`,
|
|
211
|
+
animationTimingFunction: "ease-in-out",
|
|
212
|
+
animationIterationCount: "infinite",
|
|
213
|
+
animationPlayState: isPaused ? "paused" : "running",
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{React.Children.map(children, (child) => (
|
|
217
|
+
<div className="w-[calc((100%-14px)/2)] flex-none">{child}</div>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
{!prefersReducedMotion && (
|
|
224
|
+
<div className="mt-3.5 grid grid-cols-[1fr_auto_1fr] items-center px-4">
|
|
225
|
+
<span aria-hidden />
|
|
226
|
+
|
|
227
|
+
<div className="flex items-center justify-center gap-1.5" aria-hidden>
|
|
228
|
+
<span
|
|
229
|
+
className="block h-1.5 w-[18px] rounded-full bg-primary"
|
|
230
|
+
style={{
|
|
231
|
+
animationName: WEEKEND_PANEL_DOT_A_KEYFRAMES_ID,
|
|
232
|
+
animationDuration: `${durationSeconds}s`,
|
|
233
|
+
animationTimingFunction: "ease-in-out",
|
|
234
|
+
animationIterationCount: "infinite",
|
|
235
|
+
animationPlayState: isPaused ? "paused" : "running",
|
|
236
|
+
}}
|
|
237
|
+
/>
|
|
238
|
+
<span
|
|
239
|
+
className="block h-1.5 w-1.5 rounded-full bg-muted-foreground/35"
|
|
240
|
+
style={{
|
|
241
|
+
animationName: WEEKEND_PANEL_DOT_B_KEYFRAMES_ID,
|
|
242
|
+
animationDuration: `${durationSeconds}s`,
|
|
243
|
+
animationTimingFunction: "ease-in-out",
|
|
244
|
+
animationIterationCount: "infinite",
|
|
245
|
+
animationPlayState: isPaused ? "paused" : "running",
|
|
246
|
+
}}
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div className="flex justify-end">
|
|
251
|
+
{/* `MotionPlayPause`, not two swapped lucide glyphs. The triangle
|
|
252
|
+
folding into two bars IS the state change, so the control shows
|
|
253
|
+
what it did instead of cutting to a different icon — which is
|
|
254
|
+
the whole reason that gesture exists in the set.
|
|
255
|
+
|
|
256
|
+
`active` drives the HELD state and the bars are the active
|
|
257
|
+
variant, so it is `!isManuallyPaused`: while the carousel plays,
|
|
258
|
+
the button offers pause. `replayToken` bumps on pointer down so
|
|
259
|
+
the gesture answers the touch rather than its consequence.
|
|
260
|
+
|
|
261
|
+
`motion-reduce` is handled inside `MotionIcon`; the button keeps
|
|
262
|
+
its own for the background transition. */}
|
|
263
|
+
<button
|
|
264
|
+
type="button"
|
|
265
|
+
onClick={() => setIsManuallyPaused((current) => !current)}
|
|
266
|
+
onPointerDown={() => setPlayPauseReplayToken((n) => n + 1)}
|
|
267
|
+
aria-label={isManuallyPaused ? labels.play : labels.pause}
|
|
268
|
+
aria-pressed={isManuallyPaused}
|
|
269
|
+
className="flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors duration-150 hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 motion-reduce:transition-none"
|
|
270
|
+
>
|
|
271
|
+
<MotionPlayPause
|
|
272
|
+
active={!isManuallyPaused}
|
|
273
|
+
replayToken={playPauseReplayToken}
|
|
274
|
+
className="size-4"
|
|
275
|
+
/>
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export type WeekendPanelSkeletonProps = {
|
|
285
|
+
isHighlighted?: boolean;
|
|
286
|
+
className?: string;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/** The panel at rest: the two visible cards of the two-up carousel, no slide. */
|
|
290
|
+
export function WeekendPanelSkeleton({ isHighlighted, className }: WeekendPanelSkeletonProps) {
|
|
291
|
+
return (
|
|
292
|
+
<div
|
|
293
|
+
aria-hidden="true"
|
|
294
|
+
className={cn(
|
|
295
|
+
"relative overflow-hidden rounded-2xl border bg-card py-4",
|
|
296
|
+
isHighlighted ? "border-primary/40" : "border-border",
|
|
297
|
+
className
|
|
298
|
+
)}
|
|
299
|
+
>
|
|
300
|
+
<div className="flex items-baseline gap-[9px] px-4 pb-3">
|
|
301
|
+
<Skeleton className="h-[15.5px] w-16 sm:h-[17px]" />
|
|
302
|
+
<Skeleton className="h-3 w-14" />
|
|
303
|
+
<Skeleton className="ml-auto h-3 w-14" />
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div className="grid grid-cols-2 gap-3.5 px-4">
|
|
307
|
+
{Array.from({ length: 2 }, (_, index) => (
|
|
308
|
+
<div key={index} className="flex flex-col gap-1.5">
|
|
309
|
+
<Skeleton className="aspect-[4/3] w-full" />
|
|
310
|
+
<Skeleton className="h-[13.5px] w-4/5" />
|
|
311
|
+
<Skeleton className="h-3 w-3/5" />
|
|
312
|
+
</div>
|
|
313
|
+
))}
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div className="flex items-center justify-center gap-1.5 pt-3.5">
|
|
317
|
+
<span className="block h-1.5 w-[18px] rounded-full bg-muted" />
|
|
318
|
+
<span className="block h-1.5 w-1.5 rounded-full bg-muted" />
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
package/src/globals.css
CHANGED
|
@@ -137,6 +137,17 @@ h6 {
|
|
|
137
137
|
0 2px 4px -1px oklch(0.52 0 0 / 0.07), 0 10px 24px -6px oklch(0.52 0 0 / 0.16);
|
|
138
138
|
--shadow-overlay:
|
|
139
139
|
0 4px 12px -2px oklch(0.52 0 0 / 0.1), 0 16px 48px -12px oklch(0.52 0 0 / 0.2);
|
|
140
|
+
/* Panel elevation — the same apparent height as --shadow-card under a much
|
|
141
|
+
larger object. Elevation is a function of area: a 6px penumbra under a
|
|
142
|
+
700px panel reads as a print bevel, not a lift, so the blur widens and the
|
|
143
|
+
peak alpha drops and the total ink scales with the object instead of
|
|
144
|
+
stacking. Deliberately stays BELOW --shadow-card-hover in weight — a
|
|
145
|
+
RESTING panel that out-shadows a card's hover makes hover stop meaning
|
|
146
|
+
anything anywhere. And not --shadow-overlay, which is plane 2 (sheets,
|
|
147
|
+
dialogs); this sits in page flow. Chroma 0, like its neighbours: a 16px
|
|
148
|
+
blur across a full-width panel is the largest soft area on the page and
|
|
149
|
+
the least attributable place for a colour cast. */
|
|
150
|
+
--shadow-panel: 0 1px 2px oklch(0.52 0 0 / 0.04), 0 4px 16px -4px oklch(0.52 0 0 / 0.07);
|
|
140
151
|
|
|
141
152
|
/* Brown variant — replaces hardcoded #482E1F across button/badge */
|
|
142
153
|
--brown: oklch(0.31 0.05 45);
|
|
@@ -205,6 +216,13 @@ h6 {
|
|
|
205
216
|
--ease-spring-smooth: cubic-bezier(0.22, 1, 0.36, 1);
|
|
206
217
|
--ease-spring-soft: cubic-bezier(0.16, 1, 0.3, 1);
|
|
207
218
|
|
|
219
|
+
/* Timeline geometry — shared by `TimelineBand`, which lays the columns out,
|
|
220
|
+
and `HourGroup`, whose spine puck positions itself against that gutter from
|
|
221
|
+
inside the content column. Two components deriving the same offset from two
|
|
222
|
+
hardcoded numbers is how they drift apart. */
|
|
223
|
+
--timeline-lane: 396px;
|
|
224
|
+
--timeline-spine: 6rem;
|
|
225
|
+
|
|
208
226
|
/* Z-index scale */
|
|
209
227
|
--z-base: 0;
|
|
210
228
|
--z-sticky: 100;
|
|
@@ -265,6 +283,7 @@ h6 {
|
|
|
265
283
|
--shadow-card: 0 1px 3px oklch(0 0 0 / 0.2), 0 1px 2px oklch(0 0 0 / 0.15);
|
|
266
284
|
--shadow-card-hover: 0 4px 16px oklch(0 0 0 / 0.3), 0 1px 4px oklch(0 0 0 / 0.2);
|
|
267
285
|
--shadow-overlay: 0 8px 32px oklch(0 0 0 / 0.4), 0 2px 8px oklch(0 0 0 / 0.25);
|
|
286
|
+
--shadow-panel: 0 1px 3px oklch(0 0 0 / 0.24), 0 8px 24px -8px oklch(0 0 0 / 0.34);
|
|
268
287
|
--sidebar: oklch(0.205 0 0);
|
|
269
288
|
--sidebar-foreground: oklch(0.985 0 0);
|
|
270
289
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The purpose → glyph registry.
|
|
3
|
+
*
|
|
4
|
+
* One place recording which glyph means what. Before this existed, ~10 ad-hoc
|
|
5
|
+
* glyph maps were scattered across the app and the same purpose ended up drawn
|
|
6
|
+
* two different ways: back as both an arrow and a chevron, "open filters" as
|
|
7
|
+
* both a funnel and sliders (one surface used both, chosen by which view you
|
|
8
|
+
* were in), clear and close sharing a bare X.
|
|
9
|
+
*
|
|
10
|
+
* Only GENERIC action vocabulary lives here. Product taxonomy — cuisine
|
|
11
|
+
* shortcuts, facet dimensions, digest categories — stays in the consuming app.
|
|
12
|
+
*
|
|
13
|
+
* ## The rules this encodes
|
|
14
|
+
*
|
|
15
|
+
* 1. **Arrow leaves, chevron stays.** `ArrowRight` means "you are going to a new
|
|
16
|
+
* destination" (see-all links, banner CTAs). `ChevronRight` means "reveal
|
|
17
|
+
* more, in place" and is scoped to exactly two jobs: carousel paging and
|
|
18
|
+
* row-contained drill-in. Collapsing these is what produced a glyph with
|
|
19
|
+
* four meanings.
|
|
20
|
+
* 2. **Sliders, not a funnel.** A funnel reads as "narrow a list" and collides
|
|
21
|
+
* with search and sort iconography at 24px; three sliders read
|
|
22
|
+
* unambiguously as "adjust parameters" and carry a badge count cleanly.
|
|
23
|
+
* 3. **Containment separates clear from close.** A bare `X` leaves a layer. A
|
|
24
|
+
* contained `XCircle` resets a value in place without leaving. In a dense
|
|
25
|
+
* filter row, bare Xs beside each other cannot say which layer they close.
|
|
26
|
+
* 4. **Tappable has a container or a colour state; information is muted and
|
|
27
|
+
* bare.** This is what disambiguates the overloaded glyphs (`MapPin`,
|
|
28
|
+
* `Star`) rather than picking different glyphs for each sense. An
|
|
29
|
+
* interactive instance sits in a chip or button and takes an active colour;
|
|
30
|
+
* a decorative one is `text-muted-foreground`, `aria-hidden`, outside any
|
|
31
|
+
* button, with no hover or active transition.
|
|
32
|
+
* 5. **Loved and want-to-try are two actions, not one.** `Heart` is
|
|
33
|
+
* retrospective ("I have had this and I love it"), `Bookmark` is prospective
|
|
34
|
+
* ("I want to try this"). They feed different lists and are mutually
|
|
35
|
+
* exclusive per item.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
ArrowLeft,
|
|
40
|
+
ArrowRight,
|
|
41
|
+
ArrowUpDown,
|
|
42
|
+
Bookmark,
|
|
43
|
+
Check,
|
|
44
|
+
ChevronDown,
|
|
45
|
+
ChevronLeft,
|
|
46
|
+
ChevronRight,
|
|
47
|
+
ExternalLink,
|
|
48
|
+
Flag,
|
|
49
|
+
Heart,
|
|
50
|
+
Info,
|
|
51
|
+
Navigation,
|
|
52
|
+
Phone,
|
|
53
|
+
Plus,
|
|
54
|
+
RefreshCw,
|
|
55
|
+
Search,
|
|
56
|
+
Share2,
|
|
57
|
+
SlidersHorizontal,
|
|
58
|
+
Trash2,
|
|
59
|
+
X,
|
|
60
|
+
XCircle,
|
|
61
|
+
type LucideIcon,
|
|
62
|
+
} from "lucide-react";
|
|
63
|
+
|
|
64
|
+
export type IconPurpose =
|
|
65
|
+
// navigation
|
|
66
|
+
| "back"
|
|
67
|
+
| "seeAll"
|
|
68
|
+
| "drillIn"
|
|
69
|
+
| "carouselPrev"
|
|
70
|
+
| "carouselNext"
|
|
71
|
+
| "directions"
|
|
72
|
+
| "externalLink"
|
|
73
|
+
// layers and values
|
|
74
|
+
| "close"
|
|
75
|
+
| "clear"
|
|
76
|
+
| "expand"
|
|
77
|
+
// list controls
|
|
78
|
+
| "filter"
|
|
79
|
+
| "sort"
|
|
80
|
+
| "search"
|
|
81
|
+
// actions
|
|
82
|
+
| "share"
|
|
83
|
+
| "favorite"
|
|
84
|
+
| "wantToTry"
|
|
85
|
+
| "confirm"
|
|
86
|
+
| "add"
|
|
87
|
+
| "remove"
|
|
88
|
+
| "refresh"
|
|
89
|
+
| "call"
|
|
90
|
+
| "report"
|
|
91
|
+
| "info";
|
|
92
|
+
|
|
93
|
+
export const iconFor: Record<IconPurpose, LucideIcon> = {
|
|
94
|
+
back: ArrowLeft,
|
|
95
|
+
seeAll: ArrowRight,
|
|
96
|
+
drillIn: ChevronRight,
|
|
97
|
+
carouselPrev: ChevronLeft,
|
|
98
|
+
carouselNext: ChevronRight,
|
|
99
|
+
directions: Navigation,
|
|
100
|
+
externalLink: ExternalLink,
|
|
101
|
+
|
|
102
|
+
close: X,
|
|
103
|
+
clear: XCircle,
|
|
104
|
+
expand: ChevronDown,
|
|
105
|
+
|
|
106
|
+
filter: SlidersHorizontal,
|
|
107
|
+
sort: ArrowUpDown,
|
|
108
|
+
search: Search,
|
|
109
|
+
|
|
110
|
+
share: Share2,
|
|
111
|
+
favorite: Heart,
|
|
112
|
+
wantToTry: Bookmark,
|
|
113
|
+
confirm: Check,
|
|
114
|
+
add: Plus,
|
|
115
|
+
remove: Trash2,
|
|
116
|
+
refresh: RefreshCw,
|
|
117
|
+
call: Phone,
|
|
118
|
+
report: Flag,
|
|
119
|
+
info: Info,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Purposes whose glyph must rotate to show state, and by how much.
|
|
124
|
+
* `expand` is the disclosure chevron: pointing down at rest, up when open.
|
|
125
|
+
*/
|
|
126
|
+
export const iconRotation: Partial<Record<IconPurpose, number>> = {
|
|
127
|
+
expand: 180,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Retired glyphs and what replaced them. Kept so a reviewer can tell a
|
|
132
|
+
* deliberate choice from a regression, and so lint/codemods have a source.
|
|
133
|
+
*/
|
|
134
|
+
export const retiredIcons = {
|
|
135
|
+
Filter: "filter → SlidersHorizontal",
|
|
136
|
+
ChevronRightAsSeeAll: "seeAll → ArrowRight",
|
|
137
|
+
ChevronLeftAsBack: "back → ArrowLeft",
|
|
138
|
+
XAsClear: "clear → XCircle",
|
|
139
|
+
} as const;
|
package/src/test/setup.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import "@testing-library/jest-dom";
|
|
2
2
|
|
|
3
|
-
global
|
|
3
|
+
// `globalThis`, not `global`: the latter is a Node-only global, and this
|
|
4
|
+
// project's `lib` is dom / dom.iterable / esnext with no `@types/node`, so it
|
|
5
|
+
// did not typecheck. This file already reaches for `window` below — the test
|
|
6
|
+
// environment is jsdom, and `globalThis` is the spelling that is typed in both.
|
|
7
|
+
globalThis.ResizeObserver = class ResizeObserver {
|
|
4
8
|
observe() {}
|
|
5
9
|
unobserve() {}
|
|
6
10
|
disconnect() {}
|