@kerne/react 1.2.0 → 1.3.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/ui/index.js CHANGED
@@ -1,8 +1,30 @@
1
1
  "use client";
2
2
  import {
3
+ Button,
4
+ Divider,
5
+ Field,
6
+ FormSkeleton,
7
+ KerneBranding,
8
+ Masthead,
9
+ MethodSwitch,
10
+ OAuthButtons,
11
+ Panel,
12
+ PasswordStrength,
13
+ Shell,
14
+ Status,
15
+ Subsection,
16
+ TextLink,
17
+ appearanceToStyle,
3
18
  defaultLocalization,
19
+ defaultTokens,
20
+ ensureStylesInjected,
4
21
  fill,
5
22
  fillNode,
23
+ fr,
24
+ meetsPolicy,
25
+ mergeLocalization,
26
+ resolvePolicy,
27
+ resolveTheme,
6
28
  useAccess,
7
29
  useAuth,
8
30
  useAuthConfig,
@@ -17,1262 +39,32 @@ import {
17
39
  useSubscription,
18
40
  useUser,
19
41
  useWaitlist
20
- } from "../chunk-IHNWY4ML.js";
42
+ } from "../chunk-ZVL47U4B.js";
21
43
 
22
44
  // src/ui/AuthFlow.tsx
23
- import { useState as useState10 } from "react";
45
+ import { useState as useState9 } from "react";
24
46
 
25
47
  // src/ui/useUrlToken.ts
26
48
  import { useState, useEffect } from "react";
27
49
  function useUrlToken(param = "token", override) {
28
- const [token, setToken] = useState(override ?? null);
29
- useEffect(() => {
30
- if (override) {
31
- setToken(override);
32
- return;
33
- }
34
- if (typeof window === "undefined") return;
35
- setToken(new URLSearchParams(window.location.search).get(param) ?? "");
36
- }, [param, override]);
37
- return token;
38
- }
39
-
40
- // src/ui/LoginForm.tsx
41
- import { useState as useState3 } from "react";
42
-
43
- // src/ui/primitives.tsx
44
- import { useId, useState as useState2, useContext, createContext } from "react";
45
-
46
- // src/ui/styles.ts
47
- var STYLE_ELEMENT_ID = "kerne-ui-styles";
48
- var DARK_TOKENS = `
49
- --kerne-fg: #f0f1f3;
50
- --kerne-fg-muted: #9ba2ad;
51
- --kerne-fg-subtle: #838b96;
52
- --kerne-border: #2a2e36;
53
- --kerne-border-strong: #363b45;
54
- --kerne-panel: #1b1f26;
55
- --kerne-surface: #14171c;
56
- --kerne-surface-raised: #2a2f38;
57
- --kerne-track: #313743;
58
- --kerne-primary: #f0f1f3;
59
- --kerne-primary-fg: #16181d;
60
- --kerne-primary-hover: #d8dade;
61
- --kerne-accent: #91a7ff;
62
- --kerne-danger: #f0837a;
63
- --kerne-success: #5cbd8d;
64
- --kerne-warning: #d9ab5e;
65
- --kerne-shadow: 0 4px 6px -2px rgba(0,0,0,.4), 0 12px 24px -8px rgba(0,0,0,.6);
66
- `;
67
- var CSS = `
68
- .kerne-root {
69
- --kerne-fg: #16171a;
70
- --kerne-fg-muted: #6b7280;
71
- /* Meets 4.5:1 on white - this carries placeholders and the legal footnote,
72
- which used to sit around 2.5:1. */
73
- --kerne-fg-subtle: #6f7681;
74
- --kerne-border: #e3e5e9;
75
- --kerne-border-strong: #d6dade;
76
- --kerne-field-bg: transparent;
77
- --kerne-panel: #f4f5f7;
78
- /* The ground the component sits on. Transparent so an embedded instance
79
- inherits the host's page; set it (appearance.background) when the component
80
- owns the viewport, so text and background can never resolve independently. */
81
- --kerne-bg: transparent;
82
- /* An opaque surface for the things that must not show through: the card
83
- variant, the dropdown floating over host content, a pressed segment. */
84
- --kerne-surface: #ffffff;
85
- --kerne-surface-raised: #ffffff;
86
- /* Unfilled part of a meter, and the ground of a disabled button. Distinct
87
- from --kerne-panel, which is tuned to sit on the page: on a card the two
88
- are nearly the same colour, so a meter drawn in panel had invisible empty
89
- segments and read as having fewer steps than it has. */
90
- --kerne-track: #e3e5e9;
91
- --kerne-primary: #16171a;
92
- --kerne-primary-fg: #ffffff;
93
- --kerne-primary-hover: #2c2e33;
94
- --kerne-accent: #3b5bdb;
95
- --kerne-danger: #c0392f;
96
- --kerne-success: #1f7a4d;
97
- --kerne-warning: #9a6b12;
98
- --kerne-radius: 8px;
99
- --kerne-control-height: 44px;
100
- --kerne-shadow: 0 4px 6px -2px rgba(0,0,0,.06), 0 12px 24px -8px rgba(0,0,0,.16);
101
- --kerne-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
102
-
103
- box-sizing: border-box;
104
- font-family: var(--kerne-font);
105
- color: var(--kerne-fg);
106
- background: var(--kerne-bg);
107
- -webkit-font-smoothing: antialiased;
108
- display: flex;
109
- justify-content: center;
110
- width: 100%;
111
- }
112
- .kerne-root *, .kerne-root *::before, .kerne-root *::after { box-sizing: inherit; }
113
-
114
- .kerne-root[data-kerne-theme="dark"] {${DARK_TOKENS}}
115
- /* Only an explicit opt-in follows the visitor's OS. Defaulting to it meant a
116
- light-only host got a dark form on any machine set to dark, without ever
117
- asking for one - and with no way for the host to know it had happened. */
118
- @media (prefers-color-scheme: dark) {
119
- .kerne-root[data-kerne-theme="system"] {${DARK_TOKENS}}
120
- }
121
-
122
- .kerne-shell {
123
- width: 100%;
124
- max-width: 384px;
125
- display: flex;
126
- flex-direction: column;
127
- }
128
- /* Auth screens are a single column of fields; an account manager is a
129
- navigation plus a content pane and suffocates at 384px. */
130
- .kerne-shell[data-kerne-size="wide"] { max-width: 800px; }
131
-
132
- .kerne-shell[data-kerne-surface="card"] {
133
- background: var(--kerne-surface);
134
- border: 1px solid var(--kerne-border);
135
- border-radius: calc(var(--kerne-radius) + 6px);
136
- box-shadow: var(--kerne-shadow);
137
- padding: 32px 28px;
138
- /* The card is the ground from here down, so nested surfaces stop inheriting
139
- the page behind it. */
140
- --kerne-bg: var(--kerne-surface);
141
- }
142
- @media (max-width: 480px) {
143
- /* Full-bleed on a phone: a card with 16px of page showing on each side wastes
144
- the only axis a login form has. */
145
- .kerne-shell[data-kerne-surface="card"] {
146
- border: none;
147
- box-shadow: none;
148
- padding: 8px 0;
149
- background: transparent;
150
- /* The card no longer paints, so the ground is the page again. */
151
- --kerne-bg: transparent;
152
- }
153
- }
154
-
155
- /* ---------- Tabbed account manager ---------- */
156
- .kerne-profile { display: grid; grid-template-columns: 188px minmax(0, 1fr); gap: 40px; align-items: start; }
157
- @media (max-width: 640px) { .kerne-profile { grid-template-columns: 1fr; gap: 20px; } }
158
-
159
- .kerne-nav { display: flex; flex-direction: column; gap: 2px; position: sticky; top: 24px; }
160
- @media (max-width: 640px) {
161
- .kerne-nav {
162
- position: static;
163
- flex-direction: row;
164
- overflow-x: auto;
165
- gap: 4px;
166
- border-bottom: 1px solid var(--kerne-border);
167
- padding-bottom: 8px;
168
- }
169
- }
170
- .kerne-nav-item {
171
- font: inherit; font-size: 14px; text-align: left;
172
- background: none; border: none; cursor: pointer;
173
- padding: 8px 12px; border-radius: 6px;
174
- color: var(--kerne-fg-muted); white-space: nowrap;
175
- transition: background-color .12s ease, color .12s ease;
176
- }
177
- .kerne-nav-item:hover { color: var(--kerne-fg); }
178
- .kerne-nav-item[aria-current="page"] { background: var(--kerne-panel); color: var(--kerne-fg); font-weight: 500; }
179
- .kerne-nav-item:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; }
180
-
181
- .kerne-pane { display: flex; flex-direction: column; min-width: 0; }
182
- .kerne-pane-title { font-size: 20px; font-weight: 600; letter-spacing: -.01em; margin: 0 0 4px; }
183
- .kerne-pane-desc { font-size: 14px; color: var(--kerne-fg-muted); margin: 0 0 8px; }
184
-
185
- .kerne-subsection { padding: 24px 0; display: flex; flex-direction: column; gap: 16px; }
186
- .kerne-subsection:first-of-type { padding-top: 16px; }
187
- .kerne-subsection + .kerne-subsection { border-top: 1px solid var(--kerne-border); }
188
- .kerne-subsection-head { display: flex; flex-direction: column; gap: 3px; }
189
- .kerne-subsection-title { font-size: 15px; font-weight: 600; margin: 0; }
190
- .kerne-subsection-desc { font-size: 13px; color: var(--kerne-fg-muted); margin: 0; }
191
- /* Buttons inside a settings pane are actions, not page-wide calls to action. */
192
- .kerne-subsection .kerne-button { width: auto; align-self: flex-start; }
193
-
194
- /* ---------- Plan options ---------- */
195
- .kerne-plan-list { display: flex; flex-direction: column; gap: 10px; }
196
- .kerne-plan-option {
197
- display: flex; justify-content: space-between; align-items: center; gap: 16px;
198
- padding: 14px 16px; border: 1px solid var(--kerne-border); border-radius: var(--kerne-radius);
199
- }
200
- .kerne-plan-option[data-current="true"] { border-color: var(--kerne-fg); }
201
- .kerne-plan-option-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
202
- .kerne-plan-option-name { font-size: 14px; font-weight: 500; display: flex; align-items: center; gap: 8px; }
203
- .kerne-plan-option-price { font-size: 13px; color: var(--kerne-fg-muted); font-variant-numeric: tabular-nums; }
204
- .kerne-plan-option .kerne-button { width: auto; flex-shrink: 0; height: 36px; font-size: 13px; }
205
-
206
- /* Segmented control for billing interval. */
207
- .kerne-segmented { display: inline-flex; padding: 3px; gap: 2px; background: var(--kerne-panel); border-radius: var(--kerne-radius); align-self: flex-start; }
208
- .kerne-segment {
209
- font: inherit; font-size: 13px; border: none; background: none; cursor: pointer;
210
- padding: 5px 12px; border-radius: calc(var(--kerne-radius) - 3px); color: var(--kerne-fg-muted);
211
- }
212
- .kerne-segment[aria-pressed="true"] { background: var(--kerne-surface-raised); color: var(--kerne-fg); font-weight: 500; }
213
- .kerne-segment:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; }
214
-
215
- /* ---------- Masthead ---------- */
216
- .kerne-masthead {
217
- display: flex;
218
- flex-direction: column;
219
- align-items: center;
220
- gap: 16px;
221
- text-align: center;
222
- margin-bottom: 32px;
223
- }
224
- .kerne-logo { display: flex; align-items: center; justify-content: center; }
225
- .kerne-heading { display: flex; flex-direction: column; gap: 6px; }
226
- .kerne-title {
227
- margin: 0;
228
- font-size: 24px;
229
- font-weight: 600;
230
- letter-spacing: -.02em;
231
- line-height: 1.2;
232
- text-wrap: balance;
233
- }
234
- .kerne-subtitle { margin: 0; font-size: 14px; color: var(--kerne-fg-muted); line-height: 1.5; }
235
-
236
- /* Moving between steps (sign-in -> sign-up -> forgot password) used to be an
237
- instant swap: the content and the height both jumped at once, which reads
238
- as a page reload rather than one flow. */
239
- @keyframes kerne-step-in {
240
- from { opacity: 0; transform: translateY(4px); }
241
- to { opacity: 1; transform: none; }
242
- }
243
- .kerne-step { animation: kerne-step-in .18s ease-out; }
244
- @media (prefers-reduced-motion: reduce) {
245
- .kerne-step { animation: none; }
246
- }
247
-
248
- /* ---------- Stack ---------- */
249
- .kerne-stack { display: flex; flex-direction: column; gap: 16px; }
250
- .kerne-inline-actions { display: flex; gap: 8px; flex-wrap: wrap; }
251
- .kerne-form { display: flex; flex-direction: column; gap: 12px; }
252
- .kerne-field { display: flex; flex-direction: column; gap: 6px; }
253
- .kerne-label { font-size: 14px; font-weight: 500; color: var(--kerne-fg); }
254
- .kerne-label-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
255
-
256
- /* Positioning context for the reveal toggle, which sits inside the input box. */
257
- .kerne-control { position: relative; display: flex; }
258
-
259
- .kerne-input {
260
- font: inherit;
261
- font-size: 14px;
262
- height: var(--kerne-control-height);
263
- color: var(--kerne-fg);
264
- background: var(--kerne-field-bg);
265
- border: 1px solid var(--kerne-border-strong);
266
- border-radius: var(--kerne-radius);
267
- padding: 0 13px;
268
- width: 100%;
269
- transition: border-color .12s ease, box-shadow .12s ease;
270
- }
271
- /* Room for the toggle, so a long password never runs underneath it. */
272
- .kerne-input[data-kerne-reveal="true"] { padding-right: 42px; }
273
-
274
- .kerne-reveal {
275
- position: absolute;
276
- top: 0; right: 0; bottom: 0;
277
- width: 40px;
278
- display: flex; align-items: center; justify-content: center;
279
- background: none; border: none; padding: 0; cursor: pointer;
280
- color: var(--kerne-fg-subtle);
281
- border-radius: var(--kerne-radius);
282
- transition: color .12s ease;
283
- }
284
- .kerne-reveal:hover:not(:disabled) { color: var(--kerne-fg); }
285
- .kerne-reveal:disabled { cursor: not-allowed; opacity: .5; }
286
- .kerne-reveal:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: -2px; }
287
-
288
- .kerne-field-note { font-size: 12.5px; line-height: 1.4; color: var(--kerne-fg-muted); margin: 0; }
289
- .kerne-field-error { font-size: 12.5px; line-height: 1.4; color: var(--kerne-danger); margin: 0; }
290
- /* Caps Lock is a "you are about to get this wrong" signal, not background
291
- guidance - in muted grey it sat at the same weight as a hint and got read
292
- as one. */
293
- .kerne-field-note[data-kerne-warn="true"] { color: var(--kerne-warning); }
294
-
295
- /* ---------- Password rules ---------- */
296
- /* Unmet stays at hint weight rather than turning red: nothing is wrong yet,
297
- the user is still typing. Only the tick earns a color. */
298
- .kerne-rules { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
299
- .kerne-rule {
300
- display: flex; align-items: center; gap: 6px;
301
- font-size: 12.5px; line-height: 1.4; color: var(--kerne-fg-muted);
302
- }
303
- .kerne-rule[data-met] { color: var(--kerne-success); }
304
-
305
- /* ---------- Password strength ---------- */
306
- .kerne-strength { display: flex; flex-direction: column; gap: 5px; }
307
- .kerne-strength-track { display: flex; gap: 4px; }
308
- .kerne-strength-step {
309
- flex: 1; height: 3px; border-radius: 999px;
310
- background: var(--kerne-track);
311
- transition: background-color .18s ease;
312
- }
313
- .kerne-strength-label { font-size: 12px; color: var(--kerne-fg-muted); }
314
- .kerne-strength[data-score="0"] .kerne-strength-step[data-on],
315
- .kerne-strength[data-score="1"] .kerne-strength-step[data-on] { background: var(--kerne-danger); }
316
- .kerne-strength[data-score="2"] .kerne-strength-step[data-on] { background: var(--kerne-warning); }
317
- .kerne-strength[data-score="3"] .kerne-strength-step[data-on],
318
- .kerne-strength[data-score="4"] .kerne-strength-step[data-on] { background: var(--kerne-success); }
319
- @media (prefers-reduced-motion: reduce) {
320
- .kerne-strength-step { transition: none; }
321
- }
322
- .kerne-input::placeholder { color: var(--kerne-fg-subtle); }
323
- .kerne-input:hover:not(:disabled):not(:focus) { border-color: var(--kerne-fg-subtle); }
324
- .kerne-input:focus {
325
- outline: none;
326
- border-color: var(--kerne-accent);
327
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--kerne-accent) 15%, transparent);
328
- }
329
- .kerne-input:disabled { opacity: .6; cursor: not-allowed; }
330
- .kerne-input[aria-invalid="true"] { border-color: var(--kerne-danger); }
331
- .kerne-input[data-kerne-code="true"] {
332
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
333
- letter-spacing: .3em;
334
- text-transform: uppercase;
335
- }
336
-
337
- /* ---------- Buttons ---------- */
338
- .kerne-button {
339
- font: inherit;
340
- font-size: 14px;
341
- font-weight: 500;
342
- height: var(--kerne-control-height);
343
- border-radius: var(--kerne-radius);
344
- padding: 0 16px;
345
- cursor: pointer;
346
- border: 1px solid transparent;
347
- display: inline-flex;
348
- align-items: center;
349
- justify-content: center;
350
- gap: 10px;
351
- width: 100%;
352
- transition: background-color .12s ease, border-color .12s ease, opacity .12s ease;
353
- }
354
- .kerne-button:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; }
355
- .kerne-button:disabled { opacity: .5; cursor: not-allowed; }
356
- /* A disabled primary drops the brand entirely rather than wearing it at half
357
- opacity: on a dark theme the light primary faded into an opaque grey slab
358
- that read as an enabled button, and a brand colour has no business marking
359
- an action you cannot take. Loading is excluded - it is still the real
360
- button, just busy. */
361
- .kerne-button-primary:disabled:not([data-kerne-loading="true"]) {
362
- opacity: 1;
363
- background: var(--kerne-track);
364
- color: var(--kerne-fg-subtle);
365
- }
366
- /* A button mid-request is busy, not unavailable - dimming it to the disabled
367
- opacity reads as "this stopped working". */
368
- .kerne-button[data-kerne-loading="true"] { opacity: 1; cursor: progress; }
369
- .kerne-button .kerne-spinner { width: 15px; height: 15px; border-width: 2px; }
370
- .kerne-button-primary { background: var(--kerne-primary); color: var(--kerne-primary-fg); }
371
- .kerne-button-primary:hover:not(:disabled) { background: var(--kerne-primary-hover); }
372
- .kerne-button-outline {
373
- background: transparent;
374
- color: var(--kerne-fg);
375
- border-color: var(--kerne-border-strong);
376
- }
377
- .kerne-button-outline:hover:not(:disabled) { background: var(--kerne-panel); }
378
-
379
- /* Method switch - a quiet text control, not a second call to action competing
380
- with the primary button above it. */
381
- .kerne-switch {
382
- font: inherit;
383
- font-size: 14px;
384
- width: 100%;
385
- text-align: center;
386
- background: none;
387
- border: none;
388
- padding: 4px 0;
389
- cursor: pointer;
390
- color: var(--kerne-fg-muted);
391
- text-underline-offset: 4px;
392
- transition: color .12s ease;
393
- }
394
- .kerne-switch:hover { color: var(--kerne-fg); text-decoration: underline; }
395
- .kerne-switch:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; border-radius: 4px; }
396
-
397
- .kerne-link {
398
- font-size: inherit;
399
- color: var(--kerne-accent);
400
- background: none;
401
- border: none;
402
- padding: 0;
403
- cursor: pointer;
404
- font-family: inherit;
405
- text-decoration: none;
406
- text-underline-offset: 3px;
407
- }
408
- .kerne-link:hover { text-decoration: underline; }
409
- .kerne-link:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; border-radius: 2px; }
410
-
411
- /* ---------- Divider ---------- */
412
- .kerne-divider {
413
- display: flex;
414
- align-items: center;
415
- gap: 12px;
416
- color: var(--kerne-fg-muted);
417
- font-size: 11px;
418
- text-transform: uppercase;
419
- letter-spacing: .06em;
420
- padding: 2px 0;
421
- }
422
- .kerne-divider::before, .kerne-divider::after {
423
- content: "";
424
- flex: 1;
425
- height: 1px;
426
- background: var(--kerne-border);
427
- }
428
-
429
- /* ---------- Panels ---------- */
430
- .kerne-panel {
431
- display: flex;
432
- align-items: flex-start;
433
- gap: 10px;
434
- font-size: 14px;
435
- line-height: 1.5;
436
- padding: 12px 14px;
437
- border-radius: var(--kerne-radius);
438
- background: var(--kerne-panel);
439
- color: var(--kerne-fg-muted);
440
- }
441
- .kerne-panel-danger { color: var(--kerne-danger); }
442
- .kerne-panel-success { color: var(--kerne-success); }
443
- .kerne-panel svg { flex-shrink: 0; margin-top: 1px; }
444
- .kerne-panel strong { color: var(--kerne-fg); font-weight: 500; }
445
-
446
- .kerne-oauth-group { display: flex; flex-direction: column; gap: 10px; }
447
-
448
- /* A closing line under a form, e.g. "Remembered it? Back to sign in". */
449
- .kerne-footer { font-size: 14px; color: var(--kerne-fg-muted); text-align: center; margin: 4px 0 0; }
450
-
451
- /* ---------- Footnotes ---------- */
452
- .kerne-legal {
453
- font-size: 12px;
454
- line-height: 1.5;
455
- color: var(--kerne-fg-subtle);
456
- text-align: center;
457
- margin: 32px 0 0;
458
- }
459
- .kerne-branding {
460
- display: flex;
461
- align-items: center;
462
- justify-content: center;
463
- gap: 5px;
464
- margin-top: 24px;
465
- font-size: 12px;
466
- color: var(--kerne-fg-subtle);
467
- text-decoration: none;
468
- }
469
- .kerne-branding:hover { color: var(--kerne-fg-muted); }
470
-
471
- /* Centred outcome screen for the flows that resolve on their own (magic-link
472
- landing, email verification by link) - there is no form to show, only what
473
- happened. */
474
- .kerne-status { display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center; padding: 8px 0; }
475
- .kerne-status-icon { display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: 50%; background: var(--kerne-panel); }
476
- .kerne-status-icon[data-tone="danger"] { color: var(--kerne-danger); }
477
- .kerne-status-icon[data-tone="success"] { color: var(--kerne-success); }
478
- .kerne-status-icon[data-tone="muted"] { color: var(--kerne-fg-muted); }
479
- .kerne-status-text { font-size: 14px; color: var(--kerne-fg-muted); line-height: 1.5; margin: 0; max-width: 34ch; }
480
-
481
- .kerne-spinner {
482
- width: 20px; height: 20px;
483
- border: 2px solid currentColor;
484
- border-right-color: transparent;
485
- border-radius: 50%;
486
- animation: kerne-spin .6s linear infinite;
487
- }
488
- @keyframes kerne-spin { to { transform: rotate(360deg); } }
489
-
490
- .kerne-button-danger {
491
- background: transparent;
492
- color: var(--kerne-danger);
493
- border-color: color-mix(in srgb, var(--kerne-danger) 40%, transparent);
494
- }
495
- .kerne-button-danger:hover:not(:disabled) { background: color-mix(in srgb, var(--kerne-danger) 8%, transparent); }
496
-
497
- /* ---------- Usage meters ---------- */
498
- .kerne-meters { display: flex; flex-direction: column; gap: 18px; }
499
- .kerne-meter { display: flex; flex-direction: column; gap: 7px; }
500
- .kerne-meter-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
501
- .kerne-meter-name { font-size: 14px; font-weight: 500; }
502
- .kerne-meter-value {
503
- font-size: 13px;
504
- color: var(--kerne-fg-muted);
505
- /* Digits line up between rows instead of jittering as counts change. */
506
- font-variant-numeric: tabular-nums;
507
- }
508
- .kerne-meter-track { height: 6px; border-radius: 999px; background: var(--kerne-track); overflow: hidden; }
509
- .kerne-meter-fill { height: 100%; border-radius: 999px; background: var(--kerne-fg); transition: width .3s ease; }
510
- .kerne-meter[data-level="warning"] .kerne-meter-fill { background: var(--kerne-warning); }
511
- .kerne-meter[data-level="critical"] .kerne-meter-fill { background: var(--kerne-danger); }
512
- .kerne-meter[data-level="warning"] .kerne-meter-value { color: var(--kerne-warning); }
513
- .kerne-meter[data-level="critical"] .kerne-meter-value { color: var(--kerne-danger); }
514
- .kerne-meter-note { font-size: 12px; color: var(--kerne-fg-subtle); margin: 0; }
515
- @media (prefers-reduced-motion: reduce) { .kerne-meter-fill { transition: none; } }
516
-
517
- /* ---------- Pricing table ---------- */
518
- .kerne-pricing-skeleton { width: 100%; }
519
-
520
- .kerne-pricing-switch-wrap { display: flex; justify-content: center; }
521
- .kerne-pricing-switch {
522
- position: relative;
523
- display: inline-flex;
524
- gap: 2px;
525
- padding: 4px;
526
- background: var(--kerne-panel);
527
- border-radius: 999px;
528
- }
529
- .kerne-pricing-switch-thumb {
530
- position: absolute;
531
- top: 4px;
532
- bottom: 4px;
533
- left: 0;
534
- border-radius: 999px;
535
- background: var(--kerne-surface);
536
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
537
- transition: transform 0.32s cubic-bezier(0.16, 1, 0.3, 1), width 0.32s cubic-bezier(0.16, 1, 0.3, 1);
538
- }
539
- .kerne-pricing-switch-option {
540
- position: relative;
541
- z-index: 1;
542
- display: inline-flex;
543
- align-items: center;
544
- gap: 7px;
545
- font: inherit;
546
- font-size: 13.5px;
547
- font-weight: 500;
548
- white-space: nowrap;
549
- color: var(--kerne-fg-muted);
550
- background: none;
551
- border: none;
552
- border-radius: 999px;
553
- padding: 7px 16px;
554
- cursor: pointer;
555
- transition: color 0.18s ease;
556
- }
557
- .kerne-pricing-switch-option[aria-pressed="true"] { color: var(--kerne-fg); }
558
- .kerne-pricing-switch-option:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; }
559
- .kerne-pricing-switch-save {
560
- font-size: 10.5px;
561
- font-weight: 600;
562
- letter-spacing: 0.01em;
563
- color: var(--kerne-success);
564
- background: color-mix(in srgb, var(--kerne-success) 14%, transparent);
565
- padding: 2px 6px;
566
- border-radius: 999px;
567
- }
568
-
569
- .kerne-pricing-grid {
570
- display: grid;
571
- grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
572
- gap: 20px;
573
- align-items: stretch;
574
- }
575
-
576
- .kerne-pricing-card {
577
- position: relative;
578
- display: flex;
579
- flex-direction: column;
580
- gap: 24px;
581
- height: 100%;
582
- padding: 28px 26px;
583
- background: var(--kerne-surface);
584
- border: 1px solid var(--kerne-border);
585
- border-radius: calc(var(--kerne-radius) + 8px);
586
- }
587
- .kerne-pricing-card[data-featured="true"] {
588
- background: var(--kerne-surface-raised);
589
- border-color: var(--kerne-fg);
590
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 24px 48px -28px rgba(0, 0, 0, 0.22);
591
- }
592
- .kerne-pricing-card[data-current="true"]:not([data-featured="true"]) { border-color: var(--kerne-border-strong); }
593
-
594
- /* A quiet inline pill, not a loud badge escaping the card border - that
595
- shouted-uppercase-chip-on-the-edge look is the single most template-y
596
- tell a pricing page can have. Reads as confirmation of what the card's
597
- own elevation already signals, never as the primary signal itself. */
598
- .kerne-pricing-badge {
599
- align-self: flex-start;
600
- font-size: 11.5px;
601
- font-weight: 600;
602
- letter-spacing: 0.01em;
603
- color: var(--kerne-fg);
604
- background: var(--kerne-panel);
605
- padding: 3px 9px;
606
- border-radius: 999px;
607
- }
608
-
609
- .kerne-pricing-card-head { display: flex; flex-direction: column; gap: 8px; }
610
- .kerne-pricing-card-name { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
611
- .kerne-pricing-card-desc { margin: 0; font-size: 13px; line-height: 1.5; color: var(--kerne-fg-muted); }
612
-
613
- .kerne-pricing-price { display: flex; flex-direction: column; gap: 3px; min-height: 52px; justify-content: center; }
614
- .kerne-pricing-price-row { display: flex; align-items: baseline; gap: 5px; }
615
- .kerne-pricing-amount {
616
- font-size: 38px;
617
- font-weight: 600;
618
- letter-spacing: -0.025em;
619
- line-height: 1;
620
- font-variant-numeric: tabular-nums;
621
- }
622
- .kerne-pricing-interval { font-size: 14px; color: var(--kerne-fg-muted); }
623
- .kerne-pricing-billed { margin: 0; font-size: 12px; color: var(--kerne-fg-subtle); }
624
-
625
- .kerne-pricing-features { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 11px; flex: 1; }
626
- .kerne-pricing-feature { display: flex; align-items: flex-start; gap: 9px; font-size: 13.5px; line-height: 1.4; color: var(--kerne-fg); }
627
- .kerne-pricing-feature svg { flex-shrink: 0; margin-top: 2px; color: var(--kerne-fg-subtle); }
628
- .kerne-pricing-feature-more { font-size: 12.5px; color: var(--kerne-fg-subtle); padding-left: 24px; }
629
-
630
- .kerne-pricing-cta { margin-top: auto; }
631
- .kerne-pricing-cta .kerne-button { width: 100%; }
632
-
633
- @media (prefers-reduced-motion: reduce) {
634
- .kerne-pricing-switch-thumb { transition: none; }
635
- }
636
-
637
- /* ---------- Sections (profile, settings) ---------- */
638
- .kerne-section-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
639
- .kerne-section-title { font-size: 15px; font-weight: 600; margin: 0; }
640
-
641
- .kerne-rows { display: flex; flex-direction: column; gap: 10px; }
642
- .kerne-row { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; font-size: 14px; }
643
- .kerne-row dt { color: var(--kerne-fg-muted); margin: 0; }
644
- .kerne-row dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
645
-
646
- .kerne-badge {
647
- display: inline-flex;
648
- align-items: center;
649
- font-size: 12px;
650
- font-weight: 500;
651
- padding: 2px 8px;
652
- border-radius: 999px;
653
- background: var(--kerne-panel);
654
- color: var(--kerne-fg-muted);
655
- white-space: nowrap;
656
- }
657
- .kerne-badge[data-tone="success"] { color: var(--kerne-success); }
658
- .kerne-badge[data-tone="warning"] { color: var(--kerne-warning); }
659
- .kerne-badge[data-tone="danger"] { color: var(--kerne-danger); }
660
-
661
- /* ---------- Identity ---------- */
662
- .kerne-avatar {
663
- width: 32px; height: 32px; border-radius: 50%;
664
- background: var(--kerne-panel); color: var(--kerne-fg-muted);
665
- display: flex; align-items: center; justify-content: center;
666
- font-size: 12px; font-weight: 600; letter-spacing: .02em; flex-shrink: 0;
667
- object-fit: cover;
668
- }
669
- .kerne-identity { display: flex; align-items: center; gap: 12px; }
670
- .kerne-identity-text { display: flex; flex-direction: column; min-width: 0; }
671
- .kerne-identity-name { font-size: 14px; font-weight: 500; }
672
- .kerne-identity-mail { font-size: 13px; color: var(--kerne-fg-muted); overflow: hidden; text-overflow: ellipsis; }
673
-
674
- .kerne-menu-wrap { position: relative; display: inline-flex; }
675
- .kerne-menu-trigger {
676
- background: none; border: none; padding: 0; cursor: pointer;
677
- border-radius: 50%; display: inline-flex; font: inherit;
678
- }
679
- .kerne-menu-trigger:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; }
680
- .kerne-menu {
681
- position: absolute; top: calc(100% + 8px); right: 0; z-index: 50;
682
- min-width: 240px; padding: 8px;
683
- display: flex; flex-direction: column; gap: 2px;
684
- /* Needs an opaque ground of its own: it floats over host content. */
685
- background: var(--kerne-surface);
686
- border: 1px solid var(--kerne-border);
687
- border-radius: var(--kerne-radius);
688
- box-shadow: var(--kerne-shadow);
689
- }
690
- .kerne-menu-header { padding: 8px 10px 10px; border-bottom: 1px solid var(--kerne-border); margin-bottom: 6px; }
691
- .kerne-menu-item {
692
- font: inherit; font-size: 14px; text-align: left;
693
- background: none; border: none; cursor: pointer;
694
- padding: 8px 10px; border-radius: 6px; color: var(--kerne-fg);
695
- display: block; width: 100%; text-decoration: none;
696
- }
697
- .kerne-menu-item:hover { background: var(--kerne-panel); }
698
- .kerne-menu-item:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: -2px; }
699
- .kerne-menu-item[data-tone="danger"] { color: var(--kerne-danger); }
700
-
701
- /* ---------- Loading ---------- */
702
- .kerne-skeleton {
703
- background: var(--kerne-panel);
704
- border-radius: var(--kerne-radius);
705
- height: var(--kerne-control-height);
706
- animation: kerne-pulse 1.6s ease-in-out infinite;
707
- }
708
- @keyframes kerne-pulse { 50% { opacity: .55; } }
709
- @media (prefers-reduced-motion: reduce) {
710
- .kerne-skeleton { animation: none; }
711
- .kerne-spinner { animation-duration: 2s; }
712
- .kerne-input, .kerne-button, .kerne-switch { transition: none; }
713
- }
714
- `;
715
- function ensureStylesInjected() {
716
- if (typeof document === "undefined") return;
717
- if (document.getElementById(STYLE_ELEMENT_ID)) return;
718
- const style = document.createElement("style");
719
- style.id = STYLE_ELEMENT_ID;
720
- style.textContent = CSS;
721
- document.head.appendChild(style);
722
- }
723
- function parseColor(input) {
724
- const value = input.trim();
725
- const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value);
726
- if (hex) {
727
- const digits = hex[1].length === 3 ? hex[1].split("").map((c) => c + c).join("") : hex[1];
728
- return [
729
- parseInt(digits.slice(0, 2), 16),
730
- parseInt(digits.slice(2, 4), 16),
731
- parseInt(digits.slice(4, 6), 16)
732
- ];
733
- }
734
- const rgb = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(value);
735
- if (rgb) return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
736
- return null;
737
- }
738
- function luminance([r, g, b]) {
739
- const channel = (v) => {
740
- const s = Math.min(Math.max(v, 0), 255) / 255;
741
- return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
742
- };
743
- return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
744
- }
745
- function toHex([r, g, b]) {
746
- const pair = (v) => Math.round(Math.min(Math.max(v, 0), 255)).toString(16).padStart(2, "0");
747
- return `#${pair(r)}${pair(g)}${pair(b)}`;
748
- }
749
- function mix(color, toward, amount) {
750
- return color.map((c) => c + (toward - c) * amount);
751
- }
752
- var LABEL_CROSSOVER = 0.2;
753
- function appearanceToStyle(appearance) {
754
- if (!appearance) return {};
755
- const vars = {};
756
- if (appearance.primaryColor) {
757
- vars["--kerne-primary"] = appearance.primaryColor;
758
- vars["--kerne-accent"] = appearance.accentColor ?? appearance.primaryColor;
759
- const rgb = parseColor(appearance.primaryColor);
760
- if (rgb) {
761
- const light = luminance(rgb) > LABEL_CROSSOVER;
762
- vars["--kerne-primary-fg"] = appearance.primaryTextColor ?? (light ? "#16171a" : "#ffffff");
763
- vars["--kerne-primary-hover"] = toHex(mix(rgb, light ? 0 : 255, 0.14));
764
- } else if (appearance.primaryTextColor) {
765
- vars["--kerne-primary-fg"] = appearance.primaryTextColor;
766
- }
767
- } else {
768
- if (appearance.accentColor) vars["--kerne-accent"] = appearance.accentColor;
769
- if (appearance.primaryTextColor) vars["--kerne-primary-fg"] = appearance.primaryTextColor;
770
- }
771
- if (appearance.textColor) vars["--kerne-fg"] = appearance.textColor;
772
- if (appearance.borderColor) vars["--kerne-border-strong"] = appearance.borderColor;
773
- if (appearance.panelColor) vars["--kerne-panel"] = appearance.panelColor;
774
- if (appearance.background) vars["--kerne-bg"] = appearance.background;
775
- if (appearance.surfaceColor) {
776
- vars["--kerne-surface"] = appearance.surfaceColor;
777
- vars["--kerne-surface-raised"] = appearance.surfaceColor;
778
- }
779
- if (appearance.fontFamily) vars["--kerne-font"] = appearance.fontFamily;
780
- if (appearance.radius) vars["--kerne-radius"] = appearance.radius;
781
- if (appearance.controlHeight) vars["--kerne-control-height"] = appearance.controlHeight;
782
- return vars;
783
- }
784
- function resolveTheme(appearance) {
785
- return appearance?.theme ?? "light";
786
- }
787
-
788
- // src/ui/password.ts
789
- var MIN_PASSWORD_LENGTH = 8;
790
- var BASELINE_POLICY = {
791
- min_length: MIN_PASSWORD_LENGTH,
792
- require_uppercase: false,
793
- require_number: false,
794
- require_symbol: false
795
- };
796
- function resolvePolicy(config) {
797
- return config?.password_policy ?? BASELINE_POLICY;
798
- }
799
- function passwordRules(password, policy) {
800
- const rules = [{ key: "min_length", met: password.length >= policy.min_length }];
801
- if (policy.require_uppercase) {
802
- rules.push({ key: "require_uppercase", met: /[A-Z]/.test(password) });
803
- }
804
- if (policy.require_number) {
805
- rules.push({ key: "require_number", met: /\d/.test(password) });
806
- }
807
- if (policy.require_symbol) {
808
- rules.push({ key: "require_symbol", met: /[^A-Za-z0-9]/.test(password) });
809
- }
810
- return rules;
811
- }
812
- function meetsPolicy(password, policy) {
813
- return passwordRules(password, policy).every((rule) => rule.met);
814
- }
815
- function passwordStrength(password) {
816
- if (password.length < MIN_PASSWORD_LENGTH) return 0;
817
- const variety = [/[a-z]/, /[A-Z]/, /\d/, /[^A-Za-z0-9]/].filter((re) => re.test(password)).length;
818
- let score = 1;
819
- if (password.length >= 12) score++;
820
- if (password.length >= 16) score++;
821
- if (variety >= 3) score++;
822
- return Math.min(score, 4);
823
- }
824
-
825
- // src/ui/primitives.tsx
826
- import { jsx, jsxs } from "react/jsx-runtime";
827
- var InsideShell = createContext(false);
828
- function Shell({
829
- appearance,
830
- className,
831
- size = "auth",
832
- children
833
- }) {
834
- ensureStylesInjected();
835
- const nested = useContext(InsideShell);
836
- if (nested) {
837
- return /* @__PURE__ */ jsx("div", { className, children });
838
- }
839
- return /* @__PURE__ */ jsx(InsideShell.Provider, { value: true, children: /* @__PURE__ */ jsx(
840
- "div",
841
- {
842
- className: `kerne-root${className ? ` ${className}` : ""}`,
843
- style: appearanceToStyle(appearance),
844
- "data-kerne-theme": resolveTheme(appearance),
845
- children: /* @__PURE__ */ jsx(
846
- "div",
847
- {
848
- className: "kerne-shell",
849
- "data-kerne-size": size === "wide" ? "wide" : void 0,
850
- "data-kerne-surface": appearance?.surface === "card" ? "card" : void 0,
851
- children
852
- }
853
- )
854
- }
855
- ) });
856
- }
857
- function Subsection({
858
- title,
859
- description,
860
- children
861
- }) {
862
- return /* @__PURE__ */ jsxs("section", { className: "kerne-subsection", children: [
863
- /* @__PURE__ */ jsxs("div", { className: "kerne-subsection-head", children: [
864
- /* @__PURE__ */ jsx("h3", { className: "kerne-subsection-title", children: title }),
865
- description ? /* @__PURE__ */ jsx("p", { className: "kerne-subsection-desc", children: description }) : null
866
- ] }),
867
- children
868
- ] });
869
- }
870
- function Masthead({
871
- logo,
872
- title,
873
- subtitle
874
- }) {
875
- return /* @__PURE__ */ jsxs("div", { className: "kerne-masthead", children: [
876
- logo ? /* @__PURE__ */ jsx("div", { className: "kerne-logo", children: logo }) : null,
877
- /* @__PURE__ */ jsxs("div", { className: "kerne-heading", children: [
878
- /* @__PURE__ */ jsx("h1", { className: "kerne-title", children: title }),
879
- subtitle ? /* @__PURE__ */ jsx("p", { className: "kerne-subtitle", children: subtitle }) : null
880
- ] })
881
- ] });
882
- }
883
- function prefersAutoFocus() {
884
- if (typeof window === "undefined" || !window.matchMedia) return false;
885
- return window.matchMedia("(pointer: fine)").matches;
886
- }
887
- function Field({
888
- label,
889
- type,
890
- value,
891
- onChange,
892
- autoComplete,
893
- placeholder,
894
- disabled,
895
- required,
896
- invalid,
897
- error,
898
- action,
899
- code,
900
- autoFocus,
901
- children
902
- }) {
903
- const id = useId();
904
- const errorId = `${id}-error`;
905
- const noteId = `${id}-note`;
906
- const l10n = useLocalization();
907
- const isPassword = type === "password";
908
- const [revealed, setRevealed] = useState2(false);
909
- const [capsLock, setCapsLock] = useState2(false);
910
- const trackCapsLock = (e) => {
911
- if (!isPassword) return;
912
- setCapsLock(e.getModifierState?.("CapsLock") ?? false);
913
- };
914
- const describedBy = [error ? errorId : null, children ? noteId : null].filter(Boolean).join(" ");
915
- return /* @__PURE__ */ jsxs("div", { className: "kerne-field", children: [
916
- action ? /* @__PURE__ */ jsxs("div", { className: "kerne-label-row", children: [
917
- /* @__PURE__ */ jsx("label", { className: "kerne-label", htmlFor: id, children: label }),
918
- action
919
- ] }) : /* @__PURE__ */ jsx("label", { className: "kerne-label", htmlFor: id, children: label }),
920
- /* @__PURE__ */ jsxs("div", { className: "kerne-control", children: [
921
- /* @__PURE__ */ jsx(
922
- "input",
923
- {
924
- id,
925
- className: "kerne-input",
926
- type: isPassword && revealed ? "text" : type,
927
- value,
928
- onChange: (e) => onChange(e.target.value),
929
- onKeyUp: trackCapsLock,
930
- onBlur: () => setCapsLock(false),
931
- autoComplete,
932
- placeholder,
933
- disabled,
934
- required,
935
- "aria-invalid": invalid || !!error || void 0,
936
- "aria-describedby": describedBy || void 0,
937
- "data-kerne-code": code ? "true" : void 0,
938
- "data-kerne-reveal": isPassword ? "true" : void 0,
939
- autoFocus: autoFocus && prefersAutoFocus()
940
- }
941
- ),
942
- isPassword ? /* @__PURE__ */ jsx(
943
- "button",
944
- {
945
- type: "button",
946
- className: "kerne-reveal",
947
- onClick: () => setRevealed((v) => !v),
948
- disabled,
949
- "aria-label": revealed ? l10n.common.hidePassword : l10n.common.showPassword,
950
- "aria-pressed": revealed,
951
- tabIndex: -1,
952
- children: revealed ? /* @__PURE__ */ jsx(EyeOffIcon, {}) : /* @__PURE__ */ jsx(EyeIcon, {})
953
- }
954
- ) : null
955
- ] }),
956
- children ? /* @__PURE__ */ jsx("div", { id: noteId, children }) : null,
957
- capsLock ? /* @__PURE__ */ jsx("p", { className: "kerne-field-note", "data-kerne-warn": "true", role: "status", children: l10n.common.capsLockOn }) : null,
958
- error ? /* @__PURE__ */ jsx("p", { className: "kerne-field-error", id: errorId, role: "alert", children: error }) : null
959
- ] });
960
- }
961
- function EyeIcon() {
962
- return /* @__PURE__ */ jsxs(
963
- "svg",
964
- {
965
- width: "16",
966
- height: "16",
967
- viewBox: "0 0 24 24",
968
- fill: "none",
969
- stroke: "currentColor",
970
- strokeWidth: "2",
971
- "aria-hidden": "true",
972
- children: [
973
- /* @__PURE__ */ jsx("path", { d: "M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z", strokeLinejoin: "round" }),
974
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
975
- ]
976
- }
977
- );
978
- }
979
- function EyeOffIcon() {
980
- return /* @__PURE__ */ jsxs(
981
- "svg",
982
- {
983
- width: "16",
984
- height: "16",
985
- viewBox: "0 0 24 24",
986
- fill: "none",
987
- stroke: "currentColor",
988
- strokeWidth: "2",
989
- "aria-hidden": "true",
990
- children: [
991
- /* @__PURE__ */ jsx(
992
- "path",
993
- {
994
- d: "M10.6 6.2A9.9 9.9 0 0 1 12 6c6.4 0 10 7 10 7a15.7 15.7 0 0 1-3 3.7M6.2 7.4A15.6 15.6 0 0 0 2 13s3.6 7 10 7a9.7 9.7 0 0 0 4.3-1",
995
- strokeLinecap: "round",
996
- strokeLinejoin: "round"
997
- }
998
- ),
999
- /* @__PURE__ */ jsx("path", { d: "m3 3 18 18", strokeLinecap: "round" })
1000
- ]
1001
- }
1002
- );
1003
- }
1004
- function Button({
1005
- children,
1006
- variant = "primary",
1007
- disabled,
1008
- loading,
1009
- type = "submit",
1010
- onClick,
1011
- ariaLabel
1012
- }) {
1013
- return /* @__PURE__ */ jsxs(
1014
- "button",
1015
- {
1016
- type,
1017
- className: `kerne-button kerne-button-${variant}`,
1018
- disabled: disabled || loading,
1019
- onClick,
1020
- "aria-label": ariaLabel,
1021
- "aria-busy": loading || void 0,
1022
- "data-kerne-loading": loading ? "true" : void 0,
1023
- children: [
1024
- loading ? /* @__PURE__ */ jsx("span", { className: "kerne-spinner", "aria-hidden": "true" }) : null,
1025
- children
1026
- ]
1027
- }
1028
- );
1029
- }
1030
- function PasswordStrength({
1031
- password,
1032
- policy
1033
- }) {
1034
- const l10n = useLocalization();
1035
- const resolved = policy ?? resolvePolicy(null);
1036
- if (!meetsPolicy(password, resolved)) {
1037
- const labels2 = {
1038
- min_length: fill(l10n.common.passwordHint, { count: resolved.min_length }),
1039
- require_uppercase: l10n.common.passwordRequireUppercase,
1040
- require_number: l10n.common.passwordRequireNumber,
1041
- require_symbol: l10n.common.passwordRequireSymbol
1042
- };
1043
- const rules = passwordRules(password, resolved);
1044
- if (rules.length === 1) {
1045
- return /* @__PURE__ */ jsx("p", { className: "kerne-field-note", children: labels2[rules[0].key] });
1046
- }
1047
- return /* @__PURE__ */ jsx("ul", { className: "kerne-rules", children: rules.map((rule) => /* @__PURE__ */ jsxs("li", { className: "kerne-rule", "data-met": rule.met ? "true" : void 0, children: [
1048
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: rule.met ? "\u2713" : "\xB7" }),
1049
- labels2[rule.key]
1050
- ] }, rule.key)) });
1051
- }
1052
- const score = passwordStrength(password);
1053
- const labels = [
1054
- l10n.common.strengthWeak,
1055
- l10n.common.strengthWeak,
1056
- l10n.common.strengthFair,
1057
- l10n.common.strengthGood,
1058
- l10n.common.strengthStrong
1059
- ];
1060
- return /* @__PURE__ */ jsxs("div", { className: "kerne-strength", "data-score": score, children: [
1061
- /* @__PURE__ */ jsx("div", { className: "kerne-strength-track", "aria-hidden": "true", children: [1, 2, 3, 4].map((step) => /* @__PURE__ */ jsx(
1062
- "span",
1063
- {
1064
- className: "kerne-strength-step",
1065
- "data-on": score >= step ? "true" : void 0
1066
- },
1067
- step
1068
- )) }),
1069
- /* @__PURE__ */ jsxs("span", { className: "kerne-strength-label", children: [
1070
- l10n.common.strengthLabel,
1071
- ": ",
1072
- labels[score]
1073
- ] })
1074
- ] });
1075
- }
1076
- function MethodSwitch({
1077
- onClick,
1078
- children
1079
- }) {
1080
- return /* @__PURE__ */ jsx("button", { type: "button", className: "kerne-switch", onClick, children });
1081
- }
1082
- function AlertIcon() {
1083
- return /* @__PURE__ */ jsxs(
1084
- "svg",
1085
- {
1086
- width: "16",
1087
- height: "16",
1088
- viewBox: "0 0 24 24",
1089
- fill: "none",
1090
- stroke: "currentColor",
1091
- strokeWidth: "2",
1092
- "aria-hidden": "true",
1093
- children: [
1094
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1095
- /* @__PURE__ */ jsx("path", { d: "M12 8v4M12 16h.01", strokeLinecap: "round" })
1096
- ]
1097
- }
1098
- );
1099
- }
1100
- function MailIcon() {
1101
- return /* @__PURE__ */ jsxs(
1102
- "svg",
1103
- {
1104
- width: "16",
1105
- height: "16",
1106
- viewBox: "0 0 24 24",
1107
- fill: "none",
1108
- stroke: "currentColor",
1109
- strokeWidth: "2",
1110
- "aria-hidden": "true",
1111
- children: [
1112
- /* @__PURE__ */ jsx("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
1113
- /* @__PURE__ */ jsx("path", { d: "m2 7 10 6 10-6", strokeLinecap: "round", strokeLinejoin: "round" })
1114
- ]
1115
- }
1116
- );
1117
- }
1118
- function Panel({
1119
- children,
1120
- variant = "info"
1121
- }) {
1122
- const danger = variant === "danger";
1123
- const icon = danger ? /* @__PURE__ */ jsx(AlertIcon, {}) : variant === "success" ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx(MailIcon, {});
1124
- return /* @__PURE__ */ jsxs(
1125
- "div",
1126
- {
1127
- className: `kerne-panel${danger ? " kerne-panel-danger" : ""}${variant === "success" ? " kerne-panel-success" : ""}`,
1128
- ...danger ? { role: "alert" } : { role: "status", "aria-live": "polite" },
1129
- children: [
1130
- icon,
1131
- /* @__PURE__ */ jsx("span", { children })
1132
- ]
1133
- }
1134
- );
1135
- }
1136
- function CheckIcon() {
1137
- return /* @__PURE__ */ jsx(
1138
- "svg",
1139
- {
1140
- width: "16",
1141
- height: "16",
1142
- viewBox: "0 0 24 24",
1143
- fill: "none",
1144
- stroke: "currentColor",
1145
- strokeWidth: "2",
1146
- "aria-hidden": "true",
1147
- children: /* @__PURE__ */ jsx("path", { d: "m5 13 4 4L19 7", strokeLinecap: "round", strokeLinejoin: "round" })
1148
- }
1149
- );
1150
- }
1151
- function Status({
1152
- tone,
1153
- title,
1154
- children,
1155
- action
1156
- }) {
1157
- return /* @__PURE__ */ jsxs("div", { className: "kerne-status", ...tone === "danger" ? { role: "alert" } : {}, children: [
1158
- /* @__PURE__ */ jsx("div", { className: "kerne-status-icon", "data-tone": tone === "loading" ? "muted" : tone, children: tone === "loading" ? /* @__PURE__ */ jsx("span", { className: "kerne-spinner", "aria-hidden": "true" }) : tone === "success" ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx(AlertIcon, {}) }),
1159
- /* @__PURE__ */ jsxs("div", { className: "kerne-heading", children: [
1160
- /* @__PURE__ */ jsx("h1", { className: "kerne-title", children: title }),
1161
- children ? /* @__PURE__ */ jsx("p", { className: "kerne-status-text", children }) : null
1162
- ] }),
1163
- action
1164
- ] });
1165
- }
1166
- function Divider({ label }) {
1167
- return /* @__PURE__ */ jsx("div", { className: "kerne-divider", children: label });
1168
- }
1169
- function TextLink({
1170
- href,
1171
- onClick,
1172
- children
1173
- }) {
1174
- if (onClick) {
1175
- return /* @__PURE__ */ jsx("button", { type: "button", className: "kerne-link", onClick, children });
1176
- }
1177
- return /* @__PURE__ */ jsx("a", { className: "kerne-link", href: href ?? "#", children });
1178
- }
1179
- var OAUTH_LABELS = {
1180
- google: "Google",
1181
- github: "GitHub",
1182
- microsoft: "Microsoft",
1183
- apple: "Apple",
1184
- gitlab: "GitLab",
1185
- discord: "Discord"
1186
- };
1187
- function OAuthIcon({ provider }) {
1188
- const common = { width: 18, height: 18, viewBox: "0 0 24 24", "aria-hidden": true };
1189
- switch (provider) {
1190
- case "google":
1191
- return /* @__PURE__ */ jsxs("svg", { ...common, children: [
1192
- /* @__PURE__ */ jsx(
1193
- "path",
1194
- {
1195
- fill: "#4285F4",
1196
- d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
1197
- }
1198
- ),
1199
- /* @__PURE__ */ jsx(
1200
- "path",
1201
- {
1202
- fill: "#34A853",
1203
- d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
1204
- }
1205
- ),
1206
- /* @__PURE__ */ jsx(
1207
- "path",
1208
- {
1209
- fill: "#FBBC05",
1210
- d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
1211
- }
1212
- ),
1213
- /* @__PURE__ */ jsx(
1214
- "path",
1215
- {
1216
- fill: "#EA4335",
1217
- d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
1218
- }
1219
- )
1220
- ] });
1221
- case "github":
1222
- return /* @__PURE__ */ jsx("svg", { ...common, fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.26.8-.57v-2c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.75.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.14-.3-.54-1.52.1-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.28-1.55 3.29-1.23 3.29-1.23.64 1.66.24 2.88.12 3.18.77.84 1.23 1.91 1.23 3.22 0 4.61-2.8 5.63-5.48 5.92.43.37.81 1.1.81 2.22v3.29c0 .31.2.68.81.57A12 12 0 0 0 12 .3z" }) });
1223
- case "apple":
1224
- return /* @__PURE__ */ jsx("svg", { ...common, fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M17.05 12.75c-.03-2.6 2.12-3.85 2.22-3.91-1.21-1.77-3.09-2.01-3.76-2.04-1.6-.16-3.12.94-3.93.94-.81 0-2.06-.92-3.39-.9-1.74.03-3.35 1.01-4.25 2.57-1.81 3.14-.46 7.79 1.3 10.34.86 1.25 1.89 2.65 3.24 2.6 1.3-.05 1.79-.84 3.36-.84 1.57 0 2.01.84 3.38.81 1.4-.02 2.28-1.27 3.13-2.53.99-1.45 1.4-2.86 1.42-2.93-.03-.01-2.72-1.04-2.75-4.13zM14.5 4.9c.71-.87 1.19-2.07 1.06-3.27-1.02.04-2.26.68-3 1.55-.66.76-1.24 1.98-1.09 3.15 1.14.09 2.31-.58 3.03-1.43z" }) });
1225
- default:
1226
- return null;
1227
- }
1228
- }
1229
- function OAuthButtons({
1230
- providers,
1231
- disabled,
1232
- onSelect
1233
- }) {
1234
- const l10n = useLocalization();
1235
- if (providers.length === 0) return null;
1236
- return /* @__PURE__ */ jsx("div", { className: "kerne-oauth-group", children: providers.map((provider) => {
1237
- const label = fill(l10n.common.continueWith, {
1238
- provider: OAUTH_LABELS[provider] ?? provider
1239
- });
1240
- return /* @__PURE__ */ jsxs(
1241
- "button",
1242
- {
1243
- type: "button",
1244
- className: "kerne-button kerne-button-outline",
1245
- disabled,
1246
- onClick: () => onSelect?.(provider),
1247
- "aria-label": label,
1248
- children: [
1249
- /* @__PURE__ */ jsx(OAuthIcon, { provider }),
1250
- label
1251
- ]
1252
- },
1253
- provider
1254
- );
1255
- }) });
1256
- }
1257
- function KerneBranding({ show }) {
1258
- const l10n = useLocalization();
1259
- if (!show) return null;
1260
- return /* @__PURE__ */ jsx("a", { className: "kerne-branding", href: "https://kerne.io", target: "_blank", rel: "noopener noreferrer", children: l10n.common.securedByKerne });
1261
- }
1262
- function FormSkeleton() {
1263
- const l10n = useLocalization();
1264
- return /* @__PURE__ */ jsxs("div", { className: "kerne-form", "aria-busy": "true", "aria-label": l10n.common.loading, children: [
1265
- /* @__PURE__ */ jsx("div", { className: "kerne-skeleton" }),
1266
- /* @__PURE__ */ jsx("div", { className: "kerne-skeleton" })
1267
- ] });
50
+ const [token, setToken] = useState(override ?? null);
51
+ useEffect(() => {
52
+ if (override) {
53
+ setToken(override);
54
+ return;
55
+ }
56
+ if (typeof window === "undefined") return;
57
+ setToken(new URLSearchParams(window.location.search).get(param) ?? "");
58
+ }, [param, override]);
59
+ return token;
1268
60
  }
1269
61
 
1270
62
  // src/ui/LoginForm.tsx
1271
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
63
+ import { useState as useState2 } from "react";
64
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1272
65
  function LoginForm({
1273
66
  onSuccess,
1274
67
  redirectUrl,
1275
- magicLinkCallbackUrl,
1276
68
  signUpUrl,
1277
69
  onSignUpClick,
1278
70
  forgotPasswordUrl,
@@ -1290,13 +82,13 @@ function LoginForm({
1290
82
  const { config, isLoading: configLoading, error: configError } = useAuthConfig();
1291
83
  const l10n = useLocalization();
1292
84
  const showError = useErrorResolver();
1293
- const [email, setEmail] = useState3("");
1294
- const [password, setPassword] = useState3("");
1295
- const [submitting, setSubmitting] = useState3(false);
1296
- const [error, setError] = useState3(null);
1297
- const [linkSentTo, setLinkSentTo] = useState3(null);
1298
- const [passwordMode, setPasswordMode] = useState3(false);
1299
- const shell = (children) => /* @__PURE__ */ jsx2(Shell, { appearance, className, children });
85
+ const [email, setEmail] = useState2("");
86
+ const [password, setPassword] = useState2("");
87
+ const [submitting, setSubmitting] = useState2(false);
88
+ const [error, setError] = useState2(null);
89
+ const [linkSentTo, setLinkSentTo] = useState2(null);
90
+ const [passwordMode, setPasswordMode] = useState2(false);
91
+ const shell = (children) => /* @__PURE__ */ jsx(Shell, { appearance, className, children });
1300
92
  const hasOAuth = (config?.oauth_providers.length ?? 0) > 0;
1301
93
  const hasCredentials = config?.enable_credentials ?? false;
1302
94
  const hasMagicLink = config?.enable_magic_link ?? false;
@@ -1315,7 +107,7 @@ function LoginForm({
1315
107
  onSuccess?.();
1316
108
  }
1317
109
  } else {
1318
- await startPasswordless(email, magicLinkCallbackUrl);
110
+ await startPasswordless(email);
1319
111
  setLinkSentTo(email);
1320
112
  }
1321
113
  } catch (err) {
@@ -1327,43 +119,43 @@ function LoginForm({
1327
119
  const heading = title ?? l10n.signIn.title;
1328
120
  if (configLoading) {
1329
121
  return shell(
1330
- /* @__PURE__ */ jsxs2(Fragment, { children: [
1331
- /* @__PURE__ */ jsx2(Masthead, { logo, title: heading }),
1332
- /* @__PURE__ */ jsx2(FormSkeleton, {})
122
+ /* @__PURE__ */ jsxs(Fragment, { children: [
123
+ /* @__PURE__ */ jsx(Masthead, { logo, title: heading }),
124
+ /* @__PURE__ */ jsx(FormSkeleton, {})
1333
125
  ] })
1334
126
  );
1335
127
  }
1336
128
  if (configError || !config) {
1337
129
  return shell(
1338
- /* @__PURE__ */ jsxs2(Fragment, { children: [
1339
- /* @__PURE__ */ jsx2(Masthead, { logo, title: heading }),
1340
- /* @__PURE__ */ jsx2(Panel, { variant: "danger", children: l10n.signIn.loadFailed })
130
+ /* @__PURE__ */ jsxs(Fragment, { children: [
131
+ /* @__PURE__ */ jsx(Masthead, { logo, title: heading }),
132
+ /* @__PURE__ */ jsx(Panel, { variant: "danger", children: l10n.signIn.loadFailed })
1341
133
  ] })
1342
134
  );
1343
135
  }
1344
136
  if (!hasEmailMethod && !hasOAuth) {
1345
137
  return shell(
1346
- /* @__PURE__ */ jsxs2(Fragment, { children: [
1347
- /* @__PURE__ */ jsx2(Masthead, { logo, title: heading }),
1348
- /* @__PURE__ */ jsx2(Panel, { variant: "danger", children: l10n.signIn.unavailable })
138
+ /* @__PURE__ */ jsxs(Fragment, { children: [
139
+ /* @__PURE__ */ jsx(Masthead, { logo, title: heading }),
140
+ /* @__PURE__ */ jsx(Panel, { variant: "danger", children: l10n.signIn.unavailable })
1349
141
  ] })
1350
142
  );
1351
143
  }
1352
144
  const canSignUp = config.registration_mode !== "CLOSED";
1353
- const signUpLine = canSignUp && (signUpUrl || onSignUpClick) ? /* @__PURE__ */ jsxs2(Fragment, { children: [
145
+ const signUpLine = canSignUp && (signUpUrl || onSignUpClick) ? /* @__PURE__ */ jsxs(Fragment, { children: [
1354
146
  l10n.signIn.signUpPrompt,
1355
147
  " ",
1356
- /* @__PURE__ */ jsx2(TextLink, { href: signUpUrl, onClick: onSignUpClick, children: appName ? fill(l10n.signIn.signUpForApp, { app: appName }) : l10n.signIn.signUpLink }),
148
+ /* @__PURE__ */ jsx(TextLink, { href: signUpUrl, onClick: onSignUpClick, children: appName ? fill(l10n.signIn.signUpForApp, { app: appName }) : l10n.signIn.signUpLink }),
1357
149
  "."
1358
150
  ] }) : void 0;
1359
151
  const submitLabel = submitting ? usePassword ? l10n.signIn.submitting : l10n.signIn.magicSubmitting : usePassword ? l10n.signIn.submit : l10n.signIn.magicSubmit;
1360
152
  return shell(
1361
- /* @__PURE__ */ jsxs2(Fragment, { children: [
1362
- /* @__PURE__ */ jsx2(Masthead, { logo, title: heading, subtitle: subtitle ?? signUpLine }),
1363
- /* @__PURE__ */ jsxs2("div", { className: "kerne-stack", children: [
1364
- error ? /* @__PURE__ */ jsx2(Panel, { variant: "danger", children: error }) : null,
1365
- linkSentTo ? /* @__PURE__ */ jsx2(Panel, { children: fillNode(l10n.signIn.linkSent, { email: /* @__PURE__ */ jsx2("strong", { children: linkSentTo }) }) }) : hasEmailMethod ? /* @__PURE__ */ jsxs2("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
1366
- /* @__PURE__ */ jsx2(
153
+ /* @__PURE__ */ jsxs(Fragment, { children: [
154
+ /* @__PURE__ */ jsx(Masthead, { logo, title: heading, subtitle: subtitle ?? signUpLine }),
155
+ /* @__PURE__ */ jsxs("div", { className: "kerne-stack", children: [
156
+ error ? /* @__PURE__ */ jsx(Panel, { variant: "danger", children: error }) : null,
157
+ linkSentTo ? /* @__PURE__ */ jsx(Panel, { children: fillNode(l10n.signIn.linkSent, { email: /* @__PURE__ */ jsx("strong", { children: linkSentTo }) }) }) : hasEmailMethod ? /* @__PURE__ */ jsxs("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
158
+ /* @__PURE__ */ jsx(
1367
159
  Field,
1368
160
  {
1369
161
  label: l10n.common.email,
@@ -1377,7 +169,7 @@ function LoginForm({
1377
169
  autoFocus: true
1378
170
  }
1379
171
  ),
1380
- usePassword ? /* @__PURE__ */ jsx2(
172
+ usePassword ? /* @__PURE__ */ jsx(
1381
173
  Field,
1382
174
  {
1383
175
  label: l10n.common.password,
@@ -1388,11 +180,11 @@ function LoginForm({
1388
180
  placeholder: l10n.common.passwordPlaceholder,
1389
181
  disabled: submitting,
1390
182
  required: true,
1391
- action: forgotPasswordUrl || onForgotPasswordClick ? /* @__PURE__ */ jsx2(TextLink, { href: forgotPasswordUrl, onClick: onForgotPasswordClick, children: l10n.signIn.forgotPassword }) : void 0
183
+ action: forgotPasswordUrl || onForgotPasswordClick ? /* @__PURE__ */ jsx(TextLink, { href: forgotPasswordUrl, onClick: onForgotPasswordClick, children: l10n.signIn.forgotPassword }) : void 0
1392
184
  }
1393
185
  ) : null,
1394
- /* @__PURE__ */ jsx2(Button, { loading: submitting, disabled: !email || usePassword && !password, children: submitLabel }),
1395
- hasCredentials && hasMagicLink ? /* @__PURE__ */ jsx2(
186
+ /* @__PURE__ */ jsx(Button, { loading: submitting, disabled: !email || usePassword && !password, children: submitLabel }),
187
+ hasCredentials && hasMagicLink ? /* @__PURE__ */ jsx(
1396
188
  MethodSwitch,
1397
189
  {
1398
190
  onClick: () => {
@@ -1403,8 +195,8 @@ function LoginForm({
1403
195
  }
1404
196
  ) : null
1405
197
  ] }) : null,
1406
- hasOAuth && hasEmailMethod ? /* @__PURE__ */ jsx2(Divider, { label: l10n.common.or }) : null,
1407
- /* @__PURE__ */ jsx2(
198
+ hasOAuth && hasEmailMethod ? /* @__PURE__ */ jsx(Divider, { label: l10n.common.or }) : null,
199
+ /* @__PURE__ */ jsx(
1408
200
  OAuthButtons,
1409
201
  {
1410
202
  providers: config.oauth_providers,
@@ -1413,19 +205,18 @@ function LoginForm({
1413
205
  }
1414
206
  )
1415
207
  ] }),
1416
- legal ? /* @__PURE__ */ jsx2("p", { className: "kerne-legal", children: legal }) : null,
1417
- /* @__PURE__ */ jsx2(KerneBranding, { show: config.show_kerne_branding })
208
+ legal ? /* @__PURE__ */ jsx("p", { className: "kerne-legal", children: legal }) : null,
209
+ /* @__PURE__ */ jsx(KerneBranding, { show: config.show_kerne_branding })
1418
210
  ] })
1419
211
  );
1420
212
  }
1421
213
 
1422
214
  // src/ui/RegisterForm.tsx
1423
- import { useState as useState4 } from "react";
1424
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
215
+ import { useState as useState3 } from "react";
216
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1425
217
  function RegisterForm({
1426
218
  onSuccess,
1427
219
  redirectUrl,
1428
- magicLinkCallbackUrl,
1429
220
  invitationToken: invitationTokenProp,
1430
221
  signInUrl,
1431
222
  onSignInClick,
@@ -1445,45 +236,50 @@ function RegisterForm({
1445
236
  const invitationToken = useUrlToken("token", invitationTokenProp) || void 0;
1446
237
  const l10n = useLocalization();
1447
238
  const showError = useErrorResolver();
1448
- const [email, setEmail] = useState4("");
1449
- const [password, setPassword] = useState4("");
1450
- const [name, setName] = useState4("");
1451
- const [invitationCode, setInvitationCode] = useState4("");
1452
- const [submitting, setSubmitting] = useState4(false);
1453
- const [error, setError] = useState4(null);
1454
- const [linkSentTo, setLinkSentTo] = useState4(null);
1455
- const [passwordMode, setPasswordMode] = useState4(false);
1456
- const shell = (children) => /* @__PURE__ */ jsx3(Shell, { appearance, className, children });
239
+ const [email, setEmail] = useState3("");
240
+ const [password, setPassword] = useState3("");
241
+ const [name, setName] = useState3("");
242
+ const [invitationCode, setInvitationCode] = useState3("");
243
+ const [submitting, setSubmitting] = useState3(false);
244
+ const [error, setError] = useState3(null);
245
+ const [linkSentTo, setLinkSentTo] = useState3(null);
246
+ const [passwordMode, setPasswordMode] = useState3(false);
247
+ const shell = (children) => /* @__PURE__ */ jsx2(Shell, { appearance, className, children });
1457
248
  const appName = config?.app_name;
1458
249
  const heading = title ?? (appName ? fill(l10n.signUp.titleWithApp, { app: appName }) : l10n.signUp.title);
1459
- const signInLine = signInUrl || onSignInClick ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
250
+ const invitationNeeded = /* @__PURE__ */ jsxs2(Fragment2, { children: [
251
+ l10n.signUp.invitationNeeded,
252
+ " ",
253
+ /* @__PURE__ */ jsx2(TextLink, { href: signInUrl, onClick: onSignInClick, children: l10n.signUp.signInLink })
254
+ ] });
255
+ const signInLine = signInUrl || onSignInClick ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1460
256
  l10n.signUp.signInPrompt,
1461
257
  " ",
1462
- /* @__PURE__ */ jsx3(TextLink, { href: signInUrl, onClick: onSignInClick, children: l10n.signUp.signInLink }),
258
+ /* @__PURE__ */ jsx2(TextLink, { href: signInUrl, onClick: onSignInClick, children: l10n.signUp.signInLink }),
1463
259
  "."
1464
260
  ] }) : void 0;
1465
261
  if (configLoading) {
1466
262
  return shell(
1467
- /* @__PURE__ */ jsxs3(Fragment2, { children: [
1468
- /* @__PURE__ */ jsx3(Masthead, { logo, title: heading }),
1469
- /* @__PURE__ */ jsx3(FormSkeleton, {})
263
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
264
+ /* @__PURE__ */ jsx2(Masthead, { logo, title: heading }),
265
+ /* @__PURE__ */ jsx2(FormSkeleton, {})
1470
266
  ] })
1471
267
  );
1472
268
  }
1473
269
  if (configError || !config) {
1474
270
  return shell(
1475
- /* @__PURE__ */ jsxs3(Fragment2, { children: [
1476
- /* @__PURE__ */ jsx3(Masthead, { logo, title: heading }),
1477
- /* @__PURE__ */ jsx3(Panel, { variant: "danger", children: l10n.signUp.loadFailed })
271
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
272
+ /* @__PURE__ */ jsx2(Masthead, { logo, title: heading }),
273
+ /* @__PURE__ */ jsx2(Panel, { variant: "danger", children: l10n.signUp.loadFailed })
1478
274
  ] })
1479
275
  );
1480
276
  }
1481
277
  if (config.registration_mode === "CLOSED") {
1482
278
  return shell(
1483
- /* @__PURE__ */ jsxs3(Fragment2, { children: [
1484
- /* @__PURE__ */ jsx3(Masthead, { logo, title: l10n.signUp.closedTitle, subtitle: signInLine }),
1485
- /* @__PURE__ */ jsx3(Panel, { children: fill(l10n.signUp.closedBody, { app: appName ?? l10n.common.thisApp }) }),
1486
- /* @__PURE__ */ jsx3(KerneBranding, { show: config.show_kerne_branding })
279
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
280
+ /* @__PURE__ */ jsx2(Masthead, { logo, title: l10n.signUp.closedTitle, subtitle: signInLine }),
281
+ /* @__PURE__ */ jsx2(Panel, { children: fill(l10n.signUp.closedBody, { app: appName ?? l10n.common.thisApp }) }),
282
+ /* @__PURE__ */ jsx2(KerneBranding, { show: config.show_kerne_branding })
1487
283
  ] })
1488
284
  );
1489
285
  }
@@ -1512,7 +308,7 @@ function RegisterForm({
1512
308
  onSuccess?.();
1513
309
  }
1514
310
  } else {
1515
- await startPasswordless(email, magicLinkCallbackUrl, {
311
+ await startPasswordless(email, {
1516
312
  invitationToken,
1517
313
  invitationCode: invitationCode || void 0
1518
314
  });
@@ -1525,27 +321,27 @@ function RegisterForm({
1525
321
  };
1526
322
  if (!hasEmailMethod && !hasOAuth) {
1527
323
  return shell(
1528
- /* @__PURE__ */ jsxs3(Fragment2, { children: [
1529
- /* @__PURE__ */ jsx3(Masthead, { logo, title: heading }),
1530
- /* @__PURE__ */ jsx3(Panel, { variant: "danger", children: l10n.signUp.unavailable })
324
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
325
+ /* @__PURE__ */ jsx2(Masthead, { logo, title: heading }),
326
+ /* @__PURE__ */ jsx2(Panel, { variant: "danger", children: l10n.signUp.unavailable })
1531
327
  ] })
1532
328
  );
1533
329
  }
1534
330
  const submitLabel = submitting ? usePassword ? l10n.signUp.submitting : l10n.signUp.magicSubmitting : usePassword ? l10n.signUp.submit : l10n.signUp.magicSubmit;
1535
331
  return shell(
1536
- /* @__PURE__ */ jsxs3(Fragment2, { children: [
1537
- /* @__PURE__ */ jsx3(
332
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
333
+ /* @__PURE__ */ jsx2(
1538
334
  Masthead,
1539
335
  {
1540
336
  logo,
1541
337
  title: heading,
1542
- subtitle: subtitle ?? (needsCode ? l10n.signUp.invitationNeeded : signInLine)
338
+ subtitle: subtitle ?? (needsCode ? invitationNeeded : signInLine)
1543
339
  }
1544
340
  ),
1545
- /* @__PURE__ */ jsxs3("div", { className: "kerne-stack", children: [
1546
- error ? /* @__PURE__ */ jsx3(Panel, { variant: "danger", children: error }) : null,
1547
- linkSentTo ? /* @__PURE__ */ jsx3(Panel, { children: fillNode(l10n.signUp.linkSent, { email: /* @__PURE__ */ jsx3("strong", { children: linkSentTo }) }) }) : hasEmailMethod ? /* @__PURE__ */ jsxs3("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
1548
- needsCode ? /* @__PURE__ */ jsx3(
341
+ /* @__PURE__ */ jsxs2("div", { className: "kerne-stack", children: [
342
+ error ? /* @__PURE__ */ jsx2(Panel, { variant: "danger", children: error }) : null,
343
+ linkSentTo ? /* @__PURE__ */ jsx2(Panel, { children: fillNode(l10n.signUp.linkSent, { email: /* @__PURE__ */ jsx2("strong", { children: linkSentTo }) }) }) : hasEmailMethod ? /* @__PURE__ */ jsxs2("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
344
+ needsCode ? /* @__PURE__ */ jsx2(
1549
345
  Field,
1550
346
  {
1551
347
  label: l10n.signUp.invitationCode,
@@ -1560,7 +356,7 @@ function RegisterForm({
1560
356
  autoFocus: true
1561
357
  }
1562
358
  ) : null,
1563
- collectName && usePassword ? /* @__PURE__ */ jsx3(
359
+ collectName && usePassword ? /* @__PURE__ */ jsx2(
1564
360
  Field,
1565
361
  {
1566
362
  label: l10n.common.name,
@@ -1572,7 +368,7 @@ function RegisterForm({
1572
368
  disabled: submitting
1573
369
  }
1574
370
  ) : null,
1575
- /* @__PURE__ */ jsx3(
371
+ /* @__PURE__ */ jsx2(
1576
372
  Field,
1577
373
  {
1578
374
  label: l10n.common.email,
@@ -1586,7 +382,7 @@ function RegisterForm({
1586
382
  autoFocus: !needsCode
1587
383
  }
1588
384
  ),
1589
- usePassword ? /* @__PURE__ */ jsx3(
385
+ usePassword ? /* @__PURE__ */ jsx2(
1590
386
  Field,
1591
387
  {
1592
388
  label: l10n.common.password,
@@ -1597,10 +393,10 @@ function RegisterForm({
1597
393
  placeholder: l10n.common.passwordPlaceholder,
1598
394
  disabled: submitting,
1599
395
  required: true,
1600
- children: /* @__PURE__ */ jsx3(PasswordStrength, { password, policy })
396
+ children: /* @__PURE__ */ jsx2(PasswordStrength, { password, policy })
1601
397
  }
1602
398
  ) : null,
1603
- /* @__PURE__ */ jsx3(
399
+ /* @__PURE__ */ jsx2(
1604
400
  Button,
1605
401
  {
1606
402
  loading: submitting,
@@ -1608,7 +404,7 @@ function RegisterForm({
1608
404
  children: submitLabel
1609
405
  }
1610
406
  ),
1611
- hasCredentials && hasMagicLink ? /* @__PURE__ */ jsx3(
407
+ hasCredentials && hasMagicLink ? /* @__PURE__ */ jsx2(
1612
408
  MethodSwitch,
1613
409
  {
1614
410
  onClick: () => {
@@ -1619,8 +415,8 @@ function RegisterForm({
1619
415
  }
1620
416
  ) : null
1621
417
  ] }) : null,
1622
- hasOAuth && hasEmailMethod ? /* @__PURE__ */ jsx3(Divider, { label: l10n.common.or }) : null,
1623
- /* @__PURE__ */ jsx3(
418
+ hasOAuth && hasEmailMethod ? /* @__PURE__ */ jsx2(Divider, { label: l10n.common.or }) : null,
419
+ /* @__PURE__ */ jsx2(
1624
420
  OAuthButtons,
1625
421
  {
1626
422
  providers: config.oauth_providers,
@@ -1629,17 +425,16 @@ function RegisterForm({
1629
425
  }
1630
426
  )
1631
427
  ] }),
1632
- legal ? /* @__PURE__ */ jsx3("p", { className: "kerne-legal", children: legal }) : null,
1633
- /* @__PURE__ */ jsx3(KerneBranding, { show: config.show_kerne_branding })
428
+ legal ? /* @__PURE__ */ jsx2("p", { className: "kerne-legal", children: legal }) : null,
429
+ /* @__PURE__ */ jsx2(KerneBranding, { show: config.show_kerne_branding })
1634
430
  ] })
1635
431
  );
1636
432
  }
1637
433
 
1638
434
  // src/ui/ForgotPasswordForm.tsx
1639
- import { useState as useState5 } from "react";
1640
- import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
435
+ import { useState as useState4 } from "react";
436
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1641
437
  function ForgotPasswordForm({
1642
- callbackUrl,
1643
438
  signInUrl,
1644
439
  onSignInClick,
1645
440
  onSent,
@@ -1653,16 +448,16 @@ function ForgotPasswordForm({
1653
448
  const { config } = useAuthConfig();
1654
449
  const l10n = useLocalization();
1655
450
  const showError = useErrorResolver();
1656
- const [email, setEmail] = useState5("");
1657
- const [submitting, setSubmitting] = useState5(false);
1658
- const [error, setError] = useState5(null);
1659
- const [sentTo, setSentTo] = useState5(null);
451
+ const [email, setEmail] = useState4("");
452
+ const [submitting, setSubmitting] = useState4(false);
453
+ const [error, setError] = useState4(null);
454
+ const [sentTo, setSentTo] = useState4(null);
1660
455
  const handleSubmit = async (e) => {
1661
456
  e.preventDefault();
1662
457
  setError(null);
1663
458
  setSubmitting(true);
1664
459
  try {
1665
- await requestPasswordReset(email, callbackUrl);
460
+ await requestPasswordReset(email);
1666
461
  setSentTo(email);
1667
462
  onSent?.(email);
1668
463
  } catch (err) {
@@ -1671,14 +466,14 @@ function ForgotPasswordForm({
1671
466
  setSubmitting(false);
1672
467
  }
1673
468
  };
1674
- const backLine = signInUrl || onSignInClick ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
469
+ const backLine = signInUrl || onSignInClick ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
1675
470
  l10n.resetPassword.rememberedPrompt,
1676
471
  " ",
1677
- /* @__PURE__ */ jsx4(TextLink, { href: signInUrl, onClick: onSignInClick, children: l10n.common.backToSignIn }),
472
+ /* @__PURE__ */ jsx3(TextLink, { href: signInUrl, onClick: onSignInClick, children: l10n.common.backToSignIn }),
1678
473
  "."
1679
474
  ] }) : void 0;
1680
- return /* @__PURE__ */ jsxs4(Shell, { appearance, className, children: [
1681
- /* @__PURE__ */ jsx4(
475
+ return /* @__PURE__ */ jsxs3(Shell, { appearance, className, children: [
476
+ /* @__PURE__ */ jsx3(
1682
477
  Masthead,
1683
478
  {
1684
479
  logo,
@@ -1686,10 +481,10 @@ function ForgotPasswordForm({
1686
481
  subtitle: subtitle ?? (sentTo ? backLine : l10n.resetPassword.requestSubtitle)
1687
482
  }
1688
483
  ),
1689
- /* @__PURE__ */ jsxs4("div", { className: "kerne-stack", children: [
1690
- error ? /* @__PURE__ */ jsx4(Panel, { variant: "danger", children: error }) : null,
1691
- sentTo ? /* @__PURE__ */ jsx4(Panel, { children: fillNode(l10n.resetPassword.requestSent, { email: /* @__PURE__ */ jsx4("strong", { children: sentTo }) }) }) : /* @__PURE__ */ jsxs4("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
1692
- /* @__PURE__ */ jsx4(
484
+ /* @__PURE__ */ jsxs3("div", { className: "kerne-stack", children: [
485
+ error ? /* @__PURE__ */ jsx3(Panel, { variant: "danger", children: error }) : null,
486
+ sentTo ? /* @__PURE__ */ jsx3(Panel, { children: fillNode(l10n.resetPassword.requestSent, { email: /* @__PURE__ */ jsx3("strong", { children: sentTo }) }) }) : /* @__PURE__ */ jsxs3("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
487
+ /* @__PURE__ */ jsx3(
1693
488
  Field,
1694
489
  {
1695
490
  label: l10n.common.email,
@@ -1703,17 +498,17 @@ function ForgotPasswordForm({
1703
498
  autoFocus: true
1704
499
  }
1705
500
  ),
1706
- /* @__PURE__ */ jsx4(Button, { loading: submitting, disabled: !email, children: submitting ? l10n.resetPassword.requestSubmitting : l10n.resetPassword.requestSubmit }),
1707
- backLine ? /* @__PURE__ */ jsx4("p", { className: "kerne-footer", children: backLine }) : null
501
+ /* @__PURE__ */ jsx3(Button, { loading: submitting, disabled: !email, children: submitting ? l10n.resetPassword.requestSubmitting : l10n.resetPassword.requestSubmit }),
502
+ backLine ? /* @__PURE__ */ jsx3("p", { className: "kerne-footer", children: backLine }) : null
1708
503
  ] })
1709
504
  ] }),
1710
- /* @__PURE__ */ jsx4(KerneBranding, { show: config?.show_kerne_branding ?? false })
505
+ /* @__PURE__ */ jsx3(KerneBranding, { show: config?.show_kerne_branding ?? false })
1711
506
  ] });
1712
507
  }
1713
508
 
1714
509
  // src/ui/ResetPasswordForm.tsx
1715
- import { useState as useState6 } from "react";
1716
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
510
+ import { useState as useState5 } from "react";
511
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1717
512
  function ResetPasswordForm({
1718
513
  token: tokenProp,
1719
514
  signInUrl,
@@ -1731,14 +526,14 @@ function ResetPasswordForm({
1731
526
  const token = useUrlToken("token", tokenProp);
1732
527
  const l10n = useLocalization();
1733
528
  const showError = useErrorResolver();
1734
- const [password, setPassword] = useState6("");
1735
- const [confirm, setConfirm] = useState6("");
1736
- const [submitting, setSubmitting] = useState6(false);
1737
- const [error, setError] = useState6(null);
1738
- const [done, setDone] = useState6(false);
529
+ const [password, setPassword] = useState5("");
530
+ const [confirm, setConfirm] = useState5("");
531
+ const [submitting, setSubmitting] = useState5(false);
532
+ const [error, setError] = useState5(null);
533
+ const [done, setDone] = useState5(false);
1739
534
  const heading = title ?? l10n.resetPassword.title;
1740
535
  const mismatch = confirm.length > 0 && password !== confirm;
1741
- const signInAction = signInUrl || onSignInClick ? /* @__PURE__ */ jsx5(
536
+ const signInAction = signInUrl || onSignInClick ? /* @__PURE__ */ jsx4(
1742
537
  Button,
1743
538
  {
1744
539
  type: "button",
@@ -1764,31 +559,31 @@ function ResetPasswordForm({
1764
559
  }
1765
560
  };
1766
561
  if (token === null) {
1767
- return /* @__PURE__ */ jsxs5(Shell, { appearance, className, children: [
1768
- /* @__PURE__ */ jsx5(Masthead, { logo, title: heading }),
1769
- /* @__PURE__ */ jsx5(FormSkeleton, {})
562
+ return /* @__PURE__ */ jsxs4(Shell, { appearance, className, children: [
563
+ /* @__PURE__ */ jsx4(Masthead, { logo, title: heading }),
564
+ /* @__PURE__ */ jsx4(FormSkeleton, {})
1770
565
  ] });
1771
566
  }
1772
567
  if (token === "") {
1773
- return /* @__PURE__ */ jsxs5(Shell, { appearance, className, children: [
1774
- logo ? /* @__PURE__ */ jsx5("div", { className: "kerne-masthead", children: logo }) : null,
1775
- /* @__PURE__ */ jsx5(Status, { tone: "danger", title: l10n.resetPassword.incompleteTitle, action: signInAction, children: l10n.resetPassword.incompleteBody }),
1776
- /* @__PURE__ */ jsx5(KerneBranding, { show: config?.show_kerne_branding ?? false })
568
+ return /* @__PURE__ */ jsxs4(Shell, { appearance, className, children: [
569
+ logo ? /* @__PURE__ */ jsx4("div", { className: "kerne-masthead", children: logo }) : null,
570
+ /* @__PURE__ */ jsx4(Status, { tone: "danger", title: l10n.resetPassword.incompleteTitle, action: signInAction, children: l10n.resetPassword.incompleteBody }),
571
+ /* @__PURE__ */ jsx4(KerneBranding, { show: config?.show_kerne_branding ?? false })
1777
572
  ] });
1778
573
  }
1779
574
  if (done) {
1780
- return /* @__PURE__ */ jsxs5(Shell, { appearance, className, children: [
1781
- logo ? /* @__PURE__ */ jsx5("div", { className: "kerne-masthead", children: logo }) : null,
1782
- /* @__PURE__ */ jsx5(Status, { tone: "success", title: l10n.resetPassword.doneTitle, action: signInAction, children: l10n.resetPassword.doneBody }),
1783
- /* @__PURE__ */ jsx5(KerneBranding, { show: config?.show_kerne_branding ?? false })
575
+ return /* @__PURE__ */ jsxs4(Shell, { appearance, className, children: [
576
+ logo ? /* @__PURE__ */ jsx4("div", { className: "kerne-masthead", children: logo }) : null,
577
+ /* @__PURE__ */ jsx4(Status, { tone: "success", title: l10n.resetPassword.doneTitle, action: signInAction, children: l10n.resetPassword.doneBody }),
578
+ /* @__PURE__ */ jsx4(KerneBranding, { show: config?.show_kerne_branding ?? false })
1784
579
  ] });
1785
580
  }
1786
- return /* @__PURE__ */ jsxs5(Shell, { appearance, className, children: [
1787
- /* @__PURE__ */ jsx5(Masthead, { logo, title: heading, subtitle: subtitle ?? l10n.resetPassword.subtitle }),
1788
- /* @__PURE__ */ jsxs5("div", { className: "kerne-stack", children: [
1789
- error ? /* @__PURE__ */ jsx5(Panel, { variant: "danger", children: error }) : null,
1790
- /* @__PURE__ */ jsxs5("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
1791
- /* @__PURE__ */ jsx5(
581
+ return /* @__PURE__ */ jsxs4(Shell, { appearance, className, children: [
582
+ /* @__PURE__ */ jsx4(Masthead, { logo, title: heading, subtitle: subtitle ?? l10n.resetPassword.subtitle }),
583
+ /* @__PURE__ */ jsxs4("div", { className: "kerne-stack", children: [
584
+ error ? /* @__PURE__ */ jsx4(Panel, { variant: "danger", children: error }) : null,
585
+ /* @__PURE__ */ jsxs4("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
586
+ /* @__PURE__ */ jsx4(
1792
587
  Field,
1793
588
  {
1794
589
  label: l10n.resetPassword.newPassword,
@@ -1800,10 +595,10 @@ function ResetPasswordForm({
1800
595
  disabled: submitting,
1801
596
  required: true,
1802
597
  autoFocus: true,
1803
- children: /* @__PURE__ */ jsx5(PasswordStrength, { password, policy })
598
+ children: /* @__PURE__ */ jsx4(PasswordStrength, { password, policy })
1804
599
  }
1805
600
  ),
1806
- /* @__PURE__ */ jsx5(
601
+ /* @__PURE__ */ jsx4(
1807
602
  Field,
1808
603
  {
1809
604
  label: l10n.resetPassword.confirmPassword,
@@ -1817,7 +612,7 @@ function ResetPasswordForm({
1817
612
  error: mismatch ? l10n.resetPassword.mismatch : void 0
1818
613
  }
1819
614
  ),
1820
- /* @__PURE__ */ jsx5(
615
+ /* @__PURE__ */ jsx4(
1821
616
  Button,
1822
617
  {
1823
618
  loading: submitting,
@@ -1825,16 +620,16 @@ function ResetPasswordForm({
1825
620
  children: submitting ? l10n.resetPassword.submitting : l10n.resetPassword.submit
1826
621
  }
1827
622
  ),
1828
- signInUrl || onSignInClick ? /* @__PURE__ */ jsx5("p", { className: "kerne-footer", children: /* @__PURE__ */ jsx5(TextLink, { href: signInUrl, onClick: onSignInClick, children: l10n.common.backToSignIn }) }) : null
623
+ signInUrl || onSignInClick ? /* @__PURE__ */ jsx4("p", { className: "kerne-footer", children: /* @__PURE__ */ jsx4(TextLink, { href: signInUrl, onClick: onSignInClick, children: l10n.common.backToSignIn }) }) : null
1829
624
  ] })
1830
625
  ] }),
1831
- /* @__PURE__ */ jsx5(KerneBranding, { show: config?.show_kerne_branding ?? false })
626
+ /* @__PURE__ */ jsx4(KerneBranding, { show: config?.show_kerne_branding ?? false })
1832
627
  ] });
1833
628
  }
1834
629
 
1835
630
  // src/ui/MagicLinkCallback.tsx
1836
- import { useEffect as useEffect2, useRef, useState as useState7 } from "react";
1837
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
631
+ import { useEffect as useEffect2, useRef, useState as useState6 } from "react";
632
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1838
633
  function MagicLinkCallback({
1839
634
  token: tokenProp,
1840
635
  onSuccess,
@@ -1851,8 +646,8 @@ function MagicLinkCallback({
1851
646
  const token = useUrlToken("token", tokenProp);
1852
647
  const l10n = useLocalization();
1853
648
  const showError = useErrorResolver();
1854
- const [state, setState] = useState7("pending");
1855
- const [message, setMessage] = useState7(null);
649
+ const [state, setState] = useState6("pending");
650
+ const [message, setMessage] = useState6(null);
1856
651
  const consumed = useRef(false);
1857
652
  useEffect2(() => {
1858
653
  if (token === null || consumed.current) return;
@@ -1873,7 +668,7 @@ function MagicLinkCallback({
1873
668
  setMessage(showError(e));
1874
669
  });
1875
670
  }, [token, loginWithMagicLink, redirectUrl, client, onSuccess]);
1876
- const retry = signInUrl || onSignInClick ? /* @__PURE__ */ jsx6(
671
+ const retry = signInUrl || onSignInClick ? /* @__PURE__ */ jsx5(
1877
672
  Button,
1878
673
  {
1879
674
  variant: "outline",
@@ -1884,19 +679,18 @@ function MagicLinkCallback({
1884
679
  children: l10n.common.backToSignIn
1885
680
  }
1886
681
  ) : void 0;
1887
- return /* @__PURE__ */ jsxs6(Shell, { appearance, className, children: [
1888
- logo ? /* @__PURE__ */ jsx6("div", { className: "kerne-masthead", children: logo }) : null,
1889
- state === "pending" ? /* @__PURE__ */ jsx6(Status, { tone: "loading", title: l10n.magicLink.pendingTitle, children: l10n.magicLink.pendingBody }) : state === "done" ? /* @__PURE__ */ jsx6(Status, { tone: "success", title: l10n.magicLink.doneTitle, children: l10n.magicLink.doneBody }) : /* @__PURE__ */ jsx6(Status, { tone: "danger", title: l10n.magicLink.failedTitle, action: retry, children: message ?? l10n.magicLink.missingToken }),
1890
- /* @__PURE__ */ jsx6(KerneBranding, { show: config?.show_kerne_branding ?? false })
682
+ return /* @__PURE__ */ jsxs5(Shell, { appearance, className, children: [
683
+ logo ? /* @__PURE__ */ jsx5("div", { className: "kerne-masthead", children: logo }) : null,
684
+ state === "pending" ? /* @__PURE__ */ jsx5(Status, { tone: "loading", title: l10n.magicLink.pendingTitle, children: l10n.magicLink.pendingBody }) : state === "done" ? /* @__PURE__ */ jsx5(Status, { tone: "success", title: l10n.magicLink.doneTitle, children: l10n.magicLink.doneBody }) : /* @__PURE__ */ jsx5(Status, { tone: "danger", title: l10n.magicLink.failedTitle, action: retry, children: message ?? l10n.magicLink.missingToken }),
685
+ /* @__PURE__ */ jsx5(KerneBranding, { show: config?.show_kerne_branding ?? false })
1891
686
  ] });
1892
687
  }
1893
688
 
1894
689
  // src/ui/VerifyEmailForm.tsx
1895
- import { useEffect as useEffect3, useRef as useRef2, useState as useState8 } from "react";
1896
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
690
+ import { useEffect as useEffect3, useRef as useRef2, useState as useState7 } from "react";
691
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1897
692
  function VerifyEmailForm({
1898
693
  token: tokenProp,
1899
- callbackUrl,
1900
694
  onVerified,
1901
695
  title,
1902
696
  subtitle,
@@ -1909,12 +703,12 @@ function VerifyEmailForm({
1909
703
  const token = useUrlToken("token", tokenProp);
1910
704
  const l10n = useLocalization();
1911
705
  const showError = useErrorResolver();
1912
- const [code, setCode] = useState8("");
1913
- const [submitting, setSubmitting] = useState8(false);
1914
- const [error, setError] = useState8(null);
1915
- const [resent, setResent] = useState8(false);
1916
- const [linkState, setLinkState] = useState8("idle");
1917
- const [verified, setVerified] = useState8(false);
706
+ const [code, setCode] = useState7("");
707
+ const [submitting, setSubmitting] = useState7(false);
708
+ const [error, setError] = useState7(null);
709
+ const [resent, setResent] = useState7(false);
710
+ const [linkState, setLinkState] = useState7("idle");
711
+ const [verified, setVerified] = useState7(false);
1918
712
  const consumed = useRef2(false);
1919
713
  const heading = title ?? l10n.verifyEmail.title;
1920
714
  const settle = async (needsTokenRefresh) => {
@@ -1952,48 +746,48 @@ function VerifyEmailForm({
1952
746
  const handleResend = async () => {
1953
747
  setError(null);
1954
748
  try {
1955
- await sendVerificationEmail("code", callbackUrl);
749
+ await sendVerificationEmail("code");
1956
750
  setResent(true);
1957
751
  } catch (err) {
1958
752
  setError(showError(err));
1959
753
  }
1960
754
  };
1961
755
  if (verified) {
1962
- return /* @__PURE__ */ jsxs7(Shell, { appearance, className, children: [
1963
- logo ? /* @__PURE__ */ jsx7("div", { className: "kerne-masthead", children: logo }) : null,
1964
- /* @__PURE__ */ jsx7(Status, { tone: "success", title: l10n.verifyEmail.doneTitle, children: l10n.verifyEmail.doneBody }),
1965
- /* @__PURE__ */ jsx7(KerneBranding, { show: config?.show_kerne_branding ?? false })
756
+ return /* @__PURE__ */ jsxs6(Shell, { appearance, className, children: [
757
+ logo ? /* @__PURE__ */ jsx6("div", { className: "kerne-masthead", children: logo }) : null,
758
+ /* @__PURE__ */ jsx6(Status, { tone: "success", title: l10n.verifyEmail.doneTitle, children: l10n.verifyEmail.doneBody }),
759
+ /* @__PURE__ */ jsx6(KerneBranding, { show: config?.show_kerne_branding ?? false })
1966
760
  ] });
1967
761
  }
1968
762
  if (linkState === "pending") {
1969
- return /* @__PURE__ */ jsxs7(Shell, { appearance, className, children: [
1970
- logo ? /* @__PURE__ */ jsx7("div", { className: "kerne-masthead", children: logo }) : null,
1971
- /* @__PURE__ */ jsx7(Status, { tone: "loading", title: l10n.verifyEmail.verifyingTitle, children: l10n.verifyEmail.verifyingBody })
763
+ return /* @__PURE__ */ jsxs6(Shell, { appearance, className, children: [
764
+ logo ? /* @__PURE__ */ jsx6("div", { className: "kerne-masthead", children: logo }) : null,
765
+ /* @__PURE__ */ jsx6(Status, { tone: "loading", title: l10n.verifyEmail.verifyingTitle, children: l10n.verifyEmail.verifyingBody })
1972
766
  ] });
1973
767
  }
1974
768
  if (linkState === "failed") {
1975
- return /* @__PURE__ */ jsxs7(Shell, { appearance, className, children: [
1976
- logo ? /* @__PURE__ */ jsx7("div", { className: "kerne-masthead", children: logo }) : null,
1977
- /* @__PURE__ */ jsx7(
769
+ return /* @__PURE__ */ jsxs6(Shell, { appearance, className, children: [
770
+ logo ? /* @__PURE__ */ jsx6("div", { className: "kerne-masthead", children: logo }) : null,
771
+ /* @__PURE__ */ jsx6(
1978
772
  Status,
1979
773
  {
1980
774
  tone: "danger",
1981
775
  title: l10n.verifyEmail.failedTitle,
1982
- action: /* @__PURE__ */ jsx7(Button, { variant: "outline", type: "button", onClick: handleResend, children: l10n.verifyEmail.newCode }),
776
+ action: /* @__PURE__ */ jsx6(Button, { variant: "outline", type: "button", onClick: handleResend, children: l10n.verifyEmail.newCode }),
1983
777
  children: error
1984
778
  }
1985
779
  ),
1986
- resent ? /* @__PURE__ */ jsx7(Panel, { variant: "success", children: l10n.verifyEmail.resent }) : null,
1987
- /* @__PURE__ */ jsx7(KerneBranding, { show: config?.show_kerne_branding ?? false })
780
+ resent ? /* @__PURE__ */ jsx6(Panel, { variant: "success", children: l10n.verifyEmail.resent }) : null,
781
+ /* @__PURE__ */ jsx6(KerneBranding, { show: config?.show_kerne_branding ?? false })
1988
782
  ] });
1989
783
  }
1990
- return /* @__PURE__ */ jsxs7(Shell, { appearance, className, children: [
1991
- /* @__PURE__ */ jsx7(Masthead, { logo, title: heading, subtitle: subtitle ?? l10n.verifyEmail.subtitle }),
1992
- /* @__PURE__ */ jsxs7("div", { className: "kerne-stack", children: [
1993
- error ? /* @__PURE__ */ jsx7(Panel, { variant: "danger", children: error }) : null,
1994
- resent ? /* @__PURE__ */ jsx7(Panel, { variant: "success", children: l10n.verifyEmail.resent }) : null,
1995
- /* @__PURE__ */ jsxs7("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
1996
- /* @__PURE__ */ jsx7(
784
+ return /* @__PURE__ */ jsxs6(Shell, { appearance, className, children: [
785
+ /* @__PURE__ */ jsx6(Masthead, { logo, title: heading, subtitle: subtitle ?? l10n.verifyEmail.subtitle }),
786
+ /* @__PURE__ */ jsxs6("div", { className: "kerne-stack", children: [
787
+ error ? /* @__PURE__ */ jsx6(Panel, { variant: "danger", children: error }) : null,
788
+ resent ? /* @__PURE__ */ jsx6(Panel, { variant: "success", children: l10n.verifyEmail.resent }) : null,
789
+ /* @__PURE__ */ jsxs6("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
790
+ /* @__PURE__ */ jsx6(
1997
791
  Field,
1998
792
  {
1999
793
  label: l10n.verifyEmail.code,
@@ -2008,17 +802,17 @@ function VerifyEmailForm({
2008
802
  autoFocus: true
2009
803
  }
2010
804
  ),
2011
- /* @__PURE__ */ jsx7(Button, { loading: submitting, disabled: !code, children: submitting ? l10n.verifyEmail.submitting : l10n.verifyEmail.submit }),
2012
- /* @__PURE__ */ jsx7(MethodSwitch, { onClick: handleResend, children: l10n.verifyEmail.resend })
805
+ /* @__PURE__ */ jsx6(Button, { loading: submitting, disabled: !code, children: submitting ? l10n.verifyEmail.submitting : l10n.verifyEmail.submit }),
806
+ /* @__PURE__ */ jsx6(MethodSwitch, { onClick: handleResend, children: l10n.verifyEmail.resend })
2013
807
  ] })
2014
808
  ] }),
2015
- /* @__PURE__ */ jsx7(KerneBranding, { show: config?.show_kerne_branding ?? false })
809
+ /* @__PURE__ */ jsx6(KerneBranding, { show: config?.show_kerne_branding ?? false })
2016
810
  ] });
2017
811
  }
2018
812
 
2019
813
  // src/ui/ActivateAccountForm.tsx
2020
- import { useEffect as useEffect4, useState as useState9 } from "react";
2021
- import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
814
+ import { useEffect as useEffect4, useState as useState8 } from "react";
815
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2022
816
  function ActivateAccountForm({
2023
817
  token: tokenProp,
2024
818
  onSuccess,
@@ -2037,12 +831,12 @@ function ActivateAccountForm({
2037
831
  const token = useUrlToken("token", tokenProp);
2038
832
  const l10n = useLocalization();
2039
833
  const showError = useErrorResolver();
2040
- const [context, setContext] = useState9(null);
2041
- const [loadError, setLoadError] = useState9(null);
2042
- const [password, setPassword] = useState9("");
2043
- const [confirm, setConfirm] = useState9("");
2044
- const [submitting, setSubmitting] = useState9(false);
2045
- const [error, setError] = useState9(null);
834
+ const [context, setContext] = useState8(null);
835
+ const [loadError, setLoadError] = useState8(null);
836
+ const [password, setPassword] = useState8("");
837
+ const [confirm, setConfirm] = useState8("");
838
+ const [submitting, setSubmitting] = useState8(false);
839
+ const [error, setError] = useState8(null);
2046
840
  const heading = title ?? l10n.activate.title;
2047
841
  const mismatch = confirm.length > 0 && password !== confirm;
2048
842
  useEffect4(() => {
@@ -2053,7 +847,7 @@ function ActivateAccountForm({
2053
847
  mounted = false;
2054
848
  };
2055
849
  }, [token, getActivationContext]);
2056
- const signInAction = signInUrl || onSignInClick ? /* @__PURE__ */ jsx8(
850
+ const signInAction = signInUrl || onSignInClick ? /* @__PURE__ */ jsx7(
2057
851
  Button,
2058
852
  {
2059
853
  type: "button",
@@ -2081,42 +875,42 @@ function ActivateAccountForm({
2081
875
  }
2082
876
  };
2083
877
  if (token === null) {
2084
- return /* @__PURE__ */ jsxs8(Shell, { appearance, className, children: [
2085
- /* @__PURE__ */ jsx8(Masthead, { logo, title: heading }),
2086
- /* @__PURE__ */ jsx8(FormSkeleton, {})
878
+ return /* @__PURE__ */ jsxs7(Shell, { appearance, className, children: [
879
+ /* @__PURE__ */ jsx7(Masthead, { logo, title: heading }),
880
+ /* @__PURE__ */ jsx7(FormSkeleton, {})
2087
881
  ] });
2088
882
  }
2089
883
  if (token === "" || loadError) {
2090
- return /* @__PURE__ */ jsxs8(Shell, { appearance, className, children: [
2091
- logo ? /* @__PURE__ */ jsx8("div", { className: "kerne-masthead", children: logo }) : null,
2092
- /* @__PURE__ */ jsx8(Status, { tone: "danger", title: l10n.activate.failedTitle, action: signInAction, children: loadError ?? l10n.activate.missingToken }),
2093
- /* @__PURE__ */ jsx8(KerneBranding, { show: config?.show_kerne_branding ?? false })
884
+ return /* @__PURE__ */ jsxs7(Shell, { appearance, className, children: [
885
+ logo ? /* @__PURE__ */ jsx7("div", { className: "kerne-masthead", children: logo }) : null,
886
+ /* @__PURE__ */ jsx7(Status, { tone: "danger", title: l10n.activate.failedTitle, action: signInAction, children: loadError ?? l10n.activate.missingToken }),
887
+ /* @__PURE__ */ jsx7(KerneBranding, { show: config?.show_kerne_branding ?? false })
2094
888
  ] });
2095
889
  }
2096
890
  if (!context) {
2097
- return /* @__PURE__ */ jsxs8(Shell, { appearance, className, children: [
2098
- /* @__PURE__ */ jsx8(Masthead, { logo, title: heading }),
2099
- /* @__PURE__ */ jsx8(FormSkeleton, {})
891
+ return /* @__PURE__ */ jsxs7(Shell, { appearance, className, children: [
892
+ /* @__PURE__ */ jsx7(Masthead, { logo, title: heading }),
893
+ /* @__PURE__ */ jsx7(FormSkeleton, {})
2100
894
  ] });
2101
895
  }
2102
896
  const credentialsMode = context.mode === "credentials";
2103
- return /* @__PURE__ */ jsxs8(Shell, { appearance, className, children: [
2104
- /* @__PURE__ */ jsx8(
897
+ return /* @__PURE__ */ jsxs7(Shell, { appearance, className, children: [
898
+ /* @__PURE__ */ jsx7(
2105
899
  Masthead,
2106
900
  {
2107
901
  logo,
2108
902
  title: heading,
2109
903
  subtitle: fillNode(
2110
904
  credentialsMode ? l10n.activate.credentialsSubtitle : l10n.activate.magicSubtitle,
2111
- { email: /* @__PURE__ */ jsx8("strong", { children: context.email }) }
905
+ { email: /* @__PURE__ */ jsx7("strong", { children: context.email }) }
2112
906
  )
2113
907
  }
2114
908
  ),
2115
- /* @__PURE__ */ jsxs8("div", { className: "kerne-stack", children: [
2116
- error ? /* @__PURE__ */ jsx8(Panel, { variant: "danger", children: error }) : null,
2117
- /* @__PURE__ */ jsxs8("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
2118
- credentialsMode ? /* @__PURE__ */ jsxs8(Fragment4, { children: [
2119
- /* @__PURE__ */ jsx8(
909
+ /* @__PURE__ */ jsxs7("div", { className: "kerne-stack", children: [
910
+ error ? /* @__PURE__ */ jsx7(Panel, { variant: "danger", children: error }) : null,
911
+ /* @__PURE__ */ jsxs7("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
912
+ credentialsMode ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
913
+ /* @__PURE__ */ jsx7(
2120
914
  Field,
2121
915
  {
2122
916
  label: l10n.common.password,
@@ -2128,10 +922,10 @@ function ActivateAccountForm({
2128
922
  disabled: submitting,
2129
923
  required: true,
2130
924
  autoFocus: true,
2131
- children: /* @__PURE__ */ jsx8(PasswordStrength, { password, policy })
925
+ children: /* @__PURE__ */ jsx7(PasswordStrength, { password, policy })
2132
926
  }
2133
927
  ),
2134
- /* @__PURE__ */ jsx8(
928
+ /* @__PURE__ */ jsx7(
2135
929
  Field,
2136
930
  {
2137
931
  label: l10n.activate.confirmPassword,
@@ -2146,7 +940,7 @@ function ActivateAccountForm({
2146
940
  }
2147
941
  )
2148
942
  ] }) : null,
2149
- /* @__PURE__ */ jsx8(
943
+ /* @__PURE__ */ jsx7(
2150
944
  Button,
2151
945
  {
2152
946
  loading: submitting,
@@ -2156,12 +950,12 @@ function ActivateAccountForm({
2156
950
  )
2157
951
  ] })
2158
952
  ] }),
2159
- /* @__PURE__ */ jsx8(KerneBranding, { show: config?.show_kerne_branding ?? false })
953
+ /* @__PURE__ */ jsx7(KerneBranding, { show: config?.show_kerne_branding ?? false })
2160
954
  ] });
2161
955
  }
2162
956
 
2163
957
  // src/ui/AuthFlow.tsx
2164
- import { jsx as jsx9 } from "react/jsx-runtime";
958
+ import { jsx as jsx8 } from "react/jsx-runtime";
2165
959
  var TOKEN_STEPS = ["reset-password", "magic-link", "activate", "verify-email"];
2166
960
  function AuthFlow({
2167
961
  initialStep,
@@ -2169,9 +963,6 @@ function AuthFlow({
2169
963
  onStepChange,
2170
964
  onSuccess,
2171
965
  redirectUrl,
2172
- magicLinkCallbackUrl,
2173
- resetPasswordCallbackUrl,
2174
- verificationCallbackUrl,
2175
966
  allowSignUp = true,
2176
967
  legal,
2177
968
  appearance,
@@ -2180,7 +971,7 @@ function AuthFlow({
2180
971
  }) {
2181
972
  const { config } = useAuthConfig();
2182
973
  const token = useUrlToken("token");
2183
- const [internalStep, setInternalStep] = useState10(null);
974
+ const [internalStep, setInternalStep] = useState9(null);
2184
975
  const go = (next) => {
2185
976
  if (controlledStep === void 0) setInternalStep(next);
2186
977
  onStepChange?.(next);
@@ -2195,30 +986,22 @@ function AuthFlow({
2195
986
  };
2196
987
  switch (step) {
2197
988
  case "sign-up":
2198
- return /* @__PURE__ */ jsx9(
989
+ return /* @__PURE__ */ jsx8(
2199
990
  RegisterForm,
2200
991
  {
2201
992
  ...shared,
2202
993
  onSuccess,
2203
994
  redirectUrl,
2204
- magicLinkCallbackUrl,
2205
995
  onSignInClick: () => go("sign-in"),
2206
996
  legal
2207
997
  }
2208
998
  );
2209
999
  case "forgot-password":
2210
- return /* @__PURE__ */ jsx9(
2211
- ForgotPasswordForm,
2212
- {
2213
- ...shared,
2214
- callbackUrl: resetPasswordCallbackUrl,
2215
- onSignInClick: () => go("sign-in")
2216
- }
2217
- );
1000
+ return /* @__PURE__ */ jsx8(ForgotPasswordForm, { ...shared, onSignInClick: () => go("sign-in") });
2218
1001
  case "reset-password":
2219
- return /* @__PURE__ */ jsx9(ResetPasswordForm, { ...shared, onSignInClick: () => go("sign-in"), onSuccess });
1002
+ return /* @__PURE__ */ jsx8(ResetPasswordForm, { ...shared, onSignInClick: () => go("sign-in"), onSuccess });
2220
1003
  case "magic-link":
2221
- return /* @__PURE__ */ jsx9(
1004
+ return /* @__PURE__ */ jsx8(
2222
1005
  MagicLinkCallback,
2223
1006
  {
2224
1007
  ...shared,
@@ -2228,9 +1011,9 @@ function AuthFlow({
2228
1011
  }
2229
1012
  );
2230
1013
  case "verify-email":
2231
- return /* @__PURE__ */ jsx9(VerifyEmailForm, { ...shared, callbackUrl: verificationCallbackUrl, onVerified: onSuccess });
1014
+ return /* @__PURE__ */ jsx8(VerifyEmailForm, { ...shared, onVerified: onSuccess });
2232
1015
  case "activate":
2233
- return /* @__PURE__ */ jsx9(
1016
+ return /* @__PURE__ */ jsx8(
2234
1017
  ActivateAccountForm,
2235
1018
  {
2236
1019
  ...shared,
@@ -2241,13 +1024,12 @@ function AuthFlow({
2241
1024
  );
2242
1025
  case "sign-in":
2243
1026
  default:
2244
- return /* @__PURE__ */ jsx9(
1027
+ return /* @__PURE__ */ jsx8(
2245
1028
  LoginForm,
2246
1029
  {
2247
1030
  ...shared,
2248
1031
  onSuccess,
2249
1032
  redirectUrl,
2250
- magicLinkCallbackUrl,
2251
1033
  onSignUpClick: canSignUp ? () => go("sign-up") : void 0,
2252
1034
  onForgotPasswordClick: () => go("forgot-password"),
2253
1035
  legal
@@ -2257,8 +1039,8 @@ function AuthFlow({
2257
1039
  }
2258
1040
 
2259
1041
  // src/ui/ChangePasswordForm.tsx
2260
- import { useState as useState11 } from "react";
2261
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1042
+ import { useState as useState10 } from "react";
1043
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2262
1044
  function ChangePasswordForm({
2263
1045
  onSuccess,
2264
1046
  title,
@@ -2271,12 +1053,12 @@ function ChangePasswordForm({
2271
1053
  const l10n = useLocalization();
2272
1054
  const showError = useErrorResolver();
2273
1055
  const heading = title === null ? null : title ?? l10n.security.submit;
2274
- const [currentPassword, setCurrentPassword] = useState11("");
2275
- const [newPassword, setNewPassword] = useState11("");
2276
- const [confirm, setConfirm] = useState11("");
2277
- const [submitting, setSubmitting] = useState11(false);
2278
- const [error, setError] = useState11(null);
2279
- const [done, setDone] = useState11(false);
1056
+ const [currentPassword, setCurrentPassword] = useState10("");
1057
+ const [newPassword, setNewPassword] = useState10("");
1058
+ const [confirm, setConfirm] = useState10("");
1059
+ const [submitting, setSubmitting] = useState10(false);
1060
+ const [error, setError] = useState10(null);
1061
+ const [done, setDone] = useState10(false);
2280
1062
  const mismatch = confirm.length > 0 && newPassword !== confirm;
2281
1063
  const unchanged = newPassword.length > 0 && newPassword === currentPassword;
2282
1064
  const handleSubmit = async (e) => {
@@ -2298,13 +1080,13 @@ function ChangePasswordForm({
2298
1080
  setSubmitting(false);
2299
1081
  }
2300
1082
  };
2301
- return /* @__PURE__ */ jsxs9(Shell, { appearance, className, children: [
2302
- heading ? /* @__PURE__ */ jsx10("div", { className: "kerne-heading", style: { marginBottom: 20 }, children: /* @__PURE__ */ jsx10("h1", { className: "kerne-title", children: heading }) }) : null,
2303
- /* @__PURE__ */ jsxs9("div", { className: "kerne-stack", children: [
2304
- error ? /* @__PURE__ */ jsx10(Panel, { variant: "danger", children: error }) : null,
2305
- done ? /* @__PURE__ */ jsx10(Panel, { variant: "success", children: l10n.security.changed }) : null,
2306
- /* @__PURE__ */ jsxs9("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
2307
- /* @__PURE__ */ jsx10(
1083
+ return /* @__PURE__ */ jsxs8(Shell, { appearance, className, children: [
1084
+ heading ? /* @__PURE__ */ jsx9("div", { className: "kerne-heading", style: { marginBottom: 20 }, children: /* @__PURE__ */ jsx9("h1", { className: "kerne-title", children: heading }) }) : null,
1085
+ /* @__PURE__ */ jsxs8("div", { className: "kerne-stack", children: [
1086
+ error ? /* @__PURE__ */ jsx9(Panel, { variant: "danger", children: error }) : null,
1087
+ done ? /* @__PURE__ */ jsx9(Panel, { variant: "success", children: l10n.security.changed }) : null,
1088
+ /* @__PURE__ */ jsxs8("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
1089
+ /* @__PURE__ */ jsx9(
2308
1090
  Field,
2309
1091
  {
2310
1092
  label: l10n.security.currentPassword,
@@ -2317,7 +1099,7 @@ function ChangePasswordForm({
2317
1099
  required: true
2318
1100
  }
2319
1101
  ),
2320
- /* @__PURE__ */ jsx10(
1102
+ /* @__PURE__ */ jsx9(
2321
1103
  Field,
2322
1104
  {
2323
1105
  label: l10n.security.newPassword,
@@ -2329,10 +1111,10 @@ function ChangePasswordForm({
2329
1111
  disabled: submitting,
2330
1112
  required: true,
2331
1113
  error: unchanged ? l10n.security.sameAsCurrent : void 0,
2332
- children: /* @__PURE__ */ jsx10(PasswordStrength, { password: newPassword, policy })
1114
+ children: /* @__PURE__ */ jsx9(PasswordStrength, { password: newPassword, policy })
2333
1115
  }
2334
1116
  ),
2335
- /* @__PURE__ */ jsx10(
1117
+ /* @__PURE__ */ jsx9(
2336
1118
  Field,
2337
1119
  {
2338
1120
  label: l10n.security.confirmPassword,
@@ -2346,7 +1128,7 @@ function ChangePasswordForm({
2346
1128
  error: mismatch ? l10n.security.mismatch : void 0
2347
1129
  }
2348
1130
  ),
2349
- /* @__PURE__ */ jsx10(
1131
+ /* @__PURE__ */ jsx9(
2350
1132
  Button,
2351
1133
  {
2352
1134
  loading: submitting,
@@ -2360,8 +1142,8 @@ function ChangePasswordForm({
2360
1142
  }
2361
1143
 
2362
1144
  // src/ui/WaitlistForm.tsx
2363
- import { useState as useState12 } from "react";
2364
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1145
+ import { useState as useState11 } from "react";
1146
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2365
1147
  function WaitlistForm({
2366
1148
  title,
2367
1149
  subtitle,
@@ -2379,11 +1161,11 @@ function WaitlistForm({
2379
1161
  const { config } = useAuthConfig();
2380
1162
  const l10n = useLocalization();
2381
1163
  const showError = useErrorResolver();
2382
- const [email, setEmail] = useState12("");
2383
- const [name, setName] = useState12("");
2384
- const [submitting, setSubmitting] = useState12(false);
2385
- const [error, setError] = useState12(null);
2386
- const [joined, setJoined] = useState12(false);
1164
+ const [email, setEmail] = useState11("");
1165
+ const [name, setName] = useState11("");
1166
+ const [submitting, setSubmitting] = useState11(false);
1167
+ const [error, setError] = useState11(null);
1168
+ const [joined, setJoined] = useState11(false);
2387
1169
  const handleSubmit = async (e) => {
2388
1170
  e.preventDefault();
2389
1171
  setError(null);
@@ -2402,8 +1184,8 @@ function WaitlistForm({
2402
1184
  setSubmitting(false);
2403
1185
  }
2404
1186
  };
2405
- return /* @__PURE__ */ jsxs10(Shell, { appearance, className, children: [
2406
- /* @__PURE__ */ jsx11(
1187
+ return /* @__PURE__ */ jsxs9(Shell, { appearance, className, children: [
1188
+ /* @__PURE__ */ jsx10(
2407
1189
  Masthead,
2408
1190
  {
2409
1191
  logo,
@@ -2411,10 +1193,10 @@ function WaitlistForm({
2411
1193
  subtitle: joined ? void 0 : subtitle ?? l10n.waitlist.subtitle
2412
1194
  }
2413
1195
  ),
2414
- /* @__PURE__ */ jsxs10("div", { className: "kerne-stack", children: [
2415
- error ? /* @__PURE__ */ jsx11(Panel, { variant: "danger", children: error }) : null,
2416
- joined ? /* @__PURE__ */ jsx11(Panel, { variant: "success", children: successMessage ?? l10n.waitlist.joined }) : /* @__PURE__ */ jsxs10("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
2417
- collectName ? /* @__PURE__ */ jsx11(
1196
+ /* @__PURE__ */ jsxs9("div", { className: "kerne-stack", children: [
1197
+ error ? /* @__PURE__ */ jsx10(Panel, { variant: "danger", children: error }) : null,
1198
+ joined ? /* @__PURE__ */ jsx10(Panel, { variant: "success", children: successMessage ?? l10n.waitlist.joined }) : /* @__PURE__ */ jsxs9("form", { className: "kerne-form", onSubmit: handleSubmit, children: [
1199
+ collectName ? /* @__PURE__ */ jsx10(
2418
1200
  Field,
2419
1201
  {
2420
1202
  label: l10n.common.name,
@@ -2426,7 +1208,7 @@ function WaitlistForm({
2426
1208
  disabled: submitting
2427
1209
  }
2428
1210
  ) : null,
2429
- /* @__PURE__ */ jsx11(
1211
+ /* @__PURE__ */ jsx10(
2430
1212
  Field,
2431
1213
  {
2432
1214
  label: l10n.common.email,
@@ -2440,17 +1222,17 @@ function WaitlistForm({
2440
1222
  autoFocus: true
2441
1223
  }
2442
1224
  ),
2443
- /* @__PURE__ */ jsx11(Button, { loading: submitting, disabled: !email, children: submitting ? l10n.waitlist.submitting : submitLabel ?? l10n.waitlist.submit })
1225
+ /* @__PURE__ */ jsx10(Button, { loading: submitting, disabled: !email, children: submitting ? l10n.waitlist.submitting : submitLabel ?? l10n.waitlist.submit })
2444
1226
  ] })
2445
1227
  ] }),
2446
- legal ? /* @__PURE__ */ jsx11("p", { className: "kerne-legal", children: legal }) : null,
2447
- /* @__PURE__ */ jsx11(KerneBranding, { show: config?.show_kerne_branding ?? false })
1228
+ legal ? /* @__PURE__ */ jsx10("p", { className: "kerne-legal", children: legal }) : null,
1229
+ /* @__PURE__ */ jsx10(KerneBranding, { show: config?.show_kerne_branding ?? false })
2448
1230
  ] });
2449
1231
  }
2450
1232
 
2451
1233
  // src/ui/UserButton.tsx
2452
- import { useEffect as useEffect5, useRef as useRef3, useState as useState13 } from "react";
2453
- import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1234
+ import { useEffect as useEffect5, useRef as useRef3, useState as useState12 } from "react";
1235
+ import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2454
1236
  function SignOutButton({
2455
1237
  children,
2456
1238
  onSignedOut,
@@ -2466,13 +1248,13 @@ function SignOutButton({
2466
1248
  onSignedOut?.();
2467
1249
  if (redirectUrl && typeof window !== "undefined") window.location.href = redirectUrl;
2468
1250
  };
2469
- return /* @__PURE__ */ jsx12(
1251
+ return /* @__PURE__ */ jsx11(
2470
1252
  "div",
2471
1253
  {
2472
1254
  className: "kerne-root",
2473
1255
  style: { ...appearanceToStyle(appearance), display: "inline-flex", width: "auto" },
2474
1256
  "data-kerne-theme": resolveTheme(appearance),
2475
- children: /* @__PURE__ */ jsx12(
1257
+ children: /* @__PURE__ */ jsx11(
2476
1258
  "button",
2477
1259
  {
2478
1260
  type: "button",
@@ -2497,24 +1279,48 @@ function UserButton({
2497
1279
  const { logout, isAuthenticated } = useAuth();
2498
1280
  const { user, fullName, initials, email } = useUser();
2499
1281
  const l10n = useLocalization();
2500
- const [open, setOpen] = useState13(false);
1282
+ const [open, setOpen] = useState12(false);
2501
1283
  const wrapRef = useRef3(null);
1284
+ const triggerRef = useRef3(null);
1285
+ const itemRefs = useRef3([]);
2502
1286
  useEffect5(() => {
2503
1287
  if (!open) return;
2504
1288
  const onPointerDown = (e) => {
2505
1289
  if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
2506
1290
  };
2507
1291
  const onKeyDown = (e) => {
2508
- if (e.key === "Escape") setOpen(false);
1292
+ if (e.key === "Escape") {
1293
+ setOpen(false);
1294
+ triggerRef.current?.focus();
1295
+ }
1296
+ };
1297
+ const onFocusOut = (e) => {
1298
+ if (wrapRef.current && !wrapRef.current.contains(e.relatedTarget)) setOpen(false);
2509
1299
  };
2510
1300
  document.addEventListener("mousedown", onPointerDown);
2511
1301
  document.addEventListener("keydown", onKeyDown);
1302
+ wrapRef.current?.addEventListener("focusout", onFocusOut);
1303
+ itemRefs.current[0]?.focus();
2512
1304
  return () => {
2513
1305
  document.removeEventListener("mousedown", onPointerDown);
2514
1306
  document.removeEventListener("keydown", onKeyDown);
1307
+ wrapRef.current?.removeEventListener("focusout", onFocusOut);
2515
1308
  };
2516
1309
  }, [open]);
2517
- if (!isAuthenticated) return /* @__PURE__ */ jsx12(Fragment5, { children: signedOutFallback });
1310
+ const onMenuKeyDown = (e) => {
1311
+ const entries = itemRefs.current.filter((el) => el !== null);
1312
+ if (entries.length === 0) return;
1313
+ const currentIndex = entries.indexOf(document.activeElement);
1314
+ let nextIndex = null;
1315
+ if (e.key === "ArrowDown") nextIndex = (currentIndex + 1) % entries.length;
1316
+ else if (e.key === "ArrowUp") nextIndex = (currentIndex - 1 + entries.length) % entries.length;
1317
+ else if (e.key === "Home") nextIndex = 0;
1318
+ else if (e.key === "End") nextIndex = entries.length - 1;
1319
+ else return;
1320
+ e.preventDefault();
1321
+ entries[nextIndex]?.focus();
1322
+ };
1323
+ if (!isAuthenticated) return /* @__PURE__ */ jsx11(Fragment5, { children: signedOutFallback });
2518
1324
  const handleSignOut = () => {
2519
1325
  setOpen(false);
2520
1326
  logout();
@@ -2523,82 +1329,102 @@ function UserButton({
2523
1329
  window.location.href = signOutRedirectUrl;
2524
1330
  }
2525
1331
  };
2526
- return /* @__PURE__ */ jsx12(
1332
+ return /* @__PURE__ */ jsx11(
2527
1333
  "div",
2528
1334
  {
2529
1335
  className: `kerne-root${className ? ` ${className}` : ""}`,
2530
1336
  style: { ...appearanceToStyle(appearance), display: "inline-flex", width: "auto" },
2531
1337
  "data-kerne-theme": resolveTheme(appearance),
2532
- children: /* @__PURE__ */ jsxs11("div", { className: "kerne-menu-wrap", ref: wrapRef, children: [
2533
- /* @__PURE__ */ jsx12(
1338
+ children: /* @__PURE__ */ jsxs10("div", { className: "kerne-menu-wrap", ref: wrapRef, children: [
1339
+ /* @__PURE__ */ jsx11(
2534
1340
  "button",
2535
1341
  {
2536
1342
  type: "button",
1343
+ ref: triggerRef,
2537
1344
  className: "kerne-menu-trigger",
2538
1345
  "aria-haspopup": "menu",
2539
1346
  "aria-expanded": open,
2540
1347
  "aria-label": l10n.account.menuLabel,
2541
1348
  onClick: () => setOpen((v) => !v),
2542
- children: user?.avatar ? /* @__PURE__ */ jsx12("img", { className: "kerne-avatar", src: user.avatar, alt: "" }) : /* @__PURE__ */ jsx12("span", { className: "kerne-avatar", children: initials || email?.[0]?.toUpperCase() || "?" })
1349
+ children: user?.avatar ? /* @__PURE__ */ jsx11("img", { className: "kerne-avatar", src: user.avatar, alt: "" }) : /* @__PURE__ */ jsx11("span", { className: "kerne-avatar", children: initials || email?.[0]?.toUpperCase() || "?" })
2543
1350
  }
2544
1351
  ),
2545
- open ? /* @__PURE__ */ jsxs11("div", { className: "kerne-menu", role: "menu", children: [
2546
- /* @__PURE__ */ jsxs11("div", { className: "kerne-menu-header", children: [
2547
- fullName ? /* @__PURE__ */ jsx12("div", { className: "kerne-identity-name", children: fullName }) : null,
2548
- /* @__PURE__ */ jsx12("div", { className: "kerne-identity-mail", children: email })
2549
- ] }),
2550
- items.map(
2551
- (item) => item.href ? /* @__PURE__ */ jsx12("a", { role: "menuitem", className: "kerne-menu-item", href: item.href, children: item.label }, item.label) : /* @__PURE__ */ jsx12(
1352
+ open ? (() => {
1353
+ itemRefs.current = [];
1354
+ let itemIndex = 0;
1355
+ const registerItem = (el) => {
1356
+ itemRefs.current[itemIndex++] = el;
1357
+ };
1358
+ return /* @__PURE__ */ jsxs10("div", { className: "kerne-menu", role: "menu", onKeyDown: onMenuKeyDown, children: [
1359
+ /* @__PURE__ */ jsxs10("div", { className: "kerne-menu-header", children: [
1360
+ fullName ? /* @__PURE__ */ jsx11("div", { className: "kerne-identity-name", children: fullName }) : null,
1361
+ /* @__PURE__ */ jsx11("div", { className: "kerne-identity-mail", children: email })
1362
+ ] }),
1363
+ items.map(
1364
+ (item) => item.href ? /* @__PURE__ */ jsx11(
1365
+ "a",
1366
+ {
1367
+ ref: registerItem,
1368
+ role: "menuitem",
1369
+ className: "kerne-menu-item",
1370
+ href: item.href,
1371
+ children: item.label
1372
+ },
1373
+ item.label
1374
+ ) : /* @__PURE__ */ jsx11(
1375
+ "button",
1376
+ {
1377
+ ref: registerItem,
1378
+ type: "button",
1379
+ role: "menuitem",
1380
+ className: "kerne-menu-item",
1381
+ onClick: () => {
1382
+ setOpen(false);
1383
+ item.onClick?.();
1384
+ },
1385
+ children: item.label
1386
+ },
1387
+ item.label
1388
+ )
1389
+ ),
1390
+ /* @__PURE__ */ jsx11(
2552
1391
  "button",
2553
1392
  {
2554
1393
  type: "button",
1394
+ ref: registerItem,
2555
1395
  role: "menuitem",
2556
1396
  className: "kerne-menu-item",
2557
- onClick: () => {
2558
- setOpen(false);
2559
- item.onClick?.();
2560
- },
2561
- children: item.label
2562
- },
2563
- item.label
1397
+ "data-tone": "danger",
1398
+ onClick: handleSignOut,
1399
+ children: l10n.account.signOut
1400
+ }
2564
1401
  )
2565
- ),
2566
- /* @__PURE__ */ jsx12(
2567
- "button",
2568
- {
2569
- type: "button",
2570
- role: "menuitem",
2571
- className: "kerne-menu-item",
2572
- "data-tone": "danger",
2573
- onClick: handleSignOut,
2574
- children: l10n.account.signOut
2575
- }
2576
- )
2577
- ] }) : null
1402
+ ] });
1403
+ })() : null
2578
1404
  ] })
2579
1405
  }
2580
1406
  );
2581
1407
  }
2582
1408
 
2583
1409
  // src/ui/UserProfile.tsx
2584
- import { useState as useState18 } from "react";
1410
+ import { useId, useRef as useRef5, useState as useState17 } from "react";
2585
1411
 
2586
1412
  // src/ui/profile/ProfileSection.tsx
2587
- import { useEffect as useEffect6, useState as useState14 } from "react";
2588
- import { Fragment as Fragment6, jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
2589
- function ProfileSection({ verificationCallbackUrl, onSaved }) {
1413
+ import { useEffect as useEffect6, useState as useState13 } from "react";
1414
+ import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1415
+ function ProfileSection({ onSaved }) {
2590
1416
  const { updateProfile, sendVerificationEmail, getVerificationStatus } = useAuth();
2591
1417
  const { user, fullName, initials, email } = useUser();
2592
1418
  const l10n = useLocalization();
2593
1419
  const showError = useErrorResolver();
2594
- const [firstName, setFirstName] = useState14("");
2595
- const [lastName, setLastName] = useState14("");
2596
- const [saving, setSaving] = useState14(false);
2597
- const [saved, setSaved] = useState14(false);
2598
- const [error, setError] = useState14(null);
2599
- const [verified, setVerified] = useState14(null);
2600
- const [sending, setSending] = useState14(false);
2601
- const [sent, setSent] = useState14(false);
1420
+ const [firstName, setFirstName] = useState13("");
1421
+ const [lastName, setLastName] = useState13("");
1422
+ const [saving, setSaving] = useState13(false);
1423
+ const [saved, setSaved] = useState13(false);
1424
+ const [error, setError] = useState13(null);
1425
+ const [verified, setVerified] = useState13(null);
1426
+ const [sending, setSending] = useState13(false);
1427
+ const [sent, setSent] = useState13(false);
2602
1428
  useEffect6(() => {
2603
1429
  if (!user) return;
2604
1430
  setFirstName(user.first_name ?? "");
@@ -2630,7 +1456,7 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
2630
1456
  setError(null);
2631
1457
  setSending(true);
2632
1458
  try {
2633
- await sendVerificationEmail("link", verificationCallbackUrl);
1459
+ await sendVerificationEmail("link");
2634
1460
  setSent(true);
2635
1461
  } catch (err) {
2636
1462
  setError(showError(err));
@@ -2638,19 +1464,19 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
2638
1464
  setSending(false);
2639
1465
  }
2640
1466
  };
2641
- return /* @__PURE__ */ jsxs12(Fragment6, { children: [
2642
- /* @__PURE__ */ jsxs12(Subsection, { title: l10n.profile.profileTitle, children: [
2643
- /* @__PURE__ */ jsxs12("div", { className: "kerne-identity", children: [
2644
- user?.avatar ? /* @__PURE__ */ jsx13("img", { className: "kerne-avatar", src: user.avatar, alt: "" }) : /* @__PURE__ */ jsx13("div", { className: "kerne-avatar", "aria-hidden": "true", children: initials || email?.[0]?.toUpperCase() || "?" }),
2645
- /* @__PURE__ */ jsxs12("div", { className: "kerne-identity-text", children: [
2646
- /* @__PURE__ */ jsx13("span", { className: "kerne-identity-name", children: fullName || l10n.profile.unnamed }),
2647
- /* @__PURE__ */ jsx13("span", { className: "kerne-identity-mail", children: email })
1467
+ return /* @__PURE__ */ jsxs11(Fragment6, { children: [
1468
+ /* @__PURE__ */ jsxs11(Subsection, { title: l10n.profile.profileTitle, children: [
1469
+ /* @__PURE__ */ jsxs11("div", { className: "kerne-identity", children: [
1470
+ user?.avatar ? /* @__PURE__ */ jsx12("img", { className: "kerne-avatar", src: user.avatar, alt: "" }) : /* @__PURE__ */ jsx12("div", { className: "kerne-avatar", "aria-hidden": "true", children: initials || email?.[0]?.toUpperCase() || "?" }),
1471
+ /* @__PURE__ */ jsxs11("div", { className: "kerne-identity-text", children: [
1472
+ /* @__PURE__ */ jsx12("span", { className: "kerne-identity-name", children: fullName || l10n.profile.unnamed }),
1473
+ /* @__PURE__ */ jsx12("span", { className: "kerne-identity-mail", children: email })
2648
1474
  ] })
2649
1475
  ] }),
2650
- error ? /* @__PURE__ */ jsx13(Panel, { variant: "danger", children: error }) : null,
2651
- saved ? /* @__PURE__ */ jsx13(Panel, { variant: "success", children: l10n.profile.saved }) : null,
2652
- /* @__PURE__ */ jsxs12("form", { className: "kerne-form", onSubmit: handleSave, children: [
2653
- /* @__PURE__ */ jsx13(
1476
+ error ? /* @__PURE__ */ jsx12(Panel, { variant: "danger", children: error }) : null,
1477
+ saved ? /* @__PURE__ */ jsx12(Panel, { variant: "success", children: l10n.profile.saved }) : null,
1478
+ /* @__PURE__ */ jsxs11("form", { className: "kerne-form", onSubmit: handleSave, children: [
1479
+ /* @__PURE__ */ jsx12(
2654
1480
  Field,
2655
1481
  {
2656
1482
  label: l10n.profile.firstName,
@@ -2662,7 +1488,7 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
2662
1488
  disabled: saving
2663
1489
  }
2664
1490
  ),
2665
- /* @__PURE__ */ jsx13(
1491
+ /* @__PURE__ */ jsx12(
2666
1492
  Field,
2667
1493
  {
2668
1494
  label: l10n.profile.lastName,
@@ -2674,23 +1500,23 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
2674
1500
  disabled: saving
2675
1501
  }
2676
1502
  ),
2677
- /* @__PURE__ */ jsx13(Button, { loading: saving, children: saving ? l10n.profile.saving : l10n.profile.save })
1503
+ /* @__PURE__ */ jsx12(Button, { loading: saving, children: saving ? l10n.profile.saving : l10n.profile.save })
2678
1504
  ] })
2679
1505
  ] }),
2680
- /* @__PURE__ */ jsxs12(
1506
+ /* @__PURE__ */ jsxs11(
2681
1507
  Subsection,
2682
1508
  {
2683
1509
  title: l10n.profile.emailSectionTitle,
2684
1510
  description: l10n.profile.emailSectionDesc,
2685
1511
  children: [
2686
- /* @__PURE__ */ jsxs12("div", { className: "kerne-plan-option", children: [
2687
- /* @__PURE__ */ jsx13("div", { className: "kerne-plan-option-text", children: /* @__PURE__ */ jsxs12("span", { className: "kerne-plan-option-name", children: [
1512
+ /* @__PURE__ */ jsxs11("div", { className: "kerne-plan-option", children: [
1513
+ /* @__PURE__ */ jsx12("div", { className: "kerne-plan-option-text", children: /* @__PURE__ */ jsxs11("span", { className: "kerne-plan-option-name", children: [
2688
1514
  email,
2689
- verified === true ? /* @__PURE__ */ jsx13("span", { className: "kerne-badge", "data-tone": "success", children: l10n.profile.verified }) : verified === false ? /* @__PURE__ */ jsx13("span", { className: "kerne-badge", "data-tone": "warning", children: l10n.profile.unverified }) : null
1515
+ verified === true ? /* @__PURE__ */ jsx12("span", { className: "kerne-badge", "data-tone": "success", children: l10n.profile.verified }) : verified === false ? /* @__PURE__ */ jsx12("span", { className: "kerne-badge", "data-tone": "warning", children: l10n.profile.unverified }) : null
2690
1516
  ] }) }),
2691
- verified === false ? /* @__PURE__ */ jsx13(Button, { variant: "outline", type: "button", onClick: handleResend, loading: sending, children: sending ? l10n.common.sending : l10n.profile.verify }) : null
1517
+ verified === false ? /* @__PURE__ */ jsx12(Button, { variant: "outline", type: "button", onClick: handleResend, loading: sending, children: sending ? l10n.common.sending : l10n.profile.verify }) : null
2692
1518
  ] }),
2693
- sent ? /* @__PURE__ */ jsx13(Panel, { variant: "success", children: l10n.profile.verificationSent }) : null
1519
+ sent ? /* @__PURE__ */ jsx12(Panel, { variant: "success", children: l10n.profile.verificationSent }) : null
2694
1520
  ]
2695
1521
  }
2696
1522
  )
@@ -2698,16 +1524,16 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
2698
1524
  }
2699
1525
 
2700
1526
  // src/ui/profile/SecuritySection.tsx
2701
- import { useState as useState15 } from "react";
2702
- import { Fragment as Fragment7, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
1527
+ import { useState as useState14 } from "react";
1528
+ import { Fragment as Fragment7, jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
2703
1529
  function SecuritySection({ showDeleteAccount = true, onDeleted }) {
2704
1530
  const { requestAccountDeletion } = useAuth();
2705
1531
  const { config } = useAuthConfig();
2706
1532
  const l10n = useLocalization();
2707
1533
  const showError = useErrorResolver();
2708
- const [error, setError] = useState15(null);
2709
- const [confirmingDelete, setConfirmingDelete] = useState15(false);
2710
- const [deleting, setDeleting] = useState15(false);
1534
+ const [error, setError] = useState14(null);
1535
+ const [confirmingDelete, setConfirmingDelete] = useState14(false);
1536
+ const [deleting, setDeleting] = useState14(false);
2711
1537
  const hasPassword = config?.enable_credentials ?? true;
2712
1538
  const handleDelete = async () => {
2713
1539
  setError(null);
@@ -2720,12 +1546,12 @@ function SecuritySection({ showDeleteAccount = true, onDeleted }) {
2720
1546
  setDeleting(false);
2721
1547
  }
2722
1548
  };
2723
- return /* @__PURE__ */ jsxs13(Fragment7, { children: [
2724
- error ? /* @__PURE__ */ jsx14(Panel, { variant: "danger", children: error }) : null,
2725
- hasPassword ? /* @__PURE__ */ jsx14(Subsection, { title: l10n.security.passwordTitle, description: l10n.security.passwordDesc, children: /* @__PURE__ */ jsx14(ChangePasswordForm, { title: null }) }) : null,
2726
- showDeleteAccount ? /* @__PURE__ */ jsx14(Subsection, { title: l10n.security.deleteTitle, description: l10n.security.deleteDesc, children: confirmingDelete ? /* @__PURE__ */ jsxs13("div", { style: { display: "flex", gap: 10, flexWrap: "wrap" }, children: [
2727
- /* @__PURE__ */ jsx14(Button, { variant: "danger", type: "button", onClick: handleDelete, loading: deleting, children: deleting ? l10n.security.deleting : l10n.security.deleteConfirm }),
2728
- /* @__PURE__ */ jsx14(
1549
+ return /* @__PURE__ */ jsxs12(Fragment7, { children: [
1550
+ error ? /* @__PURE__ */ jsx13(Panel, { variant: "danger", children: error }) : null,
1551
+ hasPassword ? /* @__PURE__ */ jsx13(Subsection, { title: l10n.security.passwordTitle, description: l10n.security.passwordDesc, children: /* @__PURE__ */ jsx13(ChangePasswordForm, { title: null }) }) : null,
1552
+ showDeleteAccount ? /* @__PURE__ */ jsx13(Subsection, { title: l10n.security.deleteTitle, description: l10n.security.deleteDesc, children: confirmingDelete ? /* @__PURE__ */ jsxs12("div", { className: "kerne-inline-actions", children: [
1553
+ /* @__PURE__ */ jsx13(Button, { variant: "danger", type: "button", onClick: handleDelete, loading: deleting, children: deleting ? l10n.security.deleting : l10n.security.deleteConfirm }),
1554
+ /* @__PURE__ */ jsx13(
2729
1555
  Button,
2730
1556
  {
2731
1557
  variant: "outline",
@@ -2735,17 +1561,47 @@ function SecuritySection({ showDeleteAccount = true, onDeleted }) {
2735
1561
  children: l10n.common.cancel
2736
1562
  }
2737
1563
  )
2738
- ] }) : /* @__PURE__ */ jsx14(Button, { variant: "danger", type: "button", onClick: () => setConfirmingDelete(true), children: l10n.security.deleteAction }) }) : null
1564
+ ] }) : /* @__PURE__ */ jsx13(Button, { variant: "danger", type: "button", onClick: () => setConfirmingDelete(true), children: l10n.security.deleteAction }) }) : null
2739
1565
  ] });
2740
1566
  }
2741
1567
 
2742
- // src/ui/profile/BillingSection.tsx
2743
- import { useMemo, useState as useState17 } from "react";
2744
-
2745
1568
  // src/ui/format.ts
2746
1569
  function humanizeFeatureKey(key) {
2747
- const words = key.replace(/[_-]+/g, " ").trim();
2748
- return words.charAt(0).toUpperCase() + words.slice(1);
1570
+ const words = key.replace(/[_-]+/g, " ").trim().split(" ");
1571
+ return words.map((word, index) => {
1572
+ const lower = word.toLowerCase();
1573
+ if (COMMON_ACRONYMS.has(lower)) return lower.toUpperCase();
1574
+ return index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word;
1575
+ }).join(" ");
1576
+ }
1577
+ var COMMON_ACRONYMS = /* @__PURE__ */ new Set([
1578
+ "ai",
1579
+ "api",
1580
+ "cdn",
1581
+ "cpu",
1582
+ "db",
1583
+ "gb",
1584
+ "gpu",
1585
+ "id",
1586
+ "ip",
1587
+ "kb",
1588
+ "mb",
1589
+ "saas",
1590
+ "sdk",
1591
+ "sla",
1592
+ "sms",
1593
+ "sql",
1594
+ "sso",
1595
+ "tb",
1596
+ "ui",
1597
+ "url",
1598
+ "ux"
1599
+ ]);
1600
+ function lowercaseFeatureName(name) {
1601
+ return name.split(" ").map((word) => /^[A-Z0-9]{2,}$/.test(word) ? word : word.toLowerCase()).join(" ");
1602
+ }
1603
+ function capitalize(text) {
1604
+ return text.charAt(0).toUpperCase() + text.slice(1);
2749
1605
  }
2750
1606
  function formatMoney(amountMinorUnits, currency) {
2751
1607
  try {
@@ -2848,6 +1704,7 @@ function resolveLabelSavings(plans, labels) {
2848
1704
  const shortest = sorted[0];
2849
1705
  const longest = sorted[sorted.length - 1];
2850
1706
  if (shortest.slug === longest.slug) return null;
1707
+ let smallest = null;
2851
1708
  for (const plan of plans) {
2852
1709
  const shortPrice = plan.prices.find((p) => p.label.slug === shortest.slug);
2853
1710
  const longPrice = plan.prices.find((p) => p.label.slug === longest.slug);
@@ -2856,13 +1713,16 @@ function resolveLabelSavings(plans, labels) {
2856
1713
  const longMonths = approxMonths(longPrice.interval);
2857
1714
  if (!shortMonths || !longMonths) continue;
2858
1715
  const savings = 1 - longPrice.amount / longMonths / (shortPrice.amount / shortMonths);
2859
- if (savings > 0.01) return { labelSlug: longest.slug, percent: Math.round(savings * 100) };
1716
+ if (savings <= 0.01) return null;
1717
+ smallest = smallest === null ? savings : Math.min(smallest, savings);
2860
1718
  }
2861
- return null;
1719
+ if (smallest === null) return null;
1720
+ const percent = Math.floor(smallest * 100);
1721
+ return percent > 0 ? { labelSlug: longest.slug, percent } : null;
2862
1722
  }
2863
1723
 
2864
1724
  // src/ui/UsageMeters.tsx
2865
- import { Fragment as Fragment8, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
1725
+ import { Fragment as Fragment8, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2866
1726
  function isSoftOverage(overage) {
2867
1727
  return overage === "ALLOW" || overage === "BILL";
2868
1728
  }
@@ -2895,20 +1755,20 @@ function UsageMeters({
2895
1755
  const fromPlan = subscription?.entitlements.find((e) => e.feature_key === key)?.feature_name;
2896
1756
  return fromPlan ?? humanizeFeatureKey(key);
2897
1757
  };
2898
- const shell = (children) => /* @__PURE__ */ jsxs14(Shell, { appearance, className, children: [
2899
- heading ? /* @__PURE__ */ jsx15("div", { className: "kerne-section-head", style: { marginBottom: 16 }, children: /* @__PURE__ */ jsx15("h2", { className: "kerne-section-title", children: heading }) }) : null,
1758
+ const shell = (children) => /* @__PURE__ */ jsxs13(Shell, { appearance, className, children: [
1759
+ heading ? /* @__PURE__ */ jsx14("div", { className: "kerne-section-head", style: { marginBottom: 16 }, children: /* @__PURE__ */ jsx14("h2", { className: "kerne-section-title", children: heading }) }) : null,
2900
1760
  children
2901
1761
  ] });
2902
1762
  if (isLoading) {
2903
1763
  return shell(
2904
- /* @__PURE__ */ jsxs14("div", { className: "kerne-meters", "aria-busy": "true", "aria-label": l10n.usage.loading, children: [
2905
- /* @__PURE__ */ jsx15("div", { className: "kerne-skeleton", style: { height: 34 } }),
2906
- /* @__PURE__ */ jsx15("div", { className: "kerne-skeleton", style: { height: 34 } })
1764
+ /* @__PURE__ */ jsxs13("div", { className: "kerne-meters", "aria-busy": "true", "aria-label": l10n.usage.loading, children: [
1765
+ /* @__PURE__ */ jsx14("div", { className: "kerne-skeleton", style: { height: 34 } }),
1766
+ /* @__PURE__ */ jsx14("div", { className: "kerne-skeleton", style: { height: 34 } })
2907
1767
  ] })
2908
1768
  );
2909
1769
  }
2910
1770
  if (error || !entitlements) {
2911
- return shell(/* @__PURE__ */ jsx15(Panel, { variant: "danger", children: l10n.usage.loadFailed }));
1771
+ return shell(/* @__PURE__ */ jsx14(Panel, { variant: "danger", children: l10n.usage.loadFailed }));
2912
1772
  }
2913
1773
  const metered = entitlements.entitlements.filter((e) => {
2914
1774
  if (e.feature_type !== "QUOTA") return false;
@@ -2924,10 +1784,10 @@ function UsageMeters({
2924
1784
  });
2925
1785
  const rows = maxRows ? metered.slice(0, maxRows) : metered;
2926
1786
  if (rows.length === 0) {
2927
- return shell(emptyState ?? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: l10n.usage.nothingMetered }));
1787
+ return shell(emptyState ?? /* @__PURE__ */ jsx14("p", { className: "kerne-meter-note", children: l10n.usage.nothingMetered }));
2928
1788
  }
2929
1789
  return shell(
2930
- /* @__PURE__ */ jsx15("div", { className: "kerne-meters", children: rows.map((e) => {
1790
+ /* @__PURE__ */ jsx14("div", { className: "kerne-meters", children: rows.map((e, index) => {
2931
1791
  const unlimited = e.limit === null || e.limit === void 0;
2932
1792
  const softOverage = isSoftOverage(e.overage_behavior);
2933
1793
  const billed = e.overage_behavior === "BILL";
@@ -2936,12 +1796,12 @@ function UsageMeters({
2936
1796
  const over = !unlimited && e.used > e.limit;
2937
1797
  const standing = billed ? subscription?.entitlements.find((se) => se.feature_key === e.feature_key) : void 0;
2938
1798
  const capReached = billed && (standing?.cap_reached ?? false);
2939
- return /* @__PURE__ */ jsxs14("div", { className: "kerne-meter", "data-level": level, children: [
2940
- /* @__PURE__ */ jsxs14("div", { className: "kerne-meter-head", children: [
2941
- /* @__PURE__ */ jsx15("span", { className: "kerne-meter-name", children: nameFor(e.feature_key) }),
2942
- /* @__PURE__ */ jsx15("span", { className: "kerne-meter-value", children: unlimited ? fill(l10n.usage.used, { count: formatNumber(e.used) }) : `${formatNumber(e.used)} / ${formatNumber(e.limit)}` })
1799
+ return /* @__PURE__ */ jsxs13("div", { className: "kerne-meter", "data-level": level, children: [
1800
+ /* @__PURE__ */ jsxs13("div", { className: "kerne-meter-head", children: [
1801
+ /* @__PURE__ */ jsx14("span", { className: "kerne-meter-name", children: nameFor(e.feature_key) }),
1802
+ /* @__PURE__ */ jsx14("span", { className: "kerne-meter-value", children: unlimited ? fill(l10n.usage.used, { count: formatNumber(e.used) }) : `${formatNumber(e.used)} / ${formatNumber(e.limit)}` })
2943
1803
  ] }),
2944
- !unlimited ? /* @__PURE__ */ jsx15(
1804
+ !unlimited ? /* @__PURE__ */ jsx14(
2945
1805
  "div",
2946
1806
  {
2947
1807
  className: "kerne-meter-track",
@@ -2950,29 +1810,35 @@ function UsageMeters({
2950
1810
  "aria-valuemin": 0,
2951
1811
  "aria-valuemax": e.limit,
2952
1812
  "aria-label": nameFor(e.feature_key),
2953
- children: /* @__PURE__ */ jsx15("div", { className: "kerne-meter-fill", style: { width: `${ratio * 100}%` } })
1813
+ children: /* @__PURE__ */ jsx14(
1814
+ "div",
1815
+ {
1816
+ className: "kerne-meter-fill",
1817
+ style: { width: `${ratio * 100}%`, animationDelay: `${Math.min(index * 40, 200)}ms` }
1818
+ }
1819
+ )
2954
1820
  }
2955
1821
  ) : null,
2956
- over && softOverage ? /* @__PURE__ */ jsxs14(Fragment8, { children: [
2957
- /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: fill(
1822
+ over && softOverage ? /* @__PURE__ */ jsxs13(Fragment8, { children: [
1823
+ /* @__PURE__ */ jsx14("p", { className: "kerne-meter-note", children: fill(
2958
1824
  // capReached inverts overageBilled's meaning (billing stopped,
2959
1825
  // access didn't) - a variant string would say the opposite of
2960
1826
  // the truth for the one plan that just stopped costing money.
2961
1827
  capReached ? l10n.usage.capReached : billed ? l10n.usage.overageBilled : l10n.usage.overage,
2962
1828
  { count: formatNumber(e.used - e.limit) }
2963
1829
  ) }),
2964
- billed && standing?.billed_amount != null && standing.currency ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note kerne-meter-cost", children: fill(l10n.usage.billedAmount, {
1830
+ billed && standing?.billed_amount != null && standing.currency ? /* @__PURE__ */ jsx14("p", { className: "kerne-meter-note kerne-meter-cost", children: fill(l10n.usage.billedAmount, {
2965
1831
  amount: formatMoney(standing.billed_amount, standing.currency)
2966
1832
  }) }) : null
2967
- ] }) : over ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: l10n.usage.limitReached }) : null
1833
+ ] }) : over ? /* @__PURE__ */ jsx14("p", { className: "kerne-meter-note", children: l10n.usage.limitReached }) : null
2968
1834
  ] }, e.feature_key);
2969
1835
  }) })
2970
1836
  );
2971
1837
  }
2972
1838
 
2973
1839
  // src/ui/SubscriptionCard.tsx
2974
- import { useState as useState16 } from "react";
2975
- import { Fragment as Fragment9, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
1840
+ import { useState as useState15 } from "react";
1841
+ import { Fragment as Fragment9, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
2976
1842
  var STATUS_TONES = {
2977
1843
  ACTIVE: "success",
2978
1844
  TRIALING: "success",
@@ -2984,7 +1850,6 @@ var STATUS_TONES = {
2984
1850
  function SubscriptionCard({
2985
1851
  productSlug,
2986
1852
  title,
2987
- portalReturnUrl,
2988
1853
  upgradeUrl,
2989
1854
  onUpgradeClick,
2990
1855
  hidePortalButton = false,
@@ -3006,15 +1871,15 @@ function SubscriptionCard({
3006
1871
  UNPAID: l10n.billing.statusUnpaid,
3007
1872
  PAUSED: l10n.billing.statusPaused
3008
1873
  };
3009
- const [opening, setOpening] = useState16(false);
3010
- const [portalError, setPortalError] = useState16(null);
3011
- const [confirmingCancel, setConfirmingCancel] = useState16(false);
3012
- const [cancelError, setCancelError] = useState16(null);
1874
+ const [opening, setOpening] = useState15(false);
1875
+ const [portalError, setPortalError] = useState15(null);
1876
+ const [confirmingCancel, setConfirmingCancel] = useState15(false);
1877
+ const [cancelError, setCancelError] = useState15(null);
3013
1878
  const handlePortal = async () => {
3014
1879
  setPortalError(null);
3015
1880
  setOpening(true);
3016
1881
  try {
3017
- await openPortal(portalReturnUrl);
1882
+ await openPortal();
3018
1883
  } catch (e) {
3019
1884
  setPortalError(showError(e));
3020
1885
  setOpening(false);
@@ -3031,26 +1896,26 @@ function SubscriptionCard({
3031
1896
  setCancelError(showError(e));
3032
1897
  }
3033
1898
  };
3034
- const shell = (children) => /* @__PURE__ */ jsx16(Shell, { appearance, className, children: /* @__PURE__ */ jsxs15("div", { className: "kerne-stack", children: [
3035
- heading ? /* @__PURE__ */ jsx16("h2", { className: "kerne-section-title", children: heading }) : null,
1899
+ const shell = (children) => /* @__PURE__ */ jsx15(Shell, { appearance, className, children: /* @__PURE__ */ jsxs14("div", { className: "kerne-stack", children: [
1900
+ heading ? /* @__PURE__ */ jsx15("h2", { className: "kerne-section-title", children: heading }) : null,
3036
1901
  children
3037
1902
  ] }) });
3038
1903
  if (isLoading) {
3039
1904
  return shell(
3040
- /* @__PURE__ */ jsxs15("div", { className: "kerne-rows", "aria-busy": "true", "aria-label": l10n.billing.loadingPlan, children: [
3041
- /* @__PURE__ */ jsx16("div", { className: "kerne-skeleton", style: { height: 20 } }),
3042
- /* @__PURE__ */ jsx16("div", { className: "kerne-skeleton", style: { height: 20 } })
1905
+ /* @__PURE__ */ jsxs14("div", { className: "kerne-rows", "aria-busy": "true", "aria-label": l10n.billing.loadingPlan, children: [
1906
+ /* @__PURE__ */ jsx15("div", { className: "kerne-skeleton", style: { height: 20 } }),
1907
+ /* @__PURE__ */ jsx15("div", { className: "kerne-skeleton", style: { height: 20 } })
3043
1908
  ] })
3044
1909
  );
3045
1910
  }
3046
1911
  if (error) {
3047
- return shell(/* @__PURE__ */ jsx16(Panel, { variant: "danger", children: l10n.billing.loadFailed }));
1912
+ return shell(/* @__PURE__ */ jsx15(Panel, { variant: "danger", children: l10n.billing.loadFailed }));
3048
1913
  }
3049
1914
  if (!subscription) {
3050
1915
  return shell(
3051
- /* @__PURE__ */ jsxs15(Fragment9, { children: [
3052
- /* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.freePlan }),
3053
- upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx16(
1916
+ /* @__PURE__ */ jsxs14(Fragment9, { children: [
1917
+ /* @__PURE__ */ jsx15("p", { className: "kerne-subsection-desc", children: l10n.billing.freePlan }),
1918
+ upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx15(
3054
1919
  Button,
3055
1920
  {
3056
1921
  type: "button",
@@ -3067,304 +1932,239 @@ function SubscriptionCard({
3067
1932
  const renews = formatDate(current_period_end);
3068
1933
  const trialEnds = formatDate(trial_end);
3069
1934
  return shell(
3070
- /* @__PURE__ */ jsxs15(Fragment9, { children: [
3071
- portalError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: portalError }) : null,
3072
- /* @__PURE__ */ jsxs15("dl", { className: "kerne-rows", children: [
3073
- /* @__PURE__ */ jsxs15("div", { className: "kerne-row", children: [
3074
- /* @__PURE__ */ jsx16("dt", { children: l10n.billing.plan }),
3075
- /* @__PURE__ */ jsx16("dd", { children: plan.name })
1935
+ /* @__PURE__ */ jsxs14(Fragment9, { children: [
1936
+ portalError ? /* @__PURE__ */ jsx15(Panel, { variant: "danger", children: portalError }) : null,
1937
+ /* @__PURE__ */ jsxs14("dl", { className: "kerne-rows", children: [
1938
+ /* @__PURE__ */ jsxs14("div", { className: "kerne-row", children: [
1939
+ /* @__PURE__ */ jsx15("dt", { children: l10n.billing.plan }),
1940
+ /* @__PURE__ */ jsx15("dd", { children: plan.name })
3076
1941
  ] }),
3077
- /* @__PURE__ */ jsxs15("div", { className: "kerne-row", children: [
3078
- /* @__PURE__ */ jsx16("dt", { children: l10n.billing.price }),
3079
- /* @__PURE__ */ jsxs15("dd", { children: [
1942
+ /* @__PURE__ */ jsxs14("div", { className: "kerne-row", children: [
1943
+ /* @__PURE__ */ jsx15("dt", { children: l10n.billing.price }),
1944
+ /* @__PURE__ */ jsxs14("dd", { children: [
3080
1945
  formatMoney(plan_price.amount, plan_price.currency),
3081
1946
  " /",
3082
- " ",
3083
- formatInterval(plan_price.interval, l10n.billing)
3084
- ] })
3085
- ] }),
3086
- /* @__PURE__ */ jsxs15("div", { className: "kerne-row", children: [
3087
- /* @__PURE__ */ jsx16("dt", { children: l10n.billing.status }),
3088
- /* @__PURE__ */ jsx16("dd", { children: /* @__PURE__ */ jsx16("span", { className: "kerne-badge", "data-tone": STATUS_TONES[status], children: statusLabels[status] ?? status }) })
3089
- ] }),
3090
- status === "TRIALING" && trialEnds ? /* @__PURE__ */ jsxs15("div", { className: "kerne-row", children: [
3091
- /* @__PURE__ */ jsx16("dt", { children: l10n.billing.trialEnds }),
3092
- /* @__PURE__ */ jsx16("dd", { children: trialEnds })
3093
- ] }) : renews ? /* @__PURE__ */ jsxs15("div", { className: "kerne-row", children: [
3094
- /* @__PURE__ */ jsx16("dt", { children: canceled_at ? l10n.billing.ends : l10n.billing.renews }),
3095
- /* @__PURE__ */ jsx16("dd", { children: renews })
3096
- ] }) : null
3097
- ] }),
3098
- canceled_at ? /* @__PURE__ */ jsx16(Panel, { children: l10n.billing.cancelNotice }) : null,
3099
- cancelError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: cancelError }) : null,
3100
- confirmingCancel ? /* @__PURE__ */ jsxs15(Panel, { children: [
3101
- /* @__PURE__ */ jsx16("p", { children: l10n.billing.cancelConfirmTitle }),
3102
- /* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.cancelConfirmBody }),
3103
- /* @__PURE__ */ jsxs15("div", { className: "kerne-inline-actions", children: [
3104
- /* @__PURE__ */ jsx16(
3105
- Button,
3106
- {
3107
- variant: "outline",
3108
- type: "button",
3109
- onClick: () => setConfirmingCancel(false),
3110
- disabled: isCanceling,
3111
- children: l10n.billing.cancelConfirmDismiss
3112
- }
3113
- ),
3114
- /* @__PURE__ */ jsx16(Button, { type: "button", onClick: handleCancel, loading: isCanceling, children: isCanceling ? l10n.billing.canceling : l10n.billing.cancelConfirmButton })
3115
- ] })
3116
- ] }) : /* @__PURE__ */ jsxs15("div", { className: "kerne-inline-actions", children: [
3117
- !hidePortalButton ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: handlePortal, loading: opening, children: opening ? l10n.billing.opening : l10n.billing.managePortal }) : null,
3118
- !hideCancelButton && !canceled_at && status !== "CANCELED" ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: () => setConfirmingCancel(true), children: l10n.billing.cancelSubscription }) : null
3119
- ] })
3120
- ] })
3121
- );
3122
- }
3123
-
3124
- // src/ui/profile/BillingSection.tsx
3125
- import { Fragment as Fragment10, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
3126
- function intervalsOf(plans) {
3127
- const seen = [];
3128
- for (const plan of plans) {
3129
- for (const price of plan.prices) {
3130
- if (!seen.includes(price.interval)) seen.push(price.interval);
3131
- }
3132
- }
3133
- return seen;
3134
- }
3135
- function BillingSection({
3136
- productSlug,
3137
- pricingProductIdOrSlug,
3138
- portalReturnUrl,
3139
- checkoutSuccessUrl,
3140
- checkoutCancelUrl,
3141
- showPlanSwitcher = true,
3142
- showUsage = true,
3143
- usageFeatureKeys,
3144
- usageLabels
3145
- }) {
3146
- const { subscription, refetch: refetchSubscription } = useSubscription(productSlug);
3147
- const { plans, isLoading: plansLoading } = usePlans(pricingProductIdOrSlug);
3148
- const l10n = useLocalization();
3149
- const showError = useErrorResolver();
3150
- const {
3151
- pending: pendingPlan,
3152
- switching,
3153
- switchError,
3154
- checkoutPriceId: busy,
3155
- checkoutError,
3156
- selectPlan,
3157
- confirmSwitch,
3158
- dismissSwitch
3159
- } = usePlanSwitch(subscription, refetchSubscription, {
3160
- successUrl: checkoutSuccessUrl,
3161
- cancelUrl: checkoutCancelUrl
3162
- });
3163
- const error = checkoutError ?? switchError;
3164
- const intervals = useMemo(() => intervalsOf(plans), [plans]);
3165
- const [interval, setInterval] = useState17(null);
3166
- const activeInterval = interval ?? subscription?.plan_price.interval ?? intervals[0] ?? null;
3167
- return /* @__PURE__ */ jsxs16(Fragment10, { children: [
3168
- error ? /* @__PURE__ */ jsx17(Panel, { variant: "danger", children: showError(error) }) : null,
3169
- pendingPlan ? /* @__PURE__ */ jsxs16(Panel, { children: [
3170
- /* @__PURE__ */ jsx17("p", { children: fill(l10n.billing.switchConfirmTitle, { plan: pendingPlan.plan.name }) }),
3171
- /* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.switchConfirmBody }),
3172
- /* @__PURE__ */ jsxs16("div", { className: "kerne-inline-actions", children: [
3173
- /* @__PURE__ */ jsx17(Button, { variant: "outline", type: "button", onClick: dismissSwitch, disabled: switching, children: l10n.billing.switchConfirmDismiss }),
3174
- /* @__PURE__ */ jsx17(Button, { type: "button", onClick: confirmSwitch, loading: switching, children: switching ? l10n.billing.switching : l10n.billing.switchConfirmButton })
3175
- ] })
3176
- ] }) : null,
3177
- /* @__PURE__ */ jsx17(Subsection, { title: l10n.billing.currentPlan, children: /* @__PURE__ */ jsx17(
3178
- SubscriptionCard,
3179
- {
3180
- title: null,
3181
- productSlug,
3182
- portalReturnUrl
3183
- }
3184
- ) }),
3185
- showUsage ? /* @__PURE__ */ jsx17(Subsection, { title: l10n.usage.title, description: l10n.usage.subtitle, children: /* @__PURE__ */ jsx17(UsageMeters, { title: null, featureKeys: usageFeatureKeys, labels: usageLabels }) }) : null,
3186
- showPlanSwitcher ? /* @__PURE__ */ jsx17(Subsection, { title: l10n.billing.changePlan, children: plansLoading ? /* @__PURE__ */ jsx17(
3187
- "div",
3188
- {
3189
- className: "kerne-skeleton",
3190
- style: { height: 64 },
3191
- "aria-label": l10n.billing.loadingPlans
3192
- }
3193
- ) : plans.length === 0 ? /* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.noPlans }) : /* @__PURE__ */ jsxs16(Fragment10, { children: [
3194
- intervals.length > 1 ? /* @__PURE__ */ jsx17(
3195
- "div",
3196
- {
3197
- className: "kerne-segmented",
3198
- role: "group",
3199
- "aria-label": l10n.billing.intervalGroupLabel,
3200
- children: intervals.map((i) => /* @__PURE__ */ jsx17(
3201
- "button",
3202
- {
3203
- type: "button",
3204
- className: "kerne-segment",
3205
- "aria-pressed": i === activeInterval,
3206
- onClick: () => setInterval(i),
3207
- children: formatInterval(i, l10n.billing)
3208
- },
3209
- i
3210
- ))
3211
- }
3212
- ) : null,
3213
- /* @__PURE__ */ jsx17("div", { className: "kerne-plan-list", children: plans.map((plan) => {
3214
- const price = plan.prices.find((p) => p.interval === activeInterval) ?? plan.prices[0];
3215
- const isCurrent = subscription?.plan.slug === plan.slug;
3216
- return /* @__PURE__ */ jsxs16(
3217
- "div",
3218
- {
3219
- className: "kerne-plan-option",
3220
- "data-current": isCurrent ? "true" : void 0,
3221
- children: [
3222
- /* @__PURE__ */ jsxs16("div", { className: "kerne-plan-option-text", children: [
3223
- /* @__PURE__ */ jsxs16("span", { className: "kerne-plan-option-name", children: [
3224
- plan.name,
3225
- isCurrent ? /* @__PURE__ */ jsx17("span", { className: "kerne-badge", children: l10n.billing.current }) : null
3226
- ] }),
3227
- /* @__PURE__ */ jsx17("span", { className: "kerne-plan-option-price", children: price ? `${formatMoney(price.amount, price.currency)} / ${formatInterval(price.interval, l10n.billing)}` : l10n.billing.notOnInterval })
3228
- ] }),
3229
- !isCurrent && price ? /* @__PURE__ */ jsx17(
3230
- Button,
3231
- {
3232
- variant: "outline",
3233
- type: "button",
3234
- onClick: () => selectPlan(plan, price),
3235
- loading: busy === price.id,
3236
- disabled: busy !== null,
3237
- children: busy === price.id ? l10n.billing.opening : l10n.billing.switchTo
3238
- }
3239
- ) : null
3240
- ]
3241
- },
3242
- plan.id
3243
- );
3244
- }) })
3245
- ] }) }) : null
3246
- ] });
1947
+ " ",
1948
+ formatInterval(plan_price.interval, l10n.billing)
1949
+ ] })
1950
+ ] }),
1951
+ /* @__PURE__ */ jsxs14("div", { className: "kerne-row", children: [
1952
+ /* @__PURE__ */ jsx15("dt", { children: l10n.billing.status }),
1953
+ /* @__PURE__ */ jsx15("dd", { children: /* @__PURE__ */ jsx15("span", { className: "kerne-badge", "data-tone": STATUS_TONES[status], children: statusLabels[status] ?? status }) })
1954
+ ] }),
1955
+ status === "TRIALING" && trialEnds ? /* @__PURE__ */ jsxs14("div", { className: "kerne-row", children: [
1956
+ /* @__PURE__ */ jsx15("dt", { children: l10n.billing.trialEnds }),
1957
+ /* @__PURE__ */ jsx15("dd", { children: trialEnds })
1958
+ ] }) : renews ? /* @__PURE__ */ jsxs14("div", { className: "kerne-row", children: [
1959
+ /* @__PURE__ */ jsx15("dt", { children: canceled_at ? l10n.billing.ends : l10n.billing.renews }),
1960
+ /* @__PURE__ */ jsx15("dd", { children: renews })
1961
+ ] }) : null
1962
+ ] }),
1963
+ canceled_at ? /* @__PURE__ */ jsx15(Panel, { children: l10n.billing.cancelNotice }) : null,
1964
+ cancelError ? /* @__PURE__ */ jsx15(Panel, { variant: "danger", children: cancelError }) : null,
1965
+ confirmingCancel ? /* @__PURE__ */ jsxs14(Panel, { children: [
1966
+ /* @__PURE__ */ jsx15("p", { children: l10n.billing.cancelConfirmTitle }),
1967
+ /* @__PURE__ */ jsx15("p", { className: "kerne-subsection-desc", children: l10n.billing.cancelConfirmBody }),
1968
+ /* @__PURE__ */ jsxs14("div", { className: "kerne-inline-actions", children: [
1969
+ /* @__PURE__ */ jsx15(
1970
+ Button,
1971
+ {
1972
+ variant: "outline",
1973
+ type: "button",
1974
+ onClick: () => setConfirmingCancel(false),
1975
+ disabled: isCanceling,
1976
+ children: l10n.billing.cancelConfirmDismiss
1977
+ }
1978
+ ),
1979
+ /* @__PURE__ */ jsx15(Button, { type: "button", onClick: handleCancel, loading: isCanceling, children: isCanceling ? l10n.billing.canceling : l10n.billing.cancelConfirmButton })
1980
+ ] })
1981
+ ] }) : /* @__PURE__ */ jsxs14("div", { className: "kerne-inline-actions", children: [
1982
+ !hidePortalButton ? /* @__PURE__ */ jsx15(Button, { variant: "outline", type: "button", onClick: handlePortal, loading: opening, children: opening ? l10n.billing.opening : l10n.billing.managePortal }) : null,
1983
+ !hideCancelButton && !canceled_at && status !== "CANCELED" ? /* @__PURE__ */ jsx15(Button, { variant: "outline", type: "button", onClick: () => setConfirmingCancel(true), children: l10n.billing.cancelSubscription }) : null
1984
+ ] })
1985
+ ] })
1986
+ );
3247
1987
  }
3248
1988
 
3249
- // src/ui/UserProfile.tsx
3250
- import { Fragment as Fragment11, jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
3251
- function UserProfile({
3252
- tabs = ["profile", "security", "billing"],
3253
- extraTabs = [],
3254
- defaultTab,
3255
- activeTab: controlledTab,
3256
- onTabChange,
3257
- showDeleteAccount = true,
3258
- onSaved,
3259
- onDeleted,
3260
- productSlug,
3261
- pricingProductIdOrSlug,
3262
- portalReturnUrl,
3263
- checkoutSuccessUrl,
3264
- checkoutCancelUrl,
3265
- showPlanSwitcher = true,
3266
- showUsage = true,
3267
- usageFeatureKeys,
3268
- usageLabels,
3269
- verificationCallbackUrl,
3270
- appearance,
3271
- className
1989
+ // src/ui/PricingTable.tsx
1990
+ import { createContext, useContext, useEffect as useEffect7, useMemo, useRef as useRef4, useState as useState16 } from "react";
1991
+
1992
+ // src/ui/rate.ts
1993
+ function transformQuantity(rate, units) {
1994
+ const { block_size: size, mode, round } = rate.transform;
1995
+ if (size <= 1) return Math.max(0, units);
1996
+ const blocks = Math.max(0, units) / size;
1997
+ if (mode === "proportional") return blocks;
1998
+ return round === "down" ? Math.floor(blocks) : Math.ceil(blocks);
1999
+ }
2000
+ function cost(rate, units) {
2001
+ const quantity = transformQuantity(rate, units);
2002
+ if (quantity <= 0) return 0;
2003
+ const { selection } = rate;
2004
+ if (selection.kind === "flat") return Math.round(quantity * (selection.unit_amount ?? 0));
2005
+ const tiers = selection.tiers ?? [];
2006
+ if (tiers.length === 0) return 0;
2007
+ if (selection.kind === "volume") {
2008
+ const reached = tiers.find((t) => t.up_to === null || quantity <= t.up_to) ?? tiers[tiers.length - 1];
2009
+ return Math.round(quantity * reached.unit_amount);
2010
+ }
2011
+ let remaining = quantity;
2012
+ let previousBound = 0;
2013
+ let total = 0;
2014
+ for (const tier of tiers) {
2015
+ if (remaining <= 0) break;
2016
+ const span = tier.up_to === null ? remaining : Math.max(0, tier.up_to - previousBound);
2017
+ const taken = Math.min(remaining, span);
2018
+ total += taken * tier.unit_amount;
2019
+ remaining -= taken;
2020
+ previousBound = tier.up_to ?? previousBound;
2021
+ }
2022
+ return Math.round(total);
2023
+ }
2024
+ function rateFloor(rate) {
2025
+ const { selection } = rate;
2026
+ if (selection.kind === "flat") return { amount: selection.unit_amount ?? 0, declines: false };
2027
+ const amounts = (selection.tiers ?? []).map((t) => t.unit_amount);
2028
+ if (amounts.length === 0) return { amount: 0, declines: false };
2029
+ const low = Math.min(...amounts);
2030
+ return { amount: low, declines: low < Math.max(...amounts) };
2031
+ }
2032
+ var GAMMA = 2.2;
2033
+ var TICKS = 1e3;
2034
+ var TICK_LINEAR = 24;
2035
+ var FLAT_REACH = 100;
2036
+ var MAX_REACH = 1e5;
2037
+ function trackTicks(max) {
2038
+ return max > TICK_LINEAR ? TICKS : Math.max(1, max);
2039
+ }
2040
+ function ticksToSteps(tick, max) {
2041
+ if (max <= TICK_LINEAR) return Math.min(max, Math.max(0, tick));
2042
+ if (tick <= 0) return 0;
2043
+ return Math.round(1 + (max - 1) * Math.pow((tick - 1) / (TICKS - 1), GAMMA));
2044
+ }
2045
+ function stepsToTicks(steps, max) {
2046
+ if (max <= TICK_LINEAR) return Math.min(max, Math.max(0, steps));
2047
+ if (steps <= 0) return 0;
2048
+ return 1 + Math.round((TICKS - 1) * Math.pow((steps - 1) / (max - 1), 1 / GAMMA));
2049
+ }
2050
+ function sizingReach(rate) {
2051
+ const bounded = (rate.selection.tiers ?? []).filter((t) => t.up_to !== null);
2052
+ const reach = bounded.length === 0 ? FLAT_REACH : bounded[bounded.length - 1].up_to * 2;
2053
+ return Math.min(MAX_REACH, Math.max(10, Math.round(reach)));
2054
+ }
2055
+
2056
+ // src/ui/PricingTable.tsx
2057
+ import { Fragment as Fragment10, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
2058
+ function compactLimit(value) {
2059
+ const abs = Math.abs(value);
2060
+ if (abs >= 1e6) return `${trimZero(value / 1e6)}M`;
2061
+ if (abs >= 1e4) return `${trimZero(value / 1e3)}k`;
2062
+ return formatNumber(value);
2063
+ }
2064
+ function trimZero(value) {
2065
+ return String(Math.round(value * 10) / 10);
2066
+ }
2067
+ function entitlementRow(e, l10n, granted = 0) {
2068
+ const label = e.feature_name;
2069
+ if (e.feature_type === "BOOLEAN") {
2070
+ return { label, value: e.value === "1" ? l10n.included : l10n.notIncluded };
2071
+ }
2072
+ if (e.feature_type === "STATIC") return { label, value: e.value || l10n.included };
2073
+ if (e.limit === null) return { label, value: l10n.included };
2074
+ if (e.limit === -1) return { label, value: perSlice(l10n.unlimited, e, l10n) };
2075
+ const total = e.limit + granted;
2076
+ if (total === 0 && e.overage === "BILL") return { label, value: "" };
2077
+ return {
2078
+ label,
2079
+ value: perSlice(withUnit(total, compactLimit(total), e.unit_label), e, l10n)
2080
+ };
2081
+ }
2082
+ function perSlice(amount, e, l10n) {
2083
+ if (e.limit_binding !== "DIMENSION" || !e.dimension_key) return amount;
2084
+ return fill(l10n.perSlice, { amount, slice: e.dimension_key });
2085
+ }
2086
+ function withUnit(count, amount, unitLabel) {
2087
+ if (!unitLabel) return amount;
2088
+ const unit = count === 1 && unitLabel.endsWith("s") ? unitLabel.slice(0, -1) : unitLabel;
2089
+ return `${amount} ${unit}`;
2090
+ }
2091
+ function rateLead(e, from) {
2092
+ const rate = e.meter?.pricing;
2093
+ if (!rate) return null;
2094
+ const { amount, declines } = rateFloor(rate);
2095
+ const size = rate.transform.block_size;
2096
+ const per = rate.transform.block_label ?? (size === 1 ? e.unit_label ?? "unit" : `${formatNumber(size)} ${e.unit_label ?? "units"}`);
2097
+ return `${declines ? `${from} ` : ""}${formatMoney(amount, rate.currency)} / ${per}`;
2098
+ }
2099
+ function MeterNote({
2100
+ entitlement,
2101
+ lead,
2102
+ interval,
2103
+ children
3272
2104
  }) {
3273
2105
  const l10n = useLocalization();
3274
- const builtins = {
3275
- profile: {
3276
- label: l10n.profile.tabProfile,
3277
- title: l10n.profile.profileTitle,
3278
- description: l10n.profile.profileDesc
3279
- },
3280
- security: {
3281
- label: l10n.profile.tabSecurity,
3282
- title: l10n.profile.securityTitle,
3283
- description: l10n.profile.securityDesc
3284
- },
3285
- billing: {
3286
- label: l10n.profile.tabBilling,
3287
- title: l10n.profile.billingTitle,
3288
- description: l10n.profile.billingDesc
2106
+ const rate = entitlement.meter?.pricing;
2107
+ const id = `kerne-meter-${entitlement.feature_key}`;
2108
+ const countedPer = entitlement.limit_binding === "DIMENSION" && entitlement.dimension_key ? entitlement.dimension_key : l10n.pricing.meterPerCustomer;
2109
+ const rows = [];
2110
+ if (rate) {
2111
+ const size = rate.transform.block_size;
2112
+ const per = rate.transform.block_label ?? (size === 1 ? entitlement.unit_label ?? "unit" : `${formatNumber(size)} ${entitlement.unit_label ?? "units"}`);
2113
+ const tiers = rate.selection.tiers ?? [];
2114
+ const first = rate.selection.unit_amount ?? tiers[0]?.unit_amount ?? 0;
2115
+ rows.push({
2116
+ key: fill(l10n.pricing.meterRatePer, { thing: countedPer }),
2117
+ value: `${formatMoney(first, rate.currency)} / ${per}`
2118
+ });
2119
+ if (tiers.length > 1) {
2120
+ rows.push({
2121
+ key: l10n.pricing.meterShape,
2122
+ value: `${fill(l10n.pricing.meterTiers, {
2123
+ count: tiers.length,
2124
+ amount: formatMoney(rateFloor(rate).amount, rate.currency)
2125
+ })}, ${rate.selection.kind === "volume" ? l10n.pricing.meterVolume : l10n.pricing.meterGraduated}`
2126
+ });
3289
2127
  }
3290
- };
3291
- const allTabs = [
3292
- ...tabs.map((id) => ({ id, ...builtins[id] })),
3293
- ...extraTabs.map((t) => ({
3294
- id: t.id,
3295
- label: t.label,
3296
- title: t.title ?? t.label,
3297
- description: t.description
3298
- }))
3299
- ];
3300
- const [internalTab, setInternalTab] = useState18(defaultTab ?? allTabs[0]?.id ?? "profile");
3301
- const current = controlledTab ?? internalTab;
3302
- const select = (id) => {
3303
- if (controlledTab === void 0) setInternalTab(id);
3304
- onTabChange?.(id);
3305
- };
3306
- const active = allTabs.find((t) => t.id === current) ?? allTabs[0];
3307
- const renderPane = () => {
3308
- switch (active?.id) {
3309
- case "profile":
3310
- return /* @__PURE__ */ jsx18(ProfileSection, { verificationCallbackUrl, onSaved });
3311
- case "security":
3312
- return /* @__PURE__ */ jsx18(SecuritySection, { showDeleteAccount, onDeleted });
3313
- case "billing":
3314
- return /* @__PURE__ */ jsx18(
3315
- BillingSection,
3316
- {
3317
- productSlug,
3318
- pricingProductIdOrSlug,
3319
- portalReturnUrl,
3320
- checkoutSuccessUrl,
3321
- checkoutCancelUrl,
3322
- showPlanSwitcher,
3323
- showUsage,
3324
- usageFeatureKeys,
3325
- usageLabels
3326
- }
3327
- );
3328
- default:
3329
- return extraTabs.find((t) => t.id === active?.id)?.content ?? null;
2128
+ if (size > 1) {
2129
+ rows.push({
2130
+ key: l10n.pricing.meterCounted,
2131
+ value: rate.transform.round === "down" ? l10n.pricing.meterRoundedDown : l10n.pricing.meterRoundedUp
2132
+ });
3330
2133
  }
3331
- };
3332
- return /* @__PURE__ */ jsx18(Shell, { appearance, className, size: "wide", children: /* @__PURE__ */ jsxs17("div", { className: "kerne-profile", children: [
3333
- /* @__PURE__ */ jsx18("nav", { className: "kerne-nav", "aria-label": l10n.profile.navLabel, children: allTabs.map((tab) => /* @__PURE__ */ jsx18(
2134
+ }
2135
+ const resets = entitlement.reset_policy === "ACCUMULATE" ? l10n.pricing.meterResetsNever : entitlement.quota_period === "BILLING_CYCLE" ? interval ? fill(l10n.pricing.meterResetsEvery, {
2136
+ interval: formatInterval(interval, l10n.billing)
2137
+ }) : l10n.pricing.meterResetsCycle : entitlement.quota_period ? entitlement.quota_period.toLowerCase() : null;
2138
+ if (resets) rows.push({ key: l10n.pricing.meterResets, value: resets });
2139
+ if (entitlement.overage_cap_amount != null && rate) {
2140
+ rows.push({
2141
+ key: l10n.pricing.meterCapped,
2142
+ value: formatMoney(entitlement.overage_cap_amount, rate.currency)
2143
+ });
2144
+ }
2145
+ return /* @__PURE__ */ jsxs15("span", { className: "kerne-pricing-pop-host", children: [
2146
+ /* @__PURE__ */ jsx16(
3334
2147
  "button",
3335
2148
  {
3336
2149
  type: "button",
3337
- className: "kerne-nav-item",
3338
- "aria-current": tab.id === active?.id ? "page" : void 0,
3339
- onClick: () => select(tab.id),
3340
- children: tab.label
3341
- },
3342
- tab.id
3343
- )) }),
3344
- /* @__PURE__ */ jsxs17("div", { className: "kerne-pane", children: [
3345
- active ? /* @__PURE__ */ jsxs17(Fragment11, { children: [
3346
- /* @__PURE__ */ jsx18("h2", { className: "kerne-pane-title", children: active.title }),
3347
- active.description ? /* @__PURE__ */ jsx18("p", { className: "kerne-pane-desc", children: active.description }) : null
3348
- ] }) : null,
3349
- renderPane()
3350
- ] })
3351
- ] }) });
3352
- }
3353
-
3354
- // src/ui/PricingTable.tsx
3355
- import { createContext as createContext2, useContext as useContext2, useEffect as useEffect7, useMemo as useMemo2, useRef as useRef4, useState as useState19 } from "react";
3356
- import { Fragment as Fragment12, jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
3357
- function formatEntitlement(e, unlimitedLabel) {
3358
- if (e.feature_type === "BOOLEAN") return e.feature_name;
3359
- if (e.feature_type === "STATIC")
3360
- return e.value ? `${e.feature_name}: ${e.value}` : e.feature_name;
3361
- if (e.limit === null) return e.feature_name;
3362
- if (e.limit === -1) return `${unlimitedLabel} ${e.feature_name.toLowerCase()}`;
3363
- return `${formatNumber(e.limit)} ${e.feature_name.toLowerCase()}`;
2150
+ className: "kerne-pricing-feature-note",
2151
+ "data-lead": lead ? "true" : void 0,
2152
+ "aria-describedby": id,
2153
+ children
2154
+ }
2155
+ ),
2156
+ rows.length > 0 ? /* @__PURE__ */ jsxs15("span", { className: "kerne-pricing-pop", id, role: "tooltip", children: [
2157
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-pop-title", children: l10n.pricing.meterTitle }),
2158
+ rows.map((row) => /* @__PURE__ */ jsxs15("span", { className: "kerne-pricing-pop-row", children: [
2159
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-pop-key", children: row.key }),
2160
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-pop-value", children: row.value })
2161
+ ] }, row.key))
2162
+ ] }) : null
2163
+ ] });
3364
2164
  }
3365
- var PricingTableContext = createContext2(null);
2165
+ var PricingTableContext = createContext(null);
3366
2166
  function usePricingTable() {
3367
- const ctx = useContext2(PricingTableContext);
2167
+ const ctx = useContext(PricingTableContext);
3368
2168
  if (!ctx) {
3369
2169
  throw new Error("usePricingTable() must be called inside <PricingTable>.");
3370
2170
  }
@@ -3373,8 +2173,6 @@ function usePricingTable() {
3373
2173
  function PricingTable({
3374
2174
  product,
3375
2175
  defaultLabel,
3376
- checkoutSuccessUrl,
3377
- checkoutCancelUrl,
3378
2176
  onSelectPlan,
3379
2177
  appearance,
3380
2178
  className,
@@ -3384,7 +2182,7 @@ function PricingTable({
3384
2182
  const { subscription, refetch: refetchSubscription } = useSubscription(product);
3385
2183
  const l10n = useLocalization();
3386
2184
  const showError = useErrorResolver();
3387
- const [selectedLabelSlug, setSelectedLabelSlug] = useState19(defaultLabel ?? null);
2185
+ const [selectedLabelSlug, setSelectedLabelSlug] = useState16(defaultLabel ?? null);
3388
2186
  const {
3389
2187
  pending: pendingSwitch,
3390
2188
  switching,
@@ -3394,25 +2192,22 @@ function PricingTable({
3394
2192
  selectPlan,
3395
2193
  confirmSwitch,
3396
2194
  dismissSwitch
3397
- } = usePlanSwitch(subscription, refetchSubscription, {
3398
- successUrl: checkoutSuccessUrl,
3399
- cancelUrl: checkoutCancelUrl
3400
- });
3401
- const labels = useMemo2(() => collectPriceLabels(plans), [plans]);
3402
- const savings = useMemo2(() => resolveLabelSavings(plans, labels), [plans, labels]);
2195
+ } = usePlanSwitch(subscription, refetchSubscription);
2196
+ const labels = useMemo(() => collectPriceLabels(plans), [plans]);
2197
+ const savings = useMemo(() => resolveLabelSavings(plans, labels), [plans, labels]);
3403
2198
  useEffect7(() => {
3404
2199
  if (labels.length === 0) return;
3405
2200
  if (selectedLabelSlug && labels.some((l) => l.slug === selectedLabelSlug)) return;
3406
2201
  setSelectedLabelSlug(labels[0].slug);
3407
2202
  }, [labels]);
3408
- const handleSelectPlan = (plan) => {
3409
- const price = plan.prices.find((p) => p.label.slug === selectedLabelSlug) ?? plan.prices[0];
2203
+ const handleSelectPlan = (plan, drivers) => {
2204
+ const price = selectedLabelSlug ? plan.prices.find((p) => p.label.slug === selectedLabelSlug) : plan.prices[0];
3410
2205
  if (!price) return;
3411
2206
  if (onSelectPlan) {
3412
- onSelectPlan(plan, price);
2207
+ onSelectPlan(plan, price, drivers);
3413
2208
  return;
3414
2209
  }
3415
- void selectPlan(plan, price);
2210
+ void selectPlan(plan, price, drivers);
3416
2211
  };
3417
2212
  const ctx = {
3418
2213
  plans,
@@ -3421,17 +2216,31 @@ function PricingTable({
3421
2216
  setSelectedLabelSlug,
3422
2217
  savings,
3423
2218
  currentPlanId: subscription?.plan?.id ?? null,
2219
+ currentPriceCurrency: subscription?.plan_price.currency ?? null,
3424
2220
  busyPriceId,
3425
2221
  onSelectPlan: handleSelectPlan
3426
2222
  };
3427
- return /* @__PURE__ */ jsx19(Shell, { appearance, className, size: "wide", children: isLoading ? /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-skeleton", "aria-busy": "true", "aria-label": l10n.pricing.loading, children: /* @__PURE__ */ jsx19("div", { className: "kerne-skeleton", style: { height: 320 } }) }) : error ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: l10n.pricing.loadFailed }) : /* @__PURE__ */ jsxs18(PricingTableContext.Provider, { value: ctx, children: [
3428
- checkoutError ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: showError(checkoutError) }) : null,
3429
- pendingSwitch ? /* @__PURE__ */ jsxs18(Panel, { children: [
3430
- /* @__PURE__ */ jsx19("p", { children: fill(l10n.billing.switchConfirmTitle, { plan: pendingSwitch.plan.name }) }),
3431
- /* @__PURE__ */ jsx19("p", { className: "kerne-subsection-desc", children: l10n.billing.switchConfirmBody }),
3432
- switchError ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: showError(switchError) }) : null,
3433
- /* @__PURE__ */ jsxs18("div", { className: "kerne-inline-actions", children: [
3434
- /* @__PURE__ */ jsx19(
2223
+ return /* @__PURE__ */ jsx16(Shell, { appearance, className, size: "wide", children: isLoading ? (
2224
+ // The shape of what is coming, not one grey slab: a card-sized
2225
+ // placeholder per column means the page does not jump when plans land.
2226
+ /* @__PURE__ */ jsx16("div", { className: "kerne-pricing-grid", "aria-busy": "true", "aria-label": l10n.pricing.loading, children: [0, 1, 2].map((index) => /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-card kerne-pricing-card-ghost", children: [
2227
+ /* @__PURE__ */ jsx16("div", { className: "kerne-skeleton", style: { height: 20, width: "45%" } }),
2228
+ /* @__PURE__ */ jsx16("div", { className: "kerne-skeleton", style: { height: 38, width: "60%" } }),
2229
+ /* @__PURE__ */ jsx16("div", { className: "kerne-skeleton", style: { height: 40 } }),
2230
+ /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-ghost-rows", children: [
2231
+ /* @__PURE__ */ jsx16("div", { className: "kerne-skeleton", style: { height: 12 } }),
2232
+ /* @__PURE__ */ jsx16("div", { className: "kerne-skeleton", style: { height: 12 } }),
2233
+ /* @__PURE__ */ jsx16("div", { className: "kerne-skeleton", style: { height: 12, width: "70%" } })
2234
+ ] })
2235
+ ] }, index)) })
2236
+ ) : error ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: l10n.pricing.loadFailed }) : /* @__PURE__ */ jsxs15(PricingTableContext.Provider, { value: ctx, children: [
2237
+ checkoutError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: showError(checkoutError) }) : null,
2238
+ pendingSwitch ? /* @__PURE__ */ jsxs15(Panel, { children: [
2239
+ /* @__PURE__ */ jsx16("p", { children: fill(l10n.billing.switchConfirmTitle, { plan: pendingSwitch.plan.name }) }),
2240
+ /* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.switchConfirmBody }),
2241
+ switchError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: showError(switchError) }) : null,
2242
+ /* @__PURE__ */ jsxs15("div", { className: "kerne-inline-actions", children: [
2243
+ /* @__PURE__ */ jsx16(
3435
2244
  Button,
3436
2245
  {
3437
2246
  variant: "outline",
@@ -3441,7 +2250,7 @@ function PricingTable({
3441
2250
  children: l10n.billing.switchConfirmDismiss
3442
2251
  }
3443
2252
  ),
3444
- /* @__PURE__ */ jsx19(Button, { type: "button", onClick: confirmSwitch, loading: switching, children: switching ? l10n.billing.switching : l10n.billing.switchConfirmButton })
2253
+ /* @__PURE__ */ jsx16(Button, { type: "button", onClick: confirmSwitch, loading: switching, children: switching ? l10n.billing.switching : l10n.billing.switchConfirmButton })
3445
2254
  ] })
3446
2255
  ] }) : null,
3447
2256
  children
@@ -3452,7 +2261,7 @@ function LabelSwitch() {
3452
2261
  const l10n = useLocalization();
3453
2262
  const containerRef = useRef4(null);
3454
2263
  const buttonRefs = useRef4(/* @__PURE__ */ new Map());
3455
- const [thumb, setThumb] = useState19(null);
2264
+ const [thumb, setThumb] = useState16(null);
3456
2265
  useEffect7(() => {
3457
2266
  const measure = () => {
3458
2267
  const container = containerRef.current;
@@ -3467,7 +2276,7 @@ function LabelSwitch() {
3467
2276
  return () => window.removeEventListener("resize", measure);
3468
2277
  }, [selectedLabelSlug, labels]);
3469
2278
  if (labels.length < 2) return null;
3470
- return /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-switch-wrap", children: /* @__PURE__ */ jsxs18(
2279
+ return /* @__PURE__ */ jsx16("div", { className: "kerne-pricing-switch-wrap", children: /* @__PURE__ */ jsxs15(
3471
2280
  "div",
3472
2281
  {
3473
2282
  ref: containerRef,
@@ -3475,7 +2284,7 @@ function LabelSwitch() {
3475
2284
  role: "group",
3476
2285
  "aria-label": l10n.billing.intervalGroupLabel,
3477
2286
  children: [
3478
- thumb ? /* @__PURE__ */ jsx19(
2287
+ thumb ? /* @__PURE__ */ jsx16(
3479
2288
  "span",
3480
2289
  {
3481
2290
  className: "kerne-pricing-switch-thumb",
@@ -3483,7 +2292,7 @@ function LabelSwitch() {
3483
2292
  "aria-hidden": "true"
3484
2293
  }
3485
2294
  ) : null,
3486
- labels.map((label) => /* @__PURE__ */ jsxs18(
2295
+ labels.map((label) => /* @__PURE__ */ jsxs15(
3487
2296
  "button",
3488
2297
  {
3489
2298
  ref: (el) => {
@@ -3496,7 +2305,7 @@ function LabelSwitch() {
3496
2305
  onClick: () => setSelectedLabelSlug(label.slug),
3497
2306
  children: [
3498
2307
  label.name,
3499
- savings && savings.labelSlug === label.slug ? /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-switch-save", children: fill(l10n.pricing.save, { percent: savings.percent }) }) : null
2308
+ savings && savings.labelSlug === label.slug ? /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-switch-save", children: fill(l10n.pricing.save, { percent: savings.percent }) }) : null
3500
2309
  ]
3501
2310
  },
3502
2311
  label.slug
@@ -3506,7 +2315,7 @@ function LabelSwitch() {
3506
2315
  ) });
3507
2316
  }
3508
2317
  function useAnimatedAmount(target, durationMs = 420) {
3509
- const [displayed, setDisplayed] = useState19(target);
2318
+ const [displayed, setDisplayed] = useState16(target);
3510
2319
  const displayedRef = useRef4(target);
3511
2320
  const frameRef = useRef4();
3512
2321
  useEffect7(() => {
@@ -3535,7 +2344,14 @@ function useAnimatedAmount(target, durationMs = 420) {
3535
2344
  return displayed;
3536
2345
  }
3537
2346
  function Plan({ slug, featured, badge, maxFeatures }) {
3538
- const { plans, selectedLabelSlug, currentPlanId, busyPriceId, onSelectPlan } = usePricingTable();
2347
+ const {
2348
+ plans,
2349
+ selectedLabelSlug,
2350
+ currentPlanId,
2351
+ currentPriceCurrency,
2352
+ busyPriceId,
2353
+ onSelectPlan
2354
+ } = usePricingTable();
3539
2355
  const l10n = useLocalization();
3540
2356
  const plan = plans.find((p) => p.slug === slug);
3541
2357
  useEffect7(() => {
@@ -3545,80 +2361,158 @@ function Plan({ slug, featured, badge, maxFeatures }) {
3545
2361
  );
3546
2362
  }
3547
2363
  }, [plan, slug]);
3548
- const price = plan?.prices.find((p) => p.label.slug === selectedLabelSlug) ?? plan?.prices[0];
2364
+ const price = selectedLabelSlug ? plan?.prices.find((p) => p.label.slug === selectedLabelSlug) : plan?.prices[0];
3549
2365
  const resolved = price ? resolvePriceDisplay(price, price.label.display_interval) : null;
3550
- const animatedAmount = useAnimatedAmount(resolved?.displayAmount ?? 0);
2366
+ const [sizing, setSizing] = useState16({});
2367
+ const sizable = price && plan ? plan.entitlements.map((e) => ({ e, driver: e.drivers.find((d) => d.interval === price.interval) })).filter(
2368
+ (row) => Boolean(row.driver)
2369
+ ) : [];
2370
+ const granted = (featureKey) => {
2371
+ const row = sizable.find((r) => r.e.feature_key === featureKey);
2372
+ return row ? (sizing[featureKey] ?? 0) * row.driver.step_units : 0;
2373
+ };
2374
+ const restate = resolved && resolved.billedAmount > 0 ? resolved.displayAmount / resolved.billedAmount : 1;
2375
+ const added = sizable.reduce(
2376
+ (sum, { e, driver }) => sum + cost(driver.pricing, sizing[e.feature_key] ?? 0) * restate,
2377
+ 0
2378
+ );
2379
+ const animatedAmount = useAnimatedAmount((resolved?.displayAmount ?? 0) + added);
2380
+ const selectedDrivers = () => sizable.filter(({ e }) => (sizing[e.feature_key] ?? 0) > 0).map(({ e }) => ({ feature_key: e.feature_key, steps: sizing[e.feature_key] }));
3551
2381
  if (!plan) return null;
3552
2382
  const isCurrent = plan.id === currentPlanId;
3553
2383
  const isBusy = !!price && busyPriceId === price.id;
2384
+ const currencyMismatch = !!price && !!currentPriceCurrency && price.currency.toLowerCase() !== currentPriceCurrency.toLowerCase();
3554
2385
  const shownEntitlements = maxFeatures ? plan.entitlements.slice(0, maxFeatures) : plan.entitlements;
3555
2386
  const remaining = plan.entitlements.length - shownEntitlements.length;
2387
+ const isFree = !!resolved && resolved.billedAmount === 0 && added === 0;
2388
+ const trialDays = price?.trial_days ?? plan.trial_days;
3556
2389
  const billedCaption = resolved && resolved.displayInterval !== resolved.billedInterval ? fill(l10n.pricing.billedAt, {
3557
2390
  amount: formatMoney(resolved.billedAmount, price.currency),
3558
2391
  interval: formatInterval(resolved.billedInterval, l10n.billing)
3559
2392
  }) : null;
3560
- return /* @__PURE__ */ jsxs18(
2393
+ return /* @__PURE__ */ jsxs15(
3561
2394
  "div",
3562
2395
  {
3563
2396
  className: "kerne-pricing-card",
3564
2397
  "data-featured": featured ? "true" : void 0,
3565
2398
  "data-current": isCurrent ? "true" : void 0,
3566
2399
  children: [
3567
- /* @__PURE__ */ jsxs18("div", { className: "kerne-pricing-card-head", children: [
3568
- badge ? /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-badge", children: badge }) : null,
3569
- /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-card-name", children: plan.name }),
3570
- plan.description ? /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-card-desc", children: plan.description }) : null
3571
- ] }),
3572
- /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-price", children: resolved ? /* @__PURE__ */ jsxs18(Fragment12, { children: [
3573
- /* @__PURE__ */ jsxs18("div", { className: "kerne-pricing-price-row", children: [
3574
- /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-amount", children: formatMoney(animatedAmount, price.currency) }),
3575
- /* @__PURE__ */ jsxs18("span", { className: "kerne-pricing-interval", children: [
3576
- "/ ",
3577
- formatInterval(resolved.displayInterval, l10n.billing)
3578
- ] })
2400
+ /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-card-head", children: [
2401
+ /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-card-titles", children: [
2402
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-card-name", children: plan.name }),
2403
+ badge ? /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-badge", children: badge }) : null
3579
2404
  ] }),
3580
- billedCaption ? /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-billed", children: billedCaption }) : null
3581
- ] }) : /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-billed", children: l10n.billing.notOnInterval }) }),
3582
- shownEntitlements.length > 0 ? /* @__PURE__ */ jsxs18("ul", { className: "kerne-pricing-features", children: [
3583
- shownEntitlements.map((e) => /* @__PURE__ */ jsxs18("li", { className: "kerne-pricing-feature", children: [
3584
- /* @__PURE__ */ jsx19(CheckIcon2, {}),
3585
- /* @__PURE__ */ jsx19("span", { children: formatEntitlement(e, l10n.pricing.unlimited) })
3586
- ] }, e.feature_key)),
3587
- remaining > 0 ? /* @__PURE__ */ jsx19("li", { className: "kerne-pricing-feature-more", children: fill(l10n.pricing.moreFeatures, { count: remaining }) }) : null
3588
- ] }) : null,
3589
- /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-cta", children: isCurrent ? /* @__PURE__ */ jsx19(Button, { variant: "outline", type: "button", disabled: true, children: l10n.billing.current }) : /* @__PURE__ */ jsx19(
3590
- Button,
3591
- {
3592
- variant: featured ? "primary" : "outline",
3593
- type: "button",
3594
- disabled: !price || busyPriceId !== null && !isBusy,
3595
- loading: isBusy,
3596
- onClick: () => onSelectPlan(plan),
3597
- children: currentPlanId ? l10n.billing.switchTo : l10n.pricing.getStarted
3598
- }
3599
- ) })
2405
+ plan.description ? /* @__PURE__ */ jsx16("p", { className: "kerne-pricing-card-desc", children: plan.description }) : null
2406
+ ] }),
2407
+ /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-body", children: [
2408
+ /* @__PURE__ */ jsx16("div", { className: "kerne-pricing-price", children: resolved ? /* @__PURE__ */ jsxs15(Fragment10, { children: [
2409
+ /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-price-row", children: [
2410
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-amount", children: isFree ? l10n.pricing.free : formatMoney(animatedAmount, price.currency) }),
2411
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-interval", children: isFree ? l10n.pricing.forever : `/ ${formatInterval(resolved.displayInterval, l10n.billing)}` })
2412
+ ] }),
2413
+ billedCaption && !isFree ? /* @__PURE__ */ jsx16("p", { className: "kerne-pricing-billed", children: billedCaption }) : null
2414
+ ] }) : /* @__PURE__ */ jsx16("p", { className: "kerne-pricing-billed", children: l10n.billing.notOnInterval }) }),
2415
+ sizable.length > 0 && !currentPlanId ? /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-sizer", children: [
2416
+ /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-sizer-head", children: [
2417
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-sizer-title", children: l10n.pricing.sizeUpFront }),
2418
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-sizer-note", children: l10n.pricing.billedWithPlan })
2419
+ ] }),
2420
+ sizable.map(({ e, driver }) => {
2421
+ const steps = sizing[e.feature_key] ?? 0;
2422
+ const max = Math.max(
2423
+ 1,
2424
+ driver.max_steps ?? Math.max(sizingReach(driver.pricing), steps)
2425
+ );
2426
+ const units = steps * driver.step_units;
2427
+ const total = (e.limit ?? 0) + units;
2428
+ return /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-sizer-row", children: [
2429
+ /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-sizer-line", children: [
2430
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-sizer-name", children: e.feature_name }),
2431
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-sizer-value", children: withUnit(total, formatNumber(total), e.unit_label) })
2432
+ ] }),
2433
+ /* @__PURE__ */ jsx16(
2434
+ "input",
2435
+ {
2436
+ className: "kerne-pricing-range",
2437
+ type: "range",
2438
+ min: 0,
2439
+ max: trackTicks(max),
2440
+ step: 1,
2441
+ value: stepsToTicks(steps, max),
2442
+ "aria-label": fill(l10n.pricing.sizeUpFront, {}),
2443
+ onChange: (event) => setSizing((current) => ({
2444
+ ...current,
2445
+ [e.feature_key]: ticksToSteps(Number(event.target.value) || 0, max)
2446
+ })),
2447
+ style: {
2448
+ "--kerne-fill": `${Math.round(
2449
+ stepsToTicks(steps, max) / trackTicks(max) * 100
2450
+ )}%`
2451
+ }
2452
+ }
2453
+ ),
2454
+ /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-sizer-line", children: [
2455
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-sizer-foot", children: fill(
2456
+ steps === 0 ? l10n.pricing.includedAmount : l10n.pricing.prepaidAmount,
2457
+ {
2458
+ amount: withUnit(
2459
+ steps === 0 ? e.limit ?? 0 : units,
2460
+ formatNumber(steps === 0 ? e.limit ?? 0 : units),
2461
+ e.unit_label
2462
+ )
2463
+ }
2464
+ ) }),
2465
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-sizer-foot", "data-strong": "true", children: steps === 0 ? l10n.pricing.included : `+${formatMoney(cost(driver.pricing, steps) * restate, price.currency)}` })
2466
+ ] })
2467
+ ] }, e.feature_key);
2468
+ })
2469
+ ] }) : null,
2470
+ /* @__PURE__ */ jsx16("div", { className: "kerne-pricing-cta", children: isCurrent ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", disabled: true, children: l10n.billing.current }) : currencyMismatch ? /* @__PURE__ */ jsx16("p", { className: "kerne-pricing-billed", children: l10n.billing.differentCurrency }) : /* @__PURE__ */ jsx16(
2471
+ Button,
2472
+ {
2473
+ variant: featured ? "primary" : "outline",
2474
+ type: "button",
2475
+ disabled: !price || busyPriceId !== null && !isBusy,
2476
+ loading: isBusy,
2477
+ onClick: () => onSelectPlan(plan, selectedDrivers()),
2478
+ children: currentPlanId ? l10n.billing.switchTo : isFree ? l10n.pricing.startFree : l10n.pricing.getStarted
2479
+ }
2480
+ ) }),
2481
+ trialDays > 0 && !isCurrent ? /* @__PURE__ */ jsx16("p", { className: "kerne-pricing-trial", children: fill(l10n.pricing.trialNote, { days: trialDays }) }) : null
2482
+ ] }),
2483
+ /* @__PURE__ */ jsxs15("ul", { className: "kerne-pricing-features", children: [
2484
+ shownEntitlements.length === 0 ? /* @__PURE__ */ jsx16("li", { className: "kerne-pricing-feature-empty", children: l10n.pricing.nothingListed }) : shownEntitlements.map((e) => {
2485
+ const row = entitlementRow(e, l10n.pricing, granted(e.feature_key));
2486
+ const note = e.overage === "BILL" ? row.value === "" ? rateLead(e, l10n.pricing.from) ?? l10n.pricing.metered : l10n.pricing.thenMetered : null;
2487
+ return /* @__PURE__ */ jsxs15("li", { className: "kerne-pricing-feature", children: [
2488
+ /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-feature-name", children: row.label }),
2489
+ /* @__PURE__ */ jsxs15("span", { className: "kerne-pricing-feature-side", children: [
2490
+ row.value ? /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-feature-value", children: row.value }) : null,
2491
+ note ? /* @__PURE__ */ jsx16(
2492
+ MeterNote,
2493
+ {
2494
+ entitlement: e,
2495
+ lead: row.value === "",
2496
+ interval: price?.interval ?? null,
2497
+ children: note
2498
+ }
2499
+ ) : null
2500
+ ] })
2501
+ ] }, e.feature_key);
2502
+ }),
2503
+ remaining > 0 ? /* @__PURE__ */ jsx16("li", { className: "kerne-pricing-feature-more", children: fill(l10n.pricing.moreFeatures, { count: remaining }) }) : null
2504
+ ] })
3600
2505
  ]
3601
2506
  }
3602
2507
  );
3603
2508
  }
3604
- function CheckIcon2() {
3605
- return /* @__PURE__ */ jsx19(
3606
- "svg",
3607
- {
3608
- width: "15",
3609
- height: "15",
3610
- viewBox: "0 0 24 24",
3611
- fill: "none",
3612
- stroke: "currentColor",
3613
- strokeWidth: "2",
3614
- "aria-hidden": "true",
3615
- children: /* @__PURE__ */ jsx19("path", { d: "m5 13 4 4L19 7", strokeLinecap: "round", strokeLinejoin: "round" })
3616
- }
3617
- );
3618
- }
3619
2509
  function Plans({ featuredSlug, featuredBadge, maxFeatures }) {
3620
2510
  const { plans } = usePricingTable();
3621
- return /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-grid", children: plans.map((plan) => /* @__PURE__ */ jsx19(
2511
+ const l10n = useLocalization();
2512
+ if (plans.length === 0) {
2513
+ return /* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.noPlans });
2514
+ }
2515
+ return /* @__PURE__ */ jsx16("div", { className: "kerne-pricing-grid", children: plans.map((plan) => /* @__PURE__ */ jsx16(
3622
2516
  Plan,
3623
2517
  {
3624
2518
  slug: plan.slug,
@@ -3629,13 +2523,192 @@ function Plans({ featuredSlug, featuredBadge, maxFeatures }) {
3629
2523
  plan.id
3630
2524
  )) });
3631
2525
  }
2526
+ function Header({ eyebrow, headline, lede }) {
2527
+ return /* @__PURE__ */ jsxs15("div", { className: "kerne-pricing-header", children: [
2528
+ eyebrow ? /* @__PURE__ */ jsx16("span", { className: "kerne-pricing-eyebrow", children: eyebrow }) : null,
2529
+ /* @__PURE__ */ jsx16("h2", { className: "kerne-pricing-headline", children: headline }),
2530
+ lede ? /* @__PURE__ */ jsx16("p", { className: "kerne-pricing-lede", children: lede }) : null
2531
+ ] });
2532
+ }
2533
+ PricingTable.Header = Header;
3632
2534
  PricingTable.LabelSwitch = LabelSwitch;
3633
2535
  PricingTable.Plan = Plan;
3634
2536
  PricingTable.Plans = Plans;
3635
2537
 
2538
+ // src/ui/profile/BillingSection.tsx
2539
+ import { Fragment as Fragment11, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
2540
+ function BillingSection({
2541
+ productSlug,
2542
+ pricingProductIdOrSlug,
2543
+ showPlanSwitcher = true,
2544
+ showUsage = true,
2545
+ usageFeatureKeys,
2546
+ usageLabels
2547
+ }) {
2548
+ const l10n = useLocalization();
2549
+ return /* @__PURE__ */ jsxs16(Fragment11, { children: [
2550
+ /* @__PURE__ */ jsx17(Subsection, { title: l10n.billing.currentPlan, children: /* @__PURE__ */ jsx17(SubscriptionCard, { title: null, productSlug }) }),
2551
+ showUsage ? /* @__PURE__ */ jsx17(Subsection, { title: l10n.usage.title, description: l10n.usage.subtitle, children: /* @__PURE__ */ jsx17(UsageMeters, { title: null, featureKeys: usageFeatureKeys, labels: usageLabels }) }) : null,
2552
+ showPlanSwitcher ? /* @__PURE__ */ jsx17(Subsection, { title: l10n.billing.changePlan, children: /* @__PURE__ */ jsxs16(PricingTable, { product: pricingProductIdOrSlug, children: [
2553
+ /* @__PURE__ */ jsx17(PricingTable.LabelSwitch, {}),
2554
+ /* @__PURE__ */ jsx17(PricingTable.Plans, {})
2555
+ ] }) }) : null
2556
+ ] });
2557
+ }
2558
+
2559
+ // src/ui/UserProfile.tsx
2560
+ import { Fragment as Fragment12, jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
2561
+ function UserProfile({
2562
+ tabs = ["profile", "security", "billing"],
2563
+ extraTabs = [],
2564
+ defaultTab,
2565
+ activeTab: controlledTab,
2566
+ onTabChange,
2567
+ showDeleteAccount = true,
2568
+ onSaved,
2569
+ onDeleted,
2570
+ productSlug,
2571
+ pricingProductIdOrSlug,
2572
+ showPlanSwitcher = true,
2573
+ showUsage = true,
2574
+ usageFeatureKeys,
2575
+ usageLabels,
2576
+ appearance,
2577
+ className
2578
+ }) {
2579
+ const l10n = useLocalization();
2580
+ const builtins = {
2581
+ profile: {
2582
+ label: l10n.profile.tabProfile,
2583
+ title: l10n.profile.profileTitle,
2584
+ description: l10n.profile.profileDesc
2585
+ },
2586
+ security: {
2587
+ label: l10n.profile.tabSecurity,
2588
+ title: l10n.profile.securityTitle,
2589
+ description: l10n.profile.securityDesc
2590
+ },
2591
+ billing: {
2592
+ label: l10n.profile.tabBilling,
2593
+ title: l10n.profile.billingTitle,
2594
+ description: l10n.profile.billingDesc
2595
+ }
2596
+ };
2597
+ const allTabs = [
2598
+ ...tabs.map((id) => ({ id, ...builtins[id] })),
2599
+ ...extraTabs.map((t) => ({
2600
+ id: t.id,
2601
+ label: t.label,
2602
+ title: t.title ?? t.label,
2603
+ description: t.description
2604
+ }))
2605
+ ];
2606
+ const [internalTab, setInternalTab] = useState17(defaultTab ?? allTabs[0]?.id ?? "profile");
2607
+ const current = controlledTab ?? internalTab;
2608
+ const select = (id) => {
2609
+ if (controlledTab === void 0) setInternalTab(id);
2610
+ onTabChange?.(id);
2611
+ };
2612
+ const active = allTabs.find((t) => t.id === current) ?? allTabs[0];
2613
+ const baseId = useId();
2614
+ const tabId = (id) => `${baseId}-tab-${id}`;
2615
+ const panelId = (id) => `${baseId}-panel-${id}`;
2616
+ const tabRefs = useRef5(/* @__PURE__ */ new Map());
2617
+ const onTabKeyDown = (e, index) => {
2618
+ let nextIndex = null;
2619
+ switch (e.key) {
2620
+ case "ArrowRight":
2621
+ case "ArrowDown":
2622
+ nextIndex = (index + 1) % allTabs.length;
2623
+ break;
2624
+ case "ArrowLeft":
2625
+ case "ArrowUp":
2626
+ nextIndex = (index - 1 + allTabs.length) % allTabs.length;
2627
+ break;
2628
+ case "Home":
2629
+ nextIndex = 0;
2630
+ break;
2631
+ case "End":
2632
+ nextIndex = allTabs.length - 1;
2633
+ break;
2634
+ default:
2635
+ return;
2636
+ }
2637
+ e.preventDefault();
2638
+ const next = allTabs[nextIndex];
2639
+ if (!next) return;
2640
+ select(next.id);
2641
+ tabRefs.current.get(next.id)?.focus();
2642
+ };
2643
+ const renderPane = () => {
2644
+ switch (active?.id) {
2645
+ case "profile":
2646
+ return /* @__PURE__ */ jsx18(ProfileSection, { onSaved });
2647
+ case "security":
2648
+ return /* @__PURE__ */ jsx18(SecuritySection, { showDeleteAccount, onDeleted });
2649
+ case "billing":
2650
+ return /* @__PURE__ */ jsx18(
2651
+ BillingSection,
2652
+ {
2653
+ productSlug,
2654
+ pricingProductIdOrSlug,
2655
+ showPlanSwitcher,
2656
+ showUsage,
2657
+ usageFeatureKeys,
2658
+ usageLabels
2659
+ }
2660
+ );
2661
+ default:
2662
+ return extraTabs.find((t) => t.id === active?.id)?.content ?? null;
2663
+ }
2664
+ };
2665
+ return /* @__PURE__ */ jsx18(Shell, { appearance, className, size: "wide", children: /* @__PURE__ */ jsxs17("div", { className: "kerne-profile", children: [
2666
+ /* @__PURE__ */ jsx18("div", { className: "kerne-nav", role: "tablist", "aria-label": l10n.profile.navLabel, children: allTabs.map((tab, index) => {
2667
+ const selected = tab.id === active?.id;
2668
+ return /* @__PURE__ */ jsx18(
2669
+ "button",
2670
+ {
2671
+ ref: (el) => {
2672
+ if (el) tabRefs.current.set(tab.id, el);
2673
+ else tabRefs.current.delete(tab.id);
2674
+ },
2675
+ type: "button",
2676
+ role: "tab",
2677
+ id: tabId(tab.id),
2678
+ className: "kerne-nav-item",
2679
+ "aria-selected": selected,
2680
+ "aria-controls": panelId(tab.id),
2681
+ tabIndex: selected ? 0 : -1,
2682
+ onClick: () => select(tab.id),
2683
+ onKeyDown: (e) => onTabKeyDown(e, index),
2684
+ children: tab.label
2685
+ },
2686
+ tab.id
2687
+ );
2688
+ }) }),
2689
+ /* @__PURE__ */ jsxs17(
2690
+ "div",
2691
+ {
2692
+ className: "kerne-pane",
2693
+ role: "tabpanel",
2694
+ id: active ? panelId(active.id) : void 0,
2695
+ "aria-labelledby": active ? tabId(active.id) : void 0,
2696
+ tabIndex: 0,
2697
+ children: [
2698
+ active ? /* @__PURE__ */ jsxs17(Fragment12, { children: [
2699
+ /* @__PURE__ */ jsx18("h2", { className: "kerne-pane-title", children: active.title }),
2700
+ active.description ? /* @__PURE__ */ jsx18("p", { className: "kerne-pane-desc", children: active.description }) : null
2701
+ ] }) : null,
2702
+ renderPane()
2703
+ ]
2704
+ }
2705
+ )
2706
+ ] }) });
2707
+ }
2708
+
3636
2709
  // src/ui/CheckoutResult.tsx
3637
- import { useEffect as useEffect8, useRef as useRef5, useState as useState20 } from "react";
3638
- import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
2710
+ import { useEffect as useEffect8, useRef as useRef6, useState as useState18 } from "react";
2711
+ import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
3639
2712
  var POLL_INTERVAL_MS = 1500;
3640
2713
  function CheckoutResult({
3641
2714
  outcome = "success",
@@ -3653,8 +2726,8 @@ function CheckoutResult({
3653
2726
  const client = useClient();
3654
2727
  const { config } = useAuthConfig();
3655
2728
  const l10n = useLocalization();
3656
- const [state, setState] = useState20("checking");
3657
- const confirmed = useRef5(false);
2729
+ const [state, setState] = useState18("checking");
2730
+ const confirmed = useRef6(false);
3658
2731
  useEffect8(() => {
3659
2732
  if (outcome !== "success") return;
3660
2733
  let stopped = false;
@@ -3686,20 +2759,20 @@ function CheckoutResult({
3686
2759
  const go = (url, handler) => handler ?? (() => {
3687
2760
  if (url) window.location.href = url;
3688
2761
  });
3689
- const continueAction = continueUrl || onContinue ? /* @__PURE__ */ jsx20(Button, { type: "button", onClick: go(continueUrl, onContinue), children: l10n.common.continue }) : void 0;
3690
- const shell = (children) => /* @__PURE__ */ jsxs19(Shell, { appearance, className, children: [
3691
- logo ? /* @__PURE__ */ jsx20("div", { className: "kerne-masthead", children: logo }) : null,
2762
+ const continueAction = continueUrl || onContinue ? /* @__PURE__ */ jsx19(Button, { type: "button", onClick: go(continueUrl, onContinue), children: l10n.common.continue }) : void 0;
2763
+ const shell = (children) => /* @__PURE__ */ jsxs18(Shell, { appearance, className, children: [
2764
+ logo ? /* @__PURE__ */ jsx19("div", { className: "kerne-masthead", children: logo }) : null,
3692
2765
  children,
3693
- /* @__PURE__ */ jsx20(KerneBranding, { show: config?.show_kerne_branding ?? false })
2766
+ /* @__PURE__ */ jsx19(KerneBranding, { show: config?.show_kerne_branding ?? false })
3694
2767
  ] });
3695
2768
  if (outcome === "cancel") {
3696
2769
  return shell(
3697
- /* @__PURE__ */ jsx20(
2770
+ /* @__PURE__ */ jsx19(
3698
2771
  Status,
3699
2772
  {
3700
2773
  tone: "danger",
3701
2774
  title: l10n.checkout.cancelTitle,
3702
- action: pricingUrl || onRetry ? /* @__PURE__ */ jsx20(Button, { variant: "outline", type: "button", onClick: go(pricingUrl, onRetry), children: l10n.checkout.backToPlans }) : void 0,
2775
+ action: pricingUrl || onRetry ? /* @__PURE__ */ jsx19(Button, { variant: "outline", type: "button", onClick: go(pricingUrl, onRetry), children: l10n.checkout.backToPlans }) : void 0,
3703
2776
  children: l10n.checkout.cancelBody
3704
2777
  }
3705
2778
  )
@@ -3707,21 +2780,21 @@ function CheckoutResult({
3707
2780
  }
3708
2781
  if (state === "checking") {
3709
2782
  return shell(
3710
- /* @__PURE__ */ jsx20(Status, { tone: "loading", title: l10n.checkout.confirmingTitle, children: l10n.checkout.confirmingBody })
2783
+ /* @__PURE__ */ jsx19(Status, { tone: "loading", title: l10n.checkout.confirmingTitle, children: l10n.checkout.confirmingBody })
3711
2784
  );
3712
2785
  }
3713
2786
  if (state === "pending") {
3714
2787
  return shell(
3715
- /* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.pendingTitle, action: continueAction, children: l10n.checkout.pendingBody })
2788
+ /* @__PURE__ */ jsx19(Status, { tone: "success", title: l10n.checkout.pendingTitle, action: continueAction, children: l10n.checkout.pendingBody })
3716
2789
  );
3717
2790
  }
3718
2791
  return shell(
3719
- /* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.doneTitle, action: continueAction, children: l10n.checkout.doneBody })
2792
+ /* @__PURE__ */ jsx19(Status, { tone: "success", title: l10n.checkout.doneTitle, action: continueAction, children: l10n.checkout.doneBody })
3720
2793
  );
3721
2794
  }
3722
2795
 
3723
2796
  // src/ui/UpgradePrompt.tsx
3724
- import { Fragment as Fragment13, jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
2797
+ import { Fragment as Fragment13, jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
3725
2798
  function UpgradePrompt({
3726
2799
  featureKey,
3727
2800
  requested,
@@ -3740,12 +2813,14 @@ function UpgradePrompt({
3740
2813
  const l10n = useLocalization();
3741
2814
  if (isLoading) {
3742
2815
  if (hideWhileLoading) return null;
3743
- return /* @__PURE__ */ jsx21(Shell, { appearance, className, children: /* @__PURE__ */ jsx21("div", { className: "kerne-skeleton", style: { height: 80 }, "aria-label": l10n.upgrade.checking }) });
2816
+ return /* @__PURE__ */ jsx20(Shell, { appearance, className, children: /* @__PURE__ */ jsx20("div", { className: "kerne-skeleton", style: { height: 80 }, "aria-label": l10n.upgrade.checking }) });
3744
2817
  }
3745
- if (allowed) return /* @__PURE__ */ jsx21(Fragment13, { children });
3746
- const name = featureName ?? humanizeFeatureKey(featureKey).toLowerCase();
2818
+ if (allowed) return /* @__PURE__ */ jsx20(Fragment13, { children });
2819
+ const name = featureName ?? lowercaseFeatureName(humanizeFeatureKey(featureKey));
3747
2820
  const quotaExhausted = details?.feature_type === "QUOTA" && typeof details.limit === "number" && typeof details.used === "number";
3748
- const heading = title ?? fill(quotaExhausted ? l10n.upgrade.quotaTitle : l10n.upgrade.featureTitle, { feature: name });
2821
+ const heading = title ?? capitalize(
2822
+ fill(quotaExhausted ? l10n.upgrade.quotaTitle : l10n.upgrade.featureTitle, { feature: name })
2823
+ );
3749
2824
  const body = description ?? (quotaExhausted ? fill(l10n.upgrade.quotaBody, {
3750
2825
  used: formatNumber(details.used),
3751
2826
  // `access_limit` is where access actually stops; `limit` is only the
@@ -3753,12 +2828,12 @@ function UpgradePrompt({
3753
2828
  // Falls back to `limit` defensively - should never be needed here.
3754
2829
  limit: formatNumber(details.access_limit ?? details.limit)
3755
2830
  }) : fill(l10n.upgrade.featureBody, { feature: name }));
3756
- return /* @__PURE__ */ jsx21(Shell, { appearance, className, children: /* @__PURE__ */ jsxs20("div", { className: "kerne-status", children: [
3757
- /* @__PURE__ */ jsxs20("div", { className: "kerne-heading", children: [
3758
- /* @__PURE__ */ jsx21("h2", { className: "kerne-section-title", style: { fontSize: 17 }, children: heading }),
3759
- /* @__PURE__ */ jsx21("p", { className: "kerne-status-text", children: body })
2831
+ return /* @__PURE__ */ jsx20(Shell, { appearance, className, children: /* @__PURE__ */ jsxs19("div", { className: "kerne-status", children: [
2832
+ /* @__PURE__ */ jsxs19("div", { className: "kerne-heading", children: [
2833
+ /* @__PURE__ */ jsx20("h2", { className: "kerne-section-title", children: heading }),
2834
+ /* @__PURE__ */ jsx20("p", { className: "kerne-status-text", children: body })
3760
2835
  ] }),
3761
- upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx21(
2836
+ upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx20(
3762
2837
  Button,
3763
2838
  {
3764
2839
  type: "button",
@@ -3794,11 +2869,14 @@ export {
3794
2869
  WaitlistForm,
3795
2870
  collectPriceLabels,
3796
2871
  defaultLocalization,
2872
+ defaultTokens,
3797
2873
  formatDate,
3798
2874
  formatInterval,
3799
2875
  formatMoney,
3800
2876
  formatNumber,
2877
+ fr,
3801
2878
  humanizeFeatureKey,
2879
+ mergeLocalization,
3802
2880
  resolveLabelSavings,
3803
2881
  resolvePriceDisplay,
3804
2882
  useLocalization,