@nozle-js/react 0.10.1 → 0.11.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/index.js CHANGED
@@ -1,9 +1,238 @@
1
- import React, { createContext, useState, useRef, useId, useEffect, useMemo, useContext, useCallback } from 'react';
2
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
1
+ "use client";import { T, border, mergeAppearance, toCssVars, text, buttonStyle, table, surface, buttonRadius, primaryButtonBorder } from './chunk-NLPONQ5O.js';
2
+ import React2, { createContext, useContext, useMemo, useState, useRef, useId, useEffect, useCallback } from 'react';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
+ import { QueryClient, useQuery, useInfiniteQuery } from '@tanstack/react-query';
3
5
  import { EmbeddedCheckoutProvider, EmbeddedCheckout, Elements, useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
4
6
  import { loadStripe } from '@stripe/stripe-js';
5
7
 
6
- // src/components/portal/BillingPortal.tsx
8
+ var AppearanceContext = createContext(void 0);
9
+ function AppearanceProvider({
10
+ appearance,
11
+ children
12
+ }) {
13
+ return /* @__PURE__ */ jsx(AppearanceContext.Provider, { value: appearance, children });
14
+ }
15
+ function useAppearance(appearance) {
16
+ const inherited = useContext(AppearanceContext);
17
+ return useMemo(() => {
18
+ const { variables, elements, planCardLayout = "card" } = mergeAppearance(inherited, appearance);
19
+ const vars = toCssVars(variables);
20
+ const el = (key, style, className) => {
21
+ const override = elements[key];
22
+ return {
23
+ className: ["nozle-" + key, className, override?.className].filter(Boolean).join(" "),
24
+ style: { ...style, ...override?.style }
25
+ };
26
+ };
27
+ return {
28
+ el,
29
+ planCardLayout,
30
+ vars,
31
+ root: (key, style, className) => {
32
+ const props = el(key, style, className);
33
+ return { ...props, style: { ...vars, ...props.style } };
34
+ }
35
+ };
36
+ }, [inherited, appearance]);
37
+ }
38
+ function createClient(publishableKey, baseUrl) {
39
+ let end = baseUrl.length;
40
+ while (end > 0 && baseUrl[end - 1] === "/") end--;
41
+ const base = baseUrl.slice(0, end);
42
+ return {
43
+ publishableKey,
44
+ baseUrl: base,
45
+ catalogFetch(path, init) {
46
+ return fetch(`${base}${path}`, {
47
+ ...init,
48
+ headers: {
49
+ Authorization: `Bearer ${publishableKey}`,
50
+ "Content-Type": "application/json",
51
+ ...init?.headers
52
+ }
53
+ });
54
+ }
55
+ };
56
+ }
57
+ var BillingRequestError = class extends Error {
58
+ constructor(status, code, requestId) {
59
+ super(`Billing request failed: ${status}${code ? ` ${code}` : ""}`);
60
+ this.status = status;
61
+ this.code = code;
62
+ this.requestId = requestId;
63
+ this.name = "BillingRequestError";
64
+ }
65
+ status;
66
+ code;
67
+ requestId;
68
+ };
69
+ function createBillingFetch(endpoint, headers) {
70
+ let end = endpoint.length;
71
+ while (end > 0 && endpoint[end - 1] === "/") end--;
72
+ const base = endpoint.slice(0, end);
73
+ return async (route, params = {}) => {
74
+ const query = new URLSearchParams();
75
+ for (const [key, value] of Object.entries(params)) {
76
+ if (value !== void 0 && value !== "") query.set(key, String(value));
77
+ }
78
+ const search = query.toString();
79
+ const response = await fetch(`${base}/${route}${search ? `?${search}` : ""}`, {
80
+ credentials: "same-origin",
81
+ headers: { Accept: "application/json", ...headers ? await headers() : {} }
82
+ });
83
+ if (!response.ok) {
84
+ const body = await response.json().catch(() => ({}));
85
+ throw new BillingRequestError(response.status, body.code, body.request_id);
86
+ }
87
+ return await response.json();
88
+ };
89
+ }
90
+ function createQueryClient() {
91
+ return new QueryClient({
92
+ defaultOptions: {
93
+ queries: {
94
+ staleTime: 3e4,
95
+ // Retry only failures that may pass on a second try; a 4xx will not.
96
+ retry: (count, error) => count < 2 && !(error instanceof BillingRequestError && error.status < 500)
97
+ }
98
+ }
99
+ });
100
+ }
101
+ var BillingContext = createContext(null);
102
+ function BillingProvider({
103
+ publishableKey,
104
+ createCheckout,
105
+ verifyCheckout,
106
+ getCheckoutStatus,
107
+ baseUrl = "https://api.nozle.app/engine",
108
+ billingEndpoint,
109
+ billingHeaders,
110
+ queryClient,
111
+ appearance,
112
+ children
113
+ }) {
114
+ if (!publishableKey.startsWith("pk_")) {
115
+ throw new Error(
116
+ "BillingProvider publishableKey must be a publishable key (pk_)"
117
+ );
118
+ }
119
+ const client = useMemo(
120
+ () => createClient(publishableKey, baseUrl),
121
+ [publishableKey, baseUrl]
122
+ );
123
+ const [ownQueryClient] = useState(createQueryClient);
124
+ const activeQueryClient = queryClient ?? ownQueryClient;
125
+ const billingFetch = useMemo(
126
+ () => billingEndpoint ? createBillingFetch(billingEndpoint, billingHeaders) : void 0,
127
+ [billingEndpoint, billingHeaders]
128
+ );
129
+ const contextValue = useMemo(
130
+ () => ({
131
+ client,
132
+ createCheckout,
133
+ verifyCheckout,
134
+ getCheckoutStatus,
135
+ queryClient: activeQueryClient,
136
+ billingFetch,
137
+ billingEndpoint
138
+ }),
139
+ [client, createCheckout, verifyCheckout, getCheckoutStatus, activeQueryClient, billingFetch, billingEndpoint]
140
+ );
141
+ return React2.createElement(
142
+ BillingContext.Provider,
143
+ { value: contextValue },
144
+ React2.createElement(AppearanceProvider, { appearance, children })
145
+ );
146
+ }
147
+ function useOptionalBillingContext() {
148
+ return useContext(BillingContext);
149
+ }
150
+ function useBillingContext() {
151
+ const context = useOptionalBillingContext();
152
+ if (!context) {
153
+ throw new Error("useBillingContext must be used within a BillingProvider");
154
+ }
155
+ return context;
156
+ }
157
+ function useNozleClient() {
158
+ return useBillingContext().client;
159
+ }
160
+ var idleQueryClient = new QueryClient();
161
+ function catalogQuery(client) {
162
+ return {
163
+ queryKey: ["nozle", "plans", client?.baseUrl, client?.publishableKey],
164
+ queryFn: async ({ signal }) => {
165
+ const response = await client.catalogFetch("/api/v1/plans", { signal });
166
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
167
+ const data = await response.json();
168
+ return { plans: data.plans ?? [], nozleBranding: data.meta?.nozle_branding === true };
169
+ },
170
+ staleTime: 5 * 6e4,
171
+ // The pricing page shows a load error at once; the app can call refetch().
172
+ retry: false
173
+ };
174
+ }
175
+ function usePlans() {
176
+ const { client, queryClient } = useBillingContext();
177
+ const query = useQuery(catalogQuery(client), queryClient);
178
+ return {
179
+ plans: query.data?.plans ?? [],
180
+ isLoading: query.isPending,
181
+ error: query.error ?? null,
182
+ refetch: query.refetch
183
+ };
184
+ }
185
+
186
+ // src/components/branding/logo.ts
187
+ var LOGO_VIEW_BOX = "0 0 3208 1122";
188
+ var LOGO_MARK_PATH = "M 1108.89 330.512 C 1131.49 326.989 1184.16 334.391 1207.63 338.298 L 1207.63 511.329 C 1203.66 515.246 1199.46 519.132 1195.4 522.972 L 1286.52 523.006 C 1282.75 519.093 1278.87 515.284 1274.89 511.585 L 1274.92 358.597 C 1311.02 374.213 1344.6 395.09 1374.58 420.548 C 1451.3 486.571 1498.83 580.231 1506.81 681.131 C 1515 782.133 1482.24 882.187 1415.89 958.782 C 1362.73 1020.34 1289.84 1065.92 1209.97 1082.75 C 1185.45 1087.92 1168.82 1089.01 1144.24 1090.82 C 1092.37 1092.55 1040.69 1083.81 992.278 1065.1 C 899.111 1029.72 824.189 955.46 783.642 864.858 C 743.243 772.802 740.63 668.558 776.367 574.594 C 807.572 492.815 865.841 424.187 941.477 380.133 C 955.897 371.807 970.834 365.19 986.004 358.415 L 986.997 511.83 C 982.504 514.698 977.232 518.881 972.988 522.219 C 982.441 521.793 993.966 522.74 1003.69 522.843 L 1066.51 523.012 C 1062.58 519.054 1058.6 514.702 1054 511.616 C 1053.71 494.114 1055.01 476.198 1054.99 458.475 L 1054 337.726 C 1074.93 333.524 1088.73 333.1 1108.89 330.512 z M 1136.49 834.878 C 1198.61 831.142 1246.02 777.893 1242.55 715.759 C 1239.08 653.625 1186.03 605.989 1123.88 609.196 C 1061.36 612.423 1013.37 665.861 1016.87 728.369 C 1020.36 790.878 1074 838.637 1136.49 834.878 z";
189
+ var LOGO_WORDMARK_PATH = "M2807.48 317.546C2812.82 316.693 2819.27 316.959 2824.49 316.891 3043.76 314.061 3223.51 517.798 3205.41 733.935 3204.36 746.553 3203.33 760.715 3203.2 773.228 3175.25 773.336 3148.36 772.242 3120.74 772.167L2889.16 773.329C2863.8 773.347 2650.02 771.22 2643.56 776.302 2643.49 776.742 2643.33 778.022 2643.41 778.366 2650.33 808.489 2668.08 838.036 2690.04 858.674 2726.37 892.826 2776.29 910.3 2825.39 908.765 2887.65 906.818 2929.65 884.535 2971.8 840.731L3187.64 841.821C3176.73 876.79 3145.09 927.003 3121.73 956.25 3057.95 1036.16 2965.03 1087.46 2863.43 1098.86 2764.86 1109.46 2666.11 1080.6 2588.77 1018.59 2504.68 952.597 2450.51 855.672 2438.38 749.464 2415.16 540.793 2579.74 336.515 2790.2 318.706 2795.97 318.218 2801.75 318.181 2807.48 317.546zM2836.5 625.92 3004.04 625.92C2976.56 571.597 2937.23 529.14 2878.08 508.386 2862.06 502.767 2834.68 498.432 2817.69 499.447 2811.66 500.426 2805.72 500.948 2799.66 501.626 2736.71 508.675 2678.99 552.56 2653.08 609.964 2650.95 614.667 2649.24 618.489 2648.22 623.618L2648.73 625.573C2666.65 626.805 2695.47 625.951 2714.08 625.95L2836.5 625.92zM350.026 327.534C351.059 327.376 351.817 327.251 352.866 327.196 440.33 322.588 528.279 352.28 593.965 409.932 660.794 468.083 703.294 549.252 713.018 637.303 717.397 678.934 715.385 718.263 715.162 759.723L714.249 949.463C714.221 978.726 713.328 1009.12 714.526 1038.31 715.585 1064.08 724.618 1074.23 742.925 1090.23 655.133 1089.48 574.896 1088.28 486.53 1091.14 506.936 1074.25 516.198 1062.6 516.991 1034.69 517.509 1016.48 517.681 998.316 517.701 980.062L517.678 871.454 518.327 744.743C518.417 708.921 519.234 677.156 513.6 641.605 504.197 582.258 457.155 536.074 396.911 528.341 387.723 526.897 378.419 526.337 369.124 526.669 330.893 527.948 296.354 540.985 270.002 569.309 252.159 588.394 239.802 611.944 234.238 637.471 228.538 663.791 228.617 711.497 228.796 738.55L229.47 833.874 229.051 964.879C229.043 985.892 228.914 1006.79 229.398 1027.79 230.072 1057 235.364 1070.31 256.803 1090.19 210.541 1092.85 167.594 1091.93 121.604 1091.28L2.29567 1090.29C23.4576 1071.55 31.3577 1057.22 32.9306 1028.58 34.6194 997.827 33.8844 967.966 33.5542 937.249L33.0591 779.859C32.8532 653.775 26.6617 550.025 115.68 447.849 180 374.022 252.997 336.686 350.026 327.534zM1552.98 329.783C1585.23 341.06 1694.79 336.137 1732.02 336.28L1987.95 337.925C2031.97 338.036 2076.24 335.822 2120.7 337.349 2118.21 357.823 2118.97 389.029 2118.96 410.256 2118.6 445.944 2118.97 481.634 2120.07 517.306L1961.06 704.338C1908.01 766.03 1851.33 830.755 1801.51 894.952 1819.57 894.053 1845.55 894.949 1864.37 894.958L1989.79 895.045C2037.99 895.176 2070.48 897.452 2119.04 886.152 2118.97 952.443 2120.33 1018.73 2118.99 1085 2118.85 1091.75 2120.17 1104.35 2118.4 1110.48L2115.98 1111.17C2070.03 1077.75 2034.35 1081.57 1981.3 1081.89L1888.49 1081.97 1544.82 1082.03C1545.37 1024.58 1544.47 968.65 1544.02 911.354 1563.44 886.164 1589.99 857.395 1611.19 832.365L1782.61 629.927C1814.21 592.718 1847.38 556.886 1876.98 518.016L1552 517.984C1551.65 496.963 1554.2 471.241 1553.87 449.778 1553.69 437.667 1553.18 425.595 1553.07 413.5L1552.98 329.783zM2373.44 4.75901C2374.2 55.5907 2372.32 106.262 2372.3 157.309L2372.39 599.153 2373.25 737.25C2373.29 771.822 2369.91 823.05 2379.51 856.818 2384.04 872.719 2405.1 883.099 2420.05 889.245L2420.03 1090.04C2375.69 1091.15 2312.39 1065.98 2279.04 1037.28 2271.95 1032.7 2261.78 1023.12 2255.56 1016.95 2219.14 980.786 2191.86 928.49 2181.81 878.203 2174.98 844.049 2176.85 799.114 2176.92 763.235L2177.02 625.401 2176.97 73.7487C2189.94 73.6898 2202.93 74.1053 2215.89 73.9192 2264.74 73.2173 2312.58 61.8792 2349.93 28.7315 2357.84 21.7113 2365.92 12.4727 2373.44 4.75901z";
190
+ var BrandingScope = createContext(false);
191
+ function useNozleBranding() {
192
+ const billing = useOptionalBillingContext();
193
+ const { data } = useQuery(
194
+ // Reads the catalog request the components already make: mounting this must not refetch a
195
+ // failed catalog, or a table that shows the error would loop back into loading.
196
+ { ...catalogQuery(billing?.client), enabled: Boolean(billing), refetchOnMount: false, retryOnMount: false },
197
+ billing?.queryClient ?? idleQueryClient
198
+ );
199
+ return data?.nozleBranding === true;
200
+ }
201
+ function PoweredByNozle() {
202
+ const { root } = useAppearance();
203
+ return /* @__PURE__ */ jsxs(
204
+ "a",
205
+ {
206
+ href: "https://nozle.dev",
207
+ target: "_blank",
208
+ rel: "noopener noreferrer",
209
+ ...root("poweredBy", {
210
+ ...text.meta,
211
+ display: "flex",
212
+ alignItems: "center",
213
+ justifyContent: "center",
214
+ gap: "6px",
215
+ marginTop: "1rem",
216
+ textDecoration: "none"
217
+ }),
218
+ children: [
219
+ "Powered by",
220
+ /* @__PURE__ */ jsxs("svg", { viewBox: LOGO_VIEW_BOX, height: "14", width: "40", role: "img", "aria-label": "Nozle", style: { color: T.foreground }, children: [
221
+ /* @__PURE__ */ jsx("path", { fill: "#F34213", d: LOGO_MARK_PATH }),
222
+ /* @__PURE__ */ jsx("path", { fill: "currentColor", d: LOGO_WORDMARK_PATH })
223
+ ] })
224
+ ]
225
+ }
226
+ );
227
+ }
228
+ function NozleBranding({ show, children }) {
229
+ const nested = useContext(BrandingScope);
230
+ const fromCatalog = useNozleBranding();
231
+ return /* @__PURE__ */ jsxs(BrandingScope.Provider, { value: true, children: [
232
+ children,
233
+ !nested && (show ?? fromCatalog) && /* @__PURE__ */ jsx(PoweredByNozle, {})
234
+ ] });
235
+ }
7
236
 
8
237
  // src/components/portal/client.ts
9
238
  var BillingPortalError = class extends Error {
@@ -96,7 +325,7 @@ var identityQuery = `query SdkPortalIdentity {
96
325
  addressLine1 addressLine2 city state zipcode country
97
326
  shippingAddress { addressLine1 addressLine2 city state zipcode country }
98
327
  }
99
- customerPortalOrganization { id name }
328
+ customerPortalOrganization { id name nozleBranding }
100
329
  }`;
101
330
  var subscriptionsQuery = `query SdkPortalSubscriptions($page: Int) {
102
331
  customerPortalSubscriptions(status: [active], limit: 20, page: $page) {
@@ -145,40 +374,55 @@ var topUpMutation = `mutation SdkPortalTopUp($input: CreateCustomerPortalWalletT
145
374
  }`;
146
375
 
147
376
  // src/components/portal/styles.ts
377
+ var line = `${T.borderWidth} solid ${T.border}`;
378
+ var r = (factor) => `calc(${T.radius} * ${factor})`;
148
379
  var portalStyles = `
149
- .nozle-billing-portal{color:var(--nozle-portal-text,#202922);background:var(--nozle-portal-background,#fff);font:14px/1.55 system-ui,sans-serif;box-sizing:border-box;width:100%;min-width:0;padding:clamp(18px,4vw,40px);border-radius:12px}
380
+ .nozle-billing-portal{color:${T.foreground};background:${T.background};font:${T.fontBody};box-sizing:border-box;width:100%;min-width:0;padding:clamp(18px,4vw,40px);border-radius:${T.radius}}
150
381
  .nozle-billing-portal *{box-sizing:border-box}
151
382
  .nozle-billing-portal h2,.nozle-billing-portal h3,.nozle-billing-portal p{margin:0}
152
- .nozle-billing-portal h2{font-size:24px;letter-spacing:-.6px;line-height:1.3}
153
- .nozle-billing-portal h3{font-size:16px;line-height:1.4}
383
+ .nozle-billing-portal h2{font:${T.fontTitle}}
384
+ .nozle-billing-portal h3{font:${T.fontHeading}}
154
385
  .nozle-billing-portal header{margin-bottom:30px}
155
- .nozle-billing-portal section{margin-top:28px;padding-top:24px;border-top:1px solid var(--nozle-portal-border,#e6e9e5)}
156
- .nozle-billing-portal button,.nozle-billing-portal .nzp-link{appearance:none;display:inline-flex;align-items:center;justify-content:center;font:inherit;font-weight:550;line-height:1.4;border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:7px;background:var(--nozle-portal-background,#fff);color:var(--nozle-portal-text,#202922);padding:9px 14px;cursor:pointer;text-decoration:none;white-space:normal}
157
- .nozle-billing-portal button:hover,.nozle-billing-portal .nzp-link:hover{background:var(--nozle-portal-muted-background,#f4f6f2)}
386
+ .nozle-billing-portal section{margin-top:28px;padding-top:24px;border-top:${line}}
387
+ .nozle-billing-portal button,.nozle-billing-portal .nzp-link{appearance:none;display:inline-flex;align-items:center;justify-content:center;font:${T.fontAction};border:var(--nozle-button-border-width, ${T.borderWidth}) solid ${T.input};border-radius:var(--nozle-button-radius, ${r(0.6)});background:${T.card};color:${T.foreground};padding:9px 14px;cursor:pointer;text-decoration:none;white-space:normal}
388
+ .nozle-billing-portal button:hover,.nozle-billing-portal .nzp-link:hover{background:${T.muted}}
158
389
  .nozle-billing-portal button:disabled{opacity:.55;cursor:wait}
159
- .nozle-billing-portal :focus-visible{outline:2px solid var(--nozle-portal-accent,#526949);outline-offset:3px}
160
- .nozle-billing-portal .nzp-primary{background:var(--nozle-portal-accent,#526949);border-color:var(--nozle-portal-accent,#526949);color:#fff}
161
- .nozle-billing-portal .nzp-primary:hover{filter:brightness(.92);background:var(--nozle-portal-accent,#526949)}
162
- .nozle-billing-portal .nzp-muted{color:var(--nozle-portal-muted,#687166);font-size:13px}
390
+ .nozle-billing-portal :focus-visible{outline:2px solid ${T.primary};outline-offset:3px}
391
+ .nozle-billing-portal .nzp-primary{background:${T.primary};border-color:var(--nozle-button-border, ${T.primary});box-shadow:var(--nozle-button-shadow, none);color:${T.primaryForeground}}
392
+ .nozle-billing-portal .nzp-primary:hover{filter:brightness(.92);background:${T.primary}}
393
+ .nozle-billing-portal .nzp-muted{color:${T.mutedForeground}}
394
+ .nozle-billing-portal .nzp-notice{color:${T.successText}}
163
395
  .nozle-billing-portal .nzp-row{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}
164
396
  .nozle-billing-portal .nzp-stack{display:grid;gap:12px;margin-top:16px}
165
- .nozle-billing-portal .nzp-card{border:1px solid var(--nozle-portal-border,#e0e6dc);border-radius:10px;padding:20px;min-width:0}
166
- .nozle-billing-portal .nzp-price{font-size:26px;letter-spacing:-.5px;margin:12px 0 4px}
167
- .nozle-billing-portal .nzp-badge{display:inline-block;font-size:11px;font-weight:600;letter-spacing:.3px;border-radius:5px;background:var(--nozle-portal-muted-background,#edf3e8);padding:3px 7px}
168
- .nozle-billing-portal .nzp-empty{border:1px dashed var(--nozle-portal-border,#dce2d9);border-radius:8px;padding:22px;text-align:center;color:var(--nozle-portal-muted,#687166);margin-top:16px}
169
- .nozle-billing-portal .nzp-error{color:#922e25;background:#fff4f2;border:1px solid #f2d2ce;border-radius:8px;padding:14px;margin:16px 0;display:flex;gap:12px;align-items:center;flex-wrap:wrap}
170
- .nozle-billing-portal .nzp-table{overflow-x:auto;margin-top:16px}
171
- .nozle-billing-portal table{width:100%;border-collapse:collapse;text-align:left;font-size:13px}
172
- .nozle-billing-portal th{font-weight:500;color:var(--nozle-portal-muted,#687166);background:var(--nozle-portal-muted-background,#f7f8f5)}
173
- .nozle-billing-portal th,.nozle-billing-portal td{padding:12px 10px;border-bottom:1px solid var(--nozle-portal-border,#e6e9e5);white-space:nowrap}
174
- .nozle-billing-portal label{display:grid;gap:6px;font-size:13px;font-weight:500;min-width:0}
175
- .nozle-billing-portal input,.nozle-billing-portal select{width:100%;min-width:0;font:inherit;color:inherit;background:var(--nozle-portal-background,#fff);border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:6px;padding:9px 10px}
397
+ .nozle-billing-portal .nzp-card{background:${T.card};border:${line};border-radius:${r(0.85)};box-shadow:${T.shadow};padding:20px;min-width:0}
398
+ .nozle-billing-portal .nzp-card-actions{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:16px;padding-top:16px;border-top:${line}}
399
+ .nozle-billing-portal .nzp-card-actions>.nozle-plan-change,.nozle-billing-portal .nzp-card-actions>.nozle-cancellation,.nozle-billing-portal .nzp-card-actions .nzpc-actions{display:contents}
400
+ .nozle-billing-portal .nzp-card-actions>.nozle-plan-change>:not(.nzpc-actions),.nozle-billing-portal .nzp-card-actions>.nozle-cancellation>:not(button){flex-basis:100%;margin:0}
401
+ .nozle-billing-portal .nzp-card-actions>.nozle-cancellation>button{margin-left:auto}
402
+ .nozle-billing-portal .nzp-price{font:${T.fontMetric};font-variant-numeric:tabular-nums;margin:12px 0 4px}
403
+ .nozle-billing-portal .nzp-price .nzp-muted{font:${T.fontMeta}}
404
+ .nozle-billing-portal .nzp-badge{display:inline-block;font:${T.labelFont};letter-spacing:${T.labelSpacing};text-transform:${T.labelTransform};border-radius:${r(0.4)};background:${T.accent};color:${T.accentForeground};padding:3px 7px}
405
+ .nozle-billing-portal .nzp-empty{border:${T.borderWidth} dashed ${T.border};border-radius:${r(0.65)};padding:22px;text-align:center;color:${T.mutedForeground};margin-top:16px}
406
+ .nozle-billing-portal .nzp-error{color:${T.dangerText};background:${T.dangerSoft};border:${T.borderWidth} solid ${T.dangerBorder};border-radius:${r(0.65)};padding:14px;margin:16px 0;display:flex;gap:12px;align-items:center;flex-wrap:wrap}
407
+ .nozle-billing-portal .nzp-table{overflow-x:auto;margin-top:16px;background:${T.card};border:${line};border-radius:${T.radius}}
408
+ .nozle-billing-portal table{width:100%;border-collapse:collapse;text-align:left;font:inherit}
409
+ .nozle-billing-portal th{height:44px;padding:0 20px;font:${T.labelFont};letter-spacing:${T.labelSpacing};text-transform:${T.labelTransform};color:${T.mutedForeground};background:${T.tableHeader};border-bottom:${line};white-space:nowrap}
410
+ .nozle-billing-portal td{height:48px;padding:14px 20px;border-bottom:${T.borderWidth} solid ${T.rowLine};white-space:nowrap}
411
+ .nozle-billing-portal tbody tr:last-child td{border-bottom:0}
412
+ .nozle-billing-portal .nzp-num{text-align:right}
413
+ .nozle-billing-portal td.nzp-num{font-family:${T.fontNumbers};font-variant-numeric:tabular-nums}
414
+ .nozle-billing-portal .nzp-search{display:flex;font-weight:400;align-items:center;gap:8px;flex:1;min-width:min(100%,200px);height:40px;padding:0 12px;background:${T.card};border:${T.borderWidth} solid ${T.input};border-radius:${r(0.7)};color:${T.mutedForeground}}
415
+ .nozle-billing-portal .nzp-search:focus-within{border-color:${T.foreground}}
416
+ .nozle-billing-portal .nzp-search input{flex:1;height:100%;border:0;padding:0;background:transparent;color:${T.foreground};outline:none}
417
+ .nozle-billing-portal .nzp-search input::placeholder{color:${T.mutedForeground}}
418
+ .nozle-billing-portal label{display:grid;gap:6px;font-weight:500;min-width:0}
419
+ .nozle-billing-portal input,.nozle-billing-portal select{width:100%;min-width:0;font:inherit;color:inherit;background:${T.card};border:${line};border-radius:${r(0.5)};padding:9px 10px}
176
420
  .nozle-billing-portal .nzp-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr));gap:16px;margin:20px 0}
177
421
  .nozle-billing-portal .nzp-actions{display:flex;align-items:flex-end;gap:10px;flex-wrap:wrap;margin-top:20px}
178
422
  .nozle-billing-portal dl{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,210px),1fr));gap:18px;margin:16px 0 0}
179
- .nozle-billing-portal dt{font-size:12px;color:var(--nozle-portal-muted,#687166)}
423
+ .nozle-billing-portal dt{font:${T.fontMeta};color:${T.mutedForeground}}
180
424
  .nozle-billing-portal dd{margin:3px 0 0;overflow-wrap:anywhere}
181
- .nozle-billing-portal footer{margin-top:30px;text-align:center;color:var(--nozle-portal-muted,#687166);font-size:12px}
425
+ .nozle-billing-portal footer{margin-top:30px;text-align:center;color:${T.mutedForeground};font:${T.fontMeta}}
182
426
  `;
183
427
 
184
428
  // src/components/portal/timezones.ts
@@ -362,6 +606,25 @@ function compareDecimalStrings(left, right) {
362
606
  }
363
607
  return magnitude * a.sign;
364
608
  }
609
+ function formatDecimalString(value, maximumFractionDigits = 12) {
610
+ const parsed = parseDecimal(value);
611
+ if (!parsed) return value;
612
+ const fractionLimit = Math.max(
613
+ 0,
614
+ Math.min(30, Math.trunc(maximumFractionDigits))
615
+ );
616
+ const firstGroupLength = parsed.integer.length % 3 || 3;
617
+ const groups = [parsed.integer.slice(0, firstGroupLength)];
618
+ for (let index = firstGroupLength; index < parsed.integer.length; index += 3) {
619
+ groups.push(parsed.integer.slice(index, index + 3));
620
+ }
621
+ const groupedInteger = groups.join(",");
622
+ const fraction = parsed.fraction.slice(0, fractionLimit);
623
+ return `${parsed.sign < 0 ? "-" : ""}${groupedInteger}${fraction ? `.${fraction}` : ""}`;
624
+ }
625
+ function creditUnitLabel(value, unitName) {
626
+ return compareDecimalStrings(value, "1") === 0 ? unitName : `${unitName}s`;
627
+ }
365
628
 
366
629
  // src/components/portal/amounts.ts
367
630
  function walletAmountCents(credits, rate, currencyDigits) {
@@ -382,16 +645,16 @@ function walletAmountCents(credits, rate, currencyDigits) {
382
645
  return `${padded.slice(0, -scale)}.${padded.slice(-scale)}`;
383
646
  }
384
647
  var styles = `
385
- .nozle-cancellation{font:inherit;color:var(--nozle-portal-text,#202922)}
386
- .nozle-cancellation button{font:inherit;padding:9px 14px;border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:7px;background:var(--nozle-portal-background,#fff);color:inherit;cursor:pointer}
648
+ .nozle-cancellation{font:${T.fontBody};color:${T.foreground}}
649
+ .nozle-cancellation button{font:${T.fontAction};padding:9px 14px;border:var(--nozle-button-border-width, ${T.borderWidth}) solid ${T.input};border-radius:var(--nozle-button-radius, calc(${T.radius} * 0.6));background:${T.card};color:inherit;cursor:pointer}
387
650
  .nozle-cancellation button:disabled{opacity:.55;cursor:wait}
388
- .nozle-cancellation :focus-visible{outline:2px solid var(--nozle-portal-accent,#526949);outline-offset:3px}
651
+ .nozle-cancellation :focus-visible{outline:2px solid ${T.primary};outline-offset:3px}
389
652
  .nozle-cancellation .nzc-backdrop{position:fixed;inset:0;background:#0006;z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px}
390
- .nozle-cancellation .nzc-dialog{box-sizing:border-box;background:var(--nozle-portal-background,#fff);border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:12px;padding:24px;width:100%;max-width:440px;max-height:90dvh;overflow:auto;box-shadow:0 12px 48px #0003}
391
- .nozle-cancellation .nzc-dialog h3{margin:0 0 16px;font-size:20px}
392
- .nozle-cancellation .nzc-dialog p{margin:12px 0;line-height:1.55}
653
+ .nozle-cancellation .nzc-dialog{box-sizing:border-box;background:${T.card};border:${T.borderWidth} solid ${T.border};border-radius:${T.radius};padding:24px;width:100%;max-width:440px;max-height:90dvh;overflow:auto;box-shadow:${T.shadow},0 12px 48px #0003}
654
+ .nozle-cancellation .nzc-dialog h3{margin:0 0 16px;font:${T.fontTitle};letter-spacing:normal;color:${T.foreground}}
655
+ .nozle-cancellation .nzc-dialog p{margin:12px 0}
393
656
  .nozle-cancellation .nzc-buttons{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;margin-top:20px}
394
- .nozle-cancellation [role=alert]{color:#922e25}
657
+ .nozle-cancellation [role=alert],.nozle-cancellation .nzc-danger{color:${T.dangerText}}
395
658
  `;
396
659
  function formatDate(value, locale, timezone) {
397
660
  const timeZone = timezone.startsWith("TZ_") ? portalTimezones[timezone] ?? "UTC" : timezone;
@@ -421,8 +684,10 @@ function CancellationFlow({
421
684
  onError,
422
685
  locale = "en-US",
423
686
  timezone = "UTC",
424
- nonce
687
+ nonce,
688
+ appearance
425
689
  }) {
690
+ const { root } = useAppearance(appearance);
426
691
  const [open, setOpen] = useState(false);
427
692
  const [busy, setBusy] = useState(false);
428
693
  const [preview, setPreview] = useState();
@@ -609,7 +874,7 @@ function CancellationFlow({
609
874
  );
610
875
  });
611
876
  }
612
- return /* @__PURE__ */ jsxs("div", { className: "nozle-cancellation", children: [
877
+ return /* @__PURE__ */ jsxs("div", { ...root("cancellationControl", void 0, "nozle-cancellation"), children: [
613
878
  /* @__PURE__ */ jsx("style", { nonce, children: styles }),
614
879
  notice && /* @__PURE__ */ jsx("p", { role: "status", children: notice }),
615
880
  active && !expired && /* @__PURE__ */ jsx("button", { ref: trigger, type: "button", onClick: () => void loadPreview(), children: ending ? "Keep my subscription" : "Cancel subscription" }),
@@ -694,6 +959,7 @@ function CancellationFlow({
694
959
  "button",
695
960
  {
696
961
  type: "button",
962
+ className: preview.operation === "cancel" ? "nzc-danger" : void 0,
697
963
  disabled: busy,
698
964
  onClick: () => void confirm(),
699
965
  children: request.current ? "Check status / retry" : preview.operation === "cancel" ? "Confirm cancellation" : "Confirm keep subscription"
@@ -705,66 +971,6 @@ function CancellationFlow({
705
971
  ) })
706
972
  ] });
707
973
  }
708
- function createClient(publishableKey, baseUrl) {
709
- let end = baseUrl.length;
710
- while (end > 0 && baseUrl[end - 1] === "/") end--;
711
- const base = baseUrl.slice(0, end);
712
- return {
713
- publishableKey,
714
- baseUrl: base,
715
- catalogFetch(path, init) {
716
- return fetch(`${base}${path}`, {
717
- ...init,
718
- headers: {
719
- Authorization: `Bearer ${publishableKey}`,
720
- "Content-Type": "application/json",
721
- ...init?.headers
722
- }
723
- });
724
- }
725
- };
726
- }
727
- var BillingContext = createContext(null);
728
- function BillingProvider({
729
- publishableKey,
730
- createCheckout,
731
- verifyCheckout,
732
- getCheckoutStatus,
733
- baseUrl = "https://api.nozle.app/engine",
734
- children
735
- }) {
736
- if (!publishableKey.startsWith("pk_")) {
737
- throw new Error(
738
- "BillingProvider publishableKey must be a publishable key (pk_)"
739
- );
740
- }
741
- const client = useMemo(
742
- () => createClient(publishableKey, baseUrl),
743
- [publishableKey, baseUrl]
744
- );
745
- const contextValue = useMemo(
746
- () => ({ client, createCheckout, verifyCheckout, getCheckoutStatus }),
747
- [client, createCheckout, verifyCheckout, getCheckoutStatus]
748
- );
749
- return React.createElement(
750
- BillingContext.Provider,
751
- { value: contextValue },
752
- children
753
- );
754
- }
755
- function useOptionalBillingContext() {
756
- return useContext(BillingContext);
757
- }
758
- function useBillingContext() {
759
- const context = useOptionalBillingContext();
760
- if (!context) {
761
- throw new Error("useBillingContext must be used within a BillingProvider");
762
- }
763
- return context;
764
- }
765
- function useNozleClient() {
766
- return useBillingContext().client;
767
- }
768
974
 
769
975
  // src/components/billing/razorpay-checkout.ts
770
976
  var loadingScript;
@@ -979,8 +1185,10 @@ function CheckoutButton({
979
1185
  onStripeClientSecret,
980
1186
  onSuccess,
981
1187
  onComplete,
982
- onScheduled
1188
+ onScheduled,
1189
+ appearance
983
1190
  }) {
1191
+ const { root, vars } = useAppearance(appearance);
984
1192
  const { createCheckout, verifyCheckout, getCheckoutStatus } = useBillingContext();
985
1193
  const attempt = useRef(void 0);
986
1194
  const inFlight = useRef(false);
@@ -1043,24 +1251,13 @@ function CheckoutButton({
1043
1251
  type: "button",
1044
1252
  onClick: () => void handleClick(),
1045
1253
  disabled: loading,
1046
- className,
1047
- style: {
1048
- padding: "0.75rem 1.5rem",
1049
- borderRadius: "var(--nozle-radius, 0.5rem)",
1050
- border: "none",
1051
- background: "var(--nozle-primary, var(--primary))",
1052
- color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1053
- cursor: loading ? "not-allowed" : "pointer",
1054
- fontWeight: 500,
1055
- opacity: loading ? 0.7 : 1,
1056
- ...style
1057
- },
1254
+ ...root("checkoutButton", { ...buttonStyle("primary", loading), ...style }, className),
1058
1255
  "aria-busy": loading,
1059
1256
  children: loading ? "Loading..." : label
1060
1257
  }
1061
1258
  ),
1062
- processing && /* @__PURE__ */ jsx("span", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
1063
- error && /* @__PURE__ */ jsx("span", { role: "alert", children: error.message })
1259
+ processing && /* @__PURE__ */ jsx("span", { role: "status", style: { ...vars, ...text.body }, children: "Payment is processing. Confirmation may take a little while." }),
1260
+ error && /* @__PURE__ */ jsx("span", { role: "alert", style: { ...vars, ...text.body, color: T.dangerText }, children: error.message })
1064
1261
  ] });
1065
1262
  }
1066
1263
  var CheckoutContext = createContext(null);
@@ -1071,23 +1268,62 @@ function useCheckout() {
1071
1268
  }
1072
1269
  return ctx;
1073
1270
  }
1074
- function buildStripeAppearance() {
1075
- if (typeof document === "undefined") {
1076
- return { theme: "stripe" };
1077
- }
1078
- const style = getComputedStyle(document.documentElement);
1079
- const get = (prop, fallback) => style.getPropertyValue(prop).trim() || fallback;
1080
- return {
1081
- theme: "stripe",
1082
- variables: {
1083
- colorPrimary: get("--nozle-primary", "#16a34a"),
1084
- colorBackground: get("--nozle-background", "#f8fafc"),
1085
- colorText: get("--nozle-foreground", "#1e293b"),
1086
- colorDanger: get("--nozle-destructive", "#dc2626"),
1087
- borderRadius: get("--nozle-radius", "0.5rem"),
1088
- fontFamily: "inherit"
1271
+ function rgb(color) {
1272
+ const match = color?.match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/);
1273
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
1274
+ }
1275
+ function stripeColor(color) {
1276
+ return rgb(color) ? color : color?.startsWith("#") ? color : void 0;
1277
+ }
1278
+ function toStripeAppearance(colors) {
1279
+ const background = rgb(colors.background);
1280
+ const dark = background ? (0.2126 * background[0] + 0.7152 * background[1] + 0.0722 * background[2]) / 255 < 0.5 : false;
1281
+ const variables = Object.fromEntries(
1282
+ Object.entries({
1283
+ colorPrimary: stripeColor(colors.primary),
1284
+ colorBackground: stripeColor(colors.background),
1285
+ colorText: stripeColor(colors.foreground),
1286
+ colorDanger: stripeColor(colors.danger),
1287
+ borderRadius: colors.radius,
1288
+ fontFamily: colors.font
1289
+ }).filter(([, value]) => value)
1290
+ );
1291
+ return { theme: dark ? "night" : "stripe", variables };
1292
+ }
1293
+ function useStripeAppearance(appearanceKey) {
1294
+ const probe = useRef(null);
1295
+ const [stripeAppearance, setStripeAppearance] = useState(null);
1296
+ useEffect(() => {
1297
+ if (!probe.current) return;
1298
+ const style = getComputedStyle(probe.current);
1299
+ setStripeAppearance(
1300
+ toStripeAppearance({
1301
+ primary: style.color,
1302
+ background: style.backgroundColor,
1303
+ foreground: style.borderTopColor,
1304
+ danger: style.borderBottomColor,
1305
+ radius: style.borderTopLeftRadius,
1306
+ font: style.fontFamily
1307
+ })
1308
+ );
1309
+ }, [appearanceKey]);
1310
+ const probeElement = /* @__PURE__ */ jsx(
1311
+ "span",
1312
+ {
1313
+ ref: probe,
1314
+ "aria-hidden": "true",
1315
+ style: {
1316
+ display: "none",
1317
+ color: T.primary,
1318
+ backgroundColor: T.card,
1319
+ borderTop: `1px solid ${T.foreground}`,
1320
+ borderBottom: `1px solid ${T.danger}`,
1321
+ borderRadius: T.radius,
1322
+ fontFamily: T.font
1323
+ }
1089
1324
  }
1090
- };
1325
+ );
1326
+ return { probeElement, stripeAppearance };
1091
1327
  }
1092
1328
  function CheckoutInner({
1093
1329
  submitLabel,
@@ -1097,10 +1333,12 @@ function CheckoutInner({
1097
1333
  className,
1098
1334
  style,
1099
1335
  returnUrl,
1100
- children
1336
+ children,
1337
+ appearance
1101
1338
  }) {
1102
1339
  const stripe = useStripe();
1103
1340
  const elements = useElements();
1341
+ const { el } = useAppearance(appearance);
1104
1342
  const [isProcessing, setIsProcessing] = useState(false);
1105
1343
  const [error, setError] = useState(null);
1106
1344
  const [isReady, setIsReady] = useState(false);
@@ -1134,7 +1372,7 @@ function CheckoutInner({
1134
1372
  setIsProcessing(false);
1135
1373
  }
1136
1374
  }
1137
- return /* @__PURE__ */ jsx(CheckoutContext.Provider, { value: { confirmPayment, isProcessing, error }, children: /* @__PURE__ */ jsxs("div", { className, style, children: [
1375
+ return /* @__PURE__ */ jsx(CheckoutContext.Provider, { value: { confirmPayment, isProcessing, error }, children: /* @__PURE__ */ jsxs("div", { children: [
1138
1376
  /* @__PURE__ */ jsx("style", { children: `@keyframes nozle-skeleton-pulse { 0%{background-position:200% 0} 100%{background-position:-200% 0} }` }),
1139
1377
  !isReady && /* @__PURE__ */ jsx(
1140
1378
  "div",
@@ -1142,8 +1380,8 @@ function CheckoutInner({
1142
1380
  "data-testid": "checkout-skeleton",
1143
1381
  style: {
1144
1382
  height: "200px",
1145
- borderRadius: "var(--nozle-radius, 0.5rem)",
1146
- background: "linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)",
1383
+ borderRadius: T.radius,
1384
+ background: `linear-gradient(90deg, ${T.muted} 25%, ${T.skeleton} 50%, ${T.muted} 75%)`,
1147
1385
  backgroundSize: "200% 100%",
1148
1386
  animation: "nozle-skeleton-pulse 1.5s ease-in-out infinite"
1149
1387
  }
@@ -1158,16 +1396,7 @@ function CheckoutInner({
1158
1396
  }
1159
1397
  }
1160
1398
  ),
1161
- error !== null && /* @__PURE__ */ jsx(
1162
- "p",
1163
- {
1164
- style: {
1165
- color: "var(--nozle-destructive, #dc2626)",
1166
- margin: "0.5rem 0 0"
1167
- },
1168
- children: error.message
1169
- }
1170
- ),
1399
+ error !== null && /* @__PURE__ */ jsx("p", { ...el("checkout__error", { color: T.dangerText, margin: "0.5rem 0 0" }), children: error.message }),
1171
1400
  children ?? /* @__PURE__ */ jsx(
1172
1401
  "button",
1173
1402
  {
@@ -1175,18 +1404,7 @@ function CheckoutInner({
1175
1404
  onClick: () => void confirmPayment(),
1176
1405
  disabled: isProcessing || !stripe,
1177
1406
  "aria-busy": isProcessing,
1178
- style: {
1179
- padding: "0.75rem 1.5rem",
1180
- borderRadius: "var(--nozle-radius, 0.5rem)",
1181
- border: "none",
1182
- background: "var(--nozle-primary, var(--primary))",
1183
- color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1184
- cursor: isProcessing ? "not-allowed" : "pointer",
1185
- fontWeight: 500,
1186
- opacity: isProcessing ? 0.7 : 1,
1187
- marginTop: "1rem",
1188
- width: "100%"
1189
- },
1407
+ ...el("checkout__submit", { ...buttonStyle("primary", isProcessing), marginTop: "1rem", width: "100%" }),
1190
1408
  children: isProcessing ? "Processing..." : submitLabel ?? "Pay now"
1191
1409
  }
1192
1410
  )
@@ -1204,19 +1422,21 @@ function StripeCheckout({
1204
1422
  onReady,
1205
1423
  className,
1206
1424
  style,
1207
- children
1425
+ children,
1426
+ appearance: themeAppearance
1208
1427
  }) {
1428
+ const { root } = useAppearance(themeAppearance);
1429
+ const { probeElement, stripeAppearance } = useStripeAppearance(JSON.stringify(root("checkout").style));
1209
1430
  const stripePromise = useMemo(
1210
1431
  () => loadStripe(publishableKey, stripeAccount ? { stripeAccount } : void 0),
1211
1432
  [publishableKey, stripeAccount]
1212
1433
  );
1213
- const appearance = useMemo(() => buildStripeAppearance(), []);
1214
1434
  const embeddedOptions = useMemo(
1215
1435
  () => ({ clientSecret, onComplete }),
1216
1436
  [clientSecret, onComplete]
1217
1437
  );
1218
1438
  if (clientSecret.startsWith("cs_")) {
1219
- return /* @__PURE__ */ jsx("div", { className, style, children: /* @__PURE__ */ jsx(
1439
+ return /* @__PURE__ */ jsx("div", { ...root("checkout", { ...text.body, ...style }, className), children: /* @__PURE__ */ jsx(
1220
1440
  EmbeddedCheckoutProvider,
1221
1441
  {
1222
1442
  stripe: stripePromise,
@@ -1225,23 +1445,26 @@ function StripeCheckout({
1225
1445
  }
1226
1446
  ) });
1227
1447
  }
1228
- const options = { clientSecret, appearance };
1229
- return /* @__PURE__ */ jsx(Elements, { stripe: stripePromise, options, children: /* @__PURE__ */ jsx(
1230
- CheckoutInner,
1231
- {
1232
- submitLabel,
1233
- onSuccess,
1234
- onError,
1235
- onReady,
1236
- className,
1237
- style,
1238
- returnUrl,
1239
- children
1240
- }
1241
- ) }, clientSecret);
1448
+ const options = stripeAppearance && { clientSecret, appearance: stripeAppearance };
1449
+ return /* @__PURE__ */ jsxs("div", { ...root("checkout", { ...text.body, ...style }, className), children: [
1450
+ probeElement,
1451
+ options && /* @__PURE__ */ jsx(Elements, { stripe: stripePromise, options, children: /* @__PURE__ */ jsx(
1452
+ CheckoutInner,
1453
+ {
1454
+ submitLabel,
1455
+ onSuccess,
1456
+ onError,
1457
+ onReady,
1458
+ returnUrl,
1459
+ children,
1460
+ appearance: themeAppearance
1461
+ }
1462
+ ) }, clientSecret)
1463
+ ] });
1242
1464
  }
1243
1465
  function ProviderCheckout(props) {
1244
1466
  const billing = useOptionalBillingContext();
1467
+ const { root, el } = useAppearance(props.appearance);
1245
1468
  const [busy, setBusy] = useState(false);
1246
1469
  const [error, setError] = useState(null);
1247
1470
  const [processing, setProcessing] = useState(false);
@@ -1291,17 +1514,19 @@ function ProviderCheckout(props) {
1291
1514
  submitLabel: props.submitLabel,
1292
1515
  onSuccess: props.onSuccess,
1293
1516
  onComplete: props.onComplete,
1294
- onError: props.onError
1517
+ onError: props.onError,
1518
+ className: props.className,
1519
+ appearance: props.appearance
1295
1520
  }
1296
1521
  );
1297
1522
  }
1298
- return /* @__PURE__ */ jsx("p", { role: "alert", children: "Payment setup is incomplete. Contact support to continue." });
1523
+ return /* @__PURE__ */ jsx("p", { ...root("checkout", { ...text.body, color: T.dangerText }, props.className), role: "alert", children: "Payment setup is incomplete. Contact support to continue." });
1299
1524
  }
1300
1525
  return /* @__PURE__ */ jsx(
1301
1526
  CheckoutContext.Provider,
1302
1527
  {
1303
1528
  value: { confirmPayment: pay, isProcessing: busy, error },
1304
- children: /* @__PURE__ */ jsxs("div", { className: props.className, children: [
1529
+ children: /* @__PURE__ */ jsxs("div", { ...root("checkout", text.body, props.className), children: [
1305
1530
  /* @__PURE__ */ jsx(
1306
1531
  "button",
1307
1532
  {
@@ -1309,28 +1534,30 @@ function ProviderCheckout(props) {
1309
1534
  disabled: busy,
1310
1535
  "aria-busy": busy,
1311
1536
  onClick: () => void pay(),
1537
+ ...el("checkout__submit", buttonStyle("primary", busy)),
1312
1538
  children: busy ? "Processing\u2026" : props.submitLabel || "Pay now"
1313
1539
  }
1314
1540
  ),
1315
1541
  processing && /* @__PURE__ */ jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
1316
- error && /* @__PURE__ */ jsx("p", { role: "alert", children: error.message })
1542
+ error && /* @__PURE__ */ jsx("p", { role: "alert", ...el("checkout__error", { color: T.dangerText }), children: error.message })
1317
1543
  ] })
1318
1544
  }
1319
1545
  );
1320
1546
  }
1321
1547
  function Checkout(props) {
1322
- return "checkout" in props ? /* @__PURE__ */ jsx(ProviderCheckout, { ...props }) : /* @__PURE__ */ jsx(StripeCheckout, { ...props });
1548
+ return /* @__PURE__ */ jsx(NozleBranding, { children: "checkout" in props ? /* @__PURE__ */ jsx(ProviderCheckout, { ...props }) : /* @__PURE__ */ jsx(StripeCheckout, { ...props }) });
1323
1549
  }
1324
1550
  var styles2 = `
1325
- .nozle-upgrade-modal{position:fixed;inset:0;background:#0006;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;color:var(--nozle-portal-text,#202922);font:inherit}
1326
- .nozle-upgrade-modal .nzu-dialog{box-sizing:border-box;background:var(--nozle-portal-background,#fff);border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:12px;padding:24px;max-width:480px;width:100%;max-height:90dvh;overflow:auto;box-shadow:0 12px 48px #0003}
1327
- .nozle-upgrade-modal h2{font-size:20px;margin:0 0 16px}
1328
- .nozle-upgrade-modal p{line-height:1.55}
1329
- .nozle-upgrade-modal button{font:inherit;padding:9px 14px;border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:7px;background:var(--nozle-portal-background,#fff);color:inherit;cursor:pointer}
1330
- .nozle-upgrade-modal button:disabled{opacity:.55;cursor:wait}
1331
- .nozle-upgrade-modal :focus-visible{outline:2px solid var(--nozle-portal-accent,#526949);outline-offset:3px}
1332
- .nozle-upgrade-modal .nzu-buttons{display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end;margin-top:20px}
1333
- .nozle-upgrade-modal [role=alert]{color:#922e25}
1551
+ .nozle-upgradeModal{position:fixed;inset:0;background:#0006;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;color:${T.foreground};font:${T.fontBody}}
1552
+ .nozle-upgradeModal .nozle-upgradeModal__dialog{box-sizing:border-box;background:${T.card};border:${T.borderWidth} solid ${T.border};border-radius:${T.radius};padding:24px;max-width:480px;width:100%;max-height:90dvh;overflow:auto;box-shadow:${T.shadow},0 12px 48px #0003}
1553
+ .nozle-upgradeModal h2{font:${T.fontTitle};letter-spacing:normal;color:${T.foreground};margin:0 0 16px}
1554
+ .nozle-upgradeModal button{font:${T.fontAction};padding:9px 14px;border:var(--nozle-button-border-width, ${T.borderWidth}) solid ${T.input};border-radius:var(--nozle-button-radius, calc(${T.radius} * 0.6));background:${T.card};color:inherit;cursor:pointer}
1555
+ .nozle-upgradeModal button.nzu-primary{background:${T.primary};border-color:var(--nozle-button-border, ${T.primary});box-shadow:var(--nozle-button-shadow, none);color:${T.primaryForeground}}
1556
+ .nozle-upgradeModal button:disabled{opacity:.55;cursor:wait}
1557
+ .nozle-upgradeModal :focus-visible{outline:2px solid ${T.primary};outline-offset:3px}
1558
+ .nozle-upgradeModal .nozle-upgradeModal__actions{display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end;margin-top:20px}
1559
+ .nozle-upgradeModal .nzu-back{margin-right:auto}
1560
+ .nozle-upgradeModal [role=alert]{color:${T.dangerText}}
1334
1561
  `;
1335
1562
  function UpgradeModal(props) {
1336
1563
  const billing = useOptionalBillingContext();
@@ -1341,6 +1568,7 @@ function UpgradeModal(props) {
1341
1568
  const dialog = useRef(null);
1342
1569
  const titleId = useId();
1343
1570
  const busy = Boolean(props.busy || confirming);
1571
+ const { root, el } = useAppearance(props.appearance);
1344
1572
  useEffect(() => {
1345
1573
  if (!props.isOpen) return;
1346
1574
  const previous = document.activeElement;
@@ -1400,17 +1628,17 @@ function UpgradeModal(props) {
1400
1628
  }
1401
1629
  }
1402
1630
  if (!props.isOpen) return null;
1403
- const money2 = (value) => new Intl.NumberFormat(props.locale ?? "en-US", {
1631
+ const money3 = (value) => new Intl.NumberFormat(props.locale ?? "en-US", {
1404
1632
  style: "currency",
1405
1633
  currency: props.preview?.currency ?? "USD"
1406
1634
  }).format(value);
1407
- return /* @__PURE__ */ jsxs("div", { className: "nozle-upgrade-modal", children: [
1635
+ return /* @__PURE__ */ jsxs("div", { ...root("upgradeModal", void 0, "nozle-upgrade-modal"), children: [
1408
1636
  /* @__PURE__ */ jsx("style", { nonce: props.nonce, children: styles2 }),
1409
1637
  /* @__PURE__ */ jsxs(
1410
1638
  "div",
1411
1639
  {
1412
1640
  ref: dialog,
1413
- className: "nzu-dialog",
1641
+ ...el("upgradeModal__dialog"),
1414
1642
  role: "dialog",
1415
1643
  "aria-modal": "true",
1416
1644
  "aria-labelledby": titleId,
@@ -1448,15 +1676,15 @@ function UpgradeModal(props) {
1448
1676
  props.preview && /* @__PURE__ */ jsxs("div", { children: [
1449
1677
  props.preview.credit > 0 && /* @__PURE__ */ jsxs("p", { children: [
1450
1678
  "Credits applied: ",
1451
- money2(-props.preview.credit)
1679
+ money3(-props.preview.credit)
1452
1680
  ] }),
1453
1681
  /* @__PURE__ */ jsxs("p", { children: [
1454
1682
  "New plan charge: ",
1455
- money2(props.preview.debit)
1683
+ money3(props.preview.debit)
1456
1684
  ] }),
1457
1685
  /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsxs("strong", { children: [
1458
1686
  "Due today: ",
1459
- money2(props.preview.net)
1687
+ money3(props.preview.net)
1460
1688
  ] }) }),
1461
1689
  /* @__PURE__ */ jsxs("p", { children: [
1462
1690
  "Next billing date:",
@@ -1468,18 +1696,21 @@ function UpgradeModal(props) {
1468
1696
  ] })
1469
1697
  ] }),
1470
1698
  props.children,
1471
- /* @__PURE__ */ jsxs("div", { className: "nzu-buttons", children: [
1699
+ /* @__PURE__ */ jsxs("div", { ...el("upgradeModal__actions"), children: [
1700
+ props.onBack && /* @__PURE__ */ jsx("button", { type: "button", className: "nzu-back", onClick: props.onBack, disabled: busy, children: "Back" }),
1472
1701
  /* @__PURE__ */ jsx("button", { type: "button", onClick: props.onCancel, disabled: busy, children: "Close" }),
1473
1702
  /* @__PURE__ */ jsx(
1474
1703
  "button",
1475
1704
  {
1476
1705
  type: "button",
1706
+ className: "nzu-primary",
1477
1707
  onClick: () => void handleConfirm(),
1478
1708
  disabled: busy || props.confirmDisabled,
1479
1709
  children: busy ? "Checking subscription\u2026" : props.confirmLabel ?? "Confirm"
1480
1710
  }
1481
1711
  )
1482
- ] })
1712
+ ] }),
1713
+ /* @__PURE__ */ jsx(NozleBranding, {})
1483
1714
  ]
1484
1715
  }
1485
1716
  )
@@ -1527,17 +1758,181 @@ function formatBillingDate(value, locale = "en-US", timezone) {
1527
1758
  }).format(new Date(value));
1528
1759
  }
1529
1760
  }
1761
+
1762
+ // src/components/pricing/plan-features.ts
1763
+ function number(value) {
1764
+ const parsed = Number(value);
1765
+ return Number.isFinite(parsed) ? parsed.toLocaleString("en-US", { maximumFractionDigits: 6 }) : String(value);
1766
+ }
1767
+ function money(amount, currency) {
1768
+ const parsed = Number(amount);
1769
+ if (!Number.isFinite(parsed)) return `${amount} ${currency}`;
1770
+ try {
1771
+ return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 6 }).format(parsed);
1772
+ } catch {
1773
+ return `${currency} ${number(amount)}`;
1774
+ }
1775
+ }
1776
+ function words(code) {
1777
+ return code.replace(/_/g, " ");
1778
+ }
1779
+ function entitlementRows(entitlement) {
1780
+ if (entitlement.feature_type === "config") {
1781
+ return (entitlement.privileges ?? []).flatMap((privilege) => {
1782
+ if (privilege.value === null || privilege.value === false) return [];
1783
+ if (privilege.value === true) return [`${entitlement.name}: ${privilege.name}`];
1784
+ return [`${entitlement.name}: ${privilege.value}`];
1785
+ });
1786
+ }
1787
+ if (entitlement.feature_type === "metered" && entitlement.usage_limit != null) {
1788
+ return [`${number(entitlement.usage_limit)} ${entitlement.name}`];
1789
+ }
1790
+ return [entitlement.name];
1791
+ }
1792
+ function creditGrantRow(rule) {
1793
+ const credits = `${number(rule.amount)} ${words(rule.credit_system_code)}`;
1794
+ if (rule.interval === "lifetime") return `${credits} (one time)`;
1795
+ const count = rule.interval_count ?? 1;
1796
+ return count === 1 ? `${credits} every ${rule.interval}` : `${credits} every ${count} ${rule.interval}s`;
1797
+ }
1798
+ function isZero(amount) {
1799
+ return amount === void 0 || Number(amount) === 0;
1800
+ }
1801
+ function rangeLabel(range, unit) {
1802
+ const from = number(range.from_value);
1803
+ return range.to_value === null ? `${from}+ ${unit}` : `${from} to ${number(range.to_value)} ${unit}`;
1804
+ }
1805
+ function rangeRows(ranges, unit, currency) {
1806
+ return ranges.map((range) => {
1807
+ const parts = [];
1808
+ if (!isZero(range.flat_amount)) parts.push(`${money(range.flat_amount, currency)} flat`);
1809
+ if (!isZero(range.per_unit_amount)) parts.push(`${money(range.per_unit_amount, currency)} each`);
1810
+ if (!isZero(range.rate)) parts.push(`${number(range.rate)}%`);
1811
+ return `${rangeLabel(range, unit)}: ${parts.length ? parts.join(" + ") : "free"}`;
1812
+ });
1813
+ }
1814
+ function chargeRows(charge, currency) {
1815
+ const properties = charge.properties ?? {};
1816
+ const unit = charge.feature_code ? words(charge.feature_code) : "unit";
1817
+ switch (charge.charge_model) {
1818
+ case "standard":
1819
+ return properties.amount === void 0 ? [] : [`${money(properties.amount, currency)} per ${unit}`];
1820
+ case "package": {
1821
+ if (properties.amount === void 0 || !properties.package_size) return [];
1822
+ const free = properties.free_units ? `, first ${number(properties.free_units)} free` : "";
1823
+ return [`${money(properties.amount, currency)} per ${number(properties.package_size)} ${unit}${free}`];
1824
+ }
1825
+ case "graduated":
1826
+ return rangeRows(properties.graduated_ranges ?? [], unit, currency);
1827
+ case "volume":
1828
+ return rangeRows(properties.volume_ranges ?? [], unit, currency);
1829
+ case "graduated_percentage":
1830
+ return rangeRows(properties.graduated_percentage_ranges ?? [], unit, currency);
1831
+ case "percentage": {
1832
+ if (properties.rate === void 0) return [];
1833
+ const fixed = isZero(properties.fixed_amount) ? "" : ` + ${money(properties.fixed_amount, currency)} per ${unit}`;
1834
+ return [`${number(properties.rate)}% of each ${unit}${fixed}`];
1835
+ }
1836
+ default:
1837
+ return [];
1838
+ }
1839
+ }
1840
+ function planFeatures(plan) {
1841
+ const rows = (plan.entitlements ?? []).flatMap(entitlementRows);
1842
+ rows.push(...(plan.credit_grant_rules ?? []).map(creditGrantRow));
1843
+ for (const charge of plan.charges ?? []) rows.push(...chargeRows(charge, plan.amount_currency));
1844
+ return rows;
1845
+ }
1846
+ var INTERVAL_LABELS = {
1847
+ weekly: "week",
1848
+ monthly: "month",
1849
+ quarterly: "quarter",
1850
+ semiannual: "6 months",
1851
+ yearly: "year"
1852
+ };
1853
+ function intervalLabel(interval) {
1854
+ return INTERVAL_LABELS[interval] ?? interval;
1855
+ }
1856
+ function formatPlanPrice(amountCents, currency) {
1857
+ const amount = amountCents / 100;
1858
+ try {
1859
+ return new Intl.NumberFormat("en-US", {
1860
+ style: "currency",
1861
+ currency,
1862
+ minimumFractionDigits: Number.isInteger(amount) ? 0 : 2,
1863
+ maximumFractionDigits: 2
1864
+ }).format(amount);
1865
+ } catch {
1866
+ return `${currency} ${amount}`;
1867
+ }
1868
+ }
1869
+ function entitlementValue(entitlement) {
1870
+ if (entitlement.feature_type === "config") {
1871
+ const values = (entitlement.privileges ?? []).filter((privilege) => privilege.value !== null && privilege.value !== false).map((privilege) => privilege.value === true ? privilege.name : String(privilege.value));
1872
+ return values.length ? values.join(", ") : false;
1873
+ }
1874
+ if (entitlement.feature_type === "metered" && entitlement.usage_limit != null) {
1875
+ return number(entitlement.usage_limit);
1876
+ }
1877
+ return true;
1878
+ }
1879
+ function comparisonRows(plans) {
1880
+ const rows = /* @__PURE__ */ new Map();
1881
+ const row = (key, label) => {
1882
+ let existing = rows.get(key);
1883
+ if (!existing) {
1884
+ existing = { key, label, values: {} };
1885
+ rows.set(key, existing);
1886
+ }
1887
+ return existing;
1888
+ };
1889
+ for (const plan of plans) {
1890
+ for (const entitlement of plan.entitlements ?? []) {
1891
+ row(`entitlement:${entitlement.code}`, entitlement.name).values[plan.code] = entitlementValue(entitlement);
1892
+ }
1893
+ for (const rule of plan.credit_grant_rules ?? []) {
1894
+ row(`credit:${rule.credit_system_code}`, words(rule.credit_system_code)).values[plan.code] = creditGrantRow(rule);
1895
+ }
1896
+ }
1897
+ const all = [...rows.values()];
1898
+ for (const item of all) {
1899
+ for (const plan of plans) item.values[plan.code] ??= false;
1900
+ }
1901
+ return all;
1902
+ }
1903
+ function sortPlansByPrice(plans) {
1904
+ const currencyRank = /* @__PURE__ */ new Map();
1905
+ for (const plan of plans) {
1906
+ if (!currencyRank.has(plan.amount_currency)) currencyRank.set(plan.amount_currency, currencyRank.size);
1907
+ }
1908
+ return plans.map((plan, index) => ({ plan, index })).sort(
1909
+ (a, b) => currencyRank.get(a.plan.amount_currency) - currencyRank.get(b.plan.amount_currency) || a.plan.amount_cents - b.plan.amount_cents || a.index - b.index
1910
+ ).map(({ plan }) => plan);
1911
+ }
1530
1912
  var styles3 = `
1531
- .nozle-plan-change{font:inherit;color:var(--nozle-portal-text,#202922);margin-top:12px}
1532
- .nozle-plan-change button{font:inherit;padding:9px 14px;border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:7px;background:var(--nozle-portal-background,#fff);color:inherit;cursor:pointer}
1913
+ .nozle-plan-change{font:${T.fontBody};color:${T.foreground};margin-top:12px}
1914
+ .nozle-plan-change button{font:${T.fontAction};padding:9px 14px;border:var(--nozle-button-border-width, ${T.borderWidth}) solid ${T.input};border-radius:var(--nozle-button-radius, calc(${T.radius} * 0.6));background:${T.card};color:inherit;cursor:pointer}
1533
1915
  .nozle-plan-change button:disabled{opacity:.55;cursor:wait}
1534
- .nozle-plan-change :focus-visible{outline:2px solid var(--nozle-portal-accent,#526949);outline-offset:3px}
1535
- .nozle-plan-change [role=alert]{color:#922e25}
1916
+ .nozle-plan-change :focus-visible{outline:2px solid ${T.primary};outline-offset:3px}
1917
+ .nozle-plan-change [role=alert]{color:${T.dangerText}}
1536
1918
  .nozle-plan-change .nzpc-actions{display:flex;gap:10px;flex-wrap:wrap}
1537
- .nozle-upgrade-modal select{box-sizing:border-box;width:100%;font:inherit;color:inherit;padding:10px;margin:8px 0;border:1px solid #dce2d9;border-radius:7px;background:var(--nozle-portal-background,#fff)}
1919
+ .nozle-upgrade-modal select{box-sizing:border-box;width:100%;font:inherit;color:inherit;padding:10px;margin:8px 0;border:${T.borderWidth} solid ${T.border};border-radius:calc(${T.radius} * 0.6);background:${T.card}}
1538
1920
  .nozle-upgrade-modal dl{display:block;margin:16px 0;padding:0}
1539
1921
  .nozle-upgrade-modal dl div{display:flex;justify-content:space-between;gap:16px;margin:12px 0}
1922
+ .nozle-upgrade-modal dt{font:${T.fontMeta};color:${T.mutedForeground}}
1540
1923
  .nozle-upgrade-modal dd{margin:0;text-align:right}
1924
+ .nozle-upgrade-modal label{font:${T.fontBody};font-weight:500}
1925
+ .nozle-plan-change .nzpc-plans{display:grid;gap:10px}
1926
+ .nozle-plan-change label.nzpc-plan{display:flex;gap:12px;align-items:flex-start;padding:14px 16px;border:${T.borderWidth} solid ${T.border};border-radius:calc(${T.radius} * 0.75);background:${T.card};cursor:pointer;font:${T.fontBody};color:${T.foreground}}
1927
+ .nozle-plan-change label.nzpc-plan[data-selected]{border-color:${T.primary};background:${T.primarySoft}}
1928
+ .nozle-plan-change label.nzpc-plan input{flex:none;width:auto;margin:4px 0 0;padding:0;accent-color:${T.primary}}
1929
+ .nozle-plan-change .nzpc-plan-body{display:grid;gap:4px;min-width:0;flex:1}
1930
+ .nozle-plan-change .nzpc-plan-head{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
1931
+ .nozle-plan-change .nzpc-plan-name{font:${T.fontHeading}}
1932
+ .nozle-plan-change .nzpc-plan-op{font:${T.labelFont};letter-spacing:${T.labelSpacing};text-transform:${T.labelTransform};color:${T.mutedForeground}}
1933
+ .nozle-plan-change .nzpc-plan-price{font-variant-numeric:tabular-nums}
1934
+ .nozle-plan-change .nzpc-plan-muted{font:${T.fontMeta};color:${T.mutedForeground};margin:0}
1935
+ .nozle-plan-change .nzpc-plan ul{list-style:none;margin:2px 0 0;padding:0;display:grid;gap:2px;font:${T.fontMeta};color:${T.mutedForeground}}
1541
1936
  `;
1542
1937
  var paymentPending = (state) => ["awaiting_payment", "processing", "needs_review"].includes(
1543
1938
  state.checkoutStatus
@@ -1564,8 +1959,10 @@ function PlanChangeFlow({
1564
1959
  returnUrl,
1565
1960
  locale = "en-US",
1566
1961
  timezone = "UTC",
1567
- nonce
1962
+ nonce,
1963
+ appearance
1568
1964
  }) {
1965
+ const { root } = useAppearance(appearance);
1569
1966
  const owner = useMemo(
1570
1967
  () => ({ controller: new AbortController() }),
1571
1968
  [subscriptionId, actions]
@@ -1580,6 +1977,11 @@ function PlanChangeFlow({
1580
1977
  null
1581
1978
  );
1582
1979
  const [target, setTarget] = useState("");
1980
+ const billing = useOptionalBillingContext();
1981
+ const catalog = useQuery(
1982
+ { ...catalogQuery(billing?.client), enabled: Boolean(billing) },
1983
+ billing?.queryClient ?? idleQueryClient
1984
+ );
1583
1985
  const [quote, setQuote] = useState();
1584
1986
  const [checkout, setCheckout] = useState(null);
1585
1987
  const [withdrawTarget, setWithdrawTarget] = useState();
@@ -1847,7 +2249,7 @@ function PlanChangeFlow({
1847
2249
  await reconcile(signal);
1848
2250
  });
1849
2251
  const selected = state?.eligiblePlans.find((plan) => plan.code === target);
1850
- const money2 = (amount) => formatMinorAmount(
2252
+ const money3 = (amount) => formatMinorAmount(
1851
2253
  amount,
1852
2254
  quote?.currency ?? state?.currentPlan.currency ?? "USD",
1853
2255
  locale
@@ -1855,7 +2257,7 @@ function PlanChangeFlow({
1855
2257
  const blocked = Boolean(
1856
2258
  state?.endingAt || state?.pendingChange || state && paymentPending(state) || state?.blockedReason || pending.current?.accepted
1857
2259
  );
1858
- return /* @__PURE__ */ jsxs("div", { className: "nozle-plan-change", children: [
2260
+ return /* @__PURE__ */ jsxs("div", { ...root("planChangeControl", void 0, "nozle-plan-change"), children: [
1859
2261
  /* @__PURE__ */ jsx("style", { nonce, children: styles3 }),
1860
2262
  !state ? /* @__PURE__ */ jsxs(Fragment, { children: [
1861
2263
  /* @__PURE__ */ jsx("p", { role: "status", children: "Loading plan changes\u2026" }),
@@ -1953,6 +2355,7 @@ function PlanChangeFlow({
1953
2355
  /* @__PURE__ */ jsxs(
1954
2356
  UpgradeModal,
1955
2357
  {
2358
+ appearance,
1956
2359
  isOpen: mode !== null,
1957
2360
  planCode: target,
1958
2361
  title: mode === "withdraw" ? "Keep your current plan?" : mode === "choose" ? "Change plan" : `Confirm ${quote?.operation ?? "plan change"}`,
@@ -1961,42 +2364,55 @@ function PlanChangeFlow({
1961
2364
  confirmDisabled: mode === "choose" && (!target || blocked) || mode === "confirm" && !quote,
1962
2365
  confirmLabel: pending.current?.attempted ? "Check status / retry" : mode === "choose" ? "Review change" : mode === "withdraw" ? "Confirm keep current plan" : quote?.operation === "downgrade" ? "Confirm downgrade" : "Confirm upgrade",
1963
2366
  onCancel: () => setMode(null),
2367
+ onBack: mode === "confirm" ? () => {
2368
+ setMode("choose");
2369
+ setQuote(void 0);
2370
+ setFailure(void 0);
2371
+ } : void 0,
1964
2372
  onSubmit: confirm,
1965
2373
  children: [
1966
2374
  failure && /* @__PURE__ */ jsx("p", { role: "alert", children: failure }),
1967
- mode === "choose" && /* @__PURE__ */ jsxs("label", { children: [
1968
- "Choose a plan",
1969
- /* @__PURE__ */ jsxs(
1970
- "select",
2375
+ mode === "choose" && /* @__PURE__ */ jsx("div", { className: "nzpc-plans", role: "radiogroup", "aria-label": "Choose a plan", children: state.eligiblePlans.map((plan) => {
2376
+ const details = catalog.data?.plans.find((entry) => entry.code === plan.code);
2377
+ const features = details ? planFeatures(details) : [];
2378
+ return /* @__PURE__ */ jsxs(
2379
+ "label",
1971
2380
  {
1972
- value: target,
1973
- onChange: (event) => {
1974
- setTarget(event.target.value);
1975
- setQuote(void 0);
1976
- },
1977
- disabled: busy,
2381
+ className: "nzpc-plan",
2382
+ "data-selected": target === plan.code ? "" : void 0,
1978
2383
  children: [
1979
- /* @__PURE__ */ jsx("option", { value: "", children: "Select a plan" }),
1980
- state.eligiblePlans.map((plan) => /* @__PURE__ */ jsxs("option", { value: plan.code, children: [
1981
- plan.name,
1982
- " \u2014",
1983
- " ",
1984
- formatMinorAmount(
1985
- plan.amountCents,
1986
- plan.currency,
1987
- locale
1988
- ),
1989
- " ",
1990
- "/ ",
1991
- plan.interval,
1992
- " (",
1993
- plan.operation,
1994
- ")"
1995
- ] }, plan.code))
2384
+ /* @__PURE__ */ jsx(
2385
+ "input",
2386
+ {
2387
+ type: "radio",
2388
+ name: `nozle-plan-${subscriptionId}`,
2389
+ value: plan.code,
2390
+ checked: target === plan.code,
2391
+ disabled: busy,
2392
+ onChange: () => {
2393
+ setTarget(plan.code);
2394
+ setQuote(void 0);
2395
+ }
2396
+ }
2397
+ ),
2398
+ /* @__PURE__ */ jsxs("span", { className: "nzpc-plan-body", children: [
2399
+ /* @__PURE__ */ jsxs("span", { className: "nzpc-plan-head", children: [
2400
+ /* @__PURE__ */ jsx("span", { className: "nzpc-plan-name", children: plan.name }),
2401
+ /* @__PURE__ */ jsx("span", { className: "nzpc-plan-op", children: plan.operation === "upgrade" ? "Upgrade" : "Downgrade" })
2402
+ ] }),
2403
+ /* @__PURE__ */ jsxs("span", { className: "nzpc-plan-price", children: [
2404
+ formatMinorAmount(plan.amountCents, plan.currency, locale),
2405
+ " / ",
2406
+ plan.interval
2407
+ ] }),
2408
+ details?.description && /* @__PURE__ */ jsx("p", { className: "nzpc-plan-muted", children: details.description }),
2409
+ features.length > 0 && /* @__PURE__ */ jsx("ul", { children: features.map((feature) => /* @__PURE__ */ jsx("li", { children: feature }, feature)) })
2410
+ ] })
1996
2411
  ]
1997
- }
1998
- )
1999
- ] }),
2412
+ },
2413
+ plan.code
2414
+ );
2415
+ }) }),
2000
2416
  mode === "withdraw" && /* @__PURE__ */ jsxs("p", { children: [
2001
2417
  "Remove the scheduled change to ",
2002
2418
  withdrawTarget?.name,
@@ -2025,19 +2441,19 @@ function PlanChangeFlow({
2025
2441
  /* @__PURE__ */ jsxs("dl", { children: [
2026
2442
  /* @__PURE__ */ jsxs("div", { children: [
2027
2443
  /* @__PURE__ */ jsx("dt", { children: "New plan charge" }),
2028
- /* @__PURE__ */ jsx("dd", { children: money2(quote.debitAmountCents) })
2444
+ /* @__PURE__ */ jsx("dd", { children: money3(quote.debitAmountCents) })
2029
2445
  ] }),
2030
2446
  /* @__PURE__ */ jsxs("div", { children: [
2031
2447
  /* @__PURE__ */ jsx("dt", { children: "Credits applied" }),
2032
- /* @__PURE__ */ jsx("dd", { children: money2(quote.creditAmountCents) })
2448
+ /* @__PURE__ */ jsx("dd", { children: money3(quote.creditAmountCents) })
2033
2449
  ] }),
2034
2450
  /* @__PURE__ */ jsxs("div", { children: [
2035
2451
  /* @__PURE__ */ jsx("dt", { children: "Due now" }),
2036
- /* @__PURE__ */ jsx("dd", { children: money2(quote.amountDueNowCents) })
2452
+ /* @__PURE__ */ jsx("dd", { children: money3(quote.amountDueNowCents) })
2037
2453
  ] }),
2038
2454
  /* @__PURE__ */ jsxs("div", { children: [
2039
2455
  /* @__PURE__ */ jsx("dt", { children: "Due at effective date" }),
2040
- /* @__PURE__ */ jsx("dd", { children: money2(quote.amountDueAtEffectiveCents) })
2456
+ /* @__PURE__ */ jsx("dd", { children: money3(quote.amountDueAtEffectiveCents) })
2041
2457
  ] })
2042
2458
  ] }),
2043
2459
  /* @__PURE__ */ jsxs("p", { children: [
@@ -2056,6 +2472,19 @@ function PlanChangeFlow({
2056
2472
  ] })
2057
2473
  ] });
2058
2474
  }
2475
+ var PortalAppearance = createContext({
2476
+ el: (key) => ({ className: "nozle-" + key })
2477
+ });
2478
+ function Part({
2479
+ as: Tag = "div",
2480
+ part,
2481
+ className,
2482
+ style,
2483
+ ...rest
2484
+ }) {
2485
+ const themed = useContext(PortalAppearance).el(part, style, className);
2486
+ return /* @__PURE__ */ jsx(Tag, { ...rest, ...themed });
2487
+ }
2059
2488
  function asError(error) {
2060
2489
  return error instanceof BillingPortalError ? error : new BillingPortalError("request");
2061
2490
  }
@@ -2067,8 +2496,10 @@ function BillingPortal({
2067
2496
  className = "",
2068
2497
  style,
2069
2498
  nonce,
2070
- onError
2499
+ onError,
2500
+ appearance
2071
2501
  }) {
2502
+ const { root, el } = useAppearance(appearance);
2072
2503
  const [attempt, setAttempt] = useState(0);
2073
2504
  const [state, setState] = useState();
2074
2505
  const errorCallback = useRef(onError);
@@ -2105,11 +2536,10 @@ function BillingPortal({
2105
2536
  }, [createSession, attempt]);
2106
2537
  const current = state?.source === createSession && state.attempt === attempt ? state : void 0;
2107
2538
  const reconnect = () => setAttempt((value) => value + 1);
2108
- return /* @__PURE__ */ jsxs(
2539
+ return /* @__PURE__ */ jsx(PortalAppearance.Provider, { value: { el, appearance }, children: /* @__PURE__ */ jsxs(
2109
2540
  "div",
2110
2541
  {
2111
- className: `nozle-billing-portal ${className}`,
2112
- style,
2542
+ ...root("billingPortal", style, `nozle-billing-portal ${className}`),
2113
2543
  "aria-label": "Nozle billing portal",
2114
2544
  children: [
2115
2545
  /* @__PURE__ */ jsx("style", { nonce, children: portalStyles }),
@@ -2128,9 +2558,9 @@ function BillingPortal({
2128
2558
  ) : current?.error ? /* @__PURE__ */ jsx(Failure, { error: current.error, retry: reconnect }) : /* @__PURE__ */ jsx(Loading, { label: "Starting billing session\u2026" })
2129
2559
  ]
2130
2560
  }
2131
- );
2561
+ ) });
2132
2562
  }
2133
- function useQuery(client, query, variables, onError, retainDuringRefresh = false) {
2563
+ function useQuery4(client, query, variables, onError, retainDuringRefresh = false) {
2134
2564
  const [attempt, setAttempt] = useState(0);
2135
2565
  const key = JSON.stringify(variables);
2136
2566
  const [state, setState] = useState();
@@ -2195,12 +2625,12 @@ function Failure({
2195
2625
  reconnect
2196
2626
  }) {
2197
2627
  const action = error.code === "unauthorized" ? reconnect ?? retry : retry;
2198
- return /* @__PURE__ */ jsxs("div", { role: "alert", className: "nzp-error", children: [
2628
+ return /* @__PURE__ */ jsxs(Part, { role: "alert", className: "nzp-error", part: "billingPortal__error", children: [
2199
2629
  /* @__PURE__ */ jsx("span", { children: error.message }),
2200
2630
  action && /* @__PURE__ */ jsx("button", { type: "button", onClick: action, children: error.code === "unauthorized" ? "Reconnect" : "Try again" })
2201
2631
  ] });
2202
2632
  }
2203
- function money(cents, currency, locale) {
2633
+ function money2(cents, currency, locale) {
2204
2634
  const formatter = new Intl.NumberFormat(locale, {
2205
2635
  style: "currency",
2206
2636
  currency
@@ -2227,7 +2657,7 @@ var intervals = {
2227
2657
  };
2228
2658
  function Portal(props) {
2229
2659
  const { client, locale, reconnect, onError } = props;
2230
- const { data, error, retry } = useQuery(
2660
+ const { data, error, retry } = useQuery4(
2231
2661
  client,
2232
2662
  identityQuery,
2233
2663
  {},
@@ -2240,7 +2670,7 @@ function Portal(props) {
2240
2670
  return /* @__PURE__ */ jsx(Failure, { error, retry, reconnect });
2241
2671
  if (!data) return /* @__PURE__ */ jsx(Loading, {});
2242
2672
  const customer = data.customerPortalUser;
2243
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2673
+ return /* @__PURE__ */ jsxs(NozleBranding, { show: data.customerPortalOrganization.nozleBranding === true, children: [
2244
2674
  /* @__PURE__ */ jsxs("header", { children: [
2245
2675
  /* @__PURE__ */ jsxs("div", { className: "nzp-row", children: [
2246
2676
  /* @__PURE__ */ jsx("span", { className: "nzp-muted", children: data.customerPortalOrganization.name }),
@@ -2249,7 +2679,7 @@ function Portal(props) {
2249
2679
  /* @__PURE__ */ jsx("h2", { children: "Billing portal" }),
2250
2680
  /* @__PURE__ */ jsx("p", { className: "nzp-muted", children: customer.name || [customer.firstname, customer.lastname].filter(Boolean).join(" ") || customer.email || "Your account" })
2251
2681
  ] }),
2252
- notice && /* @__PURE__ */ jsx("p", { role: "status", children: notice }),
2682
+ notice && /* @__PURE__ */ jsx("p", { className: "nzp-notice", role: "status", children: notice }),
2253
2683
  view.kind !== "overview" && /* @__PURE__ */ jsx("button", { type: "button", onClick: back, children: "\u2190 Back to overview" }),
2254
2684
  view.kind === "overview" && /* @__PURE__ */ jsxs(Fragment, { children: [
2255
2685
  /* @__PURE__ */ jsx(
@@ -2267,7 +2697,7 @@ function Portal(props) {
2267
2697
  onWallet: (wallet) => setView({ kind: "wallet", wallet })
2268
2698
  }
2269
2699
  ),
2270
- /* @__PURE__ */ jsxs("section", { "aria-label": "Billing information", children: [
2700
+ /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Billing information", children: [
2271
2701
  /* @__PURE__ */ jsxs("div", { className: "nzp-row", children: [
2272
2702
  /* @__PURE__ */ jsx("h3", { children: "Billing information" }),
2273
2703
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setView({ kind: "edit" }), children: "Edit billing information" })
@@ -2337,8 +2767,7 @@ function Portal(props) {
2337
2767
  back();
2338
2768
  }
2339
2769
  }
2340
- ),
2341
- /* @__PURE__ */ jsx("footer", { children: "Powered by Nozle" })
2770
+ )
2342
2771
  ] });
2343
2772
  }
2344
2773
  function Pager({
@@ -2385,17 +2814,20 @@ function Subscriptions({
2385
2814
  planChangeActions,
2386
2815
  nonce
2387
2816
  }) {
2817
+ const portalAppearance = useContext(PortalAppearance).appearance;
2388
2818
  const [page, setPage] = useState(1);
2389
2819
  const [updated, setUpdated] = useState({});
2390
- const { data, error, retry } = useQuery(client, subscriptionsQuery, { page }, onError, true);
2820
+ const { data, error, retry } = useQuery4(client, subscriptionsQuery, { page }, onError, true);
2391
2821
  const result = data?.customerPortalSubscriptions;
2392
2822
  useEffect(() => setUpdated({}), [result]);
2393
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Subscriptions", children: [
2823
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Subscriptions", children: [
2394
2824
  /* @__PURE__ */ jsx("h3", { children: "Subscriptions" }),
2395
2825
  error ? /* @__PURE__ */ jsx(Failure, { error, retry, reconnect }) : !result ? /* @__PURE__ */ jsx(Loading, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
2396
2826
  result.collection.length === 0 ? /* @__PURE__ */ jsx("p", { className: "nzp-empty", children: "No active subscriptions." }) : /* @__PURE__ */ jsx("div", { className: "nzp-stack", children: result.collection.map((item) => updated[item.id] ?? item).map((subscription) => /* @__PURE__ */ jsxs(
2397
- "article",
2827
+ Part,
2398
2828
  {
2829
+ as: "article",
2830
+ part: "billingPortal__card",
2399
2831
  className: "nzp-card",
2400
2832
  "data-subscription-id": subscription.externalId,
2401
2833
  children: [
@@ -2404,7 +2836,7 @@ function Subscriptions({
2404
2836
  /* @__PURE__ */ jsx("span", { className: "nzp-badge", children: subscription.endingAt ? "Scheduled to end" : "Active" })
2405
2837
  ] }),
2406
2838
  /* @__PURE__ */ jsxs("div", { className: "nzp-price", children: [
2407
- money(
2839
+ money2(
2408
2840
  subscription.plan.amountCents,
2409
2841
  subscription.plan.amountCurrency,
2410
2842
  locale
@@ -2416,25 +2848,15 @@ function Subscriptions({
2416
2848
  intervals[subscription.plan.interval] || subscription.plan.interval
2417
2849
  ] })
2418
2850
  ] }),
2419
- /* @__PURE__ */ jsxs("div", { className: "nzp-row", children: [
2420
- /* @__PURE__ */ jsxs("p", { className: "nzp-muted", children: [
2421
- "Base price, excluding tax and usage",
2422
- /* @__PURE__ */ jsx("br", {}),
2423
- "Current period ends",
2424
- " ",
2425
- date(
2426
- subscription.currentBillingPeriodEndingAt,
2427
- locale,
2428
- timezone
2429
- )
2430
- ] }),
2431
- /* @__PURE__ */ jsx(
2432
- "button",
2433
- {
2434
- type: "button",
2435
- onClick: () => onUsage(subscription),
2436
- children: "View usage"
2437
- }
2851
+ /* @__PURE__ */ jsxs("p", { className: "nzp-muted", children: [
2852
+ "Base price, excluding tax and usage",
2853
+ /* @__PURE__ */ jsx("br", {}),
2854
+ "Current period ends",
2855
+ " ",
2856
+ date(
2857
+ subscription.currentBillingPeriodEndingAt,
2858
+ locale,
2859
+ timezone
2438
2860
  )
2439
2861
  ] }),
2440
2862
  subscription.endingAt && /* @__PURE__ */ jsxs("p", { children: [
@@ -2447,48 +2869,60 @@ function Subscriptions({
2447
2869
  " ",
2448
2870
  subscription.nextSubscription.plan.name
2449
2871
  ] }),
2450
- cancellationActions && subscription.externalId && /* @__PURE__ */ jsx(
2451
- CancellationControl,
2452
- {
2453
- subscription: cancellationState(subscription),
2454
- actions: cancellationActions,
2455
- locale,
2456
- timezone,
2457
- nonce,
2458
- onError,
2459
- onChanged: retry,
2460
- refresh: async (signal) => {
2461
- const result2 = await client.request(
2462
- subscriptionQuery,
2463
- { id: subscription.id },
2464
- signal
2465
- );
2466
- if (!signal.aborted && result2.customerPortalSubscription) {
2467
- const fresh = result2.customerPortalSubscription;
2468
- setUpdated((previous) => ({
2469
- ...previous,
2470
- [fresh.id]: fresh
2471
- }));
2872
+ /* @__PURE__ */ jsxs("div", { className: "nzp-card-actions", children: [
2873
+ /* @__PURE__ */ jsx(
2874
+ "button",
2875
+ {
2876
+ type: "button",
2877
+ onClick: () => onUsage(subscription),
2878
+ children: "View usage"
2879
+ }
2880
+ ),
2881
+ planChangeActions && subscription.externalId && /* @__PURE__ */ jsx(
2882
+ PlanChangeControl,
2883
+ {
2884
+ subscriptionId: subscription.externalId,
2885
+ refreshKey: `${subscription.id}:${subscription.endingAt ?? ""}:${subscription.nextSubscription?.id ?? ""}:${subscription.plan.code}`,
2886
+ actions: planChangeActions,
2887
+ appearance: portalAppearance,
2888
+ locale,
2889
+ timezone,
2890
+ nonce,
2891
+ onError,
2892
+ onChanged: retry
2893
+ }
2894
+ ),
2895
+ cancellationActions && subscription.externalId && /* @__PURE__ */ jsx(
2896
+ CancellationControl,
2897
+ {
2898
+ subscription: cancellationState(subscription),
2899
+ actions: cancellationActions,
2900
+ appearance: portalAppearance,
2901
+ locale,
2902
+ timezone,
2903
+ nonce,
2904
+ onError,
2905
+ onChanged: retry,
2906
+ refresh: async (signal) => {
2907
+ const result2 = await client.request(
2908
+ subscriptionQuery,
2909
+ { id: subscription.id },
2910
+ signal
2911
+ );
2912
+ if (!signal.aborted && result2.customerPortalSubscription) {
2913
+ const fresh = result2.customerPortalSubscription;
2914
+ setUpdated((previous) => ({
2915
+ ...previous,
2916
+ [fresh.id]: fresh
2917
+ }));
2918
+ }
2919
+ return result2.customerPortalSubscription ? cancellationState(
2920
+ result2.customerPortalSubscription
2921
+ ) : null;
2472
2922
  }
2473
- return result2.customerPortalSubscription ? cancellationState(
2474
- result2.customerPortalSubscription
2475
- ) : null;
2476
2923
  }
2477
- }
2478
- ),
2479
- planChangeActions && subscription.externalId && /* @__PURE__ */ jsx(
2480
- PlanChangeControl,
2481
- {
2482
- subscriptionId: subscription.externalId,
2483
- refreshKey: `${subscription.id}:${subscription.endingAt ?? ""}:${subscription.nextSubscription?.id ?? ""}:${subscription.plan.code}`,
2484
- actions: planChangeActions,
2485
- locale,
2486
- timezone,
2487
- nonce,
2488
- onError,
2489
- onChanged: retry
2490
- }
2491
- )
2924
+ )
2925
+ ] })
2492
2926
  ]
2493
2927
  },
2494
2928
  subscription.externalId || subscription.id
@@ -2522,9 +2956,9 @@ function UsageDetails({
2522
2956
  subscription,
2523
2957
  timezone
2524
2958
  }) {
2525
- const { data, error, retry } = useQuery(client, usageQuery, { subscriptionId: subscription.id }, onError);
2959
+ const { data, error, retry } = useQuery4(client, usageQuery, { subscriptionId: subscription.id }, onError);
2526
2960
  const usage = data?.customerPortalCustomerUsage;
2527
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Subscription usage", children: [
2961
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Subscription usage", children: [
2528
2962
  /* @__PURE__ */ jsxs("h3", { children: [
2529
2963
  planName(subscription),
2530
2964
  " \xB7 Usage"
@@ -2536,18 +2970,18 @@ function UsageDetails({
2536
2970
  " ",
2537
2971
  date(usage.toDatetime, locale, timezone)
2538
2972
  ] }),
2539
- /* @__PURE__ */ jsx("p", { className: "nzp-price", children: money(usage.amountCents, usage.currency, locale) }),
2973
+ /* @__PURE__ */ jsx("p", { className: "nzp-price", children: money2(usage.amountCents, usage.currency, locale) }),
2540
2974
  /* @__PURE__ */ jsx("p", { className: "nzp-muted", children: "Current usage, excluding tax" }),
2541
- usage.chargesUsage.length ? /* @__PURE__ */ jsx("div", { className: "nzp-table", children: /* @__PURE__ */ jsxs("table", { children: [
2975
+ usage.chargesUsage.length ? /* @__PURE__ */ jsx(Part, { className: "nzp-table", part: "billingPortal__table", children: /* @__PURE__ */ jsxs("table", { children: [
2542
2976
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2543
2977
  /* @__PURE__ */ jsx("th", { children: "Feature" }),
2544
- /* @__PURE__ */ jsx("th", { children: "Units" }),
2545
- /* @__PURE__ */ jsx("th", { children: "Amount" })
2978
+ /* @__PURE__ */ jsx("th", { className: "nzp-num", children: "Units" }),
2979
+ /* @__PURE__ */ jsx("th", { className: "nzp-num", children: "Amount" })
2546
2980
  ] }) }),
2547
2981
  /* @__PURE__ */ jsx("tbody", { children: usage.chargesUsage.map((charge, index) => /* @__PURE__ */ jsxs("tr", { children: [
2548
2982
  /* @__PURE__ */ jsx("td", { children: charge.charge?.invoiceDisplayName || charge.feature?.name || charge.feature?.code || "Usage" }),
2549
- /* @__PURE__ */ jsx("td", { children: charge.units }),
2550
- /* @__PURE__ */ jsx("td", { children: money(charge.amountCents, usage.currency, locale) })
2983
+ /* @__PURE__ */ jsx("td", { className: "nzp-num", children: charge.units }),
2984
+ /* @__PURE__ */ jsx("td", { className: "nzp-num", children: money2(charge.amountCents, usage.currency, locale) })
2551
2985
  ] }, `${charge.id}-${index}`)) })
2552
2986
  ] }) }) : /* @__PURE__ */ jsx("p", { className: "nzp-empty", children: "No usage recorded for this billing period." })
2553
2987
  ] })
@@ -2557,25 +2991,31 @@ function Invoices(props) {
2557
2991
  const { client, locale, reconnect, onError } = props;
2558
2992
  const [search, setSearch] = useState("");
2559
2993
  const [filter, setFilter] = useState({ page: 1, searchTerm: "" });
2560
- const { data, error, retry } = useQuery(client, invoicesQuery, filter, onError);
2994
+ const { data, error, retry } = useQuery4(client, invoicesQuery, filter, onError);
2561
2995
  const result = data?.customerPortalInvoices;
2562
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Invoice history", children: [
2996
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Invoice history", children: [
2563
2997
  /* @__PURE__ */ jsx("h3", { children: "Invoice history" }),
2564
2998
  /* @__PURE__ */ jsxs(
2565
2999
  "form",
2566
3000
  {
2567
3001
  className: "nzp-actions",
3002
+ role: "search",
2568
3003
  onSubmit: (event) => {
2569
3004
  event.preventDefault();
2570
3005
  setFilter({ page: 1, searchTerm: search.trim() });
2571
3006
  },
2572
3007
  children: [
2573
- /* @__PURE__ */ jsxs("label", { children: [
2574
- "Search invoices",
3008
+ /* @__PURE__ */ jsxs("label", { className: "nzp-search", children: [
3009
+ /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.25", strokeLinecap: "round", "aria-hidden": "true", children: [
3010
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
3011
+ /* @__PURE__ */ jsx("path", { d: "m20 20-3.5-3.5" })
3012
+ ] }),
2575
3013
  /* @__PURE__ */ jsx(
2576
3014
  "input",
2577
3015
  {
2578
3016
  type: "search",
3017
+ "aria-label": "Search invoices",
3018
+ placeholder: "Search invoices",
2579
3019
  value: search,
2580
3020
  onChange: (event) => setSearch(event.target.value)
2581
3021
  }
@@ -2586,18 +3026,18 @@ function Invoices(props) {
2586
3026
  }
2587
3027
  ),
2588
3028
  error ? /* @__PURE__ */ jsx(Failure, { error, retry, reconnect }) : !result ? /* @__PURE__ */ jsx(Loading, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
2589
- !result.collection.length ? /* @__PURE__ */ jsx("p", { className: "nzp-empty", children: filter.searchTerm ? "No invoices match your search." : "No invoices yet." }) : /* @__PURE__ */ jsx("div", { className: "nzp-table", children: /* @__PURE__ */ jsxs("table", { children: [
3029
+ !result.collection.length ? /* @__PURE__ */ jsx("p", { className: "nzp-empty", children: filter.searchTerm ? "No invoices match your search." : "No invoices yet." }) : /* @__PURE__ */ jsx(Part, { className: "nzp-table", part: "billingPortal__table", children: /* @__PURE__ */ jsxs("table", { children: [
2590
3030
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2591
3031
  /* @__PURE__ */ jsx("th", { children: "Invoice" }),
2592
3032
  /* @__PURE__ */ jsx("th", { children: "Date" }),
2593
- /* @__PURE__ */ jsx("th", { children: "Amount" }),
3033
+ /* @__PURE__ */ jsx("th", { className: "nzp-num", children: "Amount" }),
2594
3034
  /* @__PURE__ */ jsx("th", { children: "Status" }),
2595
3035
  /* @__PURE__ */ jsx("th", { children: "PDF" })
2596
3036
  ] }) }),
2597
3037
  /* @__PURE__ */ jsx("tbody", { children: result.collection.map((invoice) => /* @__PURE__ */ jsxs("tr", { children: [
2598
3038
  /* @__PURE__ */ jsx("td", { children: invoice.number }),
2599
3039
  /* @__PURE__ */ jsx("td", { children: date(invoice.issuingDate, locale) }),
2600
- /* @__PURE__ */ jsx("td", { children: money(
3040
+ /* @__PURE__ */ jsx("td", { className: "nzp-num", children: money2(
2601
3041
  invoice.totalAmountCents,
2602
3042
  invoice.currency,
2603
3043
  locale
@@ -2701,7 +3141,7 @@ function CustomerForm({
2701
3141
  if (!signal.aborted) onSaved();
2702
3142
  });
2703
3143
  };
2704
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Edit billing information", children: [
3144
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Edit billing information", children: [
2705
3145
  /* @__PURE__ */ jsx("h3", { children: "Edit billing information" }),
2706
3146
  /* @__PURE__ */ jsxs("form", { onSubmit: submit, children: [
2707
3147
  /* @__PURE__ */ jsx("div", { className: "nzp-fields", children: customerFields.map(([key, label]) => /* @__PURE__ */ jsxs("label", { children: [
@@ -2743,15 +3183,15 @@ function Wallets({
2743
3183
  onWallet
2744
3184
  }) {
2745
3185
  const [page, setPage] = useState(1);
2746
- const { data, error, retry } = useQuery(client, walletsQuery, { page }, onError);
3186
+ const { data, error, retry } = useQuery4(client, walletsQuery, { page }, onError);
2747
3187
  const result = data?.customerPortalWallets;
2748
3188
  if (result && !result.collection.length) return null;
2749
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Wallets", children: [
3189
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Wallets", children: [
2750
3190
  /* @__PURE__ */ jsx("h3", { children: "Wallets" }),
2751
3191
  error ? /* @__PURE__ */ jsx(Failure, { error, retry, reconnect }) : !result ? /* @__PURE__ */ jsx(Loading, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
2752
- /* @__PURE__ */ jsx("div", { className: "nzp-stack", children: result.collection.map((wallet) => /* @__PURE__ */ jsxs("article", { className: "nzp-card", children: [
3192
+ /* @__PURE__ */ jsx("div", { className: "nzp-stack", children: result.collection.map((wallet) => /* @__PURE__ */ jsxs(Part, { as: "article", part: "billingPortal__card", className: "nzp-card", children: [
2753
3193
  /* @__PURE__ */ jsx("h3", { children: wallet.name || "Prepaid credits" }),
2754
- /* @__PURE__ */ jsx("div", { className: "nzp-price", children: money(wallet.balanceCents, wallet.currency, locale) }),
3194
+ /* @__PURE__ */ jsx("div", { className: "nzp-price", children: money2(wallet.balanceCents, wallet.currency, locale) }),
2755
3195
  /* @__PURE__ */ jsxs("div", { className: "nzp-row", children: [
2756
3196
  /* @__PURE__ */ jsxs("p", { className: "nzp-muted", children: [
2757
3197
  wallet.creditsBalance,
@@ -2796,7 +3236,7 @@ function WalletTopUp({
2796
3236
  amountCents,
2797
3237
  String(wallet.paidTopUpMaxAmountCents)
2798
3238
  ) <= 0);
2799
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Add credits", children: [
3239
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Add credits", children: [
2800
3240
  /* @__PURE__ */ jsxs("h3", { children: [
2801
3241
  "Add credits \xB7 ",
2802
3242
  wallet.name || "Wallet"
@@ -2833,19 +3273,19 @@ function WalletTopUp({
2833
3273
  )
2834
3274
  ] }) }),
2835
3275
  /* @__PURE__ */ jsxs("p", { children: [
2836
- amountCents !== null ? money(amountCents, wallet.currency, locale) : "\u2014",
3276
+ amountCents !== null ? money2(amountCents, wallet.currency, locale) : "\u2014",
2837
3277
  " ",
2838
3278
  "excluding tax"
2839
3279
  ] }),
2840
3280
  wallet.paidTopUpMinAmountCents != null && /* @__PURE__ */ jsxs("p", { className: "nzp-muted", children: [
2841
3281
  "Minimum",
2842
3282
  " ",
2843
- money(wallet.paidTopUpMinAmountCents, wallet.currency, locale)
3283
+ money2(wallet.paidTopUpMinAmountCents, wallet.currency, locale)
2844
3284
  ] }),
2845
3285
  wallet.paidTopUpMaxAmountCents != null && /* @__PURE__ */ jsxs("p", { className: "nzp-muted", children: [
2846
3286
  "Maximum",
2847
3287
  " ",
2848
- money(wallet.paidTopUpMaxAmountCents, wallet.currency, locale)
3288
+ money2(wallet.paidTopUpMaxAmountCents, wallet.currency, locale)
2849
3289
  ] }),
2850
3290
  action.error && /* @__PURE__ */ jsx(Failure, { error: action.error, reconnect }),
2851
3291
  /* @__PURE__ */ jsx("div", { className: "nzp-actions", children: /* @__PURE__ */ jsx(
@@ -2862,38 +3302,112 @@ function WalletTopUp({
2862
3302
  )
2863
3303
  ] });
2864
3304
  }
2865
- function usePlans() {
2866
- const { client } = useBillingContext();
2867
- const [plans, setPlans] = useState([]);
2868
- const [isLoading, setIsLoading] = useState(true);
2869
- const [error, setError] = useState(null);
2870
- useEffect(() => {
2871
- let cancelled = false;
2872
- setIsLoading(true);
2873
- setError(null);
2874
- setPlans([]);
2875
- if (!client) {
2876
- setIsLoading(false);
2877
- return;
2878
- }
2879
- void client.catalogFetch("/api/v1/plans").then(async (response) => {
2880
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
2881
- const data = await response.json();
2882
- if (!cancelled) setPlans(data.plans ?? []);
2883
- }).catch((cause) => {
2884
- if (!cancelled) {
2885
- setError(
2886
- cause instanceof Error ? cause : new Error("Failed to load plans")
2887
- );
3305
+ function useBilling() {
3306
+ const { billingFetch, billingEndpoint, queryClient } = useBillingContext();
3307
+ if (!billingFetch || !billingEndpoint) {
3308
+ throw new Error("Billing hooks need BillingProvider billingEndpoint, a backend route served by createBillingHandler");
3309
+ }
3310
+ return { fetchRoute: billingFetch, queryClient, endpoint: billingEndpoint };
3311
+ }
3312
+ function useSubscription() {
3313
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3314
+ return useQuery(
3315
+ { queryKey: ["nozle", endpoint, "subscription"], queryFn: () => fetchRoute("subscription") },
3316
+ queryClient
3317
+ );
3318
+ }
3319
+ function useEntitlement(featureCode, options = {}) {
3320
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3321
+ return useQuery(
3322
+ {
3323
+ queryKey: ["nozle", endpoint, "entitlement", featureCode],
3324
+ queryFn: () => fetchRoute("entitlement", { feature: featureCode }),
3325
+ enabled: Boolean(featureCode) && options.enabled !== false,
3326
+ staleTime: 1e4
3327
+ },
3328
+ queryClient
3329
+ );
3330
+ }
3331
+ function useCreditBalance(creditSystemCode) {
3332
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3333
+ return useQuery(
3334
+ {
3335
+ queryKey: ["nozle", endpoint, "credits"],
3336
+ queryFn: () => fetchRoute("credits"),
3337
+ select: (data) => {
3338
+ const balances = data.balances ?? [];
3339
+ if (creditSystemCode) return balances.find((b) => b.credit_system === creditSystemCode) ?? null;
3340
+ return balances.find((b) => Number(b.available) > 0) ?? balances[0] ?? null;
2888
3341
  }
2889
- }).finally(() => {
2890
- if (!cancelled) setIsLoading(false);
2891
- });
2892
- return () => {
2893
- cancelled = true;
2894
- };
2895
- }, [client]);
2896
- return { plans, isLoading, error };
3342
+ },
3343
+ queryClient
3344
+ );
3345
+ }
3346
+ function useCreditHistory(options = {}) {
3347
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3348
+ const query = useInfiniteQuery(
3349
+ {
3350
+ queryKey: ["nozle", endpoint, "credit-history", options.creditSystemCode, options.limit],
3351
+ queryFn: ({ pageParam }) => fetchRoute("credit-history", {
3352
+ credit_system: options.creditSystemCode,
3353
+ limit: options.limit,
3354
+ cursor: pageParam
3355
+ }),
3356
+ initialPageParam: void 0,
3357
+ getNextPageParam: (page) => page.next_cursor ?? void 0
3358
+ },
3359
+ queryClient
3360
+ );
3361
+ return { ...query, operations: query.data?.pages.flatMap((page) => page.operations) ?? [] };
3362
+ }
3363
+ function useUsage(options = {}) {
3364
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3365
+ const period = options.period ?? "current";
3366
+ return useQuery(
3367
+ {
3368
+ queryKey: ["nozle", endpoint, "usage", period, options.periods],
3369
+ queryFn: () => fetchRoute("usage", {
3370
+ period,
3371
+ periods: options.periods
3372
+ }),
3373
+ select: (data) => period === "past" ? data.periods ?? [] : data.usage ?? null
3374
+ },
3375
+ queryClient
3376
+ );
3377
+ }
3378
+ function useInvoices(options = {}) {
3379
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3380
+ const page = options.page ?? 1;
3381
+ return useQuery(
3382
+ {
3383
+ queryKey: ["nozle", endpoint, "invoices", page],
3384
+ queryFn: () => fetchRoute("invoices", { page })
3385
+ },
3386
+ queryClient
3387
+ );
3388
+ }
3389
+ function useInvoiceDownload() {
3390
+ const { fetchRoute } = useBilling();
3391
+ return useCallback(
3392
+ async (invoiceId) => (await fetchRoute("invoice-download", { id: invoiceId })).url,
3393
+ [fetchRoute]
3394
+ );
3395
+ }
3396
+ function useUpgradePreview(planCode) {
3397
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3398
+ return useQuery(
3399
+ {
3400
+ queryKey: ["nozle", endpoint, "preview", planCode],
3401
+ queryFn: () => fetchRoute("preview", { plan: planCode }),
3402
+ enabled: Boolean(planCode),
3403
+ staleTime: 0
3404
+ },
3405
+ queryClient
3406
+ );
3407
+ }
3408
+ function useBillingRefresh() {
3409
+ const { queryClient } = useBillingContext();
3410
+ return useCallback(() => queryClient.invalidateQueries({ queryKey: ["nozle"] }), [queryClient]);
2897
3411
  }
2898
3412
  function useCheckout2() {
2899
3413
  const { createCheckout } = useBillingContext();
@@ -2941,33 +3455,126 @@ function useCheckout2() {
2941
3455
  );
2942
3456
  return { fetchClientSecret, checkout, isLoading, error };
2943
3457
  }
3458
+ function PanelPlanCard({
3459
+ frame,
3460
+ el,
3461
+ name,
3462
+ description,
3463
+ price,
3464
+ interval,
3465
+ features,
3466
+ isCurrent,
3467
+ isHighlighted,
3468
+ filled: filledStyle,
3469
+ highlightLabel,
3470
+ children
3471
+ }) {
3472
+ const filled = isHighlighted && filledStyle;
3473
+ const text2 = filled ? T.highlightForeground : T.foreground;
3474
+ const muted5 = filled ? `color-mix(in srgb, ${T.highlightForeground} 75%, ${T.highlight})` : T.mutedForeground;
3475
+ const edge = isHighlighted ? T.highlight : isCurrent ? T.foreground : T.border;
3476
+ const label = isCurrent ? "Active" : isHighlighted ? highlightLabel : null;
3477
+ return /* @__PURE__ */ jsxs(
3478
+ "div",
3479
+ {
3480
+ ...frame({
3481
+ display: "flex",
3482
+ flexDirection: "column",
3483
+ overflow: "hidden",
3484
+ ...text.body,
3485
+ background: filled ? T.highlight : T.card,
3486
+ color: text2,
3487
+ border: border(edge),
3488
+ borderRadius: T.radius,
3489
+ boxShadow: T.shadow
3490
+ }),
3491
+ children: [
3492
+ /* @__PURE__ */ jsxs(
3493
+ "div",
3494
+ {
3495
+ ...el("planCard__header", {
3496
+ display: "flex",
3497
+ alignItems: "center",
3498
+ justifyContent: "space-between",
3499
+ gap: "0.75rem",
3500
+ padding: "0.875rem 1.25rem",
3501
+ borderBottom: border(filled ? `color-mix(in srgb, ${T.highlightForeground} 30%, ${T.highlight})` : T.border)
3502
+ }),
3503
+ children: [
3504
+ /* @__PURE__ */ jsx("h3", { ...el("planCard__name", { ...text.heading, color: text2, margin: 0 }), children: name }),
3505
+ label && /* @__PURE__ */ jsx(
3506
+ "span",
3507
+ {
3508
+ ...el("planCard__badge", {
3509
+ ...text.label,
3510
+ whiteSpace: "nowrap",
3511
+ // The current plan's label takes the colour of its button.
3512
+ color: filled ? text2 : isCurrent ? T.mutedForeground : T.highlight
3513
+ }),
3514
+ children: label
3515
+ }
3516
+ )
3517
+ ]
3518
+ }
3519
+ ),
3520
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "1rem", padding: "1.25rem", flex: 1 }, children: [
3521
+ /* @__PURE__ */ jsxs("div", { children: [
3522
+ /* @__PURE__ */ jsxs("p", { ...el("planCard__price", { ...text.metric, color: text2, margin: 0 }), children: [
3523
+ price,
3524
+ interval && /* @__PURE__ */ jsxs("span", { style: { ...text.meta, color: muted5 }, children: [
3525
+ " / ",
3526
+ interval
3527
+ ] })
3528
+ ] }),
3529
+ description && /* @__PURE__ */ jsx("p", { ...el("planCard__description", { ...text.secondary, color: muted5, margin: "0.25rem 0 0" }), children: description })
3530
+ ] }),
3531
+ /* @__PURE__ */ jsx(
3532
+ "ul",
3533
+ {
3534
+ ...el("planCard__features", {
3535
+ listStyle: "none",
3536
+ margin: 0,
3537
+ padding: 0,
3538
+ display: "flex",
3539
+ flexDirection: "column",
3540
+ gap: "0.375rem",
3541
+ flex: 1,
3542
+ ...text.secondary,
3543
+ color: muted5
3544
+ }),
3545
+ children: features.map((feature) => /* @__PURE__ */ jsx("li", { ...el("planCard__feature"), children: feature }, feature))
3546
+ }
3547
+ ),
3548
+ children
3549
+ ] })
3550
+ ]
3551
+ }
3552
+ );
3553
+ }
3554
+ function featureLines(override, plan, index) {
3555
+ const given = Array.isArray(override) ? override[index] : override?.[plan.code];
3556
+ return given ?? planFeatures(plan);
3557
+ }
2944
3558
  var SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5" };
2945
3559
  function fmtPrice(cents, cur) {
2946
3560
  const s = SYMBOLS[cur] ?? cur + " ";
2947
3561
  const val = cents % 100 === 0 ? String(cents / 100) : (cents / 100).toFixed(2);
2948
3562
  return `${s}${val}`;
2949
3563
  }
2950
- function isEnterprise(p) {
2951
- return /enterprise|custom/i.test(p.name + p.code);
2952
- }
2953
- function v(name, fb) {
2954
- return `var(${name}, ${fb})`;
2955
- }
2956
3564
  var C = {
2957
- bg: v("--nozle-pricing-bg", v("--nozle-background", "#ffffff")),
2958
- cardBg: v("--nozle-pricing-card-bg", v("--nozle-card", "#ffffff")),
2959
- hl: v("--nozle-pricing-highlight", v("--nozle-primary", "#6366f1")),
2960
- border: v("--nozle-pricing-border", v("--nozle-border", "#e5e7eb")),
2961
- radius: v("--nozle-pricing-radius", v("--nozle-radius", "12px")),
2962
- text: v("--nozle-foreground", "#111827"),
2963
- muted: v("--nozle-muted-foreground", "#6b7280"),
2964
- mutedBg: v("--nozle-muted", "#f3f4f6"),
2965
- hlFg: v("--nozle-primary-foreground", "#ffffff")
3565
+ cardBg: T.card,
3566
+ hl: T.primary,
3567
+ border: T.border,
3568
+ radius: T.radius,
3569
+ text: T.foreground,
3570
+ muted: T.mutedForeground,
3571
+ mutedBg: T.muted,
3572
+ hlFg: T.primaryForeground
2966
3573
  };
2967
3574
  function Check() {
2968
3575
  return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) });
2969
3576
  }
2970
- function Toggle({ isAnnual, onChange }) {
3577
+ function Toggle({ isAnnual, onChange, el }) {
2971
3578
  const pill = {
2972
3579
  display: "inline-flex",
2973
3580
  borderRadius: "9999px",
@@ -2980,24 +3587,15 @@ function Toggle({ isAnnual, onChange }) {
2980
3587
  borderRadius: "9999px",
2981
3588
  border: "none",
2982
3589
  background: active ? C.cardBg : "transparent",
3590
+ ...text.action,
2983
3591
  color: active ? C.text : C.muted,
2984
- fontWeight: active ? 600 : 400,
2985
- fontSize: "0.875rem",
2986
3592
  cursor: "pointer",
2987
- boxShadow: active ? "0 1px 3px rgba(0,0,0,0.08)" : "none",
3593
+ boxShadow: active ? T.shadow : "none",
2988
3594
  transition: "all 0.15s ease"
2989
3595
  });
2990
- return /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsxs("div", { style: pill, children: [
2991
- /* @__PURE__ */ jsx("button", { onClick: () => onChange(false), style: tab(!isAnnual), "aria-pressed": !isAnnual, children: "Monthly" }),
2992
- /* @__PURE__ */ jsxs("button", { onClick: () => onChange(true), style: tab(isAnnual), "aria-pressed": isAnnual, children: [
2993
- "Annual",
2994
- /* @__PURE__ */ jsx("span", { style: {
2995
- marginLeft: "6px",
2996
- fontSize: "0.7rem",
2997
- fontWeight: 600,
2998
- color: isAnnual ? C.hl : C.muted
2999
- }, children: "Save 20%" })
3000
- ] })
3596
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsxs("div", { ...el("pricingTable__toggle", pill), children: [
3597
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onChange(false), ...el("pricingTable__toggleButton", tab(!isAnnual)), "aria-pressed": !isAnnual, children: "Monthly" }),
3598
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onChange(true), ...el("pricingTable__toggleButton", tab(isAnnual)), "aria-pressed": isAnnual, children: "Annual" })
3001
3599
  ] }) });
3002
3600
  }
3003
3601
  function Card({
@@ -3005,13 +3603,22 @@ function Card({
3005
3603
  features,
3006
3604
  isCurrent,
3007
3605
  isHighlighted,
3606
+ filled: filledStyle,
3607
+ highlightLabel,
3008
3608
  isEnt,
3009
3609
  enterpriseEmail,
3010
- onSelect
3610
+ onSelect,
3611
+ el,
3612
+ panel,
3613
+ isUpgrade
3011
3614
  }) {
3012
3615
  const [hover, setHover] = useState(false);
3013
- const shadow = isHighlighted ? "0 8px 28px rgba(99,102,241,0.12), 0 2px 8px rgba(99,102,241,0.06)" : hover ? "0 8px 24px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.03)" : "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03)";
3014
- const interval = plan.interval === "yearly" ? "/year" : "/month";
3616
+ const filled = isHighlighted && filledStyle;
3617
+ const text2 = filled ? T.highlightForeground : C.text;
3618
+ const muted5 = filled ? `color-mix(in srgb, ${T.highlightForeground} 75%, ${T.highlight})` : C.muted;
3619
+ const accent = filled ? T.highlightForeground : C.hl;
3620
+ const badgeLabel = isCurrent ? "Current Plan" : highlightLabel;
3621
+ const interval = `/${intervalLabel(plan.interval)}`;
3015
3622
  const handleClick = () => {
3016
3623
  if (isCurrent) return;
3017
3624
  if (isEnt && enterpriseEmail) {
@@ -3020,75 +3627,97 @@ function Card({
3020
3627
  }
3021
3628
  onSelect?.();
3022
3629
  };
3630
+ const emphasised = isUpgrade ?? isHighlighted;
3631
+ const cta = onSelect && /* @__PURE__ */ jsx(
3632
+ CTAButton,
3633
+ {
3634
+ onClick: handleClick,
3635
+ disabled: isCurrent,
3636
+ variant: isCurrent ? "current" : isEnt ? "outline" : !emphasised ? "secondary" : filled ? "inverse" : isHighlighted ? "highlight" : "primary",
3637
+ el,
3638
+ children: isCurrent ? "Current Plan" : isEnt ? "Contact Sales" : `Get ${plan.name}`
3639
+ }
3640
+ );
3641
+ if (panel) {
3642
+ return /* @__PURE__ */ jsx(
3643
+ PanelPlanCard,
3644
+ {
3645
+ frame: (style) => el("planCard", style),
3646
+ el,
3647
+ name: plan.name,
3648
+ description: plan.description,
3649
+ price: isEnt ? "Custom" : fmtPrice(plan.amount_cents, plan.amount_currency),
3650
+ interval: isEnt ? void 0 : intervalLabel(plan.interval),
3651
+ features: features ?? [],
3652
+ isCurrent,
3653
+ isHighlighted,
3654
+ filled: filledStyle,
3655
+ highlightLabel,
3656
+ children: cta
3657
+ }
3658
+ );
3659
+ }
3023
3660
  return /* @__PURE__ */ jsxs(
3024
3661
  "div",
3025
3662
  {
3026
3663
  onMouseEnter: () => setHover(true),
3027
3664
  onMouseLeave: () => setHover(false),
3028
- style: {
3665
+ ...el("planCard", {
3029
3666
  position: "relative",
3030
3667
  display: "flex",
3031
3668
  flexDirection: "column",
3032
- background: C.cardBg,
3033
- border: `1px solid ${isHighlighted ? C.hl : C.border}`,
3034
- borderTopWidth: isHighlighted ? "3px" : "1px",
3669
+ background: filled ? T.highlight : C.cardBg,
3670
+ color: text2,
3671
+ border: border(isHighlighted ? T.highlight : C.border),
3672
+ borderTopWidth: isHighlighted && !filled ? `calc(${T.borderWidth} + 2px)` : T.borderWidth,
3035
3673
  borderRadius: C.radius,
3036
3674
  padding: "2rem",
3037
3675
  transition: "box-shadow 0.2s ease, transform 0.2s ease",
3038
- boxShadow: shadow,
3676
+ boxShadow: T.shadow,
3039
3677
  transform: hover && !isCurrent ? "translateY(-2px)" : "none"
3040
- },
3678
+ }),
3041
3679
  children: [
3042
- isHighlighted && /* @__PURE__ */ jsx("div", { style: {
3680
+ isHighlighted && badgeLabel && /* @__PURE__ */ jsx("div", { ...el("planCard__badge", {
3043
3681
  position: "absolute",
3044
3682
  top: "-1px",
3045
3683
  left: "50%",
3046
3684
  transform: "translate(-50%, -50%)",
3047
- background: C.hl,
3048
- color: C.hlFg,
3049
- fontSize: "0.7rem",
3050
- fontWeight: 700,
3685
+ background: filled ? T.highlightForeground : T.highlight,
3686
+ border: filled ? border(T.highlight) : void 0,
3687
+ ...text.label,
3688
+ color: filled ? T.highlight : T.highlightForeground,
3051
3689
  padding: "4px 12px",
3052
3690
  borderRadius: "9999px",
3053
- letterSpacing: "0.025em",
3054
- textTransform: "uppercase",
3055
3691
  whiteSpace: "nowrap"
3056
- }, children: isCurrent ? "Current Plan" : "Most Popular" }),
3057
- isCurrent && !isHighlighted && /* @__PURE__ */ jsx("div", { style: {
3692
+ }), children: badgeLabel }),
3693
+ isCurrent && !isHighlighted && /* @__PURE__ */ jsx("div", { ...el("planCard__badge", {
3058
3694
  position: "absolute",
3059
3695
  top: "0",
3060
3696
  left: "50%",
3061
3697
  transform: "translate(-50%, -50%)",
3062
3698
  background: C.mutedBg,
3063
- color: C.muted,
3064
- fontSize: "0.7rem",
3065
- fontWeight: 700,
3699
+ ...text.label,
3066
3700
  padding: "4px 12px",
3067
3701
  borderRadius: "9999px",
3068
- letterSpacing: "0.025em",
3069
- textTransform: "uppercase",
3070
3702
  whiteSpace: "nowrap"
3071
- }, children: "Current Plan" }),
3072
- /* @__PURE__ */ jsxs("div", { style: { marginBottom: "1.25rem", marginTop: isHighlighted || isCurrent ? "0.5rem" : 0 }, children: [
3073
- /* @__PURE__ */ jsx("h3", { style: {
3074
- fontSize: "1.25rem",
3075
- fontWeight: 700,
3076
- color: C.text,
3077
- margin: 0,
3078
- lineHeight: 1.3
3079
- }, children: plan.name }),
3080
- plan.description && /* @__PURE__ */ jsx("p", { style: {
3081
- fontSize: "0.875rem",
3082
- color: C.muted,
3083
- margin: "6px 0 0",
3084
- lineHeight: 1.5
3085
- }, children: plan.description })
3703
+ }), children: "Current Plan" }),
3704
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "1.25rem", marginTop: isHighlighted && badgeLabel || isCurrent ? "0.5rem" : 0 }, children: [
3705
+ /* @__PURE__ */ jsx("h3", { ...el("planCard__name", {
3706
+ ...text.heading,
3707
+ color: text2,
3708
+ margin: 0
3709
+ }), children: plan.name }),
3710
+ plan.description && /* @__PURE__ */ jsx("p", { ...el("planCard__description", {
3711
+ ...text.secondary,
3712
+ color: muted5,
3713
+ margin: "6px 0 0"
3714
+ }), children: plan.description })
3086
3715
  ] }),
3087
- /* @__PURE__ */ jsx("div", { style: { marginBottom: "1.5rem" }, children: isEnt ? /* @__PURE__ */ jsx("span", { style: { fontSize: "2.25rem", fontWeight: 700, color: C.text, lineHeight: 1 }, children: "Custom" }) : /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
3088
- /* @__PURE__ */ jsx("span", { style: { fontSize: "2.75rem", fontWeight: 700, color: C.text, lineHeight: 1, letterSpacing: "-0.025em" }, children: fmtPrice(plan.amount_cents, plan.amount_currency) }),
3089
- /* @__PURE__ */ jsx("span", { style: { fontSize: "0.9375rem", color: C.muted, marginLeft: "4px" }, children: interval })
3716
+ /* @__PURE__ */ jsx("div", { ...el("planCard__price", { marginBottom: "1.5rem" }), children: isEnt ? /* @__PURE__ */ jsx("span", { style: { ...text.metric, color: text2 }, children: "Custom" }) : /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
3717
+ /* @__PURE__ */ jsx("span", { style: { ...text.metric, color: text2 }, children: fmtPrice(plan.amount_cents, plan.amount_currency) }),
3718
+ /* @__PURE__ */ jsx("span", { style: { ...text.meta, color: muted5, marginLeft: "4px" }, children: interval })
3090
3719
  ] }) }),
3091
- features && features.length > 0 && /* @__PURE__ */ jsx("ul", { style: {
3720
+ features && features.length > 0 && /* @__PURE__ */ jsx("ul", { ...el("planCard__features", {
3092
3721
  listStyle: "none",
3093
3722
  padding: 0,
3094
3723
  margin: "0 0 2rem",
@@ -3096,27 +3725,18 @@ function Card({
3096
3725
  flexDirection: "column",
3097
3726
  gap: "0.75rem",
3098
3727
  flex: 1
3099
- }, children: features.map((feat) => /* @__PURE__ */ jsxs("li", { style: {
3728
+ }), children: features.map((feat) => /* @__PURE__ */ jsxs("li", { ...el("planCard__feature", {
3100
3729
  display: "flex",
3101
3730
  alignItems: "flex-start",
3102
3731
  gap: "10px",
3103
- fontSize: "0.875rem",
3104
- color: C.text,
3105
- lineHeight: 1.5
3106
- }, children: [
3107
- /* @__PURE__ */ jsx("span", { style: { color: C.hl, marginTop: "2px" }, children: /* @__PURE__ */ jsx(Check, {}) }),
3732
+ ...text.body,
3733
+ color: text2
3734
+ }), children: [
3735
+ /* @__PURE__ */ jsx("span", { style: { color: accent, marginTop: "2px" }, children: /* @__PURE__ */ jsx(Check, {}) }),
3108
3736
  /* @__PURE__ */ jsx("span", { children: feat })
3109
3737
  ] }, feat)) }),
3110
3738
  (!features || features.length === 0) && /* @__PURE__ */ jsx("div", { style: { flex: 1 } }),
3111
- onSelect && /* @__PURE__ */ jsx(
3112
- CTAButton,
3113
- {
3114
- onClick: handleClick,
3115
- disabled: isCurrent,
3116
- variant: isCurrent ? "current" : isEnt ? "outline" : isHighlighted ? "primary" : "secondary",
3117
- children: isCurrent ? "Current Plan" : isEnt ? "Contact Sales" : `Get ${plan.name}`
3118
- }
3119
- )
3739
+ cta
3120
3740
  ]
3121
3741
  }
3122
3742
  );
@@ -3125,75 +3745,53 @@ function CTAButton({
3125
3745
  children,
3126
3746
  onClick,
3127
3747
  disabled,
3128
- variant
3748
+ variant,
3749
+ el
3129
3750
  }) {
3130
3751
  const [hover, setHover] = useState(false);
3131
- const base = {
3132
- width: "100%",
3133
- padding: "12px 24px",
3134
- borderRadius: "10px",
3135
- fontSize: "0.9375rem",
3136
- fontWeight: 600,
3137
- cursor: disabled ? "default" : "pointer",
3138
- border: "none",
3139
- transition: "all 0.15s ease",
3140
- lineHeight: 1.4
3141
- };
3142
- const variants = {
3143
- primary: {
3144
- background: hover && !disabled ? "#4f46e5" : C.hl,
3145
- color: C.hlFg
3146
- },
3147
- secondary: {
3148
- background: hover && !disabled ? "#f0f0f3" : C.mutedBg,
3149
- color: C.text,
3150
- border: `1px solid ${C.border}`
3151
- },
3152
- outline: {
3153
- background: hover && !disabled ? "rgba(99,102,241,0.04)" : "transparent",
3154
- color: C.hl,
3155
- border: `1.5px solid ${C.hl}`
3156
- },
3157
- current: {
3158
- background: C.mutedBg,
3159
- color: C.muted
3160
- }
3161
- };
3752
+ const look = variant === "current" ? { ...buttonStyle("secondary"), background: C.mutedBg, color: C.muted, cursor: "default" } : variant === "highlight" ? { ...buttonStyle("primary"), background: T.highlight, color: T.highlightForeground, border: primaryButtonBorder(T.highlight) } : variant === "inverse" ? { ...buttonStyle("primary"), background: T.highlightForeground, color: T.highlight, border: primaryButtonBorder(T.highlightForeground) } : buttonStyle(variant);
3162
3753
  return /* @__PURE__ */ jsx(
3163
3754
  "button",
3164
3755
  {
3756
+ type: "button",
3165
3757
  onClick: disabled ? void 0 : onClick,
3166
3758
  disabled,
3167
3759
  onMouseEnter: () => setHover(true),
3168
3760
  onMouseLeave: () => setHover(false),
3169
- style: { ...base, ...variants[variant] },
3761
+ ...el("planCard__cta", {
3762
+ ...look,
3763
+ width: "100%",
3764
+ filter: hover && !disabled ? "brightness(0.95)" : void 0
3765
+ }),
3170
3766
  children
3171
3767
  }
3172
3768
  );
3173
3769
  }
3174
- function Skeleton() {
3175
- return /* @__PURE__ */ jsx("div", { style: {
3770
+ function Skeleton({ rootProps }) {
3771
+ return /* @__PURE__ */ jsx("div", { ...rootProps, "aria-busy": "true", style: {
3772
+ ...rootProps.style,
3176
3773
  display: "grid",
3177
3774
  gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
3178
3775
  gap: "1.5rem"
3179
3776
  }, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs("div", { style: {
3180
3777
  background: C.cardBg,
3181
- border: `1px solid ${C.border}`,
3778
+ border: border(),
3182
3779
  borderRadius: C.radius,
3183
3780
  padding: "2rem",
3184
3781
  height: "380px"
3185
3782
  }, children: [
3186
- /* @__PURE__ */ jsx("div", { style: { height: 20, width: "40%", borderRadius: 6, background: "#f0f0f0", marginBottom: 12 } }),
3187
- /* @__PURE__ */ jsx("div", { style: { height: 40, width: "50%", borderRadius: 6, background: "#f0f0f0", marginBottom: 24 } }),
3188
- /* @__PURE__ */ jsx("div", { style: { height: 14, width: "80%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
3189
- /* @__PURE__ */ jsx("div", { style: { height: 14, width: "70%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
3190
- /* @__PURE__ */ jsx("div", { style: { height: 14, width: "60%", borderRadius: 6, background: "#f0f0f0" } })
3783
+ /* @__PURE__ */ jsx("div", { style: { height: 20, width: "40%", borderRadius: 6, background: T.skeleton, marginBottom: 12 } }),
3784
+ /* @__PURE__ */ jsx("div", { style: { height: 40, width: "50%", borderRadius: 6, background: T.skeleton, marginBottom: 24 } }),
3785
+ /* @__PURE__ */ jsx("div", { style: { height: 14, width: "80%", borderRadius: 6, background: T.skeleton, marginBottom: 10 } }),
3786
+ /* @__PURE__ */ jsx("div", { style: { height: 14, width: "70%", borderRadius: 6, background: T.skeleton, marginBottom: 10 } }),
3787
+ /* @__PURE__ */ jsx("div", { style: { height: 14, width: "60%", borderRadius: 6, background: T.skeleton } })
3191
3788
  ] }, i)) });
3192
3789
  }
3193
3790
  function PricingTable({
3194
3791
  currentPlanCode,
3195
3792
  plans: propPlans,
3196
3793
  features,
3794
+ sort,
3197
3795
  onSelect,
3198
3796
  returnUrl,
3199
3797
  onStripeClientSecret,
@@ -3201,40 +3799,28 @@ function PricingTable({
3201
3799
  onCheckoutError,
3202
3800
  showToggle = true,
3203
3801
  highlightPlan,
3802
+ highlightStyle = "outline",
3803
+ highlightLabel = "Most Popular",
3204
3804
  className,
3205
- enterpriseEmail
3805
+ contactSalesPlans,
3806
+ enterpriseEmail,
3807
+ appearance
3206
3808
  }) {
3809
+ const { root, el, planCardLayout } = useAppearance(appearance);
3810
+ const isEnterprise = (p) => contactSalesPlans?.includes(p.code) ?? false;
3207
3811
  const billing = useOptionalBillingContext();
3208
3812
  const client = billing?.client;
3209
- const [plans, setPlans] = useState(propPlans ?? []);
3813
+ const catalog = useQuery(
3814
+ { ...catalogQuery(client), enabled: !propPlans && Boolean(client) },
3815
+ billing?.queryClient ?? idleQueryClient
3816
+ );
3817
+ const plans = propPlans ?? catalog.data?.plans ?? [];
3818
+ const loading = !propPlans && Boolean(client) && catalog.isPending;
3819
+ const loadError = propPlans ? null : !client ? "BillingProvider with a publishableKey is required to load plans" : catalog.error ? `Plans could not be loaded: ${catalog.error.message}` : null;
3210
3820
  const [currentPlan, setCurrentPlan] = useState(currentPlanCode ?? null);
3211
3821
  const [isAnnual, setIsAnnual] = useState(false);
3212
- const [loading, setLoading] = useState(!propPlans);
3213
3822
  const [processing, setProcessing] = useState(false);
3214
3823
  const [checkoutError, setCheckoutError] = useState(null);
3215
- useEffect(() => {
3216
- if (propPlans) {
3217
- setPlans(propPlans);
3218
- return;
3219
- }
3220
- if (!client) {
3221
- setLoading(false);
3222
- setCheckoutError("BillingProvider with a publishableKey is required to load plans");
3223
- return;
3224
- }
3225
- let cancelled = false;
3226
- client.catalogFetch("/api/v1/plans").then(async (res) => {
3227
- if (!cancelled && res.ok) {
3228
- const data = await res.json();
3229
- setPlans(data.plans || []);
3230
- }
3231
- }).finally(() => {
3232
- if (!cancelled) setLoading(false);
3233
- });
3234
- return () => {
3235
- cancelled = true;
3236
- };
3237
- }, [client, propPlans]);
3238
3824
  useEffect(() => {
3239
3825
  if (currentPlanCode !== void 0) setCurrentPlan(currentPlanCode);
3240
3826
  }, [currentPlanCode]);
@@ -3267,124 +3853,123 @@ function PricingTable({
3267
3853
  onCheckoutError?.(error);
3268
3854
  }
3269
3855
  }
3270
- const hasAnnual = plans.some((p) => p.interval === "yearly");
3271
- const filtered = showToggle && hasAnnual ? plans.filter((p) => p.interval === (isAnnual ? "yearly" : "monthly") || isEnterprise(p)) : plans;
3272
- if (loading) return /* @__PURE__ */ jsx(Skeleton, {});
3273
- return /* @__PURE__ */ jsxs("div", { className, style: {
3274
- fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
3275
- color: C.text,
3276
- background: C.bg
3277
- }, children: [
3278
- showToggle && hasAnnual && /* @__PURE__ */ jsx(Toggle, { isAnnual, onChange: setIsAnnual }),
3856
+ const ordered = sort ? [...plans].sort(sort) : [...sortPlansByPrice(plans.filter((p) => !isEnterprise(p))), ...plans.filter(isEnterprise)];
3857
+ const hasAnnual = ordered.some((p) => p.interval === "yearly");
3858
+ const current = plans.find((p) => p.code === currentPlan);
3859
+ const filtered = showToggle && hasAnnual ? ordered.filter((p) => p.interval === (isAnnual ? "yearly" : "monthly") || isEnterprise(p)) : ordered;
3860
+ const rootProps = root("pricingTable", text.body, className);
3861
+ if (loading) return /* @__PURE__ */ jsx(Skeleton, { rootProps });
3862
+ return /* @__PURE__ */ jsxs("div", { ...rootProps, children: [
3863
+ showToggle && hasAnnual && /* @__PURE__ */ jsx(Toggle, { isAnnual, onChange: setIsAnnual, el }),
3279
3864
  processing && /* @__PURE__ */ jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
3280
- checkoutError && /* @__PURE__ */ jsx("p", { role: "alert", children: checkoutError }),
3281
- /* @__PURE__ */ jsx("div", { style: {
3865
+ (checkoutError ?? loadError) && /* @__PURE__ */ jsx("p", { role: "alert", style: { color: T.dangerText }, children: checkoutError ?? loadError }),
3866
+ /* @__PURE__ */ jsx("div", { ...el("pricingTable__grid", {
3282
3867
  display: "grid",
3283
3868
  gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
3284
3869
  gap: "1.5rem"
3285
- }, children: filtered.map((plan, i) => /* @__PURE__ */ jsx(
3870
+ }), children: filtered.map((plan, i) => /* @__PURE__ */ jsx(
3286
3871
  Card,
3287
3872
  {
3288
3873
  plan,
3289
- features: features?.[i],
3874
+ features: featureLines(features, plan, i),
3290
3875
  isCurrent: currentPlan === plan.code,
3876
+ isUpgrade: current && current.amount_currency === plan.amount_currency ? plan.amount_cents > current.amount_cents : void 0,
3291
3877
  isHighlighted: highlightPlan === plan.code,
3878
+ filled: highlightStyle === "filled",
3879
+ highlightLabel,
3292
3880
  isEnt: isEnterprise(plan),
3293
3881
  enterpriseEmail,
3294
- onSelect: () => void selectPlan(plan)
3882
+ onSelect: () => void selectPlan(plan),
3883
+ el,
3884
+ panel: planCardLayout === "panel"
3295
3885
  },
3296
3886
  plan.code
3297
- )) })
3887
+ )) }),
3888
+ /* @__PURE__ */ jsx(NozleBranding, {})
3298
3889
  ] });
3299
3890
  }
3300
3891
  function PlanCard({
3301
- id,
3302
- name,
3303
- monthlyPrice,
3304
- annualPrice,
3305
- features,
3306
- description,
3307
- isAnnual,
3892
+ plan,
3893
+ features: featureOverride,
3308
3894
  isCurrent,
3309
3895
  returnUrl,
3310
- onSelect
3896
+ onSelect,
3897
+ appearance
3311
3898
  }) {
3312
- const price = isAnnual ? annualPrice : monthlyPrice;
3313
- const interval = isAnnual ? "year" : "month";
3899
+ const { root, el, planCardLayout } = useAppearance(appearance);
3900
+ const { code: id, name, description } = plan;
3901
+ const price = formatPlanPrice(plan.amount_cents, plan.amount_currency);
3902
+ const interval = intervalLabel(plan.interval);
3903
+ const features = featureOverride ?? planFeatures(plan);
3904
+ const cta = onSelect && /* @__PURE__ */ jsx(
3905
+ "button",
3906
+ {
3907
+ type: "button",
3908
+ onClick: () => onSelect?.({
3909
+ planCode: id,
3910
+ returnUrl: returnUrl ?? window.location.href
3911
+ }),
3912
+ ...el("planCard__cta", {
3913
+ ...buttonStyle(isCurrent ? "secondary" : "primary"),
3914
+ ...isCurrent && { background: T.muted, color: T.mutedForeground, cursor: "default" },
3915
+ width: "100%"
3916
+ }),
3917
+ disabled: isCurrent,
3918
+ "aria-label": isCurrent ? `Current plan: ${name}` : `Select ${name}`,
3919
+ children: isCurrent ? "Current Plan" : "Get Started"
3920
+ }
3921
+ );
3922
+ if (planCardLayout === "panel") {
3923
+ return /* @__PURE__ */ jsx(
3924
+ PanelPlanCard,
3925
+ {
3926
+ frame: (style) => ({ "data-testid": `plan-card-${id}`, "aria-current": isCurrent ? "true" : void 0, ...root("planCard", style) }),
3927
+ el,
3928
+ name,
3929
+ description,
3930
+ price,
3931
+ interval,
3932
+ features,
3933
+ isCurrent: Boolean(isCurrent),
3934
+ isHighlighted: false,
3935
+ filled: false,
3936
+ children: cta
3937
+ }
3938
+ );
3939
+ }
3314
3940
  return /* @__PURE__ */ jsxs(
3315
3941
  "div",
3316
3942
  {
3317
3943
  "data-testid": `plan-card-${id}`,
3318
3944
  "aria-current": isCurrent ? "true" : void 0,
3319
- style: {
3320
- background: "var(--nozle-background, var(--background))",
3321
- border: isCurrent ? "2px solid var(--nozle-primary, var(--primary))" : "1px solid var(--nozle-border, var(--border))",
3322
- borderRadius: "var(--nozle-radius, 0.5rem)",
3945
+ ...root("planCard", {
3946
+ ...text.body,
3947
+ background: T.card,
3948
+ border: border(isCurrent ? T.primary : T.border),
3949
+ outline: isCurrent ? `1px solid ${T.primary}` : void 0,
3950
+ borderRadius: T.radius,
3951
+ boxShadow: T.shadow,
3323
3952
  padding: "1.5rem",
3324
3953
  display: "flex",
3325
3954
  flexDirection: "column",
3326
3955
  gap: "1rem"
3327
- },
3328
- className: isCurrent ? "border-primary" : "",
3956
+ }),
3329
3957
  children: [
3330
3958
  /* @__PURE__ */ jsxs("div", { children: [
3331
- /* @__PURE__ */ jsx(
3332
- "h3",
3333
- {
3334
- style: {
3335
- fontSize: "1.25rem",
3336
- fontWeight: 600,
3337
- color: "var(--nozle-foreground, var(--foreground))",
3338
- margin: 0
3339
- },
3340
- children: name
3341
- }
3342
- ),
3343
- description && /* @__PURE__ */ jsx(
3344
- "p",
3345
- {
3346
- style: {
3347
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
3348
- fontSize: "0.875rem",
3349
- margin: "0.25rem 0 0"
3350
- },
3351
- children: description
3352
- }
3353
- )
3959
+ /* @__PURE__ */ jsx("h3", { ...el("planCard__name", { ...text.heading, margin: 0 }), children: name }),
3960
+ description && /* @__PURE__ */ jsx("p", { ...el("planCard__description", { ...text.secondary, margin: "0.25rem 0 0" }), children: description })
3354
3961
  ] }),
3355
- /* @__PURE__ */ jsxs("div", { children: [
3356
- /* @__PURE__ */ jsxs(
3357
- "span",
3358
- {
3359
- style: {
3360
- fontSize: "2rem",
3361
- fontWeight: 700,
3362
- color: "var(--nozle-foreground, var(--foreground))"
3363
- },
3364
- children: [
3365
- "$",
3366
- price
3367
- ]
3368
- }
3369
- ),
3370
- /* @__PURE__ */ jsxs(
3371
- "span",
3372
- {
3373
- style: {
3374
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
3375
- marginLeft: "0.25rem"
3376
- },
3377
- children: [
3378
- "/",
3379
- interval
3380
- ]
3381
- }
3382
- )
3962
+ /* @__PURE__ */ jsxs("div", { ...el("planCard__price"), children: [
3963
+ /* @__PURE__ */ jsx("span", { style: text.metric, children: price }),
3964
+ /* @__PURE__ */ jsxs("span", { style: { ...text.meta, marginLeft: "0.25rem" }, children: [
3965
+ "/",
3966
+ interval
3967
+ ] })
3383
3968
  ] }),
3384
3969
  /* @__PURE__ */ jsx(
3385
3970
  "ul",
3386
3971
  {
3387
- style: {
3972
+ ...el("planCard__features", {
3388
3973
  listStyle: "none",
3389
3974
  padding: 0,
3390
3975
  margin: 0,
@@ -3392,160 +3977,51 @@ function PlanCard({
3392
3977
  flexDirection: "column",
3393
3978
  gap: "0.5rem",
3394
3979
  flex: 1
3395
- },
3396
- children: features.map((feature) => /* @__PURE__ */ jsxs(
3397
- "li",
3398
- {
3399
- style: {
3400
- display: "flex",
3401
- alignItems: "center",
3402
- gap: "0.5rem",
3403
- color: "var(--nozle-foreground, var(--foreground))",
3404
- fontSize: "0.875rem"
3405
- },
3406
- children: [
3407
- /* @__PURE__ */ jsx(
3408
- "span",
3409
- {
3410
- style: { color: "var(--nozle-primary, var(--primary))" },
3411
- "aria-hidden": "true",
3412
- children: "\u2713"
3413
- }
3414
- ),
3415
- feature
3416
- ]
3417
- },
3980
+ }),
3981
+ children: features.map((feature) => /* @__PURE__ */ jsxs("li", { ...el("planCard__feature", { display: "flex", alignItems: "center", gap: "0.5rem", ...text.body }), children: [
3982
+ /* @__PURE__ */ jsx("span", { style: { color: T.primary }, "aria-hidden": "true", children: "\u2713" }),
3418
3983
  feature
3419
- ))
3984
+ ] }, feature))
3420
3985
  }
3421
3986
  ),
3422
- onSelect && /* @__PURE__ */ jsx(
3423
- "button",
3424
- {
3425
- onClick: () => onSelect?.({
3426
- planCode: id,
3427
- returnUrl: returnUrl ?? window.location.href
3428
- }),
3429
- style: {
3430
- background: isCurrent ? "var(--nozle-muted, var(--muted))" : "var(--nozle-primary, var(--primary))",
3431
- color: isCurrent ? "var(--nozle-muted-foreground, var(--muted-foreground))" : "var(--nozle-primary-foreground, var(--primary-foreground))",
3432
- border: "none",
3433
- borderRadius: "var(--nozle-radius, 0.5rem)",
3434
- padding: "0.75rem 1.5rem",
3435
- cursor: isCurrent ? "default" : "pointer",
3436
- fontWeight: 500,
3437
- width: "100%"
3438
- },
3439
- disabled: isCurrent,
3440
- "aria-label": isCurrent ? `Current plan: ${name}` : `Select ${name}`,
3441
- children: isCurrent ? "Current Plan" : "Get Started"
3442
- }
3443
- )
3987
+ cta
3444
3988
  ]
3445
3989
  }
3446
3990
  );
3447
3991
  }
3448
3992
  function PlanComparison({
3449
- plans,
3450
- features
3993
+ plans: catalogPlans,
3994
+ features: featureOverride,
3995
+ sort,
3996
+ appearance
3451
3997
  }) {
3452
- return /* @__PURE__ */ jsx(
3453
- "div",
3454
- {
3455
- style: {
3456
- overflowX: "auto",
3457
- background: "var(--nozle-background, var(--background))"
3458
- },
3459
- children: /* @__PURE__ */ jsxs(
3460
- "table",
3461
- {
3462
- style: {
3463
- width: "100%",
3464
- borderCollapse: "collapse",
3465
- fontSize: "0.875rem",
3466
- color: "var(--nozle-foreground, var(--foreground))"
3467
- },
3468
- children: [
3469
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
3470
- /* @__PURE__ */ jsx(
3471
- "th",
3472
- {
3473
- style: {
3474
- padding: "0.75rem 1rem",
3475
- textAlign: "left",
3476
- borderBottom: "1px solid var(--nozle-border, var(--border))",
3477
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
3478
- fontWeight: 500
3479
- },
3480
- children: "Feature"
3481
- }
3482
- ),
3483
- plans.map((plan) => /* @__PURE__ */ jsx(
3484
- "th",
3485
- {
3486
- style: {
3487
- padding: "0.75rem 1rem",
3488
- textAlign: "center",
3489
- borderBottom: "1px solid var(--nozle-border, var(--border))",
3490
- fontWeight: 600,
3491
- color: "var(--nozle-foreground, var(--foreground))"
3492
- },
3493
- children: plan.name
3494
- },
3495
- plan.id
3496
- ))
3497
- ] }) }),
3498
- /* @__PURE__ */ jsx("tbody", { children: features.map((feature) => /* @__PURE__ */ jsxs("tr", { children: [
3499
- /* @__PURE__ */ jsx(
3500
- "td",
3501
- {
3502
- style: {
3503
- padding: "0.75rem 1rem",
3504
- borderBottom: "1px solid var(--nozle-border, var(--border))",
3505
- color: "var(--nozle-foreground, var(--foreground))"
3506
- },
3507
- children: feature.label
3508
- }
3509
- ),
3510
- plans.map((plan) => {
3511
- const value = feature.values[plan.id];
3512
- return /* @__PURE__ */ jsx(
3513
- "td",
3514
- {
3515
- style: {
3516
- padding: "0.75rem 1rem",
3517
- textAlign: "center",
3518
- borderBottom: "1px solid var(--nozle-border, var(--border))"
3519
- },
3520
- children: typeof value === "boolean" ? value ? /* @__PURE__ */ jsx(
3521
- "span",
3522
- {
3523
- style: {
3524
- color: "var(--nozle-primary, var(--primary))"
3525
- },
3526
- "aria-label": "Included",
3527
- children: "\u2713"
3528
- }
3529
- ) : /* @__PURE__ */ jsx(
3530
- "span",
3531
- {
3532
- style: {
3533
- color: "var(--nozle-muted-foreground, var(--muted-foreground))"
3534
- },
3535
- "aria-label": "Not included",
3536
- children: "\u2715"
3537
- }
3538
- ) : /* @__PURE__ */ jsx("span", { children: value ?? "\u2014" })
3539
- },
3540
- plan.id
3541
- );
3542
- })
3543
- ] }, feature.key)) })
3544
- ]
3545
- }
3546
- )
3547
- }
3548
- );
3998
+ const { root, el } = useAppearance(appearance);
3999
+ const plans = sort ? [...catalogPlans].sort(sort) : sortPlansByPrice(catalogPlans);
4000
+ const features = featureOverride ?? comparisonRows(plans);
4001
+ const cell = (style) => el("planComparison__cell", style);
4002
+ return /* @__PURE__ */ jsxs("div", { ...root("planComparison", { minWidth: 0 }), children: [
4003
+ /* @__PURE__ */ jsx("div", { style: table.frame, children: /* @__PURE__ */ jsxs("table", { ...el("planComparison__table", table.table), children: [
4004
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
4005
+ /* @__PURE__ */ jsx("th", { ...cell(table.head()), children: "Feature" }),
4006
+ plans.map((plan) => /* @__PURE__ */ jsxs("th", { ...cell({ ...table.head(), textAlign: "center", padding: "10px 20px" }), children: [
4007
+ plan.name,
4008
+ /* @__PURE__ */ jsxs("div", { style: { ...text.meta, letterSpacing: "normal", textTransform: "none" }, children: [
4009
+ formatPlanPrice(plan.amount_cents, plan.amount_currency),
4010
+ " / ",
4011
+ intervalLabel(plan.interval)
4012
+ ] })
4013
+ ] }, plan.code))
4014
+ ] }) }),
4015
+ /* @__PURE__ */ jsx("tbody", { children: features.map((feature, index) => /* @__PURE__ */ jsxs("tr", { ...el("planComparison__row"), children: [
4016
+ /* @__PURE__ */ jsx("td", { ...cell(table.cell(false, index === features.length - 1)), children: feature.label }),
4017
+ plans.map((plan) => {
4018
+ const value = feature.values[plan.code];
4019
+ return /* @__PURE__ */ jsx("td", { ...cell({ ...table.cell(false, index === features.length - 1), textAlign: "center" }), children: typeof value === "boolean" ? value ? /* @__PURE__ */ jsx("span", { style: { color: T.primary }, "aria-label": "Included", children: "\u2713" }) : /* @__PURE__ */ jsx("span", { style: { color: T.mutedForeground }, "aria-label": "Not included", children: "\u2715" }) : /* @__PURE__ */ jsx("span", { children: value ?? "\u2014" }) }, plan.code);
4020
+ })
4021
+ ] }, feature.key)) })
4022
+ ] }) }),
4023
+ /* @__PURE__ */ jsx(NozleBranding, {})
4024
+ ] });
3549
4025
  }
3550
4026
  function UpgradeButton({
3551
4027
  planCode,
@@ -3561,26 +4037,18 @@ function UpgradeButton({
3561
4037
  onCheckoutStarted,
3562
4038
  onDowngradeScheduled,
3563
4039
  onUpgraded,
3564
- onError
4040
+ onError,
4041
+ appearance
3565
4042
  }) {
3566
4043
  const [isOpen, setIsOpen] = useState(false);
4044
+ const { root } = useAppearance(appearance);
3567
4045
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3568
4046
  /* @__PURE__ */ jsx(
3569
4047
  "button",
3570
4048
  {
3571
4049
  type: "button",
3572
4050
  onClick: () => setIsOpen(true),
3573
- className,
3574
- style: {
3575
- padding: "0.75rem 1.5rem",
3576
- borderRadius: "var(--nozle-radius, 0.5rem)",
3577
- border: "none",
3578
- background: "var(--nozle-primary, var(--primary))",
3579
- color: "var(--nozle-primary-foreground, var(--primary-foreground))",
3580
- cursor: "pointer",
3581
- fontWeight: 500,
3582
- ...style
3583
- },
4051
+ ...root("upgradeButton", { ...buttonStyle(), ...style }, className),
3584
4052
  children: label
3585
4053
  }
3586
4054
  ),
@@ -3588,6 +4056,7 @@ function UpgradeButton({
3588
4056
  UpgradeModal,
3589
4057
  {
3590
4058
  isOpen,
4059
+ appearance,
3591
4060
  planCode,
3592
4061
  returnUrl,
3593
4062
  subscriptionId,
@@ -3646,11 +4115,13 @@ function PlanGate({
3646
4115
  return /* @__PURE__ */ jsx(Fragment, { children });
3647
4116
  }
3648
4117
  function UpgradePrompt({
3649
- text = "Upgrade to access this feature",
4118
+ text: text2 = "Upgrade to access this feature",
3650
4119
  planName: planName2,
3651
- onUpgrade
4120
+ onUpgrade,
4121
+ appearance
3652
4122
  }) {
3653
- const displayText = planName2 ? `Upgrade to ${planName2} to access this feature` : text;
4123
+ const { root } = useAppearance(appearance);
4124
+ const displayText = planName2 ? `Upgrade to ${planName2} to access this feature` : text2;
3654
4125
  function handleUpgrade() {
3655
4126
  if (onUpgrade) {
3656
4127
  onUpgrade();
@@ -3661,40 +4132,23 @@ function UpgradePrompt({
3661
4132
  return /* @__PURE__ */ jsxs(
3662
4133
  "div",
3663
4134
  {
3664
- style: {
4135
+ ...root("upgradePrompt", {
3665
4136
  display: "flex",
3666
4137
  flexDirection: "column",
3667
4138
  alignItems: "center",
3668
4139
  gap: "0.75rem",
3669
4140
  padding: "1rem",
3670
- textAlign: "center"
3671
- },
4141
+ textAlign: "center",
4142
+ ...text.body
4143
+ }),
3672
4144
  children: [
3673
- /* @__PURE__ */ jsx(
3674
- "p",
3675
- {
3676
- style: {
3677
- color: "var(--nozle-foreground, var(--foreground))",
3678
- fontSize: "0.875rem",
3679
- margin: 0
3680
- },
3681
- children: displayText
3682
- }
3683
- ),
4145
+ /* @__PURE__ */ jsx("p", { style: { margin: 0 }, children: displayText }),
3684
4146
  /* @__PURE__ */ jsx(
3685
4147
  "button",
3686
4148
  {
4149
+ type: "button",
3687
4150
  onClick: handleUpgrade,
3688
- style: {
3689
- padding: "0.5rem 1.25rem",
3690
- borderRadius: "var(--nozle-radius, 0.5rem)",
3691
- border: "none",
3692
- background: "var(--nozle-primary, var(--primary))",
3693
- color: "var(--nozle-primary-foreground, var(--primary-foreground))",
3694
- cursor: "pointer",
3695
- fontWeight: 500,
3696
- fontSize: "0.875rem"
3697
- },
4151
+ style: { ...buttonStyle(), padding: "0.5rem 1.25rem" },
3698
4152
  children: "Upgrade"
3699
4153
  }
3700
4154
  )
@@ -3707,12 +4161,14 @@ function LockedOverlay({
3707
4161
  upgradeText,
3708
4162
  upgradePlanName,
3709
4163
  onUpgrade,
3710
- children
4164
+ children,
4165
+ appearance
3711
4166
  }) {
4167
+ const { root, el } = useAppearance(appearance);
3712
4168
  if (!locked) {
3713
4169
  return /* @__PURE__ */ jsx(Fragment, { children });
3714
4170
  }
3715
- return /* @__PURE__ */ jsxs("div", { style: { position: "relative", display: "inline-block" }, children: [
4171
+ return /* @__PURE__ */ jsxs("div", { ...root("lockedOverlay", { position: "relative", display: "inline-block" }), children: [
3716
4172
  /* @__PURE__ */ jsx(
3717
4173
  "div",
3718
4174
  {
@@ -3728,16 +4184,15 @@ function LockedOverlay({
3728
4184
  /* @__PURE__ */ jsx(
3729
4185
  "div",
3730
4186
  {
3731
- style: {
4187
+ ...el("lockedOverlay__panel", {
3732
4188
  position: "absolute",
3733
4189
  inset: 0,
3734
4190
  display: "flex",
3735
4191
  alignItems: "center",
3736
4192
  justifyContent: "center",
3737
- background: "var(--nozle-background, var(--background))",
3738
- opacity: 0.92,
3739
- borderRadius: "var(--nozle-radius, 0.5rem)"
3740
- },
4193
+ background: `color-mix(in srgb, ${T.background} 92%, transparent)`,
4194
+ borderRadius: T.radius
4195
+ }),
3741
4196
  role: "dialog",
3742
4197
  "aria-label": "Feature locked \u2014 upgrade required",
3743
4198
  children: /* @__PURE__ */ jsx(
@@ -3745,7 +4200,8 @@ function LockedOverlay({
3745
4200
  {
3746
4201
  text: upgradeText,
3747
4202
  planName: upgradePlanName,
3748
- onUpgrade
4203
+ onUpgrade,
4204
+ appearance
3749
4205
  }
3750
4206
  )
3751
4207
  }
@@ -3753,68 +4209,109 @@ function LockedOverlay({
3753
4209
  ] });
3754
4210
  }
3755
4211
  function getUsageMeterColor(percentage) {
3756
- if (percentage >= 95) return "var(--nozle-destructive, var(--destructive))";
3757
- if (percentage >= 80) return "var(--nozle-warning, var(--warning))";
3758
- return "var(--nozle-primary, var(--primary))";
4212
+ if (percentage >= 95) return T.danger;
4213
+ if (percentage >= 80) return T.warning;
4214
+ return T.primary;
3759
4215
  }
3760
4216
  function UsageMeter({
3761
4217
  used,
3762
4218
  limit,
3763
4219
  label,
3764
- showText = true
4220
+ showText = true,
4221
+ appearance
3765
4222
  }) {
3766
- const percentage = limit > 0 ? Math.min(100, used / limit * 100) : 0;
4223
+ const { root, el } = useAppearance(appearance);
4224
+ const unlimited = limit == null;
4225
+ const percentage = !unlimited && limit > 0 ? Math.min(100, used / limit * 100) : 0;
3767
4226
  const color = getUsageMeterColor(percentage);
3768
- return /* @__PURE__ */ jsxs("div", { className: "w-full space-y-1", children: [
3769
- label && /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-sm", children: [
3770
- /* @__PURE__ */ jsx("span", { children: label }),
3771
- showText && /* @__PURE__ */ jsxs("span", { style: { color }, children: [
3772
- used.toLocaleString(),
3773
- " / ",
3774
- limit.toLocaleString()
3775
- ] })
3776
- ] }),
3777
- /* @__PURE__ */ jsx(
3778
- "div",
3779
- {
3780
- className: "h-2 rounded-full overflow-hidden",
3781
- style: { background: "var(--nozle-border, var(--border))" },
3782
- children: /* @__PURE__ */ jsx(
4227
+ return /* @__PURE__ */ jsxs(
4228
+ "div",
4229
+ {
4230
+ ...root("usageMeter", {
4231
+ width: "100%",
4232
+ display: "flex",
4233
+ flexDirection: "column",
4234
+ gap: "0.25rem",
4235
+ ...text.body
4236
+ }),
4237
+ children: [
4238
+ label && /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
4239
+ /* @__PURE__ */ jsx("span", { ...el("usageMeter__label"), children: label }),
4240
+ showText && /* @__PURE__ */ jsxs("span", { ...el("usageMeter__value", { color: percentage >= 95 ? T.dangerText : percentage >= 80 ? T.warningText : T.foreground }), children: [
4241
+ used.toLocaleString(),
4242
+ " / ",
4243
+ unlimited ? "Unlimited" : limit.toLocaleString()
4244
+ ] })
4245
+ ] }),
4246
+ /* @__PURE__ */ jsx(
3783
4247
  "div",
3784
4248
  {
3785
- className: "h-full rounded-full",
3786
- style: {
3787
- width: `${percentage}%`,
3788
- background: color,
3789
- transition: "width 0.3s ease"
3790
- }
4249
+ ...el("usageMeter__track", {
4250
+ height: "0.5rem",
4251
+ borderRadius: "9999px",
4252
+ overflow: "hidden",
4253
+ background: T.muted
4254
+ }),
4255
+ children: /* @__PURE__ */ jsx(
4256
+ "div",
4257
+ {
4258
+ ...el("usageMeter__fill", {
4259
+ height: "100%",
4260
+ borderRadius: "9999px",
4261
+ width: `${percentage}%`,
4262
+ background: color,
4263
+ transition: "width 0.3s ease"
4264
+ })
4265
+ }
4266
+ )
3791
4267
  }
3792
4268
  )
3793
- }
3794
- )
3795
- ] });
4269
+ ]
4270
+ }
4271
+ );
4272
+ }
4273
+ function usageItems(entitlements, results) {
4274
+ return entitlements.flatMap((entitlement) => {
4275
+ const result = results[entitlement.code];
4276
+ if (entitlement.feature_type !== "metered" || !result) return [];
4277
+ return [{ key: entitlement.code, label: entitlement.name, used: result.used, limit: result.limit ?? null }];
4278
+ });
3796
4279
  }
3797
4280
  function UsageDashboard({
3798
4281
  features = [],
3799
- loading = false
4282
+ loading = false,
4283
+ error = null,
4284
+ emptyMessage = "No usage to show yet.",
4285
+ appearance
3800
4286
  }) {
4287
+ const { root } = useAppearance(appearance);
4288
+ const props = root("usageDashboard", {
4289
+ ...surface,
4290
+ margin: 0,
4291
+ display: "flex",
4292
+ flexDirection: "column",
4293
+ gap: loading ? "0.75rem" : "1rem",
4294
+ // Errors and the empty message take the same colours as in the other components.
4295
+ ...error ? { color: T.dangerText } : features.length === 0 && !loading && text.secondary
4296
+ });
3801
4297
  if (loading) {
3802
- return /* @__PURE__ */ jsx("div", { className: "animate-pulse space-y-3", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsx(
3803
- "div",
3804
- {
3805
- className: "h-8 rounded",
3806
- style: { background: "var(--nozle-border, var(--border))" }
3807
- },
3808
- i
3809
- )) });
4298
+ return /* @__PURE__ */ jsx("div", { ...props, "aria-busy": "true", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsx("div", { style: { height: "2rem", borderRadius: `calc(${T.radius} / 3)`, background: T.skeleton } }, i)) });
4299
+ }
4300
+ if (error) {
4301
+ return /* @__PURE__ */ jsx("p", { ...props, role: "alert", children: typeof error === "string" ? error : error.message });
4302
+ }
4303
+ if (features.length === 0) {
4304
+ return /* @__PURE__ */ jsx("p", { ...props, role: "status", children: emptyMessage });
3810
4305
  }
3811
- return /* @__PURE__ */ jsx("div", { className: "space-y-4", children: features.map((f) => /* @__PURE__ */ jsx(UsageMeter, { used: f.used, limit: f.limit, label: f.label }, f.key)) });
4306
+ return /* @__PURE__ */ jsx("div", { ...props, children: features.map((f) => /* @__PURE__ */ jsx(UsageMeter, { used: f.used, limit: f.limit, label: f.label, appearance }, f.key)) });
3812
4307
  }
3813
4308
  function UsageAlert({
3814
4309
  features,
3815
4310
  threshold = 80,
3816
- upgradeHref = "/plans"
4311
+ upgradeHref = "/plans",
4312
+ appearance
3817
4313
  }) {
4314
+ const { root, el } = useAppearance(appearance);
3818
4315
  const [dismissed, setDismissed] = useState(/* @__PURE__ */ new Set());
3819
4316
  const alertFeatures = features.filter(
3820
4317
  (f) => f.percentage >= threshold && !dismissed.has(f.key)
@@ -3825,15 +4322,19 @@ function UsageAlert({
3825
4322
  const handleDismiss = (key) => {
3826
4323
  setDismissed((prev) => new Set(prev).add(key));
3827
4324
  };
3828
- return /* @__PURE__ */ jsx("div", { className: "space-y-2", children: alertFeatures.map((f) => /* @__PURE__ */ jsxs(
4325
+ return /* @__PURE__ */ jsx("div", { ...root("usageAlert", { display: "flex", flexDirection: "column", gap: "0.5rem", ...text.body }), children: alertFeatures.map((f) => /* @__PURE__ */ jsxs(
3829
4326
  "div",
3830
4327
  {
3831
4328
  role: "alert",
3832
- className: "flex items-center justify-between rounded-md px-4 py-3 text-sm",
3833
4329
  style: {
3834
- background: "var(--nozle-warning-bg, var(--warning-bg, #fef9c3))",
3835
- color: "var(--nozle-warning-text, var(--warning-text, #854d0e))",
3836
- border: "1px solid var(--nozle-warning-border, var(--warning-border, #fde047))"
4330
+ display: "flex",
4331
+ alignItems: "center",
4332
+ justifyContent: "space-between",
4333
+ borderRadius: `calc(${T.radius} / 2)`,
4334
+ padding: "0.75rem 1rem",
4335
+ background: T.warningSoft,
4336
+ color: T.warningText,
4337
+ border: border(T.warningBorder)
3837
4338
  },
3838
4339
  children: [
3839
4340
  /* @__PURE__ */ jsxs("span", { children: [
@@ -3856,8 +4357,8 @@ function UsageAlert({
3856
4357
  {
3857
4358
  href: upgradeHref,
3858
4359
  style: {
4360
+ ...text.action,
3859
4361
  color: "inherit",
3860
- fontWeight: 600,
3861
4362
  textDecoration: "underline"
3862
4363
  },
3863
4364
  children: "Upgrade"
@@ -3867,16 +4368,17 @@ function UsageAlert({
3867
4368
  /* @__PURE__ */ jsx(
3868
4369
  "button",
3869
4370
  {
4371
+ type: "button",
3870
4372
  onClick: () => handleDismiss(f.key),
3871
4373
  "aria-label": `Dismiss ${f.label} alert`,
3872
- style: {
4374
+ ...el("usageAlert__dismiss", {
3873
4375
  background: "none",
3874
4376
  border: "none",
3875
4377
  cursor: "pointer",
3876
4378
  padding: "0 0 0 0.75rem",
3877
- fontSize: "1rem",
4379
+ ...text.action,
3878
4380
  color: "inherit"
3879
- },
4381
+ }),
3880
4382
  children: "\xD7"
3881
4383
  }
3882
4384
  )
@@ -3885,44 +4387,247 @@ function UsageAlert({
3885
4387
  f.key
3886
4388
  )) });
3887
4389
  }
3888
- var TIER_STYLES = {
3889
- free: {
3890
- background: "var(--nozle-muted, var(--muted, #f1f5f9))",
3891
- color: "var(--nozle-muted-foreground, var(--muted-foreground, #64748b))",
3892
- border: "1px solid var(--nozle-border, var(--border, #e2e8f0))"
3893
- },
3894
- starter: {
3895
- background: "color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 10%, transparent)",
3896
- color: "var(--nozle-primary, var(--primary, #3b82f6))",
3897
- border: "1px solid color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 30%, transparent)"
3898
- },
3899
- pro: {
3900
- background: "var(--nozle-primary, var(--primary, #3b82f6))",
3901
- color: "var(--nozle-primary-foreground, var(--primary-foreground, #ffffff))",
3902
- border: "1px solid transparent"
3903
- },
3904
- enterprise: {
3905
- background: "var(--nozle-accent, var(--accent, #8b5cf6))",
3906
- color: "var(--nozle-accent-foreground, var(--accent-foreground, #ffffff))",
3907
- border: "1px solid transparent"
4390
+ var STATUS_LABELS = {
4391
+ active: "Active",
4392
+ pending: "Pending",
4393
+ incomplete: "Awaiting payment",
4394
+ canceled: "Canceled",
4395
+ terminated: "Ended"
4396
+ };
4397
+ function statusColors(status, ending) {
4398
+ if (status === "active" && !ending) return { background: T.successSoft, color: T.successText };
4399
+ if (status === "active" || status === "pending" || status === "incomplete") {
4400
+ return { background: T.warningSoft, color: T.warningText };
4401
+ }
4402
+ return { background: T.muted, color: T.mutedForeground };
4403
+ }
4404
+ function day(value, locale) {
4405
+ const parsed = new Date(value);
4406
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString(locale, { dateStyle: "medium" });
4407
+ }
4408
+ function currentPlanMeters(entitlements, usage) {
4409
+ const units = /* @__PURE__ */ new Map();
4410
+ for (const charge of usage?.charges_usage ?? []) {
4411
+ units.set(charge.feature.code, (units.get(charge.feature.code) ?? 0) + (Number(charge.units) || 0));
4412
+ }
4413
+ return (entitlements ?? []).filter((entitlement) => entitlement.feature_type === "metered").map((entitlement) => ({
4414
+ key: entitlement.code,
4415
+ label: entitlement.name,
4416
+ used: units.get(entitlement.code) ?? 0,
4417
+ limit: entitlement.usage_limit ?? null
4418
+ }));
4419
+ }
4420
+ function CurrentPlan(props) {
4421
+ return props.subscription === void 0 ? /* @__PURE__ */ jsx(CurrentPlanFromBackend, { ...props }) : /* @__PURE__ */ jsx(CurrentPlanView, { ...props });
4422
+ }
4423
+ function CurrentPlanFromBackend(props) {
4424
+ const subscription = useSubscription();
4425
+ const plans = usePlans();
4426
+ const usage = useUsage();
4427
+ const planCode = subscription.data?.subscription?.plan_code;
4428
+ const meters = props.usage ?? currentPlanMeters(plans.plans.find((plan) => plan.code === planCode)?.entitlements, usage.data);
4429
+ return /* @__PURE__ */ jsx(
4430
+ CurrentPlanView,
4431
+ {
4432
+ ...props,
4433
+ subscription: subscription.data ?? null,
4434
+ usage: meters,
4435
+ isLoading: props.isLoading || subscription.isPending,
4436
+ error: props.error ?? subscription.error
4437
+ }
4438
+ );
4439
+ }
4440
+ function CurrentPlanView({
4441
+ subscription,
4442
+ usage = [],
4443
+ details = [],
4444
+ actions,
4445
+ warningPercent = 80,
4446
+ title = "Current plan",
4447
+ isLoading = false,
4448
+ error = null,
4449
+ emptyMessage = "No active plan.",
4450
+ pastDueMessage = "A payment is overdue. Pay the open invoice to keep your plan active.",
4451
+ locale = "en-US",
4452
+ appearance
4453
+ }) {
4454
+ const { root, el } = useAppearance(appearance);
4455
+ const frame = { ...surface, padding: 0, overflow: "hidden" };
4456
+ const header = /* @__PURE__ */ jsxs(
4457
+ "div",
4458
+ {
4459
+ style: {
4460
+ display: "flex",
4461
+ alignItems: "center",
4462
+ justifyContent: "space-between",
4463
+ gap: 12,
4464
+ padding: "0.875rem 1.25rem",
4465
+ borderBottom: border()
4466
+ },
4467
+ children: [
4468
+ /* @__PURE__ */ jsx("span", { style: text.heading, children: title }),
4469
+ actions
4470
+ ]
4471
+ }
4472
+ );
4473
+ const body = (children) => /* @__PURE__ */ jsx("div", { style: { padding: "1.25rem", display: "grid", gap: "1rem" }, children });
4474
+ if (isLoading) {
4475
+ return /* @__PURE__ */ jsxs("section", { ...root("currentPlan", frame), "aria-busy": "true", children: [
4476
+ header,
4477
+ body(/* @__PURE__ */ jsx("div", { style: { height: 72, borderRadius: `calc(${T.radius} / 2)`, background: T.skeleton } }))
4478
+ ] });
3908
4479
  }
4480
+ if (error) {
4481
+ return /* @__PURE__ */ jsxs("section", { ...root("currentPlan", frame), children: [
4482
+ header,
4483
+ body(/* @__PURE__ */ jsx("p", { role: "alert", style: { margin: 0, color: T.dangerText }, children: typeof error === "string" ? error : error.message }))
4484
+ ] });
4485
+ }
4486
+ const current = subscription?.subscription;
4487
+ if (!current) {
4488
+ return /* @__PURE__ */ jsxs("section", { ...root("currentPlan", frame), children: [
4489
+ header,
4490
+ body(/* @__PURE__ */ jsx("p", { role: "status", style: { ...text.secondary, margin: 0 }, children: emptyMessage }))
4491
+ ] });
4492
+ }
4493
+ const pending = subscription?.pending_change;
4494
+ const ending = Boolean(current.ending_at);
4495
+ const near = warningPercent === null ? [] : usage.filter((meter) => meter.limit != null && meter.limit > 0 && meter.used / meter.limit * 100 >= warningPercent);
4496
+ const statusLabel = ending && current.ending_at ? `Ends ${day(current.ending_at, locale)}` : STATUS_LABELS[current.status] ?? current.status;
4497
+ return /* @__PURE__ */ jsxs("section", { ...root("currentPlan", frame), "aria-label": title, children: [
4498
+ header,
4499
+ body(
4500
+ /* @__PURE__ */ jsxs(Fragment, { children: [
4501
+ subscription?.past_due && /* @__PURE__ */ jsx(
4502
+ "p",
4503
+ {
4504
+ role: "alert",
4505
+ ...el("currentPlan__notice", {
4506
+ margin: 0,
4507
+ padding: "0.625rem 0.875rem",
4508
+ ...text.body,
4509
+ borderRadius: `calc(${T.radius} / 2)`,
4510
+ background: T.dangerSoft,
4511
+ color: T.dangerText,
4512
+ border: border(T.dangerBorder)
4513
+ }),
4514
+ children: pastDueMessage
4515
+ }
4516
+ ),
4517
+ near.map((meter) => /* @__PURE__ */ jsxs(
4518
+ "p",
4519
+ {
4520
+ role: "status",
4521
+ ...el("currentPlan__notice", {
4522
+ margin: 0,
4523
+ padding: "0.625rem 0.875rem",
4524
+ ...text.body,
4525
+ borderRadius: `calc(${T.radius} / 2)`,
4526
+ background: T.warningSoft,
4527
+ color: T.warningText,
4528
+ border: border(T.warningBorder)
4529
+ }),
4530
+ children: [
4531
+ "You have used ",
4532
+ Math.round(meter.used / meter.limit * 100),
4533
+ "% of the ",
4534
+ meter.label,
4535
+ " included in",
4536
+ " ",
4537
+ current.plan.name,
4538
+ "."
4539
+ ]
4540
+ },
4541
+ meter.key
4542
+ )),
4543
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: "0.625rem" }, children: [
4544
+ /* @__PURE__ */ jsx("span", { ...el("currentPlan__name", text.heading), children: current.plan.name }),
4545
+ /* @__PURE__ */ jsx(
4546
+ "span",
4547
+ {
4548
+ ...el("currentPlan__status", {
4549
+ ...text.label,
4550
+ ...statusColors(current.status, ending),
4551
+ padding: "0.125rem 0.625rem",
4552
+ borderRadius: 9999
4553
+ }),
4554
+ children: statusLabel
4555
+ }
4556
+ ),
4557
+ pending && /* @__PURE__ */ jsxs(
4558
+ "span",
4559
+ {
4560
+ ...el("currentPlan__scheduled", {
4561
+ ...text.label,
4562
+ padding: "0.125rem 0.625rem",
4563
+ borderRadius: 9999,
4564
+ border: border()
4565
+ }),
4566
+ children: [
4567
+ "Changes to ",
4568
+ pending.name,
4569
+ " on ",
4570
+ day(pending.effective_at, locale)
4571
+ ]
4572
+ }
4573
+ ),
4574
+ /* @__PURE__ */ jsxs("span", { ...el("currentPlan__price", { ...text.secondary, marginLeft: "auto" }), children: [
4575
+ formatPlanPrice(Number(current.plan.amount_cents), current.plan.currency),
4576
+ " / ",
4577
+ intervalLabel(current.plan.interval)
4578
+ ] })
4579
+ ] }),
4580
+ usage.length > 0 && /* @__PURE__ */ jsx("div", { ...el("currentPlan__usage", { display: "grid", gap: "0.875rem" }), children: usage.map((meter) => /* @__PURE__ */ jsx(
4581
+ UsageMeter,
4582
+ {
4583
+ label: `${meter.label} used this period`,
4584
+ used: meter.used,
4585
+ limit: meter.limit,
4586
+ appearance
4587
+ },
4588
+ meter.key
4589
+ )) }),
4590
+ details.length > 0 && /* @__PURE__ */ jsx(
4591
+ "dl",
4592
+ {
4593
+ ...el("currentPlan__details", {
4594
+ margin: 0,
4595
+ display: "grid",
4596
+ gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))",
4597
+ gap: "0.75rem"
4598
+ }),
4599
+ children: details.map((detail) => /* @__PURE__ */ jsxs("div", { children: [
4600
+ /* @__PURE__ */ jsx("dt", { style: text.meta, children: detail.label }),
4601
+ /* @__PURE__ */ jsx("dd", { style: { margin: "0.125rem 0 0", fontWeight: 600 }, children: detail.value })
4602
+ ] }, detail.label))
4603
+ }
4604
+ )
4605
+ ] })
4606
+ ),
4607
+ /* @__PURE__ */ jsx(NozleBranding, {})
4608
+ ] });
4609
+ }
4610
+ var TIER_STYLES = {
4611
+ free: { background: T.muted, color: T.mutedForeground, border: border() },
4612
+ starter: { background: T.primarySoft, color: T.primary, border: border(`color-mix(in srgb, ${T.primary} 30%, transparent)`) },
4613
+ pro: { background: T.primary, color: T.primaryForeground, border: border("transparent") },
4614
+ enterprise: { background: T.accent, color: T.accentForeground, border: border("transparent") }
3909
4615
  };
3910
- function PlanBadge({ plan, tier = "free" }) {
4616
+ function PlanBadge({ plan, tier = "free", appearance }) {
4617
+ const { root } = useAppearance(appearance);
3911
4618
  const style = TIER_STYLES[tier] ?? TIER_STYLES.free;
3912
4619
  return /* @__PURE__ */ jsx(
3913
4620
  "span",
3914
4621
  {
3915
- style: {
4622
+ ...root("planBadge", {
4623
+ ...text.label,
3916
4624
  ...style,
3917
4625
  display: "inline-flex",
3918
4626
  alignItems: "center",
3919
4627
  padding: "0.125rem 0.5rem",
3920
4628
  borderRadius: "9999px",
3921
- fontSize: "0.75rem",
3922
- fontWeight: 600,
3923
- lineHeight: "1.25rem",
3924
4629
  whiteSpace: "nowrap"
3925
- },
4630
+ }),
3926
4631
  children: plan
3927
4632
  }
3928
4633
  );
@@ -3939,8 +4644,10 @@ var BRAND_LABELS = {
3939
4644
  function PaymentMethodDisplay({
3940
4645
  paymentMethod,
3941
4646
  onUpdatePaymentMethod,
3942
- updateHref = "/billing/update-payment"
4647
+ updateHref = "/billing/update-payment",
4648
+ appearance
3943
4649
  }) {
4650
+ const { root } = useAppearance(appearance);
3944
4651
  const handleUpdate = () => {
3945
4652
  if (onUpdatePaymentMethod) {
3946
4653
  onUpdatePaymentMethod();
@@ -3948,7 +4655,7 @@ function PaymentMethodDisplay({
3948
4655
  window.location.href = updateHref;
3949
4656
  }
3950
4657
  };
3951
- return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
4658
+ return /* @__PURE__ */ jsxs("div", { ...root("paymentMethod", { ...surface, display: "flex", flexDirection: "column", gap: "0.75rem" }), children: [
3952
4659
  paymentMethod ? /* @__PURE__ */ jsxs(
3953
4660
  "div",
3954
4661
  {
@@ -3957,80 +4664,245 @@ function PaymentMethodDisplay({
3957
4664
  alignItems: "center",
3958
4665
  gap: "0.75rem",
3959
4666
  padding: "0.75rem 1rem",
3960
- border: "1px solid var(--nozle-border, var(--border))",
3961
- borderRadius: "0.5rem",
3962
- fontSize: "0.875rem"
4667
+ border: border(),
4668
+ borderRadius: `calc(${T.radius} * 0.6)`
3963
4669
  },
3964
4670
  children: [
3965
- /* @__PURE__ */ jsx(
3966
- "span",
3967
- {
3968
- style: {
3969
- fontWeight: 600,
3970
- color: "var(--nozle-foreground, var(--foreground))"
3971
- },
3972
- children: BRAND_LABELS[paymentMethod.brand.toLowerCase()] ?? paymentMethod.brand
3973
- }
3974
- ),
3975
- /* @__PURE__ */ jsxs(
3976
- "span",
3977
- {
3978
- style: {
3979
- color: "var(--nozle-muted-foreground, var(--muted-foreground))"
3980
- },
3981
- children: [
3982
- "\u2022\u2022\u2022\u2022 ",
3983
- paymentMethod.last4
3984
- ]
3985
- }
3986
- ),
3987
- /* @__PURE__ */ jsxs(
3988
- "span",
3989
- {
3990
- style: {
3991
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
3992
- fontSize: "0.75rem"
3993
- },
3994
- children: [
3995
- "Expires ",
3996
- String(paymentMethod.expMonth).padStart(2, "0"),
3997
- "/",
3998
- String(paymentMethod.expYear).slice(-2)
3999
- ]
4000
- }
4001
- )
4671
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, color: T.foreground }, children: BRAND_LABELS[paymentMethod.brand.toLowerCase()] ?? paymentMethod.brand }),
4672
+ /* @__PURE__ */ jsxs("span", { style: text.secondary, children: [
4673
+ "\u2022\u2022\u2022\u2022 ",
4674
+ paymentMethod.last4
4675
+ ] }),
4676
+ /* @__PURE__ */ jsxs("span", { style: text.meta, children: [
4677
+ "Expires ",
4678
+ String(paymentMethod.expMonth).padStart(2, "0"),
4679
+ "/",
4680
+ String(paymentMethod.expYear).slice(-2)
4681
+ ] })
4002
4682
  ]
4003
4683
  }
4004
- ) : /* @__PURE__ */ jsx(
4005
- "p",
4006
- {
4007
- style: {
4008
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
4009
- fontSize: "0.875rem"
4010
- },
4011
- children: "No payment method on file."
4012
- }
4013
- ),
4684
+ ) : /* @__PURE__ */ jsx("p", { style: { ...text.secondary, margin: 0 }, children: "No payment method on file." }),
4014
4685
  /* @__PURE__ */ jsx(
4015
4686
  "button",
4016
4687
  {
4688
+ type: "button",
4017
4689
  onClick: handleUpdate,
4018
4690
  style: {
4019
- background: "transparent",
4020
- border: "1px solid var(--nozle-primary, var(--primary))",
4021
- color: "var(--nozle-primary, var(--primary))",
4691
+ ...buttonStyle("outline"),
4692
+ alignSelf: "flex-start",
4022
4693
  padding: "0.375rem 0.875rem",
4023
- borderRadius: "0.375rem",
4024
- fontSize: "0.875rem",
4025
- fontWeight: 500,
4026
- cursor: "pointer"
4694
+ borderRadius: buttonRadius(`calc(${T.radius} / 2)`)
4027
4695
  },
4028
4696
  children: "Update Payment Method"
4029
4697
  }
4030
4698
  )
4031
4699
  ] });
4032
4700
  }
4701
+ var SOURCE_LABELS = {
4702
+ subscription_grant: "Plan grant",
4703
+ top_up: "Top-up",
4704
+ manual_grant: "Granted",
4705
+ adjustment: "Adjustment",
4706
+ allocated_top_up: "Allocated top-up"
4707
+ };
4708
+ var muted = T.mutedForeground;
4709
+ function date2(value) {
4710
+ const parsed = new Date(value);
4711
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString("en-US", { dateStyle: "medium" });
4712
+ }
4713
+ function CreditBalance({
4714
+ balance,
4715
+ isLoading = false,
4716
+ error = null,
4717
+ emptyMessage = "No credit balance yet.",
4718
+ appearance
4719
+ }) {
4720
+ const { root, el } = useAppearance(appearance);
4721
+ if (isLoading) return /* @__PURE__ */ jsx("div", { ...root("creditBalance", { ...surface, height: 96, background: T.skeleton }), role: "status", "aria-label": "Loading credit balance" });
4722
+ if (error) return /* @__PURE__ */ jsx("p", { ...root("creditBalance", { ...surface, color: T.dangerText }), role: "alert", children: typeof error === "string" ? error : error.message });
4723
+ if (!balance) return /* @__PURE__ */ jsx("p", { ...root("creditBalance", { ...surface, color: muted }), role: "status", children: emptyMessage });
4724
+ const sources = (balance.sources ?? []).filter((source) => source.remaining !== "0");
4725
+ return /* @__PURE__ */ jsxs("section", { "aria-label": balance.credit_system_name, ...root("creditBalance", surface), children: [
4726
+ /* @__PURE__ */ jsx("h3", { style: { ...text.heading, margin: 0 }, children: balance.credit_system_name }),
4727
+ /* @__PURE__ */ jsxs("p", { style: { margin: "0.25rem 0 0" }, children: [
4728
+ /* @__PURE__ */ jsx("span", { ...el("creditBalance__amount", text.metric), children: formatDecimalString(balance.available) }),
4729
+ " ",
4730
+ /* @__PURE__ */ jsxs("span", { style: text.secondary, children: [
4731
+ creditUnitLabel(balance.available, balance.unit_name),
4732
+ " available"
4733
+ ] })
4734
+ ] }),
4735
+ /* @__PURE__ */ jsxs("p", { style: { ...text.meta, margin: "0.25rem 0 0" }, children: [
4736
+ "As of ",
4737
+ date2(balance.as_of)
4738
+ ] }),
4739
+ sources.length > 0 && /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", padding: 0, margin: "1rem 0 0" }, children: sources.map((source) => /* @__PURE__ */ jsxs("li", { style: { display: "flex", justifyContent: "space-between", padding: "0.25rem 0" }, children: [
4740
+ /* @__PURE__ */ jsx("span", { children: SOURCE_LABELS[source.type] ?? source.type }),
4741
+ /* @__PURE__ */ jsxs("span", { style: text.secondary, children: [
4742
+ formatDecimalString(source.remaining),
4743
+ source.expires_at ? ` \xB7 expires ${date2(source.expires_at)}` : ""
4744
+ ] })
4745
+ ] }, source.id)) })
4746
+ ] });
4747
+ }
4748
+ var TYPE_LABELS = {
4749
+ consume: "Used",
4750
+ grant: "Granted",
4751
+ adjustment: "Adjustment",
4752
+ expire: "Expired",
4753
+ revoke: "Revoked",
4754
+ refund: "Refunded",
4755
+ transfer: "Transfer"
4756
+ };
4757
+ var muted2 = T.mutedForeground;
4758
+ function when(value) {
4759
+ const parsed = new Date(value);
4760
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" });
4761
+ }
4762
+ function CreditHistory({
4763
+ operations = [],
4764
+ isLoading = false,
4765
+ error = null,
4766
+ emptyMessage = "No credit activity yet.",
4767
+ hasMore = false,
4768
+ onLoadMore,
4769
+ appearance
4770
+ }) {
4771
+ const { root, el } = useAppearance(appearance);
4772
+ if (error) return /* @__PURE__ */ jsx("p", { ...root("creditHistory", { ...surface, color: T.dangerText }), role: "alert", children: typeof error === "string" ? error : error.message });
4773
+ if (isLoading && operations.length === 0) return /* @__PURE__ */ jsx("div", { ...root("creditHistory", { ...surface, height: 120, background: T.skeleton }), role: "status", "aria-label": "Loading credit history" });
4774
+ if (operations.length === 0) return /* @__PURE__ */ jsx("p", { ...root("creditHistory", { ...surface, color: muted2 }), role: "status", children: emptyMessage });
4775
+ return /* @__PURE__ */ jsxs("div", { ...root("creditHistory", { ...text.body, minWidth: 0 }), children: [
4776
+ /* @__PURE__ */ jsx("div", { style: table.frame, children: /* @__PURE__ */ jsxs("table", { ...el("creditHistory__table", table.table), children: [
4777
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: ["When", "Type", "Feature", "Credits", "Status"].map((heading) => /* @__PURE__ */ jsx("th", { style: table.head(heading === "Credits"), children: heading }, heading)) }) }),
4778
+ /* @__PURE__ */ jsx("tbody", { children: operations.map((operation, index) => {
4779
+ const last = index === operations.length - 1;
4780
+ return /* @__PURE__ */ jsxs("tr", { ...el("creditHistory__row"), children: [
4781
+ /* @__PURE__ */ jsx("td", { style: table.cell(false, last), children: when(operation.occurred_at) }),
4782
+ /* @__PURE__ */ jsx("td", { style: table.cell(false, last), children: TYPE_LABELS[operation.type] ?? operation.type }),
4783
+ /* @__PURE__ */ jsx("td", { style: table.cell(false, last), children: operation.feature_code ?? "\u2014" }),
4784
+ /* @__PURE__ */ jsx("td", { style: table.cell(true, last), children: formatDecimalString(operation.credit_amount) }),
4785
+ /* @__PURE__ */ jsx("td", { style: { ...table.cell(false, last), color: operation.status === "succeeded" ? void 0 : muted2 }, children: operation.status })
4786
+ ] }, operation.id);
4787
+ }) })
4788
+ ] }) }),
4789
+ hasMore && onLoadMore && /* @__PURE__ */ jsx(
4790
+ "button",
4791
+ {
4792
+ type: "button",
4793
+ onClick: onLoadMore,
4794
+ disabled: isLoading,
4795
+ style: {
4796
+ ...buttonStyle("secondary", isLoading),
4797
+ marginTop: "0.75rem",
4798
+ padding: "0.5rem 0.875rem",
4799
+ borderRadius: buttonRadius(`calc(${T.radius} / 2)`),
4800
+ opacity: 1
4801
+ },
4802
+ children: isLoading ? "Loading\u2026" : "Load more"
4803
+ }
4804
+ )
4805
+ ] });
4806
+ }
4807
+
4808
+ // src/components/usage/usage-data.ts
4809
+ function formatAmount(amountCents, currency) {
4810
+ const amount = amountCents / 100;
4811
+ try {
4812
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
4813
+ } catch {
4814
+ return `${currency} ${amount.toFixed(2)}`;
4815
+ }
4816
+ }
4817
+ function formatDay(value) {
4818
+ const parsed = new Date(value);
4819
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString("en-US", { dateStyle: "medium", timeZone: "UTC" });
4820
+ }
4821
+ var muted3 = T.mutedForeground;
4822
+ function UsageBreakdown({
4823
+ usage,
4824
+ isLoading = false,
4825
+ error = null,
4826
+ emptyMessage = "No usage in this period yet.",
4827
+ appearance
4828
+ }) {
4829
+ const { root, el } = useAppearance(appearance);
4830
+ if (isLoading) return /* @__PURE__ */ jsx("div", { ...root("usageBreakdown", { ...surface, height: 120, background: T.skeleton }), role: "status", "aria-label": "Loading usage" });
4831
+ if (error) return /* @__PURE__ */ jsx("p", { ...root("usageBreakdown", { ...surface, color: T.dangerText }), role: "alert", children: typeof error === "string" ? error : error.message });
4832
+ if (!usage || usage.charges_usage.length === 0) return /* @__PURE__ */ jsx("p", { ...root("usageBreakdown", { ...surface, color: muted3 }), role: "status", children: emptyMessage });
4833
+ const rows = [...usage.charges_usage].sort((a, b) => b.amount_cents - a.amount_cents);
4834
+ return /* @__PURE__ */ jsxs("section", { ...root("usageBreakdown", { ...text.body, minWidth: 0 }), children: [
4835
+ /* @__PURE__ */ jsxs("p", { style: { ...text.secondary, margin: "0 0 0.5rem" }, children: [
4836
+ formatDay(usage.from_datetime),
4837
+ " to ",
4838
+ formatDay(usage.to_datetime)
4839
+ ] }),
4840
+ /* @__PURE__ */ jsx("div", { style: table.frame, children: /* @__PURE__ */ jsxs("table", { style: table.table, children: [
4841
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
4842
+ /* @__PURE__ */ jsx("th", { style: table.head(), children: "Feature" }),
4843
+ /* @__PURE__ */ jsx("th", { style: table.head(true), children: "Units" }),
4844
+ /* @__PURE__ */ jsx("th", { style: table.head(true), children: "Amount" })
4845
+ ] }) }),
4846
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((row, index) => {
4847
+ const last = index === rows.length - 1;
4848
+ return /* @__PURE__ */ jsxs("tr", { ...el("usageBreakdown__row"), children: [
4849
+ /* @__PURE__ */ jsx("td", { style: table.cell(false, last), children: row.charge.invoice_display_name || row.feature.name }),
4850
+ /* @__PURE__ */ jsx("td", { style: table.cell(true, last), children: formatDecimalString(row.units, 6) }),
4851
+ /* @__PURE__ */ jsx("td", { style: table.cell(true, last), children: formatAmount(row.amount_cents, row.amount_currency) })
4852
+ ] }, row.charge.nozle_id);
4853
+ }) }),
4854
+ /* @__PURE__ */ jsx("tfoot", { children: /* @__PURE__ */ jsxs("tr", { children: [
4855
+ /* @__PURE__ */ jsx("td", { style: table.foot(), colSpan: 2, children: "Total" }),
4856
+ /* @__PURE__ */ jsx("td", { style: table.foot(true), children: formatAmount(usage.amount_cents, usage.currency) })
4857
+ ] }) })
4858
+ ] }) })
4859
+ ] });
4860
+ }
4861
+ var muted4 = T.mutedForeground;
4862
+ var BAR_AREA = 120;
4863
+ function unitsFor(period, featureCode) {
4864
+ return period.charges_usage.filter((charge) => charge.feature.code === featureCode).reduce((sum, charge) => sum + (Number(charge.units) || 0), 0);
4865
+ }
4866
+ function UsageTrend({
4867
+ periods,
4868
+ featureCode,
4869
+ isLoading = false,
4870
+ error = null,
4871
+ emptyMessage = "No past usage yet.",
4872
+ appearance
4873
+ }) {
4874
+ const { root, el } = useAppearance(appearance);
4875
+ if (isLoading) return /* @__PURE__ */ jsx("div", { ...root("usageTrend", { ...surface, height: 160, background: T.skeleton }), role: "status", "aria-label": "Loading usage trend" });
4876
+ if (error) return /* @__PURE__ */ jsx("p", { ...root("usageTrend", { ...surface, color: T.dangerText }), role: "alert", children: typeof error === "string" ? error : error.message });
4877
+ if (!periods || periods.length === 0) return /* @__PURE__ */ jsx("p", { ...root("usageTrend", { ...surface, color: muted4 }), role: "status", children: emptyMessage });
4878
+ const ordered = [...periods].sort((a, b) => Date.parse(a.from_datetime) - Date.parse(b.from_datetime));
4879
+ const points = ordered.map((period) => ({
4880
+ key: period.from_datetime,
4881
+ label: formatDay(period.from_datetime),
4882
+ value: featureCode ? unitsFor(period, featureCode) : period.amount_cents,
4883
+ text: featureCode ? `${unitsFor(period, featureCode).toLocaleString("en-US")} ${featureCode}` : formatAmount(period.amount_cents, period.currency)
4884
+ }));
4885
+ const max = Math.max(...points.map((point) => point.value), 0);
4886
+ return /* @__PURE__ */ jsx("figure", { ...root("usageTrend", { ...surface, margin: 0 }), children: /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "flex-end", gap: "0.5rem", height: BAR_AREA + 40 }, children: points.map((point) => /* @__PURE__ */ jsxs("div", { style: { ...text.meta, flex: 1, minWidth: 0, textAlign: "center" }, children: [
4887
+ /* @__PURE__ */ jsx("div", { children: point.text }),
4888
+ /* @__PURE__ */ jsx(
4889
+ "div",
4890
+ {
4891
+ role: "img",
4892
+ "aria-label": `${point.label}: ${point.text}`,
4893
+ title: `${point.label}: ${point.text}`,
4894
+ ...el("usageTrend__bar", {
4895
+ height: max > 0 ? Math.max(2, point.value / max * BAR_AREA) : 2,
4896
+ marginTop: 4,
4897
+ borderRadius: `min(4px, ${T.radius}) min(4px, ${T.radius}) 0 0`,
4898
+ background: T.primary
4899
+ })
4900
+ }
4901
+ ),
4902
+ /* @__PURE__ */ jsx("div", { style: { marginTop: 4 }, children: point.label })
4903
+ ] }, point.key)) }) });
4904
+ }
4033
4905
 
4034
- export { BillingContext, BillingPortal, BillingPortalError, BillingProvider, CancellationControl, Checkout, CheckoutButton, FeatureGate, LockedOverlay, PaymentMethodDisplay, PlanBadge, PlanCard, PlanChangeControl, PlanComparison, PlanGate, PricingTable, UpgradeButton, UpgradeModal, UpgradePrompt, UsageAlert, UsageDashboard, UsageGate, UsageMeter, getUsageMeterColor, useBillingContext, useCheckout, useCheckout2 as useCheckoutSession, useNozleClient, useOptionalBillingContext, usePlans };
4906
+ export { BillingContext, BillingPortal, BillingPortalError, BillingProvider, BillingRequestError, CancellationControl, Checkout, CheckoutButton, CreditBalance, CreditHistory, CurrentPlan, FeatureGate, LockedOverlay, PaymentMethodDisplay, PlanBadge, PlanCard, PlanChangeControl, PlanComparison, PlanGate, PoweredByNozle, PricingTable, UpgradeButton, UpgradeModal, UpgradePrompt, UsageAlert, UsageBreakdown, UsageDashboard, UsageGate, UsageMeter, UsageTrend, comparisonRows, currentPlanMeters, getUsageMeterColor, planFeatures, sortPlansByPrice, usageItems, useAppearance, useBillingContext, useBillingRefresh, useCheckout, useCheckout2 as useCheckoutSession, useCreditBalance, useCreditHistory, useEntitlement, useInvoiceDownload, useInvoices, useNozleBranding, useNozleClient, useOptionalBillingContext, usePlans, useSubscription, useUpgradePreview, useUsage };
4035
4907
  //# sourceMappingURL=index.js.map
4036
4908
  //# sourceMappingURL=index.js.map