@striae-org/striae 4.2.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 (65) hide show
  1. package/LICENSE +1 -1
  2. package/app/components/actions/case-manage.ts +50 -17
  3. package/app/components/audit/viewer/audit-entries-list.tsx +5 -2
  4. package/app/components/audit/viewer/use-audit-viewer-data.ts +6 -3
  5. package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
  6. package/app/components/canvas/confirmation/confirmation.tsx +6 -2
  7. package/app/components/colors/colors.module.css +4 -3
  8. package/app/components/navbar/navbar.tsx +34 -9
  9. package/app/components/sidebar/cases/case-sidebar.tsx +44 -70
  10. package/app/components/sidebar/cases/cases-modal.tsx +76 -35
  11. package/app/components/sidebar/cases/cases.module.css +20 -0
  12. package/app/components/sidebar/files/files-modal.tsx +37 -39
  13. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  14. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +37 -74
  15. package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
  16. package/app/components/sidebar/notes/notes.module.css +27 -11
  17. package/app/components/sidebar/sidebar-container.tsx +1 -0
  18. package/app/components/sidebar/sidebar.tsx +3 -0
  19. package/app/{tailwind.css → global.css} +1 -3
  20. package/app/hooks/useOverlayDismiss.ts +6 -4
  21. package/app/root.tsx +1 -1
  22. package/app/routes/striae/striae.tsx +6 -0
  23. package/app/services/audit/audit.service.ts +2 -2
  24. package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
  25. package/app/types/audit.ts +1 -0
  26. package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
  27. package/app/utils/data/data-operations.ts +17 -861
  28. package/app/utils/data/index.ts +11 -1
  29. package/app/utils/data/operations/batch-operations.ts +113 -0
  30. package/app/utils/data/operations/case-operations.ts +168 -0
  31. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  32. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  33. package/app/utils/data/operations/index.ts +7 -0
  34. package/app/utils/data/operations/signing-operations.ts +225 -0
  35. package/app/utils/data/operations/types.ts +42 -0
  36. package/app/utils/data/operations/validation-operations.ts +48 -0
  37. package/app/utils/forensics/export-verification.ts +40 -111
  38. package/functions/api/_shared/firebase-auth.ts +2 -7
  39. package/functions/api/image/[[path]].ts +20 -23
  40. package/functions/api/pdf/[[path]].ts +27 -8
  41. package/package.json +5 -10
  42. package/scripts/deploy-primershear-emails.sh +1 -1
  43. package/worker-configuration.d.ts +2 -2
  44. package/workers/audit-worker/package.json +1 -1
  45. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  46. package/workers/data-worker/package.json +1 -1
  47. package/workers/data-worker/wrangler.jsonc.example +1 -1
  48. package/workers/image-worker/package.json +1 -1
  49. package/workers/image-worker/src/image-worker.example.ts +16 -5
  50. package/workers/image-worker/wrangler.jsonc.example +1 -1
  51. package/workers/keys-worker/package.json +1 -1
  52. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  53. package/workers/pdf-worker/package.json +1 -1
  54. package/workers/pdf-worker/src/formats/format-striae.ts +1 -7
  55. package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
  56. package/workers/pdf-worker/src/report-types.ts +3 -3
  57. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  58. package/workers/user-worker/package.json +1 -1
  59. package/workers/user-worker/src/user-worker.example.ts +17 -0
  60. package/workers/user-worker/wrangler.jsonc.example +1 -1
  61. package/wrangler.toml.example +1 -1
  62. package/NOTICE +0 -13
  63. package/app/components/sidebar/notes/notes-modal.tsx +0 -52
  64. package/postcss.config.js +0 -6
  65. package/tailwind.config.ts +0 -22
@@ -3,7 +3,7 @@ import { useState, useContext, useEffect } from 'react';
3
3
  import { AuthContext } from '~/contexts/auth.context';
4
4
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
5
5
  import { deleteFile } from '~/components/actions/image-manage';
6
- import { getFileAnnotations } from '~/utils/data';
6
+ import { ensureCaseConfirmationSummary } from '~/utils/data';
7
7
  import { type FileData } from '~/types';
8
8
  import styles from './files-modal.module.css';
9
9
 
@@ -16,6 +16,7 @@ interface FilesModalProps {
16
16
  setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
17
17
  isReadOnly?: boolean;
18
18
  selectedFileId?: string;
19
+ confirmationSaveVersion?: number;
19
20
  }
20
21
 
21
22
  const FILES_PER_PAGE = 10;
@@ -28,7 +29,17 @@ interface FileConfirmationStatus {
28
29
  };
29
30
  }
30
31
 
31
- export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files, setFiles, isReadOnly = false, selectedFileId }: FilesModalProps) => {
32
+ export const FilesModal = ({
33
+ isOpen,
34
+ onClose,
35
+ onFileSelect,
36
+ currentCase,
37
+ files,
38
+ setFiles,
39
+ isReadOnly = false,
40
+ selectedFileId,
41
+ confirmationSaveVersion = 0
42
+ }: FilesModalProps) => {
32
43
  const { user } = useContext(AuthContext);
33
44
  const [error, setError] = useState<string | null>(null);
34
45
  const [currentPage, setCurrentPage] = useState(0);
@@ -48,54 +59,36 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
48
59
  const endIndex = startIndex + FILES_PER_PAGE;
49
60
  const currentFiles = files.slice(startIndex, endIndex);
50
61
 
51
- // Fetch confirmation status only for currently visible paginated files
62
+ // Hydrate confirmation status from shared summary document.
52
63
  useEffect(() => {
53
- const fetchConfirmationStatuses = async () => {
54
- const visibleFiles = files.slice(
55
- currentPage * FILES_PER_PAGE,
56
- currentPage * FILES_PER_PAGE + FILES_PER_PAGE
57
- );
64
+ let isCancelled = false;
58
65
 
59
- if (!isOpen || !currentCase || !user || visibleFiles.length === 0) {
66
+ const fetchConfirmationStatuses = async () => {
67
+ if (!isOpen || !currentCase || !user || files.length === 0) {
68
+ if (!isCancelled) {
69
+ setFileConfirmationStatus({});
70
+ }
60
71
  return;
61
72
  }
62
73
 
63
- // Fetch annotations in parallel for only visible files
64
- const annotationPromises = visibleFiles.map(async (file) => {
65
- try {
66
- const annotations = await getFileAnnotations(user, currentCase, file.id);
67
- return {
68
- fileId: file.id,
69
- includeConfirmation: annotations?.includeConfirmation ?? false,
70
- isConfirmed: !!(annotations?.includeConfirmation && annotations?.confirmationData),
71
- };
72
- } catch (err) {
73
- console.error(`Error fetching annotations for file ${file.id}:`, err);
74
- return {
75
- fileId: file.id,
76
- includeConfirmation: false,
77
- isConfirmed: false,
78
- };
79
- }
74
+ const caseSummary = await ensureCaseConfirmationSummary(user, currentCase, files).catch((err) => {
75
+ console.error(`Error fetching confirmation summary for case ${currentCase}:`, err);
76
+ return null;
80
77
  });
81
78
 
82
- // Wait for all fetches to complete
83
- const results = await Promise.all(annotationPromises);
84
-
85
- // Build the statuses map from results
86
- const statuses: FileConfirmationStatus = {};
87
- results.forEach((result) => {
88
- statuses[result.fileId] = {
89
- includeConfirmation: result.includeConfirmation,
90
- isConfirmed: result.isConfirmed,
91
- };
92
- });
79
+ if (!caseSummary || isCancelled) {
80
+ return;
81
+ }
93
82
 
94
- setFileConfirmationStatus(statuses);
83
+ setFileConfirmationStatus(caseSummary.filesById);
95
84
  };
96
85
 
97
86
  fetchConfirmationStatuses();
98
- }, [isOpen, currentCase, currentPage, files, user]);
87
+
88
+ return () => {
89
+ isCancelled = true;
90
+ };
91
+ }, [isOpen, currentCase, files, user, confirmationSaveVersion]);
99
92
 
100
93
  const handleFileSelect = (file: FileData) => {
101
94
  onFileSelect?.(file);
@@ -121,6 +114,11 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
121
114
  // Remove the deleted file from the list
122
115
  const updatedFiles = files.filter(f => f.id !== fileId);
123
116
  setFiles(updatedFiles);
117
+ setFileConfirmationStatus((previous) => {
118
+ const next = { ...previous };
119
+ delete next[fileId];
120
+ return next;
121
+ });
124
122
 
125
123
  if (deleteResult.imageMissing) {
126
124
  setError(`File record deleted. Image asset "${deleteResult.fileName}" was not found and was skipped.`);
@@ -0,0 +1,82 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
+ import styles from './notes.module.css';
4
+
5
+ interface AddlNotesModalProps {
6
+ isOpen: boolean;
7
+ onClose: () => void;
8
+ notes: string;
9
+ onSave: (notes: string) => void;
10
+ showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
11
+ }
12
+
13
+ export const AddlNotesModal = ({ isOpen, onClose, notes, onSave, showNotification }: AddlNotesModalProps) => {
14
+ const [tempNotes, setTempNotes] = useState(notes);
15
+ const [isSaving, setIsSaving] = useState(false);
16
+
17
+ useEffect(() => {
18
+ if (isOpen) {
19
+ setTempNotes(notes);
20
+ }
21
+ }, [isOpen, notes]);
22
+ const {
23
+ requestClose,
24
+ overlayProps,
25
+ getCloseButtonProps
26
+ } = useOverlayDismiss({
27
+ isOpen,
28
+ onClose
29
+ });
30
+
31
+ if (!isOpen) return null;
32
+
33
+ const handleSave = async () => {
34
+ setIsSaving(true);
35
+ try {
36
+ await Promise.resolve(onSave(tempNotes));
37
+ showNotification?.('Notes saved successfully.', 'success');
38
+ requestClose();
39
+ } catch (error) {
40
+ const message = error instanceof Error ? error.message : 'Failed to save notes.';
41
+ showNotification?.(message, 'error');
42
+ } finally {
43
+ setIsSaving(false);
44
+ }
45
+ };
46
+
47
+ return (
48
+ <div
49
+ className={styles.modalOverlay}
50
+ aria-label="Close notes dialog"
51
+ {...overlayProps}
52
+ >
53
+ <div className={styles.modal}>
54
+ <button {...getCloseButtonProps({ ariaLabel: 'Close notes dialog' })}>×</button>
55
+ <h5 className={styles.modalTitle}>Additional Notes</h5>
56
+ <textarea
57
+ value={tempNotes}
58
+ onChange={(e) => setTempNotes(e.target.value)}
59
+ className={styles.modalTextarea}
60
+ placeholder="Enter additional notes..."
61
+ />
62
+ <div className={styles.modalButtons}>
63
+ <button
64
+ onClick={handleSave}
65
+ className={styles.saveButton}
66
+ disabled={isSaving}
67
+ aria-busy={isSaving}
68
+ >
69
+ {isSaving ? 'Saving...' : 'Save'}
70
+ </button>
71
+ <button
72
+ onClick={requestClose}
73
+ className={styles.cancelButton}
74
+ disabled={isSaving}
75
+ >
76
+ Cancel
77
+ </button>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ );
82
+ };
@@ -1,36 +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;
19
- showReturnButton?: boolean;
20
- stickyActionBar?: boolean;
21
- compactLayout?: boolean;
18
+ showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
22
19
  }
23
20
 
24
21
  type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
25
22
  type ClassType = 'Bullet' | 'Cartridge Case' | 'Other';
26
23
  type IndexType = 'number' | 'color';
27
24
 
28
- export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, showReturnButton = true, stickyActionBar = false, compactLayout = false }: NotesSidebarProps) => {
25
+ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, showNotification: externalShowNotification }: NotesEditorFormProps) => {
29
26
  // Loading/Saving Notes States
30
27
  const [isLoading, setIsLoading] = useState(false);
31
28
  const [loadError, setLoadError] = useState<string>();
32
- const [saveError, setSaveError] = useState<string>();
33
- const [saveSuccess, setSaveSuccess] = useState(false);
34
29
  const [isConfirmedImage, setIsConfirmedImage] = useState(false);
35
30
  // Case numbers state
36
31
  const [leftCase, setLeftCase] = useState('');
@@ -65,14 +60,18 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
65
60
  const [isSupportOpen, setIsSupportOpen] = useState(true);
66
61
  const areInputsDisabled = isUploading || isConfirmedImage;
67
62
 
63
+ const notificationHandler = (message: string, type: 'success' | 'error' | 'warning' = 'success') => {
64
+ if (externalShowNotification) {
65
+ externalShowNotification(message, type);
66
+ }
67
+ };
68
+
68
69
  useEffect(() => {
69
70
  const loadExistingNotes = async () => {
70
71
  if (!imageId || !currentCase) return;
71
72
 
72
73
  setIsLoading(true);
73
74
  setLoadError(undefined);
74
- setSaveError(undefined);
75
- setSaveSuccess(false);
76
75
  setIsConfirmedImage(false);
77
76
 
78
77
  try {
@@ -128,9 +127,6 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
128
127
  return;
129
128
  }
130
129
 
131
- setSaveError(undefined);
132
- setSaveSuccess(false);
133
-
134
130
  let existingData: AnnotationData | null = null;
135
131
 
136
132
  try {
@@ -139,7 +135,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
139
135
 
140
136
  if (existingData?.confirmationData) {
141
137
  setIsConfirmedImage(true);
142
- setSaveError('This image is confirmed. Notes cannot be modified.');
138
+ notificationHandler('This image is confirmed. Notes cannot be modified.', 'error');
143
139
  return;
144
140
  }
145
141
 
@@ -193,13 +189,12 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
193
189
  existingData,
194
190
  annotationData,
195
191
  currentCase,
196
- 'notes-sidebar',
192
+ 'notes-editor-form',
197
193
  imageId,
198
194
  originalFileName
199
195
  );
200
196
 
201
- setSaveSuccess(true);
202
- setTimeout(() => setSaveSuccess(false), 3000);
197
+ notificationHandler('Notes saved successfully.', 'success');
203
198
 
204
199
  // Refresh annotation data after saving notes
205
200
  if (onAnnotationRefresh) {
@@ -210,9 +205,9 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
210
205
  const errorMessage = error instanceof Error ? error.message : '';
211
206
  if (errorMessage.toLowerCase().includes('confirmed image')) {
212
207
  setIsConfirmedImage(true);
213
- setSaveError('This image is confirmed. Notes cannot be modified.');
208
+ notificationHandler('This image is confirmed. Notes cannot be modified.', 'error');
214
209
  } else {
215
- setSaveError('Failed to save notes. Please try again.');
210
+ notificationHandler('Failed to save notes. Please try again.', 'error');
216
211
  }
217
212
 
218
213
  // Audit logging for failed annotation save
@@ -223,7 +218,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
223
218
  existingData,
224
219
  null, // Failed save, no new value
225
220
  currentCase,
226
- 'notes-sidebar',
221
+ 'notes-editor-form',
227
222
  imageId,
228
223
  originalFileName
229
224
  );
@@ -234,7 +229,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
234
229
  };
235
230
 
236
231
  return (
237
- <div className={`${styles.notesSidebar} ${compactLayout ? styles.compactLayout : ''}`}>
232
+ <div className={`${styles.notesEditorForm} ${styles.editorLayout}`}>
238
233
  {isLoading ? (
239
234
  <div className={styles.loading}>Loading notes...</div>
240
235
  ) : loadError ? (
@@ -247,10 +242,6 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
247
242
  </div>
248
243
  )}
249
244
 
250
- {saveError && (
251
- <div className={styles.errorMessage}>{saveError}</div>
252
- )}
253
-
254
245
  <div className={styles.section}>
255
246
  <button
256
247
  type="button"
@@ -297,17 +288,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
297
288
  disabled={areInputsDisabled}
298
289
  />
299
290
  </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
- )}
309
291
  </div>
310
- {!compactLayout && <hr />}
311
292
  {/* Right side inputs */}
312
293
  <div className={styles.inputGroup}>
313
294
  <div className={styles.caseInput}>
@@ -342,21 +323,20 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
342
323
  </div>
343
324
  </div>
344
325
  </div>
345
- {!compactLayout && (
346
- <>
347
- <label htmlFor="colorSelect">Font</label>
348
- <ColorSelector
349
- selectedColor={caseFontColor}
350
- onColorSelect={setCaseFontColor}
351
- />
352
- </>
353
- )}
326
+ <hr />
327
+ <div className={styles.fontColorRow}>
328
+ <label htmlFor="colorSelect">Font</label>
329
+ <ColorSelector
330
+ selectedColor={caseFontColor}
331
+ onColorSelect={setCaseFontColor}
332
+ />
333
+ </div>
354
334
  </>
355
335
  )}
356
336
  </div>
357
337
 
358
- <div className={compactLayout ? styles.compactSectionGrid : undefined}>
359
- <div className={`${styles.section} ${compactLayout ? styles.compactFullSection : ''}`}>
338
+ <div className={styles.compactSectionGrid}>
339
+ <div className={`${styles.section} ${styles.compactFullSection}`}>
360
340
  <button
361
341
  type="button"
362
342
  className={styles.sectionToggle}
@@ -368,7 +348,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
368
348
  </button>
369
349
  {isClassOpen && (
370
350
  <>
371
- <div className={compactLayout ? styles.classCharacteristicsColumns : undefined}>
351
+ <div className={styles.classCharacteristicsColumns}>
372
352
  <div className={styles.classCharacteristicsMain}>
373
353
  <div className={styles.classCharacteristics}>
374
354
  <select
@@ -415,18 +395,16 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
415
395
  </label>
416
396
  </div>
417
397
 
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
- )}
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>
424
402
  </div>
425
403
  </>
426
404
  )}
427
405
  </div>
428
406
 
429
- <div className={`${styles.section} ${compactLayout ? styles.compactHalfSection : ''}`}>
407
+ <div className={`${styles.section} ${styles.compactHalfSection}`}>
430
408
  <button
431
409
  type="button"
432
410
  className={styles.sectionToggle}
@@ -477,7 +455,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
477
455
  )}
478
456
  </div>
479
457
 
480
- <div className={`${styles.section} ${compactLayout ? styles.compactHalfSection : ''}`}>
458
+ <div className={`${styles.section} ${styles.compactHalfSection}`}>
481
459
  <button
482
460
  type="button"
483
461
  className={styles.sectionToggle}
@@ -538,7 +516,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
538
516
  </button>
539
517
  </div>
540
518
 
541
- <div className={`${styles.notesActionBar} ${stickyActionBar ? styles.notesActionBarSticky : ''}`}>
519
+ <div className={`${styles.notesActionBar} ${styles.notesActionBarSticky}`}>
542
520
  <button
543
521
  onClick={handleSave}
544
522
  className={styles.saveButton}
@@ -547,28 +525,13 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
547
525
  >
548
526
  Save Notes
549
527
  </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
528
  </div>
561
-
562
- {saveSuccess && (
563
- <div className={styles.successMessage}>
564
- Notes saved successfully!
565
- </div>
566
- )}
567
- <NotesModal
529
+ <AddlNotesModal
568
530
  isOpen={isModalOpen}
569
531
  onClose={() => setIsModalOpen(false)}
570
532
  notes={additionalNotes}
571
533
  onSave={setAdditionalNotes}
534
+ showNotification={notificationHandler}
572
535
  />
573
536
  </>
574
537
  )}
@@ -1,6 +1,6 @@
1
1
  import type { User } from 'firebase/auth';
2
2
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
- import { NotesSidebar } from './notes-sidebar';
3
+ import { NotesEditorForm } from './notes-editor-form';
4
4
  import styles from './notes-editor-modal.module.css';
5
5
 
6
6
  interface NotesEditorModalProps {
@@ -12,6 +12,7 @@ interface NotesEditorModalProps {
12
12
  originalFileName?: string;
13
13
  onAnnotationRefresh?: () => void;
14
14
  isUploading?: boolean;
15
+ showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
15
16
  }
16
17
 
17
18
  export const NotesEditorModal = ({
@@ -23,9 +24,9 @@ export const NotesEditorModal = ({
23
24
  originalFileName,
24
25
  onAnnotationRefresh,
25
26
  isUploading = false,
27
+ showNotification,
26
28
  }: NotesEditorModalProps) => {
27
29
  const {
28
- requestClose,
29
30
  overlayProps,
30
31
  getCloseButtonProps,
31
32
  } = useOverlayDismiss({
@@ -47,17 +48,14 @@ export const NotesEditorModal = ({
47
48
  </button>
48
49
  </div>
49
50
  <div className={styles.content}>
50
- <NotesSidebar
51
+ <NotesEditorForm
51
52
  currentCase={currentCase}
52
- onReturn={requestClose}
53
53
  user={user}
54
54
  imageId={imageId}
55
55
  onAnnotationRefresh={onAnnotationRefresh}
56
56
  originalFileName={originalFileName}
57
57
  isUploading={isUploading}
58
- showReturnButton={false}
59
- stickyActionBar={true}
60
- compactLayout={true}
58
+ showNotification={showNotification}
61
59
  />
62
60
  </div>
63
61
  </div>
@@ -1,20 +1,20 @@
1
- .notesSidebar {
1
+ .notesEditorForm {
2
2
  padding: 0.3rem;
3
3
  }
4
4
 
5
- .compactLayout .caseNumbers {
5
+ .editorLayout .caseNumbers {
6
6
  display: grid;
7
7
  grid-template-columns: repeat(2, minmax(0, 1fr));
8
8
  gap: 1.25rem;
9
9
  align-items: start;
10
10
  }
11
11
 
12
- .compactLayout .caseNumbers > .inputGroup + .inputGroup {
12
+ .editorLayout .caseNumbers > .inputGroup + .inputGroup {
13
13
  border-left: 1px solid #dee2e6;
14
14
  padding-left: 1.25rem;
15
15
  }
16
16
 
17
- .compactLayout .inputGroup {
17
+ .editorLayout .inputGroup {
18
18
  margin-bottom: 0;
19
19
  }
20
20
 
@@ -35,11 +35,11 @@
35
35
  margin-bottom: 1.5rem;
36
36
  }
37
37
 
38
- .compactLayout .notesActionBarSticky {
38
+ .editorLayout .notesActionBarSticky {
39
39
  margin-top: 0.25rem;
40
40
  }
41
41
 
42
- .compactLayout .compactSectionGrid > .compactHalfSection + .compactHalfSection {
42
+ .editorLayout .compactSectionGrid > .compactHalfSection + .compactHalfSection {
43
43
  border-left: 1px solid #dee2e6;
44
44
  padding-left: 1.25rem;
45
45
  }
@@ -49,19 +49,19 @@
49
49
  padding-left: 1.25rem;
50
50
  }
51
51
 
52
- .compactLayout .additionalNotesRow {
52
+ .editorLayout .additionalNotesRow {
53
53
  border-top: 1px solid #dee2e6;
54
54
  padding-top: 1.25rem;
55
55
  }
56
56
 
57
57
  @media (max-width: 980px) {
58
- .compactLayout .caseNumbers,
58
+ .editorLayout .caseNumbers,
59
59
  .compactSectionGrid {
60
60
  grid-template-columns: 1fr;
61
61
  }
62
62
 
63
- .compactLayout .caseNumbers > .inputGroup + .inputGroup,
64
- .compactLayout
63
+ .editorLayout .caseNumbers > .inputGroup + .inputGroup,
64
+ .editorLayout
65
65
  .compactSectionGrid
66
66
  > .compactHalfSection
67
67
  + .compactHalfSection,
@@ -152,9 +152,25 @@ textarea:focus {
152
152
  }
153
153
 
154
154
  .caseNumbers {
155
+ display: grid;
156
+ grid-template-columns: 1fr 1fr;
157
+ gap: 1.5rem;
155
158
  margin-bottom: 2rem;
156
159
  }
157
160
 
161
+ .fontColorRow {
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: 0.75rem;
165
+ margin-bottom: 2rem;
166
+ }
167
+
168
+ .fontColorRow label {
169
+ font-size: 0.95rem;
170
+ font-weight: 600;
171
+ color: #212529;
172
+ }
173
+
158
174
  .caseInput {
159
175
  display: flex;
160
176
  flex-direction: column;
@@ -382,7 +398,7 @@ textarea:focus {
382
398
  width: 100%;
383
399
  }
384
400
 
385
- .compactLayout .additionalNotesRow {
401
+ .editorLayout .additionalNotesRow {
386
402
  grid-column: 1 / -1;
387
403
  }
388
404
 
@@ -13,6 +13,7 @@ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
13
13
  interface SidebarContainerProps {
14
14
  user: User;
15
15
  onImageSelect: (file: FileData) => void;
16
+ onOpenCase: () => void;
16
17
  imageId?: string;
17
18
  currentCase: string;
18
19
  files: FileData[];
@@ -8,6 +8,7 @@ import { type FileData } from '~/types';
8
8
  interface SidebarProps {
9
9
  user: User;
10
10
  onImageSelect: (file: FileData) => void;
11
+ onOpenCase: () => void;
11
12
  imageId?: string;
12
13
  currentCase: string;
13
14
  files: FileData[];
@@ -27,6 +28,7 @@ interface SidebarProps {
27
28
  export const Sidebar = ({
28
29
  user,
29
30
  onImageSelect,
31
+ onOpenCase,
30
32
  imageId,
31
33
  currentCase,
32
34
  imageLoaded,
@@ -74,6 +76,7 @@ export const Sidebar = ({
74
76
  <CaseSidebar
75
77
  user={user}
76
78
  onImageSelect={onImageSelect}
79
+ onOpenCase={onOpenCase}
77
80
  currentCase={currentCase}
78
81
  imageLoaded={imageLoaded}
79
82
  setImageLoaded={setImageLoaded}
@@ -1,6 +1,4 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
1
+ @import "modern-normalize";
4
2
 
5
3
  @layer base {
6
4
  :root {
@@ -85,11 +85,13 @@ export const useOverlayDismiss = ({
85
85
  }
86
86
  }, [closeOnBackdrop, requestClose]);
87
87
 
88
+ const isBackdropDismissInteractive = closeOnBackdrop && canDismiss;
89
+
88
90
  const overlayProps = {
89
- role: 'button' as const,
90
- tabIndex: 0,
91
- onMouseDown: handleOverlayMouseDown,
92
- onKeyDown: handleOverlayKeyDown,
91
+ role: isBackdropDismissInteractive ? 'button' : 'presentation',
92
+ tabIndex: isBackdropDismissInteractive ? 0 : undefined,
93
+ onMouseDown: isBackdropDismissInteractive ? handleOverlayMouseDown : undefined,
94
+ onKeyDown: isBackdropDismissInteractive ? handleOverlayKeyDown : undefined,
93
95
  style: { cursor: 'default' as const },
94
96
  };
95
97
 
package/app/root.tsx CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  import { AuthProvider } from '~/components/auth/auth-provider';
17
17
  import { auth } from '~/services/firebase';
18
18
  import styles from '~/styles/root.module.css';
19
- import './tailwind.css';
19
+ import './global.css';
20
20
 
21
21
  export const links: LinksFunction = () => [
22
22
  { rel: "preconnect", href: "https://fonts.googleapis.com" },