diff --git a/ci/check-codegen-drift.sh b/ci/check-codegen-drift.sh index 3b95492..1187f77 100755 --- a/ci/check-codegen-drift.sh +++ b/ci/check-codegen-drift.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # check-codegen-drift.sh — codegen 漂移检查(支柱 2:契约单源 / token 单源)。 # -# design/colors_and_type.css 是唯一 token 真相源,client/lib/pangolin_tokens.gen.dart +# design/prototype/tokens.css 是唯一 token 真相源,client/lib/pangolin_tokens.gen.dart # 由 design/codegen/gen_flutter_tokens.mjs 生成、勿手改(CLAUDE.md 铁律)。本检查重新 # 生成一遍,若生成物与已提交版本不一致 → 漂移:要么改了 CSS 没重生成,要么手改了 # 生成物。两者都会让"单源"失效、契约悄悄分叉。 @@ -22,7 +22,7 @@ echo "→ 重新生成 Flutter token (design/codegen/gen_flutter_tokens.mjs) ... node design/codegen/gen_flutter_tokens.mjs if ! git diff --quiet -- "$GEN"; then - echo "❌ codegen 漂移:$GEN 与 design/colors_and_type.css 不一致。" >&2 + echo "❌ codegen 漂移:$GEN 与 design/prototype/tokens.css 不一致。" >&2 echo " 原因:改了 CSS 真相源却没重生成,或手改了生成物(铁律:勿手改 *.gen.dart)。" >&2 echo " 修复:node design/codegen/gen_flutter_tokens.mjs 后提交生成物。" >&2 echo " ── diff(前 40 行)──" >&2 diff --git a/client/lib/pangolin_tokens.gen.dart b/client/lib/pangolin_tokens.gen.dart index d45df1f..83de116 100644 --- a/client/lib/pangolin_tokens.gen.dart +++ b/client/lib/pangolin_tokens.gen.dart @@ -1,6 +1,6 @@ // pangolin_tokens.gen.dart // AUTO-GENERATED — 勿手改。 -// 源: design/colors_and_type.css +// 源: design/prototype/tokens.css // 生成器: design/codegen/gen_flutter_tokens.mjs // // 包含:PangolinColors · PangolinSpacing · PangolinRadius · PangolinMotion · PangolinShadow @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; /// ── Primitive color ramps ─────────────────────────────────────────── -/// Auto-generated from design/colors_and_type.css :root color ramps. +/// Auto-generated from design/prototype/tokens.css :root color ramps. class PangolinColors { PangolinColors._(); diff --git a/design/codegen/gen_flutter_tokens.mjs b/design/codegen/gen_flutter_tokens.mjs index f37c3f1..7570159 100644 --- a/design/codegen/gen_flutter_tokens.mjs +++ b/design/codegen/gen_flutter_tokens.mjs @@ -2,7 +2,7 @@ /** * gen_flutter_tokens.mjs — CSS token → Dart codegen * - * 唯一真相源:design/colors_and_type.css + * 唯一真相源:design/prototype/tokens.css * 输出: client/lib/pangolin_tokens.gen.dart * * 生成内容(纯数据层,无 Flutter 实现逻辑): @@ -20,7 +20,7 @@ import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const SRC = resolve(__dirname, '../colors_and_type.css'); +const SRC = resolve(__dirname, '../prototype/tokens.css'); const OUT = resolve(__dirname, '../../client/lib/pangolin_tokens.gen.dart'); if (!existsSync(SRC)) { @@ -125,7 +125,7 @@ const lines = []; lines.push(`// pangolin_tokens.gen.dart`); lines.push(`// AUTO-GENERATED — 勿手改。`); -lines.push(`// 源: design/colors_and_type.css`); +lines.push(`// 源: design/prototype/tokens.css`); lines.push(`// 生成器: design/codegen/gen_flutter_tokens.mjs`); lines.push(`//`); lines.push(`// 包含:PangolinColors · PangolinSpacing · PangolinRadius · PangolinMotion · PangolinShadow`); @@ -136,7 +136,7 @@ lines.push(``); // ── PangolinColors ────────────────────────────────────────────────── lines.push(`/// ── Primitive color ramps ───────────────────────────────────────────`); -lines.push(`/// Auto-generated from design/colors_and_type.css :root color ramps.`); +lines.push(`/// Auto-generated from design/prototype/tokens.css :root color ramps.`); lines.push(`class PangolinColors {`); lines.push(` PangolinColors._();`); lines.push(``); diff --git a/design/colors_and_type.css b/design/colors_and_type.css index 3b1b48b..a82751b 100644 --- a/design/colors_and_type.css +++ b/design/colors_and_type.css @@ -1,218 +1,8 @@ /* ============================================================= - 穿山甲 VPN · Pangolin VPN — Design Tokens - colors_and_type.css - Single source of truth: color ramps, semantic colors (light + dark), - typography, spacing, radii, shadows, motion. + 穿山甲 VPN · Pangolin VPN — Design Tokens (兼容别名) + ⚠️ 真源已迁至 design/prototype/tokens.css(ds-flow 原型单源)。 + 本文件仅为浏览器内 @import 转发,供仍 此路径的历史 preview/*.html 使用。 + codegen(Flutter gen_flutter_tokens.mjs / Web build-tokens.mjs)已改读 prototype/tokens.css。 + 改 token 只改 design/prototype/tokens.css,勿在此加变量(不会被 codegen 解析)。 ============================================================= */ - -/* ---- Webfonts (open-source; documented as the brand's chosen faces) ---- - Sora — display / headings (geometric, friendly) - Manrope — body / UI (humanist geometric) - Noto Sans SC — Chinese (CJK companion) - JetBrains Mono — data readouts (IP, speed, keys) */ -@import url('https://fonts.googleapis.com/css2?family=Sora:wght@500;600;700&family=Manrope:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap'); - -:root { - /* =========================================================== - 1. PRIMITIVE COLOR RAMPS - =========================================================== */ - - /* Clay / Copper — the pangolin-armor primary (warm earth) */ - --clay-50: #FAF3ED; - --clay-100: #F2E2D4; - --clay-200: #E6C7AC; - --clay-300: #D9A982; - --clay-400: #CC8B5C; - --clay-500: #B96A3D; /* ← brand primary */ - --clay-600: #9E5630; - --clay-700: #7E4426; - --clay-800: #5E331D; - --clay-900: #3D2213; - - /* Sand / Taupe — warm neutral ramp */ - --sand-50: #FAF8F4; - --sand-100: #F2EEE7; - --sand-200: #E6DFD3; - --sand-300: #D2C8B8; - --sand-400: #B0A491; - --sand-500: #8C8270; - --sand-600: #6B6253; - --sand-700: #4E4940; - --sand-800: #2E2A24; - --sand-900: #1F1C18; - --sand-950: #14110E; /* warm espresso near-black */ - - /* Semantic hues (earth-tuned) */ - --green-400: #7FB07A; - --green-500: #5B8C5A; /* connected / secure */ - --green-600: #467046; - --amber-400: #E2B05A; - --amber-500: #D69A3C; /* connecting / warning */ - --amber-600: #B47E29; - --red-400: #D4715A; - --red-500: #C0533B; /* error / disconnect */ - --red-600: #9E4230; - - /* =========================================================== - 2. SEMANTIC TOKENS — LIGHT THEME (default) - =========================================================== */ - - /* Backgrounds & surfaces */ - --bg: var(--sand-50); /* app canvas */ - --bg-subtle: var(--sand-100); /* striped / inset regions */ - --surface: #FFFFFF; /* cards, sheets */ - --surface-2: var(--sand-50); /* nested surface */ - --overlay: rgba(31, 28, 24, 0.45); - - /* Foreground / text */ - --fg1: var(--sand-900); /* primary text */ - --fg2: var(--sand-600); /* secondary text */ - --fg3: var(--sand-500); /* tertiary / captions */ - --fg-on-accent: #FFFFFF; /* text on clay fills */ - - /* Brand / accent */ - --accent: var(--clay-500); - --accent-hover: var(--clay-600); - --accent-press: var(--clay-700); - --accent-subtle: var(--clay-50); - --accent-border: var(--clay-200); - - /* Borders & lines */ - --border: var(--sand-200); - --border-strong: var(--sand-300); - --ring: rgba(185, 106, 61, 0.35); /* focus ring (clay) */ - - /* Status */ - --success: var(--green-500); - --success-subtle: #E9F0E6; - --warning: var(--amber-500); - --warning-subtle: #F8EED6; - --danger: var(--red-500); - --danger-subtle: #F6E1DA; - - /* =========================================================== - 3. TYPOGRAPHY - =========================================================== */ - --font-display: 'Sora', 'Noto Sans SC', system-ui, sans-serif; - --font-sans: 'Manrope', 'Noto Sans SC', system-ui, sans-serif; - --font-cjk: 'Noto Sans SC', 'Manrope', system-ui, sans-serif; - --font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', monospace; - - /* Type scale (root 16px) */ - --text-display-xl: 3rem; /* 48 */ - --text-display: 2.25rem; /* 36 */ - --text-h1: 1.875rem; /* 30 */ - --text-h2: 1.5rem; /* 24 */ - --text-h3: 1.25rem; /* 20 */ - --text-body-lg: 1.125rem; /* 18 */ - --text-body: 1rem; /* 16 */ - --text-sm: 0.875rem; /* 14 */ - --text-caption: 0.75rem; /* 12 */ - - --leading-tight: 1.15; - --leading-snug: 1.3; - --leading-normal:1.5; - --leading-relaxed:1.65; - - --tracking-tight: -0.02em; - --tracking-snug: -0.01em; - --tracking-wide: 0.04em; - --tracking-caps: 0.08em; - - /* =========================================================== - 4. SPACING (4px base) - =========================================================== */ - --space-0: 0; - --space-1: 0.25rem; /* 4 */ - --space-2: 0.5rem; /* 8 */ - --space-3: 0.75rem; /* 12 */ - --space-4: 1rem; /* 16 */ - --space-5: 1.25rem; /* 20 */ - --space-6: 1.5rem; /* 24 */ - --space-8: 2rem; /* 32 */ - --space-10: 2.5rem; /* 40 */ - --space-12: 3rem; /* 48 */ - --space-16: 4rem; /* 64 */ - - /* =========================================================== - 5. RADII (generous = friendly) - =========================================================== */ - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 14px; - --radius-xl: 20px; - --radius-2xl: 28px; - --radius-full: 999px; - - /* =========================================================== - 6. SHADOWS (warm-tinted, soft) - =========================================================== */ - --shadow-sm: 0 1px 2px rgba(45, 30, 20, 0.06); - --shadow-md: 0 4px 14px rgba(45, 30, 20, 0.08); - --shadow-lg: 0 12px 32px rgba(45, 30, 20, 0.12); - --shadow-xl: 0 24px 60px rgba(45, 30, 20, 0.16); - --shadow-focus: 0 0 0 4px var(--ring); - - /* =========================================================== - 7. MOTION - =========================================================== */ - --ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* @kind other */ - --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* @kind other */ - --dur-fast: 140ms; /* @kind other */ - --dur-base: 220ms; /* @kind other */ - --dur-slow: 360ms; /* @kind other */ -} - -/* =========================================================== - DARK THEME — warm espresso - Apply via [data-theme="dark"] on or any container. - =========================================================== */ -[data-theme="dark"] { - --bg: var(--sand-950); - --bg-subtle: var(--sand-900); - --surface: #221E19; - --surface-2: #2A251F; - --overlay: rgba(0, 0, 0, 0.6); - - --fg1: #F4EFE8; - --fg2: #B6AC9C; - --fg3: #897F6F; - --fg-on-accent: #1F1C18; - - --accent: var(--clay-400); - --accent-hover: var(--clay-300); - --accent-press: var(--clay-500); - --accent-subtle: rgba(204, 139, 92, 0.14); - --accent-border: rgba(204, 139, 92, 0.30); - - --border: rgba(242, 238, 231, 0.10); - --border-strong: rgba(242, 238, 231, 0.18); - --ring: rgba(204, 139, 92, 0.45); - - --success: var(--green-400); - --success-subtle: rgba(127, 176, 122, 0.16); - --warning: var(--amber-400); - --warning-subtle: rgba(226, 176, 90, 0.16); - --danger: var(--red-400); - --danger-subtle: rgba(212, 113, 90, 0.16); - - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); - --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.45); - --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5); - --shadow-xl: 0 24px 60px rgba(0, 0, 0, 0.55); -} - -/* =========================================================== - SEMANTIC TYPE CLASSES (use directly in markup) - =========================================================== */ -.t-display-xl { font-family: var(--font-display); font-size: var(--text-display-xl); font-weight: 700; line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); } -.t-display { font-family: var(--font-display); font-size: var(--text-display); font-weight: 700; line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); } -.t-h1 { font-family: var(--font-display); font-size: var(--text-h1); font-weight: 600; line-height: var(--leading-snug); letter-spacing: var(--tracking-snug); } -.t-h2 { font-family: var(--font-display); font-size: var(--text-h2); font-weight: 600; line-height: var(--leading-snug); letter-spacing: var(--tracking-snug); } -.t-h3 { font-family: var(--font-sans); font-size: var(--text-h3); font-weight: 600; line-height: var(--leading-snug); } -.t-body-lg { font-family: var(--font-sans); font-size: var(--text-body-lg); font-weight: 400; line-height: var(--leading-relaxed); } -.t-body { font-family: var(--font-sans); font-size: var(--text-body); font-weight: 400; line-height: var(--leading-normal); } -.t-sm { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 400; line-height: var(--leading-normal); } -.t-caption { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 500; line-height: var(--leading-normal); } -.t-overline { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; text-transform: uppercase; letter-spacing: var(--tracking-caps); } -.t-mono { font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 400; font-feature-settings: 'tnum' 1; } +@import url('./prototype/tokens.css'); diff --git a/design/prototype/serve.mjs b/design/prototype/serve.mjs new file mode 100644 index 0000000..811eaf4 --- /dev/null +++ b/design/prototype/serve.mjs @@ -0,0 +1,97 @@ +// 零依赖本地热重载静态服务器 — pangolin 原型单源预览 +// 用法: node design/prototype/serve.mjs [port] 默认 5180 +// 评审给 URL,不截图(ds-flow L3)。 +import http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawn } from 'node:child_process'; + +const ROOT = path.dirname(fileURLToPath(import.meta.url)); +const PORT = Number(process.argv[2]) || 5180; + +const MIME = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.mjs': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.woff2': 'font/woff2', + '.otf': 'font/otf', + '.ttf': 'font/ttf', +}; + +const clients = new Set(); +const RELOAD_SNIPPET = `\n\n`; + +const server = http.createServer((req, res) => { + if (req.url === '/__reload') { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }); + res.write('retry: 500\n\n'); + clients.add(res); + req.on('close', () => clients.delete(res)); + return; + } + + let urlPath = decodeURIComponent(req.url.split('?')[0]); + if (urlPath === '/') urlPath = '/index.html'; + const filePath = path.join(ROOT, urlPath); + if (!filePath.startsWith(ROOT)) { res.writeHead(403); res.end('forbidden'); return; } + + fs.readFile(filePath, (err, data) => { + if (err) { res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('