@bearmenu/ui 0.8.8 → 0.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-24RFI4RQ.js +167 -0
- package/dist/chunk-2XCZJWTV.js +121 -0
- package/dist/chunk-5RWTOT5Y.js +72 -0
- package/dist/chunk-62ES6N32.js +27 -0
- package/dist/chunk-AKQ57NXY.js +58 -0
- package/dist/chunk-CLLY6XB3.js +41 -0
- package/dist/chunk-DTXNDLSE.js +125 -0
- package/dist/chunk-F6FMZJDG.js +113 -0
- package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
- package/dist/chunk-MZL7CGCL.js +109 -0
- package/dist/chunk-Q6QH52LJ.js +38 -0
- package/dist/chunk-SY6RV476.js +48 -0
- package/dist/chunk-TH6W2PWG.js +213 -0
- package/dist/chunk-UDN7UHJW.js +60 -0
- package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/concept-compare-rail.d.ts +50 -0
- package/dist/components/concept-compare-rail.js +5 -0
- package/dist/components/concept-compare.d.ts +110 -0
- package/dist/components/concept-compare.js +238 -0
- package/dist/components/concept-convergence.d.ts +41 -0
- package/dist/components/concept-convergence.js +53 -0
- package/dist/components/convergence.d.ts +25 -0
- package/dist/components/convergence.js +4 -0
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.d.ts +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.d.ts +171 -25
- package/dist/components/event-detail-panel.js +472 -175
- package/dist/components/event-index-row.js +1 -1
- package/dist/components/event-list-row-wide.js +2 -2
- package/dist/components/event-list-row.js +2 -2
- package/dist/components/explainer-steps.d.ts +34 -0
- package/dist/components/explainer-steps.js +36 -0
- package/dist/components/glass-ground.d.ts +66 -0
- package/dist/components/glass-ground.js +3 -0
- package/dist/components/hub-surface.d.ts +27 -0
- package/dist/components/hub-surface.js +30 -0
- package/dist/components/hue-chip.d.ts +70 -0
- package/dist/components/hue-chip.js +3 -0
- package/dist/components/kitchen-dossier.d.ts +88 -0
- package/dist/components/kitchen-dossier.js +187 -0
- package/dist/components/menu-dead-end.d.ts +56 -0
- package/dist/components/menu-dead-end.js +49 -0
- package/dist/components/menu-family-block.d.ts +99 -0
- package/dist/components/menu-family-block.js +155 -0
- package/dist/components/menu-family-card.d.ts +65 -0
- package/dist/components/menu-family-card.js +79 -0
- package/dist/components/menu-field.d.ts +57 -0
- package/dist/components/menu-field.js +93 -0
- package/dist/components/menu-filter-chips.d.ts +86 -0
- package/dist/components/menu-filter-chips.js +122 -0
- package/dist/components/menu-group-rows.d.ts +53 -0
- package/dist/components/menu-group-rows.js +67 -0
- package/dist/components/menu-hub-hero.d.ts +65 -0
- package/dist/components/menu-hub-hero.js +103 -0
- package/dist/components/menu-index-header.d.ts +37 -0
- package/dist/components/menu-index-header.js +68 -0
- package/dist/components/menu-index-list.d.ts +48 -0
- package/dist/components/menu-index-list.js +96 -0
- package/dist/components/menu-intent-hero.d.ts +69 -0
- package/dist/components/menu-intent-hero.js +87 -0
- package/dist/components/menu-intent-tiles.d.ts +31 -0
- package/dist/components/menu-intent-tiles.js +50 -0
- package/dist/components/menu-item.d.ts +127 -0
- package/dist/components/menu-item.js +5 -0
- package/dist/components/menu-kitchen-list.d.ts +82 -0
- package/dist/components/menu-kitchen-list.js +5 -0
- package/dist/components/menu-mocks.d.ts +90 -0
- package/dist/components/menu-mocks.js +504 -0
- package/dist/components/menu-types.d.ts +421 -0
- package/dist/components/menu-types.js +1 -0
- package/dist/components/motion-icon-controls.js +2 -2
- package/dist/components/motion-icon-set.js +21 -9
- package/dist/components/motion-icon.d.ts +22 -1
- package/dist/components/motion-icon.js +1 -1
- package/dist/components/open-now-list.d.ts +40 -0
- package/dist/components/open-now-list.js +69 -0
- package/dist/components/photo-ground.d.ts +64 -0
- package/dist/components/photo-ground.js +3 -0
- package/dist/components/place-about-tab.js +2 -2
- package/dist/components/place-details-mobile.js +2 -2
- package/dist/components/place-types.d.ts +112 -1
- package/dist/components/qa-block.d.ts +36 -0
- package/dist/components/qa-block.js +24 -0
- package/dist/components/scroll-fade.js +3 -121
- package/dist/components/signature-chip.d.ts +37 -0
- package/dist/components/signature-chip.js +4 -0
- package/dist/components/snap-rail.d.ts +48 -5
- package/dist/components/snap-rail.js +3 -51
- package/dist/components/stat-banner.d.ts +47 -0
- package/dist/components/stat-banner.js +61 -0
- package/dist/components/story.d.ts +34 -0
- package/dist/components/story.js +3 -0
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +1 -1
- package/dist/components/top-comparison.d.ts +28 -0
- package/dist/components/top-comparison.js +76 -0
- package/dist/components/top-cross-link.d.ts +34 -0
- package/dist/components/top-cross-link.js +101 -0
- package/dist/components/top-ranking-entry.d.ts +53 -0
- package/dist/components/top-ranking-entry.js +217 -0
- package/dist/components/top-ranking-hero.d.ts +61 -0
- package/dist/components/top-ranking-hero.js +99 -0
- package/dist/components/top-ranking-legend.d.ts +69 -0
- package/dist/components/top-ranking-legend.js +124 -0
- package/dist/components/top-types.d.ts +109 -0
- package/dist/components/top-types.js +1 -0
- package/dist/components/venue-grade-dimensions.d.ts +38 -0
- package/dist/components/venue-grade-dimensions.js +3 -0
- package/dist/components/venue-quotes.d.ts +32 -0
- package/dist/components/venue-quotes.js +3 -0
- package/dist/components/venue-wall.d.ts +41 -0
- package/dist/components/venue-wall.js +118 -0
- package/dist/components/weekday-theme-banner.d.ts +70 -0
- package/dist/components/weekday-theme-banner.js +127 -0
- package/dist/components/weekend-panel.js +2 -2
- package/package.json +1 -1
- package/src/components/colour-dictionary.stories.tsx +227 -0
- package/src/components/concept-compare-rail.tsx +131 -0
- package/src/components/concept-compare.stories.tsx +172 -0
- package/src/components/concept-compare.tsx +443 -0
- package/src/components/concept-convergence.stories.tsx +53 -0
- package/src/components/concept-convergence.tsx +90 -0
- package/src/components/convergence.stories.tsx +54 -0
- package/src/components/convergence.tsx +177 -0
- package/src/components/event-compact-row.tsx +1 -1
- package/src/components/event-detail-panel.stories.tsx +205 -97
- package/src/components/event-detail-panel.test.tsx +181 -0
- package/src/components/event-detail-panel.tsx +703 -185
- package/src/components/explainer-steps.stories.tsx +46 -0
- package/src/components/explainer-steps.tsx +74 -0
- package/src/components/glass-ground.tsx +129 -0
- package/src/components/hub-surface.stories.tsx +68 -0
- package/src/components/hub-surface.tsx +50 -0
- package/src/components/hue-chip.stories.tsx +76 -0
- package/src/components/hue-chip.test.tsx +62 -0
- package/src/components/hue-chip.tsx +99 -0
- package/src/components/kitchen-dossier.stories.tsx +108 -0
- package/src/components/kitchen-dossier.tsx +322 -0
- package/src/components/menu-dead-end.stories.tsx +67 -0
- package/src/components/menu-dead-end.tsx +127 -0
- package/src/components/menu-family-block.stories.tsx +91 -0
- package/src/components/menu-family-block.tsx +314 -0
- package/src/components/menu-family-card.stories.tsx +120 -0
- package/src/components/menu-family-card.tsx +191 -0
- package/src/components/menu-field.stories.tsx +74 -0
- package/src/components/menu-field.tsx +199 -0
- package/src/components/menu-filter-chips.stories.tsx +107 -0
- package/src/components/menu-filter-chips.tsx +238 -0
- package/src/components/menu-group-rows.stories.tsx +54 -0
- package/src/components/menu-group-rows.tsx +133 -0
- package/src/components/menu-hub-hero.stories.tsx +92 -0
- package/src/components/menu-hub-hero.tsx +187 -0
- package/src/components/menu-index-header.stories.tsx +72 -0
- package/src/components/menu-index-header.tsx +125 -0
- package/src/components/menu-index-list.stories.tsx +50 -0
- package/src/components/menu-index-list.tsx +149 -0
- package/src/components/menu-intent-hero.stories.tsx +67 -0
- package/src/components/menu-intent-hero.tsx +189 -0
- package/src/components/menu-intent-tiles.stories.tsx +54 -0
- package/src/components/menu-intent-tiles.tsx +76 -0
- package/src/components/menu-item.tsx +416 -0
- package/src/components/menu-kitchen-list.stories.tsx +67 -0
- package/src/components/menu-kitchen-list.test.tsx +78 -0
- package/src/components/menu-kitchen-list.tsx +284 -0
- package/src/components/menu-mocks.ts +592 -0
- package/src/components/menu-types.ts +442 -0
- package/src/components/motion-icon-controls.tsx +11 -11
- package/src/components/motion-icon-set.tsx +36 -8
- package/src/components/motion-icon.test.tsx +23 -1
- package/src/components/motion-icon.tsx +20 -0
- package/src/components/open-now-list.stories.tsx +76 -0
- package/src/components/open-now-list.tsx +127 -0
- package/src/components/photo-ground.tsx +147 -0
- package/src/components/place-types.ts +112 -0
- package/src/components/qa-block.stories.tsx +43 -0
- package/src/components/qa-block.tsx +60 -0
- package/src/components/signature-chip.tsx +77 -0
- package/src/components/snap-rail.tsx +133 -9
- package/src/components/stat-banner.stories.tsx +100 -0
- package/src/components/stat-banner.tsx +117 -0
- package/src/components/story.tsx +225 -0
- package/src/components/top-comparison.stories.tsx +78 -0
- package/src/components/top-comparison.tsx +151 -0
- package/src/components/top-cross-link.stories.tsx +84 -0
- package/src/components/top-cross-link.tsx +171 -0
- package/src/components/top-ranking-entry.stories.tsx +171 -0
- package/src/components/top-ranking-entry.test.tsx +105 -0
- package/src/components/top-ranking-entry.tsx +425 -0
- package/src/components/top-ranking-hero.stories.tsx +76 -0
- package/src/components/top-ranking-hero.tsx +218 -0
- package/src/components/top-ranking-legend.stories.tsx +78 -0
- package/src/components/top-ranking-legend.tsx +239 -0
- package/src/components/top-types.ts +110 -0
- package/src/components/venue-grade-dimensions.stories.tsx +83 -0
- package/src/components/venue-grade-dimensions.tsx +91 -0
- package/src/components/venue-quotes.stories.tsx +59 -0
- package/src/components/venue-quotes.tsx +64 -0
- package/src/components/venue-wall.stories.tsx +78 -0
- package/src/components/venue-wall.tsx +185 -0
- package/src/components/weekday-theme-banner.stories.tsx +113 -0
- package/src/components/weekday-theme-banner.tsx +231 -0
- package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
|
@@ -0,0 +1,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 };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { MenuKitchenList } from '../chunk-24RFI4RQ.js';
|
|
2
|
+
import { HueChip } from '../chunk-CLLY6XB3.js';
|
|
3
|
+
import { Separator } from '../chunk-WSRWFA6K.js';
|
|
4
|
+
import { PhotoGround, PHOTO_GROUND } from '../chunk-5RWTOT5Y.js';
|
|
5
|
+
import { Skeleton } from '../chunk-QXHMPJ35.js';
|
|
6
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
7
|
+
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
8
|
+
import { Wallet } from 'lucide-react';
|
|
9
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
10
|
+
|
|
11
|
+
function DefaultLink(_a) {
|
|
12
|
+
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
13
|
+
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
14
|
+
}
|
|
15
|
+
var MOSAIC_TILES = 6;
|
|
16
|
+
var PHONE_TILES = 4;
|
|
17
|
+
var GROUND = PHOTO_GROUND;
|
|
18
|
+
var FROST = "backdrop-blur-xl [mask-image:linear-gradient(to_bottom,transparent_0%,black_42%)] lg:[mask-image:linear-gradient(to_right,transparent_36%,black_54%)]";
|
|
19
|
+
var INK = "bg-[linear-gradient(to_bottom,oklch(0.16_0.012_55_/_0.42)_0%,oklch(0.16_0.012_55_/_0.6)_30%,oklch(0.16_0.012_55_/_0.9)_48%,oklch(0.16_0.012_55_/_0.96)_100%)] lg:bg-[linear-gradient(to_right,oklch(0.16_0.012_55_/_0.38)_0%,oklch(0.16_0.012_55_/_0.6)_30%,oklch(0.16_0.012_55_/_0.92)_46%,oklch(0.16_0.012_55_/_0.96)_100%)]";
|
|
20
|
+
var TEXT_SCRIM = (
|
|
21
|
+
/* `isolate` + `before:-z-10`: a positioned pseudo-element paints ABOVE the
|
|
22
|
+
column's in-flow text unless the column is its own stacking context and
|
|
23
|
+
the layer is ordered beneath. Without `isolate`, `-z-10` would instead
|
|
24
|
+
drop it behind the card's mosaic — inside the article's context. */
|
|
25
|
+
"isolate before:pointer-events-none before:absolute before:inset-x-0 before:bottom-0 before:top-[30%] before:-z-10 before:bg-[linear-gradient(to_bottom,oklch(0.16_0.012_55_/_0)_0%,oklch(0.16_0.012_55_/_0.55)_45%,oklch(0.16_0.012_55_/_0.8)_100%)] before:content-['']"
|
|
26
|
+
);
|
|
27
|
+
function MenuFamilyBlock({
|
|
28
|
+
family,
|
|
29
|
+
labels,
|
|
30
|
+
formatCount = String,
|
|
31
|
+
hue,
|
|
32
|
+
gradient,
|
|
33
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
34
|
+
className
|
|
35
|
+
}) {
|
|
36
|
+
var _a, _b, _c, _d, _e;
|
|
37
|
+
const photos = ((_a = family.photoUrls) != null ? _a : []).slice(0, MOSAIC_TILES);
|
|
38
|
+
const overflow = Math.max(family.venueCount - photos.length, 0);
|
|
39
|
+
const initial = ((_c = (_b = family.name.match(new RegExp("\\p{L}", "u"))) == null ? void 0 : _b[0]) != null ? _c : "").toLocaleUpperCase();
|
|
40
|
+
const venues = (_d = family.venues) != null ? _d : [];
|
|
41
|
+
return /* @__PURE__ */ jsxs(
|
|
42
|
+
"article",
|
|
43
|
+
{
|
|
44
|
+
className: cn(
|
|
45
|
+
/* `isolate` confines the blend and the backdrop-filter to this card.
|
|
46
|
+
`dark` scopes every token inside to its dark-theme value. */
|
|
47
|
+
"dark relative isolate overflow-hidden rounded-[14px] text-foreground",
|
|
48
|
+
GROUND,
|
|
49
|
+
className
|
|
50
|
+
),
|
|
51
|
+
children: [
|
|
52
|
+
/* @__PURE__ */ jsx(
|
|
53
|
+
PhotoGround,
|
|
54
|
+
{
|
|
55
|
+
photos: photos.length > 0 ? photos : [void 0],
|
|
56
|
+
initials: initial,
|
|
57
|
+
mosaicClassName: photos.length > 0 ? "grid-cols-2 grid-rows-2 lg:grid-cols-3" : "grid-cols-1",
|
|
58
|
+
phoneTiles: PHONE_TILES,
|
|
59
|
+
gradient
|
|
60
|
+
}
|
|
61
|
+
),
|
|
62
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("absolute inset-0", FROST) }),
|
|
63
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("absolute inset-0", INK) }),
|
|
64
|
+
/* @__PURE__ */ jsxs("div", { className: "relative grid grid-cols-1 lg:grid-cols-[38%_1fr]", children: [
|
|
65
|
+
/* @__PURE__ */ jsxs("div", { className: cn("relative flex flex-col p-3.5 lg:min-h-[320px] lg:justify-end lg:p-5", TEXT_SCRIM), children: [
|
|
66
|
+
overflow > 0 && /* @__PURE__ */ jsx("span", { className: "mb-16 self-end rounded-full bg-[oklch(0.16_0.012_55_/_0.72)] px-2.5 py-1 text-[11px] font-semibold text-white backdrop-blur-[2px] lg:mb-3 lg:self-start", children: labels.more(overflow) }),
|
|
67
|
+
/* @__PURE__ */ jsx("h3", { className: "text-balance font-serif text-[26px] font-semibold leading-tight tracking-[-0.02em] text-white lg:text-[30px]", children: family.name }),
|
|
68
|
+
/* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-[13px] font-medium tabular-nums text-white/70", children: labels.venueUnit(family.venueCount) }),
|
|
69
|
+
/* @__PURE__ */ jsx(
|
|
70
|
+
LinkComponent,
|
|
71
|
+
{
|
|
72
|
+
href: family.href,
|
|
73
|
+
className: "mt-3.5 inline-flex h-10 w-max items-center rounded-full border border-white/20 bg-white/[0.12] px-4 text-[13.5px] font-bold text-white backdrop-blur-md transition-colors hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white motion-reduce:transition-none",
|
|
74
|
+
children: labels.browse
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
] }),
|
|
78
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col px-3.5 pb-3.5 lg:p-5", children: [
|
|
79
|
+
/* @__PURE__ */ jsxs("ul", { className: "flex flex-wrap items-center gap-1.5", children: [
|
|
80
|
+
family.priceBandLabel && /* No hue: a fact about money, not about the family. In the dark
|
|
81
|
+
scope `HueChip`'s `--muted` fallback is the dark pill the design
|
|
82
|
+
draws. */
|
|
83
|
+
/* @__PURE__ */ jsx(
|
|
84
|
+
HueChip,
|
|
85
|
+
{
|
|
86
|
+
as: "li",
|
|
87
|
+
className: "font-bold tabular-nums",
|
|
88
|
+
"aria-label": `${labels.priceBand}: ${family.priceBandLabel}`,
|
|
89
|
+
icon: /* @__PURE__ */ jsx(Wallet, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }),
|
|
90
|
+
children: family.priceBandLabel
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
(_e = family.chips) == null ? void 0 : _e.map((chip) => (
|
|
94
|
+
/* Label only. The counts are VENUES beside a dish group's name,
|
|
95
|
+
which every reader takes for dishes. */
|
|
96
|
+
/* @__PURE__ */ jsx(HueChip, { as: "li", hue, children: chip.label }, chip.slug)
|
|
97
|
+
))
|
|
98
|
+
] }),
|
|
99
|
+
venues.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
100
|
+
/* @__PURE__ */ jsx(Separator, { className: "my-3" }),
|
|
101
|
+
/* @__PURE__ */ jsx(
|
|
102
|
+
MenuKitchenList,
|
|
103
|
+
{
|
|
104
|
+
kitchens: venues,
|
|
105
|
+
labels: labels.kitchens,
|
|
106
|
+
linkComponent: LinkComponent
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
] })
|
|
110
|
+
] })
|
|
111
|
+
] })
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
function MenuFamilyBlockSkeleton({
|
|
117
|
+
venues = 3,
|
|
118
|
+
className
|
|
119
|
+
}) {
|
|
120
|
+
return /* @__PURE__ */ jsx(
|
|
121
|
+
"div",
|
|
122
|
+
{
|
|
123
|
+
className: cn("dark relative overflow-hidden rounded-[14px]", GROUND, className),
|
|
124
|
+
"aria-hidden": true,
|
|
125
|
+
children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-[38%_1fr]", children: [
|
|
126
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col p-3.5 lg:min-h-[320px] lg:justify-end lg:p-5", children: [
|
|
127
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mb-16 h-6 w-[72px] self-end rounded-full lg:mb-3 lg:self-start" }),
|
|
128
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-[60%]" }),
|
|
129
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-2 h-3.5 w-[90px]" }),
|
|
130
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-3.5 h-10 w-[160px] rounded-full" })
|
|
131
|
+
] }),
|
|
132
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col px-3.5 pb-3.5 lg:p-5", children: [
|
|
133
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
|
|
134
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-[86px] rounded-full" }),
|
|
135
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-[74px] rounded-full" }),
|
|
136
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-[66px] rounded-full" })
|
|
137
|
+
] }),
|
|
138
|
+
/* @__PURE__ */ jsx(Separator, { className: "my-3" }),
|
|
139
|
+
Array.from({ length: venues }, (_, index) => /* @__PURE__ */ jsxs("div", { children: [
|
|
140
|
+
index > 0 && /* @__PURE__ */ jsx(Separator, {}),
|
|
141
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-2.5", children: [
|
|
142
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-[46px] w-[46px] rounded-[11px]" }),
|
|
143
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
|
|
144
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 w-[46%]" }),
|
|
145
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-[58%]" })
|
|
146
|
+
] })
|
|
147
|
+
] })
|
|
148
|
+
] }, index))
|
|
149
|
+
] })
|
|
150
|
+
] })
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export { MenuFamilyBlock, MenuFamilyBlockSkeleton };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { MenuFamilyPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
3
|
+
import 'react';
|
|
4
|
+
import './place-types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MenuFamilyCard — a browsable grouping of venues: a cuisine on the dishes tab,
|
|
8
|
+
* a drink family on the drinks tab. The hub's doorway to a family.
|
|
9
|
+
*
|
|
10
|
+
* ## A doorway, not a summary
|
|
11
|
+
*
|
|
12
|
+
* The cuisines index's `MenuFamilyBlock` is the family's SUMMARY: photography,
|
|
13
|
+
* chips, a price band and three venues. This card is the same family drawn
|
|
14
|
+
* one step earlier, in a row of three on the hub, where the reader is choosing
|
|
15
|
+
* a family and not yet a venue — so it carries the same photography, the same
|
|
16
|
+
* chips and the same "Browse" action, and lists no venue. A venue on the hub
|
|
17
|
+
* card was tried and it was the first venue's signature relabelled as the
|
|
18
|
+
* family's most named, which was a lie at three-card width.
|
|
19
|
+
*
|
|
20
|
+
* ## The count does the scanning
|
|
21
|
+
*
|
|
22
|
+
* The card is one number, one name and the groups the family consists of,
|
|
23
|
+
* over a mosaic of its venues. The count is VENUES: cuisine exists on places
|
|
24
|
+
* and nowhere else — no listing endpoint will tell you how many dishes are
|
|
25
|
+
* Italian — so a card counting dishes cannot be built and this one does not
|
|
26
|
+
* try. The chips are plain labels: at three cards across, chips carrying
|
|
27
|
+
* numbers turned the panel into a table, and the numbers were kitchens, which
|
|
28
|
+
* nobody reads a chip as.
|
|
29
|
+
*
|
|
30
|
+
* ## One link
|
|
31
|
+
*
|
|
32
|
+
* The whole card is the link and the "Browse" pill is its visible affordance,
|
|
33
|
+
* not a second link — a pill inside a link is a nested anchor, and a pill that
|
|
34
|
+
* is the only link leaves the photography, the number and the name inert.
|
|
35
|
+
*/
|
|
36
|
+
/** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
|
|
37
|
+
type MenuFamilyCardLinkProps = MenuLinkProps;
|
|
38
|
+
type MenuFamilyCardLabels = {
|
|
39
|
+
/** Unit under the count, e.g. "KITCHENS". */
|
|
40
|
+
venueUnit: string;
|
|
41
|
+
/**
|
|
42
|
+
* Price-band caption, e.g. "kitchen price band". Never drawn — it names the
|
|
43
|
+
* pill for a screen reader, which would otherwise be read two numbers and a
|
|
44
|
+
* currency.
|
|
45
|
+
*/
|
|
46
|
+
priceBand: string;
|
|
47
|
+
/** The action pill, e.g. "Browse Italian". Sits on the photography — keep it short. */
|
|
48
|
+
browse: string;
|
|
49
|
+
};
|
|
50
|
+
type MenuFamilyCardProps = {
|
|
51
|
+
family: MenuFamilyPreview;
|
|
52
|
+
labels: MenuFamilyCardLabels;
|
|
53
|
+
href: string;
|
|
54
|
+
/** Finished CSS colour for the chips. */
|
|
55
|
+
hue?: string;
|
|
56
|
+
/** Finished CSS gradient — the family's colour over the mosaic. */
|
|
57
|
+
gradient?: string;
|
|
58
|
+
/** Composed price band, e.g. "32–84 lei". */
|
|
59
|
+
priceBandLabel?: string;
|
|
60
|
+
linkComponent?: MenuLinkComponent;
|
|
61
|
+
className?: string;
|
|
62
|
+
};
|
|
63
|
+
declare function MenuFamilyCard({ family, labels, href, hue, gradient, priceBandLabel, linkComponent: LinkComponent, className, }: MenuFamilyCardProps): react_jsx_runtime.JSX.Element;
|
|
64
|
+
|
|
65
|
+
export { MenuFamilyCard, type MenuFamilyCardLabels, type MenuFamilyCardLinkProps, type MenuFamilyCardProps };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { HueChip } from '../chunk-CLLY6XB3.js';
|
|
2
|
+
import { PhotoGround, PHOTO_GROUND } from '../chunk-5RWTOT5Y.js';
|
|
3
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
4
|
+
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
5
|
+
import { Wallet } from 'lucide-react';
|
|
6
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
function DefaultLink(_a) {
|
|
9
|
+
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
10
|
+
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
11
|
+
}
|
|
12
|
+
var MOSAIC_CELLS = 4;
|
|
13
|
+
var GROUND = PHOTO_GROUND;
|
|
14
|
+
var FROST = "backdrop-blur-xl [mask-image:linear-gradient(to_bottom,transparent_28%,black_58%)]";
|
|
15
|
+
var INK = "bg-[linear-gradient(to_bottom,oklch(0.16_0.012_55_/_0.78)_0%,oklch(0.16_0.012_55_/_0.5)_34%,oklch(0.16_0.012_55_/_0.86)_62%,oklch(0.16_0.012_55_/_0.96)_100%)]";
|
|
16
|
+
function MenuFamilyCard({
|
|
17
|
+
family,
|
|
18
|
+
labels,
|
|
19
|
+
href,
|
|
20
|
+
hue,
|
|
21
|
+
gradient,
|
|
22
|
+
priceBandLabel,
|
|
23
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
24
|
+
className
|
|
25
|
+
}) {
|
|
26
|
+
var _a, _b, _c, _d;
|
|
27
|
+
const photos = (_a = family.venue_photo_urls) != null ? _a : [];
|
|
28
|
+
const tiles = photos.length ? Array.from({ length: MOSAIC_CELLS }, (_, cell) => photos[cell % photos.length]) : [void 0];
|
|
29
|
+
return /* @__PURE__ */ jsxs(
|
|
30
|
+
LinkComponent,
|
|
31
|
+
{
|
|
32
|
+
href,
|
|
33
|
+
className: cn(
|
|
34
|
+
"group dark relative isolate flex min-h-[280px] flex-col justify-between overflow-hidden rounded-[14px] border border-border p-3.5",
|
|
35
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white",
|
|
36
|
+
GROUND,
|
|
37
|
+
className
|
|
38
|
+
),
|
|
39
|
+
children: [
|
|
40
|
+
/* @__PURE__ */ jsx(
|
|
41
|
+
PhotoGround,
|
|
42
|
+
{
|
|
43
|
+
className: "-z-30",
|
|
44
|
+
photos: tiles,
|
|
45
|
+
initials: family.name.charAt(0),
|
|
46
|
+
mosaicClassName: photos.length ? "grid-cols-2 grid-rows-2" : "grid-cols-1",
|
|
47
|
+
gradient
|
|
48
|
+
}
|
|
49
|
+
),
|
|
50
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("absolute inset-0 -z-20", FROST) }),
|
|
51
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("absolute inset-0 -z-10", INK) }),
|
|
52
|
+
/* @__PURE__ */ jsxs("span", { className: "block text-white", children: [
|
|
53
|
+
/* @__PURE__ */ jsx("b", { className: "block text-[34px] font-extrabold leading-none tracking-[-0.03em] tabular-nums", children: family.venue_count }),
|
|
54
|
+
/* @__PURE__ */ jsx("span", { className: "mt-1 block text-[9.5px] font-bold uppercase tracking-[0.12em] opacity-80", children: labels.venueUnit })
|
|
55
|
+
] }),
|
|
56
|
+
/* @__PURE__ */ jsxs("span", { className: "block", children: [
|
|
57
|
+
/* @__PURE__ */ jsx("span", { className: "block text-balance font-serif text-[24px] font-semibold leading-tight tracking-[-0.02em] text-white", children: family.name }),
|
|
58
|
+
(priceBandLabel || ((_c = (_b = family.chips) == null ? void 0 : _b.length) != null ? _c : 0) > 0) && /* @__PURE__ */ jsxs("span", { className: "mt-2.5 flex flex-wrap items-center gap-1.5", children: [
|
|
59
|
+
priceBandLabel && /* No hue: a fact about money, not about the family. In the dark
|
|
60
|
+
scope `HueChip`'s `--muted` fallback is the dark pill. */
|
|
61
|
+
/* @__PURE__ */ jsx(
|
|
62
|
+
HueChip,
|
|
63
|
+
{
|
|
64
|
+
className: "font-bold tabular-nums",
|
|
65
|
+
"aria-label": `${labels.priceBand}: ${priceBandLabel}`,
|
|
66
|
+
icon: /* @__PURE__ */ jsx(Wallet, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }),
|
|
67
|
+
children: priceBandLabel
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
(_d = family.chips) == null ? void 0 : _d.map((chip) => /* @__PURE__ */ jsx(HueChip, { hue, children: chip.label }, chip.slug))
|
|
71
|
+
] }),
|
|
72
|
+
/* @__PURE__ */ jsx("span", { className: "mt-3.5 inline-flex h-10 items-center rounded-full border border-white/20 bg-white/[0.12] px-4 text-[13.5px] font-bold text-white backdrop-blur-md transition-colors group-hover:bg-white/20 motion-reduce:transition-none", children: labels.browse })
|
|
73
|
+
] })
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { MenuFamilyCard };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MenuField — the photoless cover surface for a dish or drink.
|
|
5
|
+
*
|
|
6
|
+
* Menu-item photography is AI-generated and never shown, so the item's own name
|
|
7
|
+
* repeats across a shallow diagonal over a gradient. The type IS the cover.
|
|
8
|
+
*
|
|
9
|
+
* The rows fade SYMMETRICALLY out from a single strong row at the centre. That
|
|
10
|
+
* shape is what keeps the type reading as texture in the colour rather than as
|
|
11
|
+
* a watermark stamped on top of it: the eye lands on one legible line and the
|
|
12
|
+
* rest falls away in both directions. A uniform wash — or a random per-row
|
|
13
|
+
* alpha — reads as noise at every alpha high enough to see.
|
|
14
|
+
*
|
|
15
|
+
* The alpha curve is IDENTICAL for every hue. Scaling it per hue to even out
|
|
16
|
+
* how white type reads over different grounds also moves where rows fall below
|
|
17
|
+
* visibility, so a damped hue lost rows and one card showed five bands where
|
|
18
|
+
* its neighbour showed three. Correct contrast in the `gradient` you pass, not
|
|
19
|
+
* in the curve.
|
|
20
|
+
*
|
|
21
|
+
* ## Why `gradient` is a prop
|
|
22
|
+
*
|
|
23
|
+
* Resolving a category slug to a hue needs the app's category enums, which the
|
|
24
|
+
* DS does not take. Callers pass a finished CSS gradient — fe from its own
|
|
25
|
+
* category-colour table, post-generator from whatever it is filming. The DS
|
|
26
|
+
* owns the typography and the falloff; the consumer owns the palette.
|
|
27
|
+
*
|
|
28
|
+
* Server-safe: no state, no effects. The row geometry is a 13-iteration loop
|
|
29
|
+
* computed on every render, which is cheaper than the hook that would let it be
|
|
30
|
+
* skipped.
|
|
31
|
+
*/
|
|
32
|
+
type MenuFieldProps = {
|
|
33
|
+
/** Item name — rendered as the repeating ghost. */
|
|
34
|
+
name: string;
|
|
35
|
+
/** CSS `background-image` value. See the note above on why this is injected. */
|
|
36
|
+
gradient: string;
|
|
37
|
+
/**
|
|
38
|
+
* Deterministic stagger seed. Pass the PARENT CONCEPT SLUG so one dish breaks
|
|
39
|
+
* its lines identically at every venue and across re-sorts. Defaults to
|
|
40
|
+
* `name`, which is stable but shared by every venue's version of a dish.
|
|
41
|
+
*/
|
|
42
|
+
seedKey?: string;
|
|
43
|
+
/**
|
|
44
|
+
* `repeat` (default) — the name as texture, thirteen fading rows. Right at
|
|
45
|
+
* swatch and thumbnail sizes, where any single line would be unreadable.
|
|
46
|
+
*
|
|
47
|
+
* `display` — the name ONCE, set large and bled off the edges. Right when the
|
|
48
|
+
* tile is the primary object on its row and the name is the thing being sold;
|
|
49
|
+
* the repeat treatment at that size reads as wallpaper rather than as a dish.
|
|
50
|
+
* Long names clip on purpose — the caption underneath carries the full text.
|
|
51
|
+
*/
|
|
52
|
+
variant?: "repeat" | "display";
|
|
53
|
+
className?: string;
|
|
54
|
+
};
|
|
55
|
+
declare function MenuField({ name, gradient, seedKey, variant, className, }: MenuFieldProps): react_jsx_runtime.JSX.Element;
|
|
56
|
+
|
|
57
|
+
export { MenuField, type MenuFieldProps };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
+
import '../chunk-3GWWZKX7.js';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var GHOST_ROWS = 13;
|
|
6
|
+
var GHOST_CENTRE = (GHOST_ROWS - 1) / 2;
|
|
7
|
+
var ALPHA_PEAK = 0.44;
|
|
8
|
+
var ALPHA_EDGE = 0;
|
|
9
|
+
var FALLOFF_EXPONENT = 3.4;
|
|
10
|
+
var GHOST_LINE_CHARS = 56;
|
|
11
|
+
function hashString(input) {
|
|
12
|
+
let hash = 5381;
|
|
13
|
+
for (let i = 0; i < input.length; i++) hash = hash * 33 ^ input.charCodeAt(i);
|
|
14
|
+
return Math.abs(hash);
|
|
15
|
+
}
|
|
16
|
+
function MenuField({
|
|
17
|
+
name,
|
|
18
|
+
gradient,
|
|
19
|
+
seedKey,
|
|
20
|
+
variant = "repeat",
|
|
21
|
+
className
|
|
22
|
+
}) {
|
|
23
|
+
if (variant === "display") {
|
|
24
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
25
|
+
const longestWord = Math.max(1, ...words.map((word) => word.length));
|
|
26
|
+
const lineCount = Math.max(1, words.length);
|
|
27
|
+
const byWidth = (96 / (longestWord * 0.62)).toFixed(1);
|
|
28
|
+
const byHeight = (90 / (lineCount * 0.9)).toFixed(1);
|
|
29
|
+
const fontSize = `clamp(9cqw, min(19cqw, ${byWidth}cqw, ${byHeight}cqh), 19cqw)`;
|
|
30
|
+
return /* @__PURE__ */ jsx(
|
|
31
|
+
"div",
|
|
32
|
+
{
|
|
33
|
+
className: cn("relative isolate overflow-hidden", className),
|
|
34
|
+
style: { backgroundImage: gradient, containerType: "size" },
|
|
35
|
+
children: /* @__PURE__ */ jsx(
|
|
36
|
+
"span",
|
|
37
|
+
{
|
|
38
|
+
"aria-hidden": true,
|
|
39
|
+
className: "pointer-events-none absolute inset-0 flex select-none items-center justify-center p-[4cqw] text-center font-black uppercase leading-[0.9] tracking-[-0.03em] text-white/[0.26]",
|
|
40
|
+
style: { fontSize },
|
|
41
|
+
children: name
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const unit = `${name} \xB7 `;
|
|
48
|
+
const repeats = Math.max(2, Math.ceil(GHOST_LINE_CHARS / Math.max(unit.length, 1)));
|
|
49
|
+
const ghostLine = unit.repeat(repeats);
|
|
50
|
+
const seed = hashString(seedKey != null ? seedKey : name);
|
|
51
|
+
const rows = Array.from({ length: GHOST_ROWS }, (_, row) => {
|
|
52
|
+
const distance = Math.abs(row - GHOST_CENTRE) / GHOST_CENTRE;
|
|
53
|
+
const alpha = ALPHA_EDGE + (ALPHA_PEAK - ALPHA_EDGE) * Math.pow(1 - distance, FALLOFF_EXPONENT);
|
|
54
|
+
return { row, alpha, offset: (seed >> row % 12) % 90 - 45 };
|
|
55
|
+
});
|
|
56
|
+
return /* @__PURE__ */ jsx(
|
|
57
|
+
"div",
|
|
58
|
+
{
|
|
59
|
+
className: cn("relative isolate overflow-hidden", className),
|
|
60
|
+
style: { backgroundImage: gradient },
|
|
61
|
+
children: /* @__PURE__ */ jsx(
|
|
62
|
+
"div",
|
|
63
|
+
{
|
|
64
|
+
"aria-hidden": true,
|
|
65
|
+
className: "pointer-events-none absolute left-1/2 top-1/2 flex h-[180%] w-[180%] -translate-x-1/2 -translate-y-1/2 -rotate-[11deg] select-none flex-col justify-center",
|
|
66
|
+
children: rows.map(({ row, alpha, offset }) => /* @__PURE__ */ jsx(
|
|
67
|
+
"span",
|
|
68
|
+
{
|
|
69
|
+
className: "block whitespace-nowrap text-center text-[15px] font-bold leading-[1.45] tracking-[-0.01em]",
|
|
70
|
+
style: {
|
|
71
|
+
color: `oklch(1 0 0 / ${alpha.toFixed(3)})`,
|
|
72
|
+
// Emboss, both edges scaled by this row's alpha: a dark edge below
|
|
73
|
+
// cuts the letterform into the field, a light edge above lifts its
|
|
74
|
+
// top. The dark edge is kept well under the fill — at parity it
|
|
75
|
+
// ringed every glyph and dragged the type back toward grey, which
|
|
76
|
+
// is the opposite of what the fill is doing.
|
|
77
|
+
textShadow: [
|
|
78
|
+
`0 1px 1px oklch(0 0 0 / ${(alpha * 0.4).toFixed(3)})`,
|
|
79
|
+
`0 -1px 0 oklch(1 0 0 / ${(alpha * 0.4).toFixed(3)})`
|
|
80
|
+
].join(", "),
|
|
81
|
+
transform: `translateX(${offset}px)`
|
|
82
|
+
},
|
|
83
|
+
children: ghostLine
|
|
84
|
+
},
|
|
85
|
+
row
|
|
86
|
+
))
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { MenuField };
|