@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,5 +1,6 @@
|
|
|
1
1
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
2
|
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
3
|
+
import { ChevronRight } from 'lucide-react';
|
|
3
4
|
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
4
5
|
|
|
5
6
|
function DefaultLink(_a) {
|
|
@@ -15,6 +16,7 @@ function WeekdayThemeBanner({
|
|
|
15
16
|
weekLabel,
|
|
16
17
|
action,
|
|
17
18
|
actionNote,
|
|
19
|
+
compactAction,
|
|
18
20
|
dayHref,
|
|
19
21
|
onDayChange,
|
|
20
22
|
imageUrl,
|
|
@@ -27,7 +29,9 @@ function WeekdayThemeBanner({
|
|
|
27
29
|
"div",
|
|
28
30
|
{
|
|
29
31
|
className: cn(
|
|
30
|
-
|
|
32
|
+
// A plate needs a ~2.8:1 window to read on a phone; the text alone
|
|
33
|
+
// would give it ~85px.
|
|
34
|
+
"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]",
|
|
31
35
|
className
|
|
32
36
|
),
|
|
33
37
|
children: [
|
|
@@ -45,7 +49,7 @@ function WeekdayThemeBanner({
|
|
|
45
49
|
"div",
|
|
46
50
|
{
|
|
47
51
|
"aria-hidden": true,
|
|
48
|
-
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%)]"
|
|
52
|
+
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%)]"
|
|
49
53
|
}
|
|
50
54
|
)
|
|
51
55
|
] }),
|
|
@@ -57,18 +61,62 @@ function WeekdayThemeBanner({
|
|
|
57
61
|
children: active.code
|
|
58
62
|
}
|
|
59
63
|
),
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
compactAction && /* @__PURE__ */ jsx(
|
|
65
|
+
"button",
|
|
66
|
+
{
|
|
67
|
+
type: "button",
|
|
68
|
+
"aria-label": compactAction.label,
|
|
69
|
+
onClick: compactAction.onClick,
|
|
70
|
+
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",
|
|
71
|
+
children: /* @__PURE__ */ jsx(
|
|
72
|
+
"span",
|
|
73
|
+
{
|
|
74
|
+
"aria-hidden": true,
|
|
75
|
+
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)]",
|
|
76
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-5 w-5" })
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
),
|
|
81
|
+
/* @__PURE__ */ jsxs(
|
|
82
|
+
"div",
|
|
83
|
+
{
|
|
84
|
+
className: cn(
|
|
85
|
+
"relative z-[1] flex flex-col gap-2.5 p-4 lg:gap-3 lg:p-6",
|
|
86
|
+
// On a phone the text keeps to the left 64% — the photograph gets
|
|
87
|
+
// the rest, and the chevron sits in it — and centres in the row.
|
|
88
|
+
imageUrl && "max-lg:max-w-[64%] max-lg:justify-center max-lg:gap-1.5",
|
|
89
|
+
// Without a photograph the text may run wide; keep it off the
|
|
90
|
+
// chevron. With one, the 36% beside the column already holds it.
|
|
91
|
+
compactAction && !imageUrl && "max-lg:pr-16"
|
|
92
|
+
),
|
|
93
|
+
children: [
|
|
94
|
+
kicker && /* @__PURE__ */ jsxs("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", children: [
|
|
95
|
+
kickerIcon,
|
|
96
|
+
kicker
|
|
97
|
+
] }),
|
|
98
|
+
/* @__PURE__ */ jsx("h3", { className: "font-serif text-[22px] font-semibold leading-[1.15] tracking-[-0.02em] lg:text-[36px] lg:leading-tight", children: active.shortHeadline ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
99
|
+
/* @__PURE__ */ jsx("span", { className: "line-clamp-2 lg:hidden", children: active.shortHeadline }),
|
|
100
|
+
/* @__PURE__ */ jsx("span", { className: "hidden lg:block", children: active.name })
|
|
101
|
+
] }) : active.name }),
|
|
102
|
+
body && /* @__PURE__ */ jsx("p", { className: "hidden max-w-[520px] text-[13.5px] leading-[1.5] opacity-90 lg:block", children: body }),
|
|
103
|
+
(action || actionNote) && /* @__PURE__ */ jsxs(
|
|
104
|
+
"div",
|
|
105
|
+
{
|
|
106
|
+
className: cn(
|
|
107
|
+
"mt-1 flex flex-wrap items-center gap-3",
|
|
108
|
+
// With a compact action the phone has its chevron instead.
|
|
109
|
+
compactAction && "hidden lg:flex"
|
|
110
|
+
),
|
|
111
|
+
children: [
|
|
112
|
+
action,
|
|
113
|
+
actionNote && /* @__PURE__ */ jsx("span", { className: "text-xs opacity-75", children: actionNote })
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
),
|
|
72
120
|
/* @__PURE__ */ jsxs(
|
|
73
121
|
"div",
|
|
74
122
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { Button } from "./button";
|
|
3
|
-
import { Heart, Mail, Plus } from "lucide-react";
|
|
3
|
+
import { ArrowRight, Heart, Mail, Plus } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
const meta = {
|
|
6
6
|
title: "Components/Button",
|
|
@@ -19,11 +19,13 @@ const meta = {
|
|
|
19
19
|
"glass",
|
|
20
20
|
"brown",
|
|
21
21
|
"antagonist",
|
|
22
|
+
"overlay",
|
|
23
|
+
"ink",
|
|
22
24
|
],
|
|
23
25
|
},
|
|
24
26
|
size: {
|
|
25
27
|
control: "select",
|
|
26
|
-
options: ["default", "sm", "lg", "icon"],
|
|
28
|
+
options: ["default", "sm", "lg", "pill", "icon"],
|
|
27
29
|
},
|
|
28
30
|
},
|
|
29
31
|
} satisfies Meta<typeof Button>;
|
|
@@ -59,6 +61,27 @@ export const Glass: Story = {
|
|
|
59
61
|
args: { children: "Glass", variant: "glass" },
|
|
60
62
|
};
|
|
61
63
|
|
|
64
|
+
/** The labelled action on a photo surface: glass on the ink, icon trailing. */
|
|
65
|
+
export const Ink: Story = {
|
|
66
|
+
args: {
|
|
67
|
+
children: (
|
|
68
|
+
<>
|
|
69
|
+
Open the menu
|
|
70
|
+
<ArrowRight aria-hidden />
|
|
71
|
+
</>
|
|
72
|
+
),
|
|
73
|
+
variant: "ink",
|
|
74
|
+
size: "pill",
|
|
75
|
+
},
|
|
76
|
+
decorators: [
|
|
77
|
+
(Story) => (
|
|
78
|
+
<div className="rounded-2xl bg-[oklch(0.21_0.012_55)] p-6">
|
|
79
|
+
<Story />
|
|
80
|
+
</div>
|
|
81
|
+
),
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
|
|
62
85
|
export const Brown: Story = {
|
|
63
86
|
args: { children: "Brown", variant: "brown" },
|
|
64
87
|
};
|
|
@@ -36,6 +36,16 @@ const buttonVariants = cva(
|
|
|
36
36
|
*/
|
|
37
37
|
overlay:
|
|
38
38
|
"bg-black/50 text-white hover:bg-black/65 focus-visible:ring-white/70",
|
|
39
|
+
/**
|
|
40
|
+
* The LABELLED action on a photo surface — "Open the menu", "See all
|
|
41
|
+
* 46 bars", "How we read menus" — where `overlay` is the unlabelled
|
|
42
|
+
* chrome. Glass on the ink: translucent white with a hairline, white
|
|
43
|
+
* type. Same reasoning as `overlay`: not theme-aware, because it
|
|
44
|
+
* contrasts with the photograph, not the canvas. Pair with `size="pill"`.
|
|
45
|
+
* Icons trail, never lead: `ArrowRight` when it navigates to a page,
|
|
46
|
+
* `ChevronRight` when it opens a sheet or drawer.
|
|
47
|
+
*/
|
|
48
|
+
ink: "border border-white/20 bg-white/[0.16] text-white hover:bg-white/25 focus-visible:ring-white/70 focus-visible:ring-offset-0",
|
|
39
49
|
brown:
|
|
40
50
|
"bg-brown text-brown-foreground shadow-sm hover:bg-brown/85 hover:shadow",
|
|
41
51
|
antagonist:
|
|
@@ -46,6 +56,8 @@ const buttonVariants = cva(
|
|
|
46
56
|
sm: "h-9 rounded-lg px-4",
|
|
47
57
|
default: "h-11 px-6 py-2",
|
|
48
58
|
lg: "h-12 rounded-2xl px-8",
|
|
59
|
+
/** The `ink` action's shape. Owns the radius so no variant fights it. */
|
|
60
|
+
pill: "h-11 rounded-full px-5 text-[13.5px] font-bold",
|
|
49
61
|
icon: "h-11 w-11 rounded-full",
|
|
50
62
|
"icon-xs": "h-8 w-8 rounded-full [&_svg]:size-3.5",
|
|
51
63
|
"icon-sm": "h-9 w-9 rounded-full",
|
|
@@ -209,7 +209,7 @@ function ColourDictionaryPage() {
|
|
|
209
209
|
<section>
|
|
210
210
|
<h2 className="text-[15px] font-extrabold">Where the values live</h2>
|
|
211
211
|
<p className="mt-2 text-[13px] leading-[1.55] text-foreground/85">
|
|
212
|
-
The design system owns the typography, the falloff and the layering (<code>MenuField</code>, <code>MenuFamilyCard</code>, <code>
|
|
212
|
+
The design system owns the typography, the falloff and the layering (<code>MenuField</code>, <code>MenuFamilyCard</code>, <code>MenuFamilyTiles</code>) and takes a finished colour as a prop. The palette lives in the fe app: the eighteen <code>--menu-field-*</code> tokens in <code>app/globals.css</code>, <code>FAMILY_HUES</code> and <code>INTENT_GRADIENTS</code> in <code>hub-gradients.ts</code>, category → family in <code>category-colors.ts</code>. The Storybook mocks carry the same values so post-generator films what the app paints.
|
|
213
213
|
</p>
|
|
214
214
|
</section>
|
|
215
215
|
</div>
|
|
@@ -79,7 +79,7 @@ export type ConceptCompareFigure = {
|
|
|
79
79
|
export type ConceptCompareLabels = {
|
|
80
80
|
/** Small caps kicker, e.g. "DISH CONCEPT". */
|
|
81
81
|
kind: string;
|
|
82
|
-
/** The badge on the lowest-priced room, e.g. "CHEAPEST"
|
|
82
|
+
/** The badge on the lowest-priced room, e.g. "CHEAPEST" — the fallback when a row carries no `badge`. */
|
|
83
83
|
cheapest: string;
|
|
84
84
|
/** The badge on every other room, e.g. "#2". Defaults to `#${rank}`. */
|
|
85
85
|
rank?: (rank: number) => string;
|
|
@@ -277,7 +277,15 @@ export function ConceptCompare({
|
|
|
277
277
|
>
|
|
278
278
|
{rooms.map((room, index) => {
|
|
279
279
|
const href = hrefs?.[room.place_slug];
|
|
280
|
-
|
|
280
|
+
// A winner room says what it won; a ranked room says its rank.
|
|
281
|
+
// The CHEAPEST badge — and only it — is green with the arrow: it
|
|
282
|
+
// is the census's signature claim, the one badge a diner scans
|
|
283
|
+
// for. Gated on the badge, not the price: drink concepts cluster
|
|
284
|
+
// on price, and a "most named" room at the minimum must not wear
|
|
285
|
+
// the cheapest's glyph.
|
|
286
|
+
const badge = room.badge ?? (room.price === cheapest ? labels.cheapest : rankOf(index + 1));
|
|
287
|
+
const isCheapest = badge === labels.cheapest;
|
|
288
|
+
const lead = room.lead ?? (isCheapest ? "price" : undefined);
|
|
281
289
|
const roomClass = cn(
|
|
282
290
|
/* Transparent on the phone — the card behind it is the room's
|
|
283
291
|
photograph — with only its scrim and its line. A photograph of
|
|
@@ -312,7 +320,7 @@ export function ConceptCompare({
|
|
|
312
320
|
)}
|
|
313
321
|
>
|
|
314
322
|
{isCheapest && <ArrowDown className="h-3 w-3 shrink-0" aria-hidden />}
|
|
315
|
-
{
|
|
323
|
+
{badge}
|
|
316
324
|
</span>
|
|
317
325
|
|
|
318
326
|
{/* Name and rating on one line: the pin leads, the name takes
|
|
@@ -322,11 +330,25 @@ export function ConceptCompare({
|
|
|
322
330
|
<b className="min-w-0 flex-1 truncate text-[15px] font-bold leading-tight">
|
|
323
331
|
{room.place_name}
|
|
324
332
|
</b>
|
|
325
|
-
{
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
{/* The BearMenu grade, in the grade's amber — the number
|
|
334
|
+
the census computed, beside the kitchen it computed it
|
|
335
|
+
for. Google's star only for a kitchen with no grade. */}
|
|
336
|
+
{room.grade ? (
|
|
337
|
+
<span
|
|
338
|
+
className={cn(
|
|
339
|
+
"shrink-0 text-[14px] font-extrabold leading-none tabular-nums text-[oklch(0.82_0.15_85)]",
|
|
340
|
+
lead === "grade" && "text-[16px]"
|
|
341
|
+
)}
|
|
342
|
+
>
|
|
343
|
+
{room.grade}
|
|
329
344
|
</span>
|
|
345
|
+
) : (
|
|
346
|
+
room.rating !== undefined && (
|
|
347
|
+
<span className="flex shrink-0 items-center gap-1 text-[12px] text-white/70">
|
|
348
|
+
<Star className="h-3 w-3 shrink-0" aria-hidden />
|
|
349
|
+
<span className="tabular-nums">{room.rating}</span>
|
|
350
|
+
</span>
|
|
351
|
+
)
|
|
330
352
|
)}
|
|
331
353
|
</span>
|
|
332
354
|
{room.place_area && (
|
|
@@ -343,7 +365,11 @@ export function ConceptCompare({
|
|
|
343
365
|
is a 0.32-lightness grey that vanishes on inked photography. */}
|
|
344
366
|
<Separator className="my-2.5 via-white/20" />
|
|
345
367
|
|
|
346
|
-
|
|
368
|
+
{/* The figures: the one the badge is about leads — the badge
|
|
369
|
+
says what the room won, the number is the evidence. Green
|
|
370
|
+
is the cheapest's alone (a dearest room's price in green
|
|
371
|
+
would read as cheap); the other winners lead in full white. */}
|
|
372
|
+
<span className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
347
373
|
<b
|
|
348
374
|
className={cn(
|
|
349
375
|
"text-[19px] font-bold leading-none tabular-nums tracking-[-0.02em]",
|
|
@@ -353,7 +379,12 @@ export function ConceptCompare({
|
|
|
353
379
|
{formatPrice(room.price)}
|
|
354
380
|
</b>
|
|
355
381
|
{room.mentions !== undefined && room.mentions > 0 && (
|
|
356
|
-
<span
|
|
382
|
+
<span
|
|
383
|
+
className={cn(
|
|
384
|
+
"text-[11.5px]",
|
|
385
|
+
lead === "mentions" ? "font-bold text-white" : "text-white/65"
|
|
386
|
+
)}
|
|
387
|
+
>
|
|
357
388
|
{labels.mentions(room.mentions)}
|
|
358
389
|
</span>
|
|
359
390
|
)}
|
package/src/components/item.tsx
CHANGED
|
@@ -52,63 +52,87 @@ const Item = React.forwardRef<HTMLDivElement, ItemProps>(
|
|
|
52
52
|
);
|
|
53
53
|
Item.displayName = "Item";
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
/**
|
|
56
|
+
* The parts take `asChild` too. An `Item asChild` around a `<button>` makes
|
|
57
|
+
* the parts the button's children, and a button may hold only phrasing
|
|
58
|
+
* content — so the parts have to be able to render as `<span>`s there.
|
|
59
|
+
*/
|
|
60
|
+
type ItemPartProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
61
|
+
asChild?: boolean;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const ItemMedia = React.forwardRef<HTMLDivElement, ItemPartProps>(
|
|
65
|
+
({ className, asChild, ...props }, ref) => {
|
|
66
|
+
const Comp = asChild ? Slot : "div";
|
|
67
|
+
return (
|
|
68
|
+
<Comp
|
|
69
|
+
ref={ref}
|
|
70
|
+
data-slot="item-media"
|
|
71
|
+
className={cn("inline-flex shrink-0 items-center justify-center text-muted-foreground", className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
64
76
|
);
|
|
65
77
|
ItemMedia.displayName = "ItemMedia";
|
|
66
78
|
|
|
67
|
-
const ItemContent = React.forwardRef<HTMLDivElement,
|
|
68
|
-
({ className, ...props }, ref) =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
const ItemContent = React.forwardRef<HTMLDivElement, ItemPartProps>(
|
|
80
|
+
({ className, asChild, ...props }, ref) => {
|
|
81
|
+
const Comp = asChild ? Slot : "div";
|
|
82
|
+
return (
|
|
83
|
+
<Comp
|
|
84
|
+
ref={ref}
|
|
85
|
+
data-slot="item-content"
|
|
86
|
+
className={cn("flex min-w-0 flex-1 flex-col gap-0.5", className)}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
76
91
|
);
|
|
77
92
|
ItemContent.displayName = "ItemContent";
|
|
78
93
|
|
|
79
|
-
const ItemTitle = React.forwardRef<HTMLDivElement,
|
|
80
|
-
({ className, ...props }, ref) =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
const ItemTitle = React.forwardRef<HTMLDivElement, ItemPartProps>(
|
|
95
|
+
({ className, asChild, ...props }, ref) => {
|
|
96
|
+
const Comp = asChild ? Slot : "div";
|
|
97
|
+
return (
|
|
98
|
+
<Comp
|
|
99
|
+
ref={ref}
|
|
100
|
+
data-slot="item-title"
|
|
101
|
+
className={cn("truncate text-sm font-semibold text-foreground", className)}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
88
106
|
);
|
|
89
107
|
ItemTitle.displayName = "ItemTitle";
|
|
90
108
|
|
|
91
|
-
const ItemDescription = React.forwardRef<HTMLDivElement,
|
|
92
|
-
({ className, ...props }, ref) =>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
const ItemDescription = React.forwardRef<HTMLDivElement, ItemPartProps>(
|
|
110
|
+
({ className, asChild, ...props }, ref) => {
|
|
111
|
+
const Comp = asChild ? Slot : "div";
|
|
112
|
+
return (
|
|
113
|
+
<Comp
|
|
114
|
+
ref={ref}
|
|
115
|
+
data-slot="item-description"
|
|
116
|
+
className={cn("truncate text-xs text-muted-foreground", className)}
|
|
117
|
+
{...props}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
100
121
|
);
|
|
101
122
|
ItemDescription.displayName = "ItemDescription";
|
|
102
123
|
|
|
103
|
-
const ItemActions = React.forwardRef<HTMLDivElement,
|
|
104
|
-
({ className, ...props }, ref) =>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
const ItemActions = React.forwardRef<HTMLDivElement, ItemPartProps>(
|
|
125
|
+
({ className, asChild, ...props }, ref) => {
|
|
126
|
+
const Comp = asChild ? Slot : "div";
|
|
127
|
+
return (
|
|
128
|
+
<Comp
|
|
129
|
+
ref={ref}
|
|
130
|
+
data-slot="item-actions"
|
|
131
|
+
className={cn("ml-auto inline-flex shrink-0 items-center gap-1", className)}
|
|
132
|
+
{...props}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
112
136
|
);
|
|
113
137
|
ItemActions.displayName = "ItemActions";
|
|
114
138
|
|
|
@@ -72,6 +72,8 @@ export type KitchenDossierLabels = {
|
|
|
72
72
|
next: string;
|
|
73
73
|
/** Dimension key → translated label. */
|
|
74
74
|
dimensions: Partial<Record<VenueGradeDimensionKey, string>>;
|
|
75
|
+
/** Beside the grade in the title row, e.g. "/10". Omit for the bare numeral. */
|
|
76
|
+
gradeUnit?: string;
|
|
75
77
|
};
|
|
76
78
|
|
|
77
79
|
export type KitchenDossierProps = {
|
|
@@ -202,25 +204,18 @@ export function KitchenDossier({
|
|
|
202
204
|
.sort((a, b) => (dimensions[b] ?? 0) - (dimensions[a] ?? 0))
|
|
203
205
|
.slice(0, DIMENSION_LIMIT)
|
|
204
206
|
: [];
|
|
205
|
-
|
|
207
|
+
// The overall grade leads the title row now, so the part shows what the
|
|
208
|
+
// title cannot: the three strongest dimensions. No numeral repeated.
|
|
209
|
+
if (dimensions && strongest.length > 0) {
|
|
206
210
|
parts.push(
|
|
207
211
|
<Part key="grade" kicker={labels.theGrade}>
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
<VenueGradeDimensions
|
|
216
|
-
className="min-w-0 flex-1"
|
|
217
|
-
dimensions={dimensions}
|
|
218
|
-
labels={labels.dimensions}
|
|
219
|
-
order={strongest}
|
|
220
|
-
formatScore={formatScore}
|
|
221
|
-
/>
|
|
222
|
-
)}
|
|
223
|
-
</div>
|
|
212
|
+
<VenueGradeDimensions
|
|
213
|
+
className="min-w-0"
|
|
214
|
+
dimensions={dimensions}
|
|
215
|
+
labels={labels.dimensions}
|
|
216
|
+
order={strongest}
|
|
217
|
+
formatScore={formatScore}
|
|
218
|
+
/>
|
|
224
219
|
</Part>
|
|
225
220
|
);
|
|
226
221
|
}
|
|
@@ -281,16 +276,42 @@ export function KitchenDossier({
|
|
|
281
276
|
{place.name}
|
|
282
277
|
</LinkComponent>
|
|
283
278
|
</h3>
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
279
|
+
{/* The GRADE leads — the number the rail is named for, in the
|
|
280
|
+
grade's amber. Google's star used to sit here while the
|
|
281
|
+
grade waited in story part three; a card rated by BearMenu
|
|
282
|
+
cannot lead with a number BearMenu did not compute. The
|
|
283
|
+
star drops to the meta line, and the title row only falls
|
|
284
|
+
back to it for a kitchen with no grade. */}
|
|
285
|
+
{grade?.overall ? (
|
|
286
|
+
<span className="inline-flex shrink-0 items-baseline gap-1 text-[22px] font-extrabold leading-none tabular-nums text-[oklch(0.82_0.15_85)] lg:text-[26px]">
|
|
287
|
+
{grade.overall}
|
|
288
|
+
{labels.gradeUnit && (
|
|
289
|
+
<span className="text-[11px] font-bold text-white/70">{labels.gradeUnit}</span>
|
|
290
|
+
)}
|
|
288
291
|
</span>
|
|
292
|
+
) : (
|
|
293
|
+
rating !== undefined && (
|
|
294
|
+
<span className="inline-flex shrink-0 items-center gap-1 text-[18px] font-extrabold leading-none tabular-nums text-white lg:text-[22px]">
|
|
295
|
+
{rating.toFixed(1)}
|
|
296
|
+
<Star className="h-3.5 w-3.5 lg:h-4 lg:w-4" aria-hidden />
|
|
297
|
+
</span>
|
|
298
|
+
)
|
|
289
299
|
)}
|
|
290
300
|
</div>
|
|
291
|
-
{(meta || labels.reviewCount) && (
|
|
292
|
-
<p className="mt-1
|
|
293
|
-
{
|
|
301
|
+
{(meta || labels.reviewCount || (grade?.overall && rating !== undefined)) && (
|
|
302
|
+
<p className="mt-1 flex min-w-0 items-center gap-1 text-[12.5px] text-white/70">
|
|
303
|
+
{(meta || labels.reviewCount) && (
|
|
304
|
+
<span className="truncate">
|
|
305
|
+
{[meta, labels.reviewCount].filter(Boolean).join(" · ")}
|
|
306
|
+
</span>
|
|
307
|
+
)}
|
|
308
|
+
{grade?.overall && rating !== undefined && (
|
|
309
|
+
<span className="inline-flex shrink-0 items-center gap-1 tabular-nums">
|
|
310
|
+
{(meta || labels.reviewCount) && <span aria-hidden>·</span>}
|
|
311
|
+
{rating.toFixed(1)}
|
|
312
|
+
<Star className="h-3 w-3" aria-hidden />
|
|
313
|
+
</span>
|
|
314
|
+
)}
|
|
294
315
|
</p>
|
|
295
316
|
)}
|
|
296
317
|
</div>
|