@businessdash/sdk 0.9.60 → 0.9.70

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 (233) hide show
  1. package/README.md +1012 -186
  2. package/assets/biab-sw.js +92 -0
  3. package/dist/account-core/index.d.ts +126 -0
  4. package/dist/account-core/index.d.ts.map +1 -0
  5. package/dist/account-core/index.js +191 -0
  6. package/dist/account-core/index.js.map +1 -0
  7. package/dist/account.d.ts +11 -0
  8. package/dist/account.d.ts.map +1 -0
  9. package/dist/account.js +11 -0
  10. package/dist/account.js.map +1 -0
  11. package/dist/auth-handler.d.ts +1 -1
  12. package/dist/auth-links.d.ts +33 -0
  13. package/dist/auth-links.d.ts.map +1 -0
  14. package/dist/auth-links.js +64 -0
  15. package/dist/auth-links.js.map +1 -0
  16. package/dist/blog-core/index.d.ts +112 -0
  17. package/dist/blog-core/index.d.ts.map +1 -0
  18. package/dist/blog-core/index.js +330 -0
  19. package/dist/blog-core/index.js.map +1 -0
  20. package/dist/blog.d.ts +11 -0
  21. package/dist/blog.d.ts.map +1 -0
  22. package/dist/blog.js +11 -0
  23. package/dist/blog.js.map +1 -0
  24. package/dist/chat-core/index.d.ts +97 -0
  25. package/dist/chat-core/index.d.ts.map +1 -0
  26. package/dist/chat-core/index.js +237 -0
  27. package/dist/chat-core/index.js.map +1 -0
  28. package/dist/chat.d.ts +11 -0
  29. package/dist/chat.d.ts.map +1 -0
  30. package/dist/chat.js +11 -0
  31. package/dist/chat.js.map +1 -0
  32. package/dist/checkout-core/index.d.ts +65 -0
  33. package/dist/checkout-core/index.d.ts.map +1 -0
  34. package/dist/checkout-core/index.js +159 -0
  35. package/dist/checkout-core/index.js.map +1 -0
  36. package/dist/checkout.d.ts +10 -0
  37. package/dist/checkout.d.ts.map +1 -0
  38. package/dist/checkout.js +10 -0
  39. package/dist/checkout.js.map +1 -0
  40. package/dist/cli.js +133 -25
  41. package/dist/cli.js.map +1 -1
  42. package/dist/client.d.ts +567 -1
  43. package/dist/client.d.ts.map +1 -1
  44. package/dist/client.js +807 -1
  45. package/dist/client.js.map +1 -1
  46. package/dist/contracts.d.ts +3309 -761
  47. package/dist/contracts.d.ts.map +1 -1
  48. package/dist/contracts.js +541 -4
  49. package/dist/contracts.js.map +1 -1
  50. package/dist/data-model-schema.d.ts +1 -1
  51. package/dist/data-model-schema.d.ts.map +1 -1
  52. package/dist/element.d.ts +30 -0
  53. package/dist/element.d.ts.map +1 -1
  54. package/dist/element.js +56 -0
  55. package/dist/element.js.map +1 -1
  56. package/dist/embed-protocol.d.ts +86 -0
  57. package/dist/embed-protocol.d.ts.map +1 -0
  58. package/dist/embed-protocol.js +60 -0
  59. package/dist/embed-protocol.js.map +1 -0
  60. package/dist/framework-support.d.ts +122 -0
  61. package/dist/framework-support.d.ts.map +1 -0
  62. package/dist/framework-support.js +323 -0
  63. package/dist/framework-support.js.map +1 -0
  64. package/dist/index.cjs +1244 -5
  65. package/dist/legal-core/index.cjs +115 -0
  66. package/dist/legal-core/index.d.ts +160 -0
  67. package/dist/legal-core/index.d.ts.map +1 -0
  68. package/dist/legal-core/index.js +220 -0
  69. package/dist/legal-core/index.js.map +1 -0
  70. package/dist/marketing-core/index.d.ts +66 -0
  71. package/dist/marketing-core/index.d.ts.map +1 -0
  72. package/dist/marketing-core/index.js +154 -0
  73. package/dist/marketing-core/index.js.map +1 -0
  74. package/dist/marketing.d.ts +7 -0
  75. package/dist/marketing.d.ts.map +1 -0
  76. package/dist/marketing.js +7 -0
  77. package/dist/marketing.js.map +1 -0
  78. package/dist/mcp-adapters/index.d.ts +135 -0
  79. package/dist/mcp-adapters/index.d.ts.map +1 -0
  80. package/dist/mcp-adapters/index.js +187 -0
  81. package/dist/mcp-adapters/index.js.map +1 -0
  82. package/dist/mcp-adapters.d.ts +7 -0
  83. package/dist/mcp-adapters.d.ts.map +1 -0
  84. package/dist/mcp-adapters.js +7 -0
  85. package/dist/mcp-adapters.js.map +1 -0
  86. package/dist/model3d-core/index.cjs +95 -0
  87. package/dist/model3d-core/index.d.ts +167 -0
  88. package/dist/model3d-core/index.d.ts.map +1 -0
  89. package/dist/model3d-core/index.js +158 -0
  90. package/dist/model3d-core/index.js.map +1 -0
  91. package/dist/notifications-core/index.cjs +315 -0
  92. package/dist/notifications-core/index.d.ts +229 -0
  93. package/dist/notifications-core/index.d.ts.map +1 -0
  94. package/dist/notifications-core/index.js +445 -0
  95. package/dist/notifications-core/index.js.map +1 -0
  96. package/dist/portal-core/index.d.ts +451 -0
  97. package/dist/portal-core/index.d.ts.map +1 -0
  98. package/dist/portal-core/index.js +819 -0
  99. package/dist/portal-core/index.js.map +1 -0
  100. package/dist/portal.d.ts +7 -0
  101. package/dist/portal.d.ts.map +1 -0
  102. package/dist/portal.js +7 -0
  103. package/dist/portal.js.map +1 -0
  104. package/dist/push-core/index.cjs +171 -0
  105. package/dist/push-core/index.d.ts +149 -0
  106. package/dist/push-core/index.d.ts.map +1 -0
  107. package/dist/push-core/index.js +231 -0
  108. package/dist/push-core/index.js.map +1 -0
  109. package/dist/react-marketing.d.ts +1 -17
  110. package/dist/react-marketing.d.ts.map +1 -1
  111. package/dist/react-marketing.js +4 -0
  112. package/dist/react-marketing.js.map +1 -1
  113. package/dist/react.d.ts +294 -101
  114. package/dist/react.d.ts.map +1 -1
  115. package/dist/react.js +302 -246
  116. package/dist/react.js.map +1 -1
  117. package/dist/seed-core/index.d.ts +354 -0
  118. package/dist/seed-core/index.d.ts.map +1 -0
  119. package/dist/seed-core/index.js +420 -0
  120. package/dist/seed-core/index.js.map +1 -0
  121. package/dist/seed.d.ts +7 -0
  122. package/dist/seed.d.ts.map +1 -0
  123. package/dist/seed.js +7 -0
  124. package/dist/seed.js.map +1 -0
  125. package/dist/seo-core/blog.d.ts +94 -0
  126. package/dist/seo-core/blog.d.ts.map +1 -0
  127. package/dist/seo-core/blog.js +149 -0
  128. package/dist/seo-core/blog.js.map +1 -0
  129. package/dist/seo-core/commerce.d.ts +64 -0
  130. package/dist/seo-core/commerce.d.ts.map +1 -0
  131. package/dist/seo-core/commerce.js +87 -0
  132. package/dist/seo-core/commerce.js.map +1 -0
  133. package/dist/seo-core/index.cjs +751 -0
  134. package/dist/seo-core/index.d.ts +226 -0
  135. package/dist/seo-core/index.d.ts.map +1 -0
  136. package/dist/seo-core/index.js +455 -0
  137. package/dist/seo-core/index.js.map +1 -0
  138. package/dist/seo-core/local.d.ts +181 -0
  139. package/dist/seo-core/local.d.ts.map +1 -0
  140. package/dist/seo-core/local.js +193 -0
  141. package/dist/seo-core/local.js.map +1 -0
  142. package/dist/sitemap-core/index.cjs +167 -0
  143. package/dist/sitemap-core/index.d.ts +155 -0
  144. package/dist/sitemap-core/index.d.ts.map +1 -0
  145. package/dist/sitemap-core/index.js +195 -0
  146. package/dist/sitemap-core/index.js.map +1 -0
  147. package/dist/social-core/index.d.ts +74 -0
  148. package/dist/social-core/index.d.ts.map +1 -0
  149. package/dist/social-core/index.js +72 -0
  150. package/dist/social-core/index.js.map +1 -0
  151. package/dist/social.d.ts +7 -0
  152. package/dist/social.d.ts.map +1 -0
  153. package/dist/social.js +7 -0
  154. package/dist/social.js.map +1 -0
  155. package/dist/solid-compat.d.ts +204 -0
  156. package/dist/solid-compat.d.ts.map +1 -0
  157. package/dist/solid-compat.js +221 -0
  158. package/dist/solid-compat.js.map +1 -0
  159. package/dist/store-core/index.d.ts +141 -0
  160. package/dist/store-core/index.d.ts.map +1 -0
  161. package/dist/store-core/index.js +277 -0
  162. package/dist/store-core/index.js.map +1 -0
  163. package/dist/store.d.ts +12 -0
  164. package/dist/store.d.ts.map +1 -0
  165. package/dist/store.js +12 -0
  166. package/dist/store.js.map +1 -0
  167. package/dist/vanilla.d.ts +111 -0
  168. package/dist/vanilla.d.ts.map +1 -1
  169. package/dist/vanilla.js +206 -0
  170. package/dist/vanilla.js.map +1 -1
  171. package/dist/verify-core/index.d.ts +98 -0
  172. package/dist/verify-core/index.d.ts.map +1 -0
  173. package/dist/verify-core/index.js +186 -0
  174. package/dist/verify-core/index.js.map +1 -0
  175. package/dist/verify.d.ts +8 -0
  176. package/dist/verify.d.ts.map +1 -0
  177. package/dist/verify.js +8 -0
  178. package/dist/verify.js.map +1 -0
  179. package/openapi.json +4316 -1313
  180. package/package.json +121 -9
  181. package/src/account-core/index.ts +309 -0
  182. package/src/account.ts +11 -0
  183. package/src/angular/headless.ts +519 -0
  184. package/src/angular/index.ts +3 -0
  185. package/src/auth-links.ts +76 -0
  186. package/src/blog-core/index.ts +423 -0
  187. package/src/blog.ts +11 -0
  188. package/src/chat-core/index.ts +335 -0
  189. package/src/chat.ts +11 -0
  190. package/src/checkout-core/index.ts +225 -0
  191. package/src/checkout.ts +10 -0
  192. package/src/cli.ts +184 -28
  193. package/src/client.ts +998 -0
  194. package/src/contracts.ts +730 -11
  195. package/src/data-model-schema.ts +1 -1
  196. package/src/element.ts +102 -0
  197. package/src/embed-protocol.ts +100 -0
  198. package/src/framework-support.ts +440 -0
  199. package/src/legal-core/index.ts +276 -0
  200. package/src/marketing-core/index.ts +209 -0
  201. package/src/marketing.ts +7 -0
  202. package/src/mcp-adapters/index.ts +264 -0
  203. package/src/mcp-adapters.ts +7 -0
  204. package/src/model3d-core/index.ts +248 -0
  205. package/src/notifications-core/index.ts +650 -0
  206. package/src/portal-core/index.ts +1564 -0
  207. package/src/portal.ts +7 -0
  208. package/src/push-core/index.ts +335 -0
  209. package/src/qwik.tsx +602 -2
  210. package/src/react-marketing.tsx +5 -0
  211. package/src/react.tsx +434 -319
  212. package/src/seed-core/index.ts +735 -0
  213. package/src/seed.ts +7 -0
  214. package/src/seo-core/blog.ts +216 -0
  215. package/src/seo-core/commerce.ts +138 -0
  216. package/src/seo-core/index.ts +605 -0
  217. package/src/seo-core/local.ts +333 -0
  218. package/src/sitemap-core/index.ts +296 -0
  219. package/src/social-core/index.ts +103 -0
  220. package/src/social.ts +7 -0
  221. package/src/solid-compat.tsx +347 -0
  222. package/src/solid-surfaces.tsx +1311 -0
  223. package/src/solid.tsx +82 -60
  224. package/src/store-core/index.ts +426 -0
  225. package/src/store.ts +12 -0
  226. package/src/svelte/SocialLinks.svelte +84 -0
  227. package/src/svelte/headless.ts +435 -0
  228. package/src/svelte/index.ts +9 -0
  229. package/src/vanilla.ts +367 -0
  230. package/src/verify-core/index.ts +288 -0
  231. package/src/verify.ts +8 -0
  232. package/src/vue/headless.ts +527 -0
  233. package/src/vue/index.ts +3 -0
@@ -0,0 +1,426 @@
1
+ /**
2
+ * `@businessdash/sdk/store` — the headless storefront + cart controller.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Until now the only storefront the SDK offered was `<Storefront>`: a
7
+ * BusinessDash-hosted page in a sandboxed iframe. It works, and it ships fast,
8
+ * but it renders OUR markup inside YOUR app — your design system stops at the
9
+ * iframe boundary, and there is no way to put a product card in your own grid.
10
+ * That was true in every binding, React included.
11
+ *
12
+ * This is the native path. It owns all of the behaviour — pagination, category
13
+ * filtering, the cart, coupons, the product detail — and none of the markup.
14
+ * Exactly the split `createFormController` already uses for forms: one
15
+ * framework-agnostic controller, and a thin reactive view per framework.
16
+ *
17
+ * A binding subscribes to `snapshot()` and renders it. That is the whole
18
+ * contract, and it is why React, Solid, Vue, Svelte, Qwik, Angular and plain
19
+ * DOM can each expose a native storefront without any of them reimplementing
20
+ * what "add to cart" means.
21
+ *
22
+ * ## Money
23
+ *
24
+ * Every price crossing this boundary is an INTEGER of the currency's minor unit
25
+ * — cents for USD. The controller never divides, rounds or formats: it passes
26
+ * what the API sent straight to your renderer. Format at the edge, where you
27
+ * know the locale.
28
+ *
29
+ * @module
30
+ */
31
+
32
+ import type {
33
+ CartAddItemInput,
34
+ CartSnapshot,
35
+ StorefrontCategory,
36
+ StorefrontProductDetail,
37
+ StorefrontProductRow,
38
+ } from "../contracts.js";
39
+
40
+ /**
41
+ * The client surface this controller needs. Declared structurally rather than
42
+ * importing `BiabDevClient`, so a consumer can hand in a same-origin proxy, a
43
+ * test double, or a narrowed client without the controller caring.
44
+ */
45
+ export type StoreCoreClient = {
46
+ storefront: {
47
+ listProducts(input?: {
48
+ limit?: number;
49
+ cursor?: number | null;
50
+ categoryId?: string;
51
+ }): Promise<{ items: StorefrontProductRow[]; nextCursor: number | null }>;
52
+ listCategories(): Promise<{ items: StorefrontCategory[] }>;
53
+ getProduct(productId: string): Promise<StorefrontProductDetail>;
54
+ };
55
+ cart: {
56
+ get(): Promise<CartSnapshot>;
57
+ addItem(input: CartAddItemInput): Promise<CartSnapshot>;
58
+ updateItem(
59
+ itemId: string,
60
+ input: { quantity: number },
61
+ ): Promise<CartSnapshot>;
62
+ removeItem(itemId: string): Promise<CartSnapshot>;
63
+ applyCoupon(input: { code: string }): Promise<CartSnapshot>;
64
+ removeCoupon(): Promise<CartSnapshot>;
65
+ clear(): Promise<CartSnapshot>;
66
+ };
67
+ };
68
+
69
+ export type StoreStatus = "idle" | "loading" | "ready" | "error";
70
+
71
+ /**
72
+ * What a binding renders. Field names are STABLE and load-bearing for every
73
+ * framework binding — renaming one is a breaking change in eight places.
74
+ */
75
+ export type StoreSnapshot = {
76
+ status: StoreStatus;
77
+ /** The current page of products, accumulated across `loadMore()`. */
78
+ products: readonly StorefrontProductRow[];
79
+ categories: readonly StorefrontCategory[];
80
+ /** `null` until the first cart read resolves. */
81
+ cart: CartSnapshot | null;
82
+ /** The product opened via `openProduct()`, if any. */
83
+ activeProduct: StorefrontProductDetail | null;
84
+ /** Active category filter, or `null` for everything. */
85
+ categoryId: string | null;
86
+ /** Whether `loadMore()` would fetch anything. */
87
+ hasMore: boolean;
88
+ /** Last failure. Cleared on the next successful action. */
89
+ error: Error | null;
90
+ /**
91
+ * In-flight work, so a binding can disable a button without inventing its
92
+ * own loading state. `cart` covers every mutation — add, update, remove,
93
+ * coupon, clear — because they all serialise onto the same cart.
94
+ */
95
+ pending: {
96
+ products: boolean;
97
+ categories: boolean;
98
+ product: boolean;
99
+ cart: boolean;
100
+ };
101
+ /** Convenience mirrors of `cart`, so a badge doesn't need a null check. */
102
+ itemCount: number;
103
+ subtotal: number;
104
+ currency: string | null;
105
+ };
106
+
107
+ export type StoreController = {
108
+ snapshot(): StoreSnapshot;
109
+ /**
110
+ * Subscribe to snapshot changes. Returns an unsubscribe function. The
111
+ * callback is NOT invoked on subscribe — read `snapshot()` for the initial
112
+ * value, the same contract `createFormController` uses.
113
+ */
114
+ subscribe(callback: (snapshot: StoreSnapshot) => void): () => void;
115
+ /** Fetch products, categories and the cart. Safe to call more than once. */
116
+ load(): Promise<void>;
117
+ /** Append the next page. No-op when `hasMore` is false or a page is in flight. */
118
+ loadMore(): Promise<void>;
119
+ /** Filter by category (or `null` for all) and reset pagination. */
120
+ filterByCategory(categoryId: string | null): Promise<void>;
121
+ /** Load one product's detail into `activeProduct`. */
122
+ openProduct(productId: string): Promise<void>;
123
+ closeProduct(): void;
124
+ addToCart(input: CartAddItemInput): Promise<void>;
125
+ /** Quantity 0 removes the line, matching the API's own semantics. */
126
+ updateQuantity(itemId: string, quantity: number): Promise<void>;
127
+ removeItem(itemId: string): Promise<void>;
128
+ applyCoupon(code: string): Promise<void>;
129
+ removeCoupon(): Promise<void>;
130
+ clearCart(): Promise<void>;
131
+ /** Re-read the cart without touching products. */
132
+ refreshCart(): Promise<void>;
133
+ /** Stop notifying and drop all subscribers. */
134
+ destroy(): void;
135
+ };
136
+
137
+ export type CreateStoreControllerOptions = {
138
+ client: StoreCoreClient;
139
+ /** Products per page. Default 24. */
140
+ pageSize?: number;
141
+ /** Start filtered to a category. */
142
+ categoryId?: string | null;
143
+ /** Called on every failure, in addition to landing in `snapshot().error`. */
144
+ onError?: (error: Error) => void;
145
+ };
146
+
147
+ const DEFAULT_PAGE_SIZE = 24;
148
+
149
+ function toError(cause: unknown, fallback: string): Error {
150
+ return cause instanceof Error ? cause : new Error(fallback);
151
+ }
152
+
153
+ export function createStoreController(
154
+ options: CreateStoreControllerOptions,
155
+ ): StoreController {
156
+ const { client, onError } = options;
157
+ const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
158
+
159
+ let destroyed = false;
160
+ let cursor: number | null = null;
161
+ /**
162
+ * Guards against a stale page landing after the category changed. Every
163
+ * product fetch captures the token it started under and drops its result if
164
+ * the token moved on — otherwise clicking two categories quickly can leave
165
+ * the second one's products under the first one's heading.
166
+ */
167
+ let requestToken = 0;
168
+
169
+ const subscribers = new Set<(s: StoreSnapshot) => void>();
170
+
171
+ const state: {
172
+ status: StoreStatus;
173
+ products: StorefrontProductRow[];
174
+ categories: StorefrontCategory[];
175
+ cart: CartSnapshot | null;
176
+ activeProduct: StorefrontProductDetail | null;
177
+ categoryId: string | null;
178
+ hasMore: boolean;
179
+ error: Error | null;
180
+ pending: StoreSnapshot["pending"];
181
+ } = {
182
+ status: "idle",
183
+ products: [],
184
+ categories: [],
185
+ cart: null,
186
+ activeProduct: null,
187
+ categoryId: options.categoryId ?? null,
188
+ hasMore: false,
189
+ error: null,
190
+ pending: {
191
+ products: false,
192
+ categories: false,
193
+ product: false,
194
+ cart: false,
195
+ },
196
+ };
197
+
198
+ let cached: StoreSnapshot | null = null;
199
+
200
+ function snapshot(): StoreSnapshot {
201
+ if (!cached) {
202
+ cached = {
203
+ status: state.status,
204
+ products: state.products,
205
+ categories: state.categories,
206
+ cart: state.cart,
207
+ activeProduct: state.activeProduct,
208
+ categoryId: state.categoryId,
209
+ hasMore: state.hasMore,
210
+ error: state.error,
211
+ pending: state.pending,
212
+ itemCount: state.cart?.itemCount ?? 0,
213
+ subtotal: state.cart?.subtotal ?? 0,
214
+ currency: state.cart?.currency ?? null,
215
+ };
216
+ }
217
+ return cached;
218
+ }
219
+
220
+ function notify(): void {
221
+ if (destroyed) return;
222
+ cached = null;
223
+ const next = snapshot();
224
+ for (const cb of subscribers) cb(next);
225
+ }
226
+
227
+ function fail(cause: unknown, fallback: string): void {
228
+ const error = toError(cause, fallback);
229
+ state.error = error;
230
+ if (state.status !== "ready") state.status = "error";
231
+ onError?.(error);
232
+ }
233
+
234
+ /** Run a cart mutation, keeping `pending.cart` honest even on failure. */
235
+ async function cartAction(
236
+ run: () => Promise<CartSnapshot>,
237
+ fallback: string,
238
+ ): Promise<void> {
239
+ if (destroyed) return;
240
+ state.pending.cart = true;
241
+ state.error = null;
242
+ notify();
243
+ try {
244
+ const cart = await run();
245
+ if (destroyed) return;
246
+ state.cart = cart;
247
+ } catch (cause) {
248
+ if (destroyed) return;
249
+ fail(cause, fallback);
250
+ } finally {
251
+ if (!destroyed) {
252
+ state.pending.cart = false;
253
+ notify();
254
+ }
255
+ }
256
+ }
257
+
258
+ async function fetchProducts(append: boolean): Promise<void> {
259
+ const token = ++requestToken;
260
+ state.pending.products = true;
261
+ if (!append) state.error = null;
262
+ notify();
263
+ try {
264
+ const page = await client.storefront.listProducts({
265
+ limit: pageSize,
266
+ ...(append && cursor !== null ? { cursor } : {}),
267
+ ...(state.categoryId ? { categoryId: state.categoryId } : {}),
268
+ });
269
+ // A newer request superseded this one — its result is now wrong.
270
+ if (destroyed || token !== requestToken) return;
271
+ state.products = append ? [...state.products, ...page.items] : page.items;
272
+ cursor = page.nextCursor;
273
+ state.hasMore = page.nextCursor !== null;
274
+ state.status = "ready";
275
+ } catch (cause) {
276
+ if (destroyed || token !== requestToken) return;
277
+ fail(cause, "Failed to load products.");
278
+ } finally {
279
+ if (!destroyed && token === requestToken) {
280
+ state.pending.products = false;
281
+ notify();
282
+ }
283
+ }
284
+ }
285
+
286
+ return {
287
+ snapshot,
288
+
289
+ subscribe(callback) {
290
+ subscribers.add(callback);
291
+ return () => {
292
+ subscribers.delete(callback);
293
+ };
294
+ },
295
+
296
+ async load() {
297
+ if (destroyed) return;
298
+ if (state.status === "idle") state.status = "loading";
299
+ state.pending.categories = true;
300
+ notify();
301
+
302
+ // Categories and the cart are independent of the product page, so all
303
+ // three go out together rather than waterfalling.
304
+ const categories = client.storefront
305
+ .listCategories()
306
+ .then((res) => {
307
+ if (destroyed) return;
308
+ state.categories = res.items;
309
+ })
310
+ .catch((cause) => {
311
+ if (!destroyed) fail(cause, "Failed to load categories.");
312
+ })
313
+ .finally(() => {
314
+ if (!destroyed) state.pending.categories = false;
315
+ });
316
+
317
+ const cart = client.cart
318
+ .get()
319
+ .then((res) => {
320
+ if (destroyed) return;
321
+ state.cart = res;
322
+ })
323
+ .catch((cause) => {
324
+ // A visitor with no cart yet is normal, not an error worth
325
+ // surfacing — leave `cart` null and let the products render.
326
+ void cause;
327
+ });
328
+
329
+ await Promise.all([fetchProducts(false), categories, cart]);
330
+ if (!destroyed) notify();
331
+ },
332
+
333
+ async loadMore() {
334
+ if (destroyed || !state.hasMore || state.pending.products) return;
335
+ await fetchProducts(true);
336
+ },
337
+
338
+ async filterByCategory(categoryId) {
339
+ if (destroyed) return;
340
+ state.categoryId = categoryId;
341
+ cursor = null;
342
+ await fetchProducts(false);
343
+ },
344
+
345
+ async openProduct(productId) {
346
+ if (destroyed) return;
347
+ state.pending.product = true;
348
+ state.error = null;
349
+ notify();
350
+ try {
351
+ const detail = await client.storefront.getProduct(productId);
352
+ if (destroyed) return;
353
+ state.activeProduct = detail;
354
+ } catch (cause) {
355
+ if (!destroyed) fail(cause, "Failed to load the product.");
356
+ } finally {
357
+ if (!destroyed) {
358
+ state.pending.product = false;
359
+ notify();
360
+ }
361
+ }
362
+ },
363
+
364
+ closeProduct() {
365
+ if (destroyed) return;
366
+ state.activeProduct = null;
367
+ notify();
368
+ },
369
+
370
+ addToCart(input) {
371
+ return cartAction(
372
+ () => client.cart.addItem(input),
373
+ "Failed to add the item to the cart.",
374
+ );
375
+ },
376
+
377
+ updateQuantity(itemId, quantity) {
378
+ // The API treats 0 as a removal; going through `removeItem` keeps that
379
+ // explicit rather than relying on every binding knowing the convention.
380
+ if (quantity <= 0) {
381
+ return cartAction(
382
+ () => client.cart.removeItem(itemId),
383
+ "Failed to remove the item.",
384
+ );
385
+ }
386
+ return cartAction(
387
+ () => client.cart.updateItem(itemId, { quantity }),
388
+ "Failed to update the quantity.",
389
+ );
390
+ },
391
+
392
+ removeItem(itemId) {
393
+ return cartAction(
394
+ () => client.cart.removeItem(itemId),
395
+ "Failed to remove the item.",
396
+ );
397
+ },
398
+
399
+ applyCoupon(code) {
400
+ return cartAction(
401
+ () => client.cart.applyCoupon({ code }),
402
+ "That coupon could not be applied.",
403
+ );
404
+ },
405
+
406
+ removeCoupon() {
407
+ return cartAction(
408
+ () => client.cart.removeCoupon(),
409
+ "Failed to remove the coupon.",
410
+ );
411
+ },
412
+
413
+ clearCart() {
414
+ return cartAction(() => client.cart.clear(), "Failed to clear the cart.");
415
+ },
416
+
417
+ refreshCart() {
418
+ return cartAction(() => client.cart.get(), "Failed to read the cart.");
419
+ },
420
+
421
+ destroy() {
422
+ destroyed = true;
423
+ subscribers.clear();
424
+ },
425
+ };
426
+ }
package/src/store.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@businessdash/sdk/store` — the framework-agnostic storefront + cart core.
3
+ *
4
+ * Bindings shipped as SOURCE (Vue / Svelte / Solid / Qwik / Angular) import
5
+ * through this public subpath rather than a relative path, so the consumer's
6
+ * bundler resolves it exactly as their own code would. `forms` works the same
7
+ * way.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ export * from "./store-core/index.js";
@@ -0,0 +1,84 @@
1
+ <!--
2
+ The company's social profiles as icon links.
3
+
4
+ Every decision that could drift between frameworks — which platforms exist,
5
+ how a handle becomes a URL, where the brand icon comes from — lives in
6
+ `social-core`. This file is only the markup, which is the one part that
7
+ genuinely cannot be shared.
8
+ -->
9
+ <script lang="ts">
10
+ import {
11
+ resolveSocialLinks,
12
+ SOCIAL_FALLBACK_GLYPH,
13
+ SOCIAL_LIST_STYLE_STRING,
14
+ } from "../social-core/index.js";
15
+
16
+ type Props = {
17
+ /** The company profile's `socials` record. */
18
+ socials: unknown;
19
+ /** Icon colour — hex without `#`, or a CSS keyword. */
20
+ color?: string;
21
+ /** Icon edge length in px. */
22
+ size?: number;
23
+ linkClass?: string;
24
+ iconClass?: string;
25
+ };
26
+
27
+ let {
28
+ socials,
29
+ color = undefined,
30
+ size = 20,
31
+ linkClass = undefined,
32
+ iconClass = undefined,
33
+ }: Props = $props();
34
+
35
+ const rows = $derived(resolveSocialLinks(socials, { color }));
36
+ </script>
37
+
38
+ <!-- Nothing at all rather than an empty <ul> in a footer. -->
39
+ {#if rows.length}
40
+ <ul style={SOCIAL_LIST_STYLE_STRING}>
41
+ {#each rows as row (row.key)}
42
+ <li>
43
+ <a
44
+ href={row.url}
45
+ target="_blank"
46
+ rel="noopener noreferrer"
47
+ title={row.label}
48
+ aria-label={row.label}
49
+ class={linkClass}
50
+ >
51
+ {#if row.iconUrl}
52
+ <img
53
+ src={row.iconUrl}
54
+ alt={row.label}
55
+ width={size}
56
+ height={size}
57
+ loading="lazy"
58
+ class={iconClass}
59
+ />
60
+ {:else}
61
+ <svg
62
+ viewBox={SOCIAL_FALLBACK_GLYPH.viewBox}
63
+ width={size}
64
+ height={size}
65
+ fill="none"
66
+ stroke="currentColor"
67
+ stroke-width={SOCIAL_FALLBACK_GLYPH.strokeWidth}
68
+ aria-hidden="true"
69
+ class={iconClass}
70
+ >
71
+ <title>{row.label}</title>
72
+ <circle
73
+ cx={SOCIAL_FALLBACK_GLYPH.circle.cx}
74
+ cy={SOCIAL_FALLBACK_GLYPH.circle.cy}
75
+ r={SOCIAL_FALLBACK_GLYPH.circle.r}
76
+ />
77
+ <path d={SOCIAL_FALLBACK_GLYPH.path} />
78
+ </svg>
79
+ {/if}
80
+ </a>
81
+ </li>
82
+ {/each}
83
+ </ul>
84
+ {/if}