@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,14 +1,49 @@
1
- import { User } from 'firebase/auth';
2
- import { CaseExportData, CaseImportPreview } from '~/types';
1
+ import type { User } from 'firebase/auth';
2
+ import { type CaseExportData, type CaseImportPreview } from '~/types';
3
3
  import { validateCaseNumber } from '../case-manage';
4
4
  import {
5
5
  extractForensicManifestData,
6
- SignedForensicManifest,
6
+ type SignedForensicManifest,
7
7
  validateCaseIntegritySecure as validateForensicIntegrity,
8
8
  verifyForensicManifestSignature
9
9
  } from '~/utils/SHA256';
10
10
  import { validateExporterUid, removeForensicWarning } from './validation';
11
11
 
12
+ function getLeafFileName(path: string): string {
13
+ const segments = path.split('/').filter(Boolean);
14
+ return segments.length > 0 ? segments[segments.length - 1] : path;
15
+ }
16
+
17
+ function selectPreferredPemPath(pemPaths: string[]): string | undefined {
18
+ if (pemPaths.length === 0) {
19
+ return undefined;
20
+ }
21
+
22
+ const sortedPaths = [...pemPaths].sort((left, right) => left.localeCompare(right));
23
+ const preferred = sortedPaths.find((path) =>
24
+ /^striae-public-signing-key.*\.pem$/i.test(getLeafFileName(path))
25
+ );
26
+
27
+ return preferred ?? sortedPaths[0];
28
+ }
29
+
30
+ async function extractVerificationPublicKeyFromZip(
31
+ zip: {
32
+ files: Record<string, { dir: boolean }>;
33
+ file: (path: string) => { async: (type: 'text') => Promise<string> } | null;
34
+ }
35
+ ): Promise<string | undefined> {
36
+ const filePaths = Object.keys(zip.files).filter((path) => !zip.files[path].dir);
37
+ const pemPaths = filePaths.filter((path) => getLeafFileName(path).toLowerCase().endsWith('.pem'));
38
+ const preferredPemPath = selectPreferredPemPath(pemPaths);
39
+
40
+ if (!preferredPemPath) {
41
+ return undefined;
42
+ }
43
+
44
+ return zip.file(preferredPemPath)?.async('text');
45
+ }
46
+
12
47
  /**
13
48
  * Extract original image ID from export filename format
14
49
  * Format: {originalFilename}-{id}.{extension}
@@ -51,6 +86,7 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
51
86
 
52
87
  try {
53
88
  const zip = await JSZip.loadAsync(zipFile);
89
+ const verificationPublicKeyPem = await extractVerificationPublicKeyFromZip(zip);
54
90
 
55
91
  // First, validate hash if forensic metadata exists
56
92
  let hashValid: boolean | undefined = undefined;
@@ -128,7 +164,10 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
128
164
  }));
129
165
  }
130
166
 
131
- const signatureResult = await verifyForensicManifestSignature(forensicManifest);
167
+ const signatureResult = await verifyForensicManifestSignature(
168
+ forensicManifest,
169
+ verificationPublicKeyPem
170
+ );
132
171
 
133
172
  // Perform comprehensive validation
134
173
  const validation = await validateForensicIntegrity(
@@ -267,12 +306,14 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
267
306
  imageIdMapping: { [exportFilename: string]: string }; // exportFilename -> originalImageId
268
307
  metadata?: Record<string, unknown>;
269
308
  cleanedContent?: string; // Add cleaned content for hash validation
309
+ verificationPublicKeyPem?: string;
270
310
  }> {
271
311
  // Dynamic import of JSZip to avoid bundle size issues
272
312
  const JSZip = (await import('jszip')).default;
273
313
 
274
314
  try {
275
315
  const zip = await JSZip.loadAsync(zipFile);
316
+ const verificationPublicKeyPem = await extractVerificationPublicKeyFromZip(zip);
276
317
 
277
318
  // Find the main data file (JSON or CSV)
278
319
  const dataFiles = Object.keys(zip.files).filter(name =>
@@ -367,7 +408,8 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
367
408
  imageFiles,
368
409
  imageIdMapping,
369
410
  metadata,
370
- cleanedContent
411
+ cleanedContent,
412
+ verificationPublicKeyPem
371
413
  };
372
414
 
373
415
  } catch (error) {
@@ -1,4 +1,4 @@
1
- import { User } from 'firebase/auth';
1
+ import type { User } from 'firebase/auth';
2
2
  import {
3
3
  canCreateCase,
4
4
  getUserCases,
@@ -13,8 +13,8 @@ import {
13
13
  duplicateCaseData,
14
14
  deleteFileAnnotations
15
15
  } from '~/utils/data-operations';
16
- import { CaseData, ReadOnlyCaseData, FileData } from '~/types';
17
- import { auditService } from '~/services/audit.service';
16
+ import { type CaseData, type ReadOnlyCaseData, type FileData } from '~/types';
17
+ import { auditService } from '~/services/audit';
18
18
  import { getImageApiKey } from '~/utils/auth';
19
19
  import paths from '~/config/config.json';
20
20
 
@@ -1,9 +1,14 @@
1
- import { User } from 'firebase/auth';
1
+ import type { User } from 'firebase/auth';
2
2
  import { calculateSHA256Secure } from '~/utils/SHA256';
3
3
  import { getUserData } from '~/utils/permissions';
4
4
  import { getCaseData, updateCaseData, signConfirmationData } from '~/utils/data-operations';
5
- import { ConfirmationData, CaseConfirmations, CaseDataWithConfirmations, ConfirmationImportData } from '~/types';
6
- import { auditService } from '~/services/audit.service';
5
+ import { type ConfirmationData, type CaseConfirmations, type CaseDataWithConfirmations, type ConfirmationImportData } from '~/types';
6
+ import {
7
+ createPublicSigningKeyFileName,
8
+ getCurrentPublicSigningKeyDetails,
9
+ getVerificationPublicKey
10
+ } from '~/utils/signature-utils';
11
+ import { auditService } from '~/services/audit';
7
12
 
8
13
  /**
9
14
  * Store a confirmation for a specific image, linked to the original image ID
@@ -267,15 +272,8 @@ export async function exportConfirmationData(
267
272
  }
268
273
  };
269
274
 
270
- // Convert final data to JSON blob
271
275
  const finalJsonString = JSON.stringify(finalExportData, null, 2);
272
- const blob = new Blob([finalJsonString], { type: 'application/json' });
273
-
274
- // Create download
275
- const url = URL.createObjectURL(blob);
276
- const a = document.createElement('a');
277
- a.href = url;
278
-
276
+
279
277
  // Use local timezone for filename timestamp
280
278
  const now = new Date();
281
279
  const year = now.getFullYear();
@@ -285,14 +283,47 @@ export async function exportConfirmationData(
285
283
  const minutes = String(now.getMinutes()).padStart(2, '0');
286
284
  const seconds = String(now.getSeconds()).padStart(2, '0');
287
285
  const timestampString = `${year}${month}${day}-${hours}${minutes}${seconds}`;
286
+
287
+ const confirmationFileName = `confirmation-data-${caseNumber}-${timestampString}.json`;
288
+
289
+ const keyFromSignature = getVerificationPublicKey(signingResult.signature.keyId);
290
+ const currentKey = getCurrentPublicSigningKeyDetails();
291
+ const publicKeyPem = keyFromSignature ?? currentKey.publicKeyPem;
292
+ const publicKeyFileName = createPublicSigningKeyFileName(
293
+ keyFromSignature ? signingResult.signature.keyId : currentKey.keyId
294
+ );
295
+
296
+ if (!publicKeyPem || publicKeyPem.trim().length === 0) {
297
+ throw new Error('No public signing key is configured for confirmation export packaging.');
298
+ }
299
+
300
+ const JSZip = (await import('jszip')).default;
301
+ const zip = new JSZip();
302
+ const normalizedPem = publicKeyPem.endsWith('\n') ? publicKeyPem : `${publicKeyPem}\n`;
303
+
304
+ zip.file(confirmationFileName, finalJsonString);
305
+ zip.file(publicKeyFileName, normalizedPem);
306
+
307
+ const zipBlob = await zip.generateAsync({
308
+ type: 'blob',
309
+ compression: 'DEFLATE',
310
+ compressionOptions: { level: 6 }
311
+ });
312
+
313
+ const exportFileName = `confirmation-export-${caseNumber}-${timestampString}.zip`;
314
+
315
+ // Create download
316
+ const url = URL.createObjectURL(zipBlob);
317
+ const a = document.createElement('a');
318
+ a.href = url;
288
319
 
289
- a.download = `confirmation-data-${caseNumber}-${timestampString}.json`;
320
+ a.download = exportFileName;
290
321
  document.body.appendChild(a);
291
322
  a.click();
292
323
  document.body.removeChild(a);
293
324
  URL.revokeObjectURL(url);
294
325
 
295
- console.log(`Confirmation data exported for case ${caseNumber}`);
326
+ console.log(`Confirmation export ZIP generated for case ${caseNumber}`);
296
327
 
297
328
  // Log successful confirmation export
298
329
  const endTime = Date.now();
@@ -300,14 +331,14 @@ export async function exportConfirmationData(
300
331
  await auditService.logConfirmationExport(
301
332
  user,
302
333
  caseNumber,
303
- `confirmation-data-${caseNumber}-${timestampString}.json`,
334
+ exportFileName,
304
335
  confirmationCount,
305
336
  'success',
306
337
  [],
307
338
  undefined, // Original examiner UID not available here
308
339
  {
309
340
  processingTimeMs: endTime - startTime,
310
- fileSizeBytes: new Blob([jsonString]).size,
341
+ fileSizeBytes: zipBlob.size,
311
342
  validationStepsCompleted: confirmationCount,
312
343
  validationStepsFailed: 0
313
344
  },
@@ -328,7 +359,7 @@ export async function exportConfirmationData(
328
359
  await auditService.logConfirmationExport(
329
360
  user,
330
361
  caseNumber,
331
- `confirmation-data-${caseNumber}-error.json`,
362
+ `confirmation-export-${caseNumber}-error.zip`,
332
363
  0,
333
364
  'failure',
334
365
  [error instanceof Error ? error.message : 'Unknown error'],
@@ -1,8 +1,8 @@
1
1
  import paths from '~/config/config.json';
2
- import { AnnotationData } from '~/types/annotations';
3
- import { auditService } from '~/services/audit.service';
2
+ import { type AnnotationData } from '~/types/annotations';
3
+ import { auditService } from '~/services/audit';
4
4
  import { getPdfApiKey } from '~/utils/auth';
5
- import { User } from 'firebase/auth';
5
+ import type { User } from 'firebase/auth';
6
6
 
7
7
  interface GeneratePDFParams {
8
8
  user: User;
@@ -1,4 +1,4 @@
1
- import { User } from 'firebase/auth';
1
+ import type { User } from 'firebase/auth';
2
2
  import paths from '~/config/config.json';
3
3
  import {
4
4
  getImageApiKey,
@@ -6,8 +6,8 @@ import {
6
6
  } from '~/utils/auth';
7
7
  import { canUploadFile } from '~/utils/permissions';
8
8
  import { getCaseData, updateCaseData, deleteFileAnnotations } from '~/utils/data-operations';
9
- import { CaseData, FileData, ImageUploadResponse } from '~/types';
10
- import { auditService } from '~/services/audit.service';
9
+ import type { CaseData, FileData, ImageUploadResponse } from '~/types';
10
+ import { auditService } from '~/services/audit';
11
11
 
12
12
  const IMAGE_URL = paths.image_worker_url;
13
13
 
@@ -1,6 +1,6 @@
1
- import { User } from 'firebase/auth';
2
- import { AnnotationData } from '~/types/annotations';
3
- import { saveFileAnnotations, getFileAnnotations, DataOperationOptions } from '~/utils/data-operations';
1
+ import type { User } from 'firebase/auth';
2
+ import { type AnnotationData } from '~/types/annotations';
3
+ import { saveFileAnnotations, getFileAnnotations, type DataOperationOptions } from '~/utils/data-operations';
4
4
 
5
5
  export const saveNotes = async (
6
6
  user: User,
@@ -1,5 +1,5 @@
1
1
  import { auth } from '~/services/firebase';
2
- import { auditService } from '~/services/audit.service';
2
+ import { auditService } from '~/services/audit';
3
3
  import { generateUniqueId } from '~/utils/id-generator';
4
4
  import styles from './signout.module.css';
5
5
 
@@ -1,8 +1,7 @@
1
1
  import { useState, useEffect, useContext, useCallback } from 'react';
2
2
  import { AuthContext } from '~/contexts/auth.context';
3
- import { auditService } from '~/services/audit.service';
4
- import { auditExportService } from '~/services/audit-export.service';
5
- import { ValidationAuditEntry, AuditAction, AuditResult, AuditTrail, UserData, WorkflowPhase } from '~/types';
3
+ import { auditService, auditExportService } from '~/services/audit';
4
+ import { type ValidationAuditEntry, type AuditAction, type AuditResult, type AuditTrail, type UserData, type WorkflowPhase } from '~/types';
6
5
  import { getUserData } from '~/utils/permissions';
7
6
  import styles from './user-audit.module.css';
8
7
 
@@ -1,11 +1,11 @@
1
- import { User } from 'firebase/auth';
1
+ import type { User } from 'firebase/auth';
2
2
  import { useEffect, useState } from 'react';
3
3
  import { auth } from '~/services/firebase';
4
4
  import { useInactivityTimeout } from '~/hooks/useInactivityTimeout';
5
5
  import { INACTIVITY_CONFIG } from '~/config/inactivity';
6
6
  import { AuthContext } from '~/contexts/auth.context';
7
7
  import { InactivityWarning } from '~/components/user/inactivity-warning';
8
- import { auditService } from '~/services/audit.service';
8
+ import { auditService } from '~/services/audit';
9
9
  import { generateUniqueId } from '~/utils/id-generator';
10
10
 
11
11
  interface AuthProviderProps {
@@ -6,11 +6,11 @@ import {
6
6
  PhoneMultiFactorGenerator,
7
7
  RecaptchaVerifier,
8
8
  multiFactor,
9
- User
9
+ type User
10
10
  } from 'firebase/auth';
11
- import { handleAuthError, getValidationError } from '~/services/firebase-errors';
11
+ import { handleAuthError, getValidationError } from '~/services/firebase/errors';
12
12
  import { SignOut } from '~/components/actions/signout';
13
- import { auditService } from '~/services/audit.service';
13
+ import { auditService } from '~/services/audit';
14
14
  import styles from './mfa-enrollment.module.css';
15
15
 
16
16
  interface MFAEnrollmentProps {
@@ -3,13 +3,13 @@ import {
3
3
  PhoneAuthProvider,
4
4
  PhoneMultiFactorGenerator,
5
5
  RecaptchaVerifier,
6
- MultiFactorResolver,
7
- UserCredential
6
+ type MultiFactorResolver,
7
+ type UserCredential
8
8
  } from 'firebase/auth';
9
9
  import { auth } from '~/services/firebase';
10
- import { handleAuthError, getValidationError } from '~/services/firebase-errors';
10
+ import { handleAuthError, getValidationError } from '~/services/firebase/errors';
11
11
  import { SignOut } from '~/components/actions/signout';
12
- import { auditService } from '~/services/audit.service';
12
+ import { auditService } from '~/services/audit';
13
13
  import { generateUniqueId } from '~/utils/id-generator';
14
14
  import styles from './mfa-verification.module.css';
15
15
 
@@ -1,7 +1,7 @@
1
1
  import { useState, useCallback, useMemo, useRef, useEffect, useContext } from 'react';
2
- import { BoxAnnotation } from '~/types';
2
+ import { type BoxAnnotation } from '~/types';
3
3
  import { AuthContext } from '~/contexts/auth.context';
4
- import { auditService } from '~/services/audit.service';
4
+ import { auditService } from '~/services/audit';
5
5
  import { resolveEarliestAnnotationTimestamp } from '~/utils/annotation-timestamp';
6
6
  import styles from './box-annotations.module.css';
7
7
 
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState, useRef, useContext, useCallback } from 'react';
2
2
  import { BoxAnnotations } from './box-annotations/box-annotations';
3
3
  import { ConfirmationModal } from './confirmation/confirmation';
4
- import { AnnotationData, BoxAnnotation, ConfirmationData } from '~/types/annotations';
4
+ import { type AnnotationData, type BoxAnnotation, type ConfirmationData } from '~/types/annotations';
5
5
  import { AuthContext } from '~/contexts/auth.context';
6
6
  import { storeConfirmation } from '~/components/actions/confirm-export';
7
7
  import styles from './canvas.module.css';
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useContext } from 'react';
2
- import { ConfirmationData } from '~/types/annotations';
2
+ import { type ConfirmationData } from '~/types/annotations';
3
3
  import { AuthContext } from '~/contexts/auth.context';
4
4
  import { generateConfirmationId } from '~/utils/id-generator';
5
5
  import styles from './confirmation.module.css';
@@ -1,7 +1,7 @@
1
1
  import styles from './form.module.css';
2
2
 
3
3
  interface FormButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4
- variant?: 'primary' | 'secondary' | 'success' | 'error';
4
+ variant?: 'primary' | 'secondary' | 'success' | 'error' | 'audit';
5
5
  isLoading?: boolean;
6
6
  loadingText?: string;
7
7
  children: React.ReactNode;
@@ -124,6 +124,15 @@
124
124
  background-color: color-mix(in lab, var(--error) 85%, var(--black));
125
125
  }
126
126
 
127
+ .buttonAudit {
128
+ background-color: #6f42c1;
129
+ color: var(--white);
130
+ }
131
+
132
+ .buttonAudit:hover:not(:disabled) {
133
+ background-color: #5a359a;
134
+ }
135
+
127
136
  .button:disabled {
128
137
  background-color: color-mix(in lab, var(--background) 95%, transparent);
129
138
  color: var(--textLight);
@@ -79,93 +79,207 @@
79
79
  font-weight: var(--fontWeightMedium);
80
80
  }
81
81
 
82
- .label {
82
+ .verifierLayout {
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: var(--spaceL);
86
+ }
87
+
88
+ .verificationField {
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: var(--spaceS);
92
+ }
93
+
94
+ .fieldHeader {
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: space-between;
98
+ gap: var(--spaceS);
99
+ }
100
+
101
+ .fieldLabel {
83
102
  font-size: var(--fontSizeBodyXS);
84
103
  font-weight: var(--fontWeightMedium);
85
104
  color: var(--textTitle);
86
105
  }
87
106
 
88
- .field {
89
- width: 100%;
90
- max-width: 100%;
91
- box-sizing: border-box;
92
- min-height: 180px;
93
- padding: var(--spaceM);
94
- border: 1px solid color-mix(in lab, var(--text) 10%, transparent);
95
- border-radius: var(--spaceXS);
96
- background: color-mix(in lab, var(--background) 96%, transparent);
97
- color: var(--textBody);
107
+ .hiddenFileInput {
108
+ display: none;
109
+ }
110
+
111
+ .clearButton {
112
+ background: none;
113
+ border: none;
114
+ padding: 0;
115
+ color: var(--primary);
98
116
  font-size: var(--fontSizeBodyXS);
99
- line-height: 1.4;
100
- font-family: Consolas, "Courier New", monospace;
101
- resize: vertical;
117
+ font-weight: var(--fontWeightMedium);
118
+ cursor: pointer;
102
119
  }
103
120
 
104
- .howToTitle {
121
+ .dropZone {
122
+ min-height: 144px;
123
+ margin: 0;
124
+ display: flex;
125
+ flex-direction: column;
126
+ justify-content: center;
127
+ gap: var(--spaceXS);
128
+ padding: var(--spaceL);
129
+ border: 1px dashed color-mix(in lab, var(--text) 18%, transparent);
130
+ border-radius: var(--radiusM);
131
+ background: linear-gradient(
132
+ 135deg,
133
+ color-mix(in lab, var(--primary) 4%, var(--backgroundLight)),
134
+ color-mix(in lab, var(--background) 94%, transparent)
135
+ );
136
+ cursor: pointer;
137
+ transition:
138
+ border-color var(--durationS) var(--bezierFastoutSlowin),
139
+ background-color var(--durationS) var(--bezierFastoutSlowin),
140
+ box-shadow var(--durationS) var(--bezierFastoutSlowin);
141
+ }
142
+
143
+ .dropZone:hover {
144
+ border-color: color-mix(in lab, var(--primary) 35%, transparent);
145
+ background: linear-gradient(
146
+ 135deg,
147
+ color-mix(in lab, var(--primary) 7%, var(--backgroundLight)),
148
+ color-mix(in lab, var(--background) 92%, transparent)
149
+ );
150
+ }
151
+
152
+ .dropZone:focus-visible {
153
+ outline: none;
154
+ border-color: color-mix(in lab, var(--primary) 48%, transparent);
155
+ box-shadow: 0 0 0 3px color-mix(in lab, var(--primary) 14%, transparent);
156
+ }
157
+
158
+ .dropZoneActive {
159
+ border-color: color-mix(in lab, var(--primary) 50%, transparent);
160
+ background: linear-gradient(
161
+ 135deg,
162
+ color-mix(in lab, var(--primary) 10%, var(--backgroundLight)),
163
+ color-mix(in lab, var(--background) 90%, transparent)
164
+ );
165
+ box-shadow: 0 0 0 3px color-mix(in lab, var(--primary) 12%, transparent);
166
+ }
167
+
168
+ .dropZoneDisabled {
169
+ opacity: 0.7;
170
+ cursor: not-allowed;
171
+ }
172
+
173
+ .dropZonePrimary {
105
174
  margin: 0;
106
175
  font-size: var(--fontSizeBodyS);
107
176
  font-weight: var(--fontWeightMedium);
108
177
  color: var(--textTitle);
109
178
  }
110
179
 
111
- .howToList {
180
+ .dropZoneSecondary {
112
181
  margin: 0;
113
- padding-left: var(--spaceL);
114
- display: flex;
115
- flex-direction: column;
116
- gap: var(--spaceXS);
182
+ font-size: var(--fontSizeBodyXS);
117
183
  color: var(--textBody);
118
- font-size: var(--fontSizeBodyS);
119
184
  }
120
185
 
121
- .actions {
186
+ .fieldActions {
122
187
  display: flex;
123
- justify-content: flex-end;
188
+ flex-wrap: wrap;
124
189
  gap: var(--spaceS);
125
190
  }
126
191
 
127
- .status {
192
+ .fieldError {
128
193
  margin: 0;
129
194
  font-size: var(--fontSizeBodyXS);
195
+ color: var(--error);
196
+ }
197
+
198
+ .resultCard {
199
+ display: flex;
200
+ flex-direction: column;
201
+ gap: var(--spaceXS);
202
+ padding: var(--spaceM) var(--spaceL);
203
+ border-radius: var(--radiusM);
204
+ }
205
+
206
+ .resultPass {
207
+ border: 1px solid color-mix(in lab, var(--success) 38%, transparent);
208
+ background: color-mix(in lab, var(--success) 12%, var(--backgroundLight));
209
+ }
210
+
211
+ .resultFail {
212
+ border: 1px solid color-mix(in lab, var(--error) 32%, transparent);
213
+ background: color-mix(in lab, var(--errorLight) 40%, var(--backgroundLight));
214
+ }
215
+
216
+ .resultTitle {
217
+ margin: 0;
218
+ font-size: var(--fontSizeBodyM);
219
+ font-weight: var(--fontWeightBold);
220
+ letter-spacing: 0.06em;
221
+ }
222
+
223
+ .resultPass .resultTitle {
224
+ color: color-mix(in lab, var(--success) 78%, var(--black));
225
+ }
226
+
227
+ .resultFail .resultTitle {
228
+ color: color-mix(in lab, var(--error) 78%, var(--black));
229
+ }
230
+
231
+ .resultMessage {
232
+ margin: 0;
233
+ font-size: var(--fontSizeBodyS);
130
234
  color: var(--textBody);
131
235
  }
132
236
 
133
- .copyButton {
134
- background: transparent;
135
- color: var(--primary);
136
- border: 1px solid color-mix(in lab, var(--primary) 35%, transparent);
237
+ .actions {
238
+ display: flex;
239
+ justify-content: flex-end;
240
+ gap: var(--spaceS);
241
+ flex-wrap: wrap;
242
+ }
243
+
244
+ .primaryButton,
245
+ .secondaryButton {
137
246
  border-radius: var(--spaceXS);
138
247
  padding: var(--spaceS) var(--spaceL);
139
248
  font-size: var(--fontSizeBodyS);
140
249
  font-weight: var(--fontWeightMedium);
141
250
  cursor: pointer;
142
- transition: all var(--durationS) var(--bezierFastoutSlowin);
251
+ transition:
252
+ background-color var(--durationS) var(--bezierFastoutSlowin),
253
+ border-color var(--durationS) var(--bezierFastoutSlowin),
254
+ color var(--durationS) var(--bezierFastoutSlowin);
143
255
  }
144
256
 
145
- .copyButton:hover:not(:disabled) {
146
- background: color-mix(in lab, var(--primary) 10%, transparent);
147
- border-color: color-mix(in lab, var(--primary) 55%, transparent);
257
+ .primaryButton {
258
+ background: var(--primary);
259
+ color: var(--white);
260
+ border: 1px solid var(--primary);
148
261
  }
149
262
 
150
- .copyButton:disabled {
151
- background: color-mix(in lab, var(--background) 95%, transparent);
152
- color: var(--textLight);
153
- border-color: color-mix(in lab, var(--text) 10%, transparent);
154
- cursor: not-allowed;
263
+ .primaryButton:hover:not(:disabled) {
264
+ background: color-mix(in lab, var(--primary) 84%, var(--black));
265
+ border-color: color-mix(in lab, var(--primary) 84%, var(--black));
155
266
  }
156
267
 
157
- .closeModalButton {
158
- background: var(--primary);
159
- color: white;
160
- border: none;
161
- border-radius: var(--spaceXS);
162
- padding: var(--spaceS) var(--spaceL);
163
- font-size: var(--fontSizeBodyS);
164
- font-weight: var(--fontWeightMedium);
165
- cursor: pointer;
166
- transition: all var(--durationS) var(--bezierFastoutSlowin);
268
+ .secondaryButton {
269
+ background: transparent;
270
+ color: var(--textTitle);
271
+ border: 1px solid color-mix(in lab, var(--text) 16%, transparent);
167
272
  }
168
273
 
169
- .closeModalButton:hover {
170
- background: color-mix(in lab, var(--primary) 85%, var(--black));
274
+ .secondaryButton:hover:not(:disabled) {
275
+ background: color-mix(in lab, var(--text) 5%, transparent);
276
+ border-color: color-mix(in lab, var(--text) 22%, transparent);
277
+ }
278
+
279
+ .primaryButton:disabled,
280
+ .secondaryButton:disabled {
281
+ background: color-mix(in lab, var(--background) 95%, transparent);
282
+ color: var(--textLight);
283
+ border-color: color-mix(in lab, var(--text) 10%, transparent);
284
+ cursor: not-allowed;
171
285
  }