@ews-admin/global-design-system 1.28.0 → 1.30.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 +12 -0
- package/dist/components/LockScreen/LockScreen.d.ts.map +1 -0
- package/dist/components/LockScreen/LockScreen.types.d.ts +65 -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/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 +2 -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/index.css +1 -1
- package/dist/index.d.ts +133 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +408 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +410 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/env-config.d.ts +5 -0
- package/dist/utils/env-config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/LockScreen/LockScreen.tsx +324 -0
- package/src/components/LockScreen/LockScreen.types.ts +67 -0
- package/src/components/LockScreen/index.ts +6 -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 +2 -0
- package/src/hooks/useIdleLock.ts +181 -0
- package/src/index.ts +21 -2
- package/src/utils/env-config.ts +34 -1
- package/tailwind.preset.js +11 -0
package/dist/index.js
CHANGED
|
@@ -58,6 +58,29 @@ function buildUrls(environment) {
|
|
|
58
58
|
adminUrl: `https://${prefix}admin.${domain}`,
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Derives the environment from the host the app is served from, for builds
|
|
63
|
+
* that did not set VITE_ENV. Hosts actually served (Traefik rules in
|
|
64
|
+
* infrastructure/medecine360-hetzner/deployment/docker-stack.*.yml):
|
|
65
|
+
* - local: localhost, 127.0.0.1, *.medecine360local.com (local.app., api., ...)
|
|
66
|
+
* - dev: dev.{app,login,dashboard,admin,api}.medecine360.com
|
|
67
|
+
* - qa: qa.{app,login,dashboard,admin,api}.medecine360.com
|
|
68
|
+
* - prod: medecine360.com, {login,dashboard,admin,api}.medecine360.com
|
|
69
|
+
* Any other host (or no window, e.g. SSR or tests) falls back to local.
|
|
70
|
+
*/
|
|
71
|
+
function environmentFromHost() {
|
|
72
|
+
const hostname = typeof window !== "undefined" ? window.location?.hostname?.toLowerCase() : undefined;
|
|
73
|
+
if (!hostname)
|
|
74
|
+
return "local";
|
|
75
|
+
if (hostname === "medecine360.com" || hostname.endsWith(".medecine360.com")) {
|
|
76
|
+
if (hostname.startsWith("dev."))
|
|
77
|
+
return "dev";
|
|
78
|
+
if (hostname.startsWith("qa."))
|
|
79
|
+
return "qa";
|
|
80
|
+
return "prod";
|
|
81
|
+
}
|
|
82
|
+
return "local";
|
|
83
|
+
}
|
|
61
84
|
/**
|
|
62
85
|
* Creates an environment configuration with all derived URLs.
|
|
63
86
|
*
|
|
@@ -67,11 +90,20 @@ function buildUrls(environment) {
|
|
|
67
90
|
* For local development, sensible defaults are provided but can be overridden
|
|
68
91
|
* (e.g. to point BFF URL at a mock server).
|
|
69
92
|
*
|
|
93
|
+
* When env is empty (build without VITE_ENV), the environment is derived from
|
|
94
|
+
* the current host instead of always assuming local, so a deployed build never
|
|
95
|
+
* links to http://local.*.medecine360local.com. A console warning names the
|
|
96
|
+
* derived environment so the misconfigured build stays visible.
|
|
97
|
+
*
|
|
70
98
|
* @param env - The environment name (VITE_ENV value)
|
|
71
99
|
* @param overrides - Optional URL overrides (e.g. for local mock servers)
|
|
72
100
|
*/
|
|
73
101
|
function createEnvConfig(env, overrides) {
|
|
74
|
-
|
|
102
|
+
let environment = env;
|
|
103
|
+
if (!environment) {
|
|
104
|
+
environment = environmentFromHost();
|
|
105
|
+
console.warn(`[env-config] VITE_ENV is not set; using "${environment}" derived from the current host.`);
|
|
106
|
+
}
|
|
75
107
|
const derived = buildUrls(environment);
|
|
76
108
|
return {
|
|
77
109
|
env: environment,
|
|
@@ -33051,6 +33083,155 @@ function useDebouncedCallback(callback, delay) {
|
|
|
33051
33083
|
return debouncedCallback;
|
|
33052
33084
|
}
|
|
33053
33085
|
|
|
33086
|
+
const DEFAULT_TIMEOUT_MS = 180000;
|
|
33087
|
+
const DEFAULT_STORAGE_KEY = "ews-idle-lock";
|
|
33088
|
+
const ACTIVITY_WRITE_INTERVAL_MS = 5000;
|
|
33089
|
+
const ACTIVITY_EVENTS = [
|
|
33090
|
+
"pointerdown",
|
|
33091
|
+
"pointermove",
|
|
33092
|
+
"keydown",
|
|
33093
|
+
"wheel",
|
|
33094
|
+
"scroll",
|
|
33095
|
+
"touchstart",
|
|
33096
|
+
];
|
|
33097
|
+
const readStorage = (key) => {
|
|
33098
|
+
try {
|
|
33099
|
+
return typeof window === "undefined" ? null : window.localStorage.getItem(key);
|
|
33100
|
+
}
|
|
33101
|
+
catch {
|
|
33102
|
+
return null;
|
|
33103
|
+
}
|
|
33104
|
+
};
|
|
33105
|
+
const writeStorage = (key, value) => {
|
|
33106
|
+
try {
|
|
33107
|
+
if (typeof window === "undefined")
|
|
33108
|
+
return;
|
|
33109
|
+
if (value === null)
|
|
33110
|
+
window.localStorage.removeItem(key);
|
|
33111
|
+
else
|
|
33112
|
+
window.localStorage.setItem(key, value);
|
|
33113
|
+
}
|
|
33114
|
+
catch {
|
|
33115
|
+
// Storage unavailable (private mode, quota): the lock still works in this tab.
|
|
33116
|
+
}
|
|
33117
|
+
};
|
|
33118
|
+
/**
|
|
33119
|
+
* Locks the screen after `timeoutMs` without user activity. The locked flag is
|
|
33120
|
+
* kept in localStorage (it survives a reload) and shared between tabs through
|
|
33121
|
+
* a BroadcastChannel, or the `storage` event where BroadcastChannel is missing:
|
|
33122
|
+
* locking or unlocking one tab does the same in every tab. Activity in any tab
|
|
33123
|
+
* keeps every tab unlocked.
|
|
33124
|
+
*/
|
|
33125
|
+
function useIdleLock({ timeoutMs = DEFAULT_TIMEOUT_MS, onLock, storageKey = DEFAULT_STORAGE_KEY, enabled = true, } = {}) {
|
|
33126
|
+
const activityKey = `${storageKey}:activity`;
|
|
33127
|
+
const [locked, setLocked] = React.useState(() => readStorage(storageKey) === "1");
|
|
33128
|
+
const lockedRef = React.useRef(locked);
|
|
33129
|
+
const onLockRef = React.useRef(onLock);
|
|
33130
|
+
const lastActivity = React.useRef(Date.now());
|
|
33131
|
+
const lastActivityWrite = React.useRef(0);
|
|
33132
|
+
const channelRef = React.useRef(null);
|
|
33133
|
+
React.useEffect(() => {
|
|
33134
|
+
onLockRef.current = onLock;
|
|
33135
|
+
}, [onLock]);
|
|
33136
|
+
const markActivity = React.useCallback((now) => {
|
|
33137
|
+
lastActivity.current = now;
|
|
33138
|
+
if (now - lastActivityWrite.current >= ACTIVITY_WRITE_INTERVAL_MS) {
|
|
33139
|
+
lastActivityWrite.current = now;
|
|
33140
|
+
writeStorage(activityKey, String(now));
|
|
33141
|
+
}
|
|
33142
|
+
}, [activityKey]);
|
|
33143
|
+
const applyLocked = React.useCallback((next) => {
|
|
33144
|
+
if (lockedRef.current === next)
|
|
33145
|
+
return;
|
|
33146
|
+
lockedRef.current = next;
|
|
33147
|
+
setLocked(next);
|
|
33148
|
+
if (next)
|
|
33149
|
+
onLockRef.current?.();
|
|
33150
|
+
}, []);
|
|
33151
|
+
const lock = React.useCallback(() => {
|
|
33152
|
+
if (lockedRef.current)
|
|
33153
|
+
return;
|
|
33154
|
+
writeStorage(storageKey, "1");
|
|
33155
|
+
channelRef.current?.postMessage("lock");
|
|
33156
|
+
applyLocked(true);
|
|
33157
|
+
}, [storageKey, applyLocked]);
|
|
33158
|
+
const unlock = React.useCallback(() => {
|
|
33159
|
+
lastActivityWrite.current = 0;
|
|
33160
|
+
markActivity(Date.now());
|
|
33161
|
+
writeStorage(storageKey, null);
|
|
33162
|
+
channelRef.current?.postMessage("unlock");
|
|
33163
|
+
applyLocked(false);
|
|
33164
|
+
}, [storageKey, applyLocked, markActivity]);
|
|
33165
|
+
// Cross-tab sync.
|
|
33166
|
+
React.useEffect(() => {
|
|
33167
|
+
if (typeof window === "undefined")
|
|
33168
|
+
return;
|
|
33169
|
+
const onRemote = (nextLocked) => {
|
|
33170
|
+
if (!nextLocked)
|
|
33171
|
+
lastActivity.current = Date.now();
|
|
33172
|
+
applyLocked(nextLocked);
|
|
33173
|
+
};
|
|
33174
|
+
if (typeof window.BroadcastChannel === "function") {
|
|
33175
|
+
const channel = new window.BroadcastChannel(storageKey);
|
|
33176
|
+
channelRef.current = channel;
|
|
33177
|
+
channel.onmessage = (event) => {
|
|
33178
|
+
if (event.data === "lock")
|
|
33179
|
+
onRemote(true);
|
|
33180
|
+
else if (event.data === "unlock")
|
|
33181
|
+
onRemote(false);
|
|
33182
|
+
};
|
|
33183
|
+
return () => {
|
|
33184
|
+
channelRef.current = null;
|
|
33185
|
+
channel.close();
|
|
33186
|
+
};
|
|
33187
|
+
}
|
|
33188
|
+
const onStorage = (event) => {
|
|
33189
|
+
if (event.key !== storageKey)
|
|
33190
|
+
return;
|
|
33191
|
+
onRemote(event.newValue === "1");
|
|
33192
|
+
};
|
|
33193
|
+
window.addEventListener("storage", onStorage);
|
|
33194
|
+
return () => window.removeEventListener("storage", onStorage);
|
|
33195
|
+
}, [storageKey, applyLocked]);
|
|
33196
|
+
// Inactivity timer.
|
|
33197
|
+
React.useEffect(() => {
|
|
33198
|
+
if (!enabled || locked || typeof window === "undefined")
|
|
33199
|
+
return;
|
|
33200
|
+
lastActivity.current = Date.now();
|
|
33201
|
+
let timer;
|
|
33202
|
+
const idleFor = () => {
|
|
33203
|
+
const shared = Number(readStorage(activityKey)) || 0;
|
|
33204
|
+
return Date.now() - Math.max(lastActivity.current, shared);
|
|
33205
|
+
};
|
|
33206
|
+
const check = () => {
|
|
33207
|
+
const idle = idleFor();
|
|
33208
|
+
if (idle >= timeoutMs)
|
|
33209
|
+
lock();
|
|
33210
|
+
else
|
|
33211
|
+
timer = setTimeout(check, timeoutMs - idle);
|
|
33212
|
+
};
|
|
33213
|
+
const onActivity = () => markActivity(Date.now());
|
|
33214
|
+
// Timers are throttled in background tabs: check the delay on return.
|
|
33215
|
+
const onVisibility = () => {
|
|
33216
|
+
if (document.visibilityState !== "visible")
|
|
33217
|
+
return;
|
|
33218
|
+
if (idleFor() >= timeoutMs)
|
|
33219
|
+
lock();
|
|
33220
|
+
else
|
|
33221
|
+
onActivity();
|
|
33222
|
+
};
|
|
33223
|
+
timer = setTimeout(check, timeoutMs);
|
|
33224
|
+
ACTIVITY_EVENTS.forEach((name) => document.addEventListener(name, onActivity, { capture: true, passive: true }));
|
|
33225
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
33226
|
+
return () => {
|
|
33227
|
+
clearTimeout(timer);
|
|
33228
|
+
ACTIVITY_EVENTS.forEach((name) => document.removeEventListener(name, onActivity, { capture: true }));
|
|
33229
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
33230
|
+
};
|
|
33231
|
+
}, [enabled, locked, timeoutMs, activityKey, lock, markActivity]);
|
|
33232
|
+
return { locked, lock, unlock };
|
|
33233
|
+
}
|
|
33234
|
+
|
|
33054
33235
|
function useSelectField({ name, control, options: _options, rules, defaultValue, }) {
|
|
33055
33236
|
const { field, fieldState: { error, invalid }, } = reactHookForm.useController({
|
|
33056
33237
|
name,
|
|
@@ -35260,6 +35441,231 @@ const PaymentDueBanner = ({ title, subtitle, warning, ctaLabel, onCtaClick, onDi
|
|
|
35260
35441
|
*/
|
|
35261
35442
|
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 }))] }) }));
|
|
35262
35443
|
|
|
35444
|
+
const DIGIT_ROWS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
|
35445
|
+
const keyClass = "flex h-16 w-16 items-center justify-center justify-self-center rounded-full text-3xl font-light text-ews-gray-900 " +
|
|
35446
|
+
"transition-colors hover:bg-ews-gray-100 active:bg-ews-gray-200 " +
|
|
35447
|
+
"focus-visible:ring-2 focus-visible:ring-ews-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent";
|
|
35448
|
+
/**
|
|
35449
|
+
* iOS-style PIN entry: a row of dots filled as digits are typed and a 3x4
|
|
35450
|
+
* keypad. Texts are provided by the caller (already translated).
|
|
35451
|
+
*/
|
|
35452
|
+
const PinPad = ({ length = 4, onComplete, onChange, disabled = false, invalid = false, resetKey, captureKeyboard = true, eraseLabel, progressLabel, keypadLabel, className, }) => {
|
|
35453
|
+
const [value, setValue] = React.useState("");
|
|
35454
|
+
const valueRef = React.useRef("");
|
|
35455
|
+
const update = React.useCallback((next) => {
|
|
35456
|
+
valueRef.current = next;
|
|
35457
|
+
setValue(next);
|
|
35458
|
+
onChange?.(next);
|
|
35459
|
+
if (next.length === length)
|
|
35460
|
+
onComplete(next);
|
|
35461
|
+
}, [length, onChange, onComplete]);
|
|
35462
|
+
// Clear the typed digits when the caller asks for it.
|
|
35463
|
+
React.useEffect(() => {
|
|
35464
|
+
valueRef.current = "";
|
|
35465
|
+
setValue("");
|
|
35466
|
+
}, [resetKey]);
|
|
35467
|
+
const press = React.useCallback((digit) => {
|
|
35468
|
+
if (disabled || valueRef.current.length >= length)
|
|
35469
|
+
return;
|
|
35470
|
+
update(valueRef.current + digit);
|
|
35471
|
+
}, [disabled, length, update]);
|
|
35472
|
+
const erase = React.useCallback(() => {
|
|
35473
|
+
if (disabled || valueRef.current.length === 0)
|
|
35474
|
+
return;
|
|
35475
|
+
update(valueRef.current.slice(0, -1));
|
|
35476
|
+
}, [disabled, update]);
|
|
35477
|
+
React.useEffect(() => {
|
|
35478
|
+
if (!captureKeyboard)
|
|
35479
|
+
return;
|
|
35480
|
+
const onKeyDown = (event) => {
|
|
35481
|
+
if (event.ctrlKey || event.metaKey || event.altKey)
|
|
35482
|
+
return;
|
|
35483
|
+
const target = event.target;
|
|
35484
|
+
if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA"))
|
|
35485
|
+
return;
|
|
35486
|
+
if (/^[0-9]$/.test(event.key)) {
|
|
35487
|
+
event.preventDefault();
|
|
35488
|
+
press(event.key);
|
|
35489
|
+
}
|
|
35490
|
+
else if (event.key === "Backspace") {
|
|
35491
|
+
event.preventDefault();
|
|
35492
|
+
erase();
|
|
35493
|
+
}
|
|
35494
|
+
};
|
|
35495
|
+
document.addEventListener("keydown", onKeyDown);
|
|
35496
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
35497
|
+
}, [captureKeyboard, press, erase]);
|
|
35498
|
+
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
|
|
35499
|
+
? "bg-ews-error"
|
|
35500
|
+
: index < value.length
|
|
35501
|
+
? "bg-ews-primary"
|
|
35502
|
+
: "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" }) })] })] }));
|
|
35503
|
+
};
|
|
35504
|
+
|
|
35505
|
+
const FIRST_STEP = {
|
|
35506
|
+
create: "new",
|
|
35507
|
+
unlock: "unlock",
|
|
35508
|
+
change: "current",
|
|
35509
|
+
reset: "otp",
|
|
35510
|
+
};
|
|
35511
|
+
const titleOf = (mode, step, texts) => {
|
|
35512
|
+
switch (step) {
|
|
35513
|
+
case "unlock":
|
|
35514
|
+
return texts.unlockTitle;
|
|
35515
|
+
case "current":
|
|
35516
|
+
return texts.currentTitle;
|
|
35517
|
+
case "otp":
|
|
35518
|
+
return texts.otpTitle;
|
|
35519
|
+
case "new":
|
|
35520
|
+
return mode === "create" ? texts.createTitle : texts.newTitle;
|
|
35521
|
+
case "confirm":
|
|
35522
|
+
return texts.confirmTitle;
|
|
35523
|
+
}
|
|
35524
|
+
};
|
|
35525
|
+
const FOCUSABLE = 'button:not([disabled]), input:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])';
|
|
35526
|
+
/**
|
|
35527
|
+
* Blocking PIN screen: creates, checks, changes or resets (with a one-time
|
|
35528
|
+
* code) the user's secret code. Full screen on mobile, centered card on a grey
|
|
35529
|
+
* backdrop from 640px. Submit handlers return promises: a rejection clears the
|
|
35530
|
+
* code and goes back to the first step, the app shows its message in `error`.
|
|
35531
|
+
*/
|
|
35532
|
+
const LockScreen = ({ mode, texts, error, busy = false, onCreate, onUnlock, onChangePin, onReset, onForgot, onCancel, pinLength = 4, otpLength = 6, logo, className, }) => {
|
|
35533
|
+
const [step, setStep] = React.useState(FIRST_STEP[mode]);
|
|
35534
|
+
const [firstValue, setFirstValue] = React.useState(""); // current PIN or one-time code
|
|
35535
|
+
const [newPin, setNewPin] = React.useState("");
|
|
35536
|
+
const [otp, setOtp] = React.useState("");
|
|
35537
|
+
const [localError, setLocalError] = React.useState(null);
|
|
35538
|
+
const [invalid, setInvalid] = React.useState(false);
|
|
35539
|
+
const [pending, setPending] = React.useState(false);
|
|
35540
|
+
const [resetKey, setResetKey] = React.useState(0);
|
|
35541
|
+
const panelRef = React.useRef(null);
|
|
35542
|
+
const mounted = React.useRef(true);
|
|
35543
|
+
const restart = React.useCallback((failed) => {
|
|
35544
|
+
setStep(FIRST_STEP[mode]);
|
|
35545
|
+
setFirstValue("");
|
|
35546
|
+
setNewPin("");
|
|
35547
|
+
setOtp("");
|
|
35548
|
+
setInvalid(failed);
|
|
35549
|
+
setResetKey((key) => key + 1);
|
|
35550
|
+
}, [mode]);
|
|
35551
|
+
React.useEffect(() => {
|
|
35552
|
+
restart(false);
|
|
35553
|
+
setLocalError(null);
|
|
35554
|
+
}, [restart]);
|
|
35555
|
+
React.useEffect(() => {
|
|
35556
|
+
mounted.current = true;
|
|
35557
|
+
return () => {
|
|
35558
|
+
mounted.current = false;
|
|
35559
|
+
};
|
|
35560
|
+
}, []);
|
|
35561
|
+
// Block page scroll and keep focus inside the screen.
|
|
35562
|
+
React.useEffect(() => {
|
|
35563
|
+
const previousOverflow = document.body.style.overflow;
|
|
35564
|
+
document.body.style.overflow = "hidden";
|
|
35565
|
+
const previousFocus = document.activeElement;
|
|
35566
|
+
panelRef.current?.focus();
|
|
35567
|
+
const keepFocus = (event) => {
|
|
35568
|
+
const panel = panelRef.current;
|
|
35569
|
+
if (panel && !panel.contains(event.target))
|
|
35570
|
+
panel.focus();
|
|
35571
|
+
};
|
|
35572
|
+
document.addEventListener("focusin", keepFocus);
|
|
35573
|
+
return () => {
|
|
35574
|
+
document.body.style.overflow = previousOverflow;
|
|
35575
|
+
document.removeEventListener("focusin", keepFocus);
|
|
35576
|
+
previousFocus?.focus?.();
|
|
35577
|
+
};
|
|
35578
|
+
}, []);
|
|
35579
|
+
const trapTab = (event) => {
|
|
35580
|
+
if (event.key !== "Tab" || !panelRef.current)
|
|
35581
|
+
return;
|
|
35582
|
+
const items = Array.from(panelRef.current.querySelectorAll(FOCUSABLE));
|
|
35583
|
+
if (items.length === 0) {
|
|
35584
|
+
event.preventDefault();
|
|
35585
|
+
return;
|
|
35586
|
+
}
|
|
35587
|
+
const first = items[0];
|
|
35588
|
+
const last = items[items.length - 1];
|
|
35589
|
+
const active = document.activeElement;
|
|
35590
|
+
if (event.shiftKey && (active === first || active === panelRef.current)) {
|
|
35591
|
+
event.preventDefault();
|
|
35592
|
+
last.focus();
|
|
35593
|
+
}
|
|
35594
|
+
else if (!event.shiftKey && active === last) {
|
|
35595
|
+
event.preventDefault();
|
|
35596
|
+
first.focus();
|
|
35597
|
+
}
|
|
35598
|
+
};
|
|
35599
|
+
const submit = async (call) => {
|
|
35600
|
+
if (!call)
|
|
35601
|
+
return;
|
|
35602
|
+
setPending(true);
|
|
35603
|
+
try {
|
|
35604
|
+
await call();
|
|
35605
|
+
if (mounted.current)
|
|
35606
|
+
restart(false);
|
|
35607
|
+
}
|
|
35608
|
+
catch {
|
|
35609
|
+
if (mounted.current)
|
|
35610
|
+
restart(true);
|
|
35611
|
+
}
|
|
35612
|
+
finally {
|
|
35613
|
+
if (mounted.current)
|
|
35614
|
+
setPending(false);
|
|
35615
|
+
}
|
|
35616
|
+
};
|
|
35617
|
+
const handlePin = (pin) => {
|
|
35618
|
+
setLocalError(null);
|
|
35619
|
+
switch (step) {
|
|
35620
|
+
case "unlock":
|
|
35621
|
+
void submit(onUnlock && (() => onUnlock(pin)));
|
|
35622
|
+
return;
|
|
35623
|
+
case "current":
|
|
35624
|
+
setFirstValue(pin);
|
|
35625
|
+
setStep("new");
|
|
35626
|
+
return;
|
|
35627
|
+
case "new":
|
|
35628
|
+
setNewPin(pin);
|
|
35629
|
+
setStep("confirm");
|
|
35630
|
+
return;
|
|
35631
|
+
case "confirm":
|
|
35632
|
+
if (pin !== newPin) {
|
|
35633
|
+
setLocalError(texts.mismatchError);
|
|
35634
|
+
setInvalid(true);
|
|
35635
|
+
setNewPin("");
|
|
35636
|
+
setStep("new");
|
|
35637
|
+
setResetKey((key) => key + 1);
|
|
35638
|
+
return;
|
|
35639
|
+
}
|
|
35640
|
+
if (mode === "create")
|
|
35641
|
+
void submit(onCreate && (() => onCreate(pin)));
|
|
35642
|
+
else if (mode === "change")
|
|
35643
|
+
void submit(onChangePin && (() => onChangePin(firstValue, pin)));
|
|
35644
|
+
else if (mode === "reset")
|
|
35645
|
+
void submit(onReset && (() => onReset(firstValue, pin)));
|
|
35646
|
+
return;
|
|
35647
|
+
}
|
|
35648
|
+
};
|
|
35649
|
+
const submitOtp = (event) => {
|
|
35650
|
+
event.preventDefault();
|
|
35651
|
+
if (otp.length !== otpLength)
|
|
35652
|
+
return;
|
|
35653
|
+
setFirstValue(otp);
|
|
35654
|
+
setInvalid(false);
|
|
35655
|
+
setStep("new");
|
|
35656
|
+
};
|
|
35657
|
+
const isBusy = busy || pending;
|
|
35658
|
+
const shownError = localError ?? error;
|
|
35659
|
+
const showForgot = !!onForgot &&
|
|
35660
|
+
!!texts.forgot &&
|
|
35661
|
+
(step === "unlock" || step === "current");
|
|
35662
|
+
const title = titleOf(mode, step, texts);
|
|
35663
|
+
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", className), "data-testid": "lock-screen", children: jsxRuntime.jsxs("div", { ref: panelRef, role: "dialog", "aria-modal": "true", "aria-label": texts.dialogLabel, "aria-busy": isBusy, tabIndex: -1, onKeyDown: trapTab, className: 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: [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) => {
|
|
35664
|
+
setOtp(event.target.value.replace(/\D/g, "").slice(0, otpLength));
|
|
35665
|
+
setInvalid(false);
|
|
35666
|
+
}, 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 }))] })] }) }));
|
|
35667
|
+
};
|
|
35668
|
+
|
|
35263
35669
|
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", }) => {
|
|
35264
35670
|
const [filteredSpecialties, setFilteredSpecialties] = React.useState([]);
|
|
35265
35671
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
@@ -37374,6 +37780,7 @@ exports.LockKeyholeOpen = LockKeyholeOpen;
|
|
|
37374
37780
|
exports.LockKeyholeOpenIcon = LockKeyholeOpen;
|
|
37375
37781
|
exports.LockOpen = LockOpen;
|
|
37376
37782
|
exports.LockOpenIcon = LockOpen;
|
|
37783
|
+
exports.LockScreen = LockScreen;
|
|
37377
37784
|
exports.LogIn = LogIn;
|
|
37378
37785
|
exports.LogInIcon = LogIn;
|
|
37379
37786
|
exports.LogOut = LogOut;
|
|
@@ -39759,6 +40166,7 @@ exports.Pin = Pin;
|
|
|
39759
40166
|
exports.PinIcon = Pin;
|
|
39760
40167
|
exports.PinOff = PinOff;
|
|
39761
40168
|
exports.PinOffIcon = PinOff;
|
|
40169
|
+
exports.PinPad = PinPad;
|
|
39762
40170
|
exports.Pipette = Pipette;
|
|
39763
40171
|
exports.PipetteIcon = Pipette;
|
|
39764
40172
|
exports.Pizza = Pizza;
|
|
@@ -40932,6 +41340,7 @@ exports.isValidPhoneNumber = isValidPhoneNumber;
|
|
|
40932
41340
|
exports.parseDateSafe = parseDateSafe;
|
|
40933
41341
|
exports.useDebounce = useDebounce;
|
|
40934
41342
|
exports.useDebouncedCallback = useDebouncedCallback;
|
|
41343
|
+
exports.useIdleLock = useIdleLock;
|
|
40935
41344
|
exports.useSelectField = useSelectField;
|
|
40936
41345
|
exports.useTheme = useTheme;
|
|
40937
41346
|
//# sourceMappingURL=index.js.map
|