@striae-org/striae 3.2.1 → 3.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 (104) hide show
  1. package/app/components/actions/case-export/core-export.ts +2 -2
  2. package/app/components/actions/case-export/data-processing.ts +19 -4
  3. package/app/components/actions/case-export/download-handlers.ts +57 -8
  4. package/app/components/actions/case-export/metadata-helpers.ts +1 -1
  5. package/app/components/actions/case-import/annotation-import.ts +2 -2
  6. package/app/components/actions/case-import/confirmation-import.ts +44 -20
  7. package/app/components/actions/case-import/confirmation-package.ts +86 -0
  8. package/app/components/actions/case-import/image-operations.ts +1 -1
  9. package/app/components/actions/case-import/index.ts +1 -0
  10. package/app/components/actions/case-import/orchestrator.ts +16 -6
  11. package/app/components/actions/case-import/storage-operations.ts +7 -7
  12. package/app/components/actions/case-import/validation.ts +7 -100
  13. package/app/components/actions/case-import/zip-processing.ts +47 -5
  14. package/app/components/actions/case-manage.ts +3 -3
  15. package/app/components/actions/confirm-export.ts +47 -16
  16. package/app/components/actions/generate-pdf.ts +3 -3
  17. package/app/components/actions/image-manage.ts +3 -3
  18. package/app/components/actions/notes-manage.ts +3 -3
  19. package/app/components/actions/signout.tsx +1 -1
  20. package/app/components/audit/user-audit-viewer.tsx +2 -3
  21. package/app/components/auth/auth-provider.tsx +2 -2
  22. package/app/components/auth/mfa-enrollment.tsx +3 -3
  23. package/app/components/auth/mfa-verification.tsx +4 -4
  24. package/app/components/canvas/box-annotations/box-annotations.tsx +2 -2
  25. package/app/components/canvas/canvas.tsx +1 -1
  26. package/app/components/canvas/confirmation/confirmation.tsx +1 -1
  27. package/app/components/form/form-button.tsx +1 -1
  28. package/app/components/form/form.module.css +9 -0
  29. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +163 -49
  30. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +365 -88
  31. package/app/components/sidebar/case-export/case-export.tsx +2 -54
  32. package/app/components/sidebar/case-import/case-import.tsx +20 -8
  33. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +1 -1
  34. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +1 -1
  35. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +9 -7
  36. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -2
  37. package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
  38. package/app/components/sidebar/cases/case-sidebar.tsx +106 -50
  39. package/app/components/sidebar/cases/cases-modal.tsx +1 -1
  40. package/app/components/sidebar/cases/cases.module.css +101 -18
  41. package/app/components/sidebar/files/files-modal.tsx +3 -2
  42. package/app/components/sidebar/notes/notes-sidebar.tsx +3 -3
  43. package/app/components/sidebar/notes/notes.module.css +33 -13
  44. package/app/components/sidebar/sidebar-container.tsx +4 -3
  45. package/app/components/sidebar/sidebar.tsx +2 -2
  46. package/app/components/sidebar/upload/image-upload-zone.tsx +2 -2
  47. package/app/components/theme-provider/theme-provider.tsx +1 -1
  48. package/app/components/user/delete-account.tsx +1 -1
  49. package/app/components/user/manage-profile.tsx +3 -3
  50. package/app/components/user/mfa-phone-update.tsx +17 -14
  51. package/app/contexts/auth.context.ts +1 -1
  52. package/app/root.tsx +2 -2
  53. package/app/routes/auth/emailActionHandler.tsx +2 -2
  54. package/app/routes/auth/emailVerification.tsx +2 -2
  55. package/app/routes/auth/login.tsx +134 -11
  56. package/app/routes/auth/passwordReset.tsx +2 -2
  57. package/app/routes/striae/striae.tsx +2 -2
  58. package/app/services/audit/audit-console-logger.ts +46 -0
  59. package/app/services/audit/audit-export-csv.ts +126 -0
  60. package/app/services/audit/audit-export-report.ts +174 -0
  61. package/app/services/audit/audit-export-signing.ts +85 -0
  62. package/app/services/audit/audit-export.service.ts +334 -0
  63. package/app/services/audit/audit-file-type.ts +13 -0
  64. package/app/services/audit/audit-query-helpers.ts +88 -0
  65. package/app/services/audit/audit-worker-client.ts +95 -0
  66. package/app/services/audit/audit.service.ts +990 -0
  67. package/app/services/audit/builders/audit-entry-builder.ts +32 -0
  68. package/app/services/audit/builders/audit-event-builders-annotation.ts +150 -0
  69. package/app/services/audit/builders/audit-event-builders-case-file.ts +249 -0
  70. package/app/services/audit/builders/audit-event-builders-user-security.ts +449 -0
  71. package/app/services/audit/builders/audit-event-builders-workflow.ts +272 -0
  72. package/app/services/audit/builders/index.ts +40 -0
  73. package/app/services/audit/index.ts +2 -0
  74. package/app/types/case.ts +2 -2
  75. package/app/types/exceljs-bare.d.ts +3 -1
  76. package/app/types/user.ts +1 -1
  77. package/app/utils/SHA256.ts +5 -1
  78. package/app/utils/audit-export-signature.ts +2 -2
  79. package/app/utils/confirmation-signature.ts +8 -4
  80. package/app/utils/data-operations.ts +5 -5
  81. package/app/utils/export-verification.ts +353 -0
  82. package/app/utils/mfa-phone.ts +1 -1
  83. package/app/utils/mfa.ts +1 -1
  84. package/app/utils/permissions.ts +2 -2
  85. package/app/utils/signature-utils.ts +74 -4
  86. package/package.json +11 -9
  87. package/public/favicon.ico +0 -0
  88. package/public/icon-256.png +0 -0
  89. package/public/icon-512.png +0 -0
  90. package/public/manifest.json +39 -0
  91. package/public/shortcut.png +0 -0
  92. package/public/social-image.png +0 -0
  93. package/react-router.config.ts +5 -0
  94. package/worker-configuration.d.ts +4435 -562
  95. package/workers/data-worker/src/data-worker.example.ts +3 -3
  96. package/workers/pdf-worker/scripts/generate-assets.js +94 -0
  97. package/workers/pdf-worker/src/{generated-assets.ts → assets/generated-assets.ts} +117 -117
  98. package/workers/pdf-worker/src/{format-striae.ts → formats/format-striae.ts} +535 -535
  99. package/workers/pdf-worker/src/pdf-worker.example.ts +1 -1
  100. package/app/services/audit-export.service.ts +0 -755
  101. package/app/services/audit.service.ts +0 -1474
  102. package/public/favicon.svg +0 -9
  103. /package/app/services/{firebase-errors.ts → firebase/errors.ts} +0 -0
  104. /package/app/services/{firebase.ts → firebase/index.ts} +0 -0
@@ -1,8 +1,8 @@
1
1
  import { useState, useCallback } from 'react';
2
- import { User } from 'firebase/auth';
3
- import { previewCaseImport } from '~/components/actions/case-review';
4
- import { CaseImportPreview } from '~/types';
5
- import { ConfirmationPreview } from '../components/ConfirmationPreviewSection';
2
+ import type { User } from 'firebase/auth';
3
+ import { previewCaseImport, extractConfirmationImportPackage } from '~/components/actions/case-review';
4
+ import { type CaseImportPreview } from '~/types';
5
+ import { type ConfirmationPreview } from '../components/ConfirmationPreviewSection';
6
6
 
7
7
  type UnknownRecord = Record<string, unknown>;
8
8
 
@@ -56,8 +56,8 @@ export const useFilePreview = (
56
56
 
57
57
  setIsLoadingPreview(true);
58
58
  try {
59
- const text = await file.text();
60
- const parsed = JSON.parse(text) as unknown;
59
+ const { confirmationData } = await extractConfirmationImportPackage(file);
60
+ const parsed = confirmationData as unknown;
61
61
 
62
62
  if (!isRecord(parsed)) {
63
63
  throw new Error('Invalid confirmation data format');
@@ -104,7 +104,9 @@ export const useFilePreview = (
104
104
  setConfirmationPreview(preview);
105
105
  } catch (error) {
106
106
  console.error('Error loading confirmation preview:', error);
107
- setError(`Failed to read confirmation data: ${error instanceof Error ? error.message : 'Invalid JSON format'}`);
107
+ setError(
108
+ `Failed to read confirmation data: ${error instanceof Error ? error.message : 'Invalid confirmation package format'}`
109
+ );
108
110
  clearImportData();
109
111
  } finally {
110
112
  setIsLoadingPreview(false);
@@ -1,7 +1,7 @@
1
1
  import { useCallback } from 'react';
2
- import { User } from 'firebase/auth';
2
+ import type { User } from 'firebase/auth';
3
3
  import { importCaseForReview, importConfirmationData } from '~/components/actions/case-review';
4
- import { ImportResult, ConfirmationImportResult } from '~/types';
4
+ import { type ImportResult, type ConfirmationImportResult } from '~/types';
5
5
 
6
6
  interface ProgressState {
7
7
  stage: string;
@@ -1,5 +1,14 @@
1
1
  import { isConfirmationDataFile } from '~/components/actions/case-review';
2
2
 
3
+ const CASE_EXPORT_DATA_FILE_REGEX = /_data\.(json|csv)$/i;
4
+ const CONFIRMATION_EXPORT_FILE_REGEX = /^confirmation-data-.*\.json$/i;
5
+ const FORENSIC_MANIFEST_FILE_NAME = 'forensic_manifest.json';
6
+
7
+ function getLeafFileName(path: string): string {
8
+ const segments = path.split('/').filter(Boolean);
9
+ return segments.length > 0 ? segments[segments.length - 1] : path;
10
+ }
11
+
3
12
  /**
4
13
  * Check if a file is a valid ZIP file
5
14
  */
@@ -13,8 +22,10 @@ export const isValidZipFile = (file: File): boolean => {
13
22
  * Check if a file is a valid confirmation JSON file
14
23
  */
15
24
  export const isValidConfirmationFile = (file: File): boolean => {
16
- return file.type === 'application/json' &&
17
- isConfirmationDataFile(file.name);
25
+ const lowerName = file.name.toLowerCase();
26
+ const jsonType = file.type === 'application/json' || file.type === '';
27
+
28
+ return lowerName.endsWith('.json') && jsonType && isConfirmationDataFile(file.name);
18
29
  };
19
30
 
20
31
  /**
@@ -33,6 +44,50 @@ export const getImportType = (file: File): 'case' | 'confirmation' | null => {
33
44
  return null;
34
45
  };
35
46
 
47
+ /**
48
+ * Resolve import type, including ZIP package inspection.
49
+ * Case ZIPs are identified by case data files or FORENSIC_MANIFEST.json.
50
+ * Confirmation ZIPs are identified by confirmation-data-*.json.
51
+ */
52
+ export const resolveImportType = async (file: File): Promise<'case' | 'confirmation' | null> => {
53
+ if (isValidConfirmationFile(file)) {
54
+ return 'confirmation';
55
+ }
56
+
57
+ if (!isValidZipFile(file)) {
58
+ return null;
59
+ }
60
+
61
+ try {
62
+ const JSZip = (await import('jszip')).default;
63
+ const zip = await JSZip.loadAsync(file);
64
+ const fileEntries = Object.keys(zip.files).filter((path) => !zip.files[path].dir);
65
+
66
+ const hasCaseData = fileEntries.some((path) =>
67
+ CASE_EXPORT_DATA_FILE_REGEX.test(getLeafFileName(path))
68
+ );
69
+ const hasManifest = fileEntries.some(
70
+ (path) => getLeafFileName(path).toLowerCase() === FORENSIC_MANIFEST_FILE_NAME
71
+ );
72
+
73
+ if (hasCaseData || hasManifest) {
74
+ return 'case';
75
+ }
76
+
77
+ const hasConfirmationData = fileEntries.some((path) =>
78
+ CONFIRMATION_EXPORT_FILE_REGEX.test(getLeafFileName(path))
79
+ );
80
+
81
+ if (hasConfirmationData) {
82
+ return 'confirmation';
83
+ }
84
+
85
+ return null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ };
90
+
36
91
  /**
37
92
  * Reset file input element
38
93
  */
@@ -1,9 +1,11 @@
1
- import { User } from 'firebase/auth';
1
+ import type { User } from 'firebase/auth';
2
+ import type * as CaseExportActions from '../../actions/case-export';
2
3
  import { useState, useEffect, useMemo, useCallback } from 'react';
3
4
  import styles from './cases.module.css';
5
+ import { Toast } from '~/components/toast/toast';
4
6
  import { CasesModal } from './cases-modal';
5
7
  import { FilesModal } from '../files/files-modal';
6
- import { CaseExport, ExportFormat } from '../case-export/case-export';
8
+ import { CaseExport, type ExportFormat } from '../case-export/case-export';
7
9
  import { ImageUploadZone } from '../upload/image-upload-zone';
8
10
  import { UserAuditViewer } from '~/components/audit/user-audit-viewer';
9
11
  import {
@@ -27,7 +29,7 @@ import {
27
29
  getUserData
28
30
  } from '~/utils/permissions';
29
31
  import { getFileAnnotations } from '~/utils/data-operations';
30
- import { FileData, CaseActionType } from '~/types';
32
+ import { type FileData, type CaseActionType } from '~/types';
31
33
 
32
34
  interface CaseSidebarProps {
33
35
  user: User;
@@ -57,7 +59,7 @@ interface CaseSidebarProps {
57
59
 
58
60
  const SUCCESS_MESSAGE_TIMEOUT = 3000;
59
61
 
60
- type CaseExportActionsModule = typeof import('../../actions/case-export');
62
+ type CaseExportActionsModule = typeof CaseExportActions;
61
63
 
62
64
  let caseExportActionsPromise: Promise<CaseExportActionsModule> | null = null;
63
65
 
@@ -102,7 +104,11 @@ export const CaseSidebar = ({
102
104
  const [, setFileError] = useState('');
103
105
  const [newCaseName, setNewCaseName] = useState('');
104
106
  const [showCaseActions, setShowCaseActions] = useState(false);
107
+ const [showCaseManagement, setShowCaseManagement] = useState(false);
105
108
  const [canCreateNewCase, setCanCreateNewCase] = useState(true);
109
+ const [isToastVisible, setIsToastVisible] = useState(false);
110
+ const [toastMessage, setToastMessage] = useState('');
111
+ const [toastType, setToastType] = useState<'success' | 'error' | 'warning'>('success');
106
112
  const [canUploadNewFile, setCanUploadNewFile] = useState(true);
107
113
  const [createCaseError, setCreateCaseError] = useState('');
108
114
  const [uploadFileError, setUploadFileError] = useState('');
@@ -312,6 +318,24 @@ export const CaseSidebar = ({
312
318
  isCancelled = true;
313
319
  };
314
320
  }, [currentCase, fileIdsKey, user, selectedFileId, confirmationSaveVersion, files.length, calculateCaseConfirmationStatus]);
321
+
322
+ useEffect(() => {
323
+ if (error) {
324
+ setToastMessage(error);
325
+ setToastType('error');
326
+ setIsToastVisible(true);
327
+ }
328
+ }, [error]);
329
+
330
+ useEffect(() => {
331
+ if (successAction) {
332
+ setToastMessage(`Case ${currentCase} ${successAction} successfully!`);
333
+ setToastType('success');
334
+ setIsToastVisible(true);
335
+ }
336
+ // currentCase intentionally omitted: we capture its value at the time successAction changes
337
+ // eslint-disable-next-line react-hooks/exhaustive-deps
338
+ }, [successAction]);
315
339
 
316
340
  const handleCase = async () => {
317
341
  setIsLoading(true);
@@ -335,6 +359,7 @@ export const CaseSidebar = ({
335
359
  setFiles(files);
336
360
  setCaseNumber('');
337
361
  setSuccessAction('loaded');
362
+ setShowCaseManagement(false);
338
363
  setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
339
364
  return;
340
365
  }
@@ -361,6 +386,7 @@ export const CaseSidebar = ({
361
386
  setFiles([]);
362
387
  setCaseNumber('');
363
388
  setSuccessAction('created');
389
+ setShowCaseManagement(false);
364
390
  setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
365
391
 
366
392
  // Refresh permissions after successful case creation
@@ -563,53 +589,73 @@ const handleImageSelect = (file: FileData) => {
563
589
  };
564
590
 
565
591
  return (
592
+ <>
566
593
  <div className={styles.caseSection}>
567
- <div className={styles.caseSection}>
568
- <h4>Case Management</h4>
569
- {limitsDescription && (
570
- <p className={styles.limitsInfo}>
571
- {limitsDescription}
572
- </p>
573
- )}
574
- <div className={`${styles.caseInput} mb-4`}>
575
- <input
576
- type="text"
577
- value={caseNumber}
578
- onChange={(e) => setCaseNumber(e.target.value)}
579
- placeholder="Case #"
580
- />
581
- </div>
594
+ {currentCase && !showCaseManagement ? (
582
595
  <div className={`${styles.caseLoad} mb-4`}>
583
- <button
584
- onClick={handleCase}
585
- disabled={isLoading || !caseNumber || permissionChecking || (isReadOnly && !!currentCase) || isUploading}
586
- title={
587
- isUploading
588
- ? "Cannot load/create cases while uploading files"
589
- : (isReadOnly && currentCase)
590
- ? "Cannot load/create cases while reviewing a read-only case. Clear the current case first."
591
- : (!canCreateNewCase ? createCaseError : undefined)
592
- }
593
- >
594
- {isLoading ? 'Loading...' : permissionChecking ? 'Checking permissions...' : 'Load/Create Case'}
595
- </button>
596
- </div>
597
- <div className={styles.caseInput}>
598
- <button
599
- onClick={() => setIsModalOpen(true)}
600
- className={styles.listButton}
601
- disabled={isUploading}
602
- title={isUploading ? "Cannot list cases while uploading files" : undefined}
603
- >
604
- List All Cases
605
- </button>
606
- </div>
607
- {error && <p className={styles.error}>{error}</p>}
608
- {successAction && (
609
- <p className={styles.success}>
610
- Case {currentCase} {successAction} successfully!
611
- </p>
612
- )}
596
+ <button
597
+ className={styles.switchCaseButton}
598
+ onClick={() => setShowCaseManagement(true)}
599
+ disabled={isUploading}
600
+ title={isUploading ? "Cannot switch cases while uploading files" : undefined}
601
+ >
602
+ Switch Case
603
+ </button>
604
+ </div>
605
+ ) : (
606
+ <>
607
+ <h4>Case Management</h4>
608
+ {limitsDescription && (
609
+ <p className={styles.limitsInfo}>
610
+ {limitsDescription}
611
+ </p>
612
+ )}
613
+ <div className={`${styles.caseInput} mb-4`}>
614
+ <input
615
+ type="text"
616
+ value={caseNumber}
617
+ onChange={(e) => setCaseNumber(e.target.value)}
618
+ placeholder="Case #"
619
+ />
620
+ </div>
621
+ <div className={`${styles.caseLoad} mb-4`}>
622
+ <button
623
+ onClick={handleCase}
624
+ disabled={isLoading || !caseNumber || permissionChecking || (isReadOnly && !!currentCase) || isUploading}
625
+ title={
626
+ isUploading
627
+ ? "Cannot load/create cases while uploading files"
628
+ : (isReadOnly && currentCase)
629
+ ? "Cannot load/create cases while reviewing a read-only case. Clear the current case first."
630
+ : (!canCreateNewCase ? createCaseError : undefined)
631
+ }
632
+ >
633
+ {isLoading ? 'Loading...' : permissionChecking ? 'Checking permissions...' : 'Load/Create Case'}
634
+ </button>
635
+ </div>
636
+ <div className={styles.caseInput}>
637
+ <button
638
+ onClick={() => setIsModalOpen(true)}
639
+ className={styles.listButton}
640
+ disabled={isUploading}
641
+ title={isUploading ? "Cannot list cases while uploading files" : undefined}
642
+ >
643
+ List All Cases
644
+ </button>
645
+ </div>
646
+ {currentCase && (
647
+ <div className="mb-4">
648
+ <button
649
+ className={styles.cancelSwitchButton}
650
+ onClick={() => setShowCaseManagement(false)}
651
+ disabled={isUploading}
652
+ >
653
+ Cancel
654
+ </button>
655
+ </div>
656
+ )}
657
+ </>
658
+ )}
613
659
  <CasesModal
614
660
  isOpen={isModalOpen}
615
661
  onClose={() => setIsModalOpen(false)}
@@ -829,6 +875,16 @@ return (
829
875
  />
830
876
 
831
877
  </div>
832
- </div>
878
+ <Toast
879
+ message={toastMessage}
880
+ type={toastType}
881
+ isVisible={isToastVisible}
882
+ onClose={() => {
883
+ setIsToastVisible(false);
884
+ setError('');
885
+ setSuccessAction(null);
886
+ }}
887
+ />
888
+ </>
833
889
  );
834
890
  };
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect } from 'react';
2
- import { User } from 'firebase/auth';
2
+ import type { User } from 'firebase/auth';
3
3
  import { listCases } from '~/components/actions/case-manage';
4
4
  import { getFileAnnotations } from '~/utils/data-operations';
5
5
  import { fetchFiles } from '~/components/actions/image-manage';
@@ -50,7 +50,7 @@
50
50
  .caseInput input:focus {
51
51
  outline: none;
52
52
  border-color: #0d6efd;
53
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
53
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
54
54
  }
55
55
 
56
56
  /* Buttons */
@@ -126,7 +126,7 @@
126
126
  background-color: #0d6efd;
127
127
  color: white;
128
128
  border: none;
129
- border-radius: 6px;
129
+ border-radius: 6px;
130
130
  font-weight: 500;
131
131
  cursor: pointer;
132
132
  transition: all 0.2s;
@@ -174,7 +174,7 @@
174
174
  padding: 0.5rem;
175
175
  border-radius: 4px;
176
176
  color: #198754;
177
- background-color: rgba(25,135,84,0.1);
177
+ background-color: rgba(25, 135, 84, 0.1);
178
178
  }
179
179
 
180
180
  /* Files Section */
@@ -182,7 +182,7 @@
182
182
  margin-top: 2rem;
183
183
  }
184
184
 
185
- .filesSection h4 {
185
+ .filesSection h4 {
186
186
  margin-bottom: 1rem;
187
187
  font-size: 1.3rem;
188
188
  font-weight: 900;
@@ -274,8 +274,6 @@
274
274
  background-color: #dee2e6;
275
275
  }
276
276
 
277
-
278
-
279
277
  /* Files and Case Management */
280
278
 
281
279
  .fileName {
@@ -285,7 +283,6 @@
285
283
  white-space: nowrap;
286
284
  }
287
285
 
288
-
289
286
  /* Rename and Delete Cases */
290
287
 
291
288
  .caseRename {
@@ -307,12 +304,12 @@
307
304
  .caseRename input:focus {
308
305
  outline: none;
309
306
  border-color: #0d6efd;
310
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
307
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
311
308
  }
312
309
 
313
310
  /* Buttons */
314
311
  .caseRename button {
315
- width: 100%;
312
+ width: 100%;
316
313
  padding: 0.75rem;
317
314
  background-color: #ffc107;
318
315
  color: #000;
@@ -681,33 +678,119 @@
681
678
  }
682
679
  /* Confirmation Status Indicators */
683
680
  .fileItemNotConfirmed {
684
- background-color: color-mix(in lab, var(--warning) 15%, var(--backgroundLight));
681
+ background-color: color-mix(
682
+ in lab,
683
+ var(--warning) 15%,
684
+ var(--backgroundLight)
685
+ );
685
686
  }
686
687
 
687
688
  .fileItemNotConfirmed:hover {
688
- background-color: color-mix(in lab, var(--warning) 20%, var(--backgroundLight));
689
+ background-color: color-mix(
690
+ in lab,
691
+ var(--warning) 20%,
692
+ var(--backgroundLight)
693
+ );
689
694
  }
690
695
 
691
696
  .fileItem.active.fileItemNotConfirmed {
692
- background-color: color-mix(in lab, var(--warning) 15%, var(--backgroundLight));
697
+ background-color: color-mix(
698
+ in lab,
699
+ var(--warning) 15%,
700
+ var(--backgroundLight)
701
+ );
693
702
  }
694
703
 
695
704
  .fileItem.active.fileItemNotConfirmed:hover {
696
- background-color: color-mix(in lab, var(--warning) 20%, var(--backgroundLight));
705
+ background-color: color-mix(
706
+ in lab,
707
+ var(--warning) 20%,
708
+ var(--backgroundLight)
709
+ );
697
710
  }
698
711
 
699
712
  .fileItemConfirmed {
700
- background-color: color-mix(in lab, var(--success) 20%, var(--backgroundLight));
713
+ background-color: color-mix(
714
+ in lab,
715
+ var(--success) 20%,
716
+ var(--backgroundLight)
717
+ );
701
718
  }
702
719
 
703
720
  .fileItemConfirmed:hover {
704
- background-color: color-mix(in lab, var(--success) 28%, var(--backgroundLight));
721
+ background-color: color-mix(
722
+ in lab,
723
+ var(--success) 28%,
724
+ var(--backgroundLight)
725
+ );
705
726
  }
706
727
 
707
728
  .fileItem.active.fileItemConfirmed {
708
- background-color: color-mix(in lab, var(--success) 20%, var(--backgroundLight));
729
+ background-color: color-mix(
730
+ in lab,
731
+ var(--success) 20%,
732
+ var(--backgroundLight)
733
+ );
709
734
  }
710
735
 
711
736
  .fileItem.active.fileItemConfirmed:hover {
712
- background-color: color-mix(in lab, var(--success) 28%, var(--backgroundLight));
713
- }
737
+ background-color: color-mix(
738
+ in lab,
739
+ var(--success) 28%,
740
+ var(--backgroundLight)
741
+ );
742
+ }
743
+
744
+ /* Switch/Cancel Case buttons */
745
+ .switchCaseButton {
746
+ width: 100%;
747
+ padding: 0.75rem;
748
+ background-color: #198754;
749
+ color: white;
750
+ border: none;
751
+ border-radius: 6px;
752
+ font-weight: 500;
753
+ cursor: pointer;
754
+ transition: all 0.2s;
755
+ margin-top: 0.5rem;
756
+ box-sizing: border-box;
757
+ }
758
+
759
+ .switchCaseButton:hover:not(:disabled) {
760
+ background-color: #105032;
761
+ }
762
+
763
+ .switchCaseButton:disabled {
764
+ background-color: #e9ecef;
765
+ color: #6c757d;
766
+ cursor: not-allowed;
767
+ }
768
+
769
+ .cancelSwitchButton {
770
+ width: 100%;
771
+ padding: 0.75rem;
772
+ margin-top: 0.75rem;
773
+ background-color: #dc3545;
774
+ color: white;
775
+ border: none;
776
+ border-radius: 6px;
777
+ font-weight: 500;
778
+ cursor: pointer;
779
+ transition: all 0.2s;
780
+ box-sizing: border-box;
781
+ }
782
+
783
+ .cancelSwitchButton:hover:not(:disabled) {
784
+ background-color: #bd2130;
785
+ }
786
+
787
+ .cancelSwitchButton:disabled {
788
+ background-color: #e9ecef;
789
+ color: #6c757d;
790
+ cursor: not-allowed;
791
+ }
792
+
793
+ .cancelSwitchButton:disabled {
794
+ opacity: 0.6;
795
+ cursor: not-allowed;
796
+ }
@@ -1,8 +1,9 @@
1
- import React, { useState, useContext, useEffect } from 'react';
1
+ import type React from 'react';
2
+ import { useState, useContext, useEffect } from 'react';
2
3
  import { AuthContext } from '~/contexts/auth.context';
3
4
  import { deleteFile } from '~/components/actions/image-manage';
4
5
  import { getFileAnnotations } from '~/utils/data-operations';
5
- import { FileData } from '~/types';
6
+ import { type FileData } from '~/types';
6
7
  import styles from './files-modal.module.css';
7
8
 
8
9
  interface FilesModalProps {
@@ -1,11 +1,11 @@
1
1
  import { useState, useEffect } from 'react';
2
- import { User } from 'firebase/auth';
2
+ import type { User } from 'firebase/auth';
3
3
  import { ColorSelector } from '~/components/colors/colors';
4
4
  import { NotesModal } from './notes-modal';
5
5
  import { getNotes, saveNotes } from '~/components/actions/notes-manage';
6
- import { AnnotationData } from '~/types/annotations';
6
+ import { type AnnotationData } from '~/types/annotations';
7
7
  import { resolveEarliestAnnotationTimestamp } from '~/utils/annotation-timestamp';
8
- import { auditService } from '~/services/audit.service';
8
+ import { auditService } from '~/services/audit';
9
9
  import styles from './notes.module.css';
10
10
 
11
11
  interface NotesSidebarProps {
@@ -10,12 +10,12 @@ hr {
10
10
  }
11
11
 
12
12
  .section {
13
- margin-bottom: 2rem;
13
+ margin-bottom: 2rem;
14
14
  }
15
15
 
16
16
  .sectionTitle {
17
17
  font-size: 1.3rem;
18
- font-weight: 600;
18
+ font-weight: 600;
19
19
  color: #495057;
20
20
  margin-bottom: 1rem;
21
21
  }
@@ -50,7 +50,7 @@ input[type="color"]:focus,
50
50
  textarea:focus {
51
51
  outline: none;
52
52
  border-color: #0d6efd;
53
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
53
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
54
54
  }
55
55
 
56
56
  .caseNumbers {
@@ -74,10 +74,14 @@ textarea:focus {
74
74
  box-sizing: border-box;
75
75
  }
76
76
 
77
+ .caseInput input:not(:disabled) {
78
+ background-color: #ffffff;
79
+ }
80
+
77
81
  .caseInput input:focus {
78
82
  outline: none;
79
83
  border-color: #0d6efd;
80
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
84
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
81
85
  }
82
86
 
83
87
  .checkboxLabel {
@@ -108,6 +112,11 @@ textarea:focus {
108
112
  transition: border-color 0.2s;
109
113
  }
110
114
 
115
+ .classCharacteristics input:not(:disabled),
116
+ .classCharacteristics textarea:not(:disabled) {
117
+ background-color: #ffffff;
118
+ }
119
+
111
120
  .classCharacteristics select,
112
121
  .support select {
113
122
  width: 100%;
@@ -124,7 +133,7 @@ textarea:focus {
124
133
  .support select:focus {
125
134
  outline: none;
126
135
  border-color: #0d6efd;
127
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
136
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
128
137
  }
129
138
 
130
139
  .classCharacteristics textarea {
@@ -138,11 +147,14 @@ textarea:focus {
138
147
  box-sizing: border-box;
139
148
  }
140
149
 
150
+ .classCharacteristics input + textarea {
151
+ margin-top: 1rem;
152
+ }
153
+
141
154
  .classCharacteristics textarea:focus {
142
155
  outline: none;
143
156
  border-color: #0d6efd;
144
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
145
- margin-top: 1rem;
157
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
146
158
  resize: vertical;
147
159
  }
148
160
 
@@ -188,13 +200,17 @@ textarea:focus {
188
200
  box-sizing: border-box;
189
201
  }
190
202
 
203
+ .indexing input[type="text"]:not(:disabled) {
204
+ background-color: #ffffff;
205
+ }
206
+
191
207
  .indexing input[type="text"]:focus {
192
208
  outline: none;
193
209
  border-color: #0d6efd;
194
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
210
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
195
211
  }
196
212
 
197
- .confirmation {
213
+ .confirmation {
198
214
  padding: 0.75rem;
199
215
  background-color: #f8f9fa;
200
216
  border-radius: 6px;
@@ -232,7 +248,7 @@ textarea:focus {
232
248
  }
233
249
 
234
250
  .notesButton:hover {
235
- background-color: color-mix(in lab, var(--primary) 95%, transparent);
251
+ background-color: color-mix(in lab, var(--primary) 95%, transparent);
236
252
  }
237
253
 
238
254
  .modalOverlay {
@@ -355,6 +371,10 @@ textarea:focus {
355
371
  }
356
372
 
357
373
  @keyframes fadeIn {
358
- from { opacity: 0; }
359
- to { opacity: 1; }
360
- }
374
+ from {
375
+ opacity: 0;
376
+ }
377
+ to {
378
+ opacity: 1;
379
+ }
380
+ }