@bearmenu/ui 0.8.7 → 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 (179) hide show
  1. package/dist/{chunk-637L7XQ3.js → chunk-3TNPSN4D.js} +28 -9
  2. package/dist/chunk-62ES6N32.js +27 -0
  3. package/dist/chunk-AHL45DD2.js +145 -0
  4. package/dist/chunk-AXV5O2IZ.js +58 -0
  5. package/dist/chunk-IYDH5IU6.js +66 -0
  6. package/dist/chunk-OPVKL4VS.js +18 -0
  7. package/dist/chunk-Q6QH52LJ.js +38 -0
  8. package/dist/chunk-TH6W2PWG.js +213 -0
  9. package/dist/chunk-THOVPZNN.js +37 -0
  10. package/dist/chunk-YS6BHKZO.js +92 -0
  11. package/dist/components/badge.d.ts +1 -1
  12. package/dist/components/button.d.ts +1 -1
  13. package/dist/components/category-deck.js +10 -1
  14. package/dist/components/command.d.ts +7 -7
  15. package/dist/components/concept-compare.d.ts +73 -0
  16. package/dist/components/concept-compare.js +162 -0
  17. package/dist/components/coverflow.d.ts +11 -3
  18. package/dist/components/coverflow.js +34 -51
  19. package/dist/components/date-input.d.ts +1 -1
  20. package/dist/components/drift-rail.d.ts +24 -10
  21. package/dist/components/drift-rail.js +1 -1
  22. package/dist/components/event-card.d.ts +8 -0
  23. package/dist/components/event-card.js +64 -34
  24. package/dist/components/event-compact-row.d.ts +33 -6
  25. package/dist/components/event-compact-row.js +120 -39
  26. package/dist/components/event-day-header.js +3 -1
  27. package/dist/components/event-day-separator.d.ts +7 -1
  28. package/dist/components/event-day-separator.js +3 -2
  29. package/dist/components/event-detail-panel.d.ts +171 -25
  30. package/dist/components/event-detail-panel.js +471 -174
  31. package/dist/components/event-hub-hero.d.ts +25 -4
  32. package/dist/components/event-hub-hero.js +89 -19
  33. package/dist/components/event-list-row-wide.js +1 -1
  34. package/dist/components/event-list-row.d.ts +19 -6
  35. package/dist/components/event-list-row.js +79 -35
  36. package/dist/components/hour-band.d.ts +38 -40
  37. package/dist/components/hour-band.js +55 -27
  38. package/dist/components/hour-group.js +1 -1
  39. package/dist/components/hub-surface.d.ts +27 -0
  40. package/dist/components/hub-surface.js +20 -0
  41. package/dist/components/menu-family-card.d.ts +59 -0
  42. package/dist/components/menu-family-card.js +121 -0
  43. package/dist/components/menu-field.d.ts +57 -0
  44. package/dist/components/menu-field.js +3 -0
  45. package/dist/components/menu-hub-hero.d.ts +65 -0
  46. package/dist/components/menu-hub-hero.js +103 -0
  47. package/dist/components/menu-intent-tiles.d.ts +31 -0
  48. package/dist/components/menu-intent-tiles.js +50 -0
  49. package/dist/components/menu-item.d.ts +127 -0
  50. package/dist/components/menu-item.js +5 -0
  51. package/dist/components/menu-mocks.d.ts +55 -0
  52. package/dist/components/menu-mocks.js +379 -0
  53. package/dist/components/menu-types.d.ts +245 -0
  54. package/dist/components/menu-types.js +1 -0
  55. package/dist/components/motion-icon-set.js +15 -3
  56. package/dist/components/open-now-list.d.ts +40 -0
  57. package/dist/components/open-now-list.js +69 -0
  58. package/dist/components/place-types.d.ts +112 -1
  59. package/dist/components/poster-tape.js +1 -1
  60. package/dist/components/poster-wall.d.ts +3 -3
  61. package/dist/components/poster-wall.js +73 -17
  62. package/dist/components/ranked-venue-list.d.ts +49 -0
  63. package/dist/components/ranked-venue-list.js +87 -0
  64. package/dist/components/section-header.d.ts +16 -1
  65. package/dist/components/section-header.js +26 -7
  66. package/dist/components/signature-chip.d.ts +37 -0
  67. package/dist/components/signature-chip.js +4 -0
  68. package/dist/components/signature-line-list.d.ts +45 -0
  69. package/dist/components/signature-line-list.js +99 -0
  70. package/dist/components/snap-rail.d.ts +54 -0
  71. package/dist/components/snap-rail.js +109 -0
  72. package/dist/components/spinner.d.ts +2 -2
  73. package/dist/components/stat-banner.d.ts +47 -0
  74. package/dist/components/stat-banner.js +61 -0
  75. package/dist/components/text.d.ts +3 -3
  76. package/dist/components/timeline-band.d.ts +20 -2
  77. package/dist/components/timeline-band.js +1 -1
  78. package/dist/components/timeline-playhead.d.ts +26 -0
  79. package/dist/components/timeline-playhead.js +25 -0
  80. package/dist/components/venue-busyness-strip.d.ts +35 -0
  81. package/dist/components/venue-busyness-strip.js +37 -0
  82. package/dist/components/venue-grade-dimensions.d.ts +36 -0
  83. package/dist/components/venue-grade-dimensions.js +3 -0
  84. package/dist/components/venue-quotes.d.ts +32 -0
  85. package/dist/components/venue-quotes.js +3 -0
  86. package/dist/components/venue-signature-block.d.ts +39 -0
  87. package/dist/components/venue-signature-block.js +6 -0
  88. package/dist/components/venue-spotlight.d.ts +74 -0
  89. package/dist/components/venue-spotlight.js +138 -0
  90. package/dist/components/venue-wall.d.ts +41 -0
  91. package/dist/components/venue-wall.js +118 -0
  92. package/dist/components/weekday-theme-banner.d.ts +70 -0
  93. package/dist/components/weekday-theme-banner.js +127 -0
  94. package/dist/components/weekend-panel.d.ts +5 -4
  95. package/dist/components/weekend-panel.js +85 -71
  96. package/dist/hooks/use-drag-swipe.d.ts +44 -0
  97. package/dist/hooks/use-drag-swipe.js +2 -0
  98. package/dist/hooks/use-media-query.d.ts +17 -0
  99. package/dist/hooks/use-media-query.js +2 -0
  100. package/package.json +1 -1
  101. package/src/components/category-deck.tsx +19 -8
  102. package/src/components/colour-dictionary.stories.tsx +227 -0
  103. package/src/components/concept-compare.stories.tsx +104 -0
  104. package/src/components/concept-compare.tsx +276 -0
  105. package/src/components/coverflow.test.tsx +28 -1
  106. package/src/components/coverflow.tsx +37 -43
  107. package/src/components/drift-rail.tsx +167 -42
  108. package/src/components/event-card.tsx +55 -10
  109. package/src/components/event-compact-row.stories.tsx +30 -0
  110. package/src/components/event-compact-row.test.tsx +49 -0
  111. package/src/components/event-compact-row.tsx +162 -38
  112. package/src/components/event-day-header.tsx +3 -1
  113. package/src/components/event-day-separator.tsx +8 -1
  114. package/src/components/event-detail-panel.stories.tsx +205 -97
  115. package/src/components/event-detail-panel.test.tsx +181 -0
  116. package/src/components/event-detail-panel.tsx +703 -185
  117. package/src/components/event-hub-hero.stories.tsx +30 -0
  118. package/src/components/event-hub-hero.tsx +102 -30
  119. package/src/components/event-list-row.stories.tsx +34 -0
  120. package/src/components/event-list-row.test.tsx +53 -0
  121. package/src/components/event-list-row.tsx +135 -87
  122. package/src/components/hour-band.stories.tsx +28 -5
  123. package/src/components/hour-band.tsx +90 -70
  124. package/src/components/hub-surface.stories.tsx +68 -0
  125. package/src/components/hub-surface.tsx +40 -0
  126. package/src/components/menu-family-card.stories.tsx +112 -0
  127. package/src/components/menu-family-card.tsx +211 -0
  128. package/src/components/menu-field.stories.tsx +74 -0
  129. package/src/components/menu-field.tsx +199 -0
  130. package/src/components/menu-hub-hero.stories.tsx +92 -0
  131. package/src/components/menu-hub-hero.tsx +187 -0
  132. package/src/components/menu-intent-tiles.stories.tsx +54 -0
  133. package/src/components/menu-intent-tiles.tsx +76 -0
  134. package/src/components/menu-item.tsx +416 -0
  135. package/src/components/menu-mocks.ts +417 -0
  136. package/src/components/menu-types.ts +259 -0
  137. package/src/components/motion-icon-set.tsx +30 -3
  138. package/src/components/open-now-list.stories.tsx +76 -0
  139. package/src/components/open-now-list.tsx +127 -0
  140. package/src/components/place-types.ts +112 -0
  141. package/src/components/poster-wall.test.tsx +28 -0
  142. package/src/components/poster-wall.tsx +96 -23
  143. package/src/components/ranked-venue-list.stories.tsx +80 -0
  144. package/src/components/ranked-venue-list.tsx +177 -0
  145. package/src/components/section-header.stories.tsx +24 -0
  146. package/src/components/section-header.test.tsx +28 -0
  147. package/src/components/section-header.tsx +45 -7
  148. package/src/components/signature-chip.tsx +77 -0
  149. package/src/components/signature-line-list.stories.tsx +85 -0
  150. package/src/components/signature-line-list.tsx +136 -0
  151. package/src/components/snap-rail.stories.tsx +79 -0
  152. package/src/components/snap-rail.test.tsx +18 -0
  153. package/src/components/snap-rail.tsx +148 -0
  154. package/src/components/stat-banner.stories.tsx +100 -0
  155. package/src/components/stat-banner.tsx +117 -0
  156. package/src/components/timeline-band.tsx +52 -12
  157. package/src/components/timeline-playhead.stories.tsx +30 -0
  158. package/src/components/timeline-playhead.test.tsx +11 -0
  159. package/src/components/timeline-playhead.tsx +46 -0
  160. package/src/components/venue-busyness-strip.stories.tsx +66 -0
  161. package/src/components/venue-busyness-strip.tsx +64 -0
  162. package/src/components/venue-grade-dimensions.stories.tsx +83 -0
  163. package/src/components/venue-grade-dimensions.tsx +91 -0
  164. package/src/components/venue-quotes.stories.tsx +59 -0
  165. package/src/components/venue-quotes.tsx +64 -0
  166. package/src/components/venue-signature-block.stories.tsx +68 -0
  167. package/src/components/venue-signature-block.tsx +71 -0
  168. package/src/components/venue-spotlight.stories.tsx +141 -0
  169. package/src/components/venue-spotlight.tsx +236 -0
  170. package/src/components/venue-wall.stories.tsx +78 -0
  171. package/src/components/venue-wall.tsx +185 -0
  172. package/src/components/weekday-theme-banner.stories.tsx +113 -0
  173. package/src/components/weekday-theme-banner.tsx +231 -0
  174. package/src/components/weekend-panel.test.tsx +43 -0
  175. package/src/components/weekend-panel.tsx +142 -108
  176. package/src/hooks/use-drag-swipe.test.tsx +66 -0
  177. package/src/hooks/use-drag-swipe.ts +105 -0
  178. package/src/hooks/use-media-query.ts +31 -0
  179. package/dist/chunk-GPH6L4DX.js +0 -55
@@ -25,24 +25,60 @@ function useInViewport() {
25
25
  React.useEffect(() => {
26
26
  const node = ref.current;
27
27
  if (!node || typeof IntersectionObserver === "undefined") return;
28
- const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting), {
29
- threshold: 0
30
- });
28
+ const observer = new IntersectionObserver(
29
+ ([entry]) => setInView(entry.isIntersecting),
30
+ {
31
+ threshold: 0
32
+ }
33
+ );
31
34
  observer.observe(node);
32
35
  return () => observer.disconnect();
33
36
  }, []);
34
37
  return [ref, inView];
35
38
  }
36
- function PosterWall({ tiles, variant = "default", children, className }) {
39
+ function PosterWall({
40
+ tiles,
41
+ variant = "default",
42
+ children,
43
+ className
44
+ }) {
37
45
  const isCompact = variant === "compact";
38
- const tileCount = isCompact ? POSTER_WALL_COMPACT_TILE_COUNT : POSTER_WALL_DESKTOP_TILE_COUNT;
39
- const slots = Array.from({ length: tileCount }, (_, index) => {
40
- var _a;
41
- return (_a = tiles == null ? void 0 : tiles[index]) != null ? _a : {};
42
- });
46
+ const maxCols = isCompact ? 6 : 12;
47
+ const maxSlots = isCompact ? POSTER_WALL_COMPACT_TILE_COUNT : POSTER_WALL_DESKTOP_TILE_COUNT;
48
+ const heightPx = isCompact ? 330 : 420;
49
+ const pictures = React.useMemo(
50
+ () => (tiles != null ? tiles : []).filter((tile) => !!tile.src),
51
+ [tiles]
52
+ );
43
53
  const prefersReducedMotion = usePrefersReducedMotion();
44
54
  const [wallRef, inView] = useInViewport();
45
55
  const isSweepPaused = prefersReducedMotion || !inView;
56
+ const [width, setWidth] = React.useState(isCompact ? 390 : 1200);
57
+ React.useEffect(() => {
58
+ const node = wallRef.current;
59
+ if (!node || typeof ResizeObserver === "undefined") return;
60
+ const observer = new ResizeObserver(
61
+ ([entry]) => setWidth(entry.contentRect.width)
62
+ );
63
+ observer.observe(node);
64
+ return () => observer.disconnect();
65
+ }, [wallRef]);
66
+ let cols = maxCols;
67
+ let slots;
68
+ if (pictures.length === 0) {
69
+ slots = Array.from({ length: maxSlots }, () => ({}));
70
+ } else {
71
+ const shown = Math.min(pictures.length, maxSlots);
72
+ cols = Math.min(
73
+ maxCols,
74
+ Math.max(2, Math.round(Math.sqrt(shown * width / heightPx)))
75
+ );
76
+ const rows = Math.max(1, Math.ceil(shown / cols));
77
+ slots = Array.from(
78
+ { length: cols * rows },
79
+ (_, index) => pictures[index % pictures.length]
80
+ );
81
+ }
46
82
  return /* @__PURE__ */ jsxs(
47
83
  "div",
48
84
  {
@@ -62,10 +98,8 @@ function PosterWall({ tiles, variant = "default", children, className }) {
62
98
  /* @__PURE__ */ jsx(
63
99
  "div",
64
100
  {
65
- className: cn(
66
- "absolute inset-0 grid auto-rows-fr gap-[2px] opacity-50",
67
- isCompact ? "grid-cols-6" : "grid-cols-12"
68
- ),
101
+ className: "absolute inset-0 grid auto-rows-fr gap-[2px] opacity-50",
102
+ style: { gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` },
69
103
  "aria-hidden": true,
70
104
  children: slots.map(
71
105
  (tile, index) => tile.src ? (
@@ -130,7 +164,10 @@ function PosterWall({ tiles, variant = "default", children, className }) {
130
164
  }
131
165
  );
132
166
  }
133
- function PosterWallSkeleton({ variant = "default", className }) {
167
+ function PosterWallSkeleton({
168
+ variant = "default",
169
+ className
170
+ }) {
134
171
  const isCompact = variant === "compact";
135
172
  const tileCount = isCompact ? POSTER_WALL_COMPACT_TILE_COUNT : POSTER_WALL_DESKTOP_TILE_COUNT;
136
173
  return /* @__PURE__ */ jsxs(
@@ -150,7 +187,13 @@ function PosterWallSkeleton({ variant = "default", className }) {
150
187
  "absolute inset-0 grid auto-rows-fr gap-[2px] opacity-50",
151
188
  isCompact ? "grid-cols-6" : "grid-cols-12"
152
189
  ),
153
- children: Array.from({ length: tileCount }, (_, index) => /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-none bg-white/10" }, index))
190
+ children: Array.from({ length: tileCount }, (_, index) => /* @__PURE__ */ jsx(
191
+ Skeleton,
192
+ {
193
+ className: "h-full w-full rounded-none bg-white/10"
194
+ },
195
+ index
196
+ ))
154
197
  }
155
198
  ),
156
199
  /* @__PURE__ */ jsx(
@@ -170,8 +213,21 @@ function PosterWallSkeleton({ variant = "default", className }) {
170
213
  isCompact ? "justify-end gap-[11px] p-5" : "justify-center gap-[15px] px-12"
171
214
  ),
172
215
  children: [
173
- /* @__PURE__ */ jsx(Skeleton, { className: cn("bg-white/15", isCompact ? "h-[11px] w-40" : "h-3 w-52") }),
174
- /* @__PURE__ */ jsx(Skeleton, { className: cn("bg-white/20", isCompact ? "h-7 w-64" : "h-10 w-96") })
216
+ /* @__PURE__ */ jsx(
217
+ Skeleton,
218
+ {
219
+ className: cn(
220
+ "bg-white/15",
221
+ isCompact ? "h-[11px] w-40" : "h-3 w-52"
222
+ )
223
+ }
224
+ ),
225
+ /* @__PURE__ */ jsx(
226
+ Skeleton,
227
+ {
228
+ className: cn("bg-white/20", isCompact ? "h-7 w-64" : "h-10 w-96")
229
+ }
230
+ )
175
231
  ]
176
232
  }
177
233
  )
@@ -0,0 +1,49 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { MenuVenuePreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
3
+ import 'react';
4
+ import './place-types.js';
5
+
6
+ /**
7
+ * RankedVenueList — venues ordered by how often reviews name their dishes.
8
+ *
9
+ * The answer to "best Italian in Cluj", and the reason it is defensible is the
10
+ * mention count sitting on every row: the order is evidence, not opinion. A
11
+ * caller that cannot supply mentions should use a different surface rather than
12
+ * this one with the column blank — an unexplained ranking is worse than none.
13
+ *
14
+ * The tab strip re-ranks in place and never navigates, so `activeTab` and
15
+ * `onTabChange` are controlled by the caller. Omit `tabs` entirely and the
16
+ * header is just a heading.
17
+ *
18
+ * No directive: without `tabs` the list renders on the server, which is the
19
+ * common case on a hub.
20
+ */
21
+ /** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
22
+ type RankedVenueListLinkProps = MenuLinkProps;
23
+ type RankedVenueListTab = {
24
+ slug: string;
25
+ label: string;
26
+ };
27
+ type RankedVenueListProps = {
28
+ venues: MenuVenuePreview[];
29
+ /** Section heading, e.g. "The kitchens Cluj rates for Italian". */
30
+ heading?: string;
31
+ /** The basis of the order, e.g. "by how often reviews name their dishes". */
32
+ caption?: string;
33
+ tabs?: RankedVenueListTab[];
34
+ activeTab?: string;
35
+ onTabChange?: (slug: string) => void;
36
+ /** Composed price band per venue, keyed by place slug, e.g. "38—84". */
37
+ priceBandLabels?: Record<string, string>;
38
+ /**
39
+ * The app's own grade per venue slug, already formatted ("8.7"). Shown in
40
+ * place of the star rating when present — the star is the third-party
41
+ * fallback for a venue the reviews have not graded yet.
42
+ */
43
+ gradeLabels?: Record<string, string>;
44
+ linkComponent?: MenuLinkComponent;
45
+ className?: string;
46
+ };
47
+ declare function RankedVenueList({ venues, heading, caption, tabs, activeTab, onTabChange, priceBandLabels, gradeLabels, linkComponent: LinkComponent, className, }: RankedVenueListProps): react_jsx_runtime.JSX.Element;
48
+
49
+ export { RankedVenueList, type RankedVenueListLinkProps, type RankedVenueListProps, type RankedVenueListTab };
@@ -0,0 +1,87 @@
1
+ import { Separator } from '../chunk-WSRWFA6K.js';
2
+ import { cn } from '../chunk-FEK5XGZW.js';
3
+ import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
4
+ import { MessageSquare, Star, ChevronRight } from 'lucide-react';
5
+ import { jsxs, jsx } 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 RankedVenueList({
12
+ venues,
13
+ heading,
14
+ caption,
15
+ tabs,
16
+ activeTab,
17
+ onTabChange,
18
+ priceBandLabels,
19
+ gradeLabels,
20
+ linkComponent: LinkComponent = DefaultLink,
21
+ className
22
+ }) {
23
+ return /* @__PURE__ */ jsxs("div", { className: cn("overflow-hidden rounded-[14px] border border-border bg-card", className), children: [
24
+ (heading || tabs) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2 border-b border-border p-3.5", children: [
25
+ heading && /* @__PURE__ */ jsx("h3", { className: "text-[15px] font-bold leading-tight tracking-[-0.01em]", children: heading }),
26
+ caption && /* @__PURE__ */ jsx("span", { className: "text-[12.5px] text-muted-foreground", children: caption }),
27
+ tabs && tabs.length > 0 && /* @__PURE__ */ jsx("div", { className: "ml-auto flex flex-wrap gap-1.5", children: tabs.map((tab) => /* @__PURE__ */ jsx(
28
+ "button",
29
+ {
30
+ type: "button",
31
+ "aria-pressed": tab.slug === activeTab,
32
+ onClick: () => onTabChange == null ? void 0 : onTabChange(tab.slug),
33
+ className: cn(
34
+ "inline-flex h-7 items-center rounded-full border px-2.5 text-xs font-semibold transition-colors",
35
+ tab.slug === activeTab ? "border-accent bg-accent-subtle text-accent" : "border-border text-muted-foreground hover:bg-muted/50"
36
+ ),
37
+ children: tab.label
38
+ },
39
+ tab.slug
40
+ )) })
41
+ ] }),
42
+ /* @__PURE__ */ jsx("ol", { children: venues.map((venue, index) => {
43
+ var _a;
44
+ const { place, signature } = venue;
45
+ const mentions = (_a = signature == null ? void 0 : signature.item.mentions) != null ? _a : 0;
46
+ return /* @__PURE__ */ jsxs("li", { children: [
47
+ index > 0 && /* @__PURE__ */ jsx(Separator, {}),
48
+ /* @__PURE__ */ jsxs(
49
+ LinkComponent,
50
+ {
51
+ href: venue.href,
52
+ className: "flex items-center gap-3 p-3 transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
53
+ children: [
54
+ /* @__PURE__ */ jsx("span", { className: "w-4 shrink-0 text-center text-[13px] font-extrabold tabular-nums text-accent", children: index + 1 }),
55
+ /* @__PURE__ */ jsx("span", { className: "h-12 w-12 shrink-0 overflow-hidden rounded-xl bg-[repeating-linear-gradient(135deg,oklch(0.78_0.018_55)_0_8px,oklch(0.84_0.012_58)_8px_16px)]", children: place.thumbnail_url && // eslint-disable-next-line @next/next/no-img-element
56
+ /* @__PURE__ */ jsx(
57
+ "img",
58
+ {
59
+ src: place.thumbnail_url,
60
+ alt: "",
61
+ className: "h-full w-full object-cover",
62
+ loading: "lazy"
63
+ }
64
+ ) }),
65
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
66
+ /* @__PURE__ */ jsx("span", { className: "block text-sm font-bold leading-snug tracking-[-0.01em]", children: place.name }),
67
+ (signature == null ? void 0 : signature.item.name) && /* @__PURE__ */ jsx("span", { className: "mt-px block truncate font-serif text-xs italic text-muted-foreground", children: `\u201C${signature.item.name}\u201D` })
68
+ ] }),
69
+ mentions > 0 && /* @__PURE__ */ jsxs("span", { className: "hidden shrink-0 items-center gap-1 text-[11.5px] tabular-nums text-muted-foreground sm:inline-flex", children: [
70
+ /* @__PURE__ */ jsx(MessageSquare, { className: "h-[11px] w-[11px]", "aria-hidden": true }),
71
+ mentions
72
+ ] }),
73
+ (gradeLabels == null ? void 0 : gradeLabels[place.slug]) ? /* @__PURE__ */ jsx("span", { className: "inline-flex h-6 shrink-0 items-center rounded-md bg-foreground px-1.5 text-[12px] font-extrabold tabular-nums text-background", children: gradeLabels[place.slug] }) : place.rating && /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 text-[12.5px] font-semibold tabular-nums", children: [
74
+ /* @__PURE__ */ jsx(Star, { className: "h-3 w-3", "aria-hidden": true }),
75
+ place.rating.rating
76
+ ] }),
77
+ (priceBandLabels == null ? void 0 : priceBandLabels[place.slug]) && /* @__PURE__ */ jsx("span", { className: "hidden shrink-0 text-[12.5px] tabular-nums text-muted-foreground sm:block", children: priceBandLabels[place.slug] }),
78
+ /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true })
79
+ ]
80
+ }
81
+ )
82
+ ] }, place.slug);
83
+ }) })
84
+ ] });
85
+ }
86
+
87
+ export { RankedVenueList };
@@ -25,6 +25,21 @@ type SectionHeaderProps = {
25
25
  /** Trailing action label, e.g. "See all". */
26
26
  action?: string;
27
27
  actionHref?: string;
28
+ /**
29
+ * `muted` (default): 14px in `--muted-foreground`, at every width. `app`:
30
+ * BELOW `lg`, 12.5px semibold in `--accent` — the way an app's "All ›" reads
31
+ * at the end of a card's title row, quieter in size and louder in colour so
32
+ * it sits in the count's tier rather than competing with the title. From
33
+ * `lg` it falls back to `muted`: the app treatment is a phone treatment, and
34
+ * the desktop page it shares a header with is unchanged by it.
35
+ */
36
+ actionTone?: "muted" | "app";
37
+ /**
38
+ * `always` (default) shows the subtitle at every width. `lg` hides it below
39
+ * the `lg` breakpoint for surfaces that narrate on desktop and not on a
40
+ * phone, where a card title with a count is the whole header.
41
+ */
42
+ subtitleFrom?: "always" | "lg";
28
43
  /** Defaults to a plain `<a>`. Pass `next/link` in a Next app. */
29
44
  linkComponent?: React.ComponentType<SectionHeaderLinkProps>;
30
45
  onAction?: (mouseEvent: React.MouseEvent) => void;
@@ -37,7 +52,7 @@ type SectionHeaderProps = {
37
52
  as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "div";
38
53
  className?: string;
39
54
  };
40
- declare function SectionHeader({ title, count, subtitle, action, actionHref, linkComponent: LinkComponent, onAction, as: Heading, className, }: SectionHeaderProps): react_jsx_runtime.JSX.Element;
55
+ declare function SectionHeader({ title, count, subtitle, action, actionHref, actionTone, subtitleFrom, linkComponent: LinkComponent, onAction, as: Heading, className, }: SectionHeaderProps): react_jsx_runtime.JSX.Element;
41
56
  type SectionHeaderSkeletonProps = {
42
57
  /** Shows the inline count placeholder, matching `SectionHeaderProps["count"]`. */
43
58
  showCount?: boolean;
@@ -8,38 +8,57 @@ function DefaultLink(_a) {
8
8
  var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
9
9
  return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
10
10
  }
11
- var ACTION_CLASSES = cn(
12
- "inline-flex shrink-0 items-center gap-0.5 rounded-sm text-[14px] text-muted-foreground",
13
- "transition-colors duration-150 hover:text-foreground motion-reduce:transition-none",
11
+ var ACTION_BASE_CLASSES = cn(
12
+ "inline-flex min-h-11 shrink-0 items-center gap-0.5 rounded-sm -my-2 py-2 -mr-1 pr-1",
13
+ "transition-colors duration-150 motion-reduce:transition-none",
14
14
  "cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
15
15
  );
16
+ var ACTION_TONE_CLASSES = {
17
+ muted: "text-[14px] text-muted-foreground hover:text-foreground",
18
+ app: cn(
19
+ "text-[12.5px] font-semibold text-accent hover:text-accent/80",
20
+ "lg:text-[14px] lg:font-normal lg:text-muted-foreground lg:hover:text-foreground"
21
+ )
22
+ };
16
23
  function SectionHeader({
17
24
  title,
18
25
  count,
19
26
  subtitle,
20
27
  action,
21
28
  actionHref,
29
+ actionTone = "muted",
30
+ subtitleFrom = "always",
22
31
  linkComponent: LinkComponent = DefaultLink,
23
32
  onAction,
24
33
  as: Heading = "h2",
25
34
  className
26
35
  }) {
27
36
  const actionIcon = /* @__PURE__ */ jsx(ChevronRight, { className: "h-3.5 w-3.5", "aria-hidden": true });
37
+ const actionClasses = cn(ACTION_BASE_CLASSES, ACTION_TONE_CLASSES[actionTone]);
28
38
  return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col mb-3.5", className), children: [
29
39
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
30
40
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-baseline", children: [
31
41
  /* @__PURE__ */ jsx(Heading, { className: "truncate text-[17px] font-bold tracking-[-0.01em] text-foreground md:text-[18px]", children: title }),
32
- count !== void 0 && /* @__PURE__ */ jsx("span", { className: "ml-0.5 shrink-0 text-[12px] text-muted-foreground", children: count })
42
+ count !== void 0 && /* @__PURE__ */ jsx("span", { className: "ml-1.5 shrink-0 text-[12px] tabular-nums text-muted-foreground lg:ml-0.5", children: count })
33
43
  ] }),
34
- action && (actionHref ? /* @__PURE__ */ jsxs(LinkComponent, { href: actionHref, className: ACTION_CLASSES, onClick: onAction, children: [
44
+ action && (actionHref ? /* @__PURE__ */ jsxs(LinkComponent, { href: actionHref, className: actionClasses, onClick: onAction, children: [
35
45
  action,
36
46
  actionIcon
37
- ] }) : onAction ? /* @__PURE__ */ jsxs("button", { type: "button", className: ACTION_CLASSES, onClick: onAction, children: [
47
+ ] }) : onAction ? /* @__PURE__ */ jsxs("button", { type: "button", className: actionClasses, onClick: onAction, children: [
38
48
  action,
39
49
  actionIcon
40
50
  ] }) : null)
41
51
  ] }),
42
- subtitle && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-[13px] text-muted-foreground md:text-[14px]", children: subtitle })
52
+ subtitle && /* @__PURE__ */ jsx(
53
+ "p",
54
+ {
55
+ className: cn(
56
+ "mt-0.5 text-[13px] text-muted-foreground md:text-[14px]",
57
+ subtitleFrom === "lg" && "hidden lg:block"
58
+ ),
59
+ children: subtitle
60
+ }
61
+ )
43
62
  ] });
44
63
  }
45
64
  function SectionHeaderSkeleton({
@@ -0,0 +1,37 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * "Signature" / "Mentioned" — the marker for a menu item the review digest
5
+ * backs, shared by `<MenuItem>` and the dish/drink cards so the same claim
6
+ * never renders two different ways.
7
+ *
8
+ * When `onEvidence` is supplied the chip becomes a button that opens the
9
+ * reviews behind it, and takes a dashed outline to read as interactive. That is
10
+ * the whole differentiator: every competitor badges items ("Nou", "NEW"), none
11
+ * can show you why. Without evidence it degrades to a plain label rather than
12
+ * offering a tap that would reveal nothing.
13
+ *
14
+ * `stopPropagation` matters on every host: the row is a `role="button"` and the
15
+ * cards carry a stretched crawlable `<a>` overlay, so an un-stopped click would
16
+ * also open the item drawer. On the cards it must additionally out-rank that
17
+ * overlay's `z-[1]`, hence `relative z-[2]` from the caller.
18
+ *
19
+ * `surface="image"` is for card covers, where the chip sits over the photo
20
+ * rather than in the caption block. The accent-on-canvas palette has no
21
+ * contrast guarantee over an arbitrary photo, so it swaps to the same frosted
22
+ * fill the place badge uses there and keeps the dashed edge — which is what
23
+ * says "there is evidence behind this" — in the inverted colour.
24
+ *
25
+ * The label is passed in: the DS owns no translation keys.
26
+ */
27
+ type SignatureChipProps = {
28
+ /** Already translated. `strong` → "Signature", `weak` → "Mentioned". */
29
+ label: string;
30
+ onEvidence?: () => void;
31
+ /** `image` = over a card cover; `canvas` = in a caption or row. */
32
+ surface?: "canvas" | "image";
33
+ className?: string;
34
+ };
35
+ declare function SignatureChip({ label, onEvidence, surface, className, }: SignatureChipProps): react_jsx_runtime.JSX.Element;
36
+
37
+ export { SignatureChip, type SignatureChipProps };
@@ -0,0 +1,4 @@
1
+ export { SignatureChip } from '../chunk-Q6QH52LJ.js';
2
+ import '../chunk-UKBNQO6Q.js';
3
+ import '../chunk-FEK5XGZW.js';
4
+ import '../chunk-3GWWZKX7.js';
@@ -0,0 +1,45 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { MenuItemLabels } from './menu-item.js';
3
+ import { MenuVenuePreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
4
+ import 'react';
5
+ import './place-types.js';
6
+
7
+ /**
8
+ * SignatureLineList — family, venue, dish, price and reputation on one line.
9
+ *
10
+ * The densest surface on the hub, and the one that most nearly justifies the
11
+ * whole catalogue: every row ties a kitchen to the single thing its reviewers
12
+ * keep naming, with the price and the count that back it.
13
+ *
14
+ * ## Two targets per row, deliberately
15
+ *
16
+ * The venue line goes to the venue; the item row goes to the item across every
17
+ * kitchen that makes it. Those are genuinely different destinations, and
18
+ * collapsing them into one tap loses the comparison that makes the item worth
19
+ * showing. They are separate anchors rather than one anchor with a nested
20
+ * button, which would be invalid markup.
21
+ *
22
+ * Below `lg` the row stacks: venue line first, then the full `<MenuItem>`
23
+ * underneath. The desktop layout puts them side by side. Same markup, one
24
+ * breakpoint — no second component.
25
+ *
26
+ * Server-safe.
27
+ */
28
+ /** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
29
+ type SignatureLineListLinkProps = MenuLinkProps;
30
+ type SignatureLineListProps = {
31
+ venues: MenuVenuePreview[];
32
+ /** Passed through to each `<MenuItem>`. */
33
+ labels: MenuItemLabels;
34
+ formatPrice: (amount: number) => string;
35
+ formatCount?: (value: number) => string;
36
+ /** Composed meta line per venue, keyed by place slug, e.g. "romanian · Centru". */
37
+ metaLabels?: Record<string, string>;
38
+ /** The row's category mark per venue, keyed by place slug. */
39
+ hues?: Record<string, string>;
40
+ linkComponent?: MenuLinkComponent;
41
+ className?: string;
42
+ };
43
+ declare function SignatureLineList({ venues, labels, formatPrice, formatCount, metaLabels, hues, linkComponent: LinkComponent, className, }: SignatureLineListProps): react_jsx_runtime.JSX.Element;
44
+
45
+ export { SignatureLineList, type SignatureLineListLinkProps, type SignatureLineListProps };
@@ -0,0 +1,99 @@
1
+ import { Separator } from '../chunk-WSRWFA6K.js';
2
+ import { MenuItem } from '../chunk-TH6W2PWG.js';
3
+ import '../chunk-Q6QH52LJ.js';
4
+ import '../chunk-UKBNQO6Q.js';
5
+ import { cn } from '../chunk-FEK5XGZW.js';
6
+ import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
7
+ import { Star, ChevronRight } from 'lucide-react';
8
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
+
10
+ function DefaultLink(_a) {
11
+ var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
12
+ return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
13
+ }
14
+ function SignatureLineList({
15
+ venues,
16
+ labels,
17
+ formatPrice,
18
+ formatCount,
19
+ metaLabels,
20
+ hues,
21
+ linkComponent: LinkComponent = DefaultLink,
22
+ className
23
+ }) {
24
+ return /* @__PURE__ */ jsx("ul", { className: cn("flex flex-col", className), children: venues.map((venue, index) => {
25
+ const { place, signature } = venue;
26
+ return /* @__PURE__ */ jsxs(
27
+ "li",
28
+ {
29
+ className: "grid gap-x-4 py-3.5 lg:grid-cols-[minmax(0,260px)_minmax(0,1fr)] lg:items-center",
30
+ children: [
31
+ index > 0 && /* @__PURE__ */ jsx(Separator, { className: "mb-3.5 lg:col-span-2" }),
32
+ /* @__PURE__ */ jsxs(
33
+ LinkComponent,
34
+ {
35
+ href: venue.href,
36
+ className: "flex items-center gap-3 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
37
+ children: [
38
+ /* @__PURE__ */ jsxs("span", { className: "relative h-12 w-12 shrink-0 overflow-hidden rounded-xl bg-[repeating-linear-gradient(135deg,oklch(0.78_0.018_55)_0_8px,oklch(0.84_0.012_58)_8px_16px)] lg:h-16 lg:w-16", children: [
39
+ place.thumbnail_url && // eslint-disable-next-line @next/next/no-img-element
40
+ /* @__PURE__ */ jsx(
41
+ "img",
42
+ {
43
+ src: place.thumbnail_url,
44
+ alt: "",
45
+ className: "h-full w-full object-cover",
46
+ loading: "lazy"
47
+ }
48
+ ),
49
+ place.open_now && /* @__PURE__ */ jsx(
50
+ "i",
51
+ {
52
+ "aria-hidden": true,
53
+ className: "absolute right-1 top-1 h-[7px] w-[7px] rounded-full bg-[var(--free,currentColor)] ring-2 ring-white/80"
54
+ }
55
+ )
56
+ ] }),
57
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
58
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-[15px] font-bold tracking-[-0.01em]", children: place.name }),
59
+ /* @__PURE__ */ jsxs("span", { className: "mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground", children: [
60
+ place.rating && /* @__PURE__ */ jsxs(Fragment, { children: [
61
+ /* @__PURE__ */ jsx(Star, { className: "h-[11px] w-[11px]", "aria-hidden": true }),
62
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: place.rating.rating })
63
+ ] }),
64
+ (metaLabels == null ? void 0 : metaLabels[place.slug]) && /* @__PURE__ */ jsx("span", { className: "truncate", children: metaLabels[place.slug] })
65
+ ] })
66
+ ] }),
67
+ /* @__PURE__ */ jsx(
68
+ ChevronRight,
69
+ {
70
+ className: "h-4 w-4 shrink-0 text-muted-foreground lg:hidden",
71
+ "aria-hidden": true
72
+ }
73
+ )
74
+ ]
75
+ }
76
+ ),
77
+ signature && /* @__PURE__ */ jsx(
78
+ MenuItem,
79
+ {
80
+ item: signature.item,
81
+ labels,
82
+ formatPrice,
83
+ formatCount,
84
+ markGradient: hues == null ? void 0 : hues[place.slug],
85
+ subtitle: signature.why,
86
+ showSignatureBadge: signature.isSignature,
87
+ href: signature.href,
88
+ linkComponent: LinkComponent,
89
+ className: "min-w-0"
90
+ }
91
+ )
92
+ ]
93
+ },
94
+ place.slug
95
+ );
96
+ }) });
97
+ }
98
+
99
+ export { SignatureLineList };
@@ -0,0 +1,54 @@
1
+ import * as class_variance_authority_types from 'class-variance-authority/types';
2
+ import * as React from 'react';
3
+ import { VariantProps } from 'class-variance-authority';
4
+
5
+ /**
6
+ * SnapRail — a horizontal, finger-driven rail with mandatory snap points, the
7
+ * way a native app lays out a row of cards. Not `DriftRail`: that one moves on
8
+ * its own and is for ambience; this one waits to be swiped and is for content
9
+ * the reader is expected to reach.
10
+ *
11
+ * Every direct child becomes a snap target and takes the rail's item width, so
12
+ * callers pass bare cards — no wrapper, no per-item width. Widths are
13
+ * percentages of the rail, so the peek of the next card is what tells the
14
+ * reader there is more; there is no scrollbar and no edge fade.
15
+ *
16
+ * `bleed` cancels the host's horizontal padding and restores it as scroll
17
+ * padding, so the first card lines up with the host's content edge and the
18
+ * rail still snaps to that same edge after a swipe. Without it, the rail
19
+ * snaps flush to the host's rounded corner and loads looking pre-scrolled.
20
+ * `card` is the 14px inset of a hub card; `page` is the 16px page gutter.
21
+ */
22
+ declare const snapRailVariants: (props?: ({
23
+ itemWidth?: "sm" | "lg" | "full" | "md" | null | undefined;
24
+ gap?: "default" | "sm" | null | undefined;
25
+ bleed?: "none" | "page" | "card" | null | undefined;
26
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
27
+ type SnapRailProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof snapRailVariants> & {
28
+ /**
29
+ * Page dots under the rail, one per child, the current one filled. For
30
+ * `itemWidth="full"`, where no next card peeks in to say there is more.
31
+ * Decorative: the rail itself is the control, and every card is reachable
32
+ * by scrolling, so the dots are `aria-hidden`.
33
+ */
34
+ dots?: boolean;
35
+ /** Classes for the dots row, e.g. `lg:hidden` where the rail becomes a grid. */
36
+ dotsClassName?: string;
37
+ };
38
+ declare const SnapRail: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
39
+ itemWidth?: "sm" | "lg" | "full" | "md" | null | undefined;
40
+ gap?: "default" | "sm" | null | undefined;
41
+ bleed?: "none" | "page" | "card" | null | undefined;
42
+ } & class_variance_authority_types.ClassProp) | undefined) => string> & {
43
+ /**
44
+ * Page dots under the rail, one per child, the current one filled. For
45
+ * `itemWidth="full"`, where no next card peeks in to say there is more.
46
+ * Decorative: the rail itself is the control, and every card is reachable
47
+ * by scrolling, so the dots are `aria-hidden`.
48
+ */
49
+ dots?: boolean;
50
+ /** Classes for the dots row, e.g. `lg:hidden` where the rail becomes a grid. */
51
+ dotsClassName?: string;
52
+ } & React.RefAttributes<HTMLDivElement>>;
53
+
54
+ export { SnapRail, type SnapRailProps };