@dreamtree-org/twreact-ui 2.0.9 → 2.1.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 (50) hide show
  1. package/DESIGN.md +3 -0
  2. package/README.md +9 -4
  3. package/ai-skills/dreamtree-ui.md +12 -8
  4. package/dist/ai-schema.json +314 -70
  5. package/dist/components/blocks/AppShell.d.ts.map +1 -1
  6. package/dist/components/blocks/ProductCard.d.ts +4 -0
  7. package/dist/components/blocks/ProductCard.d.ts.map +1 -0
  8. package/dist/components/blocks/ProductCardParts.d.ts +19 -0
  9. package/dist/components/blocks/ProductCardParts.d.ts.map +1 -0
  10. package/dist/components/blocks/StatCard.d.ts.map +1 -1
  11. package/dist/components/core/Button.d.ts.map +1 -1
  12. package/dist/components/core/OptionCard.d.ts +14 -0
  13. package/dist/components/core/OptionCard.d.ts.map +1 -0
  14. package/dist/components/core/OptionCardGroup.d.ts +2 -0
  15. package/dist/components/core/OptionCardGroup.d.ts.map +1 -0
  16. package/dist/components/core/QuantityStepper.d.ts +8 -0
  17. package/dist/components/core/QuantityStepper.d.ts.map +1 -0
  18. package/dist/components/utility/ImageGallery.d.ts +4 -0
  19. package/dist/components/utility/ImageGallery.d.ts.map +1 -0
  20. package/dist/components/utility/imageGalleryParts.d.ts +55 -0
  21. package/dist/components/utility/imageGalleryParts.d.ts.map +1 -0
  22. package/dist/esm/components/blocks/AppShell.js +17 -6
  23. package/dist/esm/components/blocks/ProductCard.js +122 -0
  24. package/dist/esm/components/blocks/ProductCardParts.js +128 -0
  25. package/dist/esm/components/blocks/StatCard.js +20 -5
  26. package/dist/esm/components/core/Button.js +17 -2
  27. package/dist/esm/components/core/OptionCard.js +340 -0
  28. package/dist/esm/components/core/QuantityStepper.js +194 -0
  29. package/dist/esm/components/utility/ImageGallery.js +150 -0
  30. package/dist/esm/components/utility/imageGalleryParts.js +380 -0
  31. package/dist/esm/index.js +4 -0
  32. package/dist/index.css +1 -1
  33. package/dist/index.d.ts +6 -1
  34. package/dist/index.esm.css +1 -1
  35. package/dist/index.esm.js +1843 -502
  36. package/dist/index.js +1847 -501
  37. package/dist/tokens/tailwind3-preset.cjs +4 -1
  38. package/dist/tokens/theme.css +3 -0
  39. package/dist/tokens/tokens.json +7 -0
  40. package/dist/tokens/vars.css +2 -0
  41. package/dist/types/blocks.d.ts +61 -1
  42. package/dist/types/blocks.d.ts.map +1 -1
  43. package/dist/types/index.d.ts +102 -1
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/doc/README.md +5 -1
  46. package/mcp/catalog.snapshot.json +479 -8
  47. package/package.json +1 -1
  48. package/tokens/brand.schema.json +22 -0
  49. package/tokens/lib.cjs +26 -0
  50. package/tokens/tokens.json +2 -1
@@ -0,0 +1,340 @@
1
+ 'use client';
2
+ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2, toConsumableArray as _toConsumableArray } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+ import { forwardRef, useMemo, useRef, useState, useId, useCallback, useContext, useLayoutEffect, useEffect, createContext } from 'react';
5
+ import { Check } from 'lucide-react';
6
+ import { cn } from '../../utils/cn.js';
7
+ import { setRefs } from '../../utils/position.js';
8
+ import { useControllableState } from '../../hooks/useControllableState.js';
9
+ import { PRESS_FEEDBACK } from '../../utils/touchTarget.js';
10
+
11
+ var _excluded = ["value", "defaultValue", "onChange", "multiple", "columns", "label", "name", "disabled", "className", "children"],
12
+ _excluded2 = ["value", "title", "description", "media", "badge", "selected", "onSelect", "disabled", "className", "children", "onClick", "onKeyDown", "onFocus"];
13
+ // useLayoutEffect warns during SSR; fall back to useEffect on the server.
14
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
15
+ var OptionCardGroupContext = /*#__PURE__*/createContext(null);
16
+ // One column on phones, growing with the breakpoint up to `columns`.
17
+ var COLUMN_CLASSES = {
18
+ 1: "grid-cols-1",
19
+ 2: "grid-cols-1 sm:grid-cols-2",
20
+ 3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
21
+ 4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"
22
+ };
23
+ var NEXT_KEYS = {
24
+ ArrowRight: 1,
25
+ ArrowDown: 1,
26
+ ArrowLeft: -1,
27
+ ArrowUp: -1
28
+ };
29
+ function isRtl(el) {
30
+ var dirEl = el.closest("[dir]");
31
+ if (dirEl) return dirEl.getAttribute("dir") === "rtl";
32
+ return typeof window !== "undefined" && window.getComputedStyle(el).direction === "rtl";
33
+ }
34
+ function toArray(value) {
35
+ if (Array.isArray(value)) return value;
36
+ return value === undefined || value === null ? [] : [value];
37
+ }
38
+ /**
39
+ * OptionCardGroup: lays out `OptionCard`s in a responsive grid and gives them
40
+ * radiogroup semantics (or checkbox-group semantics with `multiple`).
41
+ */
42
+ var OptionCardGroup = /*#__PURE__*/forwardRef(function OptionCardGroup(_ref, _ref2) {
43
+ var valueProp = _ref.value,
44
+ defaultValue = _ref.defaultValue,
45
+ onChange = _ref.onChange,
46
+ _ref$multiple = _ref.multiple,
47
+ multiple = _ref$multiple === void 0 ? false : _ref$multiple,
48
+ _ref$columns = _ref.columns,
49
+ columns = _ref$columns === void 0 ? 3 : _ref$columns,
50
+ label = _ref.label,
51
+ name = _ref.name,
52
+ _ref$disabled = _ref.disabled,
53
+ disabled = _ref$disabled === void 0 ? false : _ref$disabled,
54
+ className = _ref.className,
55
+ children = _ref.children,
56
+ rest = _objectWithoutProperties(_ref, _excluded);
57
+ var _useControllableState = useControllableState({
58
+ value: valueProp,
59
+ defaultValue: defaultValue,
60
+ onChange: onChange
61
+ }),
62
+ _useControllableState2 = _slicedToArray(_useControllableState, 2),
63
+ value = _useControllableState2[0],
64
+ setValue = _useControllableState2[1];
65
+ var selected = useMemo(function () {
66
+ return multiple ? toArray(value) : value;
67
+ }, [multiple, value]);
68
+ var rootRef = useRef(null);
69
+ // DOM node -> { value, disabled } for every card in this group.
70
+ var itemsRef = useRef(new Map());
71
+ var _useState = useState(undefined),
72
+ _useState2 = _slicedToArray(_useState, 2),
73
+ activeValue = _useState2[0],
74
+ setActiveValue = _useState2[1];
75
+ var _useState3 = useState({
76
+ ready: false,
77
+ value: undefined
78
+ }),
79
+ _useState4 = _slicedToArray(_useState3, 2),
80
+ tabStop = _useState4[0],
81
+ setTabStop = _useState4[1];
82
+ var labelId = useId();
83
+ var isSelected = useCallback(function (v) {
84
+ return multiple ? selected.some(function (s) {
85
+ return Object.is(s, v);
86
+ }) : Object.is(selected, v);
87
+ }, [multiple, selected]);
88
+ // Cards in DOM order, excluding cards of nested groups.
89
+ var getItems = useCallback(function () {
90
+ var root = rootRef.current;
91
+ if (!root) return [];
92
+ return Array.from(root.querySelectorAll("[data-option-card]")).filter(function (node) {
93
+ return node.closest("[data-option-card-group]") === root;
94
+ }).map(function (node) {
95
+ return _objectSpread2({
96
+ node: node
97
+ }, itemsRef.current.get(node) || {});
98
+ }).filter(function (item) {
99
+ return item.node && "value" in item;
100
+ });
101
+ }, []);
102
+ // Roving tabindex: the last focused card, else the first selected card, else
103
+ // the first enabled card. Recomputed after every render (cards may change).
104
+ useIsomorphicLayoutEffect(function () {
105
+ var enabled = getItems().filter(function (item) {
106
+ return !item.disabled;
107
+ });
108
+ var pick = enabled.find(function (item) {
109
+ return activeValue !== undefined && Object.is(item.value, activeValue);
110
+ }) || enabled.find(function (item) {
111
+ return isSelected(item.value);
112
+ }) || enabled[0];
113
+ var next = pick ? pick.value : undefined;
114
+ setTabStop(function (prev) {
115
+ return prev.ready && Object.is(prev.value, next) ? prev : {
116
+ ready: true,
117
+ value: next
118
+ };
119
+ });
120
+ });
121
+ var toggle = useCallback(function (v) {
122
+ if (disabled) return;
123
+ if (multiple) {
124
+ var next = isSelected(v) ? selected.filter(function (s) {
125
+ return !Object.is(s, v);
126
+ }) : [].concat(_toConsumableArray(selected), [v]);
127
+ setValue(next);
128
+ } else if (!Object.is(selected, v)) {
129
+ setValue(v);
130
+ }
131
+ }, [disabled, multiple, isSelected, selected, setValue]);
132
+ var onItemKeyDown = useCallback(function (event, v) {
133
+ var key = event.key;
134
+ var items = getItems().filter(function (item) {
135
+ return !item.disabled;
136
+ });
137
+ if (!items.length) return false;
138
+ var index = items.findIndex(function (item) {
139
+ return Object.is(item.value, v);
140
+ });
141
+ var target = -1;
142
+ if (key in NEXT_KEYS) {
143
+ var step = NEXT_KEYS[key];
144
+ if ((key === "ArrowLeft" || key === "ArrowRight") && isRtl(event.currentTarget)) step = -step;
145
+ var from = index < 0 ? step > 0 ? -1 : items.length : index;
146
+ target = ((from + step) % items.length + items.length) % items.length;
147
+ } else if (key === "Home") {
148
+ target = 0;
149
+ } else if (key === "End") {
150
+ target = items.length - 1;
151
+ } else {
152
+ return false;
153
+ }
154
+ event.preventDefault();
155
+ var item = items[target];
156
+ item.node.focus();
157
+ setActiveValue(item.value);
158
+ // Radios: selection follows focus. Checkboxes: arrows only move focus.
159
+ if (!multiple) toggle(item.value);
160
+ return true;
161
+ }, [getItems, multiple, toggle]);
162
+ var ctx = useMemo(function () {
163
+ return {
164
+ multiple: multiple,
165
+ groupDisabled: disabled,
166
+ isSelected: isSelected,
167
+ toggle: toggle,
168
+ itemsRef: itemsRef,
169
+ tabStop: tabStop,
170
+ setActiveValue: setActiveValue,
171
+ onItemKeyDown: onItemKeyDown
172
+ };
173
+ }, [multiple, disabled, isSelected, toggle, tabStop, onItemKeyDown]);
174
+ var hasLabel = label !== undefined && label !== null && label !== false;
175
+ var hiddenValues = multiple ? selected : toArray(selected);
176
+ return jsxs("div", _objectSpread2(_objectSpread2({
177
+ ref: function ref(node) {
178
+ return setRefs(node, rootRef, _ref2);
179
+ },
180
+ role: multiple ? "group" : "radiogroup",
181
+ "aria-labelledby": hasLabel && !rest["aria-label"] ? labelId : undefined,
182
+ "aria-disabled": disabled || undefined,
183
+ "data-option-card-group": "",
184
+ className: cn("flex flex-col gap-2", disabled && "cursor-not-allowed opacity-50", className)
185
+ }, rest), {}, {
186
+ children: [hasLabel && jsx("div", {
187
+ id: labelId,
188
+ className: "text-sm font-medium text-foreground",
189
+ children: label
190
+ }), jsx("div", {
191
+ className: cn("grid gap-3", COLUMN_CLASSES[columns] || COLUMN_CLASSES[3]),
192
+ children: jsx(OptionCardGroupContext.Provider, {
193
+ value: ctx,
194
+ children: children
195
+ })
196
+ }), name && hiddenValues.map(function (v) {
197
+ return jsx("input", {
198
+ type: "hidden",
199
+ name: name,
200
+ value: v !== null && v !== void 0 ? v : ""
201
+ }, String(v));
202
+ })]
203
+ }));
204
+ });
205
+ OptionCardGroup.displayName = "OptionCardGroup";
206
+ /**
207
+ * OptionCard: a selectable choice tile (title, description, optional media and
208
+ * badge). Inside an `OptionCardGroup` it is a radio (or a checkbox with
209
+ * `multiple`); on its own it is a toggle button with `aria-pressed`.
210
+ */
211
+ var OptionCard = /*#__PURE__*/forwardRef(function OptionCard(_ref3, _ref4) {
212
+ var value = _ref3.value,
213
+ title = _ref3.title,
214
+ description = _ref3.description,
215
+ media = _ref3.media,
216
+ badge = _ref3.badge,
217
+ selectedProp = _ref3.selected,
218
+ onSelect = _ref3.onSelect,
219
+ _ref3$disabled = _ref3.disabled,
220
+ disabledProp = _ref3$disabled === void 0 ? false : _ref3$disabled,
221
+ className = _ref3.className,
222
+ children = _ref3.children,
223
+ onClick = _ref3.onClick,
224
+ onKeyDown = _ref3.onKeyDown,
225
+ onFocus = _ref3.onFocus,
226
+ rest = _objectWithoutProperties(_ref3, _excluded2);
227
+ var group = useContext(OptionCardGroupContext);
228
+ var nodeRef = useRef(null);
229
+ var baseId = useId();
230
+ var titleId = "".concat(baseId, "-title");
231
+ var descId = "".concat(baseId, "-desc");
232
+ var disabled = Boolean(disabledProp) || Boolean(group === null || group === void 0 ? void 0 : group.groupDisabled);
233
+ var selected = group ? group.isSelected(value) : Boolean(selectedProp);
234
+ // Register this card with its group so keyboard navigation can map DOM order to values.
235
+ useIsomorphicLayoutEffect(function () {
236
+ if (!group) return undefined;
237
+ var node = nodeRef.current;
238
+ var items = group.itemsRef.current;
239
+ if (node) items.set(node, {
240
+ value: value,
241
+ disabled: disabled
242
+ });
243
+ return function () {
244
+ if (node) items["delete"](node);
245
+ };
246
+ });
247
+ var role = "button";
248
+ var tabIndex = disabled ? -1 : 0;
249
+ if (group) {
250
+ role = group.multiple ? "checkbox" : "radio";
251
+ if (disabled) tabIndex = -1;else if (group.tabStop.ready) tabIndex = Object.is(group.tabStop.value, value) ? 0 : -1;
252
+ // Before the first layout pass (SSR): selected cards are the tab stop.
253
+ else tabIndex = selected ? 0 : -1;
254
+ }
255
+ var activate = function activate(event) {
256
+ if (disabled) return;
257
+ if (group) {
258
+ group.setActiveValue(value);
259
+ group.toggle(value);
260
+ }
261
+ onSelect === null || onSelect === void 0 || onSelect(value, event);
262
+ };
263
+ var handleClick = function handleClick(event) {
264
+ onClick === null || onClick === void 0 || onClick(event);
265
+ if (event.defaultPrevented) return;
266
+ activate(event);
267
+ };
268
+ var handleKeyDown = function handleKeyDown(event) {
269
+ onKeyDown === null || onKeyDown === void 0 || onKeyDown(event);
270
+ if (event.defaultPrevented || event.target !== event.currentTarget || disabled) return;
271
+ if (event.key === " " || event.key === "Enter") {
272
+ event.preventDefault();
273
+ activate(event);
274
+ return;
275
+ }
276
+ if (group) group.onItemKeyDown(event, value);
277
+ };
278
+ var handleFocus = function handleFocus(event) {
279
+ onFocus === null || onFocus === void 0 || onFocus(event);
280
+ if (group && !disabled && event.target === event.currentTarget) group.setActiveValue(value);
281
+ };
282
+ var ariaState = role === "button" ? {
283
+ "aria-pressed": selected
284
+ } : {
285
+ "aria-checked": selected
286
+ };
287
+ var isCheckbox = group === null || group === void 0 ? void 0 : group.multiple;
288
+ return jsxs("div", _objectSpread2(_objectSpread2(_objectSpread2({
289
+ ref: function ref(node) {
290
+ return setRefs(node, nodeRef, _ref4);
291
+ },
292
+ role: role,
293
+ tabIndex: tabIndex
294
+ }, ariaState), {}, {
295
+ "aria-disabled": disabled || undefined,
296
+ "aria-labelledby": title ? titleId : undefined,
297
+ "aria-describedby": description ? descId : undefined,
298
+ "data-option-card": "",
299
+ "data-state": selected ? "checked" : "unchecked",
300
+ "data-disabled": disabled ? "" : undefined,
301
+ onClick: handleClick,
302
+ onKeyDown: handleKeyDown,
303
+ onFocus: handleFocus,
304
+ className: cn("relative flex min-h-11 w-full flex-col gap-3 rounded-card border bg-surface p-4 text-left outline-none select-none", "focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-background", PRESS_FEEDBACK, "transition-[border-color,box-shadow,background-color,transform] duration-fast ease-standard motion-reduce:transition-none", selected ? "border-primary-600 bg-primary-50 shadow-sm ring-1 ring-primary-600 dark:border-primary-400 dark:bg-primary-900/20 dark:ring-primary-400" : "border-line", disabled ? "cursor-not-allowed opacity-50 active:scale-100" : cn("cursor-pointer hover:shadow-md", !selected && "hover:border-primary-300 dark:hover:border-primary-700"), className)
305
+ }, rest), {}, {
306
+ children: [jsx("span", {
307
+ "aria-hidden": "true",
308
+ "data-option-card-indicator": "",
309
+ className: cn("absolute right-3 top-3 inline-flex h-5 w-5 items-center justify-center border", isCheckbox ? "rounded-md" : "rounded-full", "transition-colors duration-fast motion-reduce:transition-none", selected ? "border-primary-600 bg-primary-600 text-white dark:border-primary-500 dark:bg-primary-500" : "border-muted/40 bg-surface"),
310
+ children: selected && jsx(Check, {
311
+ className: "h-3.5 w-3.5",
312
+ strokeWidth: 3
313
+ })
314
+ }), media && jsx("div", {
315
+ className: "overflow-hidden rounded-md",
316
+ children: media
317
+ }), jsxs("div", {
318
+ className: "flex flex-col gap-1 pr-7",
319
+ children: [jsxs("div", {
320
+ className: "flex flex-wrap items-center gap-2",
321
+ children: [title && jsx("span", {
322
+ id: titleId,
323
+ className: "text-sm font-semibold text-foreground",
324
+ children: title
325
+ }), badge !== undefined && badge !== null && badge !== false && jsx("span", {
326
+ className: "inline-flex items-center rounded-full bg-primary-50 px-2 py-0.5 text-xs font-medium text-primary-700 dark:bg-primary-900/40 dark:text-primary-300",
327
+ children: badge
328
+ })]
329
+ }), description && jsx("span", {
330
+ id: descId,
331
+ className: "text-sm text-muted",
332
+ children: description
333
+ })]
334
+ }), children]
335
+ }));
336
+ });
337
+ OptionCard.displayName = "OptionCard";
338
+ OptionCard.Group = OptionCardGroup;
339
+
340
+ export { OptionCardGroup, OptionCard as default };
@@ -0,0 +1,194 @@
1
+ 'use client';
2
+ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+ import { forwardRef, useId } from 'react';
5
+ import { Minus, Loader2, Plus } from 'lucide-react';
6
+ import { cn } from '../../utils/cn.js';
7
+ import { useControllableState } from '../../hooks/useControllableState.js';
8
+ import { PRESS_FEEDBACK, TOUCH_TARGET } from '../../utils/touchTarget.js';
9
+
10
+ var _excluded = ["value", "defaultValue", "onChange", "min", "max", "step", "size", "disabled", "loading", "label", "hideLabel", "formatValue", "decrementLabel", "incrementLabel", "name", "className", "aria-label"];
11
+ // Button is square; the value cell is at least as wide as a button.
12
+ var SIZE_CLASSES = {
13
+ sm: {
14
+ button: "h-8 w-8",
15
+ value: "h-8 min-w-8 px-1 text-sm",
16
+ icon: "h-3.5 w-3.5"
17
+ },
18
+ md: {
19
+ button: "h-10 w-10",
20
+ value: "h-10 min-w-10 px-2 text-sm",
21
+ icon: "h-4 w-4"
22
+ },
23
+ lg: {
24
+ button: "h-12 w-12",
25
+ value: "h-12 min-w-12 px-3 text-base",
26
+ icon: "h-5 w-5"
27
+ }
28
+ };
29
+ // PageUp / PageDown move this many steps.
30
+ var PAGE_STEPS = 10;
31
+ // Decimal places of a number, so 0.1 + 0.2 lands on 0.3 instead of 0.30000000000000004.
32
+ function decimals(n) {
33
+ var s = String(n);
34
+ var i = s.indexOf(".");
35
+ return i < 0 ? 0 : s.length - i - 1;
36
+ }
37
+ function clamp(n, min, max) {
38
+ return Math.min(max, Math.max(min, n));
39
+ }
40
+ /**
41
+ * QuantityStepper: a − / value / + control for cart lines and buy boxes.
42
+ * The value is a `role="spinbutton"`; the buttons step it and disable at the bounds.
43
+ */
44
+ var QuantityStepper = /*#__PURE__*/forwardRef(function QuantityStepper(_ref, ref) {
45
+ var valueProp = _ref.value,
46
+ defaultValue = _ref.defaultValue,
47
+ onChange = _ref.onChange,
48
+ _ref$min = _ref.min,
49
+ min = _ref$min === void 0 ? 0 : _ref$min,
50
+ _ref$max = _ref.max,
51
+ max = _ref$max === void 0 ? Infinity : _ref$max,
52
+ _ref$step = _ref.step,
53
+ step = _ref$step === void 0 ? 1 : _ref$step,
54
+ _ref$size = _ref.size,
55
+ size = _ref$size === void 0 ? "md" : _ref$size,
56
+ _ref$disabled = _ref.disabled,
57
+ disabled = _ref$disabled === void 0 ? false : _ref$disabled,
58
+ _ref$loading = _ref.loading,
59
+ loading = _ref$loading === void 0 ? false : _ref$loading,
60
+ label = _ref.label,
61
+ _ref$hideLabel = _ref.hideLabel,
62
+ hideLabel = _ref$hideLabel === void 0 ? false : _ref$hideLabel,
63
+ formatValue = _ref.formatValue,
64
+ _ref$decrementLabel = _ref.decrementLabel,
65
+ decrementLabel = _ref$decrementLabel === void 0 ? "Decrease quantity" : _ref$decrementLabel,
66
+ _ref$incrementLabel = _ref.incrementLabel,
67
+ incrementLabel = _ref$incrementLabel === void 0 ? "Increase quantity" : _ref$incrementLabel,
68
+ name = _ref.name,
69
+ className = _ref.className,
70
+ ariaLabel = _ref["aria-label"],
71
+ rest = _objectWithoutProperties(_ref, _excluded);
72
+ var labelId = "quantity-stepper-label-".concat(useId());
73
+ var safeStep = step > 0 ? step : 1;
74
+ var precision = Math.max(decimals(safeStep), decimals(Number.isFinite(min) ? min : 0));
75
+ var initial = defaultValue !== null && defaultValue !== void 0 ? defaultValue : Number.isFinite(min) ? min : 0;
76
+ var _useControllableState = useControllableState({
77
+ value: valueProp,
78
+ defaultValue: initial,
79
+ onChange: onChange
80
+ }),
81
+ _useControllableState2 = _slicedToArray(_useControllableState, 2),
82
+ rawValue = _useControllableState2[0],
83
+ setValue = _useControllableState2[1];
84
+ var value = clamp(Number(rawValue) || 0, min, max);
85
+ var inactive = disabled || loading;
86
+ var atMin = value <= min;
87
+ var atMax = value >= max;
88
+ var commit = function commit(next) {
89
+ if (inactive) return;
90
+ var rounded = Number(clamp(next, min, max).toFixed(precision));
91
+ if (rounded !== value) setValue(rounded);
92
+ };
93
+ var onKeyDown = function onKeyDown(event) {
94
+ var next;
95
+ switch (event.key) {
96
+ case "ArrowUp":
97
+ next = value + safeStep;
98
+ break;
99
+ case "ArrowDown":
100
+ next = value - safeStep;
101
+ break;
102
+ case "PageUp":
103
+ next = value + safeStep * PAGE_STEPS;
104
+ break;
105
+ case "PageDown":
106
+ next = value - safeStep * PAGE_STEPS;
107
+ break;
108
+ case "Home":
109
+ if (!Number.isFinite(min)) return;
110
+ next = min;
111
+ break;
112
+ case "End":
113
+ if (!Number.isFinite(max)) return;
114
+ next = max;
115
+ break;
116
+ default:
117
+ return;
118
+ }
119
+ event.preventDefault();
120
+ commit(next);
121
+ };
122
+ var sizes = SIZE_CLASSES[size] || SIZE_CLASSES.md;
123
+ var display = formatValue ? formatValue(value) : value;
124
+ var valueText = formatValue && (typeof display === "string" || typeof display === "number") ? String(display) : undefined;
125
+ var buttonClass = cn("inline-flex shrink-0 items-center justify-center text-foreground outline-none select-none", "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-500", "enabled:hover:bg-line disabled:cursor-not-allowed disabled:text-muted", "motion-reduce:transition-none", TOUCH_TARGET, PRESS_FEEDBACK, sizes.button);
126
+ return jsxs("div", _objectSpread2(_objectSpread2({
127
+ ref: ref,
128
+ className: cn("inline-flex flex-col gap-1", disabled && "opacity-50", className)
129
+ }, rest), {}, {
130
+ children: [label && jsx("span", {
131
+ id: labelId,
132
+ className: cn("text-sm font-medium text-foreground", hideLabel && "sr-only"),
133
+ children: label
134
+ }), jsxs("div", {
135
+ className: cn("inline-flex items-stretch overflow-hidden rounded-lg border border-line bg-surface", disabled && "cursor-not-allowed"),
136
+ "aria-busy": loading || undefined,
137
+ children: [jsx("button", {
138
+ type: "button",
139
+ "aria-label": decrementLabel,
140
+ disabled: inactive || atMin,
141
+ tabIndex: -1,
142
+ onClick: function onClick() {
143
+ return commit(value - safeStep);
144
+ },
145
+ className: cn(buttonClass, "rounded-l-lg"),
146
+ children: jsx(Minus, {
147
+ "aria-hidden": "true",
148
+ className: sizes.icon
149
+ })
150
+ }), jsxs("div", {
151
+ role: "spinbutton",
152
+ tabIndex: disabled ? -1 : 0,
153
+ "aria-valuenow": value,
154
+ "aria-valuemin": Number.isFinite(min) ? min : undefined,
155
+ "aria-valuemax": Number.isFinite(max) ? max : undefined,
156
+ "aria-valuetext": valueText,
157
+ "aria-disabled": disabled || undefined,
158
+ "aria-readonly": loading || undefined,
159
+ "aria-labelledby": label ? labelId : undefined,
160
+ "aria-label": label ? undefined : ariaLabel,
161
+ onKeyDown: onKeyDown,
162
+ className: cn("relative inline-flex items-center justify-center border-x border-line font-medium tabular-nums text-foreground outline-none", "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-500", "[@media(pointer:coarse)]:min-h-11", sizes.value),
163
+ children: [jsx("span", {
164
+ className: cn(loading && "invisible"),
165
+ children: display
166
+ }), loading && jsx(Loader2, {
167
+ "aria-hidden": "true",
168
+ "data-quantity-stepper-spinner": "",
169
+ className: cn("absolute animate-spin text-muted motion-reduce:animate-none", sizes.icon)
170
+ })]
171
+ }), jsx("button", {
172
+ type: "button",
173
+ "aria-label": incrementLabel,
174
+ disabled: inactive || atMax,
175
+ tabIndex: -1,
176
+ onClick: function onClick() {
177
+ return commit(value + safeStep);
178
+ },
179
+ className: cn(buttonClass, "rounded-r-lg"),
180
+ children: jsx(Plus, {
181
+ "aria-hidden": "true",
182
+ className: sizes.icon
183
+ })
184
+ })]
185
+ }), name && jsx("input", {
186
+ type: "hidden",
187
+ name: name,
188
+ value: value
189
+ })]
190
+ }));
191
+ });
192
+ QuantityStepper.displayName = "QuantityStepper";
193
+
194
+ export { QuantityStepper as default };
@@ -0,0 +1,150 @@
1
+ 'use client';
2
+ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+ import React from 'react';
5
+ import { cn } from '../../utils/cn.js';
6
+ import { useControllableState } from '../../hooks/useControllableState.js';
7
+ import Dialog from '../feedback/Dialog.js';
8
+ import { PlaceholderBox, ratioClass, Viewer, Mosaic, Thumbnails } from './imageGalleryParts.js';
9
+
10
+ var _excluded = ["images", "layout", "ratio", "index", "defaultIndex", "onIndexChange", "zoom", "lightbox", "placeholder", "showCounter", "className", "aria-label"];
11
+ var clampIndex = function clampIndex(i, count) {
12
+ return Math.min(Math.max(Number(i) || 0, 0), Math.max(count - 1, 0));
13
+ };
14
+ var ImageGallery = /*#__PURE__*/React.forwardRef(function ImageGallery(props, ref) {
15
+ var _props$images = props.images,
16
+ images = _props$images === void 0 ? [] : _props$images,
17
+ _props$layout = props.layout,
18
+ layout = _props$layout === void 0 ? "thumbs-below" : _props$layout,
19
+ _props$ratio = props.ratio,
20
+ ratio = _props$ratio === void 0 ? "4:3" : _props$ratio,
21
+ indexProp = props.index,
22
+ _props$defaultIndex = props.defaultIndex,
23
+ defaultIndex = _props$defaultIndex === void 0 ? 0 : _props$defaultIndex,
24
+ onIndexChange = props.onIndexChange,
25
+ _props$zoom = props.zoom,
26
+ zoom = _props$zoom === void 0 ? false : _props$zoom,
27
+ _props$lightbox = props.lightbox,
28
+ lightbox = _props$lightbox === void 0 ? false : _props$lightbox,
29
+ placeholder = props.placeholder,
30
+ _props$showCounter = props.showCounter,
31
+ showCounter = _props$showCounter === void 0 ? true : _props$showCounter,
32
+ className = props.className,
33
+ _props$ariaLabel = props["aria-label"],
34
+ ariaLabel = _props$ariaLabel === void 0 ? "Image gallery" : _props$ariaLabel,
35
+ rest = _objectWithoutProperties(props, _excluded);
36
+ var list = Array.isArray(images) ? images : [];
37
+ var count = list.length;
38
+ var _useControllableState = useControllableState({
39
+ value: indexProp,
40
+ defaultValue: defaultIndex,
41
+ onChange: onIndexChange
42
+ }),
43
+ _useControllableState2 = _slicedToArray(_useControllableState, 2),
44
+ rawIndex = _useControllableState2[0],
45
+ setIndex = _useControllableState2[1];
46
+ var index = clampIndex(rawIndex, count);
47
+ var _React$useState = React.useState(false),
48
+ _React$useState2 = _slicedToArray(_React$useState, 2),
49
+ open = _React$useState2[0],
50
+ setOpen = _React$useState2[1];
51
+ // Wraps around at both ends; a no-op when the target is already current.
52
+ var select = React.useCallback(function (next) {
53
+ if (count === 0) return;
54
+ var target = (next % count + count) % count;
55
+ if (target !== index) setIndex(target);
56
+ }, [count, index, setIndex]);
57
+ if (count === 0) {
58
+ return jsx("div", _objectSpread2(_objectSpread2({
59
+ ref: ref,
60
+ className: cn("w-full", className),
61
+ "data-layout": layout
62
+ }, rest), {}, {
63
+ children: jsx("div", {
64
+ role: "img",
65
+ "aria-label": "No images",
66
+ className: cn("overflow-hidden rounded-xl border border-line", ratioClass(ratio)),
67
+ children: jsx(PlaceholderBox, {
68
+ children: placeholder
69
+ })
70
+ })
71
+ }));
72
+ }
73
+ var shared = {
74
+ images: list,
75
+ index: index,
76
+ onSelect: select,
77
+ placeholder: placeholder
78
+ };
79
+ var viewer = jsx(Viewer, _objectSpread2(_objectSpread2({}, shared), {}, {
80
+ ratio: ratio,
81
+ zoom: zoom,
82
+ showCounter: showCounter,
83
+ label: ariaLabel,
84
+ onExpand: lightbox ? function () {
85
+ return setOpen(true);
86
+ } : undefined
87
+ }));
88
+ var body;
89
+ if (layout === "mosaic") {
90
+ var activate = function activate(i) {
91
+ select(i);
92
+ if (lightbox) setOpen(true);
93
+ };
94
+ body = jsx(Mosaic, {
95
+ images: list,
96
+ index: index,
97
+ onActivate: activate,
98
+ ratio: ratio,
99
+ placeholder: placeholder
100
+ });
101
+ } else if (layout === "thumbs-side") {
102
+ // Thumbnails sit left of the photo from `sm` up and below it on phones.
103
+ body = jsxs("div", {
104
+ className: "flex flex-col gap-3 sm:flex-row",
105
+ children: [jsx("div", {
106
+ className: "min-w-0 sm:flex-1",
107
+ children: viewer
108
+ }), count > 1 && jsx("div", {
109
+ className: "relative shrink-0 sm:order-first sm:w-20",
110
+ children: jsx(Thumbnails, _objectSpread2(_objectSpread2({}, shared), {}, {
111
+ side: true
112
+ }))
113
+ })]
114
+ });
115
+ } else {
116
+ body = jsxs("div", {
117
+ className: "flex flex-col gap-3",
118
+ children: [viewer, count > 1 && jsx(Thumbnails, _objectSpread2({}, shared))]
119
+ });
120
+ }
121
+ return jsxs("div", _objectSpread2(_objectSpread2({
122
+ ref: ref,
123
+ className: cn("w-full", className),
124
+ "data-layout": layout,
125
+ "aria-label": layout === "mosaic" ? ariaLabel : undefined,
126
+ role: layout === "mosaic" ? "region" : undefined
127
+ }, rest), {}, {
128
+ children: [body, lightbox && jsx(Dialog, {
129
+ isOpen: open,
130
+ onClose: function onClose() {
131
+ return setOpen(false);
132
+ },
133
+ size: "full",
134
+ presentation: "popover",
135
+ title: "Image ".concat(index + 1, " of ").concat(count),
136
+ className: "max-w-5xl",
137
+ "data-gallery-lightbox": "",
138
+ children: jsx(Viewer, _objectSpread2(_objectSpread2({}, shared), {}, {
139
+ ratio: ratio,
140
+ zoom: zoom,
141
+ showCounter: showCounter,
142
+ label: "".concat(ariaLabel, ", full-screen"),
143
+ fit: "contain"
144
+ }))
145
+ })]
146
+ }));
147
+ });
148
+ ImageGallery.displayName = "ImageGallery";
149
+
150
+ export { ImageGallery, ImageGallery as default };