@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.
Files changed (100) hide show
  1. package/README.md +1658 -18
  2. package/package.json +10 -4
  3. package/src/components/DataGrid.jsx +619 -158
  4. package/src/components/Fetch.jsx +80 -3
  5. package/src/components/Form.jsx +9 -0
  6. package/src/components/Navigation.jsx +109 -50
  7. package/src/components/Notification.jsx +279 -9
  8. package/src/components/TableFilter.jsx +14 -1
  9. package/src/components/auth/AuthBrandPanel.jsx +43 -0
  10. package/src/components/auth/AuthLoading.jsx +78 -0
  11. package/src/components/auth/AuthShell.jsx +59 -0
  12. package/src/components/auth/ClientAuth.jsx +161 -0
  13. package/src/components/auth/ClientAuthFrame.jsx +59 -0
  14. package/src/components/auth/ClientLogin.jsx +266 -55
  15. package/src/components/auth/ClientOTPVerify.jsx +587 -115
  16. package/src/components/auth/ImpersonateGate.jsx +254 -0
  17. package/src/components/auth/Login.jsx +134 -41
  18. package/src/components/auth/LogoutScreen.jsx +112 -0
  19. package/src/components/auth/Reset.jsx +134 -77
  20. package/src/components/auth/TwoFactorAuth.jsx +475 -297
  21. package/src/components/auth/Verify.jsx +237 -126
  22. package/src/components/auth/authEndpoints.js +105 -0
  23. package/src/components/auth/authFont.js +23 -0
  24. package/src/components/auth/authHelpers.js +465 -0
  25. package/src/components/auth/clientAuthProtocols.js +240 -0
  26. package/src/components/auth/useOptionalRouter.js +49 -0
  27. package/src/components/callQueue/CallQueuePop.jsx +1502 -0
  28. package/src/components/callQueue/CallQueueSettings.jsx +508 -0
  29. package/src/components/callQueue/callQueueHelpers.js +280 -0
  30. package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
  31. package/src/components/columns/AutoGrowCell.jsx +141 -0
  32. package/src/components/columns/ColumnRenderers.jsx +53 -7
  33. package/src/components/generic/GenericAuth.jsx +163 -96
  34. package/src/components/generic/GenericDetail.jsx +215 -90
  35. package/src/components/generic/GenericIndex.jsx +20 -47
  36. package/src/components/generic/GenericMain.jsx +5 -0
  37. package/src/components/generic/GroupedReportRenderer.jsx +1 -5
  38. package/src/components/generic/StandardModal.jsx +17 -5
  39. package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
  40. package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
  41. package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
  42. package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
  43. package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
  44. package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
  45. package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
  46. package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
  47. package/src/components/navActive.js +60 -0
  48. package/src/components/notify/desktopNotifications.js +256 -0
  49. package/src/components/sketch/SketchField.jsx +12 -2
  50. package/src/components/sms/SmsComposeModal.jsx +495 -0
  51. package/src/components/sms/SmsInbox.jsx +596 -0
  52. package/src/components/sms/SmsInboxBadge.jsx +468 -0
  53. package/src/components/sms/SmsLineSettings.jsx +854 -0
  54. package/src/components/sms/SmsThreadPanel.jsx +1184 -0
  55. package/src/components/sms/smsEndpoints.js +56 -0
  56. package/src/components/sms/smsHelpers.js +1075 -0
  57. package/src/components/sms/smsLiveState.js +132 -0
  58. package/src/components/sms/useSmsLive.js +307 -0
  59. package/src/components/styles/CallQueuePop.module.scss +646 -0
  60. package/src/components/styles/CallQueueSettings.module.scss +460 -0
  61. package/src/components/styles/ClientAuth.module.scss +229 -0
  62. package/src/components/styles/DataGrid.module.scss +10 -0
  63. package/src/components/styles/GenericDetail.module.scss +153 -26
  64. package/src/components/styles/GenericIndex.module.scss +13 -0
  65. package/src/components/styles/GenericMain.module.scss +19 -2
  66. package/src/components/styles/ImpersonateGate.module.scss +85 -0
  67. package/src/components/styles/Login.module.scss +13 -122
  68. package/src/components/styles/LogoutScreen.module.scss +89 -0
  69. package/src/components/styles/Navigation.module.scss +509 -223
  70. package/src/components/styles/Notification.module.scss +103 -0
  71. package/src/components/styles/Reset.module.scss +52 -186
  72. package/src/components/styles/Sms.module.scss +2382 -0
  73. package/src/components/styles/TableFilter.module.scss +118 -98
  74. package/src/components/styles/TwoFactorAuth.module.scss +115 -176
  75. package/src/components/styles/Vault.module.scss +1983 -0
  76. package/src/components/styles/Verify.module.scss +57 -180
  77. package/src/components/styles/_authBrandPanel.scss +163 -0
  78. package/src/components/styles/_authShell.scss +369 -0
  79. package/src/components/styles/global-datagrid.css +29 -0
  80. package/src/components/utils/buildEnv.js +67 -0
  81. package/src/components/utils/displayValue.js +94 -0
  82. package/src/components/utils/useDensity.js +345 -9
  83. package/src/components/vault/OtpChip.jsx +208 -0
  84. package/src/components/vault/PasswordGenerator.jsx +145 -0
  85. package/src/components/vault/QrScanner.jsx +326 -0
  86. package/src/components/vault/VaultAccessLog.jsx +225 -0
  87. package/src/components/vault/VaultConfirmPanel.jsx +113 -0
  88. package/src/components/vault/VaultEntryForm.jsx +740 -0
  89. package/src/components/vault/VaultManager.jsx +1000 -0
  90. package/src/components/vault/VaultQuickSearch.jsx +681 -0
  91. package/src/components/vault/qrDecode.js +257 -0
  92. package/src/components/vault/useDebouncedValue.js +22 -0
  93. package/src/components/vault/useVaultReveal.js +160 -0
  94. package/src/components/vault/vaultClipboard.js +33 -0
  95. package/src/components/vault/vaultEndpoints.js +40 -0
  96. package/src/components/vault/vaultFit.js +116 -0
  97. package/src/components/vault/vaultHelpers.js +547 -0
  98. package/src/components/vault/vaultNavigation.jsx +58 -0
  99. package/src/components/vault/vaultOtp.js +105 -0
  100. 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
- fetchNotification();
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
- const interval = setInterval(() => {
191
- if (!document.hidden) {
192
- fetchNotification();
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
- }, 60000);
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
- }, [fetchNotification, handleClickOutside]);
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;