@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,125 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
import { ScrollFade } from "./scroll-fade";
|
|
3
|
+
import type { MenuIndexEntry, MenuLinkComponent, MenuLinkProps } from "./menu-types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MenuIndexHeader — the head of a browse index.
|
|
7
|
+
*
|
|
8
|
+
* The hub's own section title, opened up: the page reads as that section rather
|
|
9
|
+
* than as a new place, which is the whole promise of the "All 38 cuisines" link
|
|
10
|
+
* that got the reader here.
|
|
11
|
+
*
|
|
12
|
+
* The scope switch is a slot rather than a prop set, because the two scopes are
|
|
13
|
+
* two ROUTES — the caller passes a `<SegmentedNav>` with real hrefs and the DS
|
|
14
|
+
* stays out of routing.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function DefaultLink({ href, ...props }: MenuLinkProps) {
|
|
18
|
+
return <a href={href} {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type MenuIndexHeaderProps = {
|
|
22
|
+
/** Small caps over the title, e.g. "MENU · CLUJ-NAPOCA". */
|
|
23
|
+
eyebrow?: string;
|
|
24
|
+
title: string;
|
|
25
|
+
/** Sits beside the title, e.g. "38 cuisines". Pluralised by the caller. */
|
|
26
|
+
count?: string;
|
|
27
|
+
subtitle?: string;
|
|
28
|
+
/** The Food / Drinks control. A `<SegmentedNav>`, composed by the caller. */
|
|
29
|
+
scopeSwitch?: React.ReactNode;
|
|
30
|
+
className?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function MenuIndexHeader({
|
|
34
|
+
eyebrow,
|
|
35
|
+
title,
|
|
36
|
+
count,
|
|
37
|
+
subtitle,
|
|
38
|
+
scopeSwitch,
|
|
39
|
+
className,
|
|
40
|
+
}: MenuIndexHeaderProps) {
|
|
41
|
+
return (
|
|
42
|
+
<div className={cn("flex flex-col gap-4 lg:flex-row lg:items-start lg:gap-8", className)}>
|
|
43
|
+
<div className="min-w-0 flex-1">
|
|
44
|
+
{eyebrow && (
|
|
45
|
+
<p className="text-[11px] font-bold uppercase tracking-[0.1em] text-muted-foreground">
|
|
46
|
+
{eyebrow}
|
|
47
|
+
</p>
|
|
48
|
+
)}
|
|
49
|
+
<h1 className="mt-1.5 text-[30px] font-black leading-[1.05] tracking-[-0.035em] lg:text-[40px]">
|
|
50
|
+
{title}
|
|
51
|
+
{count && (
|
|
52
|
+
<span className="ml-2.5 align-middle text-base font-semibold text-muted-foreground lg:text-lg">
|
|
53
|
+
{count}
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</h1>
|
|
57
|
+
{subtitle && (
|
|
58
|
+
<p className="mt-2 max-w-[60ch] text-sm text-muted-foreground">{subtitle}</p>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Sticky under the header on the phone: the list below is several screens
|
|
63
|
+
and a reader who wants the other scope should not have to scroll back
|
|
64
|
+
up to a control they can see from anywhere else on the page. */}
|
|
65
|
+
{scopeSwitch && (
|
|
66
|
+
<div className="sticky top-0 z-10 -mx-[var(--page-gutter)] bg-background px-[var(--page-gutter)] py-2 lg:static lg:mx-0 lg:bg-transparent lg:px-0 lg:py-0">
|
|
67
|
+
{scopeSwitch}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* MenuJumpStrip — the page's contents, as chips.
|
|
76
|
+
*
|
|
77
|
+
* It answers "move between families without going back to the hub", and it
|
|
78
|
+
* doubles as a table of contents: each chip carries the family's mark and its
|
|
79
|
+
* count, so the strip says how long the page is before the reader scrolls it.
|
|
80
|
+
*
|
|
81
|
+
* Only the families drawn in full belong here. A strip listing every value in
|
|
82
|
+
* the index below would be the index, twice.
|
|
83
|
+
*/
|
|
84
|
+
export type MenuJumpStripProps = {
|
|
85
|
+
entries: MenuIndexEntry[];
|
|
86
|
+
formatCount?: (value: number) => string;
|
|
87
|
+
ariaLabel?: string;
|
|
88
|
+
linkComponent?: MenuLinkComponent;
|
|
89
|
+
className?: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export function MenuJumpStrip({
|
|
93
|
+
entries,
|
|
94
|
+
formatCount = String,
|
|
95
|
+
ariaLabel,
|
|
96
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
97
|
+
className,
|
|
98
|
+
}: MenuJumpStripProps) {
|
|
99
|
+
return (
|
|
100
|
+
<ScrollFade
|
|
101
|
+
orientation="horizontal"
|
|
102
|
+
size="sm"
|
|
103
|
+
className={className}
|
|
104
|
+
scrollClassName="scrollbar-hide"
|
|
105
|
+
>
|
|
106
|
+
<nav aria-label={ariaLabel} className="flex items-center gap-2">
|
|
107
|
+
{entries.map((entry) => (
|
|
108
|
+
<LinkComponent
|
|
109
|
+
key={entry.slug}
|
|
110
|
+
href={entry.href}
|
|
111
|
+
className="inline-flex h-9 shrink-0 items-center gap-2 whitespace-nowrap rounded-full border border-border bg-card px-4 text-sm transition-colors hover:border-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none"
|
|
112
|
+
>
|
|
113
|
+
<span
|
|
114
|
+
aria-hidden
|
|
115
|
+
className={cn("h-2.5 w-2.5 shrink-0 rounded-[3px]", !entry.gradient && "bg-border")}
|
|
116
|
+
style={entry.gradient ? { backgroundImage: entry.gradient } : undefined}
|
|
117
|
+
/>
|
|
118
|
+
{entry.name}
|
|
119
|
+
<span className="tabular-nums text-muted-foreground">{formatCount(entry.count)}</span>
|
|
120
|
+
</LinkComponent>
|
|
121
|
+
))}
|
|
122
|
+
</nav>
|
|
123
|
+
</ScrollFade>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { MenuIndexList } from "./menu-index-list";
|
|
3
|
+
import { mockIndexEntries } from "./menu-mocks";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof MenuIndexList> = {
|
|
6
|
+
title: "Menu/MenuIndexList",
|
|
7
|
+
component: MenuIndexList,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: {
|
|
10
|
+
entries: mockIndexEntries,
|
|
11
|
+
title: `All ${mockIndexEntries.length} cuisines`,
|
|
12
|
+
subtitle: "Ordered by kitchens — every one has its own page",
|
|
13
|
+
},
|
|
14
|
+
decorators: [
|
|
15
|
+
(Story) => (
|
|
16
|
+
<div className="w-[1100px] bg-background p-4">
|
|
17
|
+
<Story />
|
|
18
|
+
</div>
|
|
19
|
+
),
|
|
20
|
+
],
|
|
21
|
+
parameters: {
|
|
22
|
+
docs: {
|
|
23
|
+
description: {
|
|
24
|
+
component:
|
|
25
|
+
"The complete set, four across — and the reason the page below it can be progressive. This block carries a crawlable link to **every** destination the city has, so the richer blocks underneath are free to load ten at a time behind a *show more* without hiding anything a crawler needs. Get that backwards and the page silently stops linking to the twenty-eight it is supposed to index. Note the counts overlap: cuisines are non-exclusive tags (Cluj's thirty-eight sum to 3,182 against 767 places), so this is a ranked list of ways in, never a breakdown.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default meta;
|
|
32
|
+
type Story = StoryObj<typeof MenuIndexList>;
|
|
33
|
+
|
|
34
|
+
export const Default: Story = {};
|
|
35
|
+
|
|
36
|
+
/** The long tail, which is where a layout tuned on the head of the list breaks. */
|
|
37
|
+
export const Tail: Story = {
|
|
38
|
+
args: { entries: mockIndexEntries.slice(-8), title: "The tail" },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Two columns, not one: thirty-eight rows in one column is four screens. */
|
|
42
|
+
export const Mobile: Story = {
|
|
43
|
+
decorators: [
|
|
44
|
+
(Story) => (
|
|
45
|
+
<div className="w-[390px] bg-background p-3">
|
|
46
|
+
<Story />
|
|
47
|
+
</div>
|
|
48
|
+
),
|
|
49
|
+
],
|
|
50
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { ChevronRight } from "lucide-react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { Separator } from "./separator";
|
|
4
|
+
import { Skeleton } from "./skeleton";
|
|
5
|
+
import type { MenuIndexEntry, MenuLinkComponent, MenuLinkProps } from "./menu-types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* MenuIndexList — the complete set, four across.
|
|
9
|
+
*
|
|
10
|
+
* ## This block is why the page can afford to be progressive
|
|
11
|
+
*
|
|
12
|
+
* It carries a crawlable link to EVERY destination the city has — all
|
|
13
|
+
* thirty-eight cuisines, not the ten the page draws in full below. That makes
|
|
14
|
+
* it the page's entire link-equity job, which in turn means the rich blocks
|
|
15
|
+
* underneath are free to load ten at a time behind a "show more": nothing a
|
|
16
|
+
* crawler needs is hidden behind that control, because it is all up here.
|
|
17
|
+
*
|
|
18
|
+
* Get this backwards — put the whole set behind the disclosure and only ten
|
|
19
|
+
* links in the markup — and the page silently stops linking to twenty-eight
|
|
20
|
+
* pages it is supposed to be the index for.
|
|
21
|
+
*
|
|
22
|
+
* ## Ordered by count, and the counts overlap
|
|
23
|
+
*
|
|
24
|
+
* Cuisines are non-exclusive tags: Cluj's thirty-eight sum to 3,182 against 767
|
|
25
|
+
* places, because a trattoria is Italian AND pizza AND international. So this
|
|
26
|
+
* is a ranked list of ways in, never a partition, and no copy near it may total
|
|
27
|
+
* the numbers or call them a breakdown.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** @deprecated Alias of `MenuLinkProps`; kept for symmetry with the other menu surfaces. */
|
|
31
|
+
export type MenuIndexListLinkProps = MenuLinkProps;
|
|
32
|
+
|
|
33
|
+
function DefaultLink({ href, ...props }: MenuIndexListLinkProps) {
|
|
34
|
+
return <a href={href} {...props} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type MenuIndexListProps = {
|
|
38
|
+
entries: MenuIndexEntry[];
|
|
39
|
+
/** e.g. "All 38 cuisines". Composed and pluralised by the caller. */
|
|
40
|
+
title: string;
|
|
41
|
+
/** One line under it, e.g. "Ordered by kitchens — every one has its own page". */
|
|
42
|
+
subtitle?: string;
|
|
43
|
+
/** Locale-aware integer formatter. Defaults to `String`. */
|
|
44
|
+
formatCount?: (value: number) => string;
|
|
45
|
+
linkComponent?: MenuLinkComponent;
|
|
46
|
+
className?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function MenuIndexList({
|
|
50
|
+
entries,
|
|
51
|
+
title,
|
|
52
|
+
subtitle,
|
|
53
|
+
formatCount = String,
|
|
54
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
55
|
+
className,
|
|
56
|
+
}: MenuIndexListProps) {
|
|
57
|
+
return (
|
|
58
|
+
<section
|
|
59
|
+
className={cn(
|
|
60
|
+
"rounded-[18px] border border-border bg-card px-3.5 py-4 lg:px-5 lg:py-5",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<div className="pb-2">
|
|
65
|
+
<h2 className="text-[15px] font-bold tracking-[-0.01em]">{title}</h2>
|
|
66
|
+
{subtitle && (
|
|
67
|
+
<p className="mt-0.5 text-[12.5px] text-muted-foreground">{subtitle}</p>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/*
|
|
72
|
+
Two columns on a phone, four from `lg`. One column at 390px turns
|
|
73
|
+
thirty-eight rows into four screens of scrolling for a list whose whole
|
|
74
|
+
job is to be scanned; two halves that, and the row still holds a name of
|
|
75
|
+
realistic length beside its count.
|
|
76
|
+
*/}
|
|
77
|
+
<ul className="grid grid-cols-2 gap-x-5 lg:grid-cols-4 lg:gap-x-8">
|
|
78
|
+
{entries.map((entry) => (
|
|
79
|
+
<li key={entry.slug}>
|
|
80
|
+
{/* The DS `Separator`, above every row including the first.
|
|
81
|
+
It was `border-t … first:border-t-0 lg:first:border-t`, which
|
|
82
|
+
only exempted the very FIRST entry — so in the two-column phone
|
|
83
|
+
layout the top-left row had no rule and the top-right one, its
|
|
84
|
+
own neighbour, did. Ruling every row is both uniform and simpler
|
|
85
|
+
than restoring the exception per breakpoint. */}
|
|
86
|
+
<Separator />
|
|
87
|
+
<LinkComponent
|
|
88
|
+
href={entry.href}
|
|
89
|
+
className="group flex items-center gap-2.5 py-2.5 transition-colors hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none"
|
|
90
|
+
>
|
|
91
|
+
<span
|
|
92
|
+
aria-hidden
|
|
93
|
+
className={cn(
|
|
94
|
+
"h-2.5 w-2.5 shrink-0 rounded-[3px]",
|
|
95
|
+
!entry.gradient && "bg-border"
|
|
96
|
+
)}
|
|
97
|
+
style={entry.gradient ? { backgroundImage: entry.gradient } : undefined}
|
|
98
|
+
/>
|
|
99
|
+
<span className="min-w-0 flex-1 truncate text-[13.5px]">{entry.name}</span>
|
|
100
|
+
<span className="shrink-0 text-[13px] tabular-nums text-muted-foreground">
|
|
101
|
+
{formatCount(entry.count)}
|
|
102
|
+
</span>
|
|
103
|
+
<ChevronRight
|
|
104
|
+
className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-colors group-hover:text-accent motion-reduce:transition-none"
|
|
105
|
+
aria-hidden
|
|
106
|
+
/>
|
|
107
|
+
</LinkComponent>
|
|
108
|
+
</li>
|
|
109
|
+
))}
|
|
110
|
+
</ul>
|
|
111
|
+
</section>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Skeleton ───────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/** Two columns on a phone, four from `lg` — the grid the list itself renders. */
|
|
118
|
+
export function MenuIndexListSkeleton({
|
|
119
|
+
rows = 24,
|
|
120
|
+
className,
|
|
121
|
+
}: {
|
|
122
|
+
rows?: number;
|
|
123
|
+
className?: string;
|
|
124
|
+
}) {
|
|
125
|
+
return (
|
|
126
|
+
<section
|
|
127
|
+
className={cn(
|
|
128
|
+
"rounded-[18px] border border-border bg-card px-3.5 py-4 lg:px-5 lg:py-5",
|
|
129
|
+
className
|
|
130
|
+
)}
|
|
131
|
+
aria-hidden
|
|
132
|
+
>
|
|
133
|
+
<Skeleton className="h-[15px] w-[140px]" />
|
|
134
|
+
<Skeleton className="mt-1.5 h-3 w-[240px]" />
|
|
135
|
+
<ul className="mt-2 grid grid-cols-2 gap-x-5 lg:grid-cols-4 lg:gap-x-8">
|
|
136
|
+
{Array.from({ length: rows }, (_, index) => (
|
|
137
|
+
<li key={index}>
|
|
138
|
+
<Separator />
|
|
139
|
+
<div className="flex items-center gap-2.5 py-2.5">
|
|
140
|
+
<Skeleton className="h-2.5 w-2.5 rounded-[3px]" />
|
|
141
|
+
<Skeleton className="h-3.5 flex-1" />
|
|
142
|
+
<Skeleton className="h-3 w-6" />
|
|
143
|
+
</div>
|
|
144
|
+
</li>
|
|
145
|
+
))}
|
|
146
|
+
</ul>
|
|
147
|
+
</section>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Clock, Pizza } from "lucide-react";
|
|
3
|
+
import { MenuIntentHero } from "./menu-intent-hero";
|
|
4
|
+
import { mockGradients, mockIntentFigures } from "./menu-mocks";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof MenuIntentHero> = {
|
|
7
|
+
title: "Menu/MenuIntentHero",
|
|
8
|
+
component: MenuIntentHero,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
decorators: [
|
|
11
|
+
(Story) => (
|
|
12
|
+
<div className="w-[1100px] bg-background p-4">
|
|
13
|
+
<Story />
|
|
14
|
+
</div>
|
|
15
|
+
),
|
|
16
|
+
],
|
|
17
|
+
parameters: {
|
|
18
|
+
docs: {
|
|
19
|
+
description: {
|
|
20
|
+
component:
|
|
21
|
+
"The head of an intent page. Every number in it is real and every one of them is a control — tapping a figure applies it as a filter rather than scrolling somewhere, so the hero is also the first filter. A figure with no `onSelect` renders as a plain fact, which is normally the total, since filtering to *all of them* is not a state. Distinct from `MenuHubHero`: that one introduces a city's whole menu and carries a search field; this one introduces a single intent.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
type Story = StoryObj<typeof MenuIntentHero>;
|
|
29
|
+
|
|
30
|
+
const base = {
|
|
31
|
+
title: "Italian tonight",
|
|
32
|
+
kicker: "TONIGHT · OPEN NOW OR LATER",
|
|
33
|
+
kickerIcon: <Clock className="h-3 w-3" />,
|
|
34
|
+
description:
|
|
35
|
+
"Two hundred and forty-seven kitchens in Cluj-Napoca cook Italian. Fifty-eight are still serving, four past midnight. Ranked by how often the reviews name their dishes.",
|
|
36
|
+
figures: mockIntentFigures,
|
|
37
|
+
gradient: mockGradients.pasta,
|
|
38
|
+
icon: <Pizza />,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const Default: Story = { args: base };
|
|
42
|
+
|
|
43
|
+
/** No constraint: the kicker states the kind instead, and the figures still filter. */
|
|
44
|
+
export const Bare: Story = {
|
|
45
|
+
args: { ...base, title: "Italian", kicker: "CUISINE", kickerIcon: undefined },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Four across at 390px, and the paragraph stays.
|
|
50
|
+
*
|
|
51
|
+
* The board hid it under 640px. It is clamped here instead — it is the page's
|
|
52
|
+
* only prose and what the description meta is built from, so `display: none` on
|
|
53
|
+
* the one indexed sentence costs more than the three lines it saves.
|
|
54
|
+
*/
|
|
55
|
+
export const Mobile: Story = {
|
|
56
|
+
args: base,
|
|
57
|
+
decorators: [
|
|
58
|
+
(Story) => (
|
|
59
|
+
<div className="w-[390px] bg-background p-3">
|
|
60
|
+
<Story />
|
|
61
|
+
</div>
|
|
62
|
+
),
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** No gradient — visibly unstyled rather than white text on white. */
|
|
67
|
+
export const NoGradient: Story = { args: { ...base, gradient: undefined, icon: undefined } };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
import type { MenuIntentFigure, MenuLinkComponent, MenuLinkProps } from "./menu-types";
|
|
3
|
+
|
|
4
|
+
function DefaultLink({ href, ...props }: MenuLinkProps) {
|
|
5
|
+
return <a href={href} {...props} />;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* MenuIntentHero — the head of an intent page (`/menu/cuisines/italian`).
|
|
10
|
+
*
|
|
11
|
+
* The intent page answers "which kitchens, for this, right now", and the hero's
|
|
12
|
+
* job is to make the size of the answer legible before the reader touches a
|
|
13
|
+
* filter: how many kitchens cook this, what they are known for, how many are
|
|
14
|
+
* serving. Every number in it is real and every one of them is a control.
|
|
15
|
+
*
|
|
16
|
+
* ## Distinct from `<MenuHubHero>`
|
|
17
|
+
*
|
|
18
|
+
* The hub's hero introduces a city's whole menu and carries a search field.
|
|
19
|
+
* This one introduces ONE intent and carries figures that filter the list
|
|
20
|
+
* below it. They share a look — a tinted panel with a watermark — and nothing
|
|
21
|
+
* else, so they are two components rather than one with a mode.
|
|
22
|
+
*
|
|
23
|
+
* ## The figures are the first filter
|
|
24
|
+
*
|
|
25
|
+
* "58 open now" is both a fact and the fastest way to act on it, so tapping a
|
|
26
|
+
* figure applies it as a filter rather than scrolling somewhere. A figure with
|
|
27
|
+
* no `onSelect` renders as a plain fact: the total is normally one, because
|
|
28
|
+
* filtering to "all of them" is not a state.
|
|
29
|
+
*
|
|
30
|
+
* They stay four across on the phone. The board proved the row legible at
|
|
31
|
+
* 390px, and a 2×2 costs ~40px of height in the tallest element on the
|
|
32
|
+
* smallest screen; the labels wrap instead.
|
|
33
|
+
*
|
|
34
|
+
* ## The paragraph survives the phone
|
|
35
|
+
*
|
|
36
|
+
* The board hides it under 640px. It is kept here and clamped instead: it is
|
|
37
|
+
* the page's only prose, it is what the description meta is built from, and
|
|
38
|
+
* `display: none` on the one indexed sentence is a cost the extra three lines
|
|
39
|
+
* do not justify.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export type MenuIntentHeroProps = {
|
|
43
|
+
title: string;
|
|
44
|
+
/**
|
|
45
|
+
* Over the title — the active constraint ("TONIGHT · OPEN NOW OR LATER"), or
|
|
46
|
+
* the intent's kind ("CUISINE") when there is none.
|
|
47
|
+
*/
|
|
48
|
+
kicker?: string;
|
|
49
|
+
/** Icon node for the kicker, resolved by the caller. */
|
|
50
|
+
kickerIcon?: React.ReactNode;
|
|
51
|
+
/** One paragraph of real numbers. Generated by the caller from the facets. */
|
|
52
|
+
description?: string;
|
|
53
|
+
figures?: MenuIntentFigure[];
|
|
54
|
+
/**
|
|
55
|
+
* Finished CSS gradient for the ground, e.g. `linear-gradient(140deg, …)`.
|
|
56
|
+
*
|
|
57
|
+
* A gradient rather than a token name, for the same reason `<MenuField>` and
|
|
58
|
+
* `<MenuIntentTiles>` take one: the hue tables are app-owned, and a component
|
|
59
|
+
* referencing `var(--h-pizza)` renders an unstyled panel in any consumer that
|
|
60
|
+
* does not define it. Missing, the panel falls back to the foreground colour
|
|
61
|
+
* — visibly unstyled rather than white text on white.
|
|
62
|
+
*/
|
|
63
|
+
gradient?: string;
|
|
64
|
+
/** Watermark behind the text, resolved by the caller. */
|
|
65
|
+
icon?: React.ReactNode;
|
|
66
|
+
/** Used for any figure carrying an `href`. Defaults to a plain `<a>`. */
|
|
67
|
+
linkComponent?: MenuLinkComponent;
|
|
68
|
+
className?: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function MenuIntentHero({
|
|
72
|
+
title,
|
|
73
|
+
kicker,
|
|
74
|
+
kickerIcon,
|
|
75
|
+
description,
|
|
76
|
+
figures,
|
|
77
|
+
gradient,
|
|
78
|
+
icon,
|
|
79
|
+
linkComponent = DefaultLink,
|
|
80
|
+
className,
|
|
81
|
+
}: MenuIntentHeroProps) {
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
className={cn(
|
|
85
|
+
"relative flex overflow-hidden rounded-[18px] text-white lg:min-h-[210px]",
|
|
86
|
+
!gradient && "bg-foreground",
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
{gradient && (
|
|
91
|
+
<span aria-hidden className="absolute inset-0" style={{ backgroundImage: gradient }} />
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{icon && (
|
|
95
|
+
<span
|
|
96
|
+
aria-hidden
|
|
97
|
+
className="pointer-events-none absolute -bottom-7 -right-4 opacity-20 [&>svg]:h-[120px] [&>svg]:w-[120px] lg:[&>svg]:h-[170px] lg:[&>svg]:w-[170px]"
|
|
98
|
+
>
|
|
99
|
+
{icon}
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<div className="relative z-[1] flex w-full flex-col gap-2.5 p-5 lg:gap-[11px] lg:px-[26px] lg:py-6">
|
|
104
|
+
{kicker && (
|
|
105
|
+
<span className="flex items-center gap-1.5 text-[10.5px] font-extrabold uppercase tracking-[0.13em] opacity-[0.82]">
|
|
106
|
+
{kickerIcon}
|
|
107
|
+
{kicker}
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
<h1 className="font-serif text-[29px] font-semibold leading-[1.04] tracking-[-0.025em] lg:text-[40px]">
|
|
112
|
+
{title}
|
|
113
|
+
</h1>
|
|
114
|
+
|
|
115
|
+
{description && (
|
|
116
|
+
<p className="line-clamp-3 max-w-[520px] text-[13.5px] leading-[1.55] opacity-85 lg:line-clamp-none lg:text-sm">
|
|
117
|
+
{description}
|
|
118
|
+
</p>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{figures && figures.length > 0 && (
|
|
122
|
+
<div className="mt-auto grid grid-cols-4 gap-x-4 gap-y-3 pt-3.5 lg:flex lg:gap-[26px] lg:pt-4">
|
|
123
|
+
{figures.map((figure) => (
|
|
124
|
+
<Figure key={figure.key} figure={figure} linkComponent={linkComponent} />
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A figure is a control only when it does something.
|
|
135
|
+
*
|
|
136
|
+
* The alternative — every figure a button, the inert ones disabled — offers
|
|
137
|
+
* four controls and refuses one of them for a reason the reader cannot see.
|
|
138
|
+
*
|
|
139
|
+
* A LINK when it carries an `href`, a button only when the caller is holding
|
|
140
|
+
* the state in memory. The link is the better shape wherever the state is in
|
|
141
|
+
* the URL: it renders on the server, it is middle-clickable, and `?open=now` is
|
|
142
|
+
* a state we want a crawler to follow.
|
|
143
|
+
*/
|
|
144
|
+
function Figure({
|
|
145
|
+
figure,
|
|
146
|
+
linkComponent: LinkComponent,
|
|
147
|
+
}: {
|
|
148
|
+
figure: MenuIntentFigure;
|
|
149
|
+
linkComponent: MenuLinkComponent;
|
|
150
|
+
}) {
|
|
151
|
+
const body = (
|
|
152
|
+
<>
|
|
153
|
+
<span className="block text-xl font-black leading-none tracking-[-0.03em] tabular-nums lg:text-2xl">
|
|
154
|
+
{figure.value}
|
|
155
|
+
</span>
|
|
156
|
+
<span className="mt-1 block text-[11px] leading-tight opacity-[0.72]">{figure.label}</span>
|
|
157
|
+
</>
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const interactive = cn(
|
|
161
|
+
"block min-w-0 rounded-lg text-left transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 motion-reduce:transition-none",
|
|
162
|
+
figure.active ? "opacity-100" : "opacity-90"
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (figure.href) {
|
|
166
|
+
return (
|
|
167
|
+
<LinkComponent
|
|
168
|
+
href={figure.href}
|
|
169
|
+
aria-current={figure.active ? "page" : undefined}
|
|
170
|
+
className={interactive}
|
|
171
|
+
>
|
|
172
|
+
{body}
|
|
173
|
+
</LinkComponent>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!figure.onSelect) return <span className="block min-w-0">{body}</span>;
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
onClick={figure.onSelect}
|
|
183
|
+
aria-pressed={figure.active}
|
|
184
|
+
className={interactive}
|
|
185
|
+
>
|
|
186
|
+
{body}
|
|
187
|
+
</button>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { MenuKitchenList } from "./menu-kitchen-list";
|
|
3
|
+
import { mockKitchens, mockMatchedKitchens } from "./menu-mocks";
|
|
4
|
+
|
|
5
|
+
const labels = {
|
|
6
|
+
openVenue: (name: string) => `Open ${name}`,
|
|
7
|
+
openDish: (name: string) => `See ${name}`,
|
|
8
|
+
openNow: "Open now",
|
|
9
|
+
showMore: "Show 10 more",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof MenuKitchenList> = {
|
|
13
|
+
title: "Menu/MenuKitchenList",
|
|
14
|
+
component: MenuKitchenList,
|
|
15
|
+
tags: ["autodocs"],
|
|
16
|
+
args: { labels },
|
|
17
|
+
decorators: [
|
|
18
|
+
(Story) => (
|
|
19
|
+
<div className="w-[1100px] bg-background p-4">
|
|
20
|
+
<Story />
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
],
|
|
24
|
+
parameters: {
|
|
25
|
+
docs: {
|
|
26
|
+
description: {
|
|
27
|
+
component:
|
|
28
|
+
"The intent page's spine — a shortlist of rooms you could walk into, not a grid of dishes. **Two targets per row**: the row goes to the venue, the dish name opens the item drawer. That is why this is not `RankedVenueList`, which wraps its whole row in one link and so cannot express a second destination. The mechanism is a stretched overlay, as in `MenuItem`; nesting one anchor inside another is invalid and breaks middle-click on both. The dish is underlined so it reads as a target before it is pressed, and the chevron stays on the right where it belongs to the venue.",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default meta;
|
|
35
|
+
type Story = StoryObj<typeof MenuKitchenList>;
|
|
36
|
+
|
|
37
|
+
export const Default: Story = { args: { kitchens: mockKitchens } };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Filtered: every row swaps its most-named dish for the one that matched, and
|
|
41
|
+
* the kicker changes with it. A filtered list whose rows still advertise their
|
|
42
|
+
* most-named dish is claiming the filter matched something it did not.
|
|
43
|
+
*/
|
|
44
|
+
export const Matched: Story = { args: { kitchens: mockMatchedKitchens } };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The degradations, all from real API gaps: row 3's must-try has no `slug`, so
|
|
48
|
+
* the dish line is inert rather than a link to nowhere; row 4 has no dish at
|
|
49
|
+
* all and must still read as a venue; neither carries a mention count, because
|
|
50
|
+
* a list endpoint does not serve one.
|
|
51
|
+
*/
|
|
52
|
+
export const Degraded: Story = { args: { kitchens: mockKitchens.slice(2) } };
|
|
53
|
+
|
|
54
|
+
export const WithShowMore: Story = {
|
|
55
|
+
args: { kitchens: mockKitchens, onShowMore: () => {} },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const Mobile: Story = {
|
|
59
|
+
args: { kitchens: mockKitchens },
|
|
60
|
+
decorators: [
|
|
61
|
+
(Story) => (
|
|
62
|
+
<div className="w-[390px] bg-background p-3">
|
|
63
|
+
<Story />
|
|
64
|
+
</div>
|
|
65
|
+
),
|
|
66
|
+
],
|
|
67
|
+
};
|