@striae-org/striae 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- 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 +430 -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 +7 -0
- 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 +21 -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 +6 -12
- 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 +377 -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 +25 -519
- package/app/components/sidebar/cases/cases-modal.module.css +1 -0
- package/app/components/sidebar/cases/cases-modal.tsx +6 -8
- package/app/components/sidebar/cases/cases.module.css +62 -21
- package/app/components/sidebar/files/files-modal.module.css +1 -0
- package/app/components/sidebar/files/files-modal.tsx +12 -13
- package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
- package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
- package/app/components/sidebar/notes/notes-modal.tsx +7 -8
- package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
- package/app/components/sidebar/notes/notes.module.css +153 -0
- package/app/components/sidebar/sidebar-container.tsx +15 -28
- package/app/components/sidebar/sidebar.module.css +5 -69
- package/app/components/sidebar/sidebar.tsx +24 -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/hooks/useOverlayDismiss.ts +52 -4
- package/app/routes/auth/login.tsx +785 -774
- package/app/routes/striae/striae.module.css +10 -3
- package/app/routes/striae/striae.tsx +469 -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 +3 -1
- package/app/types/case.ts +29 -0
- package/app/types/import.ts +3 -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 +497 -22
- package/package.json +3 -3
- package/scripts/deploy-primershear-emails.sh +2 -1
- package/worker-configuration.d.ts +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/worker-configuration.d.ts +7448 -11323
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +7447 -11322
- package/workers/image-worker/wrangler.jsonc.example +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/src/formats/format-striae.ts +8 -7
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- 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/public/.well-known/keybase.txt +0 -56
|
@@ -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
|
) : (
|
|
@@ -41,8 +41,9 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
41
41
|
const confirmationId = generateConfirmationId();
|
|
42
42
|
|
|
43
43
|
const {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
requestClose,
|
|
45
|
+
overlayProps,
|
|
46
|
+
getCloseButtonProps
|
|
46
47
|
} = useOverlayDismiss({
|
|
47
48
|
isOpen,
|
|
48
49
|
onClose
|
|
@@ -100,22 +101,15 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
100
101
|
return (
|
|
101
102
|
<div
|
|
102
103
|
className={styles.overlay}
|
|
103
|
-
onMouseDown={handleOverlayMouseDown}
|
|
104
|
-
onKeyDown={handleOverlayKeyDown}
|
|
105
|
-
role="button"
|
|
106
|
-
tabIndex={0}
|
|
107
104
|
aria-label="Close confirmation dialog"
|
|
105
|
+
{...overlayProps}
|
|
108
106
|
>
|
|
109
107
|
<div className={styles.modal}>
|
|
110
108
|
<div className={styles.header}>
|
|
111
109
|
<h2 className={styles.title}>
|
|
112
110
|
{hasExistingConfirmation ? 'Confirmation Details' : 'Confirm Identification'}
|
|
113
111
|
</h2>
|
|
114
|
-
<button
|
|
115
|
-
className={styles.closeButton}
|
|
116
|
-
onClick={onClose}
|
|
117
|
-
aria-label="Close modal"
|
|
118
|
-
>
|
|
112
|
+
<button {...getCloseButtonProps({ ariaLabel: 'Close confirmation dialog' })}>
|
|
119
113
|
×
|
|
120
114
|
</button>
|
|
121
115
|
</div>
|
|
@@ -186,7 +180,7 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
186
180
|
<div className={styles.footer}>
|
|
187
181
|
<button
|
|
188
182
|
className={styles.cancelButton}
|
|
189
|
-
onClick={
|
|
183
|
+
onClick={requestClose}
|
|
190
184
|
disabled={isConfirming}
|
|
191
185
|
>
|
|
192
186
|
{hasExistingConfirmation ? 'Close' : 'Cancel'}
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
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(460px, 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
|
+
.helperText {
|
|
28
|
+
margin: 0.4rem 0 0.9rem;
|
|
29
|
+
color: #6c757d;
|
|
30
|
+
font-size: 0.85rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.input {
|
|
34
|
+
width: 100%;
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
border: 1px solid #cdd5dd;
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
padding: 0.6rem 0.75rem;
|
|
39
|
+
font-size: 0.92rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.input:focus {
|
|
43
|
+
outline: none;
|
|
44
|
+
border-color: #1f6feb;
|
|
45
|
+
box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.actions {
|
|
49
|
+
display: flex;
|
|
50
|
+
justify-content: flex-end;
|
|
51
|
+
gap: 0.65rem;
|
|
52
|
+
margin-top: 1rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.cancelButton,
|
|
56
|
+
.confirmButton {
|
|
57
|
+
border: 1px solid transparent;
|
|
58
|
+
border-radius: 8px;
|
|
59
|
+
padding: 0.55rem 0.9rem;
|
|
60
|
+
font-size: 0.86rem;
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.cancelButton {
|
|
66
|
+
background: #f3f4f6;
|
|
67
|
+
color: #3c4651;
|
|
68
|
+
border-color: #d6dce2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.confirmButton {
|
|
72
|
+
background: #198754;
|
|
73
|
+
color: #ffffff;
|
|
74
|
+
border-color: #157347;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.cancelButton:disabled,
|
|
78
|
+
.confirmButton:disabled {
|
|
79
|
+
cursor: not-allowed;
|
|
80
|
+
opacity: 0.6;
|
|
81
|
+
}
|