@bearmenu/ui 0.8.12 → 0.8.14
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-66MAPLXS.js → chunk-2ADCZXZJ.js} +1 -1
- package/dist/{chunk-2DEIXTBN.js → chunk-2ZXIXQIU.js} +1 -1
- package/dist/{chunk-YOKHBAIH.js → chunk-4OF4T4AI.js} +12 -11
- package/dist/{chunk-MZL7CGCL.js → chunk-4XDJXMLF.js} +2 -2
- package/dist/{chunk-YYGB4G4Q.js → chunk-CZK2MRS2.js} +1 -1
- package/dist/chunk-DJD3EMPK.js +126 -0
- package/dist/chunk-EZG2NELX.js +5 -0
- package/dist/{chunk-VAXOQS7W.js → chunk-F76NCVVR.js} +1 -1
- package/dist/{chunk-VG3PND3D.js → chunk-FMTJGO76.js} +1 -1
- package/dist/{chunk-UXWAOISG.js → chunk-FVSKGCK3.js} +2 -2
- package/dist/{chunk-2XCZJWTV.js → chunk-GNCWJWFE.js} +1 -1
- package/dist/{chunk-X3WMRKOW.js → chunk-HDR7V6IZ.js} +1 -1
- package/dist/{chunk-HSIYYA2E.js → chunk-HQU6QVAN.js} +5 -5
- package/dist/chunk-I5V446BX.js +27 -0
- package/dist/{chunk-WCTHZKU7.js → chunk-ICOU5QHT.js} +1 -1
- package/dist/{chunk-G7IFL4Z4.js → chunk-L3VEMP5A.js} +57 -9
- package/dist/chunk-LQWGZIYA.js +5 -0
- package/dist/{chunk-XNA2INVQ.js → chunk-MCOHLWRS.js} +2 -2
- package/dist/{chunk-3GWWZKX7.js → chunk-MMUBH76A.js} +21 -1
- package/dist/{chunk-ZDAHKI3A.js → chunk-MWB7S54O.js} +1 -1
- package/dist/{chunk-D5QHEHXJ.js → chunk-NMEVJT5Q.js} +1 -1
- package/dist/{chunk-3TNPSN4D.js → chunk-NSNEANOQ.js} +2 -2
- package/dist/{chunk-WSRWFA6K.js → chunk-NWZGSLPU.js} +1 -1
- package/dist/{chunk-LEAYBICI.js → chunk-OIV7B44M.js} +1 -1
- package/dist/{chunk-2CXU5WZO.js → chunk-P43B3ONY.js} +1 -1
- package/dist/{chunk-FRPPIXMY.js → chunk-PDTPO6NG.js} +1 -1
- package/dist/{chunk-6XMYEZVR.js → chunk-PX6RICS5.js} +1 -1
- package/dist/{chunk-6TEEY5YA.js → chunk-S33K4RC5.js} +1 -1
- package/dist/{chunk-3VMDCCCX.js → chunk-SC2VUTUI.js} +1 -1
- package/dist/{chunk-24RFI4RQ.js → chunk-SXQZ7DLH.js} +4 -4
- package/dist/{chunk-UDTG3RSE.js → chunk-T6S2ZBV6.js} +2 -2
- package/dist/{chunk-DTXNDLSE.js → chunk-WYTJR2YU.js} +16 -1
- package/dist/{chunk-QXHMPJ35.js → chunk-XBBEX4SF.js} +1 -1
- package/dist/chunk-XCB3F6SF.js +16 -0
- package/dist/{chunk-7BWTYWM3.js → chunk-ZLMRVBD7.js} +1 -1
- package/dist/{chunk-7A42J73Q.js → chunk-ZLPWEHZO.js} +1 -1
- package/dist/components/accordion.js +1 -1
- package/dist/components/alert-dialog.js +2 -2
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/alert.js +2 -2
- package/dist/components/aspect-ratio.js +2 -2
- package/dist/components/avatar.js +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +2 -2
- package/dist/components/breadcrumb.js +1 -1
- package/dist/components/button-group.js +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/button.js +2 -2
- package/dist/components/calendar.js +2 -2
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.js +3 -3
- package/dist/components/carousel.js +2 -2
- package/dist/components/category-deck.js +2 -2
- package/dist/components/checkbox.js +2 -2
- package/dist/components/circular-progress.js +1 -1
- package/dist/components/collapsible.js +1 -1
- package/dist/components/combobox.js +5 -5
- package/dist/components/command.d.ts +15 -15
- package/dist/components/command.js +3 -3
- package/dist/components/concept-card.js +2 -2
- package/dist/components/concept-compare-rail.js +3 -3
- package/dist/components/concept-compare.d.ts +2 -1
- package/dist/components/concept-compare.js +5 -5
- package/dist/components/concept-convergence.js +4 -4
- package/dist/components/convergence.js +3 -3
- package/dist/components/coverflow.js +1 -1
- package/dist/components/currency-input.js +2 -2
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/date-input.js +2 -2
- package/dist/components/day-rail-chip.js +2 -2
- package/dist/components/description-list.js +1 -1
- package/dist/components/dialog.js +2 -2
- package/dist/components/doorway-tiles.d.ts +57 -0
- package/dist/components/doorway-tiles.js +86 -0
- package/dist/components/drawer.js +1 -1
- package/dist/components/drift-rail.js +1 -1
- package/dist/components/dropdown-menu.js +1 -1
- package/dist/components/empty-state.js +3 -3
- package/dist/components/event-card.js +4 -4
- package/dist/components/event-compact-row.js +4 -4
- package/dist/components/event-day-header.js +2 -2
- package/dist/components/event-day-separator.js +3 -3
- package/dist/components/event-detail-panel.js +4 -4
- package/dist/components/event-hub-hero.js +2 -2
- package/dist/components/event-index-row.js +4 -4
- package/dist/components/event-list-row-wide.js +5 -5
- package/dist/components/event-list-row.js +5 -5
- package/dist/components/event-poster-fallback.js +1 -1
- package/dist/components/event-types.js +1 -1
- package/dist/components/events-empty-state.js +1 -1
- package/dist/components/explainer-steps.js +1 -1
- package/dist/components/facet-group.js +2 -2
- package/dist/components/featured-rail-item.js +2 -2
- package/dist/components/field.js +2 -2
- package/dist/components/filter-category-row.js +3 -3
- package/dist/components/form.js +2 -2
- package/dist/components/glass-ground.js +2 -2
- package/dist/components/grade-atom.d.ts +3 -0
- package/dist/components/grade-atom.js +4 -0
- package/dist/components/haptic-button.js +2 -2
- package/dist/components/hour-band.js +4 -4
- package/dist/components/hour-group.js +4 -4
- package/dist/components/hub-surface.js +1 -1
- package/dist/components/hue-chip.js +1 -1
- package/dist/components/icon.js +1 -1
- package/dist/components/input-group.js +1 -1
- package/dist/components/input-otp.js +1 -1
- package/dist/components/input.js +2 -2
- package/dist/components/item.d.ts +2 -2
- package/dist/components/item.js +1 -1
- package/dist/components/kbd.js +1 -1
- package/dist/components/kitchen-dossier.d.ts +2 -1
- package/dist/components/kitchen-dossier.js +3 -3
- package/dist/components/label.js +2 -2
- package/dist/components/lazy-markdown.js +1 -1
- package/dist/components/link-button.js +2 -2
- package/dist/components/markdown-renderer.js +1 -1
- package/dist/components/menu-dead-end.d.ts +2 -1
- package/dist/components/menu-dead-end.js +1 -1
- package/dist/components/menu-family-block.d.ts +2 -1
- package/dist/components/menu-family-block.js +14 -8
- package/dist/components/menu-family-card.d.ts +2 -1
- package/dist/components/menu-family-card.js +1 -1
- package/dist/components/menu-field.js +1 -1
- package/dist/components/menu-filter-chips.d.ts +2 -1
- package/dist/components/menu-filter-chips.js +3 -3
- package/dist/components/menu-group-rows.d.ts +2 -1
- package/dist/components/menu-group-rows.js +2 -2
- package/dist/components/menu-hub-hero.js +1 -1
- package/dist/components/menu-index-header.d.ts +2 -1
- package/dist/components/menu-index-header.js +2 -2
- package/dist/components/menu-index-list.d.ts +9 -8
- package/dist/components/menu-index-list.js +4 -4
- package/dist/components/menu-intent-hero.d.ts +3 -2
- package/dist/components/menu-intent-hero.js +1 -1
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +4 -4
- package/dist/components/menu-kitchen-list.d.ts +2 -1
- package/dist/components/menu-kitchen-list.js +4 -4
- package/dist/components/menu-mocks.d.ts +16 -5
- package/dist/components/menu-mocks.js +23 -2
- package/dist/components/menu-types.d.ts +21 -34
- package/dist/components/motion-icon-actions.d.ts +27 -0
- package/dist/components/motion-icon-actions.js +336 -0
- package/dist/components/motion-icon-controls.js +3 -3
- package/dist/components/motion-icon-set.d.ts +13 -1
- package/dist/components/motion-icon-set.js +197 -6
- package/dist/components/motion-icon.d.ts +15 -1
- package/dist/components/motion-icon.js +2 -2
- package/dist/components/multi-select-combobox.js +7 -7
- package/dist/components/multi-select-menu.js +2 -2
- package/dist/components/native-select.js +1 -1
- package/dist/components/open-now-list.d.ts +2 -1
- package/dist/components/open-now-list.js +2 -2
- package/dist/components/pagination.js +2 -2
- package/dist/components/phone-frame.js +1 -1
- package/dist/components/photo-ground.js +1 -1
- package/dist/components/place-about-tab.d.ts +2 -1
- package/dist/components/place-about-tab.js +6 -6
- package/dist/components/place-card.d.ts +94 -12
- package/dist/components/place-card.js +203 -128
- package/dist/components/place-details-mobile.d.ts +2 -1
- package/dist/components/place-details-mobile.js +7 -7
- package/dist/components/place-schedule-week.d.ts +2 -1
- package/dist/components/place-schedule-week.js +3 -3
- package/dist/components/place-types.d.ts +3 -265
- package/dist/components/popover.js +2 -2
- package/dist/components/poster-stack.js +1 -1
- package/dist/components/poster-tape.js +1 -1
- package/dist/components/poster-wall.d.ts +22 -6
- package/dist/components/poster-wall.js +197 -12
- package/dist/components/progress.js +1 -1
- package/dist/components/qa-block.d.ts +2 -1
- package/dist/components/qa-block.js +1 -1
- package/dist/components/quiet-stretch.js +1 -1
- package/dist/components/radio-group.js +1 -1
- package/dist/components/rating.js +1 -1
- package/dist/components/scroll-area.js +1 -1
- package/dist/components/scroll-fade.js +2 -2
- package/dist/components/search-bar.js +1 -1
- package/dist/components/searchable-select.js +5 -5
- package/dist/components/section-header.js +2 -2
- package/dist/components/segmented-nav.d.ts +24 -4
- package/dist/components/segmented-nav.js +11 -2
- package/dist/components/select.js +1 -1
- package/dist/components/selection-bar.js +1 -1
- package/dist/components/separator.js +2 -2
- package/dist/components/sheet.js +1 -1
- package/dist/components/signature-chip.js +3 -3
- package/dist/components/skeleton-card.js +3 -3
- package/dist/components/skeleton.js +2 -2
- package/dist/components/slider.js +1 -1
- package/dist/components/sliding-panels.js +2 -2
- package/dist/components/snap-rail.d.ts +0 -5
- package/dist/components/snap-rail.js +2 -2
- package/dist/components/sonner.js +1 -1
- package/dist/components/sort-option-list.js +1 -1
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/spinner.js +1 -1
- package/dist/components/stat-banner.js +1 -1
- package/dist/components/stepper.js +1 -1
- package/dist/components/sticky-container.js +1 -1
- package/dist/components/story.js +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/table.js +1 -1
- package/dist/components/tabs.js +2 -2
- package/dist/components/tag-group.js +2 -2
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/tag.js +2 -2
- package/dist/components/text.d.ts +3 -3
- package/dist/components/text.js +2 -2
- package/dist/components/textarea.js +1 -1
- package/dist/components/timeline-band.js +4 -4
- package/dist/components/timeline-playhead.js +4 -4
- package/dist/components/toggle-group.js +2 -2
- package/dist/components/toggle.js +2 -2
- package/dist/components/tooltip.js +1 -1
- package/dist/components/top-comparison.d.ts +2 -1
- package/dist/components/top-comparison.js +1 -1
- package/dist/components/top-cross-link.d.ts +2 -1
- package/dist/components/top-cross-link.js +1 -1
- package/dist/components/top-ranking-entry.d.ts +2 -1
- package/dist/components/top-ranking-entry.js +6 -6
- package/dist/components/top-ranking-hero.d.ts +2 -1
- package/dist/components/top-ranking-hero.js +1 -1
- package/dist/components/top-ranking-legend.js +1 -1
- package/dist/components/top-types.d.ts +3 -1
- package/dist/components/use-mobile.js +1 -1
- package/dist/components/venue-events-block.js +5 -5
- package/dist/components/venue-grade-dimensions.d.ts +2 -1
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/venue-quotes.d.ts +2 -1
- package/dist/components/venue-quotes.js +1 -1
- package/dist/components/venue-wall.d.ts +2 -1
- package/dist/components/venue-wall.js +1 -1
- package/dist/components/weekday-theme-banner.js +1 -1
- package/dist/components/weekend-panel.js +4 -4
- package/dist/hooks/use-drag-swipe.js +1 -1
- package/dist/hooks/use-haptic.js +1 -1
- package/dist/hooks/use-media-query.js +1 -1
- package/dist/hooks/use-mobile.js +1 -1
- package/dist/hooks/use-mounted-theme.js +1 -1
- package/dist/hooks/use-sticky.js +1 -1
- package/dist/lib/card-hover.js +2 -18
- package/dist/lib/grade.d.ts +54 -0
- package/dist/lib/grade.js +2 -0
- package/dist/lib/icon-depth.d.ts +35 -0
- package/dist/lib/icon-depth.js +2 -0
- package/dist/lib/icon-registry.js +1 -1
- package/dist/lib/motion.js +2 -2
- package/dist/lib/overlay-pill.d.ts +30 -0
- package/dist/lib/overlay-pill.js +2 -0
- package/dist/lib/utils.js +1 -1
- package/dist/place-types-BHdeMMiG.d.ts +317 -0
- package/package.json +1 -1
- package/src/components/colour-dictionary.stories.tsx +268 -82
- package/src/components/doorway-tiles.stories.tsx +83 -0
- package/src/components/doorway-tiles.test.tsx +51 -0
- package/src/components/doorway-tiles.tsx +225 -0
- package/src/components/grade-atom.stories.tsx +81 -0
- package/src/components/grade-atom.tsx +256 -0
- package/src/components/hue-chip.stories.tsx +2 -2
- package/src/components/menu-family-block.stories.tsx +29 -0
- package/src/components/menu-family-block.test.tsx +51 -0
- package/src/components/menu-family-block.tsx +20 -4
- package/src/components/menu-index-list.tsx +7 -7
- package/src/components/menu-intent-hero.tsx +1 -1
- package/src/components/menu-kitchen-list.tsx +4 -1
- package/src/components/menu-mocks.ts +39 -3
- package/src/components/menu-types.ts +17 -33
- package/src/components/motion-icon-actions.test.tsx +115 -0
- package/src/components/motion-icon-actions.tsx +580 -0
- package/src/components/motion-icon-controls.test.tsx +1 -0
- package/src/components/motion-icon-controls.tsx +18 -13
- package/src/components/motion-icon-set.stories.tsx +62 -3
- package/src/components/motion-icon-set.tsx +349 -2
- package/src/components/motion-icon.test.tsx +180 -1
- package/src/components/motion-icon.tsx +97 -8
- package/src/components/place-card.stories.tsx +66 -76
- package/src/components/place-card.test.tsx +188 -100
- package/src/components/place-card.tsx +356 -148
- package/src/components/place-types.ts +17 -4
- package/src/components/poster-wall.stories.tsx +84 -0
- package/src/components/poster-wall.test.tsx +94 -3
- package/src/components/poster-wall.tsx +257 -9
- package/src/components/segmented-nav.stories.tsx +72 -1
- package/src/components/segmented-nav.test.tsx +96 -0
- package/src/components/segmented-nav.tsx +36 -4
- package/src/components/snap-rail.tsx +19 -0
- package/src/globals.css +26 -0
- package/src/lib/grade.test.ts +82 -0
- package/src/lib/grade.ts +76 -0
- package/src/lib/icon-depth.ts +35 -0
- package/src/lib/overlay-pill.ts +29 -0
- package/dist/components/menu-family-tiles.d.ts +0 -19
- package/dist/components/menu-family-tiles.js +0 -57
- package/src/components/menu-family-tiles.stories.tsx +0 -54
- package/src/components/menu-family-tiles.test.tsx +0 -30
- package/src/components/menu-family-tiles.tsx +0 -132
|
@@ -29,13 +29,28 @@ import { cn } from "../lib/utils";
|
|
|
29
29
|
* which wins via `cn`'s merge.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* What a segment's icon render function receives. `replayToken` bumps on
|
|
34
|
+
* POINTER DOWN on that segment — before any navigation exists — which is what
|
|
35
|
+
* a character-animated glyph (`MotionIcon`) needs to answer the finger rather
|
|
36
|
+
* than the route change that follows it.
|
|
37
|
+
*/
|
|
38
|
+
export type SegmentedNavIconState = {
|
|
39
|
+
active: boolean;
|
|
40
|
+
replayToken: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
32
43
|
export type SegmentedNavItem = {
|
|
33
44
|
/** Stable identity, compared against `activeId`. */
|
|
34
45
|
id: string;
|
|
35
46
|
href: string;
|
|
36
47
|
label: string;
|
|
37
|
-
/**
|
|
38
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Rendered before the label. Size it at the call site (the design uses
|
|
50
|
+
* 15–16px). A function receives the segment's state, for glyphs that
|
|
51
|
+
* perform on touch: `({ active, replayToken }) => <MotionWine active={active} replayToken={replayToken} />`.
|
|
52
|
+
*/
|
|
53
|
+
icon?: React.ReactNode | ((state: SegmentedNavIconState) => React.ReactNode);
|
|
39
54
|
};
|
|
40
55
|
|
|
41
56
|
/**
|
|
@@ -48,6 +63,7 @@ export type SegmentedNavLinkProps = {
|
|
|
48
63
|
children: React.ReactNode;
|
|
49
64
|
"aria-current"?: "page";
|
|
50
65
|
onClick?: () => void;
|
|
66
|
+
onPointerDown?: () => void;
|
|
51
67
|
};
|
|
52
68
|
|
|
53
69
|
function DefaultLink({ href, ...props }: SegmentedNavLinkProps) {
|
|
@@ -60,7 +76,12 @@ export type SegmentedNavProps = {
|
|
|
60
76
|
activeId: string;
|
|
61
77
|
/** Accessible name for the nav landmark. Translated by the caller. */
|
|
62
78
|
label: string;
|
|
63
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Defaults to a plain `<a>`. Pass `next/link` in a Next app. Must be a
|
|
81
|
+
* stable reference (module scope, not an inline arrow): a new component
|
|
82
|
+
* identity per render remounts every segment, and with a function `icon`
|
|
83
|
+
* that cuts the pointer-down gesture off at the click.
|
|
84
|
+
*/
|
|
64
85
|
linkComponent?: React.ComponentType<SegmentedNavLinkProps>;
|
|
65
86
|
/** Fired with the segment id on click — analytics, haptics. Never navigation. */
|
|
66
87
|
onSelect?: (id: string) => void;
|
|
@@ -75,6 +96,12 @@ export function SegmentedNav({
|
|
|
75
96
|
onSelect,
|
|
76
97
|
className,
|
|
77
98
|
}: SegmentedNavProps) {
|
|
99
|
+
// One counter per segment, bumped on pointer down. Only a function `icon`
|
|
100
|
+
// ever reads it; a static icon costs nothing but the state slot.
|
|
101
|
+
const [replayTokens, setReplayTokens] = React.useState<Record<string, number>>({});
|
|
102
|
+
const replay = (id: string) =>
|
|
103
|
+
setReplayTokens((tokens) => ({ ...tokens, [id]: (tokens[id] ?? 0) + 1 }));
|
|
104
|
+
|
|
78
105
|
return (
|
|
79
106
|
<nav
|
|
80
107
|
aria-label={label}
|
|
@@ -86,12 +113,17 @@ export function SegmentedNav({
|
|
|
86
113
|
>
|
|
87
114
|
{items.map((item) => {
|
|
88
115
|
const isActive = item.id === activeId;
|
|
116
|
+
const icon =
|
|
117
|
+
typeof item.icon === "function"
|
|
118
|
+
? item.icon({ active: isActive, replayToken: replayTokens[item.id] ?? 0 })
|
|
119
|
+
: item.icon;
|
|
89
120
|
return (
|
|
90
121
|
<LinkComponent
|
|
91
122
|
key={item.id}
|
|
92
123
|
href={item.href}
|
|
93
124
|
aria-current={isActive ? "page" : undefined}
|
|
94
125
|
onClick={onSelect ? () => onSelect(item.id) : undefined}
|
|
126
|
+
onPointerDown={typeof item.icon === "function" ? () => replay(item.id) : undefined}
|
|
95
127
|
className={cn(
|
|
96
128
|
"inline-flex h-9 flex-1 items-center justify-center gap-1.5 rounded-md px-3",
|
|
97
129
|
"text-[13px] font-semibold lg:flex-none lg:text-sm",
|
|
@@ -103,7 +135,7 @@ export function SegmentedNav({
|
|
|
103
135
|
: "text-muted-foreground hover:text-foreground"
|
|
104
136
|
)}
|
|
105
137
|
>
|
|
106
|
-
{
|
|
138
|
+
{icon}
|
|
107
139
|
{item.label}
|
|
108
140
|
</LinkComponent>
|
|
109
141
|
);
|
|
@@ -166,6 +166,14 @@ SnapRail.displayName = "SnapRail";
|
|
|
166
166
|
* `onActiveChange` and wants the dots somewhere other than directly under it —
|
|
167
167
|
* inside a foot band, say. Decorative, as on the rail: the rail is the control.
|
|
168
168
|
*/
|
|
169
|
+
/**
|
|
170
|
+
* Past this many pages the dots stop being a picture of the rail — twelve
|
|
171
|
+
* 6px dots with their gaps already run 138px, and a cuisines index has
|
|
172
|
+
* thirty-six — so the row becomes a counter, "7 / 36", which says the same
|
|
173
|
+
* two things (where, and how far) at any length.
|
|
174
|
+
*/
|
|
175
|
+
const MAX_DOTS = 12;
|
|
176
|
+
|
|
169
177
|
export function SnapRailDots({
|
|
170
178
|
count,
|
|
171
179
|
active,
|
|
@@ -175,6 +183,17 @@ export function SnapRailDots({
|
|
|
175
183
|
active: number;
|
|
176
184
|
className?: string;
|
|
177
185
|
}) {
|
|
186
|
+
if (count > MAX_DOTS) {
|
|
187
|
+
return (
|
|
188
|
+
<div
|
|
189
|
+
aria-hidden
|
|
190
|
+
className={cn("flex justify-center text-[11px] tabular-nums text-muted-foreground", className)}
|
|
191
|
+
>
|
|
192
|
+
{active + 1} / {count}
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
178
197
|
return (
|
|
179
198
|
<div aria-hidden className={cn("flex justify-center gap-1.5", className)}>
|
|
180
199
|
{Array.from({ length: count }, (_, index) => (
|
package/src/globals.css
CHANGED
|
@@ -234,6 +234,17 @@ h6 {
|
|
|
234
234
|
--z-dropdown: 1200;
|
|
235
235
|
--z-tooltip: 1300;
|
|
236
236
|
--z-toast: 1400;
|
|
237
|
+
|
|
238
|
+
/* Drop-shadow filter for a glyph on the neutral placeholder panel the card
|
|
239
|
+
photo fallbacks share (`from-muted to-border`). Consumed via
|
|
240
|
+
`NEUTRAL_ICON_FILTER` in `lib/icon-depth.ts`. A black shadow of any spread
|
|
241
|
+
on a near-white panel pools under the glyph and reads as grime; the fix is
|
|
242
|
+
hue and spread, not weakness: both stops are a deep neutral dropped to
|
|
243
|
+
0.38 L so it still has weight, and the lift caps at 4px. Alpha stays high
|
|
244
|
+
enough to read at a 40px glyph — a warm shadow carries far more opacity
|
|
245
|
+
than a black one before it looks dirty. */
|
|
246
|
+
--icon-depth-neutral: drop-shadow(0 1px 1px oklch(0.38 0 0 / 0.29))
|
|
247
|
+
drop-shadow(0 2px 4px oklch(0.38 0 0 / 0.22));
|
|
237
248
|
}
|
|
238
249
|
|
|
239
250
|
.dark {
|
|
@@ -313,6 +324,21 @@ h6 {
|
|
|
313
324
|
The inset well is unchanged between themes, so it has no override. */
|
|
314
325
|
--shadow-card-image: 0 4px 14px rgba(0, 0, 0, 0.45);
|
|
315
326
|
--shadow-card-image-hover: 0 12px 28px rgba(0, 0, 0, 0.55);
|
|
327
|
+
|
|
328
|
+
/* Dark placeholder panel is oklch(0.22→0.32) with a light glyph — a wide
|
|
329
|
+
halo does nothing against a dark surface, so it's contact stops only,
|
|
330
|
+
pushed harder since the panel absorbs most of the shadow. */
|
|
331
|
+
--icon-depth-neutral: drop-shadow(0 1px 1px oklch(0 0 0 / 0.47))
|
|
332
|
+
drop-shadow(0 2px 4px oklch(0 0 0 / 0.34));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* Named 11px step (`text-2xs`) for micro-labels: overlay pills, hour
|
|
336
|
+
* sub-headers, hype badge, concept chip. One token instead of scattered
|
|
337
|
+
* `text-[11px]` arbitrary values — the smallest size we allow; anything
|
|
338
|
+
* below is illegible. */
|
|
339
|
+
@theme {
|
|
340
|
+
--text-2xs: 0.6875rem;
|
|
341
|
+
--text-2xs--line-height: 1rem;
|
|
316
342
|
}
|
|
317
343
|
|
|
318
344
|
@theme inline {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
resolveGradeBand,
|
|
4
|
+
formatGrade,
|
|
5
|
+
shouldShowGradeNumber,
|
|
6
|
+
resolveDimensionTone,
|
|
7
|
+
} from "./grade";
|
|
8
|
+
|
|
9
|
+
describe("resolveGradeBand", () => {
|
|
10
|
+
it("returns 'exceptional' from 9.0 up to 10", () => {
|
|
11
|
+
expect(resolveGradeBand(9.0)).toBe("exceptional");
|
|
12
|
+
expect(resolveGradeBand(10)).toBe("exceptional");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns 'great' across 7.5–8.9", () => {
|
|
16
|
+
expect(resolveGradeBand(7.5)).toBe("great");
|
|
17
|
+
expect(resolveGradeBand(8.9)).toBe("great");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("returns 'good' across 6.0–7.4", () => {
|
|
21
|
+
expect(resolveGradeBand(6.0)).toBe("good");
|
|
22
|
+
expect(resolveGradeBand(7.4)).toBe("good");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns 'decent' across 5.0–5.9", () => {
|
|
26
|
+
expect(resolveGradeBand(5.0)).toBe("decent");
|
|
27
|
+
expect(resolveGradeBand(5.9)).toBe("decent");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns 'weak' below 5.0", () => {
|
|
31
|
+
expect(resolveGradeBand(4.9)).toBe("weak");
|
|
32
|
+
expect(resolveGradeBand(1)).toBe("weak");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("formatGrade", () => {
|
|
37
|
+
it("keeps one decimal of precision", () => {
|
|
38
|
+
expect(formatGrade(8.6)).toBe("8.6");
|
|
39
|
+
expect(formatGrade(7.45)).toBe("7.5");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("drops a trailing .0 so whole grades read cleanly", () => {
|
|
43
|
+
expect(formatGrade(7)).toBe("7");
|
|
44
|
+
expect(formatGrade(10)).toBe("10");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("clamps to the 1–10 Romanian school scale", () => {
|
|
48
|
+
expect(formatGrade(11.2)).toBe("10");
|
|
49
|
+
expect(formatGrade(0.4)).toBe("1");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("resolveDimensionTone", () => {
|
|
54
|
+
it("is 'strong' at 75 and above", () => {
|
|
55
|
+
expect(resolveDimensionTone(75)).toBe("strong");
|
|
56
|
+
expect(resolveDimensionTone(98)).toBe("strong");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("is 'adequate' across 50–74", () => {
|
|
60
|
+
expect(resolveDimensionTone(50)).toBe("adequate");
|
|
61
|
+
expect(resolveDimensionTone(74)).toBe("adequate");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("is 'low' below 50", () => {
|
|
65
|
+
expect(resolveDimensionTone(49)).toBe("low");
|
|
66
|
+
expect(resolveDimensionTone(20)).toBe("low");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("shouldShowGradeNumber", () => {
|
|
71
|
+
it("hides the number when the backend recommends against it", () => {
|
|
72
|
+
expect(shouldShowGradeNumber({ show_number: false })).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("shows the number when recommended", () => {
|
|
76
|
+
expect(shouldShowGradeNumber({ show_number: true })).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("defaults to showing the number when unspecified", () => {
|
|
80
|
+
expect(shouldShowGradeNumber({})).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
package/src/lib/grade.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the ReviewsDigest `grade` (1–10 Romanian school scale,
|
|
3
|
+
* food-anchored). Shared by `<GradeAtom>` and any surface that demotes the
|
|
4
|
+
* Google star in favour of the grade.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Colour/quality band, derived from `grade.overall`. The label text comes
|
|
8
|
+
* from the caller's `GradeAtomLabels`; the band only drives the visual tone. */
|
|
9
|
+
export type GradeBand = "exceptional" | "great" | "good" | "decent" | "weak";
|
|
10
|
+
|
|
11
|
+
/** Map a 1–10 overall score to its band. Boundaries are inclusive at the low end
|
|
12
|
+
* (9.0 → exceptional, 7.5 → great, 6.0 → good, 5.0 → decent, below 5 → weak). */
|
|
13
|
+
export function resolveGradeBand(overall: number): GradeBand {
|
|
14
|
+
if (overall >= 9) return "exceptional";
|
|
15
|
+
if (overall >= 7.5) return "great";
|
|
16
|
+
if (overall >= 6) return "good";
|
|
17
|
+
if (overall >= 5) return "decent";
|
|
18
|
+
return "weak";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Format a grade for display: clamp to the 1–10 scale, keep one decimal of
|
|
22
|
+
* precision, and drop a trailing `.0` so whole grades read as "7" not "7.0". */
|
|
23
|
+
export function formatGrade(overall: number): string {
|
|
24
|
+
const clamped = Math.min(10, Math.max(1, overall));
|
|
25
|
+
const rounded = Math.round(clamped * 10) / 10;
|
|
26
|
+
return rounded.toFixed(1).replace(/\.0$/, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The backend `show_number` is a display recommendation: when explicitly
|
|
30
|
+
* false, fall back to the soft state instead of the number. Unspecified → show. */
|
|
31
|
+
export function shouldShowGradeNumber(grade: { show_number?: boolean }): boolean {
|
|
32
|
+
return grade.show_number !== false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether `<GradeAtom>` will render anything for this grade at this size — the
|
|
37
|
+
* predicate every caller must gate on before choosing the grade over its own
|
|
38
|
+
* Google-star fallback.
|
|
39
|
+
*
|
|
40
|
+
* `overall == null` has always meant "no grade, show the star". The `xs` rule
|
|
41
|
+
* is newer: at card size the right-hand slot is a 2–3 glyph METRIC slot, and
|
|
42
|
+
* the soft state is a two-word sentence ("Worth exploring" / "Merită
|
|
43
|
+
* explorat"). Rendered there it wrapped inside its own fixed-height pill and
|
|
44
|
+
* pushed the title into a hard truncation. The sentence still renders at `sm`+
|
|
45
|
+
* (drawer, hero) where there is room for it.
|
|
46
|
+
*
|
|
47
|
+
* Callers used to gate on `grade?.overall != null` directly, which is why this
|
|
48
|
+
* exists as a shared function: with the size rule added, that check no longer
|
|
49
|
+
* predicts whether the atom renders, and a caller using the old one lands on
|
|
50
|
+
* an empty slot instead of falling through to its star.
|
|
51
|
+
*
|
|
52
|
+
* Lives here rather than in `grade-atom.tsx` so SERVER components can call it —
|
|
53
|
+
* everything exported from a `"use client"` module is a client reference.
|
|
54
|
+
*
|
|
55
|
+
* A type predicate, so the true branch also narrows away the `undefined` that
|
|
56
|
+
* `place.reviews_digest?.grade` carries and the caller can pass it straight to
|
|
57
|
+
* `<GradeAtom grade={...}>`.
|
|
58
|
+
*/
|
|
59
|
+
export function showsGradeAtom<T extends { overall?: number | null; show_number?: boolean }>(
|
|
60
|
+
grade: T | null | undefined,
|
|
61
|
+
size: "xs" | "sm" | "md" | "lg"
|
|
62
|
+
): grade is T {
|
|
63
|
+
if (grade?.overall == null) return false;
|
|
64
|
+
return size !== "xs" || shouldShowGradeNumber(grade);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Tone for a single 0–100 dimension score, driving the page dimension-bar
|
|
68
|
+
* colour: strong (≥75) = accent, adequate (50–74) = muted, low (<50) = amber
|
|
69
|
+
* (a soft "fair warning", never error-red). */
|
|
70
|
+
export type DimensionTone = "strong" | "adequate" | "low";
|
|
71
|
+
|
|
72
|
+
export function resolveDimensionTone(score: number): DimensionTone {
|
|
73
|
+
if (score >= 75) return "strong";
|
|
74
|
+
if (score >= 50) return "adequate";
|
|
75
|
+
return "low";
|
|
76
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layered drop-shadow filter that "raises" a flat icon off a tinted surface.
|
|
3
|
+
*
|
|
4
|
+
* Two stops stacked: a tight 1px close shadow welds the icon to the surface
|
|
5
|
+
* (so it doesn't float disconnected), then a wider 3px/8px lift gives it
|
|
6
|
+
* presence. The combo is what makes the homepage cuisine tiles read as
|
|
7
|
+
* embossed plaques instead of stickers — reuse it anywhere an icon sits on
|
|
8
|
+
* a warm gradient (entity-card photo fallbacks, etc).
|
|
9
|
+
*
|
|
10
|
+
* Designed for white / light icons on the warm `--gradient-primary` family.
|
|
11
|
+
* Don't apply to icons on light surfaces — the dark drop-shadow stops will
|
|
12
|
+
* read as muddy outlines instead of depth.
|
|
13
|
+
*/
|
|
14
|
+
export const RAISED_ICON_FILTER =
|
|
15
|
+
"drop-shadow(0 1px 1px oklch(0 0 0 / 0.6)) drop-shadow(0 3px 8px oklch(0 0 0 / 0.4))";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The light-surface counterpart, for glyphs on the neutral placeholder panel
|
|
19
|
+
* (`from-muted to-border`) that entity-card photo fallbacks share.
|
|
20
|
+
*
|
|
21
|
+
* `RAISED_ICON_FILTER` is black and wide — correct under a white icon on the
|
|
22
|
+
* brown gradient, wrong everywhere else. On a near-white panel its 8px black
|
|
23
|
+
* stop pools under the glyph and reads as grime rather than lift.
|
|
24
|
+
*
|
|
25
|
+
* What fixes that is hue and spread, not just less shadow: this one is a deep
|
|
26
|
+
* brown (same 45° hue as `--shadow-card`) capped at a 4px lift. Halving the
|
|
27
|
+
* spread is what kills the smudge, which then buys enough headroom on alpha
|
|
28
|
+
* for the glyph to still read as raised rather than washed out — a warm
|
|
29
|
+
* shadow tolerates far more opacity than a black one before it looks dirty.
|
|
30
|
+
*
|
|
31
|
+
* Resolves through a token (`--icon-depth-neutral` in `globals.css`), so dark
|
|
32
|
+
* mode gets its own value — there the panel is already dark and a halo does
|
|
33
|
+
* nothing, so it collapses to one contact stop.
|
|
34
|
+
*/
|
|
35
|
+
export const NEUTRAL_ICON_FILTER = "var(--icon-depth-neutral)";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE geometry for every pill that sits on a card image — places and events.
|
|
3
|
+
*
|
|
4
|
+
* These had drifted into four different sizes on the event card (hype at
|
|
5
|
+
* `min-h-8`, Free at `text-xs px-2.5 py-1`, concept and tags at `text-[10px]
|
|
6
|
+
* px-2 py-0.5`) and three more on the place card (Trending at `min-h-8`, the
|
|
7
|
+
* Closed pill at `text-xs py-0.5`, the open dot at `h-2`), so corners of the
|
|
8
|
+
* same card — and cards sitting in the same rail — read as unrelated systems.
|
|
9
|
+
*
|
|
10
|
+
* An explicit `h-6` rather than a padding-derived height is what actually
|
|
11
|
+
* guarantees they match: the hype pill contains a gauge glyph, Trending
|
|
12
|
+
* contains a flame, and the rest contain 12px icons, so equal padding would
|
|
13
|
+
* still produce unequal heights.
|
|
14
|
+
*
|
|
15
|
+
* 24px is deliberate over the 32px `heatPillVariants` `md`. The only argument
|
|
16
|
+
* for 32 was the tap target on the interactive Trending pill, and that pill
|
|
17
|
+
* already carries an `after:-inset-2` hit-area expander (~40px effective)
|
|
18
|
+
* which solves it independently of the visual size. 24px is also exactly the
|
|
19
|
+
* WCAG 2.5.8 (AA) minimum on its own, before the expander.
|
|
20
|
+
*
|
|
21
|
+
* Pair with `Badge` (`variant="overlay"` is the scrim: `bg-foreground/80
|
|
22
|
+
* text-background`) rather than hand-rolled spans, which is what let these
|
|
23
|
+
* drift in the first place. Only the radius is overridden — the `Badge`
|
|
24
|
+
* default is `rounded-lg` and pills on photography read better fully round.
|
|
25
|
+
*/
|
|
26
|
+
export const OVERLAY_PILL = "h-6 gap-1 rounded-full px-2 text-2xs font-semibold shadow-sm";
|
|
27
|
+
|
|
28
|
+
/** Icon size inside an `OVERLAY_PILL`. */
|
|
29
|
+
export const OVERLAY_ICON = "h-3 w-3 shrink-0";
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { MenuFamilyTile, MenuLinkComponent } from './menu-types.js';
|
|
3
|
-
import 'react';
|
|
4
|
-
import './place-types.js';
|
|
5
|
-
|
|
6
|
-
type MenuFamilyTilesProps = {
|
|
7
|
-
tiles: MenuFamilyTile[];
|
|
8
|
-
/**
|
|
9
|
-
* The closing tile: the index behind the seven, drawn exactly like them —
|
|
10
|
-
* its own hue and photograph, `countLabel` as the caption. Only its place in
|
|
11
|
-
* the grid says it is the index. Omit when there is none.
|
|
12
|
-
*/
|
|
13
|
-
all?: MenuFamilyTile;
|
|
14
|
-
linkComponent?: MenuLinkComponent;
|
|
15
|
-
className?: string;
|
|
16
|
-
};
|
|
17
|
-
declare function MenuFamilyTiles({ tiles, all, linkComponent: LinkComponent, className, }: MenuFamilyTilesProps): react_jsx_runtime.JSX.Element;
|
|
18
|
-
|
|
19
|
-
export { MenuFamilyTiles, type MenuFamilyTilesProps };
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { PhotoGround } from '../chunk-CNJQUHCU.js';
|
|
2
|
-
import { cn } from '../chunk-FEK5XGZW.js';
|
|
3
|
-
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
4
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
-
|
|
6
|
-
function DefaultLink(_a) {
|
|
7
|
-
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
8
|
-
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
9
|
-
}
|
|
10
|
-
var TILE = "group relative isolate flex aspect-[16/10] min-h-[108px] flex-col justify-end overflow-hidden rounded-2xl bg-muted p-3.5 text-white lg:p-4 transition-[transform,box-shadow] duration-200 hover:-translate-y-0.5 hover:shadow-[var(--shadow-card-hover)] active:translate-y-0 active:scale-[0.99] motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2";
|
|
11
|
-
var POLAROID = "absolute -right-[4%] -top-[8%] z-0 block aspect-square h-[82%] overflow-hidden rounded-[10%] rotate-[8deg] shadow-[0_10px_22px_-6px_oklch(0.16_0.012_55_/_0.65)] transition-transform duration-300 group-hover:rotate-[5deg] motion-reduce:transition-none";
|
|
12
|
-
var SCRIM = "after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:z-0 after:h-3/5 after:bg-gradient-to-t after:from-[oklch(0.16_0.012_55_/_0.55)] after:to-transparent after:content-['']";
|
|
13
|
-
function MenuFamilyTiles({
|
|
14
|
-
tiles,
|
|
15
|
-
all,
|
|
16
|
-
linkComponent: LinkComponent = DefaultLink,
|
|
17
|
-
className
|
|
18
|
-
}) {
|
|
19
|
-
const cells = all ? [...tiles, all] : tiles;
|
|
20
|
-
return /* @__PURE__ */ jsx("div", { className: cn("grid grid-cols-2 gap-3 lg:grid-cols-4 lg:gap-3.5", className), children: cells.map((tile) => /* @__PURE__ */ jsxs(
|
|
21
|
-
LinkComponent,
|
|
22
|
-
{
|
|
23
|
-
href: tile.href,
|
|
24
|
-
className: cn(TILE, SCRIM),
|
|
25
|
-
style: tile.gradient ? { backgroundImage: tile.gradient } : void 0,
|
|
26
|
-
children: [
|
|
27
|
-
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: POLAROID, children: tile.photoUrl ? (
|
|
28
|
-
/* Not lazy: the grid sits directly under the hero, and these
|
|
29
|
-
photographs are the first thing the reader looks at. */
|
|
30
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
31
|
-
/* @__PURE__ */ jsx(
|
|
32
|
-
"img",
|
|
33
|
-
{
|
|
34
|
-
src: tile.photoUrl,
|
|
35
|
-
alt: "",
|
|
36
|
-
decoding: "async",
|
|
37
|
-
className: "absolute inset-0 h-full w-full object-cover"
|
|
38
|
-
}
|
|
39
|
-
)
|
|
40
|
-
) : /* @__PURE__ */ jsx(
|
|
41
|
-
PhotoGround,
|
|
42
|
-
{
|
|
43
|
-
photos: [void 0],
|
|
44
|
-
initials: tile.label.charAt(0),
|
|
45
|
-
mode: "stack",
|
|
46
|
-
glyphClassName: "text-[44px]"
|
|
47
|
-
}
|
|
48
|
-
) }),
|
|
49
|
-
/* @__PURE__ */ jsx("span", { className: "relative z-[1] line-clamp-2 max-w-[55%] text-[14.5px] font-extrabold leading-[1.2] tracking-[-0.015em] lg:text-[16px]", children: tile.label }),
|
|
50
|
-
tile.countLabel && /* @__PURE__ */ jsx("span", { className: "relative z-[1] mt-0.5 max-w-[55%] truncate text-[12px] font-medium leading-[1.35] tabular-nums text-white/80 lg:text-[13px]", children: tile.countLabel })
|
|
51
|
-
]
|
|
52
|
-
},
|
|
53
|
-
tile.slug
|
|
54
|
-
)) });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export { MenuFamilyTiles };
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { MenuFamilyTiles } from "./menu-family-tiles";
|
|
3
|
-
import { mockFamilyTiles, mockFamilyTilesAll } from "./menu-mocks";
|
|
4
|
-
|
|
5
|
-
const meta: Meta<typeof MenuFamilyTiles> = {
|
|
6
|
-
title: "Menu/MenuFamilyTiles",
|
|
7
|
-
component: MenuFamilyTiles,
|
|
8
|
-
tags: ["autodocs"],
|
|
9
|
-
decorators: [
|
|
10
|
-
(Story) => (
|
|
11
|
-
<div className="w-[1100px] bg-background p-4">
|
|
12
|
-
<Story />
|
|
13
|
-
</div>
|
|
14
|
-
),
|
|
15
|
-
],
|
|
16
|
-
parameters: {
|
|
17
|
-
docs: {
|
|
18
|
-
description: {
|
|
19
|
-
component:
|
|
20
|
-
"The hub's doorways: every family the city browses by, as a grid of coloured tiles — the family's name, its venue count and one venue's photograph tilted off the corner — closed by one tile that opens the whole index. Two to a row on a phone, four from `lg`, so seven families and the index make two rows at every width.",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export default meta;
|
|
27
|
-
type Story = StoryObj<typeof MenuFamilyTiles>;
|
|
28
|
-
|
|
29
|
-
export const Default: Story = { args: { tiles: mockFamilyTiles, all: mockFamilyTilesAll } };
|
|
30
|
-
|
|
31
|
-
/** Two-up on a phone — seven families and the index in four rows. */
|
|
32
|
-
export const Mobile: Story = {
|
|
33
|
-
args: { tiles: mockFamilyTiles, all: mockFamilyTilesAll },
|
|
34
|
-
// The grid's columns follow the viewport, not the wrapper.
|
|
35
|
-
parameters: { viewport: { defaultViewport: "mobile1" } },
|
|
36
|
-
decorators: [
|
|
37
|
-
(Story) => (
|
|
38
|
-
<div className="w-[390px] bg-background p-3">
|
|
39
|
-
<Story />
|
|
40
|
-
</div>
|
|
41
|
-
),
|
|
42
|
-
],
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/** No photographs anywhere: every polaroid holds the family's initial. */
|
|
46
|
-
export const WithoutPhotos: Story = {
|
|
47
|
-
args: {
|
|
48
|
-
tiles: mockFamilyTiles.map(({ photoUrl: _photoUrl, ...tile }) => tile),
|
|
49
|
-
all: { ...mockFamilyTilesAll, photoUrl: undefined },
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/** The drinks tab's five going-out families, no index tile. */
|
|
54
|
-
export const Partial: Story = { args: { tiles: mockFamilyTiles.slice(0, 5) } };
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { MenuFamilyTiles } from "./menu-family-tiles";
|
|
4
|
-
import { mockFamilyTiles, mockFamilyTilesAll } from "./menu-mocks";
|
|
5
|
-
|
|
6
|
-
describe("MenuFamilyTiles", () => {
|
|
7
|
-
it("renders one link per family plus the index tile, in order", () => {
|
|
8
|
-
render(<MenuFamilyTiles tiles={mockFamilyTiles} all={mockFamilyTilesAll} />);
|
|
9
|
-
const links = screen.getAllByRole("link");
|
|
10
|
-
expect(links).toHaveLength(mockFamilyTiles.length + 1);
|
|
11
|
-
expect(links[0]).toHaveAttribute("href", mockFamilyTiles[0].href);
|
|
12
|
-
expect(links.at(-1)).toHaveAttribute("href", mockFamilyTilesAll.href);
|
|
13
|
-
expect(links.at(-1)).toHaveTextContent(mockFamilyTilesAll.label);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
// A family without a photograph keeps the polaroid — the initial fills it —
|
|
17
|
-
// so a row of seven never has one tile with a different silhouette.
|
|
18
|
-
it("draws the initial where a family has no photograph", () => {
|
|
19
|
-
const [withPhoto, withoutPhoto] = [mockFamilyTiles[0], mockFamilyTiles.at(-1)!];
|
|
20
|
-
expect(withoutPhoto.photoUrl).toBeUndefined();
|
|
21
|
-
render(<MenuFamilyTiles tiles={[withPhoto, withoutPhoto]} />);
|
|
22
|
-
expect(document.querySelectorAll("img")).toHaveLength(1);
|
|
23
|
-
expect(screen.getByText(withoutPhoto.label.charAt(0))).toBeInTheDocument();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("omits the index tile when no index is given", () => {
|
|
27
|
-
render(<MenuFamilyTiles tiles={mockFamilyTiles.slice(0, 3)} />);
|
|
28
|
-
expect(screen.getAllByRole("link")).toHaveLength(3);
|
|
29
|
-
});
|
|
30
|
-
});
|