@bearmenu/ui 0.8.7 → 0.8.9
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-637L7XQ3.js → chunk-3TNPSN4D.js} +28 -9
- package/dist/chunk-62ES6N32.js +27 -0
- package/dist/chunk-AHL45DD2.js +145 -0
- package/dist/chunk-AXV5O2IZ.js +58 -0
- package/dist/chunk-IYDH5IU6.js +66 -0
- package/dist/chunk-OPVKL4VS.js +18 -0
- package/dist/chunk-Q6QH52LJ.js +38 -0
- package/dist/chunk-TH6W2PWG.js +213 -0
- package/dist/chunk-THOVPZNN.js +37 -0
- package/dist/chunk-YS6BHKZO.js +92 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/category-deck.js +10 -1
- package/dist/components/command.d.ts +7 -7
- package/dist/components/concept-compare.d.ts +73 -0
- package/dist/components/concept-compare.js +162 -0
- package/dist/components/coverflow.d.ts +11 -3
- package/dist/components/coverflow.js +34 -51
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/drift-rail.d.ts +24 -10
- package/dist/components/drift-rail.js +1 -1
- package/dist/components/event-card.d.ts +8 -0
- package/dist/components/event-card.js +64 -34
- package/dist/components/event-compact-row.d.ts +33 -6
- package/dist/components/event-compact-row.js +120 -39
- package/dist/components/event-day-header.js +3 -1
- package/dist/components/event-day-separator.d.ts +7 -1
- package/dist/components/event-day-separator.js +3 -2
- package/dist/components/event-detail-panel.d.ts +171 -25
- package/dist/components/event-detail-panel.js +471 -174
- package/dist/components/event-hub-hero.d.ts +25 -4
- package/dist/components/event-hub-hero.js +89 -19
- package/dist/components/event-list-row-wide.js +1 -1
- package/dist/components/event-list-row.d.ts +19 -6
- package/dist/components/event-list-row.js +79 -35
- package/dist/components/hour-band.d.ts +38 -40
- package/dist/components/hour-band.js +55 -27
- package/dist/components/hour-group.js +1 -1
- package/dist/components/hub-surface.d.ts +27 -0
- package/dist/components/hub-surface.js +20 -0
- package/dist/components/menu-family-card.d.ts +59 -0
- package/dist/components/menu-family-card.js +121 -0
- package/dist/components/menu-field.d.ts +57 -0
- package/dist/components/menu-field.js +3 -0
- package/dist/components/menu-hub-hero.d.ts +65 -0
- package/dist/components/menu-hub-hero.js +103 -0
- package/dist/components/menu-intent-tiles.d.ts +31 -0
- package/dist/components/menu-intent-tiles.js +50 -0
- package/dist/components/menu-item.d.ts +127 -0
- package/dist/components/menu-item.js +5 -0
- package/dist/components/menu-mocks.d.ts +55 -0
- package/dist/components/menu-mocks.js +379 -0
- package/dist/components/menu-types.d.ts +245 -0
- package/dist/components/menu-types.js +1 -0
- package/dist/components/motion-icon-set.js +15 -3
- package/dist/components/open-now-list.d.ts +40 -0
- package/dist/components/open-now-list.js +69 -0
- package/dist/components/place-types.d.ts +112 -1
- package/dist/components/poster-tape.js +1 -1
- package/dist/components/poster-wall.d.ts +3 -3
- package/dist/components/poster-wall.js +73 -17
- package/dist/components/ranked-venue-list.d.ts +49 -0
- package/dist/components/ranked-venue-list.js +87 -0
- package/dist/components/section-header.d.ts +16 -1
- package/dist/components/section-header.js +26 -7
- package/dist/components/signature-chip.d.ts +37 -0
- package/dist/components/signature-chip.js +4 -0
- package/dist/components/signature-line-list.d.ts +45 -0
- package/dist/components/signature-line-list.js +99 -0
- package/dist/components/snap-rail.d.ts +54 -0
- package/dist/components/snap-rail.js +109 -0
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/stat-banner.d.ts +47 -0
- package/dist/components/stat-banner.js +61 -0
- package/dist/components/text.d.ts +3 -3
- package/dist/components/timeline-band.d.ts +20 -2
- package/dist/components/timeline-band.js +1 -1
- package/dist/components/timeline-playhead.d.ts +26 -0
- package/dist/components/timeline-playhead.js +25 -0
- package/dist/components/venue-busyness-strip.d.ts +35 -0
- package/dist/components/venue-busyness-strip.js +37 -0
- package/dist/components/venue-grade-dimensions.d.ts +36 -0
- package/dist/components/venue-grade-dimensions.js +3 -0
- package/dist/components/venue-quotes.d.ts +32 -0
- package/dist/components/venue-quotes.js +3 -0
- package/dist/components/venue-signature-block.d.ts +39 -0
- package/dist/components/venue-signature-block.js +6 -0
- package/dist/components/venue-spotlight.d.ts +74 -0
- package/dist/components/venue-spotlight.js +138 -0
- package/dist/components/venue-wall.d.ts +41 -0
- package/dist/components/venue-wall.js +118 -0
- package/dist/components/weekday-theme-banner.d.ts +70 -0
- package/dist/components/weekday-theme-banner.js +127 -0
- package/dist/components/weekend-panel.d.ts +5 -4
- package/dist/components/weekend-panel.js +85 -71
- package/dist/hooks/use-drag-swipe.d.ts +44 -0
- package/dist/hooks/use-drag-swipe.js +2 -0
- package/dist/hooks/use-media-query.d.ts +17 -0
- package/dist/hooks/use-media-query.js +2 -0
- package/package.json +1 -1
- package/src/components/category-deck.tsx +19 -8
- package/src/components/colour-dictionary.stories.tsx +227 -0
- package/src/components/concept-compare.stories.tsx +104 -0
- package/src/components/concept-compare.tsx +276 -0
- package/src/components/coverflow.test.tsx +28 -1
- package/src/components/coverflow.tsx +37 -43
- package/src/components/drift-rail.tsx +167 -42
- package/src/components/event-card.tsx +55 -10
- package/src/components/event-compact-row.stories.tsx +30 -0
- package/src/components/event-compact-row.test.tsx +49 -0
- package/src/components/event-compact-row.tsx +162 -38
- package/src/components/event-day-header.tsx +3 -1
- package/src/components/event-day-separator.tsx +8 -1
- package/src/components/event-detail-panel.stories.tsx +205 -97
- package/src/components/event-detail-panel.test.tsx +181 -0
- package/src/components/event-detail-panel.tsx +703 -185
- package/src/components/event-hub-hero.stories.tsx +30 -0
- package/src/components/event-hub-hero.tsx +102 -30
- package/src/components/event-list-row.stories.tsx +34 -0
- package/src/components/event-list-row.test.tsx +53 -0
- package/src/components/event-list-row.tsx +135 -87
- package/src/components/hour-band.stories.tsx +28 -5
- package/src/components/hour-band.tsx +90 -70
- package/src/components/hub-surface.stories.tsx +68 -0
- package/src/components/hub-surface.tsx +40 -0
- package/src/components/menu-family-card.stories.tsx +112 -0
- package/src/components/menu-family-card.tsx +211 -0
- package/src/components/menu-field.stories.tsx +74 -0
- package/src/components/menu-field.tsx +199 -0
- package/src/components/menu-hub-hero.stories.tsx +92 -0
- package/src/components/menu-hub-hero.tsx +187 -0
- package/src/components/menu-intent-tiles.stories.tsx +54 -0
- package/src/components/menu-intent-tiles.tsx +76 -0
- package/src/components/menu-item.tsx +416 -0
- package/src/components/menu-mocks.ts +417 -0
- package/src/components/menu-types.ts +259 -0
- package/src/components/motion-icon-set.tsx +30 -3
- package/src/components/open-now-list.stories.tsx +76 -0
- package/src/components/open-now-list.tsx +127 -0
- package/src/components/place-types.ts +112 -0
- package/src/components/poster-wall.test.tsx +28 -0
- package/src/components/poster-wall.tsx +96 -23
- package/src/components/ranked-venue-list.stories.tsx +80 -0
- package/src/components/ranked-venue-list.tsx +177 -0
- package/src/components/section-header.stories.tsx +24 -0
- package/src/components/section-header.test.tsx +28 -0
- package/src/components/section-header.tsx +45 -7
- package/src/components/signature-chip.tsx +77 -0
- package/src/components/signature-line-list.stories.tsx +85 -0
- package/src/components/signature-line-list.tsx +136 -0
- package/src/components/snap-rail.stories.tsx +79 -0
- package/src/components/snap-rail.test.tsx +18 -0
- package/src/components/snap-rail.tsx +148 -0
- package/src/components/stat-banner.stories.tsx +100 -0
- package/src/components/stat-banner.tsx +117 -0
- package/src/components/timeline-band.tsx +52 -12
- package/src/components/timeline-playhead.stories.tsx +30 -0
- package/src/components/timeline-playhead.test.tsx +11 -0
- package/src/components/timeline-playhead.tsx +46 -0
- package/src/components/venue-busyness-strip.stories.tsx +66 -0
- package/src/components/venue-busyness-strip.tsx +64 -0
- package/src/components/venue-grade-dimensions.stories.tsx +83 -0
- package/src/components/venue-grade-dimensions.tsx +91 -0
- package/src/components/venue-quotes.stories.tsx +59 -0
- package/src/components/venue-quotes.tsx +64 -0
- package/src/components/venue-signature-block.stories.tsx +68 -0
- package/src/components/venue-signature-block.tsx +71 -0
- package/src/components/venue-spotlight.stories.tsx +141 -0
- package/src/components/venue-spotlight.tsx +236 -0
- package/src/components/venue-wall.stories.tsx +78 -0
- package/src/components/venue-wall.tsx +185 -0
- package/src/components/weekday-theme-banner.stories.tsx +113 -0
- package/src/components/weekday-theme-banner.tsx +231 -0
- package/src/components/weekend-panel.test.tsx +43 -0
- package/src/components/weekend-panel.tsx +142 -108
- package/src/hooks/use-drag-swipe.test.tsx +66 -0
- package/src/hooks/use-drag-swipe.ts +105 -0
- package/src/hooks/use-media-query.ts +31 -0
- package/dist/chunk-GPH6L4DX.js +0 -55
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { HourBand, HourBandSkeleton } from "./hour-band";
|
|
3
3
|
import { EventCard } from "./event-card";
|
|
4
|
+
import { EventCompactRow } from "./event-compact-row";
|
|
4
5
|
import type { EventCardLabels, EventFormatters, EventPreview } from "./event-types";
|
|
5
6
|
|
|
6
7
|
const labels: EventCardLabels = { free: "Free", liveNow: "Live now" };
|
|
@@ -35,7 +36,7 @@ const meta: Meta<typeof HourBand> = {
|
|
|
35
36
|
docs: {
|
|
36
37
|
description: {
|
|
37
38
|
component:
|
|
38
|
-
"One hour of the timeline: a
|
|
39
|
+
"One hour of the mobile timeline: a 58px gutter with a filled hour chip and a hairline dropping from beneath it, and that hour's rows beside it, ruled between. The rows carry no timestamp of their own — the band is the timestamp, which is why cards render with `hideWhen` and compact rows with `hideTime`.",
|
|
39
40
|
},
|
|
40
41
|
},
|
|
41
42
|
},
|
|
@@ -66,7 +67,7 @@ export const Default: Story = {
|
|
|
66
67
|
},
|
|
67
68
|
};
|
|
68
69
|
|
|
69
|
-
/** The live hour:
|
|
70
|
+
/** The live hour: tinted chip and hairline. The live label is screen-reader only in this variant. */
|
|
70
71
|
export const Live: Story = {
|
|
71
72
|
args: {
|
|
72
73
|
time: "20:00",
|
|
@@ -84,6 +85,28 @@ export const Live: Story = {
|
|
|
84
85
|
},
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
/** The shipped mobile shape: compact rows, ruled between, inside one band. */
|
|
89
|
+
export const CompactRows: Story = {
|
|
90
|
+
render: () => (
|
|
91
|
+
<HourBand time="20:00" isLive liveLabel="Now on">
|
|
92
|
+
<EventCompactRow
|
|
93
|
+
event={event("r1", "Klausen Quiz by GOtrivia", "Klausen Pubhouse", 20)}
|
|
94
|
+
labels={labels}
|
|
95
|
+
formatters={formatters}
|
|
96
|
+
hideTime
|
|
97
|
+
tags={["Drinks", "Games"]}
|
|
98
|
+
/>
|
|
99
|
+
<EventCompactRow
|
|
100
|
+
event={event("r2", "Quiz Night at Café del Mars", "Del Mars", 10)}
|
|
101
|
+
labels={labels}
|
|
102
|
+
formatters={formatters}
|
|
103
|
+
hideTime
|
|
104
|
+
tags={["Games"]}
|
|
105
|
+
/>
|
|
106
|
+
</HourBand>
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
|
|
87
110
|
/** A single event in a quiet hour. */
|
|
88
111
|
export const OneEvent: Story = {
|
|
89
112
|
args: {
|
|
@@ -133,9 +156,9 @@ export const StackedHours: Story = {
|
|
|
133
156
|
};
|
|
134
157
|
|
|
135
158
|
/**
|
|
136
|
-
* Loading. The
|
|
137
|
-
*
|
|
138
|
-
*
|
|
159
|
+
* Loading. The chip is blocked out at its fixed 58x24 size — a grey lozenge
|
|
160
|
+
* of that exact shape reads as "a time goes here" without inventing one — and
|
|
161
|
+
* the rows mirror `EventCompactRowSkeleton`.
|
|
139
162
|
*/
|
|
140
163
|
export const Loading: Story = {
|
|
141
164
|
render: () => <HourBandSkeleton />,
|
|
@@ -4,41 +4,39 @@ import * as React from "react";
|
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
5
5
|
import { Separator } from "./separator";
|
|
6
6
|
import { Skeleton } from "./skeleton";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
TIMELINE_MOBILE_GUTTER_CLASS,
|
|
9
|
+
TimelineHourPuck,
|
|
10
|
+
} from "./timeline-band";
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* HourBand — one hour on the timeline's axis, with that hour's events beside it.
|
|
11
14
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* timestamp, which is why `EventCard` has
|
|
15
|
+
* The mobile timeline is an app list, not a page: a 58px gutter carrying a
|
|
16
|
+
* filled hour chip with a hairline dropping from beneath it, and the hour's
|
|
17
|
+
* rows to the right, separated by hairlines of their own. The rows carry no
|
|
18
|
+
* timestamp — this band IS the timestamp, which is why `EventCard` has
|
|
19
|
+
* `hideWhen` and `EventCompactRow` has `hideTime`.
|
|
16
20
|
*
|
|
17
|
-
* The
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* Sharing the object changes this gutter's pixels: the mobile puck now carries
|
|
23
|
-
* a border, which is what makes it read as clipped onto the rail rather than
|
|
24
|
-
* text floating over it, and matches the station on the desktop spine.
|
|
21
|
+
* The chip is the shared `TimelineHourPuck` in its `chip` variant, so the
|
|
22
|
+
* desktop spine's station and this gutter's label are one object with two
|
|
23
|
+
* looks rather than two objects that drift. The hairline starts BELOW the chip
|
|
24
|
+
* rather than running behind it: a chip is a label at the top of its hour, a
|
|
25
|
+
* station is a tick on a continuous rail, and the two read differently.
|
|
25
26
|
*
|
|
26
|
-
* `isLive`
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* `isLive` tints the chip and the hairline with `--success`; the live label is
|
|
28
|
+
* kept for screen readers (the chip variant hides it visually). The playhead —
|
|
29
|
+
* the exact current time — is a separate `TimelinePlayhead` rendered between
|
|
30
|
+
* bands by the caller, because an hour spans sixty minutes and "now" is one of
|
|
31
|
+
* them.
|
|
29
32
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* with the rows beside it.
|
|
38
|
-
*
|
|
39
|
-
* 24px leaves ~12px of real clearance at the widest label. Do not tighten it
|
|
40
|
-
* back without re-deriving against a 12-hour locale: a 24-hour one ("09:00")
|
|
41
|
-
* is ~30px narrower and hides the problem completely.
|
|
33
|
+
* ROWS ARE SEPARATED HERE, NOT IN THE ROW. `EventCompactRow` deliberately
|
|
34
|
+
* draws no divider (its container does — see its docblock), so this band wraps
|
|
35
|
+
* each child and rules between them. The wrapper, not the child, carries the
|
|
36
|
+
* rule, so the row's own negative-margin hover wash is untouched. Children are
|
|
37
|
+
* read with `Children.toArray`, which drops `null`/`false` so a conditional
|
|
38
|
+
* first row never inherits a rule meant for a second; pass rows as siblings,
|
|
39
|
+
* not inside a fragment — a fragment is one child and gets one wrapper.
|
|
42
40
|
*/
|
|
43
41
|
|
|
44
42
|
export type HourBandProps = {
|
|
@@ -46,80 +44,102 @@ export type HourBandProps = {
|
|
|
46
44
|
time: string;
|
|
47
45
|
/** Marks this as the hour currently in progress. */
|
|
48
46
|
isLive?: boolean;
|
|
49
|
-
/**
|
|
47
|
+
/** Screen-reader text for the live hour, e.g. "Now on". Required to render it. */
|
|
50
48
|
liveLabel?: string;
|
|
51
|
-
/** The hour's events. */
|
|
49
|
+
/** The hour's events. Each direct child is one row or card. */
|
|
52
50
|
children: React.ReactNode;
|
|
51
|
+
/**
|
|
52
|
+
* `rows` (default): compact rows, ruled between with a Separator. `cards`:
|
|
53
|
+
* poster cards, stacked with a 16px gap and no rules — a card is its own
|
|
54
|
+
* frame, and a hairline between two posters reads as a third object.
|
|
55
|
+
*/
|
|
56
|
+
layout?: "rows" | "cards";
|
|
53
57
|
className?: string;
|
|
54
58
|
};
|
|
55
59
|
|
|
56
|
-
export function HourBand({
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
export function HourBand({
|
|
61
|
+
time,
|
|
62
|
+
isLive,
|
|
63
|
+
liveLabel,
|
|
64
|
+
children,
|
|
65
|
+
layout = "rows",
|
|
66
|
+
className,
|
|
67
|
+
}: HourBandProps) {
|
|
68
|
+
const isCards = layout === "cards";
|
|
59
69
|
return (
|
|
60
|
-
<div className={cn("flex gap-
|
|
61
|
-
<div className="relative
|
|
62
|
-
<Separator orientation="vertical" className="h-full" />
|
|
70
|
+
<div className={cn("flex gap-3 pt-2.5", className)}>
|
|
71
|
+
<div className={cn("relative shrink-0", TIMELINE_MOBILE_GUTTER_CLASS)}>
|
|
63
72
|
<TimelineHourPuck
|
|
64
73
|
time={time}
|
|
65
74
|
isLive={isLive}
|
|
66
75
|
liveLabel={liveLabel}
|
|
67
|
-
|
|
76
|
+
variant="chip"
|
|
77
|
+
/>
|
|
78
|
+
<Separator
|
|
79
|
+
orientation="vertical"
|
|
80
|
+
className={cn(
|
|
81
|
+
"absolute bottom-[-10px] left-1/2 top-[30px] h-auto -translate-x-1/2",
|
|
82
|
+
isLive && "bg-success/30",
|
|
83
|
+
)}
|
|
68
84
|
/>
|
|
69
85
|
</div>
|
|
70
86
|
|
|
71
87
|
<div
|
|
72
|
-
className={cn(
|
|
73
|
-
"flex min-w-0 flex-1 flex-col gap-5 pb-2",
|
|
74
|
-
showLiveLabel ? "pt-[34px]" : "pt-1"
|
|
75
|
-
)}
|
|
88
|
+
className={cn("flex min-w-0 flex-1 flex-col", isCards && "gap-4 pb-2")}
|
|
76
89
|
>
|
|
77
|
-
{children
|
|
90
|
+
{React.Children.toArray(children).map((child, index) => (
|
|
91
|
+
<div key={index}>
|
|
92
|
+
{!isCards && index > 0 && <Separator />}
|
|
93
|
+
{child}
|
|
94
|
+
</div>
|
|
95
|
+
))}
|
|
78
96
|
</div>
|
|
79
97
|
</div>
|
|
80
98
|
);
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
export type HourBandSkeletonProps = {
|
|
84
|
-
/**
|
|
102
|
+
/** Rows to reserve space for. */
|
|
85
103
|
cardCount?: number;
|
|
86
104
|
className?: string;
|
|
87
105
|
};
|
|
88
106
|
|
|
89
107
|
/**
|
|
90
|
-
* The mobile timeline's loading state, mirroring `HourBand` exactly — same
|
|
91
|
-
* gutter, same hairline, same
|
|
108
|
+
* The mobile timeline's loading state, mirroring `HourBand` exactly — same
|
|
109
|
+
* gutter, same hairline, same row rhythm — so nothing jumps when data lands.
|
|
92
110
|
*
|
|
93
|
-
* The
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* Card footprints come from the design's own loading frame: a 220px poster with
|
|
98
|
-
* an 16px title bar and a 12px meta bar, then a 170px poster with a 15px title.
|
|
111
|
+
* The chip is blocked out at its real size rather than omitted: it is a fixed
|
|
112
|
+
* 58x24 lozenge, so a grey one reads as "a time goes here" without inventing
|
|
113
|
+
* a time. Row footprints mirror `EventCompactRowSkeleton`.
|
|
99
114
|
*/
|
|
100
|
-
export function HourBandSkeleton({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
];
|
|
105
|
-
|
|
115
|
+
export function HourBandSkeleton({
|
|
116
|
+
cardCount = 2,
|
|
117
|
+
className,
|
|
118
|
+
}: HourBandSkeletonProps) {
|
|
106
119
|
return (
|
|
107
|
-
<div className={cn("flex gap-
|
|
108
|
-
<div className="relative
|
|
109
|
-
<
|
|
120
|
+
<div className={cn("flex gap-3 pt-2.5", className)} aria-hidden="true">
|
|
121
|
+
<div className={cn("relative shrink-0", TIMELINE_MOBILE_GUTTER_CLASS)}>
|
|
122
|
+
<Skeleton className="h-6 w-[58px] rounded-[7px]" />
|
|
123
|
+
<Separator
|
|
124
|
+
orientation="vertical"
|
|
125
|
+
className="absolute bottom-[-10px] left-1/2 top-[30px] h-auto -translate-x-1/2"
|
|
126
|
+
/>
|
|
110
127
|
</div>
|
|
111
128
|
|
|
112
|
-
<div className="flex min-w-0 flex-1 flex-col
|
|
113
|
-
{Array.from({ length: cardCount }).map((_, index) =>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<div
|
|
117
|
-
<Skeleton className="w-
|
|
118
|
-
<
|
|
119
|
-
|
|
129
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
130
|
+
{Array.from({ length: cardCount }).map((_, index) => (
|
|
131
|
+
<div key={index}>
|
|
132
|
+
{index > 0 && <Separator />}
|
|
133
|
+
<div className="flex gap-3 py-[11px]">
|
|
134
|
+
<Skeleton className="aspect-[4/5] w-16 shrink-0 rounded-[10px]" />
|
|
135
|
+
<div className="min-w-0 flex-1">
|
|
136
|
+
<Skeleton className="h-[14px] w-3/4" />
|
|
137
|
+
<Skeleton className="mt-1.5 h-[11.5px] w-1/2" />
|
|
138
|
+
<Skeleton className="mt-2 h-[22px] w-16 rounded-full" />
|
|
139
|
+
</div>
|
|
120
140
|
</div>
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
</div>
|
|
142
|
+
))}
|
|
123
143
|
</div>
|
|
124
144
|
</div>
|
|
125
145
|
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { HubSurface } from "./hub-surface";
|
|
3
|
+
import { SectionHeader } from "./section-header";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof HubSurface> = {
|
|
6
|
+
title: "Menu/HubSurface",
|
|
7
|
+
component: HubSurface,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
decorators: [
|
|
10
|
+
(Story) => (
|
|
11
|
+
<div className="w-[390px] bg-[var(--surface-2,oklch(0.97_0.004_60))] p-3">
|
|
12
|
+
<Story />
|
|
13
|
+
</div>
|
|
14
|
+
),
|
|
15
|
+
],
|
|
16
|
+
parameters: {
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
"The surface a hub section sits on below `lg`. On a phone a hub is an app screen, not a page: each section is a white card on the tinted ground, 18px radius, one shadow, no border. From `lg` up the card dissolves entirely and sections sit flat the way the desktop design draws them — one element, two looks, identical markup inside. Hairlines live inside a card, never across the page.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof HubSurface>;
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
render: () => (
|
|
31
|
+
<HubSurface>
|
|
32
|
+
<SectionHeader title="Open right now" />
|
|
33
|
+
<p className="text-sm text-muted-foreground">
|
|
34
|
+
Section content, inset by the surface.
|
|
35
|
+
</p>
|
|
36
|
+
</HubSurface>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** `flush` drops the inset so a rail or a photograph can bleed to the edge. */
|
|
41
|
+
export const Flush: Story = {
|
|
42
|
+
render: () => (
|
|
43
|
+
<HubSurface flush>
|
|
44
|
+
<div className="px-3.5 pt-4">
|
|
45
|
+
<SectionHeader title="Browse by cuisine" />
|
|
46
|
+
</div>
|
|
47
|
+
<div className="flex gap-3 overflow-x-auto px-3.5 pb-4">
|
|
48
|
+
{[0, 1, 2].map((card) => (
|
|
49
|
+
<div key={card} className="h-24 w-40 shrink-0 rounded-xl bg-muted" />
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
</HubSurface>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Stacked, at the 12px rhythm the hub uses between sections. */
|
|
57
|
+
export const Stacked: Story = {
|
|
58
|
+
render: () => (
|
|
59
|
+
<div className="flex flex-col gap-3">
|
|
60
|
+
{["Open right now", "One kitchen worth knowing", "Most named"].map((title) => (
|
|
61
|
+
<HubSurface key={title}>
|
|
62
|
+
<SectionHeader title={title} />
|
|
63
|
+
<div className="h-12 rounded-lg bg-muted" />
|
|
64
|
+
</HubSurface>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HubSurface — the surface a hub section sits on BELOW `lg`.
|
|
5
|
+
*
|
|
6
|
+
* On a phone a hub is an app screen, not a page: each section is a white card
|
|
7
|
+
* on the tinted ground, 18px radius, one card shadow and no border, with 14px
|
|
8
|
+
* of inset. Hairlines live inside a card, never across the page. From `lg` up
|
|
9
|
+
* the card dissolves — no fill, no radius, no inset — and the sections sit flat
|
|
10
|
+
* the way the desktop design draws them. One element, two looks; the section
|
|
11
|
+
* markup inside is identical at every width.
|
|
12
|
+
*
|
|
13
|
+
* `flush` drops the inset so a section whose content bleeds to the card edge —
|
|
14
|
+
* a snap rail, a full-width photograph — can pad its own header and let the
|
|
15
|
+
* rest run.
|
|
16
|
+
*
|
|
17
|
+
* Generalised out of the events hub's `EventsHubCard`, which had the same job
|
|
18
|
+
* and lived in the app. Nothing here is menu- or event-shaped.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type HubSurfaceProps = {
|
|
22
|
+
flush?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function HubSurface({ flush, className, children }: HubSurfaceProps) {
|
|
28
|
+
return (
|
|
29
|
+
<section
|
|
30
|
+
className={cn(
|
|
31
|
+
"rounded-[18px] border border-border bg-card shadow-none",
|
|
32
|
+
flush ? "overflow-hidden" : "px-3.5 py-4",
|
|
33
|
+
"lg:overflow-visible lg:rounded-none lg:bg-transparent lg:p-0 lg:shadow-none",
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</section>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { MenuFamilyCard } from "./menu-family-card";
|
|
3
|
+
import { mockDrinkFamily, mockFamily } from "./menu-mocks";
|
|
4
|
+
|
|
5
|
+
const itemLabels = {
|
|
6
|
+
currency: "lei",
|
|
7
|
+
mentions: (count: number) => `${count} mentions`,
|
|
8
|
+
signature: "SIGNATURE",
|
|
9
|
+
mentioned: "MENTIONED",
|
|
10
|
+
};
|
|
11
|
+
const formatPrice = (amount: number) => String(amount);
|
|
12
|
+
|
|
13
|
+
const labels = {
|
|
14
|
+
venueUnit: "KITCHENS",
|
|
15
|
+
mostNamed: "Most named",
|
|
16
|
+
priceBand: "kitchen price band",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const meta: Meta<typeof MenuFamilyCard> = {
|
|
20
|
+
title: "Menu/MenuFamilyCard",
|
|
21
|
+
component: MenuFamilyCard,
|
|
22
|
+
tags: ["autodocs"],
|
|
23
|
+
decorators: [
|
|
24
|
+
(Story) => (
|
|
25
|
+
<div className="w-[300px] bg-background p-4">
|
|
26
|
+
<Story />
|
|
27
|
+
</div>
|
|
28
|
+
),
|
|
29
|
+
],
|
|
30
|
+
parameters: {
|
|
31
|
+
docs: {
|
|
32
|
+
description: {
|
|
33
|
+
component:
|
|
34
|
+
"A browsable grouping of venues — a cuisine on the dishes tab, a drink family on the drinks tab. Two halves with two jobs: the dark panel is for scanning a row of four (one number, one name, the groups the family consists of, over a mosaic of its venues), and the white half underneath answers what you would actually order, as the production `MenuItem`. Both halves count VENUES: cuisine exists only on places, so a card counting dishes cannot be built.",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
type Story = StoryObj<typeof MenuFamilyCard>;
|
|
42
|
+
|
|
43
|
+
export const Default: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
family: mockFamily,
|
|
46
|
+
labels,
|
|
47
|
+
itemLabels,
|
|
48
|
+
formatPrice,
|
|
49
|
+
href: "/top/italian",
|
|
50
|
+
hue: "oklch(0.55 0.15 40)",
|
|
51
|
+
priceBandLabel: "32–84 lei",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** The drinks tab. Same card, different unit and vocabulary. */
|
|
56
|
+
export const DrinkFamily: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
family: mockDrinkFamily,
|
|
59
|
+
labels: { venueUnit: "BARS", mostNamed: "Most named", priceBand: "bar price band" },
|
|
60
|
+
itemLabels,
|
|
61
|
+
formatPrice,
|
|
62
|
+
href: "/top/baruri",
|
|
63
|
+
hue: "oklch(0.5 0.13 240)",
|
|
64
|
+
priceBandLabel: "18–54 lei",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** A one-venue cuisine: the count is honest, and the mosaic repeats one photo. */
|
|
69
|
+
export const Sparse: Story = {
|
|
70
|
+
args: {
|
|
71
|
+
family: {
|
|
72
|
+
slug: "portuguese",
|
|
73
|
+
name: "Portuguese",
|
|
74
|
+
venue_count: 1,
|
|
75
|
+
chips: [{ slug: "seafood-dish", label: "seafood" }],
|
|
76
|
+
signature: mockFamily.signature,
|
|
77
|
+
},
|
|
78
|
+
labels,
|
|
79
|
+
itemLabels,
|
|
80
|
+
formatPrice,
|
|
81
|
+
href: "/top/restaurante",
|
|
82
|
+
hue: "oklch(0.52 0.13 340)",
|
|
83
|
+
priceBandLabel: "42–88 lei",
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** The row of four the card is designed to be scanned in. */
|
|
88
|
+
export const Row: Story = {
|
|
89
|
+
render: () => (
|
|
90
|
+
<div className="grid grid-cols-4 gap-5">
|
|
91
|
+
{[mockFamily, mockDrinkFamily, mockFamily, mockDrinkFamily].map((family, index) => (
|
|
92
|
+
<MenuFamilyCard
|
|
93
|
+
key={index}
|
|
94
|
+
family={family}
|
|
95
|
+
labels={labels}
|
|
96
|
+
itemLabels={itemLabels}
|
|
97
|
+
formatPrice={formatPrice}
|
|
98
|
+
href={`/top/${family.slug}`}
|
|
99
|
+
hue="oklch(0.55 0.15 40)"
|
|
100
|
+
priceBandLabel="32–84 lei"
|
|
101
|
+
/>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
),
|
|
105
|
+
decorators: [
|
|
106
|
+
(Story) => (
|
|
107
|
+
<div className="w-[1200px] bg-background p-4">
|
|
108
|
+
<Story />
|
|
109
|
+
</div>
|
|
110
|
+
),
|
|
111
|
+
],
|
|
112
|
+
};
|