@striae-org/striae 4.0.3 → 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.
Files changed (118) hide show
  1. package/.env.example +8 -0
  2. package/app/components/actions/case-export/core-export.ts +14 -8
  3. package/app/components/actions/case-export/data-processing.ts +1 -0
  4. package/app/components/actions/case-export/download-handlers.ts +7 -0
  5. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  6. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  7. package/app/components/actions/case-import/orchestrator.ts +78 -32
  8. package/app/components/actions/case-import/storage-operations.ts +97 -8
  9. package/app/components/actions/case-import/zip-processing.ts +159 -86
  10. package/app/components/actions/case-manage.ts +430 -8
  11. package/app/components/actions/confirm-export.ts +13 -4
  12. package/app/components/actions/generate-pdf.ts +10 -2
  13. package/app/components/actions/image-manage.ts +77 -44
  14. package/app/components/audit/user-audit-viewer.tsx +137 -945
  15. package/app/components/audit/user-audit.module.css +41 -0
  16. package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
  17. package/app/components/audit/viewer/audit-entries-list.tsx +207 -0
  18. package/app/components/audit/viewer/audit-filters-panel.tsx +307 -0
  19. package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
  20. package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
  21. package/app/components/audit/viewer/audit-viewer-utils.ts +123 -0
  22. package/app/components/audit/viewer/types.ts +1 -0
  23. package/app/components/audit/viewer/use-audit-viewer-data.ts +186 -0
  24. package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
  25. package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
  26. package/app/components/auth/mfa-enrollment.module.css +13 -5
  27. package/app/components/auth/mfa-verification.module.css +13 -5
  28. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  29. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  30. package/app/components/canvas/canvas.module.css +64 -54
  31. package/app/components/canvas/canvas.tsx +17 -16
  32. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  33. package/app/components/canvas/confirmation/confirmation.tsx +17 -47
  34. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  35. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  36. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  37. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  38. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  39. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  40. package/app/components/navbar/navbar.module.css +447 -0
  41. package/app/components/navbar/navbar.tsx +377 -0
  42. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +2 -0
  43. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +21 -51
  44. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  45. package/app/components/sidebar/case-export/case-export.tsx +14 -77
  46. package/app/components/sidebar/case-import/case-import.module.css +25 -0
  47. package/app/components/sidebar/case-import/case-import.tsx +64 -40
  48. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  49. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  50. package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
  51. package/app/components/sidebar/cases/cases-modal.module.css +45 -9
  52. package/app/components/sidebar/cases/cases-modal.tsx +16 -16
  53. package/app/components/sidebar/cases/cases.module.css +62 -21
  54. package/app/components/sidebar/files/files-modal.module.css +46 -10
  55. package/app/components/sidebar/files/files-modal.tsx +22 -23
  56. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  57. package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
  58. package/app/components/sidebar/notes/notes-modal.tsx +18 -17
  59. package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
  60. package/app/components/sidebar/notes/notes.module.css +155 -0
  61. package/app/components/sidebar/sidebar-container.tsx +15 -28
  62. package/app/components/sidebar/sidebar.module.css +7 -71
  63. package/app/components/sidebar/sidebar.tsx +24 -125
  64. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  65. package/app/components/toast/toast.module.css +2 -1
  66. package/app/components/toast/toast.tsx +16 -11
  67. package/app/components/user/delete-account.tsx +10 -31
  68. package/app/components/user/inactivity-warning.module.css +9 -6
  69. package/app/components/user/inactivity-warning.tsx +15 -2
  70. package/app/components/user/manage-profile.module.css +2 -0
  71. package/app/components/user/manage-profile.tsx +108 -40
  72. package/app/hooks/useOverlayDismiss.ts +116 -0
  73. package/app/routes/auth/login.example.tsx +19 -8
  74. package/app/routes/auth/login.tsx +785 -774
  75. package/app/routes/auth/passwordReset.module.css +23 -13
  76. package/app/routes/striae/striae.module.css +10 -3
  77. package/app/routes/striae/striae.tsx +477 -31
  78. package/app/routes.ts +7 -0
  79. package/app/services/audit/audit-export-csv.ts +2 -0
  80. package/app/services/audit/audit.service.ts +202 -32
  81. package/app/services/audit/builders/audit-entry-builder.ts +2 -1
  82. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  83. package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
  84. package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -0
  85. package/app/services/audit/builders/index.ts +1 -0
  86. package/app/types/audit.ts +5 -2
  87. package/app/types/case.ts +29 -0
  88. package/app/types/import.ts +3 -0
  89. package/app/types/user.ts +1 -0
  90. package/app/utils/data/permissions.ts +17 -1
  91. package/app/utils/forensics/audit-export-signature.ts +5 -1
  92. package/app/utils/forensics/confirmation-signature.ts +3 -0
  93. package/app/utils/forensics/export-verification.ts +497 -22
  94. package/functions/api/pdf/[[path]].ts +32 -1
  95. package/load-context.ts +9 -0
  96. package/package.json +6 -2
  97. package/primershear.emails.example +6 -0
  98. package/scripts/deploy-pages-secrets.sh +6 -0
  99. package/scripts/deploy-primershear-emails.sh +167 -0
  100. package/worker-configuration.d.ts +7493 -7491
  101. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  102. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  103. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  104. package/workers/data-worker/wrangler.jsonc.example +1 -1
  105. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  106. package/workers/image-worker/wrangler.jsonc.example +1 -1
  107. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  108. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  109. package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
  110. package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
  111. package/workers/pdf-worker/src/report-types.ts +3 -0
  112. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  113. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  114. package/workers/user-worker/src/user-worker.example.ts +6 -1
  115. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  116. package/workers/user-worker/wrangler.jsonc.example +1 -1
  117. package/wrangler.toml.example +1 -1
  118. package/public/.well-known/keybase.txt +0 -56
@@ -1,37 +1,32 @@
1
1
  /* eslint-disable jsx-a11y/no-static-element-interactions */
2
2
  /* eslint-disable jsx-a11y/click-events-have-key-events */
3
3
  import type React from 'react';
4
- import { useState, useEffect } from 'react';
4
+ import { useState } from 'react';
5
5
  import { Link } from 'react-router';
6
6
  import { Sidebar } from './sidebar';
7
7
  import type { User } from 'firebase/auth';
8
8
  import { type FileData } from '~/types';
9
9
  import styles from './sidebar.module.css';
10
10
  import { getAppVersion } from '~/utils/common';
11
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
11
12
 
12
13
  interface SidebarContainerProps {
13
14
  user: User;
14
15
  onImageSelect: (file: FileData) => void;
15
16
  imageId?: string;
16
- onCaseChange: (caseNumber: string) => void;
17
17
  currentCase: string;
18
- setCurrentCase: (caseNumber: string) => void;
19
18
  files: FileData[];
20
19
  setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
21
20
  imageLoaded: boolean;
22
21
  setImageLoaded: (loaded: boolean) => void;
23
- caseNumber: string;
24
- setCaseNumber: (caseNumber: string) => void;
25
- error: string;
26
- setError: (error: string) => void;
27
- successAction: 'loaded' | 'created' | 'deleted' | null;
28
- setSuccessAction: (action: 'loaded' | 'created' | 'deleted' | null) => void;
29
22
  showNotes: boolean;
30
23
  setShowNotes: (show: boolean) => void;
31
24
  onAnnotationRefresh?: () => void;
32
25
  isReadOnly?: boolean;
33
26
  isConfirmed?: boolean;
34
27
  confirmationSaveVersion?: number;
28
+ isUploading?: boolean;
29
+ onUploadStatusChange?: (isUploading: boolean) => void;
35
30
  }
36
31
 
37
32
  export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
@@ -39,24 +34,16 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
39
34
  const year = new Date().getFullYear();
40
35
  const appVersion = getAppVersion();
41
36
 
42
- useEffect(() => {
43
- const handleEscape = (e: KeyboardEvent) => {
44
- if (e.key === 'Escape' && isFooterModalOpen) {
45
- setIsFooterModalOpen(false);
46
- }
47
- };
48
-
49
- if (isFooterModalOpen) {
50
- document.addEventListener('keydown', handleEscape);
51
- }
52
-
53
- return () => {
54
- document.removeEventListener('keydown', handleEscape);
55
- };
56
- }, [isFooterModalOpen]);
37
+ const {
38
+ overlayProps,
39
+ getCloseButtonProps,
40
+ } = useOverlayDismiss({
41
+ isOpen: isFooterModalOpen,
42
+ onClose: () => setIsFooterModalOpen(false),
43
+ });
57
44
 
58
45
  return (
59
- <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
46
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
60
47
  {/* Main Sidebar */}
61
48
  <Sidebar {...props} />
62
49
 
@@ -72,13 +59,13 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
72
59
 
73
60
  {/* Footer Modal */}
74
61
  {isFooterModalOpen && (
75
- <div className={styles.footerModalOverlay} onClick={() => setIsFooterModalOpen(false)}>
62
+ <div className={styles.footerModalOverlay} {...overlayProps} aria-label="Close About and Support dialog">
76
63
  <div className={styles.footerModal} onClick={(e) => e.stopPropagation()}>
77
64
  <div className={styles.footerModalHeader}>
78
65
  <h2 className={styles.footerModalTitle}>About Striae</h2>
79
- <button
80
- onClick={() => setIsFooterModalOpen(false)}
66
+ <button
81
67
  className={styles.footerModalClose}
68
+ {...getCloseButtonProps({ ariaLabel: 'Close About and Support dialog' })}
82
69
  >
83
70
  ×
84
71
  </button>
@@ -3,54 +3,17 @@
3
3
  position: relative;
4
4
  width: 300px;
5
5
  min-width: 250px;
6
- height: calc(100vh - 60px);
6
+ height: calc(100% - 60px);
7
+ display: flex;
8
+ flex-direction: column;
7
9
  background-color: #f8f9fa;
8
10
  border-right: 1px solid #dee2e6;
9
- padding: 1.5rem;
10
- overflow-y: auto;
11
+ padding: 0.25rem 1rem 0.875rem;
12
+ overflow: hidden;
11
13
  flex-shrink: 0;
12
14
  box-sizing: border-box;
13
15
  }
14
16
 
15
- /* User Section */
16
- .userInfo {
17
- display: flex;
18
- flex-direction: column;
19
- align-items: flex-start;
20
- gap: 1rem;
21
- padding-bottom: 1rem;
22
- border-bottom: 2px solid #e9ecef;
23
- margin-bottom: 1rem;
24
- }
25
-
26
- .userTitle {
27
- font-size: 1.2rem;
28
- font-weight: 600;
29
- color: #212529;
30
- margin: 0;
31
- }
32
-
33
- .userActions {
34
- display: flex;
35
- gap: 0.5rem;
36
- width: 100%;
37
- }
38
-
39
- .profileButton {
40
- padding: 0.5rem 1rem;
41
- background-color: #6c757d;
42
- color: white;
43
- border: none;
44
- border-radius: 6px;
45
- font-size: 0.875rem;
46
- cursor: pointer;
47
- transition: all 0.2s;
48
- }
49
-
50
- .profileButton:hover {
51
- background-color: #5c636a;
52
- }
53
-
54
17
  /* Footer Button */
55
18
  .footerButton {
56
19
  position: fixed;
@@ -120,6 +83,7 @@
120
83
  justify-content: center;
121
84
  align-items: center;
122
85
  z-index: 1000;
86
+ cursor: default;
123
87
  }
124
88
 
125
89
  .footerModal {
@@ -131,6 +95,7 @@
131
95
  max-height: 80vh;
132
96
  overflow-y: auto;
133
97
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
98
+ cursor: default;
134
99
  }
135
100
 
136
101
  .footerModalHeader {
@@ -237,35 +202,6 @@
237
202
  text-underline-offset: 3px;
238
203
  }
239
204
 
240
-
241
-
242
- /* Import Section */
243
- .importSection {
244
- margin-top: auto;
245
- padding: 1rem 0;
246
- border-top: 1px solid var(--divider);
247
- }
248
-
249
- .importButton {
250
- width: 100%;
251
- padding: 0.75rem 1rem;
252
- background: var(--success);
253
- color: white;
254
- border: none;
255
- border-radius: var(--spaceXS);
256
- font-size: var(--fontSizeBodyS);
257
- font-weight: var(--fontWeightMedium);
258
- cursor: pointer;
259
- transition: all var(--durationS) var(--bezierFastoutSlowin);
260
- box-shadow: 0 1px 3px color-mix(in lab, var(--success) 30%, transparent);
261
- box-sizing: border-box;
262
- }
263
-
264
- .importButton:hover {
265
- background: color-mix(in lab, var(--success) 85%, var(--black));
266
- box-shadow: 0 2px 6px color-mix(in lab, var(--success) 40%, transparent);
267
- }
268
-
269
205
  /* Hash Button */
270
206
  .hashButton {
271
207
  width: 100%;
@@ -1,31 +1,19 @@
1
1
  import type { User } from 'firebase/auth';
2
2
  import { useState, useCallback } from 'react';
3
3
  import styles from './sidebar.module.css';
4
- import { ManageProfile } from '../user/manage-profile';
5
- import { SignOut } from '../actions/signout';
6
4
  import { CaseSidebar } from './cases/case-sidebar';
7
- import { NotesSidebar } from './notes/notes-sidebar';
8
- import { CaseImport } from './case-import/case-import';
9
5
  import { Toast } from '../toast/toast';
10
- import { type FileData, type ImportResult, type ConfirmationImportResult } from '~/types';
6
+ import { type FileData } from '~/types';
11
7
 
12
8
  interface SidebarProps {
13
9
  user: User;
14
10
  onImageSelect: (file: FileData) => void;
15
11
  imageId?: string;
16
- onCaseChange: (caseNumber: string) => void;
17
12
  currentCase: string;
18
- setCurrentCase: (caseNumber: string) => void;
19
13
  files: FileData[];
20
14
  setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
21
15
  imageLoaded: boolean;
22
16
  setImageLoaded: (loaded: boolean) => void;
23
- caseNumber: string;
24
- setCaseNumber: (caseNumber: string) => void;
25
- error: string;
26
- setError: (error: string) => void;
27
- successAction: 'loaded' | 'created' | 'deleted' | null;
28
- setSuccessAction: (action: 'loaded' | 'created' | 'deleted' | null) => void;
29
17
  showNotes: boolean;
30
18
  setShowNotes: (show: boolean) => void;
31
19
  onAnnotationRefresh?: () => void;
@@ -33,66 +21,34 @@ interface SidebarProps {
33
21
  isConfirmed?: boolean;
34
22
  confirmationSaveVersion?: number;
35
23
  isUploading?: boolean;
24
+ onUploadStatusChange?: (isUploading: boolean) => void;
36
25
  }
37
26
 
38
27
  export const Sidebar = ({
39
28
  user,
40
29
  onImageSelect,
41
30
  imageId,
42
- onCaseChange,
43
31
  currentCase,
44
- setCurrentCase,
45
32
  imageLoaded,
46
33
  setImageLoaded,
47
34
  files,
48
35
  setFiles,
49
- caseNumber,
50
- setCaseNumber,
51
- error,
52
- setError,
53
- successAction,
54
- setSuccessAction,
55
- showNotes,
56
36
  setShowNotes,
57
- onAnnotationRefresh,
58
37
  isReadOnly = false,
59
38
  isConfirmed = false,
60
39
  confirmationSaveVersion = 0,
61
- isUploading: initialIsUploading = false,
40
+ isUploading: initialIsUploading = false,
41
+ onUploadStatusChange,
62
42
  }: SidebarProps) => {
63
- const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
64
- const [isImportModalOpen, setIsImportModalOpen] = useState(false);
65
43
  const [isUploading, setIsUploading] = useState(initialIsUploading);
66
44
  const [toastMessage, setToastMessage] = useState('');
67
45
  const [toastType, setToastType] = useState<'success' | 'error' | 'warning'>('success');
68
46
  const [isToastVisible, setIsToastVisible] = useState(false);
69
47
 
70
- const handleImportComplete = useCallback((result: ImportResult | ConfirmationImportResult) => {
71
- if (result.success) {
72
- // For case imports, load the imported case automatically
73
- if ('isReadOnly' in result) {
74
- // This is an ImportResult (case import)
75
- if (result.caseNumber && result.isReadOnly) {
76
- // Successful read-only case import - load the case
77
- onCaseChange(result.caseNumber);
78
- setCurrentCase(result.caseNumber);
79
- setCaseNumber(result.caseNumber);
80
- setSuccessAction('loaded');
81
- } else if (!result.caseNumber && !result.isReadOnly) {
82
- // Read-only case cleared - reset all UI state
83
- setCurrentCase('');
84
- setCaseNumber('');
85
- setFiles([]);
86
- onImageSelect({ id: 'clear', originalFilename: '/clear.jpg', uploadedAt: '' });
87
- setImageLoaded(false);
88
- onCaseChange(''); // This will trigger canvas/annotation state reset in main component
89
- setShowNotes(false); // Close notes sidebar
90
- setSuccessAction(null);
91
- }
92
- }
93
- // For confirmation imports, no action needed - the confirmations are already loaded
94
- }
95
- }, [onCaseChange, setCurrentCase, setCaseNumber, setSuccessAction, setFiles, onImageSelect, setImageLoaded, setShowNotes]);
48
+ const handleUploadStatusChange = useCallback((uploading: boolean) => {
49
+ setIsUploading(uploading);
50
+ onUploadStatusChange?.(uploading);
51
+ }, [onUploadStatusChange]);
96
52
 
97
53
  const handleUploadComplete = useCallback((result: { successCount: number; failedFiles: string[] }) => {
98
54
  if (result.successCount === 0 && result.failedFiles.length > 0) {
@@ -115,80 +71,23 @@ export const Sidebar = ({
115
71
 
116
72
  return (
117
73
  <div className={styles.sidebar}>
118
- <div className={styles.userInfo}>
119
- <h3 className={styles.userTitle}>
120
- {`${user.displayName?.split(' ')[0] || 'User'}'s Striae`}
121
- </h3>
122
- <div className={styles.userActions}>
123
- <button
124
- onClick={() => setIsProfileModalOpen(true)}
125
- className={styles.profileButton}
126
- disabled={isUploading}
127
- title={isUploading ? 'Cannot manage profile while uploading files' : undefined}
128
- >
129
- Manage Profile
130
- </button>
131
- <SignOut disabled={isUploading} />
132
- </div>
133
- </div>
134
- <ManageProfile
135
- isOpen={isProfileModalOpen}
136
- onClose={() => setIsProfileModalOpen(false)}
137
- />
138
- <CaseImport
139
- isOpen={isImportModalOpen}
140
- onClose={() => setIsImportModalOpen(false)}
141
- onImportComplete={handleImportComplete}
74
+ <CaseSidebar
75
+ user={user}
76
+ onImageSelect={onImageSelect}
77
+ currentCase={currentCase}
78
+ imageLoaded={imageLoaded}
79
+ setImageLoaded={setImageLoaded}
80
+ files={files}
81
+ setFiles={setFiles}
82
+ onNotesClick={() => setShowNotes(true)}
83
+ isReadOnly={isReadOnly}
84
+ isConfirmed={isConfirmed}
85
+ confirmationSaveVersion={confirmationSaveVersion}
86
+ selectedFileId={imageId}
87
+ isUploading={isUploading}
88
+ onUploadStatusChange={handleUploadStatusChange}
89
+ onUploadComplete={handleUploadComplete}
142
90
  />
143
- {showNotes ? (
144
- <NotesSidebar
145
- currentCase={currentCase}
146
- onReturn={() => setShowNotes(false)}
147
- user={user}
148
- imageId={imageId || ''}
149
- onAnnotationRefresh={onAnnotationRefresh}
150
- originalFileName={files.find(file => file.id === imageId)?.originalFilename}
151
- isUploading={isUploading}
152
- />
153
- ) : (
154
- <>
155
- <CaseSidebar
156
- user={user}
157
- onImageSelect={onImageSelect}
158
- onCaseChange={onCaseChange}
159
- currentCase={currentCase}
160
- setCurrentCase={setCurrentCase}
161
- imageLoaded={imageLoaded}
162
- setImageLoaded={setImageLoaded}
163
- files={files}
164
- setFiles={setFiles}
165
- caseNumber={caseNumber}
166
- setCaseNumber={setCaseNumber}
167
- error={error}
168
- setError={setError}
169
- successAction={successAction}
170
- setSuccessAction={setSuccessAction}
171
- onNotesClick={() => setShowNotes(true)}
172
- isReadOnly={isReadOnly}
173
- isConfirmed={isConfirmed}
174
- confirmationSaveVersion={confirmationSaveVersion}
175
- selectedFileId={imageId}
176
- isUploading={isUploading}
177
- onUploadStatusChange={setIsUploading}
178
- onUploadComplete={handleUploadComplete}
179
- />
180
- <div className={styles.importSection}>
181
- <button
182
- onClick={() => setIsImportModalOpen(true)}
183
- className={styles.importButton}
184
- disabled={isUploading}
185
- title={isUploading ? 'Cannot import while uploading files' : undefined}
186
- >
187
- Import/Clear RO Case
188
- </button>
189
- </div>
190
- </>
191
- )}
192
91
  <Toast
193
92
  message={toastMessage}
194
93
  type={toastType}
@@ -1,24 +1,24 @@
1
1
  /* Image Upload Zone */
2
2
  .imageUploadZone {
3
- margin: 1rem 0;
3
+ margin: 0.25rem 0;
4
4
  display: flex;
5
5
  flex-direction: column;
6
- gap: 0.5rem;
6
+ gap: 0.375rem;
7
7
  }
8
8
 
9
9
  .imageUploadZone label {
10
- font-size: 0.9rem;
10
+ font-size: 0.85rem;
11
11
  font-weight: 500;
12
12
  color: #000000;
13
- margin-bottom: 0.5rem;
13
+ margin-bottom: 0.25rem;
14
14
  }
15
15
 
16
16
  .fileInput {
17
17
  width: 100%;
18
- padding: 0.5rem;
18
+ padding: 0.375rem 0.5rem;
19
19
  border: 1px solid #dee2e6;
20
20
  border-radius: 4px;
21
- font-size: 0.875rem;
21
+ font-size: 0.8125rem;
22
22
  box-sizing: border-box;
23
23
  cursor: pointer;
24
24
  }
@@ -33,18 +33,18 @@
33
33
  display: flex;
34
34
  align-items: center;
35
35
  justify-content: center;
36
- padding: 2rem 1rem;
36
+ padding: 1rem 0.75rem;
37
37
  border: 2px dashed #dee2e6;
38
38
  border-radius: 6px;
39
39
  background-color: #fafafa;
40
40
  transition: all 0.2s ease;
41
41
  pointer-events: none;
42
- min-height: 100px;
42
+ min-height: 72px;
43
43
  }
44
44
 
45
45
  .dragDropText {
46
46
  margin: 0;
47
- font-size: 0.9rem;
47
+ font-size: 0.82rem;
48
48
  color: #6c757d;
49
49
  text-align: center;
50
50
  font-weight: 500;
@@ -68,11 +68,11 @@
68
68
 
69
69
  .progressBar {
70
70
  width: 100%;
71
- height: 6px;
71
+ height: 5px;
72
72
  background-color: #e9ecef;
73
73
  border-radius: 3px;
74
74
  overflow: hidden;
75
- margin-top: 0.5rem;
75
+ margin-top: 0.25rem;
76
76
  }
77
77
 
78
78
  .progressFill {
@@ -82,7 +82,7 @@
82
82
  }
83
83
 
84
84
  .uploadingText {
85
- font-size: 0.875rem;
85
+ font-size: 0.8125rem;
86
86
  color: var(--textBody);
87
87
  font-weight: 500;
88
88
  }
@@ -93,7 +93,7 @@
93
93
  justify-content: center;
94
94
  gap: 0.5rem;
95
95
  width: 100%;
96
- margin-top: 0.25rem;
96
+ margin-top: 0.125rem;
97
97
  }
98
98
 
99
99
  .fileCountText {
@@ -6,7 +6,7 @@
6
6
  bottom: 0;
7
7
  background: color-mix(in lab, var(--black) 40%, transparent);
8
8
  z-index: 999;
9
- cursor: pointer;
9
+ cursor: default;
10
10
  }
11
11
 
12
12
  .toast {
@@ -28,6 +28,7 @@
28
28
  max-width: 400px;
29
29
  min-width: 300px;
30
30
  backdrop-filter: blur(10px);
31
+ cursor: default;
31
32
  }
32
33
 
33
34
  .toast.show {
@@ -1,4 +1,5 @@
1
1
  import { useEffect, type ReactNode } from 'react';
2
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
2
3
  import styles from './toast.module.css';
3
4
 
4
5
  interface ToastProps {
@@ -10,30 +11,34 @@ interface ToastProps {
10
11
  }
11
12
 
12
13
  export const Toast = ({ message, type, isVisible, onClose, duration = 4000 }: ToastProps) => {
14
+ const {
15
+ requestClose,
16
+ handleOverlayMouseDown,
17
+ handleOverlayKeyDown
18
+ } = useOverlayDismiss({
19
+ isOpen: isVisible,
20
+ onClose,
21
+ closeOnEscape: false
22
+ });
23
+
13
24
  useEffect(() => {
14
25
  if (isVisible && duration > 0) {
15
26
  const timer = setTimeout(() => {
16
- onClose();
27
+ requestClose();
17
28
  }, duration);
18
29
 
19
30
  return () => clearTimeout(timer);
20
31
  }
21
- }, [isVisible, onClose, duration]);
32
+ }, [isVisible, requestClose, duration]);
22
33
 
23
34
  if (!isVisible) return null;
24
35
 
25
- const handleBackdropKeyDown = (e: React.KeyboardEvent) => {
26
- if (e.key === 'Enter' || e.key === ' ') {
27
- onClose();
28
- }
29
- };
30
-
31
36
  return (
32
37
  <>
33
38
  <div
34
39
  className={styles.backdrop}
35
- onClick={onClose}
36
- onKeyDown={handleBackdropKeyDown}
40
+ onMouseDown={handleOverlayMouseDown}
41
+ onKeyDown={handleOverlayKeyDown}
37
42
  role="button"
38
43
  tabIndex={0}
39
44
  aria-label="Close notification"
@@ -45,7 +50,7 @@ export const Toast = ({ message, type, isVisible, onClose, duration = 4000 }: To
45
50
  <span className={styles.message}>{message}</span>
46
51
  <button
47
52
  className={styles.closeButton}
48
- onClick={onClose}
53
+ onClick={requestClose}
49
54
  aria-label="Close notification"
50
55
  >
51
56
  ×
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
2
2
  import { signOut } from 'firebase/auth';
3
3
  import { auth } from '~/services/firebase';
4
4
  import { fetchUserApi } from '~/utils/api';
5
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
5
6
  import { auditService } from '~/services/audit';
6
7
  import styles from './delete-account.module.css';
7
8
 
@@ -37,6 +38,13 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
37
38
  const [error, setError] = useState('');
38
39
  const [success, setSuccess] = useState(false);
39
40
  const [deletionProgress, setDeletionProgress] = useState<DeletionProgress>(initialDeletionProgress);
41
+ const {
42
+ handleOverlayMouseDown,
43
+ handleOverlayKeyDown
44
+ } = useOverlayDismiss({
45
+ isOpen,
46
+ onClose
47
+ });
40
48
 
41
49
  // Extract first and last name from display name
42
50
  const [firstName, lastName] = (user.displayName || '').split(' ');
@@ -170,26 +178,14 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
170
178
  };
171
179
 
172
180
  useEffect(() => {
173
- const handleEscape = (e: KeyboardEvent) => {
174
- if (e.key === 'Escape' && isOpen) {
175
- onClose();
176
- }
177
- };
178
-
179
181
  if (isOpen) {
180
- document.addEventListener('keydown', handleEscape);
181
- // Reset form when modal opens
182
182
  setUidConfirmation('');
183
183
  setEmailConfirmation('');
184
184
  setError('');
185
185
  setSuccess(false);
186
186
  setDeletionProgress(initialDeletionProgress);
187
187
  }
188
-
189
- return () => {
190
- document.removeEventListener('keydown', handleEscape);
191
- };
192
- }, [isOpen, onClose]);
188
+ }, [isOpen]);
193
189
 
194
190
  const handleDeleteAccount = async () => {
195
191
  if (!isConfirmationValid) return;
@@ -317,27 +313,10 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
317
313
  ? `Deleting case ${deletionProgress.currentCaseNumber}...`
318
314
  : 'Preparing account deletion...');
319
315
 
320
- const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
321
- if (event.target === event.currentTarget) {
322
- onClose();
323
- }
324
- };
325
-
326
- const handleOverlayKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
327
- if (event.target !== event.currentTarget) {
328
- return;
329
- }
330
-
331
- if (event.key === 'Enter' || event.key === ' ') {
332
- event.preventDefault();
333
- onClose();
334
- }
335
- };
336
-
337
316
  return (
338
317
  <div
339
318
  className={styles.modalOverlay}
340
- onClick={handleOverlayClick}
319
+ onMouseDown={handleOverlayMouseDown}
341
320
  onKeyDown={handleOverlayKeyDown}
342
321
  role="button"
343
322
  tabIndex={0}