@striae-org/striae 4.1.0 → 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 (91) 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 +9 -2
  12. package/app/components/actions/image-manage.ts +77 -44
  13. package/app/components/audit/user-audit-viewer.tsx +19 -8
  14. package/app/components/audit/user-audit.module.css +21 -0
  15. package/app/components/audit/viewer/audit-entries-list.tsx +7 -0
  16. package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
  17. package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
  18. package/app/components/audit/viewer/use-audit-viewer-data.ts +21 -1
  19. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  20. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  21. package/app/components/canvas/canvas.module.css +64 -54
  22. package/app/components/canvas/canvas.tsx +14 -16
  23. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  24. package/app/components/canvas/confirmation/confirmation.tsx +6 -12
  25. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  26. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  27. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  28. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  29. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  30. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  31. package/app/components/navbar/navbar.module.css +447 -0
  32. package/app/components/navbar/navbar.tsx +377 -0
  33. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
  34. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
  35. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  36. package/app/components/sidebar/case-export/case-export.tsx +8 -46
  37. package/app/components/sidebar/case-import/case-import.module.css +23 -0
  38. package/app/components/sidebar/case-import/case-import.tsx +64 -16
  39. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  40. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  41. package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
  42. package/app/components/sidebar/cases/cases-modal.module.css +1 -0
  43. package/app/components/sidebar/cases/cases-modal.tsx +6 -8
  44. package/app/components/sidebar/cases/cases.module.css +62 -21
  45. package/app/components/sidebar/files/files-modal.module.css +1 -0
  46. package/app/components/sidebar/files/files-modal.tsx +12 -13
  47. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  48. package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
  49. package/app/components/sidebar/notes/notes-modal.tsx +7 -8
  50. package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
  51. package/app/components/sidebar/notes/notes.module.css +153 -0
  52. package/app/components/sidebar/sidebar-container.tsx +15 -28
  53. package/app/components/sidebar/sidebar.module.css +5 -69
  54. package/app/components/sidebar/sidebar.tsx +24 -125
  55. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  56. package/app/components/user/inactivity-warning.module.css +1 -0
  57. package/app/components/user/inactivity-warning.tsx +15 -2
  58. package/app/components/user/manage-profile.tsx +23 -10
  59. package/app/hooks/useOverlayDismiss.ts +52 -4
  60. package/app/routes/auth/login.tsx +785 -774
  61. package/app/routes/striae/striae.module.css +10 -3
  62. package/app/routes/striae/striae.tsx +469 -30
  63. package/app/services/audit/audit.service.ts +173 -27
  64. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  65. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
  66. package/app/services/audit/builders/index.ts +1 -0
  67. package/app/types/audit.ts +3 -1
  68. package/app/types/case.ts +29 -0
  69. package/app/types/import.ts +3 -0
  70. package/app/utils/data/permissions.ts +16 -1
  71. package/app/utils/forensics/audit-export-signature.ts +5 -1
  72. package/app/utils/forensics/confirmation-signature.ts +3 -0
  73. package/app/utils/forensics/export-verification.ts +497 -22
  74. package/package.json +3 -3
  75. package/scripts/deploy-primershear-emails.sh +2 -1
  76. package/worker-configuration.d.ts +1 -1
  77. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  78. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  79. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  80. package/workers/data-worker/wrangler.jsonc.example +1 -1
  81. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  82. package/workers/image-worker/wrangler.jsonc.example +1 -1
  83. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  84. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  85. package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
  86. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  87. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  88. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  89. package/workers/user-worker/wrangler.jsonc.example +1 -1
  90. package/wrangler.toml.example +1 -1
  91. package/public/.well-known/keybase.txt +0 -56
@@ -11,6 +11,7 @@
11
11
  }
12
12
 
13
13
  .modal {
14
+ position: relative;
14
15
  background: var(--backgroundLight);
15
16
  border-radius: var(--spaceXS);
16
17
  width: 90%;
@@ -20,8 +20,9 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
20
20
  const [error, setError] = useState<string>('');
21
21
  const [currentPage, setCurrentPage] = useState(0);
22
22
  const {
23
- handleOverlayMouseDown,
24
- handleOverlayKeyDown
23
+ requestClose,
24
+ overlayProps,
25
+ getCloseButtonProps
25
26
  } = useOverlayDismiss({
26
27
  isOpen,
27
28
  onClose
@@ -141,16 +142,13 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
141
142
  return (
142
143
  <div
143
144
  className={styles.modalOverlay}
144
- onMouseDown={handleOverlayMouseDown}
145
- onKeyDown={handleOverlayKeyDown}
146
- role="button"
147
- tabIndex={0}
148
145
  aria-label="Close cases dialog"
146
+ {...overlayProps}
149
147
  >
150
148
  <div className={styles.modal}>
151
149
  <header className={styles.modalHeader}>
152
150
  <h2>All Cases</h2>
153
- <button onClick={onClose} className={styles.closeButton}>&times;</button>
151
+ <button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close cases dialog' })}>&times;</button>
154
152
  </header>
155
153
 
156
154
  <div className={styles.modalContent}>
@@ -178,7 +176,7 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
178
176
  className={`${styles.caseItem} ${currentCase === caseNum ? styles.active : ''} ${confirmationClass}`}
179
177
  onClick={() => {
180
178
  onSelectCase(caseNum);
181
- onClose();
179
+ requestClose();
182
180
  }}
183
181
  >
184
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
  }
@@ -11,6 +11,7 @@
11
11
  }
12
12
 
13
13
  .modal {
14
+ position: relative;
14
15
  background: var(--backgroundLight);
15
16
  border-radius: var(--spaceXS);
16
17
  width: 90%;
@@ -35,8 +35,9 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
35
35
  const [deletingFileId, setDeletingFileId] = useState<string | null>(null);
36
36
  const [fileConfirmationStatus, setFileConfirmationStatus] = useState<FileConfirmationStatus>({});
37
37
  const {
38
- handleOverlayMouseDown,
39
- handleOverlayKeyDown
38
+ requestClose,
39
+ overlayProps,
40
+ getCloseButtonProps
40
41
  } = useOverlayDismiss({
41
42
  isOpen,
42
43
  onClose
@@ -98,7 +99,7 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
98
99
 
99
100
  const handleFileSelect = (file: FileData) => {
100
101
  onFileSelect?.(file);
101
- onClose();
102
+ requestClose();
102
103
  };
103
104
 
104
105
  const handleDeleteFile = async (fileId: string, event: React.MouseEvent) => {
@@ -116,10 +117,15 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
116
117
  setDeletingFileId(fileId);
117
118
 
118
119
  try {
119
- await deleteFile(user, currentCase, fileId);
120
+ const deleteResult = await deleteFile(user, currentCase, fileId);
120
121
  // Remove the deleted file from the list
121
122
  const updatedFiles = files.filter(f => f.id !== fileId);
122
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
+ }
123
129
 
124
130
  // Adjust page if needed
125
131
  const newTotalPages = Math.ceil(updatedFiles.length / FILES_PER_PAGE);
@@ -161,20 +167,13 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
161
167
  return (
162
168
  <div
163
169
  className={styles.modalOverlay}
164
- onMouseDown={handleOverlayMouseDown}
165
- onKeyDown={handleOverlayKeyDown}
166
- role="button"
167
- tabIndex={0}
168
170
  aria-label="Close files dialog"
171
+ {...overlayProps}
169
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
+ };
@@ -12,8 +12,9 @@ interface NotesModalProps {
12
12
  export const NotesModal = ({ isOpen, onClose, notes, onSave }: NotesModalProps) => {
13
13
  const [tempNotes, setTempNotes] = useState(notes);
14
14
  const {
15
- handleOverlayMouseDown,
16
- handleOverlayKeyDown
15
+ requestClose,
16
+ overlayProps,
17
+ getCloseButtonProps
17
18
  } = useOverlayDismiss({
18
19
  isOpen,
19
20
  onClose
@@ -23,19 +24,17 @@ export const NotesModal = ({ isOpen, onClose, notes, onSave }: NotesModalProps)
23
24
 
24
25
  const handleSave = () => {
25
26
  onSave(tempNotes);
26
- onClose();
27
+ requestClose();
27
28
  };
28
29
 
29
30
  return (
30
31
  <div
31
32
  className={styles.modalOverlay}
32
- onMouseDown={handleOverlayMouseDown}
33
- onKeyDown={handleOverlayKeyDown}
34
- role="button"
35
- tabIndex={0}
36
33
  aria-label="Close notes dialog"
34
+ {...overlayProps}
37
35
  >
38
36
  <div className={styles.modal}>
37
+ <button {...getCloseButtonProps({ ariaLabel: 'Close notes dialog' })}>×</button>
39
38
  <h5 className={styles.modalTitle}>Additional Notes</h5>
40
39
  <textarea
41
40
  value={tempNotes}
@@ -45,7 +44,7 @@ export const NotesModal = ({ isOpen, onClose, notes, onSave }: NotesModalProps)
45
44
  />
46
45
  <div className={styles.modalButtons}>
47
46
  <button onClick={handleSave} className={styles.saveButton}>Save</button>
48
- <button onClick={onClose} className={styles.cancelButton}>Cancel</button>
47
+ <button onClick={requestClose} className={styles.cancelButton}>Cancel</button>
49
48
  </div>
50
49
  </div>
51
50
  </div>