@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.
- package/.env.example +8 -0
- package/LICENSE +1 -1
- package/app/components/actions/case-export/core-export.ts +14 -8
- package/app/components/actions/case-export/data-processing.ts +1 -0
- package/app/components/actions/case-export/download-handlers.ts +7 -0
- package/app/components/actions/case-export/metadata-helpers.ts +2 -1
- package/app/components/actions/case-import/confirmation-import.ts +12 -2
- package/app/components/actions/case-import/orchestrator.ts +78 -32
- package/app/components/actions/case-import/storage-operations.ts +97 -8
- package/app/components/actions/case-import/zip-processing.ts +159 -86
- package/app/components/actions/case-manage.ts +463 -8
- package/app/components/actions/confirm-export.ts +9 -2
- package/app/components/actions/image-manage.ts +77 -44
- package/app/components/audit/user-audit-viewer.tsx +19 -8
- package/app/components/audit/user-audit.module.css +21 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +12 -2
- package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +24 -1
- package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
- package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
- package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
- package/app/components/canvas/canvas.module.css +64 -54
- package/app/components/canvas/canvas.tsx +14 -16
- package/app/components/canvas/confirmation/confirmation.module.css +1 -0
- package/app/components/canvas/confirmation/confirmation.tsx +12 -14
- package/app/components/colors/colors.module.css +4 -3
- package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
- package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
- package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
- package/app/components/navbar/navbar.module.css +447 -0
- package/app/components/navbar/navbar.tsx +402 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
- package/app/components/sidebar/case-export/case-export.module.css +1 -0
- package/app/components/sidebar/case-export/case-export.tsx +8 -46
- package/app/components/sidebar/case-import/case-import.module.css +23 -0
- package/app/components/sidebar/case-import/case-import.tsx +64 -16
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +68 -588
- package/app/components/sidebar/cases/cases-modal.module.css +1 -0
- package/app/components/sidebar/cases/cases-modal.tsx +82 -43
- package/app/components/sidebar/cases/cases.module.css +82 -21
- package/app/components/sidebar/files/files-modal.module.css +1 -0
- package/app/components/sidebar/files/files-modal.tsx +49 -52
- package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
- package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +187 -138
- package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
- package/app/components/sidebar/notes/notes-editor-modal.tsx +64 -0
- package/app/components/sidebar/notes/notes.module.css +170 -1
- package/app/components/sidebar/sidebar-container.tsx +16 -28
- package/app/components/sidebar/sidebar.module.css +5 -69
- package/app/components/sidebar/sidebar.tsx +27 -125
- package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
- package/app/components/user/inactivity-warning.module.css +1 -0
- package/app/components/user/inactivity-warning.tsx +15 -2
- package/app/components/user/manage-profile.tsx +23 -10
- package/app/{tailwind.css → global.css} +1 -3
- package/app/hooks/useOverlayDismiss.ts +54 -4
- package/app/root.tsx +1 -1
- package/app/routes/auth/login.tsx +785 -774
- package/app/routes/striae/striae.module.css +10 -3
- package/app/routes/striae/striae.tsx +475 -30
- package/app/services/audit/audit.service.ts +173 -27
- package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
- package/app/services/audit/builders/index.ts +1 -0
- package/app/types/audit.ts +4 -1
- package/app/types/case.ts +29 -0
- package/app/types/import.ts +3 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
- package/app/utils/data/data-operations.ts +17 -861
- package/app/utils/data/index.ts +11 -1
- package/app/utils/data/operations/batch-operations.ts +113 -0
- package/app/utils/data/operations/case-operations.ts +168 -0
- package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
- package/app/utils/data/operations/file-annotation-operations.ts +196 -0
- package/app/utils/data/operations/index.ts +7 -0
- package/app/utils/data/operations/signing-operations.ts +225 -0
- package/app/utils/data/operations/types.ts +42 -0
- package/app/utils/data/operations/validation-operations.ts +48 -0
- package/app/utils/data/permissions.ts +16 -1
- package/app/utils/forensics/audit-export-signature.ts +5 -1
- package/app/utils/forensics/confirmation-signature.ts +3 -0
- package/app/utils/forensics/export-verification.ts +426 -22
- package/functions/api/_shared/firebase-auth.ts +2 -7
- package/functions/api/image/[[path]].ts +20 -23
- package/functions/api/pdf/[[path]].ts +27 -8
- package/package.json +7 -12
- package/scripts/deploy-primershear-emails.sh +2 -1
- package/worker-configuration.d.ts +3 -3
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/worker-configuration.d.ts +7448 -11323
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/image-worker.example.ts +16 -5
- package/workers/image-worker/worker-configuration.d.ts +7447 -11322
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +1 -1
- package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +9 -14
- package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
- package/workers/pdf-worker/src/report-types.ts +3 -3
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/src/user-worker.example.ts +17 -0
- package/workers/user-worker/worker-configuration.d.ts +7448 -11323
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/NOTICE +0 -13
- package/app/components/sidebar/notes/notes-modal.tsx +0 -53
- package/postcss.config.js +0 -6
- package/public/.well-known/keybase.txt +0 -56
- package/tailwind.config.ts +0 -22
|
@@ -12,12 +12,21 @@
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
.imageContainer {
|
|
15
|
-
|
|
15
|
+
flex: 1;
|
|
16
|
+
min-width: 0;
|
|
16
17
|
display: flex;
|
|
17
18
|
justify-content: center;
|
|
18
19
|
align-items: center;
|
|
20
|
+
overflow: visible;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Tight wrapper sized to the rendered image — all overlays position relative to this */
|
|
24
|
+
.imageWrapper {
|
|
25
|
+
position: relative;
|
|
26
|
+
display: inline-block;
|
|
27
|
+
line-height: 0;
|
|
19
28
|
max-width: 100%;
|
|
20
|
-
max-height:
|
|
29
|
+
max-height: 80vh;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
.toolbarWrapper {
|
|
@@ -34,7 +43,7 @@
|
|
|
34
43
|
top: 1rem;
|
|
35
44
|
z-index: 15;
|
|
36
45
|
color: #e0e0e0;
|
|
37
|
-
font-family:
|
|
46
|
+
font-family: "Inter", sans-serif;
|
|
38
47
|
font-size: 1rem;
|
|
39
48
|
font-weight: 500;
|
|
40
49
|
pointer-events: none;
|
|
@@ -47,7 +56,7 @@
|
|
|
47
56
|
|
|
48
57
|
.confirmationIncluded {
|
|
49
58
|
font-size: 0.8rem;
|
|
50
|
-
color: #
|
|
59
|
+
color: #ffde21;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
.confirmationConfirmed {
|
|
@@ -103,31 +112,15 @@
|
|
|
103
112
|
box-shadow: 0 2px 6px rgba(0, 123, 255, 0.4);
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
/* Company Display */
|
|
107
|
-
.companyDisplay {
|
|
108
|
-
position: absolute;
|
|
109
|
-
right: 2rem;
|
|
110
|
-
top: 1rem;
|
|
111
|
-
z-index: 15;
|
|
112
|
-
color: #e0e0e0;
|
|
113
|
-
font-family: 'Inter', sans-serif;
|
|
114
|
-
font-size: 1.5rem;
|
|
115
|
-
font-weight: 500;
|
|
116
|
-
pointer-events: none;
|
|
117
|
-
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
|
118
|
-
white-space: nowrap;
|
|
119
|
-
overflow: hidden;
|
|
120
|
-
text-overflow: ellipsis;
|
|
121
|
-
max-width: calc(100vw - 16rem);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
115
|
.image {
|
|
116
|
+
display: block;
|
|
125
117
|
max-width: 100%;
|
|
126
118
|
max-height: 80vh;
|
|
127
119
|
object-fit: contain;
|
|
128
120
|
}
|
|
129
121
|
|
|
130
|
-
.placeholder,
|
|
122
|
+
.placeholder,
|
|
123
|
+
.loading {
|
|
131
124
|
color: #e0e0e0;
|
|
132
125
|
font-size: 1.1rem;
|
|
133
126
|
text-align: center;
|
|
@@ -155,7 +148,7 @@
|
|
|
155
148
|
.leftAnnotation,
|
|
156
149
|
.rightAnnotation {
|
|
157
150
|
position: absolute;
|
|
158
|
-
padding:
|
|
151
|
+
padding: 1rem 1.4rem;
|
|
159
152
|
background: rgba(0, 0, 0, 0.7);
|
|
160
153
|
border-radius: 6px;
|
|
161
154
|
backdrop-filter: blur(4px);
|
|
@@ -177,7 +170,7 @@
|
|
|
177
170
|
position: absolute;
|
|
178
171
|
bottom: 1rem;
|
|
179
172
|
left: 1rem;
|
|
180
|
-
padding:
|
|
173
|
+
padding: 1rem 1.4rem;
|
|
181
174
|
background: rgba(0, 0, 0, 0.7);
|
|
182
175
|
border-radius: 6px;
|
|
183
176
|
backdrop-filter: blur(4px);
|
|
@@ -186,7 +179,7 @@
|
|
|
186
179
|
}
|
|
187
180
|
|
|
188
181
|
.caseText {
|
|
189
|
-
font-family:
|
|
182
|
+
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
|
190
183
|
font-size: 1.1rem;
|
|
191
184
|
font-weight: 700;
|
|
192
185
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
|
@@ -197,7 +190,7 @@
|
|
|
197
190
|
/* Class Characteristics Display */
|
|
198
191
|
.classCharacteristics {
|
|
199
192
|
position: absolute;
|
|
200
|
-
|
|
193
|
+
bottom: calc(100% + 0.5rem);
|
|
201
194
|
left: 50%;
|
|
202
195
|
transform: translateX(-50%);
|
|
203
196
|
z-index: 15;
|
|
@@ -205,14 +198,14 @@
|
|
|
205
198
|
}
|
|
206
199
|
|
|
207
200
|
.classText {
|
|
208
|
-
padding:
|
|
201
|
+
padding: 1rem 2rem;
|
|
209
202
|
background: rgba(0, 0, 0, 0.8);
|
|
210
203
|
color: #ffffff;
|
|
211
204
|
border-radius: 8px;
|
|
212
205
|
backdrop-filter: blur(6px);
|
|
213
206
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
214
207
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
215
|
-
font-family:
|
|
208
|
+
font-family: "Inter", sans-serif;
|
|
216
209
|
font-size: 1.1rem;
|
|
217
210
|
font-weight: 600;
|
|
218
211
|
text-align: center;
|
|
@@ -237,7 +230,7 @@
|
|
|
237
230
|
backdrop-filter: blur(6px);
|
|
238
231
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
239
232
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
240
|
-
font-family:
|
|
233
|
+
font-family: "Inter", sans-serif;
|
|
241
234
|
font-size: 1.1rem;
|
|
242
235
|
font-weight: 700;
|
|
243
236
|
text-align: center;
|
|
@@ -249,8 +242,8 @@
|
|
|
249
242
|
/* Subclass Warning Display */
|
|
250
243
|
.subclassWarning {
|
|
251
244
|
position: absolute;
|
|
252
|
-
bottom: 1rem;
|
|
253
|
-
right: 2rem;
|
|
245
|
+
bottom: 1rem;
|
|
246
|
+
right: 2rem;
|
|
254
247
|
z-index: 20;
|
|
255
248
|
pointer-events: none;
|
|
256
249
|
}
|
|
@@ -263,7 +256,7 @@
|
|
|
263
256
|
backdrop-filter: blur(6px);
|
|
264
257
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
265
258
|
box-shadow: 0 4px 16px rgba(220, 53, 69, 0.4);
|
|
266
|
-
font-family:
|
|
259
|
+
font-family: "Inter", sans-serif;
|
|
267
260
|
font-size: 1rem;
|
|
268
261
|
font-weight: 700;
|
|
269
262
|
text-align: center;
|
|
@@ -282,33 +275,50 @@
|
|
|
282
275
|
/* Image and Notes Container */
|
|
283
276
|
.imageAndNotesContainer {
|
|
284
277
|
display: flex;
|
|
285
|
-
flex-direction:
|
|
286
|
-
align-items:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
max-height: 100%;
|
|
278
|
+
flex-direction: row;
|
|
279
|
+
align-items: stretch;
|
|
280
|
+
align-self: stretch;
|
|
281
|
+
width: 100%;
|
|
290
282
|
}
|
|
291
283
|
|
|
292
|
-
/*
|
|
293
|
-
.
|
|
284
|
+
/* Notes Panel - fixed-width right-side column */
|
|
285
|
+
.notesPanel {
|
|
286
|
+
width: 260px;
|
|
287
|
+
flex-shrink: 0;
|
|
288
|
+
/* Opt out of stretch so the explicit height takes effect, stopping the panel
|
|
289
|
+
before the Subclass badge (bottom:1rem, ~2.5rem tall = 3.5rem from canvas bottom).
|
|
290
|
+
calc(100% - 4rem) lands ~4rem from the canvas bottom, clearing the badge. */
|
|
291
|
+
align-self: flex-start;
|
|
292
|
+
height: calc(100% - 4rem);
|
|
294
293
|
display: flex;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
flex-direction: column;
|
|
295
|
+
background: rgba(20, 20, 20, 0.55);
|
|
296
|
+
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
|
297
|
+
overflow: hidden;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.notesPanelHeader {
|
|
301
|
+
padding: 0.625rem 1rem;
|
|
302
|
+
color: #adb5bd;
|
|
303
|
+
font-size: 0.75rem;
|
|
304
|
+
font-weight: 600;
|
|
305
|
+
text-transform: uppercase;
|
|
306
|
+
letter-spacing: 0.5px;
|
|
307
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
308
|
+
flex-shrink: 0;
|
|
309
|
+
font-family: "Inter", sans-serif;
|
|
298
310
|
}
|
|
299
311
|
|
|
300
312
|
.additionalNotesBox {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
font-
|
|
308
|
-
|
|
309
|
-
line-height: 1.4;
|
|
313
|
+
flex: 1;
|
|
314
|
+
overflow-y: auto;
|
|
315
|
+
background: transparent;
|
|
316
|
+
color: #e0e0e0;
|
|
317
|
+
padding: 1rem;
|
|
318
|
+
font-family: "Inter", sans-serif;
|
|
319
|
+
font-size: 0.875rem;
|
|
320
|
+
line-height: 1.5;
|
|
310
321
|
text-align: left;
|
|
311
322
|
white-space: pre-wrap;
|
|
312
323
|
word-wrap: break-word;
|
|
313
|
-
|
|
314
|
-
}
|
|
324
|
+
}
|
|
@@ -19,6 +19,7 @@ interface CanvasProps {
|
|
|
19
19
|
isBoxAnnotationMode?: boolean;
|
|
20
20
|
boxAnnotationColor?: string;
|
|
21
21
|
isReadOnly?: boolean;
|
|
22
|
+
isArchivedCase?: boolean;
|
|
22
23
|
// Confirmation data for storing case-level confirmations
|
|
23
24
|
caseNumber: string; // Required for audit logging
|
|
24
25
|
currentImageId?: string;
|
|
@@ -32,7 +33,7 @@ type ImageLoadError = {
|
|
|
32
33
|
export const Canvas = ({
|
|
33
34
|
imageUrl,
|
|
34
35
|
filename,
|
|
35
|
-
company,
|
|
36
|
+
company,
|
|
36
37
|
badgeId,
|
|
37
38
|
firstName,
|
|
38
39
|
error,
|
|
@@ -42,6 +43,7 @@ export const Canvas = ({
|
|
|
42
43
|
isBoxAnnotationMode = false,
|
|
43
44
|
boxAnnotationColor = '#FF0000',
|
|
44
45
|
isReadOnly = false,
|
|
46
|
+
isArchivedCase = false,
|
|
45
47
|
caseNumber,
|
|
46
48
|
currentImageId
|
|
47
49
|
}: CanvasProps) => {
|
|
@@ -276,7 +278,7 @@ export const Canvas = ({
|
|
|
276
278
|
<div className={styles.confirmationIncluded}>
|
|
277
279
|
{isReadOnly ? 'Confirmation Requested' : 'Confirmation Field Included'}
|
|
278
280
|
</div>
|
|
279
|
-
{isReadOnly && (
|
|
281
|
+
{isReadOnly && !isArchivedCase && (
|
|
280
282
|
<button
|
|
281
283
|
className={styles.confirmButton}
|
|
282
284
|
onClick={() => setIsConfirmationModalOpen(true)}
|
|
@@ -291,13 +293,6 @@ export const Canvas = ({
|
|
|
291
293
|
</div>
|
|
292
294
|
)}
|
|
293
295
|
|
|
294
|
-
{/* Company Display - Upper Right */}
|
|
295
|
-
{company && (
|
|
296
|
-
<div className={styles.companyDisplay}>
|
|
297
|
-
{isReadOnly ? 'CASE REVIEW ONLY' : company}
|
|
298
|
-
</div>
|
|
299
|
-
)}
|
|
300
|
-
|
|
301
296
|
{(loadError || error) ? (
|
|
302
297
|
<p className={styles.error}>{getErrorMessage()}</p>
|
|
303
298
|
) : isLoading ? (
|
|
@@ -305,6 +300,7 @@ export const Canvas = ({
|
|
|
305
300
|
) : imageUrl && imageUrl !== '/clear.jpg' ? (
|
|
306
301
|
<div className={styles.imageAndNotesContainer}>
|
|
307
302
|
<div className={styles.imageContainer}>
|
|
303
|
+
<div className={styles.imageWrapper}>
|
|
308
304
|
{/* Class Characteristics - Above Image */}
|
|
309
305
|
{activeAnnotations?.has('class') && annotationData && (annotationData.customClass || annotationData.classType) && (
|
|
310
306
|
<div className={styles.classCharacteristics}>
|
|
@@ -333,7 +329,7 @@ export const Canvas = ({
|
|
|
333
329
|
draggable={false}
|
|
334
330
|
/>
|
|
335
331
|
|
|
336
|
-
{/* Box Annotations Component -
|
|
332
|
+
{/* Box Annotations Component - contained within imageWrapper */}
|
|
337
333
|
{activeAnnotations?.has('box') && (
|
|
338
334
|
<BoxAnnotations
|
|
339
335
|
imageRef={imageRef}
|
|
@@ -350,7 +346,7 @@ export const Canvas = ({
|
|
|
350
346
|
/>
|
|
351
347
|
)}
|
|
352
348
|
|
|
353
|
-
{/* Annotations Overlay */}
|
|
349
|
+
{/* Annotations Overlay - contained within imageWrapper */}
|
|
354
350
|
{activeAnnotations?.has('number') && annotationData && (
|
|
355
351
|
<div className={styles.annotationsOverlay}>
|
|
356
352
|
{/* Left side case and item numbers */}
|
|
@@ -385,7 +381,7 @@ export const Canvas = ({
|
|
|
385
381
|
</div>
|
|
386
382
|
)}
|
|
387
383
|
|
|
388
|
-
{/* Index Number Overlay */}
|
|
384
|
+
{/* Index Number Overlay - contained within imageWrapper */}
|
|
389
385
|
{activeAnnotations?.has('index') && annotationData?.indexType === 'number' && annotationData?.indexNumber && (
|
|
390
386
|
<div className={styles.annotationsOverlay}>
|
|
391
387
|
<div
|
|
@@ -402,15 +398,17 @@ export const Canvas = ({
|
|
|
402
398
|
</div>
|
|
403
399
|
</div>
|
|
404
400
|
)}
|
|
405
|
-
|
|
401
|
+
</div>{/* end imageWrapper */}
|
|
402
|
+
</div>{/* end imageContainer */}
|
|
406
403
|
|
|
407
|
-
{/* Additional Notes -
|
|
404
|
+
{/* Additional Notes - Right Panel */}
|
|
408
405
|
{activeAnnotations?.has('notes') && annotationData?.additionalNotes && (
|
|
409
|
-
<
|
|
406
|
+
<aside className={styles.notesPanel} aria-label="Additional notes">
|
|
407
|
+
<div className={styles.notesPanelHeader}>Notes</div>
|
|
410
408
|
<div className={styles.additionalNotesBox}>
|
|
411
409
|
{annotationData.additionalNotes}
|
|
412
410
|
</div>
|
|
413
|
-
</
|
|
411
|
+
</aside>
|
|
414
412
|
)}
|
|
415
413
|
</div>
|
|
416
414
|
) : (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useContext } from 'react';
|
|
1
|
+
import { useState, useEffect, useContext, useRef } from 'react';
|
|
2
2
|
import { type ConfirmationData } from '~/types/annotations';
|
|
3
3
|
import { AuthContext } from '~/contexts/auth.context';
|
|
4
4
|
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
@@ -33,6 +33,7 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
33
33
|
const [badgeId, setBadgeId] = useState('');
|
|
34
34
|
const [error, setError] = useState('');
|
|
35
35
|
const [isConfirming, setIsConfirming] = useState(false);
|
|
36
|
+
const wasOpenRef = useRef(false);
|
|
36
37
|
|
|
37
38
|
const fullName = user?.displayName || user?.email || 'Unknown User';
|
|
38
39
|
const userEmail = user?.email || 'No email available';
|
|
@@ -41,8 +42,9 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
41
42
|
const confirmationId = generateConfirmationId();
|
|
42
43
|
|
|
43
44
|
const {
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
requestClose,
|
|
46
|
+
overlayProps,
|
|
47
|
+
getCloseButtonProps
|
|
46
48
|
} = useOverlayDismiss({
|
|
47
49
|
isOpen,
|
|
48
50
|
onClose
|
|
@@ -53,7 +55,10 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
53
55
|
|
|
54
56
|
// Reset form when modal opens
|
|
55
57
|
useEffect(() => {
|
|
56
|
-
|
|
58
|
+
const justOpened = isOpen && !wasOpenRef.current;
|
|
59
|
+
wasOpenRef.current = isOpen;
|
|
60
|
+
|
|
61
|
+
if (justOpened) {
|
|
57
62
|
if (existingConfirmation) {
|
|
58
63
|
setBadgeId(existingConfirmation.badgeId);
|
|
59
64
|
} else {
|
|
@@ -100,22 +105,15 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
100
105
|
return (
|
|
101
106
|
<div
|
|
102
107
|
className={styles.overlay}
|
|
103
|
-
onMouseDown={handleOverlayMouseDown}
|
|
104
|
-
onKeyDown={handleOverlayKeyDown}
|
|
105
|
-
role="button"
|
|
106
|
-
tabIndex={0}
|
|
107
108
|
aria-label="Close confirmation dialog"
|
|
109
|
+
{...overlayProps}
|
|
108
110
|
>
|
|
109
111
|
<div className={styles.modal}>
|
|
110
112
|
<div className={styles.header}>
|
|
111
113
|
<h2 className={styles.title}>
|
|
112
114
|
{hasExistingConfirmation ? 'Confirmation Details' : 'Confirm Identification'}
|
|
113
115
|
</h2>
|
|
114
|
-
<button
|
|
115
|
-
className={styles.closeButton}
|
|
116
|
-
onClick={onClose}
|
|
117
|
-
aria-label="Close modal"
|
|
118
|
-
>
|
|
116
|
+
<button {...getCloseButtonProps({ ariaLabel: 'Close confirmation dialog' })}>
|
|
119
117
|
×
|
|
120
118
|
</button>
|
|
121
119
|
</div>
|
|
@@ -186,7 +184,7 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
186
184
|
<div className={styles.footer}>
|
|
187
185
|
<button
|
|
188
186
|
className={styles.cancelButton}
|
|
189
|
-
onClick={
|
|
187
|
+
onClick={requestClose}
|
|
190
188
|
disabled={isConfirming}
|
|
191
189
|
>
|
|
192
190
|
{hasExistingConfirmation ? 'Close' : 'Cancel'}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-direction: column;
|
|
4
4
|
gap: 0.75rem;
|
|
5
|
+
width: fit-content;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
.colorHeader {
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
.colorWheel {
|
|
29
|
-
width:
|
|
30
|
+
width: 180px;
|
|
30
31
|
height: 40px;
|
|
31
32
|
padding: 0;
|
|
32
33
|
border: 2px solid #ced4da;
|
|
@@ -55,5 +56,5 @@
|
|
|
55
56
|
|
|
56
57
|
.colorSwatch.selected {
|
|
57
58
|
border-color: #0d6efd;
|
|
58
|
-
box-shadow: 0 0 0 2px rgba(13,110,253
|
|
59
|
-
}
|
|
59
|
+
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
|
|
60
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
.overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
background: rgba(0, 0, 0, 0.45);
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
z-index: 120;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.modal {
|
|
12
|
+
position: relative;
|
|
13
|
+
width: min(560px, calc(100vw - 2rem));
|
|
14
|
+
background: #ffffff;
|
|
15
|
+
border-radius: 12px;
|
|
16
|
+
border: 1px solid #d9e0e7;
|
|
17
|
+
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.2);
|
|
18
|
+
padding: 1.1rem;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.title {
|
|
22
|
+
margin: 0;
|
|
23
|
+
color: #212529;
|
|
24
|
+
font-size: 1.02rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.subtitle {
|
|
28
|
+
margin: 0.4rem 0 0.9rem;
|
|
29
|
+
color: #6c757d;
|
|
30
|
+
font-size: 0.85rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.warningPanel {
|
|
34
|
+
border: 1px solid color-mix(in lab, #dc3545 25%, transparent);
|
|
35
|
+
background: color-mix(in lab, #dc3545 7%, #ffffff);
|
|
36
|
+
border-radius: 10px;
|
|
37
|
+
padding: 0.75rem;
|
|
38
|
+
margin-bottom: 0.8rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.warningPanel p {
|
|
42
|
+
margin: 0;
|
|
43
|
+
color: #3f2a2e;
|
|
44
|
+
font-size: 0.86rem;
|
|
45
|
+
line-height: 1.35;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.warningPanel p + p {
|
|
49
|
+
margin-top: 0.45rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.reasonLabel {
|
|
53
|
+
display: block;
|
|
54
|
+
margin-bottom: 0.35rem;
|
|
55
|
+
color: #495057;
|
|
56
|
+
font-size: 0.8rem;
|
|
57
|
+
font-weight: 600;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.reasonInput {
|
|
61
|
+
width: 100%;
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
border: 1px solid #cdd5dd;
|
|
64
|
+
border-radius: 8px;
|
|
65
|
+
padding: 0.6rem 0.75rem;
|
|
66
|
+
font-size: 0.9rem;
|
|
67
|
+
font-family: inherit;
|
|
68
|
+
resize: vertical;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.reasonInput:focus {
|
|
72
|
+
outline: none;
|
|
73
|
+
border-color: #1f6feb;
|
|
74
|
+
box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.actions {
|
|
78
|
+
display: flex;
|
|
79
|
+
justify-content: flex-end;
|
|
80
|
+
gap: 0.65rem;
|
|
81
|
+
margin-top: 1rem;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.cancelButton,
|
|
85
|
+
.confirmButton {
|
|
86
|
+
border: 1px solid transparent;
|
|
87
|
+
border-radius: 8px;
|
|
88
|
+
padding: 0.55rem 0.9rem;
|
|
89
|
+
font-size: 0.86rem;
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.cancelButton {
|
|
95
|
+
background: #f3f4f6;
|
|
96
|
+
color: #3c4651;
|
|
97
|
+
border-color: #d6dce2;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.confirmButton {
|
|
101
|
+
background: #dc3545;
|
|
102
|
+
color: #ffffff;
|
|
103
|
+
border-color: #c82333;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.cancelButton:disabled,
|
|
107
|
+
.confirmButton:disabled {
|
|
108
|
+
cursor: not-allowed;
|
|
109
|
+
opacity: 0.6;
|
|
110
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
3
|
+
import styles from './archive-case-modal.module.css';
|
|
4
|
+
|
|
5
|
+
interface ArchiveCaseModalProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
currentCase: string;
|
|
8
|
+
isSubmitting?: boolean;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
onSubmit: (archiveReason: string) => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ArchiveCaseModal = ({
|
|
14
|
+
isOpen,
|
|
15
|
+
currentCase,
|
|
16
|
+
isSubmitting = false,
|
|
17
|
+
onClose,
|
|
18
|
+
onSubmit,
|
|
19
|
+
}: ArchiveCaseModalProps) => {
|
|
20
|
+
const [archiveReason, setArchiveReason] = useState('');
|
|
21
|
+
const reasonRef = useRef<HTMLTextAreaElement>(null);
|
|
22
|
+
const isCloseBlocked = isSubmitting;
|
|
23
|
+
|
|
24
|
+
const handleClose = () => {
|
|
25
|
+
if (isSubmitting) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setArchiveReason('');
|
|
30
|
+
onClose();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
requestClose,
|
|
35
|
+
overlayProps,
|
|
36
|
+
getCloseButtonProps,
|
|
37
|
+
} = useOverlayDismiss({
|
|
38
|
+
isOpen,
|
|
39
|
+
onClose: handleClose,
|
|
40
|
+
canDismiss: !isCloseBlocked,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!isOpen) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const focusId = window.requestAnimationFrame(() => {
|
|
49
|
+
reasonRef.current?.focus();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
window.cancelAnimationFrame(focusId);
|
|
54
|
+
};
|
|
55
|
+
}, [isOpen]);
|
|
56
|
+
|
|
57
|
+
if (!isOpen) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleSubmit = async () => {
|
|
62
|
+
await onSubmit(archiveReason.trim());
|
|
63
|
+
setArchiveReason('');
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
className={styles.overlay}
|
|
69
|
+
aria-label="Close archive case dialog"
|
|
70
|
+
{...overlayProps}
|
|
71
|
+
>
|
|
72
|
+
<div className={styles.modal} role="dialog" aria-modal="true" aria-label="Archive Case">
|
|
73
|
+
<button {...getCloseButtonProps({ ariaLabel: 'Close archive case dialog' })}>
|
|
74
|
+
×
|
|
75
|
+
</button>
|
|
76
|
+
<h3 className={styles.title}>Archive Case</h3>
|
|
77
|
+
<p className={styles.subtitle}>Case: {currentCase}</p>
|
|
78
|
+
|
|
79
|
+
<div className={styles.warningPanel}>
|
|
80
|
+
<p>
|
|
81
|
+
Archiving a case permanently renders it read-only.
|
|
82
|
+
</p>
|
|
83
|
+
<p>
|
|
84
|
+
The archive will be in JSON format and include all images.
|
|
85
|
+
</p>
|
|
86
|
+
<p>
|
|
87
|
+
The full audit trail is packaged with Striae's current public key and forensic signatures.
|
|
88
|
+
</p>
|
|
89
|
+
<p>
|
|
90
|
+
You can import the archived package back into Striae for future review.
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<label htmlFor="archiveReason" className={styles.reasonLabel}>Archive reason (recommended)</label>
|
|
95
|
+
<textarea
|
|
96
|
+
id="archiveReason"
|
|
97
|
+
ref={reasonRef}
|
|
98
|
+
value={archiveReason}
|
|
99
|
+
onChange={(event) => setArchiveReason(event.target.value)}
|
|
100
|
+
className={styles.reasonInput}
|
|
101
|
+
placeholder="Optional chain-of-custody note"
|
|
102
|
+
disabled={isSubmitting}
|
|
103
|
+
rows={3}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<div className={styles.actions}>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
className={styles.cancelButton}
|
|
110
|
+
onClick={requestClose}
|
|
111
|
+
disabled={isCloseBlocked}
|
|
112
|
+
>
|
|
113
|
+
Cancel
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
className={styles.confirmButton}
|
|
118
|
+
onClick={() => {
|
|
119
|
+
void handleSubmit();
|
|
120
|
+
}}
|
|
121
|
+
disabled={isSubmitting}
|
|
122
|
+
>
|
|
123
|
+
{isSubmitting ? 'Archiving...' : 'Confirm Archive'}
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
};
|