@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
@@ -1,16 +1,13 @@
1
1
  import type { User } from 'firebase/auth';
2
- import paths from '~/config/config.json';
3
2
  import {
4
- getImageApiKey,
5
3
  getAccountHash
6
4
  } from '~/utils/auth';
5
+ import { fetchImageApi, uploadImageApi } from '~/utils/image-api-client';
7
6
  import { canUploadFile } from '~/utils/permissions';
8
7
  import { getCaseData, updateCaseData, deleteFileAnnotations } from '~/utils/data-operations';
9
8
  import type { CaseData, FileData, ImageUploadResponse } from '~/types';
10
9
  import { auditService } from '~/services/audit';
11
10
 
12
- const IMAGE_URL = paths.image_worker_url;
13
-
14
11
  export const fetchFiles = async (
15
12
  user: User,
16
13
  caseNumber: string,
@@ -52,127 +49,70 @@ export const uploadFile = async (
52
49
  throw new Error(permission.reason || 'You cannot upload more files to this case.');
53
50
  }
54
51
 
55
- const imagesApiToken = await getImageApiKey();
56
-
57
- return new Promise((resolve, reject) => {
58
- const xhr = new XMLHttpRequest();
59
- const formData = new FormData();
60
- formData.append('file', file);
61
-
62
- xhr.upload.addEventListener('progress', (event) => {
63
- if (event.lengthComputable && onProgress) {
64
- const progress = Math.round((event.loaded / event.total) * 100);
65
- onProgress(progress);
66
- }
67
- });
68
-
69
- xhr.addEventListener('load', async () => {
70
- const endTime = Date.now();
71
-
72
- if (xhr.status === 200) {
73
- try {
74
- const imageData = JSON.parse(xhr.responseText) as ImageUploadResponse;
75
- if (!imageData.success) throw new Error('Upload failed');
76
-
77
- const newFile: FileData = {
78
- id: imageData.result.id,
79
- originalFilename: file.name,
80
- uploadedAt: new Date().toISOString()
81
- };
52
+ try {
53
+ const imageData: ImageUploadResponse = await uploadImageApi(user, file, onProgress);
54
+ const uploadedImageId = imageData.result?.id;
55
+ if (!uploadedImageId) {
56
+ throw new Error('Upload failed');
57
+ }
82
58
 
83
- // Update case data using centralized function
84
- const existingData = await getCaseData(user, caseNumber);
85
- if (!existingData) {
86
- throw new Error('Case not found');
87
- }
59
+ const newFile: FileData = {
60
+ id: uploadedImageId,
61
+ originalFilename: file.name,
62
+ uploadedAt: new Date().toISOString()
63
+ };
88
64
 
89
- const updatedData = {
90
- ...existingData,
91
- files: [...(existingData.files || []), newFile]
92
- };
65
+ // Update case data using centralized function
66
+ const existingData = await getCaseData(user, caseNumber);
67
+ if (!existingData) {
68
+ throw new Error('Case not found');
69
+ }
93
70
 
94
- await updateCaseData(user, caseNumber, updatedData);
71
+ const updatedData = {
72
+ ...existingData,
73
+ files: [...(existingData.files || []), newFile]
74
+ };
95
75
 
96
- // Log successful file upload
97
- try {
98
- await auditService.logFileUpload(
99
- user,
100
- file.name,
101
- file.size,
102
- file.type,
103
- 'file-picker',
104
- caseNumber,
105
- 'success',
106
- endTime - startTime,
107
- imageData.result.id
108
- );
109
- } catch (auditError) {
110
- console.error('Failed to log successful file upload:', auditError);
111
- }
76
+ await updateCaseData(user, caseNumber, updatedData);
112
77
 
113
- console.log(`✅ File uploaded: ${file.name} (${file.size} bytes) (${endTime - startTime}ms)`);
114
- resolve(newFile);
115
- } catch (error) {
116
- // Log failed file upload
117
- try {
118
- await auditService.logFileUpload(
119
- user,
120
- file.name,
121
- file.size,
122
- file.type,
123
- 'file-picker',
124
- caseNumber,
125
- 'failure',
126
- endTime - startTime
127
- );
128
- } catch (auditError) {
129
- console.error('Failed to log file upload failure:', auditError);
130
- }
131
- reject(error);
132
- }
133
- } else {
134
- // Log failed file upload
135
- try {
136
- await auditService.logFileUpload(
137
- user,
138
- file.name,
139
- file.size,
140
- file.type,
141
- 'file-picker',
142
- caseNumber,
143
- 'failure',
144
- endTime - startTime
145
- );
146
- } catch (auditError) {
147
- console.error('Failed to log file upload failure:', auditError);
148
- }
149
- reject(new Error('Upload failed'));
150
- }
151
- });
78
+ // Log successful file upload
79
+ try {
80
+ await auditService.logFileUpload(
81
+ user,
82
+ file.name,
83
+ file.size,
84
+ file.type,
85
+ 'file-picker',
86
+ caseNumber,
87
+ 'success',
88
+ Date.now() - startTime,
89
+ uploadedImageId
90
+ );
91
+ } catch (auditError) {
92
+ console.error('Failed to log successful file upload:', auditError);
93
+ }
152
94
 
153
- xhr.addEventListener('error', async () => {
154
- // Log upload error
155
- try {
156
- await auditService.logFileUpload(
157
- user,
158
- file.name,
159
- file.size,
160
- file.type,
161
- 'file-picker',
162
- caseNumber,
163
- 'failure',
164
- Date.now() - startTime
165
- );
166
- } catch (auditError) {
167
- console.error('Failed to log file upload error:', auditError);
168
- }
169
- reject(new Error('Upload failed'));
170
- });
95
+ console.log(`✅ File uploaded: ${file.name} (${file.size} bytes) (${Date.now() - startTime}ms)`);
96
+ return newFile;
97
+ } catch (error) {
98
+ // Log failed file upload
99
+ try {
100
+ await auditService.logFileUpload(
101
+ user,
102
+ file.name,
103
+ file.size,
104
+ file.type,
105
+ 'file-picker',
106
+ caseNumber,
107
+ 'failure',
108
+ Date.now() - startTime
109
+ );
110
+ } catch (auditError) {
111
+ console.error('Failed to log file upload failure:', auditError);
112
+ }
171
113
 
172
- xhr.open('POST', IMAGE_URL);
173
- xhr.setRequestHeader('Authorization', `Bearer ${imagesApiToken}`);
174
- xhr.send(formData);
175
- });
114
+ throw error;
115
+ }
176
116
  };
177
117
 
178
118
  export const deleteFile = async (user: User, caseNumber: string, fileId: string, deleteReason: string = 'User-requested deletion via file list'): Promise<void> => {
@@ -197,12 +137,8 @@ export const deleteFile = async (user: User, caseNumber: string, fileId: string,
197
137
  let imageDeleteError = '';
198
138
 
199
139
  // Attempt to delete image file
200
- const imagesApiToken = await getImageApiKey();
201
- const imageResponse = await fetch(`${IMAGE_URL}/${fileId}`, {
202
- method: 'DELETE',
203
- headers: {
204
- 'Authorization': `Bearer ${imagesApiToken}`
205
- }
140
+ const imageResponse = await fetchImageApi(user, `/${encodeURIComponent(fileId)}`, {
141
+ method: 'DELETE'
206
142
  });
207
143
 
208
144
  // Handle image deletion response
@@ -306,14 +242,12 @@ export const getImageUrl = async (user: User, fileData: FileData, caseNumber: st
306
242
  const defaultAccessReason = accessReason || 'Image viewer access';
307
243
 
308
244
  try {
309
- const { accountHash } = await getImageConfig();
310
- const imagesApiToken = await getImageApiKey();
245
+ const { accountHash } = await getImageConfig();
311
246
  const imageDeliveryUrl = `https://imagedelivery.net/${accountHash}/${fileData.id}/${DEFAULT_VARIANT}`;
312
-
313
- const workerResponse = await fetch(`${IMAGE_URL}/${imageDeliveryUrl}`, {
247
+
248
+ const workerResponse = await fetchImageApi(user, `/${imageDeliveryUrl}`, {
314
249
  method: 'GET',
315
250
  headers: {
316
- 'Authorization': `Bearer ${imagesApiToken}`,
317
251
  'Accept': 'text/plain'
318
252
  }
319
253
  });
@@ -9,13 +9,14 @@
9
9
  background: var(--backgroundLight);
10
10
  cursor: pointer;
11
11
  transition: all var(--durationS) var(--bezierFastoutSlowin);
12
- box-shadow: 0 1px 3px color-mix(in lab, var(--backgroundLight) 30%, transparent);
12
+ box-shadow: 0 1px 3px
13
+ color-mix(in lab, var(--backgroundLight) 30%, transparent);
13
14
  }
14
15
 
15
16
  .button:hover {
16
17
  background: color-mix(in lab, var(--backgroundLight) 85%, var(--black));
17
- transform: translateY(-1px);
18
- box-shadow: 0 2px 6px color-mix(in lab, var(--backgroundLight) 40%, transparent);
18
+ box-shadow: 0 2px 6px
19
+ color-mix(in lab, var(--backgroundLight) 40%, transparent);
19
20
  }
20
21
 
21
22
  .button.active {
@@ -25,7 +26,6 @@
25
26
 
26
27
  .button.active:hover {
27
28
  background: color-mix(in lab, var(--success) 85%, var(--black));
28
- transform: translateY(-1px);
29
29
  box-shadow: 0 2px 6px color-mix(in lab, var(--success) 40%, transparent);
30
30
  }
31
31
 
@@ -43,7 +43,7 @@
43
43
  box-shadow: none;
44
44
  }
45
45
 
46
- .icon {
46
+ .icon {
47
47
  color: var(--text);
48
48
  transition: color var(--durationS) var(--bezierFastoutSlowin);
49
49
  }
@@ -58,6 +58,10 @@
58
58
  }
59
59
 
60
60
  @keyframes spin {
61
- 0% { transform: rotate(0deg); }
62
- 100% { transform: rotate(360deg); }
63
- }
61
+ 0% {
62
+ transform: rotate(0deg);
63
+ }
64
+ 100% {
65
+ transform: rotate(360deg);
66
+ }
67
+ }
@@ -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
  }