@striae-org/striae 5.3.1 → 5.4.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 +3 -0
- package/app/components/actions/generate-pdf.ts +22 -0
- package/app/components/auth/auth.module.css +531 -0
- package/app/components/auth/mfa-enrollment.tsx +132 -79
- package/app/components/auth/mfa-totp-enrollment.tsx +231 -0
- package/app/components/auth/mfa-verification.tsx +155 -33
- package/app/components/{sidebar/cases/cases-modal.tsx → navbar/case-modals/all-cases-modal.tsx} +4 -4
- package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -10
- package/app/components/navbar/case-modals/case-modal-shared.module.css +88 -0
- package/app/components/navbar/case-modals/delete-case-modal.tsx +9 -10
- package/app/components/navbar/case-modals/export-case-modal.tsx +9 -10
- package/app/components/navbar/case-modals/export-confirmations-modal.tsx +9 -10
- package/app/components/navbar/case-modals/open-case-modal.tsx +4 -4
- package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -10
- package/app/components/navbar/navbar.tsx +1 -1
- package/app/components/sidebar/files/delete-files-modal.tsx +3 -3
- package/app/components/sidebar/files/files-modal.module.css +29 -0
- package/app/components/sidebar/notes/{class-details-fields.tsx → class-details/class-details-fields.tsx} +1 -1
- package/app/components/sidebar/notes/{class-details-modal.tsx → class-details/class-details-modal.tsx} +1 -1
- package/app/components/sidebar/notes/{class-details-sections.tsx → class-details/class-details-sections.tsx} +1 -1
- package/app/components/sidebar/notes/notes-editor-form.tsx +2 -2
- package/app/components/sidebar/notes/notes-editor-modal.tsx +6 -6
- package/app/components/sidebar/notes/notes.module.css +52 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +8 -8
- package/app/components/toolbar/toolbar.module.css +181 -2
- package/app/components/user/delete-account.tsx +7 -7
- package/app/components/user/inactivity-warning.tsx +6 -6
- package/app/components/user/manage-profile.tsx +18 -1
- package/app/components/user/mfa-enrolled-factors.tsx +117 -0
- package/app/components/user/mfa-phone-update.tsx +8 -4
- package/app/components/user/mfa-totp-section.tsx +446 -0
- package/app/components/user/user.module.css +665 -0
- package/app/routes/striae/striae.tsx +1 -1
- package/app/services/audit/audit.service.ts +1 -1
- package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
- package/app/services/firebase/errors.ts +2 -0
- package/app/utils/auth/mfa.ts +35 -1
- package/functions/api/image/[[path]].ts +19 -3
- package/package.json +16 -21
- package/scripts/deploy-all.sh +166 -0
- package/scripts/deploy-config/modules/env-utils.sh +322 -0
- package/scripts/deploy-config/modules/keys.sh +404 -0
- package/scripts/deploy-config/modules/prompt.sh +375 -0
- package/scripts/deploy-config/modules/scaffolding.sh +310 -0
- package/scripts/deploy-config/modules/validation.sh +354 -0
- package/scripts/deploy-config.sh +236 -0
- package/scripts/deploy-pages-secrets.sh +231 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-primershear-emails.sh +167 -0
- package/scripts/deploy-worker-secrets.sh +385 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/enable-totp-mfa.mjs +57 -0
- package/scripts/install-workers.sh +87 -0
- package/scripts/run-eslint.cjs +43 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/image-worker.example.ts +36 -2
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/auth/mfa-enrollment.module.css +0 -276
- package/app/components/auth/mfa-verification.module.css +0 -259
- package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -34
- package/app/components/navbar/case-modals/delete-case-modal.module.css +0 -9
- package/app/components/navbar/case-modals/export-case-modal.module.css +0 -27
- package/app/components/navbar/case-modals/export-confirmations-modal.module.css +0 -24
- package/app/components/navbar/case-modals/open-case-modal.module.css +0 -82
- package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -9
- package/app/components/sidebar/files/delete-files-modal.module.css +0 -26
- package/app/components/sidebar/notes/notes-editor-modal.module.css +0 -49
- package/app/components/toolbar/toolbar-color-selector.module.css +0 -171
- package/app/components/user/delete-account.module.css +0 -277
- package/app/components/user/inactivity-warning.module.css +0 -148
- package/app/components/user/manage-profile.module.css +0 -192
- package/app/routes/auth/login.module.css +0 -523
- package/app/routes/auth/login.tsx +0 -705
- /package/app/components/{sidebar → navbar}/case-import/case-import.module.css +0 -0
- /package/app/components/{sidebar → navbar}/case-import/case-import.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/CasePreviewSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationDialog.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationPreviewSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ExistingCaseSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/FileSelector.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ProgressSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/hooks/useFilePreview.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/hooks/useImportExecution.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/hooks/useImportState.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/index.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/utils/file-validation.ts +0 -0
- /package/app/components/{sidebar/cases/cases-modal.module.css → navbar/case-modals/all-cases-modal.module.css} +0 -0
- /package/app/components/sidebar/notes/{class-details-shared.ts → class-details/class-details-shared.ts} +0 -0
- /package/app/components/sidebar/notes/{use-class-details-state.ts → class-details/use-class-details-state.ts} +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
5
|
+
|
|
6
|
+
function getCurrentDate() {
|
|
7
|
+
const now = new Date();
|
|
8
|
+
const year = now.getFullYear();
|
|
9
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
10
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
11
|
+
return `${year}-${month}-${day}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function replaceTomlCompatibilityDate(content, date) {
|
|
15
|
+
return content.replace(
|
|
16
|
+
/(compatibility_date\s*=\s*")\d{4}-\d{2}-\d{2}(")/,
|
|
17
|
+
`$1${date}$2`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function replaceJsoncCompatibilityDate(content, date) {
|
|
22
|
+
return content.replace(
|
|
23
|
+
/("compatibility_date"\s*:\s*")\d{4}-\d{2}-\d{2}(",?)/,
|
|
24
|
+
`$1${date}$2`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function updateFile(filePath, date, replacer) {
|
|
29
|
+
if (!fs.existsSync(filePath)) {
|
|
30
|
+
return { filePath, status: 'missing' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const original = fs.readFileSync(filePath, 'utf8');
|
|
34
|
+
const updated = replacer(original, date);
|
|
35
|
+
|
|
36
|
+
if (original === updated) {
|
|
37
|
+
return { filePath, status: 'unchanged' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
41
|
+
return { filePath, status: 'updated' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function updateCompatibilityDates(date = getCurrentDate()) {
|
|
45
|
+
if (!DATE_PATTERN.test(date)) {
|
|
46
|
+
throw new Error(`Invalid date format: ${date}. Use YYYY-MM-DD.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
50
|
+
const workersDir = path.join(rootDir, 'workers');
|
|
51
|
+
|
|
52
|
+
const results = [];
|
|
53
|
+
|
|
54
|
+
results.push(
|
|
55
|
+
updateFile(
|
|
56
|
+
path.join(rootDir, 'wrangler.toml'),
|
|
57
|
+
date,
|
|
58
|
+
replaceTomlCompatibilityDate
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
results.push(
|
|
63
|
+
updateFile(
|
|
64
|
+
path.join(rootDir, 'wrangler.toml.example'),
|
|
65
|
+
date,
|
|
66
|
+
replaceTomlCompatibilityDate
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (fs.existsSync(workersDir)) {
|
|
71
|
+
const workerDirs = fs
|
|
72
|
+
.readdirSync(workersDir, { withFileTypes: true })
|
|
73
|
+
.filter((entry) => entry.isDirectory())
|
|
74
|
+
.map((entry) => entry.name);
|
|
75
|
+
|
|
76
|
+
for (const workerDir of workerDirs) {
|
|
77
|
+
const workerPath = path.join(workersDir, workerDir);
|
|
78
|
+
results.push(
|
|
79
|
+
updateFile(
|
|
80
|
+
path.join(workerPath, 'wrangler.jsonc.example'),
|
|
81
|
+
date,
|
|
82
|
+
replaceJsoncCompatibilityDate
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
results.push(
|
|
86
|
+
updateFile(
|
|
87
|
+
path.join(workerPath, 'wrangler.jsonc'),
|
|
88
|
+
date,
|
|
89
|
+
replaceJsoncCompatibilityDate
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const updatedCount = results.filter((result) => result.status === 'updated').length;
|
|
96
|
+
const unchangedCount = results.filter((result) => result.status === 'unchanged').length;
|
|
97
|
+
const missingCount = results.filter((result) => result.status === 'missing').length;
|
|
98
|
+
|
|
99
|
+
console.log(`Updated compatibility dates to ${date}`);
|
|
100
|
+
console.log(`- Updated: ${updatedCount}`);
|
|
101
|
+
console.log(`- Unchanged: ${unchangedCount}`);
|
|
102
|
+
console.log(`- Missing: ${missingCount}`);
|
|
103
|
+
|
|
104
|
+
for (const result of results) {
|
|
105
|
+
if (result.status !== 'updated') {
|
|
106
|
+
console.log(` ${result.status.toUpperCase()}: ${path.relative(rootDir, result.filePath)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (require.main === module) {
|
|
114
|
+
const dateArg = process.argv[2] || getCurrentDate();
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
updateCompatibilityDates(dateArg);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(error.message);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { updateCompatibilityDates };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const packageJson = require('../package.json');
|
|
4
|
+
|
|
5
|
+
const markdownFiles = [
|
|
6
|
+
'.github/SECURITY.md',
|
|
7
|
+
// Add other markdown files that need version updates
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function updateMarkdownVersions() {
|
|
11
|
+
console.log(`📝 Updating markdown files with version ${packageJson.version}...`);
|
|
12
|
+
|
|
13
|
+
markdownFiles.forEach(filePath => {
|
|
14
|
+
const fullPath = path.join(__dirname, '..', filePath);
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(fullPath)) {
|
|
17
|
+
console.log(`⚠️ Skipping ${filePath} (file not found)`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
let content = fs.readFileSync(fullPath, 'utf8');
|
|
23
|
+
|
|
24
|
+
// Replace version placeholders
|
|
25
|
+
content = content.replace(/{{VERSION}}/g, packageJson.version);
|
|
26
|
+
content = content.replace(/v\d+\.\d+\.\d+(-\w+)?/g, `v${packageJson.version}`);
|
|
27
|
+
|
|
28
|
+
fs.writeFileSync(fullPath, content);
|
|
29
|
+
console.log(`✅ Updated ${filePath}`);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(`❌ Error updating ${filePath}:`, error.message);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
console.log('🎉 Markdown version update complete!');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Run if called directly
|
|
39
|
+
if (require.main === module) {
|
|
40
|
+
updateMarkdownVersions();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { updateMarkdownVersions };
|
|
@@ -14,6 +14,7 @@ interface Env {
|
|
|
14
14
|
DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID?: string;
|
|
15
15
|
IMAGE_SIGNED_URL_SECRET?: string;
|
|
16
16
|
IMAGE_SIGNED_URL_TTL_SECONDS?: string;
|
|
17
|
+
IMAGE_SIGNED_URL_BASE_URL?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
interface KeyRegistryPayload {
|
|
@@ -383,6 +384,25 @@ function requireSignedUrlConfig(env: Env): void {
|
|
|
383
384
|
}
|
|
384
385
|
}
|
|
385
386
|
|
|
387
|
+
function parseSignedUrlBaseUrl(raw: string): string {
|
|
388
|
+
let parsed: URL;
|
|
389
|
+
try {
|
|
390
|
+
parsed = new URL(raw.trim());
|
|
391
|
+
} catch {
|
|
392
|
+
throw new Error(`IMAGE_SIGNED_URL_BASE_URL is not a valid absolute URL: "${raw}"`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
396
|
+
throw new Error(`IMAGE_SIGNED_URL_BASE_URL must use http or https, got: "${parsed.protocol}"`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (parsed.search || parsed.hash) {
|
|
400
|
+
throw new Error(`IMAGE_SIGNED_URL_BASE_URL must not include a query string or fragment: "${raw}"`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, '');
|
|
404
|
+
}
|
|
405
|
+
|
|
386
406
|
async function getSignedUrlHmacKey(env: Env): Promise<CryptoKey> {
|
|
387
407
|
const resolvedSecret = (env.IMAGE_SIGNED_URL_SECRET || env.IMAGES_API_TOKEN || '').trim();
|
|
388
408
|
const keyBytes = new TextEncoder().encode(resolvedSecret);
|
|
@@ -592,8 +612,22 @@ async function handleSignedUrlMinting(request: Request, env: Env, fileId: string
|
|
|
592
612
|
};
|
|
593
613
|
|
|
594
614
|
const signedToken = await signSignedAccessPayload(payload, env);
|
|
595
|
-
|
|
596
|
-
|
|
615
|
+
|
|
616
|
+
let baseUrl: string;
|
|
617
|
+
if (env.IMAGE_SIGNED_URL_BASE_URL) {
|
|
618
|
+
try {
|
|
619
|
+
baseUrl = parseSignedUrlBaseUrl(env.IMAGE_SIGNED_URL_BASE_URL);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
console.error('Invalid IMAGE_SIGNED_URL_BASE_URL configuration', {
|
|
622
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
623
|
+
});
|
|
624
|
+
return createJsonResponse({ error: 'Signed URL base URL is misconfigured' }, 500);
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
baseUrl = new URL(request.url).origin;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const signedUrl = `${baseUrl}/${encodeURIComponent(fileId)}?st=${encodeURIComponent(signedToken)}`;
|
|
597
631
|
|
|
598
632
|
return createJsonResponse({
|
|
599
633
|
success: true,
|
package/wrangler.toml.example
CHANGED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
.overlay {
|
|
2
|
-
position: fixed;
|
|
3
|
-
top: 0;
|
|
4
|
-
left: 0;
|
|
5
|
-
right: 0;
|
|
6
|
-
bottom: 0;
|
|
7
|
-
background-color: rgba(0, 0, 0, 0.75);
|
|
8
|
-
display: flex;
|
|
9
|
-
align-items: center;
|
|
10
|
-
justify-content: center;
|
|
11
|
-
z-index: 1000;
|
|
12
|
-
padding: 1rem;
|
|
13
|
-
cursor: default;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.modal {
|
|
17
|
-
background: white;
|
|
18
|
-
border-radius: 12px;
|
|
19
|
-
padding: 2rem;
|
|
20
|
-
max-width: 500px;
|
|
21
|
-
width: 100%;
|
|
22
|
-
max-height: 90vh;
|
|
23
|
-
overflow-y: auto;
|
|
24
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
25
|
-
cursor: default;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.header {
|
|
29
|
-
text-align: center;
|
|
30
|
-
margin-bottom: 2rem;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.header h2 {
|
|
34
|
-
color: #333;
|
|
35
|
-
margin: 0 0 1rem 0;
|
|
36
|
-
font-size: 1.5rem;
|
|
37
|
-
font-weight: 600;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.header p {
|
|
41
|
-
color: #666;
|
|
42
|
-
margin: 0;
|
|
43
|
-
line-height: 1.5;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.content {
|
|
47
|
-
margin-bottom: 2rem;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.phoneStep,
|
|
51
|
-
.codeStep {
|
|
52
|
-
text-align: center;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.phoneStep h3,
|
|
56
|
-
.codeStep h3 {
|
|
57
|
-
color: #333;
|
|
58
|
-
margin: 0 0 1rem 0;
|
|
59
|
-
font-size: 1.2rem;
|
|
60
|
-
font-weight: 500;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.input {
|
|
64
|
-
width: 100%;
|
|
65
|
-
padding: 1rem;
|
|
66
|
-
border: 2px solid #e1e5e9;
|
|
67
|
-
border-radius: 8px;
|
|
68
|
-
font-size: 1rem;
|
|
69
|
-
margin-bottom: 1rem;
|
|
70
|
-
transition: border-color 0.2s ease;
|
|
71
|
-
box-sizing: border-box;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.input:focus {
|
|
75
|
-
outline: none;
|
|
76
|
-
border-color: #4285f4;
|
|
77
|
-
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.input:disabled {
|
|
81
|
-
background-color: #f5f5f5;
|
|
82
|
-
cursor: not-allowed;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.note {
|
|
86
|
-
color: #666;
|
|
87
|
-
font-size: 0.9rem;
|
|
88
|
-
margin: 0 0 1.5rem 0;
|
|
89
|
-
line-height: 1.4;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.buttonGroup {
|
|
93
|
-
display: flex;
|
|
94
|
-
flex-direction: column;
|
|
95
|
-
gap: 0.75rem;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.primaryButton {
|
|
99
|
-
background-color: #4285f4;
|
|
100
|
-
color: white;
|
|
101
|
-
border: none;
|
|
102
|
-
padding: 1rem 2rem;
|
|
103
|
-
border-radius: 8px;
|
|
104
|
-
font-size: 1rem;
|
|
105
|
-
font-weight: 500;
|
|
106
|
-
cursor: pointer;
|
|
107
|
-
transition: background-color 0.2s ease;
|
|
108
|
-
width: 100%;
|
|
109
|
-
box-sizing: border-box;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.primaryButton:hover:not(:disabled) {
|
|
113
|
-
background-color: #3367d6;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.primaryButton:disabled {
|
|
117
|
-
background-color: #ccc;
|
|
118
|
-
cursor: not-allowed;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.secondaryButton {
|
|
122
|
-
background-color: transparent;
|
|
123
|
-
color: #4285f4;
|
|
124
|
-
border: 2px solid #4285f4;
|
|
125
|
-
padding: 0.75rem 1.5rem;
|
|
126
|
-
border-radius: 8px;
|
|
127
|
-
font-size: 0.9rem;
|
|
128
|
-
font-weight: 500;
|
|
129
|
-
cursor: pointer;
|
|
130
|
-
transition: all 0.2s ease;
|
|
131
|
-
width: 100%;
|
|
132
|
-
box-sizing: border-box;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
.secondaryButton:hover:not(:disabled) {
|
|
136
|
-
background-color: #4285f4;
|
|
137
|
-
color: white;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.secondaryButton:disabled {
|
|
141
|
-
border-color: #ccc;
|
|
142
|
-
color: #ccc;
|
|
143
|
-
cursor: not-allowed;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
.resendTimer {
|
|
147
|
-
color: #999;
|
|
148
|
-
font-size: 0.9rem;
|
|
149
|
-
margin: 0;
|
|
150
|
-
text-align: center;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.errorMessage {
|
|
154
|
-
background: linear-gradient(
|
|
155
|
-
135deg,
|
|
156
|
-
color-mix(in lab, var(--error) 12%, transparent),
|
|
157
|
-
color-mix(in lab, var(--error) 8%, transparent)
|
|
158
|
-
);
|
|
159
|
-
border: 1px solid color-mix(in lab, var(--error) 30%, transparent);
|
|
160
|
-
border-left: 4px solid var(--error);
|
|
161
|
-
color: color-mix(in lab, var(--error) 90%, var(--black));
|
|
162
|
-
padding: var(--spaceL);
|
|
163
|
-
border-radius: var(--spaceS);
|
|
164
|
-
font-size: 0.9rem;
|
|
165
|
-
margin-bottom: 1rem;
|
|
166
|
-
text-align: left;
|
|
167
|
-
line-height: 1.4;
|
|
168
|
-
position: relative;
|
|
169
|
-
overflow: hidden;
|
|
170
|
-
box-shadow: 0 4px 16px color-mix(in lab, var(--text) 10%, transparent);
|
|
171
|
-
animation: slideInError var(--durationM) var(--bezierFastoutSlowin);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
.errorMessage::before {
|
|
175
|
-
content: "";
|
|
176
|
-
position: absolute;
|
|
177
|
-
top: 0;
|
|
178
|
-
left: 0;
|
|
179
|
-
right: 0;
|
|
180
|
-
height: 2px;
|
|
181
|
-
background: linear-gradient(
|
|
182
|
-
90deg,
|
|
183
|
-
var(--error),
|
|
184
|
-
color-mix(in lab, var(--error) 60%, transparent)
|
|
185
|
-
);
|
|
186
|
-
animation: shimmer 2s ease-in-out infinite;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.errorMessage:empty {
|
|
190
|
-
display: none;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.footer {
|
|
194
|
-
border-top: 1px solid #e1e5e9;
|
|
195
|
-
padding-top: 1.5rem;
|
|
196
|
-
text-align: center;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.skipButton {
|
|
200
|
-
background-color: transparent;
|
|
201
|
-
color: #666;
|
|
202
|
-
border: none;
|
|
203
|
-
padding: 0.75rem 1.5rem;
|
|
204
|
-
border-radius: 8px;
|
|
205
|
-
font-size: 0.9rem;
|
|
206
|
-
cursor: pointer;
|
|
207
|
-
transition: color 0.2s ease;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
.skipButton:hover:not(:disabled) {
|
|
211
|
-
color: #333;
|
|
212
|
-
text-decoration: underline;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
.skipButton:disabled {
|
|
216
|
-
color: #ccc;
|
|
217
|
-
cursor: not-allowed;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
.signOutContainer {
|
|
221
|
-
margin-top: var(--spaceL);
|
|
222
|
-
padding-top: var(--spaceL);
|
|
223
|
-
border-top: 1px solid var(--borderLight);
|
|
224
|
-
text-align: center;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.signOutText {
|
|
228
|
-
color: var(--textLight);
|
|
229
|
-
font-size: var(--fontSizeSmall);
|
|
230
|
-
margin-bottom: var(--spaceM);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/* Animations */
|
|
234
|
-
@keyframes slideInError {
|
|
235
|
-
from {
|
|
236
|
-
opacity: 0;
|
|
237
|
-
transform: translateY(calc(-1 * var(--spaceM))) scaleY(0.8);
|
|
238
|
-
max-height: 0;
|
|
239
|
-
padding-top: 0;
|
|
240
|
-
padding-bottom: 0;
|
|
241
|
-
margin-top: 0;
|
|
242
|
-
margin-bottom: 0;
|
|
243
|
-
}
|
|
244
|
-
to {
|
|
245
|
-
opacity: 1;
|
|
246
|
-
transform: translateY(0) scaleY(1);
|
|
247
|
-
max-height: 200px;
|
|
248
|
-
padding-top: var(--spaceL);
|
|
249
|
-
padding-bottom: var(--spaceL);
|
|
250
|
-
margin-top: 0;
|
|
251
|
-
margin-bottom: 1rem;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
@keyframes shimmer {
|
|
256
|
-
0%,
|
|
257
|
-
100% {
|
|
258
|
-
opacity: 0.6;
|
|
259
|
-
transform: translateX(-100%);
|
|
260
|
-
}
|
|
261
|
-
50% {
|
|
262
|
-
opacity: 1;
|
|
263
|
-
transform: translateX(100%);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/* Reduce motion for accessibility */
|
|
268
|
-
@media (prefers-reduced-motion: reduce) {
|
|
269
|
-
.errorMessage {
|
|
270
|
-
animation: none;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
.errorMessage::before {
|
|
274
|
-
animation: none;
|
|
275
|
-
}
|
|
276
|
-
}
|