@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
|
@@ -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
|
-
};
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { cn } from "../lib/utils";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* VenueBusynessStrip — when a venue actually fills up, as a bar per hour.
|
|
5
|
-
*
|
|
6
|
-
* The API serves one score per hour of the venue's own trading day, so the
|
|
7
|
-
* strip has no fixed length: a bakery closing at three and a club open until
|
|
8
|
-
* four draw different numbers of bars, and padding either to 24 would put empty
|
|
9
|
-
* columns under hours the venue is not open.
|
|
10
|
-
*
|
|
11
|
-
* The rush is derived rather than passed: bars at or above `peakRatio` of the
|
|
12
|
-
* day's own maximum take the accent. A fixed score threshold would mark every
|
|
13
|
-
* hour at a busy venue and none at a quiet one — busyness is only meaningful
|
|
14
|
-
* relative to that venue's own day.
|
|
15
|
-
*
|
|
16
|
-
* `caption` is composed by the caller ("Usually not busy until 18:00 — the rush
|
|
17
|
-
* is nine to eleven"), because the sentence needs the venue's clock, its locale
|
|
18
|
-
* and a plural rule the DS does not own.
|
|
19
|
-
*
|
|
20
|
-
* Server-safe.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
export type VenueBusynessStripProps = {
|
|
24
|
-
busyness: Array<{ hour: number; score: number }>;
|
|
25
|
-
/** Composed sentence beside the bars. */
|
|
26
|
-
caption?: string;
|
|
27
|
-
/** Share of the day's own peak at which an hour counts as the rush. */
|
|
28
|
-
peakRatio?: number;
|
|
29
|
-
className?: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const BAR_MAX_HEIGHT = 22;
|
|
33
|
-
|
|
34
|
-
export function VenueBusynessStrip({
|
|
35
|
-
busyness,
|
|
36
|
-
caption,
|
|
37
|
-
peakRatio = 0.8,
|
|
38
|
-
className,
|
|
39
|
-
}: VenueBusynessStripProps) {
|
|
40
|
-
if (busyness.length === 0) return null;
|
|
41
|
-
|
|
42
|
-
const peak = Math.max(...busyness.map((entry) => entry.score));
|
|
43
|
-
if (peak <= 0) return null;
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<div
|
|
47
|
-
className={cn("flex items-center gap-2.5 text-[12.5px] text-muted-foreground", className)}
|
|
48
|
-
>
|
|
49
|
-
<span aria-hidden className="flex h-[22px] items-end gap-[3px]">
|
|
50
|
-
{busyness.map(({ hour, score }) => (
|
|
51
|
-
<i
|
|
52
|
-
key={hour}
|
|
53
|
-
className={cn(
|
|
54
|
-
"w-[7px] rounded-t-sm bg-[oklch(0.88_0.02_45)]",
|
|
55
|
-
score >= peak * peakRatio && "bg-[var(--accent,currentColor)]"
|
|
56
|
-
)}
|
|
57
|
-
style={{ height: Math.max(2, Math.round((score / peak) * BAR_MAX_HEIGHT)) }}
|
|
58
|
-
/>
|
|
59
|
-
))}
|
|
60
|
-
</span>
|
|
61
|
-
{caption}
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { mockGradients, mockVenues } from "./menu-mocks";
|
|
3
|
-
import { VenueSignatureBlock } from "./venue-signature-block";
|
|
4
|
-
|
|
5
|
-
const labels = {
|
|
6
|
-
currency: "lei",
|
|
7
|
-
mentions: (count: number) => `${count} mentions`,
|
|
8
|
-
signature: "SIGNATURE",
|
|
9
|
-
mentioned: "MENTIONED",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const meta: Meta<typeof VenueSignatureBlock> = {
|
|
13
|
-
title: "Menu/VenueSignatureBlock",
|
|
14
|
-
component: VenueSignatureBlock,
|
|
15
|
-
tags: ["autodocs"],
|
|
16
|
-
decorators: [
|
|
17
|
-
(Story) => (
|
|
18
|
-
<div className="w-[520px] bg-background p-4">
|
|
19
|
-
<Story />
|
|
20
|
-
</div>
|
|
21
|
-
),
|
|
22
|
-
],
|
|
23
|
-
parameters: {
|
|
24
|
-
docs: {
|
|
25
|
-
description: {
|
|
26
|
-
component:
|
|
27
|
-
"\"What it is known for\": the one dish or drink a venue's reviews single out, on a tinted band. It is a `MenuItem` with a kicker — the same row the venue's own menu renders, so the item does not look like a different object here. The kicker sits above the row rather than inside it; inline at 9.5px it competed with the dish name for the same line.",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export default meta;
|
|
34
|
-
type Story = StoryObj<typeof VenueSignatureBlock>;
|
|
35
|
-
|
|
36
|
-
export const Default: Story = {
|
|
37
|
-
args: {
|
|
38
|
-
item: mockVenues[0].signature!.item,
|
|
39
|
-
labels,
|
|
40
|
-
formatPrice: (amount: number) => String(amount),
|
|
41
|
-
kicker: "WHAT IT IS KNOWN FOR",
|
|
42
|
-
markGradient: mockGradients.meat,
|
|
43
|
-
subtitle: mockVenues[0].signature!.why,
|
|
44
|
-
href: "/menu/dishes/chicken-wings",
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/** A drink signature — same block, different scope. */
|
|
49
|
-
export const Drink: Story = {
|
|
50
|
-
args: {
|
|
51
|
-
item: mockVenues[2].signature!.item,
|
|
52
|
-
labels,
|
|
53
|
-
formatPrice: (amount: number) => String(amount),
|
|
54
|
-
kicker: "WHAT THEY POUR",
|
|
55
|
-
markGradient: mockGradients.cocktail,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/** Inert: no `href`. Used inside a surface that is itself a link. */
|
|
60
|
-
export const Static: Story = {
|
|
61
|
-
args: {
|
|
62
|
-
item: mockVenues[1].signature!.item,
|
|
63
|
-
labels,
|
|
64
|
-
formatPrice: (amount: number) => String(amount),
|
|
65
|
-
kicker: "WHAT IT IS KNOWN FOR",
|
|
66
|
-
markGradient: mockGradients.pasta,
|
|
67
|
-
},
|
|
68
|
-
};
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { cn } from "../lib/utils";
|
|
2
|
-
import { MenuItem, type MenuItemLabels, type MenuItemLinkProps } from "./menu-item";
|
|
3
|
-
import type { MenuItemPreview } from "./menu-types";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* VenueSignatureBlock — "what it is known for": the one dish or drink a venue's
|
|
7
|
-
* reviews single out, on a tinted band.
|
|
8
|
-
*
|
|
9
|
-
* ## It is a `<MenuItem>` with a kicker
|
|
10
|
-
*
|
|
11
|
-
* It used to be its own layout — swatch, label, name, price, all hand-placed —
|
|
12
|
-
* and it drifted from the row every other surface renders. It is now the
|
|
13
|
-
* production row on an accent ground, so a dish looks like itself here, in the
|
|
14
|
-
* "known for" list, and on the venue's own menu.
|
|
15
|
-
*
|
|
16
|
-
* The kicker sits ABOVE the row rather than inside it. Inline at 9.5px it
|
|
17
|
-
* competed with the dish name for the same line, which is how it ended up
|
|
18
|
-
* stacked one word per line.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
export type VenueSignatureBlockProps = {
|
|
22
|
-
item: MenuItemPreview;
|
|
23
|
-
labels: MenuItemLabels;
|
|
24
|
-
formatPrice: (amount: number) => string;
|
|
25
|
-
formatCount?: (value: number) => string;
|
|
26
|
-
/** Small caps kicker, e.g. "WHAT IT IS KNOWN FOR". */
|
|
27
|
-
kicker: string;
|
|
28
|
-
/** The row's category mark, as a CSS `background-image` value. */
|
|
29
|
-
markGradient?: string;
|
|
30
|
-
/** Reviewers' reason, in place of the item description. */
|
|
31
|
-
subtitle?: string;
|
|
32
|
-
href?: string;
|
|
33
|
-
linkComponent?: React.ComponentType<MenuItemLinkProps>;
|
|
34
|
-
className?: string;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export function VenueSignatureBlock({
|
|
38
|
-
item,
|
|
39
|
-
labels,
|
|
40
|
-
formatPrice,
|
|
41
|
-
formatCount,
|
|
42
|
-
kicker,
|
|
43
|
-
markGradient,
|
|
44
|
-
subtitle,
|
|
45
|
-
href,
|
|
46
|
-
linkComponent,
|
|
47
|
-
className,
|
|
48
|
-
}: VenueSignatureBlockProps) {
|
|
49
|
-
return (
|
|
50
|
-
<div className={cn("overflow-hidden rounded-xl bg-accent-subtle py-1", className)}>
|
|
51
|
-
<span className="block px-4 pt-2 text-[9.5px] font-bold uppercase tracking-[0.09em] text-muted-foreground">
|
|
52
|
-
{kicker}
|
|
53
|
-
</span>
|
|
54
|
-
|
|
55
|
-
<MenuItem
|
|
56
|
-
item={item}
|
|
57
|
-
labels={labels}
|
|
58
|
-
formatPrice={formatPrice}
|
|
59
|
-
formatCount={formatCount}
|
|
60
|
-
markGradient={markGradient}
|
|
61
|
-
subtitle={subtitle}
|
|
62
|
-
showSignatureBadge={false}
|
|
63
|
-
href={href}
|
|
64
|
-
linkComponent={linkComponent}
|
|
65
|
-
// The band is already the emphasis; the row must not also take a hover
|
|
66
|
-
// fill inside it.
|
|
67
|
-
className="min-h-0 md:rounded-none md:hover:bg-transparent md:hover:shadow-none"
|
|
68
|
-
/>
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { Button } from "./button";
|
|
3
|
-
import { mockFormatters, mockGradients, mockPlaces, mockVenues } from "./menu-mocks";
|
|
4
|
-
import { VenueSpotlight } from "./venue-spotlight";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const itemLabels = {
|
|
8
|
-
currency: "lei",
|
|
9
|
-
mentions: (count: number) => `${count} mentions`,
|
|
10
|
-
signature: "SIGNATURE",
|
|
11
|
-
mentioned: "MENTIONED",
|
|
12
|
-
};
|
|
13
|
-
const formatPrice = (amount: number) => String(amount);
|
|
14
|
-
|
|
15
|
-
const labels = {
|
|
16
|
-
photoCredit: "venue photograph",
|
|
17
|
-
openUntil: "Kitchen open until 23:00",
|
|
18
|
-
signatureKicker: "WHAT IT IS KNOWN FOR",
|
|
19
|
-
reviewCount: "318 reviews",
|
|
20
|
-
dimensions: {
|
|
21
|
-
food: "Food",
|
|
22
|
-
coffee_quality: "Coffee",
|
|
23
|
-
drinks_quality: "Drinks",
|
|
24
|
-
service: "Service",
|
|
25
|
-
value: "Value",
|
|
26
|
-
ambiance: "Ambiance",
|
|
27
|
-
portion: "Portion",
|
|
28
|
-
consistency: "Consistency",
|
|
29
|
-
},
|
|
30
|
-
tags: { casual: "casual", cozy: "cozy", family_friendly: "family friendly" },
|
|
31
|
-
valueVerdict: "Good value",
|
|
32
|
-
busyness: "Usually not busy until 18:00 — the rush is seven to eight.",
|
|
33
|
-
cite: (count: number) => `Read ${count} reviews`,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const meta: Meta<typeof VenueSpotlight> = {
|
|
37
|
-
title: "Menu/VenueSpotlight",
|
|
38
|
-
component: VenueSpotlight,
|
|
39
|
-
tags: ["autodocs"],
|
|
40
|
-
decorators: [
|
|
41
|
-
(Story) => (
|
|
42
|
-
<div className="w-[1100px] bg-background p-4">
|
|
43
|
-
<Story />
|
|
44
|
-
</div>
|
|
45
|
-
),
|
|
46
|
-
],
|
|
47
|
-
parameters: {
|
|
48
|
-
docs: {
|
|
49
|
-
description: {
|
|
50
|
-
component:
|
|
51
|
-
"One venue given real space: photograph, what it is known for, what reviewers say, how it grades, and when it fills up. The photograph is the point — this is the one module on a menu hub that shows an image, and it can, because venue photography is real and licensed where item photography is generated. Composed from `VenueSignatureBlock`, `VenueQuotes`, `VenueGradeDimensions` and `VenueBusynessStrip`, each exported on its own for consumers building a narrower variant.",
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export default meta;
|
|
58
|
-
type Story = StoryObj<typeof VenueSpotlight>;
|
|
59
|
-
|
|
60
|
-
export const Default: Story = {
|
|
61
|
-
args: {
|
|
62
|
-
place: mockPlaces[0],
|
|
63
|
-
href: mockVenues[0].href,
|
|
64
|
-
labels,
|
|
65
|
-
meta: "pub · Centru · 318 reviews read",
|
|
66
|
-
signature: mockVenues[0].signature,
|
|
67
|
-
signatureGradient: mockGradients.meat,
|
|
68
|
-
formatScore: mockFormatters.score,
|
|
69
|
-
itemLabels,
|
|
70
|
-
formatPrice,
|
|
71
|
-
action: <Button size="sm">Open the menu</Button>,
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/** A coffee bar: no food dimension, and a `frequent` signature rather than a signature one. */
|
|
76
|
-
export const CoffeeBar: Story = {
|
|
77
|
-
args: {
|
|
78
|
-
place: mockPlaces[2],
|
|
79
|
-
href: mockVenues[2].href,
|
|
80
|
-
labels: { ...labels, openUntil: "Open until 20:00", reviewCount: "96 reviews" },
|
|
81
|
-
meta: "coffee · Centru",
|
|
82
|
-
signature: mockVenues[2].signature,
|
|
83
|
-
signatureGradient: mockGradients.sweet,
|
|
84
|
-
formatScore: mockFormatters.score,
|
|
85
|
-
itemLabels,
|
|
86
|
-
formatPrice,
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* A venue the review census has not reached: no digest at all. Every block that
|
|
92
|
-
* depends on one drops out and the card still reads as a venue.
|
|
93
|
-
*/
|
|
94
|
-
export const WithoutDigest: Story = {
|
|
95
|
-
args: {
|
|
96
|
-
place: mockPlaces[3],
|
|
97
|
-
href: mockVenues[3].href,
|
|
98
|
-
labels: { ...labels, openUntil: undefined, reviewCount: "141 reviews" },
|
|
99
|
-
meta: "restaurant · Mărăști",
|
|
100
|
-
formatScore: mockFormatters.score,
|
|
101
|
-
itemLabels,
|
|
102
|
-
formatPrice,
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
/** Quotes wired to their evidence. Needs a client caller. */
|
|
107
|
-
export const CitableQuotes: Story = {
|
|
108
|
-
args: {
|
|
109
|
-
place: mockPlaces[0],
|
|
110
|
-
href: mockVenues[0].href,
|
|
111
|
-
labels,
|
|
112
|
-
meta: "pub · Centru",
|
|
113
|
-
signature: mockVenues[0].signature,
|
|
114
|
-
signatureGradient: mockGradients.meat,
|
|
115
|
-
formatScore: mockFormatters.score,
|
|
116
|
-
itemLabels,
|
|
117
|
-
formatPrice,
|
|
118
|
-
onCite: () => {},
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
export const Mobile: Story = {
|
|
123
|
-
args: {
|
|
124
|
-
place: mockPlaces[0],
|
|
125
|
-
href: mockVenues[0].href,
|
|
126
|
-
labels,
|
|
127
|
-
meta: "pub · Centru",
|
|
128
|
-
signature: mockVenues[0].signature,
|
|
129
|
-
signatureGradient: mockGradients.meat,
|
|
130
|
-
formatScore: mockFormatters.score,
|
|
131
|
-
itemLabels,
|
|
132
|
-
formatPrice,
|
|
133
|
-
},
|
|
134
|
-
decorators: [
|
|
135
|
-
(Story) => (
|
|
136
|
-
<div className="w-[390px] bg-background p-3">
|
|
137
|
-
<Story />
|
|
138
|
-
</div>
|
|
139
|
-
),
|
|
140
|
-
],
|
|
141
|
-
};
|