@striae-org/striae 5.2.1 → 5.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 (105) hide show
  1. package/.env.example +2 -10
  2. package/README.md +5 -46
  3. package/app/components/actions/case-export/core-export.ts +2 -174
  4. package/app/components/actions/case-export/download-handlers.ts +83 -750
  5. package/app/components/actions/case-export/index.ts +6 -30
  6. package/app/components/actions/case-export/metadata-helpers.ts +0 -78
  7. package/app/components/actions/case-export/types-constants.ts +0 -43
  8. package/app/components/actions/case-import/confirmation-import.ts +13 -14
  9. package/app/components/actions/case-import/zip-processing.ts +92 -12
  10. package/app/components/actions/generate-pdf.ts +3 -2
  11. package/app/components/audit/user-audit-viewer.tsx +0 -19
  12. package/app/components/audit/viewer/audit-viewer-header.tsx +0 -33
  13. package/app/components/navbar/case-modals/archive-case-modal.tsx +1 -1
  14. package/app/components/navbar/navbar.tsx +1 -1
  15. package/app/components/sidebar/case-import/case-import.module.css +35 -0
  16. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +59 -3
  17. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +2 -4
  18. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +1 -1
  19. package/app/components/sidebar/notes/class-details-shared.ts +2 -2
  20. package/app/components/toast/toast.module.css +36 -0
  21. package/app/components/toast/toast.tsx +6 -2
  22. package/app/components/user/manage-profile.tsx +4 -3
  23. package/app/config-example/config.json +1 -2
  24. package/app/root.tsx +0 -7
  25. package/app/routes/_index.tsx +1 -1
  26. package/app/routes/auth/login.example.tsx +22 -103
  27. package/app/routes/auth/route.ts +1 -1
  28. package/app/routes/striae/striae.tsx +53 -59
  29. package/app/services/firebase/index.ts +0 -3
  30. package/app/types/export.ts +1 -2
  31. package/app/utils/auth/index.ts +0 -1
  32. package/app/utils/data/permissions.ts +3 -2
  33. package/package.json +9 -16
  34. package/public/_headers +0 -4
  35. package/public/_routes.json +0 -1
  36. package/worker-configuration.d.ts +20 -17
  37. package/workers/audit-worker/src/audit-worker.example.ts +9 -806
  38. package/workers/audit-worker/src/config.ts +7 -0
  39. package/workers/audit-worker/src/crypto/data-at-rest.ts +410 -0
  40. package/workers/audit-worker/src/handlers/audit-routes.ts +125 -0
  41. package/workers/audit-worker/src/storage/audit-storage.ts +99 -0
  42. package/workers/audit-worker/src/types.ts +56 -0
  43. package/workers/audit-worker/worker-configuration.d.ts +1 -1
  44. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  45. package/workers/data-worker/src/config.ts +11 -0
  46. package/workers/data-worker/src/data-worker.example.ts +21 -942
  47. package/workers/data-worker/src/handlers/decrypt-export.ts +118 -0
  48. package/workers/data-worker/src/handlers/signing.ts +174 -0
  49. package/workers/data-worker/src/handlers/storage-routes.ts +129 -0
  50. package/workers/data-worker/src/registry/key-registry.ts +368 -0
  51. package/workers/data-worker/src/types.ts +46 -0
  52. package/workers/data-worker/worker-configuration.d.ts +1 -1
  53. package/workers/data-worker/wrangler.jsonc.example +1 -1
  54. package/workers/image-worker/worker-configuration.d.ts +1 -1
  55. package/workers/image-worker/wrangler.jsonc.example +1 -1
  56. package/workers/pdf-worker/worker-configuration.d.ts +2 -3
  57. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  58. package/workers/user-worker/src/auth.ts +30 -0
  59. package/workers/user-worker/src/cleanup/account-deletion.ts +337 -0
  60. package/workers/user-worker/src/config.ts +4 -0
  61. package/workers/user-worker/src/encryption-utils.ts +25 -0
  62. package/workers/user-worker/src/firebase/admin.ts +152 -0
  63. package/workers/user-worker/src/handlers/user-routes.ts +242 -0
  64. package/workers/user-worker/src/registry/user-kv.ts +172 -0
  65. package/workers/user-worker/src/storage/user-records.ts +34 -0
  66. package/workers/user-worker/src/types.ts +106 -0
  67. package/workers/user-worker/src/user-worker.example.ts +18 -964
  68. package/workers/user-worker/worker-configuration.d.ts +4 -2
  69. package/workers/user-worker/wrangler.jsonc.example +12 -1
  70. package/wrangler.toml.example +1 -1
  71. package/app/components/actions/case-export/data-processing.ts +0 -223
  72. package/app/components/sidebar/case-export/case-export.module.css +0 -418
  73. package/app/components/sidebar/case-export/case-export.tsx +0 -310
  74. package/app/types/exceljs-bare.d.ts +0 -9
  75. package/app/utils/auth/auth.ts +0 -11
  76. package/public/.well-known/security.txt +0 -6
  77. package/public/favicon.ico +0 -0
  78. package/public/icon-256.png +0 -0
  79. package/public/icon-512.png +0 -0
  80. package/public/manifest.json +0 -39
  81. package/public/shortcut.png +0 -0
  82. package/public/social-image.png +0 -0
  83. package/public/vendor/exceljs.LICENSE +0 -22
  84. package/public/vendor/exceljs.bare.min.js +0 -45
  85. package/scripts/deploy-all.sh +0 -166
  86. package/scripts/deploy-config/modules/env-utils.sh +0 -322
  87. package/scripts/deploy-config/modules/keys.sh +0 -404
  88. package/scripts/deploy-config/modules/prompt.sh +0 -372
  89. package/scripts/deploy-config/modules/scaffolding.sh +0 -344
  90. package/scripts/deploy-config/modules/validation.sh +0 -365
  91. package/scripts/deploy-config.sh +0 -236
  92. package/scripts/deploy-pages-secrets.sh +0 -231
  93. package/scripts/deploy-pages.sh +0 -34
  94. package/scripts/deploy-primershear-emails.sh +0 -167
  95. package/scripts/deploy-worker-secrets.sh +0 -374
  96. package/scripts/dev.cjs +0 -23
  97. package/scripts/install-workers.sh +0 -88
  98. package/scripts/run-eslint.cjs +0 -43
  99. package/scripts/update-compatibility-dates.cjs +0 -124
  100. package/scripts/update-markdown-versions.cjs +0 -43
  101. package/workers/keys-worker/package.json +0 -18
  102. package/workers/keys-worker/src/keys.example.ts +0 -67
  103. package/workers/keys-worker/src/keys.ts +0 -67
  104. package/workers/keys-worker/worker-configuration.d.ts +0 -7447
  105. package/workers/keys-worker/wrangler.jsonc.example +0 -15
@@ -1,9 +1,11 @@
1
1
  /* eslint-disable */
2
- // Generated by Wrangler by running `wrangler types` (hash: 9778af5afe0b181b6b7f39a3f261ec74)
3
- // Runtime types generated with workerd@1.20250823.0 2026-03-20 nodejs_compat
2
+ // Generated by Wrangler by running `wrangler types` (hash: e49bcad627d7e39b486bd9867a1c400d)
3
+ // Runtime types generated with workerd@1.20250823.0 2026-03-26 nodejs_compat
4
4
  declare namespace Cloudflare {
5
5
  interface Env {
6
6
  USER_DB: KVNamespace;
7
+ STRIAE_DATA: R2Bucket;
8
+ STRIAE_FILES: R2Bucket;
7
9
  }
8
10
  }
9
11
  interface Env extends Cloudflare.Env {}
@@ -2,7 +2,7 @@
2
2
  "name": "USER_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/user-worker.ts",
5
- "compatibility_date": "2026-03-25",
5
+ "compatibility_date": "2026-03-26",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -18,5 +18,16 @@
18
18
  }
19
19
  ],
20
20
 
21
+ "r2_buckets": [
22
+ {
23
+ "binding": "STRIAE_DATA",
24
+ "bucket_name": "DATA_BUCKET_NAME"
25
+ },
26
+ {
27
+ "binding": "STRIAE_FILES",
28
+ "bucket_name": "FILES_BUCKET_NAME"
29
+ }
30
+ ],
31
+
21
32
  "placement": { "mode": "smart" }
22
33
  }
@@ -1,6 +1,6 @@
1
1
  #:schema node_modules/wrangler/config-schema.json
2
2
  name = "PAGES_PROJECT_NAME"
3
- compatibility_date = "2026-03-25"
3
+ compatibility_date = "2026-03-26"
4
4
  compatibility_flags = ["nodejs_compat"]
5
5
  pages_build_output_dir = "./build/client"
6
6
 
@@ -1,223 +0,0 @@
1
- import { type CaseExportData } from '~/types';
2
- import { calculateSHA256Secure } from '~/utils/forensics';
3
- import { CSV_HEADERS } from './types-constants';
4
- import { addForensicDataWarning } from './metadata-helpers';
5
-
6
- export type TabularCell = string | number | boolean | null;
7
-
8
- const MAX_SPREADSHEET_CELL_LENGTH = 32767;
9
- const DANGEROUS_FORMULA_PREFIX_PATTERN = /^\s*[=+\-@]/;
10
-
11
- function stripUnsafeControlChars(input: string): string {
12
- let output = '';
13
-
14
- for (let index = 0; index < input.length; index += 1) {
15
- const code = input.charCodeAt(index);
16
- const isControlChar = code <= 0x1f || code === 0x7f;
17
- const isAllowedWhitespace = code === 0x09 || code === 0x0a || code === 0x0d;
18
-
19
- if (!isControlChar || isAllowedWhitespace) {
20
- output += input[index];
21
- }
22
- }
23
-
24
- return output;
25
- }
26
-
27
- /**
28
- * Sanitize cell values before CSV/XLSX export.
29
- * - Strips non-printable control characters (except tab/newline/carriage return)
30
- * - Prevents formula injection when files are opened in spreadsheet tools
31
- * - Caps content length to Excel's per-cell limit
32
- */
33
- export function sanitizeTabularCell(value: unknown): TabularCell {
34
- if (value === null || value === undefined) {
35
- return '';
36
- }
37
-
38
- if (typeof value === 'number' || typeof value === 'boolean') {
39
- return value;
40
- }
41
-
42
- let normalized = stripUnsafeControlChars(String(value));
43
-
44
- if (normalized.length > MAX_SPREADSHEET_CELL_LENGTH) {
45
- normalized = normalized.slice(0, MAX_SPREADSHEET_CELL_LENGTH);
46
- }
47
-
48
- if (DANGEROUS_FORMULA_PREFIX_PATTERN.test(normalized)) {
49
- return `'${normalized}`;
50
- }
51
-
52
- return normalized;
53
- }
54
-
55
- export function sanitizeTabularMatrix(
56
- rows: Array<Array<string | number | boolean | null | undefined>>
57
- ): TabularCell[][] {
58
- return rows.map((row) => row.map((cell) => sanitizeTabularCell(cell)));
59
- }
60
-
61
- /**
62
- * Generate metadata rows for tabular format
63
- */
64
- export function generateMetadataRows(exportData: CaseExportData): TabularCell[][] {
65
- return sanitizeTabularMatrix([
66
- ['Case Export Report'],
67
- [''],
68
- ['Case Number', exportData.metadata.caseNumber],
69
- ['Case Created Date', exportData.metadata.caseCreatedDate],
70
- ['Export Date', exportData.metadata.exportDate],
71
- ['Exported By (Email)', exportData.metadata.exportedBy || 'N/A'],
72
- ['Exported By (UID)', exportData.metadata.exportedByUid || 'N/A'],
73
- ['Exported By (Name)', exportData.metadata.exportedByName || 'N/A'],
74
- ['Exported By (Company)', exportData.metadata.exportedByCompany || 'N/A'],
75
- ['Exported By (Badge/ID)', exportData.metadata.exportedByBadgeId || 'N/A'],
76
- ['Striae Export Schema Version', exportData.metadata.striaeExportSchemaVersion],
77
- ['Total Files', exportData.metadata.totalFiles.toString()],
78
- [''],
79
- ['Summary'],
80
- ['Files with Annotations', (exportData.summary?.filesWithAnnotations || 0).toString()],
81
- ['Files without Annotations', (exportData.summary?.filesWithoutAnnotations || 0).toString()],
82
- ['Total Box Annotations', (exportData.summary?.totalBoxAnnotations || 0).toString()],
83
- ['Files with Confirmations', (exportData.summary?.filesWithConfirmations || 0).toString()],
84
- ['Files with Confirmations Requested', (exportData.summary?.filesWithConfirmationsRequested || 0).toString()],
85
- ['Last Modified', exportData.summary?.lastModified || 'N/A'],
86
- ['Earliest Annotation Date', exportData.summary?.earliestAnnotationDate || 'N/A'],
87
- ['Latest Annotation Date', exportData.summary?.latestAnnotationDate || 'N/A'],
88
- [''],
89
- ['File Details']
90
- ]);
91
- }
92
-
93
- /**
94
- * Process file data for tabular format (CSV/Excel)
95
- */
96
- export function processFileDataForTabular(fileEntry: CaseExportData['files'][0]): TabularCell[][] {
97
- // Full file data for the first row (excluding Additional Notes and Last Updated)
98
- const fullFileData = [
99
- fileEntry.fileData.id,
100
- fileEntry.fileData.originalFilename,
101
- fileEntry.fileData.uploadedAt,
102
- fileEntry.hasAnnotations ? 'Yes' : 'No',
103
- fileEntry.annotations?.leftCase || '',
104
- fileEntry.annotations?.rightCase || '',
105
- fileEntry.annotations?.leftItem || '',
106
- fileEntry.annotations?.rightItem || '',
107
- fileEntry.annotations?.caseFontColor || '',
108
- fileEntry.annotations?.classType || '',
109
- fileEntry.annotations?.customClass || '',
110
- fileEntry.annotations?.classNote || '',
111
- fileEntry.annotations?.indexType || '',
112
- fileEntry.annotations?.indexNumber || '',
113
- fileEntry.annotations?.indexColor || '',
114
- fileEntry.annotations?.supportLevel || '',
115
- fileEntry.annotations?.hasSubclass ? 'Yes' : 'No',
116
- fileEntry.annotations?.includeConfirmation ? 'Yes' : 'No',
117
- fileEntry.annotations?.confirmationData ? 'Confirmed' : (fileEntry.annotations?.includeConfirmation ? 'Requested' : 'Not Requested'),
118
- fileEntry.annotations?.confirmationData?.fullName || '',
119
- fileEntry.annotations?.confirmationData?.badgeId || '',
120
- fileEntry.annotations?.confirmationData?.confirmedByEmail || '',
121
- fileEntry.annotations?.confirmationData?.confirmedByCompany || '',
122
- fileEntry.annotations?.confirmationData?.confirmationId || '',
123
- fileEntry.annotations?.confirmationData?.timestamp || '',
124
- fileEntry.annotations?.confirmationData?.confirmedAt || '',
125
- (fileEntry.annotations?.boxAnnotations?.length || 0).toString()
126
- ];
127
-
128
- // Additional Notes and Last Updated (at the end)
129
- const additionalFileData = [
130
- fileEntry.annotations?.additionalNotes || '',
131
- fileEntry.annotations?.updatedAt || ''
132
- ];
133
-
134
- // Calculate array sizes programmatically from CSV_HEADERS
135
- const fileDataColumnCount = fullFileData.length; // Dynamic count based on actual data
136
- const additionalDataColumnCount = additionalFileData.length; // Dynamic count based on actual data
137
-
138
- // Empty row template for subsequent box annotations (file info columns empty)
139
- const emptyFileData = Array(fileDataColumnCount).fill('');
140
- const emptyAdditionalData = Array(additionalDataColumnCount).fill('');
141
-
142
- const rows: Array<Array<string | number | boolean | null | undefined>> = [];
143
-
144
- // If there are box annotations, create a row for each one
145
- if (fileEntry.annotations?.boxAnnotations && fileEntry.annotations.boxAnnotations.length > 0) {
146
- fileEntry.annotations.boxAnnotations.forEach((box, index) => {
147
- const rowData = index === 0 ? fullFileData : emptyFileData;
148
- const additionalData = index === 0 ? additionalFileData : emptyAdditionalData;
149
-
150
- rows.push([
151
- ...rowData,
152
- box.id,
153
- box.x.toString(),
154
- box.y.toString(),
155
- box.width.toString(),
156
- box.height.toString(),
157
- box.color || '',
158
- box.label || '',
159
- box.timestamp || '',
160
- ...additionalData
161
- ]);
162
- });
163
- } else {
164
- // If no box annotations, still include one row with empty box data
165
- rows.push([
166
- ...fullFileData,
167
- '', // Box ID
168
- '', // Box X
169
- '', // Box Y
170
- '', // Box Width
171
- '', // Box Height
172
- '', // Box Color
173
- '', // Box Label
174
- '', // Box Timestamp
175
- ...additionalFileData
176
- ]);
177
- }
178
-
179
- return sanitizeTabularMatrix(rows);
180
- }
181
-
182
- /**
183
- * Generate CSV content from export data
184
- */
185
- export async function generateCSVContent(exportData: CaseExportData, protectForensicData: boolean = true): Promise<string> {
186
- // Case metadata section
187
- const metadataRows = generateMetadataRows(exportData);
188
-
189
- // File data rows
190
- const fileRows: TabularCell[][] = [];
191
- exportData.files.forEach(fileEntry => {
192
- const processedRows = processFileDataForTabular(fileEntry);
193
- fileRows.push(...processedRows);
194
- });
195
-
196
- // Combine data rows for hash calculation (excluding header comments)
197
- const dataRows: TabularCell[][] = [
198
- ...metadataRows,
199
- ...sanitizeTabularMatrix([CSV_HEADERS]),
200
- ...fileRows
201
- ];
202
-
203
- const csvDataContent = dataRows.map(row =>
204
- row.map(field => `"${String(field).replace(/"/g, '""')}"`).join(',')
205
- ).join('\n');
206
-
207
- // Calculate hash for integrity verification
208
- const hash = await calculateSHA256Secure(csvDataContent);
209
-
210
- // Create final CSV with hash header
211
- const csvWithHash = [
212
- `# Striae Case Export - Generated: ${new Date().toISOString()}`,
213
- `# Case: ${exportData.metadata.caseNumber}`,
214
- `# Total Files: ${exportData.metadata.totalFiles}`,
215
- `# SHA-256 Hash: ${hash.toUpperCase()}`,
216
- '# Verification: Recalculate SHA-256 of data rows only (excluding these comment lines)',
217
- '',
218
- csvDataContent
219
- ].join('\n');
220
-
221
- // Add forensic protection warning if enabled
222
- return protectForensicData ? addForensicDataWarning(csvWithHash, 'csv') : csvWithHash;
223
- }
@@ -1,418 +0,0 @@
1
- .overlay {
2
- position: fixed;
3
- inset: 0;
4
- background-color: color-mix(in lab, var(--background) 50%, transparent);
5
- display: flex;
6
- justify-content: center;
7
- align-items: center;
8
- z-index: var(--zIndex5);
9
- cursor: default;
10
- transition: background-color var(--durationM) var(--bezierFastoutSlowin);
11
- }
12
-
13
- .modal {
14
- position: relative;
15
- background: var(--backgroundLight);
16
- border-radius: var(--spaceXS);
17
- width: 90%;
18
- max-width: 480px;
19
- max-height: 90vh;
20
- display: flex;
21
- flex-direction: column;
22
- overflow: hidden;
23
- box-shadow: 0 var(--spaceXS) var(--spaceL)
24
- color-mix(in lab, var(--black) 10%, transparent);
25
- cursor: default;
26
- transition: background-color var(--durationM) var(--bezierFastoutSlowin);
27
- }
28
-
29
- .header {
30
- display: flex;
31
- font-size: var(--fontSizeBodyL);
32
- justify-content: space-between;
33
- align-items: center;
34
- padding: var(--spaceL);
35
- border-bottom: 1px solid color-mix(in lab, var(--text) 10%, transparent);
36
- color: var(--textTitle);
37
- }
38
-
39
- .title {
40
- margin: 0;
41
- font-size: var(--fontSizeBodyL);
42
- font-weight: 600;
43
- color: var(--textTitle);
44
- }
45
-
46
- .closeButton {
47
- background: none;
48
- border: none;
49
- font-size: var(--fontSizeH5);
50
- cursor: pointer;
51
- padding: var(--spaceS);
52
- color: var(--textLight);
53
- transition: color var(--durationS) var(--bezierFastoutSlowin);
54
- }
55
-
56
- .closeButton:hover {
57
- color: var(--text);
58
- }
59
-
60
- .content {
61
- padding: var(--spaceL);
62
- flex: 1 1 auto;
63
- min-height: 0;
64
- overflow-y: auto;
65
- overflow-x: hidden;
66
- }
67
-
68
- .formatSelector {
69
- display: flex;
70
- align-items: center;
71
- justify-content: space-between;
72
- padding: var(--spaceM);
73
- background: var(--backgroundLight);
74
- border-radius: var(--spaceXS);
75
- border: 1px solid color-mix(in lab, var(--text) 10%, transparent);
76
- margin-top: var(--spaceM);
77
- }
78
-
79
- .formatLabel {
80
- font-size: var(--fontSizeBodyS);
81
- font-weight: var(--fontWeightMedium);
82
- color: var(--textTitle);
83
- margin: 0;
84
- }
85
-
86
- .formatToggle {
87
- display: flex;
88
- background: color-mix(in lab, var(--primary) 10%, transparent);
89
- border: 1px solid color-mix(in lab, var(--primary) 20%, transparent);
90
- border-radius: var(--spaceXS);
91
- overflow: hidden;
92
- box-shadow: 0 1px 3px color-mix(in lab, var(--primary) 15%, transparent);
93
- }
94
-
95
- .formatOption {
96
- background: transparent;
97
- border: none;
98
- padding: var(--spaceS) var(--spaceM);
99
- font-size: var(--fontSizeBodyS);
100
- font-weight: var(--fontWeightMedium);
101
- color: var(--primary);
102
- cursor: pointer;
103
- transition: all var(--durationS) var(--bezierFastoutSlowin);
104
- position: relative;
105
- min-width: 60px;
106
- }
107
-
108
- .formatOption:hover:not(:disabled) {
109
- background: color-mix(in lab, var(--primary) 15%, transparent);
110
- color: var(--primary);
111
- }
112
-
113
- .formatOption:disabled {
114
- opacity: 0.5;
115
- cursor: not-allowed;
116
- }
117
-
118
- .formatOptionActive {
119
- background: var(--primary) !important;
120
- color: white !important;
121
- box-shadow: 0 1px 3px color-mix(in lab, var(--primary) 30%, transparent);
122
- }
123
-
124
- .formatOptionActive:hover:not(:disabled) {
125
- background: color-mix(in lab, var(--primary) 85%, var(--black)) !important;
126
- }
127
-
128
- .imageOption {
129
- display: flex;
130
- align-items: center;
131
- justify-content: space-between;
132
- padding: var(--spaceM);
133
- background: var(--backgroundLight);
134
- border-radius: var(--spaceXS);
135
- border: 1px solid color-mix(in lab, var(--text) 10%, transparent);
136
- margin-top: var(--spaceM);
137
- }
138
-
139
- .checkboxLabel {
140
- display: flex;
141
- align-items: center;
142
- gap: var(--spaceS);
143
- cursor: pointer;
144
- user-select: none;
145
- }
146
-
147
- .checkbox {
148
- appearance: none;
149
- width: 18px;
150
- height: 18px;
151
- border: 2px solid color-mix(in lab, var(--primary) 40%, transparent);
152
- border-radius: 4px;
153
- background: transparent;
154
- cursor: pointer;
155
- transition: all var(--durationS) var(--bezierFastoutSlowin);
156
- position: relative;
157
- flex-shrink: 0;
158
- }
159
-
160
- .checkbox:checked {
161
- background: var(--primary);
162
- border-color: var(--primary);
163
- }
164
-
165
- .checkbox:checked::after {
166
- content: "";
167
- position: absolute;
168
- left: 3px;
169
- top: 0px;
170
- width: 6px;
171
- height: 10px;
172
- border: solid white;
173
- border-width: 0 2px 2px 0;
174
- transform: rotate(45deg);
175
- }
176
-
177
- .checkbox:hover:not(:disabled) {
178
- border-color: var(--primary);
179
- box-shadow: 0 0 0 2px color-mix(in lab, var(--primary) 20%, transparent);
180
- }
181
-
182
- .checkbox:disabled {
183
- opacity: 0.5;
184
- cursor: not-allowed;
185
- }
186
-
187
- .imageOption:has(.checkbox:disabled) {
188
- opacity: 0.6;
189
- }
190
-
191
- .imageOption:has(.checkbox:disabled) .checkboxLabel {
192
- cursor: not-allowed;
193
- }
194
-
195
- .checkboxText {
196
- font-size: var(--fontSizeBodyS);
197
- font-weight: var(--fontWeightMedium);
198
- color: var(--textTitle);
199
- margin: 0;
200
- }
201
-
202
- .checkboxTooltip {
203
- display: block;
204
- font-size: var(--fontSizeBodyXS);
205
- color: var(--textBody);
206
- margin: 0;
207
- opacity: 0.8;
208
- }
209
-
210
- .fieldGroup {
211
- display: flex;
212
- flex-direction: column;
213
- gap: var(--spaceM);
214
- }
215
-
216
- .inputGroup {
217
- display: flex;
218
- gap: var(--spaceM);
219
- align-items: center;
220
- }
221
-
222
- .input {
223
- flex: 1;
224
- padding: var(--spaceM);
225
- border: 1px solid color-mix(in lab, var(--text) 10%, transparent);
226
- border-radius: var(--spaceXS);
227
- font-size: var(--fontSizeBodyS);
228
- background: var(--backgroundLight);
229
- color: var(--textBody);
230
- transition: all var(--durationS) var(--bezierFastoutSlowin);
231
- }
232
-
233
- .input:focus {
234
- outline: none;
235
- border-color: color-mix(in lab, var(--text) 30%, transparent);
236
- box-shadow: 0 0 0 2px color-mix(in lab, var(--text) 10%, transparent);
237
- }
238
-
239
- .input:disabled {
240
- background: color-mix(in lab, var(--background) 95%, transparent);
241
- color: var(--textLight);
242
- cursor: not-allowed;
243
- }
244
-
245
- .exportButton {
246
- background: var(--success);
247
- color: white;
248
- border: none;
249
- border-radius: var(--spaceXS);
250
- padding: var(--spaceM) var(--spaceL);
251
- font-size: var(--fontSizeBodyS);
252
- font-weight: var(--fontWeightMedium);
253
- cursor: pointer;
254
- transition: all var(--durationS) var(--bezierFastoutSlowin);
255
- white-space: nowrap;
256
- min-width: 140px;
257
- box-shadow: 0 1px 3px color-mix(in lab, var(--success) 30%, transparent);
258
- }
259
-
260
- .exportButton:hover:not(:disabled) {
261
- background: color-mix(in lab, var(--success) 85%, var(--black));
262
- box-shadow: 0 2px 6px color-mix(in lab, var(--success) 40%, transparent);
263
- }
264
-
265
- .exportButton:disabled {
266
- background: color-mix(in lab, var(--background) 95%, transparent);
267
- color: var(--textLight);
268
- cursor: not-allowed;
269
- box-shadow: none;
270
- }
271
-
272
- .confirmationExportButton {
273
- background: var(--accent);
274
- color: white;
275
- border: none;
276
- border-radius: var(--spaceXS);
277
- padding: var(--spaceM) var(--spaceL);
278
- font-size: var(--fontSizeBodyS);
279
- font-weight: var(--fontWeightMedium);
280
- cursor: pointer;
281
- transition: all var(--durationS) var(--bezierFastoutSlowin);
282
- white-space: nowrap;
283
- min-width: 140px;
284
- box-shadow: 0 1px 3px color-mix(in lab, var(--accent) 30%, transparent);
285
- }
286
-
287
- .confirmationExportButton:hover:not(:disabled) {
288
- background: color-mix(in lab, var(--accent) 85%, var(--black));
289
- box-shadow: 0 2px 6px color-mix(in lab, var(--accent) 40%, transparent);
290
- }
291
-
292
- .confirmationExportButton:disabled {
293
- background: color-mix(in lab, var(--background) 95%, transparent);
294
- color: var(--textLight);
295
- cursor: not-allowed;
296
- box-shadow: none;
297
- }
298
-
299
- .publicKeySection {
300
- margin-top: var(--spaceS);
301
- }
302
-
303
- .publicKeyButton {
304
- width: 100%;
305
- background: transparent;
306
- color: var(--primary);
307
- border: 1px solid color-mix(in lab, var(--primary) 35%, transparent);
308
- border-radius: var(--spaceXS);
309
- padding: var(--spaceS) var(--spaceM);
310
- font-size: var(--fontSizeBodyS);
311
- font-weight: var(--fontWeightMedium);
312
- cursor: pointer;
313
- transition: all var(--durationS) var(--bezierFastoutSlowin);
314
- }
315
-
316
- .publicKeyButton:hover {
317
- background: color-mix(in lab, var(--primary) 10%, transparent);
318
- border-color: color-mix(in lab, var(--primary) 55%, transparent);
319
- }
320
-
321
- .divider {
322
- margin: var(--spaceL) 0;
323
- text-align: center;
324
- position: relative;
325
- color: var(--textLight);
326
- font-size: var(--fontSizeBodyS);
327
- font-weight: var(--fontWeightMedium);
328
- }
329
-
330
- .divider::before {
331
- content: "";
332
- position: absolute;
333
- top: 50%;
334
- left: 0;
335
- right: 0;
336
- height: 1px;
337
- background: color-mix(in lab, var(--text) 10%, transparent);
338
- z-index: 1;
339
- }
340
-
341
- .divider span {
342
- background: var(--backgroundLight);
343
- padding: 0 var(--spaceM);
344
- position: relative;
345
- z-index: 2;
346
- }
347
-
348
- .exportAllSection {
349
- text-align: center;
350
- margin-bottom: var(--spaceM);
351
- }
352
-
353
- .exportAllButton {
354
- background: var(--primary);
355
- color: white;
356
- border: none;
357
- border-radius: var(--spaceXS);
358
- padding: var(--spaceM) var(--spaceL);
359
- font-size: var(--fontSizeBodyS);
360
- font-weight: var(--fontWeightMedium);
361
- cursor: pointer;
362
- transition: all var(--durationS) var(--bezierFastoutSlowin);
363
- width: 100%;
364
- box-shadow: 0 1px 3px color-mix(in lab, var(--primary) 30%, transparent);
365
- }
366
-
367
- .exportAllButton:hover:not(:disabled) {
368
- background: color-mix(in lab, var(--primary) 85%, var(--black));
369
- box-shadow: 0 2px 6px color-mix(in lab, var(--primary) 40%, transparent);
370
- }
371
-
372
- .exportAllButton:disabled {
373
- background: color-mix(in lab, var(--background) 95%, transparent);
374
- color: var(--textLight);
375
- cursor: not-allowed;
376
- box-shadow: none;
377
- }
378
-
379
- .progressSection {
380
- margin: var(--spaceM) 0;
381
- padding: var(--spaceM);
382
- background: var(--backgroundLight);
383
- border-radius: var(--spaceXS);
384
- border: 1px solid color-mix(in lab, var(--text) 10%, transparent);
385
- }
386
-
387
- .progressText {
388
- font-size: var(--fontSizeBodyS);
389
- color: var(--textBody);
390
- margin-bottom: var(--spaceS);
391
- font-weight: var(--fontWeightMedium);
392
- }
393
-
394
- .progressBar {
395
- width: 100%;
396
- height: 8px;
397
- background: color-mix(in lab, var(--text) 10%, transparent);
398
- border-radius: var(--spaceXS);
399
- overflow: hidden;
400
- }
401
-
402
- .progressFill {
403
- height: 100%;
404
- background: color-mix(in lab, var(--text) 60%, transparent);
405
- border-radius: var(--spaceXS);
406
- transition: width var(--durationS) var(--bezierFastoutSlowin);
407
- }
408
-
409
- .error {
410
- margin-top: var(--spaceM);
411
- padding: var(--spaceM);
412
- background: color-mix(in lab, var(--error) 10%, transparent);
413
- border: 1px solid color-mix(in lab, var(--error) 20%, transparent);
414
- border-radius: var(--spaceXS);
415
- color: var(--error);
416
- font-size: var(--fontSizeBodyS);
417
- font-weight: var(--fontWeightMedium);
418
- }