@bearmenu/ui 0.8.7 → 0.8.9
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-637L7XQ3.js → chunk-3TNPSN4D.js} +28 -9
- package/dist/chunk-62ES6N32.js +27 -0
- package/dist/chunk-AHL45DD2.js +145 -0
- package/dist/chunk-AXV5O2IZ.js +58 -0
- package/dist/chunk-IYDH5IU6.js +66 -0
- package/dist/chunk-OPVKL4VS.js +18 -0
- package/dist/chunk-Q6QH52LJ.js +38 -0
- package/dist/chunk-TH6W2PWG.js +213 -0
- package/dist/chunk-THOVPZNN.js +37 -0
- package/dist/chunk-YS6BHKZO.js +92 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/category-deck.js +10 -1
- package/dist/components/command.d.ts +7 -7
- package/dist/components/concept-compare.d.ts +73 -0
- package/dist/components/concept-compare.js +162 -0
- package/dist/components/coverflow.d.ts +11 -3
- package/dist/components/coverflow.js +34 -51
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/drift-rail.d.ts +24 -10
- package/dist/components/drift-rail.js +1 -1
- package/dist/components/event-card.d.ts +8 -0
- package/dist/components/event-card.js +64 -34
- package/dist/components/event-compact-row.d.ts +33 -6
- package/dist/components/event-compact-row.js +120 -39
- package/dist/components/event-day-header.js +3 -1
- package/dist/components/event-day-separator.d.ts +7 -1
- package/dist/components/event-day-separator.js +3 -2
- package/dist/components/event-detail-panel.d.ts +171 -25
- package/dist/components/event-detail-panel.js +471 -174
- package/dist/components/event-hub-hero.d.ts +25 -4
- package/dist/components/event-hub-hero.js +89 -19
- package/dist/components/event-list-row-wide.js +1 -1
- package/dist/components/event-list-row.d.ts +19 -6
- package/dist/components/event-list-row.js +79 -35
- package/dist/components/hour-band.d.ts +38 -40
- package/dist/components/hour-band.js +55 -27
- package/dist/components/hour-group.js +1 -1
- package/dist/components/hub-surface.d.ts +27 -0
- package/dist/components/hub-surface.js +20 -0
- package/dist/components/menu-family-card.d.ts +59 -0
- package/dist/components/menu-family-card.js +121 -0
- package/dist/components/menu-field.d.ts +57 -0
- package/dist/components/menu-field.js +3 -0
- package/dist/components/menu-hub-hero.d.ts +65 -0
- package/dist/components/menu-hub-hero.js +103 -0
- package/dist/components/menu-intent-tiles.d.ts +31 -0
- package/dist/components/menu-intent-tiles.js +50 -0
- package/dist/components/menu-item.d.ts +127 -0
- package/dist/components/menu-item.js +5 -0
- package/dist/components/menu-mocks.d.ts +55 -0
- package/dist/components/menu-mocks.js +379 -0
- package/dist/components/menu-types.d.ts +245 -0
- package/dist/components/menu-types.js +1 -0
- package/dist/components/motion-icon-set.js +15 -3
- package/dist/components/open-now-list.d.ts +40 -0
- package/dist/components/open-now-list.js +69 -0
- package/dist/components/place-types.d.ts +112 -1
- package/dist/components/poster-tape.js +1 -1
- package/dist/components/poster-wall.d.ts +3 -3
- package/dist/components/poster-wall.js +73 -17
- package/dist/components/ranked-venue-list.d.ts +49 -0
- package/dist/components/ranked-venue-list.js +87 -0
- package/dist/components/section-header.d.ts +16 -1
- package/dist/components/section-header.js +26 -7
- package/dist/components/signature-chip.d.ts +37 -0
- package/dist/components/signature-chip.js +4 -0
- package/dist/components/signature-line-list.d.ts +45 -0
- package/dist/components/signature-line-list.js +99 -0
- package/dist/components/snap-rail.d.ts +54 -0
- package/dist/components/snap-rail.js +109 -0
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/stat-banner.d.ts +47 -0
- package/dist/components/stat-banner.js +61 -0
- package/dist/components/text.d.ts +3 -3
- package/dist/components/timeline-band.d.ts +20 -2
- package/dist/components/timeline-band.js +1 -1
- package/dist/components/timeline-playhead.d.ts +26 -0
- package/dist/components/timeline-playhead.js +25 -0
- package/dist/components/venue-busyness-strip.d.ts +35 -0
- package/dist/components/venue-busyness-strip.js +37 -0
- package/dist/components/venue-grade-dimensions.d.ts +36 -0
- package/dist/components/venue-grade-dimensions.js +3 -0
- package/dist/components/venue-quotes.d.ts +32 -0
- package/dist/components/venue-quotes.js +3 -0
- package/dist/components/venue-signature-block.d.ts +39 -0
- package/dist/components/venue-signature-block.js +6 -0
- package/dist/components/venue-spotlight.d.ts +74 -0
- package/dist/components/venue-spotlight.js +138 -0
- package/dist/components/venue-wall.d.ts +41 -0
- package/dist/components/venue-wall.js +118 -0
- package/dist/components/weekday-theme-banner.d.ts +70 -0
- package/dist/components/weekday-theme-banner.js +127 -0
- package/dist/components/weekend-panel.d.ts +5 -4
- package/dist/components/weekend-panel.js +85 -71
- package/dist/hooks/use-drag-swipe.d.ts +44 -0
- package/dist/hooks/use-drag-swipe.js +2 -0
- package/dist/hooks/use-media-query.d.ts +17 -0
- package/dist/hooks/use-media-query.js +2 -0
- package/package.json +1 -1
- package/src/components/category-deck.tsx +19 -8
- package/src/components/colour-dictionary.stories.tsx +227 -0
- package/src/components/concept-compare.stories.tsx +104 -0
- package/src/components/concept-compare.tsx +276 -0
- package/src/components/coverflow.test.tsx +28 -1
- package/src/components/coverflow.tsx +37 -43
- package/src/components/drift-rail.tsx +167 -42
- package/src/components/event-card.tsx +55 -10
- package/src/components/event-compact-row.stories.tsx +30 -0
- package/src/components/event-compact-row.test.tsx +49 -0
- package/src/components/event-compact-row.tsx +162 -38
- package/src/components/event-day-header.tsx +3 -1
- package/src/components/event-day-separator.tsx +8 -1
- package/src/components/event-detail-panel.stories.tsx +205 -97
- package/src/components/event-detail-panel.test.tsx +181 -0
- package/src/components/event-detail-panel.tsx +703 -185
- package/src/components/event-hub-hero.stories.tsx +30 -0
- package/src/components/event-hub-hero.tsx +102 -30
- package/src/components/event-list-row.stories.tsx +34 -0
- package/src/components/event-list-row.test.tsx +53 -0
- package/src/components/event-list-row.tsx +135 -87
- package/src/components/hour-band.stories.tsx +28 -5
- package/src/components/hour-band.tsx +90 -70
- package/src/components/hub-surface.stories.tsx +68 -0
- package/src/components/hub-surface.tsx +40 -0
- package/src/components/menu-family-card.stories.tsx +112 -0
- package/src/components/menu-family-card.tsx +211 -0
- package/src/components/menu-field.stories.tsx +74 -0
- package/src/components/menu-field.tsx +199 -0
- package/src/components/menu-hub-hero.stories.tsx +92 -0
- package/src/components/menu-hub-hero.tsx +187 -0
- package/src/components/menu-intent-tiles.stories.tsx +54 -0
- package/src/components/menu-intent-tiles.tsx +76 -0
- package/src/components/menu-item.tsx +416 -0
- package/src/components/menu-mocks.ts +417 -0
- package/src/components/menu-types.ts +259 -0
- package/src/components/motion-icon-set.tsx +30 -3
- package/src/components/open-now-list.stories.tsx +76 -0
- package/src/components/open-now-list.tsx +127 -0
- package/src/components/place-types.ts +112 -0
- package/src/components/poster-wall.test.tsx +28 -0
- package/src/components/poster-wall.tsx +96 -23
- package/src/components/ranked-venue-list.stories.tsx +80 -0
- package/src/components/ranked-venue-list.tsx +177 -0
- package/src/components/section-header.stories.tsx +24 -0
- package/src/components/section-header.test.tsx +28 -0
- package/src/components/section-header.tsx +45 -7
- package/src/components/signature-chip.tsx +77 -0
- package/src/components/signature-line-list.stories.tsx +85 -0
- package/src/components/signature-line-list.tsx +136 -0
- package/src/components/snap-rail.stories.tsx +79 -0
- package/src/components/snap-rail.test.tsx +18 -0
- package/src/components/snap-rail.tsx +148 -0
- package/src/components/stat-banner.stories.tsx +100 -0
- package/src/components/stat-banner.tsx +117 -0
- package/src/components/timeline-band.tsx +52 -12
- package/src/components/timeline-playhead.stories.tsx +30 -0
- package/src/components/timeline-playhead.test.tsx +11 -0
- package/src/components/timeline-playhead.tsx +46 -0
- package/src/components/venue-busyness-strip.stories.tsx +66 -0
- package/src/components/venue-busyness-strip.tsx +64 -0
- package/src/components/venue-grade-dimensions.stories.tsx +83 -0
- package/src/components/venue-grade-dimensions.tsx +91 -0
- package/src/components/venue-quotes.stories.tsx +59 -0
- package/src/components/venue-quotes.tsx +64 -0
- package/src/components/venue-signature-block.stories.tsx +68 -0
- package/src/components/venue-signature-block.tsx +71 -0
- package/src/components/venue-spotlight.stories.tsx +141 -0
- package/src/components/venue-spotlight.tsx +236 -0
- package/src/components/venue-wall.stories.tsx +78 -0
- package/src/components/venue-wall.tsx +185 -0
- package/src/components/weekday-theme-banner.stories.tsx +113 -0
- package/src/components/weekday-theme-banner.tsx +231 -0
- package/src/components/weekend-panel.test.tsx +43 -0
- package/src/components/weekend-panel.tsx +142 -108
- package/src/hooks/use-drag-swipe.test.tsx +66 -0
- package/src/hooks/use-drag-swipe.ts +105 -0
- package/src/hooks/use-media-query.ts +31 -0
- package/dist/chunk-GPH6L4DX.js +0 -55
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import { useMediaQuery, DESKTOP_MEDIA_QUERY } from '../chunk-OPVKL4VS.js';
|
|
1
2
|
import { MotionPlayPause } from '../chunk-TT3WFS6S.js';
|
|
2
3
|
import '../chunk-2WDJR6K7.js';
|
|
4
|
+
import { useDragSwipe } from '../chunk-IYDH5IU6.js';
|
|
3
5
|
import { Skeleton } from '../chunk-QXHMPJ35.js';
|
|
4
6
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
5
|
-
import {
|
|
7
|
+
import { __spreadProps, __spreadValues, __objRest } from '../chunk-3GWWZKX7.js';
|
|
6
8
|
import * as React from 'react';
|
|
7
9
|
import { ChevronRight } from 'lucide-react';
|
|
8
10
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
9
11
|
|
|
10
12
|
var WEEKEND_PANEL_TRACK_GAP_PX = 14;
|
|
11
13
|
var WEEKEND_PANEL_DEFAULT_DURATION_SECONDS = 7;
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
var WEEKEND_PANEL_DOT_B_KEYFRAMES_ID = "ds-weekend-panel-dot-b";
|
|
14
|
+
var WEEKEND_PANEL_PAGE_COUNT = 2;
|
|
15
|
+
var WEEKEND_PANEL_CARDS_PER_PAGE = 2;
|
|
15
16
|
function usePrefersReducedMotion() {
|
|
16
17
|
const [reduced, setReduced] = React.useState(false);
|
|
17
18
|
React.useEffect(() => {
|
|
@@ -30,9 +31,12 @@ function useInViewport() {
|
|
|
30
31
|
React.useEffect(() => {
|
|
31
32
|
const node = ref.current;
|
|
32
33
|
if (!node || typeof IntersectionObserver === "undefined") return;
|
|
33
|
-
const observer = new IntersectionObserver(
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const observer = new IntersectionObserver(
|
|
35
|
+
([entry]) => setInView(entry.isIntersecting),
|
|
36
|
+
{
|
|
37
|
+
threshold: 0
|
|
38
|
+
}
|
|
39
|
+
);
|
|
36
40
|
observer.observe(node);
|
|
37
41
|
return () => observer.disconnect();
|
|
38
42
|
}, []);
|
|
@@ -56,16 +60,36 @@ function WeekendPanel({
|
|
|
56
60
|
className
|
|
57
61
|
}) {
|
|
58
62
|
const prefersReducedMotion = usePrefersReducedMotion();
|
|
63
|
+
const isDesktop = useMediaQuery(DESKTOP_MEDIA_QUERY);
|
|
59
64
|
const [panelRef, inView] = useInViewport();
|
|
60
65
|
const [isHovered, setIsHovered] = React.useState(false);
|
|
61
66
|
const [isFocused, setIsFocused] = React.useState(false);
|
|
62
67
|
const [isManuallyPaused, setIsManuallyPaused] = React.useState(false);
|
|
63
68
|
const [playPauseReplayToken, setPlayPauseReplayToken] = React.useState(0);
|
|
64
|
-
const
|
|
69
|
+
const [page, setPage] = React.useState(0);
|
|
70
|
+
const autoplays = isDesktop && !prefersReducedMotion;
|
|
71
|
+
const isPaused = !autoplays || isManuallyPaused || isHovered || isFocused || !inView;
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
if (isPaused) return void 0;
|
|
74
|
+
const interval = window.setInterval(
|
|
75
|
+
() => setPage((current) => (current + 1) % WEEKEND_PANEL_PAGE_COUNT),
|
|
76
|
+
durationSeconds * 1e3 / WEEKEND_PANEL_PAGE_COUNT
|
|
77
|
+
);
|
|
78
|
+
return () => window.clearInterval(interval);
|
|
79
|
+
}, [durationSeconds, isPaused]);
|
|
80
|
+
const turnTo = (next) => {
|
|
81
|
+
setPage(Math.max(0, Math.min(WEEKEND_PANEL_PAGE_COUNT - 1, next)));
|
|
82
|
+
setIsManuallyPaused(true);
|
|
83
|
+
};
|
|
84
|
+
const { dragX, isDragging, handlers } = useDragSwipe({
|
|
85
|
+
onSwipe: (direction) => turnTo(page + direction),
|
|
86
|
+
disabled: prefersReducedMotion
|
|
87
|
+
});
|
|
65
88
|
const action = /* @__PURE__ */ jsxs("span", { className: "ml-auto flex items-center gap-0.5 text-[12.5px] text-muted-foreground", children: [
|
|
66
89
|
actionLabel,
|
|
67
90
|
/* @__PURE__ */ jsx(ChevronRight, { className: "h-3.5 w-3.5", "aria-hidden": true })
|
|
68
91
|
] });
|
|
92
|
+
const cards = React.Children.toArray(children);
|
|
69
93
|
return /* @__PURE__ */ jsxs(
|
|
70
94
|
"div",
|
|
71
95
|
{
|
|
@@ -80,25 +104,6 @@ function WeekendPanel({
|
|
|
80
104
|
className
|
|
81
105
|
),
|
|
82
106
|
children: [
|
|
83
|
-
/* @__PURE__ */ jsx("style", { children: `
|
|
84
|
-
@keyframes ${WEEKEND_PANEL_SLIDE_KEYFRAMES_ID} {
|
|
85
|
-
0%, 45% { transform: translateX(0); }
|
|
86
|
-
50%, 95% { transform: translateX(calc(-100% - ${WEEKEND_PANEL_TRACK_GAP_PX}px)); }
|
|
87
|
-
100% { transform: translateX(0); }
|
|
88
|
-
}
|
|
89
|
-
@keyframes ${WEEKEND_PANEL_DOT_A_KEYFRAMES_ID} {
|
|
90
|
-
0% { background: var(--primary); width: 18px; }
|
|
91
|
-
45% { background: var(--primary); width: 18px; }
|
|
92
|
-
50% { background: color-mix(in oklch, var(--muted-foreground) 35%, transparent); width: 6px; }
|
|
93
|
-
100% { background: color-mix(in oklch, var(--muted-foreground) 35%, transparent); width: 6px; }
|
|
94
|
-
}
|
|
95
|
-
@keyframes ${WEEKEND_PANEL_DOT_B_KEYFRAMES_ID} {
|
|
96
|
-
0% { background: color-mix(in oklch, var(--muted-foreground) 35%, transparent); width: 6px; }
|
|
97
|
-
50% { background: color-mix(in oklch, var(--muted-foreground) 35%, transparent); width: 6px; }
|
|
98
|
-
95% { background: var(--primary); width: 18px; }
|
|
99
|
-
100% { background: var(--primary); width: 18px; }
|
|
100
|
-
}
|
|
101
|
-
` }),
|
|
102
107
|
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-[9px] px-4 pb-3", children: [
|
|
103
108
|
/* @__PURE__ */ jsx(
|
|
104
109
|
"span",
|
|
@@ -130,51 +135,57 @@ function WeekendPanel({
|
|
|
130
135
|
}
|
|
131
136
|
) : action
|
|
132
137
|
] }),
|
|
133
|
-
prefersReducedMotion ? /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3.5 px-4", children }) :
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
138
|
+
prefersReducedMotion ? /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3.5 px-4", children }) : (
|
|
139
|
+
// Padding OUTSIDE the clip. `overflow-hidden` clips at the padding
|
|
140
|
+
// edge, so with the padding on the clipping element the next page's
|
|
141
|
+
// first card showed through the 16px inset — a sliver of the slide
|
|
142
|
+
// cutting in. The reader should see exactly the two cards of a page.
|
|
143
|
+
/* @__PURE__ */ jsx("div", { className: "px-4", children: /* @__PURE__ */ jsx(
|
|
144
|
+
"div",
|
|
145
|
+
__spreadProps(__spreadValues({}, handlers), {
|
|
146
|
+
className: "overflow-hidden select-none [touch-action:pan-y]",
|
|
147
|
+
children: /* @__PURE__ */ jsx(
|
|
148
|
+
"div",
|
|
149
|
+
{
|
|
150
|
+
className: cn(
|
|
151
|
+
"flex gap-3.5",
|
|
152
|
+
!isDragging && "transition-transform duration-[420ms] ease-[cubic-bezier(0.22,1,0.36,1)]"
|
|
153
|
+
),
|
|
154
|
+
style: {
|
|
155
|
+
transform: `translateX(calc(${-page} * (100% + ${WEEKEND_PANEL_TRACK_GAP_PX}px) + ${dragX}px))`
|
|
156
|
+
},
|
|
157
|
+
children: cards.map((child, index) => /* @__PURE__ */ jsx(
|
|
158
|
+
"div",
|
|
159
|
+
{
|
|
160
|
+
className: "w-[calc((100%-14px)/2)] flex-none",
|
|
161
|
+
onFocus: () => {
|
|
162
|
+
const cardPage = Math.floor(
|
|
163
|
+
index / WEEKEND_PANEL_CARDS_PER_PAGE
|
|
164
|
+
);
|
|
165
|
+
if (cardPage !== page) turnTo(cardPage);
|
|
166
|
+
},
|
|
167
|
+
children: child
|
|
168
|
+
},
|
|
169
|
+
index
|
|
170
|
+
))
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
) })
|
|
175
|
+
),
|
|
147
176
|
!prefersReducedMotion && /* @__PURE__ */ jsxs("div", { className: "mt-3.5 grid grid-cols-[1fr_auto_1fr] items-center px-4", children: [
|
|
148
177
|
/* @__PURE__ */ jsx("span", { "aria-hidden": true }),
|
|
149
|
-
/* @__PURE__ */
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
),
|
|
163
|
-
/* @__PURE__ */ jsx(
|
|
164
|
-
"span",
|
|
165
|
-
{
|
|
166
|
-
className: "block h-1.5 w-1.5 rounded-full bg-muted-foreground/35",
|
|
167
|
-
style: {
|
|
168
|
-
animationName: WEEKEND_PANEL_DOT_B_KEYFRAMES_ID,
|
|
169
|
-
animationDuration: `${durationSeconds}s`,
|
|
170
|
-
animationTimingFunction: "ease-in-out",
|
|
171
|
-
animationIterationCount: "infinite",
|
|
172
|
-
animationPlayState: isPaused ? "paused" : "running"
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
)
|
|
176
|
-
] }),
|
|
177
|
-
/* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
|
|
178
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-center gap-1.5", "aria-hidden": true, children: Array.from({ length: WEEKEND_PANEL_PAGE_COUNT }, (_, index) => /* @__PURE__ */ jsx(
|
|
179
|
+
"span",
|
|
180
|
+
{
|
|
181
|
+
className: cn(
|
|
182
|
+
"block h-1.5 rounded-full transition-[width,background-color] duration-300 motion-reduce:transition-none",
|
|
183
|
+
index === page ? "w-[18px] bg-primary" : "w-1.5 bg-muted-foreground/35"
|
|
184
|
+
)
|
|
185
|
+
},
|
|
186
|
+
index
|
|
187
|
+
)) }),
|
|
188
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-end", children: autoplays && /* @__PURE__ */ jsx(
|
|
178
189
|
"button",
|
|
179
190
|
{
|
|
180
191
|
type: "button",
|
|
@@ -198,7 +209,10 @@ function WeekendPanel({
|
|
|
198
209
|
}
|
|
199
210
|
);
|
|
200
211
|
}
|
|
201
|
-
function WeekendPanelSkeleton({
|
|
212
|
+
function WeekendPanelSkeleton({
|
|
213
|
+
isHighlighted,
|
|
214
|
+
className
|
|
215
|
+
}) {
|
|
202
216
|
return /* @__PURE__ */ jsxs(
|
|
203
217
|
"div",
|
|
204
218
|
{
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { PointerEvent } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useDragSwipe — horizontal drag-to-step for a stage that is not a native
|
|
5
|
+
* scroller (a coverflow ring, a paged track). Pointer events, so one code path
|
|
6
|
+
* covers touch, pen and mouse.
|
|
7
|
+
*
|
|
8
|
+
* Returns the live drag distance (`dragX`, for the stage to follow the finger
|
|
9
|
+
* a little while dragging) and the handlers to spread onto the stage. On
|
|
10
|
+
* release past `threshold` it calls `onSwipe(-1 | 1)` — `1` for a leftward
|
|
11
|
+
* drag, i.e. "next". `didDrag` is set for the tick after a real drag so the
|
|
12
|
+
* stage's `onClickCapture` can swallow the click a drag would otherwise
|
|
13
|
+
* become on a card underneath.
|
|
14
|
+
*
|
|
15
|
+
* The stage keeps vertical scrolling: pair these with `touch-action: pan-y`
|
|
16
|
+
* so the browser owns vertical gestures and hands only horizontal ones here.
|
|
17
|
+
* Pointer capture is taken on move, not on down, so a tap still reaches the
|
|
18
|
+
* card underneath as a plain click.
|
|
19
|
+
*
|
|
20
|
+
* `onDragStart` is part of the handlers for a reason: the cards under a stage
|
|
21
|
+
* are usually links wrapping images, and a mouse drag on either starts a
|
|
22
|
+
* NATIVE HTML5 drag, which cancels the pointer stream mid-gesture — the swipe
|
|
23
|
+
* silently never completes. Preventing it keeps the pointer events flowing.
|
|
24
|
+
*/
|
|
25
|
+
type DragSwipeOptions = {
|
|
26
|
+
/** Distance in px that counts as a swipe. Default 40. */
|
|
27
|
+
threshold?: number;
|
|
28
|
+
onSwipe: (direction: -1 | 1) => void;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
};
|
|
31
|
+
declare function useDragSwipe({ threshold, onSwipe, disabled }: DragSwipeOptions): {
|
|
32
|
+
dragX: number;
|
|
33
|
+
isDragging: boolean;
|
|
34
|
+
handlers: {
|
|
35
|
+
onPointerDown: (event: PointerEvent<HTMLElement>) => void;
|
|
36
|
+
onPointerMove: (event: PointerEvent<HTMLElement>) => void;
|
|
37
|
+
onPointerUp: (event: PointerEvent<HTMLElement>) => void;
|
|
38
|
+
onPointerCancel: (event: PointerEvent<HTMLElement>) => void;
|
|
39
|
+
onClickCapture: (event: PointerEvent<HTMLElement> | React.MouseEvent) => void;
|
|
40
|
+
onDragStart: (event: React.DragEvent<HTMLElement>) => void;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export { type DragSwipeOptions, useDragSwipe };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useMediaQuery — a boolean for a CSS media query that is hydration-safe.
|
|
3
|
+
*
|
|
4
|
+
* `useSyncExternalStore` with a fixed server snapshot (`false`) means the
|
|
5
|
+
* server and the hydrating client render the same tree; the real value
|
|
6
|
+
* arrives on the next render. Components that switch behaviour per breakpoint
|
|
7
|
+
* (not just classes) need this rather than `window.matchMedia` in an effect,
|
|
8
|
+
* which flashes the wrong variant for a frame.
|
|
9
|
+
*
|
|
10
|
+
* Mobile-first: `false` on the server and until measured, so the phone
|
|
11
|
+
* variant is the one that prerenders.
|
|
12
|
+
*/
|
|
13
|
+
declare function useMediaQuery(query: string): boolean;
|
|
14
|
+
/** The design system's desktop breakpoint, Tailwind's `lg`. */
|
|
15
|
+
declare const DESKTOP_MEDIA_QUERY = "(min-width: 1024px)";
|
|
16
|
+
|
|
17
|
+
export { DESKTOP_MEDIA_QUERY, useMediaQuery };
|
package/package.json
CHANGED
|
@@ -31,7 +31,11 @@ const DECK_POSTER_COUNT = 4;
|
|
|
31
31
|
const DECK_TRANSITION =
|
|
32
32
|
"transition-transform duration-[400ms] ease-[cubic-bezier(0.2,0.8,0.2,1)] motion-reduce:transition-none";
|
|
33
33
|
|
|
34
|
-
const DECK_CARD_TRANSFORMS: ReadonlyArray<{
|
|
34
|
+
const DECK_CARD_TRANSFORMS: ReadonlyArray<{
|
|
35
|
+
rest: string;
|
|
36
|
+
spread: string;
|
|
37
|
+
z: string;
|
|
38
|
+
}> = [
|
|
35
39
|
{
|
|
36
40
|
rest: "[transform:translateX(-50%)_rotate(-9deg)_translateX(-26px)]",
|
|
37
41
|
spread:
|
|
@@ -115,13 +119,12 @@ export function CategoryDeck({
|
|
|
115
119
|
}: CategoryDeckProps) {
|
|
116
120
|
const slots = Array.from(
|
|
117
121
|
{ length: DECK_POSTER_COUNT },
|
|
118
|
-
(_, index): CategoryDeckPoster => posters[index] ?? {}
|
|
122
|
+
(_, index): CategoryDeckPoster => posters[index] ?? {},
|
|
119
123
|
);
|
|
120
124
|
|
|
121
125
|
const body = (
|
|
122
126
|
<>
|
|
123
127
|
<div className="relative h-[150px] sm:h-[186px]" aria-hidden>
|
|
124
|
-
|
|
125
128
|
{slots.map((poster, index) => {
|
|
126
129
|
const transform = DECK_CARD_TRANSFORMS[index];
|
|
127
130
|
return (
|
|
@@ -129,12 +132,15 @@ export function CategoryDeck({
|
|
|
129
132
|
key={index}
|
|
130
133
|
className={cn(
|
|
131
134
|
"absolute left-1/2 top-0 aspect-[4/5] w-24 overflow-hidden rounded-[10px] bg-muted",
|
|
135
|
+
// Keyed to --foreground so the shadow is ink in light mode; in
|
|
136
|
+
// dark mode that same 18% is a light halo, so it drops to 7%.
|
|
132
137
|
"shadow-[0_10px_26px_color-mix(in_oklch,var(--foreground)_18%,transparent)]",
|
|
138
|
+
"dark:shadow-[0_10px_26px_color-mix(in_oklch,var(--foreground)_7%,transparent)]",
|
|
133
139
|
"sm:w-[120px]",
|
|
134
140
|
transform.rest,
|
|
135
141
|
transform.spread,
|
|
136
142
|
transform.z,
|
|
137
|
-
DECK_TRANSITION
|
|
143
|
+
DECK_TRANSITION,
|
|
138
144
|
)}
|
|
139
145
|
>
|
|
140
146
|
{poster.content ? (
|
|
@@ -161,9 +167,14 @@ export function CategoryDeck({
|
|
|
161
167
|
<div className="truncate text-[14.5px] font-extrabold tracking-[-0.015em] sm:text-base">
|
|
162
168
|
{name}
|
|
163
169
|
</div>
|
|
164
|
-
<div className="truncate font-mono text-[11.5px] text-muted-foreground">
|
|
170
|
+
<div className="truncate font-mono text-[11.5px] text-muted-foreground">
|
|
171
|
+
{meta}
|
|
172
|
+
</div>
|
|
165
173
|
</div>
|
|
166
|
-
<ChevronRight
|
|
174
|
+
<ChevronRight
|
|
175
|
+
className="ml-auto h-4 w-4 shrink-0 text-muted-foreground"
|
|
176
|
+
aria-hidden
|
|
177
|
+
/>
|
|
167
178
|
</div>
|
|
168
179
|
</>
|
|
169
180
|
);
|
|
@@ -172,7 +183,7 @@ export function CategoryDeck({
|
|
|
172
183
|
"group flex min-w-0 flex-col text-left",
|
|
173
184
|
(href || onClick) &&
|
|
174
185
|
"cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl",
|
|
175
|
-
className
|
|
186
|
+
className,
|
|
176
187
|
);
|
|
177
188
|
|
|
178
189
|
if (href) {
|
|
@@ -209,7 +220,7 @@ export function CategoryDeckSkeleton({ className }: CategoryDeckSkeletonProps) {
|
|
|
209
220
|
className={cn(
|
|
210
221
|
"absolute left-1/2 top-0 aspect-[4/5] w-24 rounded-[10px] sm:w-[120px]",
|
|
211
222
|
transform.rest,
|
|
212
|
-
transform.z
|
|
223
|
+
transform.z,
|
|
213
224
|
)}
|
|
214
225
|
/>
|
|
215
226
|
))}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Foundations / Colour dictionary — every colour a menu surface paints, in one
|
|
5
|
+
* table grouped by the forest element it is, with the reason beside it. A page,
|
|
6
|
+
* not a component: the values live in the consuming app (see the notes under
|
|
7
|
+
* the table), this is where a person goes to understand them.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type Chip = { label: string; value: string };
|
|
11
|
+
|
|
12
|
+
type Row = {
|
|
13
|
+
element: string;
|
|
14
|
+
why: string;
|
|
15
|
+
/** The three `--menu-field-<family>-<n>` tints, when this element is a card-field family. */
|
|
16
|
+
field?: { family: string; tints: [string, string, string]; draws: string };
|
|
17
|
+
/** `FAMILY_HUES` entries painted by the hub's family cards (chips, price band, photo tint). */
|
|
18
|
+
familyCards: Chip[];
|
|
19
|
+
/** `INTENT_GRADIENTS` entries painted by the hub's intent tiles. */
|
|
20
|
+
intents: Chip[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const ROWS: Row[] = [
|
|
24
|
+
{
|
|
25
|
+
element: "Wheat and honey",
|
|
26
|
+
why: "Grain and what goes on it — crust, pasta, pizza base, pastry, yolk — is this gold before it is cooked. Honey in the comb is the same colour, and so is morning light on toast, which is why brunch and beer (roasted malt) sit here too.",
|
|
27
|
+
field: { family: "larder", tints: ["oklch(0.505 0.11 74)", "oklch(0.555 0.105 80)", "oklch(0.465 0.115 68)"], draws: "bread, pasta, pizza, eggs, sides, sauces, spirits" },
|
|
28
|
+
familyCards: [{ label: "Breakfast & brunch", value: "oklch(0.62 0.14 82)" }, { label: "Bakeries", value: "oklch(0.58 0.11 72)" }, { label: "Beer", value: "oklch(0.62 0.13 86)" }],
|
|
29
|
+
intents: [{ label: "Brunch", value: "oklch(0.62 0.14 82)" }],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
element: "Lake water",
|
|
33
|
+
why: "Fish, shellfish and soup come out of water, and a cocktail is a glass of it. A forest has a lake, not a sea: dark, blue-green, in shade — so the hue stays on the green side (198–220) and never reads as sky or as a pool.",
|
|
34
|
+
field: { family: "sea", tints: ["oklch(0.485 0.095 205)", "oklch(0.535 0.09 198)", "oklch(0.445 0.095 212)"], draws: "fish, shellfish, soups, cocktails" },
|
|
35
|
+
familyCards: [{ label: "Cocktails", value: "oklch(0.47 0.10 205)" }],
|
|
36
|
+
intents: [{ label: "Cocktail bars", value: "oklch(0.47 0.10 205)" }],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
element: "Ember and terracotta",
|
|
40
|
+
why: "Meat is the family cooked over fire: the red of the coals, the crust on beef, cured and smoked meat. Red wine shares the glass and the colour. Terracotta is the same fire one step warmer — the oven's brick and the tomato — which is why Italian sits beside Romanian here.",
|
|
41
|
+
field: { family: "meat", tints: ["oklch(0.49 0.13 28)", "oklch(0.54 0.13 34)", "oklch(0.45 0.13 22)"], draws: "beef, game, cured meat, burgers, red wine" },
|
|
42
|
+
familyCards: [{ label: "Romanian", value: "oklch(0.48 0.16 25)" }, { label: "Italian", value: "oklch(0.56 0.15 38)" }],
|
|
43
|
+
intents: [{ label: "Burgers", value: "oklch(0.48 0.16 25)" }, { label: "Italian tonight", value: "oklch(0.56 0.15 38)" }],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
element: "Maple and amber",
|
|
47
|
+
why: "Sugar turns this colour when it meets heat: caramel, syrup, the top of a crème brûlée, the maple leaf in autumn — and a pizza crust out of the oven, which is why the Pizza card wears it. Beer's amber is the same chemistry, malt roasted, so beer items draw this field even though the beer card wears honey.",
|
|
48
|
+
field: { family: "sweet", tints: ["oklch(0.505 0.14 50)", "oklch(0.555 0.135 58)", "oklch(0.465 0.14 44)"], draws: "desserts, pastry, beer" },
|
|
49
|
+
familyCards: [{ label: "Pizza", value: "oklch(0.52 0.14 58)" }],
|
|
50
|
+
intents: [],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
element: "Fern and pine",
|
|
54
|
+
why: "The leaf itself. Salads, vegetables and the non-alcoholic glass — juice, lemonade, the herb in it — are the green things on the menu and take the forest's own green: pine-deep rather than lime, so it stays a forest and not a lawn.",
|
|
55
|
+
field: { family: "verdant", tints: ["oklch(0.505 0.115 148)", "oklch(0.555 0.11 138)", "oklch(0.465 0.115 156)"], draws: "salads, vegetables, non-alcoholic" },
|
|
56
|
+
familyCards: [{ label: "Vegetarian", value: "oklch(0.52 0.14 148)" }, { label: "Vegan", value: "oklch(0.55 0.12 135)" }],
|
|
57
|
+
intents: [{ label: "Vegan", value: "oklch(0.52 0.14 148)" }],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
element: "Bark and walnut",
|
|
61
|
+
why: "The roasted bean is the colour of tree bark, and a coffee house is made of both — wood and roast. Walnut is the same brown darker: a lit doorway at night, which is what \"open right now\" looks like from the street. Coffee items themselves draw the non-alcoholic field (pine), not this.",
|
|
62
|
+
familyCards: [{ label: "Coffee", value: "oklch(0.44 0.10 52)" }],
|
|
63
|
+
intents: [{ label: "Coffee", value: "oklch(0.44 0.10 52)" }, { label: "Open right now", value: "oklch(0.44 0.08 62)" }],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
element: "Rosehip",
|
|
67
|
+
why: "The forest's berry: the jam in the tart, the fruit on the cake, the glass of red. A red with no purple in it — the magenta and violet it replaced belong to no place a bear has been.",
|
|
68
|
+
familyCards: [{ label: "Wine by the glass", value: "oklch(0.48 0.14 12)" }],
|
|
69
|
+
intents: [{ label: "Patisseries", value: "oklch(0.52 0.15 12)" }],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
element: "Moss in sun",
|
|
73
|
+
why: "The sunlit forest floor. Yellow-green because a terrace is the one intent about being outdoors in daylight; deeper than lime so it is ground, not a highlighter.",
|
|
74
|
+
familyCards: [],
|
|
75
|
+
intents: [{ label: "On a terrace", value: "oklch(0.60 0.13 122)" }],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
element: "Stone",
|
|
79
|
+
why: "No colour of its own. An untagged dish gets it because there is no ingredient to be coloured by; Centru gets it because the town is the one intent that is a place and not a taste. Never a section mark — as a mark, grey reads as absent.",
|
|
80
|
+
field: { family: "neutral", tints: ["oklch(0.505 0.02 60)", "oklch(0.555 0.022 66)", "oklch(0.465 0.018 54)"], draws: "an untagged dish only" },
|
|
81
|
+
familyCards: [],
|
|
82
|
+
intents: [{ label: "In Centru", value: "oklch(0.50 0.03 70)" }],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const NOT_THESE: Chip[] = [
|
|
87
|
+
{ label: "sky blue", value: "oklch(0.5 0.13 240)" },
|
|
88
|
+
{ label: "teal", value: "oklch(0.55 0.12 190)" },
|
|
89
|
+
{ label: "violet", value: "oklch(0.5 0.11 300)" },
|
|
90
|
+
{ label: "magenta", value: "oklch(0.52 0.13 340)" },
|
|
91
|
+
{ label: "olive-yellow", value: "oklch(0.58 0.12 95)" },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const darker = (value: string) =>
|
|
95
|
+
value.replace(/oklch\(([\d.]+) /, (_m, l: string) => `oklch(${(Number(l) - 0.09).toFixed(3)} `);
|
|
96
|
+
|
|
97
|
+
function Swatch({ label, value, angle = 135, tall }: Chip & { angle?: number; tall?: boolean }) {
|
|
98
|
+
return (
|
|
99
|
+
<div className="flex w-[104px] shrink-0 flex-col gap-1">
|
|
100
|
+
<span
|
|
101
|
+
aria-hidden
|
|
102
|
+
className={tall ? "block h-14 rounded-lg" : "block h-9 rounded-lg"}
|
|
103
|
+
style={{ backgroundImage: `linear-gradient(${angle}deg, ${value}, ${darker(value)})` }}
|
|
104
|
+
/>
|
|
105
|
+
<span className="text-[11px] font-semibold leading-tight">{label}</span>
|
|
106
|
+
<code className="text-[9.5px] leading-tight text-muted-foreground">{value.replace("oklch(", "").replace(")", "")}</code>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const TH = "px-3 py-2 text-left text-[11px] font-bold uppercase tracking-[0.08em] text-muted-foreground";
|
|
112
|
+
const TD = "px-3 py-3 align-top";
|
|
113
|
+
|
|
114
|
+
function ColourDictionaryPage() {
|
|
115
|
+
return (
|
|
116
|
+
<article className="mx-auto max-w-[1440px] px-6 py-7 text-foreground">
|
|
117
|
+
<p className="text-[11px] font-bold uppercase tracking-[0.09em] text-muted-foreground">Foundations</p>
|
|
118
|
+
<h1 className="mt-0.5 text-[26px] font-extrabold tracking-[-0.02em]">The menu colour dictionary</h1>
|
|
119
|
+
<p className="mt-1 max-w-[90ch] text-[14px] leading-[1.55] text-foreground/85">
|
|
120
|
+
Every hue is a forest object, at full strength — the floor and canopy of a forest and what a bear eats off it.
|
|
121
|
+
Every colour a menu surface paints is in this table, grouped by the element it is, with why it belongs.
|
|
122
|
+
<span className="ml-2 inline-flex items-center gap-2 align-middle text-[12px] text-muted-foreground">
|
|
123
|
+
Not in the forest, so not in the palette:
|
|
124
|
+
{NOT_THESE.map((c) => (
|
|
125
|
+
<span key={c.label} className="inline-flex items-center gap-1 line-through">
|
|
126
|
+
<span aria-hidden className="inline-block h-3.5 w-5 rounded-sm" style={{ background: c.value }} />
|
|
127
|
+
{c.label}
|
|
128
|
+
</span>
|
|
129
|
+
))}
|
|
130
|
+
</span>
|
|
131
|
+
</p>
|
|
132
|
+
|
|
133
|
+
<div className="mt-5 overflow-x-auto rounded-xl border border-border">
|
|
134
|
+
<table className="w-full min-w-[1180px] border-collapse text-[13px]">
|
|
135
|
+
<thead className="bg-muted/60">
|
|
136
|
+
<tr>
|
|
137
|
+
<th className={`${TH} w-[290px]`}>Element · why it belongs</th>
|
|
138
|
+
<th className={TH}>Card fields · the three tints</th>
|
|
139
|
+
<th className={TH}>Family cards · chips, price band, photo tint</th>
|
|
140
|
+
<th className={TH}>Intent tiles</th>
|
|
141
|
+
</tr>
|
|
142
|
+
</thead>
|
|
143
|
+
<tbody>
|
|
144
|
+
{ROWS.map((row) => (
|
|
145
|
+
<tr key={row.element} className="border-t border-border">
|
|
146
|
+
<td className={TD}>
|
|
147
|
+
<div className="text-[15px] font-extrabold tracking-[-0.01em]">{row.element}</div>
|
|
148
|
+
<p className="mt-1 text-[12.5px] leading-[1.5] text-foreground/85">{row.why}</p>
|
|
149
|
+
</td>
|
|
150
|
+
<td className={TD}>
|
|
151
|
+
{row.field ? (
|
|
152
|
+
<div className="flex flex-col gap-2">
|
|
153
|
+
<div className="flex gap-2">
|
|
154
|
+
{row.field.tints.map((tint, index) => (
|
|
155
|
+
<Swatch key={tint} label={`${row.field!.family}-${index}`} value={tint} tall />
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
<div className="text-[11.5px] text-muted-foreground">
|
|
159
|
+
<span className="font-semibold text-foreground/80">draws</span> {row.field.draws}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
) : (
|
|
163
|
+
<span className="text-[12px] text-muted-foreground">—</span>
|
|
164
|
+
)}
|
|
165
|
+
</td>
|
|
166
|
+
<td className={TD}>
|
|
167
|
+
{row.familyCards.length ? (
|
|
168
|
+
<div className="flex gap-2">
|
|
169
|
+
{row.familyCards.map((c) => (
|
|
170
|
+
<Swatch key={c.label} {...c} angle={150} />
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
) : (
|
|
174
|
+
<span className="text-[12px] text-muted-foreground">—</span>
|
|
175
|
+
)}
|
|
176
|
+
</td>
|
|
177
|
+
<td className={TD}>
|
|
178
|
+
{row.intents.length ? (
|
|
179
|
+
<div className="flex gap-2">
|
|
180
|
+
{row.intents.map((c) => (
|
|
181
|
+
<Swatch key={c.label} {...c} angle={150} />
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
) : (
|
|
185
|
+
<span className="text-[12px] text-muted-foreground">—</span>
|
|
186
|
+
)}
|
|
187
|
+
</td>
|
|
188
|
+
</tr>
|
|
189
|
+
))}
|
|
190
|
+
</tbody>
|
|
191
|
+
</table>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="mt-8 grid gap-8 md:grid-cols-3">
|
|
195
|
+
<section>
|
|
196
|
+
<h2 className="text-[15px] font-extrabold">How a colour is chosen</h2>
|
|
197
|
+
<ol className="mt-2 list-decimal space-y-1.5 pl-4 text-[13px] leading-[1.55] text-foreground/85">
|
|
198
|
+
<li><b>What is the family made of, or where is it consumed?</b> Grain, fire, water, sugar, leaf; a night out, a morning, the town.</li>
|
|
199
|
+
<li><b>Which forest element already has that colour — literally?</b> Wheat is gold, embers are red, lake water is blue-green. If nothing in a forest has it, it does not go in; that is how violet and magenta left.</li>
|
|
200
|
+
<li><b>Take it at full strength.</b> An early pass pulled every chroma down to feel earthy and read as faded. Keep lightness 0.44–0.62 so white type at 0.26–0.30 alpha stays readable.</li>
|
|
201
|
+
</ol>
|
|
202
|
+
</section>
|
|
203
|
+
<section>
|
|
204
|
+
<h2 className="text-[15px] font-extrabold">The colour names a family, never an item</h2>
|
|
205
|
+
<p className="mt-2 text-[13px] leading-[1.55] text-foreground/85">
|
|
206
|
+
Sixteen appetizers draw one colour. Two dishes in one family differ only by one of the three tints, keyed on the parent concept so the same dish is the same tint on every menu and after every re-sort. A category's section mark always uses tint <code>-0</code>. Where a family has both a card and an intent tile, they share the value so the two doors to one page agree.
|
|
207
|
+
</p>
|
|
208
|
+
</section>
|
|
209
|
+
<section>
|
|
210
|
+
<h2 className="text-[15px] font-extrabold">Where the values live</h2>
|
|
211
|
+
<p className="mt-2 text-[13px] leading-[1.55] text-foreground/85">
|
|
212
|
+
The design system owns the typography, the falloff and the layering (<code>MenuField</code>, <code>MenuFamilyCard</code>, <code>MenuIntentTiles</code>) and takes a finished colour as a prop. The palette lives in the fe app: the eighteen <code>--menu-field-*</code> tokens in <code>app/globals.css</code>, <code>FAMILY_HUES</code> and <code>INTENT_GRADIENTS</code> in <code>hub-gradients.ts</code>, category → family in <code>category-colors.ts</code>. The Storybook mocks carry the same values so post-generator films what the app paints.
|
|
213
|
+
</p>
|
|
214
|
+
</section>
|
|
215
|
+
</div>
|
|
216
|
+
</article>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const meta: Meta<typeof ColourDictionaryPage> = {
|
|
221
|
+
title: "Foundations/Colour dictionary",
|
|
222
|
+
component: ColourDictionaryPage,
|
|
223
|
+
parameters: { layout: "fullscreen" },
|
|
224
|
+
};
|
|
225
|
+
export default meta;
|
|
226
|
+
|
|
227
|
+
export const Page: StoryObj<typeof ColourDictionaryPage> = {};
|