@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,28 +1,32 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { MenuFamilyCard } from "./menu-family-card";
|
|
3
|
-
import { mockDrinkFamily, mockFamily } from "./menu-mocks";
|
|
4
|
-
|
|
5
|
-
const itemLabels = {
|
|
6
|
-
currency: "lei",
|
|
7
|
-
mentions: (count: number) => `${count} mentions`,
|
|
8
|
-
signature: "SIGNATURE",
|
|
9
|
-
mentioned: "MENTIONED",
|
|
10
|
-
};
|
|
11
|
-
const formatPrice = (amount: number) => String(amount);
|
|
3
|
+
import { mockDrinkFamily, mockFamily, mockGradients } from "./menu-mocks";
|
|
12
4
|
|
|
13
5
|
const labels = {
|
|
14
|
-
venueUnit: "
|
|
15
|
-
mostNamed: "Most named",
|
|
6
|
+
venueUnit: "kitchens",
|
|
16
7
|
priceBand: "kitchen price band",
|
|
8
|
+
browse: "Browse Italian",
|
|
17
9
|
};
|
|
18
10
|
|
|
11
|
+
/** A stand-in photograph with enough variance that the ink has something to sit on. */
|
|
12
|
+
function photo(hue: number, label: string) {
|
|
13
|
+
return (
|
|
14
|
+
"data:image/svg+xml;utf8," +
|
|
15
|
+
encodeURIComponent(
|
|
16
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="320" height="240"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 30% 62%)"/><stop offset="1" stop-color="hsl(${hue} 20% 28%)"/></linearGradient></defs><rect width="320" height="240" fill="url(#g)"/><circle cx="80" cy="70" r="44" fill="hsl(${hue} 40% 80%)" opacity=".6"/><rect x="170" y="130" width="120" height="80" fill="hsl(${hue} 15% 20%)" opacity=".7"/><text x="50%" y="88%" font-family="monospace" font-size="16" fill="#fff" text-anchor="middle">${label}</text></svg>`
|
|
17
|
+
)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PHOTOS = [40, 200, 90, 300].map((h, i) => photo(h, `venue ${i + 1}`));
|
|
22
|
+
|
|
19
23
|
const meta: Meta<typeof MenuFamilyCard> = {
|
|
20
24
|
title: "Menu/MenuFamilyCard",
|
|
21
25
|
component: MenuFamilyCard,
|
|
22
26
|
tags: ["autodocs"],
|
|
23
27
|
decorators: [
|
|
24
28
|
(Story) => (
|
|
25
|
-
<div className="w-[
|
|
29
|
+
<div className="w-[360px] bg-background p-4">
|
|
26
30
|
<Story />
|
|
27
31
|
</div>
|
|
28
32
|
),
|
|
@@ -31,7 +35,7 @@ const meta: Meta<typeof MenuFamilyCard> = {
|
|
|
31
35
|
docs: {
|
|
32
36
|
description: {
|
|
33
37
|
component:
|
|
34
|
-
"
|
|
38
|
+
"The hub's doorway to a family — a cuisine on the dishes tab, a drink family on the drinks tab. The same photo language as `MenuFamilyBlock` (the mosaic is the ground, wearing the family's hue; an ink gradient carries the type) drawn one step earlier, in a row of three, where the reader is choosing a family and not yet a venue: one number, one name, the groups the family consists of, and the `Browse` pill. It lists no venue. The count is VENUES: cuisine exists only on places, so a card counting dishes cannot be built. The whole card is one link.",
|
|
35
39
|
},
|
|
36
40
|
},
|
|
37
41
|
},
|
|
@@ -42,12 +46,11 @@ type Story = StoryObj<typeof MenuFamilyCard>;
|
|
|
42
46
|
|
|
43
47
|
export const Default: Story = {
|
|
44
48
|
args: {
|
|
45
|
-
family: mockFamily,
|
|
49
|
+
family: { ...mockFamily, venue_photo_urls: PHOTOS },
|
|
46
50
|
labels,
|
|
47
|
-
|
|
48
|
-
formatPrice,
|
|
49
|
-
href: "/top/italian",
|
|
51
|
+
href: "/menu/cuisines/italian",
|
|
50
52
|
hue: "oklch(0.55 0.15 40)",
|
|
53
|
+
gradient: mockGradients.pasta,
|
|
51
54
|
priceBandLabel: "32–84 lei",
|
|
52
55
|
},
|
|
53
56
|
};
|
|
@@ -55,12 +58,11 @@ export const Default: Story = {
|
|
|
55
58
|
/** The drinks tab. Same card, different unit and vocabulary. */
|
|
56
59
|
export const DrinkFamily: Story = {
|
|
57
60
|
args: {
|
|
58
|
-
family: mockDrinkFamily,
|
|
59
|
-
labels: { venueUnit: "
|
|
60
|
-
|
|
61
|
-
formatPrice,
|
|
62
|
-
href: "/top/baruri",
|
|
61
|
+
family: { ...mockDrinkFamily, venue_photo_urls: PHOTOS },
|
|
62
|
+
labels: { venueUnit: "bars", priceBand: "bar price band", browse: "Browse cocktails" },
|
|
63
|
+
href: "/menu/drinks/cocktail",
|
|
63
64
|
hue: "oklch(0.5 0.13 240)",
|
|
65
|
+
gradient: mockGradients.cocktail,
|
|
64
66
|
priceBandLabel: "18–54 lei",
|
|
65
67
|
},
|
|
66
68
|
};
|
|
@@ -73,30 +75,36 @@ export const Sparse: Story = {
|
|
|
73
75
|
name: "Portuguese",
|
|
74
76
|
venue_count: 1,
|
|
75
77
|
chips: [{ slug: "seafood-dish", label: "seafood" }],
|
|
76
|
-
|
|
78
|
+
venue_photo_urls: PHOTOS.slice(0, 1),
|
|
77
79
|
},
|
|
78
|
-
labels,
|
|
79
|
-
|
|
80
|
-
formatPrice,
|
|
81
|
-
href: "/top/restaurante",
|
|
80
|
+
labels: { ...labels, browse: "Browse Portuguese" },
|
|
81
|
+
href: "/menu/cuisines/portuguese",
|
|
82
82
|
hue: "oklch(0.52 0.13 340)",
|
|
83
|
+
gradient: mockGradients.sweet,
|
|
83
84
|
priceBandLabel: "42–88 lei",
|
|
84
85
|
},
|
|
85
86
|
};
|
|
86
87
|
|
|
87
|
-
/**
|
|
88
|
+
/** No photography at all: the ground with the family's initial, and no tint. */
|
|
89
|
+
export const NoPhotos: Story = {
|
|
90
|
+
args: {
|
|
91
|
+
...Default.args,
|
|
92
|
+
family: { ...mockFamily, venue_photo_urls: [] },
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** The row of three the card is designed to be scanned in. */
|
|
88
97
|
export const Row: Story = {
|
|
89
98
|
render: () => (
|
|
90
|
-
<div className="grid grid-cols-
|
|
91
|
-
{[mockFamily, mockDrinkFamily, mockFamily
|
|
99
|
+
<div className="grid grid-cols-3 gap-5">
|
|
100
|
+
{[mockFamily, mockDrinkFamily, mockFamily].map((family, index) => (
|
|
92
101
|
<MenuFamilyCard
|
|
93
102
|
key={index}
|
|
94
|
-
family={family}
|
|
103
|
+
family={{ ...family, venue_photo_urls: PHOTOS }}
|
|
95
104
|
labels={labels}
|
|
96
|
-
|
|
97
|
-
formatPrice={formatPrice}
|
|
98
|
-
href={`/top/${family.slug}`}
|
|
105
|
+
href={`/menu/cuisines/${family.slug}`}
|
|
99
106
|
hue="oklch(0.55 0.15 40)"
|
|
107
|
+
gradient={Object.values(mockGradients)[index]}
|
|
100
108
|
priceBandLabel="32–84 lei"
|
|
101
109
|
/>
|
|
102
110
|
))}
|
|
@@ -104,7 +112,7 @@ export const Row: Story = {
|
|
|
104
112
|
),
|
|
105
113
|
decorators: [
|
|
106
114
|
(Story) => (
|
|
107
|
-
<div className="w-[
|
|
115
|
+
<div className="w-[1100px] bg-background p-4">
|
|
108
116
|
<Story />
|
|
109
117
|
</div>
|
|
110
118
|
),
|
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
import { Wallet } from "lucide-react";
|
|
2
2
|
import { cn } from "../lib/utils";
|
|
3
|
-
import {
|
|
3
|
+
import { HueChip } from "./hue-chip";
|
|
4
4
|
import type { MenuFamilyPreview, MenuLinkComponent, MenuLinkProps } from "./menu-types";
|
|
5
|
+
import { PHOTO_GROUND, PhotoGround } from "./photo-ground";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* MenuFamilyCard — a browsable grouping of venues: a cuisine on the dishes tab,
|
|
8
|
-
* a drink family on the drinks tab.
|
|
9
|
+
* a drink family on the drinks tab. The hub's doorway to a family.
|
|
9
10
|
*
|
|
10
|
-
* ##
|
|
11
|
+
* ## A doorway, not a summary
|
|
11
12
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* The cuisines index's `MenuFamilyBlock` is the family's SUMMARY: photography,
|
|
14
|
+
* chips, a price band and three venues. This card is the same family drawn
|
|
15
|
+
* one step earlier, in a row of three on the hub, where the reader is choosing
|
|
16
|
+
* a family and not yet a venue — so it carries the same photography, the same
|
|
17
|
+
* chips and the same "Browse" action, and lists no venue. A venue on the hub
|
|
18
|
+
* card was tried and it was the first venue's signature relabelled as the
|
|
19
|
+
* family's most named, which was a lie at three-card width.
|
|
18
20
|
*
|
|
19
|
-
* ##
|
|
21
|
+
* ## The count does the scanning
|
|
20
22
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* The card is one number, one name and the groups the family consists of,
|
|
24
|
+
* over a mosaic of its venues. The count is VENUES: cuisine exists on places
|
|
25
|
+
* and nowhere else — no listing endpoint will tell you how many dishes are
|
|
26
|
+
* Italian — so a card counting dishes cannot be built and this one does not
|
|
27
|
+
* try. The chips are plain labels: at three cards across, chips carrying
|
|
28
|
+
* numbers turned the panel into a table, and the numbers were kitchens, which
|
|
29
|
+
* nobody reads a chip as.
|
|
26
30
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
31
|
+
* ## One link
|
|
32
|
+
*
|
|
33
|
+
* The whole card is the link and the "Browse" pill is its visible affordance,
|
|
34
|
+
* not a second link — a pill inside a link is a nested anchor, and a pill that
|
|
35
|
+
* is the only link leaves the photography, the number and the name inert.
|
|
30
36
|
*/
|
|
31
37
|
|
|
32
38
|
/** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
|
|
@@ -39,22 +45,24 @@ function DefaultLink({ href, ...props }: MenuFamilyCardLinkProps) {
|
|
|
39
45
|
export type MenuFamilyCardLabels = {
|
|
40
46
|
/** Unit under the count, e.g. "KITCHENS". */
|
|
41
47
|
venueUnit: string;
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Price-band caption, e.g. "kitchen price band". Never drawn — it names the
|
|
50
|
+
* pill for a screen reader, which would otherwise be read two numbers and a
|
|
51
|
+
* currency.
|
|
52
|
+
*/
|
|
45
53
|
priceBand: string;
|
|
54
|
+
/** The action pill, e.g. "Browse Italian". Sits on the photography — keep it short. */
|
|
55
|
+
browse: string;
|
|
46
56
|
};
|
|
47
57
|
|
|
48
58
|
export type MenuFamilyCardProps = {
|
|
49
59
|
family: MenuFamilyPreview;
|
|
50
60
|
labels: MenuFamilyCardLabels;
|
|
51
|
-
/** Passed through to the featured `<MenuItem>`. */
|
|
52
|
-
itemLabels: MenuItemLabels;
|
|
53
|
-
formatPrice: (amount: number) => string;
|
|
54
|
-
formatCount?: (value: number) => string;
|
|
55
61
|
href: string;
|
|
56
|
-
/** Finished CSS colour for the
|
|
62
|
+
/** Finished CSS colour for the chips. */
|
|
57
63
|
hue?: string;
|
|
64
|
+
/** Finished CSS gradient — the family's colour over the mosaic. */
|
|
65
|
+
gradient?: string;
|
|
58
66
|
/** Composed price band, e.g. "32–84 lei". */
|
|
59
67
|
priceBandLabel?: string;
|
|
60
68
|
linkComponent?: MenuLinkComponent;
|
|
@@ -65,147 +73,112 @@ export type MenuFamilyCardProps = {
|
|
|
65
73
|
* A 2×2, not a 2×4.
|
|
66
74
|
*
|
|
67
75
|
* Four cells at card width give each photograph enough room to read as a room
|
|
68
|
-
* rather than as a swatch, and
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
76
|
+
* rather than as a swatch, and four cells match the four venues a caller
|
|
77
|
+
* actually has, so no photo repeats. At eight cells the list wrapped and put
|
|
78
|
+
* the SAME image directly above itself in a column, which is the one repeat a
|
|
79
|
+
* mosaic cannot hide.
|
|
72
80
|
*/
|
|
73
81
|
const MOSAIC_CELLS = 4;
|
|
74
82
|
|
|
83
|
+
/** The card's ground is `PhotoGround`'s, so the two cannot differ. */
|
|
84
|
+
const GROUND = PHOTO_GROUND;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The ink. The count and the name sit at opposite ends of the card, so both
|
|
88
|
+
* ends are dark — a gradient that cleared in the middle looked better empty
|
|
89
|
+
* and made the type unreadable the moment there were real photographs behind
|
|
90
|
+
* it. Literal rather than `var(--scrim)`: that token lives in the app's
|
|
91
|
+
* stylesheet, not this package's.
|
|
92
|
+
*/
|
|
93
|
+
const INK =
|
|
94
|
+
"bg-[linear-gradient(to_bottom,oklch(0.16_0.012_55_/_0.78)_0%,oklch(0.16_0.012_55_/_0.5)_34%,oklch(0.16_0.012_55_/_0.86)_62%,oklch(0.16_0.012_55_/_0.96)_100%)]";
|
|
95
|
+
|
|
75
96
|
export function MenuFamilyCard({
|
|
76
97
|
family,
|
|
77
98
|
labels,
|
|
78
|
-
itemLabels,
|
|
79
|
-
formatPrice,
|
|
80
|
-
formatCount,
|
|
81
99
|
href,
|
|
82
100
|
hue,
|
|
101
|
+
gradient,
|
|
83
102
|
priceBandLabel,
|
|
84
103
|
linkComponent: LinkComponent = DefaultLink,
|
|
85
104
|
className,
|
|
86
105
|
}: MenuFamilyCardProps) {
|
|
87
106
|
const photos = family.venue_photo_urls ?? [];
|
|
88
|
-
|
|
107
|
+
/* Fewer venues than cells: the mosaic repeats rather than leaving bare
|
|
108
|
+
ground, because under the tint and the ink a repeated room is a texture
|
|
109
|
+
and an empty cell is a hole. No venues at all: one cell, the initial. */
|
|
110
|
+
const tiles = photos.length
|
|
111
|
+
? Array.from({ length: MOSAIC_CELLS }, (_, cell) => photos[cell % photos.length])
|
|
112
|
+
: [undefined];
|
|
89
113
|
|
|
90
114
|
return (
|
|
91
|
-
<
|
|
115
|
+
<LinkComponent
|
|
116
|
+
href={href}
|
|
92
117
|
className={cn(
|
|
93
|
-
"flex h-
|
|
118
|
+
"group dark relative isolate flex min-h-[280px] flex-col justify-between overflow-hidden rounded-[14px] border border-border p-3.5",
|
|
119
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white",
|
|
120
|
+
GROUND,
|
|
94
121
|
className
|
|
95
122
|
)}
|
|
96
123
|
>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
className="
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)}
|
|
124
|
+
{/* ── Layer 1: the mosaic ─────────────────────────────────────────── */}
|
|
125
|
+
<PhotoGround
|
|
126
|
+
className="-z-30"
|
|
127
|
+
photos={tiles}
|
|
128
|
+
initials={family.name.charAt(0)}
|
|
129
|
+
mosaicClassName={photos.length ? "grid-cols-2 grid-rows-2" : "grid-cols-1"}
|
|
130
|
+
gradient={gradient}
|
|
131
|
+
/>
|
|
132
|
+
|
|
133
|
+
{/* ── Layer 2: the ink ────────────────────────────────────────────── */}
|
|
134
|
+
{/* There was a masked backdrop-blur under this. Removed for the same
|
|
135
|
+
measurement that took it out of `GlassGround`: the type rests on the
|
|
136
|
+
ink, and the rooms stay sharp all the way down. */}
|
|
137
|
+
<span aria-hidden className={cn("absolute inset-0 -z-10", INK)} />
|
|
138
|
+
|
|
139
|
+
{/* ── The content, floating ───────────────────────────────────────── */}
|
|
140
|
+
<span className="block text-white">
|
|
141
|
+
<b className="block text-[34px] font-extrabold leading-none tracking-[-0.03em] tabular-nums">
|
|
142
|
+
{family.venue_count}
|
|
143
|
+
</b>
|
|
144
|
+
<span className="mt-1 block text-[9.5px] font-bold uppercase tracking-[0.12em] opacity-80">
|
|
145
|
+
{labels.venueUnit}
|
|
120
146
|
</span>
|
|
147
|
+
</span>
|
|
121
148
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
different rooms read as one cocktail-blue, one wine-red, one
|
|
127
|
-
beer-gold, one coffee-brown — the same dictionary the chips and the
|
|
128
|
-
price band already use (`markGradient`). The scrim below still owns
|
|
129
|
-
the contrast; this layer only says which family the room belongs to.
|
|
130
|
-
*/}
|
|
131
|
-
{hue && (
|
|
132
|
-
<span
|
|
133
|
-
aria-hidden
|
|
134
|
-
className="absolute inset-0 opacity-90 mix-blend-color"
|
|
135
|
-
style={{ backgroundColor: hue }}
|
|
136
|
-
/>
|
|
137
|
-
)}
|
|
138
|
-
|
|
139
|
-
{/*
|
|
140
|
-
The hero's scrim, not a light tint.
|
|
141
|
-
The count and the name sit at opposite ends of this panel, so both
|
|
142
|
-
ends need to be dark — a gradient that clears in the middle looked
|
|
143
|
-
better empty and made the type unreadable the moment there were real
|
|
144
|
-
photographs behind it. Dimming the images themselves was tried first
|
|
145
|
-
and is worse: it greys the photography without ever guaranteeing
|
|
146
|
-
contrast, because the guarantee has to come from a fixed layer.
|
|
147
|
-
*/}
|
|
148
|
-
<span
|
|
149
|
-
aria-hidden
|
|
150
|
-
className="absolute inset-0 bg-[linear-gradient(180deg,oklch(0.16_0.012_55_/_0.9)_0%,oklch(0.16_0.012_55_/_0.62)_45%,oklch(0.16_0.012_55_/_0.94)_100%)]"
|
|
151
|
-
/>
|
|
152
|
-
|
|
153
|
-
{/* Taller than the half below it: the photography is what makes a row
|
|
154
|
-
of four scannable, and it was losing the card to a text block. */}
|
|
155
|
-
<span className="relative flex min-h-[260px] flex-col justify-between p-3.5 text-white">
|
|
156
|
-
<span className="block">
|
|
157
|
-
<b className="block text-[34px] font-extrabold leading-none tracking-[-0.03em] tabular-nums">
|
|
158
|
-
{family.venue_count}
|
|
159
|
-
</b>
|
|
160
|
-
<span className="mt-1 block text-[9.5px] font-bold tracking-[0.12em] opacity-80">
|
|
161
|
-
{labels.venueUnit}
|
|
162
|
-
</span>
|
|
163
|
-
</span>
|
|
164
|
-
|
|
165
|
-
<span className="block">
|
|
166
|
-
<span className="block text-[17px] font-bold tracking-[-0.01em]">{family.name}</span>
|
|
167
|
-
{groups.length > 0 && (
|
|
168
|
-
<span className="mt-0.5 block truncate text-[11.5px] opacity-75">
|
|
169
|
-
{groups.join(" · ")}
|
|
170
|
-
</span>
|
|
171
|
-
)}
|
|
149
|
+
<span className="block">
|
|
150
|
+
<span className="block text-balance font-serif text-[24px] font-semibold leading-tight tracking-[-0.02em] text-white">
|
|
151
|
+
{family.name}
|
|
152
|
+
</span>
|
|
172
153
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
half is for — the item you might actually order. */}
|
|
154
|
+
{(priceBandLabel || (family.chips?.length ?? 0) > 0) && (
|
|
155
|
+
<span className="mt-2.5 flex flex-wrap items-center gap-1.5">
|
|
176
156
|
{priceBandLabel && (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
157
|
+
/* No hue: a fact about money, not about the family. In the dark
|
|
158
|
+
scope `HueChip`'s `--muted` fallback is the dark pill. */
|
|
159
|
+
<HueChip
|
|
160
|
+
className="font-bold tabular-nums"
|
|
161
|
+
aria-label={`${labels.priceBand}: ${priceBandLabel}`}
|
|
162
|
+
icon={<Wallet className="h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden />}
|
|
163
|
+
>
|
|
164
|
+
{priceBandLabel}
|
|
165
|
+
</HueChip>
|
|
182
166
|
)}
|
|
167
|
+
{family.chips?.map((chip) => (
|
|
168
|
+
<HueChip key={chip.slug} hue={hue}>
|
|
169
|
+
{chip.label}
|
|
170
|
+
</HueChip>
|
|
171
|
+
))}
|
|
183
172
|
</span>
|
|
184
|
-
|
|
185
|
-
</LinkComponent>
|
|
186
|
-
|
|
187
|
-
{family.signature && (
|
|
188
|
-
<div className="flex flex-1 flex-col px-2 pb-2 pt-3">
|
|
189
|
-
<span className="px-2 text-[9.5px] font-bold uppercase tracking-[0.09em] text-muted-foreground">
|
|
190
|
-
{labels.mostNamed}
|
|
191
|
-
</span>
|
|
192
|
-
|
|
193
|
-
{/* MenuItem's own `px-4`: its colour mark is drawn at `-ml-4` and
|
|
194
|
-
needs that gutter — a tighter padding clipped it to a grey stub. */}
|
|
195
|
-
<MenuItem
|
|
196
|
-
className="min-h-0"
|
|
197
|
-
item={family.signature.item}
|
|
198
|
-
labels={itemLabels}
|
|
199
|
-
formatPrice={formatPrice}
|
|
200
|
-
formatCount={formatCount}
|
|
201
|
-
markGradient={hue}
|
|
202
|
-
subtitle={family.signature.why}
|
|
203
|
-
href={family.signature.href ?? href}
|
|
204
|
-
linkComponent={LinkComponent}
|
|
205
|
-
/>
|
|
173
|
+
)}
|
|
206
174
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
175
|
+
{/* The action, as a translucent pill on the photography — the block's
|
|
176
|
+
pill, so the two cards read as one family. A `span`, because the
|
|
177
|
+
card is already the link. */}
|
|
178
|
+
<span className="mt-3.5 inline-flex h-10 items-center rounded-full border border-white/20 bg-white/[0.16] px-4 text-[13.5px] font-bold text-white transition-colors group-hover:bg-white/25 motion-reduce:transition-none">
|
|
179
|
+
{labels.browse}
|
|
180
|
+
</span>
|
|
181
|
+
</span>
|
|
182
|
+
</LinkComponent>
|
|
210
183
|
);
|
|
211
184
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Clock, Leaf, MapPin, Moon, Sun, Wallet } from "lucide-react";
|
|
4
|
+
import { MenuFilterChips, MenuFilterSummary } from "./menu-filter-chips";
|
|
5
|
+
import { mockFilterChips } from "./menu-mocks";
|
|
6
|
+
|
|
7
|
+
const icons: Record<string, React.ReactNode> = {
|
|
8
|
+
open: <Clock className="h-3 w-3" />,
|
|
9
|
+
price: <Wallet className="h-3 w-3" />,
|
|
10
|
+
terrace: <Sun className="h-3 w-3" />,
|
|
11
|
+
veg: <Leaf className="h-3 w-3" />,
|
|
12
|
+
late: <Moon className="h-3 w-3" />,
|
|
13
|
+
centru: <MapPin className="h-3 w-3" />,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const chips = mockFilterChips.map((chip) => ({ ...chip, icon: icons[chip.key] }));
|
|
17
|
+
|
|
18
|
+
const meta: Meta<typeof MenuFilterChips> = {
|
|
19
|
+
title: "Menu/MenuFilterChips",
|
|
20
|
+
component: MenuFilterChips,
|
|
21
|
+
tags: ["autodocs"],
|
|
22
|
+
decorators: [
|
|
23
|
+
(Story) => (
|
|
24
|
+
<div className="w-[1100px] bg-background p-4">
|
|
25
|
+
<Story />
|
|
26
|
+
</div>
|
|
27
|
+
),
|
|
28
|
+
],
|
|
29
|
+
parameters: {
|
|
30
|
+
docs: {
|
|
31
|
+
description: {
|
|
32
|
+
component:
|
|
33
|
+
"Every chip counts KITCHENS, whatever facet is behind it — a dietary chip reads *has a vegetarian dish* and counts venues, not dishes. The unit is stated once at the head of the row and never again; the earlier version mixed venue counts and item counts and you could not tell what a number meant without re-reading the label that produced it. One line in every state: the chips scroll under a `ScrollFade`, and *More filters* is pinned opposite the unit label rather than wrapping.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default meta;
|
|
40
|
+
type Story = StoryObj<typeof MenuFilterChips>;
|
|
41
|
+
|
|
42
|
+
function Interactive({ initial }: { initial: typeof chips }) {
|
|
43
|
+
const [state, setState] = useState(initial);
|
|
44
|
+
const active = state.filter((chip) => chip.active).length;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<MenuFilterChips
|
|
49
|
+
chips={state}
|
|
50
|
+
unitLabel="KITCHENS · 247"
|
|
51
|
+
moreLabel="More filters"
|
|
52
|
+
onMore={() => {}}
|
|
53
|
+
ariaLabel="Filter kitchens"
|
|
54
|
+
onToggle={(key) =>
|
|
55
|
+
setState((chips) =>
|
|
56
|
+
chips.map((chip) => (chip.key === key ? { ...chip, active: !chip.active } : chip))
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
/>
|
|
60
|
+
{active > 0 && (
|
|
61
|
+
<MenuFilterSummary
|
|
62
|
+
summary={
|
|
63
|
+
<>
|
|
64
|
+
{active} filters · <b>34 of 247 kitchens</b> match
|
|
65
|
+
</>
|
|
66
|
+
}
|
|
67
|
+
clearLabel="Clear all"
|
|
68
|
+
onClear={() => setState((chips) => chips.map((c) => ({ ...c, active: false })))}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const Default: Story = { render: () => <Interactive initial={chips} /> };
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A chip that has gone empty.
|
|
79
|
+
*
|
|
80
|
+
* The facets endpoint omits values that would match nothing, so this state
|
|
81
|
+
* cannot arrive from the API — a chip only becomes `empty` client-side, after
|
|
82
|
+
* it was offered and the reader's next filter emptied it. It stays TAPPABLE:
|
|
83
|
+
* pressing it swaps it in and drops whichever active filter conflicts. Disabling
|
|
84
|
+
* it would refuse the reader the thing they asked for and give no reason.
|
|
85
|
+
*/
|
|
86
|
+
export const Dead: Story = {
|
|
87
|
+
render: () => (
|
|
88
|
+
<MenuFilterChips
|
|
89
|
+
chips={chips.map((chip) =>
|
|
90
|
+
chip.key === "late" ? { ...chip, count: 0, empty: true } : chip
|
|
91
|
+
)}
|
|
92
|
+
unitLabel="KITCHENS · 247"
|
|
93
|
+
onToggle={() => {}}
|
|
94
|
+
/>
|
|
95
|
+
),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const Mobile: Story = {
|
|
99
|
+
render: () => <Interactive initial={chips} />,
|
|
100
|
+
decorators: [
|
|
101
|
+
(Story) => (
|
|
102
|
+
<div className="w-[390px] bg-background p-3">
|
|
103
|
+
<Story />
|
|
104
|
+
</div>
|
|
105
|
+
),
|
|
106
|
+
],
|
|
107
|
+
};
|