@striae-org/striae 3.2.2 → 4.0.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 (82) hide show
  1. package/.env.example +1 -1
  2. package/app/components/actions/case-export/core-export.ts +5 -2
  3. package/app/components/actions/case-export/download-handlers.ts +51 -3
  4. package/app/components/actions/case-import/confirmation-import.ts +65 -40
  5. package/app/components/actions/case-import/confirmation-package.ts +86 -0
  6. package/app/components/actions/case-import/image-operations.ts +20 -49
  7. package/app/components/actions/case-import/index.ts +1 -0
  8. package/app/components/actions/case-import/orchestrator.ts +13 -3
  9. package/app/components/actions/case-import/storage-operations.ts +54 -89
  10. package/app/components/actions/case-import/validation.ts +7 -111
  11. package/app/components/actions/case-import/zip-processing.ts +44 -2
  12. package/app/components/actions/case-manage.ts +15 -27
  13. package/app/components/actions/confirm-export.ts +44 -13
  14. package/app/components/actions/generate-pdf.ts +3 -7
  15. package/app/components/actions/image-manage.ts +63 -129
  16. package/app/components/button/button.module.css +12 -8
  17. package/app/components/form/form-button.tsx +1 -1
  18. package/app/components/form/form.module.css +9 -0
  19. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +163 -49
  20. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +365 -88
  21. package/app/components/sidebar/case-export/case-export.tsx +13 -60
  22. package/app/components/sidebar/case-import/case-import.tsx +18 -6
  23. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +6 -4
  24. package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
  25. package/app/components/sidebar/cases/case-sidebar.tsx +122 -52
  26. package/app/components/sidebar/cases/cases.module.css +101 -18
  27. package/app/components/sidebar/notes/notes.module.css +33 -13
  28. package/app/components/sidebar/sidebar.module.css +0 -2
  29. package/app/components/user/delete-account.tsx +7 -7
  30. package/app/components/user/manage-profile.tsx +1 -1
  31. package/app/components/user/mfa-phone-update.tsx +15 -12
  32. package/app/config-example/config.json +2 -8
  33. package/app/hooks/useInactivityTimeout.ts +2 -5
  34. package/app/root.tsx +96 -65
  35. package/app/routes/auth/login.tsx +132 -11
  36. package/app/routes/auth/route.ts +4 -3
  37. package/app/routes/striae/striae.tsx +4 -8
  38. package/app/services/audit/audit-api-client.ts +40 -0
  39. package/app/services/audit/audit-worker-client.ts +14 -17
  40. package/app/styles/root.module.css +13 -101
  41. package/app/tailwind.css +9 -2
  42. package/app/utils/SHA256.ts +5 -1
  43. package/app/utils/auth.ts +5 -32
  44. package/app/utils/confirmation-signature.ts +5 -1
  45. package/app/utils/data-api-client.ts +43 -0
  46. package/app/utils/data-operations.ts +59 -75
  47. package/app/utils/export-verification.ts +353 -0
  48. package/app/utils/image-api-client.ts +130 -0
  49. package/app/utils/pdf-api-client.ts +43 -0
  50. package/app/utils/permissions.ts +10 -23
  51. package/app/utils/signature-utils.ts +74 -4
  52. package/app/utils/user-api-client.ts +90 -0
  53. package/functions/api/_shared/firebase-auth.ts +255 -0
  54. package/functions/api/audit/[[path]].ts +150 -0
  55. package/functions/api/data/[[path]].ts +141 -0
  56. package/functions/api/image/[[path]].ts +127 -0
  57. package/functions/api/pdf/[[path]].ts +110 -0
  58. package/functions/api/user/[[path]].ts +196 -0
  59. package/package.json +8 -4
  60. package/public/favicon.ico +0 -0
  61. package/public/icon-256.png +0 -0
  62. package/public/icon-512.png +0 -0
  63. package/public/manifest.json +39 -0
  64. package/public/shortcut.png +0 -0
  65. package/public/social-image.png +0 -0
  66. package/react-router.config.ts +5 -0
  67. package/scripts/deploy-all.sh +22 -8
  68. package/scripts/deploy-config.sh +143 -148
  69. package/scripts/deploy-pages-secrets.sh +231 -0
  70. package/scripts/deploy-worker-secrets.sh +1 -1
  71. package/workers/audit-worker/wrangler.jsonc.example +1 -8
  72. package/workers/data-worker/wrangler.jsonc.example +1 -8
  73. package/workers/image-worker/wrangler.jsonc.example +1 -8
  74. package/workers/keys-worker/wrangler.jsonc.example +2 -9
  75. package/workers/pdf-worker/scripts/generate-assets.js +94 -0
  76. package/workers/pdf-worker/src/assets/icon-256.png +0 -0
  77. package/workers/pdf-worker/wrangler.jsonc.example +1 -8
  78. package/workers/user-worker/src/user-worker.example.ts +121 -41
  79. package/workers/user-worker/wrangler.jsonc.example +1 -8
  80. package/wrangler.toml.example +1 -1
  81. package/app/styles/legal-pages.module.css +0 -113
  82. package/public/favicon.svg +0 -9
@@ -19,7 +19,7 @@ import {
19
19
  useFilePreview,
20
20
  useImportExecution,
21
21
  isValidImportFile,
22
- getImportType,
22
+ resolveImportType,
23
23
  resetFileInput
24
24
  } from './index';
25
25
  import styles from './case-import.module.css';
@@ -146,12 +146,18 @@ export const CaseImport = ({
146
146
  clearMessages();
147
147
 
148
148
  if (!isValidImportFile(file)) {
149
- setError('Only ZIP files (case imports) or JSON files (confirmation imports) are allowed. Please select a valid file.');
149
+ setError('Only Striae case ZIP files, confirmation ZIP files, or confirmation JSON files are allowed.');
150
+ clearImportData();
151
+ return;
152
+ }
153
+
154
+ const importType = await resolveImportType(file);
155
+ if (!importType) {
156
+ setError('The selected file is not a supported Striae case or confirmation import package.');
150
157
  clearImportData();
151
158
  return;
152
159
  }
153
160
 
154
- const importType = getImportType(file);
155
161
  updateImportState({
156
162
  selectedFile: file,
157
163
  importType
@@ -172,12 +178,18 @@ export const CaseImport = ({
172
178
  clearMessages();
173
179
 
174
180
  if (!isValidImportFile(file)) {
175
- setError('Only ZIP files (case imports) or JSON files (confirmation imports) are allowed. Please select a valid file.');
181
+ setError('Only Striae case ZIP files, confirmation ZIP files, or confirmation JSON files are allowed.');
182
+ clearImportData();
183
+ return;
184
+ }
185
+
186
+ const importType = await resolveImportType(file);
187
+ if (!importType) {
188
+ setError('The selected file is not a supported Striae case or confirmation import package.');
176
189
  clearImportData();
177
190
  return;
178
191
  }
179
192
 
180
- const importType = getImportType(file);
181
193
  updateImportState({
182
194
  selectedFile: file,
183
195
  importType
@@ -404,7 +416,7 @@ export const CaseImport = ({
404
416
  <br />
405
417
  <h3 className={styles.instructionsTitle}>Confirmation Import Instructions:</h3>
406
418
  <ul className={styles.instructionsList}>
407
- <li>Only JSON files (.json) with confirmation data exported from Striae are accepted</li>
419
+ <li>Confirmation imports accept either confirmation JSON files or confirmation ZIP packages exported from Striae</li>
408
420
  <li>Only one confirmation file can be imported at a time</li>
409
421
  <li>Confirmed images will become read-only and cannot be modified</li>
410
422
  <li>If an image has a pre-existing confirmation, it will be skipped</li>
@@ -1,6 +1,6 @@
1
1
  import { useState, useCallback } from 'react';
2
2
  import type { User } from 'firebase/auth';
3
- import { previewCaseImport } from '~/components/actions/case-review';
3
+ import { previewCaseImport, extractConfirmationImportPackage } from '~/components/actions/case-review';
4
4
  import { type CaseImportPreview } from '~/types';
5
5
  import { type ConfirmationPreview } from '../components/ConfirmationPreviewSection';
6
6
 
@@ -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,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
  */
@@ -2,6 +2,7 @@ import type { User } from 'firebase/auth';
2
2
  import type * as CaseExportActions from '../../actions/case-export';
3
3
  import { useState, useEffect, useMemo, useCallback } from 'react';
4
4
  import styles from './cases.module.css';
5
+ import { Toast } from '~/components/toast/toast';
5
6
  import { CasesModal } from './cases-modal';
6
7
  import { FilesModal } from '../files/files-modal';
7
8
  import { CaseExport, type ExportFormat } from '../case-export/case-export';
@@ -103,7 +104,11 @@ export const CaseSidebar = ({
103
104
  const [, setFileError] = useState('');
104
105
  const [newCaseName, setNewCaseName] = useState('');
105
106
  const [showCaseActions, setShowCaseActions] = useState(false);
107
+ const [showCaseManagement, setShowCaseManagement] = useState(false);
106
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');
107
112
  const [canUploadNewFile, setCanUploadNewFile] = useState(true);
108
113
  const [createCaseError, setCreateCaseError] = useState('');
109
114
  const [uploadFileError, setUploadFileError] = useState('');
@@ -313,6 +318,24 @@ export const CaseSidebar = ({
313
318
  isCancelled = true;
314
319
  };
315
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]);
316
339
 
317
340
  const handleCase = async () => {
318
341
  setIsLoading(true);
@@ -336,6 +359,7 @@ export const CaseSidebar = ({
336
359
  setFiles(files);
337
360
  setCaseNumber('');
338
361
  setSuccessAction('loaded');
362
+ setShowCaseManagement(false);
339
363
  setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
340
364
  return;
341
365
  }
@@ -362,6 +386,7 @@ export const CaseSidebar = ({
362
386
  setFiles([]);
363
387
  setCaseNumber('');
364
388
  setSuccessAction('created');
389
+ setShowCaseManagement(false);
365
390
  setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
366
391
 
367
392
  // Refresh permissions after successful case creation
@@ -514,25 +539,40 @@ const handleImageSelect = (file: FileData) => {
514
539
  ? 'Select an image first'
515
540
  : undefined;
516
541
 
517
- const handleExport = async (exportCaseNumber: string, format: ExportFormat, includeImages?: boolean) => {
542
+ const handleExport = async (exportCaseNumber: string, format: ExportFormat, includeImages?: boolean, onProgress?: (progress: number, label: string) => void) => {
518
543
  try {
519
544
  const caseExportActions = await loadCaseExportActions();
520
545
 
521
546
  if (includeImages) {
522
547
  // ZIP export with images - only available for single case exports
523
- await caseExportActions.downloadCaseAsZip(user, exportCaseNumber, format);
548
+ await caseExportActions.downloadCaseAsZip(user, exportCaseNumber, format, (progress) => {
549
+ const label = progress < 30 ? 'Loading case data' :
550
+ progress < 50 ? 'Preparing archive' :
551
+ progress < 80 ? 'Adding images' :
552
+ progress < 96 ? 'Finalizing' : 'Downloading';
553
+ onProgress?.(Math.round(progress), label);
554
+ });
524
555
  } else {
525
556
  // Standard data-only export
526
- const exportData = await caseExportActions.exportCaseData(user, exportCaseNumber, {
527
- includeMetadata: true
528
- });
529
-
557
+ onProgress?.(5, 'Loading case data');
558
+ const exportData = await caseExportActions.exportCaseData(
559
+ user,
560
+ exportCaseNumber,
561
+ { includeMetadata: true },
562
+ (current, total, label) => {
563
+ const p = total > 0 ? Math.round(10 + (current / total) * 60) : 10;
564
+ onProgress?.(p, label);
565
+ }
566
+ );
567
+ onProgress?.(75, 'Preparing download');
568
+
530
569
  // Download the exported data in the selected format
531
570
  if (format === 'json') {
532
571
  await caseExportActions.downloadCaseAsJSON(user, exportData);
533
572
  } else {
534
573
  await caseExportActions.downloadCaseAsCSV(user, exportData);
535
574
  }
575
+ onProgress?.(100, 'Complete');
536
576
  }
537
577
 
538
578
  } catch (error) {
@@ -564,53 +604,73 @@ const handleImageSelect = (file: FileData) => {
564
604
  };
565
605
 
566
606
  return (
607
+ <>
567
608
  <div className={styles.caseSection}>
568
- <div className={styles.caseSection}>
569
- <h4>Case Management</h4>
570
- {limitsDescription && (
571
- <p className={styles.limitsInfo}>
572
- {limitsDescription}
573
- </p>
574
- )}
575
- <div className={`${styles.caseInput} mb-4`}>
576
- <input
577
- type="text"
578
- value={caseNumber}
579
- onChange={(e) => setCaseNumber(e.target.value)}
580
- placeholder="Case #"
581
- />
582
- </div>
609
+ {currentCase && !showCaseManagement ? (
583
610
  <div className={`${styles.caseLoad} mb-4`}>
584
- <button
585
- onClick={handleCase}
586
- disabled={isLoading || !caseNumber || permissionChecking || (isReadOnly && !!currentCase) || isUploading}
587
- title={
588
- isUploading
589
- ? "Cannot load/create cases while uploading files"
590
- : (isReadOnly && currentCase)
591
- ? "Cannot load/create cases while reviewing a read-only case. Clear the current case first."
592
- : (!canCreateNewCase ? createCaseError : undefined)
593
- }
594
- >
595
- {isLoading ? 'Loading...' : permissionChecking ? 'Checking permissions...' : 'Load/Create Case'}
596
- </button>
597
- </div>
598
- <div className={styles.caseInput}>
599
- <button
600
- onClick={() => setIsModalOpen(true)}
601
- className={styles.listButton}
602
- disabled={isUploading}
603
- title={isUploading ? "Cannot list cases while uploading files" : undefined}
604
- >
605
- List All Cases
606
- </button>
607
- </div>
608
- {error && <p className={styles.error}>{error}</p>}
609
- {successAction && (
610
- <p className={styles.success}>
611
- Case {currentCase} {successAction} successfully!
612
- </p>
613
- )}
611
+ <button
612
+ className={styles.switchCaseButton}
613
+ onClick={() => setShowCaseManagement(true)}
614
+ disabled={isUploading}
615
+ title={isUploading ? "Cannot switch cases while uploading files" : undefined}
616
+ >
617
+ Switch Case
618
+ </button>
619
+ </div>
620
+ ) : (
621
+ <>
622
+ <h4>Case Management</h4>
623
+ {limitsDescription && (
624
+ <p className={styles.limitsInfo}>
625
+ {limitsDescription}
626
+ </p>
627
+ )}
628
+ <div className={`${styles.caseInput} mb-4`}>
629
+ <input
630
+ type="text"
631
+ value={caseNumber}
632
+ onChange={(e) => setCaseNumber(e.target.value)}
633
+ placeholder="Case #"
634
+ />
635
+ </div>
636
+ <div className={`${styles.caseLoad} mb-4`}>
637
+ <button
638
+ onClick={handleCase}
639
+ disabled={isLoading || !caseNumber || permissionChecking || (isReadOnly && !!currentCase) || isUploading}
640
+ title={
641
+ isUploading
642
+ ? "Cannot load/create cases while uploading files"
643
+ : (isReadOnly && currentCase)
644
+ ? "Cannot load/create cases while reviewing a read-only case. Clear the current case first."
645
+ : (!canCreateNewCase ? createCaseError : undefined)
646
+ }
647
+ >
648
+ {isLoading ? 'Loading...' : permissionChecking ? 'Checking permissions...' : 'Load/Create Case'}
649
+ </button>
650
+ </div>
651
+ <div className={styles.caseInput}>
652
+ <button
653
+ onClick={() => setIsModalOpen(true)}
654
+ className={styles.listButton}
655
+ disabled={isUploading}
656
+ title={isUploading ? "Cannot list cases while uploading files" : undefined}
657
+ >
658
+ List All Cases
659
+ </button>
660
+ </div>
661
+ {currentCase && (
662
+ <div className="mb-4">
663
+ <button
664
+ className={styles.cancelSwitchButton}
665
+ onClick={() => setShowCaseManagement(false)}
666
+ disabled={isUploading}
667
+ >
668
+ Cancel
669
+ </button>
670
+ </div>
671
+ )}
672
+ </>
673
+ )}
614
674
  <CasesModal
615
675
  isOpen={isModalOpen}
616
676
  onClose={() => setIsModalOpen(false)}
@@ -830,6 +890,16 @@ return (
830
890
  />
831
891
 
832
892
  </div>
833
- </div>
893
+ <Toast
894
+ message={toastMessage}
895
+ type={toastType}
896
+ isVisible={isToastVisible}
897
+ onClose={() => {
898
+ setIsToastVisible(false);
899
+ setError('');
900
+ setSuccessAction(null);
901
+ }}
902
+ />
903
+ </>
834
904
  );
835
905
  };
@@ -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
+ }