@striae-org/striae 4.2.1 → 4.3.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 (66) hide show
  1. package/app/components/actions/case-import/confirmation-import.ts +20 -1
  2. package/app/components/actions/case-import/orchestrator.ts +3 -0
  3. package/app/components/actions/case-manage.ts +5 -1
  4. package/app/components/actions/confirm-export.ts +12 -3
  5. package/app/components/audit/viewer/audit-entries-list.tsx +20 -2
  6. package/app/components/audit/viewer/use-audit-viewer-export.ts +2 -2
  7. package/app/components/audit/viewer/use-audit-viewer-filters.ts +11 -1
  8. package/app/components/canvas/canvas.tsx +2 -1
  9. package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -76
  10. package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
  11. package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
  12. package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
  13. package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
  14. package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
  15. package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
  16. package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
  17. package/app/components/navbar/navbar.module.css +11 -0
  18. package/app/components/navbar/navbar.tsx +38 -19
  19. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -0
  20. package/app/components/sidebar/cases/case-sidebar.tsx +27 -3
  21. package/app/components/sidebar/cases/cases-modal.module.css +312 -10
  22. package/app/components/sidebar/cases/cases-modal.tsx +690 -110
  23. package/app/components/sidebar/cases/cases.module.css +23 -0
  24. package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
  25. package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
  26. package/app/components/sidebar/files/files-modal.module.css +285 -44
  27. package/app/components/sidebar/files/files-modal.tsx +452 -145
  28. package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
  29. package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
  30. package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
  31. package/app/components/sidebar/notes/class-details-shared.ts +239 -0
  32. package/app/components/sidebar/notes/notes-editor-form.tsx +43 -5
  33. package/app/components/sidebar/notes/notes.module.css +236 -4
  34. package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
  35. package/app/components/sidebar/sidebar-container.tsx +2 -0
  36. package/app/components/sidebar/sidebar.tsx +8 -1
  37. package/app/hooks/useCaseListPreferences.ts +99 -0
  38. package/app/hooks/useFileListPreferences.ts +106 -0
  39. package/app/routes/striae/striae.tsx +45 -1
  40. package/app/services/audit/audit-export-csv.ts +4 -2
  41. package/app/services/audit/audit-export-report.ts +36 -4
  42. package/app/services/audit/audit.service.ts +2 -0
  43. package/app/services/audit/builders/audit-entry-builder.ts +1 -0
  44. package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -2
  45. package/app/types/annotations.ts +48 -1
  46. package/app/types/audit.ts +1 -0
  47. package/app/utils/data/case-filters.ts +127 -0
  48. package/app/utils/data/confirmation-summary/summary-core.ts +18 -2
  49. package/app/utils/data/file-filters.ts +201 -0
  50. package/app/utils/forensics/confirmation-signature.ts +20 -5
  51. package/functions/api/image/[[path]].ts +4 -0
  52. package/package.json +3 -4
  53. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  54. package/workers/data-worker/src/signing-payload-utils.ts +5 -0
  55. package/workers/data-worker/wrangler.jsonc.example +1 -1
  56. package/workers/image-worker/wrangler.jsonc.example +1 -1
  57. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  58. package/workers/pdf-worker/src/formats/format-striae.ts +84 -118
  59. package/workers/pdf-worker/src/pdf-worker.example.ts +28 -10
  60. package/workers/pdf-worker/src/report-layout.ts +227 -0
  61. package/workers/pdf-worker/src/report-types.ts +20 -0
  62. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  63. package/workers/user-worker/wrangler.jsonc.example +1 -1
  64. package/wrangler.toml.example +1 -1
  65. package/workers/pdf-worker/src/assets/icon-256.png +0 -0
  66. /package/workers/pdf-worker/src/assets/{generated-assets.ts → generated-assets.example.ts} +0 -0
@@ -1,6 +1,7 @@
1
1
  import type { User } from 'firebase/auth';
2
2
  import { fetchDataApi } from '~/utils/api';
3
- import { type ConfirmationImportResult, type ConfirmationImportData } from '~/types';
3
+ import { upsertFileConfirmationSummary } from '~/utils/data';
4
+ import { type AnnotationData, type ConfirmationImportResult, type ConfirmationImportData } from '~/types';
4
5
  import { checkExistingCase } from '../case-manage';
5
6
  import { extractConfirmationImportPackage } from './confirmation-package';
6
7
  import { validateExporterUid, validateConfirmationHash, validateConfirmationSignatureFile } from './validation';
@@ -37,6 +38,7 @@ export async function importConfirmationData(
37
38
  let signatureKeyId: string | undefined;
38
39
  let confirmationDataForAudit: ConfirmationImportData | null = null;
39
40
  let confirmationJsonFileNameForAudit = confirmationFile.name;
41
+ const confirmedFileNames = new Set<string>();
40
42
 
41
43
  const result: ConfirmationImportResult = {
42
44
  success: false,
@@ -234,6 +236,21 @@ export async function importConfirmationData(
234
236
  if (saveResponse.ok) {
235
237
  result.imagesUpdated++;
236
238
  result.confirmationsImported += confirmations.length;
239
+ confirmedFileNames.add(displayFilename);
240
+
241
+ try {
242
+ await upsertFileConfirmationSummary(
243
+ user,
244
+ result.caseNumber,
245
+ currentImageId,
246
+ updatedAnnotationData as AnnotationData
247
+ );
248
+ } catch (summaryError) {
249
+ console.warn(
250
+ `Failed to update confirmation summary for imported confirmation ${result.caseNumber}/${currentImageId}:`,
251
+ summaryError
252
+ );
253
+ }
237
254
 
238
255
  // Audit log successful confirmation import
239
256
  try {
@@ -298,6 +315,7 @@ export async function importConfirmationData(
298
315
  result.success ? (result.errors && result.errors.length > 0 ? 'warning' : 'success') : 'failure',
299
316
  hashValid,
300
317
  result.confirmationsImported, // Successfully imported confirmations
318
+ Array.from(confirmedFileNames).sort((left, right) => left.localeCompare(right)),
301
319
  result.errors || [],
302
320
  confirmationData.metadata.exportedByUid,
303
321
  {
@@ -390,6 +408,7 @@ export async function importConfirmationData(
390
408
  'failure',
391
409
  hashValidForAudit,
392
410
  0, // No confirmations successfully imported for failures
411
+ [],
393
412
  result.errors || [],
394
413
  reviewingExaminerUidForAudit,
395
414
  {
@@ -286,6 +286,9 @@ export async function importCaseForReview(
286
286
  if (existingRegularCase && !isArchivedExport) {
287
287
  throw new Error(`Case "${result.caseNumber}" already exists in your case list. You cannot import a case for review if you were the original analyst.`);
288
288
  }
289
+ if (existingRegularCase && isArchivedExport) {
290
+ throw new Error(`Cannot import this archive because case "${result.caseNumber}" already exists in your case list (active or archived). To import this archive, the existing case must first be deleted.`);
291
+ }
289
292
 
290
293
  // Step 2b: Check if read-only case already exists
291
294
  const existingCase = await checkReadOnlyCaseExists(user, result.caseNumber);
@@ -807,7 +807,11 @@ export const archiveCase = async (
807
807
  )
808
808
  );
809
809
 
810
- const auditEntries = await auditService.getAuditEntriesForUser(user.uid, { caseNumber });
810
+ const auditEntries = await auditService.getAuditEntriesForUser(user.uid, {
811
+ caseNumber,
812
+ startDate: caseData.createdAt,
813
+ endDate: archivedAt,
814
+ });
811
815
  const auditTrail: AuditTrail = {
812
816
  caseNumber,
813
817
  workflowId: `${caseNumber}-archive-${Date.now()}`,
@@ -5,8 +5,8 @@ import {
5
5
  getCurrentPublicSigningKeyDetails,
6
6
  getVerificationPublicKey
7
7
  } from '~/utils/forensics';
8
- import { getUserData, getCaseData, updateCaseData, signConfirmationData } from '~/utils/data';
9
- import { type ConfirmationData, type CaseConfirmations, type CaseDataWithConfirmations, type ConfirmationImportData } from '~/types';
8
+ import { getUserData, getCaseData, updateCaseData, signConfirmationData, upsertFileConfirmationSummary } from '~/utils/data';
9
+ import { type AnnotationData, type ConfirmationData, type CaseConfirmations, type CaseDataWithConfirmations, type ConfirmationImportData } from '~/types';
10
10
  import { auditService } from '~/services/audit';
11
11
 
12
12
  /**
@@ -17,7 +17,8 @@ export async function storeConfirmation(
17
17
  caseNumber: string,
18
18
  currentImageId: string,
19
19
  confirmationData: ConfirmationData,
20
- originalImageFileName?: string
20
+ originalImageFileName?: string,
21
+ annotationDataForSummary?: AnnotationData
21
22
  ): Promise<boolean> {
22
23
  const startTime = Date.now();
23
24
  let originalImageId: string | undefined; // Declare at function level for error handling
@@ -63,6 +64,14 @@ export async function storeConfirmation(
63
64
  // Store the updated case data using centralized function
64
65
  await updateCaseData(user, caseNumber, caseData);
65
66
 
67
+ if (annotationDataForSummary) {
68
+ try {
69
+ await upsertFileConfirmationSummary(user, caseNumber, currentImageId, annotationDataForSummary);
70
+ } catch (summaryError) {
71
+ console.warn(`Failed to update confirmation summary for ${caseNumber}/${currentImageId}:`, summaryError);
72
+ }
73
+ }
74
+
66
75
  console.log(`Confirmation stored for original image ${originalImageId}:`, confirmationData);
67
76
 
68
77
  // Log successful confirmation creation
@@ -6,6 +6,13 @@ interface AuditEntriesListProps {
6
6
  entries: ValidationAuditEntry[];
7
7
  }
8
8
 
9
+ const isConfirmationImportEntry = (entry: ValidationAuditEntry): boolean => {
10
+ return (
11
+ entry.action === 'confirmation-import' ||
12
+ (entry.action === 'import' && entry.details.workflowPhase === 'confirmation')
13
+ );
14
+ };
15
+
9
16
  export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
10
17
  return (
11
18
  <div className={styles.entriesList}>
@@ -47,13 +54,24 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
47
54
  </div>
48
55
  )}
49
56
 
50
- {entry.action === 'confirmation-import' && entry.details.reviewerBadgeId && (
57
+ {isConfirmationImportEntry(entry) && entry.details.reviewerBadgeId && (
51
58
  <div className={styles.detailRow}>
52
- <span className={styles.detailLabel}>Reviewer Badge/ID:</span>
59
+ <span className={styles.detailLabel}>Confirming Examiner Badge/ID:</span>
53
60
  <span className={styles.badgeTag}>{entry.details.reviewerBadgeId}</span>
54
61
  </div>
55
62
  )}
56
63
 
64
+ {isConfirmationImportEntry(entry) &&
65
+ entry.details.caseDetails?.confirmedFileNames &&
66
+ entry.details.caseDetails.confirmedFileNames.length > 0 && (
67
+ <div className={styles.detailRow}>
68
+ <span className={styles.detailLabel}>Confirmed Files:</span>
69
+ <span className={styles.detailValue}>
70
+ {entry.details.caseDetails.confirmedFileNames.join(', ')}
71
+ </span>
72
+ </div>
73
+ )}
74
+
57
75
  {entry.result === 'failure' && entry.details.validationErrors.length > 0 && (
58
76
  <div className={styles.detailRow}>
59
77
  <span className={styles.detailLabel}>Error:</span>
@@ -48,7 +48,7 @@ export const useAuditViewerExport = ({
48
48
  const filename = auditExportService.generateFilename(
49
49
  exportContextData.scopeType,
50
50
  exportContextData.identifier,
51
- 'json'
51
+ 'csv'
52
52
  );
53
53
 
54
54
  try {
@@ -72,7 +72,7 @@ export const useAuditViewerExport = ({
72
72
  const filename = auditExportService.generateFilename(
73
73
  exportContextData.scopeType,
74
74
  exportContextData.identifier,
75
- 'csv'
75
+ 'json'
76
76
  );
77
77
 
78
78
  try {
@@ -2,6 +2,10 @@ import { useCallback, useMemo, useState } from 'react';
2
2
  import type { AuditAction, AuditResult, ValidationAuditEntry } from '~/types';
3
3
  import type { DateRangeFilter } from './types';
4
4
 
5
+ const isConfirmationImportEntry = (entry: ValidationAuditEntry): boolean => {
6
+ return entry.action === 'import' && entry.details.workflowPhase === 'confirmation';
7
+ };
8
+
5
9
  export const useAuditViewerFilters = (caseNumber?: string) => {
6
10
  const [filterAction, setFilterAction] = useState<AuditAction | 'all'>('all');
7
11
  const [filterResult, setFilterResult] = useState<AuditResult | 'all'>('all');
@@ -76,7 +80,13 @@ export const useAuditViewerFilters = (caseNumber?: string) => {
76
80
 
77
81
  const resultMatch = filterResult === 'all' || entry.result === filterResult;
78
82
  const entryBadgeId = entry.details.userProfileDetails?.badgeId?.trim().toLowerCase() || '';
79
- const badgeMatch = normalizedBadgeFilter === '' || entryBadgeId.includes(normalizedBadgeFilter);
83
+ const reviewerBadgeId = isConfirmationImportEntry(entry)
84
+ ? entry.details.reviewerBadgeId?.trim().toLowerCase() || ''
85
+ : '';
86
+ const badgeMatch =
87
+ normalizedBadgeFilter === '' ||
88
+ entryBadgeId.includes(normalizedBadgeFilter) ||
89
+ reviewerBadgeId.includes(normalizedBadgeFilter);
80
90
 
81
91
  return actionMatch && resultMatch && badgeMatch;
82
92
  });
@@ -107,7 +107,8 @@ export const Canvas = ({
107
107
  caseNumber,
108
108
  currentImageId,
109
109
  confirmationData,
110
- filename
110
+ filename,
111
+ updatedAnnotationData
111
112
  );
112
113
 
113
114
  if (success) {
@@ -1,52 +1,5 @@
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
1
  .modal {
12
- position: relative;
13
2
  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
3
  }
51
4
 
52
5
  .reasonLabel {
@@ -74,37 +27,8 @@
74
27
  box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2);
75
28
  }
76
29
 
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
30
  .confirmButton {
101
31
  background: #dc3545;
102
32
  color: #ffffff;
103
33
  border-color: #c82333;
104
34
  }
105
-
106
- .cancelButton:disabled,
107
- .confirmButton:disabled {
108
- cursor: not-allowed;
109
- opacity: 0.6;
110
- }
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
+ import sharedStyles from './case-modal-shared.module.css';
3
4
  import styles from './archive-case-modal.module.css';
4
5
 
5
6
  interface ArchiveCaseModalProps {
@@ -65,18 +66,18 @@ export const ArchiveCaseModal = ({
65
66
 
66
67
  return (
67
68
  <div
68
- className={styles.overlay}
69
+ className={sharedStyles.overlay}
69
70
  aria-label="Close archive case dialog"
70
71
  {...overlayProps}
71
72
  >
72
- <div className={styles.modal} role="dialog" aria-modal="true" aria-label="Archive Case">
73
+ <div className={`${sharedStyles.modal} ${styles.modal}`} role="dialog" aria-modal="true" aria-label="Archive Case">
73
74
  <button {...getCloseButtonProps({ ariaLabel: 'Close archive case dialog' })}>
74
75
  ×
75
76
  </button>
76
- <h3 className={styles.title}>Archive Case</h3>
77
- <p className={styles.subtitle}>Case: {currentCase}</p>
77
+ <h3 className={sharedStyles.title}>Archive Case</h3>
78
+ <p className={sharedStyles.subtitle}>Case: {currentCase}</p>
78
79
 
79
- <div className={styles.warningPanel}>
80
+ <div className={sharedStyles.warningPanel}>
80
81
  <p>
81
82
  Archiving a case permanently renders it read-only.
82
83
  </p>
@@ -103,10 +104,10 @@ export const ArchiveCaseModal = ({
103
104
  rows={3}
104
105
  />
105
106
 
106
- <div className={styles.actions}>
107
+ <div className={sharedStyles.actions}>
107
108
  <button
108
109
  type="button"
109
- className={styles.cancelButton}
110
+ className={sharedStyles.cancelButton}
110
111
  onClick={requestClose}
111
112
  disabled={isCloseBlocked}
112
113
  >
@@ -114,7 +115,7 @@ export const ArchiveCaseModal = ({
114
115
  </button>
115
116
  <button
116
117
  type="button"
117
- className={styles.confirmButton}
118
+ className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
118
119
  onClick={() => {
119
120
  void handleSubmit();
120
121
  }}
@@ -0,0 +1,94 @@
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
+ background: #ffffff;
14
+ border-radius: var(--spaceXS);
15
+ overflow: hidden;
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
+ .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
+ .warningPanel {
49
+ border: 1px solid color-mix(in lab, #dc3545 25%, transparent);
50
+ background: color-mix(in lab, #dc3545 7%, #ffffff);
51
+ border-radius: 10px;
52
+ padding: 0.75rem;
53
+ margin-bottom: 0.8rem;
54
+ }
55
+
56
+ .warningPanel p {
57
+ margin: 0;
58
+ color: #3f2a2e;
59
+ font-size: 0.86rem;
60
+ line-height: 1.35;
61
+ }
62
+
63
+ .warningPanel p + p {
64
+ margin-top: 0.45rem;
65
+ }
66
+
67
+ .actions {
68
+ display: flex;
69
+ justify-content: flex-end;
70
+ gap: 0.65rem;
71
+ margin-top: 1rem;
72
+ }
73
+
74
+ .cancelButton,
75
+ .confirmButton {
76
+ border: 1px solid transparent;
77
+ border-radius: 8px;
78
+ padding: 0.55rem 0.9rem;
79
+ font-size: 0.86rem;
80
+ font-weight: 500;
81
+ cursor: pointer;
82
+ }
83
+
84
+ .cancelButton {
85
+ background: #f3f4f6;
86
+ color: #3c4651;
87
+ border-color: #d6dce2;
88
+ }
89
+
90
+ .cancelButton:disabled,
91
+ .confirmButton:disabled {
92
+ cursor: not-allowed;
93
+ opacity: 0.6;
94
+ }
@@ -0,0 +1,9 @@
1
+ .modal {
2
+ width: min(560px, calc(100vw - 2rem));
3
+ }
4
+
5
+ .confirmButton {
6
+ background: #dc3545;
7
+ color: #ffffff;
8
+ border-color: #c82333;
9
+ }
@@ -0,0 +1,79 @@
1
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
2
+ import sharedStyles from './case-modal-shared.module.css';
3
+ import styles from './delete-case-modal.module.css';
4
+
5
+ interface DeleteCaseModalProps {
6
+ isOpen: boolean;
7
+ currentCase: string;
8
+ isSubmitting?: boolean;
9
+ onClose: () => void;
10
+ onSubmit: () => Promise<void>;
11
+ }
12
+
13
+ export const DeleteCaseModal = ({
14
+ isOpen,
15
+ currentCase,
16
+ isSubmitting = false,
17
+ onClose,
18
+ onSubmit,
19
+ }: DeleteCaseModalProps) => {
20
+ const isCloseBlocked = isSubmitting;
21
+
22
+ const {
23
+ requestClose,
24
+ overlayProps,
25
+ getCloseButtonProps,
26
+ } = useOverlayDismiss({
27
+ isOpen,
28
+ onClose,
29
+ canDismiss: !isCloseBlocked,
30
+ });
31
+
32
+ if (!isOpen) {
33
+ return null;
34
+ }
35
+
36
+ return (
37
+ <div
38
+ className={sharedStyles.overlay}
39
+ aria-label="Close delete case dialog"
40
+ {...overlayProps}
41
+ >
42
+ <div className={`${sharedStyles.modal} ${styles.modal}`} role="dialog" aria-modal="true" aria-label="Delete Case">
43
+ <button {...getCloseButtonProps({ ariaLabel: 'Close delete case dialog' })}>
44
+ ×
45
+ </button>
46
+
47
+ <h3 className={sharedStyles.title}>Delete Case</h3>
48
+ <p className={sharedStyles.subtitle}>Case: {currentCase}</p>
49
+
50
+ <div className={sharedStyles.warningPanel}>
51
+ <p>This action permanently deletes the case and all associated files.</p>
52
+ <p>This operation cannot be undone.</p>
53
+ <p>Any image assets that are already missing will be skipped automatically.</p>
54
+ </div>
55
+
56
+ <div className={sharedStyles.actions}>
57
+ <button
58
+ type="button"
59
+ className={sharedStyles.cancelButton}
60
+ onClick={requestClose}
61
+ disabled={isCloseBlocked}
62
+ >
63
+ Cancel
64
+ </button>
65
+ <button
66
+ type="button"
67
+ className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
68
+ onClick={() => {
69
+ void onSubmit();
70
+ }}
71
+ disabled={isSubmitting}
72
+ >
73
+ {isSubmitting ? 'Deleting...' : 'Confirm Delete'}
74
+ </button>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ );
79
+ };
@@ -12,7 +12,8 @@
12
12
  position: relative;
13
13
  width: min(460px, calc(100vw - 2rem));
14
14
  background: #ffffff;
15
- border-radius: 12px;
15
+ border-radius: var(--spaceXS);
16
+ overflow: hidden;
16
17
  border: 1px solid #d9e0e7;
17
18
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.2);
18
19
  padding: 1.1rem;
@@ -1,71 +1,5 @@
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
1
  .modal {
12
- position: relative;
13
2
  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
- .subtitle {
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
3
  }
70
4
 
71
5
  .confirmButton {
@@ -73,9 +7,3 @@
73
7
  color: #3f2f00;
74
8
  border-color: #e8b103;
75
9
  }
76
-
77
- .cancelButton:disabled,
78
- .confirmButton:disabled {
79
- cursor: not-allowed;
80
- opacity: 0.6;
81
- }
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
+ import sharedStyles from './case-modal-shared.module.css';
3
4
  import styles from './rename-case-modal.module.css';
4
5
 
5
6
  interface RenameCaseModalProps {
@@ -59,22 +60,22 @@ export const RenameCaseModal = ({
59
60
 
60
61
  return (
61
62
  <div
62
- className={styles.overlay}
63
+ className={sharedStyles.overlay}
63
64
  aria-label="Close rename case dialog"
64
65
  {...overlayProps}
65
66
  >
66
- <div className={styles.modal} role="dialog" aria-modal="true" aria-label="Rename Case">
67
+ <div className={`${sharedStyles.modal} ${styles.modal}`} role="dialog" aria-modal="true" aria-label="Rename Case">
67
68
  <button {...getCloseButtonProps({ ariaLabel: 'Close rename case dialog' })}>
68
69
  ×
69
70
  </button>
70
- <h3 className={styles.title}>Rename Case</h3>
71
- <p className={styles.subtitle}>Current case: {currentCase}</p>
71
+ <h3 className={sharedStyles.title}>Rename Case</h3>
72
+ <p className={sharedStyles.subtitle}>Current case: {currentCase}</p>
72
73
  <input
73
74
  ref={inputRef}
74
75
  type="text"
75
76
  value={newCaseName}
76
77
  onChange={(event) => setNewCaseName(event.target.value)}
77
- className={styles.input}
78
+ className={sharedStyles.input}
78
79
  placeholder="New case number"
79
80
  disabled={isSubmitting}
80
81
  onKeyDown={(event) => {
@@ -83,10 +84,10 @@ export const RenameCaseModal = ({
83
84
  }
84
85
  }}
85
86
  />
86
- <div className={styles.actions}>
87
+ <div className={sharedStyles.actions}>
87
88
  <button
88
89
  type="button"
89
- className={styles.cancelButton}
90
+ className={sharedStyles.cancelButton}
90
91
  onClick={requestClose}
91
92
  disabled={isCloseBlocked}
92
93
  >
@@ -94,7 +95,7 @@ export const RenameCaseModal = ({
94
95
  </button>
95
96
  <button
96
97
  type="button"
97
- className={styles.confirmButton}
98
+ className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
98
99
  onClick={() => void handleSubmit()}
99
100
  disabled={isSubmitting || !newCaseName.trim()}
100
101
  >