@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
@@ -16,13 +16,16 @@ interface NotesSidebarProps {
16
16
  onAnnotationRefresh?: () => void;
17
17
  originalFileName?: string;
18
18
  isUploading?: boolean;
19
+ showReturnButton?: boolean;
20
+ stickyActionBar?: boolean;
21
+ compactLayout?: boolean;
19
22
  }
20
23
 
21
24
  type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
22
25
  type ClassType = 'Bullet' | 'Cartridge Case' | 'Other';
23
26
  type IndexType = 'number' | 'color';
24
27
 
25
- export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false }: NotesSidebarProps) => {
28
+ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, showReturnButton = true, stickyActionBar = false, compactLayout = false }: NotesSidebarProps) => {
26
29
  // Loading/Saving Notes States
27
30
  const [isLoading, setIsLoading] = useState(false);
28
31
  const [loadError, setLoadError] = useState<string>();
@@ -56,6 +59,10 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
56
59
  // Additional Notes Modal
57
60
  const [isModalOpen, setIsModalOpen] = useState(false);
58
61
  const [additionalNotes, setAdditionalNotes] = useState('');
62
+ const [isCaseInfoOpen, setIsCaseInfoOpen] = useState(true);
63
+ const [isClassOpen, setIsClassOpen] = useState(true);
64
+ const [isIndexOpen, setIsIndexOpen] = useState(true);
65
+ const [isSupportOpen, setIsSupportOpen] = useState(true);
59
66
  const areInputsDisabled = isUploading || isConfirmedImage;
60
67
 
61
68
  useEffect(() => {
@@ -227,7 +234,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
227
234
  };
228
235
 
229
236
  return (
230
- <div className={styles.notesSidebar}>
237
+ <div className={`${styles.notesSidebar} ${compactLayout ? styles.compactLayout : ''}`}>
231
238
  {isLoading ? (
232
239
  <div className={styles.loading}>Loading notes...</div>
233
240
  ) : loadError ? (
@@ -245,7 +252,17 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
245
252
  )}
246
253
 
247
254
  <div className={styles.section}>
248
- <h5 className={styles.sectionTitle}>Case Information</h5>
255
+ <button
256
+ type="button"
257
+ className={styles.sectionToggle}
258
+ onClick={() => setIsCaseInfoOpen((prev) => !prev)}
259
+ aria-expanded={isCaseInfoOpen}
260
+ >
261
+ <span className={styles.sectionTitle}>Case Information</span>
262
+ <span className={styles.sectionToggleIcon}>{isCaseInfoOpen ? '−' : '+'}</span>
263
+ </button>
264
+ {isCaseInfoOpen && (
265
+ <>
249
266
  <hr />
250
267
  <div className={styles.caseNumbers}>
251
268
  {/* Left side inputs */}
@@ -279,9 +296,18 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
279
296
  onChange={(e) => setLeftItem(e.target.value)}
280
297
  disabled={areInputsDisabled}
281
298
  />
282
- </div>
299
+ </div>
300
+ {compactLayout && (
301
+ <div className={styles.caseInput}>
302
+ <label htmlFor="colorSelect">Font</label>
303
+ <ColorSelector
304
+ selectedColor={caseFontColor}
305
+ onColorSelect={setCaseFontColor}
306
+ />
307
+ </div>
308
+ )}
283
309
  </div>
284
- <hr />
310
+ {!compactLayout && <hr />}
285
311
  {/* Right side inputs */}
286
312
  <div className={styles.inputGroup}>
287
313
  <div className={styles.caseInput}>
@@ -316,63 +342,102 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
316
342
  </div>
317
343
  </div>
318
344
  </div>
319
- <label htmlFor="colorSelect">Font</label>
320
- <ColorSelector
321
- selectedColor={caseFontColor}
322
- onColorSelect={setCaseFontColor}
323
- />
345
+ {!compactLayout && (
346
+ <>
347
+ <label htmlFor="colorSelect">Font</label>
348
+ <ColorSelector
349
+ selectedColor={caseFontColor}
350
+ onColorSelect={setCaseFontColor}
351
+ />
352
+ </>
353
+ )}
354
+ </>
355
+ )}
324
356
  </div>
325
357
 
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>
358
+ <div className={compactLayout ? styles.compactSectionGrid : undefined}>
359
+ <div className={`${styles.section} ${compactLayout ? styles.compactFullSection : ''}`}>
360
+ <button
361
+ type="button"
362
+ className={styles.sectionToggle}
363
+ onClick={() => setIsClassOpen((prev) => !prev)}
364
+ aria-expanded={isClassOpen}
365
+ >
366
+ <span className={styles.sectionTitle}>Class Characteristics</span>
367
+ <span className={styles.sectionToggleIcon}>{isClassOpen ? '−' : '+'}</span>
368
+ </button>
369
+ {isClassOpen && (
370
+ <>
371
+ <div className={compactLayout ? styles.classCharacteristicsColumns : undefined}>
372
+ <div className={styles.classCharacteristicsMain}>
373
+ <div className={styles.classCharacteristics}>
374
+ <select
375
+ id="classType"
376
+ aria-label="Class Type"
377
+ value={classType}
378
+ onChange={(e) => setClassType(e.target.value as ClassType)}
379
+ className={styles.select}
380
+ disabled={areInputsDisabled}
381
+ >
382
+ <option value="">Select class type...</option>
383
+ <option value="Bullet">Bullet</option>
384
+ <option value="Cartridge Case">Cartridge Case</option>
385
+ <option value="Other">Other</option>
386
+ </select>
342
387
 
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
- )}
388
+ {classType === 'Other' && (
389
+ <input
390
+ type="text"
391
+ value={customClass}
392
+ onChange={(e) => setCustomClass(e.target.value)}
393
+ placeholder="Specify object type"
394
+ disabled={areInputsDisabled}
395
+ />
396
+ )}
352
397
 
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>
398
+ <textarea
399
+ value={classNote}
400
+ onChange={(e) => setClassNote(e.target.value)}
401
+ placeholder="Enter class characteristic details..."
402
+ className={styles.textarea}
403
+ disabled={areInputsDisabled}
404
+ />
405
+ </div>
406
+ <label className={`${styles.checkboxLabel} mb-4`}>
407
+ <input
408
+ type="checkbox"
409
+ checked={hasSubclass}
410
+ onChange={(e) => setHasSubclass(e.target.checked)}
411
+ className={styles.checkbox}
412
+ disabled={areInputsDisabled}
413
+ />
414
+ <span>Potential subclass?</span>
415
+ </label>
416
+ </div>
417
+
418
+ {compactLayout && (
419
+ <div className={styles.characteristicsPlaceholder}>
420
+ <h6 className={styles.placeholderTitle}>Characteristics Details</h6>
421
+ <p className={styles.placeholderText}>This section is reserved for future development.</p>
422
+ </div>
423
+ )}
424
+ </div>
425
+ </>
426
+ )}
371
427
  </div>
372
428
 
373
- <div className={styles.section}>
374
- <h5 className={styles.sectionTitle}>Index Type</h5>
375
- <div className={styles.indexing}>
429
+ <div className={`${styles.section} ${compactLayout ? styles.compactHalfSection : ''}`}>
430
+ <button
431
+ type="button"
432
+ className={styles.sectionToggle}
433
+ onClick={() => setIsIndexOpen((prev) => !prev)}
434
+ aria-expanded={isIndexOpen}
435
+ >
436
+ <span className={styles.sectionTitle}>Index Type</span>
437
+ <span className={styles.sectionToggleIcon}>{isIndexOpen ? '−' : '+'}</span>
438
+ </button>
439
+ {isIndexOpen && (
440
+ <div className={styles.indexing}>
376
441
  <div className={styles.radioGroup}>
377
442
  <label className={styles.radioLabel}>
378
443
  <input
@@ -409,75 +474,96 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
409
474
  />
410
475
  ) : null}
411
476
  </div>
477
+ )}
412
478
  </div>
413
479
 
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>
480
+ <div className={`${styles.section} ${compactLayout ? styles.compactHalfSection : ''}`}>
481
+ <button
482
+ type="button"
483
+ className={styles.sectionToggle}
484
+ onClick={() => setIsSupportOpen((prev) => !prev)}
485
+ aria-expanded={isSupportOpen}
486
+ >
487
+ <span className={styles.sectionTitle}>Support Level</span>
488
+ <span className={styles.sectionToggleIcon}>{isSupportOpen ? '−' : '+'}</span>
489
+ </button>
490
+ {isSupportOpen && (
491
+ <>
492
+ <div className={styles.support}>
493
+ <select
494
+ id="supportLevel"
495
+ aria-label="Support Level"
496
+ value={supportLevel}
497
+ onChange={(e) => {
498
+ const newSupportLevel = e.target.value as SupportLevel;
499
+ setSupportLevel(newSupportLevel);
500
+
501
+ // Automatically check confirmation field when ID is selected
502
+ if (newSupportLevel === 'ID') {
503
+ setIncludeConfirmation(true);
504
+ }
505
+ }}
506
+ className={styles.select}
507
+ disabled={areInputsDisabled}
508
+ >
509
+ <option value="">Select support level...</option>
510
+ <option value="ID">Identification</option>
511
+ <option value="Exclusion">Exclusion</option>
512
+ <option value="Inconclusive">Inconclusive</option>
513
+ </select>
514
+ <label className={`${styles.checkboxLabel} mb-4`}>
515
+ <input
516
+ type="checkbox"
517
+ checked={includeConfirmation}
518
+ onChange={(e) => setIncludeConfirmation(e.target.checked)}
519
+ className={styles.checkbox}
520
+ disabled={areInputsDisabled}
521
+ />
522
+ <span>Include confirmation field</span>
523
+ </label>
524
+ </div>
525
+ </>
526
+ )}
457
527
  </div>
458
- <button
459
- onClick={handleSave}
460
- className={styles.saveButton}
528
+ </div>
529
+
530
+ <div className={styles.additionalNotesRow}>
531
+ <button
532
+ onClick={() => setIsModalOpen(true)}
533
+ className={styles.notesButton}
461
534
  disabled={areInputsDisabled}
462
- title={isConfirmedImage ? "Cannot save notes for confirmed images" : isUploading ? "Cannot save notes while uploading" : undefined}
535
+ title={isConfirmedImage ? "Cannot edit notes for confirmed images" : isUploading ? "Cannot add notes while uploading" : undefined}
463
536
  >
464
- Save Notes
537
+ Additional Notes
465
538
  </button>
539
+ </div>
540
+
541
+ <div className={`${styles.notesActionBar} ${stickyActionBar ? styles.notesActionBarSticky : ''}`}>
542
+ <button
543
+ onClick={handleSave}
544
+ className={styles.saveButton}
545
+ disabled={areInputsDisabled}
546
+ title={isConfirmedImage ? "Cannot save notes for confirmed images" : isUploading ? "Cannot save notes while uploading" : undefined}
547
+ >
548
+ Save Notes
549
+ </button>
550
+ {showReturnButton && (
551
+ <button
552
+ onClick={onReturn}
553
+ className={styles.returnButton}
554
+ disabled={isUploading}
555
+ title={isUploading ? "Cannot return while uploading" : undefined}
556
+ >
557
+ Return to Case Management
558
+ </button>
559
+ )}
560
+ </div>
466
561
 
467
562
  {saveSuccess && (
468
563
  <div className={styles.successMessage}>
469
564
  Notes saved successfully!
470
565
  </div>
471
566
  )}
472
-
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
567
  <NotesModal
482
568
  isOpen={isModalOpen}
483
569
  onClose={() => setIsModalOpen(false)}
@@ -2,6 +2,84 @@
2
2
  padding: 0.3rem;
3
3
  }
4
4
 
5
+ .compactLayout .caseNumbers {
6
+ display: grid;
7
+ grid-template-columns: repeat(2, minmax(0, 1fr));
8
+ gap: 1.25rem;
9
+ align-items: start;
10
+ }
11
+
12
+ .compactLayout .caseNumbers > .inputGroup + .inputGroup {
13
+ border-left: 1px solid #dee2e6;
14
+ padding-left: 1.25rem;
15
+ }
16
+
17
+ .compactLayout .inputGroup {
18
+ margin-bottom: 0;
19
+ }
20
+
21
+ .compactSectionGrid {
22
+ display: grid;
23
+ grid-template-columns: repeat(2, minmax(0, 1fr));
24
+ gap: 1.25rem;
25
+ align-items: start;
26
+ border-top: 1px solid #dee2e6;
27
+ padding-top: 1.25rem;
28
+ }
29
+
30
+ .compactFullSection {
31
+ grid-column: 1 / -1;
32
+ }
33
+
34
+ .compactHalfSection {
35
+ margin-bottom: 1.5rem;
36
+ }
37
+
38
+ .compactLayout .notesActionBarSticky {
39
+ margin-top: 0.25rem;
40
+ }
41
+
42
+ .compactLayout .compactSectionGrid > .compactHalfSection + .compactHalfSection {
43
+ border-left: 1px solid #dee2e6;
44
+ padding-left: 1.25rem;
45
+ }
46
+
47
+ .classCharacteristicsColumns > .characteristicsPlaceholder {
48
+ border-left: 1px solid #dee2e6;
49
+ padding-left: 1.25rem;
50
+ }
51
+
52
+ .compactLayout .additionalNotesRow {
53
+ border-top: 1px solid #dee2e6;
54
+ padding-top: 1.25rem;
55
+ }
56
+
57
+ @media (max-width: 980px) {
58
+ .compactLayout .caseNumbers,
59
+ .compactSectionGrid {
60
+ grid-template-columns: 1fr;
61
+ }
62
+
63
+ .compactLayout .caseNumbers > .inputGroup + .inputGroup,
64
+ .compactLayout
65
+ .compactSectionGrid
66
+ > .compactHalfSection
67
+ + .compactHalfSection,
68
+ .classCharacteristicsColumns > .characteristicsPlaceholder {
69
+ border-left: none;
70
+ padding-left: 0;
71
+ }
72
+
73
+ .classCharacteristicsColumns {
74
+ grid-template-columns: 1fr;
75
+ }
76
+
77
+ .compactFullSection,
78
+ .compactHalfSection {
79
+ grid-column: auto;
80
+ }
81
+ }
82
+
5
83
  hr {
6
84
  padding-bottom: 0.5rem;
7
85
  margin: 0.5rem 0;
@@ -13,6 +91,26 @@ hr {
13
91
  margin-bottom: 2rem;
14
92
  }
15
93
 
94
+ .sectionToggle {
95
+ width: 100%;
96
+ border: none;
97
+ background: none;
98
+ padding: 0;
99
+ margin: 0;
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: space-between;
103
+ cursor: pointer;
104
+ text-align: left;
105
+ }
106
+
107
+ .sectionToggleIcon {
108
+ color: #6c757d;
109
+ font-size: 1.2rem;
110
+ line-height: 1;
111
+ font-weight: 500;
112
+ }
113
+
16
114
  .sectionTitle {
17
115
  font-size: 1.3rem;
18
116
  font-weight: 600;
@@ -103,6 +201,39 @@ textarea:focus {
103
201
  margin-bottom: 2rem;
104
202
  }
105
203
 
204
+ .classCharacteristicsColumns {
205
+ display: grid;
206
+ grid-template-columns: repeat(2, minmax(0, 1fr));
207
+ gap: 1.25rem;
208
+ align-items: start;
209
+ }
210
+
211
+ .classCharacteristicsMain {
212
+ min-width: 0;
213
+ }
214
+
215
+ .characteristicsPlaceholder {
216
+ border: 1px dashed #ced4da;
217
+ border-radius: 8px;
218
+ padding: 0.85rem;
219
+ background: #f8f9fa;
220
+ min-height: 150px;
221
+ }
222
+
223
+ .placeholderTitle {
224
+ margin: 0 0 0.5rem;
225
+ color: #495057;
226
+ font-size: 0.92rem;
227
+ font-weight: 600;
228
+ }
229
+
230
+ .placeholderText {
231
+ margin: 0;
232
+ color: #6c757d;
233
+ font-size: 0.85rem;
234
+ line-height: 1.4;
235
+ }
236
+
106
237
  .classCharacteristics input {
107
238
  width: 100%;
108
239
  padding: 0.75rem;
@@ -247,6 +378,14 @@ textarea:focus {
247
378
  margin-top: 1rem;
248
379
  }
249
380
 
381
+ .additionalNotesRow {
382
+ width: 100%;
383
+ }
384
+
385
+ .compactLayout .additionalNotesRow {
386
+ grid-column: 1 / -1;
387
+ }
388
+
250
389
  .notesButton:hover {
251
390
  background-color: color-mix(in lab, var(--primary) 95%, transparent);
252
391
  }
@@ -266,6 +405,7 @@ textarea:focus {
266
405
  }
267
406
 
268
407
  .modal {
408
+ position: relative;
269
409
  background: white;
270
410
  padding: 2rem;
271
411
  border-radius: 8px;
@@ -332,6 +472,19 @@ textarea:focus {
332
472
  box-sizing: border-box;
333
473
  }
334
474
 
475
+ .notesActionBar {
476
+ display: flex;
477
+ flex-direction: column;
478
+ gap: 0.75rem;
479
+ }
480
+
481
+ .notesActionBarSticky {
482
+ position: sticky;
483
+ bottom: 0;
484
+ padding: 0.75rem 0 0.25rem;
485
+ background: linear-gradient(to top, white 72%, rgba(255, 255, 255, 0));
486
+ }
487
+
335
488
  .saveButton:hover {
336
489
  background-color: color-mix(in lab, var(--accent) 95%, transparent);
337
490
  }
@@ -1,37 +1,32 @@
1
1
  /* eslint-disable jsx-a11y/no-static-element-interactions */
2
2
  /* eslint-disable jsx-a11y/click-events-have-key-events */
3
3
  import type React from 'react';
4
- import { useState, useEffect } from 'react';
4
+ import { useState } from 'react';
5
5
  import { Link } from 'react-router';
6
6
  import { Sidebar } from './sidebar';
7
7
  import type { User } from 'firebase/auth';
8
8
  import { type FileData } from '~/types';
9
9
  import styles from './sidebar.module.css';
10
10
  import { getAppVersion } from '~/utils/common';
11
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
11
12
 
12
13
  interface SidebarContainerProps {
13
14
  user: User;
14
15
  onImageSelect: (file: FileData) => void;
15
16
  imageId?: string;
16
- onCaseChange: (caseNumber: string) => void;
17
17
  currentCase: string;
18
- setCurrentCase: (caseNumber: string) => void;
19
18
  files: FileData[];
20
19
  setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
21
20
  imageLoaded: boolean;
22
21
  setImageLoaded: (loaded: boolean) => void;
23
- caseNumber: string;
24
- setCaseNumber: (caseNumber: string) => void;
25
- error: string;
26
- setError: (error: string) => void;
27
- successAction: 'loaded' | 'created' | 'deleted' | null;
28
- setSuccessAction: (action: 'loaded' | 'created' | 'deleted' | null) => void;
29
22
  showNotes: boolean;
30
23
  setShowNotes: (show: boolean) => void;
31
24
  onAnnotationRefresh?: () => void;
32
25
  isReadOnly?: boolean;
33
26
  isConfirmed?: boolean;
34
27
  confirmationSaveVersion?: number;
28
+ isUploading?: boolean;
29
+ onUploadStatusChange?: (isUploading: boolean) => void;
35
30
  }
36
31
 
37
32
  export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
@@ -39,24 +34,16 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
39
34
  const year = new Date().getFullYear();
40
35
  const appVersion = getAppVersion();
41
36
 
42
- useEffect(() => {
43
- const handleEscape = (e: KeyboardEvent) => {
44
- if (e.key === 'Escape' && isFooterModalOpen) {
45
- setIsFooterModalOpen(false);
46
- }
47
- };
48
-
49
- if (isFooterModalOpen) {
50
- document.addEventListener('keydown', handleEscape);
51
- }
52
-
53
- return () => {
54
- document.removeEventListener('keydown', handleEscape);
55
- };
56
- }, [isFooterModalOpen]);
37
+ const {
38
+ overlayProps,
39
+ getCloseButtonProps,
40
+ } = useOverlayDismiss({
41
+ isOpen: isFooterModalOpen,
42
+ onClose: () => setIsFooterModalOpen(false),
43
+ });
57
44
 
58
45
  return (
59
- <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
46
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
60
47
  {/* Main Sidebar */}
61
48
  <Sidebar {...props} />
62
49
 
@@ -72,13 +59,13 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
72
59
 
73
60
  {/* Footer Modal */}
74
61
  {isFooterModalOpen && (
75
- <div className={styles.footerModalOverlay} onClick={() => setIsFooterModalOpen(false)}>
62
+ <div className={styles.footerModalOverlay} {...overlayProps} aria-label="Close About and Support dialog">
76
63
  <div className={styles.footerModal} onClick={(e) => e.stopPropagation()}>
77
64
  <div className={styles.footerModalHeader}>
78
65
  <h2 className={styles.footerModalTitle}>About Striae</h2>
79
- <button
80
- onClick={() => setIsFooterModalOpen(false)}
66
+ <button
81
67
  className={styles.footerModalClose}
68
+ {...getCloseButtonProps({ ariaLabel: 'Close About and Support dialog' })}
82
69
  >
83
70
  ×
84
71
  </button>