@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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface UseIdleLockOptions {
|
|
4
|
+
/** Inactivity delay before locking. Default 3 minutes. */
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
/** Called each time the screen becomes locked (idle, `lock()` or another tab). */
|
|
7
|
+
onLock?: () => void;
|
|
8
|
+
/** localStorage key of the locked flag, also the BroadcastChannel name. */
|
|
9
|
+
storageKey?: string;
|
|
10
|
+
/** Turns the inactivity timer off (e.g. until the session is loaded). Default true. */
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseIdleLockResult {
|
|
15
|
+
locked: boolean;
|
|
16
|
+
lock: () => void;
|
|
17
|
+
unlock: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
21
|
+
const DEFAULT_STORAGE_KEY = "ews-idle-lock";
|
|
22
|
+
const ACTIVITY_WRITE_INTERVAL_MS = 5_000;
|
|
23
|
+
const ACTIVITY_EVENTS = [
|
|
24
|
+
"pointerdown",
|
|
25
|
+
"pointermove",
|
|
26
|
+
"keydown",
|
|
27
|
+
"wheel",
|
|
28
|
+
"scroll",
|
|
29
|
+
"touchstart",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
const readStorage = (key: string): string | null => {
|
|
33
|
+
try {
|
|
34
|
+
return typeof window === "undefined" ? null : window.localStorage.getItem(key);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const writeStorage = (key: string, value: string | null) => {
|
|
41
|
+
try {
|
|
42
|
+
if (typeof window === "undefined") return;
|
|
43
|
+
if (value === null) window.localStorage.removeItem(key);
|
|
44
|
+
else window.localStorage.setItem(key, value);
|
|
45
|
+
} catch {
|
|
46
|
+
// Storage unavailable (private mode, quota): the lock still works in this tab.
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Locks the screen after `timeoutMs` without user activity. The locked flag is
|
|
52
|
+
* kept in localStorage (it survives a reload) and shared between tabs through
|
|
53
|
+
* a BroadcastChannel, or the `storage` event where BroadcastChannel is missing:
|
|
54
|
+
* locking or unlocking one tab does the same in every tab. Activity in any tab
|
|
55
|
+
* keeps every tab unlocked.
|
|
56
|
+
*/
|
|
57
|
+
export function useIdleLock({
|
|
58
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
59
|
+
onLock,
|
|
60
|
+
storageKey = DEFAULT_STORAGE_KEY,
|
|
61
|
+
enabled = true,
|
|
62
|
+
}: UseIdleLockOptions = {}): UseIdleLockResult {
|
|
63
|
+
const activityKey = `${storageKey}:activity`;
|
|
64
|
+
const [locked, setLocked] = useState(() => readStorage(storageKey) === "1");
|
|
65
|
+
const lockedRef = useRef(locked);
|
|
66
|
+
const onLockRef = useRef(onLock);
|
|
67
|
+
const lastActivity = useRef(Date.now());
|
|
68
|
+
const lastActivityWrite = useRef(0);
|
|
69
|
+
const channelRef = useRef<{ postMessage: (message: string) => void } | null>(
|
|
70
|
+
null,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
onLockRef.current = onLock;
|
|
75
|
+
}, [onLock]);
|
|
76
|
+
|
|
77
|
+
const markActivity = useCallback(
|
|
78
|
+
(now: number) => {
|
|
79
|
+
lastActivity.current = now;
|
|
80
|
+
if (now - lastActivityWrite.current >= ACTIVITY_WRITE_INTERVAL_MS) {
|
|
81
|
+
lastActivityWrite.current = now;
|
|
82
|
+
writeStorage(activityKey, String(now));
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
[activityKey],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const applyLocked = useCallback((next: boolean) => {
|
|
89
|
+
if (lockedRef.current === next) return;
|
|
90
|
+
lockedRef.current = next;
|
|
91
|
+
setLocked(next);
|
|
92
|
+
if (next) onLockRef.current?.();
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
const lock = useCallback(() => {
|
|
96
|
+
if (lockedRef.current) return;
|
|
97
|
+
writeStorage(storageKey, "1");
|
|
98
|
+
channelRef.current?.postMessage("lock");
|
|
99
|
+
applyLocked(true);
|
|
100
|
+
}, [storageKey, applyLocked]);
|
|
101
|
+
|
|
102
|
+
const unlock = useCallback(() => {
|
|
103
|
+
lastActivityWrite.current = 0;
|
|
104
|
+
markActivity(Date.now());
|
|
105
|
+
writeStorage(storageKey, null);
|
|
106
|
+
channelRef.current?.postMessage("unlock");
|
|
107
|
+
applyLocked(false);
|
|
108
|
+
}, [storageKey, applyLocked, markActivity]);
|
|
109
|
+
|
|
110
|
+
// Cross-tab sync.
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (typeof window === "undefined") return;
|
|
113
|
+
const onRemote = (nextLocked: boolean) => {
|
|
114
|
+
if (!nextLocked) lastActivity.current = Date.now();
|
|
115
|
+
applyLocked(nextLocked);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (typeof window.BroadcastChannel === "function") {
|
|
119
|
+
const channel = new window.BroadcastChannel(storageKey);
|
|
120
|
+
channelRef.current = channel;
|
|
121
|
+
channel.onmessage = (event: MessageEvent) => {
|
|
122
|
+
if (event.data === "lock") onRemote(true);
|
|
123
|
+
else if (event.data === "unlock") onRemote(false);
|
|
124
|
+
};
|
|
125
|
+
return () => {
|
|
126
|
+
channelRef.current = null;
|
|
127
|
+
channel.close();
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const onStorage = (event: StorageEvent) => {
|
|
132
|
+
if (event.key !== storageKey) return;
|
|
133
|
+
onRemote(event.newValue === "1");
|
|
134
|
+
};
|
|
135
|
+
window.addEventListener("storage", onStorage);
|
|
136
|
+
return () => window.removeEventListener("storage", onStorage);
|
|
137
|
+
}, [storageKey, applyLocked]);
|
|
138
|
+
|
|
139
|
+
// Inactivity timer.
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!enabled || locked || typeof window === "undefined") return;
|
|
142
|
+
lastActivity.current = Date.now();
|
|
143
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
144
|
+
|
|
145
|
+
const idleFor = () => {
|
|
146
|
+
const shared = Number(readStorage(activityKey)) || 0;
|
|
147
|
+
return Date.now() - Math.max(lastActivity.current, shared);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const check = () => {
|
|
151
|
+
const idle = idleFor();
|
|
152
|
+
if (idle >= timeoutMs) lock();
|
|
153
|
+
else timer = setTimeout(check, timeoutMs - idle);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const onActivity = () => markActivity(Date.now());
|
|
157
|
+
|
|
158
|
+
// Timers are throttled in background tabs: check the delay on return.
|
|
159
|
+
const onVisibility = () => {
|
|
160
|
+
if (document.visibilityState !== "visible") return;
|
|
161
|
+
if (idleFor() >= timeoutMs) lock();
|
|
162
|
+
else onActivity();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
timer = setTimeout(check, timeoutMs);
|
|
166
|
+
ACTIVITY_EVENTS.forEach((name) =>
|
|
167
|
+
document.addEventListener(name, onActivity, { capture: true, passive: true }),
|
|
168
|
+
);
|
|
169
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
ACTIVITY_EVENTS.forEach((name) =>
|
|
174
|
+
document.removeEventListener(name, onActivity, { capture: true }),
|
|
175
|
+
);
|
|
176
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
177
|
+
};
|
|
178
|
+
}, [enabled, locked, timeoutMs, activityKey, lock, markActivity]);
|
|
179
|
+
|
|
180
|
+
return { locked, lock, unlock };
|
|
181
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -55,6 +55,16 @@ export type { PaymentDueBannerProps } from "./components/PaymentDueBanner";
|
|
|
55
55
|
export { SecureLoader } from "./components/SecureLoader";
|
|
56
56
|
export type { SecureLoaderProps } from "./components/SecureLoader";
|
|
57
57
|
|
|
58
|
+
export { PinPad } from "./components/PinPad";
|
|
59
|
+
export type { PinPadProps } from "./components/PinPad";
|
|
60
|
+
|
|
61
|
+
export { LockScreen } from "./components/LockScreen";
|
|
62
|
+
export type {
|
|
63
|
+
LockScreenMode,
|
|
64
|
+
LockScreenProps,
|
|
65
|
+
LockScreenTexts,
|
|
66
|
+
} from "./components/LockScreen";
|
|
67
|
+
|
|
58
68
|
// Molecules
|
|
59
69
|
export { SpecialtySearchAutocomplete } from "./molecules";
|
|
60
70
|
export type { Specialty, SpecialtySearchAutocompleteProps } from "./molecules";
|
|
@@ -95,8 +105,17 @@ export type {
|
|
|
95
105
|
} from "./utils";
|
|
96
106
|
|
|
97
107
|
// Hooks
|
|
98
|
-
export {
|
|
99
|
-
|
|
108
|
+
export {
|
|
109
|
+
useDebounce,
|
|
110
|
+
useDebouncedCallback,
|
|
111
|
+
useIdleLock,
|
|
112
|
+
useSelectField,
|
|
113
|
+
} from "./hooks";
|
|
114
|
+
export type {
|
|
115
|
+
UseIdleLockOptions,
|
|
116
|
+
UseIdleLockResult,
|
|
117
|
+
UseSelectFieldProps,
|
|
118
|
+
} from "./hooks";
|
|
100
119
|
|
|
101
120
|
// Theme
|
|
102
121
|
export { ThemeProvider, useTheme } from "./theme/ThemeProvider";
|
package/src/utils/env-config.ts
CHANGED
|
@@ -56,6 +56,28 @@ function buildUrls(environment: Environment): Omit<EnvConfig, "env"> {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Derives the environment from the host the app is served from, for builds
|
|
61
|
+
* that did not set VITE_ENV. Hosts actually served (Traefik rules in
|
|
62
|
+
* infrastructure/medecine360-hetzner/deployment/docker-stack.*.yml):
|
|
63
|
+
* - local: localhost, 127.0.0.1, *.medecine360local.com (local.app., api., ...)
|
|
64
|
+
* - dev: dev.{app,login,dashboard,admin,api}.medecine360.com
|
|
65
|
+
* - qa: qa.{app,login,dashboard,admin,api}.medecine360.com
|
|
66
|
+
* - prod: medecine360.com, {login,dashboard,admin,api}.medecine360.com
|
|
67
|
+
* Any other host (or no window, e.g. SSR or tests) falls back to local.
|
|
68
|
+
*/
|
|
69
|
+
function environmentFromHost(): Environment {
|
|
70
|
+
const hostname =
|
|
71
|
+
typeof window !== "undefined" ? window.location?.hostname?.toLowerCase() : undefined;
|
|
72
|
+
if (!hostname) return "local";
|
|
73
|
+
if (hostname === "medecine360.com" || hostname.endsWith(".medecine360.com")) {
|
|
74
|
+
if (hostname.startsWith("dev.")) return "dev";
|
|
75
|
+
if (hostname.startsWith("qa.")) return "qa";
|
|
76
|
+
return "prod";
|
|
77
|
+
}
|
|
78
|
+
return "local";
|
|
79
|
+
}
|
|
80
|
+
|
|
59
81
|
/**
|
|
60
82
|
* Creates an environment configuration with all derived URLs.
|
|
61
83
|
*
|
|
@@ -65,6 +87,11 @@ function buildUrls(environment: Environment): Omit<EnvConfig, "env"> {
|
|
|
65
87
|
* For local development, sensible defaults are provided but can be overridden
|
|
66
88
|
* (e.g. to point BFF URL at a mock server).
|
|
67
89
|
*
|
|
90
|
+
* When env is empty (build without VITE_ENV), the environment is derived from
|
|
91
|
+
* the current host instead of always assuming local, so a deployed build never
|
|
92
|
+
* links to http://local.*.medecine360local.com. A console warning names the
|
|
93
|
+
* derived environment so the misconfigured build stays visible.
|
|
94
|
+
*
|
|
68
95
|
* @param env - The environment name (VITE_ENV value)
|
|
69
96
|
* @param overrides - Optional URL overrides (e.g. for local mock servers)
|
|
70
97
|
*/
|
|
@@ -72,7 +99,13 @@ export function createEnvConfig(
|
|
|
72
99
|
env: string,
|
|
73
100
|
overrides?: EnvConfigOverrides,
|
|
74
101
|
): EnvConfig {
|
|
75
|
-
|
|
102
|
+
let environment = env as Environment;
|
|
103
|
+
if (!environment) {
|
|
104
|
+
environment = environmentFromHost();
|
|
105
|
+
console.warn(
|
|
106
|
+
`[env-config] VITE_ENV is not set; using "${environment}" derived from the current host.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
76
109
|
const derived = buildUrls(environment);
|
|
77
110
|
|
|
78
111
|
return {
|
package/tailwind.preset.js
CHANGED
|
@@ -135,6 +135,17 @@ module.exports = {
|
|
|
135
135
|
large:
|
|
136
136
|
"0 10px 40px -10px rgba(0, 0, 0, 0.15), 0 2px 10px -2px rgba(0, 0, 0, 0.05)",
|
|
137
137
|
},
|
|
138
|
+
// PinPad: wrong PIN feedback
|
|
139
|
+
keyframes: {
|
|
140
|
+
"pin-shake": {
|
|
141
|
+
"0%, 100%": { transform: "translateX(0)" },
|
|
142
|
+
"20%, 60%": { transform: "translateX(-8px)" },
|
|
143
|
+
"40%, 80%": { transform: "translateX(8px)" },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
animation: {
|
|
147
|
+
"pin-shake": "pin-shake 0.4s ease-in-out",
|
|
148
|
+
},
|
|
138
149
|
},
|
|
139
150
|
},
|
|
140
151
|
plugins: [
|