@absolutejs/commerce 0.2.1-beta.0 → 0.4.0-beta.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/dist/client/cartStore.d.ts +2 -0
- package/dist/client/index.js +43 -4
- package/dist/core/email.d.ts +51 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +79 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +10 -0
- package/package.json +14 -3
package/dist/client/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// src/client/cartStore.ts
|
|
2
|
+
var EMPTY = [];
|
|
2
3
|
var createCartStore = (key, normalize) => {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
return [];
|
|
4
|
+
const eventName = `commerce-cart:${key}`;
|
|
5
|
+
const parse = (raw) => {
|
|
6
6
|
try {
|
|
7
|
-
const raw = window.localStorage.getItem(key);
|
|
8
7
|
const parsed = raw ? JSON.parse(raw) : [];
|
|
9
8
|
if (normalize)
|
|
10
9
|
return normalize(parsed);
|
|
@@ -13,10 +12,32 @@ var createCartStore = (key, normalize) => {
|
|
|
13
12
|
return [];
|
|
14
13
|
}
|
|
15
14
|
};
|
|
15
|
+
const read = () => {
|
|
16
|
+
if (typeof window === "undefined")
|
|
17
|
+
return [];
|
|
18
|
+
return parse(window.localStorage.getItem(key));
|
|
19
|
+
};
|
|
20
|
+
let lastRaw = null;
|
|
21
|
+
let cache = EMPTY;
|
|
22
|
+
const getSnapshot = () => {
|
|
23
|
+
if (typeof window === "undefined")
|
|
24
|
+
return EMPTY;
|
|
25
|
+
const raw = window.localStorage.getItem(key) ?? "";
|
|
26
|
+
if (raw !== lastRaw) {
|
|
27
|
+
lastRaw = raw;
|
|
28
|
+
cache = parse(raw);
|
|
29
|
+
}
|
|
30
|
+
return cache;
|
|
31
|
+
};
|
|
32
|
+
const announce = () => {
|
|
33
|
+
if (typeof window !== "undefined")
|
|
34
|
+
window.dispatchEvent(new Event(eventName));
|
|
35
|
+
};
|
|
16
36
|
const write = (items) => {
|
|
17
37
|
if (typeof window === "undefined")
|
|
18
38
|
return;
|
|
19
39
|
window.localStorage.setItem(key, JSON.stringify(items));
|
|
40
|
+
announce();
|
|
20
41
|
};
|
|
21
42
|
return {
|
|
22
43
|
add(item) {
|
|
@@ -26,8 +47,26 @@ var createCartStore = (key, normalize) => {
|
|
|
26
47
|
if (typeof window === "undefined")
|
|
27
48
|
return;
|
|
28
49
|
window.localStorage.removeItem(key);
|
|
50
|
+
announce();
|
|
29
51
|
},
|
|
52
|
+
getSnapshot,
|
|
30
53
|
read,
|
|
54
|
+
subscribe(listener) {
|
|
55
|
+
if (typeof window === "undefined")
|
|
56
|
+
return () => {
|
|
57
|
+
return;
|
|
58
|
+
};
|
|
59
|
+
const onStorage = (event) => {
|
|
60
|
+
if (event.key === key)
|
|
61
|
+
listener();
|
|
62
|
+
};
|
|
63
|
+
window.addEventListener(eventName, listener);
|
|
64
|
+
window.addEventListener("storage", onStorage);
|
|
65
|
+
return () => {
|
|
66
|
+
window.removeEventListener(eventName, listener);
|
|
67
|
+
window.removeEventListener("storage", onStorage);
|
|
68
|
+
};
|
|
69
|
+
},
|
|
31
70
|
write
|
|
32
71
|
};
|
|
33
72
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type EmailMessage = {
|
|
2
|
+
to: string;
|
|
3
|
+
subject: string;
|
|
4
|
+
html: string;
|
|
5
|
+
};
|
|
6
|
+
export type EmailProvider = {
|
|
7
|
+
send(message: EmailMessage): Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export type EmailTheme = {
|
|
10
|
+
brandName: string;
|
|
11
|
+
tagline?: string;
|
|
12
|
+
footerNote?: string;
|
|
13
|
+
colors: {
|
|
14
|
+
ink: string;
|
|
15
|
+
accent: string;
|
|
16
|
+
gold: string;
|
|
17
|
+
paper: string;
|
|
18
|
+
card: string;
|
|
19
|
+
muted: string;
|
|
20
|
+
hairline: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export declare const DEFAULT_EMAIL_THEME: EmailTheme;
|
|
24
|
+
/** A short, human order reference derived from a session id. */
|
|
25
|
+
export declare const orderNumber: (sessionId: string) => string;
|
|
26
|
+
/** Format a minor-unit amount for display (null → em dash). */
|
|
27
|
+
export declare const formatMoneyCents: (cents: number | null, currency?: string | null) => string;
|
|
28
|
+
/** A carrier tracking URL, or '' for an unknown carrier. */
|
|
29
|
+
export declare const carrierTrackingUrl: (carrier: string, trackingNumber: string) => string;
|
|
30
|
+
/** A branded call-to-action button (inline-styled for email clients). */
|
|
31
|
+
export declare const emailButton: (theme: EmailTheme, href: string, label: string) => string;
|
|
32
|
+
export type EmailLineItem = {
|
|
33
|
+
label: string;
|
|
34
|
+
amountCents: number;
|
|
35
|
+
};
|
|
36
|
+
export type LineItemsOptions = {
|
|
37
|
+
currency?: string | null;
|
|
38
|
+
totalCents?: number | null;
|
|
39
|
+
totalLabel?: string;
|
|
40
|
+
};
|
|
41
|
+
/** A line-items table with an optional total row. */
|
|
42
|
+
export declare const emailLineItems: (theme: EmailTheme, items: EmailLineItem[], options?: LineItemsOptions) => string;
|
|
43
|
+
export type RenderEmailArgs = {
|
|
44
|
+
preheader: string;
|
|
45
|
+
heading: string;
|
|
46
|
+
intro: string;
|
|
47
|
+
/** Pre-built inner HTML (line tables, buttons, paragraphs). */
|
|
48
|
+
inner?: string;
|
|
49
|
+
};
|
|
50
|
+
/** Wrap content in the branded, responsive email shell. */
|
|
51
|
+
export declare const renderEmail: (theme: EmailTheme, { preheader, heading, intro, inner }: RenderEmailArgs) => string;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -26,6 +26,78 @@ var formatPrice = (value, currency = "USD") => {
|
|
|
26
26
|
const code = currency.toUpperCase();
|
|
27
27
|
return code === "USD" ? `$${value.toFixed(2)}` : `${value.toFixed(2)} ${code}`;
|
|
28
28
|
};
|
|
29
|
+
|
|
30
|
+
// src/core/email.ts
|
|
31
|
+
var DEFAULT_EMAIL_THEME = {
|
|
32
|
+
brandName: "AbsoluteJS Commerce",
|
|
33
|
+
colors: {
|
|
34
|
+
accent: "#1f6f5c",
|
|
35
|
+
card: "#fffdf8",
|
|
36
|
+
gold: "#b5862f",
|
|
37
|
+
hairline: "#e6ddcb",
|
|
38
|
+
ink: "#1a1712",
|
|
39
|
+
muted: "#7a7264",
|
|
40
|
+
paper: "#efece4"
|
|
41
|
+
},
|
|
42
|
+
footerNote: "Questions? Just reply to this email."
|
|
43
|
+
};
|
|
44
|
+
var orderNumber = (sessionId) => `#${sessionId.slice(-8).toUpperCase()}`;
|
|
45
|
+
var formatMoneyCents = (cents, currency = "usd") => {
|
|
46
|
+
if (cents === null)
|
|
47
|
+
return "\u2014";
|
|
48
|
+
const code = (currency ?? "usd").toUpperCase();
|
|
49
|
+
const value = fromCents(cents).toFixed(2);
|
|
50
|
+
return code === "USD" ? `$${value}` : `${value} ${code}`;
|
|
51
|
+
};
|
|
52
|
+
var carrierTrackingUrl = (carrier, trackingNumber) => {
|
|
53
|
+
const slug = carrier.toLowerCase();
|
|
54
|
+
if (slug.includes("usps"))
|
|
55
|
+
return `https://tools.usps.com/go/TrackConfirmAction?tLabels=${trackingNumber}`;
|
|
56
|
+
if (slug.includes("ups"))
|
|
57
|
+
return `https://www.ups.com/track?tracknum=${trackingNumber}`;
|
|
58
|
+
if (slug.includes("fedex"))
|
|
59
|
+
return `https://www.fedex.com/fedextrack/?trknbr=${trackingNumber}`;
|
|
60
|
+
return "";
|
|
61
|
+
};
|
|
62
|
+
var emailButton = (theme, href, label) => `<a href="${href}" style="display:inline-block;background:${theme.colors.accent};color:#ffffff;text-decoration:none;font-weight:700;font-size:14px;padding:12px 22px;border-radius:6px;">${label}</a>`;
|
|
63
|
+
var emailLineItems = (theme, items, options = {}) => {
|
|
64
|
+
const { colors } = theme;
|
|
65
|
+
const rows = items.map((item) => `<tr>
|
|
66
|
+
<td style="padding:8px 0;border-bottom:1px solid ${colors.hairline};font-size:14px;color:${colors.ink};">${item.label}</td>
|
|
67
|
+
<td style="padding:8px 0;border-bottom:1px solid ${colors.hairline};text-align:right;font-family:monospace;font-size:14px;color:${colors.ink};">${formatMoneyCents(item.amountCents, options.currency)}</td>
|
|
68
|
+
</tr>`).join("");
|
|
69
|
+
const total = options.totalCents === undefined ? "" : `<tr>
|
|
70
|
+
<td style="padding:12px 0 0;font-weight:700;font-size:15px;color:${colors.ink};">${options.totalLabel ?? "Total"}</td>
|
|
71
|
+
<td style="padding:12px 0 0;text-align:right;font-weight:700;font-family:monospace;font-size:15px;color:${colors.ink};">${formatMoneyCents(options.totalCents, options.currency)}</td>
|
|
72
|
+
</tr>`;
|
|
73
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:18px 0;">${rows}${total}</table>`;
|
|
74
|
+
};
|
|
75
|
+
var renderEmail = (theme, { preheader, heading, intro, inner = "" }) => {
|
|
76
|
+
const { colors } = theme;
|
|
77
|
+
const tagline = theme.tagline ? ` \xB7 ${theme.tagline}` : "";
|
|
78
|
+
const footer = theme.footerNote ?? "";
|
|
79
|
+
return `
|
|
80
|
+
<div style="display:none;max-height:0;overflow:hidden;opacity:0;">${preheader}</div>
|
|
81
|
+
<div style="margin:0;padding:24px 12px;background:${colors.paper};font-family:'Helvetica Neue',Arial,sans-serif;">
|
|
82
|
+
<table width="100%" cellpadding="0" cellspacing="0"><tr><td align="center">
|
|
83
|
+
<table width="560" cellpadding="0" cellspacing="0" style="max-width:560px;width:100%;background:${colors.card};border:1.5px solid ${colors.ink};">
|
|
84
|
+
<tr><td style="height:6px;background:${colors.accent};font-size:0;line-height:0;"> </td></tr>
|
|
85
|
+
<tr><td style="padding:24px 28px 4px;">
|
|
86
|
+
<span style="font-family:'Courier New',monospace;font-size:11px;letter-spacing:0.22em;text-transform:uppercase;color:${colors.gold};font-weight:700;">${theme.brandName}</span>
|
|
87
|
+
</td></tr>
|
|
88
|
+
<tr><td style="padding:8px 28px 28px;">
|
|
89
|
+
<h1 style="margin:0 0 10px;font-size:26px;line-height:1.15;color:${colors.accent};">${heading}</h1>
|
|
90
|
+
<p style="margin:0 0 4px;font-size:15px;line-height:1.55;color:${colors.ink};">${intro}</p>
|
|
91
|
+
${inner}
|
|
92
|
+
</td></tr>
|
|
93
|
+
<tr><td style="padding:18px 28px;border-top:1px solid ${colors.hairline};font-family:'Courier New',monospace;font-size:11px;line-height:1.6;color:${colors.muted};">
|
|
94
|
+
${theme.brandName}${tagline}<br/>
|
|
95
|
+
${footer}
|
|
96
|
+
</td></tr>
|
|
97
|
+
</table>
|
|
98
|
+
</td></tr></table>
|
|
99
|
+
</div>`;
|
|
100
|
+
};
|
|
29
101
|
// src/core/orders.ts
|
|
30
102
|
var clampPercent = (percent) => Math.min(100, Math.max(0, percent));
|
|
31
103
|
var depositCents = (totalCents, percent) => percent > 0 ? Math.round(totalCents * clampPercent(percent) / 100) : 0;
|
|
@@ -56,19 +128,26 @@ export {
|
|
|
56
128
|
toProductionStage,
|
|
57
129
|
toCents,
|
|
58
130
|
roundMoney,
|
|
131
|
+
renderEmail,
|
|
59
132
|
quantityDiscount,
|
|
60
133
|
prevStage,
|
|
134
|
+
orderNumber,
|
|
61
135
|
nextStage,
|
|
62
136
|
nextQuantityBreak,
|
|
63
137
|
lineTotal,
|
|
64
138
|
isDiscountValid,
|
|
65
139
|
fromCents,
|
|
66
140
|
formatPrice,
|
|
141
|
+
formatMoneyCents,
|
|
142
|
+
emailLineItems,
|
|
143
|
+
emailButton,
|
|
67
144
|
discountAmountCents,
|
|
68
145
|
depositCents,
|
|
69
146
|
cartSubtotal,
|
|
70
147
|
cartSetupTotal,
|
|
71
148
|
cartCount,
|
|
149
|
+
carrierTrackingUrl,
|
|
72
150
|
PRODUCTION_STAGES,
|
|
151
|
+
DEFAULT_EMAIL_THEME,
|
|
73
152
|
DEFAULT_APPAREL_PARCEL
|
|
74
153
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CartStore } from '../client/cartStore';
|
|
2
|
+
/** Live cart items for a store — re-renders on add/clear and cross-tab changes. */
|
|
3
|
+
export declare const useCart: <T>(store: CartStore<T>) => T[];
|
|
4
|
+
/**
|
|
5
|
+
* Live derived value over the cart (e.g. item count, subtotal). `select` runs
|
|
6
|
+
* on the current items each render.
|
|
7
|
+
*/
|
|
8
|
+
export declare const useCartValue: <T, V>(store: CartStore<T>, select: (items: T[]) => V) => V;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/react/index.ts
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
var EMPTY = [];
|
|
5
|
+
var useCart = (store) => useSyncExternalStore(store.subscribe, store.getSnapshot, () => EMPTY);
|
|
6
|
+
var useCartValue = (store, select) => select(useCart(store));
|
|
7
|
+
export {
|
|
8
|
+
useCartValue,
|
|
9
|
+
useCart
|
|
10
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/commerce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-beta.0",
|
|
4
4
|
"description": "Provider-agnostic commerce primitives (cart, orders, fulfillment, shipping) for AbsoluteJS",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"import": "./dist/client/index.js",
|
|
29
29
|
"types": "./dist/client/index.d.ts"
|
|
30
30
|
},
|
|
31
|
+
"./react": {
|
|
32
|
+
"browser": "./dist/react/index.js",
|
|
33
|
+
"import": "./dist/react/index.js",
|
|
34
|
+
"types": "./dist/react/index.d.ts"
|
|
35
|
+
},
|
|
31
36
|
"./drizzle": {
|
|
32
37
|
"import": "./dist/drizzle/index.js",
|
|
33
38
|
"types": "./dist/drizzle/index.d.ts",
|
|
@@ -37,22 +42,28 @@
|
|
|
37
42
|
"license": "BSL-1.1",
|
|
38
43
|
"author": "Alex Kahn",
|
|
39
44
|
"scripts": {
|
|
40
|
-
"build": "rm -rf dist && bun build ./src/index.ts ./src/drizzle/index.ts --outdir dist --target bun --external drizzle-orm && bun build ./src/client/index.ts --outdir dist/client --target browser --format esm && tsc --emitDeclarationOnly --project tsconfig.json",
|
|
45
|
+
"build": "rm -rf dist && bun build ./src/index.ts ./src/drizzle/index.ts ./src/react/index.ts --outdir dist --target bun --external drizzle-orm --external react && bun build ./src/client/index.ts --outdir dist/client --target browser --format esm && tsc --emitDeclarationOnly --project tsconfig.json",
|
|
41
46
|
"format": "prettier --write \"./**/*.{js,ts,json,md}\"",
|
|
42
47
|
"release": "bun run build && bun publish --tag beta",
|
|
43
48
|
"typecheck": "tsc --noEmit"
|
|
44
49
|
},
|
|
45
50
|
"peerDependencies": {
|
|
46
|
-
"drizzle-orm": ">=0.30.0"
|
|
51
|
+
"drizzle-orm": ">=0.30.0",
|
|
52
|
+
"react": ">=18.0.0"
|
|
47
53
|
},
|
|
48
54
|
"peerDependenciesMeta": {
|
|
49
55
|
"drizzle-orm": {
|
|
50
56
|
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"react": {
|
|
59
|
+
"optional": true
|
|
51
60
|
}
|
|
52
61
|
},
|
|
53
62
|
"devDependencies": {
|
|
54
63
|
"@types/bun": "1.3.9",
|
|
64
|
+
"@types/react": "^19.0.0",
|
|
55
65
|
"drizzle-orm": "^0.44.0",
|
|
66
|
+
"react": "^19.0.0",
|
|
56
67
|
"typescript": "^5.9.3"
|
|
57
68
|
}
|
|
58
69
|
}
|