@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.
Files changed (166) hide show
  1. package/dist/components/Breadcrumb/Breadcrumb.d.ts +9 -0
  2. package/dist/components/Breadcrumb/Breadcrumb.d.ts.map +1 -0
  3. package/dist/components/Breadcrumb/Breadcrumb.js +32 -0
  4. package/dist/components/Breadcrumb/Breadcrumb.styles.d.ts +8 -0
  5. package/dist/components/Breadcrumb/Breadcrumb.styles.d.ts.map +1 -0
  6. package/dist/components/Breadcrumb/Breadcrumb.styles.js +20 -0
  7. package/dist/components/Breadcrumb/Breadcrumb.types.d.ts +27 -0
  8. package/dist/components/Breadcrumb/Breadcrumb.types.d.ts.map +1 -0
  9. package/dist/components/Breadcrumb/index.d.ts +4 -0
  10. package/dist/components/Breadcrumb/index.d.ts.map +1 -0
  11. package/dist/components/Chip/Chip.d.ts.map +1 -1
  12. package/dist/components/Chip/Chip.js +13 -3
  13. package/dist/components/Chip/Chip.types.d.ts +5 -0
  14. package/dist/components/Chip/Chip.types.d.ts.map +1 -1
  15. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  16. package/dist/components/Drawer/Drawer.js +4 -4
  17. package/dist/components/Drawer/Drawer.styles.d.ts +1 -0
  18. package/dist/components/Drawer/Drawer.styles.d.ts.map +1 -1
  19. package/dist/components/Drawer/Drawer.styles.js +7 -1
  20. package/dist/components/Drawer/Drawer.types.d.ts +2 -0
  21. package/dist/components/Drawer/Drawer.types.d.ts.map +1 -1
  22. package/dist/components/FilterChips/FilterChips.d.ts +8 -0
  23. package/dist/components/FilterChips/FilterChips.d.ts.map +1 -0
  24. package/dist/components/FilterChips/FilterChips.js +39 -0
  25. package/dist/components/FilterChips/FilterChips.types.d.ts +17 -0
  26. package/dist/components/FilterChips/FilterChips.types.d.ts.map +1 -0
  27. package/dist/components/FilterChips/index.d.ts +3 -0
  28. package/dist/components/FilterChips/index.d.ts.map +1 -0
  29. package/dist/components/FilterDrawer/FilterDrawer.d.ts +12 -0
  30. package/dist/components/FilterDrawer/FilterDrawer.d.ts.map +1 -0
  31. package/dist/components/FilterDrawer/FilterDrawer.js +157 -0
  32. package/dist/components/FilterDrawer/FilterDrawer.styles.d.ts +12 -0
  33. package/dist/components/FilterDrawer/FilterDrawer.styles.d.ts.map +1 -0
  34. package/dist/components/FilterDrawer/FilterDrawer.styles.js +30 -0
  35. package/dist/components/FilterDrawer/FilterDrawer.types.d.ts +28 -0
  36. package/dist/components/FilterDrawer/FilterDrawer.types.d.ts.map +1 -0
  37. package/dist/components/FilterDrawer/index.d.ts +4 -0
  38. package/dist/components/FilterDrawer/index.d.ts.map +1 -0
  39. package/dist/components/FilterPanel/FilterPanel.d.ts +33 -0
  40. package/dist/components/FilterPanel/FilterPanel.d.ts.map +1 -0
  41. package/dist/components/FilterPanel/FilterPanel.js +239 -0
  42. package/dist/components/FilterPanel/FilterPanel.styles.d.ts +16 -0
  43. package/dist/components/FilterPanel/FilterPanel.styles.d.ts.map +1 -0
  44. package/dist/components/FilterPanel/FilterPanel.styles.js +31 -0
  45. package/dist/components/FilterPanel/FilterPanel.types.d.ts +80 -0
  46. package/dist/components/FilterPanel/FilterPanel.types.d.ts.map +1 -0
  47. package/dist/components/FilterPanel/FilterPanel.types.js +3 -0
  48. package/dist/components/FilterPanel/index.d.ts +4 -0
  49. package/dist/components/FilterPanel/index.d.ts.map +1 -0
  50. package/dist/components/ImageUpload/ImageUpload.styles.d.ts +3 -3
  51. package/dist/components/MediaCard/MediaCard.d.ts.map +1 -1
  52. package/dist/components/MediaCard/MediaCard.js +3 -1
  53. package/dist/components/NavHeader/NavHeader.styles.d.ts +7 -0
  54. package/dist/components/NavHeader/NavHeader.styles.d.ts.map +1 -1
  55. package/dist/components/NavHeader/NavHeader.styles.js +20 -2
  56. package/dist/components/NavHeader/NavHeader.types.d.ts +49 -1
  57. package/dist/components/NavHeader/NavHeader.types.d.ts.map +1 -1
  58. package/dist/components/NavHeader/index.d.ts +3 -2
  59. package/dist/components/NavHeader/index.d.ts.map +1 -1
  60. package/dist/components/NavHeader/subcomponents/NavMobileSearchDrawer.d.ts.map +1 -1
  61. package/dist/components/NavHeader/subcomponents/NavMobileSearchDrawer.js +15 -33
  62. package/dist/components/NavHeader/subcomponents/NavSearch.d.ts.map +1 -1
  63. package/dist/components/NavHeader/subcomponents/NavSearch.js +37 -83
  64. package/dist/components/NavHeader/subcomponents/NavSearchPanel.d.ts +63 -0
  65. package/dist/components/NavHeader/subcomponents/NavSearchPanel.d.ts.map +1 -0
  66. package/dist/components/NavHeader/subcomponents/NavSearchPanel.js +289 -0
  67. package/dist/components/NavMenuColumns/NavMenuColumns.d.ts +8 -1
  68. package/dist/components/NavMenuColumns/NavMenuColumns.d.ts.map +1 -1
  69. package/dist/components/NavMenuColumns/NavMenuColumns.js +27 -1
  70. package/dist/components/NavMenuColumns/NavMenuColumns.styles.js +1 -1
  71. package/dist/components/NavMenuColumns/NavMenuColumns.types.d.ts +18 -0
  72. package/dist/components/NavMenuColumns/NavMenuColumns.types.d.ts.map +1 -1
  73. package/dist/components/OptionPicker/OptionPicker.d.ts +10 -0
  74. package/dist/components/OptionPicker/OptionPicker.d.ts.map +1 -0
  75. package/dist/components/OptionPicker/OptionPicker.js +116 -0
  76. package/dist/components/OptionPicker/OptionPicker.styles.d.ts +20 -0
  77. package/dist/components/OptionPicker/OptionPicker.styles.d.ts.map +1 -0
  78. package/dist/components/OptionPicker/OptionPicker.styles.js +60 -0
  79. package/dist/components/OptionPicker/OptionPicker.types.d.ts +39 -0
  80. package/dist/components/OptionPicker/OptionPicker.types.d.ts.map +1 -0
  81. package/dist/components/OptionPicker/index.d.ts +4 -0
  82. package/dist/components/OptionPicker/index.d.ts.map +1 -0
  83. package/dist/components/Price/Price.styles.d.ts.map +1 -1
  84. package/dist/components/Price/Price.styles.js +3 -2
  85. package/dist/components/ProductGallery/ProductGallery.d.ts +10 -0
  86. package/dist/components/ProductGallery/ProductGallery.d.ts.map +1 -0
  87. package/dist/components/ProductGallery/ProductGallery.js +306 -0
  88. package/dist/components/ProductGallery/ProductGallery.styles.d.ts +32 -0
  89. package/dist/components/ProductGallery/ProductGallery.styles.d.ts.map +1 -0
  90. package/dist/components/ProductGallery/ProductGallery.styles.js +86 -0
  91. package/dist/components/ProductGallery/ProductGallery.types.d.ts +55 -0
  92. package/dist/components/ProductGallery/ProductGallery.types.d.ts.map +1 -0
  93. package/dist/components/ProductGallery/ProductGalleryLightbox.d.ts +20 -0
  94. package/dist/components/ProductGallery/ProductGalleryLightbox.d.ts.map +1 -0
  95. package/dist/components/ProductGallery/ProductGalleryLightbox.js +210 -0
  96. package/dist/components/ProductGallery/index.d.ts +4 -0
  97. package/dist/components/ProductGallery/index.d.ts.map +1 -0
  98. package/dist/components/QuantityStepper/QuantityStepper.d.ts +8 -0
  99. package/dist/components/QuantityStepper/QuantityStepper.d.ts.map +1 -0
  100. package/dist/components/QuantityStepper/QuantityStepper.js +65 -0
  101. package/dist/components/QuantityStepper/QuantityStepper.styles.d.ts +8 -0
  102. package/dist/components/QuantityStepper/QuantityStepper.styles.d.ts.map +1 -0
  103. package/dist/components/QuantityStepper/QuantityStepper.styles.js +24 -0
  104. package/dist/components/QuantityStepper/QuantityStepper.types.d.ts +20 -0
  105. package/dist/components/QuantityStepper/QuantityStepper.types.d.ts.map +1 -0
  106. package/dist/components/QuantityStepper/index.d.ts +4 -0
  107. package/dist/components/QuantityStepper/index.d.ts.map +1 -0
  108. package/dist/components/RangeSlider/RangeSlider.d.ts.map +1 -1
  109. package/dist/components/RangeSlider/RangeSlider.js +5 -3
  110. package/dist/components/RangeSlider/RangeSlider.types.d.ts +7 -0
  111. package/dist/components/RangeSlider/RangeSlider.types.d.ts.map +1 -1
  112. package/dist/components/Select/Select.d.ts.map +1 -1
  113. package/dist/components/Select/Select.js +14 -1
  114. package/dist/components/Select/Select.types.d.ts +4 -0
  115. package/dist/components/Select/Select.types.d.ts.map +1 -1
  116. package/dist/components/Snackbar/Snackbar.d.ts +9 -0
  117. package/dist/components/Snackbar/Snackbar.d.ts.map +1 -0
  118. package/dist/components/Snackbar/Snackbar.js +108 -0
  119. package/dist/components/Snackbar/Snackbar.styles.d.ts +17 -0
  120. package/dist/components/Snackbar/Snackbar.styles.d.ts.map +1 -0
  121. package/dist/components/Snackbar/Snackbar.styles.js +37 -0
  122. package/dist/components/Snackbar/Snackbar.types.d.ts +36 -0
  123. package/dist/components/Snackbar/Snackbar.types.d.ts.map +1 -0
  124. package/dist/components/Snackbar/SnackbarHost.d.ts +4 -0
  125. package/dist/components/Snackbar/SnackbarHost.d.ts.map +1 -0
  126. package/dist/components/Snackbar/SnackbarHost.js +24 -0
  127. package/dist/components/Snackbar/index.d.ts +6 -0
  128. package/dist/components/Snackbar/index.d.ts.map +1 -0
  129. package/dist/components/Snackbar/snackbarStore.d.ts +42 -0
  130. package/dist/components/Snackbar/snackbarStore.d.ts.map +1 -0
  131. package/dist/components/Snackbar/snackbarStore.js +87 -0
  132. package/dist/components/StockMeter/StockMeter.d.ts +8 -0
  133. package/dist/components/StockMeter/StockMeter.d.ts.map +1 -0
  134. package/dist/components/StockMeter/StockMeter.js +19 -0
  135. package/dist/components/StockMeter/StockMeter.styles.d.ts +4 -0
  136. package/dist/components/StockMeter/StockMeter.styles.d.ts.map +1 -0
  137. package/dist/components/StockMeter/StockMeter.styles.js +7 -0
  138. package/dist/components/StockMeter/StockMeter.types.d.ts +10 -0
  139. package/dist/components/StockMeter/StockMeter.types.d.ts.map +1 -0
  140. package/dist/components/StockMeter/index.d.ts +4 -0
  141. package/dist/components/StockMeter/index.d.ts.map +1 -0
  142. package/dist/components/Swatch/Swatch.d.ts +11 -0
  143. package/dist/components/Swatch/Swatch.d.ts.map +1 -0
  144. package/dist/components/Swatch/Swatch.js +40 -0
  145. package/dist/components/Swatch/Swatch.styles.d.ts +8 -0
  146. package/dist/components/Swatch/Swatch.styles.d.ts.map +1 -0
  147. package/dist/components/Swatch/Swatch.styles.js +26 -0
  148. package/dist/components/Swatch/Swatch.types.d.ts +11 -0
  149. package/dist/components/Swatch/Swatch.types.d.ts.map +1 -0
  150. package/dist/components/Swatch/index.d.ts +4 -0
  151. package/dist/components/Swatch/index.d.ts.map +1 -0
  152. package/dist/components/Toast/Toast.d.ts.map +1 -1
  153. package/dist/components/Toast/Toast.js +49 -42
  154. package/dist/components/Toast/Toast.types.d.ts +7 -0
  155. package/dist/components/Toast/Toast.types.d.ts.map +1 -1
  156. package/dist/components/Toast/ToastContainer.d.ts.map +1 -1
  157. package/dist/components/Toast/ToastContainer.js +5 -0
  158. package/dist/components/Toast/toastStore.d.ts +11 -0
  159. package/dist/components/Toast/toastStore.d.ts.map +1 -1
  160. package/dist/components/Toast/toastStore.js +70 -30
  161. package/dist/index.d.ts +10 -0
  162. package/dist/index.d.ts.map +1 -1
  163. package/dist/index.js +25 -2
  164. package/dist/providers/TimmbrConfigProvider.d.ts.map +1 -1
  165. package/dist/providers/TimmbrConfigProvider.js +5 -9
  166. 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 { NavMenuColumnsProps, NavMenuGroup, NavMenuPromoProps, NavMenuTilesProps } from './NavMenuColumns.types';
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;AAmB/B,OAAO,KAAK,EAEV,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EAClB,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"}
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-[20px] leading-[1.3] [&_em]:font-display [&_em]:not-italic"
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":"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;AAED,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACtB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,0EAA0E;IAC1E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,uBAAuB,CAAC,CAAC;IAC7D,sDAAsD;IACtD,YAAY,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;CAC3D"}
@@ -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