@striae-org/striae 4.2.0 → 4.3.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.
Files changed (90) hide show
  1. package/LICENSE +1 -1
  2. package/app/components/actions/case-manage.ts +50 -17
  3. package/app/components/audit/viewer/audit-entries-list.tsx +5 -2
  4. package/app/components/audit/viewer/use-audit-viewer-data.ts +6 -3
  5. package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
  6. package/app/components/canvas/confirmation/confirmation.tsx +6 -2
  7. package/app/components/colors/colors.module.css +4 -3
  8. package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -76
  9. package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
  10. package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
  11. package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
  12. package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
  13. package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
  14. package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
  15. package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
  16. package/app/components/navbar/navbar.tsx +34 -9
  17. package/app/components/sidebar/cases/case-sidebar.tsx +93 -73
  18. package/app/components/sidebar/cases/cases-modal.module.css +312 -10
  19. package/app/components/sidebar/cases/cases-modal.tsx +737 -116
  20. package/app/components/sidebar/cases/cases.module.css +43 -0
  21. package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
  22. package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
  23. package/app/components/sidebar/files/files-modal.module.css +285 -44
  24. package/app/components/sidebar/files/files-modal.tsx +482 -177
  25. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  26. package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
  27. package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
  28. package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
  29. package/app/components/sidebar/notes/class-details-shared.ts +239 -0
  30. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +77 -76
  31. package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
  32. package/app/components/sidebar/notes/notes.module.css +262 -14
  33. package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
  34. package/app/components/sidebar/sidebar-container.tsx +2 -0
  35. package/app/components/sidebar/sidebar.tsx +15 -1
  36. package/app/{tailwind.css → global.css} +1 -3
  37. package/app/hooks/useCaseListPreferences.ts +99 -0
  38. package/app/hooks/useFileListPreferences.ts +106 -0
  39. package/app/hooks/useOverlayDismiss.ts +6 -4
  40. package/app/root.tsx +1 -1
  41. package/app/routes/striae/striae.tsx +7 -0
  42. package/app/services/audit/audit.service.ts +2 -2
  43. package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
  44. package/app/types/annotations.ts +48 -1
  45. package/app/types/audit.ts +1 -0
  46. package/app/utils/data/case-filters.ts +127 -0
  47. package/app/utils/data/confirmation-summary/summary-core.ts +295 -0
  48. package/app/utils/data/data-operations.ts +17 -861
  49. package/app/utils/data/file-filters.ts +201 -0
  50. package/app/utils/data/index.ts +11 -1
  51. package/app/utils/data/operations/batch-operations.ts +113 -0
  52. package/app/utils/data/operations/case-operations.ts +168 -0
  53. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  54. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  55. package/app/utils/data/operations/index.ts +7 -0
  56. package/app/utils/data/operations/signing-operations.ts +225 -0
  57. package/app/utils/data/operations/types.ts +42 -0
  58. package/app/utils/data/operations/validation-operations.ts +48 -0
  59. package/app/utils/forensics/export-verification.ts +40 -111
  60. package/functions/api/_shared/firebase-auth.ts +2 -7
  61. package/functions/api/image/[[path]].ts +23 -22
  62. package/functions/api/pdf/[[path]].ts +27 -8
  63. package/package.json +7 -13
  64. package/scripts/deploy-primershear-emails.sh +1 -1
  65. package/worker-configuration.d.ts +2 -2
  66. package/workers/audit-worker/package.json +1 -1
  67. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  68. package/workers/data-worker/package.json +1 -1
  69. package/workers/data-worker/wrangler.jsonc.example +1 -1
  70. package/workers/image-worker/package.json +1 -1
  71. package/workers/image-worker/src/image-worker.example.ts +16 -5
  72. package/workers/image-worker/wrangler.jsonc.example +1 -1
  73. package/workers/keys-worker/package.json +1 -1
  74. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  75. package/workers/pdf-worker/package.json +1 -1
  76. package/workers/pdf-worker/src/formats/format-striae.ts +84 -124
  77. package/workers/pdf-worker/src/pdf-worker.example.ts +58 -61
  78. package/workers/pdf-worker/src/report-layout.ts +227 -0
  79. package/workers/pdf-worker/src/report-types.ts +23 -3
  80. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  81. package/workers/user-worker/package.json +1 -1
  82. package/workers/user-worker/src/user-worker.example.ts +17 -0
  83. package/workers/user-worker/wrangler.jsonc.example +1 -1
  84. package/wrangler.toml.example +1 -1
  85. package/NOTICE +0 -13
  86. package/app/components/sidebar/notes/notes-modal.tsx +0 -52
  87. package/postcss.config.js +0 -6
  88. package/tailwind.config.ts +0 -22
  89. package/workers/pdf-worker/src/assets/icon-256.png +0 -0
  90. /package/workers/pdf-worker/src/assets/{generated-assets.ts → generated-assets.example.ts} +0 -0
package/LICENSE CHANGED
@@ -175,7 +175,7 @@
175
175
 
176
176
  END OF TERMS AND CONDITIONS
177
177
 
178
- Striae © 2025. All rights reserved.
178
+ © 2025 Stephen J. Lu
179
179
 
180
180
  Licensed under the Apache License, Version 2.0 (the "License");
181
181
  you may not use this file except in compliance with the License.
@@ -11,9 +11,10 @@ import {
11
11
  deleteCaseData,
12
12
  duplicateCaseData,
13
13
  deleteFileAnnotations,
14
- signForensicManifest
14
+ signForensicManifest,
15
+ removeCaseConfirmationSummary
15
16
  } from '~/utils/data';
16
- import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail } from '~/types';
17
+ import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail, type CaseExportData } from '~/types';
17
18
  import { auditService } from '~/services/audit';
18
19
  import { fetchImageApi } from '~/utils/api';
19
20
  import { exportCaseData, formatDateForFilename } from '~/components/actions/case-export';
@@ -569,6 +570,13 @@ export const deleteCase = async (user: User, caseNumber: string): Promise<Delete
569
570
  // Delete case data using centralized function (skip validation since user no longer has access)
570
571
  await deleteCaseData(user, caseNumber, { skipValidation: true });
571
572
 
573
+ // Clean up confirmation status metadata for this case
574
+ try {
575
+ await removeCaseConfirmationSummary(user, caseNumber);
576
+ } catch (summaryError) {
577
+ console.warn(`Failed to remove confirmation summary for case ${caseNumber}:`, summaryError);
578
+ }
579
+
572
580
  // Add a small delay before audit logging to reduce rate limiting
573
581
  await new Promise(resolve => setTimeout(resolve, 100));
574
582
 
@@ -593,6 +601,13 @@ export const deleteCase = async (user: User, caseNumber: string): Promise<Delete
593
601
  // Delete case data using centralized function (skip validation since user no longer has access)
594
602
  await deleteCaseData(user, caseNumber, { skipValidation: true });
595
603
 
604
+ // Clean up confirmation status metadata for this case
605
+ try {
606
+ await removeCaseConfirmationSummary(user, caseNumber);
607
+ } catch (summaryError) {
608
+ console.warn(`Failed to remove confirmation summary for case ${caseNumber}:`, summaryError);
609
+ }
610
+
596
611
  // Add a small delay before audit logging to reduce rate limiting
597
612
  await new Promise(resolve => setTimeout(resolve, 100));
598
613
 
@@ -736,22 +751,19 @@ export const archiveCase = async (
736
751
  isReadOnly: true,
737
752
  } as CaseData;
738
753
 
739
- await updateCaseData(user, caseNumber, archiveData);
740
-
741
- await auditService.logCaseArchive(
742
- user,
743
- caseNumber,
744
- caseNumber,
745
- archiveReason?.trim() || 'No reason provided',
746
- 'success',
747
- [],
748
- archiveData.files?.length || 0,
749
- archivedAt,
750
- Date.now() - startTime
751
- );
752
-
753
754
  const exportData = await exportCaseData(user, caseNumber, { includeMetadata: true });
754
- const caseJsonContent = JSON.stringify(exportData, null, 2);
755
+ const archivedExportData: CaseExportData = {
756
+ ...exportData,
757
+ metadata: {
758
+ ...exportData.metadata,
759
+ archived: true,
760
+ archivedAt,
761
+ archivedBy: user.uid,
762
+ archivedByDisplay,
763
+ archiveReason: archiveReason?.trim() || undefined,
764
+ },
765
+ };
766
+ const caseJsonContent = JSON.stringify(archivedExportData, null, 2);
755
767
 
756
768
  const JSZip = (await import('jszip')).default;
757
769
  const zip = new JSZip();
@@ -876,6 +888,27 @@ export const archiveCase = async (
876
888
  compressionOptions: { level: 6 },
877
889
  });
878
890
 
891
+ await updateCaseData(user, caseNumber, archiveData);
892
+
893
+ // Clean up confirmation status metadata for this archived case
894
+ try {
895
+ await removeCaseConfirmationSummary(user, caseNumber);
896
+ } catch (summaryError) {
897
+ console.warn(`Failed to remove confirmation summary for case ${caseNumber}:`, summaryError);
898
+ }
899
+
900
+ await auditService.logCaseArchive(
901
+ user,
902
+ caseNumber,
903
+ caseNumber,
904
+ archiveReason?.trim() || 'No reason provided',
905
+ 'success',
906
+ [],
907
+ archiveData.files?.length || 0,
908
+ archivedAt,
909
+ Date.now() - startTime
910
+ );
911
+
879
912
  const downloadUrl = URL.createObjectURL(zipBlob);
880
913
  const archiveFileName = `striae-case-${caseNumber}-archive-${formatDateForFilename(new Date())}.zip`;
881
914
  const anchor = document.createElement('a');
@@ -15,8 +15,11 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
15
15
  <p>No activities match the current filters.</p>
16
16
  </div>
17
17
  ) : (
18
- entries.map((entry, index) => (
19
- <div key={index} className={`${styles.entry} ${styles[entry.result]}`}>
18
+ entries.map((entry) => (
19
+ <div
20
+ key={`${entry.timestamp}-${entry.userId}-${entry.action}-${entry.details.fileName || ''}`}
21
+ className={`${styles.entry} ${styles[entry.result]}`}
22
+ >
20
23
  <div className={styles.entryHeader}>
21
24
  <div className={styles.entryIcons}>
22
25
  <span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
@@ -112,10 +112,13 @@ export const useAuditViewerData = ({
112
112
 
113
113
  if (effectiveCaseNumber) {
114
114
  const caseData = await getCaseData(user, effectiveCaseNumber);
115
- const archivedReadOnlyCase = Boolean(caseData?.isReadOnly && caseData.archived === true);
116
- setIsArchivedReadOnlyCase(archivedReadOnlyCase);
115
+ const isArchiveBundleCase = Boolean(
116
+ caseData?.archived === true &&
117
+ caseData?.bundledAuditTrail?.source === 'archive-bundle'
118
+ );
119
+ setIsArchivedReadOnlyCase(isArchiveBundleCase);
117
120
 
118
- if (archivedReadOnlyCase && !caseData?.bundledAuditTrail?.entries?.length) {
121
+ if (isArchiveBundleCase && !Array.isArray(caseData?.bundledAuditTrail?.entries)) {
119
122
  setBundledAuditWarning(
120
123
  'This imported archived case does not include bundled audit trail data. No audit entries are available for this case.'
121
124
  );
@@ -48,7 +48,7 @@ export const useAuditViewerExport = ({
48
48
  const filename = auditExportService.generateFilename(
49
49
  exportContextData.scopeType,
50
50
  exportContextData.identifier,
51
- 'csv'
51
+ 'json'
52
52
  );
53
53
 
54
54
  try {
@@ -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';
@@ -54,7 +55,10 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
54
55
 
55
56
  // Reset form when modal opens
56
57
  useEffect(() => {
57
- if (isOpen) {
58
+ const justOpened = isOpen && !wasOpenRef.current;
59
+ wasOpenRef.current = isOpen;
60
+
61
+ if (justOpened) {
58
62
  if (existingConfirmation) {
59
63
  setBadgeId(existingConfirmation.badgeId);
60
64
  } else {
@@ -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: 100%;
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,.25);
59
- }
59
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
60
+ }
@@ -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
  >