@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.
- package/.env.example +1 -1
- package/app/components/actions/case-export/core-export.ts +5 -2
- package/app/components/actions/case-export/download-handlers.ts +51 -3
- package/app/components/actions/case-import/confirmation-import.ts +65 -40
- package/app/components/actions/case-import/confirmation-package.ts +86 -0
- package/app/components/actions/case-import/image-operations.ts +20 -49
- package/app/components/actions/case-import/index.ts +1 -0
- package/app/components/actions/case-import/orchestrator.ts +13 -3
- package/app/components/actions/case-import/storage-operations.ts +54 -89
- package/app/components/actions/case-import/validation.ts +7 -111
- package/app/components/actions/case-import/zip-processing.ts +44 -2
- package/app/components/actions/case-manage.ts +15 -27
- package/app/components/actions/confirm-export.ts +44 -13
- package/app/components/actions/generate-pdf.ts +3 -7
- package/app/components/actions/image-manage.ts +63 -129
- package/app/components/button/button.module.css +12 -8
- package/app/components/form/form-button.tsx +1 -1
- package/app/components/form/form.module.css +9 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +163 -49
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +365 -88
- package/app/components/sidebar/case-export/case-export.tsx +13 -60
- package/app/components/sidebar/case-import/case-import.tsx +18 -6
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +6 -4
- package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
- package/app/components/sidebar/cases/case-sidebar.tsx +122 -52
- package/app/components/sidebar/cases/cases.module.css +101 -18
- package/app/components/sidebar/notes/notes.module.css +33 -13
- package/app/components/sidebar/sidebar.module.css +0 -2
- package/app/components/user/delete-account.tsx +7 -7
- package/app/components/user/manage-profile.tsx +1 -1
- package/app/components/user/mfa-phone-update.tsx +15 -12
- package/app/config-example/config.json +2 -8
- package/app/hooks/useInactivityTimeout.ts +2 -5
- package/app/root.tsx +96 -65
- package/app/routes/auth/login.tsx +132 -11
- package/app/routes/auth/route.ts +4 -3
- package/app/routes/striae/striae.tsx +4 -8
- package/app/services/audit/audit-api-client.ts +40 -0
- package/app/services/audit/audit-worker-client.ts +14 -17
- package/app/styles/root.module.css +13 -101
- package/app/tailwind.css +9 -2
- package/app/utils/SHA256.ts +5 -1
- package/app/utils/auth.ts +5 -32
- package/app/utils/confirmation-signature.ts +5 -1
- package/app/utils/data-api-client.ts +43 -0
- package/app/utils/data-operations.ts +59 -75
- package/app/utils/export-verification.ts +353 -0
- package/app/utils/image-api-client.ts +130 -0
- package/app/utils/pdf-api-client.ts +43 -0
- package/app/utils/permissions.ts +10 -23
- package/app/utils/signature-utils.ts +74 -4
- package/app/utils/user-api-client.ts +90 -0
- package/functions/api/_shared/firebase-auth.ts +255 -0
- package/functions/api/audit/[[path]].ts +150 -0
- package/functions/api/data/[[path]].ts +141 -0
- package/functions/api/image/[[path]].ts +127 -0
- package/functions/api/pdf/[[path]].ts +110 -0
- package/functions/api/user/[[path]].ts +196 -0
- package/package.json +8 -4
- package/public/favicon.ico +0 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +39 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/react-router.config.ts +5 -0
- package/scripts/deploy-all.sh +22 -8
- package/scripts/deploy-config.sh +143 -148
- package/scripts/deploy-pages-secrets.sh +231 -0
- package/scripts/deploy-worker-secrets.sh +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -8
- package/workers/data-worker/wrangler.jsonc.example +1 -8
- package/workers/image-worker/wrangler.jsonc.example +1 -8
- package/workers/keys-worker/wrangler.jsonc.example +2 -9
- package/workers/pdf-worker/scripts/generate-assets.js +94 -0
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -8
- package/workers/user-worker/src/user-worker.example.ts +121 -41
- package/workers/user-worker/wrangler.jsonc.example +1 -8
- package/wrangler.toml.example +1 -1
- package/app/styles/legal-pages.module.css +0 -113
- 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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
59
|
+
const newFile: FileData = {
|
|
60
|
+
id: uploadedImageId,
|
|
61
|
+
originalFilename: file.name,
|
|
62
|
+
uploadedAt: new Date().toISOString()
|
|
63
|
+
};
|
|
88
64
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
71
|
+
const updatedData = {
|
|
72
|
+
...existingData,
|
|
73
|
+
files: [...(existingData.files || []), newFile]
|
|
74
|
+
};
|
|
95
75
|
|
|
96
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
|
201
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
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% {
|
|
62
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
border:
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
resize: vertical;
|
|
117
|
+
font-weight: var(--fontWeightMedium);
|
|
118
|
+
cursor: pointer;
|
|
102
119
|
}
|
|
103
120
|
|
|
104
|
-
.
|
|
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
|
-
.
|
|
180
|
+
.dropZoneSecondary {
|
|
112
181
|
margin: 0;
|
|
113
|
-
|
|
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
|
-
.
|
|
186
|
+
.fieldActions {
|
|
122
187
|
display: flex;
|
|
123
|
-
|
|
188
|
+
flex-wrap: wrap;
|
|
124
189
|
gap: var(--spaceS);
|
|
125
190
|
}
|
|
126
191
|
|
|
127
|
-
.
|
|
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
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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:
|
|
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
|
-
.
|
|
146
|
-
background:
|
|
147
|
-
|
|
257
|
+
.primaryButton {
|
|
258
|
+
background: var(--primary);
|
|
259
|
+
color: var(--white);
|
|
260
|
+
border: 1px solid var(--primary);
|
|
148
261
|
}
|
|
149
262
|
|
|
150
|
-
.
|
|
151
|
-
background: color-mix(in lab, var(--
|
|
152
|
-
color: var(--
|
|
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
|
-
.
|
|
158
|
-
background:
|
|
159
|
-
color:
|
|
160
|
-
border:
|
|
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
|
-
.
|
|
170
|
-
background: color-mix(in lab, var(--
|
|
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
|
}
|