@striae-org/striae 4.0.3 → 4.1.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 (66) hide show
  1. package/app/components/actions/confirm-export.ts +4 -2
  2. package/app/components/actions/generate-pdf.ts +10 -2
  3. package/app/components/audit/user-audit-viewer.tsx +121 -940
  4. package/app/components/audit/user-audit.module.css +20 -0
  5. package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
  6. package/app/components/audit/viewer/audit-entries-list.tsx +200 -0
  7. package/app/components/audit/viewer/audit-filters-panel.tsx +306 -0
  8. package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
  9. package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
  10. package/app/components/audit/viewer/audit-viewer-utils.ts +121 -0
  11. package/app/components/audit/viewer/types.ts +1 -0
  12. package/app/components/audit/viewer/use-audit-viewer-data.ts +166 -0
  13. package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
  14. package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
  15. package/app/components/auth/mfa-enrollment.module.css +13 -5
  16. package/app/components/auth/mfa-verification.module.css +13 -5
  17. package/app/components/canvas/canvas.tsx +3 -0
  18. package/app/components/canvas/confirmation/confirmation.tsx +13 -37
  19. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
  20. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +8 -37
  21. package/app/components/sidebar/case-export/case-export.tsx +9 -34
  22. package/app/components/sidebar/case-import/case-import.module.css +2 -0
  23. package/app/components/sidebar/case-import/case-import.tsx +10 -34
  24. package/app/components/sidebar/cases/cases-modal.module.css +44 -9
  25. package/app/components/sidebar/cases/cases-modal.tsx +16 -14
  26. package/app/components/sidebar/files/files-modal.module.css +45 -10
  27. package/app/components/sidebar/files/files-modal.tsx +16 -16
  28. package/app/components/sidebar/notes/notes-modal.tsx +17 -15
  29. package/app/components/sidebar/notes/notes.module.css +2 -0
  30. package/app/components/sidebar/sidebar.module.css +2 -2
  31. package/app/components/toast/toast.module.css +2 -1
  32. package/app/components/toast/toast.tsx +16 -11
  33. package/app/components/user/delete-account.tsx +10 -31
  34. package/app/components/user/inactivity-warning.module.css +8 -6
  35. package/app/components/user/manage-profile.module.css +2 -0
  36. package/app/components/user/manage-profile.tsx +85 -30
  37. package/app/hooks/useOverlayDismiss.ts +68 -0
  38. package/app/routes/auth/login.example.tsx +19 -8
  39. package/app/routes/auth/passwordReset.module.css +23 -13
  40. package/app/routes/striae/striae.tsx +8 -1
  41. package/app/routes.ts +7 -0
  42. package/app/services/audit/audit-export-csv.ts +2 -0
  43. package/app/services/audit/audit.service.ts +29 -5
  44. package/app/services/audit/builders/audit-entry-builder.ts +2 -1
  45. package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
  46. package/app/services/audit/builders/audit-event-builders-workflow.ts +6 -0
  47. package/app/types/audit.ts +2 -1
  48. package/app/types/user.ts +1 -0
  49. package/app/utils/data/permissions.ts +1 -0
  50. package/functions/api/pdf/[[path]].ts +32 -1
  51. package/load-context.ts +9 -0
  52. package/package.json +5 -1
  53. package/primershear.emails.example +6 -0
  54. package/scripts/deploy-pages-secrets.sh +6 -0
  55. package/scripts/deploy-primershear-emails.sh +166 -0
  56. package/worker-configuration.d.ts +7493 -7491
  57. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  58. package/workers/data-worker/wrangler.jsonc.example +1 -1
  59. package/workers/image-worker/wrangler.jsonc.example +1 -1
  60. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  61. package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
  62. package/workers/pdf-worker/src/report-types.ts +3 -0
  63. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  64. package/workers/user-worker/src/user-worker.example.ts +6 -1
  65. package/workers/user-worker/wrangler.jsonc.example +1 -1
  66. package/wrangler.toml.example +1 -1
@@ -9,6 +9,7 @@
9
9
  justify-content: center;
10
10
  align-items: center;
11
11
  z-index: 1000;
12
+ cursor: default;
12
13
  }
13
14
 
14
15
  .modal {
@@ -18,6 +19,7 @@
18
19
  width: 100%;
19
20
  max-width: 400px;
20
21
  box-shadow: 0 var(--spaceM) var(--spaceXL) rgba(0, 0, 0, 0.2);
22
+ cursor: default;
21
23
  }
22
24
 
23
25
  .title {
@@ -63,7 +65,8 @@
63
65
  }
64
66
 
65
67
  .errorMessage {
66
- background: linear-gradient(135deg,
68
+ background: linear-gradient(
69
+ 135deg,
67
70
  color-mix(in lab, var(--error) 12%, transparent),
68
71
  color-mix(in lab, var(--error) 8%, transparent)
69
72
  );
@@ -83,13 +86,17 @@
83
86
  }
84
87
 
85
88
  .errorMessage::before {
86
- content: '';
89
+ content: "";
87
90
  position: absolute;
88
91
  top: 0;
89
92
  left: 0;
90
93
  right: 0;
91
94
  height: 2px;
92
- background: linear-gradient(90deg, var(--error), color-mix(in lab, var(--error) 60%, transparent));
95
+ background: linear-gradient(
96
+ 90deg,
97
+ var(--error),
98
+ color-mix(in lab, var(--error) 60%, transparent)
99
+ );
93
100
  animation: shimmer 2s ease-in-out infinite;
94
101
  }
95
102
 
@@ -229,7 +236,8 @@
229
236
  }
230
237
 
231
238
  @keyframes shimmer {
232
- 0%, 100% {
239
+ 0%,
240
+ 100% {
233
241
  opacity: 0.6;
234
242
  transform: translateX(-100%);
235
243
  }
@@ -244,7 +252,7 @@
244
252
  .errorMessage {
245
253
  animation: none;
246
254
  }
247
-
255
+
248
256
  .errorMessage::before {
249
257
  animation: none;
250
258
  }
@@ -10,6 +10,7 @@ interface CanvasProps {
10
10
  imageUrl?: string;
11
11
  filename?: string;
12
12
  company?: string;
13
+ badgeId?: string;
13
14
  firstName?: string;
14
15
  error?: string;
15
16
  activeAnnotations?: Set<string>;
@@ -32,6 +33,7 @@ export const Canvas = ({
32
33
  imageUrl,
33
34
  filename,
34
35
  company,
36
+ badgeId,
35
37
  firstName,
36
38
  error,
37
39
  activeAnnotations,
@@ -453,6 +455,7 @@ export const Canvas = ({
453
455
  onClose={() => setIsConfirmationModalOpen(false)}
454
456
  onConfirm={handleConfirmation}
455
457
  company={company}
458
+ defaultBadgeId={badgeId}
456
459
  existingConfirmation={annotationData?.confirmationData || null}
457
460
  />
458
461
  </div>
@@ -1,6 +1,7 @@
1
1
  import { useState, useEffect, useContext } from 'react';
2
2
  import { type ConfirmationData } from '~/types/annotations';
3
3
  import { AuthContext } from '~/contexts/auth.context';
4
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
4
5
  import { generateConfirmationId } from '~/utils/common';
5
6
  import styles from './confirmation.module.css';
6
7
 
@@ -9,6 +10,7 @@ interface ConfirmationModalProps {
9
10
  onClose: () => void;
10
11
  onConfirm?: (confirmationData: ConfirmationData) => void;
11
12
  company?: string;
13
+ defaultBadgeId?: string;
12
14
  existingConfirmation?: ConfirmationData | null;
13
15
  }
14
16
 
@@ -26,7 +28,7 @@ const formatTimestamp = (): string => {
26
28
  });
27
29
  };
28
30
 
29
- export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, existingConfirmation }: ConfirmationModalProps) => {
31
+ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, defaultBadgeId, existingConfirmation }: ConfirmationModalProps) => {
30
32
  const { user } = useContext(AuthContext);
31
33
  const [badgeId, setBadgeId] = useState('');
32
34
  const [error, setError] = useState('');
@@ -38,38 +40,29 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, existin
38
40
  const timestamp = formatTimestamp();
39
41
  const confirmationId = generateConfirmationId();
40
42
 
43
+ const {
44
+ handleOverlayMouseDown,
45
+ handleOverlayKeyDown
46
+ } = useOverlayDismiss({
47
+ isOpen,
48
+ onClose
49
+ });
50
+
41
51
  // Check if this is an existing confirmation
42
52
  const hasExistingConfirmation = !!existingConfirmation;
43
53
 
44
- // Handle Escape key to close modal
45
- useEffect(() => {
46
- const handleEscapeKey = (event: KeyboardEvent) => {
47
- if (event.key === 'Escape' && isOpen) {
48
- onClose();
49
- }
50
- };
51
-
52
- if (isOpen) {
53
- document.addEventListener('keydown', handleEscapeKey);
54
- }
55
-
56
- return () => {
57
- document.removeEventListener('keydown', handleEscapeKey);
58
- };
59
- }, [isOpen, onClose]);
60
-
61
54
  // Reset form when modal opens
62
55
  useEffect(() => {
63
56
  if (isOpen) {
64
57
  if (existingConfirmation) {
65
58
  setBadgeId(existingConfirmation.badgeId);
66
59
  } else {
67
- setBadgeId('');
60
+ setBadgeId(defaultBadgeId || '');
68
61
  }
69
62
  setError('');
70
63
  setIsConfirming(false);
71
64
  }
72
- }, [isOpen, existingConfirmation]);
65
+ }, [isOpen, defaultBadgeId, existingConfirmation]);
73
66
 
74
67
  if (!isOpen) return null;
75
68
 
@@ -104,23 +97,6 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, existin
104
97
  }
105
98
  };
106
99
 
107
- const handleOverlayMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
108
- if (e.target === e.currentTarget) {
109
- onClose();
110
- }
111
- };
112
-
113
- const handleOverlayKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
114
- if (e.target !== e.currentTarget) {
115
- return;
116
- }
117
-
118
- if (e.key === 'Enter' || e.key === ' ') {
119
- e.preventDefault();
120
- onClose();
121
- }
122
- };
123
-
124
100
  return (
125
101
  <div
126
102
  className={styles.overlay}
@@ -7,6 +7,7 @@
7
7
  align-items: center;
8
8
  z-index: var(--zIndex5);
9
9
  padding: var(--spaceL);
10
+ cursor: default;
10
11
  }
11
12
 
12
13
  .modal {
@@ -5,10 +5,9 @@ import {
5
5
  useState,
6
6
  type ChangeEvent,
7
7
  type DragEvent,
8
- type KeyboardEvent,
9
- type MouseEvent
10
8
  } from 'react';
11
9
  import styles from './public-signing-key-modal.module.css';
10
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
12
11
  import { verifyExportFile } from '~/utils/forensics';
13
12
 
14
13
  const NO_PUBLIC_KEY_MESSAGE = 'No public signing key is configured for this environment.';
@@ -230,6 +229,13 @@ export const PublicSigningKeyModal = ({
230
229
  const publicSigningKeyTitleId = useId();
231
230
  const publicKeyInputId = useId();
232
231
  const exportFileInputId = useId();
232
+ const {
233
+ handleOverlayMouseDown,
234
+ handleOverlayKeyDown
235
+ } = useOverlayDismiss({
236
+ isOpen,
237
+ onClose
238
+ });
233
239
 
234
240
  useEffect(() => {
235
241
  if (!isOpen) {
@@ -242,45 +248,10 @@ export const PublicSigningKeyModal = ({
242
248
  }
243
249
  }, [isOpen]);
244
250
 
245
- useEffect(() => {
246
- if (!isOpen) {
247
- return;
248
- }
249
-
250
- const handleEscapeKey = (event: globalThis.KeyboardEvent) => {
251
- if (event.key === 'Escape') {
252
- onClose();
253
- }
254
- };
255
-
256
- document.addEventListener('keydown', handleEscapeKey);
257
-
258
- return () => {
259
- document.removeEventListener('keydown', handleEscapeKey);
260
- };
261
- }, [isOpen, onClose]);
262
-
263
251
  if (!isOpen) {
264
252
  return null;
265
253
  }
266
254
 
267
- const handleOverlayMouseDown = (event: MouseEvent<HTMLDivElement>) => {
268
- if (event.target === event.currentTarget) {
269
- onClose();
270
- }
271
- };
272
-
273
- const handleOverlayKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
274
- if (event.target !== event.currentTarget) {
275
- return;
276
- }
277
-
278
- if (event.key === 'Enter' || event.key === ' ') {
279
- event.preventDefault();
280
- onClose();
281
- }
282
- };
283
-
284
255
  const resetVerificationState = () => {
285
256
  setVerificationOutcome(null);
286
257
  };
@@ -1,6 +1,7 @@
1
1
  import { useState, useEffect, useContext } from 'react';
2
2
  import styles from './case-export.module.css';
3
3
  import { AuthContext } from '~/contexts/auth.context';
4
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
4
5
  import { PublicSigningKeyModal } from '~/components/public-signing-key-modal/public-signing-key-modal';
5
6
  import { getCurrentPublicSigningKeyDetails } from '~/utils/forensics';
6
7
  import { getCaseConfirmations, exportConfirmationData } from '../../actions/confirm-export';
@@ -36,6 +37,14 @@ export const CaseExport = ({
36
37
  const [hasConfirmationData, setHasConfirmationData] = useState(false);
37
38
  const [isPublicKeyModalOpen, setIsPublicKeyModalOpen] = useState(false);
38
39
  const { keyId: publicSigningKeyId, publicKeyPem } = getCurrentPublicSigningKeyDetails();
40
+ const {
41
+ handleOverlayMouseDown,
42
+ handleOverlayKeyDown
43
+ } = useOverlayDismiss({
44
+ isOpen,
45
+ onClose,
46
+ canDismiss: !isPublicKeyModalOpen
47
+ });
39
48
 
40
49
  // Update caseNumber when currentCaseNumber prop changes
41
50
  useEffect(() => {
@@ -100,23 +109,6 @@ export const CaseExport = ({
100
109
  }
101
110
  }, [isOpen]);
102
111
 
103
- // Handle Escape key to close modal
104
- useEffect(() => {
105
- const handleEscapeKey = (event: KeyboardEvent) => {
106
- if (event.key === 'Escape' && isOpen && !isPublicKeyModalOpen) {
107
- onClose();
108
- }
109
- };
110
-
111
- if (isOpen) {
112
- document.addEventListener('keydown', handleEscapeKey);
113
- }
114
-
115
- return () => {
116
- document.removeEventListener('keydown', handleEscapeKey);
117
- };
118
- }, [isOpen, isPublicKeyModalOpen, onClose]);
119
-
120
112
  if (!isOpen) return null;
121
113
 
122
114
  const handleExport = async () => {
@@ -182,23 +174,6 @@ export const CaseExport = ({
182
174
  }
183
175
  };
184
176
 
185
- const handleOverlayMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
186
- if (e.target === e.currentTarget) {
187
- onClose();
188
- }
189
- };
190
-
191
- const handleOverlayKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
192
- if (e.target !== e.currentTarget) {
193
- return;
194
- }
195
-
196
- if (e.key === 'Enter' || e.key === ' ') {
197
- e.preventDefault();
198
- onClose();
199
- }
200
- };
201
-
202
177
  return (
203
178
  <div
204
179
  className={styles.overlay}
@@ -521,6 +521,7 @@
521
521
  justify-content: center;
522
522
  align-items: center;
523
523
  z-index: 9999;
524
+ cursor: default;
524
525
  }
525
526
 
526
527
  .confirmationModal {
@@ -530,6 +531,7 @@
530
531
  max-width: 400px;
531
532
  box-shadow: 0 var(--spaceM) var(--spaceXL)
532
533
  color-mix(in lab, var(--black) 20%, transparent);
534
+ cursor: default;
533
535
  }
534
536
 
535
537
  .confirmationContent {
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect, useRef, useContext, useCallback } from 'react';
2
2
  import { AuthContext } from '~/contexts/auth.context';
3
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
4
  import {
4
5
  listReadOnlyCases,
5
6
  deleteReadOnlyCase
@@ -49,6 +50,15 @@ export const CaseImport = ({
49
50
  resetImportState,
50
51
  setImportProgress
51
52
  } = useImportState();
53
+ const canDismissOverlay = !importState.isImporting && !importState.isClearing;
54
+ const {
55
+ handleOverlayMouseDown,
56
+ handleOverlayKeyDown
57
+ } = useOverlayDismiss({
58
+ isOpen,
59
+ onClose,
60
+ canDismiss: canDismissOverlay
61
+ });
52
62
 
53
63
  const [existingReadOnlyCase, setExistingReadOnlyCase] = useState<string | null>(null);
54
64
  const fileInputRef = useRef<HTMLInputElement>(null);
@@ -229,23 +239,6 @@ export const CaseImport = ({
229
239
  onClose();
230
240
  }, [clearImportData, onClose]);
231
241
 
232
- const handleOverlayMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
233
- if (e.target === e.currentTarget && !importState.isImporting && !importState.isClearing) {
234
- onClose();
235
- }
236
- }, [importState.isImporting, importState.isClearing, onClose]);
237
-
238
- const handleOverlayKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
239
- if (e.target !== e.currentTarget || importState.isImporting || importState.isClearing) {
240
- return;
241
- }
242
-
243
- if (e.key === 'Enter' || e.key === ' ') {
244
- e.preventDefault();
245
- onClose();
246
- }
247
- }, [importState.isImporting, importState.isClearing, onClose]);
248
-
249
242
  // Effects
250
243
  useEffect(() => {
251
244
  if (user && isOpen) {
@@ -253,23 +246,6 @@ export const CaseImport = ({
253
246
  }
254
247
  }, [user, isOpen, checkForExistingReadOnlyCase]);
255
248
 
256
- // Handle keyboard events
257
- useEffect(() => {
258
- const handleEscapeKey = (event: KeyboardEvent) => {
259
- if (event.key === 'Escape' && isOpen && !importState.isImporting && !importState.isClearing) {
260
- onClose();
261
- }
262
- };
263
-
264
- if (isOpen) {
265
- document.addEventListener('keydown', handleEscapeKey);
266
- }
267
-
268
- return () => {
269
- document.removeEventListener('keydown', handleEscapeKey);
270
- };
271
- }, [isOpen, onClose, importState.isImporting, importState.isClearing]);
272
-
273
249
  // Reset state when modal closes
274
250
  useEffect(() => {
275
251
  if (!isOpen) {
@@ -6,6 +6,7 @@
6
6
  justify-content: center;
7
7
  align-items: center;
8
8
  z-index: var(--zIndex5);
9
+ cursor: default;
9
10
  transition: background-color var(--durationM) var(--bezierFastoutSlowin);
10
11
  }
11
12
 
@@ -15,7 +16,9 @@
15
16
  width: 90%;
16
17
  max-width: var(--maxWidthL);
17
18
  max-height: 80vh;
18
- box-shadow: 0 var(--spaceXS) var(--spaceL) color-mix(in lab, var(--black) 10%, transparent);
19
+ box-shadow: 0 var(--spaceXS) var(--spaceL)
20
+ color-mix(in lab, var(--black) 10%, transparent);
21
+ cursor: default;
19
22
  transition: background-color var(--durationM) var(--bezierFastoutSlowin);
20
23
  display: flex;
21
24
  flex-direction: column;
@@ -134,33 +137,65 @@
134
137
 
135
138
  /* Case Confirmation Status Indicators */
136
139
  .caseItemNotConfirmed {
137
- background-color: color-mix(in lab, var(--warning) 15%, var(--backgroundLight));
140
+ background-color: color-mix(
141
+ in lab,
142
+ var(--warning) 15%,
143
+ var(--backgroundLight)
144
+ );
138
145
  }
139
146
 
140
147
  .caseItemNotConfirmed:hover {
141
- background-color: color-mix(in lab, var(--warning) 20%, var(--backgroundLight));
148
+ background-color: color-mix(
149
+ in lab,
150
+ var(--warning) 20%,
151
+ var(--backgroundLight)
152
+ );
142
153
  }
143
154
 
144
155
  .caseItem.active.caseItemNotConfirmed {
145
- background-color: color-mix(in lab, var(--warning) 15%, var(--backgroundLight));
156
+ background-color: color-mix(
157
+ in lab,
158
+ var(--warning) 15%,
159
+ var(--backgroundLight)
160
+ );
146
161
  }
147
162
 
148
163
  .caseItem.active.caseItemNotConfirmed:hover {
149
- background-color: color-mix(in lab, var(--warning) 20%, var(--backgroundLight));
164
+ background-color: color-mix(
165
+ in lab,
166
+ var(--warning) 20%,
167
+ var(--backgroundLight)
168
+ );
150
169
  }
151
170
 
152
171
  .caseItemConfirmed {
153
- background-color: color-mix(in lab, var(--success) 20%, var(--backgroundLight));
172
+ background-color: color-mix(
173
+ in lab,
174
+ var(--success) 20%,
175
+ var(--backgroundLight)
176
+ );
154
177
  }
155
178
 
156
179
  .caseItemConfirmed:hover {
157
- background-color: color-mix(in lab, var(--success) 28%, var(--backgroundLight));
180
+ background-color: color-mix(
181
+ in lab,
182
+ var(--success) 28%,
183
+ var(--backgroundLight)
184
+ );
158
185
  }
159
186
 
160
187
  .caseItem.active.caseItemConfirmed {
161
- background-color: color-mix(in lab, var(--success) 20%, var(--backgroundLight));
188
+ background-color: color-mix(
189
+ in lab,
190
+ var(--success) 20%,
191
+ var(--backgroundLight)
192
+ );
162
193
  }
163
194
 
164
195
  .caseItem.active.caseItemConfirmed:hover {
165
- background-color: color-mix(in lab, var(--success) 28%, var(--backgroundLight));
196
+ background-color: color-mix(
197
+ in lab,
198
+ var(--success) 28%,
199
+ var(--backgroundLight)
200
+ );
166
201
  }
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import type { User } from 'firebase/auth';
3
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
4
  import { listCases } from '~/components/actions/case-manage';
4
5
  import { getFileAnnotations } from '~/utils/data';
5
6
  import { fetchFiles } from '~/components/actions/image-manage';
@@ -18,6 +19,13 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
18
19
  const [isLoading, setIsLoading] = useState(false);
19
20
  const [error, setError] = useState<string>('');
20
21
  const [currentPage, setCurrentPage] = useState(0);
22
+ const {
23
+ handleOverlayMouseDown,
24
+ handleOverlayKeyDown
25
+ } = useOverlayDismiss({
26
+ isOpen,
27
+ onClose
28
+ });
21
29
  const [caseConfirmationStatus, setCaseConfirmationStatus] = useState<{
22
30
  [caseNum: string]: { includeConfirmation: boolean; isConfirmed: boolean }
23
31
  }>({});
@@ -28,19 +36,6 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
28
36
  setError('');
29
37
  };
30
38
 
31
- useEffect(() => {
32
- const handleEscape = (e: KeyboardEvent) => {
33
- if (e.key === 'Escape') {
34
- onClose();
35
- }
36
- };
37
-
38
- if (isOpen) {
39
- document.addEventListener('keydown', handleEscape);
40
- return () => document.removeEventListener('keydown', handleEscape);
41
- }
42
- }, [isOpen, onClose]);
43
-
44
39
  useEffect(() => {
45
40
  if (isOpen) {
46
41
  const loadingTimer = window.setTimeout(() => {
@@ -144,7 +139,14 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
144
139
  if (!isOpen) return null;
145
140
 
146
141
  return (
147
- <div className={styles.modalOverlay}>
142
+ <div
143
+ className={styles.modalOverlay}
144
+ onMouseDown={handleOverlayMouseDown}
145
+ onKeyDown={handleOverlayKeyDown}
146
+ role="button"
147
+ tabIndex={0}
148
+ aria-label="Close cases dialog"
149
+ >
148
150
  <div className={styles.modal}>
149
151
  <header className={styles.modalHeader}>
150
152
  <h2>All Cases</h2>
@@ -6,6 +6,7 @@
6
6
  justify-content: center;
7
7
  align-items: center;
8
8
  z-index: var(--zIndex5);
9
+ cursor: default;
9
10
  transition: background-color var(--durationM) var(--bezierFastoutSlowin);
10
11
  }
11
12
 
@@ -15,7 +16,9 @@
15
16
  width: 90%;
16
17
  max-width: var(--maxWidthL);
17
18
  max-height: 80vh;
18
- box-shadow: 0 var(--spaceXS) var(--spaceL) color-mix(in lab, var(--black) 10%, transparent);
19
+ box-shadow: 0 var(--spaceXS) var(--spaceL)
20
+ color-mix(in lab, var(--black) 10%, transparent);
21
+ cursor: default;
19
22
  transition: background-color var(--durationM) var(--bezierFastoutSlowin);
20
23
  display: flex;
21
24
  flex-direction: column;
@@ -177,33 +180,65 @@
177
180
  }
178
181
  /* Confirmation Status Indicators */
179
182
  .fileItemNotConfirmed {
180
- background-color: color-mix(in lab, var(--warning) 15%, var(--backgroundLight));
183
+ background-color: color-mix(
184
+ in lab,
185
+ var(--warning) 15%,
186
+ var(--backgroundLight)
187
+ );
181
188
  }
182
189
 
183
190
  .fileItemNotConfirmed:hover {
184
- background-color: color-mix(in lab, var(--warning) 20%, var(--backgroundLight));
191
+ background-color: color-mix(
192
+ in lab,
193
+ var(--warning) 20%,
194
+ var(--backgroundLight)
195
+ );
185
196
  }
186
197
 
187
198
  .fileItem.active.fileItemNotConfirmed {
188
- background-color: color-mix(in lab, var(--warning) 15%, var(--backgroundLight));
199
+ background-color: color-mix(
200
+ in lab,
201
+ var(--warning) 15%,
202
+ var(--backgroundLight)
203
+ );
189
204
  }
190
205
 
191
206
  .fileItem.active.fileItemNotConfirmed:hover {
192
- background-color: color-mix(in lab, var(--warning) 20%, var(--backgroundLight));
207
+ background-color: color-mix(
208
+ in lab,
209
+ var(--warning) 20%,
210
+ var(--backgroundLight)
211
+ );
193
212
  }
194
213
 
195
214
  .fileItemConfirmed {
196
- background-color: color-mix(in lab, var(--success) 20%, var(--backgroundLight));
215
+ background-color: color-mix(
216
+ in lab,
217
+ var(--success) 20%,
218
+ var(--backgroundLight)
219
+ );
197
220
  }
198
221
 
199
222
  .fileItemConfirmed:hover {
200
- background-color: color-mix(in lab, var(--success) 28%, var(--backgroundLight));
223
+ background-color: color-mix(
224
+ in lab,
225
+ var(--success) 28%,
226
+ var(--backgroundLight)
227
+ );
201
228
  }
202
229
 
203
230
  .fileItem.active.fileItemConfirmed {
204
- background-color: color-mix(in lab, var(--success) 20%, var(--backgroundLight));
231
+ background-color: color-mix(
232
+ in lab,
233
+ var(--success) 20%,
234
+ var(--backgroundLight)
235
+ );
205
236
  }
206
237
 
207
238
  .fileItem.active.fileItemConfirmed:hover {
208
- background-color: color-mix(in lab, var(--success) 28%, var(--backgroundLight));
209
- }
239
+ background-color: color-mix(
240
+ in lab,
241
+ var(--success) 28%,
242
+ var(--backgroundLight)
243
+ );
244
+ }