@nozle-js/react 0.10.2 → 0.12.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,243 @@
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 {
169
+ plans: data.plans ?? [],
170
+ families: Array.isArray(data.families) ? data.families : null,
171
+ nozleBranding: data.meta?.nozle_branding === true
172
+ };
173
+ },
174
+ staleTime: 5 * 6e4,
175
+ // The pricing page shows a load error at once; the app can call refetch().
176
+ retry: false
177
+ };
178
+ }
179
+ function usePlans() {
180
+ const { client, queryClient } = useBillingContext();
181
+ const query = useQuery(catalogQuery(client), queryClient);
182
+ return {
183
+ plans: query.data?.plans ?? [],
184
+ families: query.data?.families ?? null,
185
+ isLoading: query.isPending,
186
+ error: query.error ?? null,
187
+ refetch: query.refetch
188
+ };
189
+ }
190
+
191
+ // src/components/branding/logo.ts
192
+ var LOGO_VIEW_BOX = "0 0 3208 1122";
193
+ 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";
194
+ 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";
195
+ var BrandingScope = createContext(false);
196
+ function useNozleBranding() {
197
+ const billing = useOptionalBillingContext();
198
+ const { data } = useQuery(
199
+ // Reads the catalog request the components already make: mounting this must not refetch a
200
+ // failed catalog, or a table that shows the error would loop back into loading.
201
+ { ...catalogQuery(billing?.client), enabled: Boolean(billing), refetchOnMount: false, retryOnMount: false },
202
+ billing?.queryClient ?? idleQueryClient
203
+ );
204
+ return data?.nozleBranding === true;
205
+ }
206
+ function PoweredByNozle() {
207
+ const { root } = useAppearance();
208
+ return /* @__PURE__ */ jsxs(
209
+ "a",
210
+ {
211
+ href: "https://nozle.dev",
212
+ target: "_blank",
213
+ rel: "noopener noreferrer",
214
+ ...root("poweredBy", {
215
+ ...text.meta,
216
+ display: "flex",
217
+ alignItems: "center",
218
+ justifyContent: "center",
219
+ gap: "6px",
220
+ marginTop: "1rem",
221
+ textDecoration: "none"
222
+ }),
223
+ children: [
224
+ "Powered by",
225
+ /* @__PURE__ */ jsxs("svg", { viewBox: LOGO_VIEW_BOX, height: "14", width: "40", role: "img", "aria-label": "Nozle", style: { color: T.foreground }, children: [
226
+ /* @__PURE__ */ jsx("path", { fill: "#F34213", d: LOGO_MARK_PATH }),
227
+ /* @__PURE__ */ jsx("path", { fill: "currentColor", d: LOGO_WORDMARK_PATH })
228
+ ] })
229
+ ]
230
+ }
231
+ );
232
+ }
233
+ function NozleBranding({ show, children }) {
234
+ const nested = useContext(BrandingScope);
235
+ const fromCatalog = useNozleBranding();
236
+ return /* @__PURE__ */ jsxs(BrandingScope.Provider, { value: true, children: [
237
+ children,
238
+ !nested && (show ?? fromCatalog) && /* @__PURE__ */ jsx(PoweredByNozle, {})
239
+ ] });
240
+ }
7
241
 
8
242
  // src/components/portal/client.ts
9
243
  var BillingPortalError = class extends Error {
@@ -96,7 +330,7 @@ var identityQuery = `query SdkPortalIdentity {
96
330
  addressLine1 addressLine2 city state zipcode country
97
331
  shippingAddress { addressLine1 addressLine2 city state zipcode country }
98
332
  }
99
- customerPortalOrganization { id name }
333
+ customerPortalOrganization { id name nozleBranding }
100
334
  }`;
101
335
  var subscriptionsQuery = `query SdkPortalSubscriptions($page: Int) {
102
336
  customerPortalSubscriptions(status: [active], limit: 20, page: $page) {
@@ -145,40 +379,55 @@ var topUpMutation = `mutation SdkPortalTopUp($input: CreateCustomerPortalWalletT
145
379
  }`;
146
380
 
147
381
  // src/components/portal/styles.ts
382
+ var line = `${T.borderWidth} solid ${T.border}`;
383
+ var r = (factor) => `calc(${T.radius} * ${factor})`;
148
384
  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}
385
+ .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
386
  .nozle-billing-portal *{box-sizing:border-box}
151
387
  .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}
388
+ .nozle-billing-portal h2{font:${T.fontTitle}}
389
+ .nozle-billing-portal h3{font:${T.fontHeading}}
154
390
  .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)}
391
+ .nozle-billing-portal section{margin-top:28px;padding-top:24px;border-top:${line}}
392
+ .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}
393
+ .nozle-billing-portal button:hover,.nozle-billing-portal .nzp-link:hover{background:${T.muted}}
158
394
  .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}
395
+ .nozle-billing-portal :focus-visible{outline:2px solid ${T.primary};outline-offset:3px}
396
+ .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}}
397
+ .nozle-billing-portal .nzp-primary:hover{filter:brightness(.92);background:${T.primary}}
398
+ .nozle-billing-portal .nzp-muted{color:${T.mutedForeground}}
399
+ .nozle-billing-portal .nzp-notice{color:${T.successText}}
163
400
  .nozle-billing-portal .nzp-row{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}
164
401
  .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}
402
+ .nozle-billing-portal .nzp-card{background:${T.card};border:${line};border-radius:${r(0.85)};box-shadow:${T.shadow};padding:20px;min-width:0}
403
+ .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}}
404
+ .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}
405
+ .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}
406
+ .nozle-billing-portal .nzp-card-actions>.nozle-cancellation>button{margin-left:auto}
407
+ .nozle-billing-portal .nzp-price{font:${T.fontMetric};font-variant-numeric:tabular-nums;margin:12px 0 4px}
408
+ .nozle-billing-portal .nzp-price .nzp-muted{font:${T.fontMeta}}
409
+ .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}
410
+ .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}
411
+ .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}
412
+ .nozle-billing-portal .nzp-table{overflow-x:auto;margin-top:16px;background:${T.card};border:${line};border-radius:${T.radius}}
413
+ .nozle-billing-portal table{width:100%;border-collapse:collapse;text-align:left;font:inherit}
414
+ .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}
415
+ .nozle-billing-portal td{height:48px;padding:14px 20px;border-bottom:${T.borderWidth} solid ${T.rowLine};white-space:nowrap}
416
+ .nozle-billing-portal tbody tr:last-child td{border-bottom:0}
417
+ .nozle-billing-portal .nzp-num{text-align:right}
418
+ .nozle-billing-portal td.nzp-num{font-family:${T.fontNumbers};font-variant-numeric:tabular-nums}
419
+ .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}}
420
+ .nozle-billing-portal .nzp-search:focus-within{border-color:${T.foreground}}
421
+ .nozle-billing-portal .nzp-search input{flex:1;height:100%;border:0;padding:0;background:transparent;color:${T.foreground};outline:none}
422
+ .nozle-billing-portal .nzp-search input::placeholder{color:${T.mutedForeground}}
423
+ .nozle-billing-portal label{display:grid;gap:6px;font-weight:500;min-width:0}
424
+ .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
425
  .nozle-billing-portal .nzp-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr));gap:16px;margin:20px 0}
177
426
  .nozle-billing-portal .nzp-actions{display:flex;align-items:flex-end;gap:10px;flex-wrap:wrap;margin-top:20px}
178
427
  .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)}
428
+ .nozle-billing-portal dt{font:${T.fontMeta};color:${T.mutedForeground}}
180
429
  .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}
430
+ .nozle-billing-portal footer{margin-top:30px;text-align:center;color:${T.mutedForeground};font:${T.fontMeta}}
182
431
  `;
183
432
 
184
433
  // src/components/portal/timezones.ts
@@ -362,6 +611,25 @@ function compareDecimalStrings(left, right) {
362
611
  }
363
612
  return magnitude * a.sign;
364
613
  }
614
+ function formatDecimalString(value, maximumFractionDigits = 12) {
615
+ const parsed = parseDecimal(value);
616
+ if (!parsed) return value;
617
+ const fractionLimit = Math.max(
618
+ 0,
619
+ Math.min(30, Math.trunc(maximumFractionDigits))
620
+ );
621
+ const firstGroupLength = parsed.integer.length % 3 || 3;
622
+ const groups = [parsed.integer.slice(0, firstGroupLength)];
623
+ for (let index = firstGroupLength; index < parsed.integer.length; index += 3) {
624
+ groups.push(parsed.integer.slice(index, index + 3));
625
+ }
626
+ const groupedInteger = groups.join(",");
627
+ const fraction = parsed.fraction.slice(0, fractionLimit);
628
+ return `${parsed.sign < 0 ? "-" : ""}${groupedInteger}${fraction ? `.${fraction}` : ""}`;
629
+ }
630
+ function creditUnitLabel(value, unitName) {
631
+ return compareDecimalStrings(value, "1") === 0 ? unitName : `${unitName}s`;
632
+ }
365
633
 
366
634
  // src/components/portal/amounts.ts
367
635
  function walletAmountCents(credits, rate, currencyDigits) {
@@ -382,16 +650,16 @@ function walletAmountCents(credits, rate, currencyDigits) {
382
650
  return `${padded.slice(0, -scale)}.${padded.slice(-scale)}`;
383
651
  }
384
652
  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}
653
+ .nozle-cancellation{font:${T.fontBody};color:${T.foreground}}
654
+ .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
655
  .nozle-cancellation button:disabled{opacity:.55;cursor:wait}
388
- .nozle-cancellation :focus-visible{outline:2px solid var(--nozle-portal-accent,#526949);outline-offset:3px}
656
+ .nozle-cancellation :focus-visible{outline:2px solid ${T.primary};outline-offset:3px}
389
657
  .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}
658
+ .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}
659
+ .nozle-cancellation .nzc-dialog h3{margin:0 0 16px;font:${T.fontTitle};letter-spacing:normal;color:${T.foreground}}
660
+ .nozle-cancellation .nzc-dialog p{margin:12px 0}
393
661
  .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}
662
+ .nozle-cancellation [role=alert],.nozle-cancellation .nzc-danger{color:${T.dangerText}}
395
663
  `;
396
664
  function formatDate(value, locale, timezone) {
397
665
  const timeZone = timezone.startsWith("TZ_") ? portalTimezones[timezone] ?? "UTC" : timezone;
@@ -421,8 +689,10 @@ function CancellationFlow({
421
689
  onError,
422
690
  locale = "en-US",
423
691
  timezone = "UTC",
424
- nonce
692
+ nonce,
693
+ appearance
425
694
  }) {
695
+ const { root } = useAppearance(appearance);
426
696
  const [open, setOpen] = useState(false);
427
697
  const [busy, setBusy] = useState(false);
428
698
  const [preview, setPreview] = useState();
@@ -609,7 +879,7 @@ function CancellationFlow({
609
879
  );
610
880
  });
611
881
  }
612
- return /* @__PURE__ */ jsxs("div", { className: "nozle-cancellation", children: [
882
+ return /* @__PURE__ */ jsxs("div", { ...root("cancellationControl", void 0, "nozle-cancellation"), children: [
613
883
  /* @__PURE__ */ jsx("style", { nonce, children: styles }),
614
884
  notice && /* @__PURE__ */ jsx("p", { role: "status", children: notice }),
615
885
  active && !expired && /* @__PURE__ */ jsx("button", { ref: trigger, type: "button", onClick: () => void loadPreview(), children: ending ? "Keep my subscription" : "Cancel subscription" }),
@@ -694,6 +964,7 @@ function CancellationFlow({
694
964
  "button",
695
965
  {
696
966
  type: "button",
967
+ className: preview.operation === "cancel" ? "nzc-danger" : void 0,
697
968
  disabled: busy,
698
969
  onClick: () => void confirm(),
699
970
  children: request.current ? "Check status / retry" : preview.operation === "cancel" ? "Confirm cancellation" : "Confirm keep subscription"
@@ -705,66 +976,6 @@ function CancellationFlow({
705
976
  ) })
706
977
  ] });
707
978
  }
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
979
 
769
980
  // src/components/billing/razorpay-checkout.ts
770
981
  var loadingScript;
@@ -979,8 +1190,10 @@ function CheckoutButton({
979
1190
  onStripeClientSecret,
980
1191
  onSuccess,
981
1192
  onComplete,
982
- onScheduled
1193
+ onScheduled,
1194
+ appearance
983
1195
  }) {
1196
+ const { root, vars } = useAppearance(appearance);
984
1197
  const { createCheckout, verifyCheckout, getCheckoutStatus } = useBillingContext();
985
1198
  const attempt = useRef(void 0);
986
1199
  const inFlight = useRef(false);
@@ -1043,24 +1256,13 @@ function CheckoutButton({
1043
1256
  type: "button",
1044
1257
  onClick: () => void handleClick(),
1045
1258
  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
- },
1259
+ ...root("checkoutButton", { ...buttonStyle("primary", loading), ...style }, className),
1058
1260
  "aria-busy": loading,
1059
1261
  children: loading ? "Loading..." : label
1060
1262
  }
1061
1263
  ),
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 })
1264
+ processing && /* @__PURE__ */ jsx("span", { role: "status", style: { ...vars, ...text.body }, children: "Payment is processing. Confirmation may take a little while." }),
1265
+ error && /* @__PURE__ */ jsx("span", { role: "alert", style: { ...vars, ...text.body, color: T.dangerText }, children: error.message })
1064
1266
  ] });
1065
1267
  }
1066
1268
  var CheckoutContext = createContext(null);
@@ -1071,23 +1273,62 @@ function useCheckout() {
1071
1273
  }
1072
1274
  return ctx;
1073
1275
  }
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"
1276
+ function rgb(color) {
1277
+ const match = color?.match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/);
1278
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
1279
+ }
1280
+ function stripeColor(color) {
1281
+ return rgb(color) ? color : color?.startsWith("#") ? color : void 0;
1282
+ }
1283
+ function toStripeAppearance(colors) {
1284
+ const background = rgb(colors.background);
1285
+ const dark = background ? (0.2126 * background[0] + 0.7152 * background[1] + 0.0722 * background[2]) / 255 < 0.5 : false;
1286
+ const variables = Object.fromEntries(
1287
+ Object.entries({
1288
+ colorPrimary: stripeColor(colors.primary),
1289
+ colorBackground: stripeColor(colors.background),
1290
+ colorText: stripeColor(colors.foreground),
1291
+ colorDanger: stripeColor(colors.danger),
1292
+ borderRadius: colors.radius,
1293
+ fontFamily: colors.font
1294
+ }).filter(([, value]) => value)
1295
+ );
1296
+ return { theme: dark ? "night" : "stripe", variables };
1297
+ }
1298
+ function useStripeAppearance(appearanceKey) {
1299
+ const probe = useRef(null);
1300
+ const [stripeAppearance, setStripeAppearance] = useState(null);
1301
+ useEffect(() => {
1302
+ if (!probe.current) return;
1303
+ const style = getComputedStyle(probe.current);
1304
+ setStripeAppearance(
1305
+ toStripeAppearance({
1306
+ primary: style.color,
1307
+ background: style.backgroundColor,
1308
+ foreground: style.borderTopColor,
1309
+ danger: style.borderBottomColor,
1310
+ radius: style.borderTopLeftRadius,
1311
+ font: style.fontFamily
1312
+ })
1313
+ );
1314
+ }, [appearanceKey]);
1315
+ const probeElement = /* @__PURE__ */ jsx(
1316
+ "span",
1317
+ {
1318
+ ref: probe,
1319
+ "aria-hidden": "true",
1320
+ style: {
1321
+ display: "none",
1322
+ color: T.primary,
1323
+ backgroundColor: T.card,
1324
+ borderTop: `1px solid ${T.foreground}`,
1325
+ borderBottom: `1px solid ${T.danger}`,
1326
+ borderRadius: T.radius,
1327
+ fontFamily: T.font
1328
+ }
1089
1329
  }
1090
- };
1330
+ );
1331
+ return { probeElement, stripeAppearance };
1091
1332
  }
1092
1333
  function CheckoutInner({
1093
1334
  submitLabel,
@@ -1097,10 +1338,12 @@ function CheckoutInner({
1097
1338
  className,
1098
1339
  style,
1099
1340
  returnUrl,
1100
- children
1341
+ children,
1342
+ appearance
1101
1343
  }) {
1102
1344
  const stripe = useStripe();
1103
1345
  const elements = useElements();
1346
+ const { el } = useAppearance(appearance);
1104
1347
  const [isProcessing, setIsProcessing] = useState(false);
1105
1348
  const [error, setError] = useState(null);
1106
1349
  const [isReady, setIsReady] = useState(false);
@@ -1134,7 +1377,7 @@ function CheckoutInner({
1134
1377
  setIsProcessing(false);
1135
1378
  }
1136
1379
  }
1137
- return /* @__PURE__ */ jsx(CheckoutContext.Provider, { value: { confirmPayment, isProcessing, error }, children: /* @__PURE__ */ jsxs("div", { className, style, children: [
1380
+ return /* @__PURE__ */ jsx(CheckoutContext.Provider, { value: { confirmPayment, isProcessing, error }, children: /* @__PURE__ */ jsxs("div", { children: [
1138
1381
  /* @__PURE__ */ jsx("style", { children: `@keyframes nozle-skeleton-pulse { 0%{background-position:200% 0} 100%{background-position:-200% 0} }` }),
1139
1382
  !isReady && /* @__PURE__ */ jsx(
1140
1383
  "div",
@@ -1142,8 +1385,8 @@ function CheckoutInner({
1142
1385
  "data-testid": "checkout-skeleton",
1143
1386
  style: {
1144
1387
  height: "200px",
1145
- borderRadius: "var(--nozle-radius, 0.5rem)",
1146
- background: "linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)",
1388
+ borderRadius: T.radius,
1389
+ background: `linear-gradient(90deg, ${T.muted} 25%, ${T.skeleton} 50%, ${T.muted} 75%)`,
1147
1390
  backgroundSize: "200% 100%",
1148
1391
  animation: "nozle-skeleton-pulse 1.5s ease-in-out infinite"
1149
1392
  }
@@ -1158,16 +1401,7 @@ function CheckoutInner({
1158
1401
  }
1159
1402
  }
1160
1403
  ),
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
- ),
1404
+ error !== null && /* @__PURE__ */ jsx("p", { ...el("checkout__error", { color: T.dangerText, margin: "0.5rem 0 0" }), children: error.message }),
1171
1405
  children ?? /* @__PURE__ */ jsx(
1172
1406
  "button",
1173
1407
  {
@@ -1175,18 +1409,7 @@ function CheckoutInner({
1175
1409
  onClick: () => void confirmPayment(),
1176
1410
  disabled: isProcessing || !stripe,
1177
1411
  "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
- },
1412
+ ...el("checkout__submit", { ...buttonStyle("primary", isProcessing), marginTop: "1rem", width: "100%" }),
1190
1413
  children: isProcessing ? "Processing..." : submitLabel ?? "Pay now"
1191
1414
  }
1192
1415
  )
@@ -1204,19 +1427,21 @@ function StripeCheckout({
1204
1427
  onReady,
1205
1428
  className,
1206
1429
  style,
1207
- children
1430
+ children,
1431
+ appearance: themeAppearance
1208
1432
  }) {
1433
+ const { root } = useAppearance(themeAppearance);
1434
+ const { probeElement, stripeAppearance } = useStripeAppearance(JSON.stringify(root("checkout").style));
1209
1435
  const stripePromise = useMemo(
1210
1436
  () => loadStripe(publishableKey, stripeAccount ? { stripeAccount } : void 0),
1211
1437
  [publishableKey, stripeAccount]
1212
1438
  );
1213
- const appearance = useMemo(() => buildStripeAppearance(), []);
1214
1439
  const embeddedOptions = useMemo(
1215
1440
  () => ({ clientSecret, onComplete }),
1216
1441
  [clientSecret, onComplete]
1217
1442
  );
1218
1443
  if (clientSecret.startsWith("cs_")) {
1219
- return /* @__PURE__ */ jsx("div", { className, style, children: /* @__PURE__ */ jsx(
1444
+ return /* @__PURE__ */ jsx("div", { ...root("checkout", { ...text.body, ...style }, className), children: /* @__PURE__ */ jsx(
1220
1445
  EmbeddedCheckoutProvider,
1221
1446
  {
1222
1447
  stripe: stripePromise,
@@ -1225,23 +1450,26 @@ function StripeCheckout({
1225
1450
  }
1226
1451
  ) });
1227
1452
  }
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);
1453
+ const options = stripeAppearance && { clientSecret, appearance: stripeAppearance };
1454
+ return /* @__PURE__ */ jsxs("div", { ...root("checkout", { ...text.body, ...style }, className), children: [
1455
+ probeElement,
1456
+ options && /* @__PURE__ */ jsx(Elements, { stripe: stripePromise, options, children: /* @__PURE__ */ jsx(
1457
+ CheckoutInner,
1458
+ {
1459
+ submitLabel,
1460
+ onSuccess,
1461
+ onError,
1462
+ onReady,
1463
+ returnUrl,
1464
+ children,
1465
+ appearance: themeAppearance
1466
+ }
1467
+ ) }, clientSecret)
1468
+ ] });
1242
1469
  }
1243
1470
  function ProviderCheckout(props) {
1244
1471
  const billing = useOptionalBillingContext();
1472
+ const { root, el } = useAppearance(props.appearance);
1245
1473
  const [busy, setBusy] = useState(false);
1246
1474
  const [error, setError] = useState(null);
1247
1475
  const [processing, setProcessing] = useState(false);
@@ -1291,17 +1519,19 @@ function ProviderCheckout(props) {
1291
1519
  submitLabel: props.submitLabel,
1292
1520
  onSuccess: props.onSuccess,
1293
1521
  onComplete: props.onComplete,
1294
- onError: props.onError
1522
+ onError: props.onError,
1523
+ className: props.className,
1524
+ appearance: props.appearance
1295
1525
  }
1296
1526
  );
1297
1527
  }
1298
- return /* @__PURE__ */ jsx("p", { role: "alert", children: "Payment setup is incomplete. Contact support to continue." });
1528
+ 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
1529
  }
1300
1530
  return /* @__PURE__ */ jsx(
1301
1531
  CheckoutContext.Provider,
1302
1532
  {
1303
1533
  value: { confirmPayment: pay, isProcessing: busy, error },
1304
- children: /* @__PURE__ */ jsxs("div", { className: props.className, children: [
1534
+ children: /* @__PURE__ */ jsxs("div", { ...root("checkout", text.body, props.className), children: [
1305
1535
  /* @__PURE__ */ jsx(
1306
1536
  "button",
1307
1537
  {
@@ -1309,28 +1539,30 @@ function ProviderCheckout(props) {
1309
1539
  disabled: busy,
1310
1540
  "aria-busy": busy,
1311
1541
  onClick: () => void pay(),
1542
+ ...el("checkout__submit", buttonStyle("primary", busy)),
1312
1543
  children: busy ? "Processing\u2026" : props.submitLabel || "Pay now"
1313
1544
  }
1314
1545
  ),
1315
1546
  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 })
1547
+ error && /* @__PURE__ */ jsx("p", { role: "alert", ...el("checkout__error", { color: T.dangerText }), children: error.message })
1317
1548
  ] })
1318
1549
  }
1319
1550
  );
1320
1551
  }
1321
1552
  function Checkout(props) {
1322
- return "checkout" in props ? /* @__PURE__ */ jsx(ProviderCheckout, { ...props }) : /* @__PURE__ */ jsx(StripeCheckout, { ...props });
1553
+ return /* @__PURE__ */ jsx(NozleBranding, { children: "checkout" in props ? /* @__PURE__ */ jsx(ProviderCheckout, { ...props }) : /* @__PURE__ */ jsx(StripeCheckout, { ...props }) });
1323
1554
  }
1324
1555
  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}
1556
+ .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}}
1557
+ .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}
1558
+ .nozle-upgradeModal h2{font:${T.fontTitle};letter-spacing:normal;color:${T.foreground};margin:0 0 16px}
1559
+ .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}
1560
+ .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}}
1561
+ .nozle-upgradeModal button:disabled{opacity:.55;cursor:wait}
1562
+ .nozle-upgradeModal :focus-visible{outline:2px solid ${T.primary};outline-offset:3px}
1563
+ .nozle-upgradeModal .nozle-upgradeModal__actions{display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end;margin-top:20px}
1564
+ .nozle-upgradeModal .nzu-back{margin-right:auto}
1565
+ .nozle-upgradeModal [role=alert]{color:${T.dangerText}}
1334
1566
  `;
1335
1567
  function UpgradeModal(props) {
1336
1568
  const billing = useOptionalBillingContext();
@@ -1341,6 +1573,7 @@ function UpgradeModal(props) {
1341
1573
  const dialog = useRef(null);
1342
1574
  const titleId = useId();
1343
1575
  const busy = Boolean(props.busy || confirming);
1576
+ const { root, el } = useAppearance(props.appearance);
1344
1577
  useEffect(() => {
1345
1578
  if (!props.isOpen) return;
1346
1579
  const previous = document.activeElement;
@@ -1400,17 +1633,17 @@ function UpgradeModal(props) {
1400
1633
  }
1401
1634
  }
1402
1635
  if (!props.isOpen) return null;
1403
- const money2 = (value) => new Intl.NumberFormat(props.locale ?? "en-US", {
1636
+ const money3 = (value) => new Intl.NumberFormat(props.locale ?? "en-US", {
1404
1637
  style: "currency",
1405
1638
  currency: props.preview?.currency ?? "USD"
1406
1639
  }).format(value);
1407
- return /* @__PURE__ */ jsxs("div", { className: "nozle-upgrade-modal", children: [
1640
+ return /* @__PURE__ */ jsxs("div", { ...root("upgradeModal", void 0, "nozle-upgrade-modal"), children: [
1408
1641
  /* @__PURE__ */ jsx("style", { nonce: props.nonce, children: styles2 }),
1409
1642
  /* @__PURE__ */ jsxs(
1410
1643
  "div",
1411
1644
  {
1412
1645
  ref: dialog,
1413
- className: "nzu-dialog",
1646
+ ...el("upgradeModal__dialog"),
1414
1647
  role: "dialog",
1415
1648
  "aria-modal": "true",
1416
1649
  "aria-labelledby": titleId,
@@ -1448,15 +1681,15 @@ function UpgradeModal(props) {
1448
1681
  props.preview && /* @__PURE__ */ jsxs("div", { children: [
1449
1682
  props.preview.credit > 0 && /* @__PURE__ */ jsxs("p", { children: [
1450
1683
  "Credits applied: ",
1451
- money2(-props.preview.credit)
1684
+ money3(-props.preview.credit)
1452
1685
  ] }),
1453
1686
  /* @__PURE__ */ jsxs("p", { children: [
1454
1687
  "New plan charge: ",
1455
- money2(props.preview.debit)
1688
+ money3(props.preview.debit)
1456
1689
  ] }),
1457
1690
  /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsxs("strong", { children: [
1458
1691
  "Due today: ",
1459
- money2(props.preview.net)
1692
+ money3(props.preview.net)
1460
1693
  ] }) }),
1461
1694
  /* @__PURE__ */ jsxs("p", { children: [
1462
1695
  "Next billing date:",
@@ -1468,18 +1701,21 @@ function UpgradeModal(props) {
1468
1701
  ] })
1469
1702
  ] }),
1470
1703
  props.children,
1471
- /* @__PURE__ */ jsxs("div", { className: "nzu-buttons", children: [
1704
+ /* @__PURE__ */ jsxs("div", { ...el("upgradeModal__actions"), children: [
1705
+ props.onBack && /* @__PURE__ */ jsx("button", { type: "button", className: "nzu-back", onClick: props.onBack, disabled: busy, children: "Back" }),
1472
1706
  /* @__PURE__ */ jsx("button", { type: "button", onClick: props.onCancel, disabled: busy, children: "Close" }),
1473
1707
  /* @__PURE__ */ jsx(
1474
1708
  "button",
1475
1709
  {
1476
1710
  type: "button",
1711
+ className: "nzu-primary",
1477
1712
  onClick: () => void handleConfirm(),
1478
1713
  disabled: busy || props.confirmDisabled,
1479
1714
  children: busy ? "Checking subscription\u2026" : props.confirmLabel ?? "Confirm"
1480
1715
  }
1481
1716
  )
1482
- ] })
1717
+ ] }),
1718
+ /* @__PURE__ */ jsx(NozleBranding, {})
1483
1719
  ]
1484
1720
  }
1485
1721
  )
@@ -1487,7 +1723,7 @@ function UpgradeModal(props) {
1487
1723
  }
1488
1724
 
1489
1725
  // src/components/billing/amounts.ts
1490
- function formatMinorAmount(value, currency, locale = "en-US") {
1726
+ function formatMinorAmount(value, currency, locale = "en-US", options = {}) {
1491
1727
  const raw = String(value);
1492
1728
  if (!/^-?\d+$/.test(raw) || typeof value === "number" && !Number.isSafeInteger(value)) {
1493
1729
  return "Amount unavailable";
@@ -1503,12 +1739,16 @@ function formatMinorAmount(value, currency, locale = "en-US") {
1503
1739
  const scale = 10n ** BigInt(digits);
1504
1740
  const whole = absolute / scale;
1505
1741
  const signed = minor < 0n ? whole === 0n ? -0 : -whole : whole;
1506
- const decimal = String(absolute % scale).padStart(digits, "0");
1742
+ const remainder = absolute % scale;
1743
+ const trim = options.trimZeroFraction === true && remainder === 0n;
1744
+ const decimal = String(remainder).padStart(digits, "0");
1507
1745
  const digitFormatter = new Intl.NumberFormat(locale, {
1508
1746
  useGrouping: false
1509
1747
  });
1510
1748
  const fraction = [...decimal].map((digit) => digitFormatter.format(Number(digit))).join("");
1511
- return formatter.formatToParts(signed).map((part) => part.type === "fraction" ? fraction : part.value).join("");
1749
+ return formatter.formatToParts(signed).filter(
1750
+ (part) => !trim || part.type !== "fraction" && part.type !== "decimal"
1751
+ ).map((part) => part.type === "fraction" ? fraction : part.value).join("");
1512
1752
  } catch {
1513
1753
  return "Amount unavailable";
1514
1754
  }
@@ -1527,17 +1767,181 @@ function formatBillingDate(value, locale = "en-US", timezone) {
1527
1767
  }).format(new Date(value));
1528
1768
  }
1529
1769
  }
1770
+
1771
+ // src/components/pricing/plan-features.ts
1772
+ function number(value) {
1773
+ const parsed = Number(value);
1774
+ return Number.isFinite(parsed) ? parsed.toLocaleString("en-US", { maximumFractionDigits: 6 }) : String(value);
1775
+ }
1776
+ function money(amount, currency) {
1777
+ const parsed = Number(amount);
1778
+ if (!Number.isFinite(parsed)) return `${amount} ${currency}`;
1779
+ try {
1780
+ return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 6 }).format(parsed);
1781
+ } catch {
1782
+ return `${currency} ${number(amount)}`;
1783
+ }
1784
+ }
1785
+ function words(code) {
1786
+ return code.replace(/_/g, " ");
1787
+ }
1788
+ function entitlementRows(entitlement) {
1789
+ if (entitlement.feature_type === "config") {
1790
+ return (entitlement.privileges ?? []).flatMap((privilege) => {
1791
+ if (privilege.value === null || privilege.value === false) return [];
1792
+ if (privilege.value === true) return [`${entitlement.name}: ${privilege.name}`];
1793
+ return [`${entitlement.name}: ${privilege.value}`];
1794
+ });
1795
+ }
1796
+ if (entitlement.feature_type === "metered" && entitlement.usage_limit != null) {
1797
+ return [`${number(entitlement.usage_limit)} ${entitlement.name}`];
1798
+ }
1799
+ return [entitlement.name];
1800
+ }
1801
+ function creditGrantRow(rule) {
1802
+ const credits = `${number(rule.amount)} ${words(rule.credit_system_code)}`;
1803
+ if (rule.interval === "lifetime") return `${credits} (one time)`;
1804
+ const count = rule.interval_count ?? 1;
1805
+ return count === 1 ? `${credits} every ${rule.interval}` : `${credits} every ${count} ${rule.interval}s`;
1806
+ }
1807
+ function isZero(amount) {
1808
+ return amount === void 0 || Number(amount) === 0;
1809
+ }
1810
+ function rangeLabel(range, unit) {
1811
+ const from = number(range.from_value);
1812
+ return range.to_value === null ? `${from}+ ${unit}` : `${from} to ${number(range.to_value)} ${unit}`;
1813
+ }
1814
+ function rangeRows(ranges, unit, currency) {
1815
+ return ranges.map((range) => {
1816
+ const parts = [];
1817
+ if (!isZero(range.flat_amount)) parts.push(`${money(range.flat_amount, currency)} flat`);
1818
+ if (!isZero(range.per_unit_amount)) parts.push(`${money(range.per_unit_amount, currency)} each`);
1819
+ if (!isZero(range.rate)) parts.push(`${number(range.rate)}%`);
1820
+ return `${rangeLabel(range, unit)}: ${parts.length ? parts.join(" + ") : "free"}`;
1821
+ });
1822
+ }
1823
+ function chargeRows(charge, currency) {
1824
+ const properties = charge.properties ?? {};
1825
+ const unit = charge.feature_code ? words(charge.feature_code) : "unit";
1826
+ switch (charge.charge_model) {
1827
+ case "standard":
1828
+ return properties.amount === void 0 ? [] : [`${money(properties.amount, currency)} per ${unit}`];
1829
+ case "package": {
1830
+ if (properties.amount === void 0 || !properties.package_size) return [];
1831
+ const free = properties.free_units ? `, first ${number(properties.free_units)} free` : "";
1832
+ return [`${money(properties.amount, currency)} per ${number(properties.package_size)} ${unit}${free}`];
1833
+ }
1834
+ case "graduated":
1835
+ return rangeRows(properties.graduated_ranges ?? [], unit, currency);
1836
+ case "volume":
1837
+ return rangeRows(properties.volume_ranges ?? [], unit, currency);
1838
+ case "graduated_percentage":
1839
+ return rangeRows(properties.graduated_percentage_ranges ?? [], unit, currency);
1840
+ case "percentage": {
1841
+ if (properties.rate === void 0) return [];
1842
+ const fixed = isZero(properties.fixed_amount) ? "" : ` + ${money(properties.fixed_amount, currency)} per ${unit}`;
1843
+ return [`${number(properties.rate)}% of each ${unit}${fixed}`];
1844
+ }
1845
+ default:
1846
+ return [];
1847
+ }
1848
+ }
1849
+ function planFeatures(plan) {
1850
+ const rows = (plan.entitlements ?? []).flatMap(entitlementRows);
1851
+ rows.push(...(plan.credit_grant_rules ?? []).map(creditGrantRow));
1852
+ for (const charge of plan.charges ?? []) rows.push(...chargeRows(charge, plan.amount_currency));
1853
+ return rows;
1854
+ }
1855
+ var INTERVAL_LABELS = {
1856
+ weekly: "week",
1857
+ monthly: "month",
1858
+ quarterly: "quarter",
1859
+ semiannual: "6 months",
1860
+ yearly: "year"
1861
+ };
1862
+ function intervalLabel(interval) {
1863
+ return INTERVAL_LABELS[interval] ?? interval;
1864
+ }
1865
+ function formatPlanPrice(amountCents, currency) {
1866
+ const amount = amountCents / 100;
1867
+ try {
1868
+ return new Intl.NumberFormat("en-US", {
1869
+ style: "currency",
1870
+ currency,
1871
+ minimumFractionDigits: Number.isInteger(amount) ? 0 : 2,
1872
+ maximumFractionDigits: 2
1873
+ }).format(amount);
1874
+ } catch {
1875
+ return `${currency} ${amount}`;
1876
+ }
1877
+ }
1878
+ function entitlementValue(entitlement) {
1879
+ if (entitlement.feature_type === "config") {
1880
+ const values = (entitlement.privileges ?? []).filter((privilege) => privilege.value !== null && privilege.value !== false).map((privilege) => privilege.value === true ? privilege.name : String(privilege.value));
1881
+ return values.length ? values.join(", ") : false;
1882
+ }
1883
+ if (entitlement.feature_type === "metered" && entitlement.usage_limit != null) {
1884
+ return number(entitlement.usage_limit);
1885
+ }
1886
+ return true;
1887
+ }
1888
+ function comparisonRows(plans) {
1889
+ const rows = /* @__PURE__ */ new Map();
1890
+ const row = (key, label) => {
1891
+ let existing = rows.get(key);
1892
+ if (!existing) {
1893
+ existing = { key, label, values: {} };
1894
+ rows.set(key, existing);
1895
+ }
1896
+ return existing;
1897
+ };
1898
+ for (const plan of plans) {
1899
+ for (const entitlement of plan.entitlements ?? []) {
1900
+ row(`entitlement:${entitlement.code}`, entitlement.name).values[plan.code] = entitlementValue(entitlement);
1901
+ }
1902
+ for (const rule of plan.credit_grant_rules ?? []) {
1903
+ row(`credit:${rule.credit_system_code}`, words(rule.credit_system_code)).values[plan.code] = creditGrantRow(rule);
1904
+ }
1905
+ }
1906
+ const all = [...rows.values()];
1907
+ for (const item of all) {
1908
+ for (const plan of plans) item.values[plan.code] ??= false;
1909
+ }
1910
+ return all;
1911
+ }
1912
+ function sortPlansByPrice(plans) {
1913
+ const currencyRank = /* @__PURE__ */ new Map();
1914
+ for (const plan of plans) {
1915
+ if (!currencyRank.has(plan.amount_currency)) currencyRank.set(plan.amount_currency, currencyRank.size);
1916
+ }
1917
+ return plans.map((plan, index) => ({ plan, index })).sort(
1918
+ (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
1919
+ ).map(({ plan }) => plan);
1920
+ }
1530
1921
  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}
1922
+ .nozle-plan-change{font:${T.fontBody};color:${T.foreground};margin-top:12px}
1923
+ .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
1924
  .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}
1925
+ .nozle-plan-change :focus-visible{outline:2px solid ${T.primary};outline-offset:3px}
1926
+ .nozle-plan-change [role=alert]{color:${T.dangerText}}
1536
1927
  .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)}
1928
+ .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
1929
  .nozle-upgrade-modal dl{display:block;margin:16px 0;padding:0}
1539
1930
  .nozle-upgrade-modal dl div{display:flex;justify-content:space-between;gap:16px;margin:12px 0}
1931
+ .nozle-upgrade-modal dt{font:${T.fontMeta};color:${T.mutedForeground}}
1540
1932
  .nozle-upgrade-modal dd{margin:0;text-align:right}
1933
+ .nozle-upgrade-modal label{font:${T.fontBody};font-weight:500}
1934
+ .nozle-plan-change .nzpc-plans{display:grid;gap:10px}
1935
+ .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}}
1936
+ .nozle-plan-change label.nzpc-plan[data-selected]{border-color:${T.primary};background:${T.primarySoft}}
1937
+ .nozle-plan-change label.nzpc-plan input{flex:none;width:auto;margin:4px 0 0;padding:0;accent-color:${T.primary}}
1938
+ .nozle-plan-change .nzpc-plan-body{display:grid;gap:4px;min-width:0;flex:1}
1939
+ .nozle-plan-change .nzpc-plan-head{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
1940
+ .nozle-plan-change .nzpc-plan-name{font:${T.fontHeading}}
1941
+ .nozle-plan-change .nzpc-plan-op{font:${T.labelFont};letter-spacing:${T.labelSpacing};text-transform:${T.labelTransform};color:${T.mutedForeground}}
1942
+ .nozle-plan-change .nzpc-plan-price{font-variant-numeric:tabular-nums}
1943
+ .nozle-plan-change .nzpc-plan-muted{font:${T.fontMeta};color:${T.mutedForeground};margin:0}
1944
+ .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
1945
  `;
1542
1946
  var paymentPending = (state) => ["awaiting_payment", "processing", "needs_review"].includes(
1543
1947
  state.checkoutStatus
@@ -1564,8 +1968,10 @@ function PlanChangeFlow({
1564
1968
  returnUrl,
1565
1969
  locale = "en-US",
1566
1970
  timezone = "UTC",
1567
- nonce
1971
+ nonce,
1972
+ appearance
1568
1973
  }) {
1974
+ const { root } = useAppearance(appearance);
1569
1975
  const owner = useMemo(
1570
1976
  () => ({ controller: new AbortController() }),
1571
1977
  [subscriptionId, actions]
@@ -1580,6 +1986,11 @@ function PlanChangeFlow({
1580
1986
  null
1581
1987
  );
1582
1988
  const [target, setTarget] = useState("");
1989
+ const billing = useOptionalBillingContext();
1990
+ const catalog = useQuery(
1991
+ { ...catalogQuery(billing?.client), enabled: Boolean(billing) },
1992
+ billing?.queryClient ?? idleQueryClient
1993
+ );
1583
1994
  const [quote, setQuote] = useState();
1584
1995
  const [checkout, setCheckout] = useState(null);
1585
1996
  const [withdrawTarget, setWithdrawTarget] = useState();
@@ -1847,7 +2258,7 @@ function PlanChangeFlow({
1847
2258
  await reconcile(signal);
1848
2259
  });
1849
2260
  const selected = state?.eligiblePlans.find((plan) => plan.code === target);
1850
- const money2 = (amount) => formatMinorAmount(
2261
+ const money3 = (amount) => formatMinorAmount(
1851
2262
  amount,
1852
2263
  quote?.currency ?? state?.currentPlan.currency ?? "USD",
1853
2264
  locale
@@ -1855,7 +2266,7 @@ function PlanChangeFlow({
1855
2266
  const blocked = Boolean(
1856
2267
  state?.endingAt || state?.pendingChange || state && paymentPending(state) || state?.blockedReason || pending.current?.accepted
1857
2268
  );
1858
- return /* @__PURE__ */ jsxs("div", { className: "nozle-plan-change", children: [
2269
+ return /* @__PURE__ */ jsxs("div", { ...root("planChangeControl", void 0, "nozle-plan-change"), children: [
1859
2270
  /* @__PURE__ */ jsx("style", { nonce, children: styles3 }),
1860
2271
  !state ? /* @__PURE__ */ jsxs(Fragment, { children: [
1861
2272
  /* @__PURE__ */ jsx("p", { role: "status", children: "Loading plan changes\u2026" }),
@@ -1953,6 +2364,7 @@ function PlanChangeFlow({
1953
2364
  /* @__PURE__ */ jsxs(
1954
2365
  UpgradeModal,
1955
2366
  {
2367
+ appearance,
1956
2368
  isOpen: mode !== null,
1957
2369
  planCode: target,
1958
2370
  title: mode === "withdraw" ? "Keep your current plan?" : mode === "choose" ? "Change plan" : `Confirm ${quote?.operation ?? "plan change"}`,
@@ -1961,42 +2373,55 @@ function PlanChangeFlow({
1961
2373
  confirmDisabled: mode === "choose" && (!target || blocked) || mode === "confirm" && !quote,
1962
2374
  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
2375
  onCancel: () => setMode(null),
2376
+ onBack: mode === "confirm" ? () => {
2377
+ setMode("choose");
2378
+ setQuote(void 0);
2379
+ setFailure(void 0);
2380
+ } : void 0,
1964
2381
  onSubmit: confirm,
1965
2382
  children: [
1966
2383
  failure && /* @__PURE__ */ jsx("p", { role: "alert", children: failure }),
1967
- mode === "choose" && /* @__PURE__ */ jsxs("label", { children: [
1968
- "Choose a plan",
1969
- /* @__PURE__ */ jsxs(
1970
- "select",
2384
+ mode === "choose" && /* @__PURE__ */ jsx("div", { className: "nzpc-plans", role: "radiogroup", "aria-label": "Choose a plan", children: state.eligiblePlans.map((plan) => {
2385
+ const details = catalog.data?.plans.find((entry) => entry.code === plan.code);
2386
+ const features = details ? planFeatures(details) : [];
2387
+ return /* @__PURE__ */ jsxs(
2388
+ "label",
1971
2389
  {
1972
- value: target,
1973
- onChange: (event) => {
1974
- setTarget(event.target.value);
1975
- setQuote(void 0);
1976
- },
1977
- disabled: busy,
2390
+ className: "nzpc-plan",
2391
+ "data-selected": target === plan.code ? "" : void 0,
1978
2392
  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))
2393
+ /* @__PURE__ */ jsx(
2394
+ "input",
2395
+ {
2396
+ type: "radio",
2397
+ name: `nozle-plan-${subscriptionId}`,
2398
+ value: plan.code,
2399
+ checked: target === plan.code,
2400
+ disabled: busy,
2401
+ onChange: () => {
2402
+ setTarget(plan.code);
2403
+ setQuote(void 0);
2404
+ }
2405
+ }
2406
+ ),
2407
+ /* @__PURE__ */ jsxs("span", { className: "nzpc-plan-body", children: [
2408
+ /* @__PURE__ */ jsxs("span", { className: "nzpc-plan-head", children: [
2409
+ /* @__PURE__ */ jsx("span", { className: "nzpc-plan-name", children: plan.name }),
2410
+ /* @__PURE__ */ jsx("span", { className: "nzpc-plan-op", children: plan.operation === "upgrade" ? "Upgrade" : "Downgrade" })
2411
+ ] }),
2412
+ /* @__PURE__ */ jsxs("span", { className: "nzpc-plan-price", children: [
2413
+ formatMinorAmount(plan.amountCents, plan.currency, locale),
2414
+ " / ",
2415
+ plan.interval
2416
+ ] }),
2417
+ details?.description && /* @__PURE__ */ jsx("p", { className: "nzpc-plan-muted", children: details.description }),
2418
+ features.length > 0 && /* @__PURE__ */ jsx("ul", { children: features.map((feature) => /* @__PURE__ */ jsx("li", { children: feature }, feature)) })
2419
+ ] })
1996
2420
  ]
1997
- }
1998
- )
1999
- ] }),
2421
+ },
2422
+ plan.code
2423
+ );
2424
+ }) }),
2000
2425
  mode === "withdraw" && /* @__PURE__ */ jsxs("p", { children: [
2001
2426
  "Remove the scheduled change to ",
2002
2427
  withdrawTarget?.name,
@@ -2025,19 +2450,19 @@ function PlanChangeFlow({
2025
2450
  /* @__PURE__ */ jsxs("dl", { children: [
2026
2451
  /* @__PURE__ */ jsxs("div", { children: [
2027
2452
  /* @__PURE__ */ jsx("dt", { children: "New plan charge" }),
2028
- /* @__PURE__ */ jsx("dd", { children: money2(quote.debitAmountCents) })
2453
+ /* @__PURE__ */ jsx("dd", { children: money3(quote.debitAmountCents) })
2029
2454
  ] }),
2030
2455
  /* @__PURE__ */ jsxs("div", { children: [
2031
2456
  /* @__PURE__ */ jsx("dt", { children: "Credits applied" }),
2032
- /* @__PURE__ */ jsx("dd", { children: money2(quote.creditAmountCents) })
2457
+ /* @__PURE__ */ jsx("dd", { children: money3(quote.creditAmountCents) })
2033
2458
  ] }),
2034
2459
  /* @__PURE__ */ jsxs("div", { children: [
2035
2460
  /* @__PURE__ */ jsx("dt", { children: "Due now" }),
2036
- /* @__PURE__ */ jsx("dd", { children: money2(quote.amountDueNowCents) })
2461
+ /* @__PURE__ */ jsx("dd", { children: money3(quote.amountDueNowCents) })
2037
2462
  ] }),
2038
2463
  /* @__PURE__ */ jsxs("div", { children: [
2039
2464
  /* @__PURE__ */ jsx("dt", { children: "Due at effective date" }),
2040
- /* @__PURE__ */ jsx("dd", { children: money2(quote.amountDueAtEffectiveCents) })
2465
+ /* @__PURE__ */ jsx("dd", { children: money3(quote.amountDueAtEffectiveCents) })
2041
2466
  ] })
2042
2467
  ] }),
2043
2468
  /* @__PURE__ */ jsxs("p", { children: [
@@ -2056,6 +2481,19 @@ function PlanChangeFlow({
2056
2481
  ] })
2057
2482
  ] });
2058
2483
  }
2484
+ var PortalAppearance = createContext({
2485
+ el: (key) => ({ className: "nozle-" + key })
2486
+ });
2487
+ function Part({
2488
+ as: Tag = "div",
2489
+ part,
2490
+ className,
2491
+ style,
2492
+ ...rest
2493
+ }) {
2494
+ const themed = useContext(PortalAppearance).el(part, style, className);
2495
+ return /* @__PURE__ */ jsx(Tag, { ...rest, ...themed });
2496
+ }
2059
2497
  function asError(error) {
2060
2498
  return error instanceof BillingPortalError ? error : new BillingPortalError("request");
2061
2499
  }
@@ -2067,8 +2505,10 @@ function BillingPortal({
2067
2505
  className = "",
2068
2506
  style,
2069
2507
  nonce,
2070
- onError
2508
+ onError,
2509
+ appearance
2071
2510
  }) {
2511
+ const { root, el } = useAppearance(appearance);
2072
2512
  const [attempt, setAttempt] = useState(0);
2073
2513
  const [state, setState] = useState();
2074
2514
  const errorCallback = useRef(onError);
@@ -2105,11 +2545,10 @@ function BillingPortal({
2105
2545
  }, [createSession, attempt]);
2106
2546
  const current = state?.source === createSession && state.attempt === attempt ? state : void 0;
2107
2547
  const reconnect = () => setAttempt((value) => value + 1);
2108
- return /* @__PURE__ */ jsxs(
2548
+ return /* @__PURE__ */ jsx(PortalAppearance.Provider, { value: { el, appearance }, children: /* @__PURE__ */ jsxs(
2109
2549
  "div",
2110
2550
  {
2111
- className: `nozle-billing-portal ${className}`,
2112
- style,
2551
+ ...root("billingPortal", style, `nozle-billing-portal ${className}`),
2113
2552
  "aria-label": "Nozle billing portal",
2114
2553
  children: [
2115
2554
  /* @__PURE__ */ jsx("style", { nonce, children: portalStyles }),
@@ -2128,9 +2567,9 @@ function BillingPortal({
2128
2567
  ) : current?.error ? /* @__PURE__ */ jsx(Failure, { error: current.error, retry: reconnect }) : /* @__PURE__ */ jsx(Loading, { label: "Starting billing session\u2026" })
2129
2568
  ]
2130
2569
  }
2131
- );
2570
+ ) });
2132
2571
  }
2133
- function useQuery(client, query, variables, onError, retainDuringRefresh = false) {
2572
+ function useQuery4(client, query, variables, onError, retainDuringRefresh = false) {
2134
2573
  const [attempt, setAttempt] = useState(0);
2135
2574
  const key = JSON.stringify(variables);
2136
2575
  const [state, setState] = useState();
@@ -2195,12 +2634,12 @@ function Failure({
2195
2634
  reconnect
2196
2635
  }) {
2197
2636
  const action = error.code === "unauthorized" ? reconnect ?? retry : retry;
2198
- return /* @__PURE__ */ jsxs("div", { role: "alert", className: "nzp-error", children: [
2637
+ return /* @__PURE__ */ jsxs(Part, { role: "alert", className: "nzp-error", part: "billingPortal__error", children: [
2199
2638
  /* @__PURE__ */ jsx("span", { children: error.message }),
2200
2639
  action && /* @__PURE__ */ jsx("button", { type: "button", onClick: action, children: error.code === "unauthorized" ? "Reconnect" : "Try again" })
2201
2640
  ] });
2202
2641
  }
2203
- function money(cents, currency, locale) {
2642
+ function money2(cents, currency, locale) {
2204
2643
  const formatter = new Intl.NumberFormat(locale, {
2205
2644
  style: "currency",
2206
2645
  currency
@@ -2227,7 +2666,7 @@ var intervals = {
2227
2666
  };
2228
2667
  function Portal(props) {
2229
2668
  const { client, locale, reconnect, onError } = props;
2230
- const { data, error, retry } = useQuery(
2669
+ const { data, error, retry } = useQuery4(
2231
2670
  client,
2232
2671
  identityQuery,
2233
2672
  {},
@@ -2240,7 +2679,7 @@ function Portal(props) {
2240
2679
  return /* @__PURE__ */ jsx(Failure, { error, retry, reconnect });
2241
2680
  if (!data) return /* @__PURE__ */ jsx(Loading, {});
2242
2681
  const customer = data.customerPortalUser;
2243
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2682
+ return /* @__PURE__ */ jsxs(NozleBranding, { show: data.customerPortalOrganization.nozleBranding === true, children: [
2244
2683
  /* @__PURE__ */ jsxs("header", { children: [
2245
2684
  /* @__PURE__ */ jsxs("div", { className: "nzp-row", children: [
2246
2685
  /* @__PURE__ */ jsx("span", { className: "nzp-muted", children: data.customerPortalOrganization.name }),
@@ -2249,7 +2688,7 @@ function Portal(props) {
2249
2688
  /* @__PURE__ */ jsx("h2", { children: "Billing portal" }),
2250
2689
  /* @__PURE__ */ jsx("p", { className: "nzp-muted", children: customer.name || [customer.firstname, customer.lastname].filter(Boolean).join(" ") || customer.email || "Your account" })
2251
2690
  ] }),
2252
- notice && /* @__PURE__ */ jsx("p", { role: "status", children: notice }),
2691
+ notice && /* @__PURE__ */ jsx("p", { className: "nzp-notice", role: "status", children: notice }),
2253
2692
  view.kind !== "overview" && /* @__PURE__ */ jsx("button", { type: "button", onClick: back, children: "\u2190 Back to overview" }),
2254
2693
  view.kind === "overview" && /* @__PURE__ */ jsxs(Fragment, { children: [
2255
2694
  /* @__PURE__ */ jsx(
@@ -2267,7 +2706,7 @@ function Portal(props) {
2267
2706
  onWallet: (wallet) => setView({ kind: "wallet", wallet })
2268
2707
  }
2269
2708
  ),
2270
- /* @__PURE__ */ jsxs("section", { "aria-label": "Billing information", children: [
2709
+ /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Billing information", children: [
2271
2710
  /* @__PURE__ */ jsxs("div", { className: "nzp-row", children: [
2272
2711
  /* @__PURE__ */ jsx("h3", { children: "Billing information" }),
2273
2712
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setView({ kind: "edit" }), children: "Edit billing information" })
@@ -2337,8 +2776,7 @@ function Portal(props) {
2337
2776
  back();
2338
2777
  }
2339
2778
  }
2340
- ),
2341
- /* @__PURE__ */ jsx("footer", { children: "Powered by Nozle" })
2779
+ )
2342
2780
  ] });
2343
2781
  }
2344
2782
  function Pager({
@@ -2385,17 +2823,20 @@ function Subscriptions({
2385
2823
  planChangeActions,
2386
2824
  nonce
2387
2825
  }) {
2826
+ const portalAppearance = useContext(PortalAppearance).appearance;
2388
2827
  const [page, setPage] = useState(1);
2389
2828
  const [updated, setUpdated] = useState({});
2390
- const { data, error, retry } = useQuery(client, subscriptionsQuery, { page }, onError, true);
2829
+ const { data, error, retry } = useQuery4(client, subscriptionsQuery, { page }, onError, true);
2391
2830
  const result = data?.customerPortalSubscriptions;
2392
2831
  useEffect(() => setUpdated({}), [result]);
2393
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Subscriptions", children: [
2832
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Subscriptions", children: [
2394
2833
  /* @__PURE__ */ jsx("h3", { children: "Subscriptions" }),
2395
2834
  error ? /* @__PURE__ */ jsx(Failure, { error, retry, reconnect }) : !result ? /* @__PURE__ */ jsx(Loading, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
2396
2835
  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",
2836
+ Part,
2398
2837
  {
2838
+ as: "article",
2839
+ part: "billingPortal__card",
2399
2840
  className: "nzp-card",
2400
2841
  "data-subscription-id": subscription.externalId,
2401
2842
  children: [
@@ -2404,7 +2845,7 @@ function Subscriptions({
2404
2845
  /* @__PURE__ */ jsx("span", { className: "nzp-badge", children: subscription.endingAt ? "Scheduled to end" : "Active" })
2405
2846
  ] }),
2406
2847
  /* @__PURE__ */ jsxs("div", { className: "nzp-price", children: [
2407
- money(
2848
+ money2(
2408
2849
  subscription.plan.amountCents,
2409
2850
  subscription.plan.amountCurrency,
2410
2851
  locale
@@ -2416,25 +2857,15 @@ function Subscriptions({
2416
2857
  intervals[subscription.plan.interval] || subscription.plan.interval
2417
2858
  ] })
2418
2859
  ] }),
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
- }
2860
+ /* @__PURE__ */ jsxs("p", { className: "nzp-muted", children: [
2861
+ "Base price, excluding tax and usage",
2862
+ /* @__PURE__ */ jsx("br", {}),
2863
+ "Current period ends",
2864
+ " ",
2865
+ date(
2866
+ subscription.currentBillingPeriodEndingAt,
2867
+ locale,
2868
+ timezone
2438
2869
  )
2439
2870
  ] }),
2440
2871
  subscription.endingAt && /* @__PURE__ */ jsxs("p", { children: [
@@ -2447,48 +2878,60 @@ function Subscriptions({
2447
2878
  " ",
2448
2879
  subscription.nextSubscription.plan.name
2449
2880
  ] }),
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
- }));
2881
+ /* @__PURE__ */ jsxs("div", { className: "nzp-card-actions", children: [
2882
+ /* @__PURE__ */ jsx(
2883
+ "button",
2884
+ {
2885
+ type: "button",
2886
+ onClick: () => onUsage(subscription),
2887
+ children: "View usage"
2888
+ }
2889
+ ),
2890
+ planChangeActions && subscription.externalId && /* @__PURE__ */ jsx(
2891
+ PlanChangeControl,
2892
+ {
2893
+ subscriptionId: subscription.externalId,
2894
+ refreshKey: `${subscription.id}:${subscription.endingAt ?? ""}:${subscription.nextSubscription?.id ?? ""}:${subscription.plan.code}`,
2895
+ actions: planChangeActions,
2896
+ appearance: portalAppearance,
2897
+ locale,
2898
+ timezone,
2899
+ nonce,
2900
+ onError,
2901
+ onChanged: retry
2902
+ }
2903
+ ),
2904
+ cancellationActions && subscription.externalId && /* @__PURE__ */ jsx(
2905
+ CancellationControl,
2906
+ {
2907
+ subscription: cancellationState(subscription),
2908
+ actions: cancellationActions,
2909
+ appearance: portalAppearance,
2910
+ locale,
2911
+ timezone,
2912
+ nonce,
2913
+ onError,
2914
+ onChanged: retry,
2915
+ refresh: async (signal) => {
2916
+ const result2 = await client.request(
2917
+ subscriptionQuery,
2918
+ { id: subscription.id },
2919
+ signal
2920
+ );
2921
+ if (!signal.aborted && result2.customerPortalSubscription) {
2922
+ const fresh = result2.customerPortalSubscription;
2923
+ setUpdated((previous) => ({
2924
+ ...previous,
2925
+ [fresh.id]: fresh
2926
+ }));
2927
+ }
2928
+ return result2.customerPortalSubscription ? cancellationState(
2929
+ result2.customerPortalSubscription
2930
+ ) : null;
2472
2931
  }
2473
- return result2.customerPortalSubscription ? cancellationState(
2474
- result2.customerPortalSubscription
2475
- ) : null;
2476
2932
  }
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
- )
2933
+ )
2934
+ ] })
2492
2935
  ]
2493
2936
  },
2494
2937
  subscription.externalId || subscription.id
@@ -2522,9 +2965,9 @@ function UsageDetails({
2522
2965
  subscription,
2523
2966
  timezone
2524
2967
  }) {
2525
- const { data, error, retry } = useQuery(client, usageQuery, { subscriptionId: subscription.id }, onError);
2968
+ const { data, error, retry } = useQuery4(client, usageQuery, { subscriptionId: subscription.id }, onError);
2526
2969
  const usage = data?.customerPortalCustomerUsage;
2527
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Subscription usage", children: [
2970
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Subscription usage", children: [
2528
2971
  /* @__PURE__ */ jsxs("h3", { children: [
2529
2972
  planName(subscription),
2530
2973
  " \xB7 Usage"
@@ -2536,18 +2979,18 @@ function UsageDetails({
2536
2979
  " ",
2537
2980
  date(usage.toDatetime, locale, timezone)
2538
2981
  ] }),
2539
- /* @__PURE__ */ jsx("p", { className: "nzp-price", children: money(usage.amountCents, usage.currency, locale) }),
2982
+ /* @__PURE__ */ jsx("p", { className: "nzp-price", children: money2(usage.amountCents, usage.currency, locale) }),
2540
2983
  /* @__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: [
2984
+ usage.chargesUsage.length ? /* @__PURE__ */ jsx(Part, { className: "nzp-table", part: "billingPortal__table", children: /* @__PURE__ */ jsxs("table", { children: [
2542
2985
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2543
2986
  /* @__PURE__ */ jsx("th", { children: "Feature" }),
2544
- /* @__PURE__ */ jsx("th", { children: "Units" }),
2545
- /* @__PURE__ */ jsx("th", { children: "Amount" })
2987
+ /* @__PURE__ */ jsx("th", { className: "nzp-num", children: "Units" }),
2988
+ /* @__PURE__ */ jsx("th", { className: "nzp-num", children: "Amount" })
2546
2989
  ] }) }),
2547
2990
  /* @__PURE__ */ jsx("tbody", { children: usage.chargesUsage.map((charge, index) => /* @__PURE__ */ jsxs("tr", { children: [
2548
2991
  /* @__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) })
2992
+ /* @__PURE__ */ jsx("td", { className: "nzp-num", children: charge.units }),
2993
+ /* @__PURE__ */ jsx("td", { className: "nzp-num", children: money2(charge.amountCents, usage.currency, locale) })
2551
2994
  ] }, `${charge.id}-${index}`)) })
2552
2995
  ] }) }) : /* @__PURE__ */ jsx("p", { className: "nzp-empty", children: "No usage recorded for this billing period." })
2553
2996
  ] })
@@ -2557,25 +3000,31 @@ function Invoices(props) {
2557
3000
  const { client, locale, reconnect, onError } = props;
2558
3001
  const [search, setSearch] = useState("");
2559
3002
  const [filter, setFilter] = useState({ page: 1, searchTerm: "" });
2560
- const { data, error, retry } = useQuery(client, invoicesQuery, filter, onError);
3003
+ const { data, error, retry } = useQuery4(client, invoicesQuery, filter, onError);
2561
3004
  const result = data?.customerPortalInvoices;
2562
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Invoice history", children: [
3005
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Invoice history", children: [
2563
3006
  /* @__PURE__ */ jsx("h3", { children: "Invoice history" }),
2564
3007
  /* @__PURE__ */ jsxs(
2565
3008
  "form",
2566
3009
  {
2567
3010
  className: "nzp-actions",
3011
+ role: "search",
2568
3012
  onSubmit: (event) => {
2569
3013
  event.preventDefault();
2570
3014
  setFilter({ page: 1, searchTerm: search.trim() });
2571
3015
  },
2572
3016
  children: [
2573
- /* @__PURE__ */ jsxs("label", { children: [
2574
- "Search invoices",
3017
+ /* @__PURE__ */ jsxs("label", { className: "nzp-search", children: [
3018
+ /* @__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: [
3019
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
3020
+ /* @__PURE__ */ jsx("path", { d: "m20 20-3.5-3.5" })
3021
+ ] }),
2575
3022
  /* @__PURE__ */ jsx(
2576
3023
  "input",
2577
3024
  {
2578
3025
  type: "search",
3026
+ "aria-label": "Search invoices",
3027
+ placeholder: "Search invoices",
2579
3028
  value: search,
2580
3029
  onChange: (event) => setSearch(event.target.value)
2581
3030
  }
@@ -2586,18 +3035,18 @@ function Invoices(props) {
2586
3035
  }
2587
3036
  ),
2588
3037
  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: [
3038
+ !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
3039
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2591
3040
  /* @__PURE__ */ jsx("th", { children: "Invoice" }),
2592
3041
  /* @__PURE__ */ jsx("th", { children: "Date" }),
2593
- /* @__PURE__ */ jsx("th", { children: "Amount" }),
3042
+ /* @__PURE__ */ jsx("th", { className: "nzp-num", children: "Amount" }),
2594
3043
  /* @__PURE__ */ jsx("th", { children: "Status" }),
2595
3044
  /* @__PURE__ */ jsx("th", { children: "PDF" })
2596
3045
  ] }) }),
2597
3046
  /* @__PURE__ */ jsx("tbody", { children: result.collection.map((invoice) => /* @__PURE__ */ jsxs("tr", { children: [
2598
3047
  /* @__PURE__ */ jsx("td", { children: invoice.number }),
2599
3048
  /* @__PURE__ */ jsx("td", { children: date(invoice.issuingDate, locale) }),
2600
- /* @__PURE__ */ jsx("td", { children: money(
3049
+ /* @__PURE__ */ jsx("td", { className: "nzp-num", children: money2(
2601
3050
  invoice.totalAmountCents,
2602
3051
  invoice.currency,
2603
3052
  locale
@@ -2701,7 +3150,7 @@ function CustomerForm({
2701
3150
  if (!signal.aborted) onSaved();
2702
3151
  });
2703
3152
  };
2704
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Edit billing information", children: [
3153
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Edit billing information", children: [
2705
3154
  /* @__PURE__ */ jsx("h3", { children: "Edit billing information" }),
2706
3155
  /* @__PURE__ */ jsxs("form", { onSubmit: submit, children: [
2707
3156
  /* @__PURE__ */ jsx("div", { className: "nzp-fields", children: customerFields.map(([key, label]) => /* @__PURE__ */ jsxs("label", { children: [
@@ -2743,15 +3192,15 @@ function Wallets({
2743
3192
  onWallet
2744
3193
  }) {
2745
3194
  const [page, setPage] = useState(1);
2746
- const { data, error, retry } = useQuery(client, walletsQuery, { page }, onError);
3195
+ const { data, error, retry } = useQuery4(client, walletsQuery, { page }, onError);
2747
3196
  const result = data?.customerPortalWallets;
2748
3197
  if (result && !result.collection.length) return null;
2749
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Wallets", children: [
3198
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Wallets", children: [
2750
3199
  /* @__PURE__ */ jsx("h3", { children: "Wallets" }),
2751
3200
  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: [
3201
+ /* @__PURE__ */ jsx("div", { className: "nzp-stack", children: result.collection.map((wallet) => /* @__PURE__ */ jsxs(Part, { as: "article", part: "billingPortal__card", className: "nzp-card", children: [
2753
3202
  /* @__PURE__ */ jsx("h3", { children: wallet.name || "Prepaid credits" }),
2754
- /* @__PURE__ */ jsx("div", { className: "nzp-price", children: money(wallet.balanceCents, wallet.currency, locale) }),
3203
+ /* @__PURE__ */ jsx("div", { className: "nzp-price", children: money2(wallet.balanceCents, wallet.currency, locale) }),
2755
3204
  /* @__PURE__ */ jsxs("div", { className: "nzp-row", children: [
2756
3205
  /* @__PURE__ */ jsxs("p", { className: "nzp-muted", children: [
2757
3206
  wallet.creditsBalance,
@@ -2796,7 +3245,7 @@ function WalletTopUp({
2796
3245
  amountCents,
2797
3246
  String(wallet.paidTopUpMaxAmountCents)
2798
3247
  ) <= 0);
2799
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Add credits", children: [
3248
+ return /* @__PURE__ */ jsxs(Part, { as: "section", part: "billingPortal__section", "aria-label": "Add credits", children: [
2800
3249
  /* @__PURE__ */ jsxs("h3", { children: [
2801
3250
  "Add credits \xB7 ",
2802
3251
  wallet.name || "Wallet"
@@ -2833,19 +3282,19 @@ function WalletTopUp({
2833
3282
  )
2834
3283
  ] }) }),
2835
3284
  /* @__PURE__ */ jsxs("p", { children: [
2836
- amountCents !== null ? money(amountCents, wallet.currency, locale) : "\u2014",
3285
+ amountCents !== null ? money2(amountCents, wallet.currency, locale) : "\u2014",
2837
3286
  " ",
2838
3287
  "excluding tax"
2839
3288
  ] }),
2840
3289
  wallet.paidTopUpMinAmountCents != null && /* @__PURE__ */ jsxs("p", { className: "nzp-muted", children: [
2841
3290
  "Minimum",
2842
3291
  " ",
2843
- money(wallet.paidTopUpMinAmountCents, wallet.currency, locale)
3292
+ money2(wallet.paidTopUpMinAmountCents, wallet.currency, locale)
2844
3293
  ] }),
2845
3294
  wallet.paidTopUpMaxAmountCents != null && /* @__PURE__ */ jsxs("p", { className: "nzp-muted", children: [
2846
3295
  "Maximum",
2847
3296
  " ",
2848
- money(wallet.paidTopUpMaxAmountCents, wallet.currency, locale)
3297
+ money2(wallet.paidTopUpMaxAmountCents, wallet.currency, locale)
2849
3298
  ] }),
2850
3299
  action.error && /* @__PURE__ */ jsx(Failure, { error: action.error, reconnect }),
2851
3300
  /* @__PURE__ */ jsx("div", { className: "nzp-actions", children: /* @__PURE__ */ jsx(
@@ -2862,38 +3311,112 @@ function WalletTopUp({
2862
3311
  )
2863
3312
  ] });
2864
3313
  }
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
- );
3314
+ function useBilling() {
3315
+ const { billingFetch, billingEndpoint, queryClient } = useBillingContext();
3316
+ if (!billingFetch || !billingEndpoint) {
3317
+ throw new Error("Billing hooks need BillingProvider billingEndpoint, a backend route served by createBillingHandler");
3318
+ }
3319
+ return { fetchRoute: billingFetch, queryClient, endpoint: billingEndpoint };
3320
+ }
3321
+ function useSubscription() {
3322
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3323
+ return useQuery(
3324
+ { queryKey: ["nozle", endpoint, "subscription"], queryFn: () => fetchRoute("subscription") },
3325
+ queryClient
3326
+ );
3327
+ }
3328
+ function useEntitlement(featureCode, options = {}) {
3329
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3330
+ return useQuery(
3331
+ {
3332
+ queryKey: ["nozle", endpoint, "entitlement", featureCode],
3333
+ queryFn: () => fetchRoute("entitlement", { feature: featureCode }),
3334
+ enabled: Boolean(featureCode) && options.enabled !== false,
3335
+ staleTime: 1e4
3336
+ },
3337
+ queryClient
3338
+ );
3339
+ }
3340
+ function useCreditBalance(creditSystemCode) {
3341
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3342
+ return useQuery(
3343
+ {
3344
+ queryKey: ["nozle", endpoint, "credits"],
3345
+ queryFn: () => fetchRoute("credits"),
3346
+ select: (data) => {
3347
+ const balances = data.balances ?? [];
3348
+ if (creditSystemCode) return balances.find((b) => b.credit_system === creditSystemCode) ?? null;
3349
+ return balances.find((b) => Number(b.available) > 0) ?? balances[0] ?? null;
2888
3350
  }
2889
- }).finally(() => {
2890
- if (!cancelled) setIsLoading(false);
2891
- });
2892
- return () => {
2893
- cancelled = true;
2894
- };
2895
- }, [client]);
2896
- return { plans, isLoading, error };
3351
+ },
3352
+ queryClient
3353
+ );
3354
+ }
3355
+ function useCreditHistory(options = {}) {
3356
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3357
+ const query = useInfiniteQuery(
3358
+ {
3359
+ queryKey: ["nozle", endpoint, "credit-history", options.creditSystemCode, options.limit],
3360
+ queryFn: ({ pageParam }) => fetchRoute("credit-history", {
3361
+ credit_system: options.creditSystemCode,
3362
+ limit: options.limit,
3363
+ cursor: pageParam
3364
+ }),
3365
+ initialPageParam: void 0,
3366
+ getNextPageParam: (page) => page.next_cursor ?? void 0
3367
+ },
3368
+ queryClient
3369
+ );
3370
+ return { ...query, operations: query.data?.pages.flatMap((page) => page.operations) ?? [] };
3371
+ }
3372
+ function useUsage(options = {}) {
3373
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3374
+ const period = options.period ?? "current";
3375
+ return useQuery(
3376
+ {
3377
+ queryKey: ["nozle", endpoint, "usage", period, options.periods],
3378
+ queryFn: () => fetchRoute("usage", {
3379
+ period,
3380
+ periods: options.periods
3381
+ }),
3382
+ select: (data) => period === "past" ? data.periods ?? [] : data.usage ?? null
3383
+ },
3384
+ queryClient
3385
+ );
3386
+ }
3387
+ function useInvoices(options = {}) {
3388
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3389
+ const page = options.page ?? 1;
3390
+ return useQuery(
3391
+ {
3392
+ queryKey: ["nozle", endpoint, "invoices", page],
3393
+ queryFn: () => fetchRoute("invoices", { page })
3394
+ },
3395
+ queryClient
3396
+ );
3397
+ }
3398
+ function useInvoiceDownload() {
3399
+ const { fetchRoute } = useBilling();
3400
+ return useCallback(
3401
+ async (invoiceId) => (await fetchRoute("invoice-download", { id: invoiceId })).url,
3402
+ [fetchRoute]
3403
+ );
3404
+ }
3405
+ function useUpgradePreview(planCode) {
3406
+ const { fetchRoute, queryClient, endpoint } = useBilling();
3407
+ return useQuery(
3408
+ {
3409
+ queryKey: ["nozle", endpoint, "preview", planCode],
3410
+ queryFn: () => fetchRoute("preview", { plan: planCode }),
3411
+ enabled: Boolean(planCode),
3412
+ staleTime: 0
3413
+ },
3414
+ queryClient
3415
+ );
3416
+ }
3417
+ function useBillingRefresh() {
3418
+ const { queryClient } = useBillingContext();
3419
+ return useCallback(() => queryClient.invalidateQueries({ queryKey: ["nozle"] }), [queryClient]);
2897
3420
  }
2898
3421
  function useCheckout2() {
2899
3422
  const { createCheckout } = useBillingContext();
@@ -2941,77 +3464,539 @@ function useCheckout2() {
2941
3464
  );
2942
3465
  return { fetchClientSecret, checkout, isLoading, error };
2943
3466
  }
2944
- var SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5" };
2945
- function fmtPrice(cents, cur) {
2946
- const s = SYMBOLS[cur] ?? cur + " ";
2947
- const val = cents % 100 === 0 ? String(cents / 100) : (cents / 100).toFixed(2);
2948
- return `${s}${val}`;
2949
- }
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
- 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")
3467
+
3468
+ // src/components/pricing/catalog.ts
3469
+ var PRICING_INTERVALS = [
3470
+ "weekly",
3471
+ "monthly",
3472
+ "quarterly",
3473
+ "semiannual",
3474
+ "yearly"
3475
+ ];
3476
+ var INTERVAL_LABELS2 = {
3477
+ weekly: "Weekly",
3478
+ monthly: "Monthly",
3479
+ quarterly: "Quarterly",
3480
+ semiannual: "Every 6 months",
3481
+ yearly: "Annual"
2966
3482
  };
3483
+ var BILLED_WORDS = {
3484
+ weekly: "weekly",
3485
+ monthly: "monthly",
3486
+ quarterly: "quarterly",
3487
+ semiannual: "every 6 months",
3488
+ yearly: "yearly"
3489
+ };
3490
+ var PERIODS_PER_YEAR = {
3491
+ weekly: 52n,
3492
+ monthly: 12n,
3493
+ quarterly: 4n,
3494
+ semiannual: 2n,
3495
+ yearly: 1n
3496
+ };
3497
+ function isPricingInterval(value) {
3498
+ return typeof value === "string" && PRICING_INTERVALS.includes(value);
3499
+ }
3500
+ function intervalRank(interval) {
3501
+ return PRICING_INTERVALS.indexOf(interval);
3502
+ }
3503
+ function normalizePricingInterval(value) {
3504
+ if (value === "annual") return "yearly";
3505
+ return isPricingInterval(value) ? value : null;
3506
+ }
3507
+ function hasPosition(value) {
3508
+ return typeof value === "number" && Number.isFinite(value);
3509
+ }
3510
+ function comparePositions(a, b) {
3511
+ const aSet = hasPosition(a);
3512
+ const bSet = hasPosition(b);
3513
+ if (aSet && bSet) return a - b;
3514
+ if (aSet) return -1;
3515
+ if (bSet) return 1;
3516
+ return 0;
3517
+ }
3518
+ var DEFAULT_LOCALE = "en-US";
3519
+ function resolvePricingLocale(locale) {
3520
+ if (typeof locale !== "string" || locale.trim() === "") return DEFAULT_LOCALE;
3521
+ try {
3522
+ Intl.getCanonicalLocales(locale);
3523
+ return locale;
3524
+ } catch {
3525
+ return DEFAULT_LOCALE;
3526
+ }
3527
+ }
3528
+ function isLive(plan) {
3529
+ return plan.pending_deletion !== true;
3530
+ }
3531
+ function hasFamilies(catalog) {
3532
+ return Array.isArray(catalog.families);
3533
+ }
3534
+ function getPricingGroups(catalog) {
3535
+ const plans = catalog.plans ?? [];
3536
+ if (!hasFamilies(catalog)) {
3537
+ return plans.filter(isLive).map((plan) => ({ key: plan.code, family: null, plans: [plan] }));
3538
+ }
3539
+ const byCode = /* @__PURE__ */ new Map();
3540
+ for (const plan of plans) {
3541
+ if (isLive(plan) && !byCode.has(plan.code)) byCode.set(plan.code, plan);
3542
+ }
3543
+ return catalog.families.map((family, index) => ({ family, index })).filter(({ family }) => family.show_on_pricing !== false).sort(
3544
+ (a, b) => comparePositions(a.family.pricing_position, b.family.pricing_position) || a.index - b.index
3545
+ ).map(({ family }) => {
3546
+ const seen = /* @__PURE__ */ new Set();
3547
+ const familyPlans = [];
3548
+ for (const code of family.plan_codes ?? []) {
3549
+ const plan = byCode.get(code);
3550
+ if (plan && !seen.has(code)) {
3551
+ seen.add(code);
3552
+ familyPlans.push(plan);
3553
+ }
3554
+ }
3555
+ return { key: family.code, family, plans: familyPlans };
3556
+ }).filter((group) => group.plans.length > 0);
3557
+ }
3558
+ function toMinor(value) {
3559
+ const raw = String(value);
3560
+ if (!/^-?\d+$/.test(raw)) return null;
3561
+ if (typeof value === "number" && !Number.isSafeInteger(value)) return null;
3562
+ return BigInt(raw);
3563
+ }
3564
+ function divideRounded(numerator, denominator) {
3565
+ if (numerator < 0n) return -divideRounded(-numerator, denominator);
3566
+ return (2n * numerator + denominator) / (2n * denominator);
3567
+ }
3568
+ function perMonthMinor(amount, interval) {
3569
+ return divideRounded(amount * PERIODS_PER_YEAR[interval], 12n);
3570
+ }
3571
+ function fmt(amount, currency, locale) {
3572
+ return formatMinorAmount(String(amount), currency, locale, {
3573
+ trimZeroFraction: true
3574
+ });
3575
+ }
3576
+ function savingsAgainstMonthly(plan, monthly) {
3577
+ if (!monthly || monthly === plan) return null;
3578
+ if (!isPricingInterval(plan.interval) || plan.interval === "monthly") return null;
3579
+ if (monthly.amount_currency !== plan.amount_currency) return null;
3580
+ const monthlyAmount = toMinor(monthly.amount_cents);
3581
+ const amount = toMinor(plan.amount_cents);
3582
+ if (monthlyAmount === null || amount === null) return null;
3583
+ const base = monthlyAmount * 12n;
3584
+ const cost = amount * PERIODS_PER_YEAR[plan.interval];
3585
+ if (base <= 0n || cost >= base) return null;
3586
+ const percent = Number(divideRounded(100n * (base - cost), base));
3587
+ return percent > 0 ? percent : null;
3588
+ }
3589
+ function monthlyPlanOf(group) {
3590
+ return group.family ? group.plans.find((plan) => plan.interval === "monthly") : void 0;
3591
+ }
3592
+ function getAvailableIntervals(catalog) {
3593
+ const present = /* @__PURE__ */ new Set();
3594
+ for (const group of getPricingGroups(catalog)) {
3595
+ for (const plan of group.plans) present.add(plan.interval);
3596
+ }
3597
+ return PRICING_INTERVALS.filter((interval) => present.has(interval));
3598
+ }
3599
+ function getPricingIntervalOptions(catalog) {
3600
+ const groups = getPricingGroups(catalog);
3601
+ return getAvailableIntervals(catalog).map((interval) => {
3602
+ let best = null;
3603
+ if (interval !== "monthly") {
3604
+ for (const group of groups) {
3605
+ const plan = group.plans.find((p) => p.interval === interval);
3606
+ const saving = plan ? savingsAgainstMonthly(plan, monthlyPlanOf(group)) : null;
3607
+ if (saving !== null && (best === null || saving > best)) best = saving;
3608
+ }
3609
+ }
3610
+ return { interval, label: INTERVAL_LABELS2[interval], savingsPercent: best };
3611
+ });
3612
+ }
3613
+ function getDefaultInterval(catalog, preferred) {
3614
+ const available = getAvailableIntervals(catalog);
3615
+ const wanted = normalizePricingInterval(preferred);
3616
+ if (wanted && available.includes(wanted)) return wanted;
3617
+ if (available.includes("monthly")) return "monthly";
3618
+ return available[0] ?? null;
3619
+ }
3620
+ function pickPlan(plans, interval) {
3621
+ if (interval === null) return plans[0];
3622
+ const exact = plans.find((plan) => plan.interval === interval);
3623
+ if (exact) return exact;
3624
+ const target = intervalRank(interval);
3625
+ let best = plans[0];
3626
+ let bestScore = Number.POSITIVE_INFINITY;
3627
+ for (const plan of plans) {
3628
+ const rank = intervalRank(plan.interval);
3629
+ if (rank < 0) continue;
3630
+ const score = Math.abs(rank - target) * 2 + (rank > target ? 1 : 0);
3631
+ if (score < bestScore) {
3632
+ best = plan;
3633
+ bestScore = score;
3634
+ }
3635
+ }
3636
+ return best;
3637
+ }
3638
+ function isTrueBoolean(privilege) {
3639
+ const { value } = privilege;
3640
+ if (value === true) return true;
3641
+ return privilege.value_type === "boolean" && typeof value === "string" && value.toLowerCase() === "true";
3642
+ }
3643
+ function formatPrivilegeValue(privilege, numberFormat) {
3644
+ const { value } = privilege;
3645
+ if (value === null || value === void 0) return null;
3646
+ if (isTrueBoolean(privilege)) return null;
3647
+ if (value === false) return "No";
3648
+ if (typeof value === "number") {
3649
+ return Number.isFinite(value) ? numberFormat.format(value) : null;
3650
+ }
3651
+ const text2 = String(value).trim();
3652
+ if (text2 === "") return null;
3653
+ if (privilege.value_type === "boolean" && text2.toLowerCase() === "false") {
3654
+ return "No";
3655
+ }
3656
+ if (privilege.value_type === "integer" && /^-?\d+$/.test(text2)) {
3657
+ const parsed = Number(text2);
3658
+ if (Number.isSafeInteger(parsed)) return numberFormat.format(parsed);
3659
+ }
3660
+ return text2;
3661
+ }
3662
+ function featureLabel(name, entitlement, numberFormat) {
3663
+ const values = (entitlement?.privileges ?? []).map((privilege) => ({
3664
+ privilege,
3665
+ text: formatPrivilegeValue(privilege, numberFormat)
3666
+ })).filter(
3667
+ (entry) => entry.text !== null
3668
+ );
3669
+ if (values.length === 1) return `${name}: ${values[0].text}`;
3670
+ if (values.length > 1) {
3671
+ const parts = values.map(({ privilege, text: text2 }) => {
3672
+ const privilegeName = privilege.name?.trim() || privilege.code;
3673
+ return `${privilegeName}: ${text2}`;
3674
+ });
3675
+ return `${name} (${parts.join(", ")})`;
3676
+ }
3677
+ const limit = entitlement?.usage_limit;
3678
+ if (typeof limit === "number" && Number.isFinite(limit)) {
3679
+ return `${name}: ${numberFormat.format(limit)}`;
3680
+ }
3681
+ return name;
3682
+ }
3683
+ function trimmed(value) {
3684
+ return typeof value === "string" ? value.trim() : "";
3685
+ }
3686
+ function familyFeatures(family, plan, locale) {
3687
+ const numberFormat = new Intl.NumberFormat(locale);
3688
+ const entitlements = /* @__PURE__ */ new Map();
3689
+ for (const entitlement of plan.entitlements ?? []) {
3690
+ if (!entitlements.has(entitlement.code)) {
3691
+ entitlements.set(entitlement.code, entitlement);
3692
+ }
3693
+ }
3694
+ return (family.features ?? []).filter((feature) => feature.show_on_pricing !== false).map((feature, index) => {
3695
+ const entitlement = entitlements.get(feature.code);
3696
+ const name = trimmed(feature.name) || trimmed(entitlement?.name) || feature.code;
3697
+ return { feature, entitlement, index, name };
3698
+ }).sort(
3699
+ (a, b) => comparePositions(a.feature.display_position, b.feature.display_position) || a.name.localeCompare(b.name, locale) || a.index - b.index
3700
+ ).map(({ feature, entitlement, name }) => {
3701
+ const label = featureLabel(name, entitlement, numberFormat);
3702
+ const description = trimmed(feature.description);
3703
+ return feature.show_description === true && description ? { label, description } : { label };
3704
+ });
3705
+ }
3706
+ function planCardFeatures(plan) {
3707
+ return planFeatures(plan).map((label) => ({ label }));
3708
+ }
3709
+ function normalizeFeatures(items) {
3710
+ return items.map((item) => typeof item === "string" ? { label: item } : item).filter(
3711
+ (item) => Boolean(item) && typeof item.label === "string"
3712
+ );
3713
+ }
3714
+ function overrideFeatures(overrides, group, plan, cardIndex) {
3715
+ if (!overrides) return null;
3716
+ if (Array.isArray(overrides)) {
3717
+ const entry = overrides[cardIndex];
3718
+ return entry ? normalizeFeatures(entry) : null;
3719
+ }
3720
+ const keys = [group.key, plan.code, ...group.plans.map((p) => p.code)];
3721
+ for (const key of keys) {
3722
+ if (Object.prototype.hasOwnProperty.call(overrides, key)) {
3723
+ return normalizeFeatures(overrides[key] ?? []);
3724
+ }
3725
+ }
3726
+ return null;
3727
+ }
3728
+ function yearlyMinor(plan) {
3729
+ const amount = toMinor(plan.amount_cents);
3730
+ if (amount === null || !isPricingInterval(plan.interval)) return null;
3731
+ return amount * PERIODS_PER_YEAR[plan.interval];
3732
+ }
3733
+ function planPriceStrings(plan, locale) {
3734
+ const amount = toMinor(plan.amount_cents);
3735
+ return {
3736
+ price: fmt(amount ?? plan.amount_cents, plan.amount_currency, locale),
3737
+ pricePeriod: plan.interval ? `/${intervalLabel(plan.interval)}` : "",
3738
+ billing: "",
3739
+ savingsPercent: null
3740
+ };
3741
+ }
3742
+ function priceStrings(plan, monthly, locale) {
3743
+ const amount = toMinor(plan.amount_cents);
3744
+ const currency = plan.amount_currency;
3745
+ if (!isPricingInterval(plan.interval) || amount === null) {
3746
+ return {
3747
+ price: fmt(amount ?? plan.amount_cents, currency, locale),
3748
+ pricePeriod: "",
3749
+ billing: plan.interval ? `Billed ${plan.interval}` : "",
3750
+ savingsPercent: null
3751
+ };
3752
+ }
3753
+ const price = fmt(perMonthMinor(amount, plan.interval), currency, locale);
3754
+ if (plan.interval === "monthly") {
3755
+ return { price, pricePeriod: "/mo", billing: "Billed monthly", savingsPercent: null };
3756
+ }
3757
+ const savingsPercent = savingsAgainstMonthly(plan, monthly);
3758
+ const billed = `Billed ${fmt(amount, currency, locale)} ${BILLED_WORDS[plan.interval]}`;
3759
+ return {
3760
+ price,
3761
+ pricePeriod: "/mo",
3762
+ billing: savingsPercent === null ? billed : `${billed} \xB7 save ${savingsPercent}%`,
3763
+ savingsPercent
3764
+ };
3765
+ }
3766
+ function getPricingCards(catalog, options = {}) {
3767
+ const locale = resolvePricingLocale(options.locale);
3768
+ const families = hasFamilies(catalog);
3769
+ const requested = normalizePricingInterval(options.interval ?? null);
3770
+ const interval = requested ?? getDefaultInterval(catalog);
3771
+ const contactSales = new Set(options.contactSales ?? []);
3772
+ const isContactSales = (group) => contactSales.has(group.key) || group.plans.some((plan) => contactSales.has(plan.code));
3773
+ let groups = getPricingGroups(catalog);
3774
+ if (!families) {
3775
+ if (options.sort) {
3776
+ const sort = options.sort;
3777
+ groups = groups.map((group, index) => ({ group, index })).sort((a, b) => sort(a.group.plans[0], b.group.plans[0]) || a.index - b.index).map(({ group }) => group);
3778
+ } else {
3779
+ const sales = groups.filter(isContactSales);
3780
+ const byPlan = new Map(groups.map((group) => [group.plans[0], group]));
3781
+ groups = [
3782
+ ...sortPlansByPrice(groups.filter((group) => !isContactSales(group)).map((group) => group.plans[0])).map((plan) => byPlan.get(plan)),
3783
+ ...sales
3784
+ ];
3785
+ }
3786
+ if (requested) {
3787
+ groups = groups.filter(
3788
+ (group) => group.plans[0].interval === requested || isContactSales(group)
3789
+ );
3790
+ }
3791
+ }
3792
+ const highlight = options.highlight ?? null;
3793
+ const highlightedKey = highlight !== null ? groups.find(
3794
+ (group) => group.key === highlight || group.plans.some((plan) => plan.code === highlight)
3795
+ )?.key : groups.find((group) => group.family?.pricing_highlighted === true)?.key;
3796
+ const badge = (family) => options.badge !== void 0 ? options.badge === null ? null : trimmed(options.badge) || null : trimmed(family?.badge_text) || "Most Popular";
3797
+ return groups.map((group, cardIndex) => {
3798
+ const plan = pickPlan(group.plans, interval);
3799
+ const family = group.family;
3800
+ const name = trimmed(family?.name) || plan.name;
3801
+ const description = trimmed(family?.description) || trimmed(plan.description) || null;
3802
+ const highlighted = highlightedKey !== void 0 && highlightedKey === group.key;
3803
+ const features = overrideFeatures(options.features, group, plan, cardIndex) ?? (family ? familyFeatures(family, plan, locale) : planCardFeatures(plan));
3804
+ return {
3805
+ key: group.key,
3806
+ family,
3807
+ plans: group.plans,
3808
+ plan,
3809
+ isFallbackInterval: families && interval !== null && plan.interval !== interval,
3810
+ name,
3811
+ description,
3812
+ ...family ? priceStrings(plan, monthlyPlanOf(group), locale) : planPriceStrings(plan, locale),
3813
+ features,
3814
+ highlighted,
3815
+ badge: highlighted ? badge(family) : null,
3816
+ cta: trimmed(family?.cta_text) || `Get ${name}`,
3817
+ enterprise: isContactSales(group)
3818
+ };
3819
+ });
3820
+ }
3821
+ function PanelPlanCard({
3822
+ frame,
3823
+ el,
3824
+ name,
3825
+ description,
3826
+ price,
3827
+ interval,
3828
+ priceNote,
3829
+ features,
3830
+ isCurrent,
3831
+ isHighlighted,
3832
+ filled: filledStyle,
3833
+ highlightLabel,
3834
+ children
3835
+ }) {
3836
+ const filled = isHighlighted && filledStyle;
3837
+ const text2 = filled ? T.highlightForeground : T.foreground;
3838
+ const muted5 = filled ? `color-mix(in srgb, ${T.highlightForeground} 75%, ${T.highlight})` : T.mutedForeground;
3839
+ const edge = isHighlighted ? T.highlight : isCurrent ? T.foreground : T.border;
3840
+ const label = isCurrent ? "Active" : isHighlighted ? highlightLabel : null;
3841
+ return /* @__PURE__ */ jsxs(
3842
+ "div",
3843
+ {
3844
+ ...frame({
3845
+ display: "flex",
3846
+ flexDirection: "column",
3847
+ overflow: "hidden",
3848
+ ...text.body,
3849
+ background: filled ? T.highlight : T.card,
3850
+ color: text2,
3851
+ border: border(edge),
3852
+ borderRadius: T.radius,
3853
+ boxShadow: T.shadow
3854
+ }),
3855
+ children: [
3856
+ /* @__PURE__ */ jsxs(
3857
+ "div",
3858
+ {
3859
+ ...el("planCard__header", {
3860
+ display: "flex",
3861
+ alignItems: "center",
3862
+ justifyContent: "space-between",
3863
+ gap: "0.75rem",
3864
+ padding: "0.875rem 1.25rem",
3865
+ borderBottom: border(filled ? `color-mix(in srgb, ${T.highlightForeground} 30%, ${T.highlight})` : T.border)
3866
+ }),
3867
+ children: [
3868
+ /* @__PURE__ */ jsx("h3", { ...el("planCard__name", { ...text.heading, color: text2, margin: 0 }), children: name }),
3869
+ label && /* @__PURE__ */ jsx(
3870
+ "span",
3871
+ {
3872
+ ...el("planCard__badge", {
3873
+ ...text.label,
3874
+ whiteSpace: "nowrap",
3875
+ // The current plan's label takes the colour of its button.
3876
+ color: filled ? text2 : isCurrent ? T.mutedForeground : T.highlight
3877
+ }),
3878
+ children: label
3879
+ }
3880
+ )
3881
+ ]
3882
+ }
3883
+ ),
3884
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "1rem", padding: "1.25rem", flex: 1 }, children: [
3885
+ /* @__PURE__ */ jsxs("div", { children: [
3886
+ /* @__PURE__ */ jsxs("p", { ...el("planCard__price", { ...text.metric, color: text2, margin: 0 }), children: [
3887
+ price,
3888
+ interval && /* @__PURE__ */ jsxs("span", { style: { ...text.meta, color: muted5 }, children: [
3889
+ " / ",
3890
+ interval
3891
+ ] })
3892
+ ] }),
3893
+ priceNote && /* @__PURE__ */ jsx("p", { "data-testid": "pricing-billing", style: { ...text.meta, color: muted5, margin: "0.25rem 0 0" }, children: priceNote }),
3894
+ description && /* @__PURE__ */ jsx("p", { ...el("planCard__description", { ...text.secondary, color: muted5, margin: "0.25rem 0 0" }), children: description })
3895
+ ] }),
3896
+ /* @__PURE__ */ jsx(
3897
+ "ul",
3898
+ {
3899
+ ...el("planCard__features", {
3900
+ listStyle: "none",
3901
+ margin: 0,
3902
+ padding: 0,
3903
+ display: "flex",
3904
+ flexDirection: "column",
3905
+ gap: "0.375rem",
3906
+ flex: 1,
3907
+ ...text.secondary,
3908
+ color: muted5
3909
+ }),
3910
+ children: features.map((feature, index) => {
3911
+ const item = typeof feature === "string" ? { label: feature } : feature;
3912
+ return /* @__PURE__ */ jsxs("li", { ...el("planCard__feature"), children: [
3913
+ item.label,
3914
+ item.description && /* @__PURE__ */ jsx("span", { style: { display: "block", ...text.meta, color: muted5 }, children: item.description })
3915
+ ] }, `${index}:${item.label}`);
3916
+ })
3917
+ }
3918
+ ),
3919
+ children
3920
+ ] })
3921
+ ]
3922
+ }
3923
+ );
3924
+ }
2967
3925
  function Check() {
2968
3926
  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
3927
  }
2970
- function Toggle({ isAnnual, onChange }) {
3928
+ function IntervalSwitch({
3929
+ options,
3930
+ selected,
3931
+ onChange,
3932
+ annualDiscountLabel,
3933
+ el
3934
+ }) {
2971
3935
  const pill = {
2972
3936
  display: "inline-flex",
3937
+ flexWrap: "wrap",
3938
+ justifyContent: "center",
2973
3939
  borderRadius: "9999px",
2974
3940
  padding: "4px",
2975
- background: C.mutedBg,
3941
+ background: T.muted,
2976
3942
  gap: "2px"
2977
3943
  };
2978
3944
  const tab = (active) => ({
2979
3945
  padding: "8px 20px",
2980
3946
  borderRadius: "9999px",
2981
3947
  border: "none",
2982
- background: active ? C.cardBg : "transparent",
2983
- color: active ? C.text : C.muted,
2984
- fontWeight: active ? 600 : 400,
2985
- fontSize: "0.875rem",
3948
+ background: active ? T.card : "transparent",
3949
+ ...text.action,
3950
+ color: active ? T.foreground : T.mutedForeground,
2986
3951
  cursor: "pointer",
2987
- boxShadow: active ? "0 1px 3px rgba(0,0,0,0.08)" : "none",
3952
+ boxShadow: active ? T.shadow : "none",
2988
3953
  transition: "all 0.15s ease"
2989
3954
  });
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
- ] })
3001
- ] }) });
3955
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsx("div", { role: "group", "aria-label": "Billing interval", ...el("pricingTable__toggle", pill), children: options.map((option) => {
3956
+ const active = option.interval === selected;
3957
+ const note = option.interval === "yearly" && annualDiscountLabel ? annualDiscountLabel : option.interval !== "monthly" && option.savingsPercent !== null ? `Save ${option.savingsPercent}%` : null;
3958
+ return /* @__PURE__ */ jsxs(
3959
+ "button",
3960
+ {
3961
+ type: "button",
3962
+ "data-interval": option.interval,
3963
+ onClick: () => onChange(option.interval),
3964
+ ...el("pricingTable__toggleButton", tab(active)),
3965
+ "aria-pressed": active,
3966
+ "aria-label": note ? `${option.label}, ${note.replace(/^Save /, "save ")}` : void 0,
3967
+ children: [
3968
+ option.label,
3969
+ note && /* @__PURE__ */ jsx("span", { style: {
3970
+ marginLeft: "6px",
3971
+ ...text.meta,
3972
+ fontWeight: 600,
3973
+ color: active ? T.primary : T.mutedForeground
3974
+ }, children: note })
3975
+ ]
3976
+ },
3977
+ option.interval
3978
+ );
3979
+ }) }) });
3002
3980
  }
3003
3981
  function Card({
3004
- plan,
3005
- features,
3982
+ card,
3006
3983
  isCurrent,
3007
- isHighlighted,
3008
- isEnt,
3984
+ filled: filledStyle,
3009
3985
  enterpriseEmail,
3010
- onSelect
3986
+ onSelect,
3987
+ el,
3988
+ panel,
3989
+ isUpgrade
3011
3990
  }) {
3012
3991
  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";
3992
+ const { plan, features } = card;
3993
+ const isHighlighted = card.highlighted;
3994
+ const isEnt = card.enterprise;
3995
+ const filled = isHighlighted && filledStyle;
3996
+ const text2 = filled ? T.highlightForeground : T.foreground;
3997
+ const muted5 = filled ? `color-mix(in srgb, ${T.highlightForeground} 75%, ${T.highlight})` : T.mutedForeground;
3998
+ const accent = filled ? T.highlightForeground : T.primary;
3999
+ const badgeLabel = isCurrent ? "Current Plan" : card.badge;
3015
4000
  const handleClick = () => {
3016
4001
  if (isCurrent) return;
3017
4002
  if (isEnt && enterpriseEmail) {
@@ -3020,75 +4005,110 @@ function Card({
3020
4005
  }
3021
4006
  onSelect?.();
3022
4007
  };
4008
+ const ctaText = isCurrent ? "Current Plan" : card.family?.cta_text?.trim() ? card.cta : isEnt ? "Contact Sales" : card.cta;
4009
+ const emphasised = isUpgrade ?? isHighlighted;
4010
+ const cta = onSelect && /* @__PURE__ */ jsx(
4011
+ CTAButton,
4012
+ {
4013
+ onClick: handleClick,
4014
+ disabled: isCurrent,
4015
+ variant: isCurrent ? "current" : isEnt ? "outline" : !emphasised ? "secondary" : filled ? "inverse" : isHighlighted ? "highlight" : "primary",
4016
+ el,
4017
+ children: ctaText
4018
+ }
4019
+ );
4020
+ const dataAttributes = {
4021
+ "data-card-key": card.key,
4022
+ "data-family-code": card.family?.code,
4023
+ "data-plan-code": plan.code,
4024
+ "data-interval": plan.interval,
4025
+ "data-highlighted": isHighlighted ? "true" : void 0
4026
+ };
4027
+ if (panel) {
4028
+ return /* @__PURE__ */ jsx(
4029
+ PanelPlanCard,
4030
+ {
4031
+ frame: (style) => ({ ...el("planCard", style), ...dataAttributes }),
4032
+ el,
4033
+ name: card.name,
4034
+ description: card.description,
4035
+ price: isEnt ? "Custom" : card.price,
4036
+ interval: isEnt || !card.pricePeriod ? void 0 : card.pricePeriod.replace(/^\//, ""),
4037
+ priceNote: isEnt ? null : card.billing,
4038
+ features,
4039
+ isCurrent,
4040
+ isHighlighted,
4041
+ filled: filledStyle,
4042
+ highlightLabel: card.badge,
4043
+ children: cta
4044
+ }
4045
+ );
4046
+ }
3023
4047
  return /* @__PURE__ */ jsxs(
3024
4048
  "div",
3025
4049
  {
4050
+ ...dataAttributes,
3026
4051
  onMouseEnter: () => setHover(true),
3027
4052
  onMouseLeave: () => setHover(false),
3028
- style: {
4053
+ ...el("planCard", {
3029
4054
  position: "relative",
3030
4055
  display: "flex",
3031
4056
  flexDirection: "column",
3032
- background: C.cardBg,
3033
- border: `1px solid ${isHighlighted ? C.hl : C.border}`,
3034
- borderTopWidth: isHighlighted ? "3px" : "1px",
3035
- borderRadius: C.radius,
4057
+ background: filled ? T.highlight : T.card,
4058
+ color: text2,
4059
+ border: border(isHighlighted ? T.highlight : T.border),
4060
+ borderTopWidth: isHighlighted && !filled ? `calc(${T.borderWidth} + 2px)` : T.borderWidth,
4061
+ borderRadius: T.radius,
3036
4062
  padding: "2rem",
3037
4063
  transition: "box-shadow 0.2s ease, transform 0.2s ease",
3038
- boxShadow: shadow,
4064
+ boxShadow: T.shadow,
3039
4065
  transform: hover && !isCurrent ? "translateY(-2px)" : "none"
3040
- },
4066
+ }),
3041
4067
  children: [
3042
- isHighlighted && /* @__PURE__ */ jsx("div", { style: {
4068
+ isHighlighted && badgeLabel && /* @__PURE__ */ jsx("div", { ...el("planCard__badge", {
3043
4069
  position: "absolute",
3044
4070
  top: "-1px",
3045
4071
  left: "50%",
3046
4072
  transform: "translate(-50%, -50%)",
3047
- background: C.hl,
3048
- color: C.hlFg,
3049
- fontSize: "0.7rem",
3050
- fontWeight: 700,
4073
+ background: filled ? T.highlightForeground : T.highlight,
4074
+ border: filled ? border(T.highlight) : void 0,
4075
+ ...text.label,
4076
+ color: filled ? T.highlight : T.highlightForeground,
3051
4077
  padding: "4px 12px",
3052
4078
  borderRadius: "9999px",
3053
- letterSpacing: "0.025em",
3054
- textTransform: "uppercase",
3055
4079
  whiteSpace: "nowrap"
3056
- }, children: isCurrent ? "Current Plan" : "Most Popular" }),
3057
- isCurrent && !isHighlighted && /* @__PURE__ */ jsx("div", { style: {
4080
+ }), children: badgeLabel }),
4081
+ isCurrent && !isHighlighted && /* @__PURE__ */ jsx("div", { ...el("planCard__badge", {
3058
4082
  position: "absolute",
3059
4083
  top: "0",
3060
4084
  left: "50%",
3061
4085
  transform: "translate(-50%, -50%)",
3062
- background: C.mutedBg,
3063
- color: C.muted,
3064
- fontSize: "0.7rem",
3065
- fontWeight: 700,
4086
+ background: T.muted,
4087
+ ...text.label,
3066
4088
  padding: "4px 12px",
3067
4089
  borderRadius: "9999px",
3068
- letterSpacing: "0.025em",
3069
- textTransform: "uppercase",
3070
4090
  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 })
4091
+ }), children: "Current Plan" }),
4092
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "1.25rem", marginTop: isHighlighted && badgeLabel || isCurrent ? "0.5rem" : 0 }, children: [
4093
+ /* @__PURE__ */ jsx("h3", { ...el("planCard__name", {
4094
+ ...text.heading,
4095
+ color: text2,
4096
+ margin: 0
4097
+ }), children: card.name }),
4098
+ card.description && /* @__PURE__ */ jsx("p", { ...el("planCard__description", {
4099
+ ...text.secondary,
4100
+ color: muted5,
4101
+ margin: "6px 0 0"
4102
+ }), children: card.description })
3086
4103
  ] }),
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 })
4104
+ /* @__PURE__ */ jsx("div", { ...el("planCard__price", { marginBottom: "1.5rem" }), children: isEnt ? /* @__PURE__ */ jsx("span", { style: { ...text.metric, color: text2 }, children: "Custom" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4105
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
4106
+ /* @__PURE__ */ jsx("span", { "data-testid": "pricing-price", style: { ...text.metric, color: text2 }, children: card.price }),
4107
+ card.pricePeriod && /* @__PURE__ */ jsx("span", { style: { ...text.meta, color: muted5, marginLeft: "4px" }, children: card.pricePeriod })
4108
+ ] }),
4109
+ card.billing && /* @__PURE__ */ jsx("p", { "data-testid": "pricing-billing", style: { ...text.meta, color: muted5, margin: "8px 0 0" }, children: card.billing })
3090
4110
  ] }) }),
3091
- features && features.length > 0 && /* @__PURE__ */ jsx("ul", { style: {
4111
+ features.length > 0 && /* @__PURE__ */ jsx("ul", { ...el("planCard__features", {
3092
4112
  listStyle: "none",
3093
4113
  padding: 0,
3094
4114
  margin: "0 0 2rem",
@@ -3096,27 +4116,21 @@ function Card({
3096
4116
  flexDirection: "column",
3097
4117
  gap: "0.75rem",
3098
4118
  flex: 1
3099
- }, children: features.map((feat) => /* @__PURE__ */ jsxs("li", { style: {
4119
+ }), children: features.map((feat, index) => /* @__PURE__ */ jsxs("li", { ...el("planCard__feature", {
3100
4120
  display: "flex",
3101
4121
  alignItems: "flex-start",
3102
4122
  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, {}) }),
3108
- /* @__PURE__ */ jsx("span", { children: feat })
3109
- ] }, feat)) }),
3110
- (!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
- )
4123
+ ...text.body,
4124
+ color: text2
4125
+ }), children: [
4126
+ /* @__PURE__ */ jsx("span", { style: { color: accent, marginTop: "2px" }, children: /* @__PURE__ */ jsx(Check, {}) }),
4127
+ /* @__PURE__ */ jsxs("span", { style: { display: "flex", flexDirection: "column" }, children: [
4128
+ /* @__PURE__ */ jsx("span", { children: feat.label }),
4129
+ feat.description && /* @__PURE__ */ jsx("span", { style: { ...text.meta, color: muted5, marginTop: "2px" }, children: feat.description })
4130
+ ] })
4131
+ ] }, `${index}:${feat.label}`)) }),
4132
+ features.length === 0 && /* @__PURE__ */ jsx("div", { style: { flex: 1 } }),
4133
+ cta
3120
4134
  ]
3121
4135
  }
3122
4136
  );
@@ -3125,124 +4139,94 @@ function CTAButton({
3125
4139
  children,
3126
4140
  onClick,
3127
4141
  disabled,
3128
- variant
4142
+ variant,
4143
+ el
3129
4144
  }) {
3130
4145
  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
- };
4146
+ const look = variant === "current" ? { ...buttonStyle("secondary"), background: T.muted, color: T.mutedForeground, 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
4147
  return /* @__PURE__ */ jsx(
3163
4148
  "button",
3164
4149
  {
4150
+ type: "button",
3165
4151
  onClick: disabled ? void 0 : onClick,
3166
4152
  disabled,
3167
4153
  onMouseEnter: () => setHover(true),
3168
4154
  onMouseLeave: () => setHover(false),
3169
- style: { ...base, ...variants[variant] },
4155
+ ...el("planCard__cta", {
4156
+ ...look,
4157
+ width: "100%",
4158
+ filter: hover && !disabled ? "brightness(0.95)" : void 0
4159
+ }),
3170
4160
  children
3171
4161
  }
3172
4162
  );
3173
4163
  }
3174
- function Skeleton() {
3175
- return /* @__PURE__ */ jsx("div", { style: {
4164
+ function Skeleton({ rootProps }) {
4165
+ return /* @__PURE__ */ jsx("div", { ...rootProps, "aria-busy": "true", style: {
4166
+ ...rootProps.style,
3176
4167
  display: "grid",
3177
4168
  gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
3178
4169
  gap: "1.5rem"
3179
4170
  }, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs("div", { style: {
3180
- background: C.cardBg,
3181
- border: `1px solid ${C.border}`,
3182
- borderRadius: C.radius,
4171
+ background: T.card,
4172
+ border: border(),
4173
+ borderRadius: T.radius,
3183
4174
  padding: "2rem",
3184
4175
  height: "380px"
3185
4176
  }, 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" } })
4177
+ /* @__PURE__ */ jsx("div", { style: { height: 20, width: "40%", borderRadius: 6, background: T.skeleton, marginBottom: 12 } }),
4178
+ /* @__PURE__ */ jsx("div", { style: { height: 40, width: "50%", borderRadius: 6, background: T.skeleton, marginBottom: 24 } }),
4179
+ /* @__PURE__ */ jsx("div", { style: { height: 14, width: "80%", borderRadius: 6, background: T.skeleton, marginBottom: 10 } }),
4180
+ /* @__PURE__ */ jsx("div", { style: { height: 14, width: "70%", borderRadius: 6, background: T.skeleton, marginBottom: 10 } }),
4181
+ /* @__PURE__ */ jsx("div", { style: { height: 14, width: "60%", borderRadius: 6, background: T.skeleton } })
3191
4182
  ] }, i)) });
3192
4183
  }
3193
4184
  function PricingTable({
3194
4185
  currentPlanCode,
3195
4186
  plans: propPlans,
4187
+ families: propFamilies,
3196
4188
  features,
4189
+ sort,
3197
4190
  onSelect,
3198
4191
  returnUrl,
3199
4192
  onStripeClientSecret,
3200
4193
  onCheckoutResult,
3201
4194
  onCheckoutError,
3202
4195
  showToggle = true,
4196
+ defaultInterval,
3203
4197
  highlightPlan,
4198
+ highlightStyle = "outline",
4199
+ highlightLabel,
3204
4200
  className,
3205
- enterpriseEmail
4201
+ contactSalesPlans,
4202
+ enterpriseEmail,
4203
+ annualDiscountLabel,
4204
+ locale: localeProp,
4205
+ appearance
3206
4206
  }) {
4207
+ const { root, el, planCardLayout } = useAppearance(appearance);
4208
+ const locale = resolvePricingLocale(localeProp);
3207
4209
  const billing = useOptionalBillingContext();
3208
4210
  const client = billing?.client;
3209
- const [plans, setPlans] = useState(propPlans ?? []);
4211
+ const fetched = useQuery(
4212
+ { ...catalogQuery(client), enabled: !propPlans && Boolean(client) },
4213
+ billing?.queryClient ?? idleQueryClient
4214
+ );
4215
+ const catalog = propPlans ? { plans: propPlans, families: Array.isArray(propFamilies) ? propFamilies : null } : { plans: fetched.data?.plans ?? [], families: fetched.data?.families ?? null };
4216
+ const loading = !propPlans && Boolean(client) && fetched.isPending;
4217
+ const loadError = propPlans ? null : !client ? "BillingProvider with a publishableKey is required to load plans" : fetched.error ? `Plans could not be loaded: ${fetched.error.message}` : null;
3210
4218
  const [currentPlan, setCurrentPlan] = useState(currentPlanCode ?? null);
3211
- const [isAnnual, setIsAnnual] = useState(false);
3212
- const [loading, setLoading] = useState(!propPlans);
4219
+ const [selectedInterval, setSelectedInterval] = useState(null);
3213
4220
  const [processing, setProcessing] = useState(false);
3214
4221
  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 (!res.ok) throw new Error(`HTTP ${res.status}`);
3228
- const data = await res.json();
3229
- if (!cancelled) setPlans(data.plans || []);
3230
- }).catch((cause) => {
3231
- if (!cancelled) setCheckoutError(`Plans could not be loaded: ${cause instanceof Error ? cause.message : "unknown error"}`);
3232
- }).finally(() => {
3233
- if (!cancelled) setLoading(false);
3234
- });
3235
- return () => {
3236
- cancelled = true;
3237
- };
3238
- }, [client, propPlans]);
3239
4222
  useEffect(() => {
3240
4223
  if (currentPlanCode !== void 0) setCurrentPlan(currentPlanCode);
3241
4224
  }, [currentPlanCode]);
3242
- async function selectPlan(plan) {
4225
+ const withFamilies = Array.isArray(catalog.families);
4226
+ async function selectPlan(plan, family) {
3243
4227
  setCheckoutError(null);
3244
4228
  if (onSelect) {
3245
- onSelect(plan);
4229
+ onSelect(plan, family);
3246
4230
  return;
3247
4231
  }
3248
4232
  try {
@@ -3268,124 +4252,143 @@ function PricingTable({
3268
4252
  onCheckoutError?.(error);
3269
4253
  }
3270
4254
  }
3271
- const hasAnnual = plans.some((p) => p.interval === "yearly");
3272
- const filtered = showToggle && hasAnnual ? plans.filter((p) => p.interval === (isAnnual ? "yearly" : "monthly") || isEnterprise(p)) : plans;
3273
- if (loading) return /* @__PURE__ */ jsx(Skeleton, {});
3274
- return /* @__PURE__ */ jsxs("div", { className, style: {
3275
- fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
3276
- color: C.text,
3277
- background: C.bg
3278
- }, children: [
3279
- showToggle && hasAnnual && /* @__PURE__ */ jsx(Toggle, { isAnnual, onChange: setIsAnnual }),
4255
+ const available = getAvailableIntervals(catalog);
4256
+ const interval = selectedInterval !== null && available.includes(selectedInterval) ? selectedInterval : getDefaultInterval(catalog, defaultInterval);
4257
+ const switchVisible = showToggle && available.length > 1 && interval !== null;
4258
+ const cards = getPricingCards(catalog, {
4259
+ interval: withFamilies || switchVisible ? interval : null,
4260
+ locale,
4261
+ highlight: highlightPlan,
4262
+ badge: highlightLabel,
4263
+ features,
4264
+ contactSales: contactSalesPlans,
4265
+ sort
4266
+ });
4267
+ const current = catalog.plans.find((p) => p.code === currentPlan);
4268
+ const currentYearly = current ? yearlyMinor(current) : null;
4269
+ const isUpgrade = (plan) => {
4270
+ if (!current || current.amount_currency !== plan.amount_currency) return void 0;
4271
+ const yearly = yearlyMinor(plan);
4272
+ return currentYearly !== null && yearly !== null ? yearly > currentYearly : plan.amount_cents > current.amount_cents;
4273
+ };
4274
+ const rootProps = root("pricingTable", text.body, className);
4275
+ if (loading) return /* @__PURE__ */ jsx(Skeleton, { rootProps });
4276
+ return /* @__PURE__ */ jsxs("div", { ...rootProps, children: [
4277
+ switchVisible && interval !== null && /* @__PURE__ */ jsx(
4278
+ IntervalSwitch,
4279
+ {
4280
+ options: getPricingIntervalOptions(catalog),
4281
+ selected: interval,
4282
+ onChange: setSelectedInterval,
4283
+ annualDiscountLabel,
4284
+ el
4285
+ }
4286
+ ),
3280
4287
  processing && /* @__PURE__ */ jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
3281
- checkoutError && /* @__PURE__ */ jsx("p", { role: "alert", children: checkoutError }),
3282
- /* @__PURE__ */ jsx("div", { style: {
4288
+ (checkoutError ?? loadError) && /* @__PURE__ */ jsx("p", { role: "alert", style: { color: T.dangerText }, children: checkoutError ?? loadError }),
4289
+ /* @__PURE__ */ jsx("div", { ...el("pricingTable__grid", {
3283
4290
  display: "grid",
3284
4291
  gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
3285
4292
  gap: "1.5rem"
3286
- }, children: filtered.map((plan, i) => /* @__PURE__ */ jsx(
4293
+ }), children: cards.map((card) => /* @__PURE__ */ jsx(
3287
4294
  Card,
3288
4295
  {
3289
- plan,
3290
- features: features?.[i],
3291
- isCurrent: currentPlan === plan.code,
3292
- isHighlighted: highlightPlan === plan.code,
3293
- isEnt: isEnterprise(plan),
4296
+ card,
4297
+ isCurrent: currentPlan === card.plan.code,
4298
+ isUpgrade: isUpgrade(card.plan),
4299
+ filled: highlightStyle === "filled",
3294
4300
  enterpriseEmail,
3295
- onSelect: () => void selectPlan(plan)
4301
+ onSelect: () => void selectPlan(card.plan, card.family),
4302
+ el,
4303
+ panel: planCardLayout === "panel"
3296
4304
  },
3297
- plan.code
3298
- )) })
4305
+ card.key
4306
+ )) }),
4307
+ /* @__PURE__ */ jsx(NozleBranding, {})
3299
4308
  ] });
3300
4309
  }
3301
4310
  function PlanCard({
3302
- id,
3303
- name,
3304
- monthlyPrice,
3305
- annualPrice,
3306
- features,
3307
- description,
3308
- isAnnual,
4311
+ plan,
4312
+ features: featureOverride,
3309
4313
  isCurrent,
3310
4314
  returnUrl,
3311
- onSelect
4315
+ onSelect,
4316
+ appearance
3312
4317
  }) {
3313
- const price = isAnnual ? annualPrice : monthlyPrice;
3314
- const interval = isAnnual ? "year" : "month";
4318
+ const { root, el, planCardLayout } = useAppearance(appearance);
4319
+ const { code: id, name, description } = plan;
4320
+ const price = formatPlanPrice(plan.amount_cents, plan.amount_currency);
4321
+ const interval = intervalLabel(plan.interval);
4322
+ const features = featureOverride ?? planFeatures(plan);
4323
+ const cta = onSelect && /* @__PURE__ */ jsx(
4324
+ "button",
4325
+ {
4326
+ type: "button",
4327
+ onClick: () => onSelect?.({
4328
+ planCode: id,
4329
+ returnUrl: returnUrl ?? window.location.href
4330
+ }),
4331
+ ...el("planCard__cta", {
4332
+ ...buttonStyle(isCurrent ? "secondary" : "primary"),
4333
+ ...isCurrent && { background: T.muted, color: T.mutedForeground, cursor: "default" },
4334
+ width: "100%"
4335
+ }),
4336
+ disabled: isCurrent,
4337
+ "aria-label": isCurrent ? `Current plan: ${name}` : `Select ${name}`,
4338
+ children: isCurrent ? "Current Plan" : "Get Started"
4339
+ }
4340
+ );
4341
+ if (planCardLayout === "panel") {
4342
+ return /* @__PURE__ */ jsx(
4343
+ PanelPlanCard,
4344
+ {
4345
+ frame: (style) => ({ "data-testid": `plan-card-${id}`, "aria-current": isCurrent ? "true" : void 0, ...root("planCard", style) }),
4346
+ el,
4347
+ name,
4348
+ description,
4349
+ price,
4350
+ interval,
4351
+ features,
4352
+ isCurrent: Boolean(isCurrent),
4353
+ isHighlighted: false,
4354
+ filled: false,
4355
+ children: cta
4356
+ }
4357
+ );
4358
+ }
3315
4359
  return /* @__PURE__ */ jsxs(
3316
4360
  "div",
3317
4361
  {
3318
4362
  "data-testid": `plan-card-${id}`,
3319
4363
  "aria-current": isCurrent ? "true" : void 0,
3320
- style: {
3321
- background: "var(--nozle-background, var(--background))",
3322
- border: isCurrent ? "2px solid var(--nozle-primary, var(--primary))" : "1px solid var(--nozle-border, var(--border))",
3323
- borderRadius: "var(--nozle-radius, 0.5rem)",
4364
+ ...root("planCard", {
4365
+ ...text.body,
4366
+ background: T.card,
4367
+ border: border(isCurrent ? T.primary : T.border),
4368
+ outline: isCurrent ? `1px solid ${T.primary}` : void 0,
4369
+ borderRadius: T.radius,
4370
+ boxShadow: T.shadow,
3324
4371
  padding: "1.5rem",
3325
4372
  display: "flex",
3326
4373
  flexDirection: "column",
3327
4374
  gap: "1rem"
3328
- },
3329
- className: isCurrent ? "border-primary" : "",
4375
+ }),
3330
4376
  children: [
3331
4377
  /* @__PURE__ */ jsxs("div", { children: [
3332
- /* @__PURE__ */ jsx(
3333
- "h3",
3334
- {
3335
- style: {
3336
- fontSize: "1.25rem",
3337
- fontWeight: 600,
3338
- color: "var(--nozle-foreground, var(--foreground))",
3339
- margin: 0
3340
- },
3341
- children: name
3342
- }
3343
- ),
3344
- description && /* @__PURE__ */ jsx(
3345
- "p",
3346
- {
3347
- style: {
3348
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
3349
- fontSize: "0.875rem",
3350
- margin: "0.25rem 0 0"
3351
- },
3352
- children: description
3353
- }
3354
- )
4378
+ /* @__PURE__ */ jsx("h3", { ...el("planCard__name", { ...text.heading, margin: 0 }), children: name }),
4379
+ description && /* @__PURE__ */ jsx("p", { ...el("planCard__description", { ...text.secondary, margin: "0.25rem 0 0" }), children: description })
3355
4380
  ] }),
3356
- /* @__PURE__ */ jsxs("div", { children: [
3357
- /* @__PURE__ */ jsxs(
3358
- "span",
3359
- {
3360
- style: {
3361
- fontSize: "2rem",
3362
- fontWeight: 700,
3363
- color: "var(--nozle-foreground, var(--foreground))"
3364
- },
3365
- children: [
3366
- "$",
3367
- price
3368
- ]
3369
- }
3370
- ),
3371
- /* @__PURE__ */ jsxs(
3372
- "span",
3373
- {
3374
- style: {
3375
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
3376
- marginLeft: "0.25rem"
3377
- },
3378
- children: [
3379
- "/",
3380
- interval
3381
- ]
3382
- }
3383
- )
4381
+ /* @__PURE__ */ jsxs("div", { ...el("planCard__price"), children: [
4382
+ /* @__PURE__ */ jsx("span", { style: text.metric, children: price }),
4383
+ /* @__PURE__ */ jsxs("span", { style: { ...text.meta, marginLeft: "0.25rem" }, children: [
4384
+ "/",
4385
+ interval
4386
+ ] })
3384
4387
  ] }),
3385
4388
  /* @__PURE__ */ jsx(
3386
4389
  "ul",
3387
4390
  {
3388
- style: {
4391
+ ...el("planCard__features", {
3389
4392
  listStyle: "none",
3390
4393
  padding: 0,
3391
4394
  margin: 0,
@@ -3393,160 +4396,51 @@ function PlanCard({
3393
4396
  flexDirection: "column",
3394
4397
  gap: "0.5rem",
3395
4398
  flex: 1
3396
- },
3397
- children: features.map((feature) => /* @__PURE__ */ jsxs(
3398
- "li",
3399
- {
3400
- style: {
3401
- display: "flex",
3402
- alignItems: "center",
3403
- gap: "0.5rem",
3404
- color: "var(--nozle-foreground, var(--foreground))",
3405
- fontSize: "0.875rem"
3406
- },
3407
- children: [
3408
- /* @__PURE__ */ jsx(
3409
- "span",
3410
- {
3411
- style: { color: "var(--nozle-primary, var(--primary))" },
3412
- "aria-hidden": "true",
3413
- children: "\u2713"
3414
- }
3415
- ),
3416
- feature
3417
- ]
3418
- },
4399
+ }),
4400
+ children: features.map((feature) => /* @__PURE__ */ jsxs("li", { ...el("planCard__feature", { display: "flex", alignItems: "center", gap: "0.5rem", ...text.body }), children: [
4401
+ /* @__PURE__ */ jsx("span", { style: { color: T.primary }, "aria-hidden": "true", children: "\u2713" }),
3419
4402
  feature
3420
- ))
4403
+ ] }, feature))
3421
4404
  }
3422
4405
  ),
3423
- onSelect && /* @__PURE__ */ jsx(
3424
- "button",
3425
- {
3426
- onClick: () => onSelect?.({
3427
- planCode: id,
3428
- returnUrl: returnUrl ?? window.location.href
3429
- }),
3430
- style: {
3431
- background: isCurrent ? "var(--nozle-muted, var(--muted))" : "var(--nozle-primary, var(--primary))",
3432
- color: isCurrent ? "var(--nozle-muted-foreground, var(--muted-foreground))" : "var(--nozle-primary-foreground, var(--primary-foreground))",
3433
- border: "none",
3434
- borderRadius: "var(--nozle-radius, 0.5rem)",
3435
- padding: "0.75rem 1.5rem",
3436
- cursor: isCurrent ? "default" : "pointer",
3437
- fontWeight: 500,
3438
- width: "100%"
3439
- },
3440
- disabled: isCurrent,
3441
- "aria-label": isCurrent ? `Current plan: ${name}` : `Select ${name}`,
3442
- children: isCurrent ? "Current Plan" : "Get Started"
3443
- }
3444
- )
4406
+ cta
3445
4407
  ]
3446
4408
  }
3447
4409
  );
3448
4410
  }
3449
4411
  function PlanComparison({
3450
- plans,
3451
- features
4412
+ plans: catalogPlans,
4413
+ features: featureOverride,
4414
+ sort,
4415
+ appearance
3452
4416
  }) {
3453
- return /* @__PURE__ */ jsx(
3454
- "div",
3455
- {
3456
- style: {
3457
- overflowX: "auto",
3458
- background: "var(--nozle-background, var(--background))"
3459
- },
3460
- children: /* @__PURE__ */ jsxs(
3461
- "table",
3462
- {
3463
- style: {
3464
- width: "100%",
3465
- borderCollapse: "collapse",
3466
- fontSize: "0.875rem",
3467
- color: "var(--nozle-foreground, var(--foreground))"
3468
- },
3469
- children: [
3470
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
3471
- /* @__PURE__ */ jsx(
3472
- "th",
3473
- {
3474
- style: {
3475
- padding: "0.75rem 1rem",
3476
- textAlign: "left",
3477
- borderBottom: "1px solid var(--nozle-border, var(--border))",
3478
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
3479
- fontWeight: 500
3480
- },
3481
- children: "Feature"
3482
- }
3483
- ),
3484
- plans.map((plan) => /* @__PURE__ */ jsx(
3485
- "th",
3486
- {
3487
- style: {
3488
- padding: "0.75rem 1rem",
3489
- textAlign: "center",
3490
- borderBottom: "1px solid var(--nozle-border, var(--border))",
3491
- fontWeight: 600,
3492
- color: "var(--nozle-foreground, var(--foreground))"
3493
- },
3494
- children: plan.name
3495
- },
3496
- plan.id
3497
- ))
3498
- ] }) }),
3499
- /* @__PURE__ */ jsx("tbody", { children: features.map((feature) => /* @__PURE__ */ jsxs("tr", { children: [
3500
- /* @__PURE__ */ jsx(
3501
- "td",
3502
- {
3503
- style: {
3504
- padding: "0.75rem 1rem",
3505
- borderBottom: "1px solid var(--nozle-border, var(--border))",
3506
- color: "var(--nozle-foreground, var(--foreground))"
3507
- },
3508
- children: feature.label
3509
- }
3510
- ),
3511
- plans.map((plan) => {
3512
- const value = feature.values[plan.id];
3513
- return /* @__PURE__ */ jsx(
3514
- "td",
3515
- {
3516
- style: {
3517
- padding: "0.75rem 1rem",
3518
- textAlign: "center",
3519
- borderBottom: "1px solid var(--nozle-border, var(--border))"
3520
- },
3521
- children: typeof value === "boolean" ? value ? /* @__PURE__ */ jsx(
3522
- "span",
3523
- {
3524
- style: {
3525
- color: "var(--nozle-primary, var(--primary))"
3526
- },
3527
- "aria-label": "Included",
3528
- children: "\u2713"
3529
- }
3530
- ) : /* @__PURE__ */ jsx(
3531
- "span",
3532
- {
3533
- style: {
3534
- color: "var(--nozle-muted-foreground, var(--muted-foreground))"
3535
- },
3536
- "aria-label": "Not included",
3537
- children: "\u2715"
3538
- }
3539
- ) : /* @__PURE__ */ jsx("span", { children: value ?? "\u2014" })
3540
- },
3541
- plan.id
3542
- );
3543
- })
3544
- ] }, feature.key)) })
3545
- ]
3546
- }
3547
- )
3548
- }
3549
- );
4417
+ const { root, el } = useAppearance(appearance);
4418
+ const plans = sort ? [...catalogPlans].sort(sort) : sortPlansByPrice(catalogPlans);
4419
+ const features = featureOverride ?? comparisonRows(plans);
4420
+ const cell = (style) => el("planComparison__cell", style);
4421
+ return /* @__PURE__ */ jsxs("div", { ...root("planComparison", { minWidth: 0 }), children: [
4422
+ /* @__PURE__ */ jsx("div", { style: table.frame, children: /* @__PURE__ */ jsxs("table", { ...el("planComparison__table", table.table), children: [
4423
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
4424
+ /* @__PURE__ */ jsx("th", { ...cell(table.head()), children: "Feature" }),
4425
+ plans.map((plan) => /* @__PURE__ */ jsxs("th", { ...cell({ ...table.head(), textAlign: "center", padding: "10px 20px" }), children: [
4426
+ plan.name,
4427
+ /* @__PURE__ */ jsxs("div", { style: { ...text.meta, letterSpacing: "normal", textTransform: "none" }, children: [
4428
+ formatPlanPrice(plan.amount_cents, plan.amount_currency),
4429
+ " / ",
4430
+ intervalLabel(plan.interval)
4431
+ ] })
4432
+ ] }, plan.code))
4433
+ ] }) }),
4434
+ /* @__PURE__ */ jsx("tbody", { children: features.map((feature, index) => /* @__PURE__ */ jsxs("tr", { ...el("planComparison__row"), children: [
4435
+ /* @__PURE__ */ jsx("td", { ...cell(table.cell(false, index === features.length - 1)), children: feature.label }),
4436
+ plans.map((plan) => {
4437
+ const value = feature.values[plan.code];
4438
+ 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);
4439
+ })
4440
+ ] }, feature.key)) })
4441
+ ] }) }),
4442
+ /* @__PURE__ */ jsx(NozleBranding, {})
4443
+ ] });
3550
4444
  }
3551
4445
  function UpgradeButton({
3552
4446
  planCode,
@@ -3562,26 +4456,18 @@ function UpgradeButton({
3562
4456
  onCheckoutStarted,
3563
4457
  onDowngradeScheduled,
3564
4458
  onUpgraded,
3565
- onError
4459
+ onError,
4460
+ appearance
3566
4461
  }) {
3567
4462
  const [isOpen, setIsOpen] = useState(false);
4463
+ const { root } = useAppearance(appearance);
3568
4464
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3569
4465
  /* @__PURE__ */ jsx(
3570
4466
  "button",
3571
4467
  {
3572
4468
  type: "button",
3573
4469
  onClick: () => setIsOpen(true),
3574
- className,
3575
- style: {
3576
- padding: "0.75rem 1.5rem",
3577
- borderRadius: "var(--nozle-radius, 0.5rem)",
3578
- border: "none",
3579
- background: "var(--nozle-primary, var(--primary))",
3580
- color: "var(--nozle-primary-foreground, var(--primary-foreground))",
3581
- cursor: "pointer",
3582
- fontWeight: 500,
3583
- ...style
3584
- },
4470
+ ...root("upgradeButton", { ...buttonStyle(), ...style }, className),
3585
4471
  children: label
3586
4472
  }
3587
4473
  ),
@@ -3589,6 +4475,7 @@ function UpgradeButton({
3589
4475
  UpgradeModal,
3590
4476
  {
3591
4477
  isOpen,
4478
+ appearance,
3592
4479
  planCode,
3593
4480
  returnUrl,
3594
4481
  subscriptionId,
@@ -3647,11 +4534,13 @@ function PlanGate({
3647
4534
  return /* @__PURE__ */ jsx(Fragment, { children });
3648
4535
  }
3649
4536
  function UpgradePrompt({
3650
- text = "Upgrade to access this feature",
4537
+ text: text2 = "Upgrade to access this feature",
3651
4538
  planName: planName2,
3652
- onUpgrade
4539
+ onUpgrade,
4540
+ appearance
3653
4541
  }) {
3654
- const displayText = planName2 ? `Upgrade to ${planName2} to access this feature` : text;
4542
+ const { root } = useAppearance(appearance);
4543
+ const displayText = planName2 ? `Upgrade to ${planName2} to access this feature` : text2;
3655
4544
  function handleUpgrade() {
3656
4545
  if (onUpgrade) {
3657
4546
  onUpgrade();
@@ -3662,40 +4551,23 @@ function UpgradePrompt({
3662
4551
  return /* @__PURE__ */ jsxs(
3663
4552
  "div",
3664
4553
  {
3665
- style: {
4554
+ ...root("upgradePrompt", {
3666
4555
  display: "flex",
3667
4556
  flexDirection: "column",
3668
4557
  alignItems: "center",
3669
4558
  gap: "0.75rem",
3670
4559
  padding: "1rem",
3671
- textAlign: "center"
3672
- },
4560
+ textAlign: "center",
4561
+ ...text.body
4562
+ }),
3673
4563
  children: [
3674
- /* @__PURE__ */ jsx(
3675
- "p",
3676
- {
3677
- style: {
3678
- color: "var(--nozle-foreground, var(--foreground))",
3679
- fontSize: "0.875rem",
3680
- margin: 0
3681
- },
3682
- children: displayText
3683
- }
3684
- ),
4564
+ /* @__PURE__ */ jsx("p", { style: { margin: 0 }, children: displayText }),
3685
4565
  /* @__PURE__ */ jsx(
3686
4566
  "button",
3687
4567
  {
4568
+ type: "button",
3688
4569
  onClick: handleUpgrade,
3689
- style: {
3690
- padding: "0.5rem 1.25rem",
3691
- borderRadius: "var(--nozle-radius, 0.5rem)",
3692
- border: "none",
3693
- background: "var(--nozle-primary, var(--primary))",
3694
- color: "var(--nozle-primary-foreground, var(--primary-foreground))",
3695
- cursor: "pointer",
3696
- fontWeight: 500,
3697
- fontSize: "0.875rem"
3698
- },
4570
+ style: { ...buttonStyle(), padding: "0.5rem 1.25rem" },
3699
4571
  children: "Upgrade"
3700
4572
  }
3701
4573
  )
@@ -3708,12 +4580,14 @@ function LockedOverlay({
3708
4580
  upgradeText,
3709
4581
  upgradePlanName,
3710
4582
  onUpgrade,
3711
- children
4583
+ children,
4584
+ appearance
3712
4585
  }) {
4586
+ const { root, el } = useAppearance(appearance);
3713
4587
  if (!locked) {
3714
4588
  return /* @__PURE__ */ jsx(Fragment, { children });
3715
4589
  }
3716
- return /* @__PURE__ */ jsxs("div", { style: { position: "relative", display: "inline-block" }, children: [
4590
+ return /* @__PURE__ */ jsxs("div", { ...root("lockedOverlay", { position: "relative", display: "inline-block" }), children: [
3717
4591
  /* @__PURE__ */ jsx(
3718
4592
  "div",
3719
4593
  {
@@ -3729,16 +4603,15 @@ function LockedOverlay({
3729
4603
  /* @__PURE__ */ jsx(
3730
4604
  "div",
3731
4605
  {
3732
- style: {
4606
+ ...el("lockedOverlay__panel", {
3733
4607
  position: "absolute",
3734
4608
  inset: 0,
3735
4609
  display: "flex",
3736
4610
  alignItems: "center",
3737
4611
  justifyContent: "center",
3738
- background: "var(--nozle-background, var(--background))",
3739
- opacity: 0.92,
3740
- borderRadius: "var(--nozle-radius, 0.5rem)"
3741
- },
4612
+ background: `color-mix(in srgb, ${T.background} 92%, transparent)`,
4613
+ borderRadius: T.radius
4614
+ }),
3742
4615
  role: "dialog",
3743
4616
  "aria-label": "Feature locked \u2014 upgrade required",
3744
4617
  children: /* @__PURE__ */ jsx(
@@ -3746,7 +4619,8 @@ function LockedOverlay({
3746
4619
  {
3747
4620
  text: upgradeText,
3748
4621
  planName: upgradePlanName,
3749
- onUpgrade
4622
+ onUpgrade,
4623
+ appearance
3750
4624
  }
3751
4625
  )
3752
4626
  }
@@ -3754,68 +4628,109 @@ function LockedOverlay({
3754
4628
  ] });
3755
4629
  }
3756
4630
  function getUsageMeterColor(percentage) {
3757
- if (percentage >= 95) return "var(--nozle-destructive, var(--destructive))";
3758
- if (percentage >= 80) return "var(--nozle-warning, var(--warning))";
3759
- return "var(--nozle-primary, var(--primary))";
4631
+ if (percentage >= 95) return T.danger;
4632
+ if (percentage >= 80) return T.warning;
4633
+ return T.primary;
3760
4634
  }
3761
4635
  function UsageMeter({
3762
4636
  used,
3763
4637
  limit,
3764
4638
  label,
3765
- showText = true
4639
+ showText = true,
4640
+ appearance
3766
4641
  }) {
3767
- const percentage = limit > 0 ? Math.min(100, used / limit * 100) : 0;
4642
+ const { root, el } = useAppearance(appearance);
4643
+ const unlimited = limit == null;
4644
+ const percentage = !unlimited && limit > 0 ? Math.min(100, used / limit * 100) : 0;
3768
4645
  const color = getUsageMeterColor(percentage);
3769
- return /* @__PURE__ */ jsxs("div", { className: "w-full space-y-1", children: [
3770
- label && /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-sm", children: [
3771
- /* @__PURE__ */ jsx("span", { children: label }),
3772
- showText && /* @__PURE__ */ jsxs("span", { style: { color }, children: [
3773
- used.toLocaleString(),
3774
- " / ",
3775
- limit.toLocaleString()
3776
- ] })
3777
- ] }),
3778
- /* @__PURE__ */ jsx(
3779
- "div",
3780
- {
3781
- className: "h-2 rounded-full overflow-hidden",
3782
- style: { background: "var(--nozle-border, var(--border))" },
3783
- children: /* @__PURE__ */ jsx(
4646
+ return /* @__PURE__ */ jsxs(
4647
+ "div",
4648
+ {
4649
+ ...root("usageMeter", {
4650
+ width: "100%",
4651
+ display: "flex",
4652
+ flexDirection: "column",
4653
+ gap: "0.25rem",
4654
+ ...text.body
4655
+ }),
4656
+ children: [
4657
+ label && /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
4658
+ /* @__PURE__ */ jsx("span", { ...el("usageMeter__label"), children: label }),
4659
+ showText && /* @__PURE__ */ jsxs("span", { ...el("usageMeter__value", { color: percentage >= 95 ? T.dangerText : percentage >= 80 ? T.warningText : T.foreground }), children: [
4660
+ used.toLocaleString(),
4661
+ " / ",
4662
+ unlimited ? "Unlimited" : limit.toLocaleString()
4663
+ ] })
4664
+ ] }),
4665
+ /* @__PURE__ */ jsx(
3784
4666
  "div",
3785
4667
  {
3786
- className: "h-full rounded-full",
3787
- style: {
3788
- width: `${percentage}%`,
3789
- background: color,
3790
- transition: "width 0.3s ease"
3791
- }
4668
+ ...el("usageMeter__track", {
4669
+ height: "0.5rem",
4670
+ borderRadius: "9999px",
4671
+ overflow: "hidden",
4672
+ background: T.muted
4673
+ }),
4674
+ children: /* @__PURE__ */ jsx(
4675
+ "div",
4676
+ {
4677
+ ...el("usageMeter__fill", {
4678
+ height: "100%",
4679
+ borderRadius: "9999px",
4680
+ width: `${percentage}%`,
4681
+ background: color,
4682
+ transition: "width 0.3s ease"
4683
+ })
4684
+ }
4685
+ )
3792
4686
  }
3793
4687
  )
3794
- }
3795
- )
3796
- ] });
4688
+ ]
4689
+ }
4690
+ );
4691
+ }
4692
+ function usageItems(entitlements, results) {
4693
+ return entitlements.flatMap((entitlement) => {
4694
+ const result = results[entitlement.code];
4695
+ if (entitlement.feature_type !== "metered" || !result) return [];
4696
+ return [{ key: entitlement.code, label: entitlement.name, used: result.used, limit: result.limit ?? null }];
4697
+ });
3797
4698
  }
3798
4699
  function UsageDashboard({
3799
4700
  features = [],
3800
- loading = false
4701
+ loading = false,
4702
+ error = null,
4703
+ emptyMessage = "No usage to show yet.",
4704
+ appearance
3801
4705
  }) {
4706
+ const { root } = useAppearance(appearance);
4707
+ const props = root("usageDashboard", {
4708
+ ...surface,
4709
+ margin: 0,
4710
+ display: "flex",
4711
+ flexDirection: "column",
4712
+ gap: loading ? "0.75rem" : "1rem",
4713
+ // Errors and the empty message take the same colours as in the other components.
4714
+ ...error ? { color: T.dangerText } : features.length === 0 && !loading && text.secondary
4715
+ });
3802
4716
  if (loading) {
3803
- return /* @__PURE__ */ jsx("div", { className: "animate-pulse space-y-3", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsx(
3804
- "div",
3805
- {
3806
- className: "h-8 rounded",
3807
- style: { background: "var(--nozle-border, var(--border))" }
3808
- },
3809
- i
3810
- )) });
4717
+ 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)) });
4718
+ }
4719
+ if (error) {
4720
+ return /* @__PURE__ */ jsx("p", { ...props, role: "alert", children: typeof error === "string" ? error : error.message });
3811
4721
  }
3812
- 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)) });
4722
+ if (features.length === 0) {
4723
+ return /* @__PURE__ */ jsx("p", { ...props, role: "status", children: emptyMessage });
4724
+ }
4725
+ return /* @__PURE__ */ jsx("div", { ...props, children: features.map((f) => /* @__PURE__ */ jsx(UsageMeter, { used: f.used, limit: f.limit, label: f.label, appearance }, f.key)) });
3813
4726
  }
3814
4727
  function UsageAlert({
3815
4728
  features,
3816
4729
  threshold = 80,
3817
- upgradeHref = "/plans"
4730
+ upgradeHref = "/plans",
4731
+ appearance
3818
4732
  }) {
4733
+ const { root, el } = useAppearance(appearance);
3819
4734
  const [dismissed, setDismissed] = useState(/* @__PURE__ */ new Set());
3820
4735
  const alertFeatures = features.filter(
3821
4736
  (f) => f.percentage >= threshold && !dismissed.has(f.key)
@@ -3826,15 +4741,19 @@ function UsageAlert({
3826
4741
  const handleDismiss = (key) => {
3827
4742
  setDismissed((prev) => new Set(prev).add(key));
3828
4743
  };
3829
- return /* @__PURE__ */ jsx("div", { className: "space-y-2", children: alertFeatures.map((f) => /* @__PURE__ */ jsxs(
4744
+ return /* @__PURE__ */ jsx("div", { ...root("usageAlert", { display: "flex", flexDirection: "column", gap: "0.5rem", ...text.body }), children: alertFeatures.map((f) => /* @__PURE__ */ jsxs(
3830
4745
  "div",
3831
4746
  {
3832
4747
  role: "alert",
3833
- className: "flex items-center justify-between rounded-md px-4 py-3 text-sm",
3834
4748
  style: {
3835
- background: "var(--nozle-warning-bg, var(--warning-bg, #fef9c3))",
3836
- color: "var(--nozle-warning-text, var(--warning-text, #854d0e))",
3837
- border: "1px solid var(--nozle-warning-border, var(--warning-border, #fde047))"
4749
+ display: "flex",
4750
+ alignItems: "center",
4751
+ justifyContent: "space-between",
4752
+ borderRadius: `calc(${T.radius} / 2)`,
4753
+ padding: "0.75rem 1rem",
4754
+ background: T.warningSoft,
4755
+ color: T.warningText,
4756
+ border: border(T.warningBorder)
3838
4757
  },
3839
4758
  children: [
3840
4759
  /* @__PURE__ */ jsxs("span", { children: [
@@ -3857,8 +4776,8 @@ function UsageAlert({
3857
4776
  {
3858
4777
  href: upgradeHref,
3859
4778
  style: {
4779
+ ...text.action,
3860
4780
  color: "inherit",
3861
- fontWeight: 600,
3862
4781
  textDecoration: "underline"
3863
4782
  },
3864
4783
  children: "Upgrade"
@@ -3868,16 +4787,17 @@ function UsageAlert({
3868
4787
  /* @__PURE__ */ jsx(
3869
4788
  "button",
3870
4789
  {
4790
+ type: "button",
3871
4791
  onClick: () => handleDismiss(f.key),
3872
4792
  "aria-label": `Dismiss ${f.label} alert`,
3873
- style: {
4793
+ ...el("usageAlert__dismiss", {
3874
4794
  background: "none",
3875
4795
  border: "none",
3876
4796
  cursor: "pointer",
3877
4797
  padding: "0 0 0 0.75rem",
3878
- fontSize: "1rem",
4798
+ ...text.action,
3879
4799
  color: "inherit"
3880
- },
4800
+ }),
3881
4801
  children: "\xD7"
3882
4802
  }
3883
4803
  )
@@ -3886,44 +4806,247 @@ function UsageAlert({
3886
4806
  f.key
3887
4807
  )) });
3888
4808
  }
3889
- var TIER_STYLES = {
3890
- free: {
3891
- background: "var(--nozle-muted, var(--muted, #f1f5f9))",
3892
- color: "var(--nozle-muted-foreground, var(--muted-foreground, #64748b))",
3893
- border: "1px solid var(--nozle-border, var(--border, #e2e8f0))"
3894
- },
3895
- starter: {
3896
- background: "color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 10%, transparent)",
3897
- color: "var(--nozle-primary, var(--primary, #3b82f6))",
3898
- border: "1px solid color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 30%, transparent)"
3899
- },
3900
- pro: {
3901
- background: "var(--nozle-primary, var(--primary, #3b82f6))",
3902
- color: "var(--nozle-primary-foreground, var(--primary-foreground, #ffffff))",
3903
- border: "1px solid transparent"
3904
- },
3905
- enterprise: {
3906
- background: "var(--nozle-accent, var(--accent, #8b5cf6))",
3907
- color: "var(--nozle-accent-foreground, var(--accent-foreground, #ffffff))",
3908
- border: "1px solid transparent"
4809
+ var STATUS_LABELS = {
4810
+ active: "Active",
4811
+ pending: "Pending",
4812
+ incomplete: "Awaiting payment",
4813
+ canceled: "Canceled",
4814
+ terminated: "Ended"
4815
+ };
4816
+ function statusColors(status, ending) {
4817
+ if (status === "active" && !ending) return { background: T.successSoft, color: T.successText };
4818
+ if (status === "active" || status === "pending" || status === "incomplete") {
4819
+ return { background: T.warningSoft, color: T.warningText };
4820
+ }
4821
+ return { background: T.muted, color: T.mutedForeground };
4822
+ }
4823
+ function day(value, locale) {
4824
+ const parsed = new Date(value);
4825
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString(locale, { dateStyle: "medium" });
4826
+ }
4827
+ function currentPlanMeters(entitlements, usage) {
4828
+ const units = /* @__PURE__ */ new Map();
4829
+ for (const charge of usage?.charges_usage ?? []) {
4830
+ units.set(charge.feature.code, (units.get(charge.feature.code) ?? 0) + (Number(charge.units) || 0));
4831
+ }
4832
+ return (entitlements ?? []).filter((entitlement) => entitlement.feature_type === "metered").map((entitlement) => ({
4833
+ key: entitlement.code,
4834
+ label: entitlement.name,
4835
+ used: units.get(entitlement.code) ?? 0,
4836
+ limit: entitlement.usage_limit ?? null
4837
+ }));
4838
+ }
4839
+ function CurrentPlan(props) {
4840
+ return props.subscription === void 0 ? /* @__PURE__ */ jsx(CurrentPlanFromBackend, { ...props }) : /* @__PURE__ */ jsx(CurrentPlanView, { ...props });
4841
+ }
4842
+ function CurrentPlanFromBackend(props) {
4843
+ const subscription = useSubscription();
4844
+ const plans = usePlans();
4845
+ const usage = useUsage();
4846
+ const planCode = subscription.data?.subscription?.plan_code;
4847
+ const meters = props.usage ?? currentPlanMeters(plans.plans.find((plan) => plan.code === planCode)?.entitlements, usage.data);
4848
+ return /* @__PURE__ */ jsx(
4849
+ CurrentPlanView,
4850
+ {
4851
+ ...props,
4852
+ subscription: subscription.data ?? null,
4853
+ usage: meters,
4854
+ isLoading: props.isLoading || subscription.isPending,
4855
+ error: props.error ?? subscription.error
4856
+ }
4857
+ );
4858
+ }
4859
+ function CurrentPlanView({
4860
+ subscription,
4861
+ usage = [],
4862
+ details = [],
4863
+ actions,
4864
+ warningPercent = 80,
4865
+ title = "Current plan",
4866
+ isLoading = false,
4867
+ error = null,
4868
+ emptyMessage = "No active plan.",
4869
+ pastDueMessage = "A payment is overdue. Pay the open invoice to keep your plan active.",
4870
+ locale = "en-US",
4871
+ appearance
4872
+ }) {
4873
+ const { root, el } = useAppearance(appearance);
4874
+ const frame = { ...surface, padding: 0, overflow: "hidden" };
4875
+ const header = /* @__PURE__ */ jsxs(
4876
+ "div",
4877
+ {
4878
+ style: {
4879
+ display: "flex",
4880
+ alignItems: "center",
4881
+ justifyContent: "space-between",
4882
+ gap: 12,
4883
+ padding: "0.875rem 1.25rem",
4884
+ borderBottom: border()
4885
+ },
4886
+ children: [
4887
+ /* @__PURE__ */ jsx("span", { style: text.heading, children: title }),
4888
+ actions
4889
+ ]
4890
+ }
4891
+ );
4892
+ const body = (children) => /* @__PURE__ */ jsx("div", { style: { padding: "1.25rem", display: "grid", gap: "1rem" }, children });
4893
+ if (isLoading) {
4894
+ return /* @__PURE__ */ jsxs("section", { ...root("currentPlan", frame), "aria-busy": "true", children: [
4895
+ header,
4896
+ body(/* @__PURE__ */ jsx("div", { style: { height: 72, borderRadius: `calc(${T.radius} / 2)`, background: T.skeleton } }))
4897
+ ] });
4898
+ }
4899
+ if (error) {
4900
+ return /* @__PURE__ */ jsxs("section", { ...root("currentPlan", frame), children: [
4901
+ header,
4902
+ body(/* @__PURE__ */ jsx("p", { role: "alert", style: { margin: 0, color: T.dangerText }, children: typeof error === "string" ? error : error.message }))
4903
+ ] });
3909
4904
  }
4905
+ const current = subscription?.subscription;
4906
+ if (!current) {
4907
+ return /* @__PURE__ */ jsxs("section", { ...root("currentPlan", frame), children: [
4908
+ header,
4909
+ body(/* @__PURE__ */ jsx("p", { role: "status", style: { ...text.secondary, margin: 0 }, children: emptyMessage }))
4910
+ ] });
4911
+ }
4912
+ const pending = subscription?.pending_change;
4913
+ const ending = Boolean(current.ending_at);
4914
+ const near = warningPercent === null ? [] : usage.filter((meter) => meter.limit != null && meter.limit > 0 && meter.used / meter.limit * 100 >= warningPercent);
4915
+ const statusLabel = ending && current.ending_at ? `Ends ${day(current.ending_at, locale)}` : STATUS_LABELS[current.status] ?? current.status;
4916
+ return /* @__PURE__ */ jsxs("section", { ...root("currentPlan", frame), "aria-label": title, children: [
4917
+ header,
4918
+ body(
4919
+ /* @__PURE__ */ jsxs(Fragment, { children: [
4920
+ subscription?.past_due && /* @__PURE__ */ jsx(
4921
+ "p",
4922
+ {
4923
+ role: "alert",
4924
+ ...el("currentPlan__notice", {
4925
+ margin: 0,
4926
+ padding: "0.625rem 0.875rem",
4927
+ ...text.body,
4928
+ borderRadius: `calc(${T.radius} / 2)`,
4929
+ background: T.dangerSoft,
4930
+ color: T.dangerText,
4931
+ border: border(T.dangerBorder)
4932
+ }),
4933
+ children: pastDueMessage
4934
+ }
4935
+ ),
4936
+ near.map((meter) => /* @__PURE__ */ jsxs(
4937
+ "p",
4938
+ {
4939
+ role: "status",
4940
+ ...el("currentPlan__notice", {
4941
+ margin: 0,
4942
+ padding: "0.625rem 0.875rem",
4943
+ ...text.body,
4944
+ borderRadius: `calc(${T.radius} / 2)`,
4945
+ background: T.warningSoft,
4946
+ color: T.warningText,
4947
+ border: border(T.warningBorder)
4948
+ }),
4949
+ children: [
4950
+ "You have used ",
4951
+ Math.round(meter.used / meter.limit * 100),
4952
+ "% of the ",
4953
+ meter.label,
4954
+ " included in",
4955
+ " ",
4956
+ current.plan.name,
4957
+ "."
4958
+ ]
4959
+ },
4960
+ meter.key
4961
+ )),
4962
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: "0.625rem" }, children: [
4963
+ /* @__PURE__ */ jsx("span", { ...el("currentPlan__name", text.heading), children: current.plan.name }),
4964
+ /* @__PURE__ */ jsx(
4965
+ "span",
4966
+ {
4967
+ ...el("currentPlan__status", {
4968
+ ...text.label,
4969
+ ...statusColors(current.status, ending),
4970
+ padding: "0.125rem 0.625rem",
4971
+ borderRadius: 9999
4972
+ }),
4973
+ children: statusLabel
4974
+ }
4975
+ ),
4976
+ pending && /* @__PURE__ */ jsxs(
4977
+ "span",
4978
+ {
4979
+ ...el("currentPlan__scheduled", {
4980
+ ...text.label,
4981
+ padding: "0.125rem 0.625rem",
4982
+ borderRadius: 9999,
4983
+ border: border()
4984
+ }),
4985
+ children: [
4986
+ "Changes to ",
4987
+ pending.name,
4988
+ " on ",
4989
+ day(pending.effective_at, locale)
4990
+ ]
4991
+ }
4992
+ ),
4993
+ /* @__PURE__ */ jsxs("span", { ...el("currentPlan__price", { ...text.secondary, marginLeft: "auto" }), children: [
4994
+ formatPlanPrice(Number(current.plan.amount_cents), current.plan.currency),
4995
+ " / ",
4996
+ intervalLabel(current.plan.interval)
4997
+ ] })
4998
+ ] }),
4999
+ usage.length > 0 && /* @__PURE__ */ jsx("div", { ...el("currentPlan__usage", { display: "grid", gap: "0.875rem" }), children: usage.map((meter) => /* @__PURE__ */ jsx(
5000
+ UsageMeter,
5001
+ {
5002
+ label: `${meter.label} used this period`,
5003
+ used: meter.used,
5004
+ limit: meter.limit,
5005
+ appearance
5006
+ },
5007
+ meter.key
5008
+ )) }),
5009
+ details.length > 0 && /* @__PURE__ */ jsx(
5010
+ "dl",
5011
+ {
5012
+ ...el("currentPlan__details", {
5013
+ margin: 0,
5014
+ display: "grid",
5015
+ gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))",
5016
+ gap: "0.75rem"
5017
+ }),
5018
+ children: details.map((detail) => /* @__PURE__ */ jsxs("div", { children: [
5019
+ /* @__PURE__ */ jsx("dt", { style: text.meta, children: detail.label }),
5020
+ /* @__PURE__ */ jsx("dd", { style: { margin: "0.125rem 0 0", fontWeight: 600 }, children: detail.value })
5021
+ ] }, detail.label))
5022
+ }
5023
+ )
5024
+ ] })
5025
+ ),
5026
+ /* @__PURE__ */ jsx(NozleBranding, {})
5027
+ ] });
5028
+ }
5029
+ var TIER_STYLES = {
5030
+ free: { background: T.muted, color: T.mutedForeground, border: border() },
5031
+ starter: { background: T.primarySoft, color: T.primary, border: border(`color-mix(in srgb, ${T.primary} 30%, transparent)`) },
5032
+ pro: { background: T.primary, color: T.primaryForeground, border: border("transparent") },
5033
+ enterprise: { background: T.accent, color: T.accentForeground, border: border("transparent") }
3910
5034
  };
3911
- function PlanBadge({ plan, tier = "free" }) {
5035
+ function PlanBadge({ plan, tier = "free", appearance }) {
5036
+ const { root } = useAppearance(appearance);
3912
5037
  const style = TIER_STYLES[tier] ?? TIER_STYLES.free;
3913
5038
  return /* @__PURE__ */ jsx(
3914
5039
  "span",
3915
5040
  {
3916
- style: {
5041
+ ...root("planBadge", {
5042
+ ...text.label,
3917
5043
  ...style,
3918
5044
  display: "inline-flex",
3919
5045
  alignItems: "center",
3920
5046
  padding: "0.125rem 0.5rem",
3921
5047
  borderRadius: "9999px",
3922
- fontSize: "0.75rem",
3923
- fontWeight: 600,
3924
- lineHeight: "1.25rem",
3925
5048
  whiteSpace: "nowrap"
3926
- },
5049
+ }),
3927
5050
  children: plan
3928
5051
  }
3929
5052
  );
@@ -3940,8 +5063,10 @@ var BRAND_LABELS = {
3940
5063
  function PaymentMethodDisplay({
3941
5064
  paymentMethod,
3942
5065
  onUpdatePaymentMethod,
3943
- updateHref = "/billing/update-payment"
5066
+ updateHref = "/billing/update-payment",
5067
+ appearance
3944
5068
  }) {
5069
+ const { root } = useAppearance(appearance);
3945
5070
  const handleUpdate = () => {
3946
5071
  if (onUpdatePaymentMethod) {
3947
5072
  onUpdatePaymentMethod();
@@ -3949,7 +5074,7 @@ function PaymentMethodDisplay({
3949
5074
  window.location.href = updateHref;
3950
5075
  }
3951
5076
  };
3952
- return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
5077
+ return /* @__PURE__ */ jsxs("div", { ...root("paymentMethod", { ...surface, display: "flex", flexDirection: "column", gap: "0.75rem" }), children: [
3953
5078
  paymentMethod ? /* @__PURE__ */ jsxs(
3954
5079
  "div",
3955
5080
  {
@@ -3958,80 +5083,245 @@ function PaymentMethodDisplay({
3958
5083
  alignItems: "center",
3959
5084
  gap: "0.75rem",
3960
5085
  padding: "0.75rem 1rem",
3961
- border: "1px solid var(--nozle-border, var(--border))",
3962
- borderRadius: "0.5rem",
3963
- fontSize: "0.875rem"
5086
+ border: border(),
5087
+ borderRadius: `calc(${T.radius} * 0.6)`
3964
5088
  },
3965
5089
  children: [
3966
- /* @__PURE__ */ jsx(
3967
- "span",
3968
- {
3969
- style: {
3970
- fontWeight: 600,
3971
- color: "var(--nozle-foreground, var(--foreground))"
3972
- },
3973
- children: BRAND_LABELS[paymentMethod.brand.toLowerCase()] ?? paymentMethod.brand
3974
- }
3975
- ),
3976
- /* @__PURE__ */ jsxs(
3977
- "span",
3978
- {
3979
- style: {
3980
- color: "var(--nozle-muted-foreground, var(--muted-foreground))"
3981
- },
3982
- children: [
3983
- "\u2022\u2022\u2022\u2022 ",
3984
- paymentMethod.last4
3985
- ]
3986
- }
3987
- ),
3988
- /* @__PURE__ */ jsxs(
3989
- "span",
3990
- {
3991
- style: {
3992
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
3993
- fontSize: "0.75rem"
3994
- },
3995
- children: [
3996
- "Expires ",
3997
- String(paymentMethod.expMonth).padStart(2, "0"),
3998
- "/",
3999
- String(paymentMethod.expYear).slice(-2)
4000
- ]
4001
- }
4002
- )
5090
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, color: T.foreground }, children: BRAND_LABELS[paymentMethod.brand.toLowerCase()] ?? paymentMethod.brand }),
5091
+ /* @__PURE__ */ jsxs("span", { style: text.secondary, children: [
5092
+ "\u2022\u2022\u2022\u2022 ",
5093
+ paymentMethod.last4
5094
+ ] }),
5095
+ /* @__PURE__ */ jsxs("span", { style: text.meta, children: [
5096
+ "Expires ",
5097
+ String(paymentMethod.expMonth).padStart(2, "0"),
5098
+ "/",
5099
+ String(paymentMethod.expYear).slice(-2)
5100
+ ] })
4003
5101
  ]
4004
5102
  }
4005
- ) : /* @__PURE__ */ jsx(
4006
- "p",
4007
- {
4008
- style: {
4009
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
4010
- fontSize: "0.875rem"
4011
- },
4012
- children: "No payment method on file."
4013
- }
4014
- ),
5103
+ ) : /* @__PURE__ */ jsx("p", { style: { ...text.secondary, margin: 0 }, children: "No payment method on file." }),
4015
5104
  /* @__PURE__ */ jsx(
4016
5105
  "button",
4017
5106
  {
5107
+ type: "button",
4018
5108
  onClick: handleUpdate,
4019
5109
  style: {
4020
- background: "transparent",
4021
- border: "1px solid var(--nozle-primary, var(--primary))",
4022
- color: "var(--nozle-primary, var(--primary))",
5110
+ ...buttonStyle("outline"),
5111
+ alignSelf: "flex-start",
4023
5112
  padding: "0.375rem 0.875rem",
4024
- borderRadius: "0.375rem",
4025
- fontSize: "0.875rem",
4026
- fontWeight: 500,
4027
- cursor: "pointer"
5113
+ borderRadius: buttonRadius(`calc(${T.radius} / 2)`)
4028
5114
  },
4029
5115
  children: "Update Payment Method"
4030
5116
  }
4031
5117
  )
4032
5118
  ] });
4033
5119
  }
5120
+ var SOURCE_LABELS = {
5121
+ subscription_grant: "Plan grant",
5122
+ top_up: "Top-up",
5123
+ manual_grant: "Granted",
5124
+ adjustment: "Adjustment",
5125
+ allocated_top_up: "Allocated top-up"
5126
+ };
5127
+ var muted = T.mutedForeground;
5128
+ function date2(value) {
5129
+ const parsed = new Date(value);
5130
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString("en-US", { dateStyle: "medium" });
5131
+ }
5132
+ function CreditBalance({
5133
+ balance,
5134
+ isLoading = false,
5135
+ error = null,
5136
+ emptyMessage = "No credit balance yet.",
5137
+ appearance
5138
+ }) {
5139
+ const { root, el } = useAppearance(appearance);
5140
+ if (isLoading) return /* @__PURE__ */ jsx("div", { ...root("creditBalance", { ...surface, height: 96, background: T.skeleton }), role: "status", "aria-label": "Loading credit balance" });
5141
+ if (error) return /* @__PURE__ */ jsx("p", { ...root("creditBalance", { ...surface, color: T.dangerText }), role: "alert", children: typeof error === "string" ? error : error.message });
5142
+ if (!balance) return /* @__PURE__ */ jsx("p", { ...root("creditBalance", { ...surface, color: muted }), role: "status", children: emptyMessage });
5143
+ const sources = (balance.sources ?? []).filter((source) => source.remaining !== "0");
5144
+ return /* @__PURE__ */ jsxs("section", { "aria-label": balance.credit_system_name, ...root("creditBalance", surface), children: [
5145
+ /* @__PURE__ */ jsx("h3", { style: { ...text.heading, margin: 0 }, children: balance.credit_system_name }),
5146
+ /* @__PURE__ */ jsxs("p", { style: { margin: "0.25rem 0 0" }, children: [
5147
+ /* @__PURE__ */ jsx("span", { ...el("creditBalance__amount", text.metric), children: formatDecimalString(balance.available) }),
5148
+ " ",
5149
+ /* @__PURE__ */ jsxs("span", { style: text.secondary, children: [
5150
+ creditUnitLabel(balance.available, balance.unit_name),
5151
+ " available"
5152
+ ] })
5153
+ ] }),
5154
+ /* @__PURE__ */ jsxs("p", { style: { ...text.meta, margin: "0.25rem 0 0" }, children: [
5155
+ "As of ",
5156
+ date2(balance.as_of)
5157
+ ] }),
5158
+ 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: [
5159
+ /* @__PURE__ */ jsx("span", { children: SOURCE_LABELS[source.type] ?? source.type }),
5160
+ /* @__PURE__ */ jsxs("span", { style: text.secondary, children: [
5161
+ formatDecimalString(source.remaining),
5162
+ source.expires_at ? ` \xB7 expires ${date2(source.expires_at)}` : ""
5163
+ ] })
5164
+ ] }, source.id)) })
5165
+ ] });
5166
+ }
5167
+ var TYPE_LABELS = {
5168
+ consume: "Used",
5169
+ grant: "Granted",
5170
+ adjustment: "Adjustment",
5171
+ expire: "Expired",
5172
+ revoke: "Revoked",
5173
+ refund: "Refunded",
5174
+ transfer: "Transfer"
5175
+ };
5176
+ var muted2 = T.mutedForeground;
5177
+ function when(value) {
5178
+ const parsed = new Date(value);
5179
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" });
5180
+ }
5181
+ function CreditHistory({
5182
+ operations = [],
5183
+ isLoading = false,
5184
+ error = null,
5185
+ emptyMessage = "No credit activity yet.",
5186
+ hasMore = false,
5187
+ onLoadMore,
5188
+ appearance
5189
+ }) {
5190
+ const { root, el } = useAppearance(appearance);
5191
+ if (error) return /* @__PURE__ */ jsx("p", { ...root("creditHistory", { ...surface, color: T.dangerText }), role: "alert", children: typeof error === "string" ? error : error.message });
5192
+ if (isLoading && operations.length === 0) return /* @__PURE__ */ jsx("div", { ...root("creditHistory", { ...surface, height: 120, background: T.skeleton }), role: "status", "aria-label": "Loading credit history" });
5193
+ if (operations.length === 0) return /* @__PURE__ */ jsx("p", { ...root("creditHistory", { ...surface, color: muted2 }), role: "status", children: emptyMessage });
5194
+ return /* @__PURE__ */ jsxs("div", { ...root("creditHistory", { ...text.body, minWidth: 0 }), children: [
5195
+ /* @__PURE__ */ jsx("div", { style: table.frame, children: /* @__PURE__ */ jsxs("table", { ...el("creditHistory__table", table.table), children: [
5196
+ /* @__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)) }) }),
5197
+ /* @__PURE__ */ jsx("tbody", { children: operations.map((operation, index) => {
5198
+ const last = index === operations.length - 1;
5199
+ return /* @__PURE__ */ jsxs("tr", { ...el("creditHistory__row"), children: [
5200
+ /* @__PURE__ */ jsx("td", { style: table.cell(false, last), children: when(operation.occurred_at) }),
5201
+ /* @__PURE__ */ jsx("td", { style: table.cell(false, last), children: TYPE_LABELS[operation.type] ?? operation.type }),
5202
+ /* @__PURE__ */ jsx("td", { style: table.cell(false, last), children: operation.feature_code ?? "\u2014" }),
5203
+ /* @__PURE__ */ jsx("td", { style: table.cell(true, last), children: formatDecimalString(operation.credit_amount) }),
5204
+ /* @__PURE__ */ jsx("td", { style: { ...table.cell(false, last), color: operation.status === "succeeded" ? void 0 : muted2 }, children: operation.status })
5205
+ ] }, operation.id);
5206
+ }) })
5207
+ ] }) }),
5208
+ hasMore && onLoadMore && /* @__PURE__ */ jsx(
5209
+ "button",
5210
+ {
5211
+ type: "button",
5212
+ onClick: onLoadMore,
5213
+ disabled: isLoading,
5214
+ style: {
5215
+ ...buttonStyle("secondary", isLoading),
5216
+ marginTop: "0.75rem",
5217
+ padding: "0.5rem 0.875rem",
5218
+ borderRadius: buttonRadius(`calc(${T.radius} / 2)`),
5219
+ opacity: 1
5220
+ },
5221
+ children: isLoading ? "Loading\u2026" : "Load more"
5222
+ }
5223
+ )
5224
+ ] });
5225
+ }
5226
+
5227
+ // src/components/usage/usage-data.ts
5228
+ function formatAmount(amountCents, currency) {
5229
+ const amount = amountCents / 100;
5230
+ try {
5231
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
5232
+ } catch {
5233
+ return `${currency} ${amount.toFixed(2)}`;
5234
+ }
5235
+ }
5236
+ function formatDay(value) {
5237
+ const parsed = new Date(value);
5238
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString("en-US", { dateStyle: "medium", timeZone: "UTC" });
5239
+ }
5240
+ var muted3 = T.mutedForeground;
5241
+ function UsageBreakdown({
5242
+ usage,
5243
+ isLoading = false,
5244
+ error = null,
5245
+ emptyMessage = "No usage in this period yet.",
5246
+ appearance
5247
+ }) {
5248
+ const { root, el } = useAppearance(appearance);
5249
+ if (isLoading) return /* @__PURE__ */ jsx("div", { ...root("usageBreakdown", { ...surface, height: 120, background: T.skeleton }), role: "status", "aria-label": "Loading usage" });
5250
+ if (error) return /* @__PURE__ */ jsx("p", { ...root("usageBreakdown", { ...surface, color: T.dangerText }), role: "alert", children: typeof error === "string" ? error : error.message });
5251
+ if (!usage || usage.charges_usage.length === 0) return /* @__PURE__ */ jsx("p", { ...root("usageBreakdown", { ...surface, color: muted3 }), role: "status", children: emptyMessage });
5252
+ const rows = [...usage.charges_usage].sort((a, b) => b.amount_cents - a.amount_cents);
5253
+ return /* @__PURE__ */ jsxs("section", { ...root("usageBreakdown", { ...text.body, minWidth: 0 }), children: [
5254
+ /* @__PURE__ */ jsxs("p", { style: { ...text.secondary, margin: "0 0 0.5rem" }, children: [
5255
+ formatDay(usage.from_datetime),
5256
+ " to ",
5257
+ formatDay(usage.to_datetime)
5258
+ ] }),
5259
+ /* @__PURE__ */ jsx("div", { style: table.frame, children: /* @__PURE__ */ jsxs("table", { style: table.table, children: [
5260
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
5261
+ /* @__PURE__ */ jsx("th", { style: table.head(), children: "Feature" }),
5262
+ /* @__PURE__ */ jsx("th", { style: table.head(true), children: "Units" }),
5263
+ /* @__PURE__ */ jsx("th", { style: table.head(true), children: "Amount" })
5264
+ ] }) }),
5265
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((row, index) => {
5266
+ const last = index === rows.length - 1;
5267
+ return /* @__PURE__ */ jsxs("tr", { ...el("usageBreakdown__row"), children: [
5268
+ /* @__PURE__ */ jsx("td", { style: table.cell(false, last), children: row.charge.invoice_display_name || row.feature.name }),
5269
+ /* @__PURE__ */ jsx("td", { style: table.cell(true, last), children: formatDecimalString(row.units, 6) }),
5270
+ /* @__PURE__ */ jsx("td", { style: table.cell(true, last), children: formatAmount(row.amount_cents, row.amount_currency) })
5271
+ ] }, row.charge.nozle_id);
5272
+ }) }),
5273
+ /* @__PURE__ */ jsx("tfoot", { children: /* @__PURE__ */ jsxs("tr", { children: [
5274
+ /* @__PURE__ */ jsx("td", { style: table.foot(), colSpan: 2, children: "Total" }),
5275
+ /* @__PURE__ */ jsx("td", { style: table.foot(true), children: formatAmount(usage.amount_cents, usage.currency) })
5276
+ ] }) })
5277
+ ] }) })
5278
+ ] });
5279
+ }
5280
+ var muted4 = T.mutedForeground;
5281
+ var BAR_AREA = 120;
5282
+ function unitsFor(period, featureCode) {
5283
+ return period.charges_usage.filter((charge) => charge.feature.code === featureCode).reduce((sum, charge) => sum + (Number(charge.units) || 0), 0);
5284
+ }
5285
+ function UsageTrend({
5286
+ periods,
5287
+ featureCode,
5288
+ isLoading = false,
5289
+ error = null,
5290
+ emptyMessage = "No past usage yet.",
5291
+ appearance
5292
+ }) {
5293
+ const { root, el } = useAppearance(appearance);
5294
+ if (isLoading) return /* @__PURE__ */ jsx("div", { ...root("usageTrend", { ...surface, height: 160, background: T.skeleton }), role: "status", "aria-label": "Loading usage trend" });
5295
+ if (error) return /* @__PURE__ */ jsx("p", { ...root("usageTrend", { ...surface, color: T.dangerText }), role: "alert", children: typeof error === "string" ? error : error.message });
5296
+ if (!periods || periods.length === 0) return /* @__PURE__ */ jsx("p", { ...root("usageTrend", { ...surface, color: muted4 }), role: "status", children: emptyMessage });
5297
+ const ordered = [...periods].sort((a, b) => Date.parse(a.from_datetime) - Date.parse(b.from_datetime));
5298
+ const points = ordered.map((period) => ({
5299
+ key: period.from_datetime,
5300
+ label: formatDay(period.from_datetime),
5301
+ value: featureCode ? unitsFor(period, featureCode) : period.amount_cents,
5302
+ text: featureCode ? `${unitsFor(period, featureCode).toLocaleString("en-US")} ${featureCode}` : formatAmount(period.amount_cents, period.currency)
5303
+ }));
5304
+ const max = Math.max(...points.map((point) => point.value), 0);
5305
+ 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: [
5306
+ /* @__PURE__ */ jsx("div", { children: point.text }),
5307
+ /* @__PURE__ */ jsx(
5308
+ "div",
5309
+ {
5310
+ role: "img",
5311
+ "aria-label": `${point.label}: ${point.text}`,
5312
+ title: `${point.label}: ${point.text}`,
5313
+ ...el("usageTrend__bar", {
5314
+ height: max > 0 ? Math.max(2, point.value / max * BAR_AREA) : 2,
5315
+ marginTop: 4,
5316
+ borderRadius: `min(4px, ${T.radius}) min(4px, ${T.radius}) 0 0`,
5317
+ background: T.primary
5318
+ })
5319
+ }
5320
+ ),
5321
+ /* @__PURE__ */ jsx("div", { style: { marginTop: 4 }, children: point.label })
5322
+ ] }, point.key)) }) });
5323
+ }
4034
5324
 
4035
- 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 };
5325
+ 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, getAvailableIntervals, getDefaultInterval, getPricingCards, getPricingIntervalOptions, getUsageMeterColor, planFeatures, sortPlansByPrice, usageItems, useAppearance, useBillingContext, useBillingRefresh, useCheckout, useCheckout2 as useCheckoutSession, useCreditBalance, useCreditHistory, useEntitlement, useInvoiceDownload, useInvoices, useNozleBranding, useNozleClient, useOptionalBillingContext, usePlans, useSubscription, useUpgradePreview, useUsage };
4036
5326
  //# sourceMappingURL=index.js.map
4037
5327
  //# sourceMappingURL=index.js.map