@bearmenu/ui 0.8.11 → 0.8.13
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 → chunk-3NMG3KNA.js} +1 -1
- package/dist/{chunk-5RWTOT5Y.js → chunk-CNJQUHCU.js} +11 -1
- package/dist/{chunk-DTXNDLSE.js → chunk-FKNBZF5T.js} +15 -0
- package/dist/{chunk-H3ZZDX5L.js → chunk-P7XTW2CG.js} +2 -2
- package/dist/{chunk-JS3EINZD.js → chunk-VAXOQS7W.js} +12 -0
- package/dist/{chunk-AKQ57NXY.js → chunk-ZQ53AGML.js} +2 -2
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/avatar.d.ts +3 -3
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/carousel.js +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/command.d.ts +15 -15
- package/dist/components/concept-compare-rail.js +3 -3
- package/dist/components/concept-compare.d.ts +1 -1
- package/dist/components/concept-compare.js +29 -8
- package/dist/components/concept-convergence.js +1 -1
- package/dist/components/dialog.d.ts +2 -2
- package/dist/components/empty-state.js +1 -1
- package/dist/components/event-detail-panel.js +1 -1
- package/dist/components/event-list-row-wide.js +1 -1
- package/dist/components/haptic-button.js +1 -1
- package/dist/components/input-group.d.ts +4 -4
- package/dist/components/input.d.ts +2 -2
- package/dist/components/item.d.ts +17 -7
- package/dist/components/item.js +15 -10
- package/dist/components/kbd.d.ts +2 -2
- package/dist/components/kitchen-dossier.d.ts +2 -0
- package/dist/components/kitchen-dossier.js +25 -18
- package/dist/components/link-button.js +1 -1
- package/dist/components/menu-family-block.d.ts +15 -9
- package/dist/components/menu-family-block.js +63 -60
- package/dist/components/menu-family-card.js +1 -1
- package/dist/components/menu-family-tiles.d.ts +19 -0
- package/dist/components/menu-family-tiles.js +57 -0
- package/dist/components/menu-hub-hero.js +8 -6
- package/dist/components/menu-index-list.d.ts +31 -12
- package/dist/components/menu-index-list.js +68 -31
- package/dist/components/menu-intent-hero.d.ts +1 -1
- package/dist/components/menu-kitchen-list.js +1 -1
- package/dist/components/menu-mocks.d.ts +10 -3
- package/dist/components/menu-mocks.js +21 -9
- package/dist/components/menu-types.d.ts +33 -10
- package/dist/components/multi-select-combobox.js +1 -1
- package/dist/components/native-select.d.ts +2 -2
- package/dist/components/pagination.js +1 -1
- package/dist/components/photo-ground.d.ts +7 -1
- package/dist/components/photo-ground.js +1 -1
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/select.d.ts +1 -1
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/snap-rail.d.ts +2 -7
- package/dist/components/snap-rail.js +1 -1
- package/dist/components/spinner.d.ts +4 -4
- package/dist/components/tag.d.ts +4 -4
- package/dist/components/text.d.ts +5 -5
- package/dist/components/toggle.d.ts +1 -1
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/weekday-theme-banner.d.ts +24 -3
- package/dist/components/weekday-theme-banner.js +62 -14
- package/package.json +1 -1
- package/src/components/button.stories.tsx +25 -2
- package/src/components/button.tsx +12 -0
- package/src/components/colour-dictionary.stories.tsx +1 -1
- package/src/components/concept-compare.tsx +40 -9
- package/src/components/item.tsx +69 -45
- package/src/components/kitchen-dossier.tsx +45 -24
- package/src/components/menu-family-block.tsx +86 -79
- package/src/components/menu-family-tiles.stories.tsx +54 -0
- package/src/components/menu-family-tiles.test.tsx +30 -0
- package/src/components/menu-family-tiles.tsx +132 -0
- package/src/components/menu-hub-hero.tsx +13 -7
- package/src/components/menu-index-list.stories.tsx +6 -1
- package/src/components/menu-index-list.tsx +101 -54
- package/src/components/menu-intent-hero.tsx +1 -1
- package/src/components/menu-kitchen-list.tsx +4 -1
- package/src/components/menu-mocks.ts +32 -9
- package/src/components/menu-types.ts +32 -9
- package/src/components/photo-ground.tsx +13 -1
- package/src/components/snap-rail.tsx +19 -0
- package/src/components/venue-grade-dimensions.tsx +9 -2
- package/src/components/weekday-theme-banner.stories.tsx +43 -4
- package/src/components/weekday-theme-banner.tsx +94 -17
- package/dist/components/menu-intent-tiles.d.ts +0 -31
- package/dist/components/menu-intent-tiles.js +0 -50
- package/src/components/menu-intent-tiles.stories.tsx +0 -54
- package/src/components/menu-intent-tiles.tsx +0 -76
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ChevronRight } from "lucide-react";
|
|
1
2
|
import { cn } from "../lib/utils";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -32,8 +33,15 @@ function DefaultLink({ href, ...props }: WeekdayThemeBannerLinkProps) {
|
|
|
32
33
|
export type WeekdayThemeDay = {
|
|
33
34
|
/** Short code, e.g. "THU". Already localised. */
|
|
34
35
|
code: string;
|
|
35
|
-
/** Theme name, e.g. "Burger Thursday". */
|
|
36
|
+
/** Theme name, e.g. "Burger Thursday in Cluj". */
|
|
36
37
|
name: string;
|
|
38
|
+
/**
|
|
39
|
+
* The phone's title, e.g. "Burger Thursday" — one line where `name` takes
|
|
40
|
+
* two, so the photograph behind it stays a photograph. A separate string,
|
|
41
|
+
* never `name` with a clause stripped: word order is the locale's. Falls
|
|
42
|
+
* back to `name`.
|
|
43
|
+
*/
|
|
44
|
+
shortHeadline?: string;
|
|
37
45
|
/** Short form for the rail, e.g. "Burger". Falls back to `name`. */
|
|
38
46
|
shortName?: string;
|
|
39
47
|
/** Items under the theme. Omit where there is no count to show. */
|
|
@@ -51,10 +59,21 @@ export type WeekdayThemeBannerProps = {
|
|
|
51
59
|
body?: string;
|
|
52
60
|
/** Heading over the week rail, e.g. "THE WEEK". */
|
|
53
61
|
weekLabel?: string;
|
|
54
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* The primary action — pass a rendered button or link. From `lg` up only
|
|
64
|
+
* when `compactAction` is also given: on a phone the banner is a row, and a
|
|
65
|
+
* row does not carry a labelled button.
|
|
66
|
+
*/
|
|
55
67
|
action?: React.ReactNode;
|
|
56
68
|
/** Note beside the action, e.g. "Bruno picks nine, ranked on mentions". */
|
|
57
69
|
actionNote?: string;
|
|
70
|
+
/**
|
|
71
|
+
* The phone's action: the whole banner becomes the target and a chevron on
|
|
72
|
+
* the right is the signal — a disclosure row, not a call to action, so the
|
|
73
|
+
* banner stays as tall as its title. `label` names the target for a screen
|
|
74
|
+
* reader. Hidden from `lg`, where `action` takes over.
|
|
75
|
+
*/
|
|
76
|
+
compactAction?: { label: string; onClick: () => void };
|
|
58
77
|
/** Destination per day index. Renders the rail as links. */
|
|
59
78
|
dayHref?: (day: WeekdayThemeDay, index: number) => string;
|
|
60
79
|
/** Selects a day in place. Renders the rail as buttons. */
|
|
@@ -82,6 +101,7 @@ export function WeekdayThemeBanner({
|
|
|
82
101
|
weekLabel,
|
|
83
102
|
action,
|
|
84
103
|
actionNote,
|
|
104
|
+
compactAction,
|
|
85
105
|
dayHref,
|
|
86
106
|
onDayChange,
|
|
87
107
|
imageUrl,
|
|
@@ -94,7 +114,9 @@ export function WeekdayThemeBanner({
|
|
|
94
114
|
return (
|
|
95
115
|
<div
|
|
96
116
|
className={cn(
|
|
97
|
-
|
|
117
|
+
// A plate needs a ~2.8:1 window to read on a phone; the text alone
|
|
118
|
+
// would give it ~85px.
|
|
119
|
+
"group relative grid overflow-hidden rounded-[18px] bg-[linear-gradient(120deg,oklch(0.32_0.06_45),oklch(0.24_0.04_40))] text-white max-lg:min-h-[128px] lg:grid-cols-[1fr_auto]",
|
|
98
120
|
className
|
|
99
121
|
)}
|
|
100
122
|
>
|
|
@@ -108,17 +130,19 @@ export function WeekdayThemeBanner({
|
|
|
108
130
|
className="absolute inset-0 h-full w-full object-cover"
|
|
109
131
|
/>
|
|
110
132
|
{/*
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
better in isolation and made the week rail — seven lines of
|
|
114
|
-
type — sit directly on whatever the plate happened to be.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
Two scrims. From `lg` it covers the WHOLE banner, not just the
|
|
134
|
+
text side: it used to fade to near-transparent on the right, which
|
|
135
|
+
looked better in isolation and made the week rail — seven lines of
|
|
136
|
+
12px type — sit directly on whatever the plate happened to be.
|
|
137
|
+
|
|
138
|
+
On a phone there is no rail, the title is one line in the left
|
|
139
|
+
64%, and the plates are composed centre-right — so the ramp is
|
|
140
|
+
dense under the text and clears to 0.2 by the right edge, and the
|
|
141
|
+
part of the photograph that carries it is the part that shows.
|
|
118
142
|
*/}
|
|
119
143
|
<div
|
|
120
144
|
aria-hidden
|
|
121
|
-
className="absolute inset-0 bg-[linear-gradient(100deg,oklch(0.18_0.03_45_/_0.94)_0%,oklch(0.18_0.03_45_/_0.82)_45%,oklch(0.18_0.03_45_/_0.66)_100%)]"
|
|
145
|
+
className="absolute inset-0 bg-[linear-gradient(100deg,oklch(0.18_0.03_45_/_0.85)_0%,oklch(0.18_0.03_45_/_0.45)_48%,oklch(0.18_0.03_45_/_0.2)_100%)] lg:bg-[linear-gradient(100deg,oklch(0.18_0.03_45_/_0.94)_0%,oklch(0.18_0.03_45_/_0.82)_45%,oklch(0.18_0.03_45_/_0.66)_100%)]"
|
|
122
146
|
/>
|
|
123
147
|
</>
|
|
124
148
|
)}
|
|
@@ -132,22 +156,75 @@ export function WeekdayThemeBanner({
|
|
|
132
156
|
</span>
|
|
133
157
|
)}
|
|
134
158
|
|
|
135
|
-
|
|
159
|
+
{/* The phone's target: the whole banner, the chevron as the signal.
|
|
160
|
+
Above the rail's ground and the scrim, below nothing — the week rail
|
|
161
|
+
is `lg` only, so there is no other control to cover on a phone. */}
|
|
162
|
+
{compactAction && (
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
aria-label={compactAction.label}
|
|
166
|
+
onClick={compactAction.onClick}
|
|
167
|
+
className="absolute inset-0 z-[2] rounded-[18px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/70 lg:hidden"
|
|
168
|
+
>
|
|
169
|
+
<span
|
|
170
|
+
aria-hidden
|
|
171
|
+
// The scrim's own ink rather than glass: the chevron sits where
|
|
172
|
+
// the scrim has cleared to 0.2, and white glass on a bright plate
|
|
173
|
+
// would vanish. Chrome on a photograph, the `overlay` rule.
|
|
174
|
+
className="absolute right-4 top-1/2 flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full border border-white/20 bg-[oklch(0.18_0.03_45_/_0.55)] text-white transition-colors [@media(pointer:coarse)]:group-active:bg-[oklch(0.18_0.03_45_/_0.75)]"
|
|
175
|
+
>
|
|
176
|
+
<ChevronRight className="h-5 w-5" />
|
|
177
|
+
</span>
|
|
178
|
+
</button>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
<div
|
|
182
|
+
className={cn(
|
|
183
|
+
"relative z-[1] flex flex-col gap-2.5 p-4 lg:gap-3 lg:p-6",
|
|
184
|
+
// On a phone the text keeps to the left 64% — the photograph gets
|
|
185
|
+
// the rest, and the chevron sits in it — and centres in the row.
|
|
186
|
+
imageUrl && "max-lg:max-w-[64%] max-lg:justify-center max-lg:gap-1.5",
|
|
187
|
+
// Without a photograph the text may run wide; keep it off the
|
|
188
|
+
// chevron. With one, the 36% beside the column already holds it.
|
|
189
|
+
compactAction && !imageUrl && "max-lg:pr-16"
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{/* A plain line on the phone, the pill from `lg`: one less box on
|
|
193
|
+
the photograph. */}
|
|
136
194
|
{kicker && (
|
|
137
|
-
<span className="inline-flex w-max items-center gap-1.5 rounded-full bg-white/15 px-2.5 py-1 text-
|
|
195
|
+
<span className="inline-flex w-max items-center gap-1.5 text-[10.5px] font-bold tracking-[0.09em] text-white/75 lg:rounded-full lg:bg-white/15 lg:px-2.5 lg:py-1 lg:text-white">
|
|
138
196
|
{kickerIcon}
|
|
139
197
|
{kicker}
|
|
140
198
|
</span>
|
|
141
199
|
)}
|
|
142
200
|
|
|
143
|
-
|
|
144
|
-
|
|
201
|
+
{/* The phone's title is the short one — one line expected, two
|
|
202
|
+
allowed, never cut. Both render; the breakpoint picks. */}
|
|
203
|
+
<h3 className="font-serif text-[22px] font-semibold leading-[1.15] tracking-[-0.02em] lg:text-[36px] lg:leading-tight">
|
|
204
|
+
{active.shortHeadline ? (
|
|
205
|
+
<>
|
|
206
|
+
<span className="line-clamp-2 lg:hidden">{active.shortHeadline}</span>
|
|
207
|
+
<span className="hidden lg:block">{active.name}</span>
|
|
208
|
+
</>
|
|
209
|
+
) : (
|
|
210
|
+
active.name
|
|
211
|
+
)}
|
|
145
212
|
</h3>
|
|
146
213
|
|
|
147
|
-
{body &&
|
|
214
|
+
{body && (
|
|
215
|
+
<p className="hidden max-w-[520px] text-[13.5px] leading-[1.5] opacity-90 lg:block">
|
|
216
|
+
{body}
|
|
217
|
+
</p>
|
|
218
|
+
)}
|
|
148
219
|
|
|
149
220
|
{(action || actionNote) && (
|
|
150
|
-
<div
|
|
221
|
+
<div
|
|
222
|
+
className={cn(
|
|
223
|
+
"mt-1 flex flex-wrap items-center gap-3",
|
|
224
|
+
// With a compact action the phone has its chevron instead.
|
|
225
|
+
compactAction && "hidden lg:flex"
|
|
226
|
+
)}
|
|
227
|
+
>
|
|
151
228
|
{action}
|
|
152
229
|
{actionNote && <span className="text-xs opacity-75">{actionNote}</span>}
|
|
153
230
|
</div>
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { MenuIntentTile, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
3
|
-
import 'react';
|
|
4
|
-
import './place-types.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* MenuIntentTiles — the sentence people arrive with, as six coloured tiles.
|
|
8
|
-
*
|
|
9
|
-
* An intent is a family plus a constraint: Italian *tonight*, burgers, coffee.
|
|
10
|
-
* Each tile is a destination, so `href` is required on the view model — a
|
|
11
|
-
* category page that does not exist is filtered out by the caller rather than
|
|
12
|
-
* rendered as a tile that links nowhere. Roughly thirty-two of thirty-eight
|
|
13
|
-
* cuisines have a page, so that case is normal, not exceptional.
|
|
14
|
-
*
|
|
15
|
-
* The icon is a watermark bleeding off the bottom-right corner at low opacity,
|
|
16
|
-
* not a badge: it gives the tile a silhouette to be recognised by in a rail
|
|
17
|
-
* without competing with the label. It is `aria-hidden` — the label already
|
|
18
|
-
* says what the tile is.
|
|
19
|
-
*
|
|
20
|
-
* Server-safe.
|
|
21
|
-
*/
|
|
22
|
-
/** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
|
|
23
|
-
type MenuIntentTilesLinkProps = MenuLinkProps;
|
|
24
|
-
type MenuIntentTilesProps = {
|
|
25
|
-
tiles: MenuIntentTile[];
|
|
26
|
-
linkComponent?: MenuLinkComponent;
|
|
27
|
-
className?: string;
|
|
28
|
-
};
|
|
29
|
-
declare function MenuIntentTiles({ tiles, linkComponent: LinkComponent, className, }: MenuIntentTilesProps): react_jsx_runtime.JSX.Element;
|
|
30
|
-
|
|
31
|
-
export { MenuIntentTiles, type MenuIntentTilesLinkProps, type MenuIntentTilesProps };
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
-
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
3
|
-
import { ChevronRight } from 'lucide-react';
|
|
4
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
-
|
|
6
|
-
function DefaultLink(_a) {
|
|
7
|
-
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
8
|
-
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
9
|
-
}
|
|
10
|
-
function MenuIntentTiles({
|
|
11
|
-
tiles,
|
|
12
|
-
linkComponent: LinkComponent = DefaultLink,
|
|
13
|
-
className
|
|
14
|
-
}) {
|
|
15
|
-
return /* @__PURE__ */ jsx(
|
|
16
|
-
"div",
|
|
17
|
-
{
|
|
18
|
-
className: cn(
|
|
19
|
-
"grid grid-cols-2 gap-3 lg:grid-cols-6 lg:gap-3.5",
|
|
20
|
-
className
|
|
21
|
-
),
|
|
22
|
-
children: tiles.map((tile) => /* @__PURE__ */ jsxs(
|
|
23
|
-
LinkComponent,
|
|
24
|
-
{
|
|
25
|
-
href: tile.href,
|
|
26
|
-
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]",
|
|
27
|
-
style: tile.gradient ? { backgroundImage: tile.gradient } : void 0,
|
|
28
|
-
children: [
|
|
29
|
-
tile.icon && /* @__PURE__ */ jsx(
|
|
30
|
-
"span",
|
|
31
|
-
{
|
|
32
|
-
"aria-hidden": true,
|
|
33
|
-
className: "pointer-events-none absolute -bottom-2.5 -right-1.5 z-0 opacity-[0.22] [&>svg]:h-[62px] [&>svg]:w-[62px]",
|
|
34
|
-
children: tile.icon
|
|
35
|
-
}
|
|
36
|
-
),
|
|
37
|
-
/* @__PURE__ */ jsxs("span", { className: "relative z-[1] flex h-full flex-col", children: [
|
|
38
|
-
/* @__PURE__ */ jsx("span", { className: "text-[14.5px] font-extrabold leading-[1.2] tracking-[-0.015em]", children: tile.label }),
|
|
39
|
-
tile.caption && /* @__PURE__ */ jsx("span", { className: "mt-[3px] text-[11.5px] leading-[1.35]", children: tile.caption }),
|
|
40
|
-
/* @__PURE__ */ jsx(ChevronRight, { className: "mt-auto h-4 w-4 opacity-85", "aria-hidden": true })
|
|
41
|
-
] })
|
|
42
|
-
]
|
|
43
|
-
},
|
|
44
|
-
tile.slug
|
|
45
|
-
))
|
|
46
|
-
}
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export { MenuIntentTiles };
|
|
@@ -1,54 +0,0 @@
|
|
|
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) } };
|
|
@@ -1,76 +0,0 @@
|
|
|
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
|
-
}
|