@timmbr/ui 1.9.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.styles.js +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 +24 -1
- package/dist/providers/TimmbrConfigProvider.d.ts.map +1 -1
- package/dist/providers/TimmbrConfigProvider.js +5 -9
- package/package.json +5 -5
|
@@ -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 };
|
|
@@ -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"
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OptionPicker.types.d.ts","sourceRoot":"","sources":["../../../src/components/OptionPicker/OptionPicker.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,cAAc,GAAG,aAAa,CAAC;AAE7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC;IAC/F,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;OAGG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;IACtC,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC7B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/OptionPicker/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Price.styles.d.ts","sourceRoot":"","sources":["../../../src/components/Price/Price.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAElE,eAAO,MAAM,aAAa;;8EASxB,CAAC;AAEH,eAAO,MAAM,mBAAmB;;8EAS9B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;8EAS/B,CAAC;AAEH,eAAO,MAAM,qBAAqB;;
|
|
1
|
+
{"version":3,"file":"Price.styles.d.ts","sourceRoot":"","sources":["../../../src/components/Price/Price.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAElE,eAAO,MAAM,aAAa;;8EASxB,CAAC;AAEH,eAAO,MAAM,mBAAmB;;8EAS9B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;8EAS/B,CAAC;AAEH,eAAO,MAAM,qBAAqB;;8EAUhC,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC,OAAO,aAAa,CAAC,CAAC"}
|
|
@@ -4,7 +4,7 @@ const priceVariants = cva("inline-flex flex-wrap items-baseline font-sans text-f
|
|
|
4
4
|
variants: {
|
|
5
5
|
size: {
|
|
6
6
|
sm: "gap-x-1.5",
|
|
7
|
-
md: "gap-x-1",
|
|
7
|
+
md: "gap-x-1.5",
|
|
8
8
|
lg: "gap-x-2"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
@@ -34,7 +34,8 @@ const priceDiscountVariants = cva("font-semibold text-success", {
|
|
|
34
34
|
variants: {
|
|
35
35
|
size: {
|
|
36
36
|
sm: "text-body-3",
|
|
37
|
-
|
|
37
|
+
// No own margin: the row gap spaces it, and it starts flush left when it wraps.
|
|
38
|
+
md: "text-body-2",
|
|
38
39
|
lg: "text-body-1"
|
|
39
40
|
}
|
|
40
41
|
},
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { ProductGalleryProps } from './ProductGallery.types';
|
|
3
|
+
/**
|
|
4
|
+
* Product images: a main image you can swipe (scroll-snap) or step with arrows, a thumbnail row
|
|
5
|
+
* (visible on phones too: easier to aim than dots), and a fullscreen zoomable viewer when the main
|
|
6
|
+
* image is clicked. The first image is marked high priority (it's usually the page's LCP), the
|
|
7
|
+
* rest load lazily; the frame keeps its aspect ratio, so nothing shifts while images load.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ProductGallery: React.ForwardRefExoticComponent<ProductGalleryProps & React.RefAttributes<HTMLDivElement>>;
|
|
10
|
+
//# sourceMappingURL=ProductGallery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductGallery.d.ts","sourceRoot":"","sources":["../../../src/components/ProductGallery/ProductGallery.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAmB/B,OAAO,KAAK,EAAE,mBAAmB,EAAoD,MAAM,wBAAwB,CAAC;AAoBpH;;;;;GAKG;AACH,eAAO,MAAM,cAAc,4FAkS1B,CAAC"}
|