@striae-org/striae 4.0.3 → 4.2.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 (118) hide show
  1. package/.env.example +8 -0
  2. package/app/components/actions/case-export/core-export.ts +14 -8
  3. package/app/components/actions/case-export/data-processing.ts +1 -0
  4. package/app/components/actions/case-export/download-handlers.ts +7 -0
  5. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  6. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  7. package/app/components/actions/case-import/orchestrator.ts +78 -32
  8. package/app/components/actions/case-import/storage-operations.ts +97 -8
  9. package/app/components/actions/case-import/zip-processing.ts +159 -86
  10. package/app/components/actions/case-manage.ts +430 -8
  11. package/app/components/actions/confirm-export.ts +13 -4
  12. package/app/components/actions/generate-pdf.ts +10 -2
  13. package/app/components/actions/image-manage.ts +77 -44
  14. package/app/components/audit/user-audit-viewer.tsx +137 -945
  15. package/app/components/audit/user-audit.module.css +41 -0
  16. package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
  17. package/app/components/audit/viewer/audit-entries-list.tsx +207 -0
  18. package/app/components/audit/viewer/audit-filters-panel.tsx +307 -0
  19. package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
  20. package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
  21. package/app/components/audit/viewer/audit-viewer-utils.ts +123 -0
  22. package/app/components/audit/viewer/types.ts +1 -0
  23. package/app/components/audit/viewer/use-audit-viewer-data.ts +186 -0
  24. package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
  25. package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
  26. package/app/components/auth/mfa-enrollment.module.css +13 -5
  27. package/app/components/auth/mfa-verification.module.css +13 -5
  28. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  29. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  30. package/app/components/canvas/canvas.module.css +64 -54
  31. package/app/components/canvas/canvas.tsx +17 -16
  32. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  33. package/app/components/canvas/confirmation/confirmation.tsx +17 -47
  34. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  35. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  36. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  37. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  38. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  39. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  40. package/app/components/navbar/navbar.module.css +447 -0
  41. package/app/components/navbar/navbar.tsx +377 -0
  42. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +2 -0
  43. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +21 -51
  44. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  45. package/app/components/sidebar/case-export/case-export.tsx +14 -77
  46. package/app/components/sidebar/case-import/case-import.module.css +25 -0
  47. package/app/components/sidebar/case-import/case-import.tsx +64 -40
  48. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  49. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  50. package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
  51. package/app/components/sidebar/cases/cases-modal.module.css +45 -9
  52. package/app/components/sidebar/cases/cases-modal.tsx +16 -16
  53. package/app/components/sidebar/cases/cases.module.css +62 -21
  54. package/app/components/sidebar/files/files-modal.module.css +46 -10
  55. package/app/components/sidebar/files/files-modal.tsx +22 -23
  56. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  57. package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
  58. package/app/components/sidebar/notes/notes-modal.tsx +18 -17
  59. package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
  60. package/app/components/sidebar/notes/notes.module.css +155 -0
  61. package/app/components/sidebar/sidebar-container.tsx +15 -28
  62. package/app/components/sidebar/sidebar.module.css +7 -71
  63. package/app/components/sidebar/sidebar.tsx +24 -125
  64. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  65. package/app/components/toast/toast.module.css +2 -1
  66. package/app/components/toast/toast.tsx +16 -11
  67. package/app/components/user/delete-account.tsx +10 -31
  68. package/app/components/user/inactivity-warning.module.css +9 -6
  69. package/app/components/user/inactivity-warning.tsx +15 -2
  70. package/app/components/user/manage-profile.module.css +2 -0
  71. package/app/components/user/manage-profile.tsx +108 -40
  72. package/app/hooks/useOverlayDismiss.ts +116 -0
  73. package/app/routes/auth/login.example.tsx +19 -8
  74. package/app/routes/auth/login.tsx +785 -774
  75. package/app/routes/auth/passwordReset.module.css +23 -13
  76. package/app/routes/striae/striae.module.css +10 -3
  77. package/app/routes/striae/striae.tsx +477 -31
  78. package/app/routes.ts +7 -0
  79. package/app/services/audit/audit-export-csv.ts +2 -0
  80. package/app/services/audit/audit.service.ts +202 -32
  81. package/app/services/audit/builders/audit-entry-builder.ts +2 -1
  82. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  83. package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
  84. package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -0
  85. package/app/services/audit/builders/index.ts +1 -0
  86. package/app/types/audit.ts +5 -2
  87. package/app/types/case.ts +29 -0
  88. package/app/types/import.ts +3 -0
  89. package/app/types/user.ts +1 -0
  90. package/app/utils/data/permissions.ts +17 -1
  91. package/app/utils/forensics/audit-export-signature.ts +5 -1
  92. package/app/utils/forensics/confirmation-signature.ts +3 -0
  93. package/app/utils/forensics/export-verification.ts +497 -22
  94. package/functions/api/pdf/[[path]].ts +32 -1
  95. package/load-context.ts +9 -0
  96. package/package.json +6 -2
  97. package/primershear.emails.example +6 -0
  98. package/scripts/deploy-pages-secrets.sh +6 -0
  99. package/scripts/deploy-primershear-emails.sh +167 -0
  100. package/worker-configuration.d.ts +7493 -7491
  101. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  102. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  103. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  104. package/workers/data-worker/wrangler.jsonc.example +1 -1
  105. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  106. package/workers/image-worker/wrangler.jsonc.example +1 -1
  107. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  108. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  109. package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
  110. package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
  111. package/workers/pdf-worker/src/report-types.ts +3 -0
  112. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  113. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  114. package/workers/user-worker/src/user-worker.example.ts +6 -1
  115. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  116. package/workers/user-worker/wrangler.jsonc.example +1 -1
  117. package/wrangler.toml.example +1 -1
  118. package/public/.well-known/keybase.txt +0 -56
@@ -6,16 +6,20 @@
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
 
12
13
  .modal {
14
+ position: relative;
13
15
  background: var(--backgroundLight);
14
16
  border-radius: var(--spaceXS);
15
17
  width: 90%;
16
18
  max-width: var(--maxWidthL);
17
19
  max-height: 80vh;
18
- box-shadow: 0 var(--spaceXS) var(--spaceL) color-mix(in lab, var(--black) 10%, transparent);
20
+ box-shadow: 0 var(--spaceXS) var(--spaceL)
21
+ color-mix(in lab, var(--black) 10%, transparent);
22
+ cursor: default;
19
23
  transition: background-color var(--durationM) var(--bezierFastoutSlowin);
20
24
  display: flex;
21
25
  flex-direction: column;
@@ -134,33 +138,65 @@
134
138
 
135
139
  /* Case Confirmation Status Indicators */
136
140
  .caseItemNotConfirmed {
137
- background-color: color-mix(in lab, var(--warning) 15%, var(--backgroundLight));
141
+ background-color: color-mix(
142
+ in lab,
143
+ var(--warning) 15%,
144
+ var(--backgroundLight)
145
+ );
138
146
  }
139
147
 
140
148
  .caseItemNotConfirmed:hover {
141
- background-color: color-mix(in lab, var(--warning) 20%, var(--backgroundLight));
149
+ background-color: color-mix(
150
+ in lab,
151
+ var(--warning) 20%,
152
+ var(--backgroundLight)
153
+ );
142
154
  }
143
155
 
144
156
  .caseItem.active.caseItemNotConfirmed {
145
- background-color: color-mix(in lab, var(--warning) 15%, var(--backgroundLight));
157
+ background-color: color-mix(
158
+ in lab,
159
+ var(--warning) 15%,
160
+ var(--backgroundLight)
161
+ );
146
162
  }
147
163
 
148
164
  .caseItem.active.caseItemNotConfirmed:hover {
149
- background-color: color-mix(in lab, var(--warning) 20%, var(--backgroundLight));
165
+ background-color: color-mix(
166
+ in lab,
167
+ var(--warning) 20%,
168
+ var(--backgroundLight)
169
+ );
150
170
  }
151
171
 
152
172
  .caseItemConfirmed {
153
- background-color: color-mix(in lab, var(--success) 20%, var(--backgroundLight));
173
+ background-color: color-mix(
174
+ in lab,
175
+ var(--success) 20%,
176
+ var(--backgroundLight)
177
+ );
154
178
  }
155
179
 
156
180
  .caseItemConfirmed:hover {
157
- background-color: color-mix(in lab, var(--success) 28%, var(--backgroundLight));
181
+ background-color: color-mix(
182
+ in lab,
183
+ var(--success) 28%,
184
+ var(--backgroundLight)
185
+ );
158
186
  }
159
187
 
160
188
  .caseItem.active.caseItemConfirmed {
161
- background-color: color-mix(in lab, var(--success) 20%, var(--backgroundLight));
189
+ background-color: color-mix(
190
+ in lab,
191
+ var(--success) 20%,
192
+ var(--backgroundLight)
193
+ );
162
194
  }
163
195
 
164
196
  .caseItem.active.caseItemConfirmed:hover {
165
- background-color: color-mix(in lab, var(--success) 28%, var(--backgroundLight));
197
+ background-color: color-mix(
198
+ in lab,
199
+ var(--success) 28%,
200
+ var(--backgroundLight)
201
+ );
166
202
  }
@@ -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,14 @@ 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
+ requestClose,
24
+ overlayProps,
25
+ getCloseButtonProps
26
+ } = useOverlayDismiss({
27
+ isOpen,
28
+ onClose
29
+ });
21
30
  const [caseConfirmationStatus, setCaseConfirmationStatus] = useState<{
22
31
  [caseNum: string]: { includeConfirmation: boolean; isConfirmed: boolean }
23
32
  }>({});
@@ -28,19 +37,6 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
28
37
  setError('');
29
38
  };
30
39
 
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
40
  useEffect(() => {
45
41
  if (isOpen) {
46
42
  const loadingTimer = window.setTimeout(() => {
@@ -144,11 +140,15 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
144
140
  if (!isOpen) return null;
145
141
 
146
142
  return (
147
- <div className={styles.modalOverlay}>
143
+ <div
144
+ className={styles.modalOverlay}
145
+ aria-label="Close cases dialog"
146
+ {...overlayProps}
147
+ >
148
148
  <div className={styles.modal}>
149
149
  <header className={styles.modalHeader}>
150
150
  <h2>All Cases</h2>
151
- <button onClick={onClose} className={styles.closeButton}>&times;</button>
151
+ <button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close cases dialog' })}>&times;</button>
152
152
  </header>
153
153
 
154
154
  <div className={styles.modalContent}>
@@ -176,7 +176,7 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
176
176
  className={`${styles.caseItem} ${currentCase === caseNum ? styles.active : ''} ${confirmationClass}`}
177
177
  onClick={() => {
178
178
  onSelectCase(caseNum);
179
- onClose();
179
+ requestClose();
180
180
  }}
181
181
  >
182
182
  {caseNum}
@@ -1,6 +1,11 @@
1
1
  /* Case Management */
2
2
  .caseSection {
3
- margin-bottom: 2rem;
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 0.75rem;
6
+ height: 100%;
7
+ min-height: 0;
8
+ margin-bottom: 0;
4
9
  }
5
10
 
6
11
  .caseSection h4 {
@@ -141,17 +146,18 @@
141
146
  }
142
147
 
143
148
  .filesModalSection {
144
- margin: 1rem 0;
149
+ margin: 0.25rem 0 0;
145
150
  }
146
151
 
147
152
  .filesModalButton {
148
153
  width: 100%;
149
- padding: 0.75rem;
154
+ padding: 0.625rem 0.75rem;
150
155
  background-color: #17a2b8;
151
156
  color: white;
152
157
  border: none;
153
158
  border-radius: 6px;
154
159
  font-weight: 500;
160
+ font-size: 0.9rem;
155
161
  cursor: pointer;
156
162
  transition: all 0.2s;
157
163
  box-sizing: border-box;
@@ -179,16 +185,39 @@
179
185
 
180
186
  /* Files Section */
181
187
  .filesSection {
182
- margin-top: 2rem;
188
+ display: flex;
189
+ flex: 1;
190
+ flex-direction: column;
191
+ gap: 0.625rem;
192
+ min-height: 0;
193
+ margin-top: 0.25rem;
183
194
  }
184
195
 
185
196
  .filesSection h4 {
186
- margin-bottom: 1rem;
187
- font-size: 1.3rem;
197
+ margin-bottom: 0;
198
+ font-size: 1.1rem;
188
199
  font-weight: 900;
189
200
  text-align: center;
190
201
  }
191
202
 
203
+ .emptyCaseHeader {
204
+ margin-top: 0.75rem;
205
+ }
206
+
207
+ .fileListPlaceholder {
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ min-height: 5rem;
212
+ padding: 0.875rem;
213
+ border: 1px dashed #dee2e6;
214
+ border-radius: 6px;
215
+ background: #f8f9fa;
216
+ color: #6c757d;
217
+ font-size: 0.875rem;
218
+ text-align: center;
219
+ }
220
+
192
221
  .emptyState {
193
222
  color: #6c757d;
194
223
  font-size: 0.9rem;
@@ -207,10 +236,20 @@
207
236
  border: 1px solid #dee2e6;
208
237
  border-radius: 6px;
209
238
  overflow: hidden;
210
- max-height: calc(5 * 3.5rem);
239
+ flex: 1;
240
+ min-height: 0;
241
+ max-height: none;
211
242
  overflow-y: auto;
212
243
  }
213
244
 
245
+ .fileListMessage {
246
+ padding: 0.875rem;
247
+ color: #6c757d;
248
+ font-size: 0.875rem;
249
+ text-align: center;
250
+ background: #f8f9fa;
251
+ }
252
+
214
253
  .fileList::-webkit-scrollbar {
215
254
  width: 6px;
216
255
  }
@@ -231,7 +270,7 @@
231
270
  .fileItem {
232
271
  display: flex;
233
272
  align-items: center;
234
- padding: 0.5rem 1rem;
273
+ padding: 0.375rem 0.625rem;
235
274
  border-bottom: 1px solid #dee2e6;
236
275
  background: white;
237
276
  transition: background-color 0.2s;
@@ -240,13 +279,14 @@
240
279
  .fileButton {
241
280
  flex: 1;
242
281
  text-align: left;
243
- padding: 0.5rem;
282
+ padding: 0.375rem;
244
283
  background: none;
245
284
  border: none;
246
285
  cursor: pointer;
247
286
  overflow: hidden;
248
287
  text-overflow: ellipsis;
249
288
  white-space: nowrap;
289
+ font-size: 0.875rem;
250
290
  }
251
291
 
252
292
  .fileItem:last-child {
@@ -335,14 +375,14 @@
335
375
  background: none;
336
376
  border: none;
337
377
  color: #dc3545;
338
- font-size: 1.2rem;
378
+ font-size: 1rem;
339
379
  cursor: pointer;
340
- padding: 0.5rem;
380
+ padding: 0.375rem;
341
381
  display: flex;
342
382
  align-items: center;
343
383
  justify-content: center;
344
- min-width: 32px;
345
- height: 32px;
384
+ min-width: 28px;
385
+ height: 28px;
346
386
  }
347
387
 
348
388
  .deleteButton:hover {
@@ -404,18 +444,19 @@
404
444
  /* Notes Toggle */
405
445
 
406
446
  .sidebarToggle {
407
- margin-bottom: 1rem;
408
- padding: 1rem 1rem;
447
+ margin-top: 0;
448
+ padding: 0;
409
449
  }
410
450
 
411
451
  .sidebarToggle button {
412
452
  width: 100%;
413
- padding: 0.75rem;
453
+ padding: 0.625rem 0.75rem;
414
454
  background-color: var(--primary);
415
455
  color: white;
416
456
  border: none;
417
457
  border-radius: 6px;
418
458
  font-weight: 500;
459
+ font-size: 0.9rem;
419
460
  cursor: pointer;
420
461
  transition: all 0.2s;
421
462
  }
@@ -643,27 +684,27 @@
643
684
  background: #fff3cd;
644
685
  border: 1px solid #ffeaa7;
645
686
  border-radius: 4px;
646
- padding: 0.75rem;
647
- margin-bottom: 1rem;
687
+ padding: 0.625rem 0.75rem;
688
+ margin-bottom: 0;
648
689
  }
649
690
 
650
691
  .caseNumber {
651
692
  margin: 0;
652
- font-size: 1rem;
693
+ font-size: 0.95rem;
653
694
  font-weight: 600;
654
695
  }
655
696
 
656
697
  /* Case Confirmation Status Indicators */
657
698
  .caseNumber.caseNotConfirmed {
658
699
  background-color: #fffacd;
659
- padding: 0.75rem;
700
+ padding: 0.625rem 0.75rem;
660
701
  border-radius: 4px;
661
702
  margin: 0;
662
703
  }
663
704
 
664
705
  .caseNumber.caseConfirmed {
665
706
  background-color: #c8e6c9;
666
- padding: 0.75rem;
707
+ padding: 0.625rem 0.75rem;
667
708
  border-radius: 4px;
668
709
  margin: 0;
669
710
  }
@@ -6,16 +6,20 @@
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
 
12
13
  .modal {
14
+ position: relative;
13
15
  background: var(--backgroundLight);
14
16
  border-radius: var(--spaceXS);
15
17
  width: 90%;
16
18
  max-width: var(--maxWidthL);
17
19
  max-height: 80vh;
18
- box-shadow: 0 var(--spaceXS) var(--spaceL) color-mix(in lab, var(--black) 10%, transparent);
20
+ box-shadow: 0 var(--spaceXS) var(--spaceL)
21
+ color-mix(in lab, var(--black) 10%, transparent);
22
+ cursor: default;
19
23
  transition: background-color var(--durationM) var(--bezierFastoutSlowin);
20
24
  display: flex;
21
25
  flex-direction: column;
@@ -177,33 +181,65 @@
177
181
  }
178
182
  /* Confirmation Status Indicators */
179
183
  .fileItemNotConfirmed {
180
- background-color: color-mix(in lab, var(--warning) 15%, var(--backgroundLight));
184
+ background-color: color-mix(
185
+ in lab,
186
+ var(--warning) 15%,
187
+ var(--backgroundLight)
188
+ );
181
189
  }
182
190
 
183
191
  .fileItemNotConfirmed:hover {
184
- background-color: color-mix(in lab, var(--warning) 20%, var(--backgroundLight));
192
+ background-color: color-mix(
193
+ in lab,
194
+ var(--warning) 20%,
195
+ var(--backgroundLight)
196
+ );
185
197
  }
186
198
 
187
199
  .fileItem.active.fileItemNotConfirmed {
188
- background-color: color-mix(in lab, var(--warning) 15%, var(--backgroundLight));
200
+ background-color: color-mix(
201
+ in lab,
202
+ var(--warning) 15%,
203
+ var(--backgroundLight)
204
+ );
189
205
  }
190
206
 
191
207
  .fileItem.active.fileItemNotConfirmed:hover {
192
- background-color: color-mix(in lab, var(--warning) 20%, var(--backgroundLight));
208
+ background-color: color-mix(
209
+ in lab,
210
+ var(--warning) 20%,
211
+ var(--backgroundLight)
212
+ );
193
213
  }
194
214
 
195
215
  .fileItemConfirmed {
196
- background-color: color-mix(in lab, var(--success) 20%, var(--backgroundLight));
216
+ background-color: color-mix(
217
+ in lab,
218
+ var(--success) 20%,
219
+ var(--backgroundLight)
220
+ );
197
221
  }
198
222
 
199
223
  .fileItemConfirmed:hover {
200
- background-color: color-mix(in lab, var(--success) 28%, var(--backgroundLight));
224
+ background-color: color-mix(
225
+ in lab,
226
+ var(--success) 28%,
227
+ var(--backgroundLight)
228
+ );
201
229
  }
202
230
 
203
231
  .fileItem.active.fileItemConfirmed {
204
- background-color: color-mix(in lab, var(--success) 20%, var(--backgroundLight));
232
+ background-color: color-mix(
233
+ in lab,
234
+ var(--success) 20%,
235
+ var(--backgroundLight)
236
+ );
205
237
  }
206
238
 
207
239
  .fileItem.active.fileItemConfirmed:hover {
208
- background-color: color-mix(in lab, var(--success) 28%, var(--backgroundLight));
209
- }
240
+ background-color: color-mix(
241
+ in lab,
242
+ var(--success) 28%,
243
+ var(--backgroundLight)
244
+ );
245
+ }
@@ -1,6 +1,7 @@
1
1
  import type React from 'react';
2
2
  import { useState, useContext, useEffect } from 'react';
3
3
  import { AuthContext } from '~/contexts/auth.context';
4
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
4
5
  import { deleteFile } from '~/components/actions/image-manage';
5
6
  import { getFileAnnotations } from '~/utils/data';
6
7
  import { type FileData } from '~/types';
@@ -33,6 +34,14 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
33
34
  const [currentPage, setCurrentPage] = useState(0);
34
35
  const [deletingFileId, setDeletingFileId] = useState<string | null>(null);
35
36
  const [fileConfirmationStatus, setFileConfirmationStatus] = useState<FileConfirmationStatus>({});
37
+ const {
38
+ requestClose,
39
+ overlayProps,
40
+ getCloseButtonProps
41
+ } = useOverlayDismiss({
42
+ isOpen,
43
+ onClose
44
+ });
36
45
 
37
46
  const totalPages = Math.ceil(files.length / FILES_PER_PAGE);
38
47
  const startIndex = currentPage * FILES_PER_PAGE;
@@ -88,24 +97,9 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
88
97
  fetchConfirmationStatuses();
89
98
  }, [isOpen, currentCase, currentPage, files, user]);
90
99
 
91
- useEffect(() => {
92
- const handleEscape = (event: KeyboardEvent) => {
93
- if (event.key === 'Escape' && isOpen) {
94
- onClose();
95
- }
96
- };
97
-
98
- if (isOpen) {
99
- document.addEventListener('keydown', handleEscape);
100
- return () => {
101
- document.removeEventListener('keydown', handleEscape);
102
- };
103
- }
104
- }, [isOpen, onClose]);
105
-
106
100
  const handleFileSelect = (file: FileData) => {
107
101
  onFileSelect?.(file);
108
- onClose();
102
+ requestClose();
109
103
  };
110
104
 
111
105
  const handleDeleteFile = async (fileId: string, event: React.MouseEvent) => {
@@ -123,10 +117,15 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
123
117
  setDeletingFileId(fileId);
124
118
 
125
119
  try {
126
- await deleteFile(user, currentCase, fileId);
120
+ const deleteResult = await deleteFile(user, currentCase, fileId);
127
121
  // Remove the deleted file from the list
128
122
  const updatedFiles = files.filter(f => f.id !== fileId);
129
123
  setFiles(updatedFiles);
124
+
125
+ if (deleteResult.imageMissing) {
126
+ setError(`File record deleted. Image asset "${deleteResult.fileName}" was not found and was skipped.`);
127
+ setTimeout(() => setError(null), 4000);
128
+ }
130
129
 
131
130
  // Adjust page if needed
132
131
  const newTotalPages = Math.ceil(updatedFiles.length / FILES_PER_PAGE);
@@ -166,15 +165,15 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
166
165
  if (!isOpen) return null;
167
166
 
168
167
  return (
169
- <div className={styles.modalOverlay}>
168
+ <div
169
+ className={styles.modalOverlay}
170
+ aria-label="Close files dialog"
171
+ {...overlayProps}
172
+ >
170
173
  <div className={styles.modal}>
171
174
  <div className={styles.modalHeader}>
172
175
  <h2>Files in Case {currentCase}</h2>
173
- <button
174
- className={styles.closeButton}
175
- onClick={onClose}
176
- aria-label="Close modal"
177
- >
176
+ <button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close files dialog' })}>
178
177
  ×
179
178
  </button>
180
179
  </div>
@@ -0,0 +1,49 @@
1
+ .overlay {
2
+ position: fixed;
3
+ inset: 0;
4
+ background-color: color-mix(in lab, var(--background) 56%, transparent);
5
+ display: flex;
6
+ justify-content: center;
7
+ align-items: center;
8
+ z-index: var(--zIndex5);
9
+ }
10
+
11
+ .modal {
12
+ position: relative;
13
+ width: min(900px, calc(100vw - 2rem));
14
+ max-height: calc(100vh - 4rem);
15
+ background: var(--backgroundLight);
16
+ border-radius: var(--spaceXS);
17
+ box-shadow: 0 var(--spaceXS) var(--spaceL)
18
+ color-mix(in lab, var(--black) 16%, transparent);
19
+ display: flex;
20
+ flex-direction: column;
21
+ overflow: hidden;
22
+ }
23
+
24
+ .header {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ padding: var(--spaceM) var(--spaceL);
29
+ border-bottom: 1px solid color-mix(in lab, var(--text) 12%, transparent);
30
+ }
31
+
32
+ .title {
33
+ margin: 0;
34
+ color: var(--textTitle);
35
+ font-size: var(--fontSizeBodyM);
36
+ font-weight: var(--fontWeightMedium);
37
+ }
38
+
39
+ .closeButton {
40
+ background: none;
41
+ border: none;
42
+ color: var(--textLight);
43
+ }
44
+
45
+ .content {
46
+ padding: var(--spaceM) var(--spaceL);
47
+ overflow-y: auto;
48
+ max-height: calc(100vh - 11rem);
49
+ }
@@ -0,0 +1,66 @@
1
+ import type { User } from 'firebase/auth';
2
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
+ import { NotesSidebar } from './notes-sidebar';
4
+ import styles from './notes-editor-modal.module.css';
5
+
6
+ interface NotesEditorModalProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ currentCase: string;
10
+ user: User;
11
+ imageId: string;
12
+ originalFileName?: string;
13
+ onAnnotationRefresh?: () => void;
14
+ isUploading?: boolean;
15
+ }
16
+
17
+ export const NotesEditorModal = ({
18
+ isOpen,
19
+ onClose,
20
+ currentCase,
21
+ user,
22
+ imageId,
23
+ originalFileName,
24
+ onAnnotationRefresh,
25
+ isUploading = false,
26
+ }: NotesEditorModalProps) => {
27
+ const {
28
+ requestClose,
29
+ overlayProps,
30
+ getCloseButtonProps,
31
+ } = useOverlayDismiss({
32
+ isOpen,
33
+ onClose,
34
+ });
35
+
36
+ if (!isOpen) {
37
+ return null;
38
+ }
39
+
40
+ return (
41
+ <div className={styles.overlay} aria-label="Close image notes dialog" {...overlayProps}>
42
+ <div className={styles.modal} role="dialog" aria-modal="true" aria-label="Image Notes">
43
+ <div className={styles.header}>
44
+ <h2 className={styles.title}>Image Notes</h2>
45
+ <button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close image notes dialog' })}>
46
+ ×
47
+ </button>
48
+ </div>
49
+ <div className={styles.content}>
50
+ <NotesSidebar
51
+ currentCase={currentCase}
52
+ onReturn={requestClose}
53
+ user={user}
54
+ imageId={imageId}
55
+ onAnnotationRefresh={onAnnotationRefresh}
56
+ originalFileName={originalFileName}
57
+ isUploading={isUploading}
58
+ showReturnButton={false}
59
+ stickyActionBar={true}
60
+ compactLayout={true}
61
+ />
62
+ </div>
63
+ </div>
64
+ </div>
65
+ );
66
+ };