@visns-studio/visns-components 6.6.3 → 6.15.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/README.md +1658 -18
- package/package.json +10 -4
- package/src/components/DataGrid.jsx +619 -158
- package/src/components/Fetch.jsx +80 -3
- package/src/components/Form.jsx +9 -0
- package/src/components/Navigation.jsx +109 -50
- package/src/components/Notification.jsx +279 -9
- package/src/components/TableFilter.jsx +14 -1
- package/src/components/auth/AuthBrandPanel.jsx +43 -0
- package/src/components/auth/AuthLoading.jsx +78 -0
- package/src/components/auth/AuthShell.jsx +59 -0
- package/src/components/auth/ClientAuth.jsx +161 -0
- package/src/components/auth/ClientAuthFrame.jsx +59 -0
- package/src/components/auth/ClientLogin.jsx +266 -55
- package/src/components/auth/ClientOTPVerify.jsx +587 -115
- package/src/components/auth/ImpersonateGate.jsx +254 -0
- package/src/components/auth/Login.jsx +134 -41
- package/src/components/auth/LogoutScreen.jsx +112 -0
- package/src/components/auth/Reset.jsx +134 -77
- package/src/components/auth/TwoFactorAuth.jsx +475 -297
- package/src/components/auth/Verify.jsx +237 -126
- package/src/components/auth/authEndpoints.js +105 -0
- package/src/components/auth/authFont.js +23 -0
- package/src/components/auth/authHelpers.js +465 -0
- package/src/components/auth/clientAuthProtocols.js +240 -0
- package/src/components/auth/useOptionalRouter.js +49 -0
- package/src/components/callQueue/CallQueuePop.jsx +1502 -0
- package/src/components/callQueue/CallQueueSettings.jsx +508 -0
- package/src/components/callQueue/callQueueHelpers.js +280 -0
- package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
- package/src/components/columns/AutoGrowCell.jsx +141 -0
- package/src/components/columns/ColumnRenderers.jsx +53 -7
- package/src/components/generic/GenericAuth.jsx +163 -96
- package/src/components/generic/GenericDetail.jsx +215 -90
- package/src/components/generic/GenericIndex.jsx +20 -47
- package/src/components/generic/GenericMain.jsx +5 -0
- package/src/components/generic/GroupedReportRenderer.jsx +1 -5
- package/src/components/generic/StandardModal.jsx +17 -5
- package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
- package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
- package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
- package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
- package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
- package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
- package/src/components/navActive.js +60 -0
- package/src/components/notify/desktopNotifications.js +256 -0
- package/src/components/sketch/SketchField.jsx +12 -2
- package/src/components/sms/SmsComposeModal.jsx +495 -0
- package/src/components/sms/SmsInbox.jsx +596 -0
- package/src/components/sms/SmsInboxBadge.jsx +468 -0
- package/src/components/sms/SmsLineSettings.jsx +854 -0
- package/src/components/sms/SmsThreadPanel.jsx +1184 -0
- package/src/components/sms/smsEndpoints.js +56 -0
- package/src/components/sms/smsHelpers.js +1075 -0
- package/src/components/sms/smsLiveState.js +132 -0
- package/src/components/sms/useSmsLive.js +307 -0
- package/src/components/styles/CallQueuePop.module.scss +646 -0
- package/src/components/styles/CallQueueSettings.module.scss +460 -0
- package/src/components/styles/ClientAuth.module.scss +229 -0
- package/src/components/styles/DataGrid.module.scss +10 -0
- package/src/components/styles/GenericDetail.module.scss +153 -26
- package/src/components/styles/GenericIndex.module.scss +13 -0
- package/src/components/styles/GenericMain.module.scss +19 -2
- package/src/components/styles/ImpersonateGate.module.scss +85 -0
- package/src/components/styles/Login.module.scss +13 -122
- package/src/components/styles/LogoutScreen.module.scss +89 -0
- package/src/components/styles/Navigation.module.scss +509 -223
- package/src/components/styles/Notification.module.scss +103 -0
- package/src/components/styles/Reset.module.scss +52 -186
- package/src/components/styles/Sms.module.scss +2382 -0
- package/src/components/styles/TableFilter.module.scss +118 -98
- package/src/components/styles/TwoFactorAuth.module.scss +115 -176
- package/src/components/styles/Vault.module.scss +1983 -0
- package/src/components/styles/Verify.module.scss +57 -180
- package/src/components/styles/_authBrandPanel.scss +163 -0
- package/src/components/styles/_authShell.scss +369 -0
- package/src/components/styles/global-datagrid.css +29 -0
- package/src/components/utils/buildEnv.js +67 -0
- package/src/components/utils/displayValue.js +94 -0
- package/src/components/utils/useDensity.js +345 -9
- package/src/components/vault/OtpChip.jsx +208 -0
- package/src/components/vault/PasswordGenerator.jsx +145 -0
- package/src/components/vault/QrScanner.jsx +326 -0
- package/src/components/vault/VaultAccessLog.jsx +225 -0
- package/src/components/vault/VaultConfirmPanel.jsx +113 -0
- package/src/components/vault/VaultEntryForm.jsx +740 -0
- package/src/components/vault/VaultManager.jsx +1000 -0
- package/src/components/vault/VaultQuickSearch.jsx +681 -0
- package/src/components/vault/qrDecode.js +257 -0
- package/src/components/vault/useDebouncedValue.js +22 -0
- package/src/components/vault/useVaultReveal.js +160 -0
- package/src/components/vault/vaultClipboard.js +33 -0
- package/src/components/vault/vaultEndpoints.js +40 -0
- package/src/components/vault/vaultFit.js +116 -0
- package/src/components/vault/vaultHelpers.js +547 -0
- package/src/components/vault/vaultNavigation.jsx +58 -0
- package/src/components/vault/vaultOtp.js +105 -0
- package/src/index.js +233 -1
|
@@ -3,10 +3,36 @@ import { useNavigate } from 'react-router-dom';
|
|
|
3
3
|
import moment from 'moment';
|
|
4
4
|
import parse from 'html-react-parser';
|
|
5
5
|
import { toast } from 'react-toastify';
|
|
6
|
-
import { Bell, X, Check } from 'lucide-react';
|
|
6
|
+
import { Bell, X, Check, Monitor } from 'lucide-react';
|
|
7
7
|
import CustomFetch from './Fetch';
|
|
8
|
+
import {
|
|
9
|
+
getPermission,
|
|
10
|
+
isEnabled as desktopAlertsEnabled,
|
|
11
|
+
isSupported as desktopAlertsSupported,
|
|
12
|
+
notify as desktopNotify,
|
|
13
|
+
requestPermission as requestDesktopPermission,
|
|
14
|
+
setEnabled as setDesktopAlertsEnabled,
|
|
15
|
+
} from './notify/desktopNotifications';
|
|
16
|
+
// Pure subscription bookkeeping. It lives under sms/ because that is where it
|
|
17
|
+
// was written, but there is nothing messaging-specific in it — importing it
|
|
18
|
+
// keeps one implementation of "has this channel actually been authorised?".
|
|
19
|
+
import { subscriptionMonitor } from './sms/smsLiveState';
|
|
8
20
|
import styles from './styles/Notification.module.scss';
|
|
9
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The event Laravel broadcasts a notification as, and what Echo's
|
|
24
|
+
* `.notification()` helper binds to. Named here so the fallback path below can
|
|
25
|
+
* unbind it explicitly.
|
|
26
|
+
*/
|
|
27
|
+
const BROADCAST_EVENT = '.Illuminate\\Notifications\\Events\\BroadcastNotificationCreated';
|
|
28
|
+
|
|
29
|
+
/** Polling, with and without a confirmed websocket. */
|
|
30
|
+
const POLL_INTERVAL = 60000;
|
|
31
|
+
const LIVE_POLL_INTERVAL = 300000;
|
|
32
|
+
|
|
33
|
+
/** The bold line on a desktop alert. */
|
|
34
|
+
const ALERT_TITLE = 'Throughlife';
|
|
35
|
+
|
|
10
36
|
// Helper function to sanitize HTML content and remove unwanted styling from anchor tags
|
|
11
37
|
const sanitizeHtml = (html) => {
|
|
12
38
|
if (!html) return '';
|
|
@@ -47,6 +73,26 @@ const sanitizeHtml = (html) => {
|
|
|
47
73
|
return tempDiv.innerHTML;
|
|
48
74
|
};
|
|
49
75
|
|
|
76
|
+
/**
|
|
77
|
+
* A label with its markup taken off, for the desktop toast.
|
|
78
|
+
*
|
|
79
|
+
* Notification labels are stored as HTML (the bell parses them), and the OS
|
|
80
|
+
* toast renders text — so "<b>New task</b>" would otherwise appear verbatim.
|
|
81
|
+
*/
|
|
82
|
+
const plainText = (html) => {
|
|
83
|
+
if (!html) return '';
|
|
84
|
+
|
|
85
|
+
if (typeof document === 'undefined') {
|
|
86
|
+
return String(html).replace(/<[^>]*>/g, '').trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const holder = document.createElement('div');
|
|
90
|
+
|
|
91
|
+
holder.innerHTML = String(html);
|
|
92
|
+
|
|
93
|
+
return (holder.textContent || '').replace(/\s+/g, ' ').trim();
|
|
94
|
+
};
|
|
95
|
+
|
|
50
96
|
function Notification(props) {
|
|
51
97
|
const navigate = useNavigate();
|
|
52
98
|
const wrapperRef = useRef(null);
|
|
@@ -54,7 +100,22 @@ function Notification(props) {
|
|
|
54
100
|
const [show, setShow] = useState(false);
|
|
55
101
|
const [loading, setLoading] = useState(false);
|
|
56
102
|
const [processingIds, setProcessingIds] = useState([]);
|
|
57
|
-
const { setSystemAuth } = props;
|
|
103
|
+
const { setSystemAuth, echo = null, channel = null } = props;
|
|
104
|
+
|
|
105
|
+
// Live means *confirmed*: Echo hands back a channel object long before
|
|
106
|
+
// /broadcasting/auth has approved it, and treating that as live is how a
|
|
107
|
+
// bell ends up subscribed to nothing and polling every five minutes.
|
|
108
|
+
const [live, setLive] = useState(false);
|
|
109
|
+
const [alertsOn, setAlertsOn] = useState(() => desktopAlertsEnabled());
|
|
110
|
+
const [permission, setPermission] = useState(() => getPermission());
|
|
111
|
+
|
|
112
|
+
const alertsSupported = desktopAlertsSupported();
|
|
113
|
+
|
|
114
|
+
// Read through refs so a consumer passing inline arrows — `echo={() =>
|
|
115
|
+
// getEcho()}` is the normal way to write it — does not tear the
|
|
116
|
+
// subscription down on every render. Only the channel name does that.
|
|
117
|
+
const echoRef = useRef(echo);
|
|
118
|
+
echoRef.current = echo;
|
|
58
119
|
|
|
59
120
|
const fetchNotification = useCallback(() => {
|
|
60
121
|
setLoading(true);
|
|
@@ -184,26 +245,191 @@ function Notification(props) {
|
|
|
184
245
|
setShow(false);
|
|
185
246
|
}, [navigate]);
|
|
186
247
|
|
|
248
|
+
/**
|
|
249
|
+
* One notification, straight off the websocket.
|
|
250
|
+
*
|
|
251
|
+
* The payload is App\Notifications\SystemAlert::toBroadcast() — the same
|
|
252
|
+
* id the database row was written under, so a live item and a polled one
|
|
253
|
+
* are interchangeable and "mark as read" works on either.
|
|
254
|
+
*/
|
|
255
|
+
const handleBroadcast = useCallback(
|
|
256
|
+
(payload) => {
|
|
257
|
+
const label = payload?.label;
|
|
258
|
+
|
|
259
|
+
if (!label) return;
|
|
260
|
+
|
|
261
|
+
const id = payload.id ?? `live-${Date.now()}`;
|
|
262
|
+
const url = payload.url ?? null;
|
|
263
|
+
|
|
264
|
+
setItems((prev) => {
|
|
265
|
+
// Pusher can redeliver, and a poll may have raced us here.
|
|
266
|
+
if (prev.some((item) => item.id === id)) return prev;
|
|
267
|
+
|
|
268
|
+
return [
|
|
269
|
+
{
|
|
270
|
+
id,
|
|
271
|
+
data: { label, url },
|
|
272
|
+
read_at: null,
|
|
273
|
+
created_at: payload.created_at ?? new Date().toISOString(),
|
|
274
|
+
},
|
|
275
|
+
...prev,
|
|
276
|
+
];
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
desktopNotify({
|
|
280
|
+
title: ALERT_TITLE,
|
|
281
|
+
body: plainText(label),
|
|
282
|
+
tag: `notification-${id}`,
|
|
283
|
+
url,
|
|
284
|
+
navigate,
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
[navigate]
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const broadcastRef = useRef(handleBroadcast);
|
|
291
|
+
broadcastRef.current = handleBroadcast;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Subscribe to this user's private channel, when the consumer named one.
|
|
295
|
+
*
|
|
296
|
+
* `channel` is deliberately allowed to be null — a build with no
|
|
297
|
+
* VITE_APP_ENV cannot know whether it is dev or production, and dev and
|
|
298
|
+
* production share one Pusher app. Guessing there would put a developer's
|
|
299
|
+
* browser on the production channel; polling is the safe answer.
|
|
300
|
+
*/
|
|
187
301
|
useEffect(() => {
|
|
188
|
-
|
|
302
|
+
if (!channel || typeof window === 'undefined') {
|
|
303
|
+
setLive(false);
|
|
304
|
+
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const source = echoRef.current;
|
|
309
|
+
const instance = typeof source === 'function' ? source() : source;
|
|
310
|
+
|
|
311
|
+
if (!instance || typeof instance.private !== 'function') {
|
|
312
|
+
setLive(false);
|
|
313
|
+
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let subscription;
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
subscription = instance.private(channel);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
setLive(false);
|
|
323
|
+
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!subscription) {
|
|
328
|
+
setLive(false);
|
|
189
329
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const handler = (payload) => broadcastRef.current?.(payload || {});
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
// Echo's own helper for Laravel's broadcast notifications; the
|
|
337
|
+
// raw `listen` is the fallback for anything hand-rolled.
|
|
338
|
+
if (typeof subscription.notification === 'function') {
|
|
339
|
+
subscription.notification(handler);
|
|
340
|
+
} else {
|
|
341
|
+
subscription.listen(BROADCAST_EVENT, handler);
|
|
193
342
|
}
|
|
194
|
-
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
setLive(false);
|
|
345
|
+
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const stopMonitoring = subscriptionMonitor(subscription, {
|
|
350
|
+
onSuccess: () => setLive(true),
|
|
351
|
+
onError: () => setLive(false),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return () => {
|
|
355
|
+
setLive(false);
|
|
356
|
+
stopMonitoring();
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
subscription.stopListening?.(BROADCAST_EVENT);
|
|
360
|
+
instance.leave(channel);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
// Nothing to clean up.
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}, [channel]);
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The fallback, and the safety net.
|
|
369
|
+
*
|
|
370
|
+
* Off the websocket this is the only thing keeping the bell current, so it
|
|
371
|
+
* runs every minute. Once a channel confirms it drops to five — slow
|
|
372
|
+
* enough to be free, quick enough that a dropped event is invisible.
|
|
373
|
+
*/
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
fetchNotification();
|
|
376
|
+
|
|
377
|
+
const interval = setInterval(
|
|
378
|
+
() => {
|
|
379
|
+
if (!document.hidden) {
|
|
380
|
+
fetchNotification();
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
live ? LIVE_POLL_INTERVAL : POLL_INTERVAL
|
|
384
|
+
);
|
|
195
385
|
|
|
386
|
+
return () => clearInterval(interval);
|
|
387
|
+
}, [fetchNotification, live]);
|
|
388
|
+
|
|
389
|
+
useEffect(() => {
|
|
196
390
|
document.addEventListener('mousedown', handleClickOutside, false);
|
|
197
391
|
|
|
198
392
|
return function cleanup() {
|
|
199
|
-
clearInterval(interval);
|
|
200
393
|
document.removeEventListener(
|
|
201
394
|
'mousedown',
|
|
202
395
|
handleClickOutside,
|
|
203
396
|
false
|
|
204
397
|
);
|
|
205
398
|
};
|
|
206
|
-
}, [
|
|
399
|
+
}, [handleClickOutside]);
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The switch at the foot of the dropdown.
|
|
403
|
+
*
|
|
404
|
+
* Turning it on for the first time is what asks the browser for
|
|
405
|
+
* permission — the prompt only appears inside a user gesture, which is why
|
|
406
|
+
* this cannot be done on mount. A refusal leaves the switch off and the
|
|
407
|
+
* row explains itself; the browser will not ask twice.
|
|
408
|
+
*/
|
|
409
|
+
const toggleDesktopAlerts = useCallback(() => {
|
|
410
|
+
if (alertsOn) {
|
|
411
|
+
setDesktopAlertsEnabled(false);
|
|
412
|
+
setAlertsOn(false);
|
|
413
|
+
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (getPermission() === 'granted') {
|
|
418
|
+
setDesktopAlertsEnabled(true);
|
|
419
|
+
setAlertsOn(true);
|
|
420
|
+
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
requestDesktopPermission().then((result) => {
|
|
425
|
+
setPermission(result);
|
|
426
|
+
|
|
427
|
+
if (result !== 'granted') return;
|
|
428
|
+
|
|
429
|
+
setDesktopAlertsEnabled(true);
|
|
430
|
+
setAlertsOn(true);
|
|
431
|
+
});
|
|
432
|
+
}, [alertsOn]);
|
|
207
433
|
|
|
208
434
|
const formatTimeAgo = (date) => {
|
|
209
435
|
const now = moment();
|
|
@@ -225,6 +451,7 @@ function Notification(props) {
|
|
|
225
451
|
className={styles.notificationTrigger}
|
|
226
452
|
onClick={toggleNotification}
|
|
227
453
|
data-tooltip-id="system-tooltip"
|
|
454
|
+
data-tooltip-place="bottom"
|
|
228
455
|
data-tooltip-content="Notifications"
|
|
229
456
|
aria-label="Notifications"
|
|
230
457
|
>
|
|
@@ -331,6 +558,49 @@ function Notification(props) {
|
|
|
331
558
|
)}
|
|
332
559
|
</div>
|
|
333
560
|
|
|
561
|
+
{alertsSupported && (
|
|
562
|
+
<div className={styles.alertsRow}>
|
|
563
|
+
<span className={styles.alertsLabel}>
|
|
564
|
+
<Monitor
|
|
565
|
+
size={14}
|
|
566
|
+
strokeWidth={2}
|
|
567
|
+
aria-hidden="true"
|
|
568
|
+
/>
|
|
569
|
+
Desktop alerts
|
|
570
|
+
</span>
|
|
571
|
+
|
|
572
|
+
{permission === 'denied' ? (
|
|
573
|
+
<span className={styles.alertsBlocked}>
|
|
574
|
+
Blocked in browser settings
|
|
575
|
+
</span>
|
|
576
|
+
) : (
|
|
577
|
+
<button
|
|
578
|
+
type="button"
|
|
579
|
+
className={[
|
|
580
|
+
styles.toggle,
|
|
581
|
+
alertsOn ? styles.toggleOn : '',
|
|
582
|
+
]
|
|
583
|
+
.filter(Boolean)
|
|
584
|
+
.join(' ')}
|
|
585
|
+
onClick={toggleDesktopAlerts}
|
|
586
|
+
role="switch"
|
|
587
|
+
aria-checked={alertsOn}
|
|
588
|
+
aria-label="Desktop alerts"
|
|
589
|
+
>
|
|
590
|
+
<span
|
|
591
|
+
className={styles.toggleTrack}
|
|
592
|
+
aria-hidden="true"
|
|
593
|
+
>
|
|
594
|
+
<span
|
|
595
|
+
className={styles.toggleKnob}
|
|
596
|
+
/>
|
|
597
|
+
</span>
|
|
598
|
+
{alertsOn ? 'On' : 'Off'}
|
|
599
|
+
</button>
|
|
600
|
+
)}
|
|
601
|
+
</div>
|
|
602
|
+
)}
|
|
603
|
+
|
|
334
604
|
<div className={styles.notiboxActions}>
|
|
335
605
|
<button
|
|
336
606
|
className={styles.viewAllBtn}
|
|
@@ -9,6 +9,15 @@ function TableFilter({
|
|
|
9
9
|
type,
|
|
10
10
|
onFilterChange,
|
|
11
11
|
tabs,
|
|
12
|
+
// `variant="underline"` lays the rail out as a horizontal tab strip with
|
|
13
|
+
// an accent edge under the selected tab, at every width — the same
|
|
14
|
+
// treatment narrow viewports already get for a flat rail (see the
|
|
15
|
+
// `.flatFilter` block in the stylesheet), promoted to an opt-in.
|
|
16
|
+
//
|
|
17
|
+
// A vertical rail spends a fifth of the page on navigation, which pays for
|
|
18
|
+
// itself at seven tabs and does not at two. Pages say which they are; the
|
|
19
|
+
// default is unchanged.
|
|
20
|
+
variant,
|
|
12
21
|
}) {
|
|
13
22
|
const [visibleChildren, setVisibleChildren] = useState({});
|
|
14
23
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
@@ -661,11 +670,15 @@ function TableFilter({
|
|
|
661
670
|
}
|
|
662
671
|
}, [isFlatStrip, activeId]);
|
|
663
672
|
|
|
673
|
+
// Gated on `isFlatStrip`: the strip is one row of tabs, and a rail with
|
|
674
|
+
// expandable parents has no single row to lay out.
|
|
675
|
+
const isUnderline = variant === 'underline' && isFlatStrip;
|
|
676
|
+
|
|
664
677
|
return (
|
|
665
678
|
<div
|
|
666
679
|
className={`${styles.tableFilterContainer} ${
|
|
667
680
|
isFlatStrip ? styles.flatFilter : ''
|
|
668
|
-
}`}
|
|
681
|
+
} ${isUnderline ? styles.underlineFilter : ''}`}
|
|
669
682
|
>
|
|
670
683
|
{!isFlatStrip && (
|
|
671
684
|
<div className={styles.mobileToggle} onClick={toggleMobileMenu}>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The brand side of a two-pane auth screen.
|
|
5
|
+
*
|
|
6
|
+
* Markup counterpart to the `auth-brand-panel` mixin in
|
|
7
|
+
* styles/_authBrandPanel.scss — the staff Login, the shared AuthShell and the
|
|
8
|
+
* client portal's split layout all render exactly this, and each supplies its
|
|
9
|
+
* own CSS module (the class names are identical because they all include that
|
|
10
|
+
* one mixin).
|
|
11
|
+
*
|
|
12
|
+
* `aria-hidden` is deliberate and applies to every consumer: everything this
|
|
13
|
+
* panel says is said again, in words, by the form beside it. A screen reader
|
|
14
|
+
* announcing the brand line twice before reaching the first field is worse
|
|
15
|
+
* than not announcing the photograph at all.
|
|
16
|
+
*
|
|
17
|
+
* Renders the `<aside>` unconditionally, even with nothing to put in it — it is
|
|
18
|
+
* a layout column, and a grid with a missing child is a broken screen rather
|
|
19
|
+
* than a tidier one. Only the ink block is conditional.
|
|
20
|
+
*/
|
|
21
|
+
const AuthBrandPanel = ({ styles, loginBg, branding = {} }) => (
|
|
22
|
+
<aside
|
|
23
|
+
className={styles.brand}
|
|
24
|
+
style={loginBg ? { backgroundImage: `url(${loginBg})` } : undefined}
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
>
|
|
27
|
+
{(branding.eyebrow || branding.name || branding.tagline) && (
|
|
28
|
+
<div className={styles.brandInk}>
|
|
29
|
+
{branding.eyebrow && (
|
|
30
|
+
<p className={styles.eyebrow}>{branding.eyebrow}</p>
|
|
31
|
+
)}
|
|
32
|
+
{branding.name && (
|
|
33
|
+
<p className={styles.brandLine}>{branding.name}</p>
|
|
34
|
+
)}
|
|
35
|
+
{branding.tagline && (
|
|
36
|
+
<p className={styles.brandSub}>{branding.tagline}</p>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
</aside>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export default AuthBrandPanel;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The please-wait curtain.
|
|
5
|
+
*
|
|
6
|
+
* Shown while the profile check is in flight, and — since 6.8.1 — for the
|
|
7
|
+
* whole of any transition that ends in the browser unloading the page.
|
|
8
|
+
* Rendering null through either of those leaves a blank white screen, or worse,
|
|
9
|
+
* a screen that still looks interactive while nothing on it will respond.
|
|
10
|
+
*
|
|
11
|
+
* Lived inside GenericAuth until the auth screens needed it too. Extracted
|
|
12
|
+
* verbatim rather than reimplemented, so the curtain a screen raises for itself
|
|
13
|
+
* and the one GenericAuth raises over it are the same object: when both are up
|
|
14
|
+
* at once — which is exactly what happens on a successful 2FA hand-off — there
|
|
15
|
+
* is nothing to see between them.
|
|
16
|
+
*
|
|
17
|
+
* Self-contained on purpose: a SMIL animation, no stylesheet dependency, and
|
|
18
|
+
* the consuming app's design tokens with neutral fallbacks. It has to be able
|
|
19
|
+
* to render when the page it covers has not finished loading its CSS.
|
|
20
|
+
*/
|
|
21
|
+
const AuthLoading = ({ label }) => (
|
|
22
|
+
<div
|
|
23
|
+
role="status"
|
|
24
|
+
aria-label="Loading"
|
|
25
|
+
aria-live="polite"
|
|
26
|
+
style={{
|
|
27
|
+
position: 'fixed',
|
|
28
|
+
inset: 0,
|
|
29
|
+
display: 'flex',
|
|
30
|
+
flexDirection: 'column',
|
|
31
|
+
gap: '1rem',
|
|
32
|
+
alignItems: 'center',
|
|
33
|
+
justifyContent: 'center',
|
|
34
|
+
background: 'var(--bg-color, #f4f4f5)',
|
|
35
|
+
// Above every app surface — this is a curtain, not a page.
|
|
36
|
+
zIndex: 2000,
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<svg width="42" height="42" viewBox="0 0 42 42" fill="none">
|
|
40
|
+
<circle
|
|
41
|
+
cx="21"
|
|
42
|
+
cy="21"
|
|
43
|
+
r="18"
|
|
44
|
+
stroke="var(--border-color, #dfe3eb)"
|
|
45
|
+
strokeWidth="4"
|
|
46
|
+
/>
|
|
47
|
+
<path
|
|
48
|
+
d="M39 21a18 18 0 0 0-18-18"
|
|
49
|
+
stroke="var(--primary-color, #52525b)"
|
|
50
|
+
strokeWidth="4"
|
|
51
|
+
strokeLinecap="round"
|
|
52
|
+
>
|
|
53
|
+
<animateTransform
|
|
54
|
+
attributeName="transform"
|
|
55
|
+
type="rotate"
|
|
56
|
+
from="0 21 21"
|
|
57
|
+
to="360 21 21"
|
|
58
|
+
dur="0.9s"
|
|
59
|
+
repeatCount="indefinite"
|
|
60
|
+
/>
|
|
61
|
+
</path>
|
|
62
|
+
</svg>
|
|
63
|
+
{label ? (
|
|
64
|
+
<p
|
|
65
|
+
style={{
|
|
66
|
+
margin: 0,
|
|
67
|
+
fontFamily: 'var(--font-family, inherit)',
|
|
68
|
+
fontSize: 'var(--font-size-md, 0.9375rem)',
|
|
69
|
+
color: 'var(--paragraph-color, #52525b)',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{label}
|
|
73
|
+
</p>
|
|
74
|
+
) : null}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export default AuthLoading;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two-pane frame every staff auth screen sits in.
|
|
3
|
+
*
|
|
4
|
+
* Login.jsx keeps its own copy of this markup — it is the screen the design was
|
|
5
|
+
* drawn for and it has an SSO disclosure the others do not — but TwoFactorAuth,
|
|
6
|
+
* Reset and Verify all render the same frame around different contents, so it
|
|
7
|
+
* lives here rather than three times over.
|
|
8
|
+
*
|
|
9
|
+
* The styles come from the caller: each screen owns its own CSS module (they
|
|
10
|
+
* all `@include auth-shell` from styles/_authShell.scss, so the class names are
|
|
11
|
+
* identical) and passes it in as `styles`. That keeps the module scoping intact
|
|
12
|
+
* while the markup stays in one place.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React from 'react';
|
|
16
|
+
|
|
17
|
+
import AuthBrandPanel from './AuthBrandPanel';
|
|
18
|
+
|
|
19
|
+
const AuthShell = ({
|
|
20
|
+
styles,
|
|
21
|
+
logo,
|
|
22
|
+
loginBg,
|
|
23
|
+
branding = {},
|
|
24
|
+
title,
|
|
25
|
+
subtitle,
|
|
26
|
+
children,
|
|
27
|
+
footer,
|
|
28
|
+
}) => (
|
|
29
|
+
<div className={styles.auth}>
|
|
30
|
+
<AuthBrandPanel
|
|
31
|
+
styles={styles}
|
|
32
|
+
loginBg={loginBg}
|
|
33
|
+
branding={branding}
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
<main className={styles.panel}>
|
|
37
|
+
<div className={styles.panelInner}>
|
|
38
|
+
{logo ? (
|
|
39
|
+
<img
|
|
40
|
+
src={logo}
|
|
41
|
+
alt={branding.name || 'Logo'}
|
|
42
|
+
className={styles.logo}
|
|
43
|
+
/>
|
|
44
|
+
) : null}
|
|
45
|
+
|
|
46
|
+
{title ? <h1 className={styles.title}>{title}</h1> : null}
|
|
47
|
+
{subtitle ? (
|
|
48
|
+
<p className={styles.subtitle}>{subtitle}</p>
|
|
49
|
+
) : null}
|
|
50
|
+
|
|
51
|
+
{children}
|
|
52
|
+
|
|
53
|
+
{footer ? <p className={styles.foot}>{footer}</p> : null}
|
|
54
|
+
</div>
|
|
55
|
+
</main>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
export default AuthShell;
|