@striae-org/striae 4.1.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +1 -1
  3. package/app/components/actions/case-export/core-export.ts +14 -8
  4. package/app/components/actions/case-export/data-processing.ts +1 -0
  5. package/app/components/actions/case-export/download-handlers.ts +7 -0
  6. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  7. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  8. package/app/components/actions/case-import/orchestrator.ts +78 -32
  9. package/app/components/actions/case-import/storage-operations.ts +97 -8
  10. package/app/components/actions/case-import/zip-processing.ts +159 -86
  11. package/app/components/actions/case-manage.ts +463 -8
  12. package/app/components/actions/confirm-export.ts +9 -2
  13. package/app/components/actions/image-manage.ts +77 -44
  14. package/app/components/audit/user-audit-viewer.tsx +19 -8
  15. package/app/components/audit/user-audit.module.css +21 -0
  16. package/app/components/audit/viewer/audit-entries-list.tsx +12 -2
  17. package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
  18. package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
  19. package/app/components/audit/viewer/use-audit-viewer-data.ts +24 -1
  20. package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
  21. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  22. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  23. package/app/components/canvas/canvas.module.css +64 -54
  24. package/app/components/canvas/canvas.tsx +14 -16
  25. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  26. package/app/components/canvas/confirmation/confirmation.tsx +12 -14
  27. package/app/components/colors/colors.module.css +4 -3
  28. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  29. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  30. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  31. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  32. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  33. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  34. package/app/components/navbar/navbar.module.css +447 -0
  35. package/app/components/navbar/navbar.tsx +402 -0
  36. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
  37. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
  38. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  39. package/app/components/sidebar/case-export/case-export.tsx +8 -46
  40. package/app/components/sidebar/case-import/case-import.module.css +23 -0
  41. package/app/components/sidebar/case-import/case-import.tsx +64 -16
  42. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  43. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  44. package/app/components/sidebar/cases/case-sidebar.tsx +68 -588
  45. package/app/components/sidebar/cases/cases-modal.module.css +1 -0
  46. package/app/components/sidebar/cases/cases-modal.tsx +82 -43
  47. package/app/components/sidebar/cases/cases.module.css +82 -21
  48. package/app/components/sidebar/files/files-modal.module.css +1 -0
  49. package/app/components/sidebar/files/files-modal.tsx +49 -52
  50. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  51. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +187 -138
  52. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  53. package/app/components/sidebar/notes/notes-editor-modal.tsx +64 -0
  54. package/app/components/sidebar/notes/notes.module.css +170 -1
  55. package/app/components/sidebar/sidebar-container.tsx +16 -28
  56. package/app/components/sidebar/sidebar.module.css +5 -69
  57. package/app/components/sidebar/sidebar.tsx +27 -125
  58. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  59. package/app/components/user/inactivity-warning.module.css +1 -0
  60. package/app/components/user/inactivity-warning.tsx +15 -2
  61. package/app/components/user/manage-profile.tsx +23 -10
  62. package/app/{tailwind.css → global.css} +1 -3
  63. package/app/hooks/useOverlayDismiss.ts +54 -4
  64. package/app/root.tsx +1 -1
  65. package/app/routes/auth/login.tsx +785 -774
  66. package/app/routes/striae/striae.module.css +10 -3
  67. package/app/routes/striae/striae.tsx +475 -30
  68. package/app/services/audit/audit.service.ts +173 -27
  69. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  70. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
  71. package/app/services/audit/builders/index.ts +1 -0
  72. package/app/types/audit.ts +4 -1
  73. package/app/types/case.ts +29 -0
  74. package/app/types/import.ts +3 -0
  75. package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
  76. package/app/utils/data/data-operations.ts +17 -861
  77. package/app/utils/data/index.ts +11 -1
  78. package/app/utils/data/operations/batch-operations.ts +113 -0
  79. package/app/utils/data/operations/case-operations.ts +168 -0
  80. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  81. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  82. package/app/utils/data/operations/index.ts +7 -0
  83. package/app/utils/data/operations/signing-operations.ts +225 -0
  84. package/app/utils/data/operations/types.ts +42 -0
  85. package/app/utils/data/operations/validation-operations.ts +48 -0
  86. package/app/utils/data/permissions.ts +16 -1
  87. package/app/utils/forensics/audit-export-signature.ts +5 -1
  88. package/app/utils/forensics/confirmation-signature.ts +3 -0
  89. package/app/utils/forensics/export-verification.ts +426 -22
  90. package/functions/api/_shared/firebase-auth.ts +2 -7
  91. package/functions/api/image/[[path]].ts +20 -23
  92. package/functions/api/pdf/[[path]].ts +27 -8
  93. package/package.json +7 -12
  94. package/scripts/deploy-primershear-emails.sh +2 -1
  95. package/worker-configuration.d.ts +3 -3
  96. package/workers/audit-worker/package.json +1 -1
  97. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  98. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  99. package/workers/data-worker/package.json +1 -1
  100. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  101. package/workers/data-worker/wrangler.jsonc.example +1 -1
  102. package/workers/image-worker/package.json +1 -1
  103. package/workers/image-worker/src/image-worker.example.ts +16 -5
  104. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  105. package/workers/image-worker/wrangler.jsonc.example +1 -1
  106. package/workers/keys-worker/package.json +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/package.json +1 -1
  110. package/workers/pdf-worker/src/formats/format-striae.ts +9 -14
  111. package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
  112. package/workers/pdf-worker/src/report-types.ts +3 -3
  113. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  114. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  115. package/workers/user-worker/package.json +1 -1
  116. package/workers/user-worker/src/user-worker.example.ts +17 -0
  117. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  118. package/workers/user-worker/wrangler.jsonc.example +1 -1
  119. package/wrangler.toml.example +1 -1
  120. package/NOTICE +0 -13
  121. package/app/components/sidebar/notes/notes-modal.tsx +0 -53
  122. package/postcss.config.js +0 -6
  123. package/public/.well-known/keybase.txt +0 -56
  124. package/tailwind.config.ts +0 -22
@@ -7,6 +7,17 @@ import { canUploadFile, getCaseData, updateCaseData, deleteFileAnnotations } fro
7
7
  import type { CaseData, FileData, ImageUploadResponse } from '~/types';
8
8
  import { auditService } from '~/services/audit';
9
9
 
10
+ export interface DeleteFileResult {
11
+ imageMissing: boolean;
12
+ fileName: string;
13
+ }
14
+
15
+ export interface DeleteFileOptions {
16
+ skipValidation?: boolean;
17
+ skipCaseDataUpdate?: boolean;
18
+ suppressAudit?: boolean;
19
+ }
20
+
10
21
  export const fetchFiles = async (
11
22
  user: User,
12
23
  caseNumber: string,
@@ -114,7 +125,13 @@ export const uploadFile = async (
114
125
  }
115
126
  };
116
127
 
117
- export const deleteFile = async (user: User, caseNumber: string, fileId: string, deleteReason: string = 'User-requested deletion via file list'): Promise<void> => {
128
+ export const deleteFile = async (
129
+ user: User,
130
+ caseNumber: string,
131
+ fileId: string,
132
+ deleteReason: string = 'User-requested deletion via file list',
133
+ options: DeleteFileOptions = {}
134
+ ): Promise<DeleteFileResult> => {
118
135
  const startTime = Date.now();
119
136
 
120
137
  // Get file info for audit logging (outside try block so it's available in catch)
@@ -123,7 +140,9 @@ export const deleteFile = async (user: User, caseNumber: string, fileId: string,
123
140
 
124
141
  try {
125
142
  // Get the case data using centralized function
126
- const caseData = await getCaseData(user, caseNumber);
143
+ const caseData = await getCaseData(user, caseNumber, {
144
+ skipValidation: options.skipValidation === true
145
+ });
127
146
  if (!caseData) {
128
147
  throw new Error('Case not found');
129
148
  }
@@ -133,6 +152,7 @@ export const deleteFile = async (user: User, caseNumber: string, fileId: string,
133
152
  const fileSize = 0; // We don't store file size, so use 0
134
153
 
135
154
  let imageDeleteFailed = false;
155
+ let imageMissing = false;
136
156
  let imageDeleteError = '';
137
157
 
138
158
  // Attempt to delete image file
@@ -145,6 +165,7 @@ export const deleteFile = async (user: User, caseNumber: string, fileId: string,
145
165
  if (imageResponse.status === 404) {
146
166
  // Image already doesn't exist - proceed with data cleanup
147
167
  console.warn(`Image ${fileId} not found (404) - proceeding with data cleanup`);
168
+ imageMissing = true;
148
169
  } else {
149
170
  // Other errors should still fail the operation
150
171
  imageDeleteFailed = true;
@@ -160,64 +181,76 @@ export const deleteFile = async (user: User, caseNumber: string, fileId: string,
160
181
  // Clean up data files regardless of image deletion success/404
161
182
  // Try to delete notes file using centralized function
162
183
  try {
163
- await deleteFileAnnotations(user, caseNumber, fileId);
184
+ await deleteFileAnnotations(user, caseNumber, fileId, {
185
+ skipValidation: options.skipValidation === true
186
+ });
164
187
  } catch (error) {
165
188
  // Ignore 404 errors - notes file might not exist
166
189
  console.log('Notes file deletion result:', error);
167
190
  }
168
191
 
169
- // Update case data.json to remove file reference using centralized function
170
- const updatedData: CaseData = {
171
- ...caseData,
172
- files: (caseData.files || []).filter((f: FileData) => f.id !== fileId)
173
- };
192
+ if (options.skipCaseDataUpdate !== true) {
193
+ // Update case data.json to remove file reference using centralized function
194
+ const updatedData: CaseData = {
195
+ ...caseData,
196
+ files: (caseData.files || []).filter((f: FileData) => f.id !== fileId)
197
+ };
174
198
 
175
- await updateCaseData(user, caseNumber, updatedData);
199
+ await updateCaseData(user, caseNumber, updatedData);
200
+ }
176
201
 
177
202
  // Log successful file deletion
178
203
  const endTime = Date.now();
179
- try {
180
- await auditService.logFileDeletion(
181
- user,
182
- fileName,
183
- fileSize,
184
- deleteReason,
185
- caseNumber,
186
- fileId,
187
- fileToDelete?.originalFilename
188
- );
189
- } catch (auditError) {
190
- console.error('Failed to log file deletion:', auditError);
204
+ if (options.suppressAudit !== true) {
205
+ try {
206
+ await auditService.logFileDeletion(
207
+ user,
208
+ fileName,
209
+ fileSize,
210
+ deleteReason,
211
+ caseNumber,
212
+ fileId,
213
+ fileToDelete?.originalFilename
214
+ );
215
+ } catch (auditError) {
216
+ console.error('Failed to log file deletion:', auditError);
217
+ }
191
218
  }
192
219
 
193
220
  console.log(`✅ File deleted: ${fileName} (${endTime - startTime}ms)`);
221
+ return {
222
+ imageMissing,
223
+ fileName
224
+ };
194
225
 
195
226
  } catch (error) {
196
227
  // Log failed file deletion
197
228
  const endTime = Date.now();
198
- try {
199
- await auditService.logEvent({
200
- userId: user.uid,
201
- userEmail: user.email || '',
202
- action: 'file-delete',
203
- result: 'failure',
204
- fileName: fileName, // Now uses the original filename
205
- fileType: 'unknown',
206
- validationErrors: [error instanceof Error ? error.message : 'Unknown error'],
207
- caseNumber,
208
- fileDetails: {
209
- fileId: fileId,
210
- fileSize: 0,
211
- deleteReason: 'Failed deletion attempt',
212
- originalFileName: fileToDelete?.originalFilename
213
- },
214
- performanceMetrics: {
215
- processingTimeMs: endTime - startTime,
216
- fileSizeBytes: 0
217
- }
218
- });
219
- } catch (auditError) {
220
- console.error('Failed to log file deletion failure:', auditError);
229
+ if (options.suppressAudit !== true) {
230
+ try {
231
+ await auditService.logEvent({
232
+ userId: user.uid,
233
+ userEmail: user.email || '',
234
+ action: 'file-delete',
235
+ result: 'failure',
236
+ fileName: fileName, // Now uses the original filename
237
+ fileType: 'unknown',
238
+ validationErrors: [error instanceof Error ? error.message : 'Unknown error'],
239
+ caseNumber,
240
+ fileDetails: {
241
+ fileId: fileId,
242
+ fileSize: 0,
243
+ deleteReason: 'Failed deletion attempt',
244
+ originalFileName: fileToDelete?.originalFilename
245
+ },
246
+ performanceMetrics: {
247
+ processingTimeMs: endTime - startTime,
248
+ fileSizeBytes: 0
249
+ }
250
+ });
251
+ } catch (auditError) {
252
+ console.error('Failed to log file deletion failure:', auditError);
253
+ }
221
254
  }
222
255
 
223
256
  console.error('Error in deleteFile:', error);
@@ -58,6 +58,8 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
58
58
  error,
59
59
  setError,
60
60
  auditTrail,
61
+ isArchivedReadOnlyCase,
62
+ bundledAuditWarning,
61
63
  loadAuditData
62
64
  } = useAuditViewerData({
63
65
  isOpen,
@@ -84,8 +86,8 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
84
86
  });
85
87
 
86
88
  const {
87
- handleOverlayMouseDown,
88
- handleOverlayKeyDown
89
+ requestClose,
90
+ overlayProps
89
91
  } = useOverlayDismiss({
90
92
  isOpen,
91
93
  onClose
@@ -98,11 +100,8 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
98
100
  return (
99
101
  <div
100
102
  className={styles.overlay}
101
- onMouseDown={handleOverlayMouseDown}
102
- onKeyDown={handleOverlayKeyDown}
103
- role="button"
104
- tabIndex={0}
105
103
  aria-label="Close audit trail dialog"
104
+ {...overlayProps}
106
105
  >
107
106
  <div className={styles.modal}>
108
107
  <AuditViewerHeader
@@ -111,7 +110,7 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
111
110
  onExportCSV={handleExportCSV}
112
111
  onExportJSON={handleExportJSON}
113
112
  onGenerateReport={handleGenerateReport}
114
- onClose={onClose}
113
+ onClose={requestClose}
115
114
  />
116
115
 
117
116
  <div className={styles.content}>
@@ -133,6 +132,14 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
133
132
 
134
133
  {!loading && !error && (
135
134
  <>
135
+ {isArchivedReadOnlyCase && (
136
+ <div className={bundledAuditWarning ? styles.archivedWarning : styles.archivedNotice}>
137
+ <p>
138
+ {bundledAuditWarning || 'Viewing bundled audit trail data from this imported archived case package.'}
139
+ </p>
140
+ </div>
141
+ )}
142
+
136
143
  {/* User Information Section */}
137
144
  {user && (
138
145
  <AuditUserInfoCard user={user} userData={userData} userBadgeId={userBadgeId} />
@@ -183,7 +190,11 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
183
190
 
184
191
  {auditEntries.length === 0 && !loading && !error && (
185
192
  <div className={styles.noData}>
186
- <p>No audit trail available. Your activities will appear here as you use Striae.</p>
193
+ <p>
194
+ {isArchivedReadOnlyCase
195
+ ? 'No bundled audit trail entries are available for this imported archived case.'
196
+ : 'No audit trail available. Your activities will appear here as you use Striae.'}
197
+ </p>
187
198
  </div>
188
199
  )}
189
200
  </div>
@@ -101,6 +101,27 @@
101
101
  padding: 20px;
102
102
  }
103
103
 
104
+ .archivedNotice,
105
+ .archivedWarning {
106
+ margin-bottom: 16px;
107
+ padding: 12px 14px;
108
+ border-radius: 6px;
109
+ border: 1px solid;
110
+ font-size: 0.9rem;
111
+ }
112
+
113
+ .archivedNotice {
114
+ background: color-mix(in lab, var(--primary) 8%, transparent);
115
+ border-color: color-mix(in lab, var(--primary) 35%, transparent);
116
+ color: var(--textBody);
117
+ }
118
+
119
+ .archivedWarning {
120
+ background: color-mix(in lab, var(--warning) 12%, transparent);
121
+ border-color: color-mix(in lab, var(--warning) 40%, transparent);
122
+ color: var(--text);
123
+ }
124
+
104
125
  /* Loading & Error States */
105
126
  .loading,
106
127
  .error {
@@ -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>
@@ -44,6 +47,13 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
44
47
  </div>
45
48
  )}
46
49
 
50
+ {entry.action === 'confirmation-import' && entry.details.reviewerBadgeId && (
51
+ <div className={styles.detailRow}>
52
+ <span className={styles.detailLabel}>Reviewer Badge/ID:</span>
53
+ <span className={styles.badgeTag}>{entry.details.reviewerBadgeId}</span>
54
+ </div>
55
+ )}
56
+
47
57
  {entry.result === 'failure' && entry.details.validationErrors.length > 0 && (
48
58
  <div className={styles.detailRow}>
49
59
  <span className={styles.detailLabel}>Error:</span>
@@ -259,6 +259,7 @@ export const AuditFiltersPanel = ({
259
259
  <option value="case-create">Case Create</option>
260
260
  <option value="case-rename">Case Rename</option>
261
261
  <option value="case-delete">Case Delete</option>
262
+ <option value="case-archive">Case Archive</option>
262
263
  <option value="case-export">Case Export</option>
263
264
  <option value="case-import">Case Import</option>
264
265
  </optgroup>
@@ -32,6 +32,8 @@ export const getAuditActionIcon = (action: AuditAction): string => {
32
32
  return '✏️';
33
33
  case 'case-delete':
34
34
  return '🗑️';
35
+ case 'case-archive':
36
+ return '📦';
35
37
  case 'case-export':
36
38
  return '📤';
37
39
  case 'case-import':
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
2
2
  import type { User } from 'firebase/auth';
3
3
  import { auditService } from '~/services/audit';
4
4
  import { type AuditTrail, type UserData, type ValidationAuditEntry, type WorkflowPhase } from '~/types';
5
- import { getUserData } from '~/utils/data';
5
+ import { getCaseData, getUserData } from '~/utils/data';
6
6
  import type { DateRangeFilter } from './types';
7
7
 
8
8
  const isWorkflowPhase = (phase: unknown): phase is WorkflowPhase =>
@@ -82,6 +82,8 @@ export const useAuditViewerData = ({
82
82
  const [loading, setLoading] = useState(false);
83
83
  const [error, setError] = useState<string>('');
84
84
  const [auditTrail, setAuditTrail] = useState<AuditTrail | null>(null);
85
+ const [isArchivedReadOnlyCase, setIsArchivedReadOnlyCase] = useState(false);
86
+ const [bundledAuditWarning, setBundledAuditWarning] = useState<string>('');
85
87
 
86
88
  const loadUserData = useCallback(async () => {
87
89
  if (!user) {
@@ -103,11 +105,30 @@ export const useAuditViewerData = ({
103
105
 
104
106
  setLoading(true);
105
107
  setError('');
108
+ setBundledAuditWarning('');
106
109
 
107
110
  try {
108
111
  const { startDate, endDate } = buildAuditDateQuery(dateRange, customStartDate, customEndDate);
109
112
 
113
+ if (effectiveCaseNumber) {
114
+ const caseData = await getCaseData(user, effectiveCaseNumber);
115
+ const isArchiveBundleCase = Boolean(
116
+ caseData?.archived === true &&
117
+ caseData?.bundledAuditTrail?.source === 'archive-bundle'
118
+ );
119
+ setIsArchivedReadOnlyCase(isArchiveBundleCase);
120
+
121
+ if (isArchiveBundleCase && !Array.isArray(caseData?.bundledAuditTrail?.entries)) {
122
+ setBundledAuditWarning(
123
+ 'This imported archived case does not include bundled audit trail data. No audit entries are available for this case.'
124
+ );
125
+ }
126
+ } else {
127
+ setIsArchivedReadOnlyCase(false);
128
+ }
129
+
110
130
  const entries = await auditService.getAuditEntriesForUser(user.uid, {
131
+ requestingUser: user,
111
132
  caseNumber: effectiveCaseNumber,
112
133
  startDate,
113
134
  endDate,
@@ -161,6 +182,8 @@ export const useAuditViewerData = ({
161
182
  error,
162
183
  setError,
163
184
  auditTrail,
185
+ isArchivedReadOnlyCase,
186
+ bundledAuditWarning,
164
187
  loadAuditData
165
188
  };
166
189
  };
@@ -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 {
@@ -59,16 +59,17 @@
59
59
 
60
60
  .annotationLabel {
61
61
  position: absolute;
62
- bottom: -25px;
62
+ bottom: -30px;
63
63
  left: 0;
64
64
  background: rgba(0, 0, 0, 0.8);
65
65
  color: white;
66
- padding: 2px 6px;
67
- font-size: 12px;
68
- border-radius: 3px;
66
+ padding: 4px 10px;
67
+ font-size: 13px;
68
+ line-height: 1.3;
69
+ border-radius: 4px;
69
70
  white-space: nowrap;
70
71
  pointer-events: none;
71
- max-width: 200px;
72
+ max-width: 260px;
72
73
  overflow: hidden;
73
74
  text-overflow: ellipsis;
74
75
  }
@@ -82,33 +83,33 @@
82
83
  background: white;
83
84
  border: 1px solid #ccc;
84
85
  border-radius: 6px;
85
- padding: 12px;
86
+ padding: 16px;
86
87
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
87
- min-width: 200px;
88
+ min-width: 250px;
88
89
  }
89
90
 
90
91
  .labelDialogTitle {
91
- font-size: 14px;
92
+ font-size: 15px;
92
93
  font-weight: 600;
93
- margin-bottom: 6px;
94
+ margin-bottom: 8px;
94
95
  color: #333;
95
96
  }
96
97
 
97
98
  .labelDialogNote {
98
- font-size: 12px;
99
+ font-size: 13px;
99
100
  color: #666;
100
- margin-bottom: 10px;
101
+ margin-bottom: 12px;
101
102
  font-style: italic;
102
- line-height: 1.3;
103
+ line-height: 1.4;
103
104
  }
104
105
 
105
106
  .labelInput {
106
107
  width: 100%;
107
- padding: 6px 8px;
108
+ padding: 8px 10px;
108
109
  border: 1px solid #ccc;
109
110
  border-radius: 4px;
110
111
  font-size: 14px;
111
- margin-bottom: 10px;
112
+ margin-bottom: 12px;
112
113
  box-sizing: border-box;
113
114
  }
114
115
 
@@ -120,18 +121,21 @@
120
121
 
121
122
  .labelDialogButtons {
122
123
  display: flex;
123
- gap: 8px;
124
+ gap: 10px;
124
125
  justify-content: flex-end;
126
+ margin-top: 8px;
125
127
  }
126
128
 
127
129
  .labelConfirmButton,
128
130
  .labelCancelButton {
129
- padding: 6px 12px;
131
+ padding: 10px 18px;
130
132
  border: none;
131
133
  border-radius: 4px;
132
- font-size: 12px;
134
+ font-size: 14px;
135
+ font-weight: 500;
133
136
  cursor: pointer;
134
137
  transition: background-color 0.2s ease;
138
+ min-width: 88px;
135
139
  }
136
140
 
137
141
  .labelConfirmButton {
@@ -167,4 +171,4 @@
167
171
 
168
172
  .readOnlyAnnotation:hover::after {
169
173
  display: none !important; /* Hide the delete button for read-only annotations */
170
- }
174
+ }
@@ -89,6 +89,7 @@ export const BoxAnnotations = ({
89
89
 
90
90
  // Ref to track if component is mounted to prevent state updates after unmount
91
91
  const isMountedRef = useRef(true);
92
+ const labelInputRef = useRef<HTMLInputElement>(null);
92
93
 
93
94
  useEffect(() => {
94
95
  return () => {
@@ -96,6 +97,19 @@ export const BoxAnnotations = ({
96
97
  };
97
98
  }, []);
98
99
 
100
+ useEffect(() => {
101
+ if (!labelDialog.isVisible) return;
102
+
103
+ const focusFrame = window.requestAnimationFrame(() => {
104
+ labelInputRef.current?.focus();
105
+ labelInputRef.current?.select();
106
+ });
107
+
108
+ return () => {
109
+ window.cancelAnimationFrame(focusFrame);
110
+ };
111
+ }, [labelDialog.isVisible]);
112
+
99
113
  // Memoized function to get relative coordinates (more stable reference)
100
114
  const getRelativeCoordinates = useCallback((e: React.MouseEvent): { x: number; y: number } => {
101
115
  const imageElement = imageRef.current;
@@ -602,6 +616,7 @@ export const BoxAnnotations = ({
602
616
  }
603
617
  </div>
604
618
  <input
619
+ ref={labelInputRef}
605
620
  type="text"
606
621
  value={labelDialog.label}
607
622
  onChange={handleLabelChange}