@thebes/cadmea-ecommerce-ui 1.0.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.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +339 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BowenLabs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @thebes/cadmea-ecommerce-ui
|
|
2
|
+
|
|
3
|
+
Storefront SolidJS components for
|
|
4
|
+
[`@thebes/cadmea-plugin-ecommerce`](https://www.npmjs.com/package/@thebes/cadmea-plugin-ecommerce) —
|
|
5
|
+
`ProductDetail`, `CartProvider`/`useCart`, `CartDrawer`, `CheckoutForm`.
|
|
6
|
+
|
|
7
|
+
This is a plain **library** — no CMS-config opinion, no Cadmus interface,
|
|
8
|
+
the same "neither axis" categorization
|
|
9
|
+
[EXTENDING.md](https://github.com/bowenlabs/project-thebes/blob/main/EXTENDING.md)
|
|
10
|
+
gives `@thebes/cadmea-design-system`.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add @thebes/cadmea-ecommerce-ui solid-js
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Why SolidJS here, when the public site's own tier is Alpine.js
|
|
17
|
+
|
|
18
|
+
`DECISIONS.md`'s "Component framework tiering" entry reserves SolidJS for
|
|
19
|
+
`core/`+the Panel and Alpine.js for the public site's own sprinkle-on
|
|
20
|
+
interactivity — but that same entry explicitly carves out framework
|
|
21
|
+
discretion for operator/community **extensions**, since they sit outside
|
|
22
|
+
the `core`/`custom` boundary. This package is exactly that kind of
|
|
23
|
+
extension. SolidJS specifically (not Alpine, not a third framework): cart/
|
|
24
|
+
checkout has real multi-step state, async tokenize callbacks, and
|
|
25
|
+
field-level validation — more than Alpine's `x-data`/`x-show` model
|
|
26
|
+
comfortably expresses — and Solid is already a runtime the Panel depends
|
|
27
|
+
on, so an operator using this on a page doesn't pay for a *second*
|
|
28
|
+
component-framework runtime.
|
|
29
|
+
|
|
30
|
+
Mounting these as Astro islands needs `@astrojs/solid-js` added to your
|
|
31
|
+
project — not implied by anything else in the stack.
|
|
32
|
+
|
|
33
|
+
## One island, one `<CartProvider>`
|
|
34
|
+
|
|
35
|
+
Astro hydrates each `client:*` directive as its own isolated component
|
|
36
|
+
root — `CartProvider`'s context is **not** shared across separate islands.
|
|
37
|
+
Compose everything that needs to share cart state into one component, and
|
|
38
|
+
mount that as a single island:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// ShopIsland.tsx
|
|
42
|
+
import { CartDrawer, CartProvider, CheckoutForm, ProductDetail } from "@thebes/cadmea-ecommerce-ui";
|
|
43
|
+
import { createSquareCardField } from "@thebes/cadmea-plugin-ecommerce-square/client";
|
|
44
|
+
|
|
45
|
+
export function ShopIsland(props: { product: Product }) {
|
|
46
|
+
return (
|
|
47
|
+
<CartProvider>
|
|
48
|
+
<ProductDetail product={props.product} />
|
|
49
|
+
<CartDrawer
|
|
50
|
+
checkoutSlot={() => (
|
|
51
|
+
<CheckoutForm
|
|
52
|
+
tokenize={/* wire to createSquareCardField/createStripeCardField — see below */}
|
|
53
|
+
mountCardField={(el) => createSquareCardField(el, { applicationId, locationId })}
|
|
54
|
+
/>
|
|
55
|
+
)}
|
|
56
|
+
/>
|
|
57
|
+
</CartProvider>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```astro
|
|
63
|
+
---
|
|
64
|
+
import { ShopIsland } from "../components/ShopIsland";
|
|
65
|
+
---
|
|
66
|
+
<ShopIsland client:load product={product} />
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Provider-agnostic checkout
|
|
70
|
+
|
|
71
|
+
`CheckoutForm` takes a `tokenize: () => Promise<string>` prop rather than
|
|
72
|
+
knowing about Square/Stripe itself — wire in whichever provider's
|
|
73
|
+
`/client` helper (`createSquareCardField`/`createStripeCardField`, both
|
|
74
|
+
sharing the exact same `{ tokenize(), destroy() }` shape) you're using.
|
|
75
|
+
Swapping providers means swapping that one prop, not rewriting this
|
|
76
|
+
component.
|
|
77
|
+
|
|
78
|
+
## Cart state
|
|
79
|
+
|
|
80
|
+
`CartProvider`/`useCart` is `localStorage`-backed, `createSignal`-based
|
|
81
|
+
reactive state — no DB sync needed until checkout, the same pattern found
|
|
82
|
+
independently in two of the three Next.js + Payload projects this
|
|
83
|
+
component's design was generalized from.
|
|
84
|
+
|
|
85
|
+
See `examples/cadmea-smb-template` in the
|
|
86
|
+
[main repo](https://github.com/bowenlabs/project-thebes) for all of this
|
|
87
|
+
wired together end to end, including the backend checkout/webhook Worker.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT © BowenLabs
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Accessor, JSX } from "solid-js";
|
|
2
|
+
|
|
3
|
+
//#region src/CartDrawer.d.ts
|
|
4
|
+
interface CartDrawerProps {
|
|
5
|
+
/** Rendered inside the drawer below the line items, typically a checkout link/button. */
|
|
6
|
+
checkoutSlot?: () => JSX.Element;
|
|
7
|
+
}
|
|
8
|
+
declare function CartDrawer(props: CartDrawerProps): JSX.Element;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/CartProvider.d.ts
|
|
11
|
+
interface CartLineItem {
|
|
12
|
+
catalogRef: string;
|
|
13
|
+
name: string;
|
|
14
|
+
quantity: number;
|
|
15
|
+
unitPrice: {
|
|
16
|
+
amount: number;
|
|
17
|
+
currency: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
interface CartContextValue {
|
|
21
|
+
items: Accessor<CartLineItem[]>;
|
|
22
|
+
isOpen: Accessor<boolean>;
|
|
23
|
+
subtotal: Accessor<number>;
|
|
24
|
+
addItem: (item: Omit<CartLineItem, "quantity">, quantity?: number) => void;
|
|
25
|
+
removeItem: (catalogRef: string) => void;
|
|
26
|
+
updateQuantity: (catalogRef: string, quantity: number) => void;
|
|
27
|
+
clear: () => void;
|
|
28
|
+
open: () => void;
|
|
29
|
+
close: () => void;
|
|
30
|
+
}
|
|
31
|
+
interface CartProviderProps {
|
|
32
|
+
children: JSX.Element;
|
|
33
|
+
}
|
|
34
|
+
declare function CartProvider(props: CartProviderProps): JSX.Element;
|
|
35
|
+
declare function useCart(): CartContextValue;
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/CheckoutForm.d.ts
|
|
38
|
+
interface CheckoutFormProps {
|
|
39
|
+
/** Mounts the provider's card field and produces a one-time payment source token — see this package's README for wiring either provider's `/client` helper here. */
|
|
40
|
+
tokenize: () => Promise<string>;
|
|
41
|
+
/** Called once the provider's card field should be mounted — receives the container element to attach to. Most consumers wire this to `createSquareCardField(el, opts)`/`createStripeCardField(el, opts)` directly and ignore the return value here (the `tokenize` prop is what's actually called on submit). */
|
|
42
|
+
mountCardField?: (container: HTMLDivElement) => void;
|
|
43
|
+
/** Default: "/api/checkout". */
|
|
44
|
+
checkoutEndpoint?: string;
|
|
45
|
+
onSuccess?: (order: unknown) => void;
|
|
46
|
+
onError?: (error: Error) => void;
|
|
47
|
+
}
|
|
48
|
+
declare function CheckoutForm(props: CheckoutFormProps): import("solid-js").JSX.Element;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/ProductDetail.d.ts
|
|
51
|
+
interface ProductVariant {
|
|
52
|
+
sku: string;
|
|
53
|
+
catalogRef: string;
|
|
54
|
+
priceCents: number;
|
|
55
|
+
currency: string;
|
|
56
|
+
inventoryCount?: number;
|
|
57
|
+
}
|
|
58
|
+
interface Product {
|
|
59
|
+
id: number;
|
|
60
|
+
name: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
variants: ProductVariant[];
|
|
63
|
+
}
|
|
64
|
+
interface ProductDetailProps {
|
|
65
|
+
product: Product;
|
|
66
|
+
onAddToCart?: (variant: ProductVariant) => void;
|
|
67
|
+
}
|
|
68
|
+
declare function ProductDetail(props: ProductDetailProps): import("solid-js").JSX.Element;
|
|
69
|
+
//#endregion
|
|
70
|
+
export { CartContextValue, CartDrawer, CartDrawerProps, CartLineItem, CartProvider, CartProviderProps, CheckoutForm, CheckoutFormProps, Product, ProductDetail, ProductDetailProps, ProductVariant, useCart };
|
|
71
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { createComponent, delegateEvents, effect, insert, memo, setAttribute, template, use } from "solid-js/web";
|
|
2
|
+
import { For, Show, createContext, createEffect, createMemo, createSignal, onCleanup, onMount, useContext } from "solid-js";
|
|
3
|
+
//#region src/CartProvider.tsx
|
|
4
|
+
const CartContext = createContext();
|
|
5
|
+
const STORAGE_KEY = "cadmea-ecommerce-cart";
|
|
6
|
+
function loadStoredItems() {
|
|
7
|
+
if (typeof window === "undefined") return [];
|
|
8
|
+
try {
|
|
9
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
10
|
+
return raw ? JSON.parse(raw) : [];
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function CartProvider(props) {
|
|
16
|
+
const [items, setItems] = createSignal([]);
|
|
17
|
+
const [isOpen, setIsOpen] = createSignal(false);
|
|
18
|
+
onMount(() => setItems(loadStoredItems()));
|
|
19
|
+
function persist(next) {
|
|
20
|
+
setItems(next);
|
|
21
|
+
if (typeof window === "undefined") return;
|
|
22
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
23
|
+
}
|
|
24
|
+
const value = {
|
|
25
|
+
items,
|
|
26
|
+
isOpen,
|
|
27
|
+
subtotal: () => items().reduce((sum, item) => sum + item.unitPrice.amount * item.quantity, 0),
|
|
28
|
+
addItem(item, quantity = 1) {
|
|
29
|
+
if (items().find((i) => i.catalogRef === item.catalogRef)) persist(items().map((i) => i.catalogRef === item.catalogRef ? {
|
|
30
|
+
...i,
|
|
31
|
+
quantity: i.quantity + quantity
|
|
32
|
+
} : i));
|
|
33
|
+
else persist([...items(), {
|
|
34
|
+
...item,
|
|
35
|
+
quantity
|
|
36
|
+
}]);
|
|
37
|
+
},
|
|
38
|
+
removeItem(catalogRef) {
|
|
39
|
+
persist(items().filter((i) => i.catalogRef !== catalogRef));
|
|
40
|
+
},
|
|
41
|
+
updateQuantity(catalogRef, quantity) {
|
|
42
|
+
if (quantity <= 0) {
|
|
43
|
+
persist(items().filter((i) => i.catalogRef !== catalogRef));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
persist(items().map((i) => i.catalogRef === catalogRef ? {
|
|
47
|
+
...i,
|
|
48
|
+
quantity
|
|
49
|
+
} : i));
|
|
50
|
+
},
|
|
51
|
+
clear() {
|
|
52
|
+
persist([]);
|
|
53
|
+
},
|
|
54
|
+
open: () => setIsOpen(true),
|
|
55
|
+
close: () => setIsOpen(false)
|
|
56
|
+
};
|
|
57
|
+
return createComponent(CartContext.Provider, {
|
|
58
|
+
value,
|
|
59
|
+
get children() {
|
|
60
|
+
return props.children;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function useCart() {
|
|
65
|
+
const context = useContext(CartContext);
|
|
66
|
+
if (!context) throw new Error("useCart() must be called within a <CartProvider>");
|
|
67
|
+
return context;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/CartDrawer.tsx
|
|
71
|
+
var _tmpl$$2 = /*#__PURE__*/ template(`<ul class="mt-4 flex flex-col gap-3">`), _tmpl$2$2 = /*#__PURE__*/ template(`<div class="mt-4 flex items-center justify-between border-t pt-4"><span class=font-semibold>Subtotal</span><span data-testid=cart-subtotal>`), _tmpl$3$1 = /*#__PURE__*/ template(`<div class="cart-drawer fixed inset-0 z-50"data-testid=cart-drawer><button type=button class="fixed inset-0 bg-[var(--color-backdrop)]"aria-label="Close cart"></button><div aria-live=polite aria-atomic=true class=sr-only></div><aside role=dialog aria-modal=true aria-label="Shopping cart"class="fixed right-0 top-0 h-full w-full max-w-sm bg-base-100 p-4 shadow-xl"><div class="flex items-center justify-between"><h2 class="text-lg font-bold">Your cart</h2><button type=button class="btn btn-sm btn-circle"aria-label="Close cart"><i class="ph ph-x"aria-hidden=true>`), _tmpl$4$1 = /*#__PURE__*/ template(`<p class=opacity-70>Your cart is empty.`), _tmpl$5$1 = /*#__PURE__*/ template(`<li class="flex items-center justify-between gap-2"><div><p class=font-medium></p><p class="text-sm opacity-70"> × </p></div><div class="flex items-center gap-1"><button type=button class="btn btn-xs">−</button><span></span><button type=button class="btn btn-xs">+</button><button type=button class="btn btn-xs btn-ghost"><i class="ph ph-trash"aria-hidden=true>`);
|
|
72
|
+
const FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
73
|
+
function formatPrice$1(cents, currency) {
|
|
74
|
+
return new Intl.NumberFormat(void 0, {
|
|
75
|
+
style: "currency",
|
|
76
|
+
currency
|
|
77
|
+
}).format(cents / 100);
|
|
78
|
+
}
|
|
79
|
+
function CartDrawer(props) {
|
|
80
|
+
const cart = useCart();
|
|
81
|
+
let asideRef;
|
|
82
|
+
let closeButtonRef;
|
|
83
|
+
let triggeredBy = null;
|
|
84
|
+
createEffect(() => {
|
|
85
|
+
if (!cart.isOpen()) {
|
|
86
|
+
triggeredBy?.focus();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
triggeredBy = document.activeElement;
|
|
90
|
+
closeButtonRef?.focus();
|
|
91
|
+
const previousOverflow = document.body.style.overflow;
|
|
92
|
+
document.body.style.overflow = "hidden";
|
|
93
|
+
function onKeyDown(event) {
|
|
94
|
+
if (event.key === "Escape") {
|
|
95
|
+
cart.close();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (event.key !== "Tab" || !asideRef) return;
|
|
99
|
+
const focusable = Array.from(asideRef.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
100
|
+
if (focusable.length === 0) return;
|
|
101
|
+
const first = focusable[0];
|
|
102
|
+
const last = focusable[focusable.length - 1];
|
|
103
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
last.focus();
|
|
106
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
first.focus();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
document.addEventListener("keydown", onKeyDown);
|
|
112
|
+
onCleanup(() => {
|
|
113
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
114
|
+
document.body.style.overflow = previousOverflow;
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return createComponent(Show, {
|
|
118
|
+
get when() {
|
|
119
|
+
return cart.isOpen();
|
|
120
|
+
},
|
|
121
|
+
get children() {
|
|
122
|
+
var _el$ = _tmpl$3$1(), _el$2 = _el$.firstChild, _el$3 = _el$2.nextSibling, _el$4 = _el$3.nextSibling, _el$7 = _el$4.firstChild.firstChild.nextSibling;
|
|
123
|
+
_el$2.$$click = () => cart.close();
|
|
124
|
+
insert(_el$3, createComponent(Show, {
|
|
125
|
+
get when() {
|
|
126
|
+
return cart.items().length > 0;
|
|
127
|
+
},
|
|
128
|
+
fallback: "Cart is empty.",
|
|
129
|
+
get children() {
|
|
130
|
+
return `Cart has ${cart.items().length} item${cart.items().length === 1 ? "" : "s"}, subtotal ${formatPrice$1(cart.subtotal(), cart.items()[0]?.unitPrice.currency ?? "USD")}.`;
|
|
131
|
+
}
|
|
132
|
+
}));
|
|
133
|
+
var _ref$ = asideRef;
|
|
134
|
+
typeof _ref$ === "function" ? use(_ref$, _el$4) : asideRef = _el$4;
|
|
135
|
+
_el$7.$$click = () => cart.close();
|
|
136
|
+
var _ref$2 = closeButtonRef;
|
|
137
|
+
typeof _ref$2 === "function" ? use(_ref$2, _el$7) : closeButtonRef = _el$7;
|
|
138
|
+
insert(_el$4, createComponent(Show, {
|
|
139
|
+
get when() {
|
|
140
|
+
return cart.items().length > 0;
|
|
141
|
+
},
|
|
142
|
+
get fallback() {
|
|
143
|
+
return _tmpl$4$1();
|
|
144
|
+
},
|
|
145
|
+
get children() {
|
|
146
|
+
return [
|
|
147
|
+
(() => {
|
|
148
|
+
var _el$8 = _tmpl$$2();
|
|
149
|
+
insert(_el$8, createComponent(For, {
|
|
150
|
+
get each() {
|
|
151
|
+
return cart.items();
|
|
152
|
+
},
|
|
153
|
+
children: (item) => (() => {
|
|
154
|
+
var _el$11 = _tmpl$5$1(), _el$12 = _el$11.firstChild, _el$13 = _el$12.firstChild, _el$14 = _el$13.nextSibling, _el$15 = _el$14.firstChild, _el$18 = _el$12.nextSibling.firstChild, _el$19 = _el$18.nextSibling, _el$20 = _el$19.nextSibling, _el$21 = _el$20.nextSibling;
|
|
155
|
+
insert(_el$13, () => item.name);
|
|
156
|
+
insert(_el$14, () => formatPrice$1(item.unitPrice.amount, item.unitPrice.currency), _el$15);
|
|
157
|
+
insert(_el$14, () => item.quantity, null);
|
|
158
|
+
_el$18.$$click = () => cart.updateQuantity(item.catalogRef, item.quantity - 1);
|
|
159
|
+
insert(_el$19, () => item.quantity);
|
|
160
|
+
_el$20.$$click = () => cart.updateQuantity(item.catalogRef, item.quantity + 1);
|
|
161
|
+
_el$21.$$click = () => cart.removeItem(item.catalogRef);
|
|
162
|
+
effect((_p$) => {
|
|
163
|
+
var _v$ = `Decrease quantity of ${item.name}`, _v$2 = `quantity-${item.catalogRef}`, _v$3 = `Increase quantity of ${item.name}`, _v$4 = `Remove ${item.name}`;
|
|
164
|
+
_v$ !== _p$.e && setAttribute(_el$18, "aria-label", _p$.e = _v$);
|
|
165
|
+
_v$2 !== _p$.t && setAttribute(_el$19, "data-testid", _p$.t = _v$2);
|
|
166
|
+
_v$3 !== _p$.a && setAttribute(_el$20, "aria-label", _p$.a = _v$3);
|
|
167
|
+
_v$4 !== _p$.o && setAttribute(_el$21, "aria-label", _p$.o = _v$4);
|
|
168
|
+
return _p$;
|
|
169
|
+
}, {
|
|
170
|
+
e: void 0,
|
|
171
|
+
t: void 0,
|
|
172
|
+
a: void 0,
|
|
173
|
+
o: void 0
|
|
174
|
+
});
|
|
175
|
+
return _el$11;
|
|
176
|
+
})()
|
|
177
|
+
}));
|
|
178
|
+
return _el$8;
|
|
179
|
+
})(),
|
|
180
|
+
(() => {
|
|
181
|
+
var _el$9 = _tmpl$2$2(), _el$1 = _el$9.firstChild.nextSibling;
|
|
182
|
+
insert(_el$1, () => formatPrice$1(cart.subtotal(), cart.items()[0]?.unitPrice.currency ?? "USD"));
|
|
183
|
+
return _el$9;
|
|
184
|
+
})(),
|
|
185
|
+
memo(() => props.checkoutSlot?.())
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
}), null);
|
|
189
|
+
return _el$;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
delegateEvents(["click"]);
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/CheckoutForm.tsx
|
|
196
|
+
var _tmpl$$1 = /*#__PURE__*/ template(`<form class="checkout-form flex flex-col gap-4"><label class=form-control><span class=label-text>Email</span><input type=email required class="input input-bordered"></label><div data-testid=card-field-container></div><button type=submit class="btn btn-primary">`), _tmpl$2$1 = /*#__PURE__*/ template(`<p class=text-error role=alert data-testid=checkout-error>`);
|
|
197
|
+
function CheckoutForm(props) {
|
|
198
|
+
const cart = useCart();
|
|
199
|
+
const [email, setEmail] = createSignal("");
|
|
200
|
+
const [submitting, setSubmitting] = createSignal(false);
|
|
201
|
+
const [error, setError] = createSignal();
|
|
202
|
+
let cardContainer;
|
|
203
|
+
onMount(() => {
|
|
204
|
+
if (cardContainer) props.mountCardField?.(cardContainer);
|
|
205
|
+
});
|
|
206
|
+
async function handleSubmit(event) {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
setError(void 0);
|
|
209
|
+
setSubmitting(true);
|
|
210
|
+
try {
|
|
211
|
+
const paymentSourceToken = await props.tokenize();
|
|
212
|
+
const response = await fetch(props.checkoutEndpoint ?? "/api/checkout", {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: { "Content-Type": "application/json" },
|
|
215
|
+
body: JSON.stringify({
|
|
216
|
+
lineItems: cart.items().map((item) => ({
|
|
217
|
+
catalogRef: item.catalogRef,
|
|
218
|
+
quantity: item.quantity,
|
|
219
|
+
clientUnitPrice: item.unitPrice
|
|
220
|
+
})),
|
|
221
|
+
paymentSourceToken,
|
|
222
|
+
customerEmail: email(),
|
|
223
|
+
idempotencyKey: crypto.randomUUID()
|
|
224
|
+
})
|
|
225
|
+
});
|
|
226
|
+
const body = await response.json();
|
|
227
|
+
if (!response.ok) throw new Error(typeof body?.error === "string" ? body.error : "Checkout failed");
|
|
228
|
+
cart.clear();
|
|
229
|
+
props.onSuccess?.(body.order ?? body);
|
|
230
|
+
} catch (cause) {
|
|
231
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
232
|
+
setError(message);
|
|
233
|
+
props.onError?.(cause instanceof Error ? cause : new Error(message));
|
|
234
|
+
} finally {
|
|
235
|
+
setSubmitting(false);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return (() => {
|
|
239
|
+
var _el$ = _tmpl$$1(), _el$2 = _el$.firstChild, _el$4 = _el$2.firstChild.nextSibling, _el$5 = _el$2.nextSibling, _el$6 = _el$5.nextSibling;
|
|
240
|
+
_el$.addEventListener("submit", handleSubmit);
|
|
241
|
+
_el$4.$$input = (e) => setEmail(e.currentTarget.value);
|
|
242
|
+
var _ref$ = cardContainer;
|
|
243
|
+
typeof _ref$ === "function" ? use(_ref$, _el$5) : cardContainer = _el$5;
|
|
244
|
+
insert(_el$, (() => {
|
|
245
|
+
var _c$ = memo(() => !!error());
|
|
246
|
+
return () => _c$() && (() => {
|
|
247
|
+
var _el$7 = _tmpl$2$1();
|
|
248
|
+
insert(_el$7, error);
|
|
249
|
+
return _el$7;
|
|
250
|
+
})();
|
|
251
|
+
})(), _el$6);
|
|
252
|
+
insert(_el$6, () => submitting() ? "Processing…" : "Pay now");
|
|
253
|
+
effect(() => _el$6.disabled = submitting());
|
|
254
|
+
effect(() => _el$4.value = email());
|
|
255
|
+
return _el$;
|
|
256
|
+
})();
|
|
257
|
+
}
|
|
258
|
+
delegateEvents(["input"]);
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region src/ProductDetail.tsx
|
|
261
|
+
var _tmpl$ = /*#__PURE__*/ template(`<p class=opacity-80>`), _tmpl$2 = /*#__PURE__*/ template(`<label class="form-control w-full max-w-xs"><span class=label-text>Variant</span><select class="select select-bordered">`), _tmpl$3 = /*#__PURE__*/ template(`<div class=product-detail data-testid=product-detail><h1 class="text-2xl font-bold"></h1><button type=button class="btn btn-primary"><i class="ph ph-shopping-cart-simple"aria-hidden=true></i>Add to cart`), _tmpl$4 = /*#__PURE__*/ template(`<option>`), _tmpl$5 = /*#__PURE__*/ template(`<p class="text-xl font-semibold"data-testid=product-price>`);
|
|
262
|
+
function formatPrice(cents, currency) {
|
|
263
|
+
return new Intl.NumberFormat(void 0, {
|
|
264
|
+
style: "currency",
|
|
265
|
+
currency
|
|
266
|
+
}).format(cents / 100);
|
|
267
|
+
}
|
|
268
|
+
function ProductDetail(props) {
|
|
269
|
+
const cart = useCart();
|
|
270
|
+
const [selectedSku, setSelectedSku] = createSignal(props.product.variants[0]?.sku ?? "");
|
|
271
|
+
const selectedVariant = createMemo(() => props.product.variants.find((v) => v.sku === selectedSku()));
|
|
272
|
+
function handleAddToCart() {
|
|
273
|
+
const variant = selectedVariant();
|
|
274
|
+
if (!variant) return;
|
|
275
|
+
cart.addItem({
|
|
276
|
+
catalogRef: variant.catalogRef,
|
|
277
|
+
name: props.product.name,
|
|
278
|
+
unitPrice: {
|
|
279
|
+
amount: variant.priceCents,
|
|
280
|
+
currency: variant.currency
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
props.onAddToCart?.(variant);
|
|
284
|
+
}
|
|
285
|
+
return (() => {
|
|
286
|
+
var _el$ = _tmpl$3(), _el$2 = _el$.firstChild, _el$7 = _el$2.nextSibling;
|
|
287
|
+
insert(_el$2, () => props.product.name);
|
|
288
|
+
insert(_el$, createComponent(Show, {
|
|
289
|
+
get when() {
|
|
290
|
+
return props.product.description;
|
|
291
|
+
},
|
|
292
|
+
get children() {
|
|
293
|
+
var _el$3 = _tmpl$();
|
|
294
|
+
insert(_el$3, () => props.product.description);
|
|
295
|
+
return _el$3;
|
|
296
|
+
}
|
|
297
|
+
}), _el$7);
|
|
298
|
+
insert(_el$, createComponent(Show, {
|
|
299
|
+
get when() {
|
|
300
|
+
return props.product.variants.length > 1;
|
|
301
|
+
},
|
|
302
|
+
get children() {
|
|
303
|
+
var _el$4 = _tmpl$2(), _el$6 = _el$4.firstChild.nextSibling;
|
|
304
|
+
_el$6.addEventListener("change", (e) => setSelectedSku(e.currentTarget.value));
|
|
305
|
+
insert(_el$6, createComponent(For, {
|
|
306
|
+
get each() {
|
|
307
|
+
return props.product.variants;
|
|
308
|
+
},
|
|
309
|
+
children: (variant) => (() => {
|
|
310
|
+
var _el$8 = _tmpl$4();
|
|
311
|
+
insert(_el$8, () => variant.sku);
|
|
312
|
+
effect(() => _el$8.value = variant.sku);
|
|
313
|
+
return _el$8;
|
|
314
|
+
})()
|
|
315
|
+
}));
|
|
316
|
+
effect(() => _el$6.value = selectedSku());
|
|
317
|
+
return _el$4;
|
|
318
|
+
}
|
|
319
|
+
}), _el$7);
|
|
320
|
+
insert(_el$, createComponent(Show, {
|
|
321
|
+
get when() {
|
|
322
|
+
return selectedVariant();
|
|
323
|
+
},
|
|
324
|
+
children: (variant) => (() => {
|
|
325
|
+
var _el$9 = _tmpl$5();
|
|
326
|
+
insert(_el$9, () => formatPrice(variant().priceCents, variant().currency));
|
|
327
|
+
return _el$9;
|
|
328
|
+
})()
|
|
329
|
+
}), _el$7);
|
|
330
|
+
_el$7.$$click = handleAddToCart;
|
|
331
|
+
effect(() => _el$7.disabled = !selectedVariant());
|
|
332
|
+
return _el$;
|
|
333
|
+
})();
|
|
334
|
+
}
|
|
335
|
+
delegateEvents(["click"]);
|
|
336
|
+
//#endregion
|
|
337
|
+
export { CartDrawer, CartProvider, CheckoutForm, ProductDetail, useCart };
|
|
338
|
+
|
|
339
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["Accessor","createContext","createSignal","JSX","onMount","useContext","CartLineItem","catalogRef","name","quantity","unitPrice","amount","currency","CartContextValue","items","isOpen","subtotal","addItem","item","Omit","removeItem","updateQuantity","clear","open","close","CartContext","STORAGE_KEY","loadStoredItems","window","raw","localStorage","getItem","JSON","parse","CartProviderProps","children","Element","CartProvider","props","setItems","setIsOpen","persist","next","setItem","stringify","value","reduce","sum","existing","find","i","map","filter","_$createComponent","Provider","useCart","context","Error","createEffect","For","JSX","onCleanup","Show","useCart","FOCUSABLE_SELECTOR","formatPrice","cents","currency","Intl","NumberFormat","undefined","style","format","CartDrawerProps","checkoutSlot","Element","CartDrawer","props","cart","asideRef","HTMLElement","closeButtonRef","HTMLButtonElement","triggeredBy","isOpen","focus","document","activeElement","previousOverflow","body","overflow","onKeyDown","event","KeyboardEvent","key","close","focusable","Array","from","querySelectorAll","length","first","last","shiftKey","preventDefault","addEventListener","removeEventListener","_$createComponent","when","children","_el$","_tmpl$3","_el$2","firstChild","_el$3","nextSibling","_el$4","_el$5","_el$6","_el$7","$$click","_$insert","items","fallback","subtotal","unitPrice","_ref$","_$use","_ref$2","_tmpl$4","_el$8","_tmpl$","each","item","_el$11","_tmpl$5","_el$12","_el$13","_el$14","_el$15","_el$17","_el$18","_el$19","_el$20","_el$21","name","amount","quantity","updateQuantity","catalogRef","removeItem","_$effect","_p$","_v$","_v$2","_v$3","_v$4","e","_$setAttribute","t","a","o","_el$9","_tmpl$2","_el$0","_el$1","_$memo","_$delegateEvents","createSignal","onMount","useCart","CheckoutFormProps","tokenize","Promise","mountCardField","container","HTMLDivElement","checkoutEndpoint","onSuccess","order","onError","error","Error","CheckoutForm","props","cart","email","setEmail","submitting","setSubmitting","setError","cardContainer","handleSubmit","event","SubmitEvent","preventDefault","undefined","paymentSourceToken","response","fetch","method","headers","body","JSON","stringify","lineItems","items","map","item","catalogRef","quantity","clientUnitPrice","unitPrice","customerEmail","idempotencyKey","crypto","randomUUID","json","ok","clear","cause","message","String","_el$","_tmpl$","_el$2","firstChild","_el$3","_el$4","nextSibling","_el$5","_el$6","addEventListener","$$input","e","currentTarget","value","_ref$","_$use","_$insert","_c$","_$memo","_el$7","_tmpl$2","_$effect","disabled","_$delegateEvents","createMemo","createSignal","For","Show","useCart","ProductVariant","sku","catalogRef","priceCents","currency","inventoryCount","Product","id","name","description","variants","ProductDetailProps","product","onAddToCart","variant","formatPrice","cents","Intl","NumberFormat","undefined","style","format","ProductDetail","props","cart","selectedSku","setSelectedSku","selectedVariant","find","v","handleAddToCart","addItem","unitPrice","amount","_el$","_tmpl$3","_el$2","firstChild","_el$7","nextSibling","_$insert","_$createComponent","when","children","_el$3","_tmpl$","length","_el$4","_tmpl$2","_el$5","_el$6","addEventListener","e","currentTarget","value","each","_el$8","_tmpl$4","_$effect","_el$9","_tmpl$5","$$click","disabled","_$delegateEvents"],"sources":["../src/CartProvider.tsx","../src/CartDrawer.tsx","../src/CheckoutForm.tsx","../src/ProductDetail.tsx"],"sourcesContent":["// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n//\n// localStorage-backed cart state, the same pattern found independently in\n// two of the three Next.js + Payload reference repos this package's\n// design was generalized from (a `CartContext`-style provider, no DB sync\n// needed until checkout). Pure SolidJS reactivity (createSignal), no\n// React-isms.\n\nimport {\n type Accessor,\n createContext,\n createSignal,\n type JSX,\n onMount,\n useContext,\n} from \"solid-js\";\n\nexport interface CartLineItem {\n catalogRef: string;\n name: string;\n quantity: number;\n unitPrice: { amount: number; currency: string };\n}\n\nexport interface CartContextValue {\n items: Accessor<CartLineItem[]>;\n isOpen: Accessor<boolean>;\n subtotal: Accessor<number>;\n addItem: (item: Omit<CartLineItem, \"quantity\">, quantity?: number) => void;\n removeItem: (catalogRef: string) => void;\n updateQuantity: (catalogRef: string, quantity: number) => void;\n clear: () => void;\n open: () => void;\n close: () => void;\n}\n\nconst CartContext = createContext<CartContextValue>();\n\nconst STORAGE_KEY = \"cadmea-ecommerce-cart\";\n\nfunction loadStoredItems(): CartLineItem[] {\n if (typeof window === \"undefined\") return [];\n try {\n const raw = window.localStorage.getItem(STORAGE_KEY);\n return raw ? (JSON.parse(raw) as CartLineItem[]) : [];\n } catch {\n // A corrupted or inaccessible localStorage entry shouldn't crash the\n // storefront — fall back to an empty cart, same as a first-ever visit.\n return [];\n }\n}\n\nexport interface CartProviderProps {\n children: JSX.Element;\n}\n\nexport function CartProvider(props: CartProviderProps) {\n const [items, setItems] = createSignal<CartLineItem[]>([]);\n const [isOpen, setIsOpen] = createSignal(false);\n\n // Deferred to onMount rather than read at module/signal-init time —\n // keeps this component safe to import in a context where `window` isn't\n // defined yet (e.g. a build-time render pass), even though every real\n // usage is a client-only island.\n onMount(() => setItems(loadStoredItems()));\n\n function persist(next: CartLineItem[]) {\n setItems(next);\n if (typeof window === \"undefined\") return;\n window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));\n }\n\n const value: CartContextValue = {\n items,\n isOpen,\n subtotal: () =>\n items().reduce(\n (sum, item) => sum + item.unitPrice.amount * item.quantity,\n 0,\n ),\n addItem(item, quantity = 1) {\n const existing = items().find((i) => i.catalogRef === item.catalogRef);\n if (existing) {\n persist(\n items().map((i) =>\n i.catalogRef === item.catalogRef\n ? { ...i, quantity: i.quantity + quantity }\n : i,\n ),\n );\n } else {\n persist([...items(), { ...item, quantity }]);\n }\n },\n removeItem(catalogRef) {\n persist(items().filter((i) => i.catalogRef !== catalogRef));\n },\n updateQuantity(catalogRef, quantity) {\n if (quantity <= 0) {\n persist(items().filter((i) => i.catalogRef !== catalogRef));\n return;\n }\n persist(\n items().map((i) =>\n i.catalogRef === catalogRef ? { ...i, quantity } : i,\n ),\n );\n },\n clear() {\n persist([]);\n },\n open: () => setIsOpen(true),\n close: () => setIsOpen(false),\n };\n\n return (\n <CartContext.Provider value={value}>{props.children}</CartContext.Provider>\n );\n}\n\nexport function useCart(): CartContextValue {\n const context = useContext(CartContext);\n if (!context) {\n throw new Error(\"useCart() must be called within a <CartProvider>\");\n }\n return context;\n}\n","// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n\nimport { createEffect, For, type JSX, onCleanup, Show } from \"solid-js\";\nimport { useCart } from \"./CartProvider.js\";\n\nconst FOCUSABLE_SELECTOR =\n 'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\nfunction formatPrice(cents: number, currency: string): string {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency,\n }).format(cents / 100);\n}\n\nexport interface CartDrawerProps {\n /** Rendered inside the drawer below the line items, typically a checkout link/button. */\n checkoutSlot?: () => JSX.Element;\n}\n\nexport function CartDrawer(props: CartDrawerProps) {\n const cart = useCart();\n\n let asideRef: HTMLElement | undefined;\n let closeButtonRef: HTMLButtonElement | undefined;\n let triggeredBy: HTMLElement | null = null;\n\n // The drawer is a modal dialog while open: move focus in, cycle Tab\n // within it, close on Escape, restore focus on close, and lock body\n // scroll behind it. Mirrors PanelNav/SearchPalette's focus-trap idiom.\n createEffect(() => {\n if (!cart.isOpen()) {\n triggeredBy?.focus();\n return;\n }\n\n triggeredBy = document.activeElement as HTMLElement | null;\n closeButtonRef?.focus();\n\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key === \"Escape\") {\n cart.close();\n return;\n }\n if (event.key !== \"Tab\" || !asideRef) return;\n\n const focusable = Array.from(\n asideRef.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),\n );\n if (focusable.length === 0) return;\n\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n\n if (event.shiftKey && document.activeElement === first) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault();\n first.focus();\n }\n }\n\n document.addEventListener(\"keydown\", onKeyDown);\n onCleanup(() => {\n document.removeEventListener(\"keydown\", onKeyDown);\n document.body.style.overflow = previousOverflow;\n });\n });\n\n return (\n <Show when={cart.isOpen()}>\n <div class=\"cart-drawer fixed inset-0 z-50\" data-testid=\"cart-drawer\">\n <button\n type=\"button\"\n class=\"fixed inset-0 bg-[var(--color-backdrop)]\"\n aria-label=\"Close cart\"\n onClick={() => cart.close()}\n />\n {/* Announce cart contents to assistive tech as items change. */}\n <div aria-live=\"polite\" aria-atomic=\"true\" class=\"sr-only\">\n <Show when={cart.items().length > 0} fallback=\"Cart is empty.\">\n {`Cart has ${cart.items().length} item${cart.items().length === 1 ? \"\" : \"s\"}, subtotal ${formatPrice(\n cart.subtotal(),\n cart.items()[0]?.unitPrice.currency ?? \"USD\",\n )}.`}\n </Show>\n </div>\n <aside\n ref={asideRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Shopping cart\"\n class=\"fixed right-0 top-0 h-full w-full max-w-sm bg-base-100 p-4 shadow-xl\"\n >\n <div class=\"flex items-center justify-between\">\n <h2 class=\"text-lg font-bold\">Your cart</h2>\n <button\n ref={closeButtonRef}\n type=\"button\"\n class=\"btn btn-sm btn-circle\"\n aria-label=\"Close cart\"\n onClick={() => cart.close()}\n >\n <i class=\"ph ph-x\" aria-hidden=\"true\" />\n </button>\n </div>\n\n <Show\n when={cart.items().length > 0}\n fallback={<p class=\"opacity-70\">Your cart is empty.</p>}\n >\n <ul class=\"mt-4 flex flex-col gap-3\">\n <For each={cart.items()}>\n {(item) => (\n <li class=\"flex items-center justify-between gap-2\">\n <div>\n <p class=\"font-medium\">{item.name}</p>\n <p class=\"text-sm opacity-70\">\n {formatPrice(\n item.unitPrice.amount,\n item.unitPrice.currency,\n )}{\" \"}\n × {item.quantity}\n </p>\n </div>\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"btn btn-xs\"\n aria-label={`Decrease quantity of ${item.name}`}\n onClick={() =>\n cart.updateQuantity(\n item.catalogRef,\n item.quantity - 1,\n )\n }\n >\n −\n </button>\n <span data-testid={`quantity-${item.catalogRef}`}>\n {item.quantity}\n </span>\n <button\n type=\"button\"\n class=\"btn btn-xs\"\n aria-label={`Increase quantity of ${item.name}`}\n onClick={() =>\n cart.updateQuantity(\n item.catalogRef,\n item.quantity + 1,\n )\n }\n >\n +\n </button>\n <button\n type=\"button\"\n class=\"btn btn-xs btn-ghost\"\n aria-label={`Remove ${item.name}`}\n onClick={() => cart.removeItem(item.catalogRef)}\n >\n <i class=\"ph ph-trash\" aria-hidden=\"true\" />\n </button>\n </div>\n </li>\n )}\n </For>\n </ul>\n\n <div class=\"mt-4 flex items-center justify-between border-t pt-4\">\n <span class=\"font-semibold\">Subtotal</span>\n <span data-testid=\"cart-subtotal\">\n {formatPrice(\n cart.subtotal(),\n cart.items()[0]?.unitPrice.currency ?? \"USD\",\n )}\n </span>\n </div>\n\n {props.checkoutSlot?.()}\n </Show>\n </aside>\n </div>\n </Show>\n );\n}\n","// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n//\n// Provider-agnostic — takes a `tokenize` callback rather than knowing\n// about Square/Stripe itself. The consumer wires in whichever provider's\n// `/client` helper (`createSquareCardField`/`createStripeCardField`) it's\n// using; both share the exact `{ tokenize(): Promise<string> }` shape so\n// swapping providers means swapping that one prop, not rewriting this\n// component.\n\nimport { createSignal, onMount } from \"solid-js\";\nimport { useCart } from \"./CartProvider.js\";\n\nexport interface CheckoutFormProps {\n /** Mounts the provider's card field and produces a one-time payment source token — see this package's README for wiring either provider's `/client` helper here. */\n tokenize: () => Promise<string>;\n /** Called once the provider's card field should be mounted — receives the container element to attach to. Most consumers wire this to `createSquareCardField(el, opts)`/`createStripeCardField(el, opts)` directly and ignore the return value here (the `tokenize` prop is what's actually called on submit). */\n mountCardField?: (container: HTMLDivElement) => void;\n /** Default: \"/api/checkout\". */\n checkoutEndpoint?: string;\n onSuccess?: (order: unknown) => void;\n onError?: (error: Error) => void;\n}\n\nexport function CheckoutForm(props: CheckoutFormProps) {\n const cart = useCart();\n const [email, setEmail] = createSignal(\"\");\n const [submitting, setSubmitting] = createSignal(false);\n const [error, setError] = createSignal<string | undefined>();\n let cardContainer: HTMLDivElement | undefined;\n\n onMount(() => {\n if (cardContainer) props.mountCardField?.(cardContainer);\n });\n\n async function handleSubmit(event: SubmitEvent) {\n event.preventDefault();\n setError(undefined);\n setSubmitting(true);\n try {\n const paymentSourceToken = await props.tokenize();\n const response = await fetch(props.checkoutEndpoint ?? \"/api/checkout\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n lineItems: cart.items().map((item) => ({\n catalogRef: item.catalogRef,\n quantity: item.quantity,\n clientUnitPrice: item.unitPrice,\n })),\n paymentSourceToken,\n customerEmail: email(),\n idempotencyKey: crypto.randomUUID(),\n }),\n });\n const body = await response.json();\n if (!response.ok) {\n throw new Error(\n typeof body?.error === \"string\" ? body.error : \"Checkout failed\",\n );\n }\n cart.clear();\n props.onSuccess?.(body.order ?? body);\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n setError(message);\n props.onError?.(cause instanceof Error ? cause : new Error(message));\n } finally {\n setSubmitting(false);\n }\n }\n\n return (\n <form class=\"checkout-form flex flex-col gap-4\" onSubmit={handleSubmit}>\n <label class=\"form-control\">\n <span class=\"label-text\">Email</span>\n <input\n type=\"email\"\n required\n class=\"input input-bordered\"\n value={email()}\n onInput={(e) => setEmail(e.currentTarget.value)}\n />\n </label>\n\n <div ref={cardContainer} data-testid=\"card-field-container\" />\n\n {error() && (\n <p class=\"text-error\" role=\"alert\" data-testid=\"checkout-error\">\n {error()}\n </p>\n )}\n\n <button type=\"submit\" class=\"btn btn-primary\" disabled={submitting()}>\n {submitting() ? \"Processing…\" : \"Pay now\"}\n </button>\n </form>\n );\n}\n","// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n\nimport { createMemo, createSignal, For, Show } from \"solid-js\";\nimport { useCart } from \"./CartProvider.js\";\n\nexport interface ProductVariant {\n sku: string;\n catalogRef: string;\n priceCents: number;\n currency: string;\n inventoryCount?: number;\n}\n\nexport interface Product {\n id: number;\n name: string;\n description?: string;\n variants: ProductVariant[];\n}\n\nexport interface ProductDetailProps {\n product: Product;\n onAddToCart?: (variant: ProductVariant) => void;\n}\n\nfunction formatPrice(cents: number, currency: string): string {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency,\n }).format(cents / 100);\n}\n\nexport function ProductDetail(props: ProductDetailProps) {\n const cart = useCart();\n const [selectedSku, setSelectedSku] = createSignal(\n props.product.variants[0]?.sku ?? \"\",\n );\n\n const selectedVariant = createMemo(() =>\n props.product.variants.find((v) => v.sku === selectedSku()),\n );\n\n function handleAddToCart() {\n const variant = selectedVariant();\n if (!variant) return;\n cart.addItem({\n catalogRef: variant.catalogRef,\n name: props.product.name,\n unitPrice: { amount: variant.priceCents, currency: variant.currency },\n });\n props.onAddToCart?.(variant);\n }\n\n return (\n <div class=\"product-detail\" data-testid=\"product-detail\">\n <h1 class=\"text-2xl font-bold\">{props.product.name}</h1>\n <Show when={props.product.description}>\n <p class=\"opacity-80\">{props.product.description}</p>\n </Show>\n\n <Show when={props.product.variants.length > 1}>\n <label class=\"form-control w-full max-w-xs\">\n <span class=\"label-text\">Variant</span>\n <select\n class=\"select select-bordered\"\n value={selectedSku()}\n onChange={(e) => setSelectedSku(e.currentTarget.value)}\n >\n <For each={props.product.variants}>\n {(variant) => <option value={variant.sku}>{variant.sku}</option>}\n </For>\n </select>\n </label>\n </Show>\n\n <Show when={selectedVariant()}>\n {(variant) => (\n <p class=\"text-xl font-semibold\" data-testid=\"product-price\">\n {formatPrice(variant().priceCents, variant().currency)}\n </p>\n )}\n </Show>\n\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n disabled={!selectedVariant()}\n onClick={handleAddToCart}\n >\n <i class=\"ph ph-shopping-cart-simple\" aria-hidden=\"true\" />\n Add to cart\n </button>\n </div>\n );\n}\n"],"mappings":";;;AAqCA,MAAMyB,cAAcxB,cAAgC;AAEpD,MAAMyB,cAAc;AAEpB,SAASC,kBAAkC;CACzC,IAAI,OAAOC,WAAW,aAAa,OAAO,CAAA;CAC1C,IAAI;EACF,MAAMC,MAAMD,OAAOE,aAAaC,QAAQL,WAAW;EACnD,OAAOG,MAAOG,KAAKC,MAAMJ,GAAG,IAAuB,CAAA;CACrD,QAAQ;EAGN,OAAO,CAAA;CACT;AACF;AAMA,SAAgBQ,aAAaC,OAA0B;CACrD,MAAM,CAACxB,OAAOyB,YAAYrC,aAA6B,CAAA,CAAE;CACzD,MAAM,CAACa,QAAQyB,aAAatC,aAAa,KAAK;CAM9CE,cAAcmC,SAASZ,gBAAgB,CAAC,CAAC;CAEzC,SAASc,QAAQC,MAAsB;EACrCH,SAASG,IAAI;EACb,IAAI,OAAOd,WAAW,aAAa;EACnCA,OAAOE,aAAaa,QAAQjB,aAAaM,KAAKY,UAAUF,IAAI,CAAC;CAC/D;CAEA,MAAMG,QAA0B;EAC9B/B;EACAC;EACAC,gBACEF,MAAM,CAAC,CAACgC,QACLC,KAAK7B,SAAS6B,MAAM7B,KAAKR,UAAUC,SAASO,KAAKT,UAClD,CACF;EACFQ,QAAQC,MAAMT,WAAW,GAAG;GAE1B,IADiBK,MAAM,CAAC,CAACmC,MAAMC,MAAMA,EAAE3C,eAAeW,KAAKX,UACvDyC,GACFP,QACE3B,MAAM,CAAC,CAACqC,KAAKD,MACXA,EAAE3C,eAAeW,KAAKX,aAClB;IAAE,GAAG2C;IAAGzC,UAAUyC,EAAEzC,WAAWA;GAAS,IACxCyC,CACN,CACF;QAEAT,QAAQ,CAAC,GAAG3B,MAAM,GAAG;IAAE,GAAGI;IAAMT;GAAS,CAAC,CAAC;EAE/C;EACAW,WAAWb,YAAY;GACrBkC,QAAQ3B,MAAM,CAAC,CAACsC,QAAQF,MAAMA,EAAE3C,eAAeA,UAAU,CAAC;EAC5D;EACAc,eAAed,YAAYE,UAAU;GACnC,IAAIA,YAAY,GAAG;IACjBgC,QAAQ3B,MAAM,CAAC,CAACsC,QAAQF,MAAMA,EAAE3C,eAAeA,UAAU,CAAC;IAC1D;GACF;GACAkC,QACE3B,MAAM,CAAC,CAACqC,KAAKD,MACXA,EAAE3C,eAAeA,aAAa;IAAE,GAAG2C;IAAGzC;GAAS,IAAIyC,CACrD,CACF;EACF;EACA5B,QAAQ;GACNmB,QAAQ,CAAA,CAAE;EACZ;EACAlB,YAAYiB,UAAU,IAAI;EAC1BhB,aAAagB,UAAU,KAAK;CAC9B;CAEA,OAAAa,gBACG5B,YAAY6B,UAAQ;EAAQT;EAAK,IAAAV,WAAA;GAAA,OAAGG,MAAMH;EAAQ;CAAA,CAAA;AAEvD;AAEA,SAAgBoB,UAA4B;CAC1C,MAAMC,UAAUnD,WAAWoB,WAAW;CACtC,IAAI,CAAC+B,SACH,MAAM,IAAIC,MAAM,kDAAkD;CAEpE,OAAOD;AACT;;;;ACzHA,MAAMQ,qBACJ;AAEF,SAASC,cAAYC,OAAeC,UAA0B;CAC5D,OAAO,IAAIC,KAAKC,aAAaC,KAAAA,GAAW;EACtCC,OAAO;EACPJ;CACF,CAAC,CAAC,CAACK,OAAON,QAAQ,GAAG;AACvB;AAOA,SAAgBU,WAAWC,OAAwB;CACjD,MAAMC,OAAOf,QAAQ;CAErB,IAAIgB;CACJ,IAAIE;CACJ,IAAIE,cAAkC;CAKtCzB,mBAAmB;EACjB,IAAI,CAACoB,KAAKM,OAAO,GAAG;GAClBD,aAAaE,MAAM;GACnB;EACF;EAEAF,cAAcG,SAASC;EACvBN,gBAAgBI,MAAM;EAEtB,MAAMG,mBAAmBF,SAASG,KAAKlB,MAAMmB;EAC7CJ,SAASG,KAAKlB,MAAMmB,WAAW;EAE/B,SAASC,UAAUC,OAAsB;GACvC,IAAIA,MAAME,QAAQ,UAAU;IAC1BhB,KAAKiB,MAAM;IACX;GACF;GACA,IAAIH,MAAME,QAAQ,SAAS,CAACf,UAAU;GAEtC,MAAMiB,YAAYC,MAAMC,KACtBnB,SAASoB,iBAA8BnC,kBAAkB,CAC3D;GACA,IAAIgC,UAAUI,WAAW,GAAG;GAE5B,MAAMC,QAAQL,UAAU;GACxB,MAAMM,OAAON,UAAUA,UAAUI,SAAS;GAE1C,IAAIR,MAAMW,YAAYjB,SAASC,kBAAkBc,OAAO;IACtDT,MAAMY,eAAe;IACrBF,KAAKjB,MAAM;GACb,OAAO,IAAI,CAACO,MAAMW,YAAYjB,SAASC,kBAAkBe,MAAM;IAC7DV,MAAMY,eAAe;IACrBH,MAAMhB,MAAM;GACd;EACF;EAEAC,SAASmB,iBAAiB,WAAWd,SAAS;EAC9C9B,gBAAgB;GACdyB,SAASoB,oBAAoB,WAAWf,SAAS;GACjDL,SAASG,KAAKlB,MAAMmB,WAAWF;EACjC,CAAC;CACH,CAAC;CAED,OAAAmB,gBACG7C,MAAI;EAAA,IAAC8C,OAAI;GAAA,OAAE9B,KAAKM,OAAO;EAAC;EAAA,IAAAyB,WAAA;GAAA,IAAAC,OAAAC,UAAA,GAAAC,QAAAF,KAAAG,YAAAC,QAAAF,MAAAG,aAAAC,QAAAF,MAAAC,aAAAI,QAAAH,MAAAH,WAAAA,WAAAE;GAAAH,MAAAQ,gBAMJ1C,KAAKiB,MAAM;GAAC0B,OAAAP,OAAAP,gBAI1B7C,MAAI;IAAA,IAAC8C,OAAI;KAAA,OAAE9B,KAAK4C,MAAM,CAAC,CAACtB,SAAS;IAAC;IAAEuB,UAAQ;IAAA,IAAAd,WAAA;KAAA,OAC1C,YAAY/B,KAAK4C,MAAM,CAAC,CAACtB,OAAM,OAAQtB,KAAK4C,MAAM,CAAC,CAACtB,WAAW,IAAI,KAAK,IAAG,aAAcnC,cACxFa,KAAK8C,SAAS,GACd9C,KAAK4C,MAAM,CAAC,CAAC,EAAE,EAAEG,UAAU1D,YAAY,KACzC,EAAC;IAAG;GAAA,CAAA,CAAA;GAAA,IAAA2D,QAID/C;GAAQ,OAAA+C,UAAA,aAAAC,IAAAD,OAAAV,KAAA,IAARrC,WAAQqC;GAAAG,MAAAC,gBAaM1C,KAAKiB,MAAM;GAAC,IAAAiC,SAJtB/C;GAAc,OAAA+C,WAAA,aAAAD,IAAAC,QAAAT,KAAA,IAAdtC,iBAAcsC;GAAAE,OAAAL,OAAAT,gBAUtB7C,MAAI;IAAA,IACH8C,OAAI;KAAA,OAAE9B,KAAK4C,MAAM,CAAC,CAACtB,SAAS;IAAC;IAAA,IAC7BuB,WAAQ;KAAA,OAAAM,UAAA;IAAA;IAAA,IAAApB,WAAA;KAAA,OAAA;aAAA;OAAA,IAAAqB,QAAAC,SAAA;OAAAV,OAAAS,OAAAvB,gBAGLhD,KAAG;QAAA,IAACyE,OAAI;SAAA,OAAEtD,KAAK4C,MAAM;QAAC;QAAAb,WACnBwB,gBAAI;SAAA,IAAAC,SAAAC,UAAA,GAAAC,SAAAF,OAAArB,YAAAwB,SAAAD,OAAAvB,YAAAyB,SAAAD,OAAAtB,aAAAwB,SAAAD,OAAAzB,YAAA4B,SAAAL,OAAArB,YAAAF,YAAA6B,SAAAD,OAAA1B,aAAA4B,SAAAD,OAAA3B,aAAA6B,SAAAD,OAAA5B;SAAAM,OAAAgB,cAGwBJ,KAAKY,IAAI;SAAAxB,OAAAiB,cAE9BzE,cACCoE,KAAKR,UAAUqB,QACfb,KAAKR,UAAU1D,QACjB,GAACwE,MAAA;SAAAlB,OAAAiB,cACEL,KAAKc,UAAQ,IAAA;SAAAN,OAAArB,gBASd1C,KAAKsE,eACHf,KAAKgB,YACLhB,KAAKc,WAAW,CAClB;SAAC1B,OAAAqB,cAMFT,KAAKc,QAAQ;SAAAJ,OAAAvB,gBAOZ1C,KAAKsE,eACHf,KAAKgB,YACLhB,KAAKc,WAAW,CAClB;SAACH,OAAAxB,gBASY1C,KAAKwE,WAAWjB,KAAKgB,UAAU;SAACE,QAAAC,QAAA;UAAA,IAAAC,MA9BnC,wBAAwBpB,KAAKY,QAAMS,OAU9B,YAAYrB,KAAKgB,cAAYM,OAMlC,wBAAwBtB,KAAKY,QAAMW,OAanC,UAAUvB,KAAKY;UAAMQ,QAAAD,IAAAK,KAAAC,aAAAjB,QAAA,cAAAW,IAAAK,IAAAJ,GAAA;UAAAC,SAAAF,IAAAO,KAAAD,aAAAhB,QAAA,eAAAU,IAAAO,IAAAL,IAAA;UAAAC,SAAAH,IAAAQ,KAAAF,aAAAf,QAAA,cAAAS,IAAAQ,IAAAL,IAAA;UAAAC,SAAAJ,IAAAS,KAAAH,aAAAd,QAAA,cAAAQ,IAAAS,IAAAL,IAAA;UAAA,OAAAJ;SAAA,GAAA;UAAAK,GAAAvF,KAAAA;UAAAyF,GAAAzF,KAAAA;UAAA0F,GAAA1F,KAAAA;UAAA2F,GAAA3F,KAAAA;SAAA,CAAA;SAAA,OAAAgE;QAAA,EAAA,CAAA;OAOxC,CAAA,CAAA;OAAA,OAAAJ;MAAA,EAAA,CAAA;aAAA;OAAA,IAAAgC,QAAAC,UAAA,GAAAE,QAAAH,MAAAjD,WAAAE;OAAAM,OAAA4C,aAOApG,cACCa,KAAK8C,SAAS,GACd9C,KAAK4C,MAAM,CAAC,CAAC,EAAE,EAAEG,UAAU1D,YAAY,KACzC,CAAC;OAAA,OAAA+F;MAAA,EAAA,CAAA;MAAAI,WAIJzF,MAAMH,eAAe,CAAC;KAAA;IAAA;GAAA,CAAA,GAAA,IAAA;GAAA,OAAAoC;EAAA;CAAA,CAAA;AAMnC;AAACyD,eAAA,CAAA,OAAA,CAAA;;;;ACtKD,SAAgBgB,aAAaC,OAA0B;CACrD,MAAMC,OAAOf,QAAQ;CACrB,MAAM,CAACgB,OAAOC,YAAYnB,aAAa,EAAE;CACzC,MAAM,CAACoB,YAAYC,iBAAiBrB,aAAa,KAAK;CACtD,MAAM,CAACa,OAAOS,YAAYtB,aAAiC;CAC3D,IAAIuB;CAEJtB,cAAc;EACZ,IAAIsB,eAAeP,MAAMV,iBAAiBiB,aAAa;CACzD,CAAC;CAED,eAAeC,aAAaC,OAAoB;EAC9CA,MAAME,eAAe;EACrBL,SAASM,KAAAA,CAAS;EAClBP,cAAc,IAAI;EAClB,IAAI;GACF,MAAMQ,qBAAqB,MAAMb,MAAMZ,SAAS;GAChD,MAAM0B,WAAW,MAAMC,MAAMf,MAAMP,oBAAoB,iBAAiB;IACtEuB,QAAQ;IACRC,SAAS,EAAE,gBAAgB,mBAAmB;IAC9CC,MAAMC,KAAKC,UAAU;KACnBC,WAAWpB,KAAKqB,MAAM,CAAC,CAACC,KAAKC,UAAU;MACrCC,YAAYD,KAAKC;MACjBC,UAAUF,KAAKE;MACfC,iBAAiBH,KAAKI;KACxB,EAAE;KACFf;KACAgB,eAAe3B,MAAM;KACrB4B,gBAAgBC,OAAOC,WAAW;IACpC,CAAC;GACH,CAAC;GACD,MAAMd,OAAO,MAAMJ,SAASmB,KAAK;GACjC,IAAI,CAACnB,SAASoB,IACZ,MAAM,IAAIpC,MACR,OAAOoB,MAAMrB,UAAU,WAAWqB,KAAKrB,QAAQ,iBACjD;GAEFI,KAAKkC,MAAM;GACXnC,MAAMN,YAAYwB,KAAKvB,SAASuB,IAAI;EACtC,SAASkB,OAAO;GACd,MAAMC,UAAUD,iBAAiBtC,QAAQsC,MAAMC,UAAUC,OAAOF,KAAK;GACrE9B,SAAS+B,OAAO;GAChBrC,MAAMJ,UAAUwC,iBAAiBtC,QAAQsC,QAAQ,IAAItC,MAAMuC,OAAO,CAAC;EACrE,UAAU;GACRhC,cAAc,KAAK;EACrB;CACF;CAEA,cAAA;EAAA,IAAAkC,OAAAC,SAAA,GAAAC,QAAAF,KAAAG,YAAAE,QAAAH,MAAAC,WAAAG,aAAAC,QAAAL,MAAAI,aAAAE,QAAAD,MAAAD;EAAAN,KAAAS,iBAAA,UAC4DxC,YAAY;EAAAoC,MAAAK,WAQtDC,MAAM/C,SAAS+C,EAAEC,cAAcC,KAAK;EAAC,IAAAC,QAIzC9C;EAAa,OAAA8C,UAAA,aAAAC,IAAAD,OAAAP,KAAA,IAAbvC,gBAAauC;EAAAS,OAAAhB,aAAA;GAAA,IAAAiB,MAAAC,WAAA,CAAA,CAEtB5D,MAAM,CAAC;GAAA,aAAP2D,IAAA,YAAA;IAAA,IAAAE,QAAAC,UAAA;IAAAJ,OAAAG,OAEI7D,KAAK;IAAA,OAAA6D;GAAA,EAAA,CAAA;EAET,EAAA,CAAA,GAAAX,KAAA;EAAAQ,OAAAR,aAGE3C,WAAW,IAAI,gBAAgB,SAAS;EAAAwD,aAAAb,MAAAc,WADazD,WAAW,CAAC;EAAAwD,aAAAhB,MAAAQ,QAbzDlD,MAAM,CAAC;EAAA,OAAAqC;CAAA,EAAA,CAAA;AAkBxB;AAACuB,eAAA,CAAA,OAAA,CAAA;;;;ACxED,SAASqB,YAAYC,OAAeZ,UAA0B;CAC5D,OAAO,IAAIa,KAAKC,aAAaC,KAAAA,GAAW;EACtCC,OAAO;EACPhB;CACF,CAAC,CAAC,CAACiB,OAAOL,QAAQ,GAAG;AACvB;AAEA,SAAgBM,cAAcC,OAA2B;CACvD,MAAMC,OAAOzB,QAAQ;CACrB,MAAM,CAAC0B,aAAaC,kBAAkB9B,aACpC2B,MAAMX,QAAQF,SAAS,EAAE,EAAET,OAAO,EACpC;CAEA,MAAM0B,kBAAkBhC,iBACtB4B,MAAMX,QAAQF,SAASkB,MAAMC,MAAMA,EAAE5B,QAAQwB,YAAY,CAAC,CAC5D;CAEA,SAASK,kBAAkB;EACzB,MAAMhB,UAAUa,gBAAgB;EAChC,IAAI,CAACb,SAAS;EACdU,KAAKO,QAAQ;GACX7B,YAAYY,QAAQZ;GACpBM,MAAMe,MAAMX,QAAQJ;GACpBwB,WAAW;IAAEC,QAAQnB,QAAQX;IAAYC,UAAUU,QAAQV;GAAS;EACtE,CAAC;EACDmB,MAAMV,cAAcC,OAAO;CAC7B;CAEA,cAAA;EAAA,IAAAoB,OAAAC,QAAA,GAAAC,QAAAF,KAAAG,YAAAC,QAAAF,MAAAG;EAAAC,OAAAJ,aAEoCb,MAAMX,QAAQJ,IAAI;EAAAgC,OAAAN,MAAAO,gBACjD3C,MAAI;GAAA,IAAC4C,OAAI;IAAA,OAAEnB,MAAMX,QAAQH;GAAW;GAAA,IAAAkC,WAAA;IAAA,IAAAC,QAAAC,OAAA;IAAAL,OAAAI,aACZrB,MAAMX,QAAQH,WAAW;IAAA,OAAAmC;GAAA;EAAA,CAAA,GAAAN,KAAA;EAAAE,OAAAN,MAAAO,gBAGjD3C,MAAI;GAAA,IAAC4C,OAAI;IAAA,OAAEnB,MAAMX,QAAQF,SAASoC,SAAS;GAAC;GAAA,IAAAH,WAAA;IAAA,IAAAI,QAAAC,QAAA,GAAAE,QAAAH,MAAAV,WAAAE;IAAAW,MAAAC,iBAAA,WAM5BC,MAAM1B,eAAe0B,EAAEC,cAAcC,KAAK,CAAC;IAAAd,OAAAU,OAAAT,gBAErD5C,KAAG;KAAA,IAAC0D,OAAI;MAAA,OAAEhC,MAAMX,QAAQF;KAAQ;KAAAiC,WAC7B7B,mBAAO;MAAA,IAAA0C,QAAAC,QAAA;MAAAjB,OAAAgB,aAAkC1C,QAAQb,GAAG;MAAAyD,aAAAF,MAAAF,QAAzBxC,QAAQb,GAAG;MAAA,OAAAuD;KAAA,EAAA,CAAA;IAAwB,CAAA,CAAA;IAAAE,aAAAR,MAAAI,QAJ3D7B,YAAY,CAAC;IAAA,OAAAsB;GAAA;EAAA,CAAA,GAAAT,KAAA;EAAAE,OAAAN,MAAAO,gBAUzB3C,MAAI;GAAA,IAAC4C,OAAI;IAAA,OAAEf,gBAAgB;GAAC;GAAAgB,WACzB7B,mBAAO;IAAA,IAAA6C,QAAAC,QAAA;IAAApB,OAAAmB,aAEJ5C,YAAYD,QAAQ,CAAC,CAACX,YAAYW,QAAQ,CAAC,CAACV,QAAQ,CAAC;IAAA,OAAAuD;GAAA,EAAA,CAAA;EAEzD,CAAA,GAAArB,KAAA;EAAAA,MAAAuB,UAOQ/B;EAAe4B,aAAApB,MAAAwB,WADd,CAACnC,gBAAgB,CAAC;EAAA,OAAAO;CAAA,EAAA,CAAA;AAQpC;AAAC6B,eAAA,CAAA,OAAA,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thebes/cadmea-ecommerce-ui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Storefront SolidJS components for @thebes/cadmea-plugin-ecommerce — ProductDetail, cart, and checkout",
|
|
5
|
+
"author": "BowenLabs <hello@bowenlabs.io>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/bowenlabs/project-thebes",
|
|
10
|
+
"directory": "packages/cadmea-ecommerce-ui"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/bowenlabs/project-thebes/tree/main/packages/cadmea-ecommerce-ui#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/bowenlabs/project-thebes/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"cadmea",
|
|
18
|
+
"cadmus",
|
|
19
|
+
"ecommerce",
|
|
20
|
+
"solidjs",
|
|
21
|
+
"cloudflare"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"solid-js": ">=1.9.0",
|
|
35
|
+
"@thebes/cadmus": "^0.2.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@solidjs/testing-library": "latest",
|
|
39
|
+
"@testing-library/dom": "latest",
|
|
40
|
+
"@testing-library/jest-dom": "latest",
|
|
41
|
+
"@rolldown/plugin-babel": "^0.2.3",
|
|
42
|
+
"babel-preset-solid": "^1.9.12",
|
|
43
|
+
"@babel/core": "^7.28.0",
|
|
44
|
+
"jsdom": "latest",
|
|
45
|
+
"solid-js": "^1.9.13",
|
|
46
|
+
"typescript": "latest",
|
|
47
|
+
"vite-plugin-solid": "latest",
|
|
48
|
+
"vite-plus": "latest",
|
|
49
|
+
"vitest": "latest",
|
|
50
|
+
"@thebes/cadmus": "^0.2.1"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"README.md",
|
|
55
|
+
"LICENSE"
|
|
56
|
+
],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "vp pack",
|
|
59
|
+
"dev": "vp pack --watch",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest"
|
|
62
|
+
}
|
|
63
|
+
}
|