@shopbb/helium 0.2.0 → 0.3.0-alpha.1

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 (36) hide show
  1. package/dist/components/AddToCartButton.d.ts +41 -0
  2. package/dist/components/AddToCartButton.d.ts.map +1 -0
  3. package/dist/components/AddToCartButton.js +51 -0
  4. package/dist/components/AddToCartButton.js.map +1 -0
  5. package/dist/components/CartLineQuantityAdjustButton.d.ts +40 -0
  6. package/dist/components/CartLineQuantityAdjustButton.d.ts.map +1 -0
  7. package/dist/components/CartLineQuantityAdjustButton.js +58 -0
  8. package/dist/components/CartLineQuantityAdjustButton.js.map +1 -0
  9. package/dist/components/Image.d.ts +39 -0
  10. package/dist/components/Image.d.ts.map +1 -0
  11. package/dist/components/Image.js +28 -0
  12. package/dist/components/Image.js.map +1 -0
  13. package/dist/components/Money.d.ts +41 -0
  14. package/dist/components/Money.d.ts.map +1 -0
  15. package/dist/components/Money.js +48 -0
  16. package/dist/components/Money.js.map +1 -0
  17. package/dist/components/ProductPrice.d.ts +28 -0
  18. package/dist/components/ProductPrice.d.ts.map +1 -0
  19. package/dist/components/ProductPrice.js +10 -0
  20. package/dist/components/ProductPrice.js.map +1 -0
  21. package/dist/components/VariantSelector.d.ts +80 -0
  22. package/dist/components/VariantSelector.d.ts.map +1 -0
  23. package/dist/components/VariantSelector.js +87 -0
  24. package/dist/components/VariantSelector.js.map +1 -0
  25. package/dist/components/index.d.ts +24 -0
  26. package/dist/components/index.d.ts.map +1 -0
  27. package/dist/components/index.js +18 -0
  28. package/dist/components/index.js.map +1 -0
  29. package/package.json +12 -4
  30. package/src/components/AddToCartButton.tsx +90 -0
  31. package/src/components/CartLineQuantityAdjustButton.tsx +119 -0
  32. package/src/components/Image.tsx +93 -0
  33. package/src/components/Money.tsx +106 -0
  34. package/src/components/ProductPrice.tsx +61 -0
  35. package/src/components/VariantSelector.tsx +148 -0
  36. package/src/components/index.ts +33 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * <AddToCartButton> — 加入购物车按钮
3
+ *
4
+ * 对齐 @shopify/hydrogen <AddToCartButton>:
5
+ * - 内置 loading 态、disabled、error 处理
6
+ * - 支持 onAdd 回调(实际加购逻辑由外部 cart hook 提供)
7
+ * - 触发 analytics 事件钩子
8
+ * - 不带样式,商家自己控制
9
+ *
10
+ * 用法(Phase 1,需要传 onAdd):
11
+ * const { addLine } = useCart();
12
+ * <AddToCartButton
13
+ * variantId={selectedVariant.id}
14
+ * quantity={1}
15
+ * disabled={!selectedVariant.availableForSale}
16
+ * onAdd={(vid, qty) => addLine(vid, qty)}
17
+ * >
18
+ * 加入购物车
19
+ * </AddToCartButton>
20
+ *
21
+ * Phase 2 后:组件会自动从 <CartProvider> 拿,无需 onAdd 也能用。
22
+ */
23
+ import * as React from 'react';
24
+ export interface AddToCartButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'onError'> {
25
+ /** 必传:variant 的 GID(gid://shopbb/ProductVariant/...) */
26
+ variantId: string;
27
+ /** 数量,默认 1 */
28
+ quantity?: number;
29
+ /** 实际加购回调。Phase 1 必传;Phase 2 后可从 Provider 自动获取 */
30
+ onAdd?: (variantId: string, quantity: number) => Promise<void>;
31
+ /** 加购成功回调(如导航到 /cart) */
32
+ onAdded?: () => void;
33
+ /** 加购失败回调 */
34
+ onError?: (err: Error) => void;
35
+ /** 加购中显示的文本,默认 "加入中..." */
36
+ loadingText?: React.ReactNode;
37
+ /** 不可用时显示的文本,默认 "缺货" */
38
+ unavailableText?: React.ReactNode;
39
+ }
40
+ export declare function AddToCartButton(props: AddToCartButtonProps): import("react/jsx-runtime").JSX.Element;
41
+ //# sourceMappingURL=AddToCartButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AddToCartButton.d.ts","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IACtH,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,yBAAyB;IACzB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa;IACb,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;IAC/B,2BAA2B;IAC3B,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,wBAAwB;IACxB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACnC;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CA+C1D"}
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * <AddToCartButton> — 加入购物车按钮
4
+ *
5
+ * 对齐 @shopify/hydrogen <AddToCartButton>:
6
+ * - 内置 loading 态、disabled、error 处理
7
+ * - 支持 onAdd 回调(实际加购逻辑由外部 cart hook 提供)
8
+ * - 触发 analytics 事件钩子
9
+ * - 不带样式,商家自己控制
10
+ *
11
+ * 用法(Phase 1,需要传 onAdd):
12
+ * const { addLine } = useCart();
13
+ * <AddToCartButton
14
+ * variantId={selectedVariant.id}
15
+ * quantity={1}
16
+ * disabled={!selectedVariant.availableForSale}
17
+ * onAdd={(vid, qty) => addLine(vid, qty)}
18
+ * >
19
+ * 加入购物车
20
+ * </AddToCartButton>
21
+ *
22
+ * Phase 2 后:组件会自动从 <CartProvider> 拿,无需 onAdd 也能用。
23
+ */
24
+ import * as React from 'react';
25
+ export function AddToCartButton(props) {
26
+ const { variantId, quantity = 1, onAdd, onAdded, onError, loadingText = '加入中...', unavailableText = '缺货', children = '加入购物车', disabled: disabledProp, ...rest } = props;
27
+ const [adding, setAdding] = React.useState(false);
28
+ const handleClick = async (e) => {
29
+ e.preventDefault();
30
+ if (adding || disabledProp)
31
+ return;
32
+ if (!onAdd) {
33
+ console.warn('[AddToCartButton] onAdd not provided; this button does nothing in Phase 1');
34
+ return;
35
+ }
36
+ setAdding(true);
37
+ try {
38
+ await onAdd(variantId, quantity);
39
+ onAdded?.();
40
+ }
41
+ catch (err) {
42
+ console.error('[AddToCartButton] add failed:', err);
43
+ onError?.(err instanceof Error ? err : new Error(String(err)));
44
+ }
45
+ finally {
46
+ setAdding(false);
47
+ }
48
+ };
49
+ return (_jsx("button", { type: "button", ...rest, "data-add-to-cart": true, "data-loading": adding ? '' : undefined, disabled: disabledProp || adding, onClick: handleClick, children: adding ? loadingText : disabledProp ? unavailableText : children }));
50
+ }
51
+ //# sourceMappingURL=AddToCartButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AddToCartButton.js","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAmB/B,MAAM,UAAU,eAAe,CAAC,KAA2B;IACzD,MAAM,EACJ,SAAS,EACT,QAAQ,GAAG,CAAC,EACZ,KAAK,EACL,OAAO,EACP,OAAO,EACP,WAAW,GAAG,QAAQ,EACtB,eAAe,GAAG,IAAI,EACtB,QAAQ,GAAG,OAAO,EAClB,QAAQ,EAAE,YAAY,EACtB,GAAG,IAAI,EACR,GAAG,KAAK,CAAC;IAEV,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAElD,MAAM,WAAW,GAAG,KAAK,EAAE,CAAsC,EAAE,EAAE;QACnE,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,IAAI,MAAM,IAAI,YAAY;YAAE,OAAO;QACnC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,2EAA2E,CAAC,CAAC;YAC1F,OAAO;QACT,CAAC;QACD,SAAS,CAAC,IAAI,CAAC,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;YACjC,OAAO,EAAE,EAAE,CAAC;QACd,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,GAAG,CAAC,CAAC;YACpD,OAAO,EAAE,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACjE,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,KAAK,CAAC,CAAC;QACnB,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,CACL,iBACE,IAAI,EAAC,QAAQ,KACT,IAAI,4CAEM,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,EACrC,QAAQ,EAAE,YAAY,IAAI,MAAM,EAChC,OAAO,EAAE,WAAW,YAEnB,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,QAAQ,GAC1D,CACV,CAAC;AACJ,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * <CartLineQuantityAdjustButton> — cart 行数量加减按钮
3
+ *
4
+ * 对齐 @shopify/hydrogen <CartLineQuantityAdjustButton>:
5
+ * - +/- 按钮调用 onChange
6
+ * - 防抖(300ms)避免点快导致多次请求
7
+ * - min/max 校验
8
+ * - 移除时弹确认(可选)
9
+ *
10
+ * 用法:
11
+ * const { updateLine, removeLine } = useCart();
12
+ * <CartLineQuantityAdjustButton
13
+ * line={line}
14
+ * onUpdate={(newQty) => updateLine(line.id, newQty)}
15
+ * onRemove={() => removeLine(line.id)}
16
+ * />
17
+ */
18
+ import * as React from 'react';
19
+ export interface CartLineQuantityAdjustButtonProps {
20
+ /** line 的当前数量 */
21
+ quantity: number;
22
+ /** 最小值,默认 1(小于此值改成 remove) */
23
+ min?: number;
24
+ /** 最大值,默认 99 */
25
+ max?: number;
26
+ /** 数量变化回调 */
27
+ onUpdate?: (newQuantity: number) => Promise<void> | void;
28
+ /** 数量降到 0 时触发 remove */
29
+ onRemove?: () => Promise<void> | void;
30
+ /** 防抖延迟(ms),默认 300 */
31
+ debounceMs?: number;
32
+ /** 自定义包装容器 className */
33
+ className?: string;
34
+ /** 减号按钮 props 透传 */
35
+ decreaseProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
36
+ /** 加号按钮 props 透传 */
37
+ increaseProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
38
+ }
39
+ export declare function CartLineQuantityAdjustButton(props: CartLineQuantityAdjustButtonProps): import("react/jsx-runtime").JSX.Element;
40
+ //# sourceMappingURL=CartLineQuantityAdjustButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CartLineQuantityAdjustButton.d.ts","sourceRoot":"","sources":["../../src/components/CartLineQuantityAdjustButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,WAAW,iCAAiC;IAChD,iBAAiB;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa;IACb,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD,wBAAwB;IACxB,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACtC,sBAAsB;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,aAAa,CAAC,EAAE,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;IAC9D,oBAAoB;IACpB,aAAa,CAAC,EAAE,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;CAC/D;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,iCAAiC,2CA6EpF"}
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * <CartLineQuantityAdjustButton> — cart 行数量加减按钮
4
+ *
5
+ * 对齐 @shopify/hydrogen <CartLineQuantityAdjustButton>:
6
+ * - +/- 按钮调用 onChange
7
+ * - 防抖(300ms)避免点快导致多次请求
8
+ * - min/max 校验
9
+ * - 移除时弹确认(可选)
10
+ *
11
+ * 用法:
12
+ * const { updateLine, removeLine } = useCart();
13
+ * <CartLineQuantityAdjustButton
14
+ * line={line}
15
+ * onUpdate={(newQty) => updateLine(line.id, newQty)}
16
+ * onRemove={() => removeLine(line.id)}
17
+ * />
18
+ */
19
+ import * as React from 'react';
20
+ export function CartLineQuantityAdjustButton(props) {
21
+ const { quantity, min = 1, max = 99, onUpdate, onRemove, debounceMs = 300, className, decreaseProps, increaseProps, } = props;
22
+ // optimistic 显示的 quantity
23
+ const [optimistic, setOptimistic] = React.useState(quantity);
24
+ React.useEffect(() => setOptimistic(quantity), [quantity]);
25
+ const [pending, setPending] = React.useState(false);
26
+ const timerRef = React.useRef(null);
27
+ const flushUpdate = React.useCallback((target) => {
28
+ if (timerRef.current)
29
+ clearTimeout(timerRef.current);
30
+ timerRef.current = setTimeout(async () => {
31
+ setPending(true);
32
+ try {
33
+ if (target < min) {
34
+ await onRemove?.();
35
+ }
36
+ else {
37
+ await onUpdate?.(target);
38
+ }
39
+ }
40
+ finally {
41
+ setPending(false);
42
+ }
43
+ }, debounceMs);
44
+ }, [min, onRemove, onUpdate, debounceMs]);
45
+ const handleAdjust = (delta) => {
46
+ const next = optimistic + delta;
47
+ if (next > max)
48
+ return;
49
+ setOptimistic(next);
50
+ flushUpdate(next);
51
+ };
52
+ React.useEffect(() => () => {
53
+ if (timerRef.current)
54
+ clearTimeout(timerRef.current);
55
+ }, []);
56
+ return (_jsxs("div", { className: className, "data-quantity-adjust": true, style: { display: 'inline-flex', alignItems: 'center' }, children: [_jsx("button", { type: "button", ...decreaseProps, "aria-label": "\u51CF\u5C11", "data-quantity-decrease": true, onClick: () => handleAdjust(-1), disabled: pending && optimistic <= min, children: decreaseProps?.children ?? '−' }), _jsx("span", { "data-quantity-value": true, "aria-live": "polite", style: { margin: '0 0.5em', minWidth: '1.5em', textAlign: 'center' }, children: optimistic }), _jsx("button", { type: "button", ...increaseProps, "aria-label": "\u589E\u52A0", "data-quantity-increase": true, onClick: () => handleAdjust(1), disabled: pending && optimistic >= max, children: increaseProps?.children ?? '+' })] }));
57
+ }
58
+ //# sourceMappingURL=CartLineQuantityAdjustButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CartLineQuantityAdjustButton.js","sourceRoot":"","sources":["../../src/components/CartLineQuantityAdjustButton.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * <Image> — 响应式商品图片
3
+ *
4
+ * 对齐 @shopify/hydrogen 的 <Image>:
5
+ * - 自动生成 srcset(5 个分辨率档:375 / 750 / 1024 / 1536 / 1920)
6
+ * - sizes 媒体查询
7
+ * - loading="lazy" 默认(首屏可关)
8
+ * - decoding="async"
9
+ * - aspectRatio 占位(不抖)
10
+ * - 自动 alt
11
+ *
12
+ * srcset 里的 url 通过 ?width= query 表达,服务端可忽略或接 cdn-cgi/image。
13
+ *
14
+ * 用法:
15
+ * <Image data={{ url, altText, width, height }} sizes="(min-width: 768px) 50vw, 100vw" />
16
+ * <Image data={...} aspectRatio="1/1" />
17
+ * <Image data={...} loading="eager" /> // 首屏
18
+ */
19
+ import * as React from 'react';
20
+ export interface ImageData {
21
+ url: string;
22
+ altText?: string | null;
23
+ width?: number | null;
24
+ height?: number | null;
25
+ }
26
+ export interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src' | 'srcSet' | 'loading'> {
27
+ /** 必传:图片对象 */
28
+ data: ImageData;
29
+ /** 媒体查询 sizes,例如 (min-width: 768px) 50vw, 100vw */
30
+ sizes?: string;
31
+ /** 强制宽高比,例如 '1/1' '4/3' '16/9' */
32
+ aspectRatio?: string;
33
+ /** lazy(默认) / eager(首屏图用) */
34
+ loading?: 'lazy' | 'eager';
35
+ /** 自定义 srcset 档位 */
36
+ widths?: number[];
37
+ }
38
+ export declare function Image(props: ImageProps): import("react/jsx-runtime").JSX.Element;
39
+ //# sourceMappingURL=Image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../../src/components/Image.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC/G,cAAc;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6BAA6B;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAkBD,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,2CAmCtC"}
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // 默认 srcset 档位
3
+ const DEFAULT_WIDTHS = [375, 750, 1024, 1536, 1920];
4
+ /** 给 url 加 ?width=N query */
5
+ function withWidth(url, width) {
6
+ try {
7
+ const u = new URL(url);
8
+ u.searchParams.set('width', String(width));
9
+ return u.toString();
10
+ }
11
+ catch {
12
+ // 不是绝对 URL,按相对路径处理
13
+ const sep = url.includes('?') ? '&' : '?';
14
+ return `${url}${sep}width=${width}`;
15
+ }
16
+ }
17
+ export function Image(props) {
18
+ const { data, sizes, aspectRatio, loading = 'lazy', widths = DEFAULT_WIDTHS, style, alt: altProp, ...rest } = props;
19
+ const srcset = widths.map((w) => `${withWidth(data.url, w)} ${w}w`).join(', ');
20
+ // src 用中间档位
21
+ const src = withWidth(data.url, widths[Math.floor(widths.length / 2)]);
22
+ const finalStyle = {
23
+ ...(aspectRatio ? { aspectRatio, objectFit: 'cover' } : null),
24
+ ...style,
25
+ };
26
+ return (_jsx("img", { ...rest, src: src, srcSet: srcset, sizes: sizes, alt: altProp ?? data.altText ?? '', loading: loading, decoding: "async", width: data.width || undefined, height: data.height || undefined, style: finalStyle }));
27
+ }
28
+ //# sourceMappingURL=Image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Image.js","sourceRoot":"","sources":["../../src/components/Image.tsx"],"names":[],"mappings":";AAyCA,eAAe;AACf,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAEpD,6BAA6B;AAC7B,SAAS,SAAS,CAAC,GAAW,EAAE,KAAa;IAC3C,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3C,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,mBAAmB;QACnB,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;QAC1C,OAAO,GAAG,GAAG,GAAG,GAAG,SAAS,KAAK,EAAE,CAAC;IACtC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,KAAiB;IACrC,MAAM,EACJ,IAAI,EACJ,KAAK,EACL,WAAW,EACX,OAAO,GAAG,MAAM,EAChB,MAAM,GAAG,cAAc,EACvB,KAAK,EACL,GAAG,EAAE,OAAO,EACZ,GAAG,IAAI,EACR,GAAG,KAAK,CAAC;IAEV,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/E,YAAY;IACZ,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvE,MAAM,UAAU,GAAwB;QACtC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,OAAgB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,GAAG,KAAK;KACT,CAAC;IAEF,OAAO,CACL,iBACM,IAAI,EACR,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,OAAO,IAAI,IAAI,CAAC,OAAO,IAAI,EAAE,EAClC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAC,OAAO,EAChB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,SAAS,EAC9B,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,SAAS,EAChC,KAAK,EAAE,UAAU,GACjB,CACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * <Money> — 货币格式化
3
+ *
4
+ * 对齐 @shopify/hydrogen 的 <Money>。
5
+ * - 自动按 currencyCode + locale 用 Intl.NumberFormat 格式化
6
+ * - 支持 withoutCurrency / withoutTrailingZeros
7
+ * - 支持 as 渲染成任意元素(默认 <span>)
8
+ * - 支持 measurement(unit price,例如 ¥9.99/100g)
9
+ *
10
+ * 用法:
11
+ * <Money data={{ amount: '9.99', currencyCode: 'CNY' }} />
12
+ * <Money data={{ ... }} withoutCurrency />
13
+ * <Money data={{ ... }} as="div" measurement={{ referenceUnit: 'kg', quantity: 0.1 }} />
14
+ */
15
+ import * as React from 'react';
16
+ export interface MoneyData {
17
+ amount: string;
18
+ currencyCode: string;
19
+ }
20
+ export interface MoneyMeasurement {
21
+ /** 比如 'kg' / '100g' / 'l' */
22
+ referenceUnit: string;
23
+ /** 比如 0.1 表示每 100g */
24
+ quantity?: number;
25
+ }
26
+ export interface MoneyProps extends React.HTMLAttributes<HTMLElement> {
27
+ /** 必传:金额对象 */
28
+ data: MoneyData;
29
+ /** 不显示货币符号,只显示数字 */
30
+ withoutCurrency?: boolean;
31
+ /** 整数时不显示 .00 */
32
+ withoutTrailingZeros?: boolean;
33
+ /** locale 字符串(zh-CN / en-US / ...)。默认按 currencyCode 推断 */
34
+ locale?: string;
35
+ /** 渲染元素,默认 span */
36
+ as?: keyof JSX.IntrinsicElements;
37
+ /** unit price:渲染 ¥99.00/100g */
38
+ measurement?: MoneyMeasurement;
39
+ }
40
+ export declare function Money(props: MoneyProps): import("react/jsx-runtime").JSX.Element;
41
+ //# sourceMappingURL=Money.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Money.d.ts","sourceRoot":"","sources":["../../src/components/Money.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAW,SAAQ,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC;IACnE,cAAc;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,oBAAoB;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB;IACjB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB;IACnB,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,iBAAiB,CAAC;IACjC,gCAAgC;IAChC,WAAW,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAYD,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,2CAmDtC"}
@@ -0,0 +1,48 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ // currencyCode → 默认 locale 推断
3
+ const DEFAULT_LOCALE_BY_CURRENCY = {
4
+ CNY: 'zh-CN',
5
+ USD: 'en-US',
6
+ EUR: 'de-DE',
7
+ GBP: 'en-GB',
8
+ JPY: 'ja-JP',
9
+ KRW: 'ko-KR',
10
+ };
11
+ export function Money(props) {
12
+ const { data, withoutCurrency, withoutTrailingZeros, locale: localeProp, as, measurement, ...rest } = props;
13
+ const Tag = as || 'span';
14
+ const locale = localeProp || DEFAULT_LOCALE_BY_CURRENCY[data.currencyCode] || 'en-US';
15
+ const amount = Number(data.amount);
16
+ let formatted;
17
+ try {
18
+ const opts = withoutCurrency
19
+ ? {
20
+ minimumFractionDigits: withoutTrailingZeros && Number.isInteger(amount) ? 0 : 2,
21
+ maximumFractionDigits: 2,
22
+ }
23
+ : {
24
+ style: 'currency',
25
+ currency: data.currencyCode,
26
+ minimumFractionDigits: withoutTrailingZeros && Number.isInteger(amount) ? 0 : 2,
27
+ maximumFractionDigits: 2,
28
+ };
29
+ formatted = new Intl.NumberFormat(locale, opts).format(amount);
30
+ }
31
+ catch {
32
+ // Fallback:Intl 不支持的 currencyCode
33
+ formatted = withoutCurrency ? amount.toFixed(2) : `${data.currencyCode} ${amount.toFixed(2)}`;
34
+ }
35
+ // measurement suffix(unit price)
36
+ let suffix = '';
37
+ if (measurement) {
38
+ const q = measurement.quantity ?? 1;
39
+ if (q === 1) {
40
+ suffix = `/${measurement.referenceUnit}`;
41
+ }
42
+ else {
43
+ suffix = `/${q}${measurement.referenceUnit}`;
44
+ }
45
+ }
46
+ return (_jsxs(Tag, { ...rest, "data-money": data.amount, "data-money-currency": data.currencyCode, children: [formatted, suffix] }));
47
+ }
48
+ //# sourceMappingURL=Money.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Money.js","sourceRoot":"","sources":["../../src/components/Money.tsx"],"names":[],"mappings":";AA4CA,8BAA8B;AAC9B,MAAM,0BAA0B,GAA2B;IACzD,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;CACb,CAAC;AAEF,MAAM,UAAU,KAAK,CAAC,KAAiB;IACrC,MAAM,EACJ,IAAI,EACJ,eAAe,EACf,oBAAoB,EACpB,MAAM,EAAE,UAAU,EAClB,EAAE,EACF,WAAW,EACX,GAAG,IAAI,EACR,GAAG,KAAK,CAAC;IAEV,MAAM,GAAG,GAAQ,EAAE,IAAI,MAAM,CAAC;IAC9B,MAAM,MAAM,GAAG,UAAU,IAAI,0BAA0B,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC;IACtF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAEnC,IAAI,SAAiB,CAAC;IACtB,IAAI,CAAC;QACH,MAAM,IAAI,GAA6B,eAAe;YACpD,CAAC,CAAC;gBACE,qBAAqB,EAAE,oBAAoB,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC/E,qBAAqB,EAAE,CAAC;aACzB;YACH,CAAC,CAAC;gBACE,KAAK,EAAE,UAAU;gBACjB,QAAQ,EAAE,IAAI,CAAC,YAAY;gBAC3B,qBAAqB,EAAE,oBAAoB,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC/E,qBAAqB,EAAE,CAAC;aACzB,CAAC;QACN,SAAS,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,kCAAkC;QAClC,SAAS,GAAG,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAChG,CAAC;IAED,iCAAiC;IACjC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,CAAC,GAAG,WAAW,CAAC,QAAQ,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACZ,MAAM,GAAG,IAAI,WAAW,CAAC,aAAa,EAAE,CAAC;QAC3C,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,IAAI,CAAC,GAAG,WAAW,CAAC,aAAa,EAAE,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,OAAO,CACL,MAAC,GAAG,OAAK,IAAI,gBAAc,IAAI,CAAC,MAAM,yBAAuB,IAAI,CAAC,YAAY,aAC3E,SAAS,EACT,MAAM,IACH,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * <ProductPrice> — 商品价格
3
+ *
4
+ * 对齐 @shopify/hydrogen <ProductPrice>:
5
+ * - 渲染当前价
6
+ * - 当 compareAtPrice > price 时自动显示划线对比价
7
+ * - 支持 unit price 模式
8
+ * - 可定制 className / 容器
9
+ *
10
+ * 用法:
11
+ * <ProductPrice price={{ amount: '99', currencyCode: 'CNY' }} />
12
+ * <ProductPrice price={...} compareAtPrice={{ amount: '129', ... }} />
13
+ */
14
+ import * as React from 'react';
15
+ import { type MoneyData, type MoneyMeasurement } from './Money';
16
+ export interface ProductPriceProps extends React.HTMLAttributes<HTMLElement> {
17
+ price: MoneyData;
18
+ compareAtPrice?: MoneyData | null;
19
+ /** unit price,例如每 100g */
20
+ measurement?: MoneyMeasurement;
21
+ /** locale 透传 */
22
+ locale?: string;
23
+ /** 划线价的额外 className */
24
+ compareAtClassName?: string;
25
+ as?: keyof JSX.IntrinsicElements;
26
+ }
27
+ export declare function ProductPrice(props: ProductPriceProps): import("react/jsx-runtime").JSX.Element;
28
+ //# sourceMappingURL=ProductPrice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProductPrice.d.ts","sourceRoot":"","sources":["../../src/components/ProductPrice.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAS,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEvE,MAAM,WAAW,iBAAkB,SAAQ,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC;IAC1E,KAAK,EAAE,SAAS,CAAC;IACjB,cAAc,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,0BAA0B;IAC1B,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,gBAAgB;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,iBAAiB,CAAC;CAClC;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,2CA+BpD"}
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Money } from './Money';
3
+ export function ProductPrice(props) {
4
+ const { price, compareAtPrice, measurement, locale, compareAtClassName, as, ...rest } = props;
5
+ const Tag = as || 'div';
6
+ const hasDiscount = compareAtPrice &&
7
+ Number(compareAtPrice.amount) > Number(price.amount);
8
+ return (_jsxs(Tag, { ...rest, "data-product-price": true, children: [_jsx(Money, { data: price, locale: locale, measurement: measurement }), hasDiscount && compareAtPrice && (_jsx(Money, { data: compareAtPrice, locale: locale, as: "s", className: compareAtClassName, "data-compare-at": "true", style: { marginLeft: '0.5em', opacity: 0.6 } }))] }));
9
+ }
10
+ //# sourceMappingURL=ProductPrice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProductPrice.js","sourceRoot":"","sources":["../../src/components/ProductPrice.tsx"],"names":[],"mappings":";AAeA,OAAO,EAAE,KAAK,EAAyC,MAAM,SAAS,CAAC;AAcvE,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,EACJ,KAAK,EACL,cAAc,EACd,WAAW,EACX,MAAM,EACN,kBAAkB,EAClB,EAAE,EACF,GAAG,IAAI,EACR,GAAG,KAAK,CAAC;IAEV,MAAM,GAAG,GAAQ,EAAE,IAAI,KAAK,CAAC;IAC7B,MAAM,WAAW,GACf,cAAc;QACd,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAEvD,OAAO,CACL,MAAC,GAAG,OAAK,IAAI,yCACX,KAAC,KAAK,IAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAI,EAC/D,WAAW,IAAI,cAAc,IAAI,CAChC,KAAC,KAAK,IACJ,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EACd,EAAE,EAAC,GAAG,EACN,SAAS,EAAE,kBAAkB,qBACb,MAAM,EACtB,KAAK,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAC5C,CACH,IACG,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * <VariantSelector> — 商品 variant 选择器
3
+ *
4
+ * 对齐 @shopify/hydrogen <VariantSelector>:
5
+ * - 自动解析 variants 的 selectedOptions 维度(颜色 / 尺寸 等)
6
+ * - 渲染每个维度的选项按钮
7
+ * - 不可购买组合 disabled
8
+ * - URL 同步(可选)
9
+ * - 默认选第一个 availableForSale
10
+ *
11
+ * 设计:组件用 render prop,把每个 option 的选项交给商家渲染。
12
+ *
13
+ * 用法:
14
+ * <VariantSelector
15
+ * product={product}
16
+ * value={selectedVariantId}
17
+ * onChange={setSelectedVariantId}
18
+ * >
19
+ * {({ option, value, onSelect }) => (
20
+ * <div>
21
+ * <h4>{option.name}</h4>
22
+ * {option.values.map((v) => (
23
+ * <button
24
+ * key={v.value}
25
+ * disabled={!v.available}
26
+ * onClick={() => onSelect(v.value)}
27
+ * aria-pressed={v.value === value}
28
+ * >
29
+ * {v.value}
30
+ * </button>
31
+ * ))}
32
+ * </div>
33
+ * )}
34
+ * </VariantSelector>
35
+ */
36
+ import * as React from 'react';
37
+ interface SelectedOption {
38
+ name: string;
39
+ value: string;
40
+ }
41
+ interface ProductVariant {
42
+ id: string;
43
+ availableForSale: boolean;
44
+ selectedOptions?: SelectedOption[];
45
+ }
46
+ interface ProductLike {
47
+ options?: Array<{
48
+ name: string;
49
+ values: string[];
50
+ }>;
51
+ variants: {
52
+ nodes: ProductVariant[];
53
+ };
54
+ }
55
+ export interface VariantOption {
56
+ name: string;
57
+ values: Array<{
58
+ value: string;
59
+ available: boolean;
60
+ }>;
61
+ }
62
+ export interface VariantSelectorRenderProps {
63
+ option: VariantOption;
64
+ /** 当前已选择的 value(在这个 option 维度上) */
65
+ value: string | undefined;
66
+ /** 选择 value,触发 onChange 更新 selectedVariantId */
67
+ onSelect: (value: string) => void;
68
+ }
69
+ export interface VariantSelectorProps {
70
+ product: ProductLike;
71
+ /** 当前选中 variant 的 GID */
72
+ value: string;
73
+ /** variant 切换时调用 */
74
+ onChange?: (variantId: string) => void;
75
+ /** 每个 option 的渲染函数 */
76
+ children: (renderProps: VariantSelectorRenderProps) => React.ReactNode;
77
+ }
78
+ export declare function VariantSelector(props: VariantSelectorProps): import("react/jsx-runtime").JSX.Element;
79
+ export {};
80
+ //# sourceMappingURL=VariantSelector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VariantSelector.d.ts","sourceRoot":"","sources":["../../src/components/VariantSelector.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACpC;AAED,UAAU,WAAW;IACnB,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACpD,QAAQ,EAAE;QAAE,KAAK,EAAE,cAAc,EAAE,CAAA;KAAE,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,aAAa,CAAC;IACtB,mCAAmC;IACnC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,gDAAgD;IAChD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,WAAW,CAAC;IACrB,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,sBAAsB;IACtB,QAAQ,EAAE,CAAC,WAAW,EAAE,0BAA0B,KAAK,KAAK,CAAC,SAAS,CAAC;CACxE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAsE1D"}
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * <VariantSelector> — 商品 variant 选择器
4
+ *
5
+ * 对齐 @shopify/hydrogen <VariantSelector>:
6
+ * - 自动解析 variants 的 selectedOptions 维度(颜色 / 尺寸 等)
7
+ * - 渲染每个维度的选项按钮
8
+ * - 不可购买组合 disabled
9
+ * - URL 同步(可选)
10
+ * - 默认选第一个 availableForSale
11
+ *
12
+ * 设计:组件用 render prop,把每个 option 的选项交给商家渲染。
13
+ *
14
+ * 用法:
15
+ * <VariantSelector
16
+ * product={product}
17
+ * value={selectedVariantId}
18
+ * onChange={setSelectedVariantId}
19
+ * >
20
+ * {({ option, value, onSelect }) => (
21
+ * <div>
22
+ * <h4>{option.name}</h4>
23
+ * {option.values.map((v) => (
24
+ * <button
25
+ * key={v.value}
26
+ * disabled={!v.available}
27
+ * onClick={() => onSelect(v.value)}
28
+ * aria-pressed={v.value === value}
29
+ * >
30
+ * {v.value}
31
+ * </button>
32
+ * ))}
33
+ * </div>
34
+ * )}
35
+ * </VariantSelector>
36
+ */
37
+ import * as React from 'react';
38
+ export function VariantSelector(props) {
39
+ const { product, value, onChange, children } = props;
40
+ const variants = product.variants.nodes;
41
+ const currentVariant = variants.find((v) => v.id === value) || variants[0];
42
+ // 推断 options(如果 product.options 没传,从 variants.selectedOptions 提取)
43
+ const options = React.useMemo(() => {
44
+ if (product.options && product.options.length > 0) {
45
+ return product.options.map((opt) => ({
46
+ name: opt.name,
47
+ values: opt.values.map((v) => ({
48
+ value: v,
49
+ available: variants.some((variant) => variant.availableForSale &&
50
+ variant.selectedOptions?.some((so) => so.name === opt.name && so.value === v)),
51
+ })),
52
+ }));
53
+ }
54
+ // 从 variants.selectedOptions 反推
55
+ const dimMap = new Map();
56
+ for (const v of variants) {
57
+ for (const so of v.selectedOptions ?? []) {
58
+ if (!dimMap.has(so.name))
59
+ dimMap.set(so.name, new Set());
60
+ dimMap.get(so.name).add(so.value);
61
+ }
62
+ }
63
+ return Array.from(dimMap.entries()).map(([name, values]) => ({
64
+ name,
65
+ values: Array.from(values).map((v) => ({
66
+ value: v,
67
+ available: variants.some((variant) => variant.availableForSale &&
68
+ variant.selectedOptions?.some((so) => so.name === name && so.value === v)),
69
+ })),
70
+ }));
71
+ }, [product, variants]);
72
+ // 切换某 option 的 value:找一个匹配的 variant
73
+ const handleSelect = (optionName, optionValue) => {
74
+ const currentSelected = new Map((currentVariant?.selectedOptions ?? []).map((so) => [so.name, so.value]));
75
+ currentSelected.set(optionName, optionValue);
76
+ // 找最匹配 + available 的 variant
77
+ const target = variants.find((v) => Array.from(currentSelected.entries()).every(([n, val]) => v.selectedOptions?.some((so) => so.name === n && so.value === val)));
78
+ if (target)
79
+ onChange?.(target.id);
80
+ };
81
+ return (_jsx(_Fragment, { children: options.map((option) => (_jsx(React.Fragment, { children: children({
82
+ option,
83
+ value: currentVariant?.selectedOptions?.find((so) => so.name === option.name)?.value,
84
+ onSelect: (val) => handleSelect(option.name, val),
85
+ }) }, option.name))) }));
86
+ }
87
+ //# sourceMappingURL=VariantSelector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VariantSelector.js","sourceRoot":"","sources":["../../src/components/VariantSelector.tsx"],"names":[],"mappings":"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"}