@bearmenu/ui 0.8.8 → 0.8.10
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-24RFI4RQ.js +167 -0
- package/dist/chunk-2XCZJWTV.js +121 -0
- package/dist/chunk-5RWTOT5Y.js +72 -0
- package/dist/chunk-62ES6N32.js +27 -0
- package/dist/chunk-AKQ57NXY.js +58 -0
- package/dist/chunk-CLLY6XB3.js +41 -0
- package/dist/chunk-DTXNDLSE.js +125 -0
- package/dist/chunk-F6FMZJDG.js +113 -0
- package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
- package/dist/chunk-MZL7CGCL.js +109 -0
- package/dist/chunk-Q6QH52LJ.js +38 -0
- package/dist/chunk-SY6RV476.js +48 -0
- package/dist/chunk-TH6W2PWG.js +213 -0
- package/dist/chunk-UDN7UHJW.js +60 -0
- package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/concept-compare-rail.d.ts +50 -0
- package/dist/components/concept-compare-rail.js +5 -0
- package/dist/components/concept-compare.d.ts +110 -0
- package/dist/components/concept-compare.js +238 -0
- package/dist/components/concept-convergence.d.ts +41 -0
- package/dist/components/concept-convergence.js +53 -0
- package/dist/components/convergence.d.ts +25 -0
- package/dist/components/convergence.js +4 -0
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.d.ts +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.d.ts +171 -25
- package/dist/components/event-detail-panel.js +472 -175
- package/dist/components/event-index-row.js +1 -1
- package/dist/components/event-list-row-wide.js +2 -2
- package/dist/components/event-list-row.js +2 -2
- package/dist/components/explainer-steps.d.ts +34 -0
- package/dist/components/explainer-steps.js +36 -0
- package/dist/components/glass-ground.d.ts +66 -0
- package/dist/components/glass-ground.js +3 -0
- package/dist/components/hub-surface.d.ts +27 -0
- package/dist/components/hub-surface.js +30 -0
- package/dist/components/hue-chip.d.ts +70 -0
- package/dist/components/hue-chip.js +3 -0
- package/dist/components/kitchen-dossier.d.ts +88 -0
- package/dist/components/kitchen-dossier.js +187 -0
- package/dist/components/menu-dead-end.d.ts +56 -0
- package/dist/components/menu-dead-end.js +49 -0
- package/dist/components/menu-family-block.d.ts +99 -0
- package/dist/components/menu-family-block.js +155 -0
- package/dist/components/menu-family-card.d.ts +65 -0
- package/dist/components/menu-family-card.js +79 -0
- package/dist/components/menu-field.d.ts +57 -0
- package/dist/components/menu-field.js +93 -0
- package/dist/components/menu-filter-chips.d.ts +86 -0
- package/dist/components/menu-filter-chips.js +122 -0
- package/dist/components/menu-group-rows.d.ts +53 -0
- package/dist/components/menu-group-rows.js +67 -0
- package/dist/components/menu-hub-hero.d.ts +65 -0
- package/dist/components/menu-hub-hero.js +103 -0
- package/dist/components/menu-index-header.d.ts +37 -0
- package/dist/components/menu-index-header.js +68 -0
- package/dist/components/menu-index-list.d.ts +48 -0
- package/dist/components/menu-index-list.js +96 -0
- package/dist/components/menu-intent-hero.d.ts +69 -0
- package/dist/components/menu-intent-hero.js +87 -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-kitchen-list.d.ts +82 -0
- package/dist/components/menu-kitchen-list.js +5 -0
- package/dist/components/menu-mocks.d.ts +90 -0
- package/dist/components/menu-mocks.js +504 -0
- package/dist/components/menu-types.d.ts +421 -0
- package/dist/components/menu-types.js +1 -0
- package/dist/components/motion-icon-controls.js +2 -2
- package/dist/components/motion-icon-set.js +21 -9
- package/dist/components/motion-icon.d.ts +22 -1
- package/dist/components/motion-icon.js +1 -1
- package/dist/components/open-now-list.d.ts +40 -0
- package/dist/components/open-now-list.js +69 -0
- package/dist/components/photo-ground.d.ts +64 -0
- package/dist/components/photo-ground.js +3 -0
- package/dist/components/place-about-tab.js +2 -2
- package/dist/components/place-details-mobile.js +2 -2
- package/dist/components/place-types.d.ts +112 -1
- package/dist/components/qa-block.d.ts +36 -0
- package/dist/components/qa-block.js +24 -0
- package/dist/components/scroll-fade.js +3 -121
- package/dist/components/signature-chip.d.ts +37 -0
- package/dist/components/signature-chip.js +4 -0
- package/dist/components/snap-rail.d.ts +48 -5
- package/dist/components/snap-rail.js +3 -51
- package/dist/components/stat-banner.d.ts +47 -0
- package/dist/components/stat-banner.js +61 -0
- package/dist/components/story.d.ts +34 -0
- package/dist/components/story.js +3 -0
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +1 -1
- package/dist/components/top-comparison.d.ts +28 -0
- package/dist/components/top-comparison.js +76 -0
- package/dist/components/top-cross-link.d.ts +34 -0
- package/dist/components/top-cross-link.js +101 -0
- package/dist/components/top-ranking-entry.d.ts +53 -0
- package/dist/components/top-ranking-entry.js +217 -0
- package/dist/components/top-ranking-hero.d.ts +61 -0
- package/dist/components/top-ranking-hero.js +99 -0
- package/dist/components/top-ranking-legend.d.ts +69 -0
- package/dist/components/top-ranking-legend.js +124 -0
- package/dist/components/top-types.d.ts +109 -0
- package/dist/components/top-types.js +1 -0
- package/dist/components/venue-grade-dimensions.d.ts +38 -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-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.js +2 -2
- package/package.json +1 -1
- package/src/components/colour-dictionary.stories.tsx +227 -0
- package/src/components/concept-compare-rail.tsx +131 -0
- package/src/components/concept-compare.stories.tsx +172 -0
- package/src/components/concept-compare.tsx +443 -0
- package/src/components/concept-convergence.stories.tsx +53 -0
- package/src/components/concept-convergence.tsx +90 -0
- package/src/components/convergence.stories.tsx +54 -0
- package/src/components/convergence.tsx +177 -0
- package/src/components/event-compact-row.tsx +1 -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/explainer-steps.stories.tsx +46 -0
- package/src/components/explainer-steps.tsx +74 -0
- package/src/components/glass-ground.tsx +129 -0
- package/src/components/hub-surface.stories.tsx +68 -0
- package/src/components/hub-surface.tsx +50 -0
- package/src/components/hue-chip.stories.tsx +76 -0
- package/src/components/hue-chip.test.tsx +62 -0
- package/src/components/hue-chip.tsx +99 -0
- package/src/components/kitchen-dossier.stories.tsx +108 -0
- package/src/components/kitchen-dossier.tsx +322 -0
- package/src/components/menu-dead-end.stories.tsx +67 -0
- package/src/components/menu-dead-end.tsx +127 -0
- package/src/components/menu-family-block.stories.tsx +91 -0
- package/src/components/menu-family-block.tsx +314 -0
- package/src/components/menu-family-card.stories.tsx +120 -0
- package/src/components/menu-family-card.tsx +191 -0
- package/src/components/menu-field.stories.tsx +74 -0
- package/src/components/menu-field.tsx +199 -0
- package/src/components/menu-filter-chips.stories.tsx +107 -0
- package/src/components/menu-filter-chips.tsx +238 -0
- package/src/components/menu-group-rows.stories.tsx +54 -0
- package/src/components/menu-group-rows.tsx +133 -0
- package/src/components/menu-hub-hero.stories.tsx +92 -0
- package/src/components/menu-hub-hero.tsx +187 -0
- package/src/components/menu-index-header.stories.tsx +72 -0
- package/src/components/menu-index-header.tsx +125 -0
- package/src/components/menu-index-list.stories.tsx +50 -0
- package/src/components/menu-index-list.tsx +149 -0
- package/src/components/menu-intent-hero.stories.tsx +67 -0
- package/src/components/menu-intent-hero.tsx +189 -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-kitchen-list.stories.tsx +67 -0
- package/src/components/menu-kitchen-list.test.tsx +78 -0
- package/src/components/menu-kitchen-list.tsx +284 -0
- package/src/components/menu-mocks.ts +592 -0
- package/src/components/menu-types.ts +442 -0
- package/src/components/motion-icon-controls.tsx +11 -11
- package/src/components/motion-icon-set.tsx +36 -8
- package/src/components/motion-icon.test.tsx +23 -1
- package/src/components/motion-icon.tsx +20 -0
- package/src/components/open-now-list.stories.tsx +76 -0
- package/src/components/open-now-list.tsx +127 -0
- package/src/components/photo-ground.tsx +147 -0
- package/src/components/place-types.ts +112 -0
- package/src/components/qa-block.stories.tsx +43 -0
- package/src/components/qa-block.tsx +60 -0
- package/src/components/signature-chip.tsx +77 -0
- package/src/components/snap-rail.tsx +133 -9
- package/src/components/stat-banner.stories.tsx +100 -0
- package/src/components/stat-banner.tsx +117 -0
- package/src/components/story.tsx +225 -0
- package/src/components/top-comparison.stories.tsx +78 -0
- package/src/components/top-comparison.tsx +151 -0
- package/src/components/top-cross-link.stories.tsx +84 -0
- package/src/components/top-cross-link.tsx +171 -0
- package/src/components/top-ranking-entry.stories.tsx +171 -0
- package/src/components/top-ranking-entry.test.tsx +105 -0
- package/src/components/top-ranking-entry.tsx +425 -0
- package/src/components/top-ranking-hero.stories.tsx +76 -0
- package/src/components/top-ranking-hero.tsx +218 -0
- package/src/components/top-ranking-legend.stories.tsx +78 -0
- package/src/components/top-ranking-legend.tsx +239 -0
- package/src/components/top-types.ts +110 -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-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/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
import type { MenuIntentFigure, MenuLinkComponent, MenuLinkProps } from "./menu-types";
|
|
3
|
+
|
|
4
|
+
function DefaultLink({ href, ...props }: MenuLinkProps) {
|
|
5
|
+
return <a href={href} {...props} />;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* MenuIntentHero — the head of an intent page (`/menu/cuisines/italian`).
|
|
10
|
+
*
|
|
11
|
+
* The intent page answers "which kitchens, for this, right now", and the hero's
|
|
12
|
+
* job is to make the size of the answer legible before the reader touches a
|
|
13
|
+
* filter: how many kitchens cook this, what they are known for, how many are
|
|
14
|
+
* serving. Every number in it is real and every one of them is a control.
|
|
15
|
+
*
|
|
16
|
+
* ## Distinct from `<MenuHubHero>`
|
|
17
|
+
*
|
|
18
|
+
* The hub's hero introduces a city's whole menu and carries a search field.
|
|
19
|
+
* This one introduces ONE intent and carries figures that filter the list
|
|
20
|
+
* below it. They share a look — a tinted panel with a watermark — and nothing
|
|
21
|
+
* else, so they are two components rather than one with a mode.
|
|
22
|
+
*
|
|
23
|
+
* ## The figures are the first filter
|
|
24
|
+
*
|
|
25
|
+
* "58 open now" is both a fact and the fastest way to act on it, so tapping a
|
|
26
|
+
* figure applies it as a filter rather than scrolling somewhere. A figure with
|
|
27
|
+
* no `onSelect` renders as a plain fact: the total is normally one, because
|
|
28
|
+
* filtering to "all of them" is not a state.
|
|
29
|
+
*
|
|
30
|
+
* They stay four across on the phone. The board proved the row legible at
|
|
31
|
+
* 390px, and a 2×2 costs ~40px of height in the tallest element on the
|
|
32
|
+
* smallest screen; the labels wrap instead.
|
|
33
|
+
*
|
|
34
|
+
* ## The paragraph survives the phone
|
|
35
|
+
*
|
|
36
|
+
* The board hides it under 640px. It is kept here and clamped instead: it is
|
|
37
|
+
* the page's only prose, it is what the description meta is built from, and
|
|
38
|
+
* `display: none` on the one indexed sentence is a cost the extra three lines
|
|
39
|
+
* do not justify.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export type MenuIntentHeroProps = {
|
|
43
|
+
title: string;
|
|
44
|
+
/**
|
|
45
|
+
* Over the title — the active constraint ("TONIGHT · OPEN NOW OR LATER"), or
|
|
46
|
+
* the intent's kind ("CUISINE") when there is none.
|
|
47
|
+
*/
|
|
48
|
+
kicker?: string;
|
|
49
|
+
/** Icon node for the kicker, resolved by the caller. */
|
|
50
|
+
kickerIcon?: React.ReactNode;
|
|
51
|
+
/** One paragraph of real numbers. Generated by the caller from the facets. */
|
|
52
|
+
description?: string;
|
|
53
|
+
figures?: MenuIntentFigure[];
|
|
54
|
+
/**
|
|
55
|
+
* Finished CSS gradient for the ground, e.g. `linear-gradient(140deg, …)`.
|
|
56
|
+
*
|
|
57
|
+
* A gradient rather than a token name, for the same reason `<MenuField>` and
|
|
58
|
+
* `<MenuIntentTiles>` take one: the hue tables are app-owned, and a component
|
|
59
|
+
* referencing `var(--h-pizza)` renders an unstyled panel in any consumer that
|
|
60
|
+
* does not define it. Missing, the panel falls back to the foreground colour
|
|
61
|
+
* — visibly unstyled rather than white text on white.
|
|
62
|
+
*/
|
|
63
|
+
gradient?: string;
|
|
64
|
+
/** Watermark behind the text, resolved by the caller. */
|
|
65
|
+
icon?: React.ReactNode;
|
|
66
|
+
/** Used for any figure carrying an `href`. Defaults to a plain `<a>`. */
|
|
67
|
+
linkComponent?: MenuLinkComponent;
|
|
68
|
+
className?: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function MenuIntentHero({
|
|
72
|
+
title,
|
|
73
|
+
kicker,
|
|
74
|
+
kickerIcon,
|
|
75
|
+
description,
|
|
76
|
+
figures,
|
|
77
|
+
gradient,
|
|
78
|
+
icon,
|
|
79
|
+
linkComponent = DefaultLink,
|
|
80
|
+
className,
|
|
81
|
+
}: MenuIntentHeroProps) {
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
className={cn(
|
|
85
|
+
"relative flex overflow-hidden rounded-[18px] text-white lg:min-h-[210px]",
|
|
86
|
+
!gradient && "bg-foreground",
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
{gradient && (
|
|
91
|
+
<span aria-hidden className="absolute inset-0" style={{ backgroundImage: gradient }} />
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{icon && (
|
|
95
|
+
<span
|
|
96
|
+
aria-hidden
|
|
97
|
+
className="pointer-events-none absolute -bottom-7 -right-4 opacity-20 [&>svg]:h-[120px] [&>svg]:w-[120px] lg:[&>svg]:h-[170px] lg:[&>svg]:w-[170px]"
|
|
98
|
+
>
|
|
99
|
+
{icon}
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<div className="relative z-[1] flex w-full flex-col gap-2.5 p-5 lg:gap-[11px] lg:px-[26px] lg:py-6">
|
|
104
|
+
{kicker && (
|
|
105
|
+
<span className="flex items-center gap-1.5 text-[10.5px] font-extrabold uppercase tracking-[0.13em] opacity-[0.82]">
|
|
106
|
+
{kickerIcon}
|
|
107
|
+
{kicker}
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
<h1 className="font-serif text-[29px] font-semibold leading-[1.04] tracking-[-0.025em] lg:text-[40px]">
|
|
112
|
+
{title}
|
|
113
|
+
</h1>
|
|
114
|
+
|
|
115
|
+
{description && (
|
|
116
|
+
<p className="line-clamp-3 max-w-[520px] text-[13.5px] leading-[1.55] opacity-85 lg:line-clamp-none lg:text-sm">
|
|
117
|
+
{description}
|
|
118
|
+
</p>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{figures && figures.length > 0 && (
|
|
122
|
+
<div className="mt-auto grid grid-cols-4 gap-x-4 gap-y-3 pt-3.5 lg:flex lg:gap-[26px] lg:pt-4">
|
|
123
|
+
{figures.map((figure) => (
|
|
124
|
+
<Figure key={figure.key} figure={figure} linkComponent={linkComponent} />
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A figure is a control only when it does something.
|
|
135
|
+
*
|
|
136
|
+
* The alternative — every figure a button, the inert ones disabled — offers
|
|
137
|
+
* four controls and refuses one of them for a reason the reader cannot see.
|
|
138
|
+
*
|
|
139
|
+
* A LINK when it carries an `href`, a button only when the caller is holding
|
|
140
|
+
* the state in memory. The link is the better shape wherever the state is in
|
|
141
|
+
* the URL: it renders on the server, it is middle-clickable, and `?open=now` is
|
|
142
|
+
* a state we want a crawler to follow.
|
|
143
|
+
*/
|
|
144
|
+
function Figure({
|
|
145
|
+
figure,
|
|
146
|
+
linkComponent: LinkComponent,
|
|
147
|
+
}: {
|
|
148
|
+
figure: MenuIntentFigure;
|
|
149
|
+
linkComponent: MenuLinkComponent;
|
|
150
|
+
}) {
|
|
151
|
+
const body = (
|
|
152
|
+
<>
|
|
153
|
+
<span className="block text-xl font-black leading-none tracking-[-0.03em] tabular-nums lg:text-2xl">
|
|
154
|
+
{figure.value}
|
|
155
|
+
</span>
|
|
156
|
+
<span className="mt-1 block text-[11px] leading-tight opacity-[0.72]">{figure.label}</span>
|
|
157
|
+
</>
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const interactive = cn(
|
|
161
|
+
"block min-w-0 rounded-lg text-left transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 motion-reduce:transition-none",
|
|
162
|
+
figure.active ? "opacity-100" : "opacity-90"
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (figure.href) {
|
|
166
|
+
return (
|
|
167
|
+
<LinkComponent
|
|
168
|
+
href={figure.href}
|
|
169
|
+
aria-current={figure.active ? "page" : undefined}
|
|
170
|
+
className={interactive}
|
|
171
|
+
>
|
|
172
|
+
{body}
|
|
173
|
+
</LinkComponent>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!figure.onSelect) return <span className="block min-w-0">{body}</span>;
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
onClick={figure.onSelect}
|
|
183
|
+
aria-pressed={figure.active}
|
|
184
|
+
className={interactive}
|
|
185
|
+
>
|
|
186
|
+
{body}
|
|
187
|
+
</button>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Beef, Coffee, Leaf, MapPin, Pizza, UtensilsCrossed } from "lucide-react";
|
|
3
|
+
import { MenuIntentTiles } from "./menu-intent-tiles";
|
|
4
|
+
import { mockIntentTiles } from "./menu-mocks";
|
|
5
|
+
|
|
6
|
+
const icons = [Pizza, Beef, Coffee, UtensilsCrossed, Leaf, MapPin];
|
|
7
|
+
const tiles = mockIntentTiles.map((tile, index) => {
|
|
8
|
+
const Icon = icons[index];
|
|
9
|
+
return { ...tile, icon: <Icon /> };
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof MenuIntentTiles> = {
|
|
13
|
+
title: "Menu/MenuIntentTiles",
|
|
14
|
+
component: MenuIntentTiles,
|
|
15
|
+
tags: ["autodocs"],
|
|
16
|
+
decorators: [
|
|
17
|
+
(Story) => (
|
|
18
|
+
<div className="w-[1100px] bg-background p-4">
|
|
19
|
+
<Story />
|
|
20
|
+
</div>
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
parameters: {
|
|
24
|
+
docs: {
|
|
25
|
+
description: {
|
|
26
|
+
component:
|
|
27
|
+
"The sentence people arrive with, as six coloured tiles. An intent is a family plus a constraint — Italian *tonight*, burgers, coffee — and each tile is a destination, so `href` is required on the view model: a category page that does not exist is filtered out by the caller rather than rendered as a tile linking nowhere. Roughly thirty-two of thirty-eight cuisines have a page, so that is the normal case, not the edge one.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof MenuIntentTiles>;
|
|
35
|
+
|
|
36
|
+
export const Default: Story = { args: { tiles } };
|
|
37
|
+
|
|
38
|
+
/** Two-up on a phone, six-up from `lg`. */
|
|
39
|
+
export const Mobile: Story = {
|
|
40
|
+
args: { tiles },
|
|
41
|
+
decorators: [
|
|
42
|
+
(Story) => (
|
|
43
|
+
<div className="w-[390px] bg-background p-3">
|
|
44
|
+
<Story />
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Without icons the tiles still read — the watermark is recognition, not information. */
|
|
51
|
+
export const WithoutIcons: Story = { args: { tiles: mockIntentTiles } };
|
|
52
|
+
|
|
53
|
+
/** Fewer tiles than a full row: the grid does not stretch them. */
|
|
54
|
+
export const Partial: Story = { args: { tiles: tiles.slice(0, 3) } };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ChevronRight } from "lucide-react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import type { MenuIntentTile, MenuLinkProps, MenuLinkComponent } from "./menu-types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MenuIntentTiles — the sentence people arrive with, as six coloured tiles.
|
|
7
|
+
*
|
|
8
|
+
* An intent is a family plus a constraint: Italian *tonight*, burgers, coffee.
|
|
9
|
+
* Each tile is a destination, so `href` is required on the view model — a
|
|
10
|
+
* category page that does not exist is filtered out by the caller rather than
|
|
11
|
+
* rendered as a tile that links nowhere. Roughly thirty-two of thirty-eight
|
|
12
|
+
* cuisines have a page, so that case is normal, not exceptional.
|
|
13
|
+
*
|
|
14
|
+
* The icon is a watermark bleeding off the bottom-right corner at low opacity,
|
|
15
|
+
* not a badge: it gives the tile a silhouette to be recognised by in a rail
|
|
16
|
+
* without competing with the label. It is `aria-hidden` — the label already
|
|
17
|
+
* says what the tile is.
|
|
18
|
+
*
|
|
19
|
+
* Server-safe.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
|
|
23
|
+
export type MenuIntentTilesLinkProps = MenuLinkProps;
|
|
24
|
+
|
|
25
|
+
function DefaultLink({ href, ...props }: MenuIntentTilesLinkProps) {
|
|
26
|
+
return <a href={href} {...props} />;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type MenuIntentTilesProps = {
|
|
30
|
+
tiles: MenuIntentTile[];
|
|
31
|
+
linkComponent?: MenuLinkComponent;
|
|
32
|
+
className?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function MenuIntentTiles({
|
|
36
|
+
tiles,
|
|
37
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
38
|
+
className,
|
|
39
|
+
}: MenuIntentTilesProps) {
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
className={cn(
|
|
43
|
+
"grid grid-cols-2 gap-3 lg:grid-cols-6 lg:gap-3.5",
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{tiles.map((tile) => (
|
|
48
|
+
<LinkComponent
|
|
49
|
+
key={tile.slug}
|
|
50
|
+
href={tile.href}
|
|
51
|
+
className="relative flex min-h-[110px] flex-col overflow-hidden rounded-2xl bg-muted p-3.5 text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 lg:min-h-[124px] lg:px-3.5 lg:pb-3.5 lg:pt-[15px]"
|
|
52
|
+
style={tile.gradient ? { backgroundImage: tile.gradient } : undefined}
|
|
53
|
+
>
|
|
54
|
+
{tile.icon && (
|
|
55
|
+
<span
|
|
56
|
+
aria-hidden
|
|
57
|
+
className="pointer-events-none absolute -bottom-2.5 -right-1.5 z-0 opacity-[0.22] [&>svg]:h-[62px] [&>svg]:w-[62px]"
|
|
58
|
+
>
|
|
59
|
+
{tile.icon}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<span className="relative z-[1] flex h-full flex-col">
|
|
64
|
+
<span className="text-[14.5px] font-extrabold leading-[1.2] tracking-[-0.015em]">
|
|
65
|
+
{tile.label}
|
|
66
|
+
</span>
|
|
67
|
+
{tile.caption && (
|
|
68
|
+
<span className="mt-[3px] text-[11.5px] leading-[1.35]">{tile.caption}</span>
|
|
69
|
+
)}
|
|
70
|
+
<ChevronRight className="mt-auto h-4 w-4 opacity-85" aria-hidden />
|
|
71
|
+
</span>
|
|
72
|
+
</LinkComponent>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|