@nymbalhq/web-components 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/astro/NymbalElement.astro +19 -0
- package/dist/base-element-CPMgAdyQ.d.ts +91 -0
- package/dist/chunk-2TPQNQYO.js +155 -0
- package/dist/chunk-2TPQNQYO.js.map +1 -0
- package/dist/chunk-2XXBQPDB.js +113 -0
- package/dist/chunk-2XXBQPDB.js.map +1 -0
- package/dist/chunk-42SSME5H.js +84 -0
- package/dist/chunk-42SSME5H.js.map +1 -0
- package/dist/chunk-4COVK727.js +204 -0
- package/dist/chunk-4COVK727.js.map +1 -0
- package/dist/chunk-5VWZ2U5K.js +149 -0
- package/dist/chunk-5VWZ2U5K.js.map +1 -0
- package/dist/chunk-6XCCAAMF.js +656 -0
- package/dist/chunk-6XCCAAMF.js.map +1 -0
- package/dist/chunk-7JAAOVCE.js +114 -0
- package/dist/chunk-7JAAOVCE.js.map +1 -0
- package/dist/chunk-B73QVSA5.js +206 -0
- package/dist/chunk-B73QVSA5.js.map +1 -0
- package/dist/chunk-DR5QB5VN.js +17 -0
- package/dist/chunk-DR5QB5VN.js.map +1 -0
- package/dist/chunk-DXWMNB5F.js +70 -0
- package/dist/chunk-DXWMNB5F.js.map +1 -0
- package/dist/chunk-EBMA7JGV.js +130 -0
- package/dist/chunk-EBMA7JGV.js.map +1 -0
- package/dist/chunk-EYDMJNNK.js +176 -0
- package/dist/chunk-EYDMJNNK.js.map +1 -0
- package/dist/chunk-FIF6GQ6U.js +141 -0
- package/dist/chunk-FIF6GQ6U.js.map +1 -0
- package/dist/chunk-GHXTJ2CF.js +109 -0
- package/dist/chunk-GHXTJ2CF.js.map +1 -0
- package/dist/chunk-H3ZBYNHV.js +153 -0
- package/dist/chunk-H3ZBYNHV.js.map +1 -0
- package/dist/chunk-HPBC3OG7.js +174 -0
- package/dist/chunk-HPBC3OG7.js.map +1 -0
- package/dist/chunk-IYT7VXFS.js +105 -0
- package/dist/chunk-IYT7VXFS.js.map +1 -0
- package/dist/chunk-LHYXNQWY.js +105 -0
- package/dist/chunk-LHYXNQWY.js.map +1 -0
- package/dist/chunk-LO2EW5YP.js +25 -0
- package/dist/chunk-LO2EW5YP.js.map +1 -0
- package/dist/chunk-LSQPUZK6.js +291 -0
- package/dist/chunk-LSQPUZK6.js.map +1 -0
- package/dist/chunk-N4Y5DI4X.js +82 -0
- package/dist/chunk-N4Y5DI4X.js.map +1 -0
- package/dist/chunk-NE6UHTOT.js +148 -0
- package/dist/chunk-NE6UHTOT.js.map +1 -0
- package/dist/chunk-NW47OJVN.js +138 -0
- package/dist/chunk-NW47OJVN.js.map +1 -0
- package/dist/chunk-O2YKCDR2.js +77 -0
- package/dist/chunk-O2YKCDR2.js.map +1 -0
- package/dist/chunk-PN6QKYE4.js +71 -0
- package/dist/chunk-PN6QKYE4.js.map +1 -0
- package/dist/chunk-RAOGZH3Z.js +188 -0
- package/dist/chunk-RAOGZH3Z.js.map +1 -0
- package/dist/chunk-RVSKX2RX.js +11 -0
- package/dist/chunk-RVSKX2RX.js.map +1 -0
- package/dist/chunk-RZVFA3N5.js +154 -0
- package/dist/chunk-RZVFA3N5.js.map +1 -0
- package/dist/chunk-SM7TASEW.js +135 -0
- package/dist/chunk-SM7TASEW.js.map +1 -0
- package/dist/chunk-U2PU4CWB.js +124 -0
- package/dist/chunk-U2PU4CWB.js.map +1 -0
- package/dist/chunk-UCEVCLQ7.js +5 -0
- package/dist/chunk-UCEVCLQ7.js.map +1 -0
- package/dist/chunk-UYSZOEOA.js +32 -0
- package/dist/chunk-UYSZOEOA.js.map +1 -0
- package/dist/chunk-V55K2CS5.js +126 -0
- package/dist/chunk-V55K2CS5.js.map +1 -0
- package/dist/chunk-VTQVGNVC.js +346 -0
- package/dist/chunk-VTQVGNVC.js.map +1 -0
- package/dist/chunk-W23P7SI4.js +239 -0
- package/dist/chunk-W23P7SI4.js.map +1 -0
- package/dist/chunk-W7D3STJH.js +279 -0
- package/dist/chunk-W7D3STJH.js.map +1 -0
- package/dist/chunk-WEEHXWM3.js +172 -0
- package/dist/chunk-WEEHXWM3.js.map +1 -0
- package/dist/chunk-WTEVTCIL.js +128 -0
- package/dist/chunk-WTEVTCIL.js.map +1 -0
- package/dist/chunk-XE6IWISA.js +137 -0
- package/dist/chunk-XE6IWISA.js.map +1 -0
- package/dist/elements/nymbal-add-to-cart.d.ts +14 -0
- package/dist/elements/nymbal-add-to-cart.js +10 -0
- package/dist/elements/nymbal-add-to-cart.js.map +1 -0
- package/dist/elements/nymbal-address-book.d.ts +30 -0
- package/dist/elements/nymbal-address-book.js +13 -0
- package/dist/elements/nymbal-address-book.js.map +1 -0
- package/dist/elements/nymbal-address-form.d.ts +29 -0
- package/dist/elements/nymbal-address-form.js +12 -0
- package/dist/elements/nymbal-address-form.js.map +1 -0
- package/dist/elements/nymbal-breadcrumb.d.ts +15 -0
- package/dist/elements/nymbal-breadcrumb.js +11 -0
- package/dist/elements/nymbal-breadcrumb.js.map +1 -0
- package/dist/elements/nymbal-cart-drawer.d.ts +24 -0
- package/dist/elements/nymbal-cart-drawer.js +11 -0
- package/dist/elements/nymbal-cart-drawer.js.map +1 -0
- package/dist/elements/nymbal-cart-page.d.ts +20 -0
- package/dist/elements/nymbal-cart-page.js +14 -0
- package/dist/elements/nymbal-cart-page.js.map +1 -0
- package/dist/elements/nymbal-category-nav.d.ts +15 -0
- package/dist/elements/nymbal-category-nav.js +11 -0
- package/dist/elements/nymbal-category-nav.js.map +1 -0
- package/dist/elements/nymbal-checkout.d.ts +77 -0
- package/dist/elements/nymbal-checkout.js +15 -0
- package/dist/elements/nymbal-checkout.js.map +1 -0
- package/dist/elements/nymbal-image-gallery.d.ts +19 -0
- package/dist/elements/nymbal-image-gallery.js +10 -0
- package/dist/elements/nymbal-image-gallery.js.map +1 -0
- package/dist/elements/nymbal-login-form.d.ts +17 -0
- package/dist/elements/nymbal-login-form.js +12 -0
- package/dist/elements/nymbal-login-form.js.map +1 -0
- package/dist/elements/nymbal-mini-cart.d.ts +17 -0
- package/dist/elements/nymbal-mini-cart.js +10 -0
- package/dist/elements/nymbal-mini-cart.js.map +1 -0
- package/dist/elements/nymbal-order-summary.d.ts +20 -0
- package/dist/elements/nymbal-order-summary.js +11 -0
- package/dist/elements/nymbal-order-summary.js.map +1 -0
- package/dist/elements/nymbal-payment-element.d.ts +4 -0
- package/dist/elements/nymbal-payment-element.js +10 -0
- package/dist/elements/nymbal-payment-element.js.map +1 -0
- package/dist/elements/nymbal-product-card.d.ts +4 -0
- package/dist/elements/nymbal-product-card.js +12 -0
- package/dist/elements/nymbal-product-card.js.map +1 -0
- package/dist/elements/nymbal-product-filter.d.ts +32 -0
- package/dist/elements/nymbal-product-filter.js +10 -0
- package/dist/elements/nymbal-product-filter.js.map +1 -0
- package/dist/elements/nymbal-product-grid.d.ts +24 -0
- package/dist/elements/nymbal-product-grid.js +14 -0
- package/dist/elements/nymbal-product-grid.js.map +1 -0
- package/dist/elements/nymbal-product-price.d.ts +18 -0
- package/dist/elements/nymbal-product-price.js +12 -0
- package/dist/elements/nymbal-product-price.js.map +1 -0
- package/dist/elements/nymbal-profile-form.d.ts +19 -0
- package/dist/elements/nymbal-profile-form.js +12 -0
- package/dist/elements/nymbal-profile-form.js.map +1 -0
- package/dist/elements/nymbal-quantity-selector.d.ts +20 -0
- package/dist/elements/nymbal-quantity-selector.js +10 -0
- package/dist/elements/nymbal-quantity-selector.js.map +1 -0
- package/dist/elements/nymbal-register-form.d.ts +17 -0
- package/dist/elements/nymbal-register-form.js +12 -0
- package/dist/elements/nymbal-register-form.js.map +1 -0
- package/dist/elements/nymbal-review-form.d.ts +17 -0
- package/dist/elements/nymbal-review-form.js +12 -0
- package/dist/elements/nymbal-review-form.js.map +1 -0
- package/dist/elements/nymbal-review-list.d.ts +14 -0
- package/dist/elements/nymbal-review-list.js +10 -0
- package/dist/elements/nymbal-review-list.js.map +1 -0
- package/dist/elements/nymbal-search-bar.d.ts +19 -0
- package/dist/elements/nymbal-search-bar.js +10 -0
- package/dist/elements/nymbal-search-bar.js.map +1 -0
- package/dist/elements/nymbal-search-results.d.ts +29 -0
- package/dist/elements/nymbal-search-results.js +14 -0
- package/dist/elements/nymbal-search-results.js.map +1 -0
- package/dist/elements/nymbal-site-footer.d.ts +15 -0
- package/dist/elements/nymbal-site-footer.js +11 -0
- package/dist/elements/nymbal-site-footer.js.map +1 -0
- package/dist/elements/nymbal-site-header.d.ts +19 -0
- package/dist/elements/nymbal-site-header.js +11 -0
- package/dist/elements/nymbal-site-header.js.map +1 -0
- package/dist/elements/nymbal-sort-dropdown.d.ts +28 -0
- package/dist/elements/nymbal-sort-dropdown.js +10 -0
- package/dist/elements/nymbal-sort-dropdown.js.map +1 -0
- package/dist/elements/nymbal-stock-badge.d.ts +16 -0
- package/dist/elements/nymbal-stock-badge.js +11 -0
- package/dist/elements/nymbal-stock-badge.js.map +1 -0
- package/dist/elements/nymbal-toast.d.ts +18 -0
- package/dist/elements/nymbal-toast.js +10 -0
- package/dist/elements/nymbal-toast.js.map +1 -0
- package/dist/elements/nymbal-variant-selector.d.ts +16 -0
- package/dist/elements/nymbal-variant-selector.js +10 -0
- package/dist/elements/nymbal-variant-selector.js.map +1 -0
- package/dist/form-element-C-qo3gpD.d.ts +25 -0
- package/dist/hydrate-support.d.ts +2 -0
- package/dist/hydrate-support.js +3 -0
- package/dist/hydrate-support.js.map +1 -0
- package/dist/index.d.ts +37 -105
- package/dist/index.js +130 -1098
- package/dist/index.js.map +1 -1
- package/dist/nymbal-payment-element-Bt8EF42T.d.ts +51 -0
- package/dist/nymbal-product-card-C8HkE_rG.d.ts +47 -0
- package/dist/register-components.d.ts +46 -0
- package/dist/register-components.js +48 -0
- package/dist/register-components.js.map +1 -0
- package/dist/selected-variant-Dym0shlJ.d.ts +23 -0
- package/dist/ssr/index.d.ts +30 -0
- package/dist/ssr/index.js +143 -0
- package/dist/ssr/index.js.map +1 -0
- package/package.json +33 -6
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
|
|
2
|
+
import {
|
|
3
|
+
NymbalElement,
|
|
4
|
+
propertiesOf
|
|
5
|
+
} from "./chunk-EYDMJNNK.js";
|
|
6
|
+
|
|
7
|
+
// src/components/payment-element.ts
|
|
8
|
+
import {
|
|
9
|
+
getPaymentDriver,
|
|
10
|
+
registerPaymentDriver
|
|
11
|
+
} from "@nymbalhq/sdk";
|
|
12
|
+
import { css, nothing } from "lit";
|
|
13
|
+
|
|
14
|
+
// src/payment-drivers/stripe.ts
|
|
15
|
+
var STRIPE_JS = "https://js.stripe.com/v3/";
|
|
16
|
+
function billingDetails(context) {
|
|
17
|
+
const details = {};
|
|
18
|
+
if (context?.email) details.email = context.email;
|
|
19
|
+
const billing = context?.billing;
|
|
20
|
+
if (billing) {
|
|
21
|
+
const name = `${billing.firstName} ${billing.lastName}`.trim();
|
|
22
|
+
if (name) details.name = name;
|
|
23
|
+
details.address = {
|
|
24
|
+
line1: billing.addressLine1,
|
|
25
|
+
...billing.addressLine2 ? { line2: billing.addressLine2 } : {},
|
|
26
|
+
city: billing.city,
|
|
27
|
+
state: billing.region,
|
|
28
|
+
postal_code: billing.postalCode,
|
|
29
|
+
country: billing.country
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return Object.keys(details).length > 0 ? details : void 0;
|
|
33
|
+
}
|
|
34
|
+
var loading = null;
|
|
35
|
+
function loadStripeJs() {
|
|
36
|
+
const win = window;
|
|
37
|
+
if (win.Stripe) return Promise.resolve(win.Stripe);
|
|
38
|
+
loading ??= new Promise((resolve, reject) => {
|
|
39
|
+
const script = document.createElement("script");
|
|
40
|
+
script.src = STRIPE_JS;
|
|
41
|
+
script.async = true;
|
|
42
|
+
script.addEventListener("load", () => {
|
|
43
|
+
if (win.Stripe) resolve(win.Stripe);
|
|
44
|
+
else reject(new Error("Stripe.js loaded but window.Stripe is missing."));
|
|
45
|
+
});
|
|
46
|
+
script.addEventListener("error", () => {
|
|
47
|
+
loading = null;
|
|
48
|
+
script.remove();
|
|
49
|
+
reject(new Error("Stripe.js could not be loaded."));
|
|
50
|
+
});
|
|
51
|
+
document.head.appendChild(script);
|
|
52
|
+
});
|
|
53
|
+
return loading;
|
|
54
|
+
}
|
|
55
|
+
var instances = /* @__PURE__ */ new Map();
|
|
56
|
+
var mounted = [];
|
|
57
|
+
var stripeDriver = {
|
|
58
|
+
async mount(el, options) {
|
|
59
|
+
const key = options["publishableKey"];
|
|
60
|
+
if (!key)
|
|
61
|
+
throw new Error("Card payments are not configured: the store has no Stripe publishable key.");
|
|
62
|
+
const factory = await loadStripeJs();
|
|
63
|
+
let stripe = instances.get(key);
|
|
64
|
+
if (stripe === void 0) {
|
|
65
|
+
stripe = factory(key);
|
|
66
|
+
instances.set(key, stripe);
|
|
67
|
+
}
|
|
68
|
+
const target = document.createElement("div");
|
|
69
|
+
target.className = "nymbal-payment-card";
|
|
70
|
+
el.replaceChildren(target);
|
|
71
|
+
const card = stripe.elements().create("card", {
|
|
72
|
+
style: {
|
|
73
|
+
base: { fontSize: "16px", color: "#1a1a1a", "::placeholder": { color: "#9ca3af" } }
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
card.mount(target);
|
|
77
|
+
mounted = [
|
|
78
|
+
...mounted.filter((m) => m.host.isConnected && m.host !== el),
|
|
79
|
+
{ host: el, stripe, card }
|
|
80
|
+
];
|
|
81
|
+
},
|
|
82
|
+
async confirm(clientSecret, context) {
|
|
83
|
+
const current = mounted.findLast((m) => m.host.isConnected) ?? mounted.at(-1);
|
|
84
|
+
if (current === void 0) return { ok: false, error: "The card form is not ready." };
|
|
85
|
+
const billing_details = billingDetails(context);
|
|
86
|
+
const result = await current.stripe.confirmCardPayment(clientSecret, {
|
|
87
|
+
payment_method: { card: current.card, ...billing_details ? { billing_details } : {} }
|
|
88
|
+
});
|
|
89
|
+
if (result.error) return { ok: false, error: result.error.message ?? "Payment failed." };
|
|
90
|
+
return { ok: true, confirmed: result.paymentIntent?.status === "succeeded" };
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/payment-drivers/test-mode.ts
|
|
95
|
+
var testModeDriver = {
|
|
96
|
+
mount(el) {
|
|
97
|
+
el.innerHTML = '<p class="nymbal-payment-note">Test mode \u2014 payment skipped</p>';
|
|
98
|
+
},
|
|
99
|
+
confirm() {
|
|
100
|
+
return Promise.resolve({ ok: true, confirmed: false });
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/components/payment-element.ts
|
|
105
|
+
var SCHEMA = {
|
|
106
|
+
tag: "nymbal-payment-element",
|
|
107
|
+
reactName: "PaymentElement",
|
|
108
|
+
attributes: [{ name: "client", prop: "client", type: "json" }],
|
|
109
|
+
events: [
|
|
110
|
+
{
|
|
111
|
+
name: "nymbal:payment:status",
|
|
112
|
+
prop: "onStatusChange",
|
|
113
|
+
detail: "{ status: 'pending' | 'ready' | 'error' }"
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
parts: ["error"],
|
|
117
|
+
slots: [],
|
|
118
|
+
children: false
|
|
119
|
+
};
|
|
120
|
+
var NO_PAYMENT_STEP = { ok: true, confirmed: false };
|
|
121
|
+
function parseClient(value) {
|
|
122
|
+
if (value === null || value === "" || value === "null") return null;
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(value);
|
|
125
|
+
if (parsed === null) return null;
|
|
126
|
+
if (typeof parsed.driver !== "string" || typeof parsed.options !== "object") return void 0;
|
|
127
|
+
return { driver: parsed.driver, options: { ...parsed.options ?? {} } };
|
|
128
|
+
} catch {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
var NymbalPaymentElement = class _NymbalPaymentElement extends NymbalElement {
|
|
133
|
+
static schema = SCHEMA;
|
|
134
|
+
/** The built-in drivers, except a name the project registered first: the project's driver wins, as its tag does. */
|
|
135
|
+
static beforeDefine() {
|
|
136
|
+
const drivers = [
|
|
137
|
+
["test-mode", testModeDriver],
|
|
138
|
+
["stripe", stripeDriver]
|
|
139
|
+
];
|
|
140
|
+
for (const [name, driver] of drivers) {
|
|
141
|
+
if (getPaymentDriver(name) === void 0) registerPaymentDriver(name, driver);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
static properties = propertiesOf(SCHEMA);
|
|
145
|
+
static styles = css`
|
|
146
|
+
nymbal-payment-element {
|
|
147
|
+
display: block;
|
|
148
|
+
font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);
|
|
149
|
+
}
|
|
150
|
+
nymbal-payment-element .nymbal-payment-note {
|
|
151
|
+
margin: 0;
|
|
152
|
+
font-size: var(--nymbal-font-size-sm, 0.875rem);
|
|
153
|
+
color: var(--nymbal-color-text-muted, #71717a);
|
|
154
|
+
}
|
|
155
|
+
nymbal-payment-element .nymbal-payment-card {
|
|
156
|
+
padding: var(--nymbal-spacing-sm, 0.5rem) var(--nymbal-spacing-md, 1rem);
|
|
157
|
+
border: 1px solid var(--nymbal-color-border, #e4e4e7);
|
|
158
|
+
border-radius: var(--nymbal-radius-md, 0.375rem);
|
|
159
|
+
background: var(--nymbal-color-surface, #ffffff);
|
|
160
|
+
min-height: 40px;
|
|
161
|
+
}
|
|
162
|
+
nymbal-payment-element [part~='error'] {
|
|
163
|
+
margin: 0;
|
|
164
|
+
font-size: var(--nymbal-font-size-sm, 0.875rem);
|
|
165
|
+
color: var(--nymbal-color-error, #dc2626);
|
|
166
|
+
}
|
|
167
|
+
`;
|
|
168
|
+
_status = "pending";
|
|
169
|
+
_error = "";
|
|
170
|
+
_driver = null;
|
|
171
|
+
_mounted = Promise.resolve(true);
|
|
172
|
+
_rendered = void 0;
|
|
173
|
+
_generation = 0;
|
|
174
|
+
get status() {
|
|
175
|
+
return this._status;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The light DOM, so the payment driver's UI (Stripe's iframe) and the checkout form share one tree. Its styles join
|
|
179
|
+
* the sheets of the tree it is in, where a shadow root would have held them.
|
|
180
|
+
*/
|
|
181
|
+
createRenderRoot() {
|
|
182
|
+
const root = this.getRootNode();
|
|
183
|
+
const sheet = _NymbalPaymentElement.styles.styleSheet;
|
|
184
|
+
if (sheet !== void 0 && "adoptedStyleSheets" in root && !root.adoptedStyleSheets.includes(sheet)) {
|
|
185
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
186
|
+
}
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
connectedCallback() {
|
|
190
|
+
super.connectedCallback();
|
|
191
|
+
this.mountClient();
|
|
192
|
+
}
|
|
193
|
+
attributeChangedCallback(name, old, value) {
|
|
194
|
+
super.attributeChangedCallback(name, old, value);
|
|
195
|
+
if (this.isConnected) this.mountClient();
|
|
196
|
+
}
|
|
197
|
+
/** The children are the driver's, mounted imperatively, so Lit renders none of its own. */
|
|
198
|
+
render() {
|
|
199
|
+
return nothing;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Completes a payment the API left `requires_action`, once the driver has mounted, handing the driver the
|
|
203
|
+
* checkout's `context`; the driver's answer as is.
|
|
204
|
+
*/
|
|
205
|
+
async confirm(clientSecret, context) {
|
|
206
|
+
const driver = this._driver;
|
|
207
|
+
if (this._status === "error") return { ok: false, error: this._error };
|
|
208
|
+
if (driver === null) return NO_PAYMENT_STEP;
|
|
209
|
+
if (!await this._mounted) return { ok: false, error: this._error };
|
|
210
|
+
return driver.confirm(clientSecret, context);
|
|
211
|
+
}
|
|
212
|
+
mountClient() {
|
|
213
|
+
const attribute = this.getAttribute("client");
|
|
214
|
+
if (attribute === this._rendered) return;
|
|
215
|
+
this._rendered = attribute;
|
|
216
|
+
const generation = ++this._generation;
|
|
217
|
+
this._driver = null;
|
|
218
|
+
this._mounted = Promise.resolve(true);
|
|
219
|
+
this.clear();
|
|
220
|
+
const client = parseClient(attribute);
|
|
221
|
+
if (client === void 0)
|
|
222
|
+
return this.fail("The store sent a payment client this page cannot read.");
|
|
223
|
+
if (client === null) return this.setStatus("ready");
|
|
224
|
+
const driver = getPaymentDriver(client.driver);
|
|
225
|
+
if (driver === void 0) {
|
|
226
|
+
return this.fail(`Payment method "${client.driver}" is not available on this storefront.`);
|
|
227
|
+
}
|
|
228
|
+
this._driver = driver;
|
|
229
|
+
this.setStatus("pending");
|
|
230
|
+
this._mounted = Promise.resolve().then(() => driver.mount(this, client.options)).then(
|
|
231
|
+
() => {
|
|
232
|
+
if (generation === this._generation) this.setStatus("ready");
|
|
233
|
+
return true;
|
|
234
|
+
},
|
|
235
|
+
(err) => {
|
|
236
|
+
if (generation === this._generation) {
|
|
237
|
+
this.fail(err instanceof Error ? err.message : "The payment form could not be loaded.");
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
fail(message) {
|
|
244
|
+
this._error = message;
|
|
245
|
+
const p = document.createElement("p");
|
|
246
|
+
p.setAttribute("part", "error");
|
|
247
|
+
p.setAttribute("data-testid", "payment-element-error");
|
|
248
|
+
p.setAttribute("role", "alert");
|
|
249
|
+
p.textContent = message;
|
|
250
|
+
this.clear(p);
|
|
251
|
+
this.setStatus("error");
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Replaces what a driver or an error rendered. Lit's marker comment stays: its next update (a new `client`) writes
|
|
255
|
+
* at it, and throws once it is gone.
|
|
256
|
+
*/
|
|
257
|
+
clear(...nodes) {
|
|
258
|
+
for (const child of [...this.childNodes]) {
|
|
259
|
+
if (child.nodeType !== Node.COMMENT_NODE) child.remove();
|
|
260
|
+
}
|
|
261
|
+
this.append(...nodes);
|
|
262
|
+
}
|
|
263
|
+
setStatus(status) {
|
|
264
|
+
if (status === this._status) return;
|
|
265
|
+
this._status = status;
|
|
266
|
+
this.dispatchEvent(
|
|
267
|
+
new CustomEvent("nymbal:payment:status", {
|
|
268
|
+
bubbles: true,
|
|
269
|
+
composed: true,
|
|
270
|
+
detail: { status }
|
|
271
|
+
})
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export {
|
|
277
|
+
NymbalPaymentElement
|
|
278
|
+
};
|
|
279
|
+
//# sourceMappingURL=chunk-W7D3STJH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/payment-element.ts","../src/payment-drivers/stripe.ts","../src/payment-drivers/test-mode.ts"],"sourcesContent":["import {\n getPaymentDriver,\n registerPaymentDriver,\n type PaymentConfirmContext,\n type PaymentConfirmation,\n type PaymentDriver,\n} from '@nymbalhq/sdk'\nimport type { PaymentClient } from '@nymbalhq/types'\nimport { css, nothing, type CSSResult } from 'lit'\nimport { NymbalElement, propertiesOf, type ElementSchema } from '../base-element.js'\nimport { stripeDriver } from '../payment-drivers/stripe.js'\nimport { testModeDriver } from '../payment-drivers/test-mode.js'\n\nconst SCHEMA: ElementSchema = {\n tag: 'nymbal-payment-element',\n reactName: 'PaymentElement',\n attributes: [{ name: 'client', prop: 'client', type: 'json' }],\n events: [\n {\n name: 'nymbal:payment:status',\n prop: 'onStatusChange',\n detail: \"{ status: 'pending' | 'ready' | 'error' }\",\n },\n ],\n parts: ['error'],\n slots: [],\n children: false,\n}\n\n/** `pending` while a driver mounts; `error` when no payment can be taken, which a checkout must not submit past. */\nexport type PaymentElementStatus = 'pending' | 'ready' | 'error'\n\n/** No client half: nothing to collect, so nothing is confirmed here. */\nconst NO_PAYMENT_STEP: PaymentConfirmation = { ok: true, confirmed: false }\n\nfunction parseClient(value: string | null): PaymentClient | null | undefined {\n if (value === null || value === '' || value === 'null') return null\n try {\n const parsed = JSON.parse(value) as Partial<PaymentClient> | null\n if (parsed === null) return null\n if (typeof parsed.driver !== 'string' || typeof parsed.options !== 'object') return undefined\n return { driver: parsed.driver, options: { ...(parsed.options ?? {}) } }\n } catch {\n return undefined\n }\n}\n\n/**\n * <nymbal-payment-element client='<json>'>: renders the payments adapter's client half (GET /api/capabilities'\n * `payments.client`) through the driver the SDK registry holds under its `driver` name, and confirms a payment\n * through it. It emits `nymbal:payment:status` whenever `status` changes.\n */\nexport class NymbalPaymentElement extends NymbalElement {\n static override schema = SCHEMA\n\n /** The built-in drivers, except a name the project registered first: the project's driver wins, as its tag does. */\n static beforeDefine(): void {\n const drivers: Array<[string, PaymentDriver]> = [\n ['test-mode', testModeDriver],\n ['stripe', stripeDriver],\n ]\n for (const [name, driver] of drivers) {\n if (getPaymentDriver(name) === undefined) registerPaymentDriver(name, driver)\n }\n }\n static override properties = propertiesOf(SCHEMA)\n\n static override styles = css`\n nymbal-payment-element {\n display: block;\n font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);\n }\n nymbal-payment-element .nymbal-payment-note {\n margin: 0;\n font-size: var(--nymbal-font-size-sm, 0.875rem);\n color: var(--nymbal-color-text-muted, #71717a);\n }\n nymbal-payment-element .nymbal-payment-card {\n padding: var(--nymbal-spacing-sm, 0.5rem) var(--nymbal-spacing-md, 1rem);\n border: 1px solid var(--nymbal-color-border, #e4e4e7);\n border-radius: var(--nymbal-radius-md, 0.375rem);\n background: var(--nymbal-color-surface, #ffffff);\n min-height: 40px;\n }\n nymbal-payment-element [part~='error'] {\n margin: 0;\n font-size: var(--nymbal-font-size-sm, 0.875rem);\n color: var(--nymbal-color-error, #dc2626);\n }\n `\n\n private _status: PaymentElementStatus = 'pending'\n private _error = ''\n private _driver: PaymentDriver | null = null\n private _mounted: Promise<boolean> = Promise.resolve(true)\n private _rendered: string | null | undefined = undefined\n private _generation = 0\n\n get status(): PaymentElementStatus {\n return this._status\n }\n\n /**\n * The light DOM, so the payment driver's UI (Stripe's iframe) and the checkout form share one tree. Its styles join\n * the sheets of the tree it is in, where a shadow root would have held them.\n */\n protected override createRenderRoot(): HTMLElement {\n const root = this.getRootNode() as Document | ShadowRoot\n const sheet = (NymbalPaymentElement.styles as CSSResult).styleSheet\n if (sheet !== undefined && 'adoptedStyleSheets' in root && !root.adoptedStyleSheets.includes(sheet)) {\n root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet]\n }\n return this\n }\n\n override connectedCallback(): void {\n super.connectedCallback()\n this.mountClient()\n }\n\n override attributeChangedCallback(name: string, old: string | null, value: string | null): void {\n super.attributeChangedCallback(name, old, value)\n if (this.isConnected) this.mountClient()\n }\n\n /** The children are the driver's, mounted imperatively, so Lit renders none of its own. */\n protected override render() {\n return nothing\n }\n\n /**\n * Completes a payment the API left `requires_action`, once the driver has mounted, handing the driver the\n * checkout's `context`; the driver's answer as is.\n */\n async confirm(clientSecret: string, context?: PaymentConfirmContext): Promise<PaymentConfirmation> {\n const driver = this._driver\n if (this._status === 'error') return { ok: false, error: this._error }\n if (driver === null) return NO_PAYMENT_STEP\n if (!(await this._mounted)) return { ok: false, error: this._error }\n return driver.confirm(clientSecret, context)\n }\n\n private mountClient(): void {\n const attribute = this.getAttribute('client')\n // The same client half again (a reconnect, a framework re-setting the attribute) keeps what is mounted.\n if (attribute === this._rendered) return\n this._rendered = attribute\n const generation = ++this._generation\n this._driver = null\n this._mounted = Promise.resolve(true)\n this.clear()\n\n const client = parseClient(attribute)\n if (client === undefined)\n return this.fail('The store sent a payment client this page cannot read.')\n if (client === null) return this.setStatus('ready')\n const driver = getPaymentDriver(client.driver)\n if (driver === undefined) {\n return this.fail(`Payment method \"${client.driver}\" is not available on this storefront.`)\n }\n\n this._driver = driver\n this.setStatus('pending')\n this._mounted = Promise.resolve()\n .then(() => driver.mount(this, client.options))\n .then(\n () => {\n if (generation === this._generation) this.setStatus('ready')\n return true\n },\n (err: unknown) => {\n if (generation === this._generation) {\n this.fail(err instanceof Error ? err.message : 'The payment form could not be loaded.')\n }\n return false\n },\n )\n }\n\n private fail(message: string): void {\n this._error = message\n const p = document.createElement('p')\n p.setAttribute('part', 'error')\n p.setAttribute('data-testid', 'payment-element-error')\n p.setAttribute('role', 'alert')\n p.textContent = message\n this.clear(p)\n this.setStatus('error')\n }\n\n /**\n * Replaces what a driver or an error rendered. Lit's marker comment stays: its next update (a new `client`) writes\n * at it, and throws once it is gone.\n */\n private clear(...nodes: Node[]): void {\n for (const child of [...this.childNodes]) {\n if (child.nodeType !== Node.COMMENT_NODE) child.remove()\n }\n this.append(...nodes)\n }\n\n private setStatus(status: PaymentElementStatus): void {\n if (status === this._status) return\n this._status = status\n this.dispatchEvent(\n new CustomEvent('nymbal:payment:status', {\n bubbles: true,\n composed: true,\n detail: { status },\n }),\n )\n }\n}\n","import type { PaymentConfirmContext, PaymentConfirmation, PaymentDriver } from '@nymbalhq/sdk'\n\n// Stripe.js from Stripe's own host, as Stripe requires, so no project installs a Stripe package to take a card.\nconst STRIPE_JS = 'https://js.stripe.com/v3/'\n\ninterface StripeCard {\n mount(target: HTMLElement): void\n}\n\ninterface StripeBillingDetails {\n email?: string\n name?: string\n address?: {\n line1: string\n line2?: string\n city: string\n state: string\n postal_code: string\n country: string\n }\n}\n\ninterface StripeInstance {\n elements(): { create(type: 'card', options?: Record<string, unknown>): StripeCard }\n confirmCardPayment(\n clientSecret: string,\n data: { payment_method: { card: StripeCard; billing_details?: StripeBillingDetails } },\n ): Promise<{ paymentIntent?: { status: string }; error?: { message?: string } }>\n}\n\n/** The checkout's shopper details as Stripe's `billing_details`, which its fraud checks read; none when empty. */\nfunction billingDetails(context: PaymentConfirmContext | undefined): StripeBillingDetails | undefined {\n const details: StripeBillingDetails = {}\n if (context?.email) details.email = context.email\n const billing = context?.billing\n if (billing) {\n const name = `${billing.firstName} ${billing.lastName}`.trim()\n if (name) details.name = name\n details.address = {\n line1: billing.addressLine1,\n ...(billing.addressLine2 ? { line2: billing.addressLine2 } : {}),\n city: billing.city,\n state: billing.region,\n postal_code: billing.postalCode,\n country: billing.country,\n }\n }\n return Object.keys(details).length > 0 ? details : undefined\n}\n\ntype StripeFactory = (publishableKey: string) => StripeInstance\n\nlet loading: Promise<StripeFactory> | null = null\n\n/** `window.Stripe`, injecting Stripe.js once per page when it is not there yet. */\nfunction loadStripeJs(): Promise<StripeFactory> {\n const win = window as unknown as { Stripe?: StripeFactory }\n if (win.Stripe) return Promise.resolve(win.Stripe)\n loading ??= new Promise<StripeFactory>((resolve, reject) => {\n const script = document.createElement('script')\n script.src = STRIPE_JS\n script.async = true\n script.addEventListener('load', () => {\n if (win.Stripe) resolve(win.Stripe)\n else reject(new Error('Stripe.js loaded but window.Stripe is missing.'))\n })\n script.addEventListener('error', () => {\n loading = null\n script.remove()\n reject(new Error('Stripe.js could not be loaded.'))\n })\n document.head.appendChild(script)\n })\n return loading\n}\n\nconst instances = new Map<string, StripeInstance>()\nlet mounted: Array<{ host: HTMLElement; stripe: StripeInstance; card: StripeCard }> = []\n\n/** A card form from Stripe.js; confirm pays the intent with the card the shopper typed into. */\nexport const stripeDriver: PaymentDriver = {\n async mount(el, options) {\n const key = options['publishableKey']\n if (!key)\n throw new Error('Card payments are not configured: the store has no Stripe publishable key.')\n const factory = await loadStripeJs()\n let stripe = instances.get(key)\n if (stripe === undefined) {\n stripe = factory(key)\n instances.set(key, stripe)\n }\n const target = document.createElement('div')\n target.className = 'nymbal-payment-card'\n el.replaceChildren(target)\n const card = stripe.elements().create('card', {\n style: {\n base: { fontSize: '16px', color: '#1a1a1a', '::placeholder': { color: '#9ca3af' } },\n },\n })\n card.mount(target)\n mounted = [\n ...mounted.filter((m) => m.host.isConnected && m.host !== el),\n { host: el, stripe, card },\n ]\n },\n\n async confirm(clientSecret, context): Promise<PaymentConfirmation> {\n const current = mounted.findLast((m) => m.host.isConnected) ?? mounted.at(-1)\n if (current === undefined) return { ok: false, error: 'The card form is not ready.' }\n const billing_details = billingDetails(context)\n const result = await current.stripe.confirmCardPayment(clientSecret, {\n payment_method: { card: current.card, ...(billing_details ? { billing_details } : {}) },\n })\n if (result.error) return { ok: false, error: result.error.message ?? 'Payment failed.' }\n // `processing` is accepted but not paid yet: the webhook confirms it.\n return { ok: true, confirmed: result.paymentIntent?.status === 'succeeded' }\n },\n}\n","import type { PaymentDriver } from '@nymbalhq/sdk'\n\n/**\n * For a store whose payments need no shopper input (the native stub): nothing to collect, and nothing confirmed here.\n * An order the stub holds as `requires_action` stays pending until its webhook pays it.\n */\nexport const testModeDriver: PaymentDriver = {\n mount(el) {\n el.innerHTML = '<p class=\"nymbal-payment-note\">Test mode — payment skipped</p>'\n },\n confirm() {\n return Promise.resolve({ ok: true, confirmed: false })\n },\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
|
|
2
|
+
import {
|
|
3
|
+
NymbalElement,
|
|
4
|
+
baseStyles,
|
|
5
|
+
parsedJson,
|
|
6
|
+
propertiesOf
|
|
7
|
+
} from "./chunk-EYDMJNNK.js";
|
|
8
|
+
|
|
9
|
+
// src/components/product-filter.ts
|
|
10
|
+
import { css, html, nothing } from "lit";
|
|
11
|
+
var SCHEMA = {
|
|
12
|
+
tag: "nymbal-product-filter",
|
|
13
|
+
reactName: "ProductFilter",
|
|
14
|
+
attributes: [
|
|
15
|
+
{ name: "facets", prop: "facets", type: "json" },
|
|
16
|
+
{ name: "source", prop: "source", type: "string" }
|
|
17
|
+
],
|
|
18
|
+
events: [
|
|
19
|
+
{
|
|
20
|
+
name: "nymbal:filter:changed",
|
|
21
|
+
prop: "onFilterChange",
|
|
22
|
+
detail: "{ name: string; value: string }"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
parts: ["group", "label", "option", "checkbox", "option-label", "count"],
|
|
26
|
+
slots: [],
|
|
27
|
+
children: false
|
|
28
|
+
};
|
|
29
|
+
var NymbalProductFilter = class extends NymbalElement {
|
|
30
|
+
static schema = SCHEMA;
|
|
31
|
+
static properties = propertiesOf(SCHEMA);
|
|
32
|
+
static styles = [
|
|
33
|
+
baseStyles,
|
|
34
|
+
css`
|
|
35
|
+
:host {
|
|
36
|
+
display: block;
|
|
37
|
+
font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);
|
|
38
|
+
}
|
|
39
|
+
[part~='group'] {
|
|
40
|
+
margin-bottom: var(--nymbal-spacing-lg, 1.5rem);
|
|
41
|
+
}
|
|
42
|
+
[part~='label'] {
|
|
43
|
+
display: block;
|
|
44
|
+
font-size: var(--nymbal-font-size-sm, 0.875rem);
|
|
45
|
+
font-weight: var(--nymbal-font-weight-bold, 700);
|
|
46
|
+
margin-bottom: var(--nymbal-spacing-sm, 0.5rem);
|
|
47
|
+
color: var(--nymbal-color-text, #18181b);
|
|
48
|
+
text-transform: uppercase;
|
|
49
|
+
letter-spacing: 0.05em;
|
|
50
|
+
}
|
|
51
|
+
[part~='option'] {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: var(--nymbal-spacing-sm, 0.5rem);
|
|
55
|
+
padding: var(--nymbal-spacing-xs, 0.25rem) 0;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
font-size: var(--nymbal-font-size-sm, 0.875rem);
|
|
58
|
+
color: var(--nymbal-color-text, #18181b);
|
|
59
|
+
}
|
|
60
|
+
[part~='checkbox'] {
|
|
61
|
+
margin: 0;
|
|
62
|
+
accent-color: var(--nymbal-color-primary, #18181b);
|
|
63
|
+
}
|
|
64
|
+
[part~='count'] {
|
|
65
|
+
color: var(--nymbal-color-text-muted, #71717a);
|
|
66
|
+
font-size: var(--nymbal-font-size-xs, 0.75rem);
|
|
67
|
+
}
|
|
68
|
+
`
|
|
69
|
+
];
|
|
70
|
+
/** Both: a wrapper may set `source` after the element has connected. */
|
|
71
|
+
stores() {
|
|
72
|
+
return [this.client.productList, this.client.search];
|
|
73
|
+
}
|
|
74
|
+
/** With `source="search"` the `facets` a server read stand until the search settles; a listing's always show. */
|
|
75
|
+
storeSettled(store) {
|
|
76
|
+
if (store === this.client.search && this.getAttribute("source") === "search") {
|
|
77
|
+
this.facets = void 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** The facets this render shows, and whether each value is checked. */
|
|
81
|
+
shown() {
|
|
82
|
+
if (this.getAttribute("source") === "search") {
|
|
83
|
+
const given = parsedJson(this, "facets", this.facets);
|
|
84
|
+
const { facets, category } = given === void 0 && this.readsStores ? this.client.search.getState() : { facets: given ?? [], category: null };
|
|
85
|
+
return {
|
|
86
|
+
facets: facets.filter((f) => f.field === "category"),
|
|
87
|
+
checked: (_field, value) => value === category
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const filters = this.readsStores ? this.client.productList.getState().filters : {};
|
|
91
|
+
return {
|
|
92
|
+
facets: this.facetsAttribute(),
|
|
93
|
+
checked: (field, value) => {
|
|
94
|
+
const active = filters[field];
|
|
95
|
+
return Array.isArray(active) ? active.includes(value) : active === value;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
_shown = { facets: [], checked: () => false };
|
|
100
|
+
willUpdate(_changed) {
|
|
101
|
+
this._shown = this.shown();
|
|
102
|
+
this.toggleAttribute("empty", this._shown.facets.length === 0);
|
|
103
|
+
}
|
|
104
|
+
render() {
|
|
105
|
+
const { facets, checked } = this._shown;
|
|
106
|
+
return facets.map(
|
|
107
|
+
(facet) => html`
|
|
108
|
+
<div part="group" data-testid="filter-group-${facet.field}">
|
|
109
|
+
<span part="label">${facet.label}</span>
|
|
110
|
+
${facet.values.map(
|
|
111
|
+
(v) => html`
|
|
112
|
+
<label part="option" data-testid="filter-option-${v.value}">
|
|
113
|
+
<input
|
|
114
|
+
part="checkbox"
|
|
115
|
+
type="checkbox"
|
|
116
|
+
data-filter-name=${facet.field}
|
|
117
|
+
data-filter-value=${v.value}
|
|
118
|
+
.checked=${checked(facet.field, v.value)}
|
|
119
|
+
@change=${this.handleChange}
|
|
120
|
+
/>
|
|
121
|
+
<span part="option-label">${v.label ?? v.value}</span>
|
|
122
|
+
${v.count !== void 0 ? html`<span part="count" class="nymbal-filter-count">(${v.count})</span>` : nothing}
|
|
123
|
+
</label>
|
|
124
|
+
`
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
handleChange = (e) => {
|
|
131
|
+
const input = e.target;
|
|
132
|
+
const value = input.dataset["filterValue"];
|
|
133
|
+
if (this.getAttribute("source") === "search") {
|
|
134
|
+
void this.client.search.setCategory(input.checked ? value : null);
|
|
135
|
+
this.emit("category", input.checked ? value : "");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const name = input.dataset["filterName"];
|
|
139
|
+
const current = this.client.productList.getState().filters[name];
|
|
140
|
+
if (input.checked) {
|
|
141
|
+
const values = Array.isArray(current) ? [...current, value] : current ? [current, value] : value;
|
|
142
|
+
void this.client.productList.applyFilter(name, values);
|
|
143
|
+
} else if (Array.isArray(current)) {
|
|
144
|
+
const remaining = current.filter((v) => v !== value);
|
|
145
|
+
if (remaining.length > 0) {
|
|
146
|
+
void this.client.productList.applyFilter(name, remaining);
|
|
147
|
+
} else {
|
|
148
|
+
void this.client.productList.removeFilter(name);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
void this.client.productList.removeFilter(name);
|
|
152
|
+
}
|
|
153
|
+
this.emit(name, value);
|
|
154
|
+
};
|
|
155
|
+
emit(name, value) {
|
|
156
|
+
this.dispatchEvent(
|
|
157
|
+
new CustomEvent("nymbal:filter:changed", {
|
|
158
|
+
bubbles: true,
|
|
159
|
+
composed: true,
|
|
160
|
+
detail: { name, value }
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
facetsAttribute() {
|
|
165
|
+
return parsedJson(this, "facets", this.facets) ?? [];
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export {
|
|
170
|
+
NymbalProductFilter
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=chunk-WEEHXWM3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/product-filter.ts"],"sourcesContent":["import { css, html, nothing, type PropertyValues } from 'lit'\nimport type { Facet } from '@nymbalhq/types'\nimport {\n baseStyles,\n NymbalElement,\n parsedJson,\n propertiesOf,\n type ElementSchema,\n type Subscribable,\n} from '../base-element.js'\n\nconst SCHEMA: ElementSchema = {\n tag: 'nymbal-product-filter',\n reactName: 'ProductFilter',\n attributes: [\n { name: 'facets', prop: 'facets', type: 'json' },\n { name: 'source', prop: 'source', type: 'string' },\n ],\n events: [\n {\n name: 'nymbal:filter:changed',\n prop: 'onFilterChange',\n detail: '{ name: string; value: string }',\n },\n ],\n parts: ['group', 'label', 'option', 'checkbox', 'option-label', 'count'],\n slots: [],\n children: false,\n}\n\n/**\n * The facet filter. By default it reads the `facets` attribute and filters the product list. With `source=\"search\"`\n * it renders the search's own facets and narrows the search to one category: choosing an option selects it, choosing\n * the selected option again clears it. The host carries `empty` while it renders no facet, so a page can lay out\n * around it (its light DOM is always empty).\n */\nexport class NymbalProductFilter extends NymbalElement {\n static override schema = SCHEMA\n static override properties = propertiesOf(SCHEMA)\n\n static override styles = [\n baseStyles,\n css`\n :host {\n display: block;\n font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);\n }\n [part~='group'] {\n margin-bottom: var(--nymbal-spacing-lg, 1.5rem);\n }\n [part~='label'] {\n display: block;\n font-size: var(--nymbal-font-size-sm, 0.875rem);\n font-weight: var(--nymbal-font-weight-bold, 700);\n margin-bottom: var(--nymbal-spacing-sm, 0.5rem);\n color: var(--nymbal-color-text, #18181b);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n }\n [part~='option'] {\n display: flex;\n align-items: center;\n gap: var(--nymbal-spacing-sm, 0.5rem);\n padding: var(--nymbal-spacing-xs, 0.25rem) 0;\n cursor: pointer;\n font-size: var(--nymbal-font-size-sm, 0.875rem);\n color: var(--nymbal-color-text, #18181b);\n }\n [part~='checkbox'] {\n margin: 0;\n accent-color: var(--nymbal-color-primary, #18181b);\n }\n [part~='count'] {\n color: var(--nymbal-color-text-muted, #71717a);\n font-size: var(--nymbal-font-size-xs, 0.75rem);\n }\n `,\n ]\n\n declare facets: unknown\n\n /** Both: a wrapper may set `source` after the element has connected. */\n protected override stores() {\n return [this.client.productList, this.client.search]\n }\n\n /** With `source=\"search\"` the `facets` a server read stand until the search settles; a listing's always show. */\n protected override storeSettled(store: Subscribable): void {\n if (store === this.client.search && this.getAttribute('source') === 'search') {\n this.facets = undefined\n }\n }\n\n /** The facets this render shows, and whether each value is checked. */\n private shown(): { facets: Facet[]; checked: (field: string, value: string) => boolean } {\n if (this.getAttribute('source') === 'search') {\n const given = parsedJson<Facet[]>(this, 'facets', this.facets)\n const { facets, category } =\n given === undefined && this.readsStores\n ? this.client.search.getState()\n : { facets: given ?? [], category: null }\n return {\n facets: facets.filter((f) => f.field === 'category'),\n checked: (_field, value) => value === category,\n }\n }\n const filters = this.readsStores ? this.client.productList.getState().filters : {}\n return {\n facets: this.facetsAttribute(),\n checked: (field, value) => {\n const active = filters[field]\n return Array.isArray(active) ? active.includes(value) : active === value\n },\n }\n }\n\n private _shown: ReturnType<NymbalProductFilter['shown']> = { facets: [], checked: () => false }\n\n protected override willUpdate(_changed: PropertyValues): void {\n this._shown = this.shown()\n this.toggleAttribute('empty', this._shown.facets.length === 0)\n }\n\n protected override render() {\n const { facets, checked } = this._shown\n return facets.map(\n (facet) => html`\n <div part=\"group\" data-testid=\"filter-group-${facet.field}\">\n <span part=\"label\">${facet.label}</span>\n ${facet.values.map(\n (v) => html`\n <label part=\"option\" data-testid=\"filter-option-${v.value}\">\n <input\n part=\"checkbox\"\n type=\"checkbox\"\n data-filter-name=${facet.field}\n data-filter-value=${v.value}\n .checked=${checked(facet.field, v.value)}\n @change=${this.handleChange}\n />\n <span part=\"option-label\">${v.label ?? v.value}</span>\n ${v.count !== undefined ? html`<span part=\"count\" class=\"nymbal-filter-count\">(${v.count})</span>` : nothing}\n </label>\n `,\n )}\n </div>\n `,\n )\n }\n\n private handleChange = (e: Event): void => {\n const input = e.target as HTMLInputElement\n const value = input.dataset['filterValue']!\n if (this.getAttribute('source') === 'search') {\n void this.client.search.setCategory(input.checked ? value : null)\n this.emit('category', input.checked ? value : '')\n return\n }\n const name = input.dataset['filterName']!\n const current = this.client.productList.getState().filters[name]\n if (input.checked) {\n const values = Array.isArray(current)\n ? [...current, value]\n : current\n ? [current, value]\n : value\n void this.client.productList.applyFilter(name, values)\n } else if (Array.isArray(current)) {\n const remaining = current.filter((v) => v !== value)\n if (remaining.length > 0) {\n void this.client.productList.applyFilter(name, remaining)\n } else {\n void this.client.productList.removeFilter(name)\n }\n } else {\n void this.client.productList.removeFilter(name)\n }\n this.emit(name, value)\n }\n\n private emit(name: string, value: string): void {\n this.dispatchEvent(\n new CustomEvent('nymbal:filter:changed', {\n bubbles: true,\n composed: true,\n detail: { name, value },\n }),\n )\n }\n\n private facetsAttribute(): Facet[] {\n return parsedJson<Facet[]>(this, 'facets', this.facets) ?? []\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
|
|
2
|
+
import {
|
|
3
|
+
NymbalFormElement,
|
|
4
|
+
renderField
|
|
5
|
+
} from "./chunk-PN6QKYE4.js";
|
|
6
|
+
import {
|
|
7
|
+
buttonStyles,
|
|
8
|
+
fieldStyles
|
|
9
|
+
} from "./chunk-GHXTJ2CF.js";
|
|
10
|
+
import {
|
|
11
|
+
baseStyles,
|
|
12
|
+
parsedJson,
|
|
13
|
+
propertiesOf
|
|
14
|
+
} from "./chunk-EYDMJNNK.js";
|
|
15
|
+
|
|
16
|
+
// src/components/profile-form.ts
|
|
17
|
+
import { css, html, nothing } from "lit";
|
|
18
|
+
var SCHEMA = {
|
|
19
|
+
tag: "nymbal-profile-form",
|
|
20
|
+
reactName: "ProfileForm",
|
|
21
|
+
// The customer a server read, shown until the auth store settles.
|
|
22
|
+
attributes: [{ name: "customer", prop: "customer", type: "json" }],
|
|
23
|
+
events: [
|
|
24
|
+
{
|
|
25
|
+
name: "nymbal:profile:saved",
|
|
26
|
+
prop: "onSaved",
|
|
27
|
+
detail: "{ customer: unknown }"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
parts: ["form", "row", "field", "label", "input", "actions", "submit", "error", "message"],
|
|
31
|
+
slots: ["submit-label"],
|
|
32
|
+
children: false
|
|
33
|
+
};
|
|
34
|
+
var NymbalProfileForm = class extends NymbalFormElement {
|
|
35
|
+
static schema = SCHEMA;
|
|
36
|
+
static properties = propertiesOf(SCHEMA);
|
|
37
|
+
static styles = [
|
|
38
|
+
baseStyles,
|
|
39
|
+
buttonStyles,
|
|
40
|
+
fieldStyles,
|
|
41
|
+
css`
|
|
42
|
+
[part~='row'] {
|
|
43
|
+
display: grid;
|
|
44
|
+
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
|
45
|
+
gap: 0 var(--nymbal-spacing-md, 1rem);
|
|
46
|
+
}
|
|
47
|
+
[part~='submit'] {
|
|
48
|
+
width: auto;
|
|
49
|
+
}
|
|
50
|
+
`
|
|
51
|
+
];
|
|
52
|
+
stores() {
|
|
53
|
+
return [this.client.auth];
|
|
54
|
+
}
|
|
55
|
+
storeSettled() {
|
|
56
|
+
this.customer = void 0;
|
|
57
|
+
}
|
|
58
|
+
async send() {
|
|
59
|
+
await this.client.auth.updateProfile({
|
|
60
|
+
firstName: this.value("firstName"),
|
|
61
|
+
lastName: this.value("lastName"),
|
|
62
|
+
phone: this.value("phone")
|
|
63
|
+
});
|
|
64
|
+
this.notice = "Profile updated successfully.";
|
|
65
|
+
this.emit("nymbal:profile:saved", { customer: this.client.auth.getState().customer });
|
|
66
|
+
}
|
|
67
|
+
failureText(err) {
|
|
68
|
+
return err instanceof Error && err.message !== "" ? err.message : "Failed to update profile.";
|
|
69
|
+
}
|
|
70
|
+
render() {
|
|
71
|
+
const customer = parsedJson(this, "customer", this.customer) ?? (this.readsStores ? this.client.auth.getState().customer : null);
|
|
72
|
+
return html`
|
|
73
|
+
${this.renderError()}
|
|
74
|
+
<form part="form" data-testid="profile-form" @submit=${this.onSubmit}>
|
|
75
|
+
<div part="row">
|
|
76
|
+
${renderField({
|
|
77
|
+
id: "profile-firstName",
|
|
78
|
+
name: "firstName",
|
|
79
|
+
label: "First Name",
|
|
80
|
+
autocomplete: "given-name",
|
|
81
|
+
value: customer?.firstName ?? ""
|
|
82
|
+
})}
|
|
83
|
+
${renderField({
|
|
84
|
+
id: "profile-lastName",
|
|
85
|
+
name: "lastName",
|
|
86
|
+
label: "Last Name",
|
|
87
|
+
autocomplete: "family-name",
|
|
88
|
+
value: customer?.lastName ?? ""
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
${renderField({
|
|
92
|
+
id: "profile-email",
|
|
93
|
+
name: "email",
|
|
94
|
+
label: "Email",
|
|
95
|
+
type: "email",
|
|
96
|
+
autocomplete: "email",
|
|
97
|
+
disabled: true,
|
|
98
|
+
value: customer?.email ?? ""
|
|
99
|
+
})}
|
|
100
|
+
${renderField({
|
|
101
|
+
id: "profile-phone",
|
|
102
|
+
name: "phone",
|
|
103
|
+
label: "Phone",
|
|
104
|
+
type: "tel",
|
|
105
|
+
autocomplete: "tel",
|
|
106
|
+
value: customer?.phone ?? ""
|
|
107
|
+
})}
|
|
108
|
+
<div part="actions">
|
|
109
|
+
<button
|
|
110
|
+
type="submit"
|
|
111
|
+
part="submit"
|
|
112
|
+
class="btn btn-primary"
|
|
113
|
+
data-testid="profile-save"
|
|
114
|
+
?disabled=${this.pending}
|
|
115
|
+
>
|
|
116
|
+
${this.pending ? "Saving..." : html`<slot name="submit-label">Save Changes</slot>`}
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
${this.notice === null ? nothing : html`<p part="message" role="status">${this.notice}</p>`}
|
|
120
|
+
</form>
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export {
|
|
126
|
+
NymbalProfileForm
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=chunk-WTEVTCIL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/profile-form.ts"],"sourcesContent":["import { css, html, nothing } from 'lit'\nimport type { Customer } from '@nymbalhq/types'\nimport { baseStyles, parsedJson, propertiesOf, type ElementSchema } from '../base-element.js'\nimport { NymbalFormElement, renderField } from '../form-element.js'\nimport { buttonStyles, fieldStyles } from '../view-styles.js'\n\nconst SCHEMA: ElementSchema = {\n tag: 'nymbal-profile-form',\n reactName: 'ProfileForm',\n // The customer a server read, shown until the auth store settles.\n attributes: [{ name: 'customer', prop: 'customer', type: 'json' }],\n events: [\n {\n name: 'nymbal:profile:saved',\n prop: 'onSaved',\n detail: '{ customer: unknown }',\n },\n ],\n parts: ['form', 'row', 'field', 'label', 'input', 'actions', 'submit', 'error', 'message'],\n slots: ['submit-label'],\n children: false,\n}\n\n/** The signed-in customer's name and phone, filled from the auth store and saved through it. */\nexport class NymbalProfileForm extends NymbalFormElement {\n static override schema = SCHEMA\n static override properties = propertiesOf(SCHEMA)\n static override styles = [\n baseStyles,\n buttonStyles,\n fieldStyles,\n css`\n [part~='row'] {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));\n gap: 0 var(--nymbal-spacing-md, 1rem);\n }\n [part~='submit'] {\n width: auto;\n }\n `,\n ]\n\n declare customer: unknown\n\n protected override stores() {\n return [this.client.auth]\n }\n\n protected override storeSettled(): void {\n this.customer = undefined\n }\n\n protected override async send(): Promise<void> {\n await this.client.auth.updateProfile({\n firstName: this.value('firstName'),\n lastName: this.value('lastName'),\n phone: this.value('phone'),\n })\n this.notice = 'Profile updated successfully.'\n this.emit('nymbal:profile:saved', { customer: this.client.auth.getState().customer })\n }\n\n protected override failureText(err: unknown): string {\n return err instanceof Error && err.message !== '' ? err.message : 'Failed to update profile.'\n }\n\n protected override render() {\n const customer =\n parsedJson<Customer>(this, 'customer', this.customer) ??\n (this.readsStores ? this.client.auth.getState().customer : null)\n return html`\n ${this.renderError()}\n <form part=\"form\" data-testid=\"profile-form\" @submit=${this.onSubmit}>\n <div part=\"row\">\n ${renderField({\n id: 'profile-firstName',\n name: 'firstName',\n label: 'First Name',\n autocomplete: 'given-name',\n value: customer?.firstName ?? '',\n })}\n ${renderField({\n id: 'profile-lastName',\n name: 'lastName',\n label: 'Last Name',\n autocomplete: 'family-name',\n value: customer?.lastName ?? '',\n })}\n </div>\n ${renderField({\n id: 'profile-email',\n name: 'email',\n label: 'Email',\n type: 'email',\n autocomplete: 'email',\n disabled: true,\n value: customer?.email ?? '',\n })}\n ${renderField({\n id: 'profile-phone',\n name: 'phone',\n label: 'Phone',\n type: 'tel',\n autocomplete: 'tel',\n value: customer?.phone ?? '',\n })}\n <div part=\"actions\">\n <button\n type=\"submit\"\n part=\"submit\"\n class=\"btn btn-primary\"\n data-testid=\"profile-save\"\n ?disabled=${this.pending}\n >\n ${this.pending ? 'Saving...' : html`<slot name=\"submit-label\">Save Changes</slot>`}\n </button>\n </div>\n ${this.notice === null\n ? nothing\n : html`<p part=\"message\" role=\"status\">${this.notice}</p>`}\n </form>\n `\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA,SAAS,KAAK,MAAM,eAAe;AAMnC,IAAM,SAAwB;AAAA,EAC5B,KAAK;AAAA,EACL,WAAW;AAAA;AAAA,EAEX,YAAY,CAAC,EAAE,MAAM,YAAY,MAAM,YAAY,MAAM,OAAO,CAAC;AAAA,EACjE,QAAQ;AAAA,IACN;AAAA,MACE,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,IACV;AAAA,EACF;AAAA,EACA,OAAO,CAAC,QAAQ,OAAO,SAAS,SAAS,SAAS,WAAW,UAAU,SAAS,SAAS;AAAA,EACzF,OAAO,CAAC,cAAc;AAAA,EACtB,UAAU;AACZ;AAGO,IAAM,oBAAN,cAAgC,kBAAkB;AAAA,EACvD,OAAgB,SAAS;AAAA,EACzB,OAAgB,aAAa,aAAa,MAAM;AAAA,EAChD,OAAgB,SAAS;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUF;AAAA,EAImB,SAAS;AAC1B,WAAO,CAAC,KAAK,OAAO,IAAI;AAAA,EAC1B;AAAA,EAEmB,eAAqB;AACtC,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAyB,OAAsB;AAC7C,UAAM,KAAK,OAAO,KAAK,cAAc;AAAA,MACnC,WAAW,KAAK,MAAM,WAAW;AAAA,MACjC,UAAU,KAAK,MAAM,UAAU;AAAA,MAC/B,OAAO,KAAK,MAAM,OAAO;AAAA,IAC3B,CAAC;AACD,SAAK,SAAS;AACd,SAAK,KAAK,wBAAwB,EAAE,UAAU,KAAK,OAAO,KAAK,SAAS,EAAE,SAAS,CAAC;AAAA,EACtF;AAAA,EAEmB,YAAY,KAAsB;AACnD,WAAO,eAAe,SAAS,IAAI,YAAY,KAAK,IAAI,UAAU;AAAA,EACpE;AAAA,EAEmB,SAAS;AAC1B,UAAM,WACJ,WAAqB,MAAM,YAAY,KAAK,QAAQ,MACnD,KAAK,cAAc,KAAK,OAAO,KAAK,SAAS,EAAE,WAAW;AAC7D,WAAO;AAAA,QACH,KAAK,YAAY,CAAC;AAAA,6DACmC,KAAK,QAAQ;AAAA;AAAA,YAE9D,YAAY;AAAA,MACZ,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,OAAO;AAAA,MACP,cAAc;AAAA,MACd,OAAO,UAAU,aAAa;AAAA,IAChC,CAAC,CAAC;AAAA,YACA,YAAY;AAAA,MACZ,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,OAAO;AAAA,MACP,cAAc;AAAA,MACd,OAAO,UAAU,YAAY;AAAA,IAC/B,CAAC,CAAC;AAAA;AAAA,UAEF,YAAY;AAAA,MACZ,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,OAAO;AAAA,MACP,MAAM;AAAA,MACN,cAAc;AAAA,MACd,UAAU;AAAA,MACV,OAAO,UAAU,SAAS;AAAA,IAC5B,CAAC,CAAC;AAAA,UACA,YAAY;AAAA,MACZ,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,OAAO;AAAA,MACP,MAAM;AAAA,MACN,cAAc;AAAA,MACd,OAAO,UAAU,SAAS;AAAA,IAC5B,CAAC,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAOc,KAAK,OAAO;AAAA;AAAA,cAEtB,KAAK,UAAU,cAAc,mDAAmD;AAAA;AAAA;AAAA,UAGpF,KAAK,WAAW,OACd,UACA,uCAAuC,KAAK,MAAM,MAAM;AAAA;AAAA;AAAA,EAGlE;AACF;","names":[]}
|