@ews-admin/global-design-system 1.29.0 → 1.31.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.
Files changed (47) hide show
  1. package/dist/components/LockScreen/LockScreen.d.ts +13 -0
  2. package/dist/components/LockScreen/LockScreen.d.ts.map +1 -0
  3. package/dist/components/LockScreen/LockScreen.types.d.ts +73 -0
  4. package/dist/components/LockScreen/LockScreen.types.d.ts.map +1 -0
  5. package/dist/components/LockScreen/index.d.ts +3 -0
  6. package/dist/components/LockScreen/index.d.ts.map +1 -0
  7. package/dist/components/PinCodeInput/PinCodeInput.d.ts +11 -0
  8. package/dist/components/PinCodeInput/PinCodeInput.d.ts.map +1 -0
  9. package/dist/components/PinCodeInput/PinCodeInput.types.d.ts +26 -0
  10. package/dist/components/PinCodeInput/PinCodeInput.types.d.ts.map +1 -0
  11. package/dist/components/PinCodeInput/index.d.ts +3 -0
  12. package/dist/components/PinCodeInput/index.d.ts.map +1 -0
  13. package/dist/components/PinPad/PinPad.d.ts +10 -0
  14. package/dist/components/PinPad/PinPad.d.ts.map +1 -0
  15. package/dist/components/PinPad/PinPad.types.d.ts +24 -0
  16. package/dist/components/PinPad/PinPad.types.d.ts.map +1 -0
  17. package/dist/components/PinPad/index.d.ts +3 -0
  18. package/dist/components/PinPad/index.d.ts.map +1 -0
  19. package/dist/hooks/index.d.ts +3 -0
  20. package/dist/hooks/index.d.ts.map +1 -1
  21. package/dist/hooks/useIdleLock.d.ts +24 -0
  22. package/dist/hooks/useIdleLock.d.ts.map +1 -0
  23. package/dist/hooks/useMediaQuery.d.ts +7 -0
  24. package/dist/hooks/useMediaQuery.d.ts.map +1 -0
  25. package/dist/index.css +1 -1
  26. package/dist/index.d.ts +177 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.esm.css +1 -1
  29. package/dist/index.esm.js +554 -1
  30. package/dist/index.esm.js.map +1 -1
  31. package/dist/index.js +558 -0
  32. package/dist/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/components/LockScreen/LockScreen.tsx +450 -0
  35. package/src/components/LockScreen/LockScreen.types.ts +75 -0
  36. package/src/components/LockScreen/index.ts +6 -0
  37. package/src/components/PinCodeInput/PinCodeInput.tsx +184 -0
  38. package/src/components/PinCodeInput/PinCodeInput.types.ts +25 -0
  39. package/src/components/PinCodeInput/index.ts +2 -0
  40. package/src/components/PinPad/PinPad.tsx +150 -0
  41. package/src/components/PinPad/PinPad.types.ts +23 -0
  42. package/src/components/PinPad/index.ts +2 -0
  43. package/src/hooks/index.ts +3 -0
  44. package/src/hooks/useIdleLock.ts +181 -0
  45. package/src/hooks/useMediaQuery.ts +31 -0
  46. package/src/index.ts +25 -2
  47. package/tailwind.preset.js +11 -0
package/dist/index.js CHANGED
@@ -33083,6 +33083,182 @@ function useDebouncedCallback(callback, delay) {
33083
33083
  return debouncedCallback;
33084
33084
  }
33085
33085
 
33086
+ const matches = (query, fallback) => typeof window !== "undefined" && typeof window.matchMedia === "function"
33087
+ ? window.matchMedia(query).matches
33088
+ : fallback;
33089
+ /**
33090
+ * Whether a CSS media query matches, updated when it changes. Returns
33091
+ * `defaultValue` where `matchMedia` does not exist (server rendering, old
33092
+ * test environments).
33093
+ */
33094
+ const useMediaQuery = (query, defaultValue = false) => {
33095
+ const [value, setValue] = React.useState(() => matches(query, defaultValue));
33096
+ React.useEffect(() => {
33097
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function")
33098
+ return;
33099
+ const list = window.matchMedia(query);
33100
+ const onChange = () => setValue(list.matches);
33101
+ onChange();
33102
+ // Safari < 14 only has addListener.
33103
+ if (list.addEventListener) {
33104
+ list.addEventListener("change", onChange);
33105
+ return () => list.removeEventListener("change", onChange);
33106
+ }
33107
+ list.addListener(onChange);
33108
+ return () => list.removeListener(onChange);
33109
+ }, [query]);
33110
+ return value;
33111
+ };
33112
+
33113
+ const DEFAULT_TIMEOUT_MS = 180000;
33114
+ const DEFAULT_STORAGE_KEY = "ews-idle-lock";
33115
+ const ACTIVITY_WRITE_INTERVAL_MS = 5000;
33116
+ const ACTIVITY_EVENTS = [
33117
+ "pointerdown",
33118
+ "pointermove",
33119
+ "keydown",
33120
+ "wheel",
33121
+ "scroll",
33122
+ "touchstart",
33123
+ ];
33124
+ const readStorage = (key) => {
33125
+ try {
33126
+ return typeof window === "undefined" ? null : window.localStorage.getItem(key);
33127
+ }
33128
+ catch {
33129
+ return null;
33130
+ }
33131
+ };
33132
+ const writeStorage = (key, value) => {
33133
+ try {
33134
+ if (typeof window === "undefined")
33135
+ return;
33136
+ if (value === null)
33137
+ window.localStorage.removeItem(key);
33138
+ else
33139
+ window.localStorage.setItem(key, value);
33140
+ }
33141
+ catch {
33142
+ // Storage unavailable (private mode, quota): the lock still works in this tab.
33143
+ }
33144
+ };
33145
+ /**
33146
+ * Locks the screen after `timeoutMs` without user activity. The locked flag is
33147
+ * kept in localStorage (it survives a reload) and shared between tabs through
33148
+ * a BroadcastChannel, or the `storage` event where BroadcastChannel is missing:
33149
+ * locking or unlocking one tab does the same in every tab. Activity in any tab
33150
+ * keeps every tab unlocked.
33151
+ */
33152
+ function useIdleLock({ timeoutMs = DEFAULT_TIMEOUT_MS, onLock, storageKey = DEFAULT_STORAGE_KEY, enabled = true, } = {}) {
33153
+ const activityKey = `${storageKey}:activity`;
33154
+ const [locked, setLocked] = React.useState(() => readStorage(storageKey) === "1");
33155
+ const lockedRef = React.useRef(locked);
33156
+ const onLockRef = React.useRef(onLock);
33157
+ const lastActivity = React.useRef(Date.now());
33158
+ const lastActivityWrite = React.useRef(0);
33159
+ const channelRef = React.useRef(null);
33160
+ React.useEffect(() => {
33161
+ onLockRef.current = onLock;
33162
+ }, [onLock]);
33163
+ const markActivity = React.useCallback((now) => {
33164
+ lastActivity.current = now;
33165
+ if (now - lastActivityWrite.current >= ACTIVITY_WRITE_INTERVAL_MS) {
33166
+ lastActivityWrite.current = now;
33167
+ writeStorage(activityKey, String(now));
33168
+ }
33169
+ }, [activityKey]);
33170
+ const applyLocked = React.useCallback((next) => {
33171
+ if (lockedRef.current === next)
33172
+ return;
33173
+ lockedRef.current = next;
33174
+ setLocked(next);
33175
+ if (next)
33176
+ onLockRef.current?.();
33177
+ }, []);
33178
+ const lock = React.useCallback(() => {
33179
+ if (lockedRef.current)
33180
+ return;
33181
+ writeStorage(storageKey, "1");
33182
+ channelRef.current?.postMessage("lock");
33183
+ applyLocked(true);
33184
+ }, [storageKey, applyLocked]);
33185
+ const unlock = React.useCallback(() => {
33186
+ lastActivityWrite.current = 0;
33187
+ markActivity(Date.now());
33188
+ writeStorage(storageKey, null);
33189
+ channelRef.current?.postMessage("unlock");
33190
+ applyLocked(false);
33191
+ }, [storageKey, applyLocked, markActivity]);
33192
+ // Cross-tab sync.
33193
+ React.useEffect(() => {
33194
+ if (typeof window === "undefined")
33195
+ return;
33196
+ const onRemote = (nextLocked) => {
33197
+ if (!nextLocked)
33198
+ lastActivity.current = Date.now();
33199
+ applyLocked(nextLocked);
33200
+ };
33201
+ if (typeof window.BroadcastChannel === "function") {
33202
+ const channel = new window.BroadcastChannel(storageKey);
33203
+ channelRef.current = channel;
33204
+ channel.onmessage = (event) => {
33205
+ if (event.data === "lock")
33206
+ onRemote(true);
33207
+ else if (event.data === "unlock")
33208
+ onRemote(false);
33209
+ };
33210
+ return () => {
33211
+ channelRef.current = null;
33212
+ channel.close();
33213
+ };
33214
+ }
33215
+ const onStorage = (event) => {
33216
+ if (event.key !== storageKey)
33217
+ return;
33218
+ onRemote(event.newValue === "1");
33219
+ };
33220
+ window.addEventListener("storage", onStorage);
33221
+ return () => window.removeEventListener("storage", onStorage);
33222
+ }, [storageKey, applyLocked]);
33223
+ // Inactivity timer.
33224
+ React.useEffect(() => {
33225
+ if (!enabled || locked || typeof window === "undefined")
33226
+ return;
33227
+ lastActivity.current = Date.now();
33228
+ let timer;
33229
+ const idleFor = () => {
33230
+ const shared = Number(readStorage(activityKey)) || 0;
33231
+ return Date.now() - Math.max(lastActivity.current, shared);
33232
+ };
33233
+ const check = () => {
33234
+ const idle = idleFor();
33235
+ if (idle >= timeoutMs)
33236
+ lock();
33237
+ else
33238
+ timer = setTimeout(check, timeoutMs - idle);
33239
+ };
33240
+ const onActivity = () => markActivity(Date.now());
33241
+ // Timers are throttled in background tabs: check the delay on return.
33242
+ const onVisibility = () => {
33243
+ if (document.visibilityState !== "visible")
33244
+ return;
33245
+ if (idleFor() >= timeoutMs)
33246
+ lock();
33247
+ else
33248
+ onActivity();
33249
+ };
33250
+ timer = setTimeout(check, timeoutMs);
33251
+ ACTIVITY_EVENTS.forEach((name) => document.addEventListener(name, onActivity, { capture: true, passive: true }));
33252
+ document.addEventListener("visibilitychange", onVisibility);
33253
+ return () => {
33254
+ clearTimeout(timer);
33255
+ ACTIVITY_EVENTS.forEach((name) => document.removeEventListener(name, onActivity, { capture: true }));
33256
+ document.removeEventListener("visibilitychange", onVisibility);
33257
+ };
33258
+ }, [enabled, locked, timeoutMs, activityKey, lock, markActivity]);
33259
+ return { locked, lock, unlock };
33260
+ }
33261
+
33086
33262
  function useSelectField({ name, control, options: _options, rules, defaultValue, }) {
33087
33263
  const { field, fieldState: { error, invalid }, } = reactHookForm.useController({
33088
33264
  name,
@@ -35292,6 +35468,383 @@ const PaymentDueBanner = ({ title, subtitle, warning, ctaLabel, onCtaClick, onDi
35292
35468
  */
35293
35469
  const SecureLoader = ({ label, description }) => (jsxRuntime.jsx("div", { "data-testid": "secure-loader", className: "flex min-h-screen items-center justify-center bg-gray-100", children: jsxRuntime.jsxs("div", { className: "flex flex-col items-center rounded-lg bg-white p-6 shadow-lg", children: [jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx(Lock, { className: "h-16 w-16 text-gray-500" }), jsxRuntime.jsx("div", { className: "absolute bottom-2 right-1 h-6 w-6 animate-spin rounded-full border-4 border-gray-300 border-t-green-500" })] }), jsxRuntime.jsx("p", { className: "mt-4 text-lg text-gray-600", children: label }), description && (jsxRuntime.jsx("p", { className: "mt-2 text-sm text-gray-500", children: description }))] }) }));
35294
35470
 
35471
+ const DIGIT_ROWS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
35472
+ const keyClass = "flex h-16 w-16 items-center justify-center justify-self-center rounded-full text-3xl font-light text-ews-gray-900 " +
35473
+ "transition-colors hover:bg-ews-gray-100 active:bg-ews-gray-200 " +
35474
+ "focus-visible:ring-2 focus-visible:ring-ews-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent";
35475
+ /**
35476
+ * iOS-style PIN entry: a row of dots filled as digits are typed and a 3x4
35477
+ * keypad. Texts are provided by the caller (already translated).
35478
+ */
35479
+ const PinPad = ({ length = 4, onComplete, onChange, disabled = false, invalid = false, resetKey, captureKeyboard = true, eraseLabel, progressLabel, keypadLabel, className, }) => {
35480
+ const [value, setValue] = React.useState("");
35481
+ const valueRef = React.useRef("");
35482
+ const update = React.useCallback((next) => {
35483
+ valueRef.current = next;
35484
+ setValue(next);
35485
+ onChange?.(next);
35486
+ if (next.length === length)
35487
+ onComplete(next);
35488
+ }, [length, onChange, onComplete]);
35489
+ // Clear the typed digits when the caller asks for it.
35490
+ React.useEffect(() => {
35491
+ valueRef.current = "";
35492
+ setValue("");
35493
+ }, [resetKey]);
35494
+ const press = React.useCallback((digit) => {
35495
+ if (disabled || valueRef.current.length >= length)
35496
+ return;
35497
+ update(valueRef.current + digit);
35498
+ }, [disabled, length, update]);
35499
+ const erase = React.useCallback(() => {
35500
+ if (disabled || valueRef.current.length === 0)
35501
+ return;
35502
+ update(valueRef.current.slice(0, -1));
35503
+ }, [disabled, update]);
35504
+ React.useEffect(() => {
35505
+ if (!captureKeyboard)
35506
+ return;
35507
+ const onKeyDown = (event) => {
35508
+ if (event.ctrlKey || event.metaKey || event.altKey)
35509
+ return;
35510
+ const target = event.target;
35511
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA"))
35512
+ return;
35513
+ if (/^[0-9]$/.test(event.key)) {
35514
+ event.preventDefault();
35515
+ press(event.key);
35516
+ }
35517
+ else if (event.key === "Backspace") {
35518
+ event.preventDefault();
35519
+ erase();
35520
+ }
35521
+ };
35522
+ document.addEventListener("keydown", onKeyDown);
35523
+ return () => document.removeEventListener("keydown", onKeyDown);
35524
+ }, [captureKeyboard, press, erase]);
35525
+ return (jsxRuntime.jsxs("div", { className: cn("flex flex-col items-center gap-10", className), children: [jsxRuntime.jsx("div", { dir: "ltr", "aria-hidden": "true", "data-testid": "pin-dots", className: cn("flex gap-5", invalid && "animate-pin-shake"), children: Array.from({ length }, (_, index) => (jsxRuntime.jsx("span", { "data-filled": index < value.length, className: cn("h-3.5 w-3.5 rounded-full transition-colors", invalid
35526
+ ? "bg-ews-error"
35527
+ : index < value.length
35528
+ ? "bg-ews-primary"
35529
+ : "bg-ews-primary-light", disabled && index < value.length && "animate-pulse") }, index))) }), jsxRuntime.jsx("p", { className: "sr-only", "aria-live": "polite", "aria-atomic": "true", children: progressLabel(value.length, length) }), jsxRuntime.jsxs("div", { dir: "ltr", role: "group", "aria-label": keypadLabel, className: "grid grid-cols-3 gap-x-8 gap-y-4", children: [DIGIT_ROWS.map((digit) => (jsxRuntime.jsx("button", { type: "button", className: keyClass, disabled: disabled, onClick: () => press(digit), children: digit }, digit))), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("button", { type: "button", className: keyClass, disabled: disabled, onClick: () => press("0"), children: "0" }), jsxRuntime.jsx("button", { type: "button", className: cn(keyClass, "text-ews-gray-700"), disabled: disabled || value.length === 0, "aria-label": eraseLabel, onClick: erase, children: jsxRuntime.jsx(Delete, { className: "h-7 w-7", strokeWidth: 1.5, "aria-hidden": "true" }) })] })] }));
35530
+ };
35531
+
35532
+ const MASK = "•";
35533
+ const onlyDigits = (text) => text.replace(/\D/g, "");
35534
+ /**
35535
+ * Row of one-digit boxes for a PIN or a one-time code (desktop keyboards):
35536
+ * typing moves to the next box, Backspace goes back, arrows move, pasting a
35537
+ * code fills the boxes and `onComplete` fires once every box is filled.
35538
+ */
35539
+ const PinCodeInput = ({ length = 4, onComplete, onChange, masked = true, autoComplete = "off", autoFocus = true, disabled = false, invalid = false, resetKey, digitLabel, groupLabel, className, }) => {
35540
+ const empty = React.useCallback(() => Array.from({ length }, () => ""), [length]);
35541
+ const [digits, setDigits] = React.useState(empty);
35542
+ const digitsRef = React.useRef(digits);
35543
+ const inputs = React.useRef([]);
35544
+ const focusBox = (index) => {
35545
+ const input = inputs.current[Math.max(0, Math.min(index, length - 1))];
35546
+ input?.focus();
35547
+ input?.select();
35548
+ };
35549
+ const update = (next) => {
35550
+ digitsRef.current = next;
35551
+ setDigits(next);
35552
+ const value = next.join("");
35553
+ onChange?.(value);
35554
+ if (value.length === length && next.every(Boolean))
35555
+ onComplete(value);
35556
+ };
35557
+ /** Writes `chars` from box `start` on and focuses the box after them. */
35558
+ const fill = (start, chars) => {
35559
+ const next = [...digitsRef.current];
35560
+ const written = chars.slice(0, length - start).split("");
35561
+ written.forEach((char, offset) => {
35562
+ next[start + offset] = char;
35563
+ });
35564
+ focusBox(start + written.length);
35565
+ update(next);
35566
+ };
35567
+ // Clear the boxes when the caller asks for it.
35568
+ React.useEffect(() => {
35569
+ const cleared = empty();
35570
+ digitsRef.current = cleared;
35571
+ setDigits(cleared);
35572
+ }, [resetKey, empty]);
35573
+ // Focus the first empty box on mount, after a reset and once re-enabled.
35574
+ React.useEffect(() => {
35575
+ if (!autoFocus || disabled)
35576
+ return;
35577
+ const firstEmpty = digitsRef.current.findIndex((digit) => !digit);
35578
+ focusBox(firstEmpty === -1 ? length - 1 : firstEmpty);
35579
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35580
+ }, [autoFocus, disabled, resetKey, length]);
35581
+ const handleChange = (index, event) => {
35582
+ if (disabled)
35583
+ return;
35584
+ const { value, selectionStart } = event.target;
35585
+ const typed = onlyDigits(value);
35586
+ if (!typed) {
35587
+ const next = [...digitsRef.current];
35588
+ next[index] = "";
35589
+ update(next);
35590
+ return;
35591
+ }
35592
+ // A digit typed next to the one already in the box: keep the new one.
35593
+ if (digitsRef.current[index] && value.length === 2) {
35594
+ const inserted = value.charAt((selectionStart ?? value.length) - 1);
35595
+ if (/\d/.test(inserted)) {
35596
+ fill(index, inserted);
35597
+ return;
35598
+ }
35599
+ }
35600
+ // One digit, or a whole code (browser autofill of the one-time code).
35601
+ fill(typed.length >= length ? 0 : index, typed);
35602
+ };
35603
+ const handleKeyDown = (index, event) => {
35604
+ if (disabled)
35605
+ return;
35606
+ switch (event.key) {
35607
+ case "Backspace": {
35608
+ event.preventDefault();
35609
+ const next = [...digitsRef.current];
35610
+ const target = next[index] ? index : Math.max(0, index - 1);
35611
+ next[target] = "";
35612
+ focusBox(target);
35613
+ update(next);
35614
+ return;
35615
+ }
35616
+ case "Delete": {
35617
+ event.preventDefault();
35618
+ const next = [...digitsRef.current];
35619
+ next[index] = "";
35620
+ update(next);
35621
+ return;
35622
+ }
35623
+ case "ArrowLeft":
35624
+ event.preventDefault();
35625
+ focusBox(index - 1);
35626
+ return;
35627
+ case "ArrowRight":
35628
+ event.preventDefault();
35629
+ focusBox(index + 1);
35630
+ return;
35631
+ case "Home":
35632
+ event.preventDefault();
35633
+ focusBox(0);
35634
+ return;
35635
+ case "End":
35636
+ event.preventDefault();
35637
+ focusBox(length - 1);
35638
+ return;
35639
+ }
35640
+ };
35641
+ const handlePaste = (index, event) => {
35642
+ event.preventDefault();
35643
+ if (disabled)
35644
+ return;
35645
+ const pasted = onlyDigits(event.clipboardData.getData("text"));
35646
+ if (!pasted)
35647
+ return;
35648
+ fill(pasted.length >= length ? 0 : index, pasted);
35649
+ };
35650
+ return (
35651
+ // Codes stay left-to-right in RTL locales.
35652
+ jsxRuntime.jsx("div", { dir: "ltr", role: "group", "aria-label": groupLabel, "data-testid": "pin-code-input", className: cn("flex justify-center gap-3", invalid && "animate-pin-shake", className), children: digits.map((digit, index) => (jsxRuntime.jsx("input", { ref: (element) => {
35653
+ inputs.current[index] = element;
35654
+ }, type: "text", inputMode: "numeric", pattern: "[0-9]*", autoComplete: index === 0 ? autoComplete : "off", autoCorrect: "off", spellCheck: false, value: digit && masked ? MASK : digit, disabled: disabled, "aria-label": digitLabel(index + 1, length), "aria-invalid": invalid || undefined, "data-filled": !!digit, onChange: (event) => handleChange(index, event), onKeyDown: (event) => handleKeyDown(index, event), onPaste: (event) => handlePaste(index, event), onFocus: (event) => event.target.select(), className: cn("h-14 w-12 rounded-lg border bg-white text-center text-2xl font-medium text-ews-gray-900 outline-none", "transition-colors focus:border-ews-primary focus:ring-2 focus:ring-ews-primary", "disabled:cursor-not-allowed disabled:opacity-60", invalid ? "border-ews-error" : digit ? "border-ews-primary" : "border-ews-gray-300", disabled && digit && "animate-pulse") }, index))) }));
35655
+ };
35656
+
35657
+ const FIRST_STEP = {
35658
+ create: "new",
35659
+ unlock: "unlock",
35660
+ change: "current",
35661
+ reset: "otp",
35662
+ };
35663
+ const titleOf = (mode, step, texts) => {
35664
+ switch (step) {
35665
+ case "unlock":
35666
+ return texts.unlockTitle;
35667
+ case "current":
35668
+ return texts.currentTitle;
35669
+ case "otp":
35670
+ return texts.otpTitle;
35671
+ case "new":
35672
+ return mode === "create" ? texts.createTitle : texts.newTitle;
35673
+ case "confirm":
35674
+ return texts.confirmTitle;
35675
+ }
35676
+ };
35677
+ /** Same breakpoint as Tailwind's `sm`. */
35678
+ const DESKTOP_QUERY = "(min-width: 640px)";
35679
+ /** Open lock screens, oldest first; only the last one traps focus. */
35680
+ const trapStack = [];
35681
+ const FOCUSABLE = 'button:not([disabled]), input:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])';
35682
+ /**
35683
+ * Blocking PIN screen: creates, checks, changes or resets (with a one-time
35684
+ * code) the user's secret code. Below 640px: full screen with an on-screen
35685
+ * keypad. From 640px: compact dialog on a grey backdrop with one box per digit,
35686
+ * typed on the keyboard. Submit handlers return promises: a rejection clears
35687
+ * the code and goes back to the first step, the app shows its message in `error`.
35688
+ */
35689
+ const LockScreen = ({ mode, texts, error, busy = false, onCreate, onUnlock, onChangePin, onReset, onForgot, onCancel, onSignOut, subtitle, pinLength = 4, otpLength = 6, logo, className, }) => {
35690
+ const [step, setStep] = React.useState(FIRST_STEP[mode]);
35691
+ const [firstValue, setFirstValue] = React.useState(""); // current PIN or one-time code
35692
+ const [newPin, setNewPin] = React.useState("");
35693
+ const [otp, setOtp] = React.useState("");
35694
+ const [localError, setLocalError] = React.useState(null);
35695
+ const [invalid, setInvalid] = React.useState(false);
35696
+ const [pending, setPending] = React.useState(false);
35697
+ const [resetKey, setResetKey] = React.useState(0);
35698
+ const panelRef = React.useRef(null);
35699
+ const mounted = React.useRef(true);
35700
+ const isDesktop = useMediaQuery(DESKTOP_QUERY);
35701
+ const restart = React.useCallback((failed) => {
35702
+ setStep(FIRST_STEP[mode]);
35703
+ setFirstValue("");
35704
+ setNewPin("");
35705
+ setOtp("");
35706
+ setInvalid(failed);
35707
+ setResetKey((key) => key + 1);
35708
+ }, [mode]);
35709
+ React.useEffect(() => {
35710
+ restart(false);
35711
+ setLocalError(null);
35712
+ }, [restart]);
35713
+ React.useEffect(() => {
35714
+ mounted.current = true;
35715
+ return () => {
35716
+ mounted.current = false;
35717
+ };
35718
+ }, []);
35719
+ // Block page scroll and keep focus inside the screen. Only the most recently
35720
+ // mounted lock screen traps focus: two traps pulling focus back to their own
35721
+ // panel would loop forever (e.g. a Storybook docs page rendering every story).
35722
+ React.useEffect(() => {
35723
+ const token = {};
35724
+ trapStack.push(token);
35725
+ const previousOverflow = document.body.style.overflow;
35726
+ document.body.style.overflow = "hidden";
35727
+ const previousFocus = document.activeElement;
35728
+ // The desktop code boxes focus themselves first.
35729
+ if (!panelRef.current?.contains(document.activeElement))
35730
+ panelRef.current?.focus();
35731
+ const keepFocus = (event) => {
35732
+ if (trapStack[trapStack.length - 1] !== token)
35733
+ return;
35734
+ const panel = panelRef.current;
35735
+ if (panel && !panel.contains(event.target))
35736
+ panel.focus();
35737
+ };
35738
+ document.addEventListener("focusin", keepFocus);
35739
+ return () => {
35740
+ const wasTop = trapStack[trapStack.length - 1] === token;
35741
+ trapStack.splice(trapStack.indexOf(token), 1);
35742
+ document.body.style.overflow = previousOverflow;
35743
+ document.removeEventListener("focusin", keepFocus);
35744
+ if (wasTop)
35745
+ previousFocus?.focus?.();
35746
+ };
35747
+ }, []);
35748
+ const trapTab = (event) => {
35749
+ if (event.key !== "Tab" || !panelRef.current)
35750
+ return;
35751
+ const items = Array.from(panelRef.current.querySelectorAll(FOCUSABLE));
35752
+ if (items.length === 0) {
35753
+ event.preventDefault();
35754
+ return;
35755
+ }
35756
+ const first = items[0];
35757
+ const last = items[items.length - 1];
35758
+ const active = document.activeElement;
35759
+ if (event.shiftKey && (active === first || active === panelRef.current)) {
35760
+ event.preventDefault();
35761
+ last.focus();
35762
+ }
35763
+ else if (!event.shiftKey && active === last) {
35764
+ event.preventDefault();
35765
+ first.focus();
35766
+ }
35767
+ };
35768
+ const submit = async (call) => {
35769
+ if (!call)
35770
+ return;
35771
+ setPending(true);
35772
+ try {
35773
+ await call();
35774
+ if (mounted.current)
35775
+ restart(false);
35776
+ }
35777
+ catch {
35778
+ if (mounted.current)
35779
+ restart(true);
35780
+ }
35781
+ finally {
35782
+ if (mounted.current)
35783
+ setPending(false);
35784
+ }
35785
+ };
35786
+ const handlePin = (pin) => {
35787
+ setLocalError(null);
35788
+ switch (step) {
35789
+ case "unlock":
35790
+ void submit(onUnlock && (() => onUnlock(pin)));
35791
+ return;
35792
+ case "current":
35793
+ setFirstValue(pin);
35794
+ setStep("new");
35795
+ return;
35796
+ case "new":
35797
+ setNewPin(pin);
35798
+ setStep("confirm");
35799
+ return;
35800
+ case "confirm":
35801
+ if (pin !== newPin) {
35802
+ setLocalError(texts.mismatchError);
35803
+ setInvalid(true);
35804
+ setNewPin("");
35805
+ setStep("new");
35806
+ setResetKey((key) => key + 1);
35807
+ return;
35808
+ }
35809
+ if (mode === "create")
35810
+ void submit(onCreate && (() => onCreate(pin)));
35811
+ else if (mode === "change")
35812
+ void submit(onChangePin && (() => onChangePin(firstValue, pin)));
35813
+ else if (mode === "reset")
35814
+ void submit(onReset && (() => onReset(firstValue, pin)));
35815
+ return;
35816
+ }
35817
+ };
35818
+ const submitOtp = (event) => {
35819
+ event.preventDefault();
35820
+ if (otp.length !== otpLength)
35821
+ return;
35822
+ setFirstValue(otp);
35823
+ setInvalid(false);
35824
+ setStep("new");
35825
+ };
35826
+ const completeOtp = (code) => {
35827
+ setLocalError(null);
35828
+ setFirstValue(code);
35829
+ setInvalid(false);
35830
+ setStep("new");
35831
+ };
35832
+ const isBusy = busy || pending;
35833
+ const shownError = localError ?? error;
35834
+ const showForgot = !!onForgot &&
35835
+ !!texts.forgot &&
35836
+ (step === "unlock" || step === "current");
35837
+ const title = titleOf(mode, step, texts);
35838
+ const digitLabel = texts.digit ?? texts.progress;
35839
+ const desktopContent = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "pb-5", children: logo ?? jsxRuntime.jsx(Logo, { size: "sm", variant: "normal" }) }), title && (jsxRuntime.jsx("h2", { className: "text-center text-xl font-semibold text-ews-gray-900", children: title })), subtitle && (jsxRuntime.jsx("p", { className: "mt-1 text-center text-sm text-ews-gray-600", children: subtitle })), step === "otp" && texts.otpDescription && (jsxRuntime.jsx("p", { className: "mt-2 text-center text-sm text-ews-gray-600", children: texts.otpDescription })), jsxRuntime.jsx("div", { className: "mt-6", children: step === "otp" ? (jsxRuntime.jsx(PinCodeInput, { length: otpLength, masked: false, autoComplete: "one-time-code", onComplete: completeOtp, onChange: () => setInvalid(false), disabled: isBusy, invalid: invalid, resetKey: resetKey, digitLabel: digitLabel, groupLabel: texts.otpLabel ?? title }, step)) : (jsxRuntime.jsx(PinCodeInput, { length: pinLength, onComplete: handlePin, onChange: () => setInvalid(false), disabled: isBusy, invalid: invalid, resetKey: resetKey, digitLabel: digitLabel, groupLabel: title }, step)) }), jsxRuntime.jsx("p", { role: "alert", className: "mt-3 min-h-[1.25rem] text-center text-sm text-ews-error", children: shownError }), (showForgot || (onCancel && texts.cancel) || (onSignOut && texts.signOut)) && (jsxRuntime.jsxs("div", { className: "mt-4 flex w-full flex-col items-center gap-2 border-t border-ews-gray-200 pt-4", children: [showForgot && (jsxRuntime.jsx("button", { type: "button", onClick: onForgot, disabled: isBusy, className: "text-sm font-medium text-ews-primary hover:underline focus-visible:underline", children: texts.forgot })), onCancel && texts.cancel && (jsxRuntime.jsx("button", { type: "button", onClick: onCancel, disabled: isBusy, className: "text-sm text-ews-gray-600 hover:underline focus-visible:underline", children: texts.cancel })), onSignOut && texts.signOut && (jsxRuntime.jsx("button", { type: "button", onClick: onSignOut, disabled: isBusy, className: "text-sm text-ews-gray-600 hover:underline focus-visible:underline", children: texts.signOut }))] }))] }));
35840
+ return (jsxRuntime.jsx("div", { className: cn("fixed inset-0 z-[1000] flex justify-center overflow-y-auto bg-white", "sm:items-center sm:bg-ews-gray-900/50 sm:backdrop-blur-sm", isDesktop && "p-4", className), "data-testid": "lock-screen", "data-layout": isDesktop ? "desktop" : "mobile", children: jsxRuntime.jsx("div", { ref: panelRef, role: "dialog", "aria-modal": "true", "aria-label": texts.dialogLabel, "aria-busy": isBusy, tabIndex: -1, onKeyDown: trapTab, className: isDesktop
35841
+ ? "flex w-full max-w-[400px] flex-col items-center rounded-2xl bg-white px-8 py-8 shadow-xl outline-none"
35842
+ : cn("flex min-h-full w-full flex-col items-center px-6 pb-8 outline-none", "sm:min-h-0 sm:w-auto sm:min-w-[22rem] sm:rounded-2xl sm:bg-white sm:px-10 sm:py-10 sm:shadow-xl"), children: isDesktop ? (desktopContent) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "flex min-h-[33vh] shrink-0 items-center justify-center sm:min-h-0 sm:pb-8", children: logo ?? jsxRuntime.jsx(Logo, { size: "lg", variant: "normal" }) }), title && (jsxRuntime.jsx("h2", { className: "mb-8 text-center text-lg font-normal text-ews-gray-900", children: title })), step === "otp" ? (jsxRuntime.jsxs("form", { onSubmit: submitOtp, className: "flex w-full max-w-xs flex-col items-center gap-4", children: [texts.otpDescription && (jsxRuntime.jsx("p", { className: "text-center text-sm text-ews-gray-600", children: texts.otpDescription })), jsxRuntime.jsx("label", { className: "sr-only", htmlFor: "lock-screen-otp", children: texts.otpLabel }), jsxRuntime.jsx("input", { id: "lock-screen-otp", dir: "ltr", inputMode: "numeric", autoComplete: "one-time-code", maxLength: otpLength, value: otp, disabled: isBusy, "aria-label": texts.otpLabel, "aria-invalid": invalid || undefined, onChange: (event) => {
35843
+ setOtp(event.target.value.replace(/\D/g, "").slice(0, otpLength));
35844
+ setInvalid(false);
35845
+ }, className: cn("w-full rounded-lg border bg-white px-4 py-3 text-center text-2xl tracking-[0.5em]", "focus:border-ews-primary focus:ring-2 focus:ring-ews-primary", invalid ? "border-ews-error" : "border-ews-gray-300") }), jsxRuntime.jsx(Button, { type: "submit", variant: "ews-primary", fullWidth: true, disabled: otp.length !== otpLength || isBusy, children: texts.otpSubmit })] })) : (jsxRuntime.jsx(PinPad, { length: pinLength, onComplete: handlePin, onChange: () => setInvalid(false), disabled: isBusy, invalid: invalid, resetKey: resetKey, eraseLabel: texts.erase, progressLabel: texts.progress, keypadLabel: texts.keypad }, step)), jsxRuntime.jsx("p", { role: "alert", className: "mt-6 min-h-[1.25rem] text-center text-sm text-ews-error", children: shownError }), jsxRuntime.jsxs("div", { className: "mt-auto flex flex-col items-center gap-3 pt-6 sm:mt-4", children: [showForgot && (jsxRuntime.jsx("button", { type: "button", onClick: onForgot, disabled: isBusy, className: "text-base font-medium text-ews-primary hover:underline focus-visible:underline", children: texts.forgot })), onCancel && texts.cancel && (jsxRuntime.jsx("button", { type: "button", onClick: onCancel, disabled: isBusy, className: "text-sm text-ews-gray-600 hover:underline focus-visible:underline", children: texts.cancel }))] })] })) }) }));
35846
+ };
35847
+
35295
35848
  const SpecialtySearchAutocomplete = ({ selectedSpecialties = [], onSpecialtiesChange, specialties: availableSpecialties, placeholder = "Search and select medical specialties...", className = "", disabled = false, maxSelections, showSelectedCount = true, title = "Medical Specialties", label = "Select Specialties", getSelectedCountText, removeSpecialtyLabel = "Remove specialty", }) => {
35296
35849
  const [filteredSpecialties, setFilteredSpecialties] = React.useState([]);
35297
35850
  const [isLoading, setIsLoading] = React.useState(false);
@@ -37406,6 +37959,7 @@ exports.LockKeyholeOpen = LockKeyholeOpen;
37406
37959
  exports.LockKeyholeOpenIcon = LockKeyholeOpen;
37407
37960
  exports.LockOpen = LockOpen;
37408
37961
  exports.LockOpenIcon = LockOpen;
37962
+ exports.LockScreen = LockScreen;
37409
37963
  exports.LogIn = LogIn;
37410
37964
  exports.LogInIcon = LogIn;
37411
37965
  exports.LogOut = LogOut;
@@ -39788,9 +40342,11 @@ exports.PillBottle = PillBottle;
39788
40342
  exports.PillBottleIcon = PillBottle;
39789
40343
  exports.PillIcon = Pill;
39790
40344
  exports.Pin = Pin;
40345
+ exports.PinCodeInput = PinCodeInput;
39791
40346
  exports.PinIcon = Pin;
39792
40347
  exports.PinOff = PinOff;
39793
40348
  exports.PinOffIcon = PinOff;
40349
+ exports.PinPad = PinPad;
39794
40350
  exports.Pipette = Pipette;
39795
40351
  exports.PipetteIcon = Pipette;
39796
40352
  exports.Pizza = Pizza;
@@ -40964,6 +41520,8 @@ exports.isValidPhoneNumber = isValidPhoneNumber;
40964
41520
  exports.parseDateSafe = parseDateSafe;
40965
41521
  exports.useDebounce = useDebounce;
40966
41522
  exports.useDebouncedCallback = useDebouncedCallback;
41523
+ exports.useIdleLock = useIdleLock;
41524
+ exports.useMediaQuery = useMediaQuery;
40967
41525
  exports.useSelectField = useSelectField;
40968
41526
  exports.useTheme = useTheme;
40969
41527
  //# sourceMappingURL=index.js.map