@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
@@ -1,4 +1,5 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState } from 'react';
2
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
2
3
  import styles from './notes.module.css';
3
4
 
4
5
  interface NotesModalProps {
@@ -10,30 +11,30 @@ interface NotesModalProps {
10
11
 
11
12
  export const NotesModal = ({ isOpen, onClose, notes, onSave }: NotesModalProps) => {
12
13
  const [tempNotes, setTempNotes] = useState(notes);
13
-
14
- useEffect(() => {
15
- const handleEscape = (e: KeyboardEvent) => {
16
- if (e.key === 'Escape') {
17
- onClose();
18
- }
19
- };
20
-
21
- if (isOpen) {
22
- document.addEventListener('keydown', handleEscape);
23
- return () => document.removeEventListener('keydown', handleEscape);
24
- }
25
- }, [isOpen, onClose]);
14
+ const {
15
+ requestClose,
16
+ overlayProps,
17
+ getCloseButtonProps
18
+ } = useOverlayDismiss({
19
+ isOpen,
20
+ onClose
21
+ });
26
22
 
27
23
  if (!isOpen) return null;
28
24
 
29
25
  const handleSave = () => {
30
26
  onSave(tempNotes);
31
- onClose();
27
+ requestClose();
32
28
  };
33
29
 
34
30
  return (
35
- <div className={styles.modalOverlay}>
31
+ <div
32
+ className={styles.modalOverlay}
33
+ aria-label="Close notes dialog"
34
+ {...overlayProps}
35
+ >
36
36
  <div className={styles.modal}>
37
+ <button {...getCloseButtonProps({ ariaLabel: 'Close notes dialog' })}>×</button>
37
38
  <h5 className={styles.modalTitle}>Additional Notes</h5>
38
39
  <textarea
39
40
  value={tempNotes}
@@ -43,7 +44,7 @@ export const NotesModal = ({ isOpen, onClose, notes, onSave }: NotesModalProps)
43
44
  />
44
45
  <div className={styles.modalButtons}>
45
46
  <button onClick={handleSave} className={styles.saveButton}>Save</button>
46
- <button onClick={onClose} className={styles.cancelButton}>Cancel</button>
47
+ <button onClick={requestClose} className={styles.cancelButton}>Cancel</button>
47
48
  </div>
48
49
  </div>
49
50
  </div>
@@ -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
  }
@@ -262,15 +401,18 @@ textarea:focus {
262
401
  justify-content: center;
263
402
  align-items: center;
264
403
  z-index: 1000;
404
+ cursor: default;
265
405
  }
266
406
 
267
407
  .modal {
408
+ position: relative;
268
409
  background: white;
269
410
  padding: 2rem;
270
411
  border-radius: 8px;
271
412
  width: 90%;
272
413
  max-width: 500px;
273
414
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
415
+ cursor: default;
274
416
  }
275
417
 
276
418
  .modalTitle {
@@ -330,6 +472,19 @@ textarea:focus {
330
472
  box-sizing: border-box;
331
473
  }
332
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
+
333
488
  .saveButton:hover {
334
489
  background-color: color-mix(in lab, var(--accent) 95%, transparent);
335
490
  }