feat(ui-refactor 1/4): token 基座对齐 beautifului.dev foundation 原版——蓝色 accent、ring+分层阴影、圆角体系 6/8/10/14、强缓动曲线、暗色专属 ring;Button 恢复 pill 体系

This commit is contained in:
Rick Ye
2026-09-11 11:58:27 +08:00
parent 665f848c93
commit 60fc9b6f4f
31 changed files with 1397 additions and 198 deletions
@@ -4,29 +4,33 @@ import { ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
/* 设计规范:矩形控件统一 5px 圆角(rounded-control),rounded-full 仅限
* icon 圆钮/状态点/头像。动效走全局默认 400ms + ease(@theme 注入)。 */
/* beautifului.dev 原版 Button:Pill 胶囊形是应用的核心按钮语言。显式对称
* padding(不锁高度)保证上下留白永远相等;filled 变体带一道 1px 内高光;
* 按压回缩 0.96、150ms ease-out。 */
const filledShadow = "shadow-[inset_0_1px_0_rgba(255,255,255,0.14)]";
export const buttonVariants = cva(
`inline-flex items-center justify-center font-medium select-none
transition-[transform,background-color,opacity]
active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none`,
transition-[transform,background-color,opacity] duration-150 ease-out
active:scale-[0.96] disabled:opacity-50 disabled:pointer-events-none`,
{
variants: {
variant: {
primary: "bg-ink text-canvas hover:opacity-90",
primary: `bg-ink text-canvas hover:opacity-90 dark:bg-ink dark:text-canvas ${filledShadow}`,
secondary: "bg-surface text-ink shadow-btn hover:bg-inset aria-expanded:bg-hover",
ghost: "bg-hover-2 text-ink hover:bg-line-strong",
accent: "bg-accent text-white hover:bg-accent-ink",
success: "bg-green text-white hover:brightness-95",
accent: `bg-accent text-white hover:bg-accent-ink ${filledShadow}`,
success: `bg-green text-white hover:brightness-95 ${filledShadow}`,
/* transparent until hovered — for dense toolbars/action rows */
quiet: "text-ink hover:bg-hover",
},
size: {
/* compact toolbar control — fixed height, lighter weight */
xs: "h-7 rounded-control px-2.5 text-[12px] font-normal leading-none gap-1",
/* canonical action button */
sm: "h-[27px] px-3 text-[13px] leading-none rounded-control gap-1.5",
md: "px-4 py-[9px] text-sm leading-none rounded-control gap-2",
/* compact toolbar pill — fixed height, lighter weight */
xs: "h-7 rounded-full px-2.5 text-[12px] font-normal leading-none gap-1",
/* canonical action pill — 27px tall, roomy sides */
sm: "h-[27px] px-3 text-[13px] leading-none rounded-full gap-1.5",
md: "px-4 py-[9px] text-sm leading-none rounded-full gap-2",
},
},
defaultVariants: { variant: "secondary", size: "md" },
@@ -1,115 +1,127 @@
/* Beautiful UI foundation — design tokens 按项目设计规范(OpenAI 浅色体系)重映射。
* 规范要点:浅色界面;accent #8e8ea0(muted purple-gray);正文近黑;8px 间距网格;
* 圆角统一 5px(圆形仅限 icon 按钮/状态点/头像);动效统一 400ms + ease;
* 平面层级:surface 之间用 hairline 边框分层,仅浮层(modal/popover/审批卡)允许一层中性软阴影。
* 层级语义:--accent 负责焦点环/选区/选中态/装饰 tint;--accent-ink(加深版)负责
* 链接与可交互文本(保证 AA 对比度);--ink-3 是弱化文本的 AA 安全灰。 */
/* Beautiful UI foundation — design tokens 与 beautifului.dev 原版 foundation.css
* 对齐(MIT):冷调近白画布、白色卡片、hairline 边框、「ring + 柔和分层阴影」
* 的层级体系、蓝色 accent、克制语义色。亮暗两套完整 token,切换 = 一次重绘。
* 圆角体系:chip 6 / control 8 / card 10 / window 14 / pill 999。
* 动效:强缓动曲线(--ease-out-strong 等)+ pill 控件 150ms ease-out。 */
@import "shadow-plugin/unprefixed";
@custom-variant dark (&:where(.dark, .dark *));
/* ──────────────────────────────────────────────────────────
* OpenAI light — design tokens
* Beautiful UI — design tokens
* Cool near-white canvas, white cards, hairline borders, layered
* single-digit-opacity shadows, a neutral ink ramp, and semantic
* color used sparingly as a condiment.
* ────────────────────────────────────────────────────────── */
:root {
/* motion — 规范统一值 */
--duration-fast: 400ms;
--duration-base: 400ms;
--duration-slow: 400ms;
--ease-standard: ease;
/* motion — pill 控件/悬停反馈走 150ms ease-out;大位移动效用强曲线变量 */
--duration-fast: 150ms;
--duration-base: 220ms;
--duration-slow: 360ms;
--ease-standard: ease-out;
/* surfaces — 中性灰阶,白底为主 */
--page: #ffffff;
--canvas: #ffffff;
--surface: #ffffff;
--inset: #f4f4f5;
--hover: #f0f0f1;
--hover-2: #e6e6e8;
/* surfaces — cool, blue-tinted neutrals */
--page: oklch(0.985 0.001 286.376);
--canvas: oklch(0.961 0.002 247.84);
--surface: oklch(1 0 0);
--inset: oklch(0.979 0.002 247.839);
--hover: oklch(0.97 0.002 247.839);
--hover-2: oklch(0.933 0.003 247.86);
/* ink ramp — 正文近黑(规范 #000 的 a11y 修正版),弱化层为 AA 安全灰 */
--ink: #0d0d0d;
--ink-2: #5d5d5d;
--ink-3: #75757f;
/* ink ramp */
--ink: oklch(0.247 0.006 258.361);
--ink-2: oklch(0.506 0.01 264.477);
--ink-3: oklch(0.695 0.009 264.505);
/* borders — 实色 hairline,不用 alpha */
--line: #ececec;
--line-strong: #dcdce0;
--line-soft: #f4f4f5;
/* borders — solid and crisp, not alpha */
--line: oklch(0.946 0.003 264.542);
--line-strong: oklch(0.912 0.005 258.326);
--line-soft: oklch(0.966 0.002 264.542);
--grid-line: color-mix(in srgb, var(--line) 78%, transparent);
--field: #f4f4f5;
--stripe: rgba(13, 13, 13, 0.04);
--stripe-bg: #f7f7f8;
--field: oklch(0.961 0.001 286.375);
--stripe: oklch(0.405 0 0 / 0.075);
--stripe-bg: oklch(0.97 0 0);
/* accent — 规范主色 #8e8ea0 及其加深可交互版 */
--accent: #8e8ea0;
--accent-ink: #707084;
--accent-tint: rgba(142, 142, 160, 0.12);
/* accent */
--accent: oklch(0.626 0.205 254.947);
--accent-ink: oklch(0.556 0.187 255.617);
--accent-tint: oklch(0.96 0.019 252.878);
/* semantic — 唯一允许的彩色例外(状态语义),已降饱和融入中性界面 */
--green: #2e7d5b;
--green-tint: #e9f3ee;
--orange: #b45309;
--orange-tint: #faf1e3;
--red: #b91c1c;
--red-tint: #fbecec;
/* semantic */
--green: oklch(0.603 0.155 150.883);
--green-tint: oklch(0.958 0.017 159.118);
--orange: oklch(0.689 0.179 49.902);
--orange-tint: oklch(0.964 0.021 67.581);
--red: oklch(0.621 0.192 23.042);
--red-tint: oklch(0.956 0.017 17.462);
/* chart overlays */
--tooltip-bg: #2a2a2e;
--tooltip-fg: #f7f7f8;
--tooltip-muted: #a5a5af;
--tooltip-border: #454549;
/* chart overlays — neutral system colors with enough contrast on either theme */
--tooltip-bg: oklch(0.272 0.008 264.435);
--tooltip-fg: oklch(0.976 0.002 247.839);
--tooltip-muted: oklch(0.731 0.008 260.731);
--tooltip-border: oklch(0.356 0.007 264.474);
/* shadows — 平面优先:hairline 分层;仅浮层保留一层中性软阴影(文档化例外) */
/* shadows — solid hairline ring + shadow-plugin's smooth layered stacks
* (--shadow-xs…lg come from shadow-plugin/unprefixed) so every surface reads
* consistently across the app. */
--shadow-hairline: 0 0 0 1px var(--line);
--shadow-btn: 0 0 0 1px var(--line-strong);
--shadow-card: 0 0 0 1px var(--line);
--shadow-raised: 0 0 0 1px var(--line-strong);
--shadow-overlay: 0 0 0 1px var(--line-strong), 0 12px 32px rgba(13, 13, 13, 0.08);
--shadow-inset-field: inset 0 0 0 1px var(--line-strong);
--shadow-btn: 0 0 0 1px var(--line-strong), var(--shadow-xs);
--shadow-card: 0 0 0 1px var(--line), var(--shadow-sm);
--shadow-raised: 0 0 0 1px var(--line), var(--shadow-md);
--shadow-overlay: 0 0 0 1px var(--line), var(--shadow-lg);
--shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12);
}
.dark {
--page: #212121;
--canvas: #212121;
--surface: #2f2f2f;
--inset: #181818;
--hover: #2f2f2f;
--hover-2: #424242;
--page: oklch(0.209 0.004 264.477);
--canvas: oklch(0.231 0.004 264.487);
--surface: oklch(0.26 0.006 271.191);
--inset: oklch(0.243 0.004 264.492);
--hover: oklch(0.289 0.006 271.22);
--hover-2: oklch(0.318 0.007 274.747);
--ink: #ececec;
--ink-2: #b4b4b4;
--ink-3: #8e8ea0;
--ink: oklch(0.964 0.002 247.839);
--ink-2: oklch(0.731 0.008 260.731);
--ink-3: oklch(0.541 0.01 264.484);
--line: #383838;
--line-strong: #4a4a4a;
--line-soft: #2a2a2a;
--field: #2f2f2f;
--stripe: rgba(255, 255, 255, 0.05);
--stripe-bg: #262626;
--line: oklch(0.308 0.006 258.354);
--line-strong: oklch(0.356 0.007 264.474);
--line-soft: oklch(0.278 0.006 258.354);
--field: oklch(0.293 0.006 271.223);
--stripe: oklch(1 0 0 / 0.055);
--stripe-bg: oklch(0.226 0.004 264.485);
--accent: #a3a3b5;
--accent-ink: #bcbccc;
--accent-tint: rgba(142, 142, 160, 0.18);
--accent: oklch(0.68 0.173 253.301);
--accent-ink: oklch(0.788 0.113 248.33);
--accent-tint: oklch(0.68 0.173 253.301 / 0.16);
--green: #6fbf95;
--green-tint: rgba(111, 191, 149, 0.15);
--orange: #d19a55;
--orange-tint: rgba(209, 154, 85, 0.15);
--red: #e07a70;
--red-tint: rgba(224, 122, 112, 0.15);
--green: oklch(0.705 0.154 153.814);
--green-tint: oklch(0.705 0.154 153.814 / 0.14);
--orange: oklch(0.746 0.156 55.642);
--orange-tint: oklch(0.746 0.156 55.642 / 0.14);
--red: oklch(0.666 0.18 21.433);
--red-tint: oklch(0.666 0.18 21.433 / 0.14);
--tooltip-bg: #171717;
--tooltip-fg: #ececec;
--tooltip-muted: #8e8ea0;
--tooltip-border: #383838;
--tooltip-bg: oklch(0.182 0.004 264.459);
--tooltip-fg: oklch(0.964 0.002 247.839);
--tooltip-muted: oklch(0.731 0.008 260.731);
--tooltip-border: oklch(0.308 0.006 258.354);
--shadow-hairline: 0 0 0 1px var(--line);
--shadow-btn: 0 0 0 1px var(--line-strong);
--shadow-card: 0 0 0 1px var(--line);
--shadow-raised: 0 0 0 1px var(--line-strong);
--shadow-overlay: 0 0 0 1px var(--line-strong), 0 16px 40px rgba(0, 0, 0, 0.45);
--shadow-inset-field: inset 0 0 0 1px var(--line-strong);
/* Elevation rings go to a subtle *light* hairline in dark mode: the ring
* paints over the surface behind the element, so a low-alpha white edge
* dissolves into the shadow as one continuous stroke (per smooth-shadow-ring),
* graduated by how far the surface floats. */
--shadow-btn: 0 0 0 1px oklch(1 0 0 / 0.1), 0 1px 2px oklch(0 0 0 / 0.3);
--shadow-card:
0 0 0 1px oklch(1 0 0 / 0.11), 0 1px 2px oklch(0 0 0 / 0.2),
0 2px 6px oklch(0 0 0 / 0.2);
--shadow-raised:
0 0 0 1px oklch(1 0 0 / 0.13), 0 2px 10px oklch(0 0 0 / 0.22);
--shadow-overlay:
0 0 0 1px oklch(1 0 0 / 0.15), 0 8px 28px oklch(0 0 0 / 0.34);
--shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4);
}
@theme inline {
@@ -146,20 +158,20 @@
/* type 沿用 globals.css 中的 --font-sans / --font-mono,不覆盖 */
/* radii — 规范统一 5px;rounded-full 仅限 icon 圆钮/状态点/头像 */
--radius-chip: 5px;
--radius-control: 5px;
--radius-card: 5px;
--radius-window: 5px;
/* radii — chips 6, controls 8, cards 10, windows 14, pills 999 */
--radius-chip: 6px;
--radius-control: 8px;
--radius-card: 10px;
--radius-window: 14px;
/* easing — 规范统一 ease;保留旧变量名以兼容 primitives */
--ease-out-strong: ease;
--ease-in-out-strong: ease;
--ease-link: ease;
/* easing — strong curves; built-ins are too weak */
--ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out-strong: cubic-bezier(0.77, 0, 0.175, 1);
--ease-link: cubic-bezier(0.16, 1, 0.3, 1);
/* motion — Tailwind transition-* 工具类的全局默认:400ms + ease */
--default-transition-duration: 400ms;
--default-transition-timing-function: ease;
/* motion — pill 控件/悬停反馈的全局默认:150ms ease-out(原版手感) */
--default-transition-duration: 150ms;
--default-transition-timing-function: ease-out;
}
/* ── base ─────────────────────────────────────────────── */
@@ -170,6 +182,43 @@ html {
text-rendering: optimizeLegibility;
}
body {
color: var(--ink);
font-feature-settings: "cv11", "ss01";
letter-spacing: -0.01em;
}
::selection {
background: var(--accent-tint);
color: var(--ink);
}
/* headings balance; descriptions avoid orphans */
h1,
h2,
h3,
h4 {
text-wrap: balance;
}
p {
text-wrap: pretty;
}
/* underlines clear descenders */
a {
text-underline-position: from-font;
text-decoration-skip-ink: auto;
}
/* theme switches swap every token at once — freeze transitions so the flip
* is one clean repaint instead of hundreds of mismatched color fades */
.theme-switching *,
.theme-switching *::before,
.theme-switching *::after {
transition: none !important;
}
/* Tailwind v4 resets buttons to cursor:default — anything
* clickable must invite the click */
button:not(:disabled),
@@ -207,7 +256,7 @@ select:focus-visible,
background: currentColor;
transform: scaleX(0);
transform-origin: left center;
transition: transform var(--duration-base) var(--ease-standard);
transition: transform 280ms var(--ease-link);
}
a:hover .animated-underline::after,
@@ -216,23 +265,24 @@ a:focus-visible .animated-underline::after {
}
/* ── primitive spacing system ─────────────────────────────
* 8px 网格上的共享间距。
* Shared optical offsets for small cards. Use these
* instead of hand-tuning 10/12/14px per component.
*/
.primitive-card-pad {
padding: 8px;
padding: 12px;
}
.primitive-card-bar {
padding: 8px;
padding: 10px 12px;
}
.primitive-card-footer {
padding: 8px;
padding: 10px;
}
.primitive-table-cell {
padding: 8px;
padding: 10px 12px;
}
.primitive-icon-button {
@@ -244,83 +294,6 @@ a:focus-visible .animated-underline::after {
border-radius: var(--radius-control);
}
/* ── sidebar nav(beautifului.dev Sidebar Nav,官方组件级 CSS)──
* 收起态靠行宽切换保证图标对齐:.sidebar-row 固定 208px,
* data-sidebar-collapsed 时收为 36px(size-5 图标 + px-2 ×2),
* 恰好落在 52px 轨道正中;glide 高亮层同步收窄。
* --sidebar-copy-duration / --sidebar-copy-offset / --sidebar-easing
* 由 <aside data-sidebar-collapsed> 上注入。
*/
.sidebar-row {
width: 208px;
transition-duration: var(--sidebar-copy-duration, 180ms);
transition-timing-function: var(--sidebar-easing, ease);
}
.sidebar-copy {
opacity: 1;
transform: translateX(0);
transition:
opacity var(--sidebar-copy-duration, 180ms) ease-out,
transform var(--sidebar-copy-duration, 180ms) var(--sidebar-easing, ease);
}
.sidebar-logo,
.sidebar-collapse-control,
.sidebar-expand-control {
transition-duration: var(--sidebar-copy-duration, 180ms);
transition-timing-function: ease-out;
}
.sidebar-expand-control {
pointer-events: none;
opacity: 0;
}
.sidebar-glide-highlight {
right: 8px;
left: 8px;
}
[data-sidebar-collapsed='true'] .sidebar-row {
width: 36px;
}
[data-sidebar-collapsed='true'] .sidebar-copy {
pointer-events: none;
opacity: 0;
transform: translateX(calc(var(--sidebar-copy-offset, 8px) * -1));
}
[data-sidebar-collapsed='true'] .sidebar-workspace-control {
pointer-events: none;
}
[data-sidebar-collapsed='true'] .sidebar-logo,
[data-sidebar-collapsed='true'] .sidebar-collapse-control {
pointer-events: none;
opacity: 0;
}
[data-sidebar-collapsed='true'] .sidebar-expand-control {
pointer-events: auto;
opacity: 1;
}
[data-sidebar-collapsed='true'] .sidebar-glide-highlight {
right: auto;
width: 36px;
}
/* 折叠态专属元素(会话切换入口):展开时彻底不占位,折叠时以 flex 显示 */
.sidebar-collapsed-only {
display: none;
}
[data-sidebar-collapsed='true'] .sidebar-collapsed-only {
display: flex;
}
/* ── shared keyframes ─────────────────────────────────── */
@keyframes shimmer-text {
@@ -391,3 +364,61 @@ a:focus-visible .animated-underline::after {
transition-duration: 0.01ms !important;
}
}
/* ── 项目机制类:侧栏折叠(对应 SidebarNav 原版 CSS,含应用内专用入口) ── */
.sidebar-logo,
.sidebar-collapse-control,
.sidebar-expand-control {
transition-duration: var(--sidebar-copy-duration, 180ms);
transition-timing-function: ease-out;
}
.sidebar-expand-control {
pointer-events: none;
opacity: 0;
}
.sidebar-glide-highlight {
right: 8px;
left: 8px;
}
[data-sidebar-collapsed='true'] .sidebar-row {
width: 36px;
}
[data-sidebar-collapsed='true'] .sidebar-copy {
pointer-events: none;
opacity: 0;
transform: translateX(calc(var(--sidebar-copy-offset, 8px) * -1));
}
[data-sidebar-collapsed='true'] .sidebar-workspace-control {
pointer-events: none;
}
[data-sidebar-collapsed='true'] .sidebar-logo,
[data-sidebar-collapsed='true'] .sidebar-collapse-control {
pointer-events: none;
opacity: 0;
}
[data-sidebar-collapsed='true'] .sidebar-expand-control {
pointer-events: auto;
opacity: 1;
}
[data-sidebar-collapsed='true'] .sidebar-glide-highlight {
right: auto;
width: 36px;
}
/* 折叠态专属元素(会话切换入口):展开时彻底不占位,折叠时以 flex 显示 */
.sidebar-collapsed-only {
display: none;
}
[data-sidebar-collapsed='true'] .sidebar-collapsed-only {
display: flex;
}
@@ -15,7 +15,7 @@ body,
* {
scrollbar-width: thin;
scrollbar-color: rgba(142, 142, 160, 0.35) transparent;
scrollbar-color: color-mix(in srgb, var(--ink-3) 45%, transparent) transparent;
}
*::-webkit-scrollbar {
@@ -24,21 +24,19 @@ body,
}
*::-webkit-scrollbar-thumb {
background: rgba(142, 142, 160, 0.35);
background: color-mix(in srgb, var(--ink-3) 45%, transparent);
border-radius: 4px;
}
*::-webkit-scrollbar-thumb:hover {
background: rgba(142, 142, 160, 0.55);
background: color-mix(in srgb, var(--ink-3) 70%, transparent);
}
*::-webkit-scrollbar-track {
background: transparent;
}
::selection {
background: rgba(142, 142, 160, 0.28);
}
/* ::selection 在 beautifului.css 基座(accent-tint + ink) */
/* 流式输出光标 */
@keyframes caret-blink {