@bearmenu/ui 0.8.8 → 0.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/chunk-62ES6N32.js +27 -0
  2. package/dist/chunk-AXV5O2IZ.js +58 -0
  3. package/dist/chunk-Q6QH52LJ.js +38 -0
  4. package/dist/chunk-TH6W2PWG.js +213 -0
  5. package/dist/chunk-THOVPZNN.js +37 -0
  6. package/dist/chunk-YS6BHKZO.js +92 -0
  7. package/dist/components/alert.d.ts +2 -2
  8. package/dist/components/badge.d.ts +1 -1
  9. package/dist/components/button.d.ts +1 -1
  10. package/dist/components/command.d.ts +7 -7
  11. package/dist/components/concept-compare.d.ts +73 -0
  12. package/dist/components/concept-compare.js +162 -0
  13. package/dist/components/event-card.js +1 -1
  14. package/dist/components/event-compact-row.d.ts +1 -1
  15. package/dist/components/event-compact-row.js +1 -1
  16. package/dist/components/event-detail-panel.d.ts +171 -25
  17. package/dist/components/event-detail-panel.js +471 -174
  18. package/dist/components/event-list-row-wide.js +1 -1
  19. package/dist/components/event-list-row.js +1 -1
  20. package/dist/components/hub-surface.d.ts +27 -0
  21. package/dist/components/hub-surface.js +20 -0
  22. package/dist/components/menu-family-card.d.ts +59 -0
  23. package/dist/components/menu-family-card.js +121 -0
  24. package/dist/components/menu-field.d.ts +57 -0
  25. package/dist/components/menu-field.js +3 -0
  26. package/dist/components/menu-hub-hero.d.ts +65 -0
  27. package/dist/components/menu-hub-hero.js +103 -0
  28. package/dist/components/menu-intent-tiles.d.ts +31 -0
  29. package/dist/components/menu-intent-tiles.js +50 -0
  30. package/dist/components/menu-item.d.ts +127 -0
  31. package/dist/components/menu-item.js +5 -0
  32. package/dist/components/menu-mocks.d.ts +55 -0
  33. package/dist/components/menu-mocks.js +379 -0
  34. package/dist/components/menu-types.d.ts +245 -0
  35. package/dist/components/menu-types.js +1 -0
  36. package/dist/components/motion-icon-set.js +15 -3
  37. package/dist/components/open-now-list.d.ts +40 -0
  38. package/dist/components/open-now-list.js +69 -0
  39. package/dist/components/place-types.d.ts +112 -1
  40. package/dist/components/ranked-venue-list.d.ts +49 -0
  41. package/dist/components/ranked-venue-list.js +87 -0
  42. package/dist/components/signature-chip.d.ts +37 -0
  43. package/dist/components/signature-chip.js +4 -0
  44. package/dist/components/signature-line-list.d.ts +45 -0
  45. package/dist/components/signature-line-list.js +99 -0
  46. package/dist/components/snap-rail.d.ts +24 -4
  47. package/dist/components/snap-rail.js +66 -8
  48. package/dist/components/spinner.d.ts +2 -2
  49. package/dist/components/stat-banner.d.ts +47 -0
  50. package/dist/components/stat-banner.js +61 -0
  51. package/dist/components/tag.d.ts +2 -2
  52. package/dist/components/text.d.ts +3 -3
  53. package/dist/components/venue-busyness-strip.d.ts +35 -0
  54. package/dist/components/venue-busyness-strip.js +37 -0
  55. package/dist/components/venue-grade-dimensions.d.ts +36 -0
  56. package/dist/components/venue-grade-dimensions.js +3 -0
  57. package/dist/components/venue-quotes.d.ts +32 -0
  58. package/dist/components/venue-quotes.js +3 -0
  59. package/dist/components/venue-signature-block.d.ts +39 -0
  60. package/dist/components/venue-signature-block.js +6 -0
  61. package/dist/components/venue-spotlight.d.ts +74 -0
  62. package/dist/components/venue-spotlight.js +138 -0
  63. package/dist/components/venue-wall.d.ts +41 -0
  64. package/dist/components/venue-wall.js +118 -0
  65. package/dist/components/weekday-theme-banner.d.ts +70 -0
  66. package/dist/components/weekday-theme-banner.js +127 -0
  67. package/package.json +1 -1
  68. package/src/components/colour-dictionary.stories.tsx +227 -0
  69. package/src/components/concept-compare.stories.tsx +104 -0
  70. package/src/components/concept-compare.tsx +276 -0
  71. package/src/components/event-compact-row.tsx +1 -1
  72. package/src/components/event-detail-panel.stories.tsx +205 -97
  73. package/src/components/event-detail-panel.test.tsx +181 -0
  74. package/src/components/event-detail-panel.tsx +703 -185
  75. package/src/components/hub-surface.stories.tsx +68 -0
  76. package/src/components/hub-surface.tsx +40 -0
  77. package/src/components/menu-family-card.stories.tsx +112 -0
  78. package/src/components/menu-family-card.tsx +211 -0
  79. package/src/components/menu-field.stories.tsx +74 -0
  80. package/src/components/menu-field.tsx +199 -0
  81. package/src/components/menu-hub-hero.stories.tsx +92 -0
  82. package/src/components/menu-hub-hero.tsx +187 -0
  83. package/src/components/menu-intent-tiles.stories.tsx +54 -0
  84. package/src/components/menu-intent-tiles.tsx +76 -0
  85. package/src/components/menu-item.tsx +416 -0
  86. package/src/components/menu-mocks.ts +417 -0
  87. package/src/components/menu-types.ts +259 -0
  88. package/src/components/motion-icon-set.tsx +30 -3
  89. package/src/components/open-now-list.stories.tsx +76 -0
  90. package/src/components/open-now-list.tsx +127 -0
  91. package/src/components/place-types.ts +112 -0
  92. package/src/components/ranked-venue-list.stories.tsx +80 -0
  93. package/src/components/ranked-venue-list.tsx +177 -0
  94. package/src/components/signature-chip.tsx +77 -0
  95. package/src/components/signature-line-list.stories.tsx +85 -0
  96. package/src/components/signature-line-list.tsx +136 -0
  97. package/src/components/snap-rail.tsx +90 -9
  98. package/src/components/stat-banner.stories.tsx +100 -0
  99. package/src/components/stat-banner.tsx +117 -0
  100. package/src/components/venue-busyness-strip.stories.tsx +66 -0
  101. package/src/components/venue-busyness-strip.tsx +64 -0
  102. package/src/components/venue-grade-dimensions.stories.tsx +83 -0
  103. package/src/components/venue-grade-dimensions.tsx +91 -0
  104. package/src/components/venue-quotes.stories.tsx +59 -0
  105. package/src/components/venue-quotes.tsx +64 -0
  106. package/src/components/venue-signature-block.stories.tsx +68 -0
  107. package/src/components/venue-signature-block.tsx +71 -0
  108. package/src/components/venue-spotlight.stories.tsx +141 -0
  109. package/src/components/venue-spotlight.tsx +236 -0
  110. package/src/components/venue-wall.stories.tsx +78 -0
  111. package/src/components/venue-wall.tsx +185 -0
  112. package/src/components/weekday-theme-banner.stories.tsx +113 -0
  113. package/src/components/weekday-theme-banner.tsx +231 -0
@@ -0,0 +1,27 @@
1
+ import { cn } from './chunk-FEK5XGZW.js';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ function VenueQuotes({ quotes, onCite, citeLabel, limit, className }) {
5
+ const shown = typeof limit === "number" ? quotes.slice(0, limit) : quotes;
6
+ if (shown.length === 0) return null;
7
+ return /* @__PURE__ */ jsx("div", { className: cn("grid gap-3 lg:grid-cols-2", className), children: shown.map((quote, index) => {
8
+ var _a, _b;
9
+ const citations = (_b = (_a = quote.review_ids) == null ? void 0 : _a.length) != null ? _b : 0;
10
+ const canCite = !!onCite && citations > 0;
11
+ const text = /* @__PURE__ */ jsx("blockquote", { className: "border-l-2 border-border pl-3 font-serif text-[13px] italic leading-[1.45] text-muted-foreground", children: `\u201C${quote.text}\u201D` });
12
+ if (!canCite) return /* @__PURE__ */ jsx("div", { children: text }, index);
13
+ return /* @__PURE__ */ jsx(
14
+ "button",
15
+ {
16
+ type: "button",
17
+ onClick: () => onCite(quote),
18
+ "aria-label": citeLabel == null ? void 0 : citeLabel(citations),
19
+ className: "rounded-md text-left transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
20
+ children: text
21
+ },
22
+ index
23
+ );
24
+ }) });
25
+ }
26
+
27
+ export { VenueQuotes };
@@ -0,0 +1,58 @@
1
+ import { cn } from './chunk-FEK5XGZW.js';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+
4
+ var DEFAULT_ORDER = [
5
+ "food",
6
+ "coffee_quality",
7
+ "drinks_quality",
8
+ "service",
9
+ "value",
10
+ "ambiance",
11
+ "portion",
12
+ "consistency",
13
+ "cleanliness"
14
+ ];
15
+ function VenueGradeDimensions({
16
+ dimensions,
17
+ labels,
18
+ order = DEFAULT_ORDER,
19
+ formatScore,
20
+ className
21
+ }) {
22
+ const present = order.map((key) => ({ key, value: dimensions[key] })).filter(
23
+ (entry) => typeof entry.value === "number"
24
+ );
25
+ if (present.length === 0) return null;
26
+ return /* @__PURE__ */ jsx(
27
+ "dl",
28
+ {
29
+ className: cn(
30
+ "grid grid-cols-3 gap-2.5 sm:grid-cols-4 lg:flex lg:gap-2.5",
31
+ className
32
+ ),
33
+ children: present.map(({ key, value }) => {
34
+ var _a;
35
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 lg:flex-1", children: [
36
+ /* @__PURE__ */ jsx("dd", { className: "text-[15px] font-extrabold tracking-[-0.02em] tabular-nums", children: formatScore(value) }),
37
+ /* @__PURE__ */ jsx(
38
+ "div",
39
+ {
40
+ "aria-hidden": true,
41
+ className: "relative block h-[3px] overflow-hidden rounded-full bg-muted",
42
+ children: /* @__PURE__ */ jsx(
43
+ "span",
44
+ {
45
+ className: "absolute inset-y-0 left-0 rounded-full bg-[var(--gradient-primary,currentColor)]",
46
+ style: { width: `${Math.min(100, Math.max(0, value))}%` }
47
+ }
48
+ )
49
+ }
50
+ ),
51
+ /* @__PURE__ */ jsx("dt", { className: "text-[10.5px] text-muted-foreground", children: (_a = labels[key]) != null ? _a : key })
52
+ ] }, key);
53
+ })
54
+ }
55
+ );
56
+ }
57
+
58
+ export { VenueGradeDimensions };
@@ -0,0 +1,38 @@
1
+ import { Badge } from './chunk-UKBNQO6Q.js';
2
+ import { cn } from './chunk-FEK5XGZW.js';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ function SignatureChip({
6
+ label,
7
+ onEvidence,
8
+ surface = "canvas",
9
+ className
10
+ }) {
11
+ const shared = "h-4 shrink-0 px-1.5 text-[10px] font-medium uppercase tracking-wider";
12
+ const onImage = surface === "image";
13
+ if (!onEvidence) {
14
+ return /* @__PURE__ */ jsx(Badge, { variant: onImage ? "overlay" : "secondary", className: cn(shared, className), children: label });
15
+ }
16
+ return /* @__PURE__ */ jsx(
17
+ "button",
18
+ {
19
+ type: "button",
20
+ onClick: (event) => {
21
+ event.stopPropagation();
22
+ event.preventDefault();
23
+ onEvidence();
24
+ },
25
+ className: cn(
26
+ shared,
27
+ "inline-flex items-center rounded-full border border-dashed",
28
+ onImage ? "border-background/70 bg-foreground/70 text-background backdrop-blur-sm hover:bg-foreground/85" : "border-accent/40 text-accent hover:bg-accent/10",
29
+ "focus-visible:outline-none focus-visible:ring-2",
30
+ onImage ? "focus-visible:ring-background/70" : "focus-visible:ring-accent/40",
31
+ className
32
+ ),
33
+ children: label
34
+ }
35
+ );
36
+ }
37
+
38
+ export { SignatureChip };
@@ -0,0 +1,213 @@
1
+ import { SignatureChip } from './chunk-Q6QH52LJ.js';
2
+ import { cn } from './chunk-FEK5XGZW.js';
3
+ import { __objRest, __spreadValues } from './chunk-3GWWZKX7.js';
4
+ import { memo } from 'react';
5
+ import { MessageSquareQuote } 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
+ function MentionSignal({
13
+ label,
14
+ count,
15
+ onMentions,
16
+ formatCount = String
17
+ }) {
18
+ const content = /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-0.5", children: [
19
+ /* @__PURE__ */ jsx(MessageSquareQuote, { className: "h-3.5 w-3.5 opacity-60", "aria-hidden": true }),
20
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: formatCount(count) })
21
+ ] });
22
+ const shared = cn(
23
+ // `relative z-[2]` keeps this above the row's stretched link overlay.
24
+ "relative z-[2] inline-flex shrink-0 items-center text-accent",
25
+ // Same size and weight as the price at the other end of the meta row — keep
26
+ // these two in step if the price's type changes.
27
+ "text-[15px] font-bold",
28
+ // `min-h-6` holds the tap target at the 24px WCAG 2.5.8 floor; `px-1.5
29
+ // -mx-1.5` buys the horizontal half without moving the pair on screen.
30
+ "min-h-6 px-1.5 -mx-1.5"
31
+ );
32
+ if (!onMentions) {
33
+ return /* @__PURE__ */ jsx("span", { className: shared, "aria-label": label, children: content });
34
+ }
35
+ return /* @__PURE__ */ jsx(
36
+ "button",
37
+ {
38
+ type: "button",
39
+ "data-slot": "mentions",
40
+ "aria-label": label,
41
+ onClick: (event) => {
42
+ event.stopPropagation();
43
+ event.preventDefault();
44
+ onMentions();
45
+ },
46
+ className: cn(
47
+ shared,
48
+ "rounded-sm hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
49
+ ),
50
+ children: content
51
+ }
52
+ );
53
+ }
54
+ var MenuItem = memo(function MenuItem2({
55
+ item,
56
+ labels,
57
+ formatPrice,
58
+ formatCount = String,
59
+ onOpen,
60
+ showSignatureBadge = false,
61
+ signatureTier = "strong",
62
+ onSignatureEvidence,
63
+ onMentions,
64
+ markGradient,
65
+ venueName,
66
+ subtitle,
67
+ href,
68
+ linkLabel,
69
+ linkComponent: LinkComponent = DefaultLink,
70
+ disabled = false,
71
+ className
72
+ }) {
73
+ var _a, _b, _c, _d, _e;
74
+ const firstEntry = (_a = item.sizes_and_prices) == null ? void 0 : _a[0];
75
+ const quantity = firstEntry == null ? void 0 : firstEntry.size;
76
+ const hasQuantity = quantity != null && quantity > 0;
77
+ const allPrices = (_c = (_b = item.sizes_and_prices) == null ? void 0 : _b.map((entry) => entry.price).filter((price) => typeof price === "number" && price > 0)) != null ? _c : [];
78
+ const minPrice = allPrices.length > 0 ? Math.min(...allPrices) : null;
79
+ const maxPrice = allPrices.length > 0 ? Math.max(...allPrices) : null;
80
+ const isPriceRange = minPrice != null && maxPrice != null && minPrice !== maxPrice;
81
+ const mentionCount = (_e = (_d = item.mentions) != null ? _d : item.review_count) != null ? _e : 0;
82
+ const isInert = !href && !onOpen;
83
+ const isActionable = !disabled && !isInert;
84
+ const handleRowClick = () => {
85
+ if (disabled) return;
86
+ onOpen == null ? void 0 : onOpen();
87
+ };
88
+ const handleKeyDown = (event) => {
89
+ if (disabled) return;
90
+ if (event.key === "Enter" || event.key === " ") {
91
+ event.preventDefault();
92
+ onOpen == null ? void 0 : onOpen();
93
+ }
94
+ };
95
+ return /* @__PURE__ */ jsxs(
96
+ "div",
97
+ {
98
+ role: href || isInert ? void 0 : "button",
99
+ tabIndex: href || isInert ? void 0 : disabled ? -1 : 0,
100
+ "aria-disabled": disabled || void 0,
101
+ onClick: href || isInert ? void 0 : handleRowClick,
102
+ onKeyDown: href || isInert ? void 0 : handleKeyDown,
103
+ className: cn(
104
+ "group relative flex min-h-[72px] items-start gap-3 px-4 py-3",
105
+ // Radius only where the row is a grid cell. Full-bleed rows run edge to
106
+ // edge, where a corner radius would round against the viewport.
107
+ "md:rounded-xl",
108
+ // 180ms is the EXIT; `hover:duration-*` overrides it to 120ms on the
109
+ // way in. A hover that leaves as fast as it arrives reads as a flicker
110
+ // when the pointer crosses a list.
111
+ !isInert && [
112
+ "transition-[background-color,box-shadow,transform] duration-[180ms] ease-out motion-reduce:transition-none",
113
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
114
+ ],
115
+ isActionable && [
116
+ "cursor-pointer hover:duration-[120ms]",
117
+ // Opaque, never alpha: `hover:bg-muted/40` composited to a lightness
118
+ // delta of 0.004 on the light canvas — a hover state that measured as
119
+ // nothing.
120
+ "hover:bg-card dark:hover:bg-surface-2",
121
+ // Grid cells only. On a full-bleed row the shadow runs off both
122
+ // viewport edges and reads as a smear; that row widens its tab instead.
123
+ "md:hover:shadow-[var(--shadow-row-hover)]",
124
+ // Hover lifts, press sinks — the two must not read as one gesture.
125
+ "active:scale-[0.985] active:duration-[90ms] active:ease-in active:shadow-none",
126
+ "active:bg-muted",
127
+ "motion-reduce:active:scale-100",
128
+ // The mention count is a button inside a button. While the pointer is
129
+ // on it the row stops claiming the hover, so only one target is lit.
130
+ "has-[[data-slot=mentions]:hover]:bg-transparent has-[[data-slot=mentions]:hover]:shadow-none"
131
+ ],
132
+ disabled && "cursor-not-allowed opacity-50",
133
+ className
134
+ ),
135
+ children: [
136
+ href && /* @__PURE__ */ jsx(
137
+ LinkComponent,
138
+ {
139
+ href,
140
+ "aria-label": linkLabel != null ? linkLabel : item.name,
141
+ className: "absolute inset-0 z-[1] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent md:rounded-xl"
142
+ }
143
+ ),
144
+ /* @__PURE__ */ jsx(
145
+ "div",
146
+ {
147
+ "aria-hidden": true,
148
+ className: cn(
149
+ "-ml-4 w-2 shrink-0 self-stretch rounded-r-full bg-muted",
150
+ // The full-bleed row's hover signal, since it cannot take a shadow.
151
+ // `-mr-1` absorbs the extra 4px so the text column does not shift as
152
+ // the mark grows. Grid cells get the shadow instead and hold the mark
153
+ // still — the tab encodes which section you are in, so the less it
154
+ // moves, the better it does that job.
155
+ !isInert && [
156
+ "transition-[width] duration-[120ms] ease-out motion-reduce:transition-none",
157
+ "group-hover:-mr-1 group-hover:w-3 md:group-hover:mr-0 md:group-hover:w-2"
158
+ ]
159
+ ),
160
+ style: markGradient ? { backgroundImage: markGradient } : void 0
161
+ }
162
+ ),
163
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
164
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
165
+ /* @__PURE__ */ jsx("h3", { className: "text-[15px] font-semibold leading-snug text-foreground", children: item.name }),
166
+ showSignatureBadge && /* @__PURE__ */ jsx(
167
+ SignatureChip,
168
+ {
169
+ label: signatureTier === "weak" ? labels.mentioned : labels.signature,
170
+ onEvidence: onSignatureEvidence,
171
+ className: "mt-0.5"
172
+ }
173
+ )
174
+ ] }),
175
+ venueName && /* @__PURE__ */ jsx("p", { className: "mt-0.5 truncate text-[12px] font-medium text-foreground/70", children: venueName }),
176
+ subtitle ? /* @__PURE__ */ jsx("p", { className: "mt-0.5 line-clamp-2 text-xs italic leading-snug text-muted-foreground", children: subtitle }) : item.description && /* @__PURE__ */ jsx(
177
+ "p",
178
+ {
179
+ className: cn(
180
+ "mt-0.5 text-xs text-muted-foreground",
181
+ venueName ? "line-clamp-1" : "line-clamp-2"
182
+ ),
183
+ children: item.description
184
+ }
185
+ ),
186
+ (minPrice != null || hasQuantity || mentionCount > 0) && /* @__PURE__ */ jsxs("div", { className: "mt-1.5 flex items-baseline justify-between gap-x-2 gap-y-1", children: [
187
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-baseline gap-x-2", children: [
188
+ minPrice != null && /* @__PURE__ */ jsxs("span", { className: "flex items-baseline gap-1", children: [
189
+ /* @__PURE__ */ jsx("span", { className: "text-[15px] font-bold tabular-nums text-foreground", children: isPriceRange ? `${formatPrice(minPrice)}\u2013${formatPrice(maxPrice)}` : formatPrice(minPrice) }),
190
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: labels.currency })
191
+ ] }),
192
+ hasQuantity && /* @__PURE__ */ jsxs("span", { className: "text-[12px] tabular-nums text-muted-foreground", children: [
193
+ quantity,
194
+ item.kind === "dish" ? "g" : "ml"
195
+ ] })
196
+ ] }),
197
+ mentionCount > 0 && /* @__PURE__ */ jsx(
198
+ MentionSignal,
199
+ {
200
+ label: labels.mentions(mentionCount),
201
+ count: mentionCount,
202
+ onMentions,
203
+ formatCount
204
+ }
205
+ )
206
+ ] })
207
+ ] })
208
+ ]
209
+ }
210
+ );
211
+ });
212
+
213
+ export { MentionSignal, MenuItem };
@@ -0,0 +1,37 @@
1
+ import { MenuItem } from './chunk-TH6W2PWG.js';
2
+ import { cn } from './chunk-FEK5XGZW.js';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ function VenueSignatureBlock({
6
+ item,
7
+ labels,
8
+ formatPrice,
9
+ formatCount,
10
+ kicker,
11
+ markGradient,
12
+ subtitle,
13
+ href,
14
+ linkComponent,
15
+ className
16
+ }) {
17
+ return /* @__PURE__ */ jsxs("div", { className: cn("overflow-hidden rounded-xl bg-accent-subtle py-1", className), children: [
18
+ /* @__PURE__ */ jsx("span", { className: "block px-4 pt-2 text-[9.5px] font-bold uppercase tracking-[0.09em] text-muted-foreground", children: kicker }),
19
+ /* @__PURE__ */ jsx(
20
+ MenuItem,
21
+ {
22
+ item,
23
+ labels,
24
+ formatPrice,
25
+ formatCount,
26
+ markGradient,
27
+ subtitle,
28
+ showSignatureBadge: false,
29
+ href,
30
+ linkComponent,
31
+ className: "min-h-0 md:rounded-none md:hover:bg-transparent md:hover:shadow-none"
32
+ }
33
+ )
34
+ ] });
35
+ }
36
+
37
+ export { VenueSignatureBlock };
@@ -0,0 +1,92 @@
1
+ import { cn } from './chunk-FEK5XGZW.js';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ var GHOST_ROWS = 13;
5
+ var GHOST_CENTRE = (GHOST_ROWS - 1) / 2;
6
+ var ALPHA_PEAK = 0.44;
7
+ var ALPHA_EDGE = 0;
8
+ var FALLOFF_EXPONENT = 3.4;
9
+ var GHOST_LINE_CHARS = 56;
10
+ function hashString(input) {
11
+ let hash = 5381;
12
+ for (let i = 0; i < input.length; i++) hash = hash * 33 ^ input.charCodeAt(i);
13
+ return Math.abs(hash);
14
+ }
15
+ function MenuField({
16
+ name,
17
+ gradient,
18
+ seedKey,
19
+ variant = "repeat",
20
+ className
21
+ }) {
22
+ if (variant === "display") {
23
+ const words = name.trim().split(/\s+/).filter(Boolean);
24
+ const longestWord = Math.max(1, ...words.map((word) => word.length));
25
+ const lineCount = Math.max(1, words.length);
26
+ const byWidth = (96 / (longestWord * 0.62)).toFixed(1);
27
+ const byHeight = (90 / (lineCount * 0.9)).toFixed(1);
28
+ const fontSize = `clamp(9cqw, min(19cqw, ${byWidth}cqw, ${byHeight}cqh), 19cqw)`;
29
+ return /* @__PURE__ */ jsx(
30
+ "div",
31
+ {
32
+ className: cn("relative isolate overflow-hidden", className),
33
+ style: { backgroundImage: gradient, containerType: "size" },
34
+ children: /* @__PURE__ */ jsx(
35
+ "span",
36
+ {
37
+ "aria-hidden": true,
38
+ 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]",
39
+ style: { fontSize },
40
+ children: name
41
+ }
42
+ )
43
+ }
44
+ );
45
+ }
46
+ const unit = `${name} \xB7 `;
47
+ const repeats = Math.max(2, Math.ceil(GHOST_LINE_CHARS / Math.max(unit.length, 1)));
48
+ const ghostLine = unit.repeat(repeats);
49
+ const seed = hashString(seedKey != null ? seedKey : name);
50
+ const rows = Array.from({ length: GHOST_ROWS }, (_, row) => {
51
+ const distance = Math.abs(row - GHOST_CENTRE) / GHOST_CENTRE;
52
+ const alpha = ALPHA_EDGE + (ALPHA_PEAK - ALPHA_EDGE) * Math.pow(1 - distance, FALLOFF_EXPONENT);
53
+ return { row, alpha, offset: (seed >> row % 12) % 90 - 45 };
54
+ });
55
+ return /* @__PURE__ */ jsx(
56
+ "div",
57
+ {
58
+ className: cn("relative isolate overflow-hidden", className),
59
+ style: { backgroundImage: gradient },
60
+ children: /* @__PURE__ */ jsx(
61
+ "div",
62
+ {
63
+ "aria-hidden": true,
64
+ 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",
65
+ children: rows.map(({ row, alpha, offset }) => /* @__PURE__ */ jsx(
66
+ "span",
67
+ {
68
+ className: "block whitespace-nowrap text-center text-[15px] font-bold leading-[1.45] tracking-[-0.01em]",
69
+ style: {
70
+ color: `oklch(1 0 0 / ${alpha.toFixed(3)})`,
71
+ // Emboss, both edges scaled by this row's alpha: a dark edge below
72
+ // cuts the letterform into the field, a light edge above lifts its
73
+ // top. The dark edge is kept well under the fill — at parity it
74
+ // ringed every glyph and dragged the type back toward grey, which
75
+ // is the opposite of what the fill is doing.
76
+ textShadow: [
77
+ `0 1px 1px oklch(0 0 0 / ${(alpha * 0.4).toFixed(3)})`,
78
+ `0 -1px 0 oklch(1 0 0 / ${(alpha * 0.4).toFixed(3)})`
79
+ ].join(", "),
80
+ transform: `translateX(${offset}px)`
81
+ },
82
+ children: ghostLine
83
+ },
84
+ row
85
+ ))
86
+ }
87
+ )
88
+ }
89
+ );
90
+ }
91
+
92
+ export { MenuField };
@@ -3,10 +3,10 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const alertVariants: (props?: ({
6
- variant?: "success" | "warning" | "default" | "destructive" | null | undefined;
6
+ variant?: "default" | "destructive" | "success" | "warning" | null | undefined;
7
7
  } & class_variance_authority_types.ClassProp) | undefined) => string;
8
8
  declare const Alert: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
9
- variant?: "success" | "warning" | "default" | "destructive" | null | undefined;
9
+ variant?: "default" | "destructive" | "success" | "warning" | null | undefined;
10
10
  } & class_variance_authority_types.ClassProp) | undefined) => string> & React.RefAttributes<HTMLDivElement>>;
11
11
  declare const AlertTitle: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLHeadingElement> & React.RefAttributes<HTMLParagraphElement>>;
12
12
  declare const AlertDescription: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
  import { VariantProps } from 'class-variance-authority';
5
5
 
6
6
  declare const badgeVariants: (props?: ({
7
- variant?: "success" | "warning" | "default" | "destructive" | "outline" | "secondary" | "glass" | "overlay" | "brown" | null | undefined;
7
+ variant?: "default" | "secondary" | "destructive" | "outline" | "overlay" | "glass" | "success" | "warning" | "brown" | null | undefined;
8
8
  live?: boolean | null | undefined;
9
9
  } & class_variance_authority_types.ClassProp) | undefined) => string;
10
10
  interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
@@ -3,7 +3,7 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const buttonVariants: (props?: ({
6
- variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | "glass" | "overlay" | "brown" | "antagonist" | null | undefined;
6
+ variant?: "default" | "secondary" | "destructive" | "outline" | "overlay" | "glass" | "brown" | "link" | "ghost" | "antagonist" | null | undefined;
7
7
  size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
8
8
  fullWidth?: boolean | null | undefined;
9
9
  } & class_variance_authority_types.ClassProp) | undefined) => string;
@@ -8,7 +8,7 @@ declare const Command: React.ForwardRefExoticComponent<Omit<{
8
8
  ref?: React.Ref<HTMLDivElement>;
9
9
  } & {
10
10
  asChild?: boolean;
11
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
11
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & {
12
12
  label?: string;
13
13
  shouldFilter?: boolean;
14
14
  filter?: (value: string, search: string, keywords?: string[]) => number;
@@ -38,7 +38,7 @@ declare const CommandList: React.ForwardRefExoticComponent<Omit<{
38
38
  ref?: React.Ref<HTMLDivElement>;
39
39
  } & {
40
40
  asChild?: boolean;
41
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
41
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & {
42
42
  label?: string;
43
43
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
44
44
  declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
@@ -47,14 +47,14 @@ declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
47
47
  ref?: React.Ref<HTMLDivElement>;
48
48
  } & {
49
49
  asChild?: boolean;
50
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
50
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
51
51
  declare const CommandLoading: React.ForwardRefExoticComponent<Omit<{
52
52
  children?: React.ReactNode;
53
53
  } & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
54
54
  ref?: React.Ref<HTMLDivElement>;
55
55
  } & {
56
56
  asChild?: boolean;
57
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
57
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & {
58
58
  progress?: number;
59
59
  label?: string;
60
60
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
@@ -64,7 +64,7 @@ declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
64
64
  ref?: React.Ref<HTMLDivElement>;
65
65
  } & {
66
66
  asChild?: boolean;
67
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "heading" | "value"> & {
67
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>>, "heading" | "value"> & {
68
68
  heading?: React.ReactNode;
69
69
  value?: string;
70
70
  forceMount?: boolean;
@@ -73,7 +73,7 @@ declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<R
73
73
  ref?: React.Ref<HTMLDivElement>;
74
74
  } & {
75
75
  asChild?: boolean;
76
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
76
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & {
77
77
  alwaysRender?: boolean;
78
78
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
79
79
  declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
@@ -82,7 +82,7 @@ declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
82
82
  ref?: React.Ref<HTMLDivElement>;
83
83
  } & {
84
84
  asChild?: boolean;
85
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "onSelect" | "disabled" | "value"> & {
85
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>>, "onSelect" | "disabled" | "value"> & {
86
86
  disabled?: boolean;
87
87
  onSelect?: (value: string) => void;
88
88
  value?: string;
@@ -0,0 +1,73 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { MenuItemLabels } from './menu-item.js';
3
+ import { MenuConceptPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
4
+ import 'react';
5
+ import './place-types.js';
6
+
7
+ /**
8
+ * ConceptCompare — one dish or drink across every kitchen that makes it, under
9
+ * whatever name each of them uses.
10
+ *
11
+ * The keystone module: a single menu can list papanași, only a catalogue can
12
+ * say that seventy kitchens make them under fifty different names between 8 and
13
+ * 44 lei. The alias column is what makes it honest — these are not the same row
14
+ * seventy times, they are seventy differently-named rows we matched, and
15
+ * showing each kitchen's own wording is the evidence for that.
16
+ *
17
+ * ## The row is place-led
18
+ *
19
+ * Each row leads with the VENUE PHOTOGRAPH, which is real and licensed. It
20
+ * replaced a price bar, and the trade is not close: the bar measured the one
21
+ * value already printed beside it in words, while the thing a reader is
22
+ * actually choosing between — the restaurant — had no image at all. Five
23
+ * restaurants that disagree about a name is the story; five bars of similar
24
+ * length is not.
25
+ *
26
+ * ## Two layouts, one component
27
+ *
28
+ * From `lg` the row is a table: rank, venue, alias, mentions, price. Below it,
29
+ * the rows become the production `<MenuItem>` — the alias as the name, the
30
+ * venue underneath — because a five-column table does not survive 390px and
31
+ * the item row already solves this exact shape everywhere else in the app.
32
+ */
33
+ /** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
34
+ type ConceptCompareLinkProps = MenuLinkProps;
35
+ type ConceptCompareFigure = {
36
+ /** Pre-formatted, e.g. "44". */
37
+ value: string;
38
+ label: string;
39
+ };
40
+ type ConceptCompareLabels = {
41
+ /** Small caps kicker, e.g. "DISH CONCEPT". */
42
+ kind: string;
43
+ /** Column heading over the aliases, e.g. "ON THEIR MENU AS". */
44
+ alias: string;
45
+ /** Under each price, e.g. "per portion". */
46
+ perPortion?: string;
47
+ /** Badge on the lowest-priced row, e.g. "cheapest shown". */
48
+ cheapest?: string;
49
+ };
50
+ type ConceptCompareProps = {
51
+ concept: MenuConceptPreview;
52
+ labels: ConceptCompareLabels;
53
+ /** Passed to the mobile `<MenuItem>` rows. */
54
+ itemLabels: MenuItemLabels;
55
+ formatPrice: (amount: number) => string;
56
+ formatCount?: (value: number) => string;
57
+ /** CSS gradient for the header swatch, when there is no photograph. */
58
+ gradient: string;
59
+ /** A real photograph of the concept, when the matched menu row carries one. */
60
+ imageUrl?: string;
61
+ figures?: ConceptCompareFigure[];
62
+ /** Destination per row, keyed by place slug. */
63
+ hrefs?: Record<string, string>;
64
+ /** The provenance note in the footer. */
65
+ footnote?: string;
66
+ /** Actions in the footer — pass rendered links or buttons. */
67
+ actions?: React.ReactNode;
68
+ linkComponent?: MenuLinkComponent;
69
+ className?: string;
70
+ };
71
+ declare function ConceptCompare({ concept, labels, itemLabels, formatPrice, formatCount, gradient, imageUrl, figures, hrefs, footnote, actions, linkComponent: LinkComponent, className, }: ConceptCompareProps): react_jsx_runtime.JSX.Element;
72
+
73
+ export { ConceptCompare, type ConceptCompareFigure, type ConceptCompareLabels, type ConceptCompareLinkProps, type ConceptCompareProps };