@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.
- package/DESIGN.md +3 -0
- package/README.md +9 -4
- package/ai-skills/dreamtree-ui.md +12 -8
- package/dist/ai-schema.json +314 -70
- package/dist/components/blocks/AppShell.d.ts.map +1 -1
- package/dist/components/blocks/ProductCard.d.ts +4 -0
- package/dist/components/blocks/ProductCard.d.ts.map +1 -0
- package/dist/components/blocks/ProductCardParts.d.ts +19 -0
- package/dist/components/blocks/ProductCardParts.d.ts.map +1 -0
- package/dist/components/blocks/StatCard.d.ts.map +1 -1
- package/dist/components/core/Button.d.ts.map +1 -1
- package/dist/components/core/OptionCard.d.ts +14 -0
- package/dist/components/core/OptionCard.d.ts.map +1 -0
- package/dist/components/core/OptionCardGroup.d.ts +2 -0
- package/dist/components/core/OptionCardGroup.d.ts.map +1 -0
- package/dist/components/core/QuantityStepper.d.ts +8 -0
- package/dist/components/core/QuantityStepper.d.ts.map +1 -0
- package/dist/components/utility/ImageGallery.d.ts +4 -0
- package/dist/components/utility/ImageGallery.d.ts.map +1 -0
- package/dist/components/utility/imageGalleryParts.d.ts +55 -0
- package/dist/components/utility/imageGalleryParts.d.ts.map +1 -0
- package/dist/esm/components/blocks/AppShell.js +17 -6
- package/dist/esm/components/blocks/ProductCard.js +122 -0
- package/dist/esm/components/blocks/ProductCardParts.js +128 -0
- package/dist/esm/components/blocks/StatCard.js +20 -5
- package/dist/esm/components/core/Button.js +17 -2
- package/dist/esm/components/core/OptionCard.js +340 -0
- package/dist/esm/components/core/QuantityStepper.js +194 -0
- package/dist/esm/components/utility/ImageGallery.js +150 -0
- package/dist/esm/components/utility/imageGalleryParts.js +380 -0
- package/dist/esm/index.js +4 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +1843 -502
- package/dist/index.js +1847 -501
- package/dist/tokens/tailwind3-preset.cjs +4 -1
- package/dist/tokens/theme.css +3 -0
- package/dist/tokens/tokens.json +7 -0
- package/dist/tokens/vars.css +2 -0
- package/dist/types/blocks.d.ts +61 -1
- package/dist/types/blocks.d.ts.map +1 -1
- package/dist/types/index.d.ts +102 -1
- package/dist/types/index.d.ts.map +1 -1
- package/doc/README.md +5 -1
- package/mcp/catalog.snapshot.json +479 -8
- package/package.json +1 -1
- package/tokens/brand.schema.json +22 -0
- package/tokens/lib.cjs +26 -0
- package/tokens/tokens.json +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductCard.d.ts","sourceRoot":"","sources":["../../../src/components/blocks/ProductCard.jsx"],"names":[],"mappings":";AAgBA,qFA8FG;kBA9GsC,OAAO"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function defaultPriceFormatter(value: any): any;
|
|
2
|
+
export function formatPrice(value: any, formatter?: (value: any) => any): any;
|
|
3
|
+
export function computeDiscount(price: any, compareAtPrice: any): string | null;
|
|
4
|
+
export function normalizeImages(image: any, images: any, fallbackAlt?: string): any[];
|
|
5
|
+
export function ProductMedia({ images, ratio, hover, placeholder, row, children }: {
|
|
6
|
+
images: any;
|
|
7
|
+
ratio?: string | undefined;
|
|
8
|
+
hover?: string | undefined;
|
|
9
|
+
placeholder: any;
|
|
10
|
+
row: any;
|
|
11
|
+
children: any;
|
|
12
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export function ProductPrice({ price, compareAtPrice, discountLabel, priceFormatter }: {
|
|
14
|
+
price: any;
|
|
15
|
+
compareAtPrice: any;
|
|
16
|
+
discountLabel: any;
|
|
17
|
+
priceFormatter: any;
|
|
18
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
19
|
+
//# sourceMappingURL=ProductCardParts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductCardParts.d.ts","sourceRoot":"","sources":["../../../src/components/blocks/ProductCardParts.jsx"],"names":[],"mappings":"AASO,uDAGN;AAGM,8EACyE;AAGzE,gFAKN;AAGM,sFAGN;AAIM;;;;;;;4CAgDN;AAEM;;;;;mDAuBN"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatCard.d.ts","sourceRoot":"","sources":["../../../src/components/blocks/StatCard.jsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"StatCard.d.ts","sourceRoot":"","sources":["../../../src/components/blocks/StatCard.jsx"],"names":[],"mappings":";AA2BA,kFA6CG;kBAxEe,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../src/components/core/Button.jsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../src/components/core/Button.jsx"],"names":[],"mappings":";AAsBA,gFAwGG;kBA9H+B,OAAO"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OptionCardGroup: lays out `OptionCard`s in a responsive grid and gives them
|
|
3
|
+
* radiogroup semantics (or checkbox-group semantics with `multiple`).
|
|
4
|
+
*/
|
|
5
|
+
export const OptionCardGroup: React.ForwardRefExoticComponent<React.RefAttributes<any>>;
|
|
6
|
+
export default OptionCard;
|
|
7
|
+
import React from "react";
|
|
8
|
+
/**
|
|
9
|
+
* OptionCard: a selectable choice tile (title, description, optional media and
|
|
10
|
+
* badge). Inside an `OptionCardGroup` it is a radio (or a checkbox with
|
|
11
|
+
* `multiple`); on its own it is a toggle button with `aria-pressed`.
|
|
12
|
+
*/
|
|
13
|
+
declare const OptionCard: React.ForwardRefExoticComponent<React.RefAttributes<any>>;
|
|
14
|
+
//# sourceMappingURL=OptionCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OptionCard.d.ts","sourceRoot":"","sources":["../../../src/components/core/OptionCard.jsx"],"names":[],"mappings":"AA4CA;;;GAGG;AACH,wFAsIG;;kBA3KI,OAAO;AA+Kd;;;;GAIG;AACH,oFAsJG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OptionCardGroup.d.ts","sourceRoot":"","sources":["../../../src/components/core/OptionCardGroup.jsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export default QuantityStepper;
|
|
2
|
+
/**
|
|
3
|
+
* QuantityStepper: a − / value / + control for cart lines and buy boxes.
|
|
4
|
+
* The value is a `role="spinbutton"`; the buttons step it and disable at the bounds.
|
|
5
|
+
*/
|
|
6
|
+
declare const QuantityStepper: React.ForwardRefExoticComponent<React.RefAttributes<any>>;
|
|
7
|
+
import React from "react";
|
|
8
|
+
//# sourceMappingURL=QuantityStepper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QuantityStepper.d.ts","sourceRoot":"","sources":["../../../src/components/core/QuantityStepper.jsx"],"names":[],"mappings":";AA2BA;;;GAGG;AACH,yFA2JG;kBA1LsC,OAAO"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageGallery.d.ts","sourceRoot":"","sources":["../../../src/components/utility/ImageGallery.jsx"],"names":[],"mappings":"AAQA,qFAuHE;;kBA/HgB,OAAO"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function PlaceholderBox({ children, className }: {
|
|
2
|
+
children: any;
|
|
3
|
+
className: any;
|
|
4
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
export function GalleryImg({ src, alt, placeholder, className, style }: {
|
|
6
|
+
src: any;
|
|
7
|
+
alt: any;
|
|
8
|
+
placeholder: any;
|
|
9
|
+
className: any;
|
|
10
|
+
style: any;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export function useSwipe({ onPrev, onNext, disabled }: {
|
|
13
|
+
onPrev: any;
|
|
14
|
+
onNext: any;
|
|
15
|
+
disabled: any;
|
|
16
|
+
}): {
|
|
17
|
+
handlers: {
|
|
18
|
+
onPointerDown: (e: any) => void;
|
|
19
|
+
onPointerUp: (e: any) => void;
|
|
20
|
+
onPointerCancel: () => void;
|
|
21
|
+
};
|
|
22
|
+
consumeSwipe: () => boolean;
|
|
23
|
+
};
|
|
24
|
+
export function handleRovingKeyDown(e: any): void;
|
|
25
|
+
export function Thumbnails({ images, index, onSelect, side, placeholder }: {
|
|
26
|
+
images: any;
|
|
27
|
+
index: any;
|
|
28
|
+
onSelect: any;
|
|
29
|
+
side: any;
|
|
30
|
+
placeholder: any;
|
|
31
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
export function Mosaic({ images, index, onActivate, ratio, placeholder }: {
|
|
33
|
+
images: any;
|
|
34
|
+
index: any;
|
|
35
|
+
onActivate: any;
|
|
36
|
+
ratio: any;
|
|
37
|
+
placeholder: any;
|
|
38
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
export function Viewer({ images, index, onSelect, ratio, zoom, showCounter, placeholder, label, onExpand, fit }: {
|
|
40
|
+
images: any;
|
|
41
|
+
index: any;
|
|
42
|
+
onSelect: any;
|
|
43
|
+
ratio: any;
|
|
44
|
+
zoom: any;
|
|
45
|
+
showCounter: any;
|
|
46
|
+
placeholder: any;
|
|
47
|
+
label: any;
|
|
48
|
+
onExpand: any;
|
|
49
|
+
fit?: string | undefined;
|
|
50
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
51
|
+
export function ratioClass(ratio: any): any;
|
|
52
|
+
export const SWIPE_THRESHOLD: 40;
|
|
53
|
+
export const MOSAIC_MAX_TILES: 5;
|
|
54
|
+
export function slideLabel(images: any, i: any): any;
|
|
55
|
+
//# sourceMappingURL=imageGalleryParts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageGalleryParts.d.ts","sourceRoot":"","sources":["../../../src/components/utility/imageGalleryParts.jsx"],"names":[],"mappings":"AAsCA;;;4CASC;AAGD;;;;;;4CAcC;AAID;;;;;;;;;;;EA2BC;AAID,kDAYC;AAED;;;;;;4CAoCC;AAED;;;;;;4CAgDC;AAiBD;;;;;;;;;;;4CAkGC;AApSM,4CAA0E;AAIjF,8BAA+B,EAAE,CAAA;AAEjC,+BAAgC,CAAC,CAAA;AAG1B,qDAA+F"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2, createForOfIteratorHelper as _createForOfIteratorHelper } from '../../_virtual/_rollupPluginBabelHelpers.js';
|
|
3
3
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
4
|
-
import React, { useState,
|
|
4
|
+
import React, { useState, useCallback, useRef } from 'react';
|
|
5
5
|
import { Menu, X } from 'lucide-react';
|
|
6
6
|
import { cn } from '../../utils/cn.js';
|
|
7
7
|
import { TOUCH_TARGET } from '../../utils/touchTarget.js';
|
|
8
8
|
import Sheet from '../feedback/Sheet.js';
|
|
9
9
|
import FootNav from '../navigation/FootNav.js';
|
|
10
10
|
|
|
11
|
-
var _excluded = ["brand", "nav", "sidebarFooter", "topbar", "pageTitle", "bottomNav", "linkAs", "children", "className"];
|
|
11
|
+
var _excluded = ["brand", "nav", "sidebarFooter", "topbar", "pageTitle", "bottomNav", "linkAs", "navOpen", "onNavOpenChange", "topbarFrom", "children", "className"];
|
|
12
12
|
// Extracted from procurementpro/components/app-shell.tsx (12 of 13 products hand-roll a shell).
|
|
13
13
|
// Sidebar on lg+, top bar + drawer below, optional bottom nav on phones. Colours come from the
|
|
14
14
|
// shell / role tokens, so a brand file restyles it and dark mode needs no extra classes. Nav items
|
|
@@ -102,17 +102,28 @@ var AppShell = /*#__PURE__*/React.forwardRef(function (_ref2, ref) {
|
|
|
102
102
|
bottomNav = _ref2.bottomNav,
|
|
103
103
|
_ref2$linkAs = _ref2.linkAs,
|
|
104
104
|
Link = _ref2$linkAs === void 0 ? 'a' : _ref2$linkAs,
|
|
105
|
+
navOpen = _ref2.navOpen,
|
|
106
|
+
onNavOpenChange = _ref2.onNavOpenChange,
|
|
107
|
+
_ref2$topbarFrom = _ref2.topbarFrom,
|
|
108
|
+
topbarFrom = _ref2$topbarFrom === void 0 ? 'always' : _ref2$topbarFrom,
|
|
105
109
|
children = _ref2.children,
|
|
106
110
|
className = _ref2.className,
|
|
107
111
|
props = _objectWithoutProperties(_ref2, _excluded);
|
|
112
|
+
// The drawer is controlled when `navOpen` is set (#187), e.g. so a FootNav "More" tab can open it.
|
|
108
113
|
var _useState = useState(false),
|
|
109
114
|
_useState2 = _slicedToArray(_useState, 2),
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
openState = _useState2[0],
|
|
116
|
+
setOpenState = _useState2[1];
|
|
117
|
+
var controlled = navOpen !== undefined;
|
|
118
|
+
var open = controlled ? navOpen : openState;
|
|
119
|
+
var setOpen = useCallback(function (next) {
|
|
120
|
+
if (!controlled) setOpenState(next);
|
|
121
|
+
onNavOpenChange === null || onNavOpenChange === void 0 || onNavOpenChange(next);
|
|
122
|
+
}, [controlled, onNavOpenChange]);
|
|
112
123
|
var menuButton = useRef(null);
|
|
113
124
|
var close = useCallback(function () {
|
|
114
125
|
return setOpen(false);
|
|
115
|
-
}, []);
|
|
126
|
+
}, [setOpen]);
|
|
116
127
|
// The mobile drawer is a left Sheet: a modal dialog that moves focus in, traps
|
|
117
128
|
// Tab, closes on Escape / backdrop / drag, locks the page behind and returns
|
|
118
129
|
// focus to the menu button.
|
|
@@ -171,7 +182,7 @@ var AppShell = /*#__PURE__*/React.forwardRef(function (_ref2, ref) {
|
|
|
171
182
|
}), jsxs("div", {
|
|
172
183
|
className: "flex min-w-0 flex-col",
|
|
173
184
|
children: [jsx("header", {
|
|
174
|
-
className:
|
|
185
|
+
className: cn('sticky top-0 z-sticky border-b border-line bg-surface pt-safe px-safe', topbarFrom === 'below-lg' && 'lg:hidden'),
|
|
175
186
|
children: jsxs("div", {
|
|
176
187
|
className: "flex h-14 items-center gap-3 px-4 lg:px-6",
|
|
177
188
|
children: [jsx("button", {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
import React, { forwardRef, useId } from 'react';
|
|
5
|
+
import { cn } from '../../utils/cn.js';
|
|
6
|
+
import Card from '../utility/Card.js';
|
|
7
|
+
import Checkbox from '../core/Checkbox.js';
|
|
8
|
+
import { normalizeImages, ProductMedia, ProductPrice } from './ProductCardParts.js';
|
|
9
|
+
|
|
10
|
+
var _excluded = ["image", "images", "imageAlt", "imageRatio", "hover", "title", "eyebrow", "meta", "badges", "price", "compareAtPrice", "discountLabel", "priceFormatter", "layout", "href", "to", "as", "actions", "selectable", "selected", "onSelectedChange", "placeholder", "className"];
|
|
11
|
+
// One link target per card: the title link's ::after stretches over the whole card, so the image
|
|
12
|
+
// and blank space are clickable without nesting interactive elements. Actions and the compare
|
|
13
|
+
// checkbox sit above it: the actions by DOM order, the compare checkbox (before the link) with a card-local
|
|
14
|
+
// stacking index inside the card's `isolate` context. The ring is inset because the card clips its overflow.
|
|
15
|
+
var STRETCHED_LINK = cn("after:absolute after:inset-0 after:rounded-card after:content-['']", 'focus:outline-none focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-primary-500', 'hover:text-primary-700 dark:hover:text-primary-300');
|
|
16
|
+
var ProductCard = /*#__PURE__*/forwardRef(function (_ref, ref) {
|
|
17
|
+
var image = _ref.image,
|
|
18
|
+
images = _ref.images,
|
|
19
|
+
imageAlt = _ref.imageAlt,
|
|
20
|
+
_ref$imageRatio = _ref.imageRatio,
|
|
21
|
+
imageRatio = _ref$imageRatio === void 0 ? '4:3' : _ref$imageRatio,
|
|
22
|
+
_ref$hover = _ref.hover,
|
|
23
|
+
hover = _ref$hover === void 0 ? 'none' : _ref$hover,
|
|
24
|
+
title = _ref.title,
|
|
25
|
+
eyebrow = _ref.eyebrow,
|
|
26
|
+
meta = _ref.meta,
|
|
27
|
+
badges = _ref.badges,
|
|
28
|
+
price = _ref.price,
|
|
29
|
+
compareAtPrice = _ref.compareAtPrice,
|
|
30
|
+
discountLabel = _ref.discountLabel,
|
|
31
|
+
priceFormatter = _ref.priceFormatter,
|
|
32
|
+
_ref$layout = _ref.layout,
|
|
33
|
+
layout = _ref$layout === void 0 ? 'stack' : _ref$layout,
|
|
34
|
+
href = _ref.href,
|
|
35
|
+
to = _ref.to,
|
|
36
|
+
as = _ref.as,
|
|
37
|
+
actions = _ref.actions,
|
|
38
|
+
_ref$selectable = _ref.selectable,
|
|
39
|
+
selectable = _ref$selectable === void 0 ? false : _ref$selectable,
|
|
40
|
+
selected = _ref.selected,
|
|
41
|
+
onSelectedChange = _ref.onSelectedChange,
|
|
42
|
+
placeholder = _ref.placeholder,
|
|
43
|
+
className = _ref.className,
|
|
44
|
+
props = _objectWithoutProperties(_ref, _excluded);
|
|
45
|
+
var titleId = useId();
|
|
46
|
+
var row = layout === 'row';
|
|
47
|
+
var altFallback = imageAlt !== null && imageAlt !== void 0 ? imageAlt : typeof title === 'string' ? title : '';
|
|
48
|
+
var media = normalizeImages(image, images, altFallback);
|
|
49
|
+
// Mirrors Button (#145): `as` renders any link component (e.g. a router Link, which gets `to`);
|
|
50
|
+
// a bare `href` or `to` implies an <a>. No target at all leaves the title as plain text.
|
|
51
|
+
var linked = Boolean(as) || href !== undefined || to !== undefined;
|
|
52
|
+
var LinkComponent = as || 'a';
|
|
53
|
+
// Only defined keys, so an `href: undefined` can't clobber what a router Link derives from `to`.
|
|
54
|
+
var linkProps = as ? _objectSpread2(_objectSpread2({}, href !== undefined && {
|
|
55
|
+
href: href
|
|
56
|
+
}), to !== undefined && {
|
|
57
|
+
to: to
|
|
58
|
+
}) : {
|
|
59
|
+
href: href !== null && href !== void 0 ? href : to
|
|
60
|
+
};
|
|
61
|
+
return jsxs(Card, _objectSpread2(_objectSpread2({
|
|
62
|
+
ref: ref,
|
|
63
|
+
hover: linked,
|
|
64
|
+
"data-layout": row ? 'row' : 'stack',
|
|
65
|
+
className: cn('group relative isolate flex h-full overflow-hidden', row ? 'flex-row sm:flex-col' : 'flex-col', className)
|
|
66
|
+
}, props), {}, {
|
|
67
|
+
children: [jsxs(ProductMedia, {
|
|
68
|
+
images: media,
|
|
69
|
+
ratio: imageRatio,
|
|
70
|
+
hover: hover,
|
|
71
|
+
placeholder: placeholder,
|
|
72
|
+
row: row,
|
|
73
|
+
children: [badges && badges.length > 0 && jsx("div", {
|
|
74
|
+
className: "pointer-events-none absolute left-2 top-2 flex flex-wrap gap-1",
|
|
75
|
+
"data-slot": "badges",
|
|
76
|
+
children: React.Children.toArray(badges)
|
|
77
|
+
}), selectable && jsx("div", {
|
|
78
|
+
className: "absolute right-2 top-2 z-10 rounded-md bg-surface/90 px-2 py-1 shadow-sm",
|
|
79
|
+
children: jsx(Checkbox, _objectSpread2(_objectSpread2({
|
|
80
|
+
size: "sm",
|
|
81
|
+
label: "Compare",
|
|
82
|
+
ariaLabel: typeof title === 'string' ? "Compare ".concat(title) : undefined,
|
|
83
|
+
"aria-describedby": typeof title === 'string' ? undefined : titleId
|
|
84
|
+
}, selected !== undefined ? {
|
|
85
|
+
checked: Boolean(selected)
|
|
86
|
+
} : {}), {}, {
|
|
87
|
+
onChange: function onChange(e) {
|
|
88
|
+
return onSelectedChange === null || onSelectedChange === void 0 ? void 0 : onSelectedChange(e.target.checked, e);
|
|
89
|
+
}
|
|
90
|
+
}))
|
|
91
|
+
})]
|
|
92
|
+
}), jsxs("div", {
|
|
93
|
+
className: "flex min-w-0 flex-1 flex-col p-3 sm:p-4",
|
|
94
|
+
children: [eyebrow && jsx("p", {
|
|
95
|
+
className: "text-xs font-medium uppercase tracking-wide text-muted",
|
|
96
|
+
children: eyebrow
|
|
97
|
+
}), jsx("h3", {
|
|
98
|
+
id: titleId,
|
|
99
|
+
className: "mt-0.5 line-clamp-2 text-sm font-semibold text-foreground sm:text-base",
|
|
100
|
+
children: linked ? jsx(LinkComponent, _objectSpread2(_objectSpread2({}, linkProps), {}, {
|
|
101
|
+
className: STRETCHED_LINK,
|
|
102
|
+
children: title
|
|
103
|
+
})) : title
|
|
104
|
+
}), meta && jsx("p", {
|
|
105
|
+
className: "mt-1 line-clamp-1 text-xs text-muted sm:text-sm",
|
|
106
|
+
children: meta
|
|
107
|
+
}), jsx(ProductPrice, {
|
|
108
|
+
price: price,
|
|
109
|
+
compareAtPrice: compareAtPrice,
|
|
110
|
+
discountLabel: discountLabel,
|
|
111
|
+
priceFormatter: priceFormatter
|
|
112
|
+
}), actions && jsx("div", {
|
|
113
|
+
className: "relative mt-auto flex flex-wrap gap-2 pt-3",
|
|
114
|
+
"data-slot": "actions",
|
|
115
|
+
children: actions
|
|
116
|
+
})]
|
|
117
|
+
})]
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
ProductCard.displayName = 'ProductCard';
|
|
121
|
+
|
|
122
|
+
export { ProductCard as default };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { toConsumableArray as _toConsumableArray, objectSpread2 as _objectSpread2, slicedToArray as _slicedToArray } from '../../_virtual/_rollupPluginBabelHelpers.js';
|
|
3
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { ImageOff } from 'lucide-react';
|
|
6
|
+
import { cn } from '../../utils/cn.js';
|
|
7
|
+
|
|
8
|
+
// Class names are written out in full so Tailwind keeps them.
|
|
9
|
+
var RATIOS = {
|
|
10
|
+
'4:3': 'aspect-[4/3]',
|
|
11
|
+
'1:1': 'aspect-square',
|
|
12
|
+
'3:4': 'aspect-[3/4]'
|
|
13
|
+
};
|
|
14
|
+
// Created lazily: Intl.NumberFormat is costly to build and most grids render dozens of cards.
|
|
15
|
+
var inrFormatter;
|
|
16
|
+
var defaultPriceFormatter = function defaultPriceFormatter(value) {
|
|
17
|
+
if (!inrFormatter) inrFormatter = new Intl.NumberFormat('en-IN', {
|
|
18
|
+
style: 'currency',
|
|
19
|
+
currency: 'INR',
|
|
20
|
+
maximumFractionDigits: 0
|
|
21
|
+
});
|
|
22
|
+
return inrFormatter.format(value);
|
|
23
|
+
};
|
|
24
|
+
/** Numbers go through the formatter; strings (e.g. "From ₹999") pass through untouched. */
|
|
25
|
+
var formatPrice = function formatPrice(value) {
|
|
26
|
+
var formatter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultPriceFormatter;
|
|
27
|
+
return typeof value === 'number' && Number.isFinite(value) ? formatter(value) : value;
|
|
28
|
+
};
|
|
29
|
+
/** "26% off" when both prices are numbers and the compare-at price is higher; otherwise null. */
|
|
30
|
+
var computeDiscount = function computeDiscount(price, compareAtPrice) {
|
|
31
|
+
if (typeof price !== 'number' || typeof compareAtPrice !== 'number') return null;
|
|
32
|
+
if (!(compareAtPrice > price) || price < 0) return null;
|
|
33
|
+
var pct = Math.round((1 - price / compareAtPrice) * 100);
|
|
34
|
+
return pct > 0 ? "".concat(pct, "% off") : null;
|
|
35
|
+
};
|
|
36
|
+
/** `image` / `images` (strings or `{ src, alt }`) → `[{ src, alt }]`, cover first. */
|
|
37
|
+
var normalizeImages = function normalizeImages(image, images) {
|
|
38
|
+
var fallbackAlt = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
|
|
39
|
+
var list = [image].concat(_toConsumableArray(images || [])).filter(Boolean);
|
|
40
|
+
return list.map(function (img) {
|
|
41
|
+
return typeof img === 'string' ? {
|
|
42
|
+
src: img,
|
|
43
|
+
alt: fallbackAlt
|
|
44
|
+
} : _objectSpread2({
|
|
45
|
+
alt: fallbackAlt
|
|
46
|
+
}, img);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
var HOVER_SWAP = 'motion-safe:transition-opacity motion-safe:duration-base motion-safe:ease-standard';
|
|
50
|
+
var ProductMedia = function ProductMedia(_ref) {
|
|
51
|
+
var images = _ref.images,
|
|
52
|
+
_ref$ratio = _ref.ratio,
|
|
53
|
+
ratio = _ref$ratio === void 0 ? '4:3' : _ref$ratio,
|
|
54
|
+
_ref$hover = _ref.hover,
|
|
55
|
+
hover = _ref$hover === void 0 ? 'none' : _ref$hover,
|
|
56
|
+
placeholder = _ref.placeholder,
|
|
57
|
+
row = _ref.row,
|
|
58
|
+
children = _ref.children;
|
|
59
|
+
var cover = images[0];
|
|
60
|
+
var second = hover === 'second-image' ? images[1] : undefined;
|
|
61
|
+
// Remember which src failed, so a new cover gets a fresh chance to load.
|
|
62
|
+
var _useState = useState(null),
|
|
63
|
+
_useState2 = _slicedToArray(_useState, 2),
|
|
64
|
+
failedSrc = _useState2[0],
|
|
65
|
+
setFailedSrc = _useState2[1];
|
|
66
|
+
var showImage = cover && failedSrc !== cover.src;
|
|
67
|
+
return jsxs("div", {
|
|
68
|
+
className: cn('relative shrink-0 overflow-hidden bg-secondary-100 dark:bg-secondary-800', RATIOS[ratio] || RATIOS['4:3'], row ? 'w-28 sm:w-full' : 'w-full'),
|
|
69
|
+
"data-slot": "media",
|
|
70
|
+
children: [showImage ? jsxs(Fragment, {
|
|
71
|
+
children: [jsx("img", {
|
|
72
|
+
src: cover.src,
|
|
73
|
+
alt: cover.alt,
|
|
74
|
+
loading: "lazy",
|
|
75
|
+
onError: function onError() {
|
|
76
|
+
return setFailedSrc(cover.src);
|
|
77
|
+
},
|
|
78
|
+
className: cn('h-full w-full object-cover', hover === 'zoom' && 'motion-safe:transition-transform motion-safe:duration-slow motion-safe:ease-standard motion-safe:group-hover:scale-105', second && cn(HOVER_SWAP, 'group-hover:opacity-0'))
|
|
79
|
+
}), second &&
|
|
80
|
+
// Same product from another angle: decorative, the cover's alt already names it.
|
|
81
|
+
jsx("img", {
|
|
82
|
+
src: second.src,
|
|
83
|
+
alt: "",
|
|
84
|
+
"aria-hidden": "true",
|
|
85
|
+
loading: "lazy",
|
|
86
|
+
className: cn('absolute inset-0 h-full w-full object-cover opacity-0 group-hover:opacity-100', HOVER_SWAP)
|
|
87
|
+
})]
|
|
88
|
+
}) : jsx("div", {
|
|
89
|
+
className: "grid h-full w-full place-items-center text-muted",
|
|
90
|
+
"data-slot": "placeholder",
|
|
91
|
+
children: placeholder !== null && placeholder !== void 0 ? placeholder : jsx(ImageOff, {
|
|
92
|
+
className: "h-8 w-8",
|
|
93
|
+
"aria-hidden": "true"
|
|
94
|
+
})
|
|
95
|
+
}), children]
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
var ProductPrice = function ProductPrice(_ref2) {
|
|
99
|
+
var price = _ref2.price,
|
|
100
|
+
compareAtPrice = _ref2.compareAtPrice,
|
|
101
|
+
discountLabel = _ref2.discountLabel,
|
|
102
|
+
priceFormatter = _ref2.priceFormatter;
|
|
103
|
+
if (price == null && compareAtPrice == null) return null;
|
|
104
|
+
var discount = discountLabel !== null && discountLabel !== void 0 ? discountLabel : computeDiscount(price, compareAtPrice);
|
|
105
|
+
var hasCompare = compareAtPrice != null;
|
|
106
|
+
return jsxs("p", {
|
|
107
|
+
className: "mt-2 flex flex-wrap items-baseline gap-x-2 gap-y-0.5",
|
|
108
|
+
"data-slot": "price",
|
|
109
|
+
children: [price != null && jsxs("span", {
|
|
110
|
+
className: "text-lg font-semibold tracking-tight text-foreground tabular-nums",
|
|
111
|
+
children: [hasCompare && jsx("span", {
|
|
112
|
+
className: "sr-only",
|
|
113
|
+
children: "Sale price: "
|
|
114
|
+
}), formatPrice(price, priceFormatter)]
|
|
115
|
+
}), hasCompare && jsxs("s", {
|
|
116
|
+
className: "text-sm text-muted tabular-nums",
|
|
117
|
+
children: [jsx("span", {
|
|
118
|
+
className: "sr-only",
|
|
119
|
+
children: "Original price: "
|
|
120
|
+
}), formatPrice(compareAtPrice, priceFormatter)]
|
|
121
|
+
}), discount && jsx("span", {
|
|
122
|
+
className: "text-sm font-semibold text-success-700 dark:text-success-400",
|
|
123
|
+
children: discount
|
|
124
|
+
})]
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export { ProductMedia, ProductPrice, computeDiscount, defaultPriceFormatter, formatPrice, normalizeImages };
|
|
@@ -7,7 +7,7 @@ import { cn } from '../../utils/cn.js';
|
|
|
7
7
|
import Card from '../utility/Card.js';
|
|
8
8
|
import { Bone } from './LoadingState.js';
|
|
9
9
|
|
|
10
|
-
var _excluded = ["label", "value", "icon", "tone", "change", "loading", "className"];
|
|
10
|
+
var _excluded = ["label", "value", "icon", "tone", "change", "loading", "tinted", "children", "className"];
|
|
11
11
|
// Extracted from PracticePro/components/ui/StatCard.tsx (28 imports), with tones mapped to the
|
|
12
12
|
// semantic scales instead of product colours. Class names are written out in full so Tailwind keeps them.
|
|
13
13
|
var TONES = {
|
|
@@ -18,6 +18,15 @@ var TONES = {
|
|
|
18
18
|
error: 'bg-error-50 text-error-600 dark:bg-error-900/40 dark:text-error-300',
|
|
19
19
|
info: 'bg-info-50 text-info-600 dark:bg-info-900/40 dark:text-info-300'
|
|
20
20
|
};
|
|
21
|
+
// `tinted` (#188): the whole card in the tone, e.g. an overdue card in error-50 / error-200 / error-800.
|
|
22
|
+
var TINTS = {
|
|
23
|
+
primary: 'border-primary-200 bg-primary-50 text-primary-800 dark:border-primary-800 dark:bg-primary-900/30 dark:text-primary-200',
|
|
24
|
+
secondary: 'border-secondary-200 bg-secondary-50 text-secondary-800 dark:border-secondary-700 dark:bg-secondary-800/40 dark:text-secondary-200',
|
|
25
|
+
success: 'border-success-200 bg-success-50 text-success-800 dark:border-success-800 dark:bg-success-900/30 dark:text-success-200',
|
|
26
|
+
warning: 'border-warning-200 bg-warning-50 text-warning-800 dark:border-warning-800 dark:bg-warning-900/30 dark:text-warning-200',
|
|
27
|
+
error: 'border-error-200 bg-error-50 text-error-800 dark:border-error-800 dark:bg-error-900/30 dark:text-error-200',
|
|
28
|
+
info: 'border-info-200 bg-info-50 text-info-800 dark:border-info-800 dark:bg-info-900/30 dark:text-info-200'
|
|
29
|
+
};
|
|
21
30
|
var StatCard = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
|
|
22
31
|
var _change$goodWhenUp, _change$unit;
|
|
23
32
|
var label = _ref.label,
|
|
@@ -28,13 +37,16 @@ var StatCard = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
|
|
|
28
37
|
change = _ref.change,
|
|
29
38
|
_ref$loading = _ref.loading,
|
|
30
39
|
loading = _ref$loading === void 0 ? false : _ref$loading,
|
|
40
|
+
_ref$tinted = _ref.tinted,
|
|
41
|
+
tinted = _ref$tinted === void 0 ? false : _ref$tinted,
|
|
42
|
+
children = _ref.children,
|
|
31
43
|
className = _ref.className,
|
|
32
44
|
props = _objectWithoutProperties(_ref, _excluded);
|
|
33
45
|
var up = change ? change.value >= 0 : false;
|
|
34
46
|
var good = change ? ((_change$goodWhenUp = change.goodWhenUp) !== null && _change$goodWhenUp !== void 0 ? _change$goodWhenUp : true) === up : false;
|
|
35
47
|
return jsx(Card, _objectSpread2(_objectSpread2({
|
|
36
48
|
ref: ref,
|
|
37
|
-
className: cn('h-full', className)
|
|
49
|
+
className: cn('h-full', tinted && (TINTS[tone] || TINTS.primary), className)
|
|
38
50
|
}, props), {}, {
|
|
39
51
|
children: jsxs("div", {
|
|
40
52
|
className: "flex items-start gap-4 p-5",
|
|
@@ -43,16 +55,16 @@ var StatCard = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
|
|
|
43
55
|
"aria-hidden": "true",
|
|
44
56
|
children: icon
|
|
45
57
|
}), jsxs("div", {
|
|
46
|
-
className: "min-w-0",
|
|
58
|
+
className: "min-w-0 flex-1",
|
|
47
59
|
children: [jsx("p", {
|
|
48
|
-
className:
|
|
60
|
+
className: cn('text-sm', tinted ? 'text-current opacity-80' : 'text-muted'),
|
|
49
61
|
children: label
|
|
50
62
|
}), loading ? jsx(Bone, {
|
|
51
63
|
height: 32,
|
|
52
64
|
width: 72,
|
|
53
65
|
className: "mt-1"
|
|
54
66
|
}) : jsx("p", {
|
|
55
|
-
className:
|
|
67
|
+
className: cn('mt-0.5 text-2xl font-semibold tracking-tight tabular-nums sm:text-3xl', tinted ? 'text-current' : 'text-foreground'),
|
|
56
68
|
children: value
|
|
57
69
|
}), change && !loading && jsxs("p", {
|
|
58
70
|
className: "mt-1 flex items-center gap-1 text-xs text-muted",
|
|
@@ -69,6 +81,9 @@ var StatCard = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
|
|
|
69
81
|
children: [up ? 'Up' : 'Down', " "]
|
|
70
82
|
}), Math.abs(change.value), (_change$unit = change.unit) !== null && _change$unit !== void 0 ? _change$unit : '%']
|
|
71
83
|
}), change.period]
|
|
84
|
+
}), children != null && children !== false && jsx("div", {
|
|
85
|
+
className: "mt-3 space-y-2 text-sm",
|
|
86
|
+
children: children
|
|
72
87
|
})]
|
|
73
88
|
})]
|
|
74
89
|
})
|
|
@@ -6,19 +6,30 @@ import { cn } from '../../utils/cn.js';
|
|
|
6
6
|
import { TOUCH_TARGET, PRESS_FEEDBACK } from '../../utils/touchTarget.js';
|
|
7
7
|
import { Loader2 } from 'lucide-react';
|
|
8
8
|
|
|
9
|
-
var _excluded = ["className", "variant", "size", "disabled", "loading", "children", "leftIcon", "rightIcon", "fullWidth", "as", "onClick"];
|
|
9
|
+
var _excluded = ["className", "variant", "size", "tone", "disabled", "loading", "children", "leftIcon", "rightIcon", "fullWidth", "as", "onClick"];
|
|
10
10
|
// Square, icon-only sizes. Pair with an `aria-label` (there is no visible text).
|
|
11
11
|
var ICON_SIZES = {
|
|
12
12
|
'icon-sm': 'h-8 w-8 p-0',
|
|
13
13
|
icon: 'h-10 w-10 p-0',
|
|
14
14
|
'icon-lg': 'h-12 w-12 p-0'
|
|
15
15
|
};
|
|
16
|
+
// Text colour of `variant="link"`, picked with `tone`. Written out in full so Tailwind keeps them.
|
|
17
|
+
var LINK_TONES = {
|
|
18
|
+
primary: 'text-primary-700 focus-visible:ring-primary-500 dark:text-primary-300',
|
|
19
|
+
secondary: 'text-secondary-700 focus-visible:ring-secondary-500 dark:text-secondary-300',
|
|
20
|
+
success: 'text-success-700 focus-visible:ring-success-500 dark:text-success-400',
|
|
21
|
+
warning: 'text-warning-800 focus-visible:ring-warning-500 dark:text-warning-400',
|
|
22
|
+
error: 'text-error-700 focus-visible:ring-error-500 dark:text-error-400',
|
|
23
|
+
info: 'text-info-700 focus-visible:ring-info-500 dark:text-info-300'
|
|
24
|
+
};
|
|
16
25
|
var Button = /*#__PURE__*/forwardRef(function (_ref, ref) {
|
|
17
26
|
var className = _ref.className,
|
|
18
27
|
_ref$variant = _ref.variant,
|
|
19
28
|
variant = _ref$variant === void 0 ? 'primary' : _ref$variant,
|
|
20
29
|
_ref$size = _ref.size,
|
|
21
30
|
size = _ref$size === void 0 ? 'md' : _ref$size,
|
|
31
|
+
_ref$tone = _ref.tone,
|
|
32
|
+
tone = _ref$tone === void 0 ? 'primary' : _ref$tone,
|
|
22
33
|
disabled = _ref.disabled,
|
|
23
34
|
loading = _ref.loading,
|
|
24
35
|
children = _ref.children,
|
|
@@ -34,6 +45,7 @@ var Button = /*#__PURE__*/forwardRef(function (_ref, ref) {
|
|
|
34
45
|
var isNativeButton = Component === 'button';
|
|
35
46
|
var isDisabled = disabled || loading;
|
|
36
47
|
var isIconSize = Boolean(ICON_SIZES[size]);
|
|
48
|
+
var isLink = variant === 'link';
|
|
37
49
|
var baseClasses = cn('relative inline-flex items-center justify-center rounded-md font-medium',
|
|
38
50
|
// Press shrink + an explicit transition for colours and the shrink;
|
|
39
51
|
// both are off under prefers-reduced-motion.
|
|
@@ -53,7 +65,10 @@ var Button = /*#__PURE__*/forwardRef(function (_ref, ref) {
|
|
|
53
65
|
'h-12 px-6 text-base': size === 'lg',
|
|
54
66
|
'h-14 px-8 text-lg': size === 'xl',
|
|
55
67
|
'h-16 px-10 text-xl': size === '2xl'
|
|
56
|
-
}, TOUCH_TARGET, size === 'xs' || size === 'sm' || size === 'icon-sm' || size === 'icon'), 'w-full', fullWidth), ICON_SIZES[size],
|
|
68
|
+
}, TOUCH_TARGET, isLink || size === 'xs' || size === 'sm' || size === 'icon-sm' || size === 'icon'), 'w-full', fullWidth), ICON_SIZES[size],
|
|
69
|
+
// A text link: no background, border or padding (it keeps the size's text
|
|
70
|
+
// scale), underlined on hover, coloured by `tone`.
|
|
71
|
+
isLink && cn('h-auto bg-transparent p-0 underline-offset-4 hover:underline focus-visible:underline', LINK_TONES[tone] || LINK_TONES.primary), className);
|
|
57
72
|
var content = jsxs(Fragment, {
|
|
58
73
|
children: [leftIcon && jsx("span", {
|
|
59
74
|
className: isIconSize ? undefined : 'mr-2',
|