@numueg/theme-sdk 0.1.2 → 0.2.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.
@@ -1,4 +1,4 @@
1
- import { T as ThemeSettingsV3 } from './theme-NWJAU9jd.mjs';
1
+ import { T as ThemeSettingsV3 } from './theme-D0QybTQS.mjs';
2
2
 
3
3
  /**
4
4
  * Dual-Read normalization: converts V1/V2 legacy payloads to V3 in memory.
@@ -1,4 +1,4 @@
1
- import { T as ThemeSettingsV3 } from './theme-NWJAU9jd.js';
1
+ import { T as ThemeSettingsV3 } from './theme-D0QybTQS.js';
2
2
 
3
3
  /**
4
4
  * Dual-Read normalization: converts V1/V2 legacy payloads to V3 in memory.
@@ -0,0 +1,197 @@
1
+ /** V3 Theme Settings — the canonical data shape */
2
+ interface ThemeSettingsV3 {
3
+ schema_version: 3;
4
+ theme_id: string;
5
+ global_settings: Record<string, any>;
6
+ templates: Record<string, PageTemplate>;
7
+ section_groups: Record<string, SectionGroup>;
8
+ external_theme?: ExternalThemeMetadata | null;
9
+ }
10
+ interface PageTemplate {
11
+ name: string;
12
+ sections: Record<string, SectionInstance>;
13
+ order: string[];
14
+ }
15
+ interface SectionGroup {
16
+ name: string;
17
+ sections: Record<string, SectionInstance>;
18
+ order: string[];
19
+ }
20
+ interface SectionInstance {
21
+ type: string;
22
+ settings: Record<string, any>;
23
+ disabled?: boolean;
24
+ blocks?: Record<string, BlockInstance>;
25
+ block_order?: string[];
26
+ }
27
+ interface BlockInstance {
28
+ type: string;
29
+ settings: Record<string, any>;
30
+ disabled?: boolean;
31
+ /** Nested child blocks (blocks-in-blocks): a block can hold its own
32
+ * block container — e.g. a footer "column" holding "link" blocks, or
33
+ * a mega-menu. Same shape as a section's block container, so the
34
+ * render/editor walk is uniform at any depth. Optional + absent on
35
+ * leaf blocks, so pre-nesting payloads load unchanged. */
36
+ blocks?: Record<string, BlockInstance>;
37
+ block_order?: string[];
38
+ }
39
+ interface ExternalThemeMetadata {
40
+ bundle_url: string;
41
+ css_url?: string | null;
42
+ mode?: string;
43
+ settings_schema?: Record<string, any> | null;
44
+ section_schemas?: Record<string, any> | null;
45
+ }
46
+ /**
47
+ * Live mount handle returned by a V3 theme bundle's `mount(el, ctx)`.
48
+ *
49
+ * Two shapes accepted by the host:
50
+ *
51
+ * 1. **Legacy** — `mount(el, ctx): () => void`
52
+ * The bundle returns only a cleanup function. Every host-side
53
+ * themeSettings change forces a full unmount + remount, which is
54
+ * expensive but always correct. Hosts must keep supporting this
55
+ * for older bundles built against pre-0.2 SDK versions.
56
+ *
57
+ * 2. **Wave 3+** — `mount(el, ctx): MountResult`
58
+ * The bundle returns an object with `cleanup` + an `applyDraft`
59
+ * method that takes a fresh `ThemeSettingsV3` and re-renders the
60
+ * bundle's React tree in-place (no createRoot churn). This is the
61
+ * fast path used by the customizer's live preview — a keystroke
62
+ * in the dashboard becomes a single React reconciliation in the
63
+ * iframe, typically < 16 ms.
64
+ *
65
+ * Hosts feature-detect by checking `typeof handle === "function"`
66
+ * (legacy) vs `typeof handle === "object" && handle.cleanup`
67
+ * (Wave 3+). Theme authors should prefer the MountResult shape — the
68
+ * `numuTheme` plugin's typings will flag the missing applyDraft once
69
+ * the editor's live-preview path requires it.
70
+ */
71
+ interface MountResult {
72
+ /** Unmount the bundle's React tree and release any resources. */
73
+ cleanup: () => void;
74
+ /**
75
+ * Apply a fresh draft (e.g. from the customizer iframe's
76
+ * `numu:theme:update` postMessage) without tearing down the tree.
77
+ * Implementations typically forward the value to a useState setter
78
+ * inside a wrapper that renders `<NuMuProvider themeSettings={...}>`.
79
+ *
80
+ * Safe to call repeatedly; the bundle is expected to dedup
81
+ * reference-equal calls on its own. Returns nothing — selection /
82
+ * navigation echo back via postMessage.
83
+ */
84
+ applyDraft: (next: ThemeSettingsV3) => void;
85
+ }
86
+ /** Section schema for customizer form generation */
87
+ interface SectionSchema {
88
+ type: string;
89
+ name: string;
90
+ name_ar?: string;
91
+ tag?: string;
92
+ class?: string;
93
+ limit?: number;
94
+ settings: SettingDefinition[];
95
+ blocks?: BlockSchema[];
96
+ max_blocks?: number;
97
+ presets?: SectionPreset[];
98
+ }
99
+ interface BlockSchema {
100
+ type: string;
101
+ name: string;
102
+ name_ar?: string;
103
+ limit?: number;
104
+ settings: SettingDefinition[];
105
+ /** Child block types this block accepts (recursive). When present,
106
+ * the customizer lets merchants add/remove/reorder these inside the
107
+ * block, up to `max_blocks`. Enables footer columns, mega-menus,
108
+ * multi-column layouts. The host caps practical nesting depth
109
+ * (MAX_BLOCK_DEPTH). */
110
+ blocks?: BlockSchema[];
111
+ max_blocks?: number;
112
+ }
113
+ /** Max nesting depth the customizer allows for blocks-in-blocks. Depth
114
+ * 1 = a top-level block in a section; a block at this depth can't take
115
+ * children. Generous (below Shopify's 8) and cheap to raise. Kept in
116
+ * sync with the merchant hub's MAX_BLOCK_DEPTH. */
117
+ declare const MAX_BLOCK_DEPTH = 5;
118
+ /**
119
+ * Setting types the V3 customizer renders. Themes can declare any
120
+ * string here; unknown types fall through to a plain text input. The
121
+ * union below documents the canonical set so theme authors get
122
+ * autocomplete and TypeScript errors on typos.
123
+ */
124
+ type SettingType = "text" | "textarea" | "richtext" | "number" | "range" | "color" | "checkbox" | "select" | "radio" | "font" | "image_picker" | "url" | "product" | "product_list" | "collection" | "collection_list" | "header" | "paragraph" | "html" | "date" | "time" | "video_picker" | "color_scheme" | "page_picker" | "blog_picker" | "link_list_picker" | "variant_picker" | "file_upload" | "icon_picker" | "icon";
125
+ /**
126
+ * `visible_if` — conditional visibility expression evaluated against
127
+ * sibling settings in the same schema. Two flavors:
128
+ *
129
+ * - String DSL: `"settings.show_button == true && settings.layout != 'minimal'"`
130
+ * - Object form: `{ show_button: true, layout: ["full", "split"] }`
131
+ *
132
+ * Evaluator lives in the merchant hub: any setting whose expression is
133
+ * falsy is skipped from the rendered form.
134
+ */
135
+ type VisibleIf = string | Record<string, unknown>;
136
+ interface SettingDefinition {
137
+ type: SettingType | (string & {});
138
+ id: string;
139
+ label: string;
140
+ label_ar?: string;
141
+ default?: any;
142
+ info?: string;
143
+ info_ar?: string;
144
+ placeholder?: string;
145
+ options?: {
146
+ value: string;
147
+ label: string;
148
+ label_ar?: string;
149
+ }[];
150
+ min?: number;
151
+ max?: number;
152
+ step?: number;
153
+ unit?: string;
154
+ /** Hide this setting unless the expression evaluates truthy. */
155
+ visible_if?: VisibleIf;
156
+ }
157
+ interface PresetBlock {
158
+ type: string;
159
+ settings?: Record<string, any>;
160
+ /** Nested starter blocks materialized when the preset is applied
161
+ * (recursive). Lets a preset ship, e.g., a footer column already
162
+ * populated with link blocks. */
163
+ blocks?: PresetBlock[];
164
+ }
165
+ interface SectionPreset {
166
+ name: string;
167
+ /** Localized names so the Add Section dialog reads in the editor's
168
+ * current locale. Falls back to `name` when the locale is missing. */
169
+ locales?: {
170
+ en?: {
171
+ name?: string;
172
+ };
173
+ ar?: {
174
+ name?: string;
175
+ };
176
+ };
177
+ settings?: Record<string, any>;
178
+ blocks?: PresetBlock[];
179
+ }
180
+ /** Section component props */
181
+ interface SectionProps {
182
+ settings: Record<string, any>;
183
+ blocks?: Record<string, BlockInstance>;
184
+ blockOrder?: string[];
185
+ storeData?: any;
186
+ }
187
+ /** Block component props */
188
+ interface BlockProps {
189
+ settings: Record<string, any>;
190
+ /** Child block instances when this block nests others (footer column,
191
+ * mega-menu, …). Mirror the section→block render: map `blockOrder`
192
+ * and look each id up in `blocks`, wrapping each child in `<Block>`. */
193
+ blocks?: Record<string, BlockInstance>;
194
+ blockOrder?: string[];
195
+ }
196
+
197
+ export { type BlockInstance as B, type ExternalThemeMetadata as E, type MountResult as M, type PageTemplate as P, type SectionGroup as S, type ThemeSettingsV3 as T, type BlockProps as a, type BlockSchema as b, type SectionInstance as c, type SectionPreset as d, type SectionProps as e, type SectionSchema as f, type SettingDefinition as g, MAX_BLOCK_DEPTH as h, type PresetBlock as i };
@@ -0,0 +1,197 @@
1
+ /** V3 Theme Settings — the canonical data shape */
2
+ interface ThemeSettingsV3 {
3
+ schema_version: 3;
4
+ theme_id: string;
5
+ global_settings: Record<string, any>;
6
+ templates: Record<string, PageTemplate>;
7
+ section_groups: Record<string, SectionGroup>;
8
+ external_theme?: ExternalThemeMetadata | null;
9
+ }
10
+ interface PageTemplate {
11
+ name: string;
12
+ sections: Record<string, SectionInstance>;
13
+ order: string[];
14
+ }
15
+ interface SectionGroup {
16
+ name: string;
17
+ sections: Record<string, SectionInstance>;
18
+ order: string[];
19
+ }
20
+ interface SectionInstance {
21
+ type: string;
22
+ settings: Record<string, any>;
23
+ disabled?: boolean;
24
+ blocks?: Record<string, BlockInstance>;
25
+ block_order?: string[];
26
+ }
27
+ interface BlockInstance {
28
+ type: string;
29
+ settings: Record<string, any>;
30
+ disabled?: boolean;
31
+ /** Nested child blocks (blocks-in-blocks): a block can hold its own
32
+ * block container — e.g. a footer "column" holding "link" blocks, or
33
+ * a mega-menu. Same shape as a section's block container, so the
34
+ * render/editor walk is uniform at any depth. Optional + absent on
35
+ * leaf blocks, so pre-nesting payloads load unchanged. */
36
+ blocks?: Record<string, BlockInstance>;
37
+ block_order?: string[];
38
+ }
39
+ interface ExternalThemeMetadata {
40
+ bundle_url: string;
41
+ css_url?: string | null;
42
+ mode?: string;
43
+ settings_schema?: Record<string, any> | null;
44
+ section_schemas?: Record<string, any> | null;
45
+ }
46
+ /**
47
+ * Live mount handle returned by a V3 theme bundle's `mount(el, ctx)`.
48
+ *
49
+ * Two shapes accepted by the host:
50
+ *
51
+ * 1. **Legacy** — `mount(el, ctx): () => void`
52
+ * The bundle returns only a cleanup function. Every host-side
53
+ * themeSettings change forces a full unmount + remount, which is
54
+ * expensive but always correct. Hosts must keep supporting this
55
+ * for older bundles built against pre-0.2 SDK versions.
56
+ *
57
+ * 2. **Wave 3+** — `mount(el, ctx): MountResult`
58
+ * The bundle returns an object with `cleanup` + an `applyDraft`
59
+ * method that takes a fresh `ThemeSettingsV3` and re-renders the
60
+ * bundle's React tree in-place (no createRoot churn). This is the
61
+ * fast path used by the customizer's live preview — a keystroke
62
+ * in the dashboard becomes a single React reconciliation in the
63
+ * iframe, typically < 16 ms.
64
+ *
65
+ * Hosts feature-detect by checking `typeof handle === "function"`
66
+ * (legacy) vs `typeof handle === "object" && handle.cleanup`
67
+ * (Wave 3+). Theme authors should prefer the MountResult shape — the
68
+ * `numuTheme` plugin's typings will flag the missing applyDraft once
69
+ * the editor's live-preview path requires it.
70
+ */
71
+ interface MountResult {
72
+ /** Unmount the bundle's React tree and release any resources. */
73
+ cleanup: () => void;
74
+ /**
75
+ * Apply a fresh draft (e.g. from the customizer iframe's
76
+ * `numu:theme:update` postMessage) without tearing down the tree.
77
+ * Implementations typically forward the value to a useState setter
78
+ * inside a wrapper that renders `<NuMuProvider themeSettings={...}>`.
79
+ *
80
+ * Safe to call repeatedly; the bundle is expected to dedup
81
+ * reference-equal calls on its own. Returns nothing — selection /
82
+ * navigation echo back via postMessage.
83
+ */
84
+ applyDraft: (next: ThemeSettingsV3) => void;
85
+ }
86
+ /** Section schema for customizer form generation */
87
+ interface SectionSchema {
88
+ type: string;
89
+ name: string;
90
+ name_ar?: string;
91
+ tag?: string;
92
+ class?: string;
93
+ limit?: number;
94
+ settings: SettingDefinition[];
95
+ blocks?: BlockSchema[];
96
+ max_blocks?: number;
97
+ presets?: SectionPreset[];
98
+ }
99
+ interface BlockSchema {
100
+ type: string;
101
+ name: string;
102
+ name_ar?: string;
103
+ limit?: number;
104
+ settings: SettingDefinition[];
105
+ /** Child block types this block accepts (recursive). When present,
106
+ * the customizer lets merchants add/remove/reorder these inside the
107
+ * block, up to `max_blocks`. Enables footer columns, mega-menus,
108
+ * multi-column layouts. The host caps practical nesting depth
109
+ * (MAX_BLOCK_DEPTH). */
110
+ blocks?: BlockSchema[];
111
+ max_blocks?: number;
112
+ }
113
+ /** Max nesting depth the customizer allows for blocks-in-blocks. Depth
114
+ * 1 = a top-level block in a section; a block at this depth can't take
115
+ * children. Generous (below Shopify's 8) and cheap to raise. Kept in
116
+ * sync with the merchant hub's MAX_BLOCK_DEPTH. */
117
+ declare const MAX_BLOCK_DEPTH = 5;
118
+ /**
119
+ * Setting types the V3 customizer renders. Themes can declare any
120
+ * string here; unknown types fall through to a plain text input. The
121
+ * union below documents the canonical set so theme authors get
122
+ * autocomplete and TypeScript errors on typos.
123
+ */
124
+ type SettingType = "text" | "textarea" | "richtext" | "number" | "range" | "color" | "checkbox" | "select" | "radio" | "font" | "image_picker" | "url" | "product" | "product_list" | "collection" | "collection_list" | "header" | "paragraph" | "html" | "date" | "time" | "video_picker" | "color_scheme" | "page_picker" | "blog_picker" | "link_list_picker" | "variant_picker" | "file_upload" | "icon_picker" | "icon";
125
+ /**
126
+ * `visible_if` — conditional visibility expression evaluated against
127
+ * sibling settings in the same schema. Two flavors:
128
+ *
129
+ * - String DSL: `"settings.show_button == true && settings.layout != 'minimal'"`
130
+ * - Object form: `{ show_button: true, layout: ["full", "split"] }`
131
+ *
132
+ * Evaluator lives in the merchant hub: any setting whose expression is
133
+ * falsy is skipped from the rendered form.
134
+ */
135
+ type VisibleIf = string | Record<string, unknown>;
136
+ interface SettingDefinition {
137
+ type: SettingType | (string & {});
138
+ id: string;
139
+ label: string;
140
+ label_ar?: string;
141
+ default?: any;
142
+ info?: string;
143
+ info_ar?: string;
144
+ placeholder?: string;
145
+ options?: {
146
+ value: string;
147
+ label: string;
148
+ label_ar?: string;
149
+ }[];
150
+ min?: number;
151
+ max?: number;
152
+ step?: number;
153
+ unit?: string;
154
+ /** Hide this setting unless the expression evaluates truthy. */
155
+ visible_if?: VisibleIf;
156
+ }
157
+ interface PresetBlock {
158
+ type: string;
159
+ settings?: Record<string, any>;
160
+ /** Nested starter blocks materialized when the preset is applied
161
+ * (recursive). Lets a preset ship, e.g., a footer column already
162
+ * populated with link blocks. */
163
+ blocks?: PresetBlock[];
164
+ }
165
+ interface SectionPreset {
166
+ name: string;
167
+ /** Localized names so the Add Section dialog reads in the editor's
168
+ * current locale. Falls back to `name` when the locale is missing. */
169
+ locales?: {
170
+ en?: {
171
+ name?: string;
172
+ };
173
+ ar?: {
174
+ name?: string;
175
+ };
176
+ };
177
+ settings?: Record<string, any>;
178
+ blocks?: PresetBlock[];
179
+ }
180
+ /** Section component props */
181
+ interface SectionProps {
182
+ settings: Record<string, any>;
183
+ blocks?: Record<string, BlockInstance>;
184
+ blockOrder?: string[];
185
+ storeData?: any;
186
+ }
187
+ /** Block component props */
188
+ interface BlockProps {
189
+ settings: Record<string, any>;
190
+ /** Child block instances when this block nests others (footer column,
191
+ * mega-menu, …). Mirror the section→block render: map `blockOrder`
192
+ * and look each id up in `blocks`, wrapping each child in `<Block>`. */
193
+ blocks?: Record<string, BlockInstance>;
194
+ blockOrder?: string[];
195
+ }
196
+
197
+ export { type BlockInstance as B, type ExternalThemeMetadata as E, type MountResult as M, type PageTemplate as P, type SectionGroup as S, type ThemeSettingsV3 as T, type BlockProps as a, type BlockSchema as b, type SectionInstance as c, type SectionPreset as d, type SectionProps as e, type SectionSchema as f, type SettingDefinition as g, MAX_BLOCK_DEPTH as h, type PresetBlock as i };
package/dist/types.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export { A as Address, a as Cart, e as CartItem, C as Collection, b as Customer, O as Order, f as OrderItem, c as Page, P as Product, g as ProductImage, d as ProductVariant, S as Store } from './index-zSb0FIyh.mjs';
2
- export { d as BlockInstance, a as BlockProps, B as BlockSchema, E as ExternalThemeMetadata, P as PageTemplate, e as SectionGroup, S as SectionInstance, f as SectionPreset, c as SectionProps, b as SectionSchema, g as SettingDefinition, T as ThemeSettingsV3 } from './theme-NWJAU9jd.mjs';
1
+ export { A as Address, C as Cart, a as CartItem, b as Collection, c as Customer, O as Order, d as OrderItem, P as Page, e as Product, f as ProductImage, g as ProductVariant, S as Store } from './entities-iiuRSPpk.mjs';
2
+ export { B as BlockInstance, a as BlockProps, b as BlockSchema, E as ExternalThemeMetadata, P as PageTemplate, S as SectionGroup, c as SectionInstance, d as SectionPreset, e as SectionProps, f as SectionSchema, g as SettingDefinition, T as ThemeSettingsV3 } from './theme-D0QybTQS.mjs';
package/dist/types.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { A as Address, a as Cart, e as CartItem, C as Collection, b as Customer, O as Order, f as OrderItem, c as Page, P as Product, g as ProductImage, d as ProductVariant, S as Store } from './index-CGx6FNqb.js';
2
- export { d as BlockInstance, a as BlockProps, B as BlockSchema, E as ExternalThemeMetadata, P as PageTemplate, e as SectionGroup, S as SectionInstance, f as SectionPreset, c as SectionProps, b as SectionSchema, g as SettingDefinition, T as ThemeSettingsV3 } from './theme-NWJAU9jd.js';
1
+ export { A as Address, C as Cart, a as CartItem, b as Collection, c as Customer, O as Order, d as OrderItem, P as Page, e as Product, f as ProductImage, g as ProductVariant, S as Store } from './entities-iiuRSPpk.js';
2
+ export { B as BlockInstance, a as BlockProps, b as BlockSchema, E as ExternalThemeMetadata, P as PageTemplate, S as SectionGroup, c as SectionInstance, d as SectionPreset, e as SectionProps, f as SectionSchema, g as SettingDefinition, T as ThemeSettingsV3 } from './theme-D0QybTQS.js';
@@ -0,0 +1,248 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ var ShopContext = react.createContext(null);
6
+ react.createContext(null);
7
+ react.createContext(null);
8
+ react.createContext(null);
9
+ var CustomerContext = react.createContext(null);
10
+ var ThemeSettingsContext = react.createContext(null);
11
+ var LocalizationContext = react.createContext(null);
12
+ var PageContext = react.createContext(null);
13
+ react.createContext("home");
14
+ react.createContext(
15
+ {}
16
+ );
17
+
18
+ // src/hooks/usePage.ts
19
+ function usePage() {
20
+ return react.useContext(PageContext);
21
+ }
22
+ function useLocalization() {
23
+ const ctx = react.useContext(LocalizationContext);
24
+ if (!ctx) throw new Error("useLocalization must be used within NuMuProvider");
25
+ return ctx;
26
+ }
27
+ function useLocale() {
28
+ const { locale } = useLocalization();
29
+ return locale;
30
+ }
31
+
32
+ // src/hooks/useShop.ts
33
+ var DEFAULT_PROTOCOL = typeof window !== "undefined" ? window.location.protocol.replace(":", "") : "https";
34
+ function resolveDomain(store) {
35
+ if (store.domain) return store.domain;
36
+ if (store.subdomain) {
37
+ const platform = typeof window !== "undefined" && window.__NUMU_PLATFORM_DOMAIN || "numueg.app";
38
+ return `${store.subdomain}.${platform}`;
39
+ }
40
+ return store.slug;
41
+ }
42
+ function useShop() {
43
+ const ctx = react.useContext(ShopContext);
44
+ if (!ctx) throw new Error("useShop must be used within NuMuProvider");
45
+ const locale = useLocale();
46
+ return react.useMemo(() => {
47
+ const domain = resolveDomain(ctx);
48
+ const settings = ctx.settings;
49
+ const localePrefixEnabled = Boolean(
50
+ settings && settings.locale_url_prefix_enabled
51
+ );
52
+ const defaultLocale = ctx.default_language || "en";
53
+ const prefixedLocales = settings?.locale_url_prefix_locales || null;
54
+ const shouldPrefix = (l) => {
55
+ if (!localePrefixEnabled) return false;
56
+ if (prefixedLocales) return prefixedLocales.includes(l);
57
+ return l !== defaultLocale;
58
+ };
59
+ const formatUrl = (path) => {
60
+ if (!path) return `${DEFAULT_PROTOCOL}://${domain}/`;
61
+ if (/^https?:\/\//i.test(path)) return path;
62
+ if (path.startsWith("//")) return `${DEFAULT_PROTOCOL}:${path}`;
63
+ let normalized = path.startsWith("/") ? path : `/${path}`;
64
+ if (locale && shouldPrefix(locale)) {
65
+ const prefix = `/${locale}/`;
66
+ const root = `/${locale}`;
67
+ if (normalized !== root && !normalized.startsWith(prefix)) {
68
+ normalized = `${root}${normalized}`;
69
+ }
70
+ }
71
+ return `${DEFAULT_PROTOCOL}://${domain}${normalized}`;
72
+ };
73
+ return { ...ctx, domain, formatUrl };
74
+ }, [ctx, locale]);
75
+ }
76
+
77
+ // src/hooks/useProducts.ts
78
+ function useProducts(opts = {}) {
79
+ const { limit, fetchIfMissing = false } = opts;
80
+ const page = usePage();
81
+ const shop = useShop();
82
+ const initial = page?.data?.products ?? null;
83
+ const [products, setProducts] = react.useState(initial ?? []);
84
+ const [loading, setLoading] = react.useState(
85
+ initial == null && fetchIfMissing
86
+ );
87
+ const [error, setError] = react.useState(null);
88
+ react.useEffect(() => {
89
+ if (initial != null) return;
90
+ if (!fetchIfMissing) return;
91
+ if (!shop?.id) return;
92
+ let cancelled = false;
93
+ (async () => {
94
+ try {
95
+ const params = new URLSearchParams({ store_id: shop.id });
96
+ if (limit) params.set("limit", String(limit));
97
+ const res = await fetch(`/api/products?${params.toString()}`);
98
+ if (!res.ok) throw new Error(`/api/products \u2192 ${res.status}`);
99
+ const data = await res.json();
100
+ if (cancelled) return;
101
+ setProducts(data.products ?? []);
102
+ setLoading(false);
103
+ } catch (err) {
104
+ if (cancelled) return;
105
+ setError(err instanceof Error ? err : new Error(String(err)));
106
+ setLoading(false);
107
+ }
108
+ })();
109
+ return () => {
110
+ cancelled = true;
111
+ };
112
+ }, [initial, fetchIfMissing, limit, shop?.id]);
113
+ const sliced = limit != null ? products.slice(0, limit) : products;
114
+ return { products: sliced, loading, error };
115
+ }
116
+ function useCollections(opts = {}) {
117
+ const { limit, fetchIfMissing = false } = opts;
118
+ const page = usePage();
119
+ const shop = useShop();
120
+ const initial = page?.data?.collections ?? null;
121
+ const [collections, setCollections] = react.useState(initial ?? []);
122
+ const [loading, setLoading] = react.useState(
123
+ initial == null && fetchIfMissing
124
+ );
125
+ const [error, setError] = react.useState(null);
126
+ react.useEffect(() => {
127
+ if (initial != null) return;
128
+ if (!fetchIfMissing) return;
129
+ if (!shop?.id) return;
130
+ let cancelled = false;
131
+ (async () => {
132
+ try {
133
+ const params = new URLSearchParams({ store_id: shop.id });
134
+ const res = await fetch(`/api/collections?${params.toString()}`);
135
+ if (!res.ok) throw new Error(`/api/collections \u2192 ${res.status}`);
136
+ const data = await res.json();
137
+ if (cancelled) return;
138
+ setCollections(data.collections ?? []);
139
+ setLoading(false);
140
+ } catch (err) {
141
+ if (cancelled) return;
142
+ setError(err instanceof Error ? err : new Error(String(err)));
143
+ setLoading(false);
144
+ }
145
+ })();
146
+ return () => {
147
+ cancelled = true;
148
+ };
149
+ }, [initial, fetchIfMissing, shop?.id]);
150
+ const sliced = limit != null ? collections.slice(0, limit) : collections;
151
+ return { collections: sliced, loading, error };
152
+ }
153
+ function useCustomer() {
154
+ return react.useContext(CustomerContext);
155
+ }
156
+ function useThemeSettings() {
157
+ const ctx = react.useContext(ThemeSettingsContext);
158
+ if (!ctx) throw new Error("useThemeSettings must be used within NuMuProvider");
159
+ return ctx;
160
+ }
161
+
162
+ // src/v2-compat.ts
163
+ function useV2Products() {
164
+ const { products, loading } = useProducts();
165
+ return react.useMemo(
166
+ () => ({
167
+ products,
168
+ loading
169
+ }),
170
+ [products, loading]
171
+ );
172
+ }
173
+ function collectionToV2Category(c) {
174
+ return {
175
+ id: c.id,
176
+ name: c.name,
177
+ slug: c.slug,
178
+ image_url: c.image_url ?? null,
179
+ description: c.description ?? null
180
+ };
181
+ }
182
+ function useV2Categories() {
183
+ const { collections, loading } = useCollections();
184
+ return react.useMemo(
185
+ () => ({
186
+ categories: collections.map(collectionToV2Category),
187
+ loading
188
+ }),
189
+ [collections, loading]
190
+ );
191
+ }
192
+ function useV2Auth() {
193
+ const customer = useCustomer();
194
+ return react.useMemo(
195
+ () => ({
196
+ user: customer,
197
+ isAuthenticated: customer !== null
198
+ }),
199
+ [customer]
200
+ );
201
+ }
202
+ function useV2Language() {
203
+ const { locale, direction, translations, setLocale } = useLocalization();
204
+ const normalisedLocale = locale === "ar" ? "ar" : "en";
205
+ return react.useMemo(
206
+ () => ({
207
+ language: normalisedLocale,
208
+ direction,
209
+ setLanguage: (next) => setLocale(next),
210
+ t: (key, fallback) => translations[key] ?? fallback ?? key
211
+ }),
212
+ [normalisedLocale, direction, translations, setLocale]
213
+ );
214
+ }
215
+ function useV2Theme() {
216
+ const settings = useThemeSettings();
217
+ return react.useMemo(() => {
218
+ const g = settings.global_settings ?? {};
219
+ const get = (key) => {
220
+ const v = g[key];
221
+ return typeof v === "string" ? v : void 0;
222
+ };
223
+ return {
224
+ themeSettings: {
225
+ theme: {
226
+ primary_color: get("primary_color"),
227
+ accent_color: get("accent_color"),
228
+ background_color: get("background_color"),
229
+ text_color: get("text_color"),
230
+ heading_font: get("heading_font"),
231
+ ...g
232
+ },
233
+ identity: {
234
+ logo_url: get("logo_url"),
235
+ store_name: get("store_name")
236
+ }
237
+ }
238
+ };
239
+ }, [settings]);
240
+ }
241
+
242
+ exports.useV2Auth = useV2Auth;
243
+ exports.useV2Categories = useV2Categories;
244
+ exports.useV2Language = useV2Language;
245
+ exports.useV2Products = useV2Products;
246
+ exports.useV2Theme = useV2Theme;
247
+ //# sourceMappingURL=v2-compat.cjs.map
248
+ //# sourceMappingURL=v2-compat.cjs.map