@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,7 +1,85 @@
1
- .notesSidebar {
1
+ .notesEditorForm {
2
2
  padding: 0.3rem;
3
3
  }
4
4
 
5
+ .editorLayout .caseNumbers {
6
+ display: grid;
7
+ grid-template-columns: repeat(2, minmax(0, 1fr));
8
+ gap: 1.25rem;
9
+ align-items: start;
10
+ }
11
+
12
+ .editorLayout .caseNumbers > .inputGroup + .inputGroup {
13
+ border-left: 1px solid #dee2e6;
14
+ padding-left: 1.25rem;
15
+ }
16
+
17
+ .editorLayout .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
+ .editorLayout .notesActionBarSticky {
39
+ margin-top: 0.25rem;
40
+ }
41
+
42
+ .editorLayout .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
+ .editorLayout .additionalNotesRow {
53
+ border-top: 1px solid #dee2e6;
54
+ padding-top: 1.25rem;
55
+ }
56
+
57
+ @media (max-width: 980px) {
58
+ .editorLayout .caseNumbers,
59
+ .compactSectionGrid {
60
+ grid-template-columns: 1fr;
61
+ }
62
+
63
+ .editorLayout .caseNumbers > .inputGroup + .inputGroup,
64
+ .editorLayout
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;
@@ -54,9 +152,25 @@ textarea:focus {
54
152
  }
55
153
 
56
154
  .caseNumbers {
155
+ display: grid;
156
+ grid-template-columns: 1fr 1fr;
157
+ gap: 1.5rem;
158
+ margin-bottom: 2rem;
159
+ }
160
+
161
+ .fontColorRow {
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: 0.75rem;
57
165
  margin-bottom: 2rem;
58
166
  }
59
167
 
168
+ .fontColorRow label {
169
+ font-size: 0.95rem;
170
+ font-weight: 600;
171
+ color: #212529;
172
+ }
173
+
60
174
  .caseInput {
61
175
  display: flex;
62
176
  flex-direction: column;
@@ -103,6 +217,39 @@ textarea:focus {
103
217
  margin-bottom: 2rem;
104
218
  }
105
219
 
220
+ .classCharacteristicsColumns {
221
+ display: grid;
222
+ grid-template-columns: repeat(2, minmax(0, 1fr));
223
+ gap: 1.25rem;
224
+ align-items: start;
225
+ }
226
+
227
+ .classCharacteristicsMain {
228
+ min-width: 0;
229
+ }
230
+
231
+ .characteristicsPlaceholder {
232
+ border: 1px dashed #ced4da;
233
+ border-radius: 8px;
234
+ padding: 0.85rem;
235
+ background: #f8f9fa;
236
+ min-height: 150px;
237
+ }
238
+
239
+ .placeholderTitle {
240
+ margin: 0 0 0.5rem;
241
+ color: #495057;
242
+ font-size: 0.92rem;
243
+ font-weight: 600;
244
+ }
245
+
246
+ .placeholderText {
247
+ margin: 0;
248
+ color: #6c757d;
249
+ font-size: 0.85rem;
250
+ line-height: 1.4;
251
+ }
252
+
106
253
  .classCharacteristics input {
107
254
  width: 100%;
108
255
  padding: 0.75rem;
@@ -247,6 +394,14 @@ textarea:focus {
247
394
  margin-top: 1rem;
248
395
  }
249
396
 
397
+ .additionalNotesRow {
398
+ width: 100%;
399
+ }
400
+
401
+ .editorLayout .additionalNotesRow {
402
+ grid-column: 1 / -1;
403
+ }
404
+
250
405
  .notesButton:hover {
251
406
  background-color: color-mix(in lab, var(--primary) 95%, transparent);
252
407
  }
@@ -266,6 +421,7 @@ textarea:focus {
266
421
  }
267
422
 
268
423
  .modal {
424
+ position: relative;
269
425
  background: white;
270
426
  padding: 2rem;
271
427
  border-radius: 8px;
@@ -332,6 +488,19 @@ textarea:focus {
332
488
  box-sizing: border-box;
333
489
  }
334
490
 
491
+ .notesActionBar {
492
+ display: flex;
493
+ flex-direction: column;
494
+ gap: 0.75rem;
495
+ }
496
+
497
+ .notesActionBarSticky {
498
+ position: sticky;
499
+ bottom: 0;
500
+ padding: 0.75rem 0 0.25rem;
501
+ background: linear-gradient(to top, white 72%, rgba(255, 255, 255, 0));
502
+ }
503
+
335
504
  .saveButton:hover {
336
505
  background-color: color-mix(in lab, var(--accent) 95%, transparent);
337
506
  }
@@ -1,37 +1,33 @@
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;
16
+ onOpenCase: () => void;
15
17
  imageId?: string;
16
- onCaseChange: (caseNumber: string) => void;
17
18
  currentCase: string;
18
- setCurrentCase: (caseNumber: string) => void;
19
19
  files: FileData[];
20
20
  setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
21
21
  imageLoaded: boolean;
22
22
  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
23
  showNotes: boolean;
30
24
  setShowNotes: (show: boolean) => void;
31
25
  onAnnotationRefresh?: () => void;
32
26
  isReadOnly?: boolean;
33
27
  isConfirmed?: boolean;
34
28
  confirmationSaveVersion?: number;
29
+ isUploading?: boolean;
30
+ onUploadStatusChange?: (isUploading: boolean) => void;
35
31
  }
36
32
 
37
33
  export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
@@ -39,24 +35,16 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
39
35
  const year = new Date().getFullYear();
40
36
  const appVersion = getAppVersion();
41
37
 
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]);
38
+ const {
39
+ overlayProps,
40
+ getCloseButtonProps,
41
+ } = useOverlayDismiss({
42
+ isOpen: isFooterModalOpen,
43
+ onClose: () => setIsFooterModalOpen(false),
44
+ });
57
45
 
58
46
  return (
59
- <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
47
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
60
48
  {/* Main Sidebar */}
61
49
  <Sidebar {...props} />
62
50
 
@@ -72,13 +60,13 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
72
60
 
73
61
  {/* Footer Modal */}
74
62
  {isFooterModalOpen && (
75
- <div className={styles.footerModalOverlay} onClick={() => setIsFooterModalOpen(false)}>
63
+ <div className={styles.footerModalOverlay} {...overlayProps} aria-label="Close About and Support dialog">
76
64
  <div className={styles.footerModal} onClick={(e) => e.stopPropagation()}>
77
65
  <div className={styles.footerModalHeader}>
78
66
  <h2 className={styles.footerModalTitle}>About Striae</h2>
79
- <button
80
- onClick={() => setIsFooterModalOpen(false)}
67
+ <button
81
68
  className={styles.footerModalClose}
69
+ {...getCloseButtonProps({ ariaLabel: 'Close About and Support dialog' })}
82
70
  >
83
71
  ×
84
72
  </button>
@@ -3,54 +3,17 @@
3
3
  position: relative;
4
4
  width: 300px;
5
5
  min-width: 250px;
6
- height: calc(100vh - 60px);
6
+ height: calc(100% - 60px);
7
+ display: flex;
8
+ flex-direction: column;
7
9
  background-color: #f8f9fa;
8
10
  border-right: 1px solid #dee2e6;
9
- padding: 1.5rem;
10
- overflow-y: auto;
11
+ padding: 0.25rem 1rem 0.875rem;
12
+ overflow: hidden;
11
13
  flex-shrink: 0;
12
14
  box-sizing: border-box;
13
15
  }
14
16
 
15
- /* User Section */
16
- .userInfo {
17
- display: flex;
18
- flex-direction: column;
19
- align-items: flex-start;
20
- gap: 1rem;
21
- padding-bottom: 1rem;
22
- border-bottom: 2px solid #e9ecef;
23
- margin-bottom: 1rem;
24
- }
25
-
26
- .userTitle {
27
- font-size: 1.2rem;
28
- font-weight: 600;
29
- color: #212529;
30
- margin: 0;
31
- }
32
-
33
- .userActions {
34
- display: flex;
35
- gap: 0.5rem;
36
- width: 100%;
37
- }
38
-
39
- .profileButton {
40
- padding: 0.5rem 1rem;
41
- background-color: #6c757d;
42
- color: white;
43
- border: none;
44
- border-radius: 6px;
45
- font-size: 0.875rem;
46
- cursor: pointer;
47
- transition: all 0.2s;
48
- }
49
-
50
- .profileButton:hover {
51
- background-color: #5c636a;
52
- }
53
-
54
17
  /* Footer Button */
55
18
  .footerButton {
56
19
  position: fixed;
@@ -239,33 +202,6 @@
239
202
  text-underline-offset: 3px;
240
203
  }
241
204
 
242
- /* Import Section */
243
- .importSection {
244
- margin-top: auto;
245
- padding: 1rem 0;
246
- border-top: 1px solid var(--divider);
247
- }
248
-
249
- .importButton {
250
- width: 100%;
251
- padding: 0.75rem 1rem;
252
- background: var(--success);
253
- color: white;
254
- border: none;
255
- border-radius: var(--spaceXS);
256
- font-size: var(--fontSizeBodyS);
257
- font-weight: var(--fontWeightMedium);
258
- cursor: pointer;
259
- transition: all var(--durationS) var(--bezierFastoutSlowin);
260
- box-shadow: 0 1px 3px color-mix(in lab, var(--success) 30%, transparent);
261
- box-sizing: border-box;
262
- }
263
-
264
- .importButton:hover {
265
- background: color-mix(in lab, var(--success) 85%, var(--black));
266
- box-shadow: 0 2px 6px color-mix(in lab, var(--success) 40%, transparent);
267
- }
268
-
269
205
  /* Hash Button */
270
206
  .hashButton {
271
207
  width: 100%;
@@ -1,31 +1,20 @@
1
1
  import type { User } from 'firebase/auth';
2
2
  import { useState, useCallback } from 'react';
3
3
  import styles from './sidebar.module.css';
4
- import { ManageProfile } from '../user/manage-profile';
5
- import { SignOut } from '../actions/signout';
6
4
  import { CaseSidebar } from './cases/case-sidebar';
7
- import { NotesSidebar } from './notes/notes-sidebar';
8
- import { CaseImport } from './case-import/case-import';
9
5
  import { Toast } from '../toast/toast';
10
- import { type FileData, type ImportResult, type ConfirmationImportResult } from '~/types';
6
+ import { type FileData } from '~/types';
11
7
 
12
8
  interface SidebarProps {
13
9
  user: User;
14
10
  onImageSelect: (file: FileData) => void;
11
+ onOpenCase: () => void;
15
12
  imageId?: string;
16
- onCaseChange: (caseNumber: string) => void;
17
13
  currentCase: string;
18
- setCurrentCase: (caseNumber: string) => void;
19
14
  files: FileData[];
20
15
  setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
21
16
  imageLoaded: boolean;
22
17
  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
18
  showNotes: boolean;
30
19
  setShowNotes: (show: boolean) => void;
31
20
  onAnnotationRefresh?: () => void;
@@ -33,66 +22,35 @@ interface SidebarProps {
33
22
  isConfirmed?: boolean;
34
23
  confirmationSaveVersion?: number;
35
24
  isUploading?: boolean;
25
+ onUploadStatusChange?: (isUploading: boolean) => void;
36
26
  }
37
27
 
38
28
  export const Sidebar = ({
39
29
  user,
40
30
  onImageSelect,
31
+ onOpenCase,
41
32
  imageId,
42
- onCaseChange,
43
33
  currentCase,
44
- setCurrentCase,
45
34
  imageLoaded,
46
35
  setImageLoaded,
47
36
  files,
48
37
  setFiles,
49
- caseNumber,
50
- setCaseNumber,
51
- error,
52
- setError,
53
- successAction,
54
- setSuccessAction,
55
- showNotes,
56
38
  setShowNotes,
57
- onAnnotationRefresh,
58
39
  isReadOnly = false,
59
40
  isConfirmed = false,
60
41
  confirmationSaveVersion = 0,
61
- isUploading: initialIsUploading = false,
42
+ isUploading: initialIsUploading = false,
43
+ onUploadStatusChange,
62
44
  }: SidebarProps) => {
63
- const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
64
- const [isImportModalOpen, setIsImportModalOpen] = useState(false);
65
45
  const [isUploading, setIsUploading] = useState(initialIsUploading);
66
46
  const [toastMessage, setToastMessage] = useState('');
67
47
  const [toastType, setToastType] = useState<'success' | 'error' | 'warning'>('success');
68
48
  const [isToastVisible, setIsToastVisible] = useState(false);
69
49
 
70
- const handleImportComplete = useCallback((result: ImportResult | ConfirmationImportResult) => {
71
- if (result.success) {
72
- // For case imports, load the imported case automatically
73
- if ('isReadOnly' in result) {
74
- // This is an ImportResult (case import)
75
- if (result.caseNumber && result.isReadOnly) {
76
- // Successful read-only case import - load the case
77
- onCaseChange(result.caseNumber);
78
- setCurrentCase(result.caseNumber);
79
- setCaseNumber(result.caseNumber);
80
- setSuccessAction('loaded');
81
- } else if (!result.caseNumber && !result.isReadOnly) {
82
- // Read-only case cleared - reset all UI state
83
- setCurrentCase('');
84
- setCaseNumber('');
85
- setFiles([]);
86
- onImageSelect({ id: 'clear', originalFilename: '/clear.jpg', uploadedAt: '' });
87
- setImageLoaded(false);
88
- onCaseChange(''); // This will trigger canvas/annotation state reset in main component
89
- setShowNotes(false); // Close notes sidebar
90
- setSuccessAction(null);
91
- }
92
- }
93
- // For confirmation imports, no action needed - the confirmations are already loaded
94
- }
95
- }, [onCaseChange, setCurrentCase, setCaseNumber, setSuccessAction, setFiles, onImageSelect, setImageLoaded, setShowNotes]);
50
+ const handleUploadStatusChange = useCallback((uploading: boolean) => {
51
+ setIsUploading(uploading);
52
+ onUploadStatusChange?.(uploading);
53
+ }, [onUploadStatusChange]);
96
54
 
97
55
  const handleUploadComplete = useCallback((result: { successCount: number; failedFiles: string[] }) => {
98
56
  if (result.successCount === 0 && result.failedFiles.length > 0) {
@@ -115,80 +73,24 @@ export const Sidebar = ({
115
73
 
116
74
  return (
117
75
  <div className={styles.sidebar}>
118
- <div className={styles.userInfo}>
119
- <h3 className={styles.userTitle}>
120
- {`${user.displayName?.split(' ')[0] || 'User'}'s Striae`}
121
- </h3>
122
- <div className={styles.userActions}>
123
- <button
124
- onClick={() => setIsProfileModalOpen(true)}
125
- className={styles.profileButton}
126
- disabled={isUploading}
127
- title={isUploading ? 'Cannot manage profile while uploading files' : undefined}
128
- >
129
- Manage Profile
130
- </button>
131
- <SignOut disabled={isUploading} />
132
- </div>
133
- </div>
134
- <ManageProfile
135
- isOpen={isProfileModalOpen}
136
- onClose={() => setIsProfileModalOpen(false)}
137
- />
138
- <CaseImport
139
- isOpen={isImportModalOpen}
140
- onClose={() => setIsImportModalOpen(false)}
141
- onImportComplete={handleImportComplete}
76
+ <CaseSidebar
77
+ user={user}
78
+ onImageSelect={onImageSelect}
79
+ onOpenCase={onOpenCase}
80
+ currentCase={currentCase}
81
+ imageLoaded={imageLoaded}
82
+ setImageLoaded={setImageLoaded}
83
+ files={files}
84
+ setFiles={setFiles}
85
+ onNotesClick={() => setShowNotes(true)}
86
+ isReadOnly={isReadOnly}
87
+ isConfirmed={isConfirmed}
88
+ confirmationSaveVersion={confirmationSaveVersion}
89
+ selectedFileId={imageId}
90
+ isUploading={isUploading}
91
+ onUploadStatusChange={handleUploadStatusChange}
92
+ onUploadComplete={handleUploadComplete}
142
93
  />
143
- {showNotes ? (
144
- <NotesSidebar
145
- currentCase={currentCase}
146
- onReturn={() => setShowNotes(false)}
147
- user={user}
148
- imageId={imageId || ''}
149
- onAnnotationRefresh={onAnnotationRefresh}
150
- originalFileName={files.find(file => file.id === imageId)?.originalFilename}
151
- isUploading={isUploading}
152
- />
153
- ) : (
154
- <>
155
- <CaseSidebar
156
- user={user}
157
- onImageSelect={onImageSelect}
158
- onCaseChange={onCaseChange}
159
- currentCase={currentCase}
160
- setCurrentCase={setCurrentCase}
161
- imageLoaded={imageLoaded}
162
- setImageLoaded={setImageLoaded}
163
- files={files}
164
- setFiles={setFiles}
165
- caseNumber={caseNumber}
166
- setCaseNumber={setCaseNumber}
167
- error={error}
168
- setError={setError}
169
- successAction={successAction}
170
- setSuccessAction={setSuccessAction}
171
- onNotesClick={() => setShowNotes(true)}
172
- isReadOnly={isReadOnly}
173
- isConfirmed={isConfirmed}
174
- confirmationSaveVersion={confirmationSaveVersion}
175
- selectedFileId={imageId}
176
- isUploading={isUploading}
177
- onUploadStatusChange={setIsUploading}
178
- onUploadComplete={handleUploadComplete}
179
- />
180
- <div className={styles.importSection}>
181
- <button
182
- onClick={() => setIsImportModalOpen(true)}
183
- className={styles.importButton}
184
- disabled={isUploading}
185
- title={isUploading ? 'Cannot import while uploading files' : undefined}
186
- >
187
- Import/Clear RO Case
188
- </button>
189
- </div>
190
- </>
191
- )}
192
94
  <Toast
193
95
  message={toastMessage}
194
96
  type={toastType}
@@ -1,24 +1,24 @@
1
1
  /* Image Upload Zone */
2
2
  .imageUploadZone {
3
- margin: 1rem 0;
3
+ margin: 0.25rem 0;
4
4
  display: flex;
5
5
  flex-direction: column;
6
- gap: 0.5rem;
6
+ gap: 0.375rem;
7
7
  }
8
8
 
9
9
  .imageUploadZone label {
10
- font-size: 0.9rem;
10
+ font-size: 0.85rem;
11
11
  font-weight: 500;
12
12
  color: #000000;
13
- margin-bottom: 0.5rem;
13
+ margin-bottom: 0.25rem;
14
14
  }
15
15
 
16
16
  .fileInput {
17
17
  width: 100%;
18
- padding: 0.5rem;
18
+ padding: 0.375rem 0.5rem;
19
19
  border: 1px solid #dee2e6;
20
20
  border-radius: 4px;
21
- font-size: 0.875rem;
21
+ font-size: 0.8125rem;
22
22
  box-sizing: border-box;
23
23
  cursor: pointer;
24
24
  }
@@ -33,18 +33,18 @@
33
33
  display: flex;
34
34
  align-items: center;
35
35
  justify-content: center;
36
- padding: 2rem 1rem;
36
+ padding: 1rem 0.75rem;
37
37
  border: 2px dashed #dee2e6;
38
38
  border-radius: 6px;
39
39
  background-color: #fafafa;
40
40
  transition: all 0.2s ease;
41
41
  pointer-events: none;
42
- min-height: 100px;
42
+ min-height: 72px;
43
43
  }
44
44
 
45
45
  .dragDropText {
46
46
  margin: 0;
47
- font-size: 0.9rem;
47
+ font-size: 0.82rem;
48
48
  color: #6c757d;
49
49
  text-align: center;
50
50
  font-weight: 500;
@@ -68,11 +68,11 @@
68
68
 
69
69
  .progressBar {
70
70
  width: 100%;
71
- height: 6px;
71
+ height: 5px;
72
72
  background-color: #e9ecef;
73
73
  border-radius: 3px;
74
74
  overflow: hidden;
75
- margin-top: 0.5rem;
75
+ margin-top: 0.25rem;
76
76
  }
77
77
 
78
78
  .progressFill {
@@ -82,7 +82,7 @@
82
82
  }
83
83
 
84
84
  .uploadingText {
85
- font-size: 0.875rem;
85
+ font-size: 0.8125rem;
86
86
  color: var(--textBody);
87
87
  font-weight: 500;
88
88
  }
@@ -93,7 +93,7 @@
93
93
  justify-content: center;
94
94
  gap: 0.5rem;
95
95
  width: 100%;
96
- margin-top: 0.25rem;
96
+ margin-top: 0.125rem;
97
97
  }
98
98
 
99
99
  .fileCountText {
@@ -14,6 +14,7 @@
14
14
  }
15
15
 
16
16
  .modal {
17
+ position: relative;
17
18
  background: #ffffff;
18
19
  border-radius: 12px;
19
20
  padding: 2rem;