@bearmenu/ui 0.8.9 → 0.8.11
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.
- package/dist/chunk-24RFI4RQ.js +167 -0
- package/dist/{chunk-UKBNQO6Q.js → chunk-2CXU5WZO.js} +1 -1
- package/dist/chunk-2XCZJWTV.js +121 -0
- package/dist/{chunk-62ES6N32.js → chunk-43RC7MHY.js} +1 -1
- package/dist/chunk-5RWTOT5Y.js +72 -0
- package/dist/{chunk-Q6QH52LJ.js → chunk-6TEEY5YA.js} +2 -2
- package/dist/{chunk-AXV5O2IZ.js → chunk-AKQ57NXY.js} +3 -3
- package/dist/chunk-CLLY6XB3.js +41 -0
- package/dist/chunk-DTXNDLSE.js +125 -0
- package/dist/chunk-F6FMZJDG.js +113 -0
- package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
- package/dist/chunk-H3ZZDX5L.js +61 -0
- package/dist/{chunk-H3BV4VAG.js → chunk-JS3EINZD.js} +2 -2
- package/dist/chunk-MZL7CGCL.js +109 -0
- package/dist/{chunk-TH6W2PWG.js → chunk-UDTG3RSE.js} +1 -1
- package/dist/chunk-WCTHZKU7.js +37 -0
- package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/avatar.d.ts +3 -3
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +1 -1
- package/dist/components/card.d.ts +4 -4
- package/dist/components/carousel.js +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/command.d.ts +2 -2
- package/dist/components/concept-compare-rail.d.ts +50 -0
- package/dist/components/concept-compare-rail.js +5 -0
- package/dist/components/concept-compare.d.ts +75 -38
- package/dist/components/concept-compare.js +209 -133
- package/dist/components/concept-convergence.d.ts +41 -0
- package/dist/components/concept-convergence.js +53 -0
- package/dist/components/convergence.d.ts +25 -0
- package/dist/components/convergence.js +4 -0
- package/dist/components/dialog.d.ts +2 -2
- package/dist/components/empty-state.js +1 -1
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.js +2 -2
- package/dist/components/event-index-row.js +1 -1
- package/dist/components/event-list-row-wide.js +2 -2
- package/dist/components/event-list-row.js +2 -2
- package/dist/components/explainer-steps.d.ts +34 -0
- package/dist/components/explainer-steps.js +36 -0
- package/dist/components/filter-category-row.js +1 -1
- package/dist/components/glass-ground.d.ts +61 -0
- package/dist/components/glass-ground.js +3 -0
- package/dist/components/haptic-button.js +1 -1
- package/dist/components/hub-surface.js +11 -1
- package/dist/components/hue-chip.d.ts +70 -0
- package/dist/components/hue-chip.js +3 -0
- package/dist/components/input-group.d.ts +2 -2
- package/dist/components/input.d.ts +1 -1
- package/dist/components/item.d.ts +4 -4
- package/dist/components/kbd.d.ts +2 -2
- package/dist/components/kitchen-dossier.d.ts +88 -0
- package/dist/components/kitchen-dossier.js +186 -0
- package/dist/components/link-button.js +1 -1
- package/dist/components/menu-dead-end.d.ts +56 -0
- package/dist/components/menu-dead-end.js +49 -0
- package/dist/components/menu-family-block.d.ts +98 -0
- package/dist/components/menu-family-block.js +153 -0
- package/dist/components/menu-family-card.d.ts +33 -27
- package/dist/components/menu-family-card.js +41 -85
- package/dist/components/menu-field.js +92 -2
- package/dist/components/menu-filter-chips.d.ts +86 -0
- package/dist/components/menu-filter-chips.js +122 -0
- package/dist/components/menu-group-rows.d.ts +53 -0
- package/dist/components/menu-group-rows.js +67 -0
- package/dist/components/menu-hub-hero.js +10 -5
- package/dist/components/menu-index-header.d.ts +37 -0
- package/dist/components/menu-index-header.js +68 -0
- package/dist/components/menu-index-list.d.ts +48 -0
- package/dist/components/menu-index-list.js +96 -0
- package/dist/components/menu-intent-hero.d.ts +69 -0
- package/dist/components/menu-intent-hero.js +87 -0
- package/dist/components/menu-item.js +3 -3
- package/dist/components/menu-kitchen-list.d.ts +82 -0
- package/dist/components/menu-kitchen-list.js +5 -0
- package/dist/components/menu-mocks.d.ts +37 -2
- package/dist/components/menu-mocks.js +152 -27
- package/dist/components/menu-types.d.ts +182 -6
- package/dist/components/motion-icon-controls.js +2 -2
- package/dist/components/motion-icon-set.js +6 -6
- package/dist/components/motion-icon.d.ts +22 -1
- package/dist/components/motion-icon.js +1 -1
- package/dist/components/multi-select-combobox.js +2 -2
- package/dist/components/native-select.d.ts +2 -2
- package/dist/components/pagination.js +1 -1
- package/dist/components/photo-ground.d.ts +64 -0
- package/dist/components/photo-ground.js +3 -0
- package/dist/components/place-about-tab.js +2 -2
- package/dist/components/place-details-mobile.js +3 -3
- package/dist/components/place-types.d.ts +2 -2
- package/dist/components/qa-block.d.ts +36 -0
- package/dist/components/qa-block.js +24 -0
- package/dist/components/scroll-fade.js +3 -121
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/select.d.ts +1 -1
- package/dist/components/signature-chip.d.ts +2 -2
- package/dist/components/signature-chip.js +2 -2
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/snap-rail.d.ts +26 -3
- package/dist/components/snap-rail.js +3 -109
- package/dist/components/spinner.d.ts +4 -4
- package/dist/components/story.d.ts +34 -0
- package/dist/components/story.js +3 -0
- package/dist/components/tag.d.ts +4 -4
- package/dist/components/text.d.ts +5 -5
- package/dist/components/toggle.d.ts +1 -1
- package/dist/components/top-comparison.d.ts +28 -0
- package/dist/components/top-comparison.js +76 -0
- package/dist/components/top-cross-link.d.ts +34 -0
- package/dist/components/top-cross-link.js +101 -0
- package/dist/components/top-ranking-entry.d.ts +53 -0
- package/dist/components/top-ranking-entry.js +217 -0
- package/dist/components/top-ranking-hero.d.ts +61 -0
- package/dist/components/top-ranking-hero.js +99 -0
- package/dist/components/top-ranking-legend.d.ts +69 -0
- package/dist/components/top-ranking-legend.js +124 -0
- package/dist/components/top-types.d.ts +109 -0
- package/dist/components/top-types.js +1 -0
- package/dist/components/venue-grade-dimensions.d.ts +3 -1
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/venue-quotes.d.ts +1 -1
- package/dist/components/venue-quotes.js +1 -1
- package/dist/components/venue-wall.js +5 -3
- package/dist/components/weekday-theme-banner.js +1 -1
- package/dist/components/weekend-panel.js +2 -2
- package/package.json +1 -1
- package/src/components/badge.tsx +1 -1
- package/src/components/button.tsx +2 -2
- package/src/components/concept-compare-rail.tsx +132 -0
- package/src/components/concept-compare.stories.tsx +122 -54
- package/src/components/concept-compare.tsx +368 -196
- package/src/components/concept-convergence.stories.tsx +53 -0
- package/src/components/concept-convergence.tsx +90 -0
- package/src/components/convergence.stories.tsx +54 -0
- package/src/components/convergence.tsx +177 -0
- package/src/components/explainer-steps.stories.tsx +46 -0
- package/src/components/explainer-steps.tsx +74 -0
- package/src/components/glass-ground.tsx +104 -0
- package/src/components/hub-surface.tsx +11 -1
- package/src/components/hue-chip.stories.tsx +76 -0
- package/src/components/hue-chip.test.tsx +62 -0
- package/src/components/hue-chip.tsx +99 -0
- package/src/components/kitchen-dossier.stories.tsx +108 -0
- package/src/components/kitchen-dossier.tsx +321 -0
- package/src/components/menu-dead-end.stories.tsx +67 -0
- package/src/components/menu-dead-end.tsx +127 -0
- package/src/components/menu-family-block.stories.tsx +91 -0
- package/src/components/menu-family-block.tsx +303 -0
- package/src/components/menu-family-card.stories.tsx +43 -35
- package/src/components/menu-family-card.tsx +115 -142
- package/src/components/menu-filter-chips.stories.tsx +107 -0
- package/src/components/menu-filter-chips.tsx +238 -0
- package/src/components/menu-group-rows.stories.tsx +54 -0
- package/src/components/menu-group-rows.tsx +133 -0
- package/src/components/menu-hub-hero.test.tsx +39 -0
- package/src/components/menu-hub-hero.tsx +17 -5
- package/src/components/menu-index-header.stories.tsx +72 -0
- package/src/components/menu-index-header.tsx +125 -0
- package/src/components/menu-index-list.stories.tsx +50 -0
- package/src/components/menu-index-list.tsx +149 -0
- package/src/components/menu-intent-hero.stories.tsx +67 -0
- package/src/components/menu-intent-hero.tsx +189 -0
- package/src/components/menu-kitchen-list.stories.tsx +67 -0
- package/src/components/menu-kitchen-list.test.tsx +78 -0
- package/src/components/menu-kitchen-list.tsx +284 -0
- package/src/components/menu-mocks.ts +198 -23
- package/src/components/menu-types.ts +189 -6
- package/src/components/motion-icon-controls.tsx +11 -11
- package/src/components/motion-icon-set.tsx +6 -5
- package/src/components/motion-icon.test.tsx +23 -1
- package/src/components/motion-icon.tsx +20 -0
- package/src/components/photo-ground.tsx +147 -0
- package/src/components/place-types.ts +2 -2
- package/src/components/qa-block.stories.tsx +43 -0
- package/src/components/qa-block.tsx +60 -0
- package/src/components/signature-chip.tsx +3 -3
- package/src/components/snap-rail.tsx +58 -15
- package/src/components/story.tsx +225 -0
- package/src/components/top-comparison.stories.tsx +78 -0
- package/src/components/top-comparison.tsx +151 -0
- package/src/components/top-cross-link.stories.tsx +84 -0
- package/src/components/top-cross-link.tsx +171 -0
- package/src/components/top-ranking-entry.stories.tsx +171 -0
- package/src/components/top-ranking-entry.test.tsx +105 -0
- package/src/components/top-ranking-entry.tsx +425 -0
- package/src/components/top-ranking-hero.stories.tsx +76 -0
- package/src/components/top-ranking-hero.tsx +218 -0
- package/src/components/top-ranking-legend.stories.tsx +78 -0
- package/src/components/top-ranking-legend.tsx +239 -0
- package/src/components/top-types.ts +110 -0
- package/src/components/venue-grade-dimensions.tsx +2 -2
- package/src/components/venue-quotes.stories.tsx +1 -1
- package/src/components/venue-quotes.tsx +4 -2
- package/src/components/venue-wall.tsx +9 -4
- package/src/components/weekday-theme-banner.tsx +1 -1
- package/dist/chunk-THOVPZNN.js +0 -37
- package/dist/chunk-YS6BHKZO.js +0 -92
- package/dist/components/ranked-venue-list.d.ts +0 -49
- package/dist/components/ranked-venue-list.js +0 -87
- package/dist/components/signature-line-list.d.ts +0 -45
- package/dist/components/signature-line-list.js +0 -99
- package/dist/components/venue-busyness-strip.d.ts +0 -35
- package/dist/components/venue-busyness-strip.js +0 -37
- package/dist/components/venue-signature-block.d.ts +0 -39
- package/dist/components/venue-signature-block.js +0 -6
- package/dist/components/venue-spotlight.d.ts +0 -74
- package/dist/components/venue-spotlight.js +0 -138
- package/src/components/ranked-venue-list.stories.tsx +0 -80
- package/src/components/ranked-venue-list.tsx +0 -177
- package/src/components/signature-line-list.stories.tsx +0 -85
- package/src/components/signature-line-list.tsx +0 -136
- package/src/components/venue-busyness-strip.stories.tsx +0 -66
- package/src/components/venue-busyness-strip.tsx +0 -64
- package/src/components/venue-signature-block.stories.tsx +0 -68
- package/src/components/venue-signature-block.tsx +0 -71
- package/src/components/venue-spotlight.stories.tsx +0 -141
- package/src/components/venue-spotlight.tsx +0 -236
- package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
+
import '../chunk-3GWWZKX7.js';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
var ACTIVE_BAND = "-20% 0px -75% 0px";
|
|
7
|
+
function TopRankingLegend({ groups, heading, meta, className }) {
|
|
8
|
+
const entries = groups.flatMap((group) => group.entries);
|
|
9
|
+
const [activeSlug, setActiveSlug] = useState(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (typeof IntersectionObserver === "undefined") return void 0;
|
|
12
|
+
const nodes = entries.map((entry) => document.getElementById(entry.slug)).filter((node) => Boolean(node));
|
|
13
|
+
if (nodes.length === 0) return void 0;
|
|
14
|
+
const observer = new IntersectionObserver(
|
|
15
|
+
(observed) => {
|
|
16
|
+
const inBand = observed.filter((record) => record.isIntersecting);
|
|
17
|
+
if (inBand.length === 0) return;
|
|
18
|
+
const topmost = inBand.reduce(
|
|
19
|
+
(best, record) => record.boundingClientRect.top < best.boundingClientRect.top ? record : best
|
|
20
|
+
);
|
|
21
|
+
setActiveSlug(topmost.target.id);
|
|
22
|
+
},
|
|
23
|
+
{ rootMargin: ACTIVE_BAND }
|
|
24
|
+
);
|
|
25
|
+
nodes.forEach((node) => observer.observe(node));
|
|
26
|
+
return () => observer.disconnect();
|
|
27
|
+
}, [entries.map((entry) => entry.slug).join("|")]);
|
|
28
|
+
return (
|
|
29
|
+
/*
|
|
30
|
+
* The height cap is a safety net, not the navigation model.
|
|
31
|
+
*
|
|
32
|
+
* `position: sticky` on an element TALLER than the viewport leaves its
|
|
33
|
+
* bottom permanently unreachable — at twenty-five rows on a 900px laptop
|
|
34
|
+
* you could never see ranks 20–25, however you scrolled. That is broken
|
|
35
|
+
* rather than merely tight, so the column scrolls internally once it
|
|
36
|
+
* exceeds the space available.
|
|
37
|
+
*
|
|
38
|
+
* It only ever engages on a viewport too short to fit the list: `max-height`
|
|
39
|
+
* with `overflow-y-auto` is inert when the content already fits, so nothing
|
|
40
|
+
* changes on a tall screen. `overscroll-contain` stops a flick inside the
|
|
41
|
+
* column from carrying on into the article behind it.
|
|
42
|
+
*/
|
|
43
|
+
/* @__PURE__ */ jsxs(
|
|
44
|
+
"nav",
|
|
45
|
+
{
|
|
46
|
+
"aria-label": heading,
|
|
47
|
+
className: cn(
|
|
48
|
+
/*
|
|
49
|
+
* Sticky at BOTH widths now, with a different offset and a different
|
|
50
|
+
* shape at each.
|
|
51
|
+
*
|
|
52
|
+
* On desktop it is the grouped column, pinned beside the article. On a
|
|
53
|
+
* phone it is the flat chip rail, pinned for the same reason the column
|
|
54
|
+
* is: this page is twenty-five long entries, so a reader at rank 14 is
|
|
55
|
+
* several screens from any means of jumping, and the rail scrolled away
|
|
56
|
+
* inside the first entry.
|
|
57
|
+
*
|
|
58
|
+
* The phone offset comes from `--legend-sticky-top`, which the CALLER
|
|
59
|
+
* sets, because it is that app's chrome — the consumer app pins under a
|
|
60
|
+
* fixed header at `calc(var(--header-offset) + env(safe-area-inset-top))`,
|
|
61
|
+
* exactly as its events day rail does. Reading the app's own
|
|
62
|
+
* `--header-offset` here would resolve to nothing in Storybook and in
|
|
63
|
+
* the other three consumers, and an invalid `calc` silently kills
|
|
64
|
+
* `top`, which turns the rail from mispositioned into not-sticky.
|
|
65
|
+
*/
|
|
66
|
+
"sticky top-[var(--legend-sticky-top,0px)] z-20 lg:top-4 lg:z-auto",
|
|
67
|
+
"lg:max-h-[calc(100vh-2rem)] lg:self-start lg:overflow-y-auto lg:overscroll-contain",
|
|
68
|
+
"lg:[scrollbar-width:thin]",
|
|
69
|
+
className
|
|
70
|
+
),
|
|
71
|
+
children: [
|
|
72
|
+
/* @__PURE__ */ jsxs("div", { className: "hidden lg:block", children: [
|
|
73
|
+
/* @__PURE__ */ jsx("h2", { className: "text-[15px] font-bold tracking-[-0.01em]", children: heading }),
|
|
74
|
+
meta && /* @__PURE__ */ jsx("p", { className: "mt-1 text-[11.5px] text-muted-foreground", children: meta })
|
|
75
|
+
] }),
|
|
76
|
+
/* @__PURE__ */ jsx("ul", { className: "scrollbar-hide -mx-[var(--page-gutter)] flex gap-2 overflow-x-auto bg-background px-[var(--page-gutter)] pb-3 pt-1 lg:hidden", children: entries.map((entry) => /* @__PURE__ */ jsx("li", { className: "shrink-0", children: /* @__PURE__ */ jsxs(
|
|
77
|
+
"a",
|
|
78
|
+
{
|
|
79
|
+
href: `#${entry.slug}`,
|
|
80
|
+
className: "inline-flex h-9 items-center gap-2 whitespace-nowrap rounded-full border border-border bg-card px-3.5 text-[13px] transition-colors hover:border-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsx("span", { className: "font-bold tabular-nums text-muted-foreground", children: entry.rank }),
|
|
83
|
+
entry.name
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
) }, entry.slug)) }),
|
|
87
|
+
/* @__PURE__ */ jsx("div", { className: "hidden lg:mt-5 lg:-mx-3 lg:block", children: groups.map((group) => /* @__PURE__ */ jsxs("div", { className: "mb-4 last:mb-0", children: [
|
|
88
|
+
/* @__PURE__ */ jsx("p", { className: "px-3 pb-1.5 text-[10.5px] font-bold uppercase tracking-[0.11em] text-muted-foreground", children: group.label }),
|
|
89
|
+
/* @__PURE__ */ jsx("ul", { children: group.entries.map((entry) => {
|
|
90
|
+
const isActive = entry.slug === activeSlug;
|
|
91
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
92
|
+
"a",
|
|
93
|
+
{
|
|
94
|
+
href: `#${entry.slug}`,
|
|
95
|
+
"aria-current": isActive ? "location" : void 0,
|
|
96
|
+
className: cn(
|
|
97
|
+
"flex gap-2.5 rounded-lg px-3 py-2 text-[13px] leading-snug transition-colors motion-reduce:transition-none",
|
|
98
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
99
|
+
isActive ? "bg-accent-subtle font-semibold text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
|
|
100
|
+
),
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsx(
|
|
103
|
+
"span",
|
|
104
|
+
{
|
|
105
|
+
className: cn(
|
|
106
|
+
"w-5 shrink-0 text-right font-bold tabular-nums",
|
|
107
|
+
isActive ? "text-accent" : "text-muted-foreground/70"
|
|
108
|
+
),
|
|
109
|
+
children: entry.rank
|
|
110
|
+
}
|
|
111
|
+
),
|
|
112
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1", children: entry.name })
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
) }, entry.slug);
|
|
116
|
+
}) })
|
|
117
|
+
] }, group.key)) })
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { TopRankingLegend };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { MenuItemPreview } from './menu-types.js';
|
|
3
|
+
import './place-types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The editorial ranking's view model — `/top/[category]`.
|
|
7
|
+
*
|
|
8
|
+
* Separate from `menu-types.ts` on purpose. The menu surfaces describe what a
|
|
9
|
+
* city HAS; these describe an argument the product is making about it, and the
|
|
10
|
+
* shapes reflect that: an entry carries a cited quote and a dated price because
|
|
11
|
+
* a rank is a claim and the page has to show why.
|
|
12
|
+
*
|
|
13
|
+
* Same contract as everywhere else: no app schemas, no hooks, no i18n. Strings
|
|
14
|
+
* arrive through labels, destinations through `href`, numbers through injected
|
|
15
|
+
* formatters.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** One entry in the ranking. */
|
|
19
|
+
type TopRankEntry = {
|
|
20
|
+
rank: number;
|
|
21
|
+
slug: string;
|
|
22
|
+
name: string;
|
|
23
|
+
/** The venue page. */
|
|
24
|
+
href: string;
|
|
25
|
+
/** Neighbourhood, e.g. "Centru". */
|
|
26
|
+
district?: string;
|
|
27
|
+
/** Pre-resolved venue photograph. */
|
|
28
|
+
coverUrl?: string;
|
|
29
|
+
/** The app's grade, already formatted — "9.4". Absent where none exists. */
|
|
30
|
+
gradeLabel?: string;
|
|
31
|
+
/** The band that grade falls in, e.g. "Exceptional". Composed by the caller. */
|
|
32
|
+
tierLabel?: string;
|
|
33
|
+
/** Third-party star rating, shown only where there is no grade. */
|
|
34
|
+
rating?: number;
|
|
35
|
+
/**
|
|
36
|
+
* The editorial line — why this one is here, in the product's own voice.
|
|
37
|
+
*
|
|
38
|
+
* Distinct from `evidence`, which is a reviewer's words. Conflating them was
|
|
39
|
+
* the old page's problem: a quote presented as editorial reads as invented,
|
|
40
|
+
* and editorial presented as a quote reads as dishonest.
|
|
41
|
+
*/
|
|
42
|
+
editorial?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The dishes this venue is named for, as PRODUCTION MENU ROWS.
|
|
45
|
+
*
|
|
46
|
+
* `MenuItemPreview`, so the entry renders the same `<MenuItem>` a venue's own
|
|
47
|
+
* menu and every hub surface renders. The first pass here hand-rolled a row —
|
|
48
|
+
* a bold name, a muted description, a price — which drifted from the real one
|
|
49
|
+
* immediately and meant a dish looked like a different object on this page
|
|
50
|
+
* than on every other.
|
|
51
|
+
*
|
|
52
|
+
* More than one, because a rank is a claim about a kitchen and one dish
|
|
53
|
+
* rarely carries it: the live page shows the group and two or three of its
|
|
54
|
+
* most-named plates.
|
|
55
|
+
*/
|
|
56
|
+
menuItems?: MenuItemPreview[];
|
|
57
|
+
/** The group those items sit in, e.g. "Pizza". From the venue's own menu. */
|
|
58
|
+
menuGroupLabel?: string;
|
|
59
|
+
/** A reviewer's own words, and how many said it. */
|
|
60
|
+
evidence?: TopEvidence;
|
|
61
|
+
};
|
|
62
|
+
type TopEvidence = {
|
|
63
|
+
/**
|
|
64
|
+
* A reviewer's own words. OPTIONAL: a venue can carry a mention count with no
|
|
65
|
+
* quotable snippet behind it, and the count is evidence in its own right —
|
|
66
|
+
* dropping the whole block for want of a quote loses the number that
|
|
67
|
+
* justifies the rank.
|
|
68
|
+
*/
|
|
69
|
+
quote?: string;
|
|
70
|
+
/**
|
|
71
|
+
* How many reviews back it — the figure alone, e.g. "37". Rendered as its own
|
|
72
|
+
* element beside `sourceLabel` so the number reads as a figure rather than as
|
|
73
|
+
* the first word of a sentence.
|
|
74
|
+
*/
|
|
75
|
+
count?: string;
|
|
76
|
+
/** What the count is evidence OF, e.g. "reviews praise the shawarma here". */
|
|
77
|
+
sourceLabel: string;
|
|
78
|
+
/**
|
|
79
|
+
* The rest of the cited reviews, behind a disclosure.
|
|
80
|
+
*
|
|
81
|
+
* A `<details>`, not a drawer, and that is the whole point: its contents stay
|
|
82
|
+
* in the HTML, so the evidence is crawlable and readable with no JavaScript.
|
|
83
|
+
* A page whose entire argument is "here is why" cannot ship most of the why
|
|
84
|
+
* in a modal.
|
|
85
|
+
*/
|
|
86
|
+
extraQuotes?: string[];
|
|
87
|
+
/** Summary text for the disclosure, e.g. "2 more reviews". Pluralised by the caller. */
|
|
88
|
+
extraLabel?: string;
|
|
89
|
+
};
|
|
90
|
+
/** One row of the side-by-side comparison. */
|
|
91
|
+
type TopComparisonRow = {
|
|
92
|
+
rank: number;
|
|
93
|
+
name: string;
|
|
94
|
+
href: string;
|
|
95
|
+
gradeLabel?: string;
|
|
96
|
+
signature?: string;
|
|
97
|
+
priceLabel?: string;
|
|
98
|
+
/** Kitchen serving past midnight. */
|
|
99
|
+
late?: boolean;
|
|
100
|
+
terrace?: boolean;
|
|
101
|
+
};
|
|
102
|
+
/** A way out of the article and into the browsing tool. */
|
|
103
|
+
type TopExit = {
|
|
104
|
+
label: string;
|
|
105
|
+
href: string;
|
|
106
|
+
icon?: ReactNode;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type { TopComparisonRow, TopEvidence, TopExit, TopRankEntry };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -21,6 +21,8 @@ import { PlaceGradeDimensions } from './place-types.js';
|
|
|
21
21
|
* Server-safe.
|
|
22
22
|
*/
|
|
23
23
|
type VenueGradeDimensionKey = keyof PlaceGradeDimensions;
|
|
24
|
+
/** Canonical order. Quality leads, and the three quality keys are mutually exclusive. */
|
|
25
|
+
declare const DIMENSION_ORDER: VenueGradeDimensionKey[];
|
|
24
26
|
type VenueGradeDimensionsProps = {
|
|
25
27
|
dimensions: PlaceGradeDimensions;
|
|
26
28
|
/** Dimension key → translated label. Keys with no label fall back to the key. */
|
|
@@ -33,4 +35,4 @@ type VenueGradeDimensionsProps = {
|
|
|
33
35
|
};
|
|
34
36
|
declare function VenueGradeDimensions({ dimensions, labels, order, formatScore, className, }: VenueGradeDimensionsProps): react_jsx_runtime.JSX.Element | null;
|
|
35
37
|
|
|
36
|
-
export { type VenueGradeDimensionKey, VenueGradeDimensions, type VenueGradeDimensionsProps };
|
|
38
|
+
export { DIMENSION_ORDER, type VenueGradeDimensionKey, VenueGradeDimensions, type VenueGradeDimensionsProps };
|
|
@@ -6,7 +6,7 @@ import { CitedText } from './place-types.js';
|
|
|
6
6
|
*
|
|
7
7
|
* These are `reviews_digest.highlights`: one to three short positives already
|
|
8
8
|
* resolved into the request locale. They are the only place on a hub where a
|
|
9
|
-
* reviewer speaks in their own register, so they are set in serif
|
|
9
|
+
* reviewer speaks in their own register, so they are set in quoted serif
|
|
10
10
|
* against the sans everything else uses.
|
|
11
11
|
*
|
|
12
12
|
* `onCite` turns a quote into a button that opens the reviews behind it —
|
|
@@ -6,6 +6,8 @@ function DefaultLink(_a) {
|
|
|
6
6
|
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
7
7
|
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
8
8
|
}
|
|
9
|
+
var WALL_CELLS = 18;
|
|
10
|
+
var PHONE_CELLS = 6;
|
|
9
11
|
function VenueWall({
|
|
10
12
|
places,
|
|
11
13
|
hrefs,
|
|
@@ -28,7 +30,7 @@ function VenueWall({
|
|
|
28
30
|
),
|
|
29
31
|
children: [
|
|
30
32
|
(tileUrls == null ? void 0 : tileUrls.length) ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
31
|
-
/* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "absolute inset-0 grid auto-rows-fr grid-cols-
|
|
33
|
+
/* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "absolute inset-0 grid auto-rows-fr grid-cols-3 gap-0.5 lg:grid-cols-6", children: Array.from({ length: WALL_CELLS }, (_, cell) => (
|
|
32
34
|
// eslint-disable-next-line @next/next/no-img-element
|
|
33
35
|
/* @__PURE__ */ jsx(
|
|
34
36
|
"img",
|
|
@@ -37,7 +39,7 @@ function VenueWall({
|
|
|
37
39
|
alt: "",
|
|
38
40
|
loading: "lazy",
|
|
39
41
|
decoding: "async",
|
|
40
|
-
className: "h-full w-full object-cover"
|
|
42
|
+
className: cn("h-full w-full object-cover", cell >= PHONE_CELLS && "hidden lg:block")
|
|
41
43
|
},
|
|
42
44
|
cell
|
|
43
45
|
)
|
|
@@ -94,7 +96,7 @@ function VenueWall({
|
|
|
94
96
|
) }),
|
|
95
97
|
/* @__PURE__ */ jsx("span", { className: "truncate text-[12.5px] font-medium", children: place.name })
|
|
96
98
|
] });
|
|
97
|
-
const chipClasses = "inline-flex max-w-[220px] items-center gap-2 rounded-full bg-white/
|
|
99
|
+
const chipClasses = "inline-flex max-w-[220px] items-center gap-2 rounded-full bg-white/[0.14] py-1 pl-1 pr-3.5";
|
|
98
100
|
return /* @__PURE__ */ jsx("li", { children: (hrefs == null ? void 0 : hrefs[place.slug]) ? /* @__PURE__ */ jsx(
|
|
99
101
|
LinkComponent,
|
|
100
102
|
{
|
|
@@ -76,7 +76,7 @@ function WeekdayThemeBanner({
|
|
|
76
76
|
"relative z-[1] hidden border-t border-white/15 p-4 lg:block lg:min-w-[240px] lg:border-l lg:border-t-0 lg:p-5",
|
|
77
77
|
// Over a photograph the rail needs its own ground, or seven lines of
|
|
78
78
|
// small type land on whatever the plate happens to be.
|
|
79
|
-
imageUrl && "bg-black/
|
|
79
|
+
imageUrl && "bg-black/55"
|
|
80
80
|
),
|
|
81
81
|
children: [
|
|
82
82
|
weekLabel && /* @__PURE__ */ jsx("div", { className: "mb-2 text-[10px] font-bold tracking-[0.12em] opacity-70", children: weekLabel }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMediaQuery, DESKTOP_MEDIA_QUERY } from '../chunk-OPVKL4VS.js';
|
|
2
|
-
import { MotionPlayPause } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
2
|
+
import { MotionPlayPause } from '../chunk-YOKHBAIH.js';
|
|
3
|
+
import '../chunk-G7IFL4Z4.js';
|
|
4
4
|
import { useDragSwipe } from '../chunk-IYDH5IU6.js';
|
|
5
5
|
import { Skeleton } from '../chunk-QXHMPJ35.js';
|
|
6
6
|
import { cn } from '../chunk-FEK5XGZW.js';
|
package/package.json
CHANGED
package/src/components/badge.tsx
CHANGED
|
@@ -14,7 +14,7 @@ const badgeVariants = cva(
|
|
|
14
14
|
"border-transparent bg-destructive text-destructive-foreground",
|
|
15
15
|
outline: "text-foreground border-border",
|
|
16
16
|
overlay:
|
|
17
|
-
"border-transparent bg-foreground/
|
|
17
|
+
"border-transparent bg-foreground/80 text-background",
|
|
18
18
|
glass: "glass text-foreground border-border/50",
|
|
19
19
|
success:
|
|
20
20
|
"border-transparent bg-success text-success-foreground",
|
|
@@ -28,14 +28,14 @@ const buttonVariants = cva(
|
|
|
28
28
|
* into a dark photo.
|
|
29
29
|
*
|
|
30
30
|
* Deliberately NOT theme-aware, and so deliberately different from
|
|
31
|
-
* `Badge`'s `overlay` (`bg-foreground/
|
|
31
|
+
* `Badge`'s `overlay` (`bg-foreground/80 text-background`, which
|
|
32
32
|
* inverts). A photo is equally a photo in both themes — this chrome has
|
|
33
33
|
* to contrast with the image beneath it, not with the app canvas, so it
|
|
34
34
|
* stays dark-on-light-glyph either way. Reconcile Badge to this, not the
|
|
35
35
|
* reverse, if the two are ever unified.
|
|
36
36
|
*/
|
|
37
37
|
overlay:
|
|
38
|
-
"bg-black/
|
|
38
|
+
"bg-black/50 text-white hover:bg-black/65 focus-visible:ring-white/70",
|
|
39
39
|
brown:
|
|
40
40
|
"bg-brown text-brown-foreground shadow-sm hover:bg-brown/85 hover:shadow",
|
|
41
41
|
antagonist:
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { PhotoGround } from "./photo-ground";
|
|
6
|
+
import { SnapRail, SnapRailDots } from "./snap-rail";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ConceptCompareRail — the rooms as a rail, the card's backdrop following the
|
|
10
|
+
* active room, and the foot band that carries the page dots.
|
|
11
|
+
*
|
|
12
|
+
* On the phone the card IS the current room's photograph: the head, the rail
|
|
13
|
+
* and the foot all sit on it, and it cross-fades to the next room as the
|
|
14
|
+
* reader swipes. That needs the rail's active index, which is state — so this
|
|
15
|
+
* is the one client boundary in `ConceptCompare`. It takes only what can cross
|
|
16
|
+
* that boundary: photograph URLs, initials, and the pre-rendered rooms and
|
|
17
|
+
* foot as children. `labels`, `formatPrice` and the rest stay server-side.
|
|
18
|
+
*
|
|
19
|
+
* ## Why the backdrop lives here and not in each slide
|
|
20
|
+
*
|
|
21
|
+
* A slide carrying its own photograph is a boxed card sliding over a dark
|
|
22
|
+
* ground; the backdrop changing under a transparent slide is the reader
|
|
23
|
+
* turning to look at the next room. The second is the smoother thing, and it
|
|
24
|
+
* is also cheaper to look at — one photograph at a time instead of the edge of
|
|
25
|
+
* the next one peeking in.
|
|
26
|
+
*
|
|
27
|
+
* ## Why the foot is here too
|
|
28
|
+
*
|
|
29
|
+
* The dots need the rail's state, and on the phone they belong INSIDE the foot
|
|
30
|
+
* band's glass — under the rail they sat on a strip of bare photograph between
|
|
31
|
+
* the room's scrim and the foot's. So the foot renders here, with the dots as
|
|
32
|
+
* its first row below `lg`, and `ConceptCompare` hands it the foot's content.
|
|
33
|
+
*
|
|
34
|
+
* Desktop ignores the backdrop and the dots (`lg:hidden`); each room carries
|
|
35
|
+
* its own photograph there, side by side, and the foot is an absolute glass
|
|
36
|
+
* band over them.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export type ConceptCompareBackdrop = {
|
|
40
|
+
/** The room's photograph. Absent: the ground with the venue's initial. */
|
|
41
|
+
src?: string;
|
|
42
|
+
/** The venue's first letter, for the no-photograph fallback. */
|
|
43
|
+
initial: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type ConceptCompareRailProps = {
|
|
47
|
+
backdrops: ConceptCompareBackdrop[];
|
|
48
|
+
/** Classes for the rail itself — the desktop grid lives here. */
|
|
49
|
+
railClassName?: string;
|
|
50
|
+
/** The foot's content: the note and the action. Omit for no foot. */
|
|
51
|
+
foot?: React.ReactNode;
|
|
52
|
+
children: React.ReactNode;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The foot band's glass — two behaviours, one element.
|
|
57
|
+
*
|
|
58
|
+
* On the phone the band sits directly under a slide whose own scrim has
|
|
59
|
+
* already reached 0.9 at its bottom edge, so the glass is SOLID from its first
|
|
60
|
+
* pixel: a fade-in here left a lighter strip at the seam where the photograph
|
|
61
|
+
* showed through between the slide's darkness and the foot's, and the two read
|
|
62
|
+
* as disconnected pieces. From `lg` the foot floats over open photography with
|
|
63
|
+
* the room text well above it, so there it fades out over its top edge on the
|
|
64
|
+
* same curve as its mask, the way the head does at its underside.
|
|
65
|
+
*
|
|
66
|
+
* Same literal as every other scrim on the card, so they read as one shadow;
|
|
67
|
+
* not `var(--scrim)`, which is the app's token and not this package's.
|
|
68
|
+
*/
|
|
69
|
+
const FOOT_GLASS = cn(
|
|
70
|
+
// Ink only — no backdrop-blur; see `GlassGround` for the measurement.
|
|
71
|
+
"before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:content-['']",
|
|
72
|
+
"before:bg-[oklch(0.16_0.012_55_/_0.9)]",
|
|
73
|
+
"lg:before:[mask-image:linear-gradient(to_top,black_60%,transparent_100%)] lg:before:bg-[linear-gradient(to_top,oklch(0.16_0.012_55_/_0.88)_0%,oklch(0.16_0.012_55_/_0.84)_60%,oklch(0.16_0.012_55_/_0.4)_85%,oklch(0.16_0.012_55_/_0)_100%)]"
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
export function ConceptCompareRail({
|
|
77
|
+
backdrops,
|
|
78
|
+
railClassName,
|
|
79
|
+
foot,
|
|
80
|
+
children,
|
|
81
|
+
}: ConceptCompareRailProps) {
|
|
82
|
+
const [active, setActive] = React.useState(0);
|
|
83
|
+
const count = React.Children.count(children);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
{/* The backdrop: every room's photograph stacked, the active one shown,
|
|
88
|
+
cross-fading on swipe. Positioned against the CARD — `ConceptCompare`
|
|
89
|
+
renders this inside its `relative isolate` frame. Desktop hides it;
|
|
90
|
+
each room carries its own photograph there. */}
|
|
91
|
+
<PhotoGround
|
|
92
|
+
className="-z-10 lg:hidden"
|
|
93
|
+
mode="stack"
|
|
94
|
+
active={active}
|
|
95
|
+
photos={backdrops.map((backdrop) => backdrop.src)}
|
|
96
|
+
initials={backdrops.map((backdrop) => backdrop.initial)}
|
|
97
|
+
overlay
|
|
98
|
+
loading="eager"
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
{/* `pb-0` over the rail's default `pb-0.5`: two pixels of half-inked
|
|
102
|
+
photograph between the slide's scrim and the foot's glass reads as a
|
|
103
|
+
hairline where none was drawn.
|
|
104
|
+
|
|
105
|
+
`overscroll-x-contain`, because this card is itself a slide in the
|
|
106
|
+
hub's concept carousel. Without it a swipe that reaches the last room
|
|
107
|
+
chains into the outer rail and jumps to the next concept — the reader
|
|
108
|
+
asked to see the fourth kitchen and got a different dish. */}
|
|
109
|
+
<SnapRail
|
|
110
|
+
itemWidth="full"
|
|
111
|
+
gap="sm"
|
|
112
|
+
onActiveChange={setActive}
|
|
113
|
+
className={cn("pb-0 overscroll-x-contain", railClassName)}
|
|
114
|
+
>
|
|
115
|
+
{children}
|
|
116
|
+
</SnapRail>
|
|
117
|
+
|
|
118
|
+
{foot && (
|
|
119
|
+
<div
|
|
120
|
+
className={cn(
|
|
121
|
+
"relative isolate flex flex-col gap-3 px-4 pb-4 pt-3",
|
|
122
|
+
"lg:absolute lg:inset-x-0 lg:bottom-0 lg:z-10 lg:flex-row lg:items-center lg:gap-5 lg:px-5 lg:pb-4 lg:pt-6",
|
|
123
|
+
FOOT_GLASS
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
<SnapRailDots count={count} active={active} className="lg:hidden" />
|
|
127
|
+
{foot}
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
}
|