feat: migrate to Tailwind CSS v4

Replace @nuxtjs/tailwindcss with @tailwindcss/vite, move theme
tokens into @theme inline in main.css, drop tailwind.config.ts.
This commit is contained in:
RhenCloud 2026-08-27 00:26:17 +08:00
parent 32a8c2ce4e
commit c75567cf15
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
8 changed files with 264 additions and 530 deletions

View file

@ -9,8 +9,8 @@ Core pipeline: GitHub Webhook → Worker (verify + filter + format) → Discord
## Key Decisions
- Runtime: Cloudflare Workers via the Nitro `cloudflare_module` preset (`_worker.js`), H3 event handlers in `server/routes/`
- UI: Vue 3 + Tailwind CSS v3 (`@nuxtjs/tailwindcss`); admin console is a client-side SPA (`routeRules: "/admin/**": { ssr: false }`), home/legal pages render server-side
- Styling: all theme colors are RGB-triplet CSS variables in `app/assets/css/main.css` mapped into `tailwind.config.ts` (so `bg-accent/10` opacity modifiers work); the design tokens switch with `prefers-color-scheme` (unless `<html data-theme="light">`); repeated control patterns are `@apply` component classes in the CSS `@layer components`
- UI: Vue 3 + Tailwind CSS v4 (`@tailwindcss/vite`); admin console is a client-side SPA (`routeRules: "/admin/**": { ssr: false }`), home/legal pages render server-side
- Styling: all theme colors are RGB-triplet CSS variables in `app/assets/css/main.css` declared as `@theme inline` tokens (so `bg-accent/10` opacity modifiers work); the design tokens switch with `prefers-color-scheme` (unless `<html data-theme="light">`); repeated control patterns are `@apply` component classes in the CSS `@layer components`
- Discord interactions: HTTPS Interactions Endpoint (`POST /discord/interactions`, Ed25519-signed) — no Discord Gateway / Durable Object; bot stays offline, messages always sent via REST
- Storage: D1 is the source of truth for config (routes/groups via `d1_routes`/`d1_groups`, see `server/lib/storage/config-store.ts`), send/audit logs, dedup (`dedup_keys`), delivery state (`delivery_state`) and message tracking (`message_tracking` via `server/lib/storage/d1.ts` `canUseD1` gate). KV keeps only cache + short-lived/ephemeral state (tokens, OAuth state, admin sessions, `msg:*`-adjacent locks, per-group secrets `tenant:*`, invites, i18n overrides) with explicit TTLs. R2 parks oversized queue payloads (`webhooks/YYYY/MM/DD/*.json`, `server/lib/storage/payload.ts`) instead of KV. A `storage-prune` scheduled task cleans up expired dedup/delivery/message-tracking rows
- Signature verification: Web Crypto API (HMAC-SHA256 for GitHub/Gitea, Ed25519 for Discord, timing-safe secret-token compare for Telegram)
@ -34,7 +34,7 @@ Core pipeline: GitHub Webhook → Worker (verify + filter + format) → Discord
```text
app/ # Vue 3 UI (Nuxt app dir)
├── app.vue # root component (NuxtPage)
├── assets/css/main.css # Tailwind entry: theme tokens (RGB-triplet vars) + @layer components (@apply) + Vue transition glue
├── assets/css/main.css # Tailwind v4 entry: @import "tailwindcss" + @theme inline tokens (RGB-triplet vars) + @layer components (@apply) + Vue transition glue
├── pages/ # index (landing), terms, privacy, admin/[...slug] (console SPA)
├── components/ # ConsolePage (sidebar shell + topbar), AdminHome (overview dashboard),
│ # RouteCard/Editor (RouteEditor has FilterNodeEditor AST builder + TagInput chips),

View file

@ -1,14 +1,61 @@
@import "tailwindcss";
/* ---------------------------------------------------------------------------
* Theme tokens RGB triplets so Tailwind opacity modifiers (bg-accent/10)
* work against them; values switch automatically with prefers-color-scheme
* (unless an explicit data-theme="light" is set on <html>).
* ------------------------------------------------------------------------- */
:root {
color-scheme: light;
@theme inline {
--font-ui: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--radius: 12px;
--radius-sm: 8px;
--radius: 12px;
--shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.06);
--shadow-card-hover: 0 8px 24px -12px rgba(15, 23, 42, 0.25);
--shadow-accent-lg: 0 12px 28px -10px rgb(var(--accent));
--shadow-drawer: -16px 0 48px rgba(15, 23, 42, 0.12);
--shadow-modal: 0 12px 40px rgba(15, 23, 42, 0.25);
--animate-rise: rise 0.35s cubic-bezier(0.2, 0.7, 0.2, 1) both;
--color-bg: rgb(var(--bg));
--color-surface: rgb(var(--surface));
--color-surface-2: rgb(var(--surface-2));
--color-surface-3: rgb(var(--surface-3));
--color-border: rgb(var(--border));
--color-border-strong: rgb(var(--border-strong));
--color-text: rgb(var(--text));
--color-muted: rgb(var(--muted));
--color-faint: rgb(var(--faint));
--color-accent: rgb(var(--accent));
--color-accent-strong: rgb(var(--accent-strong));
--color-accent-dim: rgb(var(--accent-dim));
--color-accent-text: rgb(var(--accent-text));
--color-accent-border: rgb(var(--accent-border));
--color-ok: rgb(var(--ok));
--color-ok-dim: rgb(var(--ok-dim));
--color-warn: rgb(var(--warn));
--color-warn-dim: rgb(var(--warn-dim));
--color-bad: rgb(var(--bad));
--color-bad-dim: rgb(var(--bad-dim));
--color-info: rgb(var(--info));
--color-info-dim: rgb(var(--info-dim));
--color-body-text: rgb(var(--body-text));
--color-code-bg: rgb(var(--code-bg));
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
:root {
color-scheme: light;
--header-bg: rgba(255, 255, 255, 0.82);
--scrim: rgba(15, 23, 42, 0.4);
--knob: #ffffff;
@ -77,10 +124,6 @@
}
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html,
body {

648
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@ bun run dev # Start local dev server
```text
app/ # Vue 3 UI (Nuxt app dir)
├── app.vue # Root component (NuxtPage)
├── assets/css/main.css # Tailwind CSS entry: theme tokens (RGB-triplet CSS variables) + @layer components (@apply)
├── assets/css/main.css # Tailwind CSS v4 entry: @import "tailwindcss" + @theme inline tokens (RGB-triplet CSS variables) + @layer components (@apply)
├── pages/ # index (landing), terms, privacy, admin/[...slug] (console SPA)
├── components/ # ConsolePage, RouteCard/Editor, GroupEditor, MembersPanel, WebhookPanel,
│ # SendLogs, AuditLog, AppToasts, LegalLayout

View file

@ -15,7 +15,7 @@ bun run dev # 启动本地开发服务器
```text
app/ # Vue 3 UINuxt app 目录)
├── app.vue # 根组件 (NuxtPage)
├── assets/css/main.css # Tailwind CSS 入口:主题令牌RGB 三元组 CSS 变量)+ @layer components (@apply)
├── assets/css/main.css # Tailwind CSS v4 入口:@import "tailwindcss" + @theme inline 令牌RGB 三元组 CSS 变量)+ @layer components (@apply)
├── pages/ # index落地页、terms、privacy、admin/[...slug](控制台 SPA
├── components/ # ConsolePage、RouteCard/Editor、GroupEditor、MembersPanel、WebhookPanel、
│ # SendLogs、AuditLog、AppToasts、LegalLayout

View file

@ -1,6 +1,8 @@
import tailwindcss from "@tailwindcss/vite";
export default defineNuxtConfig({
compatibilityDate: "2025-07-15",
modules: ["@nuxtjs/tailwindcss"],
css: ["~/assets/css/main.css"],
// Target: Cloudflare Workers (single _worker.js via the cloudflare_module preset).
nitro: {
preset: "cloudflare_module",
@ -23,9 +25,8 @@ export default defineNuxtConfig({
],
},
},
tailwindcss: {
cssPath: "~/assets/css/main.css",
configPath: "tailwind.config",
vite: {
plugins: [tailwindcss()],
},
devtools: { enabled: false },
runtimeConfig: {

View file

@ -33,13 +33,13 @@
},
"devDependencies": {
"@cloudflare/workers-types": "^5.20260801.1",
"@nuxtjs/tailwindcss": "^6.14.0",
"@tailwindcss/vite": "^4.3.3",
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^8.21.0",
"eslint": "^10.9.0",
"markdownlint-cli": "^0.49.1",
"prettier": "^3.9.6",
"tailwindcss": "^3.4.19",
"tailwindcss": "^4.3.3",
"typescript": "^5.7.0",
"vitepress": "^1.6.4",
"vue-router": "^5.2.0",

View file

@ -1,66 +0,0 @@
import type { Config } from "tailwindcss";
/**
* Theme colors are RGB-triplet CSS variables (see app/assets/css/main.css),
* so every token supports Tailwind opacity modifiers (bg-accent/10, ...)
* and switches automatically with prefers-color-scheme.
*/
const themeColors = {
bg: "rgb(var(--bg) / <alpha-value>)",
surface: "rgb(var(--surface) / <alpha-value>)",
"surface-2": "rgb(var(--surface-2) / <alpha-value>)",
"surface-3": "rgb(var(--surface-3) / <alpha-value>)",
border: "rgb(var(--border) / <alpha-value>)",
"border-strong": "rgb(var(--border-strong) / <alpha-value>)",
text: "rgb(var(--text) / <alpha-value>)",
muted: "rgb(var(--muted) / <alpha-value>)",
faint: "rgb(var(--faint) / <alpha-value>)",
accent: "rgb(var(--accent) / <alpha-value>)",
"accent-strong": "rgb(var(--accent-strong) / <alpha-value>)",
"accent-dim": "rgb(var(--accent-dim) / <alpha-value>)",
"accent-text": "rgb(var(--accent-text) / <alpha-value>)",
"accent-border": "rgb(var(--accent-border) / <alpha-value>)",
ok: "rgb(var(--ok) / <alpha-value>)",
"ok-dim": "rgb(var(--ok-dim) / <alpha-value>)",
warn: "rgb(var(--warn) / <alpha-value>)",
"warn-dim": "rgb(var(--warn-dim) / <alpha-value>)",
bad: "rgb(var(--bad) / <alpha-value>)",
"bad-dim": "rgb(var(--bad-dim) / <alpha-value>)",
info: "rgb(var(--info) / <alpha-value>)",
"info-dim": "rgb(var(--info-dim) / <alpha-value>)",
"body-text": "rgb(var(--body-text) / <alpha-value>)",
"code-bg": "rgb(var(--code-bg) / <alpha-value>)",
};
export default <Partial<Config>>{
content: [],
theme: {
extend: {
colors: themeColors,
fontFamily: {
ui: "var(--font-ui)",
mono: "var(--font-mono)",
},
borderRadius: {
sm: "var(--radius-sm)",
DEFAULT: "var(--radius)",
},
boxShadow: {
card: "var(--shadow)",
"card-hover": "0 8px 24px -12px rgba(15, 23, 42, 0.25)",
"accent-lg": "0 12px 28px -10px var(--accent)",
drawer: "-16px 0 48px rgba(15, 23, 42, 0.12)",
modal: "0 12px 40px rgba(15, 23, 42, 0.25)",
},
keyframes: {
rise: {
from: { opacity: "0", transform: "translateY(8px)" },
to: { opacity: "1", transform: "none" },
},
},
animation: {
rise: "rise 0.35s cubic-bezier(0.2, 0.7, 0.2, 1) both",
},
},
},
};