@bearmenu/ui 0.8.9 → 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-AXV5O2IZ.js → chunk-AKQ57NXY.js} +3 -3
- 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-SY6RV476.js +48 -0
- package/dist/chunk-UDN7UHJW.js +60 -0
- package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/command.d.ts +7 -7
- 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 +75 -38
- package/dist/components/concept-compare.js +209 -133
- 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.js +1 -1
- package/dist/components/event-detail-panel.js +1 -1
- package/dist/components/event-index-row.js +1 -1
- package/dist/components/event-list-row-wide.js +1 -1
- 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.js +11 -1
- 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 +33 -27
- package/dist/components/menu-family-card.js +43 -85
- package/dist/components/menu-field.js +92 -2
- 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-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-kitchen-list.d.ts +82 -0
- package/dist/components/menu-kitchen-list.js +5 -0
- package/dist/components/menu-mocks.d.ts +37 -2
- package/dist/components/menu-mocks.js +152 -27
- package/dist/components/menu-types.d.ts +182 -6
- package/dist/components/motion-icon-controls.js +2 -2
- package/dist/components/motion-icon-set.js +6 -6
- package/dist/components/motion-icon.d.ts +22 -1
- package/dist/components/motion-icon.js +1 -1
- 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 +2 -2
- 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/snap-rail.d.ts +24 -1
- package/dist/components/snap-rail.js +3 -109
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/story.d.ts +34 -0
- package/dist/components/story.js +3 -0
- package/dist/components/text.d.ts +3 -3
- 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 +3 -1
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/weekend-panel.js +2 -2
- package/package.json +1 -1
- package/src/components/concept-compare-rail.tsx +131 -0
- package/src/components/concept-compare.stories.tsx +122 -54
- package/src/components/concept-compare.tsx +363 -196
- 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/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.tsx +11 -1
- 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 +43 -35
- package/src/components/menu-family-card.tsx +122 -142
- 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-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-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 +198 -23
- package/src/components/menu-types.ts +189 -6
- package/src/components/motion-icon-controls.tsx +11 -11
- package/src/components/motion-icon-set.tsx +6 -5
- package/src/components/motion-icon.test.tsx +23 -1
- package/src/components/motion-icon.tsx +20 -0
- package/src/components/photo-ground.tsx +147 -0
- package/src/components/place-types.ts +2 -2
- package/src/components/qa-block.stories.tsx +43 -0
- package/src/components/qa-block.tsx +60 -0
- package/src/components/snap-rail.tsx +58 -15
- 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.tsx +2 -2
- package/dist/chunk-THOVPZNN.js +0 -37
- package/dist/chunk-YS6BHKZO.js +0 -92
- package/dist/components/ranked-venue-list.d.ts +0 -49
- package/dist/components/ranked-venue-list.js +0 -87
- package/dist/components/signature-line-list.d.ts +0 -45
- package/dist/components/signature-line-list.js +0 -99
- package/dist/components/venue-busyness-strip.d.ts +0 -35
- package/dist/components/venue-busyness-strip.js +0 -37
- package/dist/components/venue-signature-block.d.ts +0 -39
- package/dist/components/venue-signature-block.js +0 -6
- package/dist/components/venue-spotlight.d.ts +0 -74
- package/dist/components/venue-spotlight.js +0 -138
- package/src/components/ranked-venue-list.stories.tsx +0 -80
- package/src/components/ranked-venue-list.tsx +0 -177
- package/src/components/signature-line-list.stories.tsx +0 -85
- package/src/components/signature-line-list.tsx +0 -136
- package/src/components/venue-busyness-strip.stories.tsx +0 -66
- package/src/components/venue-busyness-strip.tsx +0 -64
- package/src/components/venue-signature-block.stories.tsx +0 -68
- package/src/components/venue-signature-block.tsx +0 -71
- package/src/components/venue-spotlight.stories.tsx +0 -141
- package/src/components/venue-spotlight.tsx +0 -236
- package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GlassGround — a frosted, inked caption rising out of one edge of a
|
|
5
|
+
* photograph.
|
|
6
|
+
*
|
|
7
|
+
* Layers over a `PhotoGround`: two masked `backdrop-blur`s (the frost) and
|
|
8
|
+
* an ink gradient (the contrast), running in one direction — absent where the
|
|
9
|
+
* photograph is the subject, solid where the type is. `MenuFamilyBlock` drew
|
|
10
|
+
* this first; this is the same idea with the stops as data, so a card can say
|
|
11
|
+
* "clear for 128px, then glass" without a sixth copy of the gradient strings.
|
|
12
|
+
*
|
|
13
|
+
* ## Why two blurs, and why the ramps are eased
|
|
14
|
+
*
|
|
15
|
+
* One blur under a linear mask has an edge: the eye reads the line where
|
|
16
|
+
* sharp becomes soft, however gently the mask fades it in. Two blurs — a
|
|
17
|
+
* light one that starts early and a heavier one that starts late — make the
|
|
18
|
+
* softening itself a gradient (the "progressive blur" of a phone's lock
|
|
19
|
+
* screen), and every ramp here is an ease-in through five stops rather than
|
|
20
|
+
* a straight line, because a straight ramp has a visible crease at each end
|
|
21
|
+
* where its slope changes.
|
|
22
|
+
*
|
|
23
|
+
* ## Why the stops are custom properties
|
|
24
|
+
*
|
|
25
|
+
* Tailwind emits only the classes it can read verbatim, so the gradients
|
|
26
|
+
* cannot be assembled from props into a class. The classes below are literal
|
|
27
|
+
* and read `--glass-*` variables; the component writes those inline, having
|
|
28
|
+
* interpolated the quarter stops in JS (`clear` and `full` must share a
|
|
29
|
+
* unit). Same mechanism as `HueChip`'s hue. The ink's colour is the
|
|
30
|
+
* package's one scrim literal, `oklch(0.16 0.012 55)`, never `var(--scrim)`.
|
|
31
|
+
*
|
|
32
|
+
* ## The ramp ends where the type begins
|
|
33
|
+
*
|
|
34
|
+
* `clear` is where the frost starts, `full` where it and the ink are solid.
|
|
35
|
+
* Set `full` to the exact seam the content starts at — a ramp that runs past
|
|
36
|
+
* the seam puts the first row of type on half-frost.
|
|
37
|
+
*/
|
|
38
|
+
type GlassStops = {
|
|
39
|
+
/** Which way the ink runs — toward the type. */
|
|
40
|
+
direction: "to bottom" | "to right";
|
|
41
|
+
/** Where the frost begins, e.g. `"0px"` or `"24%"`. */
|
|
42
|
+
clear: string;
|
|
43
|
+
/** Where frost and ink are solid — the seam the content starts at. Same unit as `clear`. */
|
|
44
|
+
full: string;
|
|
45
|
+
};
|
|
46
|
+
type GlassGroundProps = {
|
|
47
|
+
stops: GlassStops;
|
|
48
|
+
/** Stops from `lg`. Omit to keep the same ramp at every width. */
|
|
49
|
+
lgStops?: GlassStops;
|
|
50
|
+
/**
|
|
51
|
+
* How dark the ink gets at the far edge, 0–1. `0.96` is a caption the
|
|
52
|
+
* photograph is only a texture under; `0.4` is a photograph the caption is
|
|
53
|
+
* drawn on — the type is lifted, the room stays a room.
|
|
54
|
+
*/
|
|
55
|
+
peak?: number;
|
|
56
|
+
/**
|
|
57
|
+
* How much the frost softens the photograph where it is solid. `xl` turns
|
|
58
|
+
* a room into colour; `sm` keeps it a room and only takes the edge off the
|
|
59
|
+
* detail behind the type.
|
|
60
|
+
*/
|
|
61
|
+
blur?: "sm" | "md" | "lg" | "xl";
|
|
62
|
+
className?: string;
|
|
63
|
+
};
|
|
64
|
+
declare function GlassGround({ stops, lgStops, peak, blur, className, }: GlassGroundProps): react_jsx_runtime.JSX.Element;
|
|
65
|
+
|
|
66
|
+
export { GlassGround, type GlassGroundProps, type GlassStops };
|
|
@@ -9,7 +9,17 @@ function HubSurface({ flush, className, children }) {
|
|
|
9
9
|
className: cn(
|
|
10
10
|
"rounded-[18px] border border-border bg-card shadow-none",
|
|
11
11
|
flush ? "overflow-hidden" : "px-3.5 py-4",
|
|
12
|
-
|
|
12
|
+
/*
|
|
13
|
+
* `lg:border-0` is load-bearing and was missing.
|
|
14
|
+
*
|
|
15
|
+
* Above `lg` this surface stops being a card — the background, the
|
|
16
|
+
* radius, the padding and the shadow are all dropped so the section
|
|
17
|
+
* sits on the page. The border was not, so every section on every
|
|
18
|
+
* desktop page kept a 1px rectangle around content that no longer had
|
|
19
|
+
* a card behind it. A caller that genuinely wants an edge at desktop
|
|
20
|
+
* adds `lg:border` in `className`, which lands after this in `cn`.
|
|
21
|
+
*/
|
|
22
|
+
"lg:overflow-visible lg:rounded-none lg:border-0 lg:bg-transparent lg:p-0 lg:shadow-none",
|
|
13
23
|
className
|
|
14
24
|
),
|
|
15
25
|
children
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HueChip — a pill that wears a category's colour as a pale wash.
|
|
5
|
+
*
|
|
6
|
+
* Menu surfaces label things with a family's hue: a cuisine's dish groups, a
|
|
7
|
+
* going-out family's tags, a price band. Every one of them was an inline
|
|
8
|
+
* `style` with a hand-written `color-mix` next to a hand-written pill class
|
|
9
|
+
* list, which is how the two below drifted apart and how the mix space bug
|
|
10
|
+
* survived as long as it did — it had to be found and fixed in each copy.
|
|
11
|
+
*
|
|
12
|
+
* ## The mix space is the whole reason this is a component
|
|
13
|
+
*
|
|
14
|
+
* **`in oklab`, and never `in oklch`.** They describe the same colours, but
|
|
15
|
+
* oklch is POLAR, so `color-mix(in oklch, …)` interpolates the hue ANGLE — and
|
|
16
|
+
* the paper being mixed into is achromatic (`--card` is `oklch(1 0 0)`, hue 0).
|
|
17
|
+
* Mixing 20% of a family colour into that lands at a fifth of the family's hue:
|
|
18
|
+
* honey 82° came out at 16°, which is pink, so a Breakfast & Brunch card wore
|
|
19
|
+
* pink chips under an amber mosaic. Cool families were worse — a bar's 205°
|
|
20
|
+
* blue mixes to 41°, orange; a club's 300° violet to 60°, mustard.
|
|
21
|
+
*
|
|
22
|
+
* oklab is rectangular: mixing toward an achromatic base scales chroma along
|
|
23
|
+
* the SAME hue instead of rotating it, which is what "a pale version of this
|
|
24
|
+
* colour" has to mean.
|
|
25
|
+
*
|
|
26
|
+
* ## Why an inline style and not a Tailwind class
|
|
27
|
+
*
|
|
28
|
+
* The hue is DATA — one of ~40 cuisine values resolved by the app at request
|
|
29
|
+
* time — not one of a fixed set of variants. Tailwind can only emit classes it
|
|
30
|
+
* can see at build time, so a class-based version would mean either a
|
|
31
|
+
* safelisted colour per cuisine (a palette that must be edited to add a
|
|
32
|
+
* cuisine) or an arbitrary-value class built from a runtime string, which
|
|
33
|
+
* Tailwind cannot compile. A CSS custom property set inline is the honest
|
|
34
|
+
* mechanism; this component is the one place that writes it.
|
|
35
|
+
*/
|
|
36
|
+
/** Mix strengths, so callers say what a chip IS rather than picking a number. */
|
|
37
|
+
declare const STRENGTH: {
|
|
38
|
+
/** A labelled facet: readable at 11.5px, still clearly the family's colour. */
|
|
39
|
+
readonly chip: 20;
|
|
40
|
+
/** A quieter wash, for a chip that sits beside a stronger one. */
|
|
41
|
+
readonly subtle: 12;
|
|
42
|
+
};
|
|
43
|
+
type HueChipTone = keyof typeof STRENGTH;
|
|
44
|
+
/**
|
|
45
|
+
* The tint itself, exported for the surfaces that need the colour rather than
|
|
46
|
+
* the pill — a section ground, a hover state. Same rule: `in oklab`.
|
|
47
|
+
*/
|
|
48
|
+
declare function hueTint(hue: string, tone?: HueChipTone, base?: string): string;
|
|
49
|
+
type HueChipProps = {
|
|
50
|
+
/**
|
|
51
|
+
* A finished CSS colour — the DS owns no category table, so the app resolves
|
|
52
|
+
* it. Without one the chip falls back to `--muted`, which is the right
|
|
53
|
+
* neutral for a facet that has no family behind it.
|
|
54
|
+
*/
|
|
55
|
+
hue?: string;
|
|
56
|
+
tone?: HueChipTone;
|
|
57
|
+
/** The paper the wash is mixed into. Must be the surface the chip sits on. */
|
|
58
|
+
base?: string;
|
|
59
|
+
/** Drawn before the label, e.g. a wallet on a price band. */
|
|
60
|
+
icon?: React.ReactNode;
|
|
61
|
+
/** `li` inside a list, which is where most of these live. */
|
|
62
|
+
as?: "span" | "li" | "div";
|
|
63
|
+
children: React.ReactNode;
|
|
64
|
+
className?: string;
|
|
65
|
+
/** For a chip whose visible text is a bare figure, e.g. "22–86 lei". */
|
|
66
|
+
"aria-label"?: string;
|
|
67
|
+
};
|
|
68
|
+
declare function HueChip({ hue, tone, base, icon, as: Tag, children, className, "aria-label": ariaLabel, }: HueChipProps): react_jsx_runtime.JSX.Element;
|
|
69
|
+
|
|
70
|
+
export { HueChip, type HueChipProps, type HueChipTone, hueTint };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { MenuVenueSignature, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
4
|
+
import { PlacePreview, CitedText, PlaceGradeDimensions } from './place-types.js';
|
|
5
|
+
import { VenueGradeDimensionKey } from './venue-grade-dimensions.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* KitchenDossier — one kitchen, told as a story on its own photograph.
|
|
9
|
+
*
|
|
10
|
+
* The hub used to say this three times: a spotlight with everything the
|
|
11
|
+
* digest knows, a ranked row with a grade, a card with the signature dish —
|
|
12
|
+
* and the same kitchen led all three. This is the one card: the venue's
|
|
13
|
+
* photograph clear at the top, a frosted caption rising from the foot with
|
|
14
|
+
* the identity (rating, name, type · area · reviews), and under it a PANEL
|
|
15
|
+
* that says the three or four things the reviews established one at a time —
|
|
16
|
+
* the dish it is known for, what people say, the grade — with segments at the
|
|
17
|
+
* top edge showing which is up. The story runs on its own while the card is
|
|
18
|
+
* in view; a tap advances.
|
|
19
|
+
*
|
|
20
|
+
* ## A photograph with a caption, not a dark card
|
|
21
|
+
*
|
|
22
|
+
* `ConceptCompare` is a dark card with a photograph showing through two glass
|
|
23
|
+
* bands. This is the reverse — the room is the subject and the facts are its
|
|
24
|
+
* caption — which is `GlassGround` at its lightest and smallest: anchored to
|
|
25
|
+
* the CAPTION, not the card, so it covers the identity, the panel and the
|
|
26
|
+
* action plus a short lead above them and nothing else; a `sm` blur that
|
|
27
|
+
* only takes the edge off the detail; ink that never passes 0.45; a text
|
|
28
|
+
* shadow under the identity doing the rest. Everything above the caption is
|
|
29
|
+
* the photograph, untouched. The panel is its own glass, so its rule reads
|
|
30
|
+
* as a panel and not a hole. The same layout at every width; the desktop
|
|
31
|
+
* card is wider, not turned on its side.
|
|
32
|
+
*
|
|
33
|
+
* ## Two targets, and everything else is "next"
|
|
34
|
+
*
|
|
35
|
+
* The name and the action open the venue; the dish name in the first part
|
|
36
|
+
* opens the item. A tap anywhere else on the card advances the story.
|
|
37
|
+
*/
|
|
38
|
+
/** @deprecated Alias of `MenuLinkProps`; kept for symmetry with the other menu surfaces. */
|
|
39
|
+
type KitchenDossierLinkProps = MenuLinkProps;
|
|
40
|
+
type KitchenDossierLabels = {
|
|
41
|
+
/** Kickers over the three parts. */
|
|
42
|
+
knownFor: string;
|
|
43
|
+
whatPeopleSay: string;
|
|
44
|
+
theGrade: string;
|
|
45
|
+
/** Currency beside the price, e.g. "lei". */
|
|
46
|
+
currency: string;
|
|
47
|
+
/** Beside the mention count, e.g. "4 name it". */
|
|
48
|
+
mentions: (count: number) => string;
|
|
49
|
+
/** "427 reviews", composed by the caller. */
|
|
50
|
+
reviewCount?: string;
|
|
51
|
+
/** "Open until 22:00", composed by the caller. Omit when closed or unknown. */
|
|
52
|
+
openUntil?: string;
|
|
53
|
+
/** Accessible name per segment, e.g. "Part 2 of 3". */
|
|
54
|
+
part: (index: number, count: number) => string;
|
|
55
|
+
/** Accessible name of the tap-to-advance. */
|
|
56
|
+
next: string;
|
|
57
|
+
/** Dimension key → translated label. */
|
|
58
|
+
dimensions: Partial<Record<VenueGradeDimensionKey, string>>;
|
|
59
|
+
};
|
|
60
|
+
type KitchenDossierProps = {
|
|
61
|
+
place: PlacePreview;
|
|
62
|
+
/** The venue's destination. */
|
|
63
|
+
href: string;
|
|
64
|
+
/** "Restaurant · Centru", composed by the caller. */
|
|
65
|
+
meta?: string;
|
|
66
|
+
/** Part one. */
|
|
67
|
+
signature?: MenuVenueSignature;
|
|
68
|
+
/** Part two — the digest's highlights; two are shown. */
|
|
69
|
+
quotes?: CitedText[];
|
|
70
|
+
/** Part three — the grade, with the overall already formatted. */
|
|
71
|
+
grade?: {
|
|
72
|
+
overall?: string;
|
|
73
|
+
dimensions?: PlaceGradeDimensions;
|
|
74
|
+
};
|
|
75
|
+
labels: KitchenDossierLabels;
|
|
76
|
+
formatPrice: (amount: number) => string;
|
|
77
|
+
/** 0–100 → display numeral. */
|
|
78
|
+
formatScore: (value: number) => string;
|
|
79
|
+
/** How long each part shows. */
|
|
80
|
+
partDurationMs?: number;
|
|
81
|
+
/** The foot's action — pass a rendered link or button. */
|
|
82
|
+
action?: ReactNode;
|
|
83
|
+
linkComponent?: MenuLinkComponent;
|
|
84
|
+
className?: string;
|
|
85
|
+
};
|
|
86
|
+
declare function KitchenDossier({ place, href, meta, signature, quotes, grade, labels, formatPrice, formatScore, partDurationMs, action, linkComponent: LinkComponent, className, }: KitchenDossierProps): react_jsx_runtime.JSX.Element;
|
|
87
|
+
|
|
88
|
+
export { KitchenDossier, type KitchenDossierLabels, type KitchenDossierLinkProps, type KitchenDossierProps };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { DIMENSION_ORDER, VenueGradeDimensions } from '../chunk-AKQ57NXY.js';
|
|
2
|
+
import { VenueQuotes } from '../chunk-62ES6N32.js';
|
|
3
|
+
import { StoryProvider, StorySegments, StoryPanel } from '../chunk-F6FMZJDG.js';
|
|
4
|
+
import { GlassGround } from '../chunk-SY6RV476.js';
|
|
5
|
+
import { Separator } from '../chunk-WSRWFA6K.js';
|
|
6
|
+
import { PhotoGround, PHOTO_GROUND } from '../chunk-5RWTOT5Y.js';
|
|
7
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
8
|
+
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
9
|
+
import { MessageSquareQuote, Star } from 'lucide-react';
|
|
10
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
11
|
+
|
|
12
|
+
function DefaultLink(_a) {
|
|
13
|
+
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
14
|
+
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
15
|
+
}
|
|
16
|
+
var GLASS_LEAD = "128px";
|
|
17
|
+
var DIMENSION_LIMIT = 3;
|
|
18
|
+
var QUOTE_LIMIT = 2;
|
|
19
|
+
var initialOf = (name) => {
|
|
20
|
+
var _a, _b;
|
|
21
|
+
return ((_b = (_a = name.match(new RegExp("\\p{L}", "u"))) == null ? void 0 : _a[0]) != null ? _b : "").toLocaleUpperCase();
|
|
22
|
+
};
|
|
23
|
+
function KitchenDossier({
|
|
24
|
+
place,
|
|
25
|
+
href,
|
|
26
|
+
meta,
|
|
27
|
+
signature,
|
|
28
|
+
quotes,
|
|
29
|
+
grade,
|
|
30
|
+
labels,
|
|
31
|
+
formatPrice,
|
|
32
|
+
formatScore,
|
|
33
|
+
partDurationMs,
|
|
34
|
+
action,
|
|
35
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
36
|
+
className
|
|
37
|
+
}) {
|
|
38
|
+
var _a, _b, _c, _d;
|
|
39
|
+
const rating = (_a = place.rating) == null ? void 0 : _a.rating;
|
|
40
|
+
const parts = [];
|
|
41
|
+
if (signature) {
|
|
42
|
+
const item = signature.item;
|
|
43
|
+
const firstEntry = (_b = item.sizes_and_prices) == null ? void 0 : _b[0];
|
|
44
|
+
const mentions = (_d = (_c = item.mentions) != null ? _c : item.review_count) != null ? _d : 0;
|
|
45
|
+
parts.push(
|
|
46
|
+
/* @__PURE__ */ jsxs(Part, { kicker: labels.knownFor, children: [
|
|
47
|
+
signature.href ? /* @__PURE__ */ jsx(
|
|
48
|
+
LinkComponent,
|
|
49
|
+
{
|
|
50
|
+
href: signature.href,
|
|
51
|
+
className: "w-max max-w-full text-[16px] font-bold leading-tight tracking-[-0.01em] text-white hover:underline focus-visible:outline-none focus-visible:underline",
|
|
52
|
+
children: item.name
|
|
53
|
+
}
|
|
54
|
+
) : /* @__PURE__ */ jsx("span", { className: "text-[16px] font-bold leading-tight tracking-[-0.01em] text-white", children: item.name }),
|
|
55
|
+
signature.why && /* @__PURE__ */ jsx("p", { className: "mt-1 line-clamp-2 font-serif text-[13px] italic leading-snug text-white/75", children: signature.why }),
|
|
56
|
+
(firstEntry || mentions > 0) && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
57
|
+
/* @__PURE__ */ jsx(Separator, { className: "my-2.5 via-white/20" }),
|
|
58
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-end justify-between gap-3", children: [
|
|
59
|
+
/* @__PURE__ */ jsx("span", { className: "flex items-baseline gap-1.5", children: firstEntry && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
60
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[18px] font-extrabold leading-none tabular-nums text-white", children: [
|
|
61
|
+
formatPrice(firstEntry.price),
|
|
62
|
+
/* @__PURE__ */ jsx("span", { className: "ml-1 text-[12px] font-bold", children: labels.currency })
|
|
63
|
+
] }),
|
|
64
|
+
firstEntry.size > 0 && /* @__PURE__ */ jsxs("span", { className: "text-[11.5px] tabular-nums text-white/60", children: [
|
|
65
|
+
firstEntry.size,
|
|
66
|
+
item.kind === "dish" ? "g" : "ml"
|
|
67
|
+
] })
|
|
68
|
+
] }) }),
|
|
69
|
+
mentions > 0 && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-[12px] font-semibold tabular-nums text-white/85", children: [
|
|
70
|
+
/* @__PURE__ */ jsx(MessageSquareQuote, { className: "h-3.5 w-3.5", "aria-hidden": true }),
|
|
71
|
+
labels.mentions(mentions)
|
|
72
|
+
] })
|
|
73
|
+
] })
|
|
74
|
+
] })
|
|
75
|
+
] }, "signature")
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (quotes && quotes.length > 0) {
|
|
79
|
+
parts.push(
|
|
80
|
+
/* @__PURE__ */ jsx(Part, { kicker: labels.whatPeopleSay, children: /* @__PURE__ */ jsx(VenueQuotes, { quotes, limit: QUOTE_LIMIT, className: "lg:grid-cols-1" }) }, "quotes")
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const dimensions = grade == null ? void 0 : grade.dimensions;
|
|
84
|
+
const strongest = dimensions ? DIMENSION_ORDER.filter((key) => typeof dimensions[key] === "number").sort((a, b) => {
|
|
85
|
+
var _a2, _b2;
|
|
86
|
+
return ((_a2 = dimensions[b]) != null ? _a2 : 0) - ((_b2 = dimensions[a]) != null ? _b2 : 0);
|
|
87
|
+
}).slice(0, DIMENSION_LIMIT) : [];
|
|
88
|
+
if (grade && (grade.overall || strongest.length > 0)) {
|
|
89
|
+
parts.push(
|
|
90
|
+
/* @__PURE__ */ jsx(Part, { kicker: labels.theGrade, children: /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-4", children: [
|
|
91
|
+
grade.overall && /* @__PURE__ */ jsx("span", { className: "text-[34px] font-extrabold leading-none tracking-[-0.03em] tabular-nums text-[oklch(0.82_0.15_85)]", children: grade.overall }),
|
|
92
|
+
dimensions && strongest.length > 0 && /* @__PURE__ */ jsx(
|
|
93
|
+
VenueGradeDimensions,
|
|
94
|
+
{
|
|
95
|
+
className: "min-w-0 flex-1",
|
|
96
|
+
dimensions,
|
|
97
|
+
labels: labels.dimensions,
|
|
98
|
+
order: strongest,
|
|
99
|
+
formatScore
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
] }) }, "grade")
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return /* @__PURE__ */ jsxs(
|
|
106
|
+
"article",
|
|
107
|
+
{
|
|
108
|
+
className: cn(
|
|
109
|
+
/* `flex flex-col` so the story fills the card's height: in a rail
|
|
110
|
+
the cards are stretched to the tallest, and a story that stopped
|
|
111
|
+
at its own min-height left a band of bare photograph under the
|
|
112
|
+
action on every shorter card. */
|
|
113
|
+
"dark relative isolate flex flex-col overflow-hidden rounded-[18px] border border-border text-foreground shadow-[var(--shadow-card)]",
|
|
114
|
+
PHOTO_GROUND,
|
|
115
|
+
className
|
|
116
|
+
),
|
|
117
|
+
children: [
|
|
118
|
+
/* @__PURE__ */ jsx(PhotoGround, { className: "-z-20", photos: [place.thumbnail_url], initials: initialOf(place.name), loading: "eager" }),
|
|
119
|
+
/* @__PURE__ */ jsxs(
|
|
120
|
+
StoryProvider,
|
|
121
|
+
{
|
|
122
|
+
count: parts.length,
|
|
123
|
+
durationMs: partDurationMs,
|
|
124
|
+
nextLabel: labels.next,
|
|
125
|
+
className: "relative flex min-h-[560px] flex-1 flex-col p-4 lg:min-h-[440px] lg:p-5",
|
|
126
|
+
children: [
|
|
127
|
+
/* @__PURE__ */ jsx(
|
|
128
|
+
StorySegments,
|
|
129
|
+
{
|
|
130
|
+
labels: parts.map((_, index) => labels.part(index, parts.length)),
|
|
131
|
+
className: "-mt-1"
|
|
132
|
+
}
|
|
133
|
+
),
|
|
134
|
+
/* @__PURE__ */ jsx("div", { className: "relative min-h-[184px] flex-1", children: labels.openUntil && /* @__PURE__ */ jsxs("span", { className: "absolute left-0 top-3 inline-flex h-[30px] items-center gap-[7px] rounded-full bg-[oklch(0.16_0.012_55_/_0.72)] px-3 text-[11.5px] font-bold text-white backdrop-blur-[2px]", children: [
|
|
135
|
+
/* @__PURE__ */ jsx("i", { className: "h-[7px] w-[7px] rounded-full bg-success", "aria-hidden": true }),
|
|
136
|
+
labels.openUntil
|
|
137
|
+
] }) }),
|
|
138
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex flex-col gap-3.5", children: [
|
|
139
|
+
/* @__PURE__ */ jsx(
|
|
140
|
+
"div",
|
|
141
|
+
{
|
|
142
|
+
"aria-hidden": true,
|
|
143
|
+
className: "pointer-events-none absolute -inset-x-4 -bottom-4 -top-32 -z-10 lg:-inset-x-5 lg:-bottom-5",
|
|
144
|
+
children: /* @__PURE__ */ jsx(
|
|
145
|
+
GlassGround,
|
|
146
|
+
{
|
|
147
|
+
stops: { direction: "to bottom", clear: "0px", full: GLASS_LEAD },
|
|
148
|
+
peak: 0.4,
|
|
149
|
+
blur: "sm"
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
/* @__PURE__ */ jsxs("div", { className: "[text-shadow:0_1px_12px_oklch(0_0_0/0.55)]", children: [
|
|
155
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-4", children: [
|
|
156
|
+
/* @__PURE__ */ jsx("h3", { className: "min-w-0 font-serif text-[26px] font-semibold leading-tight tracking-[-0.02em] text-white lg:text-[32px]", children: /* @__PURE__ */ jsx(LinkComponent, { href, className: "focus-visible:outline-none focus-visible:underline", children: place.name }) }),
|
|
157
|
+
rating !== void 0 && /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 text-[18px] font-extrabold leading-none tabular-nums text-white lg:text-[22px]", children: [
|
|
158
|
+
rating.toFixed(1),
|
|
159
|
+
/* @__PURE__ */ jsx(Star, { className: "h-3.5 w-3.5 lg:h-4 lg:w-4", "aria-hidden": true })
|
|
160
|
+
] })
|
|
161
|
+
] }),
|
|
162
|
+
(meta || labels.reviewCount) && /* @__PURE__ */ jsx("p", { className: "mt-1 truncate text-[12.5px] text-white/70", children: [meta, labels.reviewCount].filter(Boolean).join(" \xB7 ") })
|
|
163
|
+
] }),
|
|
164
|
+
parts.length > 0 && /* @__PURE__ */ jsx(
|
|
165
|
+
StoryPanel,
|
|
166
|
+
{
|
|
167
|
+
parts,
|
|
168
|
+
className: "rounded-[14px] border border-white/10 bg-[oklch(0.16_0.012_55_/_0.35)] p-4 backdrop-blur-md"
|
|
169
|
+
}
|
|
170
|
+
),
|
|
171
|
+
action && /* @__PURE__ */ jsx("div", { className: "flex [&>*]:w-full lg:[&>*]:w-auto", children: action })
|
|
172
|
+
] })
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
function Part({ kicker, children }) {
|
|
181
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
182
|
+
/* @__PURE__ */ jsx("span", { className: "mb-1.5 block text-[9.5px] font-bold uppercase tracking-[0.12em] text-white/60", children: kicker }),
|
|
183
|
+
children
|
|
184
|
+
] });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { KitchenDossier };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { MenuLinkComponent, MenuLoosenOption } from './menu-types.js';
|
|
3
|
+
import 'react';
|
|
4
|
+
import './place-types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MenuDeadEnd — what dropped out, and the one press that brings it back.
|
|
8
|
+
*
|
|
9
|
+
* A filter combination that returns nothing is the intent page's worst state,
|
|
10
|
+
* and an empty-state illustration is the worst answer to it: the reader knows
|
|
11
|
+
* the list is empty, what they do not know is WHICH of their four filters did
|
|
12
|
+
* it. So this says what dropped out, why, and what widening one specific filter
|
|
13
|
+
* would restore — with the count, because "+5 kitchens, including the
|
|
14
|
+
* highest-rated Italian room in the city" is an answer and "try removing a
|
|
15
|
+
* filter" is a shrug.
|
|
16
|
+
*
|
|
17
|
+
* It is also drawn on a NON-empty list, whenever a filter has cost the reader
|
|
18
|
+
* something notable. The board's case is 34 kitchens still standing and Casa
|
|
19
|
+
* Boema — the one they came for — missing.
|
|
20
|
+
*
|
|
21
|
+
* Interactive: the consumer marks its own module `"use client"`.
|
|
22
|
+
*/
|
|
23
|
+
type MenuDeadEndProps = {
|
|
24
|
+
/**
|
|
25
|
+
* The explanation, composed by the caller.
|
|
26
|
+
*
|
|
27
|
+
* A node rather than a string because it emphasises real names ("**Casa
|
|
28
|
+
* Boema** and six others dropped out"), and assembling that from fragments
|
|
29
|
+
* here would produce translated word-salad in one of the two locales.
|
|
30
|
+
*/
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
/** The widening this offers, e.g. "Raise to 60 lei · 16 kitchens". */
|
|
33
|
+
actionLabel?: string;
|
|
34
|
+
/** Where the widening leads. Preferred over `onAction` — see `MenuLoosenOption.href`. */
|
|
35
|
+
actionHref?: string;
|
|
36
|
+
onAction?: () => void;
|
|
37
|
+
linkComponent?: MenuLinkComponent;
|
|
38
|
+
className?: string;
|
|
39
|
+
};
|
|
40
|
+
declare function MenuDeadEnd({ children, actionLabel, actionHref, onAction, linkComponent: LinkComponent, className, }: MenuDeadEndProps): react_jsx_runtime.JSX.Element;
|
|
41
|
+
/**
|
|
42
|
+
* MenuLoosenList — one row per active filter, each priced in kitchens.
|
|
43
|
+
*
|
|
44
|
+
* The companion to the panel above: the panel names the single best widening,
|
|
45
|
+
* this lists every one of them so the reader can choose which constraint they
|
|
46
|
+
* care least about. Every row states what it would return, because a filter the
|
|
47
|
+
* reader cannot price is a filter they will not drop.
|
|
48
|
+
*/
|
|
49
|
+
type MenuLoosenListProps = {
|
|
50
|
+
options: MenuLoosenOption[];
|
|
51
|
+
linkComponent?: MenuLinkComponent;
|
|
52
|
+
className?: string;
|
|
53
|
+
};
|
|
54
|
+
declare function MenuLoosenList({ options, linkComponent: LinkComponent, className, }: MenuLoosenListProps): react_jsx_runtime.JSX.Element;
|
|
55
|
+
|
|
56
|
+
export { MenuDeadEnd, type MenuDeadEndProps, MenuLoosenList, type MenuLoosenListProps };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
+
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
3
|
+
import { Info } from 'lucide-react';
|
|
4
|
+
import { jsxs, jsx, Fragment } 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 MenuDeadEnd({
|
|
11
|
+
children,
|
|
12
|
+
actionLabel,
|
|
13
|
+
actionHref,
|
|
14
|
+
onAction,
|
|
15
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
16
|
+
className
|
|
17
|
+
}) {
|
|
18
|
+
const actionClass = "inline-flex h-7 shrink-0 items-center rounded-full bg-foreground px-3 text-[12.5px] font-bold text-background transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 motion-reduce:transition-none lg:ml-auto";
|
|
19
|
+
return /* @__PURE__ */ jsxs(
|
|
20
|
+
"div",
|
|
21
|
+
{
|
|
22
|
+
className: cn(
|
|
23
|
+
"mt-4 flex flex-wrap items-center gap-3 rounded-[14px] bg-muted p-3.5 lg:flex-nowrap lg:gap-4 lg:px-[18px] lg:py-4",
|
|
24
|
+
className
|
|
25
|
+
),
|
|
26
|
+
children: [
|
|
27
|
+
/* @__PURE__ */ jsx(Info, { className: "h-[18px] w-[18px] shrink-0 text-muted-foreground", "aria-hidden": true }),
|
|
28
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-[560px] text-[13px] leading-[1.55] text-muted-foreground [&_b]:font-bold [&_b]:text-foreground", children }),
|
|
29
|
+
actionLabel && (actionHref ? /* @__PURE__ */ jsx(LinkComponent, { href: actionHref, className: actionClass, children: actionLabel }) : onAction && /* @__PURE__ */ jsx("button", { type: "button", onClick: onAction, className: actionClass, children: actionLabel }))
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function MenuLoosenList({
|
|
35
|
+
options,
|
|
36
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
37
|
+
className
|
|
38
|
+
}) {
|
|
39
|
+
const rowClass = "inline-flex h-9 items-center justify-between gap-3 rounded-full border border-border bg-card px-4 text-sm font-semibold transition-colors hover:border-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none";
|
|
40
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col gap-2.5", className), children: options.map((option) => {
|
|
41
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
42
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: option.label }),
|
|
43
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 font-bold tabular-nums text-accent", children: option.gainLabel })
|
|
44
|
+
] });
|
|
45
|
+
return option.href ? /* @__PURE__ */ jsx(LinkComponent, { href: option.href, className: rowClass, children: body }, option.key) : /* @__PURE__ */ jsx("button", { type: "button", onClick: option.onSelect, className: rowClass, children: body }, option.key);
|
|
46
|
+
}) });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { MenuDeadEnd, MenuLoosenList };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { MenuKitchenListLabels } from './menu-kitchen-list.js';
|
|
3
|
+
import { MenuFamilyBlockPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
4
|
+
import 'react';
|
|
5
|
+
import './place-types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* MenuFamilyBlock — the cuisines index's family card. The mosaic is the ground.
|
|
9
|
+
*
|
|
10
|
+
* ## Three layers, and everything floats on them
|
|
11
|
+
*
|
|
12
|
+
* The grid of venue photographs is the background for the ENTIRE card, edge to
|
|
13
|
+
* edge, and the facts sit on it. Three layers do the work:
|
|
14
|
+
*
|
|
15
|
+
* 1. **The mosaic**, full-bleed, wearing the family's hue through a
|
|
16
|
+
* `mix-blend-color` layer.
|
|
17
|
+
* 2. **A masked backdrop-filter** — absent where the photography is the
|
|
18
|
+
* subject, full where the rows begin — so the pictures stay sharp where
|
|
19
|
+
* they are looked at and go soft exactly where they are read over.
|
|
20
|
+
* 3. **An ink gradient** over that. This is what actually buys the contrast:
|
|
21
|
+
* frost alone blurs without darkening, and blurred mid-tones will not carry
|
|
22
|
+
* 16px type.
|
|
23
|
+
*
|
|
24
|
+
* On the phone the frost and ink run top to bottom: the name and the action
|
|
25
|
+
* sit on clear photography, the chips and the rows on the frosted foot. On
|
|
26
|
+
* desktop the same three layers turn ninety degrees — the photo column on the
|
|
27
|
+
* left stays clear, the row column on the right is the frosted one. The mosaic
|
|
28
|
+
* runs 3×2 there rather than 2×2, because at 1110px a four-tile grid makes each
|
|
29
|
+
* photograph too big to read as texture.
|
|
30
|
+
*
|
|
31
|
+
* ## The dark scope
|
|
32
|
+
*
|
|
33
|
+
* The card's content region carries the `dark` class, so every token inside —
|
|
34
|
+
* `text-foreground`, `bg-muted`, `border-border` — resolves to its dark-theme
|
|
35
|
+
* value. That is how `MenuKitchenList` and `HueChip` render correctly on the
|
|
36
|
+
* ink without a fork or a "tone" prop: the mechanism the app already uses to
|
|
37
|
+
* mark a region as dark. It also means the card looks the same in both themes,
|
|
38
|
+
* which is right — it is built on photography, and photography is not themed.
|
|
39
|
+
*
|
|
40
|
+
* ## The two costs, stated
|
|
41
|
+
*
|
|
42
|
+
* A `backdrop-filter` over a mask is expensive to composite, and this card is
|
|
43
|
+
* repeated ten times down a scroll. And the thumbnails now sit on a
|
|
44
|
+
* photograph; they survive only because the ink is heavy under them, and on a
|
|
45
|
+
* busy interior shot they will fight. Both were accepted with the design.
|
|
46
|
+
*
|
|
47
|
+
* ## No "most named" item, no venue count badge
|
|
48
|
+
*
|
|
49
|
+
* The family-level "most named" was the first venue's signature relabelled —
|
|
50
|
+
* Italian, Pizza and Mediterranean all claimed "Turkish Poached Eggs" — and
|
|
51
|
+
* the count used to print three times with two values. One count, under the
|
|
52
|
+
* name; the overflow tile says only how many photographs it stands for.
|
|
53
|
+
*/
|
|
54
|
+
/** @deprecated Alias of `MenuLinkProps`; kept for symmetry with the other menu surfaces. */
|
|
55
|
+
type MenuFamilyBlockLinkProps = MenuLinkProps;
|
|
56
|
+
type MenuFamilyBlockLabels = {
|
|
57
|
+
/** Unit under the family name, e.g. "247 kitchens". Pluralised by the caller. */
|
|
58
|
+
venueUnit: (count: number) => string;
|
|
59
|
+
/** The overflow pill over the mosaic, e.g. "+311 more". */
|
|
60
|
+
more: (count: number) => string;
|
|
61
|
+
/**
|
|
62
|
+
* Price-band caption, e.g. "kitchen price band".
|
|
63
|
+
*
|
|
64
|
+
* Never drawn: the band reads as a wallet and a range, and a caption on a
|
|
65
|
+
* pill that short is longer than the fact. It names the pill for a screen
|
|
66
|
+
* reader, which would otherwise be read two numbers and a currency.
|
|
67
|
+
*/
|
|
68
|
+
priceBand: string;
|
|
69
|
+
/** Main action, e.g. "Browse Italian". Sits on the photography — keep it short. */
|
|
70
|
+
browse: string;
|
|
71
|
+
/** Passed through to the venue rows. */
|
|
72
|
+
kitchens: MenuKitchenListLabels;
|
|
73
|
+
};
|
|
74
|
+
type MenuFamilyBlockProps = {
|
|
75
|
+
family: MenuFamilyBlockPreview;
|
|
76
|
+
labels: MenuFamilyBlockLabels;
|
|
77
|
+
formatCount?: (value: number) => string;
|
|
78
|
+
/** Finished CSS colour for the chips. */
|
|
79
|
+
hue?: string;
|
|
80
|
+
/** Finished CSS gradient — the family's colour over the mosaic. */
|
|
81
|
+
gradient?: string;
|
|
82
|
+
linkComponent?: MenuLinkComponent;
|
|
83
|
+
className?: string;
|
|
84
|
+
};
|
|
85
|
+
declare function MenuFamilyBlock({ family, labels, formatCount, hue, gradient, linkComponent: LinkComponent, className, }: MenuFamilyBlockProps): react_jsx_runtime.JSX.Element;
|
|
86
|
+
/**
|
|
87
|
+
* The block's own loading state, at the block's footprint.
|
|
88
|
+
*
|
|
89
|
+
* The ground is the card's own, so the placeholder is already the dark
|
|
90
|
+
* surface the photographs will land on — nothing flashes from paper to ink.
|
|
91
|
+
* `venues` matches the rows the caller will render, so a block showing three
|
|
92
|
+
* does not settle from a placeholder showing one.
|
|
93
|
+
*/
|
|
94
|
+
declare function MenuFamilyBlockSkeleton({ venues, className, }: {
|
|
95
|
+
venues?: number;
|
|
96
|
+
className?: string;
|
|
97
|
+
}): react_jsx_runtime.JSX.Element;
|
|
98
|
+
|
|
99
|
+
export { MenuFamilyBlock, type MenuFamilyBlockLabels, type MenuFamilyBlockLinkProps, type MenuFamilyBlockProps, MenuFamilyBlockSkeleton };
|