@kerne/react 0.1.2 → 0.1.3

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
@@ -16,10 +16,10 @@ import {
16
16
  useSubscription,
17
17
  useUser,
18
18
  useWaitlist
19
- } from "../chunk-AIUIBAYB.js";
19
+ } from "../chunk-F6NV76OR.js";
20
20
 
21
21
  // src/ui/AuthFlow.tsx
22
- import { useState as useState9 } from "react";
22
+ import { useState as useState10 } from "react";
23
23
 
24
24
  // src/ui/useUrlToken.ts
25
25
  import { useState, useEffect } from "react";
@@ -37,10 +37,10 @@ function useUrlToken(param = "token", override) {
37
37
  }
38
38
 
39
39
  // src/ui/LoginForm.tsx
40
- import { useState as useState2 } from "react";
40
+ import { useState as useState3 } from "react";
41
41
 
42
42
  // src/ui/primitives.tsx
43
- import { useId, useContext, createContext } from "react";
43
+ import { useId, useState as useState2, useContext, createContext } from "react";
44
44
 
45
45
  // src/ui/styles.ts
46
46
  var STYLE_ELEMENT_ID = "kerne-ui-styles";
@@ -53,6 +53,7 @@ var DARK_TOKENS = `
53
53
  --kerne-panel: #1b1f26;
54
54
  --kerne-surface: #14171c;
55
55
  --kerne-surface-raised: #2a2f38;
56
+ --kerne-track: #313743;
56
57
  --kerne-primary: #f0f1f3;
57
58
  --kerne-primary-fg: #16181d;
58
59
  --kerne-primary-hover: #d8dade;
@@ -81,6 +82,11 @@ var CSS = `
81
82
  variant, the dropdown floating over host content, a pressed segment. */
82
83
  --kerne-surface: #ffffff;
83
84
  --kerne-surface-raised: #ffffff;
85
+ /* Unfilled part of a meter, and the ground of a disabled button. Distinct
86
+ from --kerne-panel, which is tuned to sit on the page: on a card the two
87
+ are nearly the same colour, so a meter drawn in panel had invisible empty
88
+ segments and read as having fewer steps than it has. */
89
+ --kerne-track: #e3e5e9;
84
90
  --kerne-primary: #16171a;
85
91
  --kerne-primary-fg: #ffffff;
86
92
  --kerne-primary-hover: #2c2e33;
@@ -226,6 +232,18 @@ var CSS = `
226
232
  }
227
233
  .kerne-subtitle { margin: 0; font-size: 14px; color: var(--kerne-fg-muted); line-height: 1.5; }
228
234
 
235
+ /* Moving between steps (sign-in -> sign-up -> forgot password) used to be an
236
+ instant swap: the content and the height both jumped at once, which reads
237
+ as a page reload rather than one flow. */
238
+ @keyframes kerne-step-in {
239
+ from { opacity: 0; transform: translateY(4px); }
240
+ to { opacity: 1; transform: none; }
241
+ }
242
+ .kerne-step { animation: kerne-step-in .18s ease-out; }
243
+ @media (prefers-reduced-motion: reduce) {
244
+ .kerne-step { animation: none; }
245
+ }
246
+
229
247
  /* ---------- Stack ---------- */
230
248
  .kerne-stack { display: flex; flex-direction: column; gap: 16px; }
231
249
  .kerne-form { display: flex; flex-direction: column; gap: 12px; }
@@ -233,6 +251,9 @@ var CSS = `
233
251
  .kerne-label { font-size: 14px; font-weight: 500; color: var(--kerne-fg); }
234
252
  .kerne-label-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
235
253
 
254
+ /* Positioning context for the reveal toggle, which sits inside the input box. */
255
+ .kerne-control { position: relative; display: flex; }
256
+
236
257
  .kerne-input {
237
258
  font: inherit;
238
259
  font-size: 14px;
@@ -245,6 +266,57 @@ var CSS = `
245
266
  width: 100%;
246
267
  transition: border-color .12s ease, box-shadow .12s ease;
247
268
  }
269
+ /* Room for the toggle, so a long password never runs underneath it. */
270
+ .kerne-input[data-kerne-reveal="true"] { padding-right: 42px; }
271
+
272
+ .kerne-reveal {
273
+ position: absolute;
274
+ top: 0; right: 0; bottom: 0;
275
+ width: 40px;
276
+ display: flex; align-items: center; justify-content: center;
277
+ background: none; border: none; padding: 0; cursor: pointer;
278
+ color: var(--kerne-fg-subtle);
279
+ border-radius: var(--kerne-radius);
280
+ transition: color .12s ease;
281
+ }
282
+ .kerne-reveal:hover:not(:disabled) { color: var(--kerne-fg); }
283
+ .kerne-reveal:disabled { cursor: not-allowed; opacity: .5; }
284
+ .kerne-reveal:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: -2px; }
285
+
286
+ .kerne-field-note { font-size: 12.5px; line-height: 1.4; color: var(--kerne-fg-muted); margin: 0; }
287
+ .kerne-field-error { font-size: 12.5px; line-height: 1.4; color: var(--kerne-danger); margin: 0; }
288
+ /* Caps Lock is a "you are about to get this wrong" signal, not background
289
+ guidance - in muted grey it sat at the same weight as a hint and got read
290
+ as one. */
291
+ .kerne-field-note[data-kerne-warn="true"] { color: var(--kerne-warning); }
292
+
293
+ /* ---------- Password rules ---------- */
294
+ /* Unmet stays at hint weight rather than turning red: nothing is wrong yet,
295
+ the user is still typing. Only the tick earns a color. */
296
+ .kerne-rules { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
297
+ .kerne-rule {
298
+ display: flex; align-items: center; gap: 6px;
299
+ font-size: 12.5px; line-height: 1.4; color: var(--kerne-fg-muted);
300
+ }
301
+ .kerne-rule[data-met] { color: var(--kerne-success); }
302
+
303
+ /* ---------- Password strength ---------- */
304
+ .kerne-strength { display: flex; flex-direction: column; gap: 5px; }
305
+ .kerne-strength-track { display: flex; gap: 4px; }
306
+ .kerne-strength-step {
307
+ flex: 1; height: 3px; border-radius: 999px;
308
+ background: var(--kerne-track);
309
+ transition: background-color .18s ease;
310
+ }
311
+ .kerne-strength-label { font-size: 12px; color: var(--kerne-fg-muted); }
312
+ .kerne-strength[data-score="0"] .kerne-strength-step[data-on],
313
+ .kerne-strength[data-score="1"] .kerne-strength-step[data-on] { background: var(--kerne-danger); }
314
+ .kerne-strength[data-score="2"] .kerne-strength-step[data-on] { background: var(--kerne-warning); }
315
+ .kerne-strength[data-score="3"] .kerne-strength-step[data-on],
316
+ .kerne-strength[data-score="4"] .kerne-strength-step[data-on] { background: var(--kerne-success); }
317
+ @media (prefers-reduced-motion: reduce) {
318
+ .kerne-strength-step { transition: none; }
319
+ }
248
320
  .kerne-input::placeholder { color: var(--kerne-fg-subtle); }
249
321
  .kerne-input:hover:not(:disabled):not(:focus) { border-color: var(--kerne-fg-subtle); }
250
322
  .kerne-input:focus {
@@ -279,6 +351,20 @@ var CSS = `
279
351
  }
280
352
  .kerne-button:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; }
281
353
  .kerne-button:disabled { opacity: .5; cursor: not-allowed; }
354
+ /* A disabled primary drops the brand entirely rather than wearing it at half
355
+ opacity: on a dark theme the light primary faded into an opaque grey slab
356
+ that read as an enabled button, and a brand colour has no business marking
357
+ an action you cannot take. Loading is excluded - it is still the real
358
+ button, just busy. */
359
+ .kerne-button-primary:disabled:not([data-kerne-loading="true"]) {
360
+ opacity: 1;
361
+ background: var(--kerne-track);
362
+ color: var(--kerne-fg-subtle);
363
+ }
364
+ /* A button mid-request is busy, not unavailable - dimming it to the disabled
365
+ opacity reads as "this stopped working". */
366
+ .kerne-button[data-kerne-loading="true"] { opacity: 1; cursor: progress; }
367
+ .kerne-button .kerne-spinner { width: 15px; height: 15px; border-width: 2px; }
282
368
  .kerne-button-primary { background: var(--kerne-primary); color: var(--kerne-primary-fg); }
283
369
  .kerne-button-primary:hover:not(:disabled) { background: var(--kerne-primary-hover); }
284
370
  .kerne-button-outline {
@@ -417,7 +503,7 @@ var CSS = `
417
503
  /* Digits line up between rows instead of jittering as counts change. */
418
504
  font-variant-numeric: tabular-nums;
419
505
  }
420
- .kerne-meter-track { height: 6px; border-radius: 999px; background: var(--kerne-panel); overflow: hidden; }
506
+ .kerne-meter-track { height: 6px; border-radius: 999px; background: var(--kerne-track); overflow: hidden; }
421
507
  .kerne-meter-fill { height: 100%; border-radius: 999px; background: var(--kerne-fg); transition: width .3s ease; }
422
508
  .kerne-meter[data-level="warning"] .kerne-meter-fill { background: var(--kerne-warning); }
423
509
  .kerne-meter[data-level="critical"] .kerne-meter-fill { background: var(--kerne-danger); }
@@ -577,6 +663,43 @@ function resolveTheme(appearance) {
577
663
  return appearance?.theme ?? "light";
578
664
  }
579
665
 
666
+ // src/ui/password.ts
667
+ var MIN_PASSWORD_LENGTH = 8;
668
+ var BASELINE_POLICY = {
669
+ min_length: MIN_PASSWORD_LENGTH,
670
+ require_uppercase: false,
671
+ require_number: false,
672
+ require_symbol: false
673
+ };
674
+ function resolvePolicy(config) {
675
+ return config?.password_policy ?? BASELINE_POLICY;
676
+ }
677
+ function passwordRules(password, policy) {
678
+ const rules = [{ key: "min_length", met: password.length >= policy.min_length }];
679
+ if (policy.require_uppercase) {
680
+ rules.push({ key: "require_uppercase", met: /[A-Z]/.test(password) });
681
+ }
682
+ if (policy.require_number) {
683
+ rules.push({ key: "require_number", met: /\d/.test(password) });
684
+ }
685
+ if (policy.require_symbol) {
686
+ rules.push({ key: "require_symbol", met: /[^A-Za-z0-9]/.test(password) });
687
+ }
688
+ return rules;
689
+ }
690
+ function meetsPolicy(password, policy) {
691
+ return passwordRules(password, policy).every((rule) => rule.met);
692
+ }
693
+ function passwordStrength(password) {
694
+ if (password.length < MIN_PASSWORD_LENGTH) return 0;
695
+ const variety = [/[a-z]/, /[A-Z]/, /\d/, /[^A-Za-z0-9]/].filter((re) => re.test(password)).length;
696
+ let score = 1;
697
+ if (password.length >= 12) score++;
698
+ if (password.length >= 16) score++;
699
+ if (variety >= 3) score++;
700
+ return Math.min(score, 4);
701
+ }
702
+
580
703
  // src/ui/primitives.tsx
581
704
  import { jsx, jsxs } from "react/jsx-runtime";
582
705
  var InsideShell = createContext(false);
@@ -635,6 +758,10 @@ function Masthead({
635
758
  ] })
636
759
  ] });
637
760
  }
761
+ function prefersAutoFocus() {
762
+ if (typeof window === "undefined" || !window.matchMedia) return false;
763
+ return window.matchMedia("(pointer: fine)").matches;
764
+ }
638
765
  function Field({
639
766
  label,
640
767
  type,
@@ -645,55 +772,185 @@ function Field({
645
772
  disabled,
646
773
  required,
647
774
  invalid,
775
+ error,
648
776
  action,
649
777
  code,
650
- autoFocus
778
+ autoFocus,
779
+ children
651
780
  }) {
652
781
  const id = useId();
782
+ const errorId = `${id}-error`;
783
+ const noteId = `${id}-note`;
784
+ const l10n = useLocalization();
785
+ const isPassword = type === "password";
786
+ const [revealed, setRevealed] = useState2(false);
787
+ const [capsLock, setCapsLock] = useState2(false);
788
+ const trackCapsLock = (e) => {
789
+ if (!isPassword) return;
790
+ setCapsLock(e.getModifierState?.("CapsLock") ?? false);
791
+ };
792
+ const describedBy = [error ? errorId : null, children ? noteId : null].filter(Boolean).join(" ");
653
793
  return /* @__PURE__ */ jsxs("div", { className: "kerne-field", children: [
654
794
  action ? /* @__PURE__ */ jsxs("div", { className: "kerne-label-row", children: [
655
795
  /* @__PURE__ */ jsx("label", { className: "kerne-label", htmlFor: id, children: label }),
656
796
  action
657
797
  ] }) : /* @__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
- )
798
+ /* @__PURE__ */ jsxs("div", { className: "kerne-control", children: [
799
+ /* @__PURE__ */ jsx(
800
+ "input",
801
+ {
802
+ id,
803
+ className: "kerne-input",
804
+ type: isPassword && revealed ? "text" : type,
805
+ value,
806
+ onChange: (e) => onChange(e.target.value),
807
+ onKeyUp: trackCapsLock,
808
+ onBlur: () => setCapsLock(false),
809
+ autoComplete,
810
+ placeholder,
811
+ disabled,
812
+ required,
813
+ "aria-invalid": invalid || !!error || void 0,
814
+ "aria-describedby": describedBy || void 0,
815
+ "data-kerne-code": code ? "true" : void 0,
816
+ "data-kerne-reveal": isPassword ? "true" : void 0,
817
+ autoFocus: autoFocus && prefersAutoFocus()
818
+ }
819
+ ),
820
+ isPassword ? /* @__PURE__ */ jsx(
821
+ "button",
822
+ {
823
+ type: "button",
824
+ className: "kerne-reveal",
825
+ onClick: () => setRevealed((v) => !v),
826
+ disabled,
827
+ "aria-label": revealed ? l10n.common.hidePassword : l10n.common.showPassword,
828
+ "aria-pressed": revealed,
829
+ tabIndex: -1,
830
+ children: revealed ? /* @__PURE__ */ jsx(EyeOffIcon, {}) : /* @__PURE__ */ jsx(EyeIcon, {})
831
+ }
832
+ ) : null
833
+ ] }),
834
+ children ? /* @__PURE__ */ jsx("div", { id: noteId, children }) : null,
835
+ capsLock ? /* @__PURE__ */ jsx("p", { className: "kerne-field-note", "data-kerne-warn": "true", role: "status", children: l10n.common.capsLockOn }) : null,
836
+ error ? /* @__PURE__ */ jsx("p", { className: "kerne-field-error", id: errorId, role: "alert", children: error }) : null
675
837
  ] });
676
838
  }
839
+ function EyeIcon() {
840
+ return /* @__PURE__ */ jsxs(
841
+ "svg",
842
+ {
843
+ width: "16",
844
+ height: "16",
845
+ viewBox: "0 0 24 24",
846
+ fill: "none",
847
+ stroke: "currentColor",
848
+ strokeWidth: "2",
849
+ "aria-hidden": "true",
850
+ children: [
851
+ /* @__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" }),
852
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
853
+ ]
854
+ }
855
+ );
856
+ }
857
+ function EyeOffIcon() {
858
+ return /* @__PURE__ */ jsxs(
859
+ "svg",
860
+ {
861
+ width: "16",
862
+ height: "16",
863
+ viewBox: "0 0 24 24",
864
+ fill: "none",
865
+ stroke: "currentColor",
866
+ strokeWidth: "2",
867
+ "aria-hidden": "true",
868
+ children: [
869
+ /* @__PURE__ */ jsx(
870
+ "path",
871
+ {
872
+ 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",
873
+ strokeLinecap: "round",
874
+ strokeLinejoin: "round"
875
+ }
876
+ ),
877
+ /* @__PURE__ */ jsx("path", { d: "m3 3 18 18", strokeLinecap: "round" })
878
+ ]
879
+ }
880
+ );
881
+ }
677
882
  function Button({
678
883
  children,
679
884
  variant = "primary",
680
885
  disabled,
886
+ loading,
681
887
  type = "submit",
682
888
  onClick,
683
889
  ariaLabel
684
890
  }) {
685
- return /* @__PURE__ */ jsx(
891
+ return /* @__PURE__ */ jsxs(
686
892
  "button",
687
893
  {
688
894
  type,
689
895
  className: `kerne-button kerne-button-${variant}`,
690
- disabled,
896
+ disabled: disabled || loading,
691
897
  onClick,
692
898
  "aria-label": ariaLabel,
693
- children
899
+ "aria-busy": loading || void 0,
900
+ "data-kerne-loading": loading ? "true" : void 0,
901
+ children: [
902
+ loading ? /* @__PURE__ */ jsx("span", { className: "kerne-spinner", "aria-hidden": "true" }) : null,
903
+ children
904
+ ]
694
905
  }
695
906
  );
696
907
  }
908
+ function PasswordStrength({
909
+ password,
910
+ policy
911
+ }) {
912
+ const l10n = useLocalization();
913
+ const resolved = policy ?? resolvePolicy(null);
914
+ if (!meetsPolicy(password, resolved)) {
915
+ const labels2 = {
916
+ min_length: fill(l10n.common.passwordHint, { count: resolved.min_length }),
917
+ require_uppercase: l10n.common.passwordRequireUppercase,
918
+ require_number: l10n.common.passwordRequireNumber,
919
+ require_symbol: l10n.common.passwordRequireSymbol
920
+ };
921
+ const rules = passwordRules(password, resolved);
922
+ if (rules.length === 1) {
923
+ return /* @__PURE__ */ jsx("p", { className: "kerne-field-note", children: labels2[rules[0].key] });
924
+ }
925
+ 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: [
926
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: rule.met ? "\u2713" : "\xB7" }),
927
+ labels2[rule.key]
928
+ ] }, rule.key)) });
929
+ }
930
+ const score = passwordStrength(password);
931
+ const labels = [
932
+ l10n.common.strengthWeak,
933
+ l10n.common.strengthWeak,
934
+ l10n.common.strengthFair,
935
+ l10n.common.strengthGood,
936
+ l10n.common.strengthStrong
937
+ ];
938
+ return /* @__PURE__ */ jsxs("div", { className: "kerne-strength", "data-score": score, children: [
939
+ /* @__PURE__ */ jsx("div", { className: "kerne-strength-track", "aria-hidden": "true", children: [1, 2, 3, 4].map((step) => /* @__PURE__ */ jsx(
940
+ "span",
941
+ {
942
+ className: "kerne-strength-step",
943
+ "data-on": score >= step ? "true" : void 0
944
+ },
945
+ step
946
+ )) }),
947
+ /* @__PURE__ */ jsxs("span", { className: "kerne-strength-label", children: [
948
+ l10n.common.strengthLabel,
949
+ ": ",
950
+ labels[score]
951
+ ] })
952
+ ] });
953
+ }
697
954
  function MethodSwitch({
698
955
  onClick,
699
956
  children
@@ -746,7 +1003,7 @@ function Panel({
746
1003
  "div",
747
1004
  {
748
1005
  className: `kerne-panel${danger ? " kerne-panel-danger" : ""}${variant === "success" ? " kerne-panel-success" : ""}`,
749
- ...danger ? { role: "alert" } : {},
1006
+ ...danger ? { role: "alert" } : { role: "status", "aria-live": "polite" },
750
1007
  children: [
751
1008
  icon,
752
1009
  /* @__PURE__ */ jsx("span", { children })
@@ -911,12 +1168,12 @@ function LoginForm({
911
1168
  const { config, isLoading: configLoading, error: configError } = useAuthConfig();
912
1169
  const l10n = useLocalization();
913
1170
  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);
1171
+ const [email, setEmail] = useState3("");
1172
+ const [password, setPassword] = useState3("");
1173
+ const [submitting, setSubmitting] = useState3(false);
1174
+ const [error, setError] = useState3(null);
1175
+ const [linkSentTo, setLinkSentTo] = useState3(null);
1176
+ const [passwordMode, setPasswordMode] = useState3(false);
920
1177
  const shell = (children) => /* @__PURE__ */ jsx2(Shell, { appearance, className, children });
921
1178
  const hasOAuth = (config?.oauth_providers.length ?? 0) > 0;
922
1179
  const hasCredentials = config?.enable_credentials ?? false;
@@ -1012,7 +1269,7 @@ function LoginForm({
1012
1269
  action: forgotPasswordUrl || onForgotPasswordClick ? /* @__PURE__ */ jsx2(TextLink, { href: forgotPasswordUrl, onClick: onForgotPasswordClick, children: l10n.signIn.forgotPassword }) : void 0
1013
1270
  }
1014
1271
  ) : null,
1015
- /* @__PURE__ */ jsx2(Button, { disabled: submitting || !email || usePassword && !password, children: submitLabel }),
1272
+ /* @__PURE__ */ jsx2(Button, { loading: submitting, disabled: !email || usePassword && !password, children: submitLabel }),
1016
1273
  hasCredentials && hasMagicLink ? /* @__PURE__ */ jsx2(
1017
1274
  MethodSwitch,
1018
1275
  {
@@ -1041,7 +1298,7 @@ function LoginForm({
1041
1298
  }
1042
1299
 
1043
1300
  // src/ui/RegisterForm.tsx
1044
- import { useState as useState3 } from "react";
1301
+ import { useState as useState4 } from "react";
1045
1302
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1046
1303
  function RegisterForm({
1047
1304
  onSuccess,
@@ -1062,17 +1319,18 @@ function RegisterForm({
1062
1319
  const client = useClient();
1063
1320
  const { register, startPasswordless } = useAuth();
1064
1321
  const { config, isLoading: configLoading, error: configError } = useAuthConfig();
1322
+ const policy = resolvePolicy(config);
1065
1323
  const invitationToken = useUrlToken("token", invitationTokenProp) || void 0;
1066
1324
  const l10n = useLocalization();
1067
1325
  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);
1326
+ const [email, setEmail] = useState4("");
1327
+ const [password, setPassword] = useState4("");
1328
+ const [name, setName] = useState4("");
1329
+ const [invitationCode, setInvitationCode] = useState4("");
1330
+ const [submitting, setSubmitting] = useState4(false);
1331
+ const [error, setError] = useState4(null);
1332
+ const [linkSentTo, setLinkSentTo] = useState4(null);
1333
+ const [passwordMode, setPasswordMode] = useState4(false);
1076
1334
  const shell = (children) => /* @__PURE__ */ jsx3(Shell, { appearance, className, children });
1077
1335
  const appName = config?.app_name;
1078
1336
  const heading = title ?? (appName ? fill(l10n.signUp.titleWithApp, { app: appName }) : l10n.signUp.title);
@@ -1216,13 +1474,15 @@ function RegisterForm({
1216
1474
  autoComplete: "new-password",
1217
1475
  placeholder: l10n.common.passwordPlaceholder,
1218
1476
  disabled: submitting,
1219
- required: true
1477
+ required: true,
1478
+ children: /* @__PURE__ */ jsx3(PasswordStrength, { password, policy })
1220
1479
  }
1221
1480
  ) : null,
1222
1481
  /* @__PURE__ */ jsx3(
1223
1482
  Button,
1224
1483
  {
1225
- disabled: submitting || !email || needsCode && !invitationCode || usePassword && !password,
1484
+ loading: submitting,
1485
+ disabled: !email || needsCode && !invitationCode || usePassword && !meetsPolicy(password, policy),
1226
1486
  children: submitLabel
1227
1487
  }
1228
1488
  ),
@@ -1254,7 +1514,7 @@ function RegisterForm({
1254
1514
  }
1255
1515
 
1256
1516
  // src/ui/ForgotPasswordForm.tsx
1257
- import { useState as useState4 } from "react";
1517
+ import { useState as useState5 } from "react";
1258
1518
  import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1259
1519
  function ForgotPasswordForm({
1260
1520
  callbackUrl,
@@ -1271,10 +1531,10 @@ function ForgotPasswordForm({
1271
1531
  const { config } = useAuthConfig();
1272
1532
  const l10n = useLocalization();
1273
1533
  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);
1534
+ const [email, setEmail] = useState5("");
1535
+ const [submitting, setSubmitting] = useState5(false);
1536
+ const [error, setError] = useState5(null);
1537
+ const [sentTo, setSentTo] = useState5(null);
1278
1538
  const handleSubmit = async (e) => {
1279
1539
  e.preventDefault();
1280
1540
  setError(null);
@@ -1321,7 +1581,7 @@ function ForgotPasswordForm({
1321
1581
  autoFocus: true
1322
1582
  }
1323
1583
  ),
1324
- /* @__PURE__ */ jsx4(Button, { disabled: submitting || !email, children: submitting ? l10n.resetPassword.requestSubmitting : l10n.resetPassword.requestSubmit }),
1584
+ /* @__PURE__ */ jsx4(Button, { loading: submitting, disabled: !email, children: submitting ? l10n.resetPassword.requestSubmitting : l10n.resetPassword.requestSubmit }),
1325
1585
  backLine ? /* @__PURE__ */ jsx4("p", { className: "kerne-footer", children: backLine }) : null
1326
1586
  ] })
1327
1587
  ] }),
@@ -1330,7 +1590,7 @@ function ForgotPasswordForm({
1330
1590
  }
1331
1591
 
1332
1592
  // src/ui/ResetPasswordForm.tsx
1333
- import { useState as useState5 } from "react";
1593
+ import { useState as useState6 } from "react";
1334
1594
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1335
1595
  function ResetPasswordForm({
1336
1596
  token: tokenProp,
@@ -1345,14 +1605,15 @@ function ResetPasswordForm({
1345
1605
  }) {
1346
1606
  const { confirmPasswordReset } = useAuth();
1347
1607
  const { config } = useAuthConfig();
1608
+ const policy = resolvePolicy(config);
1348
1609
  const token = useUrlToken("token", tokenProp);
1349
1610
  const l10n = useLocalization();
1350
1611
  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);
1612
+ const [password, setPassword] = useState6("");
1613
+ const [confirm, setConfirm] = useState6("");
1614
+ const [submitting, setSubmitting] = useState6(false);
1615
+ const [error, setError] = useState6(null);
1616
+ const [done, setDone] = useState6(false);
1356
1617
  const heading = title ?? l10n.resetPassword.title;
1357
1618
  const mismatch = confirm.length > 0 && password !== confirm;
1358
1619
  const signInAction = signInUrl || onSignInClick ? /* @__PURE__ */ jsx5(
@@ -1416,7 +1677,8 @@ function ResetPasswordForm({
1416
1677
  placeholder: l10n.common.passwordPlaceholder,
1417
1678
  disabled: submitting,
1418
1679
  required: true,
1419
- autoFocus: true
1680
+ autoFocus: true,
1681
+ children: /* @__PURE__ */ jsx5(PasswordStrength, { password, policy })
1420
1682
  }
1421
1683
  ),
1422
1684
  /* @__PURE__ */ jsx5(
@@ -1430,11 +1692,17 @@ function ResetPasswordForm({
1430
1692
  placeholder: l10n.common.passwordPlaceholder,
1431
1693
  disabled: submitting,
1432
1694
  required: true,
1433
- invalid: mismatch
1695
+ error: mismatch ? l10n.resetPassword.mismatch : void 0
1696
+ }
1697
+ ),
1698
+ /* @__PURE__ */ jsx5(
1699
+ Button,
1700
+ {
1701
+ loading: submitting,
1702
+ disabled: !meetsPolicy(password, policy) || !confirm || mismatch,
1703
+ children: submitting ? l10n.resetPassword.submitting : l10n.resetPassword.submit
1434
1704
  }
1435
1705
  ),
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
1706
  signInUrl || onSignInClick ? /* @__PURE__ */ jsx5("p", { className: "kerne-footer", children: /* @__PURE__ */ jsx5(TextLink, { href: signInUrl, onClick: onSignInClick, children: l10n.common.backToSignIn }) }) : null
1439
1707
  ] })
1440
1708
  ] }),
@@ -1443,7 +1711,7 @@ function ResetPasswordForm({
1443
1711
  }
1444
1712
 
1445
1713
  // src/ui/MagicLinkCallback.tsx
1446
- import { useEffect as useEffect2, useRef, useState as useState6 } from "react";
1714
+ import { useEffect as useEffect2, useRef, useState as useState7 } from "react";
1447
1715
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1448
1716
  function MagicLinkCallback({
1449
1717
  token: tokenProp,
@@ -1461,8 +1729,8 @@ function MagicLinkCallback({
1461
1729
  const token = useUrlToken("token", tokenProp);
1462
1730
  const l10n = useLocalization();
1463
1731
  const showError = useErrorResolver();
1464
- const [state, setState] = useState6("pending");
1465
- const [message, setMessage] = useState6(null);
1732
+ const [state, setState] = useState7("pending");
1733
+ const [message, setMessage] = useState7(null);
1466
1734
  const consumed = useRef(false);
1467
1735
  useEffect2(() => {
1468
1736
  if (token === null || consumed.current) return;
@@ -1502,7 +1770,7 @@ function MagicLinkCallback({
1502
1770
  }
1503
1771
 
1504
1772
  // src/ui/VerifyEmailForm.tsx
1505
- import { useEffect as useEffect3, useRef as useRef2, useState as useState7 } from "react";
1773
+ import { useEffect as useEffect3, useRef as useRef2, useState as useState8 } from "react";
1506
1774
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1507
1775
  function VerifyEmailForm({
1508
1776
  token: tokenProp,
@@ -1519,12 +1787,12 @@ function VerifyEmailForm({
1519
1787
  const token = useUrlToken("token", tokenProp);
1520
1788
  const l10n = useLocalization();
1521
1789
  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);
1790
+ const [code, setCode] = useState8("");
1791
+ const [submitting, setSubmitting] = useState8(false);
1792
+ const [error, setError] = useState8(null);
1793
+ const [resent, setResent] = useState8(false);
1794
+ const [linkState, setLinkState] = useState8("idle");
1795
+ const [verified, setVerified] = useState8(false);
1528
1796
  const consumed = useRef2(false);
1529
1797
  const heading = title ?? l10n.verifyEmail.title;
1530
1798
  const settle = async (needsTokenRefresh) => {
@@ -1618,7 +1886,7 @@ function VerifyEmailForm({
1618
1886
  autoFocus: true
1619
1887
  }
1620
1888
  ),
1621
- /* @__PURE__ */ jsx7(Button, { disabled: submitting || !code, children: submitting ? l10n.verifyEmail.submitting : l10n.verifyEmail.submit }),
1889
+ /* @__PURE__ */ jsx7(Button, { loading: submitting, disabled: !code, children: submitting ? l10n.verifyEmail.submitting : l10n.verifyEmail.submit }),
1622
1890
  /* @__PURE__ */ jsx7(MethodSwitch, { onClick: handleResend, children: l10n.verifyEmail.resend })
1623
1891
  ] })
1624
1892
  ] }),
@@ -1627,7 +1895,7 @@ function VerifyEmailForm({
1627
1895
  }
1628
1896
 
1629
1897
  // src/ui/ActivateAccountForm.tsx
1630
- import { useEffect as useEffect4, useState as useState8 } from "react";
1898
+ import { useEffect as useEffect4, useState as useState9 } from "react";
1631
1899
  import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1632
1900
  function ActivateAccountForm({
1633
1901
  token: tokenProp,
@@ -1643,15 +1911,16 @@ function ActivateAccountForm({
1643
1911
  const client = useClient();
1644
1912
  const { getActivationContext, completeActivation } = useAuth();
1645
1913
  const { config } = useAuthConfig();
1914
+ const policy = resolvePolicy(config);
1646
1915
  const token = useUrlToken("token", tokenProp);
1647
1916
  const l10n = useLocalization();
1648
1917
  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);
1918
+ const [context, setContext] = useState9(null);
1919
+ const [loadError, setLoadError] = useState9(null);
1920
+ const [password, setPassword] = useState9("");
1921
+ const [confirm, setConfirm] = useState9("");
1922
+ const [submitting, setSubmitting] = useState9(false);
1923
+ const [error, setError] = useState9(null);
1655
1924
  const heading = title ?? l10n.activate.title;
1656
1925
  const mismatch = confirm.length > 0 && password !== confirm;
1657
1926
  useEffect4(() => {
@@ -1736,7 +2005,8 @@ function ActivateAccountForm({
1736
2005
  placeholder: l10n.common.passwordPlaceholder,
1737
2006
  disabled: submitting,
1738
2007
  required: true,
1739
- autoFocus: true
2008
+ autoFocus: true,
2009
+ children: /* @__PURE__ */ jsx8(PasswordStrength, { password, policy })
1740
2010
  }
1741
2011
  ),
1742
2012
  /* @__PURE__ */ jsx8(
@@ -1750,12 +2020,18 @@ function ActivateAccountForm({
1750
2020
  placeholder: l10n.common.passwordPlaceholder,
1751
2021
  disabled: submitting,
1752
2022
  required: true,
1753
- invalid: mismatch
2023
+ error: mismatch ? l10n.activate.mismatch : void 0
1754
2024
  }
1755
- ),
1756
- mismatch ? /* @__PURE__ */ jsx8("p", { className: "kerne-footer", children: l10n.activate.mismatch }) : null
2025
+ )
1757
2026
  ] }) : null,
1758
- /* @__PURE__ */ jsx8(Button, { disabled: submitting || credentialsMode && (!password || !confirm || mismatch), children: submitting ? l10n.activate.submitting : l10n.activate.submit })
2027
+ /* @__PURE__ */ jsx8(
2028
+ Button,
2029
+ {
2030
+ loading: submitting,
2031
+ disabled: credentialsMode && (!meetsPolicy(password, policy) || !confirm || mismatch),
2032
+ children: submitting ? l10n.activate.submitting : l10n.activate.submit
2033
+ }
2034
+ )
1759
2035
  ] })
1760
2036
  ] }),
1761
2037
  /* @__PURE__ */ jsx8(KerneBranding, { show: config?.show_kerne_branding ?? false })
@@ -1782,7 +2058,7 @@ function AuthFlow({
1782
2058
  }) {
1783
2059
  const { config } = useAuthConfig();
1784
2060
  const token = useUrlToken("token");
1785
- const [internalStep, setInternalStep] = useState9(null);
2061
+ const [internalStep, setInternalStep] = useState10(null);
1786
2062
  const go = (next) => {
1787
2063
  if (controlledStep === void 0) setInternalStep(next);
1788
2064
  onStepChange?.(next);
@@ -1790,7 +2066,11 @@ function AuthFlow({
1790
2066
  const tokenStep = token && (initialStep === void 0 || TOKEN_STEPS.includes(initialStep)) ? initialStep ?? "magic-link" : null;
1791
2067
  const step = controlledStep ?? internalStep ?? tokenStep ?? initialStep ?? "sign-in";
1792
2068
  const canSignUp = allowSignUp && config?.registration_mode !== "CLOSED";
1793
- const shared = { appearance, logo, className };
2069
+ const shared = {
2070
+ appearance,
2071
+ logo,
2072
+ className: `kerne-step${className ? ` ${className}` : ""}`
2073
+ };
1794
2074
  switch (step) {
1795
2075
  case "sign-up":
1796
2076
  return /* @__PURE__ */ jsx9(
@@ -1855,7 +2135,7 @@ function AuthFlow({
1855
2135
  }
1856
2136
 
1857
2137
  // src/ui/ChangePasswordForm.tsx
1858
- import { useState as useState10 } from "react";
2138
+ import { useState as useState11 } from "react";
1859
2139
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1860
2140
  function ChangePasswordForm({
1861
2141
  onSuccess,
@@ -1864,15 +2144,17 @@ function ChangePasswordForm({
1864
2144
  className
1865
2145
  }) {
1866
2146
  const { updatePassword } = useAuth();
2147
+ const { config } = useAuthConfig();
2148
+ const policy = resolvePolicy(config);
1867
2149
  const l10n = useLocalization();
1868
2150
  const showError = useErrorResolver();
1869
2151
  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);
2152
+ const [currentPassword, setCurrentPassword] = useState11("");
2153
+ const [newPassword, setNewPassword] = useState11("");
2154
+ const [confirm, setConfirm] = useState11("");
2155
+ const [submitting, setSubmitting] = useState11(false);
2156
+ const [error, setError] = useState11(null);
2157
+ const [done, setDone] = useState11(false);
1876
2158
  const mismatch = confirm.length > 0 && newPassword !== confirm;
1877
2159
  const unchanged = newPassword.length > 0 && newPassword === currentPassword;
1878
2160
  const handleSubmit = async (e) => {
@@ -1924,7 +2206,8 @@ function ChangePasswordForm({
1924
2206
  placeholder: l10n.common.passwordPlaceholder,
1925
2207
  disabled: submitting,
1926
2208
  required: true,
1927
- invalid: unchanged
2209
+ error: unchanged ? l10n.security.sameAsCurrent : void 0,
2210
+ children: /* @__PURE__ */ jsx10(PasswordStrength, { password: newPassword, policy })
1928
2211
  }
1929
2212
  ),
1930
2213
  /* @__PURE__ */ jsx10(
@@ -1938,14 +2221,14 @@ function ChangePasswordForm({
1938
2221
  placeholder: l10n.common.passwordPlaceholder,
1939
2222
  disabled: submitting,
1940
2223
  required: true,
1941
- invalid: mismatch
2224
+ error: mismatch ? l10n.security.mismatch : void 0
1942
2225
  }
1943
2226
  ),
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
2227
  /* @__PURE__ */ jsx10(
1946
2228
  Button,
1947
2229
  {
1948
- disabled: submitting || !currentPassword || !newPassword || !confirm || mismatch || unchanged,
2230
+ loading: submitting,
2231
+ disabled: !currentPassword || !meetsPolicy(newPassword, policy) || !confirm || mismatch || unchanged,
1949
2232
  children: submitting ? l10n.security.submitting : l10n.security.submit
1950
2233
  }
1951
2234
  )
@@ -1955,7 +2238,7 @@ function ChangePasswordForm({
1955
2238
  }
1956
2239
 
1957
2240
  // src/ui/WaitlistForm.tsx
1958
- import { useState as useState11 } from "react";
2241
+ import { useState as useState12 } from "react";
1959
2242
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1960
2243
  function WaitlistForm({
1961
2244
  title,
@@ -1974,11 +2257,11 @@ function WaitlistForm({
1974
2257
  const { config } = useAuthConfig();
1975
2258
  const l10n = useLocalization();
1976
2259
  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);
2260
+ const [email, setEmail] = useState12("");
2261
+ const [name, setName] = useState12("");
2262
+ const [submitting, setSubmitting] = useState12(false);
2263
+ const [error, setError] = useState12(null);
2264
+ const [joined, setJoined] = useState12(false);
1982
2265
  const handleSubmit = async (e) => {
1983
2266
  e.preventDefault();
1984
2267
  setError(null);
@@ -2035,7 +2318,7 @@ function WaitlistForm({
2035
2318
  autoFocus: true
2036
2319
  }
2037
2320
  ),
2038
- /* @__PURE__ */ jsx11(Button, { disabled: submitting || !email, children: submitting ? l10n.waitlist.submitting : submitLabel ?? l10n.waitlist.submit })
2321
+ /* @__PURE__ */ jsx11(Button, { loading: submitting, disabled: !email, children: submitting ? l10n.waitlist.submitting : submitLabel ?? l10n.waitlist.submit })
2039
2322
  ] })
2040
2323
  ] }),
2041
2324
  legal ? /* @__PURE__ */ jsx11("p", { className: "kerne-legal", children: legal }) : null,
@@ -2044,7 +2327,7 @@ function WaitlistForm({
2044
2327
  }
2045
2328
 
2046
2329
  // src/ui/UserButton.tsx
2047
- import { useEffect as useEffect5, useRef as useRef3, useState as useState12 } from "react";
2330
+ import { useEffect as useEffect5, useRef as useRef3, useState as useState13 } from "react";
2048
2331
  import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2049
2332
  function SignOutButton({
2050
2333
  children,
@@ -2092,7 +2375,7 @@ function UserButton({
2092
2375
  const { logout, isAuthenticated } = useAuth();
2093
2376
  const { user, fullName, initials, email } = useUser();
2094
2377
  const l10n = useLocalization();
2095
- const [open, setOpen] = useState12(false);
2378
+ const [open, setOpen] = useState13(false);
2096
2379
  const wrapRef = useRef3(null);
2097
2380
  useEffect5(() => {
2098
2381
  if (!open) return;
@@ -2176,24 +2459,24 @@ function UserButton({
2176
2459
  }
2177
2460
 
2178
2461
  // src/ui/UserProfile.tsx
2179
- import { useState as useState17 } from "react";
2462
+ import { useState as useState18 } from "react";
2180
2463
 
2181
2464
  // src/ui/profile/ProfileSection.tsx
2182
- import { useEffect as useEffect6, useState as useState13 } from "react";
2465
+ import { useEffect as useEffect6, useState as useState14 } from "react";
2183
2466
  import { Fragment as Fragment6, jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
2184
2467
  function ProfileSection({ verificationCallbackUrl, onSaved }) {
2185
2468
  const { updateProfile, sendVerificationEmail, getVerificationStatus } = useAuth();
2186
2469
  const { user, fullName, initials, email } = useUser();
2187
2470
  const l10n = useLocalization();
2188
2471
  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);
2472
+ const [firstName, setFirstName] = useState14("");
2473
+ const [lastName, setLastName] = useState14("");
2474
+ const [saving, setSaving] = useState14(false);
2475
+ const [saved, setSaved] = useState14(false);
2476
+ const [error, setError] = useState14(null);
2477
+ const [verified, setVerified] = useState14(null);
2478
+ const [sending, setSending] = useState14(false);
2479
+ const [sent, setSent] = useState14(false);
2197
2480
  useEffect6(() => {
2198
2481
  if (!user) return;
2199
2482
  setFirstName(user.first_name ?? "");
@@ -2269,7 +2552,7 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
2269
2552
  disabled: saving
2270
2553
  }
2271
2554
  ),
2272
- /* @__PURE__ */ jsx13(Button, { disabled: saving, children: saving ? l10n.profile.saving : l10n.profile.save })
2555
+ /* @__PURE__ */ jsx13(Button, { loading: saving, children: saving ? l10n.profile.saving : l10n.profile.save })
2273
2556
  ] })
2274
2557
  ] }),
2275
2558
  /* @__PURE__ */ jsxs12(
@@ -2283,7 +2566,7 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
2283
2566
  email,
2284
2567
  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
2568
  ] }) }),
2286
- verified === false ? /* @__PURE__ */ jsx13(Button, { variant: "outline", type: "button", onClick: handleResend, disabled: sending, children: sending ? l10n.common.sending : l10n.profile.verify }) : null
2569
+ verified === false ? /* @__PURE__ */ jsx13(Button, { variant: "outline", type: "button", onClick: handleResend, loading: sending, children: sending ? l10n.common.sending : l10n.profile.verify }) : null
2287
2570
  ] }),
2288
2571
  sent ? /* @__PURE__ */ jsx13(Panel, { variant: "success", children: l10n.profile.verificationSent }) : null
2289
2572
  ]
@@ -2293,16 +2576,16 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
2293
2576
  }
2294
2577
 
2295
2578
  // src/ui/profile/SecuritySection.tsx
2296
- import { useState as useState14 } from "react";
2579
+ import { useState as useState15 } from "react";
2297
2580
  import { Fragment as Fragment7, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2298
2581
  function SecuritySection({ showDeleteAccount = true, onDeleted }) {
2299
2582
  const { requestAccountDeletion } = useAuth();
2300
2583
  const { config } = useAuthConfig();
2301
2584
  const l10n = useLocalization();
2302
2585
  const showError = useErrorResolver();
2303
- const [error, setError] = useState14(null);
2304
- const [confirmingDelete, setConfirmingDelete] = useState14(false);
2305
- const [deleting, setDeleting] = useState14(false);
2586
+ const [error, setError] = useState15(null);
2587
+ const [confirmingDelete, setConfirmingDelete] = useState15(false);
2588
+ const [deleting, setDeleting] = useState15(false);
2306
2589
  const hasPassword = config?.enable_credentials ?? true;
2307
2590
  const handleDelete = async () => {
2308
2591
  setError(null);
@@ -2319,7 +2602,7 @@ function SecuritySection({ showDeleteAccount = true, onDeleted }) {
2319
2602
  error ? /* @__PURE__ */ jsx14(Panel, { variant: "danger", children: error }) : null,
2320
2603
  hasPassword ? /* @__PURE__ */ jsx14(Subsection, { title: l10n.security.passwordTitle, description: l10n.security.passwordDesc, children: /* @__PURE__ */ jsx14(ChangePasswordForm, { title: null }) }) : null,
2321
2604
  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 }),
2605
+ /* @__PURE__ */ jsx14(Button, { variant: "danger", type: "button", onClick: handleDelete, loading: deleting, children: deleting ? l10n.security.deleting : l10n.security.deleteConfirm }),
2323
2606
  /* @__PURE__ */ jsx14(
2324
2607
  Button,
2325
2608
  {
@@ -2335,7 +2618,7 @@ function SecuritySection({ showDeleteAccount = true, onDeleted }) {
2335
2618
  }
2336
2619
 
2337
2620
  // src/ui/profile/BillingSection.tsx
2338
- import { useMemo, useState as useState16 } from "react";
2621
+ import { useMemo, useState as useState17 } from "react";
2339
2622
 
2340
2623
  // src/ui/format.ts
2341
2624
  function humanizeFeatureKey(key) {
@@ -2486,7 +2769,7 @@ function UsageMeters({
2486
2769
  }
2487
2770
 
2488
2771
  // src/ui/SubscriptionCard.tsx
2489
- import { useState as useState15 } from "react";
2772
+ import { useState as useState16 } from "react";
2490
2773
  import { Fragment as Fragment8, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
2491
2774
  var STATUS_TONES = {
2492
2775
  ACTIVE: "success",
@@ -2519,8 +2802,8 @@ function SubscriptionCard({
2519
2802
  UNPAID: l10n.billing.statusUnpaid,
2520
2803
  PAUSED: l10n.billing.statusPaused
2521
2804
  };
2522
- const [opening, setOpening] = useState15(false);
2523
- const [portalError, setPortalError] = useState15(null);
2805
+ const [opening, setOpening] = useState16(false);
2806
+ const [portalError, setPortalError] = useState16(null);
2524
2807
  const handlePortal = async () => {
2525
2808
  setPortalError(null);
2526
2809
  setOpening(true);
@@ -2596,7 +2879,7 @@ function SubscriptionCard({
2596
2879
  ] }) : null
2597
2880
  ] }),
2598
2881
  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
2882
+ !hidePortalButton ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: handlePortal, loading: opening, children: opening ? l10n.billing.opening : l10n.billing.managePortal }) : null
2600
2883
  ] })
2601
2884
  );
2602
2885
  }
@@ -2628,10 +2911,10 @@ function BillingSection({
2628
2911
  const { openCheckout } = useCheckout();
2629
2912
  const l10n = useLocalization();
2630
2913
  const showError = useErrorResolver();
2631
- const [busy, setBusy] = useState16(null);
2632
- const [error, setError] = useState16(null);
2914
+ const [busy, setBusy] = useState17(null);
2915
+ const [error, setError] = useState17(null);
2633
2916
  const intervals = useMemo(() => intervalsOf(plans), [plans]);
2634
- const [interval, setInterval] = useState16(null);
2917
+ const [interval, setInterval] = useState17(null);
2635
2918
  const activeInterval = interval ?? subscription?.plan_price.interval ?? intervals[0] ?? null;
2636
2919
  const handleSwitch = async (planPriceId) => {
2637
2920
  setError(null);
@@ -2706,6 +2989,7 @@ function BillingSection({
2706
2989
  variant: "outline",
2707
2990
  type: "button",
2708
2991
  onClick: () => handleSwitch(price.id),
2992
+ loading: busy === price.id,
2709
2993
  disabled: busy !== null,
2710
2994
  children: busy === price.id ? l10n.billing.opening : l10n.billing.switchTo
2711
2995
  }
@@ -2770,7 +3054,7 @@ function UserProfile({
2770
3054
  description: t.description
2771
3055
  }))
2772
3056
  ];
2773
- const [internalTab, setInternalTab] = useState17(defaultTab ?? allTabs[0]?.id ?? "profile");
3057
+ const [internalTab, setInternalTab] = useState18(defaultTab ?? allTabs[0]?.id ?? "profile");
2774
3058
  const current = controlledTab ?? internalTab;
2775
3059
  const select = (id) => {
2776
3060
  if (controlledTab === void 0) setInternalTab(id);
@@ -2825,7 +3109,7 @@ function UserProfile({
2825
3109
  }
2826
3110
 
2827
3111
  // src/ui/CheckoutResult.tsx
2828
- import { useEffect as useEffect7, useRef as useRef4, useState as useState18 } from "react";
3112
+ import { useEffect as useEffect7, useRef as useRef4, useState as useState19 } from "react";
2829
3113
  import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
2830
3114
  var POLL_INTERVAL_MS = 1500;
2831
3115
  function CheckoutResult({
@@ -2844,7 +3128,7 @@ function CheckoutResult({
2844
3128
  const client = useClient();
2845
3129
  const { config } = useAuthConfig();
2846
3130
  const l10n = useLocalization();
2847
- const [state, setState] = useState18("checking");
3131
+ const [state, setState] = useState19("checking");
2848
3132
  const confirmed = useRef4(false);
2849
3133
  useEffect7(() => {
2850
3134
  if (outcome !== "success") return;