@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,66 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * GlassGround — a frosted, inked caption rising out of one edge of a
5
+ * photograph.
6
+ *
7
+ * Layers over a `PhotoGround`: two masked `backdrop-blur`s (the frost) and
8
+ * an ink gradient (the contrast), running in one direction — absent where the
9
+ * photograph is the subject, solid where the type is. `MenuFamilyBlock` drew
10
+ * this first; this is the same idea with the stops as data, so a card can say
11
+ * "clear for 128px, then glass" without a sixth copy of the gradient strings.
12
+ *
13
+ * ## Why two blurs, and why the ramps are eased
14
+ *
15
+ * One blur under a linear mask has an edge: the eye reads the line where
16
+ * sharp becomes soft, however gently the mask fades it in. Two blurs — a
17
+ * light one that starts early and a heavier one that starts late — make the
18
+ * softening itself a gradient (the "progressive blur" of a phone's lock
19
+ * screen), and every ramp here is an ease-in through five stops rather than
20
+ * a straight line, because a straight ramp has a visible crease at each end
21
+ * where its slope changes.
22
+ *
23
+ * ## Why the stops are custom properties
24
+ *
25
+ * Tailwind emits only the classes it can read verbatim, so the gradients
26
+ * cannot be assembled from props into a class. The classes below are literal
27
+ * and read `--glass-*` variables; the component writes those inline, having
28
+ * interpolated the quarter stops in JS (`clear` and `full` must share a
29
+ * unit). Same mechanism as `HueChip`'s hue. The ink's colour is the
30
+ * package's one scrim literal, `oklch(0.16 0.012 55)`, never `var(--scrim)`.
31
+ *
32
+ * ## The ramp ends where the type begins
33
+ *
34
+ * `clear` is where the frost starts, `full` where it and the ink are solid.
35
+ * Set `full` to the exact seam the content starts at — a ramp that runs past
36
+ * the seam puts the first row of type on half-frost.
37
+ */
38
+ type GlassStops = {
39
+ /** Which way the ink runs — toward the type. */
40
+ direction: "to bottom" | "to right";
41
+ /** Where the frost begins, e.g. `"0px"` or `"24%"`. */
42
+ clear: string;
43
+ /** Where frost and ink are solid — the seam the content starts at. Same unit as `clear`. */
44
+ full: string;
45
+ };
46
+ type GlassGroundProps = {
47
+ stops: GlassStops;
48
+ /** Stops from `lg`. Omit to keep the same ramp at every width. */
49
+ lgStops?: GlassStops;
50
+ /**
51
+ * How dark the ink gets at the far edge, 0–1. `0.96` is a caption the
52
+ * photograph is only a texture under; `0.4` is a photograph the caption is
53
+ * drawn on — the type is lifted, the room stays a room.
54
+ */
55
+ peak?: number;
56
+ /**
57
+ * How much the frost softens the photograph where it is solid. `xl` turns
58
+ * a room into colour; `sm` keeps it a room and only takes the edge off the
59
+ * detail behind the type.
60
+ */
61
+ blur?: "sm" | "md" | "lg" | "xl";
62
+ className?: string;
63
+ };
64
+ declare function GlassGround({ stops, lgStops, peak, blur, className, }: GlassGroundProps): react_jsx_runtime.JSX.Element;
65
+
66
+ export { GlassGround, type GlassGroundProps, type GlassStops };
@@ -0,0 +1,3 @@
1
+ export { GlassGround } from '../chunk-SY6RV476.js';
2
+ import '../chunk-FEK5XGZW.js';
3
+ import '../chunk-3GWWZKX7.js';
@@ -9,7 +9,17 @@ function HubSurface({ flush, className, children }) {
9
9
  className: cn(
10
10
  "rounded-[18px] border border-border bg-card shadow-none",
11
11
  flush ? "overflow-hidden" : "px-3.5 py-4",
12
- "lg:overflow-visible lg:rounded-none lg:bg-transparent lg:p-0 lg:shadow-none",
12
+ /*
13
+ * `lg:border-0` is load-bearing and was missing.
14
+ *
15
+ * Above `lg` this surface stops being a card — the background, the
16
+ * radius, the padding and the shadow are all dropped so the section
17
+ * sits on the page. The border was not, so every section on every
18
+ * desktop page kept a 1px rectangle around content that no longer had
19
+ * a card behind it. A caller that genuinely wants an edge at desktop
20
+ * adds `lg:border` in `className`, which lands after this in `cn`.
21
+ */
22
+ "lg:overflow-visible lg:rounded-none lg:border-0 lg:bg-transparent lg:p-0 lg:shadow-none",
13
23
  className
14
24
  ),
15
25
  children
@@ -0,0 +1,70 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * HueChip — a pill that wears a category's colour as a pale wash.
5
+ *
6
+ * Menu surfaces label things with a family's hue: a cuisine's dish groups, a
7
+ * going-out family's tags, a price band. Every one of them was an inline
8
+ * `style` with a hand-written `color-mix` next to a hand-written pill class
9
+ * list, which is how the two below drifted apart and how the mix space bug
10
+ * survived as long as it did — it had to be found and fixed in each copy.
11
+ *
12
+ * ## The mix space is the whole reason this is a component
13
+ *
14
+ * **`in oklab`, and never `in oklch`.** They describe the same colours, but
15
+ * oklch is POLAR, so `color-mix(in oklch, …)` interpolates the hue ANGLE — and
16
+ * the paper being mixed into is achromatic (`--card` is `oklch(1 0 0)`, hue 0).
17
+ * Mixing 20% of a family colour into that lands at a fifth of the family's hue:
18
+ * honey 82° came out at 16°, which is pink, so a Breakfast & Brunch card wore
19
+ * pink chips under an amber mosaic. Cool families were worse — a bar's 205°
20
+ * blue mixes to 41°, orange; a club's 300° violet to 60°, mustard.
21
+ *
22
+ * oklab is rectangular: mixing toward an achromatic base scales chroma along
23
+ * the SAME hue instead of rotating it, which is what "a pale version of this
24
+ * colour" has to mean.
25
+ *
26
+ * ## Why an inline style and not a Tailwind class
27
+ *
28
+ * The hue is DATA — one of ~40 cuisine values resolved by the app at request
29
+ * time — not one of a fixed set of variants. Tailwind can only emit classes it
30
+ * can see at build time, so a class-based version would mean either a
31
+ * safelisted colour per cuisine (a palette that must be edited to add a
32
+ * cuisine) or an arbitrary-value class built from a runtime string, which
33
+ * Tailwind cannot compile. A CSS custom property set inline is the honest
34
+ * mechanism; this component is the one place that writes it.
35
+ */
36
+ /** Mix strengths, so callers say what a chip IS rather than picking a number. */
37
+ declare const STRENGTH: {
38
+ /** A labelled facet: readable at 11.5px, still clearly the family's colour. */
39
+ readonly chip: 20;
40
+ /** A quieter wash, for a chip that sits beside a stronger one. */
41
+ readonly subtle: 12;
42
+ };
43
+ type HueChipTone = keyof typeof STRENGTH;
44
+ /**
45
+ * The tint itself, exported for the surfaces that need the colour rather than
46
+ * the pill — a section ground, a hover state. Same rule: `in oklab`.
47
+ */
48
+ declare function hueTint(hue: string, tone?: HueChipTone, base?: string): string;
49
+ type HueChipProps = {
50
+ /**
51
+ * A finished CSS colour — the DS owns no category table, so the app resolves
52
+ * it. Without one the chip falls back to `--muted`, which is the right
53
+ * neutral for a facet that has no family behind it.
54
+ */
55
+ hue?: string;
56
+ tone?: HueChipTone;
57
+ /** The paper the wash is mixed into. Must be the surface the chip sits on. */
58
+ base?: string;
59
+ /** Drawn before the label, e.g. a wallet on a price band. */
60
+ icon?: React.ReactNode;
61
+ /** `li` inside a list, which is where most of these live. */
62
+ as?: "span" | "li" | "div";
63
+ children: React.ReactNode;
64
+ className?: string;
65
+ /** For a chip whose visible text is a bare figure, e.g. "22–86 lei". */
66
+ "aria-label"?: string;
67
+ };
68
+ declare function HueChip({ hue, tone, base, icon, as: Tag, children, className, "aria-label": ariaLabel, }: HueChipProps): react_jsx_runtime.JSX.Element;
69
+
70
+ export { HueChip, type HueChipProps, type HueChipTone, hueTint };
@@ -0,0 +1,3 @@
1
+ export { HueChip, hueTint } from '../chunk-CLLY6XB3.js';
2
+ import '../chunk-FEK5XGZW.js';
3
+ import '../chunk-3GWWZKX7.js';
@@ -0,0 +1,88 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { MenuVenueSignature, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
4
+ import { PlacePreview, CitedText, PlaceGradeDimensions } from './place-types.js';
5
+ import { VenueGradeDimensionKey } from './venue-grade-dimensions.js';
6
+
7
+ /**
8
+ * KitchenDossier — one kitchen, told as a story on its own photograph.
9
+ *
10
+ * The hub used to say this three times: a spotlight with everything the
11
+ * digest knows, a ranked row with a grade, a card with the signature dish —
12
+ * and the same kitchen led all three. This is the one card: the venue's
13
+ * photograph clear at the top, a frosted caption rising from the foot with
14
+ * the identity (rating, name, type · area · reviews), and under it a PANEL
15
+ * that says the three or four things the reviews established one at a time —
16
+ * the dish it is known for, what people say, the grade — with segments at the
17
+ * top edge showing which is up. The story runs on its own while the card is
18
+ * in view; a tap advances.
19
+ *
20
+ * ## A photograph with a caption, not a dark card
21
+ *
22
+ * `ConceptCompare` is a dark card with a photograph showing through two glass
23
+ * bands. This is the reverse — the room is the subject and the facts are its
24
+ * caption — which is `GlassGround` at its lightest and smallest: anchored to
25
+ * the CAPTION, not the card, so it covers the identity, the panel and the
26
+ * action plus a short lead above them and nothing else; a `sm` blur that
27
+ * only takes the edge off the detail; ink that never passes 0.45; a text
28
+ * shadow under the identity doing the rest. Everything above the caption is
29
+ * the photograph, untouched. The panel is its own glass, so its rule reads
30
+ * as a panel and not a hole. The same layout at every width; the desktop
31
+ * card is wider, not turned on its side.
32
+ *
33
+ * ## Two targets, and everything else is "next"
34
+ *
35
+ * The name and the action open the venue; the dish name in the first part
36
+ * opens the item. A tap anywhere else on the card advances the story.
37
+ */
38
+ /** @deprecated Alias of `MenuLinkProps`; kept for symmetry with the other menu surfaces. */
39
+ type KitchenDossierLinkProps = MenuLinkProps;
40
+ type KitchenDossierLabels = {
41
+ /** Kickers over the three parts. */
42
+ knownFor: string;
43
+ whatPeopleSay: string;
44
+ theGrade: string;
45
+ /** Currency beside the price, e.g. "lei". */
46
+ currency: string;
47
+ /** Beside the mention count, e.g. "4 name it". */
48
+ mentions: (count: number) => string;
49
+ /** "427 reviews", composed by the caller. */
50
+ reviewCount?: string;
51
+ /** "Open until 22:00", composed by the caller. Omit when closed or unknown. */
52
+ openUntil?: string;
53
+ /** Accessible name per segment, e.g. "Part 2 of 3". */
54
+ part: (index: number, count: number) => string;
55
+ /** Accessible name of the tap-to-advance. */
56
+ next: string;
57
+ /** Dimension key → translated label. */
58
+ dimensions: Partial<Record<VenueGradeDimensionKey, string>>;
59
+ };
60
+ type KitchenDossierProps = {
61
+ place: PlacePreview;
62
+ /** The venue's destination. */
63
+ href: string;
64
+ /** "Restaurant · Centru", composed by the caller. */
65
+ meta?: string;
66
+ /** Part one. */
67
+ signature?: MenuVenueSignature;
68
+ /** Part two — the digest's highlights; two are shown. */
69
+ quotes?: CitedText[];
70
+ /** Part three — the grade, with the overall already formatted. */
71
+ grade?: {
72
+ overall?: string;
73
+ dimensions?: PlaceGradeDimensions;
74
+ };
75
+ labels: KitchenDossierLabels;
76
+ formatPrice: (amount: number) => string;
77
+ /** 0–100 → display numeral. */
78
+ formatScore: (value: number) => string;
79
+ /** How long each part shows. */
80
+ partDurationMs?: number;
81
+ /** The foot's action — pass a rendered link or button. */
82
+ action?: ReactNode;
83
+ linkComponent?: MenuLinkComponent;
84
+ className?: string;
85
+ };
86
+ declare function KitchenDossier({ place, href, meta, signature, quotes, grade, labels, formatPrice, formatScore, partDurationMs, action, linkComponent: LinkComponent, className, }: KitchenDossierProps): react_jsx_runtime.JSX.Element;
87
+
88
+ export { KitchenDossier, type KitchenDossierLabels, type KitchenDossierLinkProps, type KitchenDossierProps };
@@ -0,0 +1,187 @@
1
+ import { DIMENSION_ORDER, VenueGradeDimensions } from '../chunk-AKQ57NXY.js';
2
+ import { VenueQuotes } from '../chunk-62ES6N32.js';
3
+ import { StoryProvider, StorySegments, StoryPanel } from '../chunk-F6FMZJDG.js';
4
+ import { GlassGround } from '../chunk-SY6RV476.js';
5
+ import { Separator } from '../chunk-WSRWFA6K.js';
6
+ import { PhotoGround, PHOTO_GROUND } from '../chunk-5RWTOT5Y.js';
7
+ import { cn } from '../chunk-FEK5XGZW.js';
8
+ import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
9
+ import { MessageSquareQuote, Star } from 'lucide-react';
10
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
11
+
12
+ function DefaultLink(_a) {
13
+ var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
14
+ return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
15
+ }
16
+ var GLASS_LEAD = "128px";
17
+ var DIMENSION_LIMIT = 3;
18
+ var QUOTE_LIMIT = 2;
19
+ var initialOf = (name) => {
20
+ var _a, _b;
21
+ return ((_b = (_a = name.match(new RegExp("\\p{L}", "u"))) == null ? void 0 : _a[0]) != null ? _b : "").toLocaleUpperCase();
22
+ };
23
+ function KitchenDossier({
24
+ place,
25
+ href,
26
+ meta,
27
+ signature,
28
+ quotes,
29
+ grade,
30
+ labels,
31
+ formatPrice,
32
+ formatScore,
33
+ partDurationMs,
34
+ action,
35
+ linkComponent: LinkComponent = DefaultLink,
36
+ className
37
+ }) {
38
+ var _a, _b, _c, _d;
39
+ const rating = (_a = place.rating) == null ? void 0 : _a.rating;
40
+ const parts = [];
41
+ if (signature) {
42
+ const item = signature.item;
43
+ const firstEntry = (_b = item.sizes_and_prices) == null ? void 0 : _b[0];
44
+ const mentions = (_d = (_c = item.mentions) != null ? _c : item.review_count) != null ? _d : 0;
45
+ parts.push(
46
+ /* @__PURE__ */ jsxs(Part, { kicker: labels.knownFor, children: [
47
+ signature.href ? /* @__PURE__ */ jsx(
48
+ LinkComponent,
49
+ {
50
+ href: signature.href,
51
+ className: "w-max max-w-full text-[16px] font-bold leading-tight tracking-[-0.01em] text-white hover:underline focus-visible:outline-none focus-visible:underline",
52
+ children: item.name
53
+ }
54
+ ) : /* @__PURE__ */ jsx("span", { className: "text-[16px] font-bold leading-tight tracking-[-0.01em] text-white", children: item.name }),
55
+ signature.why && /* @__PURE__ */ jsx("p", { className: "mt-1 line-clamp-2 font-serif text-[13px] italic leading-snug text-white/75", children: signature.why }),
56
+ (firstEntry || mentions > 0) && /* @__PURE__ */ jsxs(Fragment, { children: [
57
+ /* @__PURE__ */ jsx(Separator, { className: "my-2.5 via-white/20" }),
58
+ /* @__PURE__ */ jsxs("div", { className: "flex items-end justify-between gap-3", children: [
59
+ /* @__PURE__ */ jsx("span", { className: "flex items-baseline gap-1.5", children: firstEntry && /* @__PURE__ */ jsxs(Fragment, { children: [
60
+ /* @__PURE__ */ jsxs("span", { className: "text-[18px] font-extrabold leading-none tabular-nums text-white", children: [
61
+ formatPrice(firstEntry.price),
62
+ /* @__PURE__ */ jsx("span", { className: "ml-1 text-[12px] font-bold", children: labels.currency })
63
+ ] }),
64
+ firstEntry.size > 0 && /* @__PURE__ */ jsxs("span", { className: "text-[11.5px] tabular-nums text-white/60", children: [
65
+ firstEntry.size,
66
+ item.kind === "dish" ? "g" : "ml"
67
+ ] })
68
+ ] }) }),
69
+ mentions > 0 && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-[12px] font-semibold tabular-nums text-white/85", children: [
70
+ /* @__PURE__ */ jsx(MessageSquareQuote, { className: "h-3.5 w-3.5", "aria-hidden": true }),
71
+ labels.mentions(mentions)
72
+ ] })
73
+ ] })
74
+ ] })
75
+ ] }, "signature")
76
+ );
77
+ }
78
+ if (quotes && quotes.length > 0) {
79
+ parts.push(
80
+ /* @__PURE__ */ jsx(Part, { kicker: labels.whatPeopleSay, children: /* @__PURE__ */ jsx(VenueQuotes, { quotes, limit: QUOTE_LIMIT, className: "lg:grid-cols-1" }) }, "quotes")
81
+ );
82
+ }
83
+ const dimensions = grade == null ? void 0 : grade.dimensions;
84
+ const strongest = dimensions ? DIMENSION_ORDER.filter((key) => typeof dimensions[key] === "number").sort((a, b) => {
85
+ var _a2, _b2;
86
+ return ((_a2 = dimensions[b]) != null ? _a2 : 0) - ((_b2 = dimensions[a]) != null ? _b2 : 0);
87
+ }).slice(0, DIMENSION_LIMIT) : [];
88
+ if (grade && (grade.overall || strongest.length > 0)) {
89
+ parts.push(
90
+ /* @__PURE__ */ jsx(Part, { kicker: labels.theGrade, children: /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-4", children: [
91
+ grade.overall && /* @__PURE__ */ jsx("span", { className: "text-[34px] font-extrabold leading-none tracking-[-0.03em] tabular-nums text-[oklch(0.82_0.15_85)]", children: grade.overall }),
92
+ dimensions && strongest.length > 0 && /* @__PURE__ */ jsx(
93
+ VenueGradeDimensions,
94
+ {
95
+ className: "min-w-0 flex-1",
96
+ dimensions,
97
+ labels: labels.dimensions,
98
+ order: strongest,
99
+ formatScore
100
+ }
101
+ )
102
+ ] }) }, "grade")
103
+ );
104
+ }
105
+ return /* @__PURE__ */ jsxs(
106
+ "article",
107
+ {
108
+ className: cn(
109
+ /* `flex flex-col` so the story fills the card's height: in a rail
110
+ the cards are stretched to the tallest, and a story that stopped
111
+ at its own min-height left a band of bare photograph under the
112
+ action on every shorter card. */
113
+ "dark relative isolate flex flex-col overflow-hidden rounded-[18px] border border-border text-foreground shadow-[var(--shadow-card)]",
114
+ PHOTO_GROUND,
115
+ className
116
+ ),
117
+ children: [
118
+ /* @__PURE__ */ jsx(PhotoGround, { className: "-z-20", photos: [place.thumbnail_url], initials: initialOf(place.name), loading: "eager" }),
119
+ /* @__PURE__ */ jsxs(
120
+ StoryProvider,
121
+ {
122
+ count: parts.length,
123
+ durationMs: partDurationMs,
124
+ nextLabel: labels.next,
125
+ className: "relative flex min-h-[560px] flex-1 flex-col p-4 lg:min-h-[440px] lg:p-5",
126
+ children: [
127
+ /* @__PURE__ */ jsx(
128
+ StorySegments,
129
+ {
130
+ labels: parts.map((_, index) => labels.part(index, parts.length)),
131
+ className: "-mt-1"
132
+ }
133
+ ),
134
+ /* @__PURE__ */ jsx("div", { className: "relative min-h-[184px] flex-1", children: labels.openUntil && /* @__PURE__ */ jsxs("span", { className: "absolute left-0 top-3 inline-flex h-[30px] items-center gap-[7px] rounded-full bg-[oklch(0.16_0.012_55_/_0.72)] px-3 text-[11.5px] font-bold text-white backdrop-blur-[2px]", children: [
135
+ /* @__PURE__ */ jsx("i", { className: "h-[7px] w-[7px] rounded-full bg-success", "aria-hidden": true }),
136
+ labels.openUntil
137
+ ] }) }),
138
+ /* @__PURE__ */ jsxs("div", { className: "relative flex flex-col gap-3.5", children: [
139
+ /* @__PURE__ */ jsx(
140
+ "div",
141
+ {
142
+ "aria-hidden": true,
143
+ className: "pointer-events-none absolute -inset-x-4 -bottom-4 -top-32 -z-10 lg:-inset-x-5 lg:-bottom-5",
144
+ children: /* @__PURE__ */ jsx(
145
+ GlassGround,
146
+ {
147
+ stops: { direction: "to bottom", clear: "0px", full: GLASS_LEAD },
148
+ peak: 0.4,
149
+ blur: "sm"
150
+ }
151
+ )
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsxs("div", { className: "[text-shadow:0_1px_12px_oklch(0_0_0/0.55)]", children: [
155
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-4", children: [
156
+ /* @__PURE__ */ jsx("h3", { className: "min-w-0 font-serif text-[26px] font-semibold leading-tight tracking-[-0.02em] text-white lg:text-[32px]", children: /* @__PURE__ */ jsx(LinkComponent, { href, className: "focus-visible:outline-none focus-visible:underline", children: place.name }) }),
157
+ rating !== void 0 && /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 text-[18px] font-extrabold leading-none tabular-nums text-white lg:text-[22px]", children: [
158
+ rating.toFixed(1),
159
+ /* @__PURE__ */ jsx(Star, { className: "h-3.5 w-3.5 lg:h-4 lg:w-4", "aria-hidden": true })
160
+ ] })
161
+ ] }),
162
+ (meta || labels.reviewCount) && /* @__PURE__ */ jsx("p", { className: "mt-1 truncate text-[12.5px] text-white/70", children: [meta, labels.reviewCount].filter(Boolean).join(" \xB7 ") })
163
+ ] }),
164
+ parts.length > 0 && /* @__PURE__ */ jsx(
165
+ StoryPanel,
166
+ {
167
+ parts,
168
+ className: "rounded-[14px] border border-white/10 bg-[oklch(0.16_0.012_55_/_0.35)] p-4 backdrop-blur-md"
169
+ }
170
+ ),
171
+ action && /* @__PURE__ */ jsx("div", { className: "flex [&>*]:w-full lg:[&>*]:w-auto", children: action })
172
+ ] })
173
+ ]
174
+ }
175
+ )
176
+ ]
177
+ }
178
+ );
179
+ }
180
+ function Part({ kicker, children }) {
181
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
182
+ /* @__PURE__ */ jsx("span", { className: "mb-1.5 block text-[9.5px] font-bold uppercase tracking-[0.12em] text-white/60", children: kicker }),
183
+ children
184
+ ] });
185
+ }
186
+
187
+ export { KitchenDossier };
@@ -0,0 +1,56 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { MenuLinkComponent, MenuLoosenOption } from './menu-types.js';
3
+ import 'react';
4
+ import './place-types.js';
5
+
6
+ /**
7
+ * MenuDeadEnd — what dropped out, and the one press that brings it back.
8
+ *
9
+ * A filter combination that returns nothing is the intent page's worst state,
10
+ * and an empty-state illustration is the worst answer to it: the reader knows
11
+ * the list is empty, what they do not know is WHICH of their four filters did
12
+ * it. So this says what dropped out, why, and what widening one specific filter
13
+ * would restore — with the count, because "+5 kitchens, including the
14
+ * highest-rated Italian room in the city" is an answer and "try removing a
15
+ * filter" is a shrug.
16
+ *
17
+ * It is also drawn on a NON-empty list, whenever a filter has cost the reader
18
+ * something notable. The board's case is 34 kitchens still standing and Casa
19
+ * Boema — the one they came for — missing.
20
+ *
21
+ * Interactive: the consumer marks its own module `"use client"`.
22
+ */
23
+ type MenuDeadEndProps = {
24
+ /**
25
+ * The explanation, composed by the caller.
26
+ *
27
+ * A node rather than a string because it emphasises real names ("**Casa
28
+ * Boema** and six others dropped out"), and assembling that from fragments
29
+ * here would produce translated word-salad in one of the two locales.
30
+ */
31
+ children: React.ReactNode;
32
+ /** The widening this offers, e.g. "Raise to 60 lei · 16 kitchens". */
33
+ actionLabel?: string;
34
+ /** Where the widening leads. Preferred over `onAction` — see `MenuLoosenOption.href`. */
35
+ actionHref?: string;
36
+ onAction?: () => void;
37
+ linkComponent?: MenuLinkComponent;
38
+ className?: string;
39
+ };
40
+ declare function MenuDeadEnd({ children, actionLabel, actionHref, onAction, linkComponent: LinkComponent, className, }: MenuDeadEndProps): react_jsx_runtime.JSX.Element;
41
+ /**
42
+ * MenuLoosenList — one row per active filter, each priced in kitchens.
43
+ *
44
+ * The companion to the panel above: the panel names the single best widening,
45
+ * this lists every one of them so the reader can choose which constraint they
46
+ * care least about. Every row states what it would return, because a filter the
47
+ * reader cannot price is a filter they will not drop.
48
+ */
49
+ type MenuLoosenListProps = {
50
+ options: MenuLoosenOption[];
51
+ linkComponent?: MenuLinkComponent;
52
+ className?: string;
53
+ };
54
+ declare function MenuLoosenList({ options, linkComponent: LinkComponent, className, }: MenuLoosenListProps): react_jsx_runtime.JSX.Element;
55
+
56
+ export { MenuDeadEnd, type MenuDeadEndProps, MenuLoosenList, type MenuLoosenListProps };
@@ -0,0 +1,49 @@
1
+ import { cn } from '../chunk-FEK5XGZW.js';
2
+ import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
3
+ import { Info } from 'lucide-react';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+
6
+ function DefaultLink(_a) {
7
+ var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
8
+ return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
9
+ }
10
+ function MenuDeadEnd({
11
+ children,
12
+ actionLabel,
13
+ actionHref,
14
+ onAction,
15
+ linkComponent: LinkComponent = DefaultLink,
16
+ className
17
+ }) {
18
+ const actionClass = "inline-flex h-7 shrink-0 items-center rounded-full bg-foreground px-3 text-[12.5px] font-bold text-background transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 motion-reduce:transition-none lg:ml-auto";
19
+ return /* @__PURE__ */ jsxs(
20
+ "div",
21
+ {
22
+ className: cn(
23
+ "mt-4 flex flex-wrap items-center gap-3 rounded-[14px] bg-muted p-3.5 lg:flex-nowrap lg:gap-4 lg:px-[18px] lg:py-4",
24
+ className
25
+ ),
26
+ children: [
27
+ /* @__PURE__ */ jsx(Info, { className: "h-[18px] w-[18px] shrink-0 text-muted-foreground", "aria-hidden": true }),
28
+ /* @__PURE__ */ jsx("p", { className: "max-w-[560px] text-[13px] leading-[1.55] text-muted-foreground [&_b]:font-bold [&_b]:text-foreground", children }),
29
+ actionLabel && (actionHref ? /* @__PURE__ */ jsx(LinkComponent, { href: actionHref, className: actionClass, children: actionLabel }) : onAction && /* @__PURE__ */ jsx("button", { type: "button", onClick: onAction, className: actionClass, children: actionLabel }))
30
+ ]
31
+ }
32
+ );
33
+ }
34
+ function MenuLoosenList({
35
+ options,
36
+ linkComponent: LinkComponent = DefaultLink,
37
+ className
38
+ }) {
39
+ const rowClass = "inline-flex h-9 items-center justify-between gap-3 rounded-full border border-border bg-card px-4 text-sm font-semibold transition-colors hover:border-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none";
40
+ return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col gap-2.5", className), children: options.map((option) => {
41
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
42
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: option.label }),
43
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 font-bold tabular-nums text-accent", children: option.gainLabel })
44
+ ] });
45
+ return option.href ? /* @__PURE__ */ jsx(LinkComponent, { href: option.href, className: rowClass, children: body }, option.key) : /* @__PURE__ */ jsx("button", { type: "button", onClick: option.onSelect, className: rowClass, children: body }, option.key);
46
+ }) });
47
+ }
48
+
49
+ export { MenuDeadEnd, MenuLoosenList };
@@ -0,0 +1,99 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { MenuKitchenListLabels } from './menu-kitchen-list.js';
3
+ import { MenuFamilyBlockPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
4
+ import 'react';
5
+ import './place-types.js';
6
+
7
+ /**
8
+ * MenuFamilyBlock — the cuisines index's family card. The mosaic is the ground.
9
+ *
10
+ * ## Three layers, and everything floats on them
11
+ *
12
+ * The grid of venue photographs is the background for the ENTIRE card, edge to
13
+ * edge, and the facts sit on it. Three layers do the work:
14
+ *
15
+ * 1. **The mosaic**, full-bleed, wearing the family's hue through a
16
+ * `mix-blend-color` layer.
17
+ * 2. **A masked backdrop-filter** — absent where the photography is the
18
+ * subject, full where the rows begin — so the pictures stay sharp where
19
+ * they are looked at and go soft exactly where they are read over.
20
+ * 3. **An ink gradient** over that. This is what actually buys the contrast:
21
+ * frost alone blurs without darkening, and blurred mid-tones will not carry
22
+ * 16px type.
23
+ *
24
+ * On the phone the frost and ink run top to bottom: the name and the action
25
+ * sit on clear photography, the chips and the rows on the frosted foot. On
26
+ * desktop the same three layers turn ninety degrees — the photo column on the
27
+ * left stays clear, the row column on the right is the frosted one. The mosaic
28
+ * runs 3×2 there rather than 2×2, because at 1110px a four-tile grid makes each
29
+ * photograph too big to read as texture.
30
+ *
31
+ * ## The dark scope
32
+ *
33
+ * The card's content region carries the `dark` class, so every token inside —
34
+ * `text-foreground`, `bg-muted`, `border-border` — resolves to its dark-theme
35
+ * value. That is how `MenuKitchenList` and `HueChip` render correctly on the
36
+ * ink without a fork or a "tone" prop: the mechanism the app already uses to
37
+ * mark a region as dark. It also means the card looks the same in both themes,
38
+ * which is right — it is built on photography, and photography is not themed.
39
+ *
40
+ * ## The two costs, stated
41
+ *
42
+ * A `backdrop-filter` over a mask is expensive to composite, and this card is
43
+ * repeated ten times down a scroll. And the thumbnails now sit on a
44
+ * photograph; they survive only because the ink is heavy under them, and on a
45
+ * busy interior shot they will fight. Both were accepted with the design.
46
+ *
47
+ * ## No "most named" item, no venue count badge
48
+ *
49
+ * The family-level "most named" was the first venue's signature relabelled —
50
+ * Italian, Pizza and Mediterranean all claimed "Turkish Poached Eggs" — and
51
+ * the count used to print three times with two values. One count, under the
52
+ * name; the overflow tile says only how many photographs it stands for.
53
+ */
54
+ /** @deprecated Alias of `MenuLinkProps`; kept for symmetry with the other menu surfaces. */
55
+ type MenuFamilyBlockLinkProps = MenuLinkProps;
56
+ type MenuFamilyBlockLabels = {
57
+ /** Unit under the family name, e.g. "247 kitchens". Pluralised by the caller. */
58
+ venueUnit: (count: number) => string;
59
+ /** The overflow pill over the mosaic, e.g. "+311 more". */
60
+ more: (count: number) => string;
61
+ /**
62
+ * Price-band caption, e.g. "kitchen price band".
63
+ *
64
+ * Never drawn: the band reads as a wallet and a range, and a caption on a
65
+ * pill that short is longer than the fact. It names the pill for a screen
66
+ * reader, which would otherwise be read two numbers and a currency.
67
+ */
68
+ priceBand: string;
69
+ /** Main action, e.g. "Browse Italian". Sits on the photography — keep it short. */
70
+ browse: string;
71
+ /** Passed through to the venue rows. */
72
+ kitchens: MenuKitchenListLabels;
73
+ };
74
+ type MenuFamilyBlockProps = {
75
+ family: MenuFamilyBlockPreview;
76
+ labels: MenuFamilyBlockLabels;
77
+ formatCount?: (value: number) => string;
78
+ /** Finished CSS colour for the chips. */
79
+ hue?: string;
80
+ /** Finished CSS gradient — the family's colour over the mosaic. */
81
+ gradient?: string;
82
+ linkComponent?: MenuLinkComponent;
83
+ className?: string;
84
+ };
85
+ declare function MenuFamilyBlock({ family, labels, formatCount, hue, gradient, linkComponent: LinkComponent, className, }: MenuFamilyBlockProps): react_jsx_runtime.JSX.Element;
86
+ /**
87
+ * The block's own loading state, at the block's footprint.
88
+ *
89
+ * The ground is the card's own, so the placeholder is already the dark
90
+ * surface the photographs will land on — nothing flashes from paper to ink.
91
+ * `venues` matches the rows the caller will render, so a block showing three
92
+ * does not settle from a placeholder showing one.
93
+ */
94
+ declare function MenuFamilyBlockSkeleton({ venues, className, }: {
95
+ venues?: number;
96
+ className?: string;
97
+ }): react_jsx_runtime.JSX.Element;
98
+
99
+ export { MenuFamilyBlock, type MenuFamilyBlockLabels, type MenuFamilyBlockLinkProps, type MenuFamilyBlockProps, MenuFamilyBlockSkeleton };