mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
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:
parent
32a8c2ce4e
commit
c75567cf15
8 changed files with 264 additions and 530 deletions
|
|
@ -9,8 +9,8 @@ Core pipeline: GitHub Webhook → Worker (verify + filter + format) → Discord
|
||||||
## Key Decisions
|
## Key Decisions
|
||||||
|
|
||||||
- Runtime: Cloudflare Workers via the Nitro `cloudflare_module` preset (`_worker.js`), H3 event handlers in `server/routes/`
|
- 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
|
- 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` 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`
|
- 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
|
- 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
|
- 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)
|
- 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
|
```text
|
||||||
app/ # Vue 3 UI (Nuxt app dir)
|
app/ # Vue 3 UI (Nuxt app dir)
|
||||||
├── app.vue # root component (NuxtPage)
|
├── 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)
|
├── pages/ # index (landing), terms, privacy, admin/[...slug] (console SPA)
|
||||||
├── components/ # ConsolePage (sidebar shell + topbar), AdminHome (overview dashboard),
|
├── components/ # ConsolePage (sidebar shell + topbar), AdminHome (overview dashboard),
|
||||||
│ # RouteCard/Editor (RouteEditor has FilterNodeEditor AST builder + TagInput chips),
|
│ # RouteCard/Editor (RouteEditor has FilterNodeEditor AST builder + TagInput chips),
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,61 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
* Theme tokens — RGB triplets so Tailwind opacity modifiers (bg-accent/10)
|
* Theme tokens — RGB triplets so Tailwind opacity modifiers (bg-accent/10)
|
||||||
* work against them; values switch automatically with prefers-color-scheme
|
* work against them; values switch automatically with prefers-color-scheme
|
||||||
* (unless an explicit data-theme="light" is set on <html>).
|
* (unless an explicit data-theme="light" is set on <html>).
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
:root {
|
@theme inline {
|
||||||
color-scheme: light;
|
|
||||||
--font-ui: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
--font-ui: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
--radius: 12px;
|
|
||||||
--radius-sm: 8px;
|
--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);
|
--header-bg: rgba(255, 255, 255, 0.82);
|
||||||
--scrim: rgba(15, 23, 42, 0.4);
|
--scrim: rgba(15, 23, 42, 0.4);
|
||||||
--knob: #ffffff;
|
--knob: #ffffff;
|
||||||
|
|
@ -77,10 +124,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ bun run dev # Start local dev server
|
||||||
```text
|
```text
|
||||||
app/ # Vue 3 UI (Nuxt app dir)
|
app/ # Vue 3 UI (Nuxt app dir)
|
||||||
├── app.vue # Root component (NuxtPage)
|
├── 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)
|
├── pages/ # index (landing), terms, privacy, admin/[...slug] (console SPA)
|
||||||
├── components/ # ConsolePage, RouteCard/Editor, GroupEditor, MembersPanel, WebhookPanel,
|
├── components/ # ConsolePage, RouteCard/Editor, GroupEditor, MembersPanel, WebhookPanel,
|
||||||
│ # SendLogs, AuditLog, AppToasts, LegalLayout
|
│ # SendLogs, AuditLog, AppToasts, LegalLayout
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ bun run dev # 启动本地开发服务器
|
||||||
```text
|
```text
|
||||||
app/ # Vue 3 UI(Nuxt app 目录)
|
app/ # Vue 3 UI(Nuxt app 目录)
|
||||||
├── app.vue # 根组件 (NuxtPage)
|
├── 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)
|
├── pages/ # index(落地页)、terms、privacy、admin/[...slug](控制台 SPA)
|
||||||
├── components/ # ConsolePage、RouteCard/Editor、GroupEditor、MembersPanel、WebhookPanel、
|
├── components/ # ConsolePage、RouteCard/Editor、GroupEditor、MembersPanel、WebhookPanel、
|
||||||
│ # SendLogs、AuditLog、AppToasts、LegalLayout
|
│ # SendLogs、AuditLog、AppToasts、LegalLayout
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2025-07-15",
|
compatibilityDate: "2025-07-15",
|
||||||
modules: ["@nuxtjs/tailwindcss"],
|
css: ["~/assets/css/main.css"],
|
||||||
// Target: Cloudflare Workers (single _worker.js via the cloudflare_module preset).
|
// Target: Cloudflare Workers (single _worker.js via the cloudflare_module preset).
|
||||||
nitro: {
|
nitro: {
|
||||||
preset: "cloudflare_module",
|
preset: "cloudflare_module",
|
||||||
|
|
@ -23,9 +25,8 @@ export default defineNuxtConfig({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tailwindcss: {
|
vite: {
|
||||||
cssPath: "~/assets/css/main.css",
|
plugins: [tailwindcss()],
|
||||||
configPath: "tailwind.config",
|
|
||||||
},
|
},
|
||||||
devtools: { enabled: false },
|
devtools: { enabled: false },
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,13 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^5.20260801.1",
|
"@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/eslint-plugin": "^8.21.0",
|
||||||
"@typescript-eslint/parser": "^8.21.0",
|
"@typescript-eslint/parser": "^8.21.0",
|
||||||
"eslint": "^10.9.0",
|
"eslint": "^10.9.0",
|
||||||
"markdownlint-cli": "^0.49.1",
|
"markdownlint-cli": "^0.49.1",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"tailwindcss": "^3.4.19",
|
"tailwindcss": "^4.3.3",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"vitepress": "^1.6.4",
|
"vitepress": "^1.6.4",
|
||||||
"vue-router": "^5.2.0",
|
"vue-router": "^5.2.0",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue