@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.
Files changed (206) hide show
  1. package/dist/chunk-24RFI4RQ.js +167 -0
  2. package/dist/chunk-2XCZJWTV.js +121 -0
  3. package/dist/chunk-5RWTOT5Y.js +72 -0
  4. package/dist/chunk-62ES6N32.js +27 -0
  5. package/dist/chunk-AKQ57NXY.js +58 -0
  6. package/dist/chunk-CLLY6XB3.js +41 -0
  7. package/dist/chunk-DTXNDLSE.js +125 -0
  8. package/dist/chunk-F6FMZJDG.js +113 -0
  9. package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
  10. package/dist/chunk-MZL7CGCL.js +109 -0
  11. package/dist/chunk-Q6QH52LJ.js +38 -0
  12. package/dist/chunk-SY6RV476.js +48 -0
  13. package/dist/chunk-TH6W2PWG.js +213 -0
  14. package/dist/chunk-UDN7UHJW.js +60 -0
  15. package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
  16. package/dist/components/alert.d.ts +2 -2
  17. package/dist/components/badge.d.ts +1 -1
  18. package/dist/components/concept-compare-rail.d.ts +50 -0
  19. package/dist/components/concept-compare-rail.js +5 -0
  20. package/dist/components/concept-compare.d.ts +110 -0
  21. package/dist/components/concept-compare.js +238 -0
  22. package/dist/components/concept-convergence.d.ts +41 -0
  23. package/dist/components/concept-convergence.js +53 -0
  24. package/dist/components/convergence.d.ts +25 -0
  25. package/dist/components/convergence.js +4 -0
  26. package/dist/components/date-input.d.ts +1 -1
  27. package/dist/components/event-card.js +1 -1
  28. package/dist/components/event-compact-row.d.ts +1 -1
  29. package/dist/components/event-compact-row.js +1 -1
  30. package/dist/components/event-detail-panel.d.ts +171 -25
  31. package/dist/components/event-detail-panel.js +472 -175
  32. package/dist/components/event-index-row.js +1 -1
  33. package/dist/components/event-list-row-wide.js +2 -2
  34. package/dist/components/event-list-row.js +2 -2
  35. package/dist/components/explainer-steps.d.ts +34 -0
  36. package/dist/components/explainer-steps.js +36 -0
  37. package/dist/components/glass-ground.d.ts +66 -0
  38. package/dist/components/glass-ground.js +3 -0
  39. package/dist/components/hub-surface.d.ts +27 -0
  40. package/dist/components/hub-surface.js +30 -0
  41. package/dist/components/hue-chip.d.ts +70 -0
  42. package/dist/components/hue-chip.js +3 -0
  43. package/dist/components/kitchen-dossier.d.ts +88 -0
  44. package/dist/components/kitchen-dossier.js +187 -0
  45. package/dist/components/menu-dead-end.d.ts +56 -0
  46. package/dist/components/menu-dead-end.js +49 -0
  47. package/dist/components/menu-family-block.d.ts +99 -0
  48. package/dist/components/menu-family-block.js +155 -0
  49. package/dist/components/menu-family-card.d.ts +65 -0
  50. package/dist/components/menu-family-card.js +79 -0
  51. package/dist/components/menu-field.d.ts +57 -0
  52. package/dist/components/menu-field.js +93 -0
  53. package/dist/components/menu-filter-chips.d.ts +86 -0
  54. package/dist/components/menu-filter-chips.js +122 -0
  55. package/dist/components/menu-group-rows.d.ts +53 -0
  56. package/dist/components/menu-group-rows.js +67 -0
  57. package/dist/components/menu-hub-hero.d.ts +65 -0
  58. package/dist/components/menu-hub-hero.js +103 -0
  59. package/dist/components/menu-index-header.d.ts +37 -0
  60. package/dist/components/menu-index-header.js +68 -0
  61. package/dist/components/menu-index-list.d.ts +48 -0
  62. package/dist/components/menu-index-list.js +96 -0
  63. package/dist/components/menu-intent-hero.d.ts +69 -0
  64. package/dist/components/menu-intent-hero.js +87 -0
  65. package/dist/components/menu-intent-tiles.d.ts +31 -0
  66. package/dist/components/menu-intent-tiles.js +50 -0
  67. package/dist/components/menu-item.d.ts +127 -0
  68. package/dist/components/menu-item.js +5 -0
  69. package/dist/components/menu-kitchen-list.d.ts +82 -0
  70. package/dist/components/menu-kitchen-list.js +5 -0
  71. package/dist/components/menu-mocks.d.ts +90 -0
  72. package/dist/components/menu-mocks.js +504 -0
  73. package/dist/components/menu-types.d.ts +421 -0
  74. package/dist/components/menu-types.js +1 -0
  75. package/dist/components/motion-icon-controls.js +2 -2
  76. package/dist/components/motion-icon-set.js +21 -9
  77. package/dist/components/motion-icon.d.ts +22 -1
  78. package/dist/components/motion-icon.js +1 -1
  79. package/dist/components/open-now-list.d.ts +40 -0
  80. package/dist/components/open-now-list.js +69 -0
  81. package/dist/components/photo-ground.d.ts +64 -0
  82. package/dist/components/photo-ground.js +3 -0
  83. package/dist/components/place-about-tab.js +2 -2
  84. package/dist/components/place-details-mobile.js +2 -2
  85. package/dist/components/place-types.d.ts +112 -1
  86. package/dist/components/qa-block.d.ts +36 -0
  87. package/dist/components/qa-block.js +24 -0
  88. package/dist/components/scroll-fade.js +3 -121
  89. package/dist/components/signature-chip.d.ts +37 -0
  90. package/dist/components/signature-chip.js +4 -0
  91. package/dist/components/snap-rail.d.ts +48 -5
  92. package/dist/components/snap-rail.js +3 -51
  93. package/dist/components/stat-banner.d.ts +47 -0
  94. package/dist/components/stat-banner.js +61 -0
  95. package/dist/components/story.d.ts +34 -0
  96. package/dist/components/story.js +3 -0
  97. package/dist/components/tag.d.ts +2 -2
  98. package/dist/components/text.d.ts +1 -1
  99. package/dist/components/top-comparison.d.ts +28 -0
  100. package/dist/components/top-comparison.js +76 -0
  101. package/dist/components/top-cross-link.d.ts +34 -0
  102. package/dist/components/top-cross-link.js +101 -0
  103. package/dist/components/top-ranking-entry.d.ts +53 -0
  104. package/dist/components/top-ranking-entry.js +217 -0
  105. package/dist/components/top-ranking-hero.d.ts +61 -0
  106. package/dist/components/top-ranking-hero.js +99 -0
  107. package/dist/components/top-ranking-legend.d.ts +69 -0
  108. package/dist/components/top-ranking-legend.js +124 -0
  109. package/dist/components/top-types.d.ts +109 -0
  110. package/dist/components/top-types.js +1 -0
  111. package/dist/components/venue-grade-dimensions.d.ts +38 -0
  112. package/dist/components/venue-grade-dimensions.js +3 -0
  113. package/dist/components/venue-quotes.d.ts +32 -0
  114. package/dist/components/venue-quotes.js +3 -0
  115. package/dist/components/venue-wall.d.ts +41 -0
  116. package/dist/components/venue-wall.js +118 -0
  117. package/dist/components/weekday-theme-banner.d.ts +70 -0
  118. package/dist/components/weekday-theme-banner.js +127 -0
  119. package/dist/components/weekend-panel.js +2 -2
  120. package/package.json +1 -1
  121. package/src/components/colour-dictionary.stories.tsx +227 -0
  122. package/src/components/concept-compare-rail.tsx +131 -0
  123. package/src/components/concept-compare.stories.tsx +172 -0
  124. package/src/components/concept-compare.tsx +443 -0
  125. package/src/components/concept-convergence.stories.tsx +53 -0
  126. package/src/components/concept-convergence.tsx +90 -0
  127. package/src/components/convergence.stories.tsx +54 -0
  128. package/src/components/convergence.tsx +177 -0
  129. package/src/components/event-compact-row.tsx +1 -1
  130. package/src/components/event-detail-panel.stories.tsx +205 -97
  131. package/src/components/event-detail-panel.test.tsx +181 -0
  132. package/src/components/event-detail-panel.tsx +703 -185
  133. package/src/components/explainer-steps.stories.tsx +46 -0
  134. package/src/components/explainer-steps.tsx +74 -0
  135. package/src/components/glass-ground.tsx +129 -0
  136. package/src/components/hub-surface.stories.tsx +68 -0
  137. package/src/components/hub-surface.tsx +50 -0
  138. package/src/components/hue-chip.stories.tsx +76 -0
  139. package/src/components/hue-chip.test.tsx +62 -0
  140. package/src/components/hue-chip.tsx +99 -0
  141. package/src/components/kitchen-dossier.stories.tsx +108 -0
  142. package/src/components/kitchen-dossier.tsx +322 -0
  143. package/src/components/menu-dead-end.stories.tsx +67 -0
  144. package/src/components/menu-dead-end.tsx +127 -0
  145. package/src/components/menu-family-block.stories.tsx +91 -0
  146. package/src/components/menu-family-block.tsx +314 -0
  147. package/src/components/menu-family-card.stories.tsx +120 -0
  148. package/src/components/menu-family-card.tsx +191 -0
  149. package/src/components/menu-field.stories.tsx +74 -0
  150. package/src/components/menu-field.tsx +199 -0
  151. package/src/components/menu-filter-chips.stories.tsx +107 -0
  152. package/src/components/menu-filter-chips.tsx +238 -0
  153. package/src/components/menu-group-rows.stories.tsx +54 -0
  154. package/src/components/menu-group-rows.tsx +133 -0
  155. package/src/components/menu-hub-hero.stories.tsx +92 -0
  156. package/src/components/menu-hub-hero.tsx +187 -0
  157. package/src/components/menu-index-header.stories.tsx +72 -0
  158. package/src/components/menu-index-header.tsx +125 -0
  159. package/src/components/menu-index-list.stories.tsx +50 -0
  160. package/src/components/menu-index-list.tsx +149 -0
  161. package/src/components/menu-intent-hero.stories.tsx +67 -0
  162. package/src/components/menu-intent-hero.tsx +189 -0
  163. package/src/components/menu-intent-tiles.stories.tsx +54 -0
  164. package/src/components/menu-intent-tiles.tsx +76 -0
  165. package/src/components/menu-item.tsx +416 -0
  166. package/src/components/menu-kitchen-list.stories.tsx +67 -0
  167. package/src/components/menu-kitchen-list.test.tsx +78 -0
  168. package/src/components/menu-kitchen-list.tsx +284 -0
  169. package/src/components/menu-mocks.ts +592 -0
  170. package/src/components/menu-types.ts +442 -0
  171. package/src/components/motion-icon-controls.tsx +11 -11
  172. package/src/components/motion-icon-set.tsx +36 -8
  173. package/src/components/motion-icon.test.tsx +23 -1
  174. package/src/components/motion-icon.tsx +20 -0
  175. package/src/components/open-now-list.stories.tsx +76 -0
  176. package/src/components/open-now-list.tsx +127 -0
  177. package/src/components/photo-ground.tsx +147 -0
  178. package/src/components/place-types.ts +112 -0
  179. package/src/components/qa-block.stories.tsx +43 -0
  180. package/src/components/qa-block.tsx +60 -0
  181. package/src/components/signature-chip.tsx +77 -0
  182. package/src/components/snap-rail.tsx +133 -9
  183. package/src/components/stat-banner.stories.tsx +100 -0
  184. package/src/components/stat-banner.tsx +117 -0
  185. package/src/components/story.tsx +225 -0
  186. package/src/components/top-comparison.stories.tsx +78 -0
  187. package/src/components/top-comparison.tsx +151 -0
  188. package/src/components/top-cross-link.stories.tsx +84 -0
  189. package/src/components/top-cross-link.tsx +171 -0
  190. package/src/components/top-ranking-entry.stories.tsx +171 -0
  191. package/src/components/top-ranking-entry.test.tsx +105 -0
  192. package/src/components/top-ranking-entry.tsx +425 -0
  193. package/src/components/top-ranking-hero.stories.tsx +76 -0
  194. package/src/components/top-ranking-hero.tsx +218 -0
  195. package/src/components/top-ranking-legend.stories.tsx +78 -0
  196. package/src/components/top-ranking-legend.tsx +239 -0
  197. package/src/components/top-types.ts +110 -0
  198. package/src/components/venue-grade-dimensions.stories.tsx +83 -0
  199. package/src/components/venue-grade-dimensions.tsx +91 -0
  200. package/src/components/venue-quotes.stories.tsx +59 -0
  201. package/src/components/venue-quotes.tsx +64 -0
  202. package/src/components/venue-wall.stories.tsx +78 -0
  203. package/src/components/venue-wall.tsx +185 -0
  204. package/src/components/weekday-theme-banner.stories.tsx +113 -0
  205. package/src/components/weekday-theme-banner.tsx +231 -0
  206. package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
@@ -0,0 +1,86 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { MenuFilterChip } from './menu-types.js';
3
+ import 'react';
4
+ import './place-types.js';
5
+
6
+ /**
7
+ * MenuFilterChips — the intent page's filter row.
8
+ *
9
+ * ## One unit, one list that moves
10
+ *
11
+ * Every chip counts KITCHENS, whatever facet is behind it. A dietary chip reads
12
+ * "has a vegetarian dish" and counts venues; a dish-group chip reads "has a
13
+ * pizza" and counts venues. The earlier version mixed venue counts and dish
14
+ * counts in one row and became unreadable — you could not tell what a number
15
+ * was counting without reading the label that produced it. The unit is stated
16
+ * once, at the head of the row (`unitLabel`, e.g. "KITCHENS · 247"), and never
17
+ * again.
18
+ *
19
+ * ## A zero-count chip is dimmed, not disabled
20
+ *
21
+ * `empty` chips stay tappable. Pressing one swaps it in and drops whichever
22
+ * active filter conflicts with it, and the caller says which in a line beneath.
23
+ * Disabling it would refuse the reader the thing they asked for and give no
24
+ * reason; this way the row answers the question by being pressed. Facets the
25
+ * API says are unavailable for this intent are not passed in at all — not
26
+ * drawn, rather than drawn dead.
27
+ *
28
+ * ## The row scrolls; the escape hatch does not
29
+ *
30
+ * One line in every state. Chips scroll horizontally under a `<ScrollFade>`,
31
+ * and "More filters" is pinned opposite the unit label rather than wrapping,
32
+ * so the way out of a full row is always in the same place.
33
+ *
34
+ * Interactive: the consumer marks its own module `"use client"`.
35
+ */
36
+ type MenuFilterChipsProps = {
37
+ chips: MenuFilterChip[];
38
+ onToggle: (key: string) => void;
39
+ /** The unit and the current total, composed by the caller, e.g. "KITCHENS · 247". */
40
+ unitLabel?: string;
41
+ /** Opens the full filter sheet. Omit for a row where every facet is a chip. */
42
+ onMore?: () => void;
43
+ moreLabel?: string;
44
+ /**
45
+ * A glyph for the phone's icon-only "more filters" button.
46
+ *
47
+ * The label is the widest fixed element in the row — "More filters" is ~80px
48
+ * of a 358px viewport, and it is the one thing that never scrolls, so it eats
49
+ * the chips' runway on every phone. Given an icon, the button collapses to a
50
+ * square below `lg` and keeps `moreLabel` as its accessible name.
51
+ *
52
+ * A node rather than a name because the app owns its iconography, and this
53
+ * one is animated on press — the DS neither knows the glyph nor drives it.
54
+ */
55
+ moreIcon?: React.ReactNode;
56
+ /** Press handler for the icon button, e.g. to replay the glyph's animation. */
57
+ onMorePointerDown?: () => void;
58
+ /** Locale-aware integer formatter for the counts. Defaults to `String`. */
59
+ formatCount?: (value: number) => string;
60
+ /** Accessible name for the row. */
61
+ ariaLabel?: string;
62
+ className?: string;
63
+ };
64
+ declare function MenuFilterChips({ chips, onToggle, unitLabel, onMore, moreLabel, moreIcon, onMorePointerDown, formatCount, ariaLabel, className, }: MenuFilterChipsProps): react_jsx_runtime.JSX.Element;
65
+ /**
66
+ * MenuFilterSummary — what the active filters did, and the way out.
67
+ *
68
+ * Sits under the row whenever anything is on. The sentence is composed by the
69
+ * caller because it is a real sentence in two languages ("Three filters · 34 of
70
+ * 247 kitchens are mid-range, have a vegetarian Italian dish, and are open
71
+ * now") and assembling it from fragments here would produce translated
72
+ * word-salad in one of them.
73
+ */
74
+ type MenuFilterSummaryProps = {
75
+ /** The composed sentence. */
76
+ summary: React.ReactNode;
77
+ clearLabel: string;
78
+ onClear: () => void;
79
+ className?: string;
80
+ };
81
+ declare function MenuFilterSummary({ summary, clearLabel, onClear, className, }: MenuFilterSummaryProps): react_jsx_runtime.JSX.Element;
82
+ declare function MenuFilterChipsSkeleton({ className }: {
83
+ className?: string;
84
+ }): react_jsx_runtime.JSX.Element;
85
+
86
+ export { MenuFilterChips, type MenuFilterChipsProps, MenuFilterChipsSkeleton, MenuFilterSummary, type MenuFilterSummaryProps };
@@ -0,0 +1,122 @@
1
+ import { ScrollFade } from '../chunk-2XCZJWTV.js';
2
+ import { Skeleton } from '../chunk-QXHMPJ35.js';
3
+ import { cn } from '../chunk-FEK5XGZW.js';
4
+ import '../chunk-3GWWZKX7.js';
5
+ import { X } from 'lucide-react';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
+
8
+ function MenuFilterChips({
9
+ chips,
10
+ onToggle,
11
+ unitLabel,
12
+ onMore,
13
+ moreLabel,
14
+ moreIcon,
15
+ onMorePointerDown,
16
+ formatCount = String,
17
+ ariaLabel,
18
+ className
19
+ }) {
20
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative flex items-center gap-2", className), children: [
21
+ unitLabel && /* @__PURE__ */ jsx("span", { className: "hidden shrink-0 pr-0.5 text-[11px] font-extrabold uppercase tracking-[0.1em] text-muted-foreground lg:inline", children: unitLabel }),
22
+ /* @__PURE__ */ jsx(
23
+ ScrollFade,
24
+ {
25
+ orientation: "horizontal",
26
+ size: "sm",
27
+ className: "min-w-0 flex-1",
28
+ scrollClassName: "scrollbar-hide",
29
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", role: "group", "aria-label": ariaLabel, children: chips.map((chip) => /* @__PURE__ */ jsx(
30
+ Chip,
31
+ {
32
+ chip,
33
+ onToggle: () => onToggle(chip.key),
34
+ formatCount
35
+ },
36
+ chip.key
37
+ )) })
38
+ }
39
+ ),
40
+ onMore && /* @__PURE__ */ jsxs(Fragment, { children: [
41
+ moreIcon && /* @__PURE__ */ jsx(
42
+ "button",
43
+ {
44
+ type: "button",
45
+ onClick: onMore,
46
+ onPointerDown: onMorePointerDown,
47
+ "aria-label": moreLabel,
48
+ "aria-haspopup": "dialog",
49
+ className: "inline-flex size-10 shrink-0 items-center justify-center rounded-full border border-border text-accent transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none lg:hidden",
50
+ children: moreIcon
51
+ }
52
+ ),
53
+ /* @__PURE__ */ jsx(
54
+ "button",
55
+ {
56
+ type: "button",
57
+ onClick: onMore,
58
+ className: cn(
59
+ "shrink-0 rounded-md pl-2 text-[12.5px] font-bold text-accent transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
60
+ moreIcon && "hidden lg:inline"
61
+ ),
62
+ children: moreLabel
63
+ }
64
+ )
65
+ ] })
66
+ ] });
67
+ }
68
+ function Chip({
69
+ chip,
70
+ onToggle,
71
+ formatCount
72
+ }) {
73
+ return /* @__PURE__ */ jsxs(
74
+ "button",
75
+ {
76
+ type: "button",
77
+ onClick: onToggle,
78
+ "aria-pressed": chip.active,
79
+ className: cn(
80
+ "inline-flex h-9 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full border px-4 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
81
+ chip.active ? "border-accent font-semibold text-accent shadow-[0_1px_2px_oklch(0.51_0.095_45_/_0.08)]" : "border-border bg-card text-foreground hover:border-accent/40",
82
+ // Dimmed, never disabled — pressing it is how the reader finds out
83
+ // which active filter is in the way.
84
+ chip.empty && "opacity-45"
85
+ ),
86
+ children: [
87
+ chip.icon,
88
+ chip.label,
89
+ chip.count !== void 0 && /* @__PURE__ */ jsx("span", { className: cn("tabular-nums", chip.active ? "text-accent" : "text-muted-foreground"), children: formatCount(chip.count) }),
90
+ chip.active && /* @__PURE__ */ jsx(X, { className: "h-3 w-3 shrink-0", "aria-hidden": true })
91
+ ]
92
+ }
93
+ );
94
+ }
95
+ function MenuFilterSummary({
96
+ summary,
97
+ clearLabel,
98
+ onClear,
99
+ className
100
+ }) {
101
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-wrap items-center gap-2.5 pt-3.5", className), children: [
102
+ /* @__PURE__ */ jsx("span", { className: "text-[13px] text-muted-foreground [&_b]:font-bold [&_b]:text-foreground", children: summary }),
103
+ /* @__PURE__ */ jsx(
104
+ "button",
105
+ {
106
+ type: "button",
107
+ onClick: onClear,
108
+ className: "ml-auto shrink-0 rounded-md text-[12.5px] font-bold text-accent transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
109
+ children: clearLabel
110
+ }
111
+ )
112
+ ] });
113
+ }
114
+ var CHIP_WIDTHS = [128, 104, 96, 148, 112, 88];
115
+ function MenuFilterChipsSkeleton({ className }) {
116
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2 overflow-hidden", className), "aria-hidden": true, children: [
117
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-[92px] shrink-0" }),
118
+ CHIP_WIDTHS.map((width, index) => /* @__PURE__ */ jsx(Skeleton, { className: "h-9 shrink-0 rounded-full", style: { width } }, index))
119
+ ] });
120
+ }
121
+
122
+ export { MenuFilterChips, MenuFilterChipsSkeleton, MenuFilterSummary };
@@ -0,0 +1,53 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { MenuLinkComponent, MenuLinkProps } from './menu-types.js';
3
+ import 'react';
4
+ import './place-types.js';
5
+
6
+ /**
7
+ * MenuGroupRows — what an intent consists of.
8
+ *
9
+ * On a cuisine page this is the `dish_groups` facet narrowed by the cuisine
10
+ * ("Pizza · 120 kitchens · 30–68"); on a going-out page it is the digest tags
11
+ * and graded strengths ("Cocktails · 63 places"). One component for both,
12
+ * because both are the same object: a facet value, its venue count, and a range.
13
+ *
14
+ * ## The counts are VENUES, and the label has to say so
15
+ *
16
+ * There is no endpoint that counts dishes per cuisine — `/dishes` accepts
17
+ * `cuisine_slugs` and ignores it — so "120" here is a hundred and twenty
18
+ * kitchens that make a pizza, not a hundred and twenty pizzas. The `meta` line
19
+ * is composed by the caller precisely so that it can say which, in a language
20
+ * where the two words differ.
21
+ *
22
+ * ## Not `<ConceptCard>`
23
+ *
24
+ * That one is a cover-led card for a thing with a date and a venue. This is a
25
+ * row in a sidebar: a hue bar, a name, a count and a range. Giving it a cover
26
+ * would mean finding a photograph of "pasta", which is the kind of picture that
27
+ * makes a page look generated.
28
+ */
29
+ /** @deprecated Alias of `MenuLinkProps`; kept for symmetry with the other menu surfaces. */
30
+ type MenuGroupRowsLinkProps = MenuLinkProps;
31
+ type MenuGroupRow = {
32
+ slug: string;
33
+ name: string;
34
+ /** Venue count line, e.g. "120 kitchens". Pluralised by the caller. */
35
+ meta: string;
36
+ /** Price range, already formatted, e.g. "30—68". Omitted where none is known. */
37
+ rangeLabel?: string;
38
+ href?: string;
39
+ /** Finished CSS gradient for the bar. A missing one falls back to the border colour. */
40
+ gradient?: string;
41
+ };
42
+ type MenuGroupRowsProps = {
43
+ rows: MenuGroupRow[];
44
+ linkComponent?: MenuLinkComponent;
45
+ className?: string;
46
+ };
47
+ declare function MenuGroupRows({ rows, linkComponent: LinkComponent, className, }: MenuGroupRowsProps): react_jsx_runtime.JSX.Element;
48
+ declare function MenuGroupRowsSkeleton({ rows, className, }: {
49
+ rows?: number;
50
+ className?: string;
51
+ }): react_jsx_runtime.JSX.Element;
52
+
53
+ export { type MenuGroupRow, MenuGroupRows, type MenuGroupRowsLinkProps, type MenuGroupRowsProps, MenuGroupRowsSkeleton };
@@ -0,0 +1,67 @@
1
+ import { Skeleton } from '../chunk-QXHMPJ35.js';
2
+ import { cn } from '../chunk-FEK5XGZW.js';
3
+ import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
4
+ import { ChevronRight } from 'lucide-react';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+
7
+ function DefaultLink(_a) {
8
+ var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
9
+ return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
10
+ }
11
+ function MenuGroupRows({
12
+ rows,
13
+ linkComponent: LinkComponent = DefaultLink,
14
+ className
15
+ }) {
16
+ return /* @__PURE__ */ jsx("ul", { className: cn("flex flex-col", className), children: rows.map((row) => {
17
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
18
+ /* @__PURE__ */ jsx(
19
+ "span",
20
+ {
21
+ "aria-hidden": true,
22
+ className: cn(
23
+ "h-[34px] w-2 shrink-0 rounded-full",
24
+ !row.gradient && "bg-border"
25
+ ),
26
+ style: row.gradient ? { backgroundImage: row.gradient } : void 0
27
+ }
28
+ ),
29
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
30
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-sm font-bold", children: row.name }),
31
+ /* @__PURE__ */ jsx("span", { className: "mt-px block truncate text-[11.5px] text-muted-foreground", children: row.meta })
32
+ ] }),
33
+ row.rangeLabel && /* @__PURE__ */ jsx("span", { className: "shrink-0 font-mono text-xs tabular-nums text-muted-foreground", children: row.rangeLabel }),
34
+ /* @__PURE__ */ jsx(ChevronRight, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true })
35
+ ] });
36
+ return /* @__PURE__ */ jsx("li", { className: "border-t border-border first:border-t-0", children: row.href ? /* @__PURE__ */ jsx(
37
+ LinkComponent,
38
+ {
39
+ href: row.href,
40
+ className: "flex items-center gap-3 py-3 transition-colors hover:bg-muted/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none",
41
+ children: body
42
+ }
43
+ ) : /* @__PURE__ */ jsx("span", { className: "flex items-center gap-3 py-3", children: body }) }, row.slug);
44
+ }) });
45
+ }
46
+ function MenuGroupRowsSkeleton({
47
+ rows = 5,
48
+ className
49
+ }) {
50
+ return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col", className), "aria-hidden": true, children: Array.from({ length: rows }, (_, index) => /* @__PURE__ */ jsxs(
51
+ "div",
52
+ {
53
+ className: "flex items-center gap-3 border-t border-border py-3 first:border-t-0",
54
+ children: [
55
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[34px] w-2 rounded-full" }),
56
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
57
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 w-[50%]" }),
58
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-[35%]" })
59
+ ] }),
60
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-[52px]" })
61
+ ]
62
+ },
63
+ index
64
+ )) });
65
+ }
66
+
67
+ export { MenuGroupRows, MenuGroupRowsSkeleton };
@@ -0,0 +1,65 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * MenuHubHero — the claim that opens a menu hub, proven by live figures.
5
+ *
6
+ * A statement rather than a count ("Nobody should have to walk in blind"),
7
+ * over a dark ground, with the catalogue's real numbers under it and one proof
8
+ * line that demonstrates what the catalogue can do that a single menu cannot.
9
+ *
10
+ * ## Why the ground is a texture and not a photograph
11
+ *
12
+ * Menu-item photography is AI-generated and never shown, and one venue
13
+ * photograph behind a claim about every venue in the city argues the wrong
14
+ * thing. The tile grid reads as "many menus" without being any of them.
15
+ * Callers that DO have a real, licensed image for the slot can pass
16
+ * `backgroundUrl` — venue photography, never a plate.
17
+ *
18
+ * ## Figures
19
+ *
20
+ * `figures` is a list, not four fixed slots: the drinks tab has different ones
21
+ * and the count changes when a number turns out to have nothing behind it.
22
+ * Every value is a pre-formatted string because thousands separators are
23
+ * locale-dependent and the DS owns no locale.
24
+ *
25
+ * Server-safe.
26
+ */
27
+ type MenuHubHeroFigure = {
28
+ /** Pre-formatted, e.g. "19,062". */
29
+ value: string;
30
+ label: string;
31
+ };
32
+ type MenuHubHeroProps = {
33
+ /** Small caps line above the headline, e.g. "397 MENUS IN CLUJ-NAPOCA, READ IN FULL". */
34
+ kicker?: string;
35
+ /** Icon for the kicker, resolved by the caller. */
36
+ kickerIcon?: React.ReactNode;
37
+ headline: string;
38
+ body?: string;
39
+ figures?: MenuHubHeroFigure[];
40
+ /**
41
+ * The proof line — one demonstrated fact, e.g. "A margherita: 87 kitchens
42
+ * make one, 10 to 120 lei". Composed by the caller; clause order is
43
+ * locale-dependent.
44
+ */
45
+ proof?: React.ReactNode;
46
+ proofIcon?: React.ReactNode;
47
+ /** The action beside the proof line. Pass a rendered link or button. */
48
+ action?: React.ReactNode;
49
+ /** Real, licensed photograph for the ground. Venue imagery only — see above. */
50
+ backgroundUrl?: string;
51
+ /**
52
+ * Tiles for the mosaic ground — generic, cinematic, licensed stills, served
53
+ * from the consumer's own origin.
54
+ *
55
+ * A mosaic rather than one photograph because the hero's claim is about EVERY
56
+ * kitchen in the city: a single image makes it about one of them. Given fewer
57
+ * tiles than cells the list repeats, so a caller can ship six and still fill
58
+ * a 1440px hero.
59
+ */
60
+ tileUrls?: string[];
61
+ className?: string;
62
+ };
63
+ declare function MenuHubHero({ kicker, kickerIcon, headline, body, figures, proof, proofIcon, action, backgroundUrl, tileUrls, className, }: MenuHubHeroProps): react_jsx_runtime.JSX.Element;
64
+
65
+ export { MenuHubHero, type MenuHubHeroFigure, type MenuHubHeroProps };
@@ -0,0 +1,103 @@
1
+ import { cn } from '../chunk-FEK5XGZW.js';
2
+ import '../chunk-3GWWZKX7.js';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ var TEXTURE_CELLS = 28;
6
+ function MenuHubHero({
7
+ kicker,
8
+ kickerIcon,
9
+ headline,
10
+ body,
11
+ figures,
12
+ proof,
13
+ proofIcon,
14
+ action,
15
+ backgroundUrl,
16
+ tileUrls,
17
+ className
18
+ }) {
19
+ return /* @__PURE__ */ jsxs(
20
+ "div",
21
+ {
22
+ className: cn(
23
+ "relative flex min-h-[260px] overflow-hidden rounded-[20px] bg-[oklch(0.21_0.012_55)] lg:min-h-[440px]",
24
+ className
25
+ ),
26
+ children: [
27
+ backgroundUrl ? (
28
+ // eslint-disable-next-line @next/next/no-img-element
29
+ /* @__PURE__ */ jsx(
30
+ "img",
31
+ {
32
+ src: backgroundUrl,
33
+ alt: "",
34
+ "aria-hidden": true,
35
+ className: "absolute inset-0 h-full w-full object-cover opacity-[0.42]"
36
+ }
37
+ )
38
+ ) : /* @__PURE__ */ jsx(
39
+ "div",
40
+ {
41
+ "aria-hidden": true,
42
+ className: "absolute inset-0 grid auto-rows-fr grid-cols-4 gap-0.5 lg:grid-cols-7",
43
+ children: Array.from({ length: TEXTURE_CELLS }, (_, cell) => {
44
+ const url = (tileUrls == null ? void 0 : tileUrls.length) ? tileUrls[cell % tileUrls.length] : void 0;
45
+ return url ? (
46
+ // eslint-disable-next-line @next/next/no-img-element
47
+ /* @__PURE__ */ jsx(
48
+ "img",
49
+ {
50
+ src: url,
51
+ alt: "",
52
+ loading: "lazy",
53
+ decoding: "async",
54
+ className: "h-full w-full object-cover"
55
+ },
56
+ cell
57
+ )
58
+ ) : /* @__PURE__ */ jsx(
59
+ "div",
60
+ {
61
+ className: "bg-[repeating-linear-gradient(135deg,oklch(0.6_0.02_55)_0_9px,oklch(0.68_0.016_58)_9px_18px)] opacity-[0.42]"
62
+ },
63
+ cell
64
+ );
65
+ })
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsx(
69
+ "div",
70
+ {
71
+ "aria-hidden": true,
72
+ className: cn(
73
+ "absolute inset-0",
74
+ (tileUrls == null ? void 0 : tileUrls.length) || backgroundUrl ? "bg-[linear-gradient(100deg,oklch(0.16_0.012_55_/_0.96)_0%,oklch(0.16_0.012_55_/_0.88)_52%,oklch(0.16_0.012_55_/_0.72)_100%)]" : "bg-[linear-gradient(100deg,oklch(0.18_0.012_55_/_0.95)_0%,oklch(0.18_0.012_55_/_0.72)_52%,oklch(0.18_0.012_55_/_0.3)_100%)]"
75
+ )
76
+ }
77
+ ),
78
+ /* @__PURE__ */ jsxs("div", { className: "relative z-[1] flex w-full flex-col justify-center gap-3 p-5 text-white lg:gap-4 lg:px-12 lg:py-11", children: [
79
+ kicker && /* @__PURE__ */ jsxs("span", { className: "inline-flex w-max items-center gap-[7px] rounded-full bg-white/15 px-3 py-1.5 text-[11px] font-extrabold tracking-[0.11em]", children: [
80
+ kickerIcon,
81
+ kicker
82
+ ] }),
83
+ /* @__PURE__ */ jsx("h1", { className: "max-w-[640px] font-serif text-[32px] font-semibold leading-[1.04] tracking-[-0.025em] lg:text-[50px]", children: headline }),
84
+ body && /* @__PURE__ */ jsx("p", { className: "hidden max-w-[540px] text-sm leading-[1.6] opacity-[0.84] lg:block lg:text-[15.5px]", children: body }),
85
+ figures && figures.length > 0 && // Two by two on the phone: four figures in one row wrapped to three and an orphan.
86
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-x-6 gap-y-3 pt-3 lg:flex lg:flex-wrap lg:gap-x-8 lg:gap-y-4 lg:pt-1.5", children: figures.map((figure) => /* @__PURE__ */ jsxs("div", { children: [
87
+ /* @__PURE__ */ jsx("div", { className: "text-2xl font-black leading-none tracking-[-0.03em] tabular-nums lg:text-[32px]", children: figure.value }),
88
+ /* @__PURE__ */ jsx("div", { className: "mt-[5px] text-xs opacity-[0.72]", children: figure.label })
89
+ ] }, figure.label)) }),
90
+ (proof || action) && /* @__PURE__ */ jsxs("div", { className: "mt-auto flex flex-wrap items-center gap-3 pt-5", children: [
91
+ proof && /* @__PURE__ */ jsxs("span", { className: "inline-flex min-h-11 items-center gap-2.5 rounded-full bg-white/10 px-4 py-2 text-[13px] backdrop-blur-[6px]", children: [
92
+ proofIcon,
93
+ proof
94
+ ] }),
95
+ action
96
+ ] })
97
+ ] })
98
+ ]
99
+ }
100
+ );
101
+ }
102
+
103
+ export { MenuHubHero };
@@ -0,0 +1,37 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { MenuIndexEntry, MenuLinkComponent } from './menu-types.js';
3
+ import 'react';
4
+ import './place-types.js';
5
+
6
+ type MenuIndexHeaderProps = {
7
+ /** Small caps over the title, e.g. "MENU · CLUJ-NAPOCA". */
8
+ eyebrow?: string;
9
+ title: string;
10
+ /** Sits beside the title, e.g. "38 cuisines". Pluralised by the caller. */
11
+ count?: string;
12
+ subtitle?: string;
13
+ /** The Food / Drinks control. A `<SegmentedNav>`, composed by the caller. */
14
+ scopeSwitch?: React.ReactNode;
15
+ className?: string;
16
+ };
17
+ declare function MenuIndexHeader({ eyebrow, title, count, subtitle, scopeSwitch, className, }: MenuIndexHeaderProps): react_jsx_runtime.JSX.Element;
18
+ /**
19
+ * MenuJumpStrip — the page's contents, as chips.
20
+ *
21
+ * It answers "move between families without going back to the hub", and it
22
+ * doubles as a table of contents: each chip carries the family's mark and its
23
+ * count, so the strip says how long the page is before the reader scrolls it.
24
+ *
25
+ * Only the families drawn in full belong here. A strip listing every value in
26
+ * the index below would be the index, twice.
27
+ */
28
+ type MenuJumpStripProps = {
29
+ entries: MenuIndexEntry[];
30
+ formatCount?: (value: number) => string;
31
+ ariaLabel?: string;
32
+ linkComponent?: MenuLinkComponent;
33
+ className?: string;
34
+ };
35
+ declare function MenuJumpStrip({ entries, formatCount, ariaLabel, linkComponent: LinkComponent, className, }: MenuJumpStripProps): react_jsx_runtime.JSX.Element;
36
+
37
+ export { MenuIndexHeader, type MenuIndexHeaderProps, MenuJumpStrip, type MenuJumpStripProps };
@@ -0,0 +1,68 @@
1
+ import { ScrollFade } from '../chunk-2XCZJWTV.js';
2
+ import { cn } from '../chunk-FEK5XGZW.js';
3
+ import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
4
+ import { jsxs, jsx } 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 MenuIndexHeader({
11
+ eyebrow,
12
+ title,
13
+ count,
14
+ subtitle,
15
+ scopeSwitch,
16
+ className
17
+ }) {
18
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-4 lg:flex-row lg:items-start lg:gap-8", className), children: [
19
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
20
+ eyebrow && /* @__PURE__ */ jsx("p", { className: "text-[11px] font-bold uppercase tracking-[0.1em] text-muted-foreground", children: eyebrow }),
21
+ /* @__PURE__ */ jsxs("h1", { className: "mt-1.5 text-[30px] font-black leading-[1.05] tracking-[-0.035em] lg:text-[40px]", children: [
22
+ title,
23
+ count && /* @__PURE__ */ jsx("span", { className: "ml-2.5 align-middle text-base font-semibold text-muted-foreground lg:text-lg", children: count })
24
+ ] }),
25
+ subtitle && /* @__PURE__ */ jsx("p", { className: "mt-2 max-w-[60ch] text-sm text-muted-foreground", children: subtitle })
26
+ ] }),
27
+ scopeSwitch && /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-10 -mx-[var(--page-gutter)] bg-background px-[var(--page-gutter)] py-2 lg:static lg:mx-0 lg:bg-transparent lg:px-0 lg:py-0", children: scopeSwitch })
28
+ ] });
29
+ }
30
+ function MenuJumpStrip({
31
+ entries,
32
+ formatCount = String,
33
+ ariaLabel,
34
+ linkComponent: LinkComponent = DefaultLink,
35
+ className
36
+ }) {
37
+ return /* @__PURE__ */ jsx(
38
+ ScrollFade,
39
+ {
40
+ orientation: "horizontal",
41
+ size: "sm",
42
+ className,
43
+ scrollClassName: "scrollbar-hide",
44
+ children: /* @__PURE__ */ jsx("nav", { "aria-label": ariaLabel, className: "flex items-center gap-2", children: entries.map((entry) => /* @__PURE__ */ jsxs(
45
+ LinkComponent,
46
+ {
47
+ href: entry.href,
48
+ className: "inline-flex h-9 shrink-0 items-center gap-2 whitespace-nowrap rounded-full border border-border bg-card px-4 text-sm transition-colors hover:border-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
49
+ children: [
50
+ /* @__PURE__ */ jsx(
51
+ "span",
52
+ {
53
+ "aria-hidden": true,
54
+ className: cn("h-2.5 w-2.5 shrink-0 rounded-[3px]", !entry.gradient && "bg-border"),
55
+ style: entry.gradient ? { backgroundImage: entry.gradient } : void 0
56
+ }
57
+ ),
58
+ entry.name,
59
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums text-muted-foreground", children: formatCount(entry.count) })
60
+ ]
61
+ },
62
+ entry.slug
63
+ )) })
64
+ }
65
+ );
66
+ }
67
+
68
+ export { MenuIndexHeader, MenuJumpStrip };
@@ -0,0 +1,48 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { MenuIndexEntry, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
3
+ import 'react';
4
+ import './place-types.js';
5
+
6
+ /**
7
+ * MenuIndexList — the complete set, four across.
8
+ *
9
+ * ## This block is why the page can afford to be progressive
10
+ *
11
+ * It carries a crawlable link to EVERY destination the city has — all
12
+ * thirty-eight cuisines, not the ten the page draws in full below. That makes
13
+ * it the page's entire link-equity job, which in turn means the rich blocks
14
+ * underneath are free to load ten at a time behind a "show more": nothing a
15
+ * crawler needs is hidden behind that control, because it is all up here.
16
+ *
17
+ * Get this backwards — put the whole set behind the disclosure and only ten
18
+ * links in the markup — and the page silently stops linking to twenty-eight
19
+ * pages it is supposed to be the index for.
20
+ *
21
+ * ## Ordered by count, and the counts overlap
22
+ *
23
+ * Cuisines are non-exclusive tags: Cluj's thirty-eight sum to 3,182 against 767
24
+ * places, because a trattoria is Italian AND pizza AND international. So this
25
+ * is a ranked list of ways in, never a partition, and no copy near it may total
26
+ * the numbers or call them a breakdown.
27
+ */
28
+ /** @deprecated Alias of `MenuLinkProps`; kept for symmetry with the other menu surfaces. */
29
+ type MenuIndexListLinkProps = MenuLinkProps;
30
+ type MenuIndexListProps = {
31
+ entries: MenuIndexEntry[];
32
+ /** e.g. "All 38 cuisines". Composed and pluralised by the caller. */
33
+ title: string;
34
+ /** One line under it, e.g. "Ordered by kitchens — every one has its own page". */
35
+ subtitle?: string;
36
+ /** Locale-aware integer formatter. Defaults to `String`. */
37
+ formatCount?: (value: number) => string;
38
+ linkComponent?: MenuLinkComponent;
39
+ className?: string;
40
+ };
41
+ declare function MenuIndexList({ entries, title, subtitle, formatCount, linkComponent: LinkComponent, className, }: MenuIndexListProps): react_jsx_runtime.JSX.Element;
42
+ /** Two columns on a phone, four from `lg` — the grid the list itself renders. */
43
+ declare function MenuIndexListSkeleton({ rows, className, }: {
44
+ rows?: number;
45
+ className?: string;
46
+ }): react_jsx_runtime.JSX.Element;
47
+
48
+ export { MenuIndexList, type MenuIndexListLinkProps, type MenuIndexListProps, MenuIndexListSkeleton };