@bearmenu/ui 0.8.11 → 0.8.12

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.
Files changed (86) hide show
  1. package/dist/{chunk-H3ZZDX5L.js → chunk-7A42J73Q.js} +1 -1
  2. package/dist/{chunk-5RWTOT5Y.js → chunk-CNJQUHCU.js} +11 -1
  3. package/dist/{chunk-JS3EINZD.js → chunk-VAXOQS7W.js} +12 -0
  4. package/dist/{chunk-AKQ57NXY.js → chunk-ZQ53AGML.js} +2 -2
  5. package/dist/components/alert-dialog.js +1 -1
  6. package/dist/components/alert.d.ts +2 -2
  7. package/dist/components/avatar.d.ts +3 -3
  8. package/dist/components/badge.d.ts +1 -1
  9. package/dist/components/button.d.ts +2 -2
  10. package/dist/components/button.js +1 -1
  11. package/dist/components/calendar.js +1 -1
  12. package/dist/components/card.d.ts +4 -4
  13. package/dist/components/carousel.js +1 -1
  14. package/dist/components/combobox.js +1 -1
  15. package/dist/components/command.d.ts +8 -8
  16. package/dist/components/concept-compare-rail.js +2 -2
  17. package/dist/components/concept-compare.d.ts +1 -1
  18. package/dist/components/concept-compare.js +28 -7
  19. package/dist/components/concept-convergence.js +1 -1
  20. package/dist/components/date-input.d.ts +1 -1
  21. package/dist/components/dialog.d.ts +2 -2
  22. package/dist/components/empty-state.js +1 -1
  23. package/dist/components/event-detail-panel.js +1 -1
  24. package/dist/components/event-list-row-wide.js +1 -1
  25. package/dist/components/haptic-button.js +1 -1
  26. package/dist/components/input-group.d.ts +4 -4
  27. package/dist/components/input.d.ts +2 -2
  28. package/dist/components/item.d.ts +19 -9
  29. package/dist/components/item.js +15 -10
  30. package/dist/components/kbd.d.ts +2 -2
  31. package/dist/components/kitchen-dossier.d.ts +2 -0
  32. package/dist/components/kitchen-dossier.js +25 -18
  33. package/dist/components/link-button.js +1 -1
  34. package/dist/components/menu-family-block.d.ts +15 -9
  35. package/dist/components/menu-family-block.js +62 -59
  36. package/dist/components/menu-family-card.js +1 -1
  37. package/dist/components/menu-family-tiles.d.ts +19 -0
  38. package/dist/components/menu-family-tiles.js +57 -0
  39. package/dist/components/menu-hub-hero.js +8 -6
  40. package/dist/components/menu-index-list.d.ts +31 -12
  41. package/dist/components/menu-index-list.js +68 -31
  42. package/dist/components/menu-intent-hero.d.ts +1 -1
  43. package/dist/components/menu-mocks.d.ts +10 -3
  44. package/dist/components/menu-mocks.js +21 -9
  45. package/dist/components/menu-types.d.ts +33 -10
  46. package/dist/components/multi-select-combobox.js +1 -1
  47. package/dist/components/native-select.d.ts +2 -2
  48. package/dist/components/pagination.js +1 -1
  49. package/dist/components/photo-ground.d.ts +7 -1
  50. package/dist/components/photo-ground.js +1 -1
  51. package/dist/components/searchable-select.js +1 -1
  52. package/dist/components/select.d.ts +1 -1
  53. package/dist/components/sliding-panels.js +1 -1
  54. package/dist/components/snap-rail.d.ts +2 -2
  55. package/dist/components/spinner.d.ts +4 -4
  56. package/dist/components/tag.d.ts +4 -4
  57. package/dist/components/text.d.ts +3 -3
  58. package/dist/components/toggle.d.ts +1 -1
  59. package/dist/components/venue-grade-dimensions.js +1 -1
  60. package/dist/components/weekday-theme-banner.d.ts +24 -3
  61. package/dist/components/weekday-theme-banner.js +62 -14
  62. package/package.json +1 -1
  63. package/src/components/button.stories.tsx +25 -2
  64. package/src/components/button.tsx +12 -0
  65. package/src/components/colour-dictionary.stories.tsx +1 -1
  66. package/src/components/concept-compare.tsx +40 -9
  67. package/src/components/item.tsx +69 -45
  68. package/src/components/kitchen-dossier.tsx +45 -24
  69. package/src/components/menu-family-block.tsx +86 -79
  70. package/src/components/menu-family-tiles.stories.tsx +54 -0
  71. package/src/components/menu-family-tiles.test.tsx +30 -0
  72. package/src/components/menu-family-tiles.tsx +132 -0
  73. package/src/components/menu-hub-hero.tsx +13 -7
  74. package/src/components/menu-index-list.stories.tsx +6 -1
  75. package/src/components/menu-index-list.tsx +101 -54
  76. package/src/components/menu-intent-hero.tsx +1 -1
  77. package/src/components/menu-mocks.ts +32 -9
  78. package/src/components/menu-types.ts +32 -9
  79. package/src/components/photo-ground.tsx +13 -1
  80. package/src/components/venue-grade-dimensions.tsx +9 -2
  81. package/src/components/weekday-theme-banner.stories.tsx +43 -4
  82. package/src/components/weekday-theme-banner.tsx +94 -17
  83. package/dist/components/menu-intent-tiles.d.ts +0 -31
  84. package/dist/components/menu-intent-tiles.js +0 -50
  85. package/src/components/menu-intent-tiles.stories.tsx +0 -54
  86. package/src/components/menu-intent-tiles.tsx +0 -76
@@ -1,5 +1,5 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
- import { Calendar, PanelRightOpen } from "lucide-react";
2
+ import { Calendar, ChevronRight } from "lucide-react";
3
3
  import { Button } from "./button";
4
4
  import { WeekdayThemeBanner, type WeekdayThemeDay } from "./weekday-theme-banner";
5
5
 
@@ -56,9 +56,9 @@ export const Default: Story = {
56
56
  body: "186 of them across 11 kitchens, from 32 lei. Two do a vegetarian version and four keep the grill on past midnight.",
57
57
  weekLabel: "THE WEEK",
58
58
  action: (
59
- <Button size="sm" variant="secondary">
60
- <PanelRightOpen className="h-3.5 w-3.5" />
59
+ <Button size="pill" variant="ink">
61
60
  See today&apos;s picks
61
+ <ChevronRight aria-hidden />
62
62
  </Button>
63
63
  ),
64
64
  actionNote: "Bruno picks nine, ranked on review mentions",
@@ -66,6 +66,40 @@ export const Default: Story = {
66
66
  },
67
67
  };
68
68
 
69
+ /**
70
+ * The phone: no labelled button — the banner is the target and the chevron
71
+ * on the right is the signal, so the banner stays as tall as its title.
72
+ */
73
+ export const CompactAction: Story = {
74
+ args: {
75
+ days: days.map((day) => ({ ...day, name: `${day.name} in Cluj-Napoca`, shortHeadline: day.name })),
76
+ activeDay: 3,
77
+ imageUrl:
78
+ "data:image/svg+xml;utf8," +
79
+ encodeURIComponent(
80
+ '<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="630"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(30 30% 40%)"/><stop offset="1" stop-color="hsl(20 40% 20%)"/></linearGradient></defs><rect width="1440" height="630" fill="url(#g)"/><circle cx="1000" cy="330" r="220" fill="hsl(40 60% 70%)"/><circle cx="1000" cy="330" r="150" fill="hsl(25 70% 45%)"/></svg>'
81
+ ),
82
+ kicker: "TODAY'S THEME",
83
+ kickerIcon: <Calendar className="h-3 w-3" />,
84
+ weekLabel: "THE WEEK",
85
+ action: (
86
+ <Button size="pill" variant="ink">
87
+ See today&apos;s picks
88
+ <ChevronRight aria-hidden />
89
+ </Button>
90
+ ),
91
+ compactAction: { label: "See today's picks", onClick: () => {} },
92
+ },
93
+ parameters: { viewport: { defaultViewport: "mobile1" } },
94
+ decorators: [
95
+ (Story) => (
96
+ <div className="w-[390px] bg-background p-3">
97
+ <Story />
98
+ </div>
99
+ ),
100
+ ],
101
+ };
102
+
69
103
  /** The drinks tab, same component. */
70
104
  export const DrinksTab: Story = {
71
105
  args: {
@@ -101,7 +135,12 @@ export const Mobile: Story = {
101
135
  kickerIcon: <Calendar className="h-3 w-3" />,
102
136
  body: "186 of them across 11 kitchens, from 32 lei.",
103
137
  weekLabel: "THE WEEK",
104
- action: <Button size="sm" variant="secondary">See today&apos;s picks</Button>,
138
+ action: (
139
+ <Button size="pill" variant="ink">
140
+ See today&apos;s picks
141
+ <ChevronRight aria-hidden />
142
+ </Button>
143
+ ),
105
144
  },
106
145
  decorators: [
107
146
  (Story) => (
@@ -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
- /** The primary action — pass a rendered button or link. */
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
- "relative grid overflow-hidden rounded-[18px] bg-[linear-gradient(120deg,oklch(0.32_0.06_45),oklch(0.24_0.04_40))] text-white lg:grid-cols-[1fr_auto]",
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
- The scrim covers the WHOLE banner, not just the text side.
112
- It used to fade to near-transparent on the right, which looked
113
- better in isolation and made the week rail — seven lines of 12px
114
- type — sit directly on whatever the plate happened to be. A photo
115
- you can half-read under is worth less than seven labels you can
116
- read. Still a gradient rather than a flat wash, so the image keeps
117
- some depth on the right.
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
- <div className="relative z-[1] flex flex-col gap-2.5 p-4 lg:gap-3 lg:p-6">
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-[10.5px] font-bold tracking-[0.09em]">
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
- <h3 className="font-serif text-[24px] font-semibold tracking-[-0.02em] lg:text-[36px]">
144
- {active.name}
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 && <p className="max-w-[520px] text-[13.5px] leading-[1.5] opacity-90">{body}</p>}
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 className="mt-1 flex flex-wrap items-center gap-3">
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
- }