@shopbb/helium 0.1.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.
- package/dist/client.d.ts +48 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +64 -0
- package/dist/client.js.map +1 -0
- package/dist/components/AddToCartButton.d.ts +41 -0
- package/dist/components/AddToCartButton.d.ts.map +1 -0
- package/dist/components/AddToCartButton.js +51 -0
- package/dist/components/AddToCartButton.js.map +1 -0
- package/dist/components/CartLineQuantityAdjustButton.d.ts +40 -0
- package/dist/components/CartLineQuantityAdjustButton.d.ts.map +1 -0
- package/dist/components/CartLineQuantityAdjustButton.js +58 -0
- package/dist/components/CartLineQuantityAdjustButton.js.map +1 -0
- package/dist/components/Image.d.ts +39 -0
- package/dist/components/Image.d.ts.map +1 -0
- package/dist/components/Image.js +28 -0
- package/dist/components/Image.js.map +1 -0
- package/dist/components/Money.d.ts +41 -0
- package/dist/components/Money.d.ts.map +1 -0
- package/dist/components/Money.js +48 -0
- package/dist/components/Money.js.map +1 -0
- package/dist/components/ProductPrice.d.ts +28 -0
- package/dist/components/ProductPrice.d.ts.map +1 -0
- package/dist/components/ProductPrice.js +10 -0
- package/dist/components/ProductPrice.js.map +1 -0
- package/dist/components/VariantSelector.d.ts +80 -0
- package/dist/components/VariantSelector.d.ts.map +1 -0
- package/dist/components/VariantSelector.js +87 -0
- package/dist/components/VariantSelector.js.map +1 -0
- package/dist/components/index.d.ts +24 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +18 -0
- package/dist/components/index.js.map +1 -0
- package/dist/react.d.ts +98 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +168 -0
- package/dist/react.js.map +1 -0
- package/package.json +33 -3
- package/src/client.tsx +103 -0
- package/src/components/AddToCartButton.tsx +90 -0
- package/src/components/CartLineQuantityAdjustButton.tsx +119 -0
- package/src/components/Image.tsx +93 -0
- package/src/components/Money.tsx +106 -0
- package/src/components/ProductPrice.tsx +61 -0
- package/src/components/VariantSelector.tsx +148 -0
- package/src/components/index.ts +33 -0
- package/src/react.tsx +296 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @shopbb/helium/client — Browser-side hydration helpers
|
|
3
|
+
*
|
|
4
|
+
* 商家的客户端 entry:
|
|
5
|
+
*
|
|
6
|
+
* import { hydrate } from '@shopbb/helium/client';
|
|
7
|
+
* import { App } from './app';
|
|
8
|
+
*
|
|
9
|
+
* hydrate(<App />);
|
|
10
|
+
*
|
|
11
|
+
* 行为:
|
|
12
|
+
* - 从 window.__HELIUM__ 拿 SSR 时塞进去的 boot data
|
|
13
|
+
* - 包一层 BrowserHeliumContextProvider(含 storefront.query helper,纯客户端 fetch)
|
|
14
|
+
* - 调 hydrateRoot
|
|
15
|
+
*/
|
|
16
|
+
import * as React from 'react';
|
|
17
|
+
interface ClientStorefrontClient {
|
|
18
|
+
query<T = any>(graphql: string, options?: {
|
|
19
|
+
variables?: any;
|
|
20
|
+
}): Promise<T>;
|
|
21
|
+
mutate<T = any>(graphql: string, options?: {
|
|
22
|
+
variables?: any;
|
|
23
|
+
}): Promise<T>;
|
|
24
|
+
}
|
|
25
|
+
interface ClientHeliumContext {
|
|
26
|
+
storefront: ClientStorefrontClient;
|
|
27
|
+
url: string;
|
|
28
|
+
}
|
|
29
|
+
declare global {
|
|
30
|
+
interface Window {
|
|
31
|
+
__HELIUM__?: {
|
|
32
|
+
storefront: {
|
|
33
|
+
apiUrl: string;
|
|
34
|
+
publicAccessToken: string;
|
|
35
|
+
storeId: string;
|
|
36
|
+
};
|
|
37
|
+
url: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export declare function useHeliumClient(): ClientHeliumContext;
|
|
42
|
+
/**
|
|
43
|
+
* 客户端 hydrate 入口。
|
|
44
|
+
* 通常调用一次即可。
|
|
45
|
+
*/
|
|
46
|
+
export declare function hydrate(app: React.ReactElement): void;
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAS/B,UAAU,sBAAsB;IAC9B,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3E,MAAM,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC7E;AAED,UAAU,mBAAmB;IAC3B,UAAU,EAAE,sBAAsB,CAAC;IACnC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,UAAU,CAAC,EAAE;YACX,UAAU,EAAE;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,iBAAiB,EAAE,MAAM,CAAC;gBAAC,OAAO,EAAE,MAAM,CAAA;aAAE,CAAC;YAC3E,GAAG,EAAE,MAAM,CAAC;SACb,CAAC;KACH;CACF;AAID,wBAAgB,eAAe,IAAI,mBAAmB,CAQrD;AA4BD;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,YAAY,QAgB9C"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @shopbb/helium/client — Browser-side hydration helpers
|
|
3
|
+
*
|
|
4
|
+
* 商家的客户端 entry:
|
|
5
|
+
*
|
|
6
|
+
* import { hydrate } from '@shopbb/helium/client';
|
|
7
|
+
* import { App } from './app';
|
|
8
|
+
*
|
|
9
|
+
* hydrate(<App />);
|
|
10
|
+
*
|
|
11
|
+
* 行为:
|
|
12
|
+
* - 从 window.__HELIUM__ 拿 SSR 时塞进去的 boot data
|
|
13
|
+
* - 包一层 BrowserHeliumContextProvider(含 storefront.query helper,纯客户端 fetch)
|
|
14
|
+
* - 调 hydrateRoot
|
|
15
|
+
*/
|
|
16
|
+
import * as React from 'react';
|
|
17
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
18
|
+
const ClientHeliumReactContext = React.createContext(null);
|
|
19
|
+
export function useHeliumClient() {
|
|
20
|
+
const ctx = React.useContext(ClientHeliumReactContext);
|
|
21
|
+
if (!ctx) {
|
|
22
|
+
throw new Error('useHeliumClient() must be called within HeliumClientProvider (use hydrate())');
|
|
23
|
+
}
|
|
24
|
+
return ctx;
|
|
25
|
+
}
|
|
26
|
+
function makeClientStorefront(boot) {
|
|
27
|
+
async function call(graphql, variables) {
|
|
28
|
+
const res = await fetch(boot.apiUrl, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'X-Storefront-Access-Token': boot.publicAccessToken,
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({ query: graphql, variables }),
|
|
35
|
+
});
|
|
36
|
+
const json = (await res.json());
|
|
37
|
+
if (json.errors) {
|
|
38
|
+
throw new Error(json.errors[0]?.message || 'GraphQL error');
|
|
39
|
+
}
|
|
40
|
+
return json.data;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
query: (q, opts) => call(q, opts?.variables),
|
|
44
|
+
mutate: (q, opts) => call(q, opts?.variables),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 客户端 hydrate 入口。
|
|
49
|
+
* 通常调用一次即可。
|
|
50
|
+
*/
|
|
51
|
+
export function hydrate(app) {
|
|
52
|
+
const boot = window.__HELIUM__;
|
|
53
|
+
if (!boot) {
|
|
54
|
+
console.warn('[helium] window.__HELIUM__ missing, hydrate skipped');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const ctx = {
|
|
58
|
+
storefront: makeClientStorefront(boot.storefront),
|
|
59
|
+
url: boot.url,
|
|
60
|
+
};
|
|
61
|
+
const wrapped = React.createElement(ClientHeliumReactContext.Provider, { value: ctx }, app);
|
|
62
|
+
hydrateRoot(document.getElementById('root'), wrapped);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AA2B/C,MAAM,wBAAwB,GAAG,KAAK,CAAC,aAAa,CAA6B,IAAI,CAAC,CAAC;AAEvF,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,wBAAwB,CAAC,CAAC;IACvD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,8EAA8E,CAC/E,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,oBAAoB,CAAC,IAI7B;IACC,KAAK,UAAU,IAAI,CAAI,OAAe,EAAE,SAAe;QACrD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE;YACnC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,2BAA2B,EAAE,IAAI,CAAC,iBAAiB;aACpD;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;SACpD,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAsD,CAAC;QACrF,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,eAAe,CAAC,CAAC;QAC9D,CAAC;QACD,OAAO,IAAI,CAAC,IAAS,CAAC;IACxB,CAAC;IACD,OAAO;QACL,KAAK,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC;QAC5C,MAAM,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC;KAC9C,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,GAAuB;IAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC;IAC/B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,CAAC,IAAI,CAAC,qDAAqD,CAAC,CAAC;QACpE,OAAO;IACT,CAAC;IACD,MAAM,GAAG,GAAwB;QAC/B,UAAU,EAAE,oBAAoB,CAAC,IAAI,CAAC,UAAU,CAAC;QACjD,GAAG,EAAE,IAAI,CAAC,GAAG;KACd,CAAC;IACF,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,CACjC,wBAAwB,CAAC,QAAQ,EACjC,EAAE,KAAK,EAAE,GAAG,EAAE,EACd,GAAG,CACJ,CAAC;IACF,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAE,EAAE,OAAO,CAAC,CAAC;AACzD,CAAC"}
|
|
@@ -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
|