fix(webapp): migrate to Tailwind CSS 4 to remove vulnerable braces dependency

This commit is contained in:
shuaiplus committed 2026-10-05 14:55:58 +08:00
1 parent 293f5ce196
commit 4c241aa64b
15 files changed
+551 -1102

No files matched your search

+3
View File
@@ -18,6 +18,9 @@ updates:
- dependency-name: "tailwindcss" - dependency-name: "tailwindcss"
update-types: update-types:
- "version-update:semver-major" - "version-update:semver-major"
- dependency-name: "@tailwindcss/*"
update-types:
- "version-update:semver-major"
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
+8
View File
@@ -98,6 +98,14 @@ Password hints are reminders, not recovery secrets. They must never contain the
master password, recovery codes, API keys, or anything that directly unlocks the master password, recovery codes, API keys, or anything that directly unlocks the
vault. vault.
### Web Vault Styles
The web vault uses Tailwind CSS 4 through `@tailwindcss/vite`. Theme aliases,
dark-mode variants, and source paths are defined in `webapp/src/tailwind.css`,
which also imports `styles.css` so component `@apply` rules share the same theme.
Keep runtime light/dark design tokens in `webapp/src/styles/tokens.css`.
Tailwind 4's browser baseline is Safari 16.4+, Chrome 111+, and Firefox 128+.
### i18n ### i18n
Locale files are complete standalone bundles. When adding or changing user-facing Locale files are complete standalone bundles. When adding or changing user-facing
+425 -1036
View File
File diff suppressed because it is too large. Load diff
+3 -5
View File
@@ -57,17 +57,15 @@
"ws": "8.21.0", "ws": "8.21.0",
"sharp": "0.35.4", "sharp": "0.35.4",
"browserslist": "4.28.7", "browserslist": "4.28.7",
"baseline-browser-mapping": "2.11.0", "baseline-browser-mapping": "2.11.0"
"postcss-selector-parser": "6.1.3"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20260630.1", "@cloudflare/workers-types": "^4.20260630.1",
"@preact/preset-vite": "^2.10.5", "@preact/preset-vite": "^2.10.5",
"@tailwindcss/vite": "^4.3.3",
"@types/node": "^26.0.1", "@types/node": "^26.0.1",
"autoprefixer": "^10.5.2",
"opencc-js": "^1.3.2", "opencc-js": "^1.3.2",
"postcss": "^8.5.23", "tailwindcss": "^4.3.3",
"tailwindcss": "^3.4.19",
"tsx": "^4.22.4", "tsx": "^4.22.4",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vite": "^8.1.3", "vite": "^8.1.3",
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
-33
View File
@@ -1,33 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./webapp/index.html', './webapp/src/**/*.{ts,tsx}'],
darkMode: ['class', '[data-theme="dark"]'],
theme: {
extend: {
colors: {
canvas: 'var(--bg-accent)',
panel: 'var(--panel)',
'panel-soft': 'var(--panel-soft)',
'panel-muted': 'var(--panel-muted)',
line: 'var(--line)',
'line-soft': 'var(--line-soft)',
ink: 'var(--text)',
muted: 'var(--muted)',
'muted-strong': 'var(--muted-strong)',
brand: 'var(--primary)',
'brand-hover': 'var(--primary-hover)',
'brand-strong': 'var(--primary-strong)',
danger: 'var(--danger)',
},
boxShadow: {
soft: 'var(--shadow-sm)',
panel: 'var(--shadow-md)',
elevated: 'var(--shadow-lg)',
},
fontFamily: {
sans: ['Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', 'sans-serif'],
},
},
},
plugins: [],
};
-1
View File
@@ -4,7 +4,6 @@ import App from './App';
import { initI18n } from './lib/i18n'; import { initI18n } from './lib/i18n';
import { registerNodeWardenServiceWorker } from './lib/pwa'; import { registerNodeWardenServiceWorker } from './lib/pwa';
import './tailwind.css'; import './tailwind.css';
import './styles.css';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
+2 -2
View File
@@ -353,7 +353,7 @@
} }
.not-found-logo { .not-found-logo {
@apply h-14 w-14 flex-shrink-0 object-contain; @apply h-14 w-14 shrink-0 object-contain;
filter: drop-shadow(0 8px 18px rgba(43, 102, 217, 0.22)); filter: drop-shadow(0 8px 18px rgba(43, 102, 217, 0.22));
} }
@@ -465,7 +465,7 @@
} }
.standalone-brand-logo { .standalone-brand-logo {
@apply h-14 w-[70px] flex-shrink-0 object-contain; @apply h-14 w-[70px] shrink-0 object-contain;
filter: drop-shadow(0 8px 18px rgba(43, 102, 217, 0.22)); filter: drop-shadow(0 8px 18px rgba(43, 102, 217, 0.22));
} }
+1 -1
View File
@@ -19,7 +19,7 @@
} }
.input { .input {
@apply h-12 w-full rounded-xl border px-3.5 py-2.5 outline-none; @apply h-12 w-full rounded-xl border px-3.5 py-2.5 outline-hidden;
background: var(--panel); background: var(--panel);
border-color: rgba(74, 103, 150, 0.34); border-color: rgba(74, 103, 150, 0.34);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82);
+15 -4
View File
@@ -802,15 +802,25 @@
} }
.yubikey-manage-dialog-body { .yubikey-manage-dialog-body {
@apply mt-3 space-y-4; @apply mt-3;
}
.yubikey-manage-dialog-body > :not([hidden]) ~ :not([hidden]) {
margin-top: 1rem;
margin-bottom: 0;
} }
.settings-plain-steps { .settings-plain-steps {
@apply m-0 space-y-1 border-b pb-3 pl-5; @apply m-0 border-b pb-3 pl-5;
border-color: var(--line); border-color: var(--line);
color: var(--text); color: var(--text);
} }
.settings-plain-steps > :not([hidden]) ~ :not([hidden]) {
margin-top: 0.25rem;
margin-bottom: 0;
}
.yubikey-input-row { .yubikey-input-row {
@apply grid items-center gap-2; @apply grid items-center gap-2;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
@@ -825,8 +835,9 @@
@apply h-9 w-9 p-0; @apply h-9 w-9 p-0;
} }
.settings-checkbox-block { .settings-checkbox-block > :not([hidden]) ~ :not([hidden]) {
@apply space-y-2; margin-top: 0.5rem;
margin-bottom: 0;
} }
.checkbox-inline { .checkbox-inline {
+3 -3
View File
@@ -132,7 +132,7 @@
} }
.topbar-actions > .network-status-badge { .topbar-actions > .network-status-badge {
@apply inline-flex h-9 w-9 min-w-9 justify-center gap-0 rounded-xl p-0 text-[0]; @apply inline-flex h-9 w-9 min-w-9 justify-center gap-0 rounded-xl p-0 text-[0px];
background: color-mix(in srgb, var(--panel) 88%, transparent); background: color-mix(in srgb, var(--panel) 88%, transparent);
border-color: var(--line); border-color: var(--line);
box-shadow: none; box-shadow: none;
@@ -144,7 +144,7 @@
.mobile-sidebar-toggle, .mobile-sidebar-toggle,
.mobile-lock-btn { .mobile-lock-btn {
@apply inline-flex h-9 w-9 min-w-9 justify-center gap-0 rounded-xl p-0 text-[0]; @apply inline-flex h-9 w-9 min-w-9 justify-center gap-0 rounded-xl p-0 text-[0px];
background: color-mix(in srgb, var(--panel) 88%, transparent); background: color-mix(in srgb, var(--panel) 88%, transparent);
border-color: var(--line); border-color: var(--line);
box-shadow: none; box-shadow: none;
@@ -451,7 +451,7 @@
} }
.mobile-fab-trigger { .mobile-fab-trigger {
@apply h-9 w-9 gap-0 rounded-full p-0 text-[0]; @apply h-9 w-9 gap-0 rounded-full p-0 text-[0px];
box-shadow: 0 14px 30px rgba(37, 99, 235, 0.28); box-shadow: 0 14px 30px rgba(37, 99, 235, 0.28);
transition: transform 180ms var(--ease-spring), box-shadow var(--dur-fast) var(--ease-out-soft); transition: transform 180ms var(--ease-spring), box-shadow var(--dur-fast) var(--ease-out-soft);
} }
+15 -4
View File
@@ -31,7 +31,12 @@
} }
.skeleton-content { .skeleton-content {
@apply min-w-0 flex-1 space-y-2; @apply min-w-0 flex-1;
}
.skeleton-content > :not([hidden]) ~ :not([hidden]) {
margin-top: 0.5rem;
margin-bottom: 0;
} }
.skeleton-line { .skeleton-line {
@@ -63,13 +68,19 @@
} }
.skeleton-page { .skeleton-page {
@apply h-full space-y-4 p-4; @apply h-full p-4;
}
.skeleton-page > :not([hidden]) ~ :not([hidden]) {
margin-top: 1rem;
margin-bottom: 0;
} }
.skeleton-header { .skeleton-header {
@apply mb-6; @apply mb-6;
} }
.skeleton-body { .skeleton-body > :not([hidden]) ~ :not([hidden]) {
@apply space-y-3; margin-top: 0.75rem;
margin-bottom: 0;
} }
+3 -3
View File
@@ -55,7 +55,7 @@
} }
.search-input { .search-input {
@apply h-12 w-full rounded-xl border px-3.5 py-2.5 text-base text-ink outline-none transition; @apply h-12 w-full rounded-xl border px-3.5 py-2.5 text-base text-ink outline-hidden transition;
background: var(--panel); background: var(--panel);
border-color: rgba(74, 103, 150, 0.34); border-color: rgba(74, 103, 150, 0.34);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82);
@@ -416,7 +416,7 @@ select.input.duplicate-mode-toolbar-select {
} }
.desktop-create-trigger { .desktop-create-trigger {
@apply h-[34px] w-[34px] min-w-[34px] gap-0 rounded-[10px] p-0 text-[0]; @apply h-[34px] w-[34px] min-w-[34px] gap-0 rounded-[10px] p-0 text-[0px];
} }
.desktop-create-trigger .btn-icon { .desktop-create-trigger .btn-icon {
@@ -586,7 +586,7 @@ select.input.duplicate-mode-toolbar-select {
} }
.card-brand-icon { .card-brand-icon {
@apply inline-grid h-6 w-9 shrink-0 place-items-center rounded border text-[7px] font-black uppercase leading-none; @apply inline-grid h-6 w-9 shrink-0 place-items-center rounded-sm border text-[7px] font-black uppercase leading-none;
letter-spacing: 0; letter-spacing: 0;
color: #1e3a8a; color: #1e3a8a;
background: linear-gradient(180deg, #ffffff 0%, #f4f7fb 100%); background: linear-gradient(180deg, #ffffff 0%, #f4f7fb 100%);
+71 -3
View File
@@ -1,3 +1,71 @@
@tailwind base; @import 'tailwindcss' source(none);
@tailwind components; @import './styles.css';
@tailwind utilities;
@source '../index.html';
@source './**/*.{ts,tsx}';
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
/* Inline aliases keep utilities tied to the active light/dark design tokens. */
@theme inline {
--color-canvas: var(--bg-accent);
--color-panel: var(--panel);
--color-panel-soft: var(--panel-soft);
--color-panel-muted: var(--panel-muted);
--color-line: var(--line);
--color-line-soft: var(--line-soft);
--color-ink: var(--text);
--color-muted: var(--muted);
--color-muted-strong: var(--muted-strong);
--color-brand: var(--primary);
--color-brand-hover: var(--primary-hover);
--color-brand-strong: var(--primary-strong);
--color-danger: var(--danger);
--shadow-soft: var(--shadow-sm);
--shadow-panel: var(--shadow-md);
--shadow-elevated: var(--shadow-lg);
--font-sans: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
/* Preserve utility radii independently of tokens.css's --radius-* names. */
--radius-xs: 0.125rem;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-3xl: 1.5rem;
/* Keep line heights fixed when component styles override the font size. */
--text-xs--line-height: 1rem;
--text-sm--line-height: 1.25rem;
--text-base--line-height: 1.5rem;
--text-lg--line-height: 1.75rem;
--text-xl--line-height: 1.75rem;
--text-2xl--line-height: 2rem;
--text-3xl--line-height: 2.25rem;
--text-4xl--line-height: 2.5rem;
}
/* Retain the v3 defaults where components do not set their own values. */
@layer base {
*,
::before,
::after,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
input::placeholder,
textarea::placeholder {
color: var(--color-gray-400);
}
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { fileURLToPath } from 'node:url';
import path from 'node:path'; import path from 'node:path';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import preact from '@preact/preset-vite'; import preact from '@preact/preset-vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig, type Plugin } from 'vite'; import { defineConfig, type Plugin } from 'vite';
const rootDir = fileURLToPath(new URL('.', import.meta.url)); const rootDir = fileURLToPath(new URL('.', import.meta.url));
@@ -263,7 +264,7 @@ export default defineConfig(({ mode }) => {
return { return {
root: rootDir, root: rootDir,
plugins: [preact(), searchIndexPolicyPlugin(isDemo), pwaServiceWorkerPlugin(isDemo)], plugins: [preact(), tailwindcss(), searchIndexPolicyPlugin(isDemo), pwaServiceWorkerPlugin(isDemo)],
define: { define: {
__NODEWARDEN_DEMO__: JSON.stringify(isDemo), __NODEWARDEN_DEMO__: JSON.stringify(isDemo),
}, },