@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.
- package/dist/components/LockScreen/LockScreen.d.ts +13 -0
- package/dist/components/LockScreen/LockScreen.d.ts.map +1 -0
- package/dist/components/LockScreen/LockScreen.types.d.ts +73 -0
- package/dist/components/LockScreen/LockScreen.types.d.ts.map +1 -0
- package/dist/components/LockScreen/index.d.ts +3 -0
- package/dist/components/LockScreen/index.d.ts.map +1 -0
- package/dist/components/PinCodeInput/PinCodeInput.d.ts +11 -0
- package/dist/components/PinCodeInput/PinCodeInput.d.ts.map +1 -0
- package/dist/components/PinCodeInput/PinCodeInput.types.d.ts +26 -0
- package/dist/components/PinCodeInput/PinCodeInput.types.d.ts.map +1 -0
- package/dist/components/PinCodeInput/index.d.ts +3 -0
- package/dist/components/PinCodeInput/index.d.ts.map +1 -0
- package/dist/components/PinPad/PinPad.d.ts +10 -0
- package/dist/components/PinPad/PinPad.d.ts.map +1 -0
- package/dist/components/PinPad/PinPad.types.d.ts +24 -0
- package/dist/components/PinPad/PinPad.types.d.ts.map +1 -0
- package/dist/components/PinPad/index.d.ts +3 -0
- package/dist/components/PinPad/index.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useIdleLock.d.ts +24 -0
- package/dist/hooks/useIdleLock.d.ts.map +1 -0
- package/dist/hooks/useMediaQuery.d.ts +7 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +177 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +554 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +558 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/LockScreen/LockScreen.tsx +450 -0
- package/src/components/LockScreen/LockScreen.types.ts +75 -0
- package/src/components/LockScreen/index.ts +6 -0
- package/src/components/PinCodeInput/PinCodeInput.tsx +184 -0
- package/src/components/PinCodeInput/PinCodeInput.types.ts +25 -0
- package/src/components/PinCodeInput/index.ts +2 -0
- package/src/components/PinPad/PinPad.tsx +150 -0
- package/src/components/PinPad/PinPad.types.ts +23 -0
- package/src/components/PinPad/index.ts +2 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useIdleLock.ts +181 -0
- package/src/hooks/useMediaQuery.ts +31 -0
- package/src/index.ts +25 -2
- 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
|