@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.
- package/dist/chunk-24RFI4RQ.js +167 -0
- package/dist/chunk-2XCZJWTV.js +121 -0
- package/dist/chunk-5RWTOT5Y.js +72 -0
- 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-MZL7CGCL.js +109 -0
- package/dist/chunk-SY6RV476.js +48 -0
- package/dist/chunk-UDN7UHJW.js +60 -0
- package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/command.d.ts +7 -7
- 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/date-input.d.ts +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 +1 -1
- package/dist/components/event-index-row.js +1 -1
- package/dist/components/event-list-row-wide.js +1 -1
- 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/glass-ground.d.ts +66 -0
- package/dist/components/glass-ground.js +3 -0
- 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/kitchen-dossier.d.ts +88 -0
- package/dist/components/kitchen-dossier.js +187 -0
- 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 +99 -0
- package/dist/components/menu-family-block.js +155 -0
- package/dist/components/menu-family-card.d.ts +33 -27
- package/dist/components/menu-family-card.js +43 -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-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-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/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 +2 -2
- 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/snap-rail.d.ts +24 -1
- package/dist/components/snap-rail.js +3 -109
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/story.d.ts +34 -0
- package/dist/components/story.js +3 -0
- package/dist/components/text.d.ts +3 -3
- 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/weekend-panel.js +2 -2
- package/package.json +1 -1
- package/src/components/concept-compare-rail.tsx +131 -0
- package/src/components/concept-compare.stories.tsx +122 -54
- package/src/components/concept-compare.tsx +363 -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 +129 -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 +322 -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 +314 -0
- package/src/components/menu-family-card.stories.tsx +43 -35
- package/src/components/menu-family-card.tsx +122 -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-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/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/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
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { VenueGradeDimensions } from '../chunk-AXV5O2IZ.js';
|
|
2
|
-
import { VenueQuotes } from '../chunk-62ES6N32.js';
|
|
3
|
-
import { VenueSignatureBlock } from '../chunk-THOVPZNN.js';
|
|
4
|
-
import { Separator } from '../chunk-WSRWFA6K.js';
|
|
5
|
-
import '../chunk-TH6W2PWG.js';
|
|
6
|
-
import '../chunk-Q6QH52LJ.js';
|
|
7
|
-
import '../chunk-UKBNQO6Q.js';
|
|
8
|
-
import { cn } from '../chunk-FEK5XGZW.js';
|
|
9
|
-
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
10
|
-
import { Camera, Star } from 'lucide-react';
|
|
11
|
-
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
12
|
-
|
|
13
|
-
function DefaultLink(_a) {
|
|
14
|
-
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
15
|
-
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
16
|
-
}
|
|
17
|
-
var QUOTE_LIMIT = 2;
|
|
18
|
-
function VenueSpotlight({
|
|
19
|
-
place,
|
|
20
|
-
href,
|
|
21
|
-
labels,
|
|
22
|
-
meta,
|
|
23
|
-
signature,
|
|
24
|
-
signatureGradient,
|
|
25
|
-
itemLabels,
|
|
26
|
-
formatPrice,
|
|
27
|
-
formatCount,
|
|
28
|
-
formatScore,
|
|
29
|
-
onCite,
|
|
30
|
-
action,
|
|
31
|
-
linkComponent: LinkComponent = DefaultLink,
|
|
32
|
-
className
|
|
33
|
-
}) {
|
|
34
|
-
var _a, _b, _c;
|
|
35
|
-
const digest = place.reviews_digest;
|
|
36
|
-
const tags = (_a = digest == null ? void 0 : digest.tags) != null ? _a : [];
|
|
37
|
-
return /* @__PURE__ */ jsxs(
|
|
38
|
-
"div",
|
|
39
|
-
{
|
|
40
|
-
className: cn(
|
|
41
|
-
"grid overflow-hidden rounded-[18px] border border-border bg-card lg:grid-cols-[404px_1fr]",
|
|
42
|
-
className
|
|
43
|
-
),
|
|
44
|
-
children: [
|
|
45
|
-
/* @__PURE__ */ jsxs("div", { className: "relative min-h-[220px] bg-[repeating-linear-gradient(135deg,oklch(0.76_0.02_55)_0_10px,oklch(0.83_0.013_58)_10px_20px)] lg:min-h-[340px]", children: [
|
|
46
|
-
place.thumbnail_url && // eslint-disable-next-line @next/next/no-img-element
|
|
47
|
-
/* @__PURE__ */ jsx(
|
|
48
|
-
"img",
|
|
49
|
-
{
|
|
50
|
-
src: place.thumbnail_url,
|
|
51
|
-
alt: place.name,
|
|
52
|
-
className: "absolute inset-0 h-full w-full object-cover"
|
|
53
|
-
}
|
|
54
|
-
),
|
|
55
|
-
labels.photoCredit && /* @__PURE__ */ jsxs("span", { className: "absolute left-3.5 top-3.5 inline-flex h-7 items-center gap-1.5 rounded-full bg-black/60 px-2.5 text-[11px] font-bold text-white", children: [
|
|
56
|
-
/* @__PURE__ */ jsx(Camera, { className: "h-2.5 w-2.5", "aria-hidden": true }),
|
|
57
|
-
labels.photoCredit
|
|
58
|
-
] }),
|
|
59
|
-
labels.openUntil && /* @__PURE__ */ jsxs("span", { className: "absolute bottom-3.5 left-3.5 inline-flex h-[30px] items-center gap-[7px] rounded-full bg-background px-3 text-[11.5px] font-bold text-[var(--free,currentColor)]", children: [
|
|
60
|
-
/* @__PURE__ */ jsx("i", { className: "h-[7px] w-[7px] rounded-full bg-current", "aria-hidden": true }),
|
|
61
|
-
labels.openUntil
|
|
62
|
-
] })
|
|
63
|
-
] }),
|
|
64
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3.5 p-4 lg:p-6", children: [
|
|
65
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [
|
|
66
|
-
/* @__PURE__ */ jsx("h3", { className: "min-w-0 flex-1 basis-0 font-serif text-xl font-semibold leading-tight tracking-[-0.02em] lg:text-[28px]", children: /* @__PURE__ */ jsx(
|
|
67
|
-
LinkComponent,
|
|
68
|
-
{
|
|
69
|
-
href,
|
|
70
|
-
className: "rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
71
|
-
children: place.name
|
|
72
|
-
}
|
|
73
|
-
) }),
|
|
74
|
-
place.rating && /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-baseline gap-1.5", children: [
|
|
75
|
-
/* @__PURE__ */ jsx("b", { className: "text-xl font-extrabold tabular-nums lg:text-2xl", children: place.rating.rating }),
|
|
76
|
-
/* @__PURE__ */ jsx(Star, { className: "h-3 w-3 self-center text-muted-foreground", "aria-hidden": true })
|
|
77
|
-
] })
|
|
78
|
-
] }),
|
|
79
|
-
(meta || labels.reviewCount) && /* @__PURE__ */ jsxs("div", { className: "-mt-2 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12.5px] text-muted-foreground", children: [
|
|
80
|
-
meta && /* @__PURE__ */ jsx("span", { children: meta }),
|
|
81
|
-
meta && labels.reviewCount && /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\xB7" }),
|
|
82
|
-
labels.reviewCount && /* @__PURE__ */ jsx("span", { children: labels.reviewCount })
|
|
83
|
-
] }),
|
|
84
|
-
signature && /* @__PURE__ */ jsx(
|
|
85
|
-
VenueSignatureBlock,
|
|
86
|
-
{
|
|
87
|
-
item: signature.item,
|
|
88
|
-
labels: itemLabels,
|
|
89
|
-
formatPrice,
|
|
90
|
-
formatCount,
|
|
91
|
-
kicker: labels.signatureKicker,
|
|
92
|
-
markGradient: signatureGradient,
|
|
93
|
-
subtitle: signature.why,
|
|
94
|
-
href: signature.href,
|
|
95
|
-
linkComponent: LinkComponent
|
|
96
|
-
}
|
|
97
|
-
),
|
|
98
|
-
(digest == null ? void 0 : digest.highlights) && /* @__PURE__ */ jsx(
|
|
99
|
-
VenueQuotes,
|
|
100
|
-
{
|
|
101
|
-
quotes: digest.highlights,
|
|
102
|
-
limit: QUOTE_LIMIT,
|
|
103
|
-
onCite,
|
|
104
|
-
citeLabel: labels.cite
|
|
105
|
-
}
|
|
106
|
-
),
|
|
107
|
-
((_b = digest == null ? void 0 : digest.grade) == null ? void 0 : _b.dimensions) && /* @__PURE__ */ jsx(Separator, {}),
|
|
108
|
-
((_c = digest == null ? void 0 : digest.grade) == null ? void 0 : _c.dimensions) && /* @__PURE__ */ jsx(
|
|
109
|
-
VenueGradeDimensions,
|
|
110
|
-
{
|
|
111
|
-
dimensions: digest.grade.dimensions,
|
|
112
|
-
labels: labels.dimensions,
|
|
113
|
-
formatScore,
|
|
114
|
-
className: "mt-1"
|
|
115
|
-
}
|
|
116
|
-
),
|
|
117
|
-
(labels.valueVerdict || tags.length > 0) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
118
|
-
labels.valueVerdict && /* @__PURE__ */ jsx("span", { className: "inline-flex h-[26px] items-center gap-1.5 rounded-full bg-accent-subtle px-2.5 text-[11.5px] font-bold text-accent", children: labels.valueVerdict }),
|
|
119
|
-
tags.map((tag) => {
|
|
120
|
-
var _a2, _b2;
|
|
121
|
-
return /* @__PURE__ */ jsx(
|
|
122
|
-
"span",
|
|
123
|
-
{
|
|
124
|
-
className: "inline-flex h-[26px] items-center rounded-full border border-border px-2.5 text-[11.5px] text-muted-foreground",
|
|
125
|
-
children: (_b2 = (_a2 = labels.tags) == null ? void 0 : _a2[tag.value]) != null ? _b2 : tag.value
|
|
126
|
-
},
|
|
127
|
-
tag.value
|
|
128
|
-
);
|
|
129
|
-
})
|
|
130
|
-
] }),
|
|
131
|
-
action && /* @__PURE__ */ jsx("div", { className: "mt-auto flex justify-end pt-1", children: action })
|
|
132
|
-
] })
|
|
133
|
-
]
|
|
134
|
-
}
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export { VenueSpotlight };
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { mockVenues } from "./menu-mocks";
|
|
3
|
-
import { RankedVenueList } from "./ranked-venue-list";
|
|
4
|
-
|
|
5
|
-
const priceBandLabels = {
|
|
6
|
-
"the-copper-yard": "19—62",
|
|
7
|
-
"salt-and-stone": "38—84",
|
|
8
|
-
"the-quiet-hours": "8—26",
|
|
9
|
-
"green-room-canteen": "22—51",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const meta: Meta<typeof RankedVenueList> = {
|
|
13
|
-
title: "Menu/RankedVenueList",
|
|
14
|
-
component: RankedVenueList,
|
|
15
|
-
tags: ["autodocs"],
|
|
16
|
-
decorators: [
|
|
17
|
-
(Story) => (
|
|
18
|
-
<div className="w-[620px] bg-background p-4">
|
|
19
|
-
<Story />
|
|
20
|
-
</div>
|
|
21
|
-
),
|
|
22
|
-
],
|
|
23
|
-
parameters: {
|
|
24
|
-
docs: {
|
|
25
|
-
description: {
|
|
26
|
-
component:
|
|
27
|
-
"Venues ordered by how often reviews name their dishes — the answer to \"best Italian in Cluj\". What makes the order defensible is the mention count on every row: it is evidence, not opinion. A caller that cannot supply mentions should use a different surface rather than this one with the column blank, because an unexplained ranking is worse than none. The tab strip re-ranks in place and never navigates.",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export default meta;
|
|
34
|
-
type Story = StoryObj<typeof RankedVenueList>;
|
|
35
|
-
|
|
36
|
-
export const Default: Story = {
|
|
37
|
-
args: {
|
|
38
|
-
venues: mockVenues.slice(0, 3),
|
|
39
|
-
heading: "The kitchens Cluj rates for Italian",
|
|
40
|
-
caption: "by how often reviews name their dishes",
|
|
41
|
-
priceBandLabels,
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/** With tabs. `activeTab` and `onTabChange` are the caller's state. */
|
|
46
|
-
export const WithTabs: Story = {
|
|
47
|
-
args: {
|
|
48
|
-
venues: mockVenues.slice(0, 3),
|
|
49
|
-
heading: "The kitchens Cluj rates",
|
|
50
|
-
caption: "from review mentions, not opinion",
|
|
51
|
-
tabs: [
|
|
52
|
-
{ slug: "italian", label: "Italian" },
|
|
53
|
-
{ slug: "romanesc", label: "Romanian" },
|
|
54
|
-
{ slug: "japonez", label: "Japanese" },
|
|
55
|
-
{ slug: "burgeri", label: "Burgers" },
|
|
56
|
-
],
|
|
57
|
-
activeTab: "italian",
|
|
58
|
-
onTabChange: () => {},
|
|
59
|
-
priceBandLabels,
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/** No header at all — the list inside a section that already has one. */
|
|
64
|
-
export const Bare: Story = { args: { venues: mockVenues.slice(0, 3), priceBandLabels } };
|
|
65
|
-
|
|
66
|
-
/** A venue with no signature still ranks; its quoted dish line is simply absent. */
|
|
67
|
-
export const MixedEvidence: Story = {
|
|
68
|
-
args: { venues: mockVenues, heading: "The kitchens Cluj rates", priceBandLabels },
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export const Mobile: Story = {
|
|
72
|
-
args: { venues: mockVenues.slice(0, 3), heading: "The kitchens Cluj rates" },
|
|
73
|
-
decorators: [
|
|
74
|
-
(Story) => (
|
|
75
|
-
<div className="w-[390px] bg-background p-3">
|
|
76
|
-
<Story />
|
|
77
|
-
</div>
|
|
78
|
-
),
|
|
79
|
-
],
|
|
80
|
-
};
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { ChevronRight, MessageSquare, Star } from "lucide-react";
|
|
2
|
-
import { cn } from "../lib/utils";
|
|
3
|
-
import { Separator } from "./separator";
|
|
4
|
-
import type { MenuVenuePreview, MenuLinkProps, MenuLinkComponent } from "./menu-types";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* RankedVenueList — venues ordered by how often reviews name their dishes.
|
|
8
|
-
*
|
|
9
|
-
* The answer to "best Italian in Cluj", and the reason it is defensible is the
|
|
10
|
-
* mention count sitting on every row: the order is evidence, not opinion. A
|
|
11
|
-
* caller that cannot supply mentions should use a different surface rather than
|
|
12
|
-
* this one with the column blank — an unexplained ranking is worse than none.
|
|
13
|
-
*
|
|
14
|
-
* The tab strip re-ranks in place and never navigates, so `activeTab` and
|
|
15
|
-
* `onTabChange` are controlled by the caller. Omit `tabs` entirely and the
|
|
16
|
-
* header is just a heading.
|
|
17
|
-
*
|
|
18
|
-
* No directive: without `tabs` the list renders on the server, which is the
|
|
19
|
-
* common case on a hub.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
|
|
23
|
-
export type RankedVenueListLinkProps = MenuLinkProps;
|
|
24
|
-
|
|
25
|
-
function DefaultLink({ href, ...props }: RankedVenueListLinkProps) {
|
|
26
|
-
return <a href={href} {...props} />;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type RankedVenueListTab = {
|
|
30
|
-
slug: string;
|
|
31
|
-
label: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type RankedVenueListProps = {
|
|
35
|
-
venues: MenuVenuePreview[];
|
|
36
|
-
/** Section heading, e.g. "The kitchens Cluj rates for Italian". */
|
|
37
|
-
heading?: string;
|
|
38
|
-
/** The basis of the order, e.g. "by how often reviews name their dishes". */
|
|
39
|
-
caption?: string;
|
|
40
|
-
tabs?: RankedVenueListTab[];
|
|
41
|
-
activeTab?: string;
|
|
42
|
-
onTabChange?: (slug: string) => void;
|
|
43
|
-
/** Composed price band per venue, keyed by place slug, e.g. "38—84". */
|
|
44
|
-
priceBandLabels?: Record<string, string>;
|
|
45
|
-
/**
|
|
46
|
-
* The app's own grade per venue slug, already formatted ("8.7"). Shown in
|
|
47
|
-
* place of the star rating when present — the star is the third-party
|
|
48
|
-
* fallback for a venue the reviews have not graded yet.
|
|
49
|
-
*/
|
|
50
|
-
gradeLabels?: Record<string, string>;
|
|
51
|
-
linkComponent?: MenuLinkComponent;
|
|
52
|
-
className?: string;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export function RankedVenueList({
|
|
56
|
-
venues,
|
|
57
|
-
heading,
|
|
58
|
-
caption,
|
|
59
|
-
tabs,
|
|
60
|
-
activeTab,
|
|
61
|
-
onTabChange,
|
|
62
|
-
priceBandLabels,
|
|
63
|
-
gradeLabels,
|
|
64
|
-
linkComponent: LinkComponent = DefaultLink,
|
|
65
|
-
className,
|
|
66
|
-
}: RankedVenueListProps) {
|
|
67
|
-
return (
|
|
68
|
-
<div className={cn("overflow-hidden rounded-[14px] border border-border bg-card", className)}>
|
|
69
|
-
{(heading || tabs) && (
|
|
70
|
-
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 border-b border-border p-3.5">
|
|
71
|
-
{heading && (
|
|
72
|
-
<h3 className="text-[15px] font-bold leading-tight tracking-[-0.01em]">{heading}</h3>
|
|
73
|
-
)}
|
|
74
|
-
{caption && (
|
|
75
|
-
<span className="text-[12.5px] text-muted-foreground">{caption}</span>
|
|
76
|
-
)}
|
|
77
|
-
{tabs && tabs.length > 0 && (
|
|
78
|
-
<div className="ml-auto flex flex-wrap gap-1.5">
|
|
79
|
-
{tabs.map((tab) => (
|
|
80
|
-
<button
|
|
81
|
-
key={tab.slug}
|
|
82
|
-
type="button"
|
|
83
|
-
aria-pressed={tab.slug === activeTab}
|
|
84
|
-
onClick={() => onTabChange?.(tab.slug)}
|
|
85
|
-
className={cn(
|
|
86
|
-
"inline-flex h-7 items-center rounded-full border px-2.5 text-xs font-semibold transition-colors",
|
|
87
|
-
tab.slug === activeTab
|
|
88
|
-
? "border-accent bg-accent-subtle text-accent"
|
|
89
|
-
: "border-border text-muted-foreground hover:bg-muted/50"
|
|
90
|
-
)}
|
|
91
|
-
>
|
|
92
|
-
{tab.label}
|
|
93
|
-
</button>
|
|
94
|
-
))}
|
|
95
|
-
</div>
|
|
96
|
-
)}
|
|
97
|
-
</div>
|
|
98
|
-
)}
|
|
99
|
-
|
|
100
|
-
<ol>
|
|
101
|
-
{venues.map((venue, index) => {
|
|
102
|
-
const { place, signature } = venue;
|
|
103
|
-
const mentions = signature?.item.mentions ?? 0;
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<li key={place.slug}>
|
|
107
|
-
{index > 0 && <Separator />}
|
|
108
|
-
<LinkComponent
|
|
109
|
-
href={venue.href}
|
|
110
|
-
className="flex items-center gap-3 p-3 transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
111
|
-
>
|
|
112
|
-
<span className="w-4 shrink-0 text-center text-[13px] font-extrabold tabular-nums text-accent">
|
|
113
|
-
{index + 1}
|
|
114
|
-
</span>
|
|
115
|
-
|
|
116
|
-
{/* 48px. Same reason as `OpenNowList`: below ~48 the venue
|
|
117
|
-
photograph stops being a photograph. */}
|
|
118
|
-
<span className="h-12 w-12 shrink-0 overflow-hidden rounded-xl bg-[repeating-linear-gradient(135deg,oklch(0.78_0.018_55)_0_8px,oklch(0.84_0.012_58)_8px_16px)]">
|
|
119
|
-
{place.thumbnail_url && (
|
|
120
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
121
|
-
<img
|
|
122
|
-
src={place.thumbnail_url}
|
|
123
|
-
alt=""
|
|
124
|
-
className="h-full w-full object-cover"
|
|
125
|
-
loading="lazy"
|
|
126
|
-
/>
|
|
127
|
-
)}
|
|
128
|
-
</span>
|
|
129
|
-
|
|
130
|
-
<span className="min-w-0 flex-1">
|
|
131
|
-
{/* The whole name: a ranked list is a list of names, and a
|
|
132
|
-
name cut to "PASQUALE | brunch & din…" is not one. */}
|
|
133
|
-
<span className="block text-sm font-bold leading-snug tracking-[-0.01em]">
|
|
134
|
-
{place.name}
|
|
135
|
-
</span>
|
|
136
|
-
{signature?.item.name && (
|
|
137
|
-
<span className="mt-px block truncate font-serif text-xs italic text-muted-foreground">
|
|
138
|
-
{`“${signature.item.name}”`}
|
|
139
|
-
</span>
|
|
140
|
-
)}
|
|
141
|
-
</span>
|
|
142
|
-
|
|
143
|
-
{mentions > 0 && (
|
|
144
|
-
<span className="hidden shrink-0 items-center gap-1 text-[11.5px] tabular-nums text-muted-foreground sm:inline-flex">
|
|
145
|
-
<MessageSquare className="h-[11px] w-[11px]" aria-hidden />
|
|
146
|
-
{mentions}
|
|
147
|
-
</span>
|
|
148
|
-
)}
|
|
149
|
-
|
|
150
|
-
{gradeLabels?.[place.slug] ? (
|
|
151
|
-
<span className="inline-flex h-6 shrink-0 items-center rounded-md bg-foreground px-1.5 text-[12px] font-extrabold tabular-nums text-background">
|
|
152
|
-
{gradeLabels[place.slug]}
|
|
153
|
-
</span>
|
|
154
|
-
) : (
|
|
155
|
-
place.rating && (
|
|
156
|
-
<span className="inline-flex shrink-0 items-center gap-1 text-[12.5px] font-semibold tabular-nums">
|
|
157
|
-
<Star className="h-3 w-3" aria-hidden />
|
|
158
|
-
{place.rating.rating}
|
|
159
|
-
</span>
|
|
160
|
-
)
|
|
161
|
-
)}
|
|
162
|
-
|
|
163
|
-
{priceBandLabels?.[place.slug] && (
|
|
164
|
-
<span className="hidden shrink-0 text-[12.5px] tabular-nums text-muted-foreground sm:block">
|
|
165
|
-
{priceBandLabels[place.slug]}
|
|
166
|
-
</span>
|
|
167
|
-
)}
|
|
168
|
-
|
|
169
|
-
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
170
|
-
</LinkComponent>
|
|
171
|
-
</li>
|
|
172
|
-
);
|
|
173
|
-
})}
|
|
174
|
-
</ol>
|
|
175
|
-
</div>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { mockHubLabels, mockVenues } from "./menu-mocks";
|
|
3
|
-
import { SignatureLineList } from "./signature-line-list";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const itemLabels = {
|
|
7
|
-
currency: "lei",
|
|
8
|
-
mentions: (count: number) => `${count} mentions`,
|
|
9
|
-
signature: "SIGNATURE",
|
|
10
|
-
mentioned: "MENTIONED",
|
|
11
|
-
};
|
|
12
|
-
const formatPrice = (amount: number) => String(amount);
|
|
13
|
-
|
|
14
|
-
const labels = itemLabels;
|
|
15
|
-
|
|
16
|
-
const metaLabels = {
|
|
17
|
-
"the-copper-yard": "pub · Centru",
|
|
18
|
-
"salt-and-stone": "italian · Centru",
|
|
19
|
-
"the-quiet-hours": "coffee · Centru",
|
|
20
|
-
"green-room-canteen": "vegetarian · Mărăști",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const hues = {
|
|
24
|
-
"the-copper-yard": "oklch(0.52 0.15 25)",
|
|
25
|
-
"salt-and-stone": "oklch(0.62 0.14 55)",
|
|
26
|
-
"the-quiet-hours": "oklch(0.58 0.09 70)",
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const meta: Meta<typeof SignatureLineList> = {
|
|
31
|
-
title: "Menu/SignatureLineList",
|
|
32
|
-
component: SignatureLineList,
|
|
33
|
-
tags: ["autodocs"],
|
|
34
|
-
decorators: [
|
|
35
|
-
(Story) => (
|
|
36
|
-
<div className="w-[980px] bg-background p-4">
|
|
37
|
-
<Story />
|
|
38
|
-
</div>
|
|
39
|
-
),
|
|
40
|
-
],
|
|
41
|
-
parameters: {
|
|
42
|
-
docs: {
|
|
43
|
-
description: {
|
|
44
|
-
component:
|
|
45
|
-
"Family, venue, dish, price and reputation on one line — the densest surface on the hub, and the one that most nearly justifies the whole catalogue. Two targets per row on purpose: the venue line goes to the venue, the item row goes to that item across every kitchen that makes it. Collapsing them into one tap loses the comparison that makes the item worth showing. They are separate anchors rather than a button nested in a link, which would be invalid.",
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export default meta;
|
|
52
|
-
type Story = StoryObj<typeof SignatureLineList>;
|
|
53
|
-
|
|
54
|
-
export const Default: Story = {
|
|
55
|
-
args: {
|
|
56
|
-
venues: mockVenues.slice(0, 3),
|
|
57
|
-
labels,
|
|
58
|
-
formatPrice,
|
|
59
|
-
metaLabels,
|
|
60
|
-
hues,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/** The last venue has no signature — the row is a venue line and nothing else. */
|
|
65
|
-
export const MixedEvidence: Story = {
|
|
66
|
-
args: { venues: mockVenues, labels, formatPrice, metaLabels, hues },
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/** Below `lg` the row stacks: venue line, then the full item row underneath. */
|
|
70
|
-
export const Mobile: Story = {
|
|
71
|
-
args: {
|
|
72
|
-
venues: mockVenues.slice(0, 3),
|
|
73
|
-
labels,
|
|
74
|
-
formatPrice,
|
|
75
|
-
metaLabels,
|
|
76
|
-
hues,
|
|
77
|
-
},
|
|
78
|
-
decorators: [
|
|
79
|
-
(Story) => (
|
|
80
|
-
<div className="w-[390px] bg-background p-3">
|
|
81
|
-
<Story />
|
|
82
|
-
</div>
|
|
83
|
-
),
|
|
84
|
-
],
|
|
85
|
-
};
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { ChevronRight, Star } from "lucide-react";
|
|
2
|
-
import { cn } from "../lib/utils";
|
|
3
|
-
import { Separator } from "./separator";
|
|
4
|
-
import { MenuItem, type MenuItemLabels } from "./menu-item";
|
|
5
|
-
import type { MenuVenuePreview, MenuLinkProps, MenuLinkComponent } from "./menu-types";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* SignatureLineList — family, venue, dish, price and reputation on one line.
|
|
9
|
-
*
|
|
10
|
-
* The densest surface on the hub, and the one that most nearly justifies the
|
|
11
|
-
* whole catalogue: every row ties a kitchen to the single thing its reviewers
|
|
12
|
-
* keep naming, with the price and the count that back it.
|
|
13
|
-
*
|
|
14
|
-
* ## Two targets per row, deliberately
|
|
15
|
-
*
|
|
16
|
-
* The venue line goes to the venue; the item row goes to the item across every
|
|
17
|
-
* kitchen that makes it. Those are genuinely different destinations, and
|
|
18
|
-
* collapsing them into one tap loses the comparison that makes the item worth
|
|
19
|
-
* showing. They are separate anchors rather than one anchor with a nested
|
|
20
|
-
* button, which would be invalid markup.
|
|
21
|
-
*
|
|
22
|
-
* Below `lg` the row stacks: venue line first, then the full `<MenuItem>`
|
|
23
|
-
* underneath. The desktop layout puts them side by side. Same markup, one
|
|
24
|
-
* breakpoint — no second component.
|
|
25
|
-
*
|
|
26
|
-
* Server-safe.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
|
|
30
|
-
export type SignatureLineListLinkProps = MenuLinkProps;
|
|
31
|
-
|
|
32
|
-
function DefaultLink({ href, ...props }: SignatureLineListLinkProps) {
|
|
33
|
-
return <a href={href} {...props} />;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type SignatureLineListProps = {
|
|
37
|
-
venues: MenuVenuePreview[];
|
|
38
|
-
/** Passed through to each `<MenuItem>`. */
|
|
39
|
-
labels: MenuItemLabels;
|
|
40
|
-
formatPrice: (amount: number) => string;
|
|
41
|
-
formatCount?: (value: number) => string;
|
|
42
|
-
/** Composed meta line per venue, keyed by place slug, e.g. "romanian · Centru". */
|
|
43
|
-
metaLabels?: Record<string, string>;
|
|
44
|
-
/** The row's category mark per venue, keyed by place slug. */
|
|
45
|
-
hues?: Record<string, string>;
|
|
46
|
-
linkComponent?: MenuLinkComponent;
|
|
47
|
-
className?: string;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export function SignatureLineList({
|
|
51
|
-
venues,
|
|
52
|
-
labels,
|
|
53
|
-
formatPrice,
|
|
54
|
-
formatCount,
|
|
55
|
-
metaLabels,
|
|
56
|
-
hues,
|
|
57
|
-
linkComponent: LinkComponent = DefaultLink,
|
|
58
|
-
className,
|
|
59
|
-
}: SignatureLineListProps) {
|
|
60
|
-
return (
|
|
61
|
-
<ul className={cn("flex flex-col", className)}>
|
|
62
|
-
{venues.map((venue, index) => {
|
|
63
|
-
const { place, signature } = venue;
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<li
|
|
67
|
-
key={place.slug}
|
|
68
|
-
className="grid gap-x-4 py-3.5 lg:grid-cols-[minmax(0,260px)_minmax(0,1fr)] lg:items-center"
|
|
69
|
-
>
|
|
70
|
-
{/* The DS rule between rows, spanning both grid columns. */}
|
|
71
|
-
{index > 0 && <Separator className="mb-3.5 lg:col-span-2" />}
|
|
72
|
-
<LinkComponent
|
|
73
|
-
href={venue.href}
|
|
74
|
-
className="flex items-center gap-3 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
75
|
-
>
|
|
76
|
-
<span className="relative h-12 w-12 shrink-0 overflow-hidden rounded-xl bg-[repeating-linear-gradient(135deg,oklch(0.78_0.018_55)_0_8px,oklch(0.84_0.012_58)_8px_16px)] lg:h-16 lg:w-16">
|
|
77
|
-
{place.thumbnail_url && (
|
|
78
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
79
|
-
<img
|
|
80
|
-
src={place.thumbnail_url}
|
|
81
|
-
alt=""
|
|
82
|
-
className="h-full w-full object-cover"
|
|
83
|
-
loading="lazy"
|
|
84
|
-
/>
|
|
85
|
-
)}
|
|
86
|
-
{place.open_now && (
|
|
87
|
-
<i
|
|
88
|
-
aria-hidden
|
|
89
|
-
className="absolute right-1 top-1 h-[7px] w-[7px] rounded-full bg-[var(--free,currentColor)] ring-2 ring-white/80"
|
|
90
|
-
/>
|
|
91
|
-
)}
|
|
92
|
-
</span>
|
|
93
|
-
|
|
94
|
-
<span className="min-w-0 flex-1">
|
|
95
|
-
<span className="block truncate text-[15px] font-bold tracking-[-0.01em]">
|
|
96
|
-
{place.name}
|
|
97
|
-
</span>
|
|
98
|
-
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
99
|
-
{place.rating && (
|
|
100
|
-
<>
|
|
101
|
-
<Star className="h-[11px] w-[11px]" aria-hidden />
|
|
102
|
-
<span className="tabular-nums">{place.rating.rating}</span>
|
|
103
|
-
</>
|
|
104
|
-
)}
|
|
105
|
-
{metaLabels?.[place.slug] && (
|
|
106
|
-
<span className="truncate">{metaLabels[place.slug]}</span>
|
|
107
|
-
)}
|
|
108
|
-
</span>
|
|
109
|
-
</span>
|
|
110
|
-
|
|
111
|
-
<ChevronRight
|
|
112
|
-
className="h-4 w-4 shrink-0 text-muted-foreground lg:hidden"
|
|
113
|
-
aria-hidden
|
|
114
|
-
/>
|
|
115
|
-
</LinkComponent>
|
|
116
|
-
|
|
117
|
-
{signature && (
|
|
118
|
-
<MenuItem
|
|
119
|
-
item={signature.item}
|
|
120
|
-
labels={labels}
|
|
121
|
-
formatPrice={formatPrice}
|
|
122
|
-
formatCount={formatCount}
|
|
123
|
-
markGradient={hues?.[place.slug]}
|
|
124
|
-
subtitle={signature.why}
|
|
125
|
-
showSignatureBadge={signature.isSignature}
|
|
126
|
-
href={signature.href}
|
|
127
|
-
linkComponent={LinkComponent}
|
|
128
|
-
className="min-w-0"
|
|
129
|
-
/>
|
|
130
|
-
)}
|
|
131
|
-
</li>
|
|
132
|
-
);
|
|
133
|
-
})}
|
|
134
|
-
</ul>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { mockPlaces } from "./menu-mocks";
|
|
3
|
-
import { VenueBusynessStrip } from "./venue-busyness-strip";
|
|
4
|
-
|
|
5
|
-
const busyness = mockPlaces[0].busyness!;
|
|
6
|
-
|
|
7
|
-
const meta: Meta<typeof VenueBusynessStrip> = {
|
|
8
|
-
title: "Menu/VenueBusynessStrip",
|
|
9
|
-
component: VenueBusynessStrip,
|
|
10
|
-
tags: ["autodocs"],
|
|
11
|
-
decorators: [
|
|
12
|
-
(Story) => (
|
|
13
|
-
<div className="w-[560px] bg-background p-4">
|
|
14
|
-
<Story />
|
|
15
|
-
</div>
|
|
16
|
-
),
|
|
17
|
-
],
|
|
18
|
-
parameters: {
|
|
19
|
-
docs: {
|
|
20
|
-
description: {
|
|
21
|
-
component:
|
|
22
|
-
"When a venue actually fills up, as a bar per hour. The API serves one score per hour of the venue's OWN trading day, so the strip has no fixed length — a bakery closing at three and a club open until four draw different numbers of bars, and padding either to 24 would put empty columns under hours the venue is shut. The rush is derived relative to that venue's own peak, because a fixed score threshold would mark every hour at a busy venue and none at a quiet one.",
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export default meta;
|
|
29
|
-
type Story = StoryObj<typeof VenueBusynessStrip>;
|
|
30
|
-
|
|
31
|
-
export const Default: Story = {
|
|
32
|
-
args: {
|
|
33
|
-
busyness,
|
|
34
|
-
caption: "Usually not busy until 18:00 — the rush is seven to eight.",
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/** No caption: the bars alone, for a tight row. */
|
|
39
|
-
export const BarsOnly: Story = { args: { busyness } };
|
|
40
|
-
|
|
41
|
-
/** A short trading day draws a short strip rather than padding to 24. */
|
|
42
|
-
export const ShortDay: Story = {
|
|
43
|
-
args: {
|
|
44
|
-
busyness: [
|
|
45
|
-
{ hour: 8, score: 40 },
|
|
46
|
-
{ hour: 9, score: 72 },
|
|
47
|
-
{ hour: 10, score: 88 },
|
|
48
|
-
{ hour: 11, score: 60 },
|
|
49
|
-
{ hour: 12, score: 34 },
|
|
50
|
-
],
|
|
51
|
-
caption: "Busiest first thing — quiet by noon.",
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/** A flat day: `peakRatio` still finds the top of it, because it is relative. */
|
|
56
|
-
export const Flat: Story = {
|
|
57
|
-
args: {
|
|
58
|
-
busyness: Array.from({ length: 10 }, (_, index) => ({ hour: 12 + index, score: 30 })),
|
|
59
|
-
caption: "Steady all day.",
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/** All zeros — no reading. The strip renders nothing rather than a flat line. */
|
|
64
|
-
export const NoReading: Story = {
|
|
65
|
-
args: { busyness: Array.from({ length: 8 }, (_, index) => ({ hour: index, score: 0 })) },
|
|
66
|
-
};
|