From 911f38434685b7a7878f6e980ab08088832a5188 Mon Sep 17 00:00:00 2001 From: Kinyoo Date: Wed, 15 Jul 2026 14:41:42 +0800 Subject: [PATCH] feat(client): add Arco semantic color tokens (text/fill/border/functional) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-layer color tokens per 基础-色彩规范.md: primitive --arco-gray-1..10 + semantic --text-1..4 / --fill-1..4 / --border-base|-deep / --success|warning|danger(+hover/active/tint) in style.css, wired to Tailwind (text-1.. / fill-1.. / border-base / bg-success ...). Functional colors are theme-independent. New gallery ColorSection. Co-Authored-By: Claude Opus 4.8 --- .../pages/_gallery/sections/ColorSection.tsx | 288 ++++++++++++++++++ src/frontend/client/src/style.css | 54 ++++ src/frontend/client/tailwind.config.cjs | 36 +++ 3 files changed, 378 insertions(+) create mode 100644 src/frontend/client/src/pages/_gallery/sections/ColorSection.tsx diff --git a/src/frontend/client/src/pages/_gallery/sections/ColorSection.tsx b/src/frontend/client/src/pages/_gallery/sections/ColorSection.tsx new file mode 100644 index 000000000..b2a1d81cc --- /dev/null +++ b/src/frontend/client/src/pages/_gallery/sections/ColorSection.tsx @@ -0,0 +1,288 @@ +/** + * Color section — visualizes the color spec from docs-ui-refactor/基础-色彩规范.md + * (brand dual-theme ramp / Arco neutral grays / functional colors / tag pairs) + * so the designer can verify the tokens landed in src/style.css + tailwind.config.cjs. + * + * DEV-ONLY internal tooling, never shipped (route gated by import.meta.env.DEV). + * NOTE on literal hex values in this file: they are DISPLAY-ONLY spec swatches — + * the blue and green brand columns must render side by side regardless of the + * active theme, and some tag colors (skill purple, the fixed-blue exception) are + * intentionally not tokenized. Business code must never hardcode these values. + */ +import { CSSProperties, ReactNode } from 'react'; +import { cn } from '~/utils'; +import { ComponentPage, ExampleGroup, ExampleGrid, ExampleCard, CompareTable } from '../components/kit'; + +/* ------------------------------------------------------------------ * + * Small presentational helpers + * ------------------------------------------------------------------ */ + +/** Inline swatch + optional code label, for table cells. */ +function ColorCell({ + hex, + className, + style, + label, +}: { + /** Literal display color (spec documentation value). */ + hex?: string; + /** Live token class (e.g. bg-blue-500) — proves the wiring works. */ + className?: string; + style?: CSSProperties; + label?: string; +}) { + return ( +
+ + {(label ?? hex) && ( + {label ?? hex} + )} +
+ ); +} + +/** Labeled swatch for the functional-color state rows. */ +function StateSwatch({ + className, + style, + label, + sub, +}: { + className?: string; + style?: CSSProperties; + label: string; + sub?: string; +}) { + return ( +
+ + {label} + {sub && {sub}} +
+ ); +} + +/** A light-bg + strong-text tag chip (§4 tag pairs). */ +function TagChip({ + className, + style, + children, +}: { + className?: string; + style?: CSSProperties; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +/* ------------------------------------------------------------------ * + * Spec data + * ------------------------------------------------------------------ */ + +/** §1.1 brand ramp — blue/green documentation values. */ +const BRAND_RAMP = [ + { step: '50', blue: '#E8F3FF', green: '#E4F1EC', usage: '浅底一档:选中背景;按钮 filled 底 / outlined·text hover' }, + { step: '100', blue: '#BEDAFF', green: '#CCE4DA', usage: '浅底二档:filled hover / outlined·text 触屏 active' }, + { step: '200', blue: '#94BFFF', green: '#A3D2C0', usage: '浅底三档:filled 触屏 active' }, + { step: '300', blue: '#6AA1FF', green: '#6FBAA0', usage: '' }, + { step: '400', blue: '#4080FF', green: '#3D9B78', usage: 'hover(比主色亮一档)' }, + { step: '500', blue: '#165DFF', green: '#169C47', usage: '主色(= --brand-main)' }, + { step: '600', blue: '#024DE3', green: '#136345', usage: 'active / 按下(深一档)' }, + { step: '700', blue: '#0239AB', green: '#0F4D36', usage: '' }, + { step: '800', blue: '#042B80', green: '#0A3826', usage: '' }, + { step: '900', blue: '#051D52', green: '#062619', usage: '' }, +]; + +/** §2.1 primitive Arco gray ramp — rendered live from --arco-gray-N. */ +const ARCO_GRAYS = [ + { n: 1, hex: '#F7F8FA', note: 'hover 底 → fill-1' }, + { n: 2, hex: '#F2F3F5', note: 'filled 控件底 → fill-2' }, + { n: 3, hex: '#E5E6EB', note: '常规边框 → border-base / fill-3' }, + { n: 4, hex: '#C9CDD4', note: '禁用/占位 → text-4 / fill-4 / border-deep' }, + { n: 5, hex: '#A9AEB8', note: '' }, + { n: 6, hex: '#86909C', note: '辅助文字 → text-3(全站最高频 hex ×263)' }, + { n: 7, hex: '#6B7785', note: '' }, + { n: 8, hex: '#4E5969', note: '次要文字 → text-2' }, + { n: 9, hex: '#272E3B', note: '' }, + { n: 10, hex: '#1D2129', note: '主文字 → text-1' }, +]; + +/** §2.2 semantic neutral layer — token / class / value / live demo. */ +const NEUTRAL_SEMANTIC: { token: string; cls: string; value: string; usage: string; demo: ReactNode }[] = [ + { token: '--text-1', cls: 'text-text-1', value: 'gray-10 #1D2129', usage: '主文字:标题、正文主体', demo: 标题、正文主体 }, + { token: '--text-2', cls: 'text-text-2', value: 'gray-8 #4E5969', usage: '次文字:次要说明、默认按钮文字', demo: 次要说明文字 }, + { token: '--text-3', cls: 'text-text-3', value: 'gray-6 #86909C', usage: '辅助文字:弱提示、时间戳、占位符', demo: 弱提示 · 10 分钟前 }, + { token: '--text-4', cls: 'text-text-4', value: 'gray-4 #C9CDD4', usage: '禁用文字', demo: 禁用状态文字 }, + { token: '--fill-1', cls: 'bg-fill-1', value: 'gray-1 #F7F8FA', usage: '浅填充:hover 底、页面浅灰背景', demo: }, + { token: '--fill-2', cls: 'bg-fill-2', value: 'gray-2 #F2F3F5', usage: '填充:active 底、filled 控件底', demo: }, + { token: '--fill-3', cls: 'bg-fill-3', value: 'gray-3 #E5E6EB', usage: '深填充:filled hover', demo: }, + { token: '--fill-4', cls: 'bg-fill-4', value: 'gray-4 #C9CDD4', usage: '重填充:filled active', demo: }, + { token: '--border-base', cls: 'border-border-base', value: 'gray-3 #E5E6EB', usage: '常规边框:输入框、卡片、分割线(规范里的 border)', demo: }, + { token: '--border-deep', cls: 'border-border-deep', value: 'gray-4 #C9CDD4', usage: '深边框:强调分割、hover 边框', demo: }, +]; + +/* ------------------------------------------------------------------ */ + +export function ColorSection() { + return ( + + Arco 色板为基准的两层 token:primitive(--arco-gray-1~10)→ semantic( + --text-1~4 / --fill-1~4 / --border-base|-deep /{' '} + --success|warning|danger-*)。命名注:规范里的语义名 border 与 + shadcn 的 --border(HSL)及 Tailwind 边框类冲突,落地为{' '} + border-base(类 border-border-base,与现有{' '} + border-border-light 同构)。 + + } + whenToUse={[ + <> + 组件与业务代码只用 semantic 层(text-text-1 / bg-fill-1 /{' '} + border-border-base / bg-success…),不用{' '} + --arco-gray-* primitive(Tailwind 故意未接线),禁止裸 hex。 + , + <> + 品牌色永远走 blue-* 类(已重指向 --brand-*,自动蓝⇄绿换肤)或{' '} + rgb(var(--brand-NNN));禁止写品牌 hex。列表/菜单选中浅底统一{' '} + bg-blue-500/[0.07];按钮浅底走 50/100/200 实档。 + , + <> + 语义色(成功/警告/危险)不参与换肤。危险浅底分两种:按钮用主色透明阶{' '} + bg-danger/10~20;tag 用实色 bg-danger-tint(#FFECE8)。 + , + <>同一语义只允许一个值(如次要文字只能 text-text-2);新颜色先在色板找替代,确需新增走 token 评审进 primitive 层。, + ]} + > + + [ + {r.step}, + , + , + r.usage, + ]), + [ + muted, + , + , + '低饱和品牌点缀(如置顶 pin)· --brand-muted', + ], + ]} + /> +

+ 固定色例外(永远不换肤,别误改):审批中 tag 永远蓝 #E8F3FF/#165DFF + (ApprovalCenterDialog);应用中心置顶 pin 用 --brand-muted;第三方 logo + 原色保留。选中浅底示例: + + bg-blue-500/[0.07] + +

+
+ + + [ + --arco-gray-{g.n}, + , + g.note, + ])} + /> + + + + [ + {s.token}, + {s.cls}, + {s.value}, + s.usage, +
{s.demo}
, + ])} + /> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + 已驳回(tag 实色) + + + + + + + + + + + + + + 技能(紫 · 未 token 化) + 助手(橙 = warning 同值) + 已完成 + 已驳回 + 审批中(例外:永远蓝) + + + + +
+ ); +} diff --git a/src/frontend/client/src/style.css b/src/frontend/client/src/style.css index dc0d9892f..05560dbc0 100644 --- a/src/frontend/client/src/style.css +++ b/src/frontend/client/src/style.css @@ -77,6 +77,60 @@ --text-h1: var(--font-size-7); --leading-h1: var(--line-height-7); --text-display: var(--font-size-8); --leading-display: var(--line-height-8); --text-metric: var(--font-size-9); --leading-metric: var(--line-height-9); + + /* ============================================================ + * Color tokens (docs-ui-refactor/基础-色彩规范.md §2/§3/§7) + * Two layers: primitive --arco-gray-1..10 (Arco gray ramp, the + * numeric source) and semantic --text-1..4 / --fill-1..4 / + * --border-base|-deep / --success|warning|danger-* — components use + * the semantic layer only, via Tailwind (text-text-1 / bg-fill-1 / + * border-border-base / bg-success ...). RGB channel triplets so + * Tailwind `/` modifiers keep working. + * Naming constraints: --gray-N / --red-N / --green-N belong to the + * legacy LibreChat ramps above and must NOT be overwritten; --border + * belongs to shadcn (HSL) — hence --arco-gray-N and --border-base. + * success/warning/danger are theme-INDEPENDENT: they never follow + * the blue⇄green brand switch. Light-mode values only for now + * (dark mode inherits them — known debt, same as --btn-*). + * ============================================================ */ + /* Primitive — Arco gray 1-10 */ + --arco-gray-1: 247 248 250; /* #F7F8FA */ + --arco-gray-2: 242 243 245; /* #F2F3F5 */ + --arco-gray-3: 229 230 235; /* #E5E6EB */ + --arco-gray-4: 201 205 212; /* #C9CDD4 */ + --arco-gray-5: 169 174 184; /* #A9AEB8 */ + --arco-gray-6: 134 144 156; /* #86909C */ + --arco-gray-7: 107 119 133; /* #6B7785 */ + --arco-gray-8: 78 89 105; /* #4E5969 */ + --arco-gray-9: 39 46 59; /* #272E3B */ + --arco-gray-10: 29 33 41; /* #1D2129 */ + + /* Semantic — neutral text / fill / border */ + --text-1: var(--arco-gray-10); /* primary text: titles, body */ + --text-2: var(--arco-gray-8); /* secondary text */ + --text-3: var(--arco-gray-6); /* hint / timestamp / placeholder */ + --text-4: var(--arco-gray-4); /* disabled text */ + --fill-1: var(--arco-gray-1); /* hover bg / light page bg */ + --fill-2: var(--arco-gray-2); /* active bg / filled control base */ + --fill-3: var(--arco-gray-3); /* filled hover */ + --fill-4: var(--arco-gray-4); /* filled active */ + --border-base: var(--arco-gray-3); /* regular border ("border" in the spec) */ + --border-deep: var(--arco-gray-4); /* emphasized divider / hover border */ + + /* Semantic — functional (never themed) */ + --success: 0 180 42; /* #00B42A (Arco green-6) */ + --success-hover: 35 195 67; /* #23C343 */ + --success-active: 0 154 41; /* #009A29 */ + --success-tint: 232 255 234; /* #E8FFEA (Arco green-1) — tag / light bg */ + --warning: 255 125 0; /* #FF7D00 (Arco orange-6) */ + --warning-hover: 255 154 46; /* #FF9A2E */ + --warning-active: 210 95 0; /* #D25F00 */ + --warning-tint: 255 247 232; /* #FFF7E8 (Arco orange-1) — tag / light bg */ + --danger: 245 63 63; /* #F53F3F (= --btn-danger) */ + --danger-hover: 214 55 58; /* #D6373A (= --btn-danger-hover) */ + --danger-active: 208 47 51; /* #D02F33 (= --btn-danger-active) */ + --danger-tint: 255 236 232; /* #FFECE8 (Arco red-1) — TAG bg only; buttons + use alpha over the main red (bg-danger/10 …) */ } /* Mobile (narrow viewport ≤768px) remap of the SEMANTIC layer only: diff --git a/src/frontend/client/tailwind.config.cjs b/src/frontend/client/tailwind.config.cjs index 71c9833b8..abb20f475 100644 --- a/src/frontend/client/tailwind.config.cjs +++ b/src/frontend/client/tailwind.config.cjs @@ -180,6 +180,42 @@ module.exports = { 'btn-danger-hover': 'rgb(var(--btn-danger-hover) / )', 'btn-danger-active': 'rgb(var(--btn-danger-active) / )', 'btn-disabled-border': 'rgb(var(--btn-disabled-border) / )', + // Arco color tokens (docs-ui-refactor/基础-色彩规范.md §2/§3/§7) — semantic + // layer only; the --arco-gray-* primitives are intentionally NOT wired so + // components can't bypass the semantic names. RGB-channel vars live in + // src/style.css :root; channel form keeps `/` modifiers working. + // Classes: text-text-1…4 / bg-fill-1…4 / border-border-base|-deep / + // bg-success|warning|danger(+-hover/-active/-tint). "border-base" because + // the plain `border` color key is taken by shadcn below. success/warning/ + // danger never follow the blue⇄green brand theme. + 'text-1': 'rgb(var(--text-1) / )', + 'text-2': 'rgb(var(--text-2) / )', + 'text-3': 'rgb(var(--text-3) / )', + 'text-4': 'rgb(var(--text-4) / )', + 'fill-1': 'rgb(var(--fill-1) / )', + 'fill-2': 'rgb(var(--fill-2) / )', + 'fill-3': 'rgb(var(--fill-3) / )', + 'fill-4': 'rgb(var(--fill-4) / )', + 'border-base': 'rgb(var(--border-base) / )', + 'border-deep': 'rgb(var(--border-deep) / )', + success: { + DEFAULT: 'rgb(var(--success) / )', + hover: 'rgb(var(--success-hover) / )', + active: 'rgb(var(--success-active) / )', + tint: 'rgb(var(--success-tint) / )', + }, + warning: { + DEFAULT: 'rgb(var(--warning) / )', + hover: 'rgb(var(--warning-hover) / )', + active: 'rgb(var(--warning-active) / )', + tint: 'rgb(var(--warning-tint) / )', + }, + danger: { + DEFAULT: 'rgb(var(--danger) / )', + hover: 'rgb(var(--danger-hover) / )', + active: 'rgb(var(--danger-active) / )', + tint: 'rgb(var(--danger-tint) / )', + }, 'presentation': 'var(--presentation)', 'text-primary': 'var(--text-primary)', 'text-secondary': 'var(--text-secondary)',