@bearmenu/ui 0.8.9 → 0.8.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) 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-AXV5O2IZ.js → chunk-AKQ57NXY.js} +3 -3
  5. package/dist/chunk-CLLY6XB3.js +41 -0
  6. package/dist/chunk-DTXNDLSE.js +125 -0
  7. package/dist/chunk-F6FMZJDG.js +113 -0
  8. package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
  9. package/dist/chunk-MZL7CGCL.js +109 -0
  10. package/dist/chunk-SY6RV476.js +48 -0
  11. package/dist/chunk-UDN7UHJW.js +60 -0
  12. package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
  13. package/dist/components/badge.d.ts +1 -1
  14. package/dist/components/button.d.ts +1 -1
  15. package/dist/components/command.d.ts +7 -7
  16. package/dist/components/concept-compare-rail.d.ts +50 -0
  17. package/dist/components/concept-compare-rail.js +5 -0
  18. package/dist/components/concept-compare.d.ts +75 -38
  19. package/dist/components/concept-compare.js +209 -133
  20. package/dist/components/concept-convergence.d.ts +41 -0
  21. package/dist/components/concept-convergence.js +53 -0
  22. package/dist/components/convergence.d.ts +25 -0
  23. package/dist/components/convergence.js +4 -0
  24. package/dist/components/date-input.d.ts +1 -1
  25. package/dist/components/event-card.js +1 -1
  26. package/dist/components/event-compact-row.js +1 -1
  27. package/dist/components/event-detail-panel.js +1 -1
  28. package/dist/components/event-index-row.js +1 -1
  29. package/dist/components/event-list-row-wide.js +1 -1
  30. package/dist/components/event-list-row.js +2 -2
  31. package/dist/components/explainer-steps.d.ts +34 -0
  32. package/dist/components/explainer-steps.js +36 -0
  33. package/dist/components/glass-ground.d.ts +66 -0
  34. package/dist/components/glass-ground.js +3 -0
  35. package/dist/components/hub-surface.js +11 -1
  36. package/dist/components/hue-chip.d.ts +70 -0
  37. package/dist/components/hue-chip.js +3 -0
  38. package/dist/components/kitchen-dossier.d.ts +88 -0
  39. package/dist/components/kitchen-dossier.js +187 -0
  40. package/dist/components/menu-dead-end.d.ts +56 -0
  41. package/dist/components/menu-dead-end.js +49 -0
  42. package/dist/components/menu-family-block.d.ts +99 -0
  43. package/dist/components/menu-family-block.js +155 -0
  44. package/dist/components/menu-family-card.d.ts +33 -27
  45. package/dist/components/menu-family-card.js +43 -85
  46. package/dist/components/menu-field.js +92 -2
  47. package/dist/components/menu-filter-chips.d.ts +86 -0
  48. package/dist/components/menu-filter-chips.js +122 -0
  49. package/dist/components/menu-group-rows.d.ts +53 -0
  50. package/dist/components/menu-group-rows.js +67 -0
  51. package/dist/components/menu-index-header.d.ts +37 -0
  52. package/dist/components/menu-index-header.js +68 -0
  53. package/dist/components/menu-index-list.d.ts +48 -0
  54. package/dist/components/menu-index-list.js +96 -0
  55. package/dist/components/menu-intent-hero.d.ts +69 -0
  56. package/dist/components/menu-intent-hero.js +87 -0
  57. package/dist/components/menu-kitchen-list.d.ts +82 -0
  58. package/dist/components/menu-kitchen-list.js +5 -0
  59. package/dist/components/menu-mocks.d.ts +37 -2
  60. package/dist/components/menu-mocks.js +152 -27
  61. package/dist/components/menu-types.d.ts +182 -6
  62. package/dist/components/motion-icon-controls.js +2 -2
  63. package/dist/components/motion-icon-set.js +6 -6
  64. package/dist/components/motion-icon.d.ts +22 -1
  65. package/dist/components/motion-icon.js +1 -1
  66. package/dist/components/photo-ground.d.ts +64 -0
  67. package/dist/components/photo-ground.js +3 -0
  68. package/dist/components/place-about-tab.js +2 -2
  69. package/dist/components/place-details-mobile.js +2 -2
  70. package/dist/components/place-types.d.ts +2 -2
  71. package/dist/components/qa-block.d.ts +36 -0
  72. package/dist/components/qa-block.js +24 -0
  73. package/dist/components/scroll-fade.js +3 -121
  74. package/dist/components/snap-rail.d.ts +24 -1
  75. package/dist/components/snap-rail.js +3 -109
  76. package/dist/components/spinner.d.ts +2 -2
  77. package/dist/components/story.d.ts +34 -0
  78. package/dist/components/story.js +3 -0
  79. package/dist/components/text.d.ts +3 -3
  80. package/dist/components/top-comparison.d.ts +28 -0
  81. package/dist/components/top-comparison.js +76 -0
  82. package/dist/components/top-cross-link.d.ts +34 -0
  83. package/dist/components/top-cross-link.js +101 -0
  84. package/dist/components/top-ranking-entry.d.ts +53 -0
  85. package/dist/components/top-ranking-entry.js +217 -0
  86. package/dist/components/top-ranking-hero.d.ts +61 -0
  87. package/dist/components/top-ranking-hero.js +99 -0
  88. package/dist/components/top-ranking-legend.d.ts +69 -0
  89. package/dist/components/top-ranking-legend.js +124 -0
  90. package/dist/components/top-types.d.ts +109 -0
  91. package/dist/components/top-types.js +1 -0
  92. package/dist/components/venue-grade-dimensions.d.ts +3 -1
  93. package/dist/components/venue-grade-dimensions.js +1 -1
  94. package/dist/components/weekend-panel.js +2 -2
  95. package/package.json +1 -1
  96. package/src/components/concept-compare-rail.tsx +131 -0
  97. package/src/components/concept-compare.stories.tsx +122 -54
  98. package/src/components/concept-compare.tsx +363 -196
  99. package/src/components/concept-convergence.stories.tsx +53 -0
  100. package/src/components/concept-convergence.tsx +90 -0
  101. package/src/components/convergence.stories.tsx +54 -0
  102. package/src/components/convergence.tsx +177 -0
  103. package/src/components/explainer-steps.stories.tsx +46 -0
  104. package/src/components/explainer-steps.tsx +74 -0
  105. package/src/components/glass-ground.tsx +129 -0
  106. package/src/components/hub-surface.tsx +11 -1
  107. package/src/components/hue-chip.stories.tsx +76 -0
  108. package/src/components/hue-chip.test.tsx +62 -0
  109. package/src/components/hue-chip.tsx +99 -0
  110. package/src/components/kitchen-dossier.stories.tsx +108 -0
  111. package/src/components/kitchen-dossier.tsx +322 -0
  112. package/src/components/menu-dead-end.stories.tsx +67 -0
  113. package/src/components/menu-dead-end.tsx +127 -0
  114. package/src/components/menu-family-block.stories.tsx +91 -0
  115. package/src/components/menu-family-block.tsx +314 -0
  116. package/src/components/menu-family-card.stories.tsx +43 -35
  117. package/src/components/menu-family-card.tsx +122 -142
  118. package/src/components/menu-filter-chips.stories.tsx +107 -0
  119. package/src/components/menu-filter-chips.tsx +238 -0
  120. package/src/components/menu-group-rows.stories.tsx +54 -0
  121. package/src/components/menu-group-rows.tsx +133 -0
  122. package/src/components/menu-index-header.stories.tsx +72 -0
  123. package/src/components/menu-index-header.tsx +125 -0
  124. package/src/components/menu-index-list.stories.tsx +50 -0
  125. package/src/components/menu-index-list.tsx +149 -0
  126. package/src/components/menu-intent-hero.stories.tsx +67 -0
  127. package/src/components/menu-intent-hero.tsx +189 -0
  128. package/src/components/menu-kitchen-list.stories.tsx +67 -0
  129. package/src/components/menu-kitchen-list.test.tsx +78 -0
  130. package/src/components/menu-kitchen-list.tsx +284 -0
  131. package/src/components/menu-mocks.ts +198 -23
  132. package/src/components/menu-types.ts +189 -6
  133. package/src/components/motion-icon-controls.tsx +11 -11
  134. package/src/components/motion-icon-set.tsx +6 -5
  135. package/src/components/motion-icon.test.tsx +23 -1
  136. package/src/components/motion-icon.tsx +20 -0
  137. package/src/components/photo-ground.tsx +147 -0
  138. package/src/components/place-types.ts +2 -2
  139. package/src/components/qa-block.stories.tsx +43 -0
  140. package/src/components/qa-block.tsx +60 -0
  141. package/src/components/snap-rail.tsx +58 -15
  142. package/src/components/story.tsx +225 -0
  143. package/src/components/top-comparison.stories.tsx +78 -0
  144. package/src/components/top-comparison.tsx +151 -0
  145. package/src/components/top-cross-link.stories.tsx +84 -0
  146. package/src/components/top-cross-link.tsx +171 -0
  147. package/src/components/top-ranking-entry.stories.tsx +171 -0
  148. package/src/components/top-ranking-entry.test.tsx +105 -0
  149. package/src/components/top-ranking-entry.tsx +425 -0
  150. package/src/components/top-ranking-hero.stories.tsx +76 -0
  151. package/src/components/top-ranking-hero.tsx +218 -0
  152. package/src/components/top-ranking-legend.stories.tsx +78 -0
  153. package/src/components/top-ranking-legend.tsx +239 -0
  154. package/src/components/top-types.ts +110 -0
  155. package/src/components/venue-grade-dimensions.tsx +2 -2
  156. package/dist/chunk-THOVPZNN.js +0 -37
  157. package/dist/chunk-YS6BHKZO.js +0 -92
  158. package/dist/components/ranked-venue-list.d.ts +0 -49
  159. package/dist/components/ranked-venue-list.js +0 -87
  160. package/dist/components/signature-line-list.d.ts +0 -45
  161. package/dist/components/signature-line-list.js +0 -99
  162. package/dist/components/venue-busyness-strip.d.ts +0 -35
  163. package/dist/components/venue-busyness-strip.js +0 -37
  164. package/dist/components/venue-signature-block.d.ts +0 -39
  165. package/dist/components/venue-signature-block.js +0 -6
  166. package/dist/components/venue-spotlight.d.ts +0 -74
  167. package/dist/components/venue-spotlight.js +0 -138
  168. package/src/components/ranked-venue-list.stories.tsx +0 -80
  169. package/src/components/ranked-venue-list.tsx +0 -177
  170. package/src/components/signature-line-list.stories.tsx +0 -85
  171. package/src/components/signature-line-list.tsx +0 -136
  172. package/src/components/venue-busyness-strip.stories.tsx +0 -66
  173. package/src/components/venue-busyness-strip.tsx +0 -64
  174. package/src/components/venue-signature-block.stories.tsx +0 -68
  175. package/src/components/venue-signature-block.tsx +0 -71
  176. package/src/components/venue-spotlight.stories.tsx +0 -141
  177. package/src/components/venue-spotlight.tsx +0 -236
  178. package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
@@ -0,0 +1,167 @@
1
+ import { Separator } from './chunk-WSRWFA6K.js';
2
+ import { Skeleton } from './chunk-QXHMPJ35.js';
3
+ import { cn } from './chunk-FEK5XGZW.js';
4
+ import { __objRest, __spreadValues } from './chunk-3GWWZKX7.js';
5
+ import { Star, Clock, Check, MessageSquare, ChevronRight } from 'lucide-react';
6
+ import { jsxs, jsx, Fragment } 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 MenuKitchenList({
13
+ kitchens,
14
+ labels,
15
+ onShowMore,
16
+ linkComponent = DefaultLink,
17
+ className
18
+ }) {
19
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col", className), children: [
20
+ /* @__PURE__ */ jsx("ol", { children: kitchens.map((kitchen, index) => /* @__PURE__ */ jsxs("li", { className: "[&:first-child>div]:pt-0", children: [
21
+ index > 0 && /* @__PURE__ */ jsx(Separator, {}),
22
+ /* @__PURE__ */ jsx(KitchenRow, { kitchen, labels, linkComponent })
23
+ ] }, kitchen.place.slug)) }),
24
+ onShowMore && labels.showMore && /* @__PURE__ */ jsx(
25
+ "button",
26
+ {
27
+ type: "button",
28
+ onClick: onShowMore,
29
+ className: "mt-2 self-start rounded-md py-2 text-[13px] font-bold text-accent transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
30
+ children: labels.showMore
31
+ }
32
+ )
33
+ ] });
34
+ }
35
+ function KitchenRow({
36
+ kitchen,
37
+ labels,
38
+ linkComponent: LinkComponent
39
+ }) {
40
+ const { place, dish } = kitchen;
41
+ const openNow = place.open_now === true;
42
+ return /* @__PURE__ */ jsxs("div", { className: "relative flex flex-wrap items-center gap-[11px] py-3 transition-colors hover:bg-muted/30 motion-reduce:transition-none lg:flex-nowrap lg:gap-4 lg:py-3.5", children: [
43
+ /* @__PURE__ */ jsx(
44
+ LinkComponent,
45
+ {
46
+ href: kitchen.href,
47
+ "aria-label": labels.openVenue(place.name),
48
+ className: "absolute inset-0 z-[1] rounded-xl focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
49
+ }
50
+ ),
51
+ /* @__PURE__ */ jsxs("span", { className: "relative h-[46px] w-[46px] shrink-0 overflow-hidden rounded-[11px] bg-[repeating-linear-gradient(135deg,oklch(0.78_0.018_55)_0_8px,oklch(0.84_0.012_58)_8px_16px)] lg:h-[58px] lg:w-[58px] lg:rounded-[13px]", children: [
52
+ place.thumbnail_url && // eslint-disable-next-line @next/next/no-img-element
53
+ /* @__PURE__ */ jsx(
54
+ "img",
55
+ {
56
+ src: place.thumbnail_url,
57
+ alt: "",
58
+ loading: "lazy",
59
+ decoding: "async",
60
+ className: "h-full w-full object-cover"
61
+ }
62
+ ),
63
+ /* @__PURE__ */ jsx(
64
+ "span",
65
+ {
66
+ title: openNow ? labels.openNow : void 0,
67
+ className: cn(
68
+ "absolute right-[5px] top-[5px] h-[7px] w-[7px] rounded-full ring-2 ring-white/85",
69
+ openNow ? "bg-[var(--free,oklch(0.55_0.13_155))]" : "bg-[oklch(0.8_0.01_60)]"
70
+ )
71
+ }
72
+ )
73
+ ] }),
74
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 lg:w-[226px] lg:flex-none", children: [
75
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[14.5px] font-bold tracking-[-0.01em] lg:text-[15.5px]", children: place.name }),
76
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 truncate text-xs text-muted-foreground", children: [
77
+ place.rating && /* @__PURE__ */ jsxs(Fragment, { children: [
78
+ /* @__PURE__ */ jsx(Star, { className: "h-[11px] w-[11px] shrink-0", "aria-hidden": true }),
79
+ place.rating.rating,
80
+ kitchen.metaLabel && " \xB7 "
81
+ ] }),
82
+ kitchen.metaLabel
83
+ ] }),
84
+ kitchen.hoursLabel && /* @__PURE__ */ jsxs(
85
+ "span",
86
+ {
87
+ className: cn(
88
+ "mt-[3px] inline-flex h-[21px] w-max items-center gap-[5px] rounded-md px-2 text-[10.5px] font-bold",
89
+ openNow ? "bg-[oklch(0.96_0.02_155)] text-[var(--free,oklch(0.45_0.13_155))]" : "bg-muted text-muted-foreground"
90
+ ),
91
+ children: [
92
+ /* @__PURE__ */ jsx(Clock, { className: "h-2.5 w-2.5 shrink-0", "aria-hidden": true }),
93
+ kitchen.hoursLabel
94
+ ]
95
+ }
96
+ )
97
+ ] }),
98
+ dish && /* DESKTOP ONLY, where it is a column of its own beside the venue.
99
+ On the phone it wrapped to a third line under the row — kicker, dish,
100
+ mention count — which turned a four-line venue row into a seven-line
101
+ block and, ten rows down, buried the venue names the list exists to
102
+ be scanned for. The dish is not what a reader is choosing here; it is
103
+ one tap away on the venue's own page, with room to state it.
104
+
105
+ `order-5` puts it last in the visual order without moving it in the
106
+ DOM, where it belongs to the venue. */
107
+ /* @__PURE__ */ jsxs("div", { className: "hidden min-w-0 basis-full pl-[57px] lg:order-none lg:block lg:flex-1 lg:basis-auto lg:pl-0", children: [
108
+ /* @__PURE__ */ jsxs(
109
+ "span",
110
+ {
111
+ className: cn(
112
+ "flex items-center gap-1.5 text-[9px] font-extrabold uppercase tracking-[0.1em]",
113
+ dish.matched ? "text-accent" : "text-[oklch(0.72_0.01_60)]"
114
+ ),
115
+ children: [
116
+ dish.kicker,
117
+ dish.matched && dish.matchLabel && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-[5px] bg-accent-subtle px-1.5 py-px text-[10.5px] font-bold normal-case tracking-normal text-accent", children: [
118
+ /* @__PURE__ */ jsx(Check, { className: "h-2.5 w-2.5 shrink-0", "aria-hidden": true }),
119
+ dish.matchLabel
120
+ ] })
121
+ ]
122
+ }
123
+ ),
124
+ dish.href ? /* @__PURE__ */ jsx(
125
+ LinkComponent,
126
+ {
127
+ href: dish.href,
128
+ "aria-label": labels.openDish(dish.name),
129
+ className: "relative z-[2] mt-0.5 block truncate font-serif text-[13.5px] italic underline decoration-[oklch(0.84_0.01_60)] decoration-1 underline-offset-[3px] transition-colors hover:text-accent hover:decoration-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none lg:text-[15px]",
130
+ children: dish.name
131
+ }
132
+ ) : /* @__PURE__ */ jsx("span", { className: "mt-0.5 block truncate font-serif text-[13.5px] italic lg:text-[15px]", children: dish.name }),
133
+ dish.meta && /* @__PURE__ */ jsxs("span", { className: "mt-[3px] flex items-center gap-1 text-[11.5px] text-muted-foreground", children: [
134
+ !dish.matched && /* @__PURE__ */ jsx(MessageSquare, { className: "h-2.5 w-2.5 shrink-0", "aria-hidden": true }),
135
+ dish.meta
136
+ ] })
137
+ ] }),
138
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-[11px] lg:ml-0", children: [
139
+ kitchen.priceLabel && /* @__PURE__ */ jsx("span", { className: "text-base font-extrabold tabular-nums text-accent", children: kitchen.priceLabel }),
140
+ /* @__PURE__ */ jsx(ChevronRight, { className: "h-[15px] w-[15px] shrink-0 text-muted-foreground", "aria-hidden": true })
141
+ ] })
142
+ ] });
143
+ }
144
+ function MenuKitchenListSkeleton({
145
+ rows = 10,
146
+ className
147
+ }) {
148
+ return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col", className), "aria-hidden": true, children: Array.from({ length: rows }, (_, index) => /* @__PURE__ */ jsxs("div", { children: [
149
+ index > 0 && /* @__PURE__ */ jsx(Separator, {}),
150
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-[11px] py-3 lg:flex-nowrap lg:gap-4 lg:py-3.5", children: [
151
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[46px] w-[46px] rounded-[11px] lg:h-[58px] lg:w-[58px] lg:rounded-[13px]" }),
152
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5 lg:w-[226px] lg:flex-none", children: [
153
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-[60%]" }),
154
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-[45%]" }),
155
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[21px] w-[120px] rounded-md" })
156
+ ] }),
157
+ /* @__PURE__ */ jsxs("div", { className: "hidden min-w-0 basis-full pl-[57px] lg:order-none lg:block lg:flex-1 lg:basis-auto lg:pl-0", children: [
158
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-2.5 w-[90px]" }),
159
+ /* @__PURE__ */ jsx(Skeleton, { className: "mt-1.5 h-4 w-[70%]" }),
160
+ /* @__PURE__ */ jsx(Skeleton, { className: "mt-1.5 h-3 w-[40%]" })
161
+ ] }),
162
+ /* @__PURE__ */ jsx(Skeleton, { className: "ml-auto h-5 w-[56px] lg:ml-0" })
163
+ ] })
164
+ ] }, index)) });
165
+ }
166
+
167
+ export { MenuKitchenList, MenuKitchenListSkeleton };
@@ -0,0 +1,121 @@
1
+ import { cn } from './chunk-FEK5XGZW.js';
2
+ import { __objRest, __spreadProps, __spreadValues } from './chunk-3GWWZKX7.js';
3
+ import * as React from 'react';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+
6
+ var fromMap = {
7
+ background: "from-background",
8
+ card: "from-card",
9
+ muted: "from-muted"
10
+ };
11
+ var sizeMap = {
12
+ vertical: { sm: "h-6", md: "h-12", lg: "h-20" },
13
+ horizontal: { sm: "w-8", md: "w-16", lg: "w-28" }
14
+ };
15
+ var ScrollFade = React.forwardRef(
16
+ (_a, ref) => {
17
+ var _b = _a, {
18
+ orientation = "vertical",
19
+ fadeColor = "background",
20
+ size = "md",
21
+ className,
22
+ scrollClassName,
23
+ children
24
+ } = _b, props = __objRest(_b, [
25
+ "orientation",
26
+ "fadeColor",
27
+ "size",
28
+ "className",
29
+ "scrollClassName",
30
+ "children"
31
+ ]);
32
+ const innerRef = React.useRef(null);
33
+ React.useImperativeHandle(ref, () => innerRef.current);
34
+ const [atStart, setAtStart] = React.useState(true);
35
+ const [atEnd, setAtEnd] = React.useState(true);
36
+ const update = React.useCallback(() => {
37
+ const el = innerRef.current;
38
+ if (!el) return;
39
+ if (orientation === "vertical") {
40
+ const overflow = el.scrollHeight - el.clientHeight;
41
+ setAtStart(el.scrollTop <= 1);
42
+ setAtEnd(overflow <= 1 || el.scrollTop >= overflow - 1);
43
+ } else {
44
+ const overflow = el.scrollWidth - el.clientWidth;
45
+ setAtStart(el.scrollLeft <= 1);
46
+ setAtEnd(overflow <= 1 || el.scrollLeft >= overflow - 1);
47
+ }
48
+ }, [orientation]);
49
+ React.useEffect(() => {
50
+ const el = innerRef.current;
51
+ if (!el) return;
52
+ update();
53
+ el.addEventListener("scroll", update, { passive: true });
54
+ const ro = new ResizeObserver(update);
55
+ ro.observe(el);
56
+ return () => {
57
+ el.removeEventListener("scroll", update);
58
+ ro.disconnect();
59
+ };
60
+ }, [update]);
61
+ const fromClass = fromMap[fadeColor];
62
+ const fadeSize = sizeMap[orientation][size];
63
+ return /* @__PURE__ */ jsxs(
64
+ "div",
65
+ {
66
+ className: cn(
67
+ "relative isolate flex",
68
+ orientation === "vertical" ? "flex-col" : "flex-row",
69
+ className
70
+ ),
71
+ children: [
72
+ /* @__PURE__ */ jsx(
73
+ "div",
74
+ __spreadProps(__spreadValues({
75
+ ref: innerRef,
76
+ className: cn(
77
+ "min-h-0 min-w-0 flex-1",
78
+ orientation === "vertical" ? "overflow-y-auto" : "overflow-x-auto",
79
+ scrollClassName
80
+ )
81
+ }, props), {
82
+ children
83
+ })
84
+ ),
85
+ /* @__PURE__ */ jsx(
86
+ "div",
87
+ {
88
+ "aria-hidden": true,
89
+ className: cn(
90
+ "pointer-events-none absolute z-10 transition-opacity duration-300",
91
+ // `from-30%` adds a solid plateau before the gradient starts
92
+ // fading — without it the gradient is a pure linear ramp and only
93
+ // the very top pixel reads as solid, leaving the cut edge visible.
94
+ // -4px inset pulls the fade past the container edge so the very
95
+ // edge of clipped content is covered by the solid plateau.
96
+ orientation === "vertical" ? `inset-x-0 -top-1 ${fadeSize} bg-gradient-to-b from-30%` : `inset-y-0 -left-1 ${fadeSize} bg-gradient-to-r from-30%`,
97
+ fromClass,
98
+ atStart ? "opacity-0" : "opacity-100"
99
+ )
100
+ }
101
+ ),
102
+ /* @__PURE__ */ jsx(
103
+ "div",
104
+ {
105
+ "aria-hidden": true,
106
+ className: cn(
107
+ "pointer-events-none absolute z-10 transition-opacity duration-300",
108
+ orientation === "vertical" ? `inset-x-0 -bottom-1 ${fadeSize} bg-gradient-to-t from-30%` : `inset-y-0 -right-1 ${fadeSize} bg-gradient-to-l from-30%`,
109
+ fromClass,
110
+ atEnd ? "opacity-0" : "opacity-100"
111
+ )
112
+ }
113
+ )
114
+ ]
115
+ }
116
+ );
117
+ }
118
+ );
119
+ ScrollFade.displayName = "ScrollFade";
120
+
121
+ export { ScrollFade };
@@ -0,0 +1,72 @@
1
+ import { cn } from './chunk-FEK5XGZW.js';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+
4
+ var PHOTO_GROUND = "bg-[oklch(0.21_0.012_55)]";
5
+ var OVERLAY = "absolute inset-0 bg-[oklch(0.16_0.012_55_/_0.5)]";
6
+ function PhotoGround({
7
+ photos,
8
+ initials,
9
+ mode = "mosaic",
10
+ active = 0,
11
+ mosaicClassName,
12
+ phoneTiles,
13
+ gradient,
14
+ overlay,
15
+ overlayClassName,
16
+ loading = "lazy",
17
+ className
18
+ }) {
19
+ const initialOf = (index) => {
20
+ var _a, _b;
21
+ return typeof initials === "string" ? initials : (_b = (_a = initials[index]) != null ? _a : initials[0]) != null ? _b : "";
22
+ };
23
+ const hasAnyPhoto = photos.some(Boolean);
24
+ const slot = (index, extra) => {
25
+ const src = photos[index];
26
+ return /* @__PURE__ */ jsx("span", { className: cn("relative block overflow-hidden", extra), children: src ? (
27
+ // eslint-disable-next-line @next/next/no-img-element
28
+ /* @__PURE__ */ jsx(
29
+ "img",
30
+ {
31
+ src,
32
+ alt: "",
33
+ loading: index === 0 ? loading : "lazy",
34
+ decoding: "async",
35
+ className: "absolute inset-0 h-full w-full object-cover"
36
+ }
37
+ )
38
+ ) : (
39
+ /* Never a grey box: the ground with the venue's initial. A missing
40
+ photograph is not a broken image, and drawing one as a placeholder
41
+ makes a real city look unfinished. */
42
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "font-serif text-[64px] font-semibold leading-none text-white/15", children: initialOf(index) }) })
43
+ ) }, index);
44
+ };
45
+ return /* @__PURE__ */ jsxs("span", { "aria-hidden": true, className: cn("absolute inset-0 block", PHOTO_GROUND, className), children: [
46
+ mode === "mosaic" ? /* @__PURE__ */ jsx("span", { className: cn("absolute inset-0 grid gap-px", mosaicClassName), children: photos.map(
47
+ (_, index) => slot(index, phoneTiles !== void 0 && index >= phoneTiles ? "hidden lg:block" : void 0)
48
+ ) }) : (
49
+ /* Stacked, not swapped, so the change is a cross-fade and not a flash
50
+ of ground between two images. */
51
+ photos.map(
52
+ (_, index) => slot(
53
+ index,
54
+ cn(
55
+ "absolute inset-0 transition-opacity duration-500 motion-reduce:transition-none",
56
+ index === active ? "opacity-100" : "opacity-0"
57
+ )
58
+ )
59
+ )
60
+ ),
61
+ gradient && hasAnyPhoto && /* @__PURE__ */ jsx(
62
+ "span",
63
+ {
64
+ className: "absolute inset-0 opacity-[0.85] mix-blend-color",
65
+ style: { backgroundImage: gradient }
66
+ }
67
+ ),
68
+ overlay && /* @__PURE__ */ jsx("span", { className: cn(OVERLAY, overlayClassName) })
69
+ ] });
70
+ }
71
+
72
+ export { PHOTO_GROUND, PhotoGround };
@@ -1,7 +1,7 @@
1
1
  import { cn } from './chunk-FEK5XGZW.js';
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
3
 
4
- var DEFAULT_ORDER = [
4
+ var DIMENSION_ORDER = [
5
5
  "food",
6
6
  "coffee_quality",
7
7
  "drinks_quality",
@@ -15,7 +15,7 @@ var DEFAULT_ORDER = [
15
15
  function VenueGradeDimensions({
16
16
  dimensions,
17
17
  labels,
18
- order = DEFAULT_ORDER,
18
+ order = DIMENSION_ORDER,
19
19
  formatScore,
20
20
  className
21
21
  }) {
@@ -55,4 +55,4 @@ function VenueGradeDimensions({
55
55
  );
56
56
  }
57
57
 
58
- export { VenueGradeDimensions };
58
+ export { DIMENSION_ORDER, VenueGradeDimensions };
@@ -0,0 +1,41 @@
1
+ import { cn } from './chunk-FEK5XGZW.js';
2
+ import { jsxs } from 'react/jsx-runtime';
3
+
4
+ var STRENGTH = {
5
+ /** A labelled facet: readable at 11.5px, still clearly the family's colour. */
6
+ chip: 20,
7
+ /** A quieter wash, for a chip that sits beside a stronger one. */
8
+ subtle: 12
9
+ };
10
+ function hueTint(hue, tone = "chip", base = "var(--card)") {
11
+ return `color-mix(in oklab, ${hue} ${STRENGTH[tone]}%, ${base})`;
12
+ }
13
+ function HueChip({
14
+ hue,
15
+ tone = "chip",
16
+ base = "var(--card)",
17
+ icon,
18
+ as: Tag = "span",
19
+ children,
20
+ className,
21
+ "aria-label": ariaLabel
22
+ }) {
23
+ return /* @__PURE__ */ jsxs(
24
+ Tag,
25
+ {
26
+ "aria-label": ariaLabel,
27
+ className: cn(
28
+ "inline-flex h-7 items-center gap-1.5 rounded-full px-2.5 text-[11.5px] text-foreground",
29
+ !hue && "bg-muted",
30
+ className
31
+ ),
32
+ style: hue ? { backgroundColor: hueTint(hue, tone, base) } : void 0,
33
+ children: [
34
+ icon,
35
+ children
36
+ ]
37
+ }
38
+ );
39
+ }
40
+
41
+ export { HueChip, hueTint };
@@ -0,0 +1,125 @@
1
+ import { cn } from './chunk-FEK5XGZW.js';
2
+ import { __objRest, __spreadProps, __spreadValues } from './chunk-3GWWZKX7.js';
3
+ import * as React from 'react';
4
+ import { cva } from 'class-variance-authority';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ var snapRailVariants = cva(
8
+ cn(
9
+ "flex snap-x snap-mandatory overflow-x-auto pb-0.5 scrollbar-hide",
10
+ "[&>*]:shrink-0 [&>*]:snap-start"
11
+ ),
12
+ {
13
+ variants: {
14
+ itemWidth: {
15
+ sm: "[&>*]:w-[46%]",
16
+ md: "[&>*]:w-[60%]",
17
+ lg: "[&>*]:w-[80%]",
18
+ /** One card per view — pair with `dots` so the reader knows there is more. */
19
+ full: "[&>*]:w-full"
20
+ },
21
+ gap: {
22
+ sm: "gap-2",
23
+ default: "gap-3"
24
+ },
25
+ bleed: {
26
+ none: "",
27
+ card: "-mx-3.5 px-3.5 scroll-px-3.5",
28
+ page: "-mx-4 px-4 scroll-px-4"
29
+ }
30
+ },
31
+ defaultVariants: {
32
+ itemWidth: "sm",
33
+ gap: "default",
34
+ bleed: "none"
35
+ }
36
+ }
37
+ );
38
+ var SnapRail = React.forwardRef(
39
+ (_a, ref) => {
40
+ var _b = _a, { className, itemWidth, gap, bleed, dots, dotsClassName, onActiveChange, children } = _b, props = __objRest(_b, ["className", "itemWidth", "gap", "bleed", "dots", "dotsClassName", "onActiveChange", "children"]);
41
+ const railRef = React.useRef(null);
42
+ const [active, setActive] = React.useState(0);
43
+ const count = React.Children.count(children);
44
+ const tracks = Boolean(dots || onActiveChange);
45
+ const onActiveChangeRef = React.useRef(onActiveChange);
46
+ onActiveChangeRef.current = onActiveChange;
47
+ const setRefs = React.useCallback(
48
+ (node) => {
49
+ railRef.current = node;
50
+ if (typeof ref === "function") ref(node);
51
+ else if (ref) ref.current = node;
52
+ },
53
+ [ref]
54
+ );
55
+ React.useEffect(() => {
56
+ if (!tracks) return;
57
+ const rail2 = railRef.current;
58
+ if (!rail2) return;
59
+ let frame = 0;
60
+ let last = -1;
61
+ const sync = () => {
62
+ var _a2;
63
+ frame = 0;
64
+ const origin = rail2.scrollLeft + (parseFloat(getComputedStyle(rail2).scrollPaddingLeft) || 0);
65
+ let best = 0;
66
+ let bestDistance = Number.POSITIVE_INFINITY;
67
+ Array.from(rail2.children).forEach((child, index) => {
68
+ const distance = Math.abs(child.offsetLeft - origin);
69
+ if (distance < bestDistance) {
70
+ bestDistance = distance;
71
+ best = index;
72
+ }
73
+ });
74
+ setActive(best);
75
+ if (best !== last) {
76
+ last = best;
77
+ (_a2 = onActiveChangeRef.current) == null ? void 0 : _a2.call(onActiveChangeRef, best);
78
+ }
79
+ };
80
+ const onScroll = () => {
81
+ if (!frame) frame = requestAnimationFrame(sync);
82
+ };
83
+ sync();
84
+ rail2.addEventListener("scroll", onScroll, { passive: true });
85
+ return () => {
86
+ if (frame) cancelAnimationFrame(frame);
87
+ rail2.removeEventListener("scroll", onScroll);
88
+ };
89
+ }, [tracks, count]);
90
+ const rail = /* @__PURE__ */ jsx(
91
+ "div",
92
+ __spreadProps(__spreadValues({
93
+ ref: setRefs,
94
+ "data-slot": "snap-rail",
95
+ className: cn(snapRailVariants({ itemWidth, gap, bleed }), className)
96
+ }, props), {
97
+ children
98
+ })
99
+ );
100
+ if (!dots) return rail;
101
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
102
+ rail,
103
+ /* @__PURE__ */ jsx(SnapRailDots, { count, active, className: dotsClassName })
104
+ ] });
105
+ }
106
+ );
107
+ SnapRail.displayName = "SnapRail";
108
+ function SnapRailDots({
109
+ count,
110
+ active,
111
+ className
112
+ }) {
113
+ return /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: cn("flex justify-center gap-1.5", className), children: Array.from({ length: count }, (_, index) => /* @__PURE__ */ jsx(
114
+ "span",
115
+ {
116
+ className: cn(
117
+ "h-1.5 rounded-full transition-[width,background-color] duration-200 motion-reduce:transition-none",
118
+ index === active ? "w-4 bg-foreground" : "w-1.5 bg-foreground/25"
119
+ )
120
+ },
121
+ index
122
+ )) });
123
+ }
124
+
125
+ export { SnapRail, SnapRailDots };
@@ -0,0 +1,113 @@
1
+ import { cn } from './chunk-FEK5XGZW.js';
2
+ import * as React from 'react';
3
+ import { useInView } from 'framer-motion';
4
+ import { jsx } from 'react/jsx-runtime';
5
+
6
+ var StoryContext = React.createContext(null);
7
+ function useStory() {
8
+ const value = React.useContext(StoryContext);
9
+ if (!value) throw new Error("Story pieces must render inside StoryProvider");
10
+ return value;
11
+ }
12
+ var TICK_MS = 100;
13
+ function StoryProvider({
14
+ count,
15
+ durationMs = 5e3,
16
+ nextLabel,
17
+ className,
18
+ children
19
+ }) {
20
+ const ref = React.useRef(null);
21
+ const inView = useInView(ref, { amount: 0.6 });
22
+ const [active, setActive] = React.useState(0);
23
+ const [elapsed, setElapsed] = React.useState(0);
24
+ const [held, setHeld] = React.useState(false);
25
+ const playing = Boolean(inView && !held && count > 1);
26
+ const go = React.useCallback(
27
+ (index) => {
28
+ setActive((index % count + count) % count);
29
+ setElapsed(0);
30
+ },
31
+ [count]
32
+ );
33
+ const next = React.useCallback(() => go(active + 1), [go, active]);
34
+ React.useEffect(() => {
35
+ if (!playing) return;
36
+ const timer = window.setInterval(() => setElapsed((previous) => previous + TICK_MS), TICK_MS);
37
+ return () => window.clearInterval(timer);
38
+ }, [playing]);
39
+ React.useEffect(() => {
40
+ if (elapsed < durationMs) return;
41
+ setActive((current) => (current + 1) % count);
42
+ setElapsed(0);
43
+ }, [elapsed, durationMs, count]);
44
+ const value = React.useMemo(
45
+ () => ({ count, active, progress: Math.min(elapsed / durationMs, 1), playing, go, next }),
46
+ [count, active, elapsed, durationMs, playing, go, next]
47
+ );
48
+ const advance = (event) => {
49
+ if (count < 2) return;
50
+ if (event.target.closest("a, button")) return;
51
+ next();
52
+ };
53
+ return /* @__PURE__ */ jsx(StoryContext.Provider, { value, children: /* @__PURE__ */ jsx(
54
+ "div",
55
+ {
56
+ ref,
57
+ className: cn(count > 1 && "cursor-pointer", className),
58
+ title: count > 1 ? nextLabel : void 0,
59
+ onClick: advance,
60
+ onPointerDown: () => setHeld(true),
61
+ onPointerUp: () => setHeld(false),
62
+ onPointerLeave: () => setHeld(false),
63
+ onPointerCancel: () => setHeld(false),
64
+ children
65
+ }
66
+ ) });
67
+ }
68
+ function StorySegments({ labels, className }) {
69
+ const { count, active, progress, go } = useStory();
70
+ if (count < 2) return null;
71
+ return /* @__PURE__ */ jsx("div", { className: cn("flex gap-1", className), role: "tablist", children: Array.from({ length: count }, (_, index) => {
72
+ const fill = index < active ? 1 : index === active ? progress : 0;
73
+ return /* @__PURE__ */ jsx(
74
+ "button",
75
+ {
76
+ type: "button",
77
+ role: "tab",
78
+ "aria-selected": index === active,
79
+ "aria-label": labels[index],
80
+ onClick: () => go(index),
81
+ className: "group/segment flex h-6 flex-1 items-center focus-visible:outline-none",
82
+ children: /* @__PURE__ */ jsx("span", { className: "relative block h-[3px] w-full overflow-hidden rounded-full bg-white/25 group-focus-visible/segment:ring-2 group-focus-visible/segment:ring-white/70", children: /* @__PURE__ */ jsx(
83
+ "span",
84
+ {
85
+ className: "absolute inset-y-0 left-0 rounded-full bg-white",
86
+ style: { width: `${fill * 100}%` }
87
+ }
88
+ ) })
89
+ },
90
+ index
91
+ );
92
+ }) });
93
+ }
94
+ function StoryPanel({ parts, className }) {
95
+ const { active } = useStory();
96
+ return /* @__PURE__ */ jsx("div", { className: cn("relative", className), children: /* @__PURE__ */ jsx("div", { className: "grid [&>*]:col-start-1 [&>*]:row-start-1", children: parts.map((part, index) => {
97
+ const isActive = index === active;
98
+ return /* @__PURE__ */ jsx(
99
+ "div",
100
+ {
101
+ "aria-hidden": !isActive,
102
+ className: cn(
103
+ "transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none",
104
+ isActive ? "opacity-100" : "pointer-events-none invisible translate-y-1.5 opacity-0"
105
+ ),
106
+ children: part
107
+ },
108
+ index
109
+ );
110
+ }) }) });
111
+ }
112
+
113
+ export { StoryPanel, StoryProvider, StorySegments };
@@ -8,6 +8,9 @@ function beats(times, duration = 0.62) {
8
8
  return { duration, times, ease: "easeInOut" };
9
9
  }
10
10
  var returnTransition = { type: "spring", stiffness: 320, damping: 26 };
11
+ function pivot(x, y) {
12
+ return { originX: `${x}px`, originY: `${y}px`, transformBox: "view-box" };
13
+ }
11
14
  function poses(rest, active, play, transition) {
12
15
  return {
13
16
  rest: __spreadProps(__spreadValues({}, rest), { transition: returnTransition }),
@@ -73,4 +76,4 @@ function MotionIcon({
73
76
  );
74
77
  }
75
78
 
76
- export { MotionIcon, beats, poses, returnTransition, useIconGesture };
79
+ export { MotionIcon, beats, pivot, poses, returnTransition, useIconGesture };