@dreamtree-org/twreact-ui 2.0.8 → 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,380 @@
1
+ 'use client';
2
+ import { objectSpread2 as _objectSpread2, slicedToArray as _slicedToArray } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import React from 'react';
5
+ import { ImageOff, ChevronLeft, ChevronRight, Maximize2 } from 'lucide-react';
6
+ import { cn } from '../../utils/cn.js';
7
+ import { TOUCH_TARGET, PRESS_FEEDBACK } from '../../utils/touchTarget.js';
8
+
9
+ // Internal building blocks of ImageGallery (not exported from the package).
10
+ // Full class literals so Tailwind's JIT sees every one of them.
11
+ var RATIO_CLASSES = {
12
+ "4:3": "aspect-[4/3]",
13
+ "1:1": "aspect-square",
14
+ "3:4": "aspect-[3/4]",
15
+ "16:9": "aspect-video"
16
+ };
17
+ // Mosaic hero: keeps the ratio on narrow screens (one column of tiles), then
18
+ // stretches to fill its 2x2 grid cell from `sm` up.
19
+ var HERO_RATIO_CLASSES = {
20
+ "4:3": "aspect-[4/3] sm:aspect-auto",
21
+ "1:1": "aspect-square sm:aspect-auto",
22
+ "3:4": "aspect-[3/4] sm:aspect-auto",
23
+ "16:9": "aspect-video sm:aspect-auto"
24
+ };
25
+ var ratioClass = function ratioClass(ratio) {
26
+ return RATIO_CLASSES[ratio] || RATIO_CLASSES["4:3"];
27
+ };
28
+ var heroRatioClass = function heroRatioClass(ratio) {
29
+ return HERO_RATIO_CLASSES[ratio] || HERO_RATIO_CLASSES["4:3"];
30
+ };
31
+ // Horizontal travel (px) a pointer must cover before a drag counts as a swipe.
32
+ var SWIPE_THRESHOLD = 40;
33
+ // Mosaic shows the hero plus up to four tiles; the last tile carries "+N".
34
+ var MOSAIC_MAX_TILES = 5;
35
+ var withAlt = function withAlt(text, alt) {
36
+ return alt ? "".concat(text, ": ").concat(alt) : text;
37
+ };
38
+ var slideLabel = function slideLabel(images, i) {
39
+ var _images$i;
40
+ return withAlt("Image ".concat(i + 1, " of ").concat(images.length), (_images$i = images[i]) === null || _images$i === void 0 ? void 0 : _images$i.alt);
41
+ };
42
+ var thumbLabel = function thumbLabel(images, i) {
43
+ var _images$i2;
44
+ return withAlt("Show image ".concat(i + 1, " of ").concat(images.length), (_images$i2 = images[i]) === null || _images$i2 === void 0 ? void 0 : _images$i2.alt);
45
+ };
46
+ var FOCUS_RING = "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500";
47
+ var OVERLAY_CONTROL = "absolute inline-flex items-center justify-center rounded-full border border-line bg-surface/80 p-2 text-foreground shadow-md backdrop-blur-md transition-colors motion-reduce:transition-none hover:bg-surface";
48
+ function PlaceholderBox(_ref) {
49
+ var children = _ref.children,
50
+ className = _ref.className;
51
+ return jsx("div", {
52
+ className: cn("flex h-full w-full items-center justify-center bg-muted/10 text-muted", className),
53
+ "data-gallery-placeholder": "",
54
+ children: children !== null && children !== void 0 ? children : jsx(ImageOff, {
55
+ className: "h-8 w-8",
56
+ "aria-hidden": "true"
57
+ })
58
+ });
59
+ }
60
+ // <img> that swaps itself for the placeholder when the source fails to load.
61
+ function GalleryImg(_ref2) {
62
+ var src = _ref2.src,
63
+ alt = _ref2.alt,
64
+ placeholder = _ref2.placeholder,
65
+ className = _ref2.className,
66
+ style = _ref2.style;
67
+ var _React$useState = React.useState(false),
68
+ _React$useState2 = _slicedToArray(_React$useState, 2),
69
+ failed = _React$useState2[0],
70
+ setFailed = _React$useState2[1];
71
+ React.useEffect(function () {
72
+ return setFailed(false);
73
+ }, [src]);
74
+ if (!src || failed) return jsx(PlaceholderBox, {
75
+ children: placeholder
76
+ });
77
+ return jsx("img", {
78
+ src: src,
79
+ alt: alt,
80
+ draggable: false,
81
+ onError: function onError() {
82
+ return setFailed(true);
83
+ },
84
+ className: className,
85
+ style: style
86
+ });
87
+ }
88
+ // Pointer-event swipe detection. `consumeSwipe()` reports (once) whether the
89
+ // last gesture was a swipe, so the click that follows it can be ignored.
90
+ function useSwipe(_ref3) {
91
+ var onPrev = _ref3.onPrev,
92
+ onNext = _ref3.onNext,
93
+ disabled = _ref3.disabled;
94
+ var start = React.useRef(null);
95
+ var swiped = React.useRef(false);
96
+ var onPointerDown = function onPointerDown(e) {
97
+ swiped.current = false;
98
+ start.current = disabled ? null : {
99
+ x: e.clientX,
100
+ y: e.clientY
101
+ };
102
+ };
103
+ var onPointerUp = function onPointerUp(e) {
104
+ var from = start.current;
105
+ start.current = null;
106
+ if (!from) return;
107
+ var dx = e.clientX - from.x;
108
+ var dy = e.clientY - from.y;
109
+ if (Math.abs(dx) < SWIPE_THRESHOLD || Math.abs(dx) <= Math.abs(dy)) return;
110
+ swiped.current = true;
111
+ if (dx < 0) onNext();else onPrev();
112
+ };
113
+ var onPointerCancel = function onPointerCancel() {
114
+ start.current = null;
115
+ };
116
+ var consumeSwipe = function consumeSwipe() {
117
+ var was = swiped.current;
118
+ swiped.current = false;
119
+ return was;
120
+ };
121
+ return {
122
+ handlers: {
123
+ onPointerDown: onPointerDown,
124
+ onPointerUp: onPointerUp,
125
+ onPointerCancel: onPointerCancel
126
+ },
127
+ consumeSwipe: consumeSwipe
128
+ };
129
+ }
130
+ // Roving focus across the `[data-gallery-thumb]` buttons of a list.
131
+ var ROVING_KEYS = {
132
+ ArrowRight: 1,
133
+ ArrowDown: 1,
134
+ ArrowLeft: -1,
135
+ ArrowUp: -1
136
+ };
137
+ function handleRovingKeyDown(e) {
138
+ var items = Array.from(e.currentTarget.querySelectorAll("[data-gallery-thumb]"));
139
+ var from = items.indexOf(document.activeElement);
140
+ if (from < 0) return;
141
+ var to;
142
+ if (e.key in ROVING_KEYS) to = (from + ROVING_KEYS[e.key] + items.length) % items.length;else if (e.key === "Home") to = 0;else if (e.key === "End") to = items.length - 1;else return;
143
+ e.preventDefault();
144
+ e.stopPropagation();
145
+ items[to].focus();
146
+ }
147
+ function Thumbnails(_ref4) {
148
+ var images = _ref4.images,
149
+ index = _ref4.index,
150
+ onSelect = _ref4.onSelect,
151
+ side = _ref4.side,
152
+ placeholder = _ref4.placeholder;
153
+ return jsx("div", {
154
+ role: "group",
155
+ "aria-label": "Thumbnails",
156
+ "data-gallery-thumbs": "",
157
+ onKeyDown: handleRovingKeyDown,
158
+ className: cn("flex gap-2 overflow-x-auto p-1 [scrollbar-width:thin]", side && "sm:absolute sm:inset-0 sm:flex-col sm:overflow-x-hidden sm:overflow-y-auto"),
159
+ children: images.map(function (img, i) {
160
+ var active = i === index;
161
+ return jsx("button", {
162
+ type: "button",
163
+ "data-gallery-thumb": "",
164
+ tabIndex: active ? 0 : -1,
165
+ "aria-current": active ? "true" : undefined,
166
+ "aria-label": thumbLabel(images, i),
167
+ onClick: function onClick() {
168
+ return onSelect(i);
169
+ },
170
+ className: cn("relative h-16 w-16 shrink-0 overflow-hidden rounded-md border-2 bg-surface", FOCUS_RING, PRESS_FEEDBACK, active ? "border-primary-600" : "border-transparent opacity-70 hover:opacity-100"),
171
+ children: jsx(GalleryImg, {
172
+ src: img.src,
173
+ alt: "",
174
+ placeholder: placeholder,
175
+ className: "h-full w-full object-cover"
176
+ })
177
+ }, i);
178
+ })
179
+ });
180
+ }
181
+ function Mosaic(_ref5) {
182
+ var images = _ref5.images,
183
+ index = _ref5.index,
184
+ onActivate = _ref5.onActivate,
185
+ ratio = _ref5.ratio,
186
+ placeholder = _ref5.placeholder;
187
+ var shown = images.slice(0, MOSAIC_MAX_TILES);
188
+ var hidden = images.length - shown.length;
189
+ return jsx("div", {
190
+ role: "group",
191
+ "aria-label": "Photos",
192
+ "data-gallery-mosaic": "",
193
+ onKeyDown: handleRovingKeyDown,
194
+ className: "grid grid-cols-2 gap-2 sm:grid-cols-4",
195
+ children: shown.map(function (img, i) {
196
+ var hero = i === 0;
197
+ var more = i === shown.length - 1 && hidden > 0;
198
+ return jsxs("button", {
199
+ type: "button",
200
+ "data-gallery-thumb": "",
201
+ tabIndex: i === Math.min(index, shown.length - 1) ? 0 : -1,
202
+ "aria-current": i === index ? "true" : undefined,
203
+ "aria-label": more ? "".concat(thumbLabel(images, i), " (").concat(hidden, " more)") : thumbLabel(images, i),
204
+ onClick: function onClick() {
205
+ return onActivate(i);
206
+ },
207
+ className: cn("group relative overflow-hidden rounded-lg border border-line bg-surface", FOCUS_RING, hero ? cn("col-span-2 sm:row-span-2", heroRatioClass(ratio)) : ratioClass(ratio)),
208
+ children: [jsx(GalleryImg, {
209
+ src: img.src,
210
+ alt: "",
211
+ placeholder: placeholder,
212
+ className: "h-full w-full object-cover transition-transform duration-base ease-standard group-hover:scale-105 motion-reduce:transition-none motion-reduce:group-hover:scale-100"
213
+ }), more && jsxs("span", {
214
+ "aria-hidden": "true",
215
+ className: "absolute inset-0 flex items-center justify-center bg-black/50 text-lg font-semibold text-white",
216
+ children: ["+", hidden]
217
+ })]
218
+ }, i);
219
+ })
220
+ });
221
+ }
222
+ // Click-to-zoom state: a 2x scale whose origin follows the pointer.
223
+ function useZoom(index) {
224
+ var _React$useState3 = React.useState(false),
225
+ _React$useState4 = _slicedToArray(_React$useState3, 2),
226
+ zoomed = _React$useState4[0],
227
+ setZoomed = _React$useState4[1];
228
+ var _React$useState5 = React.useState("50% 50%"),
229
+ _React$useState6 = _slicedToArray(_React$useState5, 2),
230
+ origin = _React$useState6[0],
231
+ setOrigin = _React$useState6[1];
232
+ React.useEffect(function () {
233
+ return setZoomed(false);
234
+ }, [index]);
235
+ var follow = function follow(e) {
236
+ var rect = e.currentTarget.getBoundingClientRect();
237
+ if (!rect.width || !rect.height) return;
238
+ var x = (e.clientX - rect.left) / rect.width * 100;
239
+ var y = (e.clientY - rect.top) / rect.height * 100;
240
+ setOrigin("".concat(x, "% ").concat(y, "%"));
241
+ };
242
+ return {
243
+ zoomed: zoomed,
244
+ setZoomed: setZoomed,
245
+ origin: origin,
246
+ follow: follow
247
+ };
248
+ }
249
+ function Viewer(_ref6) {
250
+ var images = _ref6.images,
251
+ index = _ref6.index,
252
+ onSelect = _ref6.onSelect,
253
+ ratio = _ref6.ratio,
254
+ zoom = _ref6.zoom,
255
+ showCounter = _ref6.showCounter,
256
+ placeholder = _ref6.placeholder,
257
+ label = _ref6.label,
258
+ onExpand = _ref6.onExpand,
259
+ _ref6$fit = _ref6.fit,
260
+ fit = _ref6$fit === void 0 ? "cover" : _ref6$fit;
261
+ var count = images.length;
262
+ var image = images[index];
263
+ var step = function step(delta) {
264
+ return onSelect(index + delta);
265
+ };
266
+ var _useZoom = useZoom(index),
267
+ zoomed = _useZoom.zoomed,
268
+ setZoomed = _useZoom.setZoomed,
269
+ origin = _useZoom.origin,
270
+ follow = _useZoom.follow;
271
+ var swipe = useSwipe({
272
+ onPrev: function onPrev() {
273
+ return step(-1);
274
+ },
275
+ onNext: function onNext() {
276
+ return step(1);
277
+ },
278
+ disabled: zoomed || count < 2
279
+ });
280
+ var onKeyDown = function onKeyDown(e) {
281
+ if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
282
+ e.preventDefault();
283
+ step(e.key === "ArrowLeft" ? -1 : 1);
284
+ } else if (e.key === "Escape" && zoomed) {
285
+ e.stopPropagation();
286
+ setZoomed(false);
287
+ }
288
+ };
289
+ var imgClass = cn("h-full w-full transition-transform duration-base ease-standard motion-reduce:transition-none", fit === "contain" ? "object-contain" : "object-cover", zoomed && "scale-[2]");
290
+ var img = jsx(GalleryImg, {
291
+ src: image.src,
292
+ alt: image.alt || "",
293
+ placeholder: placeholder,
294
+ className: imgClass,
295
+ style: zoom ? {
296
+ transformOrigin: origin
297
+ } : undefined
298
+ }, image.src);
299
+ return jsxs("div", {
300
+ role: "region",
301
+ "aria-roledescription": "carousel",
302
+ "aria-label": label,
303
+ tabIndex: 0,
304
+ onKeyDown: onKeyDown,
305
+ "data-gallery-viewer": "",
306
+ className: cn("min-w-0 rounded-xl", FOCUS_RING),
307
+ children: [jsxs("div", _objectSpread2(_objectSpread2({}, swipe.handlers), {}, {
308
+ className: cn("relative touch-pan-y select-none overflow-hidden rounded-xl border border-line bg-surface", ratioClass(ratio)),
309
+ children: [jsx("div", {
310
+ role: "group",
311
+ "aria-roledescription": "slide",
312
+ "aria-label": "".concat(index + 1, " of ").concat(count),
313
+ className: "h-full w-full motion-safe:animate-fade-in",
314
+ children: zoom ? jsx("button", {
315
+ type: "button",
316
+ "aria-label": zoomed ? "Zoom out" : "Zoom in",
317
+ "aria-pressed": zoomed,
318
+ onClick: function onClick() {
319
+ return !swipe.consumeSwipe() && setZoomed(function (z) {
320
+ return !z;
321
+ });
322
+ },
323
+ onPointerMove: zoomed ? follow : undefined,
324
+ className: cn("block h-full w-full overflow-hidden", zoomed ? "cursor-zoom-out" : "cursor-zoom-in", "focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-500"),
325
+ children: img
326
+ }) : img
327
+ }, index), count > 1 && jsxs(Fragment, {
328
+ children: [jsx("button", {
329
+ type: "button",
330
+ onClick: function onClick() {
331
+ return step(-1);
332
+ },
333
+ "aria-label": "Previous image",
334
+ className: cn(OVERLAY_CONTROL, TOUCH_TARGET, FOCUS_RING, "left-3 top-1/2 -translate-y-1/2"),
335
+ children: jsx(ChevronLeft, {
336
+ className: "h-5 w-5",
337
+ "aria-hidden": "true"
338
+ })
339
+ }), jsx("button", {
340
+ type: "button",
341
+ onClick: function onClick() {
342
+ return step(1);
343
+ },
344
+ "aria-label": "Next image",
345
+ className: cn(OVERLAY_CONTROL, TOUCH_TARGET, FOCUS_RING, "right-3 top-1/2 -translate-y-1/2"),
346
+ children: jsx(ChevronRight, {
347
+ className: "h-5 w-5",
348
+ "aria-hidden": "true"
349
+ })
350
+ })]
351
+ }), onExpand && jsx("button", {
352
+ type: "button",
353
+ onClick: onExpand,
354
+ "aria-label": "Open full-screen view",
355
+ "aria-haspopup": "dialog",
356
+ className: cn(OVERLAY_CONTROL, TOUCH_TARGET, FOCUS_RING, "right-3 top-3"),
357
+ children: jsx(Maximize2, {
358
+ className: "h-4 w-4",
359
+ "aria-hidden": "true"
360
+ })
361
+ }), showCounter && jsxs("span", {
362
+ "aria-hidden": "true",
363
+ "data-gallery-counter": "",
364
+ className: "absolute bottom-3 right-3 rounded-full border border-line bg-surface/80 px-2 py-0.5 text-xs font-medium tabular-nums text-foreground backdrop-blur-md",
365
+ children: [index + 1, "/", count]
366
+ })]
367
+ })), image.caption && jsx("p", {
368
+ className: "mt-2 text-sm text-muted",
369
+ children: image.caption
370
+ }), jsx("div", {
371
+ className: "sr-only",
372
+ "aria-live": "polite",
373
+ "aria-atomic": "true",
374
+ "data-gallery-status": "",
375
+ children: slideLabel(images, index)
376
+ })]
377
+ });
378
+ }
379
+
380
+ export { GalleryImg, MOSAIC_MAX_TILES, Mosaic, PlaceholderBox, SWIPE_THRESHOLD, Thumbnails, Viewer, handleRovingKeyDown, ratioClass, slideLabel, useSwipe };
package/dist/esm/index.js CHANGED
@@ -37,6 +37,8 @@ export { default as Slider } from './components/core/Slider.js';
37
37
  export { default as Popover } from './components/core/Popover.js';
38
38
  export { default as SegmentedControl } from './components/core/SegmentedControl.js';
39
39
  export { default as SearchField } from './components/core/SearchField.js';
40
+ export { default as QuantityStepper } from './components/core/QuantityStepper.js';
41
+ export { default as OptionCard, OptionCardGroup } from './components/core/OptionCard.js';
40
42
  export { default as Sidebar } from './components/navigation/Sidebar.js';
41
43
  export { Navbar } from './components/navigation/Navbar.js';
42
44
  export { default as FootNav } from './components/navigation/FootNav.js';
@@ -59,10 +61,12 @@ export { Carousel } from './components/utility/Carousel.js';
59
61
  export { default as Sortable } from './components/utility/Sortable.js';
60
62
  export { default as SortableGroup } from './components/utility/SortableGroup.js';
61
63
  export { default as PullToRefresh } from './components/utility/PullToRefresh.js';
64
+ export { ImageGallery } from './components/utility/ImageGallery.js';
62
65
  export { default as AppShell } from './components/blocks/AppShell.js';
63
66
  export { default as PageHeader } from './components/blocks/PageHeader.js';
64
67
  export { default as StatCard } from './components/blocks/StatCard.js';
65
68
  export { default as StatusPill } from './components/blocks/StatusPill.js';
69
+ export { default as ProductCard } from './components/blocks/ProductCard.js';
66
70
  export { default as EmptyState } from './components/blocks/EmptyState.js';
67
71
  export { default as ErrorState } from './components/blocks/ErrorState.js';
68
72
  export { default as LoadingState } from './components/blocks/LoadingState.js';