@striae-org/striae 4.1.0 → 4.2.1

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 (124) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +1 -1
  3. package/app/components/actions/case-export/core-export.ts +14 -8
  4. package/app/components/actions/case-export/data-processing.ts +1 -0
  5. package/app/components/actions/case-export/download-handlers.ts +7 -0
  6. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  7. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  8. package/app/components/actions/case-import/orchestrator.ts +78 -32
  9. package/app/components/actions/case-import/storage-operations.ts +97 -8
  10. package/app/components/actions/case-import/zip-processing.ts +159 -86
  11. package/app/components/actions/case-manage.ts +463 -8
  12. package/app/components/actions/confirm-export.ts +9 -2
  13. package/app/components/actions/image-manage.ts +77 -44
  14. package/app/components/audit/user-audit-viewer.tsx +19 -8
  15. package/app/components/audit/user-audit.module.css +21 -0
  16. package/app/components/audit/viewer/audit-entries-list.tsx +12 -2
  17. package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
  18. package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
  19. package/app/components/audit/viewer/use-audit-viewer-data.ts +24 -1
  20. package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
  21. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  22. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  23. package/app/components/canvas/canvas.module.css +64 -54
  24. package/app/components/canvas/canvas.tsx +14 -16
  25. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  26. package/app/components/canvas/confirmation/confirmation.tsx +12 -14
  27. package/app/components/colors/colors.module.css +4 -3
  28. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  29. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  30. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  31. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  32. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  33. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  34. package/app/components/navbar/navbar.module.css +447 -0
  35. package/app/components/navbar/navbar.tsx +402 -0
  36. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
  37. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
  38. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  39. package/app/components/sidebar/case-export/case-export.tsx +8 -46
  40. package/app/components/sidebar/case-import/case-import.module.css +23 -0
  41. package/app/components/sidebar/case-import/case-import.tsx +64 -16
  42. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  43. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  44. package/app/components/sidebar/cases/case-sidebar.tsx +68 -588
  45. package/app/components/sidebar/cases/cases-modal.module.css +1 -0
  46. package/app/components/sidebar/cases/cases-modal.tsx +82 -43
  47. package/app/components/sidebar/cases/cases.module.css +82 -21
  48. package/app/components/sidebar/files/files-modal.module.css +1 -0
  49. package/app/components/sidebar/files/files-modal.tsx +49 -52
  50. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  51. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +187 -138
  52. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  53. package/app/components/sidebar/notes/notes-editor-modal.tsx +64 -0
  54. package/app/components/sidebar/notes/notes.module.css +170 -1
  55. package/app/components/sidebar/sidebar-container.tsx +16 -28
  56. package/app/components/sidebar/sidebar.module.css +5 -69
  57. package/app/components/sidebar/sidebar.tsx +27 -125
  58. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  59. package/app/components/user/inactivity-warning.module.css +1 -0
  60. package/app/components/user/inactivity-warning.tsx +15 -2
  61. package/app/components/user/manage-profile.tsx +23 -10
  62. package/app/{tailwind.css → global.css} +1 -3
  63. package/app/hooks/useOverlayDismiss.ts +54 -4
  64. package/app/root.tsx +1 -1
  65. package/app/routes/auth/login.tsx +785 -774
  66. package/app/routes/striae/striae.module.css +10 -3
  67. package/app/routes/striae/striae.tsx +475 -30
  68. package/app/services/audit/audit.service.ts +173 -27
  69. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  70. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
  71. package/app/services/audit/builders/index.ts +1 -0
  72. package/app/types/audit.ts +4 -1
  73. package/app/types/case.ts +29 -0
  74. package/app/types/import.ts +3 -0
  75. package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
  76. package/app/utils/data/data-operations.ts +17 -861
  77. package/app/utils/data/index.ts +11 -1
  78. package/app/utils/data/operations/batch-operations.ts +113 -0
  79. package/app/utils/data/operations/case-operations.ts +168 -0
  80. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  81. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  82. package/app/utils/data/operations/index.ts +7 -0
  83. package/app/utils/data/operations/signing-operations.ts +225 -0
  84. package/app/utils/data/operations/types.ts +42 -0
  85. package/app/utils/data/operations/validation-operations.ts +48 -0
  86. package/app/utils/data/permissions.ts +16 -1
  87. package/app/utils/forensics/audit-export-signature.ts +5 -1
  88. package/app/utils/forensics/confirmation-signature.ts +3 -0
  89. package/app/utils/forensics/export-verification.ts +426 -22
  90. package/functions/api/_shared/firebase-auth.ts +2 -7
  91. package/functions/api/image/[[path]].ts +20 -23
  92. package/functions/api/pdf/[[path]].ts +27 -8
  93. package/package.json +7 -12
  94. package/scripts/deploy-primershear-emails.sh +2 -1
  95. package/worker-configuration.d.ts +3 -3
  96. package/workers/audit-worker/package.json +1 -1
  97. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  98. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  99. package/workers/data-worker/package.json +1 -1
  100. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  101. package/workers/data-worker/wrangler.jsonc.example +1 -1
  102. package/workers/image-worker/package.json +1 -1
  103. package/workers/image-worker/src/image-worker.example.ts +16 -5
  104. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  105. package/workers/image-worker/wrangler.jsonc.example +1 -1
  106. package/workers/keys-worker/package.json +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/package.json +1 -1
  110. package/workers/pdf-worker/src/formats/format-striae.ts +9 -14
  111. package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
  112. package/workers/pdf-worker/src/report-types.ts +3 -3
  113. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  114. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  115. package/workers/user-worker/package.json +1 -1
  116. package/workers/user-worker/src/user-worker.example.ts +17 -0
  117. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  118. package/workers/user-worker/wrangler.jsonc.example +1 -1
  119. package/wrangler.toml.example +1 -1
  120. package/NOTICE +0 -13
  121. package/app/components/sidebar/notes/notes-modal.tsx +0 -53
  122. package/postcss.config.js +0 -6
  123. package/public/.well-known/keybase.txt +0 -56
  124. package/tailwind.config.ts +0 -22
@@ -1,33 +1,31 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import type { User } from 'firebase/auth';
3
3
  import { ColorSelector } from '~/components/colors/colors';
4
- import { NotesModal } from './notes-modal';
4
+ import { AddlNotesModal } from './addl-notes-modal';
5
5
  import { getNotes, saveNotes } from '~/components/actions/notes-manage';
6
6
  import { type AnnotationData } from '~/types/annotations';
7
7
  import { resolveEarliestAnnotationTimestamp } from '~/utils/ui';
8
8
  import { auditService } from '~/services/audit';
9
9
  import styles from './notes.module.css';
10
10
 
11
- interface NotesSidebarProps {
11
+ interface NotesEditorFormProps {
12
12
  currentCase: string;
13
- onReturn: () => void;
14
13
  user: User;
15
14
  imageId: string;
16
15
  onAnnotationRefresh?: () => void;
17
16
  originalFileName?: string;
18
17
  isUploading?: boolean;
18
+ showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
19
19
  }
20
20
 
21
21
  type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
22
22
  type ClassType = 'Bullet' | 'Cartridge Case' | 'Other';
23
23
  type IndexType = 'number' | 'color';
24
24
 
25
- export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false }: NotesSidebarProps) => {
25
+ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, showNotification: externalShowNotification }: NotesEditorFormProps) => {
26
26
  // Loading/Saving Notes States
27
27
  const [isLoading, setIsLoading] = useState(false);
28
28
  const [loadError, setLoadError] = useState<string>();
29
- const [saveError, setSaveError] = useState<string>();
30
- const [saveSuccess, setSaveSuccess] = useState(false);
31
29
  const [isConfirmedImage, setIsConfirmedImage] = useState(false);
32
30
  // Case numbers state
33
31
  const [leftCase, setLeftCase] = useState('');
@@ -56,16 +54,24 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
56
54
  // Additional Notes Modal
57
55
  const [isModalOpen, setIsModalOpen] = useState(false);
58
56
  const [additionalNotes, setAdditionalNotes] = useState('');
57
+ const [isCaseInfoOpen, setIsCaseInfoOpen] = useState(true);
58
+ const [isClassOpen, setIsClassOpen] = useState(true);
59
+ const [isIndexOpen, setIsIndexOpen] = useState(true);
60
+ const [isSupportOpen, setIsSupportOpen] = useState(true);
59
61
  const areInputsDisabled = isUploading || isConfirmedImage;
60
62
 
63
+ const notificationHandler = (message: string, type: 'success' | 'error' | 'warning' = 'success') => {
64
+ if (externalShowNotification) {
65
+ externalShowNotification(message, type);
66
+ }
67
+ };
68
+
61
69
  useEffect(() => {
62
70
  const loadExistingNotes = async () => {
63
71
  if (!imageId || !currentCase) return;
64
72
 
65
73
  setIsLoading(true);
66
74
  setLoadError(undefined);
67
- setSaveError(undefined);
68
- setSaveSuccess(false);
69
75
  setIsConfirmedImage(false);
70
76
 
71
77
  try {
@@ -121,9 +127,6 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
121
127
  return;
122
128
  }
123
129
 
124
- setSaveError(undefined);
125
- setSaveSuccess(false);
126
-
127
130
  let existingData: AnnotationData | null = null;
128
131
 
129
132
  try {
@@ -132,7 +135,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
132
135
 
133
136
  if (existingData?.confirmationData) {
134
137
  setIsConfirmedImage(true);
135
- setSaveError('This image is confirmed. Notes cannot be modified.');
138
+ notificationHandler('This image is confirmed. Notes cannot be modified.', 'error');
136
139
  return;
137
140
  }
138
141
 
@@ -186,13 +189,12 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
186
189
  existingData,
187
190
  annotationData,
188
191
  currentCase,
189
- 'notes-sidebar',
192
+ 'notes-editor-form',
190
193
  imageId,
191
194
  originalFileName
192
195
  );
193
196
 
194
- setSaveSuccess(true);
195
- setTimeout(() => setSaveSuccess(false), 3000);
197
+ notificationHandler('Notes saved successfully.', 'success');
196
198
 
197
199
  // Refresh annotation data after saving notes
198
200
  if (onAnnotationRefresh) {
@@ -203,9 +205,9 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
203
205
  const errorMessage = error instanceof Error ? error.message : '';
204
206
  if (errorMessage.toLowerCase().includes('confirmed image')) {
205
207
  setIsConfirmedImage(true);
206
- setSaveError('This image is confirmed. Notes cannot be modified.');
208
+ notificationHandler('This image is confirmed. Notes cannot be modified.', 'error');
207
209
  } else {
208
- setSaveError('Failed to save notes. Please try again.');
210
+ notificationHandler('Failed to save notes. Please try again.', 'error');
209
211
  }
210
212
 
211
213
  // Audit logging for failed annotation save
@@ -216,7 +218,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
216
218
  existingData,
217
219
  null, // Failed save, no new value
218
220
  currentCase,
219
- 'notes-sidebar',
221
+ 'notes-editor-form',
220
222
  imageId,
221
223
  originalFileName
222
224
  );
@@ -227,7 +229,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
227
229
  };
228
230
 
229
231
  return (
230
- <div className={styles.notesSidebar}>
232
+ <div className={`${styles.notesEditorForm} ${styles.editorLayout}`}>
231
233
  {isLoading ? (
232
234
  <div className={styles.loading}>Loading notes...</div>
233
235
  ) : loadError ? (
@@ -240,12 +242,18 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
240
242
  </div>
241
243
  )}
242
244
 
243
- {saveError && (
244
- <div className={styles.errorMessage}>{saveError}</div>
245
- )}
246
-
247
245
  <div className={styles.section}>
248
- <h5 className={styles.sectionTitle}>Case Information</h5>
246
+ <button
247
+ type="button"
248
+ className={styles.sectionToggle}
249
+ onClick={() => setIsCaseInfoOpen((prev) => !prev)}
250
+ aria-expanded={isCaseInfoOpen}
251
+ >
252
+ <span className={styles.sectionTitle}>Case Information</span>
253
+ <span className={styles.sectionToggleIcon}>{isCaseInfoOpen ? '−' : '+'}</span>
254
+ </button>
255
+ {isCaseInfoOpen && (
256
+ <>
249
257
  <hr />
250
258
  <div className={styles.caseNumbers}>
251
259
  {/* Left side inputs */}
@@ -279,9 +287,8 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
279
287
  onChange={(e) => setLeftItem(e.target.value)}
280
288
  disabled={areInputsDisabled}
281
289
  />
282
- </div>
290
+ </div>
283
291
  </div>
284
- <hr />
285
292
  {/* Right side inputs */}
286
293
  <div className={styles.inputGroup}>
287
294
  <div className={styles.caseInput}>
@@ -316,63 +323,99 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
316
323
  </div>
317
324
  </div>
318
325
  </div>
319
- <label htmlFor="colorSelect">Font</label>
320
- <ColorSelector
326
+ <hr />
327
+ <div className={styles.fontColorRow}>
328
+ <label htmlFor="colorSelect">Font</label>
329
+ <ColorSelector
321
330
  selectedColor={caseFontColor}
322
331
  onColorSelect={setCaseFontColor}
323
- />
332
+ />
333
+ </div>
334
+ </>
335
+ )}
324
336
  </div>
325
337
 
326
- <div className={styles.section}>
327
- <h5 className={styles.sectionTitle}>Class Characteristics</h5>
328
- <div className={styles.classCharacteristics}>
329
- <select
330
- id="classType"
331
- aria-label="Class Type"
332
- value={classType}
333
- onChange={(e) => setClassType(e.target.value as ClassType)}
334
- className={styles.select}
335
- disabled={areInputsDisabled}
336
- >
337
- <option value="">Select class type...</option>
338
- <option value="Bullet">Bullet</option>
339
- <option value="Cartridge Case">Cartridge Case</option>
340
- <option value="Other">Other</option>
341
- </select>
338
+ <div className={styles.compactSectionGrid}>
339
+ <div className={`${styles.section} ${styles.compactFullSection}`}>
340
+ <button
341
+ type="button"
342
+ className={styles.sectionToggle}
343
+ onClick={() => setIsClassOpen((prev) => !prev)}
344
+ aria-expanded={isClassOpen}
345
+ >
346
+ <span className={styles.sectionTitle}>Class Characteristics</span>
347
+ <span className={styles.sectionToggleIcon}>{isClassOpen ? '−' : '+'}</span>
348
+ </button>
349
+ {isClassOpen && (
350
+ <>
351
+ <div className={styles.classCharacteristicsColumns}>
352
+ <div className={styles.classCharacteristicsMain}>
353
+ <div className={styles.classCharacteristics}>
354
+ <select
355
+ id="classType"
356
+ aria-label="Class Type"
357
+ value={classType}
358
+ onChange={(e) => setClassType(e.target.value as ClassType)}
359
+ className={styles.select}
360
+ disabled={areInputsDisabled}
361
+ >
362
+ <option value="">Select class type...</option>
363
+ <option value="Bullet">Bullet</option>
364
+ <option value="Cartridge Case">Cartridge Case</option>
365
+ <option value="Other">Other</option>
366
+ </select>
342
367
 
343
- {classType === 'Other' && (
344
- <input
345
- type="text"
346
- value={customClass}
347
- onChange={(e) => setCustomClass(e.target.value)}
348
- placeholder="Specify object type"
349
- disabled={areInputsDisabled}
350
- />
351
- )}
368
+ {classType === 'Other' && (
369
+ <input
370
+ type="text"
371
+ value={customClass}
372
+ onChange={(e) => setCustomClass(e.target.value)}
373
+ placeholder="Specify object type"
374
+ disabled={areInputsDisabled}
375
+ />
376
+ )}
352
377
 
353
- <textarea
354
- value={classNote}
355
- onChange={(e) => setClassNote(e.target.value)}
356
- placeholder="Enter class characteristic details..."
357
- className={styles.textarea}
358
- disabled={areInputsDisabled}
359
- />
360
- </div>
361
- <label className={`${styles.checkboxLabel} mb-4`}>
362
- <input
363
- type="checkbox"
364
- checked={hasSubclass}
365
- onChange={(e) => setHasSubclass(e.target.checked)}
366
- className={styles.checkbox}
367
- disabled={areInputsDisabled}
368
- />
369
- <span>Potential subclass?</span>
370
- </label>
378
+ <textarea
379
+ value={classNote}
380
+ onChange={(e) => setClassNote(e.target.value)}
381
+ placeholder="Enter class characteristic details..."
382
+ className={styles.textarea}
383
+ disabled={areInputsDisabled}
384
+ />
385
+ </div>
386
+ <label className={`${styles.checkboxLabel} mb-4`}>
387
+ <input
388
+ type="checkbox"
389
+ checked={hasSubclass}
390
+ onChange={(e) => setHasSubclass(e.target.checked)}
391
+ className={styles.checkbox}
392
+ disabled={areInputsDisabled}
393
+ />
394
+ <span>Potential subclass?</span>
395
+ </label>
396
+ </div>
397
+
398
+ <div className={styles.characteristicsPlaceholder}>
399
+ <h6 className={styles.placeholderTitle}>Characteristics Details</h6>
400
+ <p className={styles.placeholderText}>This section is reserved for future development.</p>
401
+ </div>
402
+ </div>
403
+ </>
404
+ )}
371
405
  </div>
372
406
 
373
- <div className={styles.section}>
374
- <h5 className={styles.sectionTitle}>Index Type</h5>
375
- <div className={styles.indexing}>
407
+ <div className={`${styles.section} ${styles.compactHalfSection}`}>
408
+ <button
409
+ type="button"
410
+ className={styles.sectionToggle}
411
+ onClick={() => setIsIndexOpen((prev) => !prev)}
412
+ aria-expanded={isIndexOpen}
413
+ >
414
+ <span className={styles.sectionTitle}>Index Type</span>
415
+ <span className={styles.sectionToggleIcon}>{isIndexOpen ? '−' : '+'}</span>
416
+ </button>
417
+ {isIndexOpen && (
418
+ <div className={styles.indexing}>
376
419
  <div className={styles.radioGroup}>
377
420
  <label className={styles.radioLabel}>
378
421
  <input
@@ -409,80 +452,86 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
409
452
  />
410
453
  ) : null}
411
454
  </div>
455
+ )}
412
456
  </div>
413
457
 
414
- <div className={styles.section}>
415
- <h5 className={styles.sectionTitle}>Support Level</h5>
416
- <div className={styles.support}>
417
- <select
418
- id="supportLevel"
419
- aria-label="Support Level"
420
- value={supportLevel}
421
- onChange={(e) => {
422
- const newSupportLevel = e.target.value as SupportLevel;
423
- setSupportLevel(newSupportLevel);
424
-
425
- // Automatically check confirmation field when ID is selected
426
- if (newSupportLevel === 'ID') {
427
- setIncludeConfirmation(true);
428
- }
429
- }}
430
- className={styles.select}
431
- disabled={areInputsDisabled}
432
- >
433
- <option value="">Select support level...</option>
434
- <option value="ID">Identification</option>
435
- <option value="Exclusion">Exclusion</option>
436
- <option value="Inconclusive">Inconclusive</option>
437
- </select>
438
- <label className={`${styles.checkboxLabel} mb-4`}>
439
- <input
440
- type="checkbox"
441
- checked={includeConfirmation}
442
- onChange={(e) => setIncludeConfirmation(e.target.checked)}
443
- className={styles.checkbox}
444
- disabled={areInputsDisabled}
445
- />
446
- <span>Include confirmation field</span>
447
- </label>
448
- </div>
449
- <button
450
- onClick={() => setIsModalOpen(true)}
451
- className={styles.notesButton}
452
- disabled={areInputsDisabled}
453
- title={isConfirmedImage ? "Cannot edit notes for confirmed images" : isUploading ? "Cannot add notes while uploading" : undefined}
454
- >
455
- Additional Notes
456
- </button>
458
+ <div className={`${styles.section} ${styles.compactHalfSection}`}>
459
+ <button
460
+ type="button"
461
+ className={styles.sectionToggle}
462
+ onClick={() => setIsSupportOpen((prev) => !prev)}
463
+ aria-expanded={isSupportOpen}
464
+ >
465
+ <span className={styles.sectionTitle}>Support Level</span>
466
+ <span className={styles.sectionToggleIcon}>{isSupportOpen ? '−' : '+'}</span>
467
+ </button>
468
+ {isSupportOpen && (
469
+ <>
470
+ <div className={styles.support}>
471
+ <select
472
+ id="supportLevel"
473
+ aria-label="Support Level"
474
+ value={supportLevel}
475
+ onChange={(e) => {
476
+ const newSupportLevel = e.target.value as SupportLevel;
477
+ setSupportLevel(newSupportLevel);
478
+
479
+ // Automatically check confirmation field when ID is selected
480
+ if (newSupportLevel === 'ID') {
481
+ setIncludeConfirmation(true);
482
+ }
483
+ }}
484
+ className={styles.select}
485
+ disabled={areInputsDisabled}
486
+ >
487
+ <option value="">Select support level...</option>
488
+ <option value="ID">Identification</option>
489
+ <option value="Exclusion">Exclusion</option>
490
+ <option value="Inconclusive">Inconclusive</option>
491
+ </select>
492
+ <label className={`${styles.checkboxLabel} mb-4`}>
493
+ <input
494
+ type="checkbox"
495
+ checked={includeConfirmation}
496
+ onChange={(e) => setIncludeConfirmation(e.target.checked)}
497
+ className={styles.checkbox}
498
+ disabled={areInputsDisabled}
499
+ />
500
+ <span>Include confirmation field</span>
501
+ </label>
502
+ </div>
503
+ </>
504
+ )}
457
505
  </div>
458
- <button
459
- onClick={handleSave}
460
- className={styles.saveButton}
506
+ </div>
507
+
508
+ <div className={styles.additionalNotesRow}>
509
+ <button
510
+ onClick={() => setIsModalOpen(true)}
511
+ className={styles.notesButton}
461
512
  disabled={areInputsDisabled}
462
- title={isConfirmedImage ? "Cannot save notes for confirmed images" : isUploading ? "Cannot save notes while uploading" : undefined}
513
+ title={isConfirmedImage ? "Cannot edit notes for confirmed images" : isUploading ? "Cannot add notes while uploading" : undefined}
463
514
  >
464
- Save Notes
515
+ Additional Notes
465
516
  </button>
466
-
467
- {saveSuccess && (
468
- <div className={styles.successMessage}>
469
- Notes saved successfully!
470
- </div>
471
- )}
517
+ </div>
472
518
 
473
- <button
474
- onClick={onReturn}
475
- className={styles.returnButton}
476
- disabled={isUploading}
477
- title={isUploading ? "Cannot return while uploading" : undefined}
478
- >
479
- Return to Case Management
480
- </button>
481
- <NotesModal
519
+ <div className={`${styles.notesActionBar} ${styles.notesActionBarSticky}`}>
520
+ <button
521
+ onClick={handleSave}
522
+ className={styles.saveButton}
523
+ disabled={areInputsDisabled}
524
+ title={isConfirmedImage ? "Cannot save notes for confirmed images" : isUploading ? "Cannot save notes while uploading" : undefined}
525
+ >
526
+ Save Notes
527
+ </button>
528
+ </div>
529
+ <AddlNotesModal
482
530
  isOpen={isModalOpen}
483
531
  onClose={() => setIsModalOpen(false)}
484
532
  notes={additionalNotes}
485
533
  onSave={setAdditionalNotes}
534
+ showNotification={notificationHandler}
486
535
  />
487
536
  </>
488
537
  )}
@@ -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,64 @@
1
+ import type { User } from 'firebase/auth';
2
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
+ import { NotesEditorForm } from './notes-editor-form';
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
+ showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
16
+ }
17
+
18
+ export const NotesEditorModal = ({
19
+ isOpen,
20
+ onClose,
21
+ currentCase,
22
+ user,
23
+ imageId,
24
+ originalFileName,
25
+ onAnnotationRefresh,
26
+ isUploading = false,
27
+ showNotification,
28
+ }: NotesEditorModalProps) => {
29
+ const {
30
+ overlayProps,
31
+ getCloseButtonProps,
32
+ } = useOverlayDismiss({
33
+ isOpen,
34
+ onClose,
35
+ });
36
+
37
+ if (!isOpen) {
38
+ return null;
39
+ }
40
+
41
+ return (
42
+ <div className={styles.overlay} aria-label="Close image notes dialog" {...overlayProps}>
43
+ <div className={styles.modal} role="dialog" aria-modal="true" aria-label="Image Notes">
44
+ <div className={styles.header}>
45
+ <h2 className={styles.title}>Image Notes</h2>
46
+ <button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close image notes dialog' })}>
47
+ ×
48
+ </button>
49
+ </div>
50
+ <div className={styles.content}>
51
+ <NotesEditorForm
52
+ currentCase={currentCase}
53
+ user={user}
54
+ imageId={imageId}
55
+ onAnnotationRefresh={onAnnotationRefresh}
56
+ originalFileName={originalFileName}
57
+ isUploading={isUploading}
58
+ showNotification={showNotification}
59
+ />
60
+ </div>
61
+ </div>
62
+ </div>
63
+ );
64
+ };