@timmbr/ui 1.8.0 → 1.10.0
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/components/Breadcrumb/Breadcrumb.d.ts +9 -0
- package/dist/components/Breadcrumb/Breadcrumb.d.ts.map +1 -0
- package/dist/components/Breadcrumb/Breadcrumb.js +32 -0
- package/dist/components/Breadcrumb/Breadcrumb.styles.d.ts +8 -0
- package/dist/components/Breadcrumb/Breadcrumb.styles.d.ts.map +1 -0
- package/dist/components/Breadcrumb/Breadcrumb.styles.js +20 -0
- package/dist/components/Breadcrumb/Breadcrumb.types.d.ts +27 -0
- package/dist/components/Breadcrumb/Breadcrumb.types.d.ts.map +1 -0
- package/dist/components/Breadcrumb/index.d.ts +4 -0
- package/dist/components/Breadcrumb/index.d.ts.map +1 -0
- package/dist/components/Chip/Chip.d.ts.map +1 -1
- package/dist/components/Chip/Chip.js +13 -3
- package/dist/components/Chip/Chip.types.d.ts +5 -0
- package/dist/components/Chip/Chip.types.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.js +4 -4
- package/dist/components/Drawer/Drawer.styles.d.ts +1 -0
- package/dist/components/Drawer/Drawer.styles.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.styles.js +7 -1
- package/dist/components/Drawer/Drawer.types.d.ts +2 -0
- package/dist/components/Drawer/Drawer.types.d.ts.map +1 -1
- package/dist/components/FilterChips/FilterChips.d.ts +8 -0
- package/dist/components/FilterChips/FilterChips.d.ts.map +1 -0
- package/dist/components/FilterChips/FilterChips.js +39 -0
- package/dist/components/FilterChips/FilterChips.types.d.ts +17 -0
- package/dist/components/FilterChips/FilterChips.types.d.ts.map +1 -0
- package/dist/components/FilterChips/index.d.ts +3 -0
- package/dist/components/FilterChips/index.d.ts.map +1 -0
- package/dist/components/FilterDrawer/FilterDrawer.d.ts +12 -0
- package/dist/components/FilterDrawer/FilterDrawer.d.ts.map +1 -0
- package/dist/components/FilterDrawer/FilterDrawer.js +157 -0
- package/dist/components/FilterDrawer/FilterDrawer.styles.d.ts +12 -0
- package/dist/components/FilterDrawer/FilterDrawer.styles.d.ts.map +1 -0
- package/dist/components/FilterDrawer/FilterDrawer.styles.js +30 -0
- package/dist/components/FilterDrawer/FilterDrawer.types.d.ts +28 -0
- package/dist/components/FilterDrawer/FilterDrawer.types.d.ts.map +1 -0
- package/dist/components/FilterDrawer/index.d.ts +4 -0
- package/dist/components/FilterDrawer/index.d.ts.map +1 -0
- package/dist/components/FilterPanel/FilterPanel.d.ts +33 -0
- package/dist/components/FilterPanel/FilterPanel.d.ts.map +1 -0
- package/dist/components/FilterPanel/FilterPanel.js +239 -0
- package/dist/components/FilterPanel/FilterPanel.styles.d.ts +16 -0
- package/dist/components/FilterPanel/FilterPanel.styles.d.ts.map +1 -0
- package/dist/components/FilterPanel/FilterPanel.styles.js +31 -0
- package/dist/components/FilterPanel/FilterPanel.types.d.ts +80 -0
- package/dist/components/FilterPanel/FilterPanel.types.d.ts.map +1 -0
- package/dist/components/FilterPanel/FilterPanel.types.js +3 -0
- package/dist/components/FilterPanel/index.d.ts +4 -0
- package/dist/components/FilterPanel/index.d.ts.map +1 -0
- package/dist/components/ImageUpload/ImageUpload.styles.d.ts +3 -3
- package/dist/components/MediaCard/MediaCard.d.ts.map +1 -1
- package/dist/components/MediaCard/MediaCard.js +3 -1
- package/dist/components/NavHeader/NavHeader.styles.d.ts +7 -0
- package/dist/components/NavHeader/NavHeader.styles.d.ts.map +1 -1
- package/dist/components/NavHeader/NavHeader.styles.js +20 -2
- package/dist/components/NavHeader/NavHeader.types.d.ts +49 -1
- package/dist/components/NavHeader/NavHeader.types.d.ts.map +1 -1
- package/dist/components/NavHeader/index.d.ts +3 -2
- package/dist/components/NavHeader/index.d.ts.map +1 -1
- package/dist/components/NavHeader/subcomponents/NavMobileSearchDrawer.d.ts.map +1 -1
- package/dist/components/NavHeader/subcomponents/NavMobileSearchDrawer.js +15 -33
- package/dist/components/NavHeader/subcomponents/NavSearch.d.ts.map +1 -1
- package/dist/components/NavHeader/subcomponents/NavSearch.js +37 -83
- package/dist/components/NavHeader/subcomponents/NavSearchPanel.d.ts +63 -0
- package/dist/components/NavHeader/subcomponents/NavSearchPanel.d.ts.map +1 -0
- package/dist/components/NavHeader/subcomponents/NavSearchPanel.js +289 -0
- package/dist/components/NavMenuColumns/NavMenuColumns.d.ts +8 -1
- package/dist/components/NavMenuColumns/NavMenuColumns.d.ts.map +1 -1
- package/dist/components/NavMenuColumns/NavMenuColumns.js +27 -1
- package/dist/components/NavMenuColumns/NavMenuColumns.styles.js +1 -1
- package/dist/components/NavMenuColumns/NavMenuColumns.types.d.ts +18 -0
- package/dist/components/NavMenuColumns/NavMenuColumns.types.d.ts.map +1 -1
- package/dist/components/OptionPicker/OptionPicker.d.ts +10 -0
- package/dist/components/OptionPicker/OptionPicker.d.ts.map +1 -0
- package/dist/components/OptionPicker/OptionPicker.js +116 -0
- package/dist/components/OptionPicker/OptionPicker.styles.d.ts +20 -0
- package/dist/components/OptionPicker/OptionPicker.styles.d.ts.map +1 -0
- package/dist/components/OptionPicker/OptionPicker.styles.js +60 -0
- package/dist/components/OptionPicker/OptionPicker.types.d.ts +39 -0
- package/dist/components/OptionPicker/OptionPicker.types.d.ts.map +1 -0
- package/dist/components/OptionPicker/index.d.ts +4 -0
- package/dist/components/OptionPicker/index.d.ts.map +1 -0
- package/dist/components/Price/Price.styles.d.ts.map +1 -1
- package/dist/components/Price/Price.styles.js +3 -2
- package/dist/components/ProductGallery/ProductGallery.d.ts +10 -0
- package/dist/components/ProductGallery/ProductGallery.d.ts.map +1 -0
- package/dist/components/ProductGallery/ProductGallery.js +306 -0
- package/dist/components/ProductGallery/ProductGallery.styles.d.ts +32 -0
- package/dist/components/ProductGallery/ProductGallery.styles.d.ts.map +1 -0
- package/dist/components/ProductGallery/ProductGallery.styles.js +86 -0
- package/dist/components/ProductGallery/ProductGallery.types.d.ts +55 -0
- package/dist/components/ProductGallery/ProductGallery.types.d.ts.map +1 -0
- package/dist/components/ProductGallery/ProductGalleryLightbox.d.ts +20 -0
- package/dist/components/ProductGallery/ProductGalleryLightbox.d.ts.map +1 -0
- package/dist/components/ProductGallery/ProductGalleryLightbox.js +210 -0
- package/dist/components/ProductGallery/index.d.ts +4 -0
- package/dist/components/ProductGallery/index.d.ts.map +1 -0
- package/dist/components/QuantityStepper/QuantityStepper.d.ts +8 -0
- package/dist/components/QuantityStepper/QuantityStepper.d.ts.map +1 -0
- package/dist/components/QuantityStepper/QuantityStepper.js +65 -0
- package/dist/components/QuantityStepper/QuantityStepper.styles.d.ts +8 -0
- package/dist/components/QuantityStepper/QuantityStepper.styles.d.ts.map +1 -0
- package/dist/components/QuantityStepper/QuantityStepper.styles.js +24 -0
- package/dist/components/QuantityStepper/QuantityStepper.types.d.ts +20 -0
- package/dist/components/QuantityStepper/QuantityStepper.types.d.ts.map +1 -0
- package/dist/components/QuantityStepper/index.d.ts +4 -0
- package/dist/components/QuantityStepper/index.d.ts.map +1 -0
- package/dist/components/RangeSlider/RangeSlider.d.ts.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.js +5 -3
- package/dist/components/RangeSlider/RangeSlider.types.d.ts +7 -0
- package/dist/components/RangeSlider/RangeSlider.types.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.js +14 -1
- package/dist/components/Select/Select.types.d.ts +4 -0
- package/dist/components/Select/Select.types.d.ts.map +1 -1
- package/dist/components/Snackbar/Snackbar.d.ts +9 -0
- package/dist/components/Snackbar/Snackbar.d.ts.map +1 -0
- package/dist/components/Snackbar/Snackbar.js +108 -0
- package/dist/components/Snackbar/Snackbar.styles.d.ts +17 -0
- package/dist/components/Snackbar/Snackbar.styles.d.ts.map +1 -0
- package/dist/components/Snackbar/Snackbar.styles.js +37 -0
- package/dist/components/Snackbar/Snackbar.types.d.ts +36 -0
- package/dist/components/Snackbar/Snackbar.types.d.ts.map +1 -0
- package/dist/components/Snackbar/SnackbarHost.d.ts +4 -0
- package/dist/components/Snackbar/SnackbarHost.d.ts.map +1 -0
- package/dist/components/Snackbar/SnackbarHost.js +24 -0
- package/dist/components/Snackbar/index.d.ts +6 -0
- package/dist/components/Snackbar/index.d.ts.map +1 -0
- package/dist/components/Snackbar/snackbarStore.d.ts +42 -0
- package/dist/components/Snackbar/snackbarStore.d.ts.map +1 -0
- package/dist/components/Snackbar/snackbarStore.js +87 -0
- package/dist/components/StockMeter/StockMeter.d.ts +8 -0
- package/dist/components/StockMeter/StockMeter.d.ts.map +1 -0
- package/dist/components/StockMeter/StockMeter.js +19 -0
- package/dist/components/StockMeter/StockMeter.styles.d.ts +4 -0
- package/dist/components/StockMeter/StockMeter.styles.d.ts.map +1 -0
- package/dist/components/StockMeter/StockMeter.styles.js +7 -0
- package/dist/components/StockMeter/StockMeter.types.d.ts +10 -0
- package/dist/components/StockMeter/StockMeter.types.d.ts.map +1 -0
- package/dist/components/StockMeter/index.d.ts +4 -0
- package/dist/components/StockMeter/index.d.ts.map +1 -0
- package/dist/components/Swatch/Swatch.d.ts +11 -0
- package/dist/components/Swatch/Swatch.d.ts.map +1 -0
- package/dist/components/Swatch/Swatch.js +40 -0
- package/dist/components/Swatch/Swatch.styles.d.ts +8 -0
- package/dist/components/Swatch/Swatch.styles.d.ts.map +1 -0
- package/dist/components/Swatch/Swatch.styles.js +26 -0
- package/dist/components/Swatch/Swatch.types.d.ts +11 -0
- package/dist/components/Swatch/Swatch.types.d.ts.map +1 -0
- package/dist/components/Swatch/index.d.ts +4 -0
- package/dist/components/Swatch/index.d.ts.map +1 -0
- package/dist/components/Toast/Toast.d.ts.map +1 -1
- package/dist/components/Toast/Toast.js +49 -42
- package/dist/components/Toast/Toast.types.d.ts +7 -0
- package/dist/components/Toast/Toast.types.d.ts.map +1 -1
- package/dist/components/Toast/ToastContainer.d.ts.map +1 -1
- package/dist/components/Toast/ToastContainer.js +5 -0
- package/dist/components/Toast/toastStore.d.ts +11 -0
- package/dist/components/Toast/toastStore.d.ts.map +1 -1
- package/dist/components/Toast/toastStore.js +70 -30
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -2
- package/dist/providers/TimmbrConfigProvider.d.ts.map +1 -1
- package/dist/providers/TimmbrConfigProvider.js +5 -9
- package/package.json +6 -6
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { History, X, TrendingUp, Search, ArrowRight } from '@timmbr/icons';
|
|
5
|
+
import { cn } from '@timmbr/utils';
|
|
6
|
+
import { resolveNavDestination, isCrossZoneNavigation } from '../NavHeader.helpers.js';
|
|
7
|
+
import { searchPanelSectionTitleVariants, searchPanelTextButtonVariants, searchPanelOptionVariants } from '../NavHeader.styles.js';
|
|
8
|
+
import { useTimmbrConfig } from '../../../providers/TimmbrConfigProvider.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MIN_QUERY_LENGTH = 2;
|
|
11
|
+
function useNavSearchCombobox({
|
|
12
|
+
config,
|
|
13
|
+
value,
|
|
14
|
+
onChange,
|
|
15
|
+
onSubmit,
|
|
16
|
+
onClose
|
|
17
|
+
}) {
|
|
18
|
+
const searchConfig = typeof config === "object" ? config : {};
|
|
19
|
+
const { recentSearches = [], results, popularSearches = [], onRecentSearchRemove } = searchConfig;
|
|
20
|
+
const listId = React.useId();
|
|
21
|
+
const typed = value.trim();
|
|
22
|
+
const minLength = results?.minQueryLength ?? DEFAULT_MIN_QUERY_LENGTH;
|
|
23
|
+
const mode = typed.length < minLength ? "idle" : results ? "results" : "none";
|
|
24
|
+
const options = React.useMemo(() => {
|
|
25
|
+
if (mode === "idle") {
|
|
26
|
+
return recentSearches.map((query, i) => ({ kind: "recent", id: `${listId}-recent-${i}`, query }));
|
|
27
|
+
}
|
|
28
|
+
if (mode === "results" && results) {
|
|
29
|
+
return [
|
|
30
|
+
...results.items.map((result, i) => ({ kind: "result", id: `${listId}-result-${i}`, result })),
|
|
31
|
+
{ kind: "viewAll", id: `${listId}-all` }
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}, [mode, recentSearches, results, listId]);
|
|
36
|
+
const [active, setActive] = React.useState({ value, index: -1 });
|
|
37
|
+
const activeIndex = active.value === value && active.index < options.length ? active.index : -1;
|
|
38
|
+
const setActiveIndex = (index) => setActive({ value, index });
|
|
39
|
+
const activeOption = activeIndex >= 0 ? options[activeIndex] : void 0;
|
|
40
|
+
const hasContent = mode === "results" || mode === "idle" && (recentSearches.length > 0 || popularSearches.length > 0);
|
|
41
|
+
const pickRecent = (query) => {
|
|
42
|
+
onChange(query);
|
|
43
|
+
onSubmit(query);
|
|
44
|
+
onClose();
|
|
45
|
+
};
|
|
46
|
+
const submitTyped = () => {
|
|
47
|
+
if (!typed) return;
|
|
48
|
+
onSubmit(typed);
|
|
49
|
+
onClose();
|
|
50
|
+
};
|
|
51
|
+
const pickResult = (result) => {
|
|
52
|
+
searchConfig.onResultSelect?.(result);
|
|
53
|
+
onClose();
|
|
54
|
+
};
|
|
55
|
+
const onKeyDown = (event) => {
|
|
56
|
+
const count = options.length;
|
|
57
|
+
switch (event.key) {
|
|
58
|
+
case "ArrowDown":
|
|
59
|
+
if (!count) return;
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
setActiveIndex((activeIndex + 1) % count);
|
|
62
|
+
return;
|
|
63
|
+
case "ArrowUp":
|
|
64
|
+
if (!count) return;
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
setActiveIndex(activeIndex <= 0 ? count - 1 : activeIndex - 1);
|
|
67
|
+
return;
|
|
68
|
+
case "Enter":
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
if (!activeOption) return submitTyped();
|
|
71
|
+
if (activeOption.kind === "recent") return pickRecent(activeOption.query);
|
|
72
|
+
if (activeOption.kind === "viewAll") return submitTyped();
|
|
73
|
+
document.getElementById(activeOption.id)?.click();
|
|
74
|
+
return;
|
|
75
|
+
case "Delete":
|
|
76
|
+
if (activeOption?.kind === "recent" && onRecentSearchRemove) {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
onRecentSearchRemove(activeOption.query);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
hasContent,
|
|
85
|
+
inputProps: {
|
|
86
|
+
role: "combobox",
|
|
87
|
+
"aria-expanded": hasContent,
|
|
88
|
+
"aria-controls": listId,
|
|
89
|
+
"aria-autocomplete": "list",
|
|
90
|
+
"aria-activedescendant": activeOption?.id,
|
|
91
|
+
onKeyDown
|
|
92
|
+
},
|
|
93
|
+
panelProps: {
|
|
94
|
+
config,
|
|
95
|
+
listId,
|
|
96
|
+
mode,
|
|
97
|
+
query: typed,
|
|
98
|
+
options,
|
|
99
|
+
activeId: activeOption?.id,
|
|
100
|
+
onHover: (id) => setActiveIndex(options.findIndex((option) => option.id === id)),
|
|
101
|
+
onPickRecent: pickRecent,
|
|
102
|
+
onPickResult: pickResult,
|
|
103
|
+
onViewAll: submitTyped,
|
|
104
|
+
onPopularPick: (tag) => {
|
|
105
|
+
onChange(tag.label);
|
|
106
|
+
searchConfig.onPopularSearchClick?.(tag);
|
|
107
|
+
tag.onClick?.(tag);
|
|
108
|
+
onSubmit(tag.label);
|
|
109
|
+
onClose();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const NavSearchPanel = ({
|
|
115
|
+
config,
|
|
116
|
+
listId,
|
|
117
|
+
mode,
|
|
118
|
+
query,
|
|
119
|
+
options,
|
|
120
|
+
activeId,
|
|
121
|
+
onHover,
|
|
122
|
+
onPickRecent,
|
|
123
|
+
onPickResult,
|
|
124
|
+
onViewAll,
|
|
125
|
+
onPopularPick,
|
|
126
|
+
linkComponent: CustomLink,
|
|
127
|
+
className
|
|
128
|
+
}) => {
|
|
129
|
+
const globalConfig = useTimmbrConfig();
|
|
130
|
+
const searchConfig = typeof config === "object" ? config : {};
|
|
131
|
+
const {
|
|
132
|
+
recentSearchesTitle = "Recent searches",
|
|
133
|
+
clearRecentSearchesLabel = "Clear",
|
|
134
|
+
removeRecentSearchLabel = (q) => `Remove "${q}" from recent searches`,
|
|
135
|
+
onRecentSearchRemove,
|
|
136
|
+
onClearRecentSearches,
|
|
137
|
+
popularSearches = [],
|
|
138
|
+
popularSearchesTitle = "Popular Searches",
|
|
139
|
+
results
|
|
140
|
+
} = searchConfig;
|
|
141
|
+
const recentOptions = options.filter((o) => o.kind === "recent");
|
|
142
|
+
const resultOptions = options.filter((o) => o.kind === "result");
|
|
143
|
+
const viewAll = options.find((o) => o.kind === "viewAll");
|
|
144
|
+
const recentTitleId = `${listId}-recent-title`;
|
|
145
|
+
const resultsTitleId = `${listId}-results-title`;
|
|
146
|
+
const renderResultLink = (option) => {
|
|
147
|
+
const { result } = option;
|
|
148
|
+
const props = {
|
|
149
|
+
id: option.id,
|
|
150
|
+
href: result.href,
|
|
151
|
+
role: "option",
|
|
152
|
+
"aria-selected": activeId === option.id,
|
|
153
|
+
tabIndex: -1,
|
|
154
|
+
onMouseEnter: () => onHover(option.id),
|
|
155
|
+
onClick: () => onPickResult(result),
|
|
156
|
+
className: searchPanelOptionVariants({ active: activeId === option.id }),
|
|
157
|
+
"data-slot": "nav-search-result"
|
|
158
|
+
};
|
|
159
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
160
|
+
/* @__PURE__ */ jsx("span", { className: "size-12 shrink-0 overflow-hidden rounded-md bg-grey-100", children: result.imageUrl ? /* @__PURE__ */ jsx(
|
|
161
|
+
"img",
|
|
162
|
+
{
|
|
163
|
+
src: result.imageUrl,
|
|
164
|
+
alt: result.imageAlt ?? "",
|
|
165
|
+
width: 48,
|
|
166
|
+
height: 48,
|
|
167
|
+
loading: "lazy",
|
|
168
|
+
decoding: "async",
|
|
169
|
+
className: "size-full object-cover"
|
|
170
|
+
}
|
|
171
|
+
) : null }),
|
|
172
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
173
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-body-2 font-medium text-grey-950", children: result.title }),
|
|
174
|
+
result.subtitle ? /* @__PURE__ */ jsx("span", { className: "truncate text-body-3 text-grey-500", children: result.subtitle }) : null
|
|
175
|
+
] }),
|
|
176
|
+
result.price ? /* @__PURE__ */ jsx("span", { className: "shrink-0 text-body-2 font-semibold text-grey-950", children: result.price }) : null
|
|
177
|
+
] });
|
|
178
|
+
if (CustomLink && !result.crossZone) {
|
|
179
|
+
return /* @__PURE__ */ jsx(CustomLink, { ...props, children: content }, option.id);
|
|
180
|
+
}
|
|
181
|
+
return /* @__PURE__ */ jsx("a", { ...props, "data-cross-zone": result.crossZone ? "true" : void 0, children: content }, option.id);
|
|
182
|
+
};
|
|
183
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-4", className), children: [
|
|
184
|
+
mode === "idle" && recentOptions.length > 0 && /* @__PURE__ */ jsxs("div", { "data-slot": "nav-search-recent", children: [
|
|
185
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between gap-2", children: [
|
|
186
|
+
/* @__PURE__ */ jsx("span", { id: recentTitleId, className: searchPanelSectionTitleVariants(), children: recentSearchesTitle }),
|
|
187
|
+
onClearRecentSearches && /* @__PURE__ */ jsx("button", { type: "button", onClick: onClearRecentSearches, className: searchPanelTextButtonVariants(), children: clearRecentSearchesLabel })
|
|
188
|
+
] }),
|
|
189
|
+
/* @__PURE__ */ jsx("div", { id: listId, role: "listbox", "aria-labelledby": recentTitleId, children: recentOptions.map((option) => /* @__PURE__ */ jsxs("div", { role: "presentation", className: "group/recent relative flex items-center", children: [
|
|
190
|
+
/* @__PURE__ */ jsxs(
|
|
191
|
+
"div",
|
|
192
|
+
{
|
|
193
|
+
id: option.id,
|
|
194
|
+
role: "option",
|
|
195
|
+
"aria-selected": activeId === option.id,
|
|
196
|
+
onMouseEnter: () => onHover(option.id),
|
|
197
|
+
onClick: () => onPickRecent(option.query),
|
|
198
|
+
className: cn(searchPanelOptionVariants({ active: activeId === option.id }), "pr-10"),
|
|
199
|
+
"data-slot": "nav-search-recent-item",
|
|
200
|
+
children: [
|
|
201
|
+
/* @__PURE__ */ jsx(History, { size: 16, className: "shrink-0 text-grey-500", "aria-hidden": true }),
|
|
202
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-body-2 text-grey-800", children: option.query })
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
),
|
|
206
|
+
onRecentSearchRemove && // Pointer shortcut only: keyboard and screen-reader users remove with Delete on
|
|
207
|
+
// the active row, so it stays out of the listbox's accessibility tree.
|
|
208
|
+
/* @__PURE__ */ jsx(
|
|
209
|
+
"button",
|
|
210
|
+
{
|
|
211
|
+
type: "button",
|
|
212
|
+
tabIndex: -1,
|
|
213
|
+
"aria-hidden": "true",
|
|
214
|
+
title: removeRecentSearchLabel(option.query),
|
|
215
|
+
"aria-label": removeRecentSearchLabel(option.query),
|
|
216
|
+
onClick: () => onRecentSearchRemove(option.query),
|
|
217
|
+
className: "absolute right-1.5 rounded-full p-1.5 text-grey-400 transition-colors hover:bg-grey-100 hover:text-grey-700",
|
|
218
|
+
children: /* @__PURE__ */ jsx(X, { size: 14, "aria-hidden": true })
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
] }, option.id)) })
|
|
222
|
+
] }),
|
|
223
|
+
mode === "idle" && popularSearches.length > 0 && /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": popularSearchesTitle, "data-slot": "nav-search-popular", children: [
|
|
224
|
+
/* @__PURE__ */ jsx("span", { className: cn(searchPanelSectionTitleVariants(), "mb-2 block"), children: popularSearchesTitle }),
|
|
225
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: popularSearches.map((tag, idx) => {
|
|
226
|
+
const href = resolveNavDestination(tag.href, tag.zone, globalConfig.zones?.zones);
|
|
227
|
+
const crossZone = isCrossZoneNavigation(tag.zone, globalConfig.zones?.currentZone);
|
|
228
|
+
const tagClass = "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-grey-200 bg-grey-50 hover:border-grey-400 text-body-3 font-medium text-grey-800 transition-all cursor-pointer";
|
|
229
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
230
|
+
/* @__PURE__ */ jsx(TrendingUp, { size: 13, className: "text-grey-500 shrink-0", "aria-hidden": true }),
|
|
231
|
+
/* @__PURE__ */ jsx("span", { children: tag.label })
|
|
232
|
+
] });
|
|
233
|
+
if (href && CustomLink && !crossZone) {
|
|
234
|
+
return /* @__PURE__ */ jsx(CustomLink, { href, className: tagClass, onClick: () => onPopularPick(tag), children: content }, tag.id ?? idx);
|
|
235
|
+
}
|
|
236
|
+
if (href) {
|
|
237
|
+
return /* @__PURE__ */ jsx(
|
|
238
|
+
"a",
|
|
239
|
+
{
|
|
240
|
+
href,
|
|
241
|
+
"data-cross-zone": crossZone ? "true" : void 0,
|
|
242
|
+
className: tagClass,
|
|
243
|
+
onClick: () => onPopularPick(tag),
|
|
244
|
+
children: content
|
|
245
|
+
},
|
|
246
|
+
tag.id ?? idx
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return /* @__PURE__ */ jsx("button", { type: "button", className: tagClass, onClick: () => onPopularPick(tag), children: content }, tag.id ?? idx);
|
|
250
|
+
}) })
|
|
251
|
+
] }),
|
|
252
|
+
mode === "results" && results && /* @__PURE__ */ jsxs("div", { "data-slot": "nav-search-results", children: [
|
|
253
|
+
/* @__PURE__ */ jsx("span", { id: resultsTitleId, className: cn(searchPanelSectionTitleVariants(), "mb-1 block"), children: results.title ?? "Products" }),
|
|
254
|
+
results.loading && resultOptions.length === 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "flex flex-col", "data-slot": "nav-search-results-loading", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-2 py-2", children: [
|
|
255
|
+
/* @__PURE__ */ jsx("span", { className: "size-12 shrink-0 animate-pulse rounded-md bg-grey-100" }),
|
|
256
|
+
/* @__PURE__ */ jsxs("span", { className: "flex flex-1 flex-col gap-1.5", children: [
|
|
257
|
+
/* @__PURE__ */ jsx("span", { className: "h-3 w-3/4 animate-pulse rounded bg-grey-100" }),
|
|
258
|
+
/* @__PURE__ */ jsx("span", { className: "h-3 w-1/3 animate-pulse rounded bg-grey-100" })
|
|
259
|
+
] })
|
|
260
|
+
] }, i)) }),
|
|
261
|
+
!results.loading && resultOptions.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-2 py-2 text-body-2 text-grey-500", role: "status", children: results.emptyLabel ?? "No products match" }),
|
|
262
|
+
/* @__PURE__ */ jsxs("div", { id: listId, role: "listbox", "aria-labelledby": resultsTitleId, "aria-busy": results.loading || void 0, children: [
|
|
263
|
+
resultOptions.map(renderResultLink),
|
|
264
|
+
viewAll && /* @__PURE__ */ jsxs(
|
|
265
|
+
"div",
|
|
266
|
+
{
|
|
267
|
+
id: viewAll.id,
|
|
268
|
+
role: "option",
|
|
269
|
+
"aria-selected": activeId === viewAll.id,
|
|
270
|
+
onMouseEnter: () => onHover(viewAll.id),
|
|
271
|
+
onClick: onViewAll,
|
|
272
|
+
className: cn(
|
|
273
|
+
searchPanelOptionVariants({ active: activeId === viewAll.id }),
|
|
274
|
+
"mt-1 border-t border-grey-100 pt-3 text-body-2 font-semibold text-primary"
|
|
275
|
+
),
|
|
276
|
+
"data-slot": "nav-search-view-all",
|
|
277
|
+
children: [
|
|
278
|
+
/* @__PURE__ */ jsx(Search, { size: 16, className: "shrink-0", "aria-hidden": true }),
|
|
279
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: results.viewAllLabel ? results.viewAllLabel(query) : `See all results for "${query}"` }),
|
|
280
|
+
/* @__PURE__ */ jsx(ArrowRight, { size: 16, className: "shrink-0", "aria-hidden": true })
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
] })
|
|
285
|
+
] })
|
|
286
|
+
] });
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export { NavSearchPanel, useNavSearchCombobox };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { NavHeaderItem } from '../NavHeader/NavHeader.types';
|
|
3
|
+
import type { NavMenuColumnsProps, NavMenuData, NavMenuGroup, NavMenuPromoProps, NavMenuTilesProps, ToNavMenuItemsOptions } from './NavMenuColumns.types';
|
|
3
4
|
/**
|
|
4
5
|
* Splits groups, in order, into at most `maxColumns` stacks so the tallest
|
|
5
6
|
* stack is as short as possible (groups never split). Uses up to
|
|
@@ -27,4 +28,10 @@ export declare const NAV_MENU_TILE_PX = 88;
|
|
|
27
28
|
* initial on a muted square.
|
|
28
29
|
*/
|
|
29
30
|
export declare const NavMenuTiles: React.ForwardRefExoticComponent<NavMenuTilesProps & React.RefAttributes<HTMLElement>>;
|
|
31
|
+
/**
|
|
32
|
+
* Turns plain menu data into `NavHeader` items: each opens a popover with
|
|
33
|
+
* `NavMenuColumns` on desktop and, when `tiles` are given, `NavMenuTiles` in
|
|
34
|
+
* the mobile tab panel. The item label itself is not a link.
|
|
35
|
+
*/
|
|
36
|
+
export declare function toNavMenuItems(menus: readonly NavMenuData[], { crossZone, linkComponent, popoverWidth }?: ToNavMenuItemsOptions): NavHeaderItem[];
|
|
30
37
|
//# sourceMappingURL=NavMenuColumns.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavMenuColumns.d.ts","sourceRoot":"","sources":["../../../src/components/NavMenuColumns/NavMenuColumns.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"NavMenuColumns.d.ts","sourceRoot":"","sources":["../../../src/components/NavMenuColumns/NavMenuColumns.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAiBlE,OAAO,KAAK,EAEV,mBAAmB,EACnB,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,qBAAqB,EACtB,MAAM,wBAAwB,CAAC;AA8BhC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,YAAY,EAAE,EAAE,CA+B9F;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,yFAgE1B,CAAC;AAGF;;;GAGG;AACH,eAAO,MAAM,YAAY,uFA0CxB,CAAC;AAGF,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC;;;;;GAKG;AACH,eAAO,MAAM,YAAY,uFAuCxB,CAAC;AAGF;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,EAAE,SAAiB,EAAE,aAAa,EAAE,YAA0B,EAAE,GAAE,qBAA0B,GAC3F,aAAa,EAAE,CAuBjB"}
|
|
@@ -134,5 +134,31 @@ const NavMenuTiles = React.forwardRef(
|
|
|
134
134
|
}
|
|
135
135
|
);
|
|
136
136
|
NavMenuTiles.displayName = "NavMenuTiles";
|
|
137
|
+
function toNavMenuItems(menus, { crossZone = false, linkComponent, popoverWidth = "container" } = {}) {
|
|
138
|
+
return menus.map((menu) => ({
|
|
139
|
+
id: menu.id,
|
|
140
|
+
label: menu.label,
|
|
141
|
+
popoverWidth,
|
|
142
|
+
content: /* @__PURE__ */ jsx(
|
|
143
|
+
NavMenuColumns,
|
|
144
|
+
{
|
|
145
|
+
"aria-label": typeof menu.label === "string" ? menu.label : void 0,
|
|
146
|
+
groups: menu.groups,
|
|
147
|
+
aside: menu.aside,
|
|
148
|
+
crossZone,
|
|
149
|
+
linkComponent
|
|
150
|
+
}
|
|
151
|
+
),
|
|
152
|
+
mobileContent: menu.tiles ? /* @__PURE__ */ jsx(
|
|
153
|
+
NavMenuTiles,
|
|
154
|
+
{
|
|
155
|
+
"aria-label": typeof menu.label === "string" ? menu.label : void 0,
|
|
156
|
+
items: menu.tiles,
|
|
157
|
+
crossZone,
|
|
158
|
+
linkComponent
|
|
159
|
+
}
|
|
160
|
+
) : void 0
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
137
163
|
|
|
138
|
-
export { NAV_MENU_TILE_PX, NavMenuColumns, NavMenuPromo, NavMenuTiles, packNavMenuGroups };
|
|
164
|
+
export { NAV_MENU_TILE_PX, NavMenuColumns, NavMenuPromo, NavMenuTiles, packNavMenuGroups, toNavMenuItems };
|
|
@@ -35,7 +35,7 @@ const navMenuPromoVariants = cva(
|
|
|
35
35
|
"relative isolate flex w-[312px] aspect-[312/270] flex-col items-start gap-1.5 overflow-hidden rounded-[10px] p-5 text-grey-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
36
36
|
);
|
|
37
37
|
const navMenuPromoTitleVariants = cva(
|
|
38
|
-
"font-sans text-
|
|
38
|
+
"font-sans text-subtitle-1 leading-[1.3] [&_em]:font-display [&_em]:not-italic"
|
|
39
39
|
);
|
|
40
40
|
const navMenuTilesVariants = cva(
|
|
41
41
|
"grid gap-x-3 gap-y-4 overflow-x-auto bg-grey-50 px-5 py-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
@@ -81,4 +81,22 @@ export interface NavMenuTilesProps extends Omit<React.HTMLAttributes<HTMLElement
|
|
|
81
81
|
/** Accessible name of the navigation landmark. */
|
|
82
82
|
'aria-label'?: string;
|
|
83
83
|
}
|
|
84
|
+
/** Plain data for one header menu: desktop groups, optional mobile tiles and aside. */
|
|
85
|
+
export interface NavMenuData {
|
|
86
|
+
id: string;
|
|
87
|
+
label: React.ReactNode;
|
|
88
|
+
groups: NavMenuGroup[];
|
|
89
|
+
/** Mobile tab panel tiles; without them the mobile panel shows the groups. */
|
|
90
|
+
tiles?: NavMenuTile[];
|
|
91
|
+
/** Desktop aside, e.g. a `NavMenuPromo`. */
|
|
92
|
+
aside?: React.ReactNode;
|
|
93
|
+
}
|
|
94
|
+
export interface ToNavMenuItemsOptions {
|
|
95
|
+
/** The menu links lead to another zone: plain `<a>` (full navigation). */
|
|
96
|
+
crossZone?: boolean;
|
|
97
|
+
/** Link component for same-zone links (e.g. `next/link`). */
|
|
98
|
+
linkComponent?: React.ComponentType<NavMenuColumnsLinkProps>;
|
|
99
|
+
/** Desktop popover width. Defaults to 'container'. */
|
|
100
|
+
popoverWidth?: 'container' | 'full' | 'dropdown' | number;
|
|
101
|
+
}
|
|
84
102
|
//# sourceMappingURL=NavMenuColumns.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavMenuColumns.types.d.ts","sourceRoot":"","sources":["../../../src/components/NavMenuColumns/NavMenuColumns.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAEpC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,yCAAyC;AACzC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,sEAAsE;IACtE,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACtB,8DAA8D;IAC9D,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACjD;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,OAAO,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,UAAU,CAAC;IAC9F,mFAAmF;IACnF,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2FAA2F;IAC3F,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,uBAAuB,CAAC,CAAC;IAC7D,mFAAmF;IACnF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IACzF,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,KAAK,EAAE,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;IAC3C,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,+CAA+C;IAC/C,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,uBAAuB,CAAC,CAAC;IAC7D,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,UAAU,CAAC;IAC5F,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,uBAAuB,CAAC,CAAC;IAC7D,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}
|
|
1
|
+
{"version":3,"file":"NavMenuColumns.types.d.ts","sourceRoot":"","sources":["../../../src/components/NavMenuColumns/NavMenuColumns.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { OptionPickerProps } from './OptionPicker.types';
|
|
3
|
+
/**
|
|
4
|
+
* One product option (finish, size…) as visible choices: colour swatches or pill buttons, in a
|
|
5
|
+
* radio group (arrow keys move and select, Tab leaves the group). Sold-out options stay
|
|
6
|
+
* selectable and are struck through; combinations that don't exist are disabled. Long lists
|
|
7
|
+
* (more than `maxVisible`) become a dropdown.
|
|
8
|
+
*/
|
|
9
|
+
export declare const OptionPicker: React.ForwardRefExoticComponent<OptionPickerProps & React.RefAttributes<HTMLDivElement>>;
|
|
10
|
+
//# sourceMappingURL=OptionPicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OptionPicker.d.ts","sourceRoot":"","sources":["../../../src/components/OptionPicker/OptionPicker.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAY/B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAIlF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,0FA0IxB,CAAC"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@timmbr/utils';
|
|
5
|
+
import { optionPickerLegendVariants, optionPickerVariants, optionStrikeVariants, optionImageVariants, optionSwatchVariants, optionPillVariants } from './OptionPicker.styles.js';
|
|
6
|
+
import { Select } from '../Select/Select.js';
|
|
7
|
+
import { Swatch } from '../Swatch/Swatch.js';
|
|
8
|
+
|
|
9
|
+
const stateOf = (option) => option.state ?? "available";
|
|
10
|
+
const OptionPicker = React.forwardRef(
|
|
11
|
+
({ label, options, value, onChange, display = "pill", maxVisible = 8, labels, className, ...props }, ref) => {
|
|
12
|
+
const legendId = React.useId();
|
|
13
|
+
const buttons = React.useRef([]);
|
|
14
|
+
const selected = options.find((option) => option.value === value);
|
|
15
|
+
const outOfStock = labels?.outOfStock ?? "Out of stock";
|
|
16
|
+
const legend = /* @__PURE__ */ jsxs("p", { id: legendId, className: optionPickerLegendVariants(), "data-slot": "option-picker-legend", children: [
|
|
17
|
+
label,
|
|
18
|
+
selected && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
19
|
+
": ",
|
|
20
|
+
/* @__PURE__ */ jsx("span", { className: "font-normal text-grey-700", children: selected.label }),
|
|
21
|
+
stateOf(selected) === "out-of-stock" && /* @__PURE__ */ jsx("span", { className: "font-normal text-destructive", children: ` · ${outOfStock}` })
|
|
22
|
+
] })
|
|
23
|
+
] });
|
|
24
|
+
if (options.length > maxVisible) {
|
|
25
|
+
const unavailable = labels?.unavailable ?? "Unavailable";
|
|
26
|
+
return /* @__PURE__ */ jsxs("div", { ref, "data-slot": "option-picker", className: cn(optionPickerVariants(), className), ...props, children: [
|
|
27
|
+
legend,
|
|
28
|
+
/* @__PURE__ */ jsx(
|
|
29
|
+
Select,
|
|
30
|
+
{
|
|
31
|
+
ariaLabel: label,
|
|
32
|
+
value,
|
|
33
|
+
onValueChange: onChange,
|
|
34
|
+
options: options.map((option) => ({
|
|
35
|
+
value: option.value,
|
|
36
|
+
label: stateOf(option) === "available" ? option.label : `${option.label} (${stateOf(option) === "out-of-stock" ? outOfStock : unavailable})`,
|
|
37
|
+
disabled: stateOf(option) === "unavailable"
|
|
38
|
+
}))
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
] });
|
|
42
|
+
}
|
|
43
|
+
const enabled = options.map((option, index) => stateOf(option) === "unavailable" ? -1 : index).filter((i) => i >= 0);
|
|
44
|
+
const selectedIndex = options.findIndex((option) => option.value === value);
|
|
45
|
+
const tabStop = selectedIndex >= 0 && enabled.includes(selectedIndex) ? selectedIndex : enabled[0];
|
|
46
|
+
const move = (from, delta) => {
|
|
47
|
+
if (enabled.length === 0) return;
|
|
48
|
+
const position = enabled.indexOf(from);
|
|
49
|
+
const next = enabled[(position + delta + enabled.length) % enabled.length];
|
|
50
|
+
buttons.current[next]?.focus();
|
|
51
|
+
onChange(options[next].value);
|
|
52
|
+
};
|
|
53
|
+
const onKeyDown = (index) => (event) => {
|
|
54
|
+
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
move(index, 1);
|
|
57
|
+
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
move(index, -1);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return /* @__PURE__ */ jsxs("div", { ref, "data-slot": "option-picker", className: cn(optionPickerVariants(), className), ...props, children: [
|
|
63
|
+
legend,
|
|
64
|
+
/* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-labelledby": legendId, className: "flex flex-wrap gap-2", children: options.map((option, index) => {
|
|
65
|
+
const state = stateOf(option);
|
|
66
|
+
const isSelected = option.value === value;
|
|
67
|
+
const asSwatch = display === "swatch" && !!option.swatchHex;
|
|
68
|
+
const asImage = display === "image" && !!option.imageUrl;
|
|
69
|
+
const name = state === "available" ? option.label : `${option.label}, ${state === "out-of-stock" ? outOfStock : labels?.unavailable ?? "Unavailable"}`;
|
|
70
|
+
return /* @__PURE__ */ jsx(
|
|
71
|
+
"button",
|
|
72
|
+
{
|
|
73
|
+
ref: (node) => {
|
|
74
|
+
buttons.current[index] = node;
|
|
75
|
+
},
|
|
76
|
+
type: "button",
|
|
77
|
+
role: "radio",
|
|
78
|
+
"aria-checked": isSelected,
|
|
79
|
+
"aria-label": name,
|
|
80
|
+
title: option.label,
|
|
81
|
+
disabled: state === "unavailable",
|
|
82
|
+
tabIndex: index === tabStop ? 0 : -1,
|
|
83
|
+
onClick: () => onChange(option.value),
|
|
84
|
+
onKeyDown: onKeyDown(index),
|
|
85
|
+
"data-state": state,
|
|
86
|
+
"data-slot": "option-picker-option",
|
|
87
|
+
className: asImage ? optionImageVariants({ selected: isSelected, state }) : asSwatch ? optionSwatchVariants({ selected: isSelected, state }) : optionPillVariants({ selected: isSelected, state }),
|
|
88
|
+
children: asImage ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
89
|
+
/* @__PURE__ */ jsx(
|
|
90
|
+
"img",
|
|
91
|
+
{
|
|
92
|
+
src: option.imageUrl,
|
|
93
|
+
alt: "",
|
|
94
|
+
loading: "lazy",
|
|
95
|
+
decoding: "async",
|
|
96
|
+
className: "size-full object-cover"
|
|
97
|
+
}
|
|
98
|
+
),
|
|
99
|
+
state !== "available" && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn(optionStrikeVariants(), "rounded-none") })
|
|
100
|
+
] }) : asSwatch ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
101
|
+
/* @__PURE__ */ jsx(Swatch, { color: option.swatchHex, size: "lg" }),
|
|
102
|
+
state !== "available" && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: optionStrikeVariants() })
|
|
103
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
104
|
+
option.swatchHex && /* @__PURE__ */ jsx(Swatch, { color: option.swatchHex, size: "sm" }),
|
|
105
|
+
option.label
|
|
106
|
+
] })
|
|
107
|
+
},
|
|
108
|
+
option.value
|
|
109
|
+
);
|
|
110
|
+
}) })
|
|
111
|
+
] });
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
OptionPicker.displayName = "OptionPicker";
|
|
115
|
+
|
|
116
|
+
export { OptionPicker };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const optionPickerVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
2
|
+
export declare const optionPickerLegendVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
3
|
+
/** Swatch button: the dot sits inside a ring that shows the selection. */
|
|
4
|
+
export declare const optionSwatchVariants: (props?: ({
|
|
5
|
+
selected?: boolean | null | undefined;
|
|
6
|
+
state?: "available" | "out-of-stock" | "unavailable" | null | undefined;
|
|
7
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
|
+
/** Compact chip; the selected one takes the brand accent (border + tint), like the filters. */
|
|
9
|
+
export declare const optionPillVariants: (props?: ({
|
|
10
|
+
selected?: boolean | null | undefined;
|
|
11
|
+
state?: "available" | "out-of-stock" | "unavailable" | null | undefined;
|
|
12
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
13
|
+
/** Photo tile of the option (e.g. the chair in that fabric); the label is in the legend. */
|
|
14
|
+
export declare const optionImageVariants: (props?: ({
|
|
15
|
+
selected?: boolean | null | undefined;
|
|
16
|
+
state?: "available" | "out-of-stock" | "unavailable" | null | undefined;
|
|
17
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
18
|
+
/** Diagonal line across a sold-out swatch. */
|
|
19
|
+
export declare const optionStrikeVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
20
|
+
//# sourceMappingURL=OptionPicker.styles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OptionPicker.styles.d.ts","sourceRoot":"","sources":["../../../src/components/OptionPicker/OptionPicker.styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,oBAAoB,oFAAuC,CAAC;AAEzE,eAAO,MAAM,0BAA0B,oFAAiD,CAAC;AAEzF,0EAA0E;AAC1E,eAAO,MAAM,oBAAoB;;;8EAgBhC,CAAC;AAEF,+FAA+F;AAC/F,eAAO,MAAM,kBAAkB;;;8EAgB9B,CAAC;AAEF,4FAA4F;AAC5F,eAAO,MAAM,mBAAmB;;;8EAgB/B,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,oBAAoB,oFAEhC,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
const optionPickerVariants = cva("flex flex-col gap-3 font-sans");
|
|
4
|
+
const optionPickerLegendVariants = cva("text-body-2 font-semibold text-grey-950");
|
|
5
|
+
const optionSwatchVariants = cva(
|
|
6
|
+
"relative flex size-10 items-center justify-center rounded-full border-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
selected: {
|
|
10
|
+
true: "border-primary",
|
|
11
|
+
false: "border-transparent hover:border-grey-300"
|
|
12
|
+
},
|
|
13
|
+
state: {
|
|
14
|
+
available: "",
|
|
15
|
+
"out-of-stock": "opacity-60",
|
|
16
|
+
unavailable: "opacity-30"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: { selected: false, state: "available" }
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
const optionPillVariants = cva(
|
|
23
|
+
"relative inline-flex h-9 min-w-11 items-center justify-center gap-2 rounded-sm border px-3.5 text-body-3 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed",
|
|
24
|
+
{
|
|
25
|
+
variants: {
|
|
26
|
+
selected: {
|
|
27
|
+
true: "border-primary bg-primary/10 font-semibold text-grey-950 shadow-[inset_0_0_0_1px_var(--color-primary)]",
|
|
28
|
+
false: "border-grey-300 bg-grey-50 text-grey-700 hover:border-grey-500 hover:text-grey-950"
|
|
29
|
+
},
|
|
30
|
+
state: {
|
|
31
|
+
available: "",
|
|
32
|
+
"out-of-stock": "text-grey-500 line-through decoration-grey-500",
|
|
33
|
+
unavailable: "border-dashed text-grey-400 line-through"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: { selected: false, state: "available" }
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
const optionImageVariants = cva(
|
|
40
|
+
"relative size-16 overflow-hidden rounded-sm border-2 bg-grey-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed",
|
|
41
|
+
{
|
|
42
|
+
variants: {
|
|
43
|
+
selected: {
|
|
44
|
+
true: "border-primary",
|
|
45
|
+
false: "border-grey-200 hover:border-grey-400"
|
|
46
|
+
},
|
|
47
|
+
state: {
|
|
48
|
+
available: "",
|
|
49
|
+
"out-of-stock": "opacity-60",
|
|
50
|
+
unavailable: "opacity-30"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
defaultVariants: { selected: false, state: "available" }
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
const optionStrikeVariants = cva(
|
|
57
|
+
"pointer-events-none absolute inset-1 rounded-full bg-[linear-gradient(to_top_right,transparent_calc(50%-1px),var(--color-grey-700)_calc(50%-1px),var(--color-grey-700)_calc(50%+1px),transparent_calc(50%+1px))]"
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export { optionImageVariants, optionPickerLegendVariants, optionPickerVariants, optionPillVariants, optionStrikeVariants, optionSwatchVariants };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* - `available`: can be picked and bought;
|
|
4
|
+
* - `out-of-stock`: exists but sold out — still selectable (shown struck through), so the shopper
|
|
5
|
+
* sees its price and can pick another option;
|
|
6
|
+
* - `unavailable`: this combination doesn't exist — disabled.
|
|
7
|
+
*/
|
|
8
|
+
export type OptionPickerState = 'available' | 'out-of-stock' | 'unavailable';
|
|
9
|
+
export interface OptionPickerOption {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
/** Swatch colour (display `swatch`); options without one fall back to a pill. */
|
|
13
|
+
swatchHex?: string | null;
|
|
14
|
+
/** Photo of the option (display `image`), e.g. the product in that fabric. */
|
|
15
|
+
imageUrl?: string | null;
|
|
16
|
+
state?: OptionPickerState;
|
|
17
|
+
}
|
|
18
|
+
export interface OptionPickerLabels {
|
|
19
|
+
/** Suffix after the selected label when it's sold out. Defaults to "Out of stock". */
|
|
20
|
+
outOfStock?: string;
|
|
21
|
+
/** Shown in the dropdown for combinations that don't exist. Defaults to "Unavailable". */
|
|
22
|
+
unavailable?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface OptionPickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
25
|
+
/** Group name, shown as "Finish: Walnut". */
|
|
26
|
+
label: string;
|
|
27
|
+
options: OptionPickerOption[];
|
|
28
|
+
value?: string;
|
|
29
|
+
onChange: (value: string) => void;
|
|
30
|
+
/**
|
|
31
|
+
* `swatch`: colour dots; `image`: photo tiles; `pill`: text chips (a colour dot inside when the
|
|
32
|
+
* option has one). Options missing their swatch colour or image fall back to a pill.
|
|
33
|
+
*/
|
|
34
|
+
display?: 'swatch' | 'image' | 'pill';
|
|
35
|
+
/** More options than this render as a dropdown. Defaults to 8. */
|
|
36
|
+
maxVisible?: number;
|
|
37
|
+
labels?: OptionPickerLabels;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=OptionPicker.types.d.ts.map
|