@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,408 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Quote, Star } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { Card } from "./card";
|
|
7
|
+
import { Separator } from "./separator";
|
|
8
|
+
import { Skeleton } from "./skeleton";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* VenueEventsBlock — "one venue, a whole fortnight": the venue and its
|
|
12
|
+
* upcoming nights on the left, its menu on the right. Every string arrives
|
|
13
|
+
* pre-formatted from the caller (ratings, prices, counts) — this component
|
|
14
|
+
* does no number or currency formatting, same contract as `EventCard`.
|
|
15
|
+
*
|
|
16
|
+
* Three independent link surfaces share one `linkComponent`: the venue
|
|
17
|
+
* header, each night row, and the "all events" chip. Each renders a link only
|
|
18
|
+
* when its own `href` is given, a plain element otherwise — none of them
|
|
19
|
+
* assume the others are interactive.
|
|
20
|
+
*
|
|
21
|
+
* `venue`/`nights`/`dishes` default to an empty venue / `[]` so a caller using
|
|
22
|
+
* only `venueContent`/`nightsContent`/`menuContent` never has to fabricate
|
|
23
|
+
* placeholder data just to satisfy these props.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export type VenueEventsBlockLinkProps = {
|
|
27
|
+
href: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function DefaultLink({ href, ...props }: VenueEventsBlockLinkProps) {
|
|
33
|
+
return <a href={href} {...props} />;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type VenueEventsBlockVenue = {
|
|
37
|
+
name: string;
|
|
38
|
+
ratingLabel?: string;
|
|
39
|
+
metaLabel?: string;
|
|
40
|
+
thumbnailUrl?: string;
|
|
41
|
+
isOpenNow?: boolean;
|
|
42
|
+
href?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type VenueEventsBlockNight = {
|
|
46
|
+
id: string;
|
|
47
|
+
dayLabel: string;
|
|
48
|
+
title: string;
|
|
49
|
+
timeLabel: string;
|
|
50
|
+
href?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type VenueEventsBlockDish = {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
priceLabel: string;
|
|
58
|
+
quantityLabel?: string;
|
|
59
|
+
mentionsLabel?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type VenueEventsBlockProps = {
|
|
63
|
+
/**
|
|
64
|
+
* Plain venue data this block formats itself into the header card. Fine
|
|
65
|
+
* for a caller with nothing richer than name/rating/meta — and kept for
|
|
66
|
+
* the existing stories — but a consuming app that already owns a place
|
|
67
|
+
* card (real photo, description, cuisine/price meta) should reach for
|
|
68
|
+
* `venueContent` instead: rebuilding that card here would fork it, and the
|
|
69
|
+
* fork would drift the moment either copy changes.
|
|
70
|
+
*/
|
|
71
|
+
venue?: VenueEventsBlockVenue;
|
|
72
|
+
/**
|
|
73
|
+
* Plain night data this block formats itself into a row. Fine for a
|
|
74
|
+
* caller with nothing richer than day/title/time — and kept for the
|
|
75
|
+
* existing stories — but a consuming app that already owns an event row
|
|
76
|
+
* component (poster, description, tags) should reach for `nightsContent`
|
|
77
|
+
* instead, for the same reason.
|
|
78
|
+
*/
|
|
79
|
+
nights?: VenueEventsBlockNight[];
|
|
80
|
+
allEventsLabel: string;
|
|
81
|
+
allEventsHref?: string;
|
|
82
|
+
menuHeader?: React.ReactNode;
|
|
83
|
+
/**
|
|
84
|
+
* Plain dish data this block formats itself into a row. Fine for a caller
|
|
85
|
+
* with nothing richer than name/price/description — and kept for the
|
|
86
|
+
* existing stories — but a consuming app that already owns a menu-row
|
|
87
|
+
* component (signature badges, an evidence/mentions control, category
|
|
88
|
+
* marks, an ingredient-hue fallback) should reach for `menuContent`
|
|
89
|
+
* instead: rebuilding that row's markup here would fork it, and the fork
|
|
90
|
+
* would drift the moment either copy changes.
|
|
91
|
+
*/
|
|
92
|
+
dishes?: VenueEventsBlockDish[];
|
|
93
|
+
/**
|
|
94
|
+
* Replaces the internally-rendered venue header entirely when given —
|
|
95
|
+
* `venue.name`/`ratingLabel`/`metaLabel`/`thumbnailUrl`/`isOpenNow` are
|
|
96
|
+
* then ignored (`venue.href` still wraps this slot in a link when given —
|
|
97
|
+
* a caller whose card is already a link should omit `venue.href` and let
|
|
98
|
+
* its own card own the anchor). Exists because this block should own the
|
|
99
|
+
* LAYOUT and nothing about the shape of a venue card: the consuming app
|
|
100
|
+
* has its own canonical place card (used on its place list, its search
|
|
101
|
+
* results, its map sheet), and the DS must not maintain a second
|
|
102
|
+
* implementation of it.
|
|
103
|
+
*/
|
|
104
|
+
venueContent?: React.ReactNode;
|
|
105
|
+
/**
|
|
106
|
+
* Replaces the internally-rendered night rows entirely when given —
|
|
107
|
+
* `nights` is then ignored. Exists because this block should own the
|
|
108
|
+
* LAYOUT and nothing about the shape of a night row: the consuming app
|
|
109
|
+
* has its own canonical event row (used on its timeline, its event
|
|
110
|
+
* detail's "more from this venue"), and the DS must not maintain a
|
|
111
|
+
* second implementation of it. Render that row component here, one per
|
|
112
|
+
* item, with its own key/link/interaction — this slot only provides the
|
|
113
|
+
* column it sits in.
|
|
114
|
+
*/
|
|
115
|
+
nightsContent?: React.ReactNode;
|
|
116
|
+
/**
|
|
117
|
+
* Replaces the internally-rendered dish rows entirely when given —
|
|
118
|
+
* `dishes` is then ignored. Exists because this block should own the
|
|
119
|
+
* LAYOUT (venue, nights, menu column) and nothing about the shape of a
|
|
120
|
+
* menu row: the consuming app has its own canonical row (used on its menu
|
|
121
|
+
* page, its place-details sheet, its must-try and pairings lists), and the
|
|
122
|
+
* DS must not maintain a second implementation of it. Render that row
|
|
123
|
+
* component here, one per item, with its own key/link/interaction — this
|
|
124
|
+
* slot only provides the column it sits in.
|
|
125
|
+
*/
|
|
126
|
+
menuContent?: React.ReactNode;
|
|
127
|
+
footnote?: string;
|
|
128
|
+
linkComponent?: React.ComponentType<VenueEventsBlockLinkProps>;
|
|
129
|
+
className?: string;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export function VenueEventsBlock({
|
|
133
|
+
venue = { name: "" },
|
|
134
|
+
nights = [],
|
|
135
|
+
allEventsLabel,
|
|
136
|
+
allEventsHref,
|
|
137
|
+
menuHeader,
|
|
138
|
+
dishes = [],
|
|
139
|
+
venueContent,
|
|
140
|
+
nightsContent,
|
|
141
|
+
menuContent,
|
|
142
|
+
footnote,
|
|
143
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
144
|
+
className,
|
|
145
|
+
}: VenueEventsBlockProps) {
|
|
146
|
+
const [thumbError, setThumbError] = React.useState(false);
|
|
147
|
+
const showThumb = !!venue.thumbnailUrl && !thumbError;
|
|
148
|
+
|
|
149
|
+
const venueHeader = (
|
|
150
|
+
<div className="flex items-center gap-3 rounded-xl border border-border bg-card p-3">
|
|
151
|
+
<div className="relative h-16 w-16 shrink-0 overflow-hidden rounded-lg bg-muted">
|
|
152
|
+
{showThumb && (
|
|
153
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
154
|
+
<img
|
|
155
|
+
src={venue.thumbnailUrl}
|
|
156
|
+
alt=""
|
|
157
|
+
loading="lazy"
|
|
158
|
+
decoding="async"
|
|
159
|
+
className="h-full w-full object-cover"
|
|
160
|
+
onError={() => setThumbError(true)}
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
{venue.isOpenNow && (
|
|
164
|
+
<span
|
|
165
|
+
className="absolute right-1 top-1 h-1.5 w-1.5 rounded-full bg-success ring-2 ring-background"
|
|
166
|
+
aria-hidden
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className="min-w-0 flex-1">
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<span className="truncate text-sm font-semibold">{venue.name}</span>
|
|
174
|
+
{venue.ratingLabel && (
|
|
175
|
+
<span className="ml-auto flex shrink-0 items-center gap-1 text-xs font-medium text-foreground">
|
|
176
|
+
<Star className="h-3 w-3 fill-current text-primary" aria-hidden />
|
|
177
|
+
{venue.ratingLabel}
|
|
178
|
+
</span>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
{venue.metaLabel && (
|
|
182
|
+
<div className="mt-0.5 truncate text-xs text-muted-foreground">{venue.metaLabel}</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
// `Card variant="elevatedPanel"` rather than a hand-rolled surface. This
|
|
190
|
+
// root WAS `border border-border bg-card` — a `surface` card reimplemented
|
|
191
|
+
// inside the DS, which is how it and the skeleton below drifted apart from
|
|
192
|
+
// the primitive and from each other.
|
|
193
|
+
//
|
|
194
|
+
// The elevation is the point: in light mode `--card` on `--background` is
|
|
195
|
+
// 1.04:1, so a panel this size was defined by a 1.32:1 hairline alone and
|
|
196
|
+
// read as a drawn rectangle rather than a raised thing.
|
|
197
|
+
//
|
|
198
|
+
// `rounded-2xl` is exactly the 18px this used to hardcode
|
|
199
|
+
// (`--radius` 0.625rem + 8px) and now re-scales with the token like every
|
|
200
|
+
// other surface. `overflow-hidden` clips descendants, never the element's
|
|
201
|
+
// own box-shadow, so the right column keeps its radius and the panel keeps
|
|
202
|
+
// its lift.
|
|
203
|
+
<Card
|
|
204
|
+
variant="elevatedPanel"
|
|
205
|
+
className={cn("grid grid-cols-1 overflow-hidden rounded-2xl md:grid-cols-2", className)}
|
|
206
|
+
>
|
|
207
|
+
<div className="p-5">
|
|
208
|
+
{venue.href ? (
|
|
209
|
+
<LinkComponent
|
|
210
|
+
href={venue.href}
|
|
211
|
+
className="block rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
212
|
+
>
|
|
213
|
+
{venueContent ?? venueHeader}
|
|
214
|
+
</LinkComponent>
|
|
215
|
+
) : (
|
|
216
|
+
venueContent ?? venueHeader
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
<div>
|
|
220
|
+
{nightsContent ??
|
|
221
|
+
nights.map((night) => {
|
|
222
|
+
const row = (
|
|
223
|
+
<>
|
|
224
|
+
<Separator className="absolute inset-x-0 top-0" />
|
|
225
|
+
|
|
226
|
+
<span className="w-11 shrink-0 font-mono text-xs text-muted-foreground">
|
|
227
|
+
{night.dayLabel}
|
|
228
|
+
</span>
|
|
229
|
+
{/* Event title, wrapped in full — same rule as every other
|
|
230
|
+
event surface. The day and time columns keep their fixed
|
|
231
|
+
widths, so a two-line title only makes the row taller. */}
|
|
232
|
+
<span className="min-w-0 flex-1 text-[13.5px] font-semibold leading-[1.35]">
|
|
233
|
+
{night.title}
|
|
234
|
+
</span>
|
|
235
|
+
<span className="shrink-0 font-mono text-xs text-muted-foreground">
|
|
236
|
+
{night.timeLabel}
|
|
237
|
+
</span>
|
|
238
|
+
</>
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const rowClasses = "relative flex items-center gap-[11px] py-[9px]";
|
|
242
|
+
|
|
243
|
+
return night.href ? (
|
|
244
|
+
<LinkComponent
|
|
245
|
+
key={night.id}
|
|
246
|
+
href={night.href}
|
|
247
|
+
className={cn(
|
|
248
|
+
rowClasses,
|
|
249
|
+
"cursor-pointer transition-colors duration-150 hover:text-primary motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md"
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
{row}
|
|
253
|
+
</LinkComponent>
|
|
254
|
+
) : (
|
|
255
|
+
<div key={night.id} className={rowClasses}>
|
|
256
|
+
{row}
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
})}
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{allEventsHref ? (
|
|
263
|
+
<LinkComponent
|
|
264
|
+
href={allEventsHref}
|
|
265
|
+
className="mt-3 inline-flex h-9 items-center rounded-full border border-border px-3 text-xs font-semibold text-foreground transition-colors duration-150 hover:bg-muted motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
266
|
+
>
|
|
267
|
+
{allEventsLabel} ›
|
|
268
|
+
</LinkComponent>
|
|
269
|
+
) : (
|
|
270
|
+
<div className="mt-3 inline-flex h-9 items-center rounded-full border border-border px-3 text-xs font-semibold text-foreground">
|
|
271
|
+
{allEventsLabel} ›
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div className="relative bg-muted/50 px-[18px] pb-5 pt-4">
|
|
277
|
+
<Separator className="absolute inset-x-0 top-0 md:hidden" />
|
|
278
|
+
<Separator orientation="vertical" className="absolute inset-y-0 left-0 hidden md:block" />
|
|
279
|
+
|
|
280
|
+
{menuHeader}
|
|
281
|
+
|
|
282
|
+
{menuContent ??
|
|
283
|
+
dishes.map((dish) => (
|
|
284
|
+
<div
|
|
285
|
+
key={dish.id}
|
|
286
|
+
className="relative flex items-start gap-3 py-3 first:[&>[data-slot=separator]]:hidden"
|
|
287
|
+
>
|
|
288
|
+
<Separator className="absolute inset-x-0 top-0" />
|
|
289
|
+
|
|
290
|
+
<div className="min-w-0 flex-1">
|
|
291
|
+
<div className="text-[15px] font-medium leading-[1.3]">{dish.name}</div>
|
|
292
|
+
{dish.description && (
|
|
293
|
+
<div className="mt-[3px] line-clamp-1 text-xs text-muted-foreground">
|
|
294
|
+
{dish.description}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
<div className="mt-1.5 flex items-center gap-2 text-[11px] tabular-nums text-muted-foreground">
|
|
298
|
+
{dish.quantityLabel && <span>{dish.quantityLabel}</span>}
|
|
299
|
+
{dish.mentionsLabel && (
|
|
300
|
+
<span className="ml-auto inline-flex items-center gap-1 text-[11.5px] text-muted-foreground">
|
|
301
|
+
<Quote className="h-3 w-3" aria-hidden />
|
|
302
|
+
{dish.mentionsLabel}
|
|
303
|
+
</span>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
<span className="shrink-0 text-sm font-bold text-primary">{dish.priceLabel}</span>
|
|
308
|
+
</div>
|
|
309
|
+
))}
|
|
310
|
+
|
|
311
|
+
{footnote && <div className="mt-3 text-xs text-muted-foreground">{footnote}</div>}
|
|
312
|
+
</div>
|
|
313
|
+
</Card>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export type VenueEventsBlockSkeletonProps = {
|
|
318
|
+
/**
|
|
319
|
+
* Mirrors `VenueEventsBlockProps.venueContent`: a caller rendering its own
|
|
320
|
+
* place card through that slot should pass its own card-shaped skeleton
|
|
321
|
+
* here too, so the loading state matches what actually mounts instead of
|
|
322
|
+
* the block's built-in thumbnail-and-two-lines placeholder.
|
|
323
|
+
*/
|
|
324
|
+
venueContent?: React.ReactNode;
|
|
325
|
+
/**
|
|
326
|
+
* Mirrors `VenueEventsBlockProps.nightsContent`: a caller rendering its own
|
|
327
|
+
* row component through that slot should pass its own row-shaped
|
|
328
|
+
* skeletons here too, so the loading state matches what actually mounts
|
|
329
|
+
* instead of three generic placeholder rows.
|
|
330
|
+
*/
|
|
331
|
+
nightsContent?: React.ReactNode;
|
|
332
|
+
/**
|
|
333
|
+
* Mirrors `VenueEventsBlockProps.menuContent`: a caller rendering its own
|
|
334
|
+
* row component through that slot should pass its own row-shaped skeleton
|
|
335
|
+
* here too, so the loading state matches what actually mounts instead of
|
|
336
|
+
* three generic placeholder rows.
|
|
337
|
+
*/
|
|
338
|
+
menuContent?: React.ReactNode;
|
|
339
|
+
className?: string;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export function VenueEventsBlockSkeleton({
|
|
343
|
+
venueContent,
|
|
344
|
+
nightsContent,
|
|
345
|
+
menuContent,
|
|
346
|
+
className,
|
|
347
|
+
}: VenueEventsBlockSkeletonProps) {
|
|
348
|
+
return (
|
|
349
|
+
// Identical root to the component above, deliberately — the two drifted
|
|
350
|
+
// once already by both hand-rolling it, so they now share the variant.
|
|
351
|
+
<Card
|
|
352
|
+
aria-hidden="true"
|
|
353
|
+
variant="elevatedPanel"
|
|
354
|
+
className={cn("grid grid-cols-1 overflow-hidden rounded-2xl md:grid-cols-2", className)}
|
|
355
|
+
>
|
|
356
|
+
<div className="p-5">
|
|
357
|
+
{venueContent ?? (
|
|
358
|
+
<div className="flex items-center gap-3 rounded-xl border border-border bg-card p-3">
|
|
359
|
+
<Skeleton className="h-16 w-16 shrink-0 rounded-lg" />
|
|
360
|
+
<div className="min-w-0 flex-1">
|
|
361
|
+
<Skeleton className="h-4 w-2/5" />
|
|
362
|
+
<Skeleton className="mt-1.5 h-3 w-1/3" />
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
)}
|
|
366
|
+
|
|
367
|
+
<div>
|
|
368
|
+
{nightsContent ??
|
|
369
|
+
Array.from({ length: 3 }, (_, index) => (
|
|
370
|
+
<div key={index} className="relative flex items-center gap-[11px] py-[9px]">
|
|
371
|
+
<Separator className="absolute inset-x-0 top-0" />
|
|
372
|
+
|
|
373
|
+
<Skeleton className="h-3 w-11 shrink-0" />
|
|
374
|
+
<Skeleton className="h-[13.5px] flex-1" />
|
|
375
|
+
<Skeleton className="h-3 w-10 shrink-0" />
|
|
376
|
+
</div>
|
|
377
|
+
))}
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<Skeleton className="mt-3 h-9 w-40 rounded-full" />
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<div className="relative bg-muted/50 px-[18px] pb-5 pt-4">
|
|
384
|
+
<Separator className="absolute inset-x-0 top-0 md:hidden" />
|
|
385
|
+
<Separator orientation="vertical" className="absolute inset-y-0 left-0 hidden md:block" />
|
|
386
|
+
|
|
387
|
+
<Skeleton className="mb-2 h-4 w-32" />
|
|
388
|
+
|
|
389
|
+
{menuContent ??
|
|
390
|
+
Array.from({ length: 3 }, (_, index) => (
|
|
391
|
+
<div
|
|
392
|
+
key={index}
|
|
393
|
+
className="relative flex items-start gap-3 py-3 first:[&>[data-slot=separator]]:hidden"
|
|
394
|
+
>
|
|
395
|
+
<Separator className="absolute inset-x-0 top-0" />
|
|
396
|
+
|
|
397
|
+
<div className="min-w-0 flex-1">
|
|
398
|
+
<Skeleton className="h-[15px] w-3/5" />
|
|
399
|
+
<Skeleton className="mt-[3px] h-3 w-4/5" />
|
|
400
|
+
<Skeleton className="mt-1.5 h-3 w-1/4" />
|
|
401
|
+
</div>
|
|
402
|
+
<Skeleton className="h-4 w-12 shrink-0" />
|
|
403
|
+
</div>
|
|
404
|
+
))}
|
|
405
|
+
</div>
|
|
406
|
+
</Card>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { WeekendPanel, WeekendPanelSkeleton } from "./weekend-panel";
|
|
3
|
+
|
|
4
|
+
function fakeCard(label: string) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex flex-col gap-1.5">
|
|
7
|
+
<div className="aspect-[4/3] w-full rounded-xl bg-muted" />
|
|
8
|
+
<div className="text-sm font-semibold">{label}</div>
|
|
9
|
+
<div className="text-xs text-muted-foreground">Atelier Café · 20:00</div>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Explicitly annotated rather than `satisfies`: the meta-level decorator makes
|
|
15
|
+
// the inferred type reference storybook's internal CSF module, which tsc cannot
|
|
16
|
+
// name portably (TS2742).
|
|
17
|
+
const meta: Meta<typeof WeekendPanel> = {
|
|
18
|
+
title: "Events/WeekendPanel",
|
|
19
|
+
component: WeekendPanel,
|
|
20
|
+
tags: ["autodocs"],
|
|
21
|
+
args: {
|
|
22
|
+
dayLabel: "Friday",
|
|
23
|
+
countLabel: "23 events",
|
|
24
|
+
actionLabel: "See all",
|
|
25
|
+
actionHref: "/events?day=friday",
|
|
26
|
+
labels: { pause: "Pause carousel", play: "Play carousel" },
|
|
27
|
+
children: (
|
|
28
|
+
<>
|
|
29
|
+
{fakeCard("Psihotrop — Infinity")}
|
|
30
|
+
{fakeCard("Warehouse Sessions")}
|
|
31
|
+
{fakeCard("Jazz at the Attic")}
|
|
32
|
+
{fakeCard("Open Mic Night")}
|
|
33
|
+
</>
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
decorators: [
|
|
37
|
+
(Story) => (
|
|
38
|
+
<div className="w-[380px]">
|
|
39
|
+
<Story />
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
parameters: {
|
|
44
|
+
docs: {
|
|
45
|
+
description: {
|
|
46
|
+
component:
|
|
47
|
+
"One day panel from the weekend section. Built for exactly four children (two pages of two) — the slide keyframes measure `translateX(-100% - gap)`, i.e. exactly one page. Auto-advances on a loop paused by hover, focus, and off-screen, and by a visible pause/play toggle next to the progress dots — the actual WCAG 2.2.2 mechanism, since hover/focus aren't reachable by every user. Falls back to a static two-column grid (control omitted) under `prefers-reduced-motion`.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default meta;
|
|
54
|
+
type Story = StoryObj<typeof WeekendPanel>;
|
|
55
|
+
|
|
56
|
+
export const Default: Story = {};
|
|
57
|
+
|
|
58
|
+
/** The bordered, primary-tinted panel — the design's "today" or "this weekend" highlight. */
|
|
59
|
+
export const Highlighted: Story = {
|
|
60
|
+
args: { isHighlighted: true },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Sibling panels stagger their loop so three side-by-side panels don't slide in lockstep. */
|
|
64
|
+
export const SlowerLoop: Story = {
|
|
65
|
+
args: { durationSeconds: 10 },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The WCAG 2.2.2 mechanism: click the pause/play toggle next to the dots.
|
|
70
|
+
* Manual pause is a latch — it stays paused until pressed again, independent
|
|
71
|
+
* of hover/focus, and stops the track and both dot animations together.
|
|
72
|
+
*/
|
|
73
|
+
export const PauseControl: Story = {
|
|
74
|
+
args: { durationSeconds: 3 },
|
|
75
|
+
parameters: {
|
|
76
|
+
docs: {
|
|
77
|
+
description: {
|
|
78
|
+
story:
|
|
79
|
+
"Toggle the button next to the progress dots to pause/resume the carousel. Unlike hover-pause, this state persists while reading — the required manual mechanism for WCAG 2.2.2.",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** The panel at rest — the two visible cards of the two-up carousel, no slide. */
|
|
86
|
+
export const Skeleton: Story = {
|
|
87
|
+
render: () => (
|
|
88
|
+
<div className="w-[380px]">
|
|
89
|
+
<WeekendPanelSkeleton />
|
|
90
|
+
</div>
|
|
91
|
+
),
|
|
92
|
+
};
|