@shopbb/helium 0.3.0-alpha.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/components/AddToCartButton.d.ts.map +1 -1
  2. package/dist/components/AddToCartButton.js +17 -5
  3. package/dist/components/AddToCartButton.js.map +1 -1
  4. package/dist/components/AnalyticsProvider.d.ts +106 -0
  5. package/dist/components/AnalyticsProvider.d.ts.map +1 -0
  6. package/dist/components/AnalyticsProvider.js +135 -0
  7. package/dist/components/AnalyticsProvider.js.map +1 -0
  8. package/dist/components/CartProvider.d.ts +111 -0
  9. package/dist/components/CartProvider.d.ts.map +1 -0
  10. package/dist/components/CartProvider.js +263 -0
  11. package/dist/components/CartProvider.js.map +1 -0
  12. package/dist/components/Money.d.ts.map +1 -1
  13. package/dist/components/Money.js +6 -1
  14. package/dist/components/Money.js.map +1 -1
  15. package/dist/components/ProductOptionsProvider.d.ts +70 -0
  16. package/dist/components/ProductOptionsProvider.d.ts.map +1 -0
  17. package/dist/components/ProductOptionsProvider.js +93 -0
  18. package/dist/components/ProductOptionsProvider.js.map +1 -0
  19. package/dist/components/ShopProvider.d.ts +49 -0
  20. package/dist/components/ShopProvider.d.ts.map +1 -0
  21. package/dist/components/ShopProvider.js +62 -0
  22. package/dist/components/ShopProvider.js.map +1 -0
  23. package/dist/components/index.d.ts +18 -1
  24. package/dist/components/index.d.ts.map +1 -1
  25. package/dist/components/index.js +16 -1
  26. package/dist/components/index.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/components/AddToCartButton.tsx +16 -5
  29. package/src/components/AnalyticsProvider.tsx +175 -0
  30. package/src/components/CartProvider.tsx +378 -0
  31. package/src/components/Money.tsx +7 -1
  32. package/src/components/ProductOptionsProvider.tsx +149 -0
  33. package/src/components/ShopProvider.tsx +86 -0
  34. package/src/components/index.ts +39 -1
@@ -0,0 +1,149 @@
1
+ /**
2
+ * <ProductOptionsProvider> + useProductOptions()
3
+ *
4
+ * 给商品详情页用。包住 product 后,自动管理 selectedVariantId 状态 +
5
+ * 暴露当前 variant、available options、setOption 等。
6
+ *
7
+ * 用法:
8
+ * <ProductOptionsProvider product={product}>
9
+ * <ProductDetailBody />
10
+ * </ProductOptionsProvider>
11
+ *
12
+ * function ProductDetailBody() {
13
+ * const { selectedVariant, options, setOptionValue } = useProductOptions();
14
+ * return ...;
15
+ * }
16
+ */
17
+
18
+ import * as React from 'react';
19
+
20
+ interface SelectedOption { name: string; value: string; }
21
+
22
+ interface ProductVariantLike {
23
+ id: string;
24
+ availableForSale: boolean;
25
+ selectedOptions?: SelectedOption[];
26
+ price?: { amount: string; currencyCode: string };
27
+ compareAtPrice?: { amount: string; currencyCode: string } | null;
28
+ }
29
+
30
+ interface ProductLike {
31
+ id: string;
32
+ options?: Array<{ name: string; values: string[] }>;
33
+ variants: { nodes: ProductVariantLike[] };
34
+ }
35
+
36
+ export interface ProductOptionItem {
37
+ name: string;
38
+ values: Array<{ value: string; available: boolean; isSelected: boolean }>;
39
+ }
40
+
41
+ export interface ProductOptionsContextValue {
42
+ product: ProductLike;
43
+ selectedVariant: ProductVariantLike | null;
44
+ selectedOptions: Record<string, string>;
45
+ options: ProductOptionItem[];
46
+ setOptionValue: (name: string, value: string) => void;
47
+ setVariantById: (variantId: string) => void;
48
+ }
49
+
50
+ const Ctx = React.createContext<ProductOptionsContextValue | null>(null);
51
+
52
+ export interface ProductOptionsProviderProps {
53
+ product: ProductLike;
54
+ /** 初始 selectedVariantId;默认第一个可购 variant */
55
+ initialVariantId?: string;
56
+ children: React.ReactNode;
57
+ }
58
+
59
+ export function ProductOptionsProvider(props: ProductOptionsProviderProps) {
60
+ const { product, initialVariantId, children } = props;
61
+ const variants = product.variants.nodes;
62
+
63
+ const findDefault = React.useCallback(() => {
64
+ if (initialVariantId) {
65
+ const found = variants.find((v) => v.id === initialVariantId);
66
+ if (found) return found;
67
+ }
68
+ return variants.find((v) => v.availableForSale) ?? variants[0] ?? null;
69
+ }, [initialVariantId, variants]);
70
+
71
+ const [selectedVariantId, setSelectedVariantId] = React.useState<string | null>(
72
+ findDefault()?.id ?? null,
73
+ );
74
+
75
+ React.useEffect(() => {
76
+ setSelectedVariantId(findDefault()?.id ?? null);
77
+ }, [findDefault]);
78
+
79
+ const selectedVariant =
80
+ variants.find((v) => v.id === selectedVariantId) ?? null;
81
+
82
+ const selectedOptions: Record<string, string> = React.useMemo(() => {
83
+ const m: Record<string, string> = {};
84
+ for (const so of selectedVariant?.selectedOptions ?? []) m[so.name] = so.value;
85
+ return m;
86
+ }, [selectedVariant]);
87
+
88
+ const options: ProductOptionItem[] = React.useMemo(() => {
89
+ const dimMap = new Map<string, Set<string>>();
90
+ // 收集所有 option 维度
91
+ if (product.options?.length) {
92
+ for (const opt of product.options) {
93
+ dimMap.set(opt.name, new Set(opt.values));
94
+ }
95
+ } else {
96
+ for (const v of variants) {
97
+ for (const so of v.selectedOptions ?? []) {
98
+ if (!dimMap.has(so.name)) dimMap.set(so.name, new Set());
99
+ dimMap.get(so.name)!.add(so.value);
100
+ }
101
+ }
102
+ }
103
+ return Array.from(dimMap.entries()).map(([name, vals]) => ({
104
+ name,
105
+ values: Array.from(vals).map((v) => ({
106
+ value: v,
107
+ available: variants.some(
108
+ (variant) =>
109
+ variant.availableForSale &&
110
+ variant.selectedOptions?.some((so) => so.name === name && so.value === v),
111
+ ),
112
+ isSelected: selectedOptions[name] === v,
113
+ })),
114
+ }));
115
+ }, [product, variants, selectedOptions]);
116
+
117
+ const setOptionValue = React.useCallback(
118
+ (name: string, value: string) => {
119
+ const next = { ...selectedOptions, [name]: value };
120
+ // 找匹配 variant
121
+ const target = variants.find((v) =>
122
+ Object.entries(next).every(([n, val]) =>
123
+ v.selectedOptions?.some((so) => so.name === n && so.value === val),
124
+ ),
125
+ );
126
+ if (target) setSelectedVariantId(target.id);
127
+ },
128
+ [selectedOptions, variants],
129
+ );
130
+
131
+ const setVariantById = React.useCallback((id: string) => setSelectedVariantId(id), []);
132
+
133
+ const value: ProductOptionsContextValue = {
134
+ product,
135
+ selectedVariant,
136
+ selectedOptions,
137
+ options,
138
+ setOptionValue,
139
+ setVariantById,
140
+ };
141
+
142
+ return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
143
+ }
144
+
145
+ export function useProductOptions(): ProductOptionsContextValue {
146
+ const v = React.useContext(Ctx);
147
+ if (!v) throw new Error('useProductOptions must be used inside <ProductOptionsProvider>');
148
+ return v;
149
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * <ShopProvider> + useShop()
3
+ *
4
+ * 全局店铺上下文 — 注入 storefront token / apiUrl / 货币 / locale / shop 元信息。
5
+ * 子组件用 useShop() 直接拿,不用 prop-drill。
6
+ *
7
+ * 用法:
8
+ * <ShopProvider
9
+ * storeId="store_xxx"
10
+ * storefrontAccessToken="shpat_pub_..."
11
+ * apiUrl="https://api.example.com/api/2026-04/graphql.json"
12
+ * shopName="Shopflare"
13
+ * currencyCode="CNY"
14
+ * locale="zh-CN"
15
+ * >
16
+ * <App />
17
+ * </ShopProvider>
18
+ *
19
+ * // 任意子组件:
20
+ * const { storeId, storefrontAccessToken } = useShop();
21
+ */
22
+
23
+ import * as React from 'react';
24
+
25
+ export interface ShopContextValue {
26
+ storeId: string;
27
+ storefrontAccessToken: string;
28
+ apiUrl: string;
29
+ shopName: string;
30
+ currencyCode: string;
31
+ locale: string;
32
+ /** 额外自定义字段,用户可塞任意业务 meta */
33
+ meta?: Record<string, any>;
34
+ }
35
+
36
+ const ShopContext = React.createContext<ShopContextValue | null>(null);
37
+
38
+ export interface ShopProviderProps extends Partial<ShopContextValue> {
39
+ storeId: string;
40
+ storefrontAccessToken: string;
41
+ apiUrl: string;
42
+ children: React.ReactNode;
43
+ }
44
+
45
+ export function ShopProvider(props: ShopProviderProps) {
46
+ const value = React.useMemo<ShopContextValue>(
47
+ () => ({
48
+ storeId: props.storeId,
49
+ storefrontAccessToken: props.storefrontAccessToken,
50
+ apiUrl: props.apiUrl,
51
+ shopName: props.shopName ?? '',
52
+ currencyCode: props.currencyCode ?? 'USD',
53
+ locale: props.locale ?? 'en-US',
54
+ meta: props.meta,
55
+ }),
56
+ [
57
+ props.storeId,
58
+ props.storefrontAccessToken,
59
+ props.apiUrl,
60
+ props.shopName,
61
+ props.currencyCode,
62
+ props.locale,
63
+ props.meta,
64
+ ],
65
+ );
66
+ return <ShopContext.Provider value={value}>{props.children}</ShopContext.Provider>;
67
+ }
68
+
69
+ /**
70
+ * 拿当前 shop context。Provider 外调用会 throw。
71
+ */
72
+ export function useShop(): ShopContextValue {
73
+ const v = React.useContext(ShopContext);
74
+ if (!v) {
75
+ throw new Error('useShop must be used inside <ShopProvider>');
76
+ }
77
+ return v;
78
+ }
79
+
80
+ /**
81
+ * 非 throw 版本:Provider 外返回 null。
82
+ * 给组件做"可选 fallback"用,比如 <Money> 在没 ShopProvider 时也要能跑。
83
+ */
84
+ export function useShopOptional(): ShopContextValue | null {
85
+ return React.useContext(ShopContext);
86
+ }
@@ -2,14 +2,52 @@
2
2
  * @shopbb/helium/components — 商家用 React 组件
3
3
  *
4
4
  * 用法:
5
- * import { Money, Image, ProductPrice, AddToCartButton } from '@shopbb/helium/components';
5
+ * import {
6
+ * ShopProvider, CartProvider, AnalyticsProvider,
7
+ * useShop, useCart, useAnalytics,
8
+ * Money, Image, ProductPrice,
9
+ * AddToCartButton, CartLineQuantityAdjustButton,
10
+ * VariantSelector, ProductOptionsProvider, useProductOptions,
11
+ * Analytics,
12
+ * } from '@shopbb/helium/components';
6
13
  *
7
14
  * 设计原则:
8
15
  * - 无样式:组件只管行为 + 语义化 DOM,样式商家自己写
9
16
  * - data-* 钩子:方便选择器
10
17
  * - 对齐 Shopify Hydrogen 同名组件的 API,迁移成本低
18
+ * - Provider 链:<ShopProvider> > <CartProvider> > <AnalyticsProvider> > App
19
+ * 组件优先从 Provider 拿;Provider 缺省时回退到 props 或 noop
11
20
  */
12
21
 
22
+ // Providers + hooks
23
+ export { ShopProvider, useShop, useShopOptional } from './ShopProvider';
24
+ export type { ShopContextValue, ShopProviderProps } from './ShopProvider';
25
+
26
+ export { CartProvider, useCart, useCartOptional } from './CartProvider';
27
+ export type {
28
+ Cart,
29
+ CartLine,
30
+ CartLineMerchandise,
31
+ CartStatus,
32
+ CartUserError,
33
+ CartContextValue,
34
+ CartProviderProps,
35
+ } from './CartProvider';
36
+
37
+ export {
38
+ ProductOptionsProvider,
39
+ useProductOptions,
40
+ } from './ProductOptionsProvider';
41
+ export type {
42
+ ProductOptionsContextValue,
43
+ ProductOptionsProviderProps,
44
+ ProductOptionItem,
45
+ } from './ProductOptionsProvider';
46
+
47
+ export { AnalyticsProvider, useAnalytics, Analytics } from './AnalyticsProvider';
48
+ export type { AnalyticsEvent, AnalyticsContextValue, AnalyticsProviderProps } from './AnalyticsProvider';
49
+
50
+ // 展示型组件
13
51
  export { Money } from './Money';
14
52
  export type { MoneyProps, MoneyData, MoneyMeasurement } from './Money';
15
53