@kerne/react 0.1.2 → 1.0.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
@@ -6,6 +6,7 @@ import {
6
6
  useAccess,
7
7
  useAuth,
8
8
  useAuthConfig,
9
+ useCancelSubscription,
9
10
  useCheckout,
10
11
  useClient,
11
12
  useEntitlements,
@@ -16,10 +17,10 @@ import {
16
17
  useSubscription,
17
18
  useUser,
18
19
  useWaitlist
19
- } from "../chunk-AIUIBAYB.js";
20
+ } from "../chunk-NFJ2S5VJ.js";
20
21
 
21
22
  // src/ui/AuthFlow.tsx
22
- import { useState as useState9 } from "react";
23
+ import { useState as useState10 } from "react";
23
24
 
24
25
  // src/ui/useUrlToken.ts
25
26
  import { useState, useEffect } from "react";
@@ -37,10 +38,10 @@ function useUrlToken(param = "token", override) {
37
38
  }
38
39
 
39
40
  // src/ui/LoginForm.tsx
40
- import { useState as useState2 } from "react";
41
+ import { useState as useState3 } from "react";
41
42
 
42
43
  // src/ui/primitives.tsx
43
- import { useId, useContext, createContext } from "react";
44
+ import { useId, useState as useState2, useContext, createContext } from "react";
44
45
 
45
46
  // src/ui/styles.ts
46
47
  var STYLE_ELEMENT_ID = "kerne-ui-styles";
@@ -53,6 +54,7 @@ var DARK_TOKENS = `
53
54
  --kerne-panel: #1b1f26;
54
55
  --kerne-surface: #14171c;
55
56
  --kerne-surface-raised: #2a2f38;
57
+ --kerne-track: #313743;
56
58
  --kerne-primary: #f0f1f3;
57
59
  --kerne-primary-fg: #16181d;
58
60
  --kerne-primary-hover: #d8dade;
@@ -81,6 +83,11 @@ var CSS = `
81
83
  variant, the dropdown floating over host content, a pressed segment. */
82
84
  --kerne-surface: #ffffff;
83
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;
84
91
  --kerne-primary: #16171a;
85
92
  --kerne-primary-fg: #ffffff;
86
93
  --kerne-primary-hover: #2c2e33;
@@ -226,13 +233,29 @@ var CSS = `
226
233
  }
227
234
  .kerne-subtitle { margin: 0; font-size: 14px; color: var(--kerne-fg-muted); line-height: 1.5; }
228
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
+
229
248
  /* ---------- Stack ---------- */
230
249
  .kerne-stack { display: flex; flex-direction: column; gap: 16px; }
250
+ .kerne-inline-actions { display: flex; gap: 8px; flex-wrap: wrap; }
231
251
  .kerne-form { display: flex; flex-direction: column; gap: 12px; }
232
252
  .kerne-field { display: flex; flex-direction: column; gap: 6px; }
233
253
  .kerne-label { font-size: 14px; font-weight: 500; color: var(--kerne-fg); }
234
254
  .kerne-label-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
235
255
 
256
+ /* Positioning context for the reveal toggle, which sits inside the input box. */
257
+ .kerne-control { position: relative; display: flex; }
258
+
236
259
  .kerne-input {
237
260
  font: inherit;
238
261
  font-size: 14px;
@@ -245,6 +268,57 @@ var CSS = `
245
268
  width: 100%;
246
269
  transition: border-color .12s ease, box-shadow .12s ease;
247
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
+ }
248
322
  .kerne-input::placeholder { color: var(--kerne-fg-subtle); }
249
323
  .kerne-input:hover:not(:disabled):not(:focus) { border-color: var(--kerne-fg-subtle); }
250
324
  .kerne-input:focus {
@@ -279,6 +353,20 @@ var CSS = `
279
353
  }
280
354
  .kerne-button:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; }
281
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; }
282
370
  .kerne-button-primary { background: var(--kerne-primary); color: var(--kerne-primary-fg); }
283
371
  .kerne-button-primary:hover:not(:disabled) { background: var(--kerne-primary-hover); }
284
372
  .kerne-button-outline {
@@ -417,7 +505,7 @@ var CSS = `
417
505
  /* Digits line up between rows instead of jittering as counts change. */
418
506
  font-variant-numeric: tabular-nums;
419
507
  }
420
- .kerne-meter-track { height: 6px; border-radius: 999px; background: var(--kerne-panel); overflow: hidden; }
508
+ .kerne-meter-track { height: 6px; border-radius: 999px; background: var(--kerne-track); overflow: hidden; }
421
509
  .kerne-meter-fill { height: 100%; border-radius: 999px; background: var(--kerne-fg); transition: width .3s ease; }
422
510
  .kerne-meter[data-level="warning"] .kerne-meter-fill { background: var(--kerne-warning); }
423
511
  .kerne-meter[data-level="critical"] .kerne-meter-fill { background: var(--kerne-danger); }
@@ -426,6 +514,126 @@ var CSS = `
426
514
  .kerne-meter-note { font-size: 12px; color: var(--kerne-fg-subtle); margin: 0; }
427
515
  @media (prefers-reduced-motion: reduce) { .kerne-meter-fill { transition: none; } }
428
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
+
429
637
  /* ---------- Sections (profile, settings) ---------- */
430
638
  .kerne-section-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
431
639
  .kerne-section-title { font-size: 15px; font-weight: 600; margin: 0; }
@@ -577,6 +785,43 @@ function resolveTheme(appearance) {
577
785
  return appearance?.theme ?? "light";
578
786
  }
579
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
+
580
825
  // src/ui/primitives.tsx
581
826
  import { jsx, jsxs } from "react/jsx-runtime";
582
827
  var InsideShell = createContext(false);
@@ -635,6 +880,10 @@ function Masthead({
635
880
  ] })
636
881
  ] });
637
882
  }
883
+ function prefersAutoFocus() {
884
+ if (typeof window === "undefined" || !window.matchMedia) return false;
885
+ return window.matchMedia("(pointer: fine)").matches;
886
+ }
638
887
  function Field({
639
888
  label,
640
889
  type,
@@ -645,55 +894,185 @@ function Field({
645
894
  disabled,
646
895
  required,
647
896
  invalid,
897
+ error,
648
898
  action,
649
899
  code,
650
- autoFocus
900
+ autoFocus,
901
+ children
651
902
  }) {
652
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(" ");
653
915
  return /* @__PURE__ */ jsxs("div", { className: "kerne-field", children: [
654
916
  action ? /* @__PURE__ */ jsxs("div", { className: "kerne-label-row", children: [
655
917
  /* @__PURE__ */ jsx("label", { className: "kerne-label", htmlFor: id, children: label }),
656
918
  action
657
919
  ] }) : /* @__PURE__ */ jsx("label", { className: "kerne-label", htmlFor: id, children: label }),
658
- /* @__PURE__ */ jsx(
659
- "input",
660
- {
661
- id,
662
- className: "kerne-input",
663
- type,
664
- value,
665
- onChange: (e) => onChange(e.target.value),
666
- autoComplete,
667
- placeholder,
668
- disabled,
669
- required,
670
- "aria-invalid": invalid || void 0,
671
- "data-kerne-code": code ? "true" : void 0,
672
- autoFocus
673
- }
674
- )
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
675
959
  ] });
676
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
+ }
677
1004
  function Button({
678
1005
  children,
679
1006
  variant = "primary",
680
1007
  disabled,
1008
+ loading,
681
1009
  type = "submit",
682
1010
  onClick,
683
1011
  ariaLabel
684
1012
  }) {
685
- return /* @__PURE__ */ jsx(
1013
+ return /* @__PURE__ */ jsxs(
686
1014
  "button",
687
1015
  {
688
1016
  type,
689
1017
  className: `kerne-button kerne-button-${variant}`,
690
- disabled,
1018
+ disabled: disabled || loading,
691
1019
  onClick,
692
1020
  "aria-label": ariaLabel,
693
- children
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
+ ]
694
1027
  }
695
1028
  );
696
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
+ }
697
1076
  function MethodSwitch({
698
1077
  onClick,
699
1078
  children
@@ -746,7 +1125,7 @@ function Panel({
746
1125
  "div",
747
1126
  {
748
1127
  className: `kerne-panel${danger ? " kerne-panel-danger" : ""}${variant === "success" ? " kerne-panel-success" : ""}`,
749
- ...danger ? { role: "alert" } : {},
1128
+ ...danger ? { role: "alert" } : { role: "status", "aria-live": "polite" },
750
1129
  children: [
751
1130
  icon,
752
1131
  /* @__PURE__ */ jsx("span", { children })
@@ -911,12 +1290,12 @@ function LoginForm({
911
1290
  const { config, isLoading: configLoading, error: configError } = useAuthConfig();
912
1291
  const l10n = useLocalization();
913
1292
  const showError = useErrorResolver();
914
- const [email, setEmail] = useState2("");
915
- const [password, setPassword] = useState2("");
916
- const [submitting, setSubmitting] = useState2(false);
917
- const [error, setError] = useState2(null);
918
- const [linkSentTo, setLinkSentTo] = useState2(null);
919
- const [passwordMode, setPasswordMode] = useState2(false);
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);
920
1299
  const shell = (children) => /* @__PURE__ */ jsx2(Shell, { appearance, className, children });
921
1300
  const hasOAuth = (config?.oauth_providers.length ?? 0) > 0;
922
1301
  const hasCredentials = config?.enable_credentials ?? false;
@@ -1012,7 +1391,7 @@ function LoginForm({
1012
1391
  action: forgotPasswordUrl || onForgotPasswordClick ? /* @__PURE__ */ jsx2(TextLink, { href: forgotPasswordUrl, onClick: onForgotPasswordClick, children: l10n.signIn.forgotPassword }) : void 0
1013
1392
  }
1014
1393
  ) : null,
1015
- /* @__PURE__ */ jsx2(Button, { disabled: submitting || !email || usePassword && !password, children: submitLabel }),
1394
+ /* @__PURE__ */ jsx2(Button, { loading: submitting, disabled: !email || usePassword && !password, children: submitLabel }),
1016
1395
  hasCredentials && hasMagicLink ? /* @__PURE__ */ jsx2(
1017
1396
  MethodSwitch,
1018
1397
  {
@@ -1041,7 +1420,7 @@ function LoginForm({
1041
1420
  }
1042
1421
 
1043
1422
  // src/ui/RegisterForm.tsx
1044
- import { useState as useState3 } from "react";
1423
+ import { useState as useState4 } from "react";
1045
1424
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1046
1425
  function RegisterForm({
1047
1426
  onSuccess,
@@ -1062,17 +1441,18 @@ function RegisterForm({
1062
1441
  const client = useClient();
1063
1442
  const { register, startPasswordless } = useAuth();
1064
1443
  const { config, isLoading: configLoading, error: configError } = useAuthConfig();
1444
+ const policy = resolvePolicy(config);
1065
1445
  const invitationToken = useUrlToken("token", invitationTokenProp) || void 0;
1066
1446
  const l10n = useLocalization();
1067
1447
  const showError = useErrorResolver();
1068
- const [email, setEmail] = useState3("");
1069
- const [password, setPassword] = useState3("");
1070
- const [name, setName] = useState3("");
1071
- const [invitationCode, setInvitationCode] = useState3("");
1072
- const [submitting, setSubmitting] = useState3(false);
1073
- const [error, setError] = useState3(null);
1074
- const [linkSentTo, setLinkSentTo] = useState3(null);
1075
- const [passwordMode, setPasswordMode] = useState3(false);
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);
1076
1456
  const shell = (children) => /* @__PURE__ */ jsx3(Shell, { appearance, className, children });
1077
1457
  const appName = config?.app_name;
1078
1458
  const heading = title ?? (appName ? fill(l10n.signUp.titleWithApp, { app: appName }) : l10n.signUp.title);
@@ -1216,13 +1596,15 @@ function RegisterForm({
1216
1596
  autoComplete: "new-password",
1217
1597
  placeholder: l10n.common.passwordPlaceholder,
1218
1598
  disabled: submitting,
1219
- required: true
1599
+ required: true,
1600
+ children: /* @__PURE__ */ jsx3(PasswordStrength, { password, policy })
1220
1601
  }
1221
1602
  ) : null,
1222
1603
  /* @__PURE__ */ jsx3(
1223
1604
  Button,
1224
1605
  {
1225
- disabled: submitting || !email || needsCode && !invitationCode || usePassword && !password,
1606
+ loading: submitting,
1607
+ disabled: !email || needsCode && !invitationCode || usePassword && !meetsPolicy(password, policy),
1226
1608
  children: submitLabel
1227
1609
  }
1228
1610
  ),
@@ -1254,7 +1636,7 @@ function RegisterForm({
1254
1636
  }
1255
1637
 
1256
1638
  // src/ui/ForgotPasswordForm.tsx
1257
- import { useState as useState4 } from "react";
1639
+ import { useState as useState5 } from "react";
1258
1640
  import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1259
1641
  function ForgotPasswordForm({
1260
1642
  callbackUrl,
@@ -1271,10 +1653,10 @@ function ForgotPasswordForm({
1271
1653
  const { config } = useAuthConfig();
1272
1654
  const l10n = useLocalization();
1273
1655
  const showError = useErrorResolver();
1274
- const [email, setEmail] = useState4("");
1275
- const [submitting, setSubmitting] = useState4(false);
1276
- const [error, setError] = useState4(null);
1277
- const [sentTo, setSentTo] = useState4(null);
1656
+ const [email, setEmail] = useState5("");
1657
+ const [submitting, setSubmitting] = useState5(false);
1658
+ const [error, setError] = useState5(null);
1659
+ const [sentTo, setSentTo] = useState5(null);
1278
1660
  const handleSubmit = async (e) => {
1279
1661
  e.preventDefault();
1280
1662
  setError(null);
@@ -1321,7 +1703,7 @@ function ForgotPasswordForm({
1321
1703
  autoFocus: true
1322
1704
  }
1323
1705
  ),
1324
- /* @__PURE__ */ jsx4(Button, { disabled: submitting || !email, children: submitting ? l10n.resetPassword.requestSubmitting : l10n.resetPassword.requestSubmit }),
1706
+ /* @__PURE__ */ jsx4(Button, { loading: submitting, disabled: !email, children: submitting ? l10n.resetPassword.requestSubmitting : l10n.resetPassword.requestSubmit }),
1325
1707
  backLine ? /* @__PURE__ */ jsx4("p", { className: "kerne-footer", children: backLine }) : null
1326
1708
  ] })
1327
1709
  ] }),
@@ -1330,7 +1712,7 @@ function ForgotPasswordForm({
1330
1712
  }
1331
1713
 
1332
1714
  // src/ui/ResetPasswordForm.tsx
1333
- import { useState as useState5 } from "react";
1715
+ import { useState as useState6 } from "react";
1334
1716
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1335
1717
  function ResetPasswordForm({
1336
1718
  token: tokenProp,
@@ -1345,14 +1727,15 @@ function ResetPasswordForm({
1345
1727
  }) {
1346
1728
  const { confirmPasswordReset } = useAuth();
1347
1729
  const { config } = useAuthConfig();
1730
+ const policy = resolvePolicy(config);
1348
1731
  const token = useUrlToken("token", tokenProp);
1349
1732
  const l10n = useLocalization();
1350
1733
  const showError = useErrorResolver();
1351
- const [password, setPassword] = useState5("");
1352
- const [confirm, setConfirm] = useState5("");
1353
- const [submitting, setSubmitting] = useState5(false);
1354
- const [error, setError] = useState5(null);
1355
- const [done, setDone] = useState5(false);
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);
1356
1739
  const heading = title ?? l10n.resetPassword.title;
1357
1740
  const mismatch = confirm.length > 0 && password !== confirm;
1358
1741
  const signInAction = signInUrl || onSignInClick ? /* @__PURE__ */ jsx5(
@@ -1416,7 +1799,8 @@ function ResetPasswordForm({
1416
1799
  placeholder: l10n.common.passwordPlaceholder,
1417
1800
  disabled: submitting,
1418
1801
  required: true,
1419
- autoFocus: true
1802
+ autoFocus: true,
1803
+ children: /* @__PURE__ */ jsx5(PasswordStrength, { password, policy })
1420
1804
  }
1421
1805
  ),
1422
1806
  /* @__PURE__ */ jsx5(
@@ -1430,11 +1814,17 @@ function ResetPasswordForm({
1430
1814
  placeholder: l10n.common.passwordPlaceholder,
1431
1815
  disabled: submitting,
1432
1816
  required: true,
1433
- invalid: mismatch
1817
+ error: mismatch ? l10n.resetPassword.mismatch : void 0
1818
+ }
1819
+ ),
1820
+ /* @__PURE__ */ jsx5(
1821
+ Button,
1822
+ {
1823
+ loading: submitting,
1824
+ disabled: !meetsPolicy(password, policy) || !confirm || mismatch,
1825
+ children: submitting ? l10n.resetPassword.submitting : l10n.resetPassword.submit
1434
1826
  }
1435
1827
  ),
1436
- mismatch ? /* @__PURE__ */ jsx5("p", { className: "kerne-footer", children: l10n.resetPassword.mismatch }) : null,
1437
- /* @__PURE__ */ jsx5(Button, { disabled: submitting || !password || !confirm || mismatch, children: submitting ? l10n.resetPassword.submitting : l10n.resetPassword.submit }),
1438
1828
  signInUrl || onSignInClick ? /* @__PURE__ */ jsx5("p", { className: "kerne-footer", children: /* @__PURE__ */ jsx5(TextLink, { href: signInUrl, onClick: onSignInClick, children: l10n.common.backToSignIn }) }) : null
1439
1829
  ] })
1440
1830
  ] }),
@@ -1443,7 +1833,7 @@ function ResetPasswordForm({
1443
1833
  }
1444
1834
 
1445
1835
  // src/ui/MagicLinkCallback.tsx
1446
- import { useEffect as useEffect2, useRef, useState as useState6 } from "react";
1836
+ import { useEffect as useEffect2, useRef, useState as useState7 } from "react";
1447
1837
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1448
1838
  function MagicLinkCallback({
1449
1839
  token: tokenProp,
@@ -1461,8 +1851,8 @@ function MagicLinkCallback({
1461
1851
  const token = useUrlToken("token", tokenProp);
1462
1852
  const l10n = useLocalization();
1463
1853
  const showError = useErrorResolver();
1464
- const [state, setState] = useState6("pending");
1465
- const [message, setMessage] = useState6(null);
1854
+ const [state, setState] = useState7("pending");
1855
+ const [message, setMessage] = useState7(null);
1466
1856
  const consumed = useRef(false);
1467
1857
  useEffect2(() => {
1468
1858
  if (token === null || consumed.current) return;
@@ -1502,7 +1892,7 @@ function MagicLinkCallback({
1502
1892
  }
1503
1893
 
1504
1894
  // src/ui/VerifyEmailForm.tsx
1505
- import { useEffect as useEffect3, useRef as useRef2, useState as useState7 } from "react";
1895
+ import { useEffect as useEffect3, useRef as useRef2, useState as useState8 } from "react";
1506
1896
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1507
1897
  function VerifyEmailForm({
1508
1898
  token: tokenProp,
@@ -1519,12 +1909,12 @@ function VerifyEmailForm({
1519
1909
  const token = useUrlToken("token", tokenProp);
1520
1910
  const l10n = useLocalization();
1521
1911
  const showError = useErrorResolver();
1522
- const [code, setCode] = useState7("");
1523
- const [submitting, setSubmitting] = useState7(false);
1524
- const [error, setError] = useState7(null);
1525
- const [resent, setResent] = useState7(false);
1526
- const [linkState, setLinkState] = useState7("idle");
1527
- const [verified, setVerified] = useState7(false);
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);
1528
1918
  const consumed = useRef2(false);
1529
1919
  const heading = title ?? l10n.verifyEmail.title;
1530
1920
  const settle = async (needsTokenRefresh) => {
@@ -1618,7 +2008,7 @@ function VerifyEmailForm({
1618
2008
  autoFocus: true
1619
2009
  }
1620
2010
  ),
1621
- /* @__PURE__ */ jsx7(Button, { disabled: submitting || !code, children: submitting ? l10n.verifyEmail.submitting : l10n.verifyEmail.submit }),
2011
+ /* @__PURE__ */ jsx7(Button, { loading: submitting, disabled: !code, children: submitting ? l10n.verifyEmail.submitting : l10n.verifyEmail.submit }),
1622
2012
  /* @__PURE__ */ jsx7(MethodSwitch, { onClick: handleResend, children: l10n.verifyEmail.resend })
1623
2013
  ] })
1624
2014
  ] }),
@@ -1627,7 +2017,7 @@ function VerifyEmailForm({
1627
2017
  }
1628
2018
 
1629
2019
  // src/ui/ActivateAccountForm.tsx
1630
- import { useEffect as useEffect4, useState as useState8 } from "react";
2020
+ import { useEffect as useEffect4, useState as useState9 } from "react";
1631
2021
  import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1632
2022
  function ActivateAccountForm({
1633
2023
  token: tokenProp,
@@ -1643,15 +2033,16 @@ function ActivateAccountForm({
1643
2033
  const client = useClient();
1644
2034
  const { getActivationContext, completeActivation } = useAuth();
1645
2035
  const { config } = useAuthConfig();
2036
+ const policy = resolvePolicy(config);
1646
2037
  const token = useUrlToken("token", tokenProp);
1647
2038
  const l10n = useLocalization();
1648
2039
  const showError = useErrorResolver();
1649
- const [context, setContext] = useState8(null);
1650
- const [loadError, setLoadError] = useState8(null);
1651
- const [password, setPassword] = useState8("");
1652
- const [confirm, setConfirm] = useState8("");
1653
- const [submitting, setSubmitting] = useState8(false);
1654
- const [error, setError] = useState8(null);
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);
1655
2046
  const heading = title ?? l10n.activate.title;
1656
2047
  const mismatch = confirm.length > 0 && password !== confirm;
1657
2048
  useEffect4(() => {
@@ -1736,7 +2127,8 @@ function ActivateAccountForm({
1736
2127
  placeholder: l10n.common.passwordPlaceholder,
1737
2128
  disabled: submitting,
1738
2129
  required: true,
1739
- autoFocus: true
2130
+ autoFocus: true,
2131
+ children: /* @__PURE__ */ jsx8(PasswordStrength, { password, policy })
1740
2132
  }
1741
2133
  ),
1742
2134
  /* @__PURE__ */ jsx8(
@@ -1750,12 +2142,18 @@ function ActivateAccountForm({
1750
2142
  placeholder: l10n.common.passwordPlaceholder,
1751
2143
  disabled: submitting,
1752
2144
  required: true,
1753
- invalid: mismatch
2145
+ error: mismatch ? l10n.activate.mismatch : void 0
1754
2146
  }
1755
- ),
1756
- mismatch ? /* @__PURE__ */ jsx8("p", { className: "kerne-footer", children: l10n.activate.mismatch }) : null
2147
+ )
1757
2148
  ] }) : null,
1758
- /* @__PURE__ */ jsx8(Button, { disabled: submitting || credentialsMode && (!password || !confirm || mismatch), children: submitting ? l10n.activate.submitting : l10n.activate.submit })
2149
+ /* @__PURE__ */ jsx8(
2150
+ Button,
2151
+ {
2152
+ loading: submitting,
2153
+ disabled: credentialsMode && (!meetsPolicy(password, policy) || !confirm || mismatch),
2154
+ children: submitting ? l10n.activate.submitting : l10n.activate.submit
2155
+ }
2156
+ )
1759
2157
  ] })
1760
2158
  ] }),
1761
2159
  /* @__PURE__ */ jsx8(KerneBranding, { show: config?.show_kerne_branding ?? false })
@@ -1782,7 +2180,7 @@ function AuthFlow({
1782
2180
  }) {
1783
2181
  const { config } = useAuthConfig();
1784
2182
  const token = useUrlToken("token");
1785
- const [internalStep, setInternalStep] = useState9(null);
2183
+ const [internalStep, setInternalStep] = useState10(null);
1786
2184
  const go = (next) => {
1787
2185
  if (controlledStep === void 0) setInternalStep(next);
1788
2186
  onStepChange?.(next);
@@ -1790,7 +2188,11 @@ function AuthFlow({
1790
2188
  const tokenStep = token && (initialStep === void 0 || TOKEN_STEPS.includes(initialStep)) ? initialStep ?? "magic-link" : null;
1791
2189
  const step = controlledStep ?? internalStep ?? tokenStep ?? initialStep ?? "sign-in";
1792
2190
  const canSignUp = allowSignUp && config?.registration_mode !== "CLOSED";
1793
- const shared = { appearance, logo, className };
2191
+ const shared = {
2192
+ appearance,
2193
+ logo,
2194
+ className: `kerne-step${className ? ` ${className}` : ""}`
2195
+ };
1794
2196
  switch (step) {
1795
2197
  case "sign-up":
1796
2198
  return /* @__PURE__ */ jsx9(
@@ -1855,7 +2257,7 @@ function AuthFlow({
1855
2257
  }
1856
2258
 
1857
2259
  // src/ui/ChangePasswordForm.tsx
1858
- import { useState as useState10 } from "react";
2260
+ import { useState as useState11 } from "react";
1859
2261
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1860
2262
  function ChangePasswordForm({
1861
2263
  onSuccess,
@@ -1864,15 +2266,17 @@ function ChangePasswordForm({
1864
2266
  className
1865
2267
  }) {
1866
2268
  const { updatePassword } = useAuth();
2269
+ const { config } = useAuthConfig();
2270
+ const policy = resolvePolicy(config);
1867
2271
  const l10n = useLocalization();
1868
2272
  const showError = useErrorResolver();
1869
2273
  const heading = title === null ? null : title ?? l10n.security.submit;
1870
- const [currentPassword, setCurrentPassword] = useState10("");
1871
- const [newPassword, setNewPassword] = useState10("");
1872
- const [confirm, setConfirm] = useState10("");
1873
- const [submitting, setSubmitting] = useState10(false);
1874
- const [error, setError] = useState10(null);
1875
- const [done, setDone] = useState10(false);
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);
1876
2280
  const mismatch = confirm.length > 0 && newPassword !== confirm;
1877
2281
  const unchanged = newPassword.length > 0 && newPassword === currentPassword;
1878
2282
  const handleSubmit = async (e) => {
@@ -1924,7 +2328,8 @@ function ChangePasswordForm({
1924
2328
  placeholder: l10n.common.passwordPlaceholder,
1925
2329
  disabled: submitting,
1926
2330
  required: true,
1927
- invalid: unchanged
2331
+ error: unchanged ? l10n.security.sameAsCurrent : void 0,
2332
+ children: /* @__PURE__ */ jsx10(PasswordStrength, { password: newPassword, policy })
1928
2333
  }
1929
2334
  ),
1930
2335
  /* @__PURE__ */ jsx10(
@@ -1938,14 +2343,14 @@ function ChangePasswordForm({
1938
2343
  placeholder: l10n.common.passwordPlaceholder,
1939
2344
  disabled: submitting,
1940
2345
  required: true,
1941
- invalid: mismatch
2346
+ error: mismatch ? l10n.security.mismatch : void 0
1942
2347
  }
1943
2348
  ),
1944
- unchanged ? /* @__PURE__ */ jsx10("p", { className: "kerne-footer", children: l10n.security.sameAsCurrent }) : mismatch ? /* @__PURE__ */ jsx10("p", { className: "kerne-footer", children: l10n.security.mismatch }) : null,
1945
2349
  /* @__PURE__ */ jsx10(
1946
2350
  Button,
1947
2351
  {
1948
- disabled: submitting || !currentPassword || !newPassword || !confirm || mismatch || unchanged,
2352
+ loading: submitting,
2353
+ disabled: !currentPassword || !meetsPolicy(newPassword, policy) || !confirm || mismatch || unchanged,
1949
2354
  children: submitting ? l10n.security.submitting : l10n.security.submit
1950
2355
  }
1951
2356
  )
@@ -1955,7 +2360,7 @@ function ChangePasswordForm({
1955
2360
  }
1956
2361
 
1957
2362
  // src/ui/WaitlistForm.tsx
1958
- import { useState as useState11 } from "react";
2363
+ import { useState as useState12 } from "react";
1959
2364
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1960
2365
  function WaitlistForm({
1961
2366
  title,
@@ -1974,11 +2379,11 @@ function WaitlistForm({
1974
2379
  const { config } = useAuthConfig();
1975
2380
  const l10n = useLocalization();
1976
2381
  const showError = useErrorResolver();
1977
- const [email, setEmail] = useState11("");
1978
- const [name, setName] = useState11("");
1979
- const [submitting, setSubmitting] = useState11(false);
1980
- const [error, setError] = useState11(null);
1981
- const [joined, setJoined] = useState11(false);
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);
1982
2387
  const handleSubmit = async (e) => {
1983
2388
  e.preventDefault();
1984
2389
  setError(null);
@@ -2035,7 +2440,7 @@ function WaitlistForm({
2035
2440
  autoFocus: true
2036
2441
  }
2037
2442
  ),
2038
- /* @__PURE__ */ jsx11(Button, { disabled: submitting || !email, children: submitting ? l10n.waitlist.submitting : submitLabel ?? l10n.waitlist.submit })
2443
+ /* @__PURE__ */ jsx11(Button, { loading: submitting, disabled: !email, children: submitting ? l10n.waitlist.submitting : submitLabel ?? l10n.waitlist.submit })
2039
2444
  ] })
2040
2445
  ] }),
2041
2446
  legal ? /* @__PURE__ */ jsx11("p", { className: "kerne-legal", children: legal }) : null,
@@ -2044,7 +2449,7 @@ function WaitlistForm({
2044
2449
  }
2045
2450
 
2046
2451
  // src/ui/UserButton.tsx
2047
- import { useEffect as useEffect5, useRef as useRef3, useState as useState12 } from "react";
2452
+ import { useEffect as useEffect5, useRef as useRef3, useState as useState13 } from "react";
2048
2453
  import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2049
2454
  function SignOutButton({
2050
2455
  children,
@@ -2092,7 +2497,7 @@ function UserButton({
2092
2497
  const { logout, isAuthenticated } = useAuth();
2093
2498
  const { user, fullName, initials, email } = useUser();
2094
2499
  const l10n = useLocalization();
2095
- const [open, setOpen] = useState12(false);
2500
+ const [open, setOpen] = useState13(false);
2096
2501
  const wrapRef = useRef3(null);
2097
2502
  useEffect5(() => {
2098
2503
  if (!open) return;
@@ -2176,24 +2581,24 @@ function UserButton({
2176
2581
  }
2177
2582
 
2178
2583
  // src/ui/UserProfile.tsx
2179
- import { useState as useState17 } from "react";
2584
+ import { useState as useState18 } from "react";
2180
2585
 
2181
2586
  // src/ui/profile/ProfileSection.tsx
2182
- import { useEffect as useEffect6, useState as useState13 } from "react";
2587
+ import { useEffect as useEffect6, useState as useState14 } from "react";
2183
2588
  import { Fragment as Fragment6, jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
2184
2589
  function ProfileSection({ verificationCallbackUrl, onSaved }) {
2185
2590
  const { updateProfile, sendVerificationEmail, getVerificationStatus } = useAuth();
2186
2591
  const { user, fullName, initials, email } = useUser();
2187
2592
  const l10n = useLocalization();
2188
2593
  const showError = useErrorResolver();
2189
- const [firstName, setFirstName] = useState13("");
2190
- const [lastName, setLastName] = useState13("");
2191
- const [saving, setSaving] = useState13(false);
2192
- const [saved, setSaved] = useState13(false);
2193
- const [error, setError] = useState13(null);
2194
- const [verified, setVerified] = useState13(null);
2195
- const [sending, setSending] = useState13(false);
2196
- const [sent, setSent] = useState13(false);
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);
2197
2602
  useEffect6(() => {
2198
2603
  if (!user) return;
2199
2604
  setFirstName(user.first_name ?? "");
@@ -2269,7 +2674,7 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
2269
2674
  disabled: saving
2270
2675
  }
2271
2676
  ),
2272
- /* @__PURE__ */ jsx13(Button, { disabled: saving, children: saving ? l10n.profile.saving : l10n.profile.save })
2677
+ /* @__PURE__ */ jsx13(Button, { loading: saving, children: saving ? l10n.profile.saving : l10n.profile.save })
2273
2678
  ] })
2274
2679
  ] }),
2275
2680
  /* @__PURE__ */ jsxs12(
@@ -2283,7 +2688,7 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
2283
2688
  email,
2284
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
2285
2690
  ] }) }),
2286
- verified === false ? /* @__PURE__ */ jsx13(Button, { variant: "outline", type: "button", onClick: handleResend, disabled: sending, children: sending ? l10n.common.sending : l10n.profile.verify }) : null
2691
+ verified === false ? /* @__PURE__ */ jsx13(Button, { variant: "outline", type: "button", onClick: handleResend, loading: sending, children: sending ? l10n.common.sending : l10n.profile.verify }) : null
2287
2692
  ] }),
2288
2693
  sent ? /* @__PURE__ */ jsx13(Panel, { variant: "success", children: l10n.profile.verificationSent }) : null
2289
2694
  ]
@@ -2293,16 +2698,16 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
2293
2698
  }
2294
2699
 
2295
2700
  // src/ui/profile/SecuritySection.tsx
2296
- import { useState as useState14 } from "react";
2701
+ import { useState as useState15 } from "react";
2297
2702
  import { Fragment as Fragment7, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2298
2703
  function SecuritySection({ showDeleteAccount = true, onDeleted }) {
2299
2704
  const { requestAccountDeletion } = useAuth();
2300
2705
  const { config } = useAuthConfig();
2301
2706
  const l10n = useLocalization();
2302
2707
  const showError = useErrorResolver();
2303
- const [error, setError] = useState14(null);
2304
- const [confirmingDelete, setConfirmingDelete] = useState14(false);
2305
- const [deleting, setDeleting] = useState14(false);
2708
+ const [error, setError] = useState15(null);
2709
+ const [confirmingDelete, setConfirmingDelete] = useState15(false);
2710
+ const [deleting, setDeleting] = useState15(false);
2306
2711
  const hasPassword = config?.enable_credentials ?? true;
2307
2712
  const handleDelete = async () => {
2308
2713
  setError(null);
@@ -2319,7 +2724,7 @@ function SecuritySection({ showDeleteAccount = true, onDeleted }) {
2319
2724
  error ? /* @__PURE__ */ jsx14(Panel, { variant: "danger", children: error }) : null,
2320
2725
  hasPassword ? /* @__PURE__ */ jsx14(Subsection, { title: l10n.security.passwordTitle, description: l10n.security.passwordDesc, children: /* @__PURE__ */ jsx14(ChangePasswordForm, { title: null }) }) : null,
2321
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: [
2322
- /* @__PURE__ */ jsx14(Button, { variant: "danger", type: "button", onClick: handleDelete, disabled: deleting, children: deleting ? l10n.security.deleting : l10n.security.deleteConfirm }),
2727
+ /* @__PURE__ */ jsx14(Button, { variant: "danger", type: "button", onClick: handleDelete, loading: deleting, children: deleting ? l10n.security.deleting : l10n.security.deleteConfirm }),
2323
2728
  /* @__PURE__ */ jsx14(
2324
2729
  Button,
2325
2730
  {
@@ -2335,7 +2740,7 @@ function SecuritySection({ showDeleteAccount = true, onDeleted }) {
2335
2740
  }
2336
2741
 
2337
2742
  // src/ui/profile/BillingSection.tsx
2338
- import { useMemo, useState as useState16 } from "react";
2743
+ import { useMemo, useState as useState17 } from "react";
2339
2744
 
2340
2745
  // src/ui/format.ts
2341
2746
  function humanizeFeatureKey(key) {
@@ -2394,15 +2799,45 @@ function formatInterval(interval, labels) {
2394
2799
  return interval;
2395
2800
  }
2396
2801
  }
2802
+ var ENCODED_INTERVAL_UNIT = { D: 12 / 365, M: 1, Y: 12 };
2803
+ function approxMonths(value) {
2804
+ const match = ENCODED_INTERVAL.exec(value);
2805
+ if (!match) return 0;
2806
+ return parseInt(match[1], 10) * ENCODED_INTERVAL_UNIT[match[2]];
2807
+ }
2808
+ function resolvePriceDisplay(price, displayInterval) {
2809
+ if (!displayInterval || displayInterval === price.interval) {
2810
+ return {
2811
+ billedAmount: price.amount,
2812
+ billedInterval: price.interval,
2813
+ displayAmount: price.amount,
2814
+ displayInterval: price.interval
2815
+ };
2816
+ }
2817
+ const ratio = approxMonths(displayInterval) / approxMonths(price.interval);
2818
+ return {
2819
+ billedAmount: price.amount,
2820
+ billedInterval: price.interval,
2821
+ displayAmount: Math.round(price.amount * ratio),
2822
+ displayInterval
2823
+ };
2824
+ }
2397
2825
 
2398
2826
  // src/ui/UsageMeters.tsx
2399
- import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
2400
- function levelFor(ratio, overageAllowed) {
2401
- if (overageAllowed) return ratio >= 1 ? "warning" : "normal";
2827
+ import { Fragment as Fragment8, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
2828
+ function isSoftOverage(overage) {
2829
+ return overage === "ALLOW" || overage === "BILL";
2830
+ }
2831
+ function levelFor(ratio, softOverage) {
2832
+ if (softOverage) return ratio >= 1 ? "warning" : "normal";
2402
2833
  if (ratio >= 0.9) return "critical";
2403
2834
  if (ratio >= 0.75) return "warning";
2404
2835
  return "normal";
2405
2836
  }
2837
+ function quotaRatio(used, limit) {
2838
+ if (limit <= 0) return used > 0 ? 1 : 0;
2839
+ return Math.min(used / limit, 1);
2840
+ }
2406
2841
  function UsageMeters({
2407
2842
  title,
2408
2843
  featureKeys,
@@ -2456,10 +2891,13 @@ function UsageMeters({
2456
2891
  return shell(
2457
2892
  /* @__PURE__ */ jsx15("div", { className: "kerne-meters", children: rows.map((e) => {
2458
2893
  const unlimited = e.limit === null || e.limit === void 0;
2459
- const overageAllowed = e.overage_behavior === "ALLOW";
2460
- const ratio = unlimited ? 0 : Math.min(e.used / e.limit, 1);
2461
- const level = unlimited ? "normal" : levelFor(e.used / e.limit, overageAllowed);
2894
+ const softOverage = isSoftOverage(e.overage_behavior);
2895
+ const billed = e.overage_behavior === "BILL";
2896
+ const ratio = unlimited ? 0 : quotaRatio(e.used, e.limit);
2897
+ const level = unlimited ? "normal" : levelFor(ratio, softOverage);
2462
2898
  const over = !unlimited && e.used > e.limit;
2899
+ const standing = billed ? subscription?.entitlements.find((se) => se.feature_key === e.feature_key) : void 0;
2900
+ const capReached = billed && (standing?.cap_reached ?? false);
2463
2901
  return /* @__PURE__ */ jsxs14("div", { className: "kerne-meter", "data-level": level, children: [
2464
2902
  /* @__PURE__ */ jsxs14("div", { className: "kerne-meter-head", children: [
2465
2903
  /* @__PURE__ */ jsx15("span", { className: "kerne-meter-name", children: nameFor(e.feature_key) }),
@@ -2477,17 +2915,26 @@ function UsageMeters({
2477
2915
  children: /* @__PURE__ */ jsx15("div", { className: "kerne-meter-fill", style: { width: `${ratio * 100}%` } })
2478
2916
  }
2479
2917
  ) : null,
2480
- over && overageAllowed ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: fill(l10n.usage.overage, {
2481
- count: formatNumber(e.used - e.limit)
2482
- }) }) : over ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: l10n.usage.limitReached }) : null
2918
+ over && softOverage ? /* @__PURE__ */ jsxs14(Fragment8, { children: [
2919
+ /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: fill(
2920
+ // capReached inverts overageBilled's meaning (billing stopped,
2921
+ // access didn't) - a variant string would say the opposite of
2922
+ // the truth for the one plan that just stopped costing money.
2923
+ capReached ? l10n.usage.capReached : billed ? l10n.usage.overageBilled : l10n.usage.overage,
2924
+ { count: formatNumber(e.used - e.limit) }
2925
+ ) }),
2926
+ billed && standing?.billed_amount != null && standing.currency ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note kerne-meter-cost", children: fill(l10n.usage.billedAmount, {
2927
+ amount: formatMoney(standing.billed_amount, standing.currency)
2928
+ }) }) : null
2929
+ ] }) : over ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: l10n.usage.limitReached }) : null
2483
2930
  ] }, e.feature_key);
2484
2931
  }) })
2485
2932
  );
2486
2933
  }
2487
2934
 
2488
2935
  // src/ui/SubscriptionCard.tsx
2489
- import { useState as useState15 } from "react";
2490
- import { Fragment as Fragment8, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
2936
+ import { useState as useState16 } from "react";
2937
+ import { Fragment as Fragment9, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
2491
2938
  var STATUS_TONES = {
2492
2939
  ACTIVE: "success",
2493
2940
  TRIALING: "success",
@@ -2503,11 +2950,13 @@ function SubscriptionCard({
2503
2950
  upgradeUrl,
2504
2951
  onUpgradeClick,
2505
2952
  hidePortalButton = false,
2953
+ hideCancelButton = false,
2506
2954
  appearance,
2507
2955
  className
2508
2956
  }) {
2509
- const { subscription, isLoading, error } = useSubscription(productSlug);
2957
+ const { subscription, isLoading, error, refetch } = useSubscription(productSlug);
2510
2958
  const { openPortal } = usePortal();
2959
+ const { cancelSubscription, isCanceling } = useCancelSubscription();
2511
2960
  const l10n = useLocalization();
2512
2961
  const showError = useErrorResolver();
2513
2962
  const heading = title === null ? null : title ?? l10n.billing.plan;
@@ -2519,8 +2968,10 @@ function SubscriptionCard({
2519
2968
  UNPAID: l10n.billing.statusUnpaid,
2520
2969
  PAUSED: l10n.billing.statusPaused
2521
2970
  };
2522
- const [opening, setOpening] = useState15(false);
2523
- const [portalError, setPortalError] = useState15(null);
2971
+ const [opening, setOpening] = useState16(false);
2972
+ const [portalError, setPortalError] = useState16(null);
2973
+ const [confirmingCancel, setConfirmingCancel] = useState16(false);
2974
+ const [cancelError, setCancelError] = useState16(null);
2524
2975
  const handlePortal = async () => {
2525
2976
  setPortalError(null);
2526
2977
  setOpening(true);
@@ -2531,6 +2982,17 @@ function SubscriptionCard({
2531
2982
  setOpening(false);
2532
2983
  }
2533
2984
  };
2985
+ const handleCancel = async () => {
2986
+ if (!subscription) return;
2987
+ setCancelError(null);
2988
+ try {
2989
+ await cancelSubscription(subscription.id, false);
2990
+ setConfirmingCancel(false);
2991
+ refetch();
2992
+ } catch (e) {
2993
+ setCancelError(showError(e));
2994
+ }
2995
+ };
2534
2996
  const shell = (children) => /* @__PURE__ */ jsx16(Shell, { appearance, className, children: /* @__PURE__ */ jsxs15("div", { className: "kerne-stack", children: [
2535
2997
  heading ? /* @__PURE__ */ jsx16("h2", { className: "kerne-section-title", children: heading }) : null,
2536
2998
  children
@@ -2548,7 +3010,7 @@ function SubscriptionCard({
2548
3010
  }
2549
3011
  if (!subscription) {
2550
3012
  return shell(
2551
- /* @__PURE__ */ jsxs15(Fragment8, { children: [
3013
+ /* @__PURE__ */ jsxs15(Fragment9, { children: [
2552
3014
  /* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.freePlan }),
2553
3015
  upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx16(
2554
3016
  Button,
@@ -2567,7 +3029,7 @@ function SubscriptionCard({
2567
3029
  const renews = formatDate(current_period_end);
2568
3030
  const trialEnds = formatDate(trial_end);
2569
3031
  return shell(
2570
- /* @__PURE__ */ jsxs15(Fragment8, { children: [
3032
+ /* @__PURE__ */ jsxs15(Fragment9, { children: [
2571
3033
  portalError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: portalError }) : null,
2572
3034
  /* @__PURE__ */ jsxs15("dl", { className: "kerne-rows", children: [
2573
3035
  /* @__PURE__ */ jsxs15("div", { className: "kerne-row", children: [
@@ -2596,13 +3058,33 @@ function SubscriptionCard({
2596
3058
  ] }) : null
2597
3059
  ] }),
2598
3060
  canceled_at ? /* @__PURE__ */ jsx16(Panel, { children: l10n.billing.cancelNotice }) : null,
2599
- !hidePortalButton ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: handlePortal, disabled: opening, children: opening ? l10n.billing.opening : l10n.billing.managePortal }) : null
3061
+ cancelError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: cancelError }) : null,
3062
+ confirmingCancel ? /* @__PURE__ */ jsxs15(Panel, { children: [
3063
+ /* @__PURE__ */ jsx16("p", { children: l10n.billing.cancelConfirmTitle }),
3064
+ /* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.cancelConfirmBody }),
3065
+ /* @__PURE__ */ jsxs15("div", { className: "kerne-inline-actions", children: [
3066
+ /* @__PURE__ */ jsx16(
3067
+ Button,
3068
+ {
3069
+ variant: "outline",
3070
+ type: "button",
3071
+ onClick: () => setConfirmingCancel(false),
3072
+ disabled: isCanceling,
3073
+ children: l10n.billing.cancelConfirmDismiss
3074
+ }
3075
+ ),
3076
+ /* @__PURE__ */ jsx16(Button, { type: "button", onClick: handleCancel, loading: isCanceling, children: isCanceling ? l10n.billing.canceling : l10n.billing.cancelConfirmButton })
3077
+ ] })
3078
+ ] }) : /* @__PURE__ */ jsxs15("div", { className: "kerne-inline-actions", children: [
3079
+ !hidePortalButton ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: handlePortal, loading: opening, children: opening ? l10n.billing.opening : l10n.billing.managePortal }) : null,
3080
+ !hideCancelButton && !canceled_at && status !== "CANCELED" ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: () => setConfirmingCancel(true), children: l10n.billing.cancelSubscription }) : null
3081
+ ] })
2600
3082
  ] })
2601
3083
  );
2602
3084
  }
2603
3085
 
2604
3086
  // src/ui/profile/BillingSection.tsx
2605
- import { Fragment as Fragment9, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
3087
+ import { Fragment as Fragment10, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
2606
3088
  function intervalsOf(plans) {
2607
3089
  const seen = [];
2608
3090
  for (const plan of plans) {
@@ -2628,10 +3110,10 @@ function BillingSection({
2628
3110
  const { openCheckout } = useCheckout();
2629
3111
  const l10n = useLocalization();
2630
3112
  const showError = useErrorResolver();
2631
- const [busy, setBusy] = useState16(null);
2632
- const [error, setError] = useState16(null);
3113
+ const [busy, setBusy] = useState17(null);
3114
+ const [error, setError] = useState17(null);
2633
3115
  const intervals = useMemo(() => intervalsOf(plans), [plans]);
2634
- const [interval, setInterval] = useState16(null);
3116
+ const [interval, setInterval] = useState17(null);
2635
3117
  const activeInterval = interval ?? subscription?.plan_price.interval ?? intervals[0] ?? null;
2636
3118
  const handleSwitch = async (planPriceId) => {
2637
3119
  setError(null);
@@ -2646,7 +3128,7 @@ function BillingSection({
2646
3128
  setBusy(null);
2647
3129
  }
2648
3130
  };
2649
- return /* @__PURE__ */ jsxs16(Fragment9, { children: [
3131
+ return /* @__PURE__ */ jsxs16(Fragment10, { children: [
2650
3132
  error ? /* @__PURE__ */ jsx17(Panel, { variant: "danger", children: error }) : null,
2651
3133
  /* @__PURE__ */ jsx17(Subsection, { title: l10n.billing.currentPlan, children: /* @__PURE__ */ jsx17(
2652
3134
  SubscriptionCard,
@@ -2664,7 +3146,7 @@ function BillingSection({
2664
3146
  style: { height: 64 },
2665
3147
  "aria-label": l10n.billing.loadingPlans
2666
3148
  }
2667
- ) : plans.length === 0 ? /* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.noPlans }) : /* @__PURE__ */ jsxs16(Fragment9, { children: [
3149
+ ) : plans.length === 0 ? /* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.noPlans }) : /* @__PURE__ */ jsxs16(Fragment10, { children: [
2668
3150
  intervals.length > 1 ? /* @__PURE__ */ jsx17(
2669
3151
  "div",
2670
3152
  {
@@ -2706,6 +3188,7 @@ function BillingSection({
2706
3188
  variant: "outline",
2707
3189
  type: "button",
2708
3190
  onClick: () => handleSwitch(price.id),
3191
+ loading: busy === price.id,
2709
3192
  disabled: busy !== null,
2710
3193
  children: busy === price.id ? l10n.billing.opening : l10n.billing.switchTo
2711
3194
  }
@@ -2720,7 +3203,7 @@ function BillingSection({
2720
3203
  }
2721
3204
 
2722
3205
  // src/ui/UserProfile.tsx
2723
- import { Fragment as Fragment10, jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
3206
+ import { Fragment as Fragment11, jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
2724
3207
  function UserProfile({
2725
3208
  tabs = ["profile", "security", "billing"],
2726
3209
  extraTabs = [],
@@ -2770,7 +3253,7 @@ function UserProfile({
2770
3253
  description: t.description
2771
3254
  }))
2772
3255
  ];
2773
- const [internalTab, setInternalTab] = useState17(defaultTab ?? allTabs[0]?.id ?? "profile");
3256
+ const [internalTab, setInternalTab] = useState18(defaultTab ?? allTabs[0]?.id ?? "profile");
2774
3257
  const current = controlledTab ?? internalTab;
2775
3258
  const select = (id) => {
2776
3259
  if (controlledTab === void 0) setInternalTab(id);
@@ -2815,7 +3298,7 @@ function UserProfile({
2815
3298
  tab.id
2816
3299
  )) }),
2817
3300
  /* @__PURE__ */ jsxs17("div", { className: "kerne-pane", children: [
2818
- active ? /* @__PURE__ */ jsxs17(Fragment10, { children: [
3301
+ active ? /* @__PURE__ */ jsxs17(Fragment11, { children: [
2819
3302
  /* @__PURE__ */ jsx18("h2", { className: "kerne-pane-title", children: active.title }),
2820
3303
  active.description ? /* @__PURE__ */ jsx18("p", { className: "kerne-pane-desc", children: active.description }) : null
2821
3304
  ] }) : null,
@@ -2824,9 +3307,317 @@ function UserProfile({
2824
3307
  ] }) });
2825
3308
  }
2826
3309
 
3310
+ // src/ui/PricingTable.tsx
3311
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect7, useMemo as useMemo2, useRef as useRef4, useState as useState19 } from "react";
3312
+ import { Fragment as Fragment12, jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
3313
+ function formatEntitlement(e, unlimitedLabel) {
3314
+ if (e.feature_type === "BOOLEAN") return e.feature_name;
3315
+ if (e.feature_type === "STATIC")
3316
+ return e.value ? `${e.feature_name}: ${e.value}` : e.feature_name;
3317
+ if (e.limit === null) return e.feature_name;
3318
+ if (e.limit === -1) return `${unlimitedLabel} ${e.feature_name.toLowerCase()}`;
3319
+ return `${formatNumber(e.limit)} ${e.feature_name.toLowerCase()}`;
3320
+ }
3321
+ function collectLabels(plans) {
3322
+ const bySlug = /* @__PURE__ */ new Map();
3323
+ for (const plan of plans) {
3324
+ for (const price of plan.prices) {
3325
+ if (!bySlug.has(price.label.slug)) bySlug.set(price.label.slug, price.label);
3326
+ }
3327
+ }
3328
+ return Array.from(bySlug.values()).sort(
3329
+ (a, b) => a.sort_order - b.sort_order || a.name.localeCompare(b.name)
3330
+ );
3331
+ }
3332
+ var ENCODED_INTERVAL_MONTHS = { D: 12 / 365, M: 1, Y: 12 };
3333
+ function approxMonths2(value) {
3334
+ const match = /^([1-9]\d*)([DMY])$/.exec(value);
3335
+ if (!match) return 0;
3336
+ return parseInt(match[1], 10) * ENCODED_INTERVAL_MONTHS[match[2]];
3337
+ }
3338
+ function labelSavings(plans, labels) {
3339
+ if (labels.length < 2) return null;
3340
+ const priceFor = (labelSlug) => {
3341
+ for (const plan of plans) {
3342
+ const price = plan.prices.find((p) => p.label.slug === labelSlug);
3343
+ if (price) return price;
3344
+ }
3345
+ return void 0;
3346
+ };
3347
+ const sorted = [...labels].sort(
3348
+ (a, b) => approxMonths2(priceFor(a.slug)?.interval ?? "1M") - approxMonths2(priceFor(b.slug)?.interval ?? "1M")
3349
+ );
3350
+ const shortest = sorted[0];
3351
+ const longest = sorted[sorted.length - 1];
3352
+ if (shortest.slug === longest.slug) return null;
3353
+ for (const plan of plans) {
3354
+ const shortPrice = plan.prices.find((p) => p.label.slug === shortest.slug);
3355
+ const longPrice = plan.prices.find((p) => p.label.slug === longest.slug);
3356
+ if (!shortPrice?.amount || !longPrice) continue;
3357
+ const shortMonths = approxMonths2(shortPrice.interval);
3358
+ const longMonths = approxMonths2(longPrice.interval);
3359
+ if (!shortMonths || !longMonths) continue;
3360
+ const savings = 1 - longPrice.amount / longMonths / (shortPrice.amount / shortMonths);
3361
+ if (savings > 0.01) return { labelSlug: longest.slug, percent: Math.round(savings * 100) };
3362
+ }
3363
+ return null;
3364
+ }
3365
+ var PricingTableContext = createContext2(null);
3366
+ function usePricingTable() {
3367
+ const ctx = useContext2(PricingTableContext);
3368
+ if (!ctx) {
3369
+ throw new Error("usePricingTable() must be called inside <PricingTable>.");
3370
+ }
3371
+ return ctx;
3372
+ }
3373
+ function PricingTable({
3374
+ product,
3375
+ defaultLabel,
3376
+ checkoutSuccessUrl,
3377
+ checkoutCancelUrl,
3378
+ onSelectPlan,
3379
+ appearance,
3380
+ className,
3381
+ children
3382
+ }) {
3383
+ const { plans, isLoading, error } = usePlans(product);
3384
+ const { subscription } = useSubscription(product);
3385
+ const { openCheckout } = useCheckout();
3386
+ const l10n = useLocalization();
3387
+ const showError = useErrorResolver();
3388
+ const [selectedLabelSlug, setSelectedLabelSlug] = useState19(defaultLabel ?? null);
3389
+ const [busyPriceId, setBusyPriceId] = useState19(null);
3390
+ const [checkoutError, setCheckoutError] = useState19(null);
3391
+ const labels = useMemo2(() => collectLabels(plans), [plans]);
3392
+ const savings = useMemo2(() => labelSavings(plans, labels), [plans, labels]);
3393
+ useEffect7(() => {
3394
+ if (labels.length === 0) return;
3395
+ if (selectedLabelSlug && labels.some((l) => l.slug === selectedLabelSlug)) return;
3396
+ setSelectedLabelSlug(labels[0].slug);
3397
+ }, [labels]);
3398
+ const handleSelectPlan = async (plan) => {
3399
+ const price = plan.prices.find((p) => p.label.slug === selectedLabelSlug) ?? plan.prices[0];
3400
+ if (!price) return;
3401
+ if (onSelectPlan) {
3402
+ onSelectPlan(plan, price);
3403
+ return;
3404
+ }
3405
+ setCheckoutError(null);
3406
+ setBusyPriceId(price.id);
3407
+ try {
3408
+ await openCheckout(price.id, {
3409
+ successUrl: checkoutSuccessUrl,
3410
+ cancelUrl: checkoutCancelUrl
3411
+ });
3412
+ } catch (e) {
3413
+ setCheckoutError(showError(e));
3414
+ setBusyPriceId(null);
3415
+ }
3416
+ };
3417
+ const ctx = {
3418
+ plans,
3419
+ labels,
3420
+ selectedLabelSlug,
3421
+ setSelectedLabelSlug,
3422
+ savings,
3423
+ currentPlanId: subscription?.plan?.id ?? null,
3424
+ busyPriceId,
3425
+ onSelectPlan: handleSelectPlan
3426
+ };
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: checkoutError }) : null,
3429
+ children
3430
+ ] }) });
3431
+ }
3432
+ function LabelSwitch() {
3433
+ const { labels, selectedLabelSlug, setSelectedLabelSlug, savings } = usePricingTable();
3434
+ const l10n = useLocalization();
3435
+ const containerRef = useRef4(null);
3436
+ const buttonRefs = useRef4(/* @__PURE__ */ new Map());
3437
+ const [thumb, setThumb] = useState19(null);
3438
+ useEffect7(() => {
3439
+ const measure = () => {
3440
+ const container = containerRef.current;
3441
+ const button = selectedLabelSlug ? buttonRefs.current.get(selectedLabelSlug) : null;
3442
+ if (!container || !button) return;
3443
+ const containerBox = container.getBoundingClientRect();
3444
+ const buttonBox = button.getBoundingClientRect();
3445
+ setThumb({ left: buttonBox.left - containerBox.left, width: buttonBox.width });
3446
+ };
3447
+ measure();
3448
+ window.addEventListener("resize", measure);
3449
+ return () => window.removeEventListener("resize", measure);
3450
+ }, [selectedLabelSlug, labels]);
3451
+ if (labels.length < 2) return null;
3452
+ return /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-switch-wrap", children: /* @__PURE__ */ jsxs18(
3453
+ "div",
3454
+ {
3455
+ ref: containerRef,
3456
+ className: "kerne-pricing-switch",
3457
+ role: "group",
3458
+ "aria-label": l10n.billing.intervalGroupLabel,
3459
+ children: [
3460
+ thumb ? /* @__PURE__ */ jsx19(
3461
+ "span",
3462
+ {
3463
+ className: "kerne-pricing-switch-thumb",
3464
+ style: { transform: `translateX(${thumb.left}px)`, width: `${thumb.width}px` },
3465
+ "aria-hidden": "true"
3466
+ }
3467
+ ) : null,
3468
+ labels.map((label) => /* @__PURE__ */ jsxs18(
3469
+ "button",
3470
+ {
3471
+ ref: (el) => {
3472
+ if (el) buttonRefs.current.set(label.slug, el);
3473
+ else buttonRefs.current.delete(label.slug);
3474
+ },
3475
+ type: "button",
3476
+ className: "kerne-pricing-switch-option",
3477
+ "aria-pressed": label.slug === selectedLabelSlug,
3478
+ onClick: () => setSelectedLabelSlug(label.slug),
3479
+ children: [
3480
+ label.name,
3481
+ savings && savings.labelSlug === label.slug ? /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-switch-save", children: fill(l10n.pricing.save, { percent: savings.percent }) }) : null
3482
+ ]
3483
+ },
3484
+ label.slug
3485
+ ))
3486
+ ]
3487
+ }
3488
+ ) });
3489
+ }
3490
+ function useAnimatedAmount(target, durationMs = 420) {
3491
+ const [displayed, setDisplayed] = useState19(target);
3492
+ const displayedRef = useRef4(target);
3493
+ const frameRef = useRef4();
3494
+ useEffect7(() => {
3495
+ if (displayedRef.current === target) return;
3496
+ const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
3497
+ if (reduceMotion) {
3498
+ displayedRef.current = target;
3499
+ setDisplayed(target);
3500
+ return;
3501
+ }
3502
+ const from = displayedRef.current;
3503
+ const start = performance.now();
3504
+ const tick = (now) => {
3505
+ const t = Math.min((now - start) / durationMs, 1);
3506
+ const eased = 1 - Math.pow(1 - t, 3);
3507
+ const value = Math.round(from + (target - from) * eased);
3508
+ displayedRef.current = value;
3509
+ setDisplayed(value);
3510
+ if (t < 1) frameRef.current = requestAnimationFrame(tick);
3511
+ };
3512
+ frameRef.current = requestAnimationFrame(tick);
3513
+ return () => {
3514
+ if (frameRef.current) cancelAnimationFrame(frameRef.current);
3515
+ };
3516
+ }, [target, durationMs]);
3517
+ return displayed;
3518
+ }
3519
+ function Plan({ slug, featured, badge, maxFeatures }) {
3520
+ const { plans, selectedLabelSlug, currentPlanId, busyPriceId, onSelectPlan } = usePricingTable();
3521
+ const l10n = useLocalization();
3522
+ const plan = plans.find((p) => p.slug === slug);
3523
+ useEffect7(() => {
3524
+ if (!plan && process.env.NODE_ENV === "development") {
3525
+ console.warn(
3526
+ `[Kerne] <PricingTable.Plan slug="${slug}"> - no plan with this slug on the loaded product.`
3527
+ );
3528
+ }
3529
+ }, [plan, slug]);
3530
+ const price = plan?.prices.find((p) => p.label.slug === selectedLabelSlug) ?? plan?.prices[0];
3531
+ const resolved = price ? resolvePriceDisplay(price, price.label.display_interval) : null;
3532
+ const animatedAmount = useAnimatedAmount(resolved?.displayAmount ?? 0);
3533
+ if (!plan) return null;
3534
+ const isCurrent = plan.id === currentPlanId;
3535
+ const isBusy = !!price && busyPriceId === price.id;
3536
+ const shownEntitlements = maxFeatures ? plan.entitlements.slice(0, maxFeatures) : plan.entitlements;
3537
+ const remaining = plan.entitlements.length - shownEntitlements.length;
3538
+ const billedCaption = resolved && resolved.displayInterval !== resolved.billedInterval ? fill(l10n.pricing.billedAt, {
3539
+ amount: formatMoney(resolved.billedAmount, price.currency),
3540
+ interval: formatInterval(resolved.billedInterval, l10n.billing)
3541
+ }) : null;
3542
+ return /* @__PURE__ */ jsxs18(
3543
+ "div",
3544
+ {
3545
+ className: "kerne-pricing-card",
3546
+ "data-featured": featured ? "true" : void 0,
3547
+ "data-current": isCurrent ? "true" : void 0,
3548
+ children: [
3549
+ /* @__PURE__ */ jsxs18("div", { className: "kerne-pricing-card-head", children: [
3550
+ badge ? /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-badge", children: badge }) : null,
3551
+ /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-card-name", children: plan.name }),
3552
+ plan.description ? /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-card-desc", children: plan.description }) : null
3553
+ ] }),
3554
+ /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-price", children: resolved ? /* @__PURE__ */ jsxs18(Fragment12, { children: [
3555
+ /* @__PURE__ */ jsxs18("div", { className: "kerne-pricing-price-row", children: [
3556
+ /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-amount", children: formatMoney(animatedAmount, price.currency) }),
3557
+ /* @__PURE__ */ jsxs18("span", { className: "kerne-pricing-interval", children: [
3558
+ "/ ",
3559
+ formatInterval(resolved.displayInterval, l10n.billing)
3560
+ ] })
3561
+ ] }),
3562
+ billedCaption ? /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-billed", children: billedCaption }) : null
3563
+ ] }) : /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-billed", children: l10n.billing.notOnInterval }) }),
3564
+ shownEntitlements.length > 0 ? /* @__PURE__ */ jsxs18("ul", { className: "kerne-pricing-features", children: [
3565
+ shownEntitlements.map((e) => /* @__PURE__ */ jsxs18("li", { className: "kerne-pricing-feature", children: [
3566
+ /* @__PURE__ */ jsx19(CheckIcon2, {}),
3567
+ /* @__PURE__ */ jsx19("span", { children: formatEntitlement(e, l10n.pricing.unlimited) })
3568
+ ] }, e.feature_key)),
3569
+ remaining > 0 ? /* @__PURE__ */ jsx19("li", { className: "kerne-pricing-feature-more", children: fill(l10n.pricing.moreFeatures, { count: remaining }) }) : null
3570
+ ] }) : null,
3571
+ /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-cta", children: isCurrent ? /* @__PURE__ */ jsx19(Button, { variant: "outline", type: "button", disabled: true, children: l10n.billing.current }) : /* @__PURE__ */ jsx19(
3572
+ Button,
3573
+ {
3574
+ variant: featured ? "primary" : "outline",
3575
+ type: "button",
3576
+ disabled: !price || busyPriceId !== null && !isBusy,
3577
+ loading: isBusy,
3578
+ onClick: () => onSelectPlan(plan),
3579
+ children: currentPlanId ? l10n.billing.switchTo : l10n.pricing.getStarted
3580
+ }
3581
+ ) })
3582
+ ]
3583
+ }
3584
+ );
3585
+ }
3586
+ function CheckIcon2() {
3587
+ return /* @__PURE__ */ jsx19(
3588
+ "svg",
3589
+ {
3590
+ width: "15",
3591
+ height: "15",
3592
+ viewBox: "0 0 24 24",
3593
+ fill: "none",
3594
+ stroke: "currentColor",
3595
+ strokeWidth: "2",
3596
+ "aria-hidden": "true",
3597
+ children: /* @__PURE__ */ jsx19("path", { d: "m5 13 4 4L19 7", strokeLinecap: "round", strokeLinejoin: "round" })
3598
+ }
3599
+ );
3600
+ }
3601
+ function Plans({ featuredSlug, featuredBadge, maxFeatures }) {
3602
+ const { plans } = usePricingTable();
3603
+ return /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-grid", children: plans.map((plan) => /* @__PURE__ */ jsx19(
3604
+ Plan,
3605
+ {
3606
+ slug: plan.slug,
3607
+ featured: plan.slug === featuredSlug,
3608
+ badge: plan.slug === featuredSlug ? featuredBadge : void 0,
3609
+ maxFeatures
3610
+ },
3611
+ plan.id
3612
+ )) });
3613
+ }
3614
+ PricingTable.LabelSwitch = LabelSwitch;
3615
+ PricingTable.Plan = Plan;
3616
+ PricingTable.Plans = Plans;
3617
+
2827
3618
  // src/ui/CheckoutResult.tsx
2828
- import { useEffect as useEffect7, useRef as useRef4, useState as useState18 } from "react";
2829
- import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
3619
+ import { useEffect as useEffect8, useRef as useRef5, useState as useState20 } from "react";
3620
+ import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
2830
3621
  var POLL_INTERVAL_MS = 1500;
2831
3622
  function CheckoutResult({
2832
3623
  outcome = "success",
@@ -2844,9 +3635,9 @@ function CheckoutResult({
2844
3635
  const client = useClient();
2845
3636
  const { config } = useAuthConfig();
2846
3637
  const l10n = useLocalization();
2847
- const [state, setState] = useState18("checking");
2848
- const confirmed = useRef4(false);
2849
- useEffect7(() => {
3638
+ const [state, setState] = useState20("checking");
3639
+ const confirmed = useRef5(false);
3640
+ useEffect8(() => {
2850
3641
  if (outcome !== "success") return;
2851
3642
  let stopped = false;
2852
3643
  const startedAt = Date.now();
@@ -2877,20 +3668,20 @@ function CheckoutResult({
2877
3668
  const go = (url, handler) => handler ?? (() => {
2878
3669
  if (url) window.location.href = url;
2879
3670
  });
2880
- const continueAction = continueUrl || onContinue ? /* @__PURE__ */ jsx19(Button, { type: "button", onClick: go(continueUrl, onContinue), children: l10n.common.continue }) : void 0;
2881
- const shell = (children) => /* @__PURE__ */ jsxs18(Shell, { appearance, className, children: [
2882
- logo ? /* @__PURE__ */ jsx19("div", { className: "kerne-masthead", children: logo }) : null,
3671
+ const continueAction = continueUrl || onContinue ? /* @__PURE__ */ jsx20(Button, { type: "button", onClick: go(continueUrl, onContinue), children: l10n.common.continue }) : void 0;
3672
+ const shell = (children) => /* @__PURE__ */ jsxs19(Shell, { appearance, className, children: [
3673
+ logo ? /* @__PURE__ */ jsx20("div", { className: "kerne-masthead", children: logo }) : null,
2883
3674
  children,
2884
- /* @__PURE__ */ jsx19(KerneBranding, { show: config?.show_kerne_branding ?? false })
3675
+ /* @__PURE__ */ jsx20(KerneBranding, { show: config?.show_kerne_branding ?? false })
2885
3676
  ] });
2886
3677
  if (outcome === "cancel") {
2887
3678
  return shell(
2888
- /* @__PURE__ */ jsx19(
3679
+ /* @__PURE__ */ jsx20(
2889
3680
  Status,
2890
3681
  {
2891
3682
  tone: "danger",
2892
3683
  title: l10n.checkout.cancelTitle,
2893
- action: pricingUrl || onRetry ? /* @__PURE__ */ jsx19(Button, { variant: "outline", type: "button", onClick: go(pricingUrl, onRetry), children: l10n.checkout.backToPlans }) : void 0,
3684
+ action: pricingUrl || onRetry ? /* @__PURE__ */ jsx20(Button, { variant: "outline", type: "button", onClick: go(pricingUrl, onRetry), children: l10n.checkout.backToPlans }) : void 0,
2894
3685
  children: l10n.checkout.cancelBody
2895
3686
  }
2896
3687
  )
@@ -2898,21 +3689,21 @@ function CheckoutResult({
2898
3689
  }
2899
3690
  if (state === "checking") {
2900
3691
  return shell(
2901
- /* @__PURE__ */ jsx19(Status, { tone: "loading", title: l10n.checkout.confirmingTitle, children: l10n.checkout.confirmingBody })
3692
+ /* @__PURE__ */ jsx20(Status, { tone: "loading", title: l10n.checkout.confirmingTitle, children: l10n.checkout.confirmingBody })
2902
3693
  );
2903
3694
  }
2904
3695
  if (state === "pending") {
2905
3696
  return shell(
2906
- /* @__PURE__ */ jsx19(Status, { tone: "success", title: l10n.checkout.pendingTitle, action: continueAction, children: l10n.checkout.pendingBody })
3697
+ /* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.pendingTitle, action: continueAction, children: l10n.checkout.pendingBody })
2907
3698
  );
2908
3699
  }
2909
3700
  return shell(
2910
- /* @__PURE__ */ jsx19(Status, { tone: "success", title: l10n.checkout.doneTitle, action: continueAction, children: l10n.checkout.doneBody })
3701
+ /* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.doneTitle, action: continueAction, children: l10n.checkout.doneBody })
2911
3702
  );
2912
3703
  }
2913
3704
 
2914
3705
  // src/ui/UpgradePrompt.tsx
2915
- import { Fragment as Fragment11, jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
3706
+ import { Fragment as Fragment13, jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
2916
3707
  function UpgradePrompt({
2917
3708
  featureKey,
2918
3709
  requested,
@@ -2931,22 +3722,25 @@ function UpgradePrompt({
2931
3722
  const l10n = useLocalization();
2932
3723
  if (isLoading) {
2933
3724
  if (hideWhileLoading) return null;
2934
- return /* @__PURE__ */ jsx20(Shell, { appearance, className, children: /* @__PURE__ */ jsx20("div", { className: "kerne-skeleton", style: { height: 80 }, "aria-label": l10n.upgrade.checking }) });
3725
+ return /* @__PURE__ */ jsx21(Shell, { appearance, className, children: /* @__PURE__ */ jsx21("div", { className: "kerne-skeleton", style: { height: 80 }, "aria-label": l10n.upgrade.checking }) });
2935
3726
  }
2936
- if (allowed) return /* @__PURE__ */ jsx20(Fragment11, { children });
3727
+ if (allowed) return /* @__PURE__ */ jsx21(Fragment13, { children });
2937
3728
  const name = featureName ?? humanizeFeatureKey(featureKey).toLowerCase();
2938
3729
  const quotaExhausted = details?.feature_type === "QUOTA" && typeof details.limit === "number" && typeof details.used === "number";
2939
3730
  const heading = title ?? fill(quotaExhausted ? l10n.upgrade.quotaTitle : l10n.upgrade.featureTitle, { feature: name });
2940
3731
  const body = description ?? (quotaExhausted ? fill(l10n.upgrade.quotaBody, {
2941
3732
  used: formatNumber(details.used),
2942
- limit: formatNumber(details.limit)
3733
+ // `access_limit` is where access actually stops; `limit` is only the
3734
+ // included allocation and can understate it past an overage ceiling.
3735
+ // Falls back to `limit` defensively - should never be needed here.
3736
+ limit: formatNumber(details.access_limit ?? details.limit)
2943
3737
  }) : fill(l10n.upgrade.featureBody, { feature: name }));
2944
- return /* @__PURE__ */ jsx20(Shell, { appearance, className, children: /* @__PURE__ */ jsxs19("div", { className: "kerne-status", children: [
2945
- /* @__PURE__ */ jsxs19("div", { className: "kerne-heading", children: [
2946
- /* @__PURE__ */ jsx20("h2", { className: "kerne-section-title", style: { fontSize: 17 }, children: heading }),
2947
- /* @__PURE__ */ jsx20("p", { className: "kerne-status-text", children: body })
3738
+ return /* @__PURE__ */ jsx21(Shell, { appearance, className, children: /* @__PURE__ */ jsxs20("div", { className: "kerne-status", children: [
3739
+ /* @__PURE__ */ jsxs20("div", { className: "kerne-heading", children: [
3740
+ /* @__PURE__ */ jsx21("h2", { className: "kerne-section-title", style: { fontSize: 17 }, children: heading }),
3741
+ /* @__PURE__ */ jsx21("p", { className: "kerne-status-text", children: body })
2948
3742
  ] }),
2949
- upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx20(
3743
+ upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx21(
2950
3744
  Button,
2951
3745
  {
2952
3746
  type: "button",
@@ -2967,6 +3761,7 @@ export {
2967
3761
  ForgotPasswordForm,
2968
3762
  LoginForm,
2969
3763
  MagicLinkCallback,
3764
+ PricingTable,
2970
3765
  ProfileSection,
2971
3766
  RegisterForm,
2972
3767
  ResetPasswordForm,
@@ -2980,5 +3775,6 @@ export {
2980
3775
  VerifyEmailForm,
2981
3776
  WaitlistForm,
2982
3777
  defaultLocalization,
2983
- useLocalization
3778
+ useLocalization,
3779
+ usePricingTable
2984
3780
  };