Merge branch 'feat/2.6.0-beta4' into feat/2.6.0

This commit is contained in:
GuoQing Zhang
2026-07-16 19:36:43 +08:00
57 changed files with 14759 additions and 1091 deletions
@@ -1,4 +0,0 @@
{
"baselineCommitHash": "6cad6e6dec0fb94234d4b446cd702f58aecc0e18",
"lastPushedCommitHash": "78a5c55f421222c2f98970a1bf0fe36d1915ec00"
}
-10
View File
@@ -1,10 +0,0 @@
{
"projects": [
{
"id": "96f41709-87dd-4d3e-8ad1-07610ce2c830",
"name": "bisheng",
"role": "viewer",
"joinedAt": "2026-07-06T04:09:17.923Z"
}
]
}
-8
View File
@@ -1,8 +0,0 @@
# 补充排除:配置与本地敏感文件(会话通道管不到,这里只保文件通道)
config.yaml
config.*.yaml
*.local.yaml
*.local.yml
.env.local
*.sqlite
*.log
+4
View File
@@ -294,3 +294,7 @@ src/backend/scripts/migrate_dm.yaml
# UI component refactor working docs (local handoff notes; never commit / ship)
docs-ui-refactor/
# CoAligne local state + sync config (per-developer; the tool rewrites them itself)
.coaligne/
.coaligneignore
@@ -0,0 +1,45 @@
"""F046: widen knowledge-file names from 200 to 500 characters.
The ``knowledgefile.file_name`` column stores both uploaded file names and
knowledge-space folder names. Widen it together with the SQLModel constraint so
names accepted by the application can be persisted on existing installations.
Revision ID: f046_knowledge_file_name_length
Revises: f045_dks_is_hidden
Create Date: 2026-07-16
"""
from collections.abc import Sequence
from typing import Union
import sqlalchemy as sa
from alembic import op
from bisheng.core.database.alembic_helpers.online import column_exists
revision: str = "f046_knowledge_file_name_length"
down_revision: Union[str, Sequence[str], None] = "f045_dks_is_hidden"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
if column_exists("knowledgefile", "file_name"):
op.alter_column(
"knowledgefile",
"file_name",
existing_type=sa.String(length=200),
type_=sa.String(length=500),
existing_nullable=False,
)
def downgrade() -> None:
if column_exists("knowledgefile", "file_name"):
op.alter_column(
"knowledgefile",
"file_name",
existing_type=sa.String(length=500),
type_=sa.String(length=200),
existing_nullable=False,
)
@@ -13,6 +13,7 @@ from bisheng.core.database import get_async_db_session, get_sync_db_session
from bisheng.core.database.dialect_helpers import UPDATE_TIME_SERVER_DEFAULT, JsonType
from bisheng.database.base import async_get_count, get_count
KNOWLEDGE_FILE_NAME_MAX_LENGTH = 500
KNOWLEDGE_REMARK_MAX_LENGTH = 4096
@@ -77,7 +78,7 @@ class KnowledgeFileBase(SQLModelSerializable):
user_name: str | None = Field(default=None, index=True)
knowledge_id: int = Field(index=True)
thumbnails: str | None = Field(default=None, description="File thumbnails in Stored object name")
file_name: str = Field(max_length=200, index=True)
file_name: str = Field(max_length=KNOWLEDGE_FILE_NAME_MAX_LENGTH, index=True)
file_type: int = Field(default=FileType.FILE.value, description="File type. 0: dir; 1: file")
file_source: str | None = Field(default=FileSource.UPLOAD.value, description="File source")
level: int | None = Field(default=0)
+1 -1
View File
@@ -290,7 +290,7 @@ CREATE TABLE IF NOT EXISTS knowledgefile (
user_name VARCHAR(255),
knowledge_id INTEGER NOT NULL,
thumbnails VARCHAR(512),
file_name VARCHAR(200) NOT NULL,
file_name VARCHAR(500) NOT NULL,
file_type INTEGER DEFAULT 1,
file_source VARCHAR(32),
level INTEGER DEFAULT 0,
@@ -0,0 +1,25 @@
import pytest
from pydantic import ValidationError
from bisheng.knowledge.domain.models.knowledge_file import (
KNOWLEDGE_FILE_NAME_MAX_LENGTH,
KnowledgeFile,
KnowledgeFileCreate,
)
def test_knowledge_file_name_allows_500_characters() -> None:
file_name = "a" * KNOWLEDGE_FILE_NAME_MAX_LENGTH
knowledge_file = KnowledgeFileCreate(knowledge_id=1, file_name=file_name)
assert knowledge_file.file_name == file_name
assert KnowledgeFile.__table__.c.file_name.type.length == KNOWLEDGE_FILE_NAME_MAX_LENGTH
def test_knowledge_file_name_rejects_more_than_500_characters() -> None:
with pytest.raises(ValidationError, match="String should have at most 500 characters"):
KnowledgeFileCreate(
knowledge_id=1,
file_name="a" * (KNOWLEDGE_FILE_NAME_MAX_LENGTH + 1),
)
+3
View File
@@ -1 +1,4 @@
.DS_Store
# rspress docs-site build output (`npm run build:docs`)
doc_build/
+7 -5
View File
@@ -23,12 +23,14 @@
`.theme-green` 覆盖成**绿色**
```
--brand-50:228 241 236(#E4F1EC) --brand-100:204 228 218(#CCE4DA) --brand-200:163 210 192(#A3D2C0)
--brand-300:111 186 160(#6FBAA0) --brand-400:61 155 120(#3D9B78) --brand-500:24 124 84(#187C54)
--brand-600:19 99 69(#136345) --brand-700:15 77 54 --brand-800:10 56 38 --brand-900:6 38 25
--brand-main:24 124 84(#187C54)
--brand-50:228 241 231(#E4F1E7) --brand-100:204 228 210(#CCE4D2) --brand-200:163 210 176(#A3D2B0)
--brand-300:111 186 133(#6FBA85) --brand-400:61 155 92(#3D9B5C) --brand-500:22 156 71(#169C47 主, 2026-06-29 was #187C54)
--brand-600:9 139 53(#098B35) --brand-700:7 105 41(#076929) --brand-800:7 78 32(#074E20) --brand-900:6 50 22(#063216)
--brand-main:22 156 71(#169C47)
/* 另外覆盖: */
--primary:154 67% 29%; --surface-active-alt:rgb(228 241 236); --surface-primary-alt:#f4faf7; --search-input:hsla(152,40%,99%,1);
--primary:142 75% 35%; --surface-active-alt:rgb(228 241 231); --surface-primary-alt:#f4faf7; --search-input:hsla(152,40%,99%,1);
/* 绿阶 2026-07-15 以固定主色 #169C47 为锚重新推导(旧阶色相偏青 ~157°、500 单独跳档):
浅档沿用旧阶明度/饱和度、仅旋色相到主色家族;深档镜像蓝阶深端 HSV 几何。 */
```
(蓝色 `--primary` 默认是 `222 100% 54%`,在 `:root`/`html` 块里。)
+11404 -194
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -6,6 +6,8 @@
"scripts": {
"check-imports": "node scripts/check-case-sensitive-imports.mjs",
"dev": "cross-env NODE_ENV=development vite",
"dev:docs": "rspress dev",
"build:docs": "rspress build",
"start": "cross-env NODE_ENV=development vite",
"build": "cross-env NODE_ENV=production vite build",
"build:vconsole": "cross-env NODE_ENV=production VITE_ENABLE_VCONSOLE=true vite build",
@@ -127,6 +129,7 @@
"@babel/preset-env": "^7.22.15",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.22.15",
"@rspress/plugin-preview": "^1.47.2",
"@tanstack/react-query-devtools": "^4.29.0",
"@testing-library/dom": "^9.3.0",
"@testing-library/jest-dom": "^5.16.5",
@@ -152,6 +155,7 @@
"jest-junit": "^16.0.0",
"postcss": "^8.4.31",
"rollup-plugin-visualizer": "^6.0.5",
"rspress": "^1.47.1",
"tailwindcss": "^3.4.1",
"ts-jest": "^29.2.5",
"typescript": "^5.3.3",
+121
View File
@@ -0,0 +1,121 @@
import * as path from 'path';
import { defineConfig } from 'rspress/config';
import { pluginPreview } from '@rspress/plugin-preview';
/**
* Component-library docs site (rspress).
*
* - Docs root is the gitignored `docs-ui-refactor/` at the repo root (design specs).
* - Real ui components are imported via the `~` / `@` alias (mirrors vite.config).
* - @rspress/plugin-preview renders live component demos inside markdown.
* - i18n and custom theme intentionally NOT enabled yet.
*
* Run: `npm run dev:docs` (cwd: src/frontend/client).
*/
const clientSrc = path.join(__dirname, 'src');
export default defineConfig({
// req 2: docs root = bisheng/docs-ui-refactor
root: path.join(__dirname, '../../../docs-ui-refactor'),
title: 'BiSheng 组件库',
description: 'BiSheng client 设计规范 + 组件库',
lang: 'zh', // single language — i18n intentionally not enabled (req 5)
// Point straight at the app's stylesheet (tailwind directives + all design
// tokens). A wrapper css with `@import` breaks rspack's cssExtractLoader.
globalStyles: path.join(clientSrc, 'style.css'),
// req 1: live component preview
plugins: [pluginPreview({ previewMode: 'internal' })],
themeConfig: {
// req 6: two top-level sections — 文档 (specs) / 组件 (demos)
// NOTE: the demos dir is ASCII (`components/`) on purpose — rspress v1's
// client router fails to match nested routes with non-ASCII dir names.
nav: [
{ text: '文档', link: '/00-总纲' },
{ text: '组件', link: '/components/button' },
],
sidebar: {
// 组件 section — component demos
'/components/': [
{ text: '组件总览', link: '/components/index' },
{ text: 'Button 按钮', link: '/components/button' },
],
// 文档 section — the existing design-spec markdown (kept flat, not moved)
'/': [
{
text: '设计规范',
items: [
{ text: '总纲', link: '/00-总纲' },
{ text: '字体 Typography', link: '/基础-字体规范' },
{ text: '色彩 Color', link: '/基础-色彩规范' },
{ text: '多端适配', link: '/基础-多端适配原则' },
{ text: '图标 Icon', link: '/基础-图标规范' },
{ text: '插画 Illustration', link: '/基础-插画规范' },
{ text: '滚动条', link: '/基础-滚动条规范' },
],
},
{
text: '组件规范',
items: [
{ text: 'Button 按钮', link: '/组件-Button按钮' },
{ text: 'Modal 弹窗', link: '/组件-Modal弹窗' },
],
},
],
},
},
builderConfig: {
source: {
define: {
// vite injects these globals (vite.config define); app code reached via
// the `~/utils` barrel reads them at module scope — must exist here too.
__APP_ENV__: JSON.stringify({ BASE_URL: '/workspace', BISHENG_HOST: '/admin' }),
__VCONSOLE_ENABLED__: JSON.stringify(false),
// vite exposes import.meta.env; rspack leaves it undefined. App code reads
// DEV/BASE_URL/MODE/VITE_* at module scope, so give it a concrete object.
'import.meta.env': JSON.stringify({
DEV: true,
PROD: false,
MODE: 'development',
BASE_URL: '/',
}),
},
},
resolve: {
// mirror vite.config aliases so ui components resolve
alias: {
'~': clientSrc,
'@': clientSrc,
$fonts: path.join(__dirname, 'public/fonts'),
// `import { URL } from 'url'` in api code must not resolve to the npm
// `url` package (no URL named export) — shim with the browser global.
url: path.join(__dirname, 'stubs/url-stub.ts'),
},
},
tools: {
cssLoader: {
url: {
// Leave root-relative (/workspace/...) and $fonts urls untouched —
// they are app public paths; the docs site uses the system font stack.
filter: (url: string) => !url.startsWith('/') && !url.startsWith('$fonts'),
},
},
rspack: {
resolve: {
// Some ui components reach the `~/utils` barrel, which transitively
// imports api modules using Node builtins. Demos never execute those
// paths at runtime; stub them out so the browser bundle compiles.
fallback: {
crypto: false,
url: false,
fs: false,
path: false,
stream: false,
},
},
},
},
},
});
@@ -367,7 +367,7 @@ export function ChannelMemberDialog({
</div>
) : members.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center py-8 text-center">
<EmptyStateIllustration className="size-[120px] mb-4 opacity-90" />
<EmptyStateIllustration className="size-[120px] mb-4" />
<p className="text-[14px] font-normal text-[#999999]">
{localize("com_subscription.nofound_mathcing_member")}
</p>
@@ -328,7 +328,7 @@ export function ChannelMemberManagementPanel({
</div>
) : members.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center py-8 text-center">
<EmptyStateIllustration className="mb-4 size-[120px] opacity-90" />
<EmptyStateIllustration className="mb-4 size-[120px]" />
<p className="text-[14px] font-normal text-[#999999]">
{localize("com_subscription.nofound_mathcing_member")}
</p>
@@ -437,7 +437,7 @@ const AiChatInput = memo(
<div className="flex h-7 min-h-7 w-full min-w-0 items-center justify-between gap-1 touch-mobile:gap-0.5">
{/* Toolbarflex-1 + overflow-hidden,避免与右侧语音/发送横向重叠 */}
<div ref={toolbarRef} className="input-bottom-left flex min-w-0 flex-1 items-center gap-1 touch-mobile:-ml-1 touch-mobile:gap-1 touch-mobile:pl-0 overflow-hidden">
<div ref={toolbarRef} className="input-bottom-left flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
{/* "+" menu — v2.5: combines file upload + knowledge space +
org knowledge base. Renders in place of ChatKnowledge when
agent mode is active (which is the v2.5 default). */}
@@ -59,7 +59,7 @@ export function WorkspaceDrawer({ open, onOpenChange, files, onPreview }: Worksp
{renderGroup('com_linsight_workspace_generated', generated)}
{!files.length && (
<div className="flex flex-col items-center justify-center py-10 text-center">
<EmptyStateIllustration className="mb-4 size-[120px] opacity-90" />
<EmptyStateIllustration className="mb-4 size-[120px]" />
<p className="text-[14px] font-normal text-[#999999]">{localize('com_linsight_workspace_empty')}</p>
</div>
)}
@@ -137,7 +137,7 @@ export function WorkspacePanel({
<div className="flex flex-col gap-2">{files.map(renderRow)}</div>
) : (
<div className="flex h-full flex-col items-center justify-center text-center">
<EmptyStateIllustration className="mb-4 size-[120px] opacity-90" />
<EmptyStateIllustration className="mb-4 size-[120px]" />
<p className="text-[14px] font-normal text-[#999999]">{localize('com_linsight_workspace_empty')}</p>
</div>
)}
@@ -47,14 +47,14 @@ export function TaskModeToggle({ active, disabled = false, onClick, compact = fa
className={cn(
// primary-100 is not a theme token; use a light primary tint to match.
'flex h-8 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md px-2 text-[13px] font-normal transition-colors outline-none hover:bg-blue-100',
active ? 'text-blue-600' : 'text-[#4E5969]',
active ? 'text-blue-500' : 'text-[#4E5969]',
disabled && 'cursor-not-allowed opacity-50',
)}
>
{showExit ? (
<X size={16} className="text-blue-600" />
<X size={16} className="text-blue-500" />
) : (
<Outlined.Binoculars size={16} className={active ? 'text-blue-600' : 'text-[#4E5969]'} />
<Outlined.Binoculars size={16} className={active ? 'text-blue-500' : 'text-[#4E5969]'} />
)}
{/* Compact: collapse to icon only to save horizontal space in the
input toolbar, matching the knowledge/tools selectors. */}
@@ -22,9 +22,12 @@ export const FolderIcon = ({ className, ...props }: React.SVGProps<SVGSVGElement
const panelD =
"M7.66309 8.68262H16.3369C17.1507 8.68262 17.7667 8.67925 18.2568 8.71289C18.7465 8.74651 19.1052 8.81657 19.4062 8.95801C19.9422 9.20996 20.3723 9.60675 20.6445 10.0996C20.7969 10.3757 20.8727 10.7042 20.9092 11.1543C20.9457 11.6054 20.9424 12.1727 20.9424 12.9238V15.5762C20.9424 16.3273 20.9457 16.8946 20.9092 17.3457C20.8727 17.7958 20.7969 18.1243 20.6445 18.4004C20.3723 18.8933 19.9422 19.29 19.4062 19.542C19.1052 19.6834 18.7465 19.7535 18.2568 19.7871C17.7667 19.8208 17.1507 19.8174 16.3369 19.8174H7.66309C6.84925 19.8174 6.23335 19.8208 5.74316 19.7871C5.25352 19.7535 4.89477 19.6834 4.59375 19.542C4.05779 19.29 3.62766 18.8933 3.35547 18.4004C3.20306 18.1243 3.12725 17.7958 3.09082 17.3457C3.05434 16.8946 3.05762 16.3273 3.05762 15.5762V12.9238C3.05762 12.1727 3.05434 11.6054 3.09082 11.1543C3.12725 10.7042 3.20306 10.3757 3.35547 10.0996C3.62766 9.60675 4.05779 9.20996 4.59375 8.95801C4.89477 8.81657 5.25352 8.74651 5.74316 8.71289C6.23335 8.67925 6.84925 8.68262 7.66309 8.68262Z";
const lightStop = { stopColor: 'rgb(var(--brand-300))' } as React.CSSProperties;
const deepStop = { stopColor: 'rgb(var(--brand-600))' } as React.CSSProperties;
const bodyFill = { fill: 'rgb(var(--brand-400))' } as React.CSSProperties;
// Use the illustration palette so the folder body matches the empty-state
// illustrations' main green (green mode illus-500 = #169C47) instead of the
// darker UI brand green. Still follows the blue ⇄ green theme.
const lightStop = { stopColor: 'rgb(var(--illus-300))' } as React.CSSProperties;
const deepStop = { stopColor: 'rgb(var(--illus-500))' } as React.CSSProperties;
const bodyFill = { fill: 'rgb(var(--illus-500))' } as React.CSSProperties;
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} {...props}>
@@ -20,8 +20,8 @@ export const CrawlingIllustration = ({ className, grey, ...props }: React.SVGPro
const maskId = `crawling-mask-${uid}`;
const fill100 = { fill: 'rgb(var(--illus-100))' } as React.CSSProperties;
const fill500 = { fill: 'rgb(var(--illus-500))' } as React.CSSProperties;
const fill300 = { fill: 'rgb(var(--illus-300))' } as React.CSSProperties;
const fill500 = { fill: 'rgb(var(--illus-500))' } as React.CSSProperties;
const lens = { fill: 'rgb(var(--illus-500))', stroke: 'rgb(var(--illus-500))' } as React.CSSProperties;
return (
@@ -116,9 +116,10 @@ const BLUE_INNER = `<g clip-path="url(#clip0___UID__)">
<image id="image1___UID__" width="1990" height="1110" preserveAspectRatio="none" xlink:href="__IMG1__"/>
</defs>`;
const GREEN_INNER = `<g clip-path="url(#clip0___UID__)">
const GREEN_INNER = `<g opacity="0.9">
<g clip-path="url(#clip0___UID__)">
<rect width="48" height="48" rx="6" fill="white"/>
<rect width="48" height="48" rx="6" fill="url(#paint0_radial___UID__)" fill-opacity="0.4"/>
<rect width="48" height="48" rx="6" fill="#3FAA66"/>
<rect width="48" height="48" rx="6" fill="white" fill-opacity="0.5"/>
<path d="M12.3999 11.4C12.3999 10.0745 13.4744 9 14.7999 9H41.9999C43.3254 9 44.3999 10.0745 44.3999 11.4V38.6C44.3999 39.9255 43.3254 41 41.9999 41H14.7999C13.4744 41 12.3999 39.9255 12.3999 38.6V11.4Z" fill="white"/>
<g clip-path="url(#clip1___UID__)">
@@ -137,11 +138,11 @@ const GREEN_INNER = `<g clip-path="url(#clip0___UID__)">
<g filter="url(#filter1_d___UID__)">
<path d="M4.3999 11.4C4.3999 10.0745 5.47442 9 6.7999 9H33.9999C35.3254 9 36.3999 10.0745 36.3999 11.4V38.6C36.3999 39.9255 35.3254 41 33.9999 41H6.7999C5.47442 41 4.3999 39.9255 4.3999 38.6V11.4Z" fill="white" shape-rendering="crispEdges"/>
<g opacity="0.3">
<path opacity="0.3" d="M24.1698 19.3862C23.7051 19.3858 23.2596 19.201 22.931 18.8724C22.6024 18.5438 22.4176 18.0982 22.4171 17.6335V15.2H14.5852C13.9786 15.2 13.3968 15.441 12.9678 15.8699C12.5388 16.2989 12.2979 16.8807 12.2979 17.4873V31.276C12.2979 31.8827 12.5388 32.4645 12.9678 32.8934C13.3968 33.3224 13.9786 33.5634 14.5852 33.5634H23.9862C24.5928 33.5634 25.1746 33.3224 25.6036 32.8934C26.0326 32.4645 26.2736 31.8827 26.2736 31.276V19.3862H24.1698Z" fill="#00C650"/>
<path opacity="0.3" d="M24.1698 19.3862C23.7051 19.3858 23.2596 19.201 22.931 18.8724C22.6024 18.5438 22.4176 18.0982 22.4171 17.6335V15.2H14.5852C13.9786 15.2 13.3968 15.441 12.9678 15.87C12.5388 16.2989 12.2979 16.8807 12.2979 17.4873V31.276C12.2979 31.8827 12.5388 32.4645 12.9678 32.8934C13.3968 33.3224 13.9786 33.5634 14.5852 33.5634H23.9862C24.5928 33.5634 25.1746 33.3224 25.6036 32.8934C26.0326 32.4645 26.2736 31.8827 26.2736 31.276V19.3862H24.1698Z" fill="#00C650"/>
</g>
<path d="M26.2744 19.3862H24.1707C23.706 19.3858 23.2604 19.201 22.9318 18.8724C22.6032 18.5438 22.4184 18.0982 22.418 17.6335V15.2L26.2744 19.3862Z" fill="#00C650"/>
<path d="M27.9315 29.735H17.3031C16.5492 29.735 15.938 30.3461 15.938 31.1001V34.2689C15.938 35.0228 16.5492 35.634 17.3031 35.634H27.9315C28.6854 35.634 29.2966 35.0228 29.2966 34.2689V31.1001C29.2966 30.3461 28.6854 29.735 27.9315 29.735Z" fill="#00C650"/>
<path d="M25.7471 32.277C25.7359 32.1716 25.6884 32.0895 25.6047 32.0307C25.5218 31.9719 25.4139 31.9426 25.2811 31.9426C25.1877 31.9426 25.1076 31.9567 25.0408 31.9848C24.974 32.013 24.9229 32.0512 24.8875 32.0995C24.8521 32.1478 24.8339 32.203 24.8331 32.2649C24.8331 32.3165 24.8448 32.3611 24.8682 32.399C24.8923 32.4368 24.9249 32.469 24.966 32.4955C25.007 32.5213 25.0525 32.543 25.1024 32.5607C25.1523 32.5785 25.2026 32.5933 25.2533 32.6054L25.4851 32.6634C25.5785 32.6851 25.6682 32.7145 25.7544 32.7515C25.8413 32.7885 25.919 32.8352 25.9874 32.8916C26.0566 32.9479 26.1114 33.0159 26.1516 33.0956C26.1919 33.1753 26.212 33.2687 26.212 33.3757C26.212 33.5206 26.1749 33.6482 26.1009 33.7585C26.0268 33.8679 25.9198 33.9537 25.7797 34.0156C25.6405 34.0768 25.4718 34.1074 25.2738 34.1074C25.0815 34.1074 24.9144 34.0776 24.7728 34.0181C24.6319 33.9585 24.5216 33.8716 24.4419 33.7573C24.3631 33.643 24.3204 33.5037 24.314 33.3395H24.7547C24.7611 33.4256 24.7877 33.4973 24.8343 33.5544C24.881 33.6116 24.9418 33.6542 25.0167 33.6824C25.0923 33.7106 25.1768 33.7247 25.2702 33.7247C25.3676 33.7247 25.4529 33.7102 25.5262 33.6812C25.6002 33.6514 25.6582 33.6104 25.7 33.558C25.7419 33.5049 25.7632 33.4429 25.764 33.3721C25.7632 33.3077 25.7443 33.2546 25.7073 33.2127C25.6703 33.1701 25.6183 33.1347 25.5515 33.1065C25.4855 33.0775 25.4083 33.0518 25.3197 33.0292L25.0384 32.9568C24.8348 32.9044 24.6738 32.8252 24.5554 32.7189C24.4379 32.6119 24.3792 32.4698 24.3792 32.2927C24.3792 32.147 24.4186 32.0194 24.4975 31.91C24.5772 31.8005 24.6854 31.7156 24.8223 31.6552C24.9591 31.594 25.1141 31.5634 25.2871 31.5634C25.4626 31.5634 25.6163 31.594 25.7483 31.6552C25.8812 31.7156 25.9854 31.7997 26.0611 31.9076C26.1367 32.0146 26.1758 32.1378 26.1782 32.277H25.7471Z" fill="white"/>
<path d="M25.7471 32.277C25.7359 32.1716 25.6884 32.0895 25.6047 32.0307C25.5218 31.972 25.4139 31.9426 25.2811 31.9426C25.1877 31.9426 25.1076 31.9567 25.0408 31.9848C24.974 32.013 24.9229 32.0512 24.8875 32.0995C24.8521 32.1478 24.8339 32.203 24.8331 32.265C24.8331 32.3165 24.8448 32.3611 24.8682 32.399C24.8923 32.4368 24.9249 32.469 24.966 32.4956C25.007 32.5213 25.0525 32.5431 25.1024 32.5608C25.1523 32.5785 25.2026 32.5934 25.2533 32.6054L25.4851 32.6634C25.5785 32.6851 25.6682 32.7145 25.7544 32.7515C25.8413 32.7886 25.919 32.8352 25.9874 32.8916C26.0566 32.9479 26.1114 33.0159 26.1516 33.0956C26.1919 33.1753 26.212 33.2687 26.212 33.3757C26.212 33.5206 26.1749 33.6482 26.1009 33.7585C26.0268 33.868 25.9198 33.9537 25.7797 34.0157C25.6405 34.0768 25.4718 34.1074 25.2738 34.1074C25.0815 34.1074 24.9144 34.0776 24.7728 34.0181C24.6319 33.9585 24.5216 33.8716 24.4419 33.7573C24.3631 33.643 24.3204 33.5037 24.314 33.3395H24.7547C24.7611 33.4257 24.7877 33.4973 24.8343 33.5544C24.881 33.6116 24.9418 33.6543 25.0167 33.6824C25.0923 33.7106 25.1768 33.7247 25.2702 33.7247C25.3676 33.7247 25.4529 33.7102 25.5262 33.6812C25.6002 33.6514 25.6582 33.6104 25.7 33.5581C25.7419 33.5049 25.7632 33.443 25.764 33.3721C25.7632 33.3077 25.7443 33.2546 25.7073 33.2128C25.6703 33.1701 25.6183 33.1347 25.5515 33.1065C25.4855 33.0775 25.4083 33.0518 25.3197 33.0292L25.0384 32.9568C24.8348 32.9045 24.6738 32.8252 24.5554 32.7189C24.4379 32.6119 24.3792 32.4698 24.3792 32.2927C24.3792 32.147 24.4186 32.0195 24.4975 31.91C24.5772 31.8005 24.6854 31.7156 24.8223 31.6552C24.9591 31.594 25.1141 31.5635 25.2871 31.5635C25.4626 31.5635 25.6163 31.594 25.7483 31.6552C25.8812 31.7156 25.9854 31.7997 26.0611 31.9076C26.1367 32.0146 26.1758 32.1378 26.1782 32.277H25.7471Z" fill="white"/>
<path d="M22.4775 34.07V31.5973H22.9255V33.6945H24.0145V34.07H22.4775Z" fill="white"/>
<path d="M20.5042 31.5973L21.0488 32.5028H21.0681L21.615 31.5973H22.1258L21.3639 32.8336L22.1378 34.07H21.6187L21.0681 33.1705H21.0488L20.4982 34.07H19.9814L20.7626 32.8336L19.9911 31.5973H20.5042Z" fill="white"/>
<path d="M19.3941 34.0966C19.3208 34.0966 19.258 34.0708 19.2057 34.0193C19.1534 33.9678 19.1276 33.905 19.1284 33.8309C19.1276 33.7585 19.1534 33.6965 19.2057 33.645C19.258 33.5935 19.3208 33.5677 19.3941 33.5677C19.4649 33.5677 19.5265 33.5935 19.5788 33.645C19.6319 33.6965 19.6589 33.7585 19.6597 33.8309C19.6589 33.88 19.646 33.9247 19.621 33.9649C19.5969 34.0052 19.5647 34.0374 19.5245 34.0615C19.485 34.0849 19.4416 34.0966 19.3941 34.0966Z" fill="white"/>
@@ -153,8 +154,9 @@ const GREEN_INNER = `<g clip-path="url(#clip0___UID__)">
</g>
<g opacity="0.9" filter="url(#filter2_d___UID__)">
<path d="M24.9517 26.2671C25.4472 26.9251 25.695 27.2541 25.9875 27.5157C26.5215 27.9933 27.174 28.3187 27.8769 28.4579C28.2617 28.5342 28.6736 28.5342 29.4974 28.5342H40C43.7712 28.5342 45.6569 28.5342 46.8284 29.7058C48 30.8773 48 32.7629 48 36.5342V59.3525C48 63.1238 48 65.0094 46.8284 66.181C45.6569 67.3525 43.7712 67.3525 40 67.3525H8C4.22876 67.3525 2.34315 67.3525 1.17157 66.181C0 65.0094 0 63.1238 0 59.3525V32C0 28.2288 0 26.3431 1.17157 25.1716C2.34315 24 4.22876 24 8 24H20.406C21.2297 24 21.6416 24 22.0265 24.0762C22.7293 24.2155 23.3818 24.5409 23.9158 25.0185C24.2083 25.2801 24.4561 25.6091 24.9517 26.2671Z" fill="white"/>
<path d="M24.9517 26.2671C25.4472 26.9251 25.695 27.2541 25.9875 27.5157C26.5215 27.9933 27.174 28.3187 27.8769 28.4579C28.2617 28.5342 28.6736 28.5342 29.4974 28.5342H40C43.7712 28.5342 45.6569 28.5342 46.8284 29.7058C48 30.8773 48 32.7629 48 36.5342V59.3525C48 63.1238 48 65.0094 46.8284 66.181C45.6569 67.3525 43.7712 67.3525 40 67.3525H8C4.22876 67.3525 2.34315 67.3525 1.17157 66.181C0 65.0094 0 63.1238 0 59.3525V32C0 28.2288 0 26.3431 1.17157 25.1716C2.34315 24 4.22876 24 8 24H20.406C21.2297 24 21.6416 24 22.0265 24.0762C22.7293 24.2155 23.3818 24.5409 23.9158 25.0185C24.2083 25.2801 24.4561 25.6091 24.9517 26.2671Z" fill="url(#paint1_radial___UID__)"/>
<path d="M24.9517 26.2671C25.4472 26.9251 25.695 27.2541 25.9875 27.5157C26.5215 27.9933 27.174 28.3187 27.8769 28.4579C28.2617 28.5342 28.6736 28.5342 29.4974 28.5342H40C43.7712 28.5342 45.6569 28.5342 46.8284 29.7058C48 30.8773 48 32.7629 48 36.5342V59.3525C48 63.1238 48 65.0094 46.8284 66.181C45.6569 67.3525 43.7712 67.3525 40 67.3525H8C4.22876 67.3525 2.34315 67.3525 1.17157 66.181C0 65.0094 0 63.1238 0 59.3525V32C0 28.2288 0 26.3431 1.17157 25.1716C2.34315 24 4.22876 24 8 24H20.406C21.2297 24 21.6416 24 22.0265 24.0762C22.7293 24.2155 23.3818 24.5409 23.9158 25.0185C24.2083 25.2801 24.4561 25.6091 24.9517 26.2671Z" fill="white" fill-opacity="0.4"/>
<path d="M24.9517 26.2671C25.4472 26.9251 25.695 27.2541 25.9875 27.5157C26.5215 27.9933 27.174 28.3187 27.8769 28.4579C28.2617 28.5342 28.6736 28.5342 29.4974 28.5342H40C43.7712 28.5342 45.6569 28.5342 46.8284 29.7058C48 30.8773 48 32.7629 48 36.5342V59.3525C48 63.1238 48 65.0094 46.8284 66.181C45.6569 67.3525 43.7712 67.3525 40 67.3525H8C4.22876 67.3525 2.34315 67.3525 1.17157 66.181C0 65.0094 0 63.1238 0 59.3525V32C0 28.2288 0 26.3431 1.17157 25.1716C2.34315 24 4.22876 24 8 24H20.406C21.2297 24 21.6416 24 22.0265 24.0762C22.7293 24.2155 23.3818 24.5409 23.9158 25.0185C24.2083 25.2801 24.4561 25.6091 24.9517 26.2671Z" fill="url(#paint0_radial___UID__)"/>
<path d="M24.9517 26.2671C25.4472 26.9251 25.695 27.2541 25.9875 27.5157C26.5215 27.9933 27.174 28.3187 27.8769 28.4579C28.2617 28.5342 28.6736 28.5342 29.4974 28.5342H40C43.7712 28.5342 45.6569 28.5342 46.8284 29.7058C48 30.8773 48 32.7629 48 36.5342V59.3525C48 63.1238 48 65.0094 46.8284 66.181C45.6569 67.3525 43.7712 67.3525 40 67.3525H8C4.22876 67.3525 2.34315 67.3525 1.17157 66.181C0 65.0094 0 63.1238 0 59.3525V32C0 28.2288 0 26.3431 1.17157 25.1716C2.34315 24 4.22876 24 8 24H20.406C21.2297 24 21.6416 24 22.0265 24.0762C22.7293 24.2155 23.3818 24.5409 23.9158 25.0185C24.2083 25.2801 24.4561 25.6091 24.9517 26.2671Z" fill="#3FAA66"/>
</g>
</g>
</g>
<defs>
@@ -194,13 +196,9 @@ const GREEN_INNER = `<g clip-path="url(#clip0___UID__)">
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow___UID__"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow___UID__" result="shape"/>
</filter>
<radialGradient id="paint0_radial___UID__" cx="0" cy="0" r="1" gradientTransform="matrix(-46.4516 -49.7102 49.6552 -53.1385 48 48.0533)" gradientUnits="userSpaceOnUse">
<stop stop-color="#19B476"/>
<stop offset="1" stop-color="#63F3B9"/>
</radialGradient>
<radialGradient id="paint1_radial___UID__" cx="0" cy="0" r="1" gradientTransform="matrix(-46.4516 -44.8972 49.6552 -47.9936 48 67.4006)" gradientUnits="userSpaceOnUse">
<stop stop-color="#19B476"/>
<stop offset="1" stop-color="#63F3B9"/>
<radialGradient id="paint0_radial___UID__" cx="0" cy="0" r="1" gradientTransform="matrix(-46.4516 -23.1728 26.446 -48.7836 48 45.6763)" gradientUnits="userSpaceOnUse">
<stop stop-color="#00EB56"/>
<stop offset="1" stop-color="#5ED7A7"/>
</radialGradient>
<clipPath id="clip0___UID__">
<rect width="48" height="48" rx="6" fill="white"/>
+313 -30
View File
@@ -1,57 +1,340 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { Outlined } from 'bisheng-icons';
import { cn } from '~/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
/**
* Button — design-system base component (docs-ui-refactor/组件-Button按钮.md).
*
* New API is the antd-style dual axis: `color` (primary/default/danger) ×
* `variant` (solid/outlined/filled/text/link) × `size` (small/medium/large),
* plus `iconOnly` for icon buttons and `shape` (square/circle, circle being
* icon-only). All colors go through semantic
* tokens (`btn-*` in tailwind.config / style.css, brand via `blue-*`); hover
* states are disabled on touch (§5.5) and disabled/loading are uniform (§5.2).
*
* The legacy shadcn API (`variant="outline" | "ghost" | ...`, `size="sm" |
* "icon" | ...`) still works through an automatic mapping (§6.3) so existing
* call sites keep rendering; they will be migrated batch-by-batch and the
* mapping removed afterwards.
*/
type ButtonColor = 'primary' | 'default' | 'danger';
type ButtonVariant = 'solid' | 'outlined' | 'filled' | 'text' | 'link';
type ButtonSize = 'small' | 'medium' | 'large';
type ButtonShape = 'square' | 'circle';
/** @deprecated Legacy single-axis variants — auto-mapped to color×variant (§6.3). */
type LegacyVariant =
| 'default'
| 'destructive'
| 'outline'
| 'secondary'
| 'secondaryBrand'
| 'ghost'
| 'submit';
/** @deprecated Legacy sizes — auto-mapped (`icon` → medium + iconOnly). */
type LegacySize = 'default' | 'sm' | 'lg' | 'icon';
const buttonStyles = cva(
// Disabled is uniform across every combo (§5.2) and must beat both the
// combo colors and legacy className overrides, hence the `!` importants.
// `relative` anchors the .btn-touch-hit ::after hot zone (style.css).
// Weight 400 across all sizes/types (§3.1) — heavier weights are not a knob.
'relative inline-flex items-center justify-center whitespace-nowrap font-normal transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:!border-btn-disabled-border disabled:!bg-black/[0.04] disabled:!text-black/25 [&_svg]:shrink-0',
{
variants: {
variant: {
// `btn-brand-primary` applies the green-theme lime override (see style.css).
// Blue theme keeps the shared --primary color; other primary-token usages unaffected.
default: 'bg-primary text-primary-foreground hover:bg-primary/90 btn-brand-primary',
destructive:
'bg-surface-destructive text-destructive-foreground hover:bg-surface-destructive-hover',
outline:
'text-text-primary border border-border-light bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
// Brand-tinted secondary: light brand bg + dark brand text. The blue-* utilities
// are re-pointed to the --brand-* vars, so this follows the blue⇄green theme.
secondaryBrand: 'bg-blue-50 text-blue-600 hover:bg-blue-100',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
// hardcoded text color because of WCAG contrast issues (text-white)
submit: 'bg-surface-submit text-white hover:bg-surface-submit-hover',
// Color axis only carries what is combo-independent (focus ring, §5.2).
color: {
primary: 'focus-visible:ring-blue-500/40',
default: 'focus-visible:ring-blue-500/40',
danger: 'focus-visible:ring-btn-danger/40',
},
variant: {
solid: '',
outlined: 'border bg-white',
filled: '',
text: '',
link: 'underline-offset-4',
},
// Heights/radii per §2 (24/32/40, 4/6/8px). Font sizes reference the
// PRIMITIVE type scale vars on purpose: the semantic --text-body remaps
// 14→16 under 768px, but control text must not follow (§5.5 / 适配原则 §3).
// Icon size is one 14/16/18 ladder for BOTH icon-only and text+icon (§3.2/3.3).
// Horizontal padding here is the borderless value (8/16/16); bordered
// variants override to 7/15/15 in compoundVariants (visual-width parity).
size: {
default: 'h-9 px-4 py-2',
sm: 'h-9 rounded-lg px-3',
lg: 'h-11 rounded-lg px-8',
icon: 'size-9',
small:
'h-6 gap-1 rounded px-2 text-[length:var(--font-size-3)] leading-[var(--line-height-3)] [&_svg]:size-3.5',
medium:
'btn-touch-hit h-8 gap-2 rounded-[6px] px-4 text-[length:var(--font-size-3)] leading-[var(--line-height-3)] [&_svg]:size-4',
large:
'h-10 gap-2 rounded-[8px] px-4 text-[length:var(--font-size-4)] leading-[var(--line-height-4)] [&_svg]:size-[18px]',
},
// `circle` is declared AFTER `size` so its rounded-full wins the merge
// over the per-size radius; resolveVariants restricts it to icon-only (§1).
shape: {
square: '',
circle: 'rounded-full',
},
iconOnly: {
true: '',
false: '',
},
},
compoundVariants: [
/* ---- color × variant matrix (§5.2; combos the spec leaves implicit
follow the same ramp logic: hover one step, active one deeper).
Active steps are TOUCH-ONLY (coarse-pointer): on hover-capable
devices a pressed button keeps its hover color — no click flash;
on touch, where hover is disabled, active is the only feedback. ---- */
{
color: 'primary',
variant: 'solid',
// btn-brand-primary = green-theme !important override (style.css) —
// kept as agreed tech debt until the theme mechanism is reworked (§6.2).
class:
'btn-brand-primary bg-blue-500 text-white hover:bg-blue-400 coarse-pointer:active:bg-blue-600',
},
{
color: 'primary',
variant: 'outlined',
// Outlined hover is a faint tint of the button's own palette, border/
// text unchanged (§5.2) — same shape as default outlined's gray tint.
class: 'border-blue-500 text-blue-500 hover:bg-blue-50 coarse-pointer:active:bg-blue-100',
},
{
color: 'primary',
variant: 'filled',
class: 'bg-blue-50 text-blue-600 hover:bg-blue-100 coarse-pointer:active:bg-blue-200',
},
{
color: 'primary',
variant: 'text',
class: 'text-blue-500 hover:bg-blue-50 coarse-pointer:active:bg-blue-100',
},
{
color: 'primary',
variant: 'link',
class:
'text-blue-500 hover:text-blue-400 hover:underline coarse-pointer:active:text-blue-600',
},
{
color: 'default',
variant: 'solid',
class:
'bg-btn-gray-text text-white hover:bg-btn-gray-text/90 coarse-pointer:active:bg-btn-gray-text/80',
},
{
color: 'default',
variant: 'outlined',
class:
'border-btn-gray-border text-btn-gray-text hover:bg-btn-fill-1 coarse-pointer:active:bg-btn-fill-2',
},
{
color: 'default',
variant: 'filled',
class:
'bg-btn-fill-2 text-btn-gray-text hover:bg-btn-fill-3 coarse-pointer:active:bg-btn-fill-4',
},
{
color: 'default',
variant: 'text',
class:
'text-btn-gray-text hover:bg-btn-fill-1 coarse-pointer:active:bg-btn-fill-2',
},
{
color: 'default',
variant: 'link',
class:
'text-btn-gray-text hover:text-btn-gray-text/80 hover:underline coarse-pointer:active:text-btn-gray-text',
},
{
color: 'danger',
variant: 'solid',
class:
'bg-btn-danger text-white hover:bg-btn-danger-hover coarse-pointer:active:bg-btn-danger-active',
},
{
color: 'danger',
variant: 'outlined',
// Same faint-tint hover as primary outlined, on the red alpha ladder.
class:
'border-btn-danger text-btn-danger hover:bg-btn-danger/10 coarse-pointer:active:bg-btn-danger/[0.15]',
},
{
color: 'danger',
variant: 'filled',
class:
'bg-btn-danger/10 text-btn-danger hover:bg-btn-danger/[0.15] coarse-pointer:active:bg-btn-danger/20',
},
{
color: 'danger',
variant: 'text',
class:
'text-btn-danger hover:bg-btn-danger/10 coarse-pointer:active:bg-btn-danger/[0.15]',
},
{
color: 'danger',
variant: 'link',
class:
'text-btn-danger hover:text-btn-danger-hover hover:underline coarse-pointer:active:text-btn-danger-active',
},
/* ---- bordered padding 7/15/15 incl. 1px border (§2 visual parity) ---- */
{ variant: 'outlined', size: 'small', class: 'px-[7px]' },
{ variant: 'outlined', size: ['medium', 'large'], class: 'px-[15px]' },
/* ---- icon-only squares 24/32/40 (§3.2, icon ladder shared with the
size axis); every size gets the ≥44px touch hot zone (§5.5) ---- */
{ iconOnly: true, size: 'small', class: 'btn-touch-hit w-6 px-0' },
{ iconOnly: true, size: 'medium', class: 'w-8 px-0' },
{ iconOnly: true, size: 'large', class: 'btn-touch-hit w-10 px-0' },
],
// Bare <Button> keeps its historical primary-solid look (§6.3).
defaultVariants: {
variant: 'default',
size: 'default',
color: 'primary',
variant: 'solid',
size: 'medium',
shape: 'square',
iconOnly: false,
},
},
);
export interface ButtonStyleProps {
// `(string & {})` keeps the three literals in autocomplete while still
// accepting `{...props}` spreads that carry the native HTML `color` attr
// (e.g. TooltipAnchor render props); non-axis strings are ignored at runtime.
color?: ButtonColor | (string & {});
variant?: ButtonVariant | LegacyVariant;
size?: ButtonSize | LegacySize;
/** `circle` renders a full circle — icon-only buttons ONLY (§1); ignored otherwise. */
shape?: ButtonShape;
/** Square icon-only button (§3.2) — must ship an `aria-label` + Tooltip. */
iconOnly?: boolean;
}
function isButtonColor(value: unknown): value is ButtonColor {
return value === 'primary' || value === 'default' || value === 'danger';
}
const LEGACY_VARIANT_MAP: Record<string, { color: ButtonColor; variant: ButtonVariant }> = {
default: { color: 'primary', variant: 'solid' },
submit: { color: 'primary', variant: 'solid' },
destructive: { color: 'danger', variant: 'solid' },
outline: { color: 'default', variant: 'outlined' },
secondary: { color: 'default', variant: 'filled' },
secondaryBrand: { color: 'primary', variant: 'filled' },
ghost: { color: 'default', variant: 'text' },
// Bare `variant="link"` predates the color axis — keep its primary look.
link: { color: 'primary', variant: 'link' },
};
function resolveVariants({ color: rawColor, variant, size, shape, iconOnly }: ButtonStyleProps) {
const color = isButtonColor(rawColor) ? rawColor : undefined;
let resolvedColor = color;
let resolvedVariant = variant as ButtonVariant | undefined;
// Legacy values only kick in while the new `color` axis is absent — any
// explicit `color` means the caller is on the new dual-axis API.
if (color === undefined && variant !== undefined && variant in LEGACY_VARIANT_MAP) {
({ color: resolvedColor, variant: resolvedVariant } = LEGACY_VARIANT_MAP[variant]);
} else if (color !== undefined && variant === undefined) {
// New-API ergonomics matching the §1 named types: <Button color="default">
// is THE default button (outlined), primary/danger default to solid.
resolvedVariant = color === 'default' ? 'outlined' : 'solid';
}
let resolvedSize: ButtonSize | undefined;
let resolvedIconOnly = iconOnly;
switch (size) {
case 'default':
case 'sm':
resolvedSize = 'medium';
break;
case 'lg':
resolvedSize = 'large';
break;
case 'icon':
resolvedSize = 'medium';
resolvedIconOnly = iconOnly ?? true;
break;
default:
resolvedSize = size;
}
return {
color: resolvedColor,
variant: resolvedVariant,
size: resolvedSize,
// Circle is an icon-only privilege (§1) — text buttons fall back to square.
shape: shape === 'circle' && resolvedIconOnly ? ('circle' as const) : ('square' as const),
iconOnly: resolvedIconOnly,
};
}
/** Class-only entry point (for <a>/Slot call sites); accepts both APIs. */
export function buttonVariants(props: ButtonStyleProps & { className?: string } = {}) {
const { className, ...styleProps } = props;
return buttonStyles({ ...resolveVariants(styleProps), className });
}
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
// Native `color` attr is shadowed by the color axis.
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'color'>,
ButtonStyleProps {
asChild?: boolean;
/** Single leading icon (§3.3, one icon max); replaced by the spinner while loading. */
icon?: React.ReactNode;
/**
* Built-in loading state (§5.2): spinner takes the icon slot, whole button
* at opacity .65 and not clickable. Do NOT pass your own Spinner.
*/
loading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
(
{
className,
color,
variant,
size,
shape,
iconOnly,
icon,
loading = false,
asChild = false,
children,
...props
},
ref,
) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp className={cn(buttonVariants({ variant, size }), className)} ref={ref} {...props} />
<Comp
ref={ref}
aria-busy={loading || undefined}
className={cn(
buttonVariants({ color, variant, size, shape, iconOnly }),
loading && 'pointer-events-none opacity-65',
className,
)}
{...props}
>
{asChild ? (
// Slot requires a single element child — icon/spinner injection is
// skipped; asChild callers render their own content.
children
) : (
<>
{loading ? <Outlined.Loading className="animate-spin" /> : icon}
{children}
</>
)}
</Comp>
);
},
);
Button.displayName = 'Button';
export { Button, buttonVariants };
export { Button };
@@ -340,7 +340,7 @@ function ChannelSquare({
</div>
) : visibleChannels.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center text-[#86909c]">
<EmptyStateIllustration className="size-[120px] mb-4 opacity-90" />
<EmptyStateIllustration className="size-[120px] mb-4" />
<p className="text-[14px] font-normal text-[#999999]">{tEmptyText}</p>
</div>
) : (
@@ -821,12 +821,12 @@ export function AddToKnowledgeModal({
</div>
) : isEmpty ? (
<div className="flex flex-col items-center justify-center h-full min-h-[200px] text-gray-800">
<EmptyStateIllustration className="size-[120px] mb-4 opacity-90" />
<EmptyStateIllustration className="size-[120px] mb-4" />
<p className="text-[14px] font-normal text-[#999999]">{localize("com_subscription.no_selectable_knowledge_space")}</p>
</div>
) : displayTree.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full min-h-[200px] text-gray-800">
<EmptyStateIllustration className="size-[120px] mb-4 opacity-90" />
<EmptyStateIllustration className="size-[120px] mb-4" />
<p className="text-[14px] font-normal text-[#999999]">{localize("com_subscription.no_matching_knowledge_space")}</p>
</div>
) : (
@@ -809,12 +809,12 @@ export function ArticleList({
<div className="flex flex-1 flex-col items-center justify-center py-8 text-center">
{(searchQuery || selectedSources.length > 0 || onlyUnread) ? (
<>
<EmptyStateIllustration className="size-[120px] mb-4 opacity-90" />
<EmptyStateIllustration className="size-[120px] mb-4" />
<p className="text-[14px] font-normal leading-6 text-[#999999]">{localize("com_subscription.no_results")}</p>
</>
) : (
<>
<EmptyStateIllustration className="size-[120px] mb-4 opacity-90" />
<EmptyStateIllustration className="size-[120px] mb-4" />
<p className="text-[14px] font-normal leading-6 text-[#999999]">
{localize("com_subscription.no_related_content")}
</p>
@@ -278,7 +278,7 @@ export function ChannelDiscoveryHome({
);
const renderEmptyIllustration = () => (
<EmptyStateIllustration className="size-[120px] opacity-90" />
<EmptyStateIllustration className="size-[120px]" />
);
const renderCarousel = () => {
@@ -161,7 +161,7 @@ function PreviewBody({ url, status, previewData, errorCode, noHoverDevice }: Pre
{status === "error" && (
<div className="flex min-h-[270px] flex-1 flex-col justify-between rounded border border-[#E5E6EB] px-6 py-8">
<div className="flex flex-1 flex-col items-center justify-center text-center">
<EmptyStateIllustration className="mb-5 size-[100px]" />
<EmptyStateIllustration className="mb-4 size-[80px]" />
<p className="text-[14px] leading-6 text-[#4E5969]">
{localize(crawlErrorMessageKey(errorCode))}
</p>
@@ -6,85 +6,178 @@
* tree-shaken out of the production build. Changing a component shown here changes the
* real shared component, so every business page updates too.
*
* Two audience-facing modes, switched by the segmented control in the sidebar:
* 规范 (spec) — how to USE the design system today; for designers/engineers/PMs.
* sections/* (antd-docs-style pages, no migration noise)
* 进度 (progress) — migration dashboards & ledgers for the effort's owner.
* progress/* (status dots live here only)
* Default export (required by React.lazy in the route). See docs-ui-refactor/00-总纲.md.
*/
import { useState } from 'react';
import { ComponentType, useState } from 'react';
import { cn } from '~/utils';
import { OverviewSection } from './sections/OverviewSection';
import { ResponsiveSection } from './sections/ResponsiveSection';
import { TypographySection } from './sections/TypographySection';
import { ColorSection } from './sections/ColorSection';
import { IllustrationSection } from './sections/IllustrationSection';
import { ModalSection } from './sections/ModalSection';
import { ConfirmDialogSection } from './sections/ConfirmDialogSection';
import { ButtonSection } from './sections/ButtonSection';
import { FeedbackSection } from './sections/FeedbackSection';
import { ProgressOverview } from './progress/ProgressOverview';
import { TypographyProgress } from './progress/TypographyProgress';
import { ColorProgress } from './progress/ColorProgress';
import { ButtonProgress } from './progress/ButtonProgress';
import { ModalProgress } from './progress/ModalProgress';
import { ConfirmProgress } from './progress/ConfirmProgress';
interface NavItem {
type Mode = 'spec' | 'progress';
type Status = 'wip' | 'todo' | 'done';
interface PageDef {
id: string;
label: string;
status?: 'wip' | 'todo' | 'done';
group: string;
/** Migration status — progress mode only (spec readers don't care). */
status?: Status;
/** Spec not finalized yet — shows a 未定稿 tag in spec mode. */
draft?: boolean;
Page: ComponentType;
}
const NAV: NavItem[] = [
{ id: 'overview', label: '总览' },
{ id: 'modal', label: 'Modal 弹窗', status: 'wip' },
{ id: 'confirm', label: '二次确认弹窗', status: 'wip' },
{ id: 'button', label: 'Button 按钮', status: 'todo' },
{ id: 'feedback', label: '点赞点踩反馈', status: 'done' },
/** Spec registry — the design system as consumers should use it today. */
const SPEC_PAGES: PageDef[] = [
{ id: 'overview', label: '总览', group: '总则', Page: OverviewSection },
{ id: 'responsive', label: '多端适配 Responsive', group: '总则', Page: ResponsiveSection },
{ id: 'typography', label: '字体 Typography', group: '基础 Foundation', Page: TypographySection },
{ id: 'color', label: '色彩 Colors', group: '基础 Foundation', Page: ColorSection },
{ id: 'illustration', label: '插画 Illustration', group: '基础 Foundation', Page: IllustrationSection },
{ id: 'button', label: 'Button 按钮', group: '通用 General', Page: ButtonSection },
{ id: 'modal', label: 'Modal 弹窗', group: '反馈 Feedback', draft: true, Page: ModalSection },
{ id: 'confirm', label: '二次确认弹窗', group: '反馈 Feedback', Page: ConfirmDialogSection },
{ id: 'feedback', label: '点赞 / 点踩', group: '反馈 Feedback', Page: FeedbackSection },
];
const STATUS_DOT: Record<NonNullable<NavItem['status']>, string> = {
/** Progress registry — migration dashboards & ledgers, one per in-flight component. */
const PROGRESS_PAGES: PageDef[] = [
{ id: 'overview', label: '总览看板', group: '总则', Page: ProgressOverview },
{ id: 'typography', label: '字体 Typography', group: '基础 Foundation', status: 'wip', Page: TypographyProgress },
{ id: 'color', label: '色彩 Colors', group: '基础 Foundation', status: 'wip', Page: ColorProgress },
{ id: 'button', label: 'Button 按钮', group: '通用 General', status: 'wip', Page: ButtonProgress },
{ id: 'modal', label: 'Modal 弹窗', group: '反馈 Feedback', status: 'wip', Page: ModalProgress },
{ id: 'confirm', label: '二次确认弹窗', group: '反馈 Feedback', status: 'wip', Page: ConfirmProgress },
];
const REGISTRY: Record<Mode, PageDef[]> = { spec: SPEC_PAGES, progress: PROGRESS_PAGES };
const MODE_LABEL: Record<Mode, string> = { spec: '设计规范', progress: '迁移进度' };
const STATUS_DOT: Record<Status, string> = {
wip: 'bg-amber-500',
todo: 'bg-gray-300',
done: 'bg-green-500',
};
export default function GalleryApp() {
const [active, setActive] = useState('overview');
/** Preserve group order as declared in the registry. */
function groupsOf(pages: PageDef[]): string[] {
return pages.reduce<string[]>((acc, p) => {
if (!acc.includes(p.group)) acc.push(p.group);
return acc;
}, []);
}
const handleNav = (id: string) => {
setActive(id);
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
export default function GalleryApp() {
const [mode, setMode] = useState<Mode>('spec');
/* Remember the active page per mode, so toggling back restores where you were. */
const [activeIds, setActiveIds] = useState<Record<Mode, string>>({
spec: 'overview',
progress: 'overview',
});
const pages = REGISTRY[mode];
const active = pages.find((p) => p.id === activeIds[mode]) ?? pages[0];
const ActivePage = active.Page;
const handleSwitchMode = (next: Mode) => {
if (next === mode) return;
/* Keep the same component page across modes when it exists on both sides. */
setActiveIds((ids) => ({
...ids,
[next]: REGISTRY[next].some((p) => p.id === active.id) ? active.id : 'overview',
}));
setMode(next);
};
return (
<div className="flex h-screen w-full overflow-hidden bg-background text-foreground">
{/* Sidebar */}
<aside className="flex w-60 shrink-0 flex-col border-r border-border-light bg-muted/20">
<aside className="flex w-64 shrink-0 flex-col border-r border-border-light bg-muted/20">
<div className="border-b border-border-light px-5 py-4">
<div className="text-base font-semibold text-text-primary"></div>
<div className="mt-0.5 text-xs text-muted-foreground">DEV only · </div>
<div className="text-h4 text-text-primary"></div>
<div className="mt-0.5 text-caption text-muted-foreground">DEV only</div>
</div>
<nav className="flex-1 overflow-y-auto p-2">
{NAV.map((item) => (
<button
key={item.id}
onClick={() => handleNav(item.id)}
className={cn(
'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors',
active === item.id
? 'bg-blue-500/[0.08] font-medium text-text-primary'
: 'text-muted-foreground hover:bg-muted/60',
)}
>
{item.status && (
<span className={cn('size-1.5 rounded-full', STATUS_DOT[item.status])} />
)}
{item.label}
</button>
{/* Mode segmented control — spec for consumers, progress for the owner */}
<div className="p-3">
{/* 36px total = 1px border + 2px padding + 30px options; 2px gap; radius 8 outer / 6 inner */}
<div className="grid h-9 grid-cols-2 gap-0.5 rounded-lg border border-border-base p-0.5">
{(Object.keys(REGISTRY) as Mode[]).map((m) => (
<button
key={m}
onClick={() => handleSwitchMode(m)}
className={cn(
'h-[30px] rounded-md px-2 text-body-sm transition-colors',
mode === m
? 'bg-blue-500/[0.08] font-medium text-blue-500'
: 'text-muted-foreground hover:bg-muted/60',
)}
>
{MODE_LABEL[m]}
</button>
))}
</div>
</div>
<nav className="flex-1 overflow-y-auto p-3">
{groupsOf(pages).map((group) => (
<div key={group} className="mb-4">
{/* antd-style 3-level hierarchy: group lightest, items darkest, active = brand */}
<div className="px-3 pb-1.5 text-caption text-text-3">{group}</div>
{pages
.filter((p) => p.group === group)
.map((p) => (
<button
key={p.id}
onClick={() => setActiveIds((ids) => ({ ...ids, [mode]: p.id }))}
className={cn(
'flex w-full items-center gap-2 rounded-lg py-2 pl-5 pr-3 text-left text-body transition-colors',
active.id === p.id
? 'bg-blue-500/[0.08] font-medium text-blue-500'
: 'text-text-1 hover:bg-muted/60',
)}
>
{mode === 'progress' && p.status && (
<span className={cn('size-1.5 rounded-full', STATUS_DOT[p.status])} />
)}
<span className="truncate">{p.label}</span>
{mode === 'spec' && p.draft && (
<span className="ml-auto shrink-0 rounded bg-amber-500/15 px-1.5 py-0.5 text-caption text-amber-600">
稿
</span>
)}
</button>
))}
</div>
))}
</nav>
<div className="border-t border-border-light px-5 py-3 text-xs text-muted-foreground">
<div className="border-t border-border-light px-5 py-3 text-caption text-muted-foreground">
docs-ui-refactor/
</div>
</aside>
{/* Content */}
<main className="flex-1 overflow-y-auto">
<div className="mx-auto max-w-5xl px-8 py-8">
<OverviewSection />
<ModalSection />
<ConfirmDialogSection />
<ButtonSection />
<FeedbackSection />
</div>
{/* Content — one page at a time */}
<main key={`${mode}-${active.id}`} className="flex-1 overflow-y-auto">
<ActivePage />
</main>
</div>
);
@@ -1,77 +1,142 @@
/**
* Gallery-only presentational helpers.
* Gallery-only presentational helpers — antd-docs-style layout.
*
* DEV-ONLY internal tooling — this whole `_gallery` folder is never shipped to
* production (the route is gated behind `import.meta.env.DEV`, see routes/index.tsx).
* It exists so the UI designer can eyeball every component's states/variants in the
* real app theme while unifying them. See docs-ui-refactor/00-总纲.md.
*
* Structure mirrors ant.design/components/*: each component is ONE page —
* ComponentPage(title + description + 何时使用/规则)
* └─ ExampleGroup(子标题)
* └─ ExampleGrid → ExampleCard(演示区 + 分隔线 + 说明)
*/
import { ReactNode } from 'react';
import { cn } from '~/utils';
/** A titled block that groups one component's examples. */
export function Section({
id,
/* ------------------------------------------------------------------ *
* Page shell — the antd component-doc page skeleton.
* ------------------------------------------------------------------ */
export function ComponentPage({
title,
eng,
description,
whenToUse,
bodyTitle,
children,
}: {
title: string;
eng?: string;
description?: ReactNode;
/** 何时使用 / 使用规则 — rendered as a bordered bullet list, antd-style. */
whenToUse?: ReactNode[];
/** Heading above the body. Defaults to 代码演示; pass null to omit (overview-style pages). */
bodyTitle?: string | null;
children: ReactNode;
}) {
return (
<article className="p-8">
<header className="mb-8">
<h1 className="flex items-baseline gap-3 text-h1 text-text-primary">
{title}
{eng && <span className="text-h4 font-normal text-muted-foreground">{eng}</span>}
</h1>
{description && (
<p className="mt-3 max-w-3xl text-body text-text-primary">{description}</p>
)}
</header>
{whenToUse && whenToUse.length > 0 && (
<section className="mb-10">
<h2 className="mb-3 text-h3 text-text-primary">使 / </h2>
<ul className="space-y-2 rounded-xl border border-border-light bg-muted/20 p-5">
{whenToUse.map((item, i) => (
<li key={i} className="flex gap-2 text-body text-text-primary">
<span className="mt-2 size-1.5 shrink-0 rounded-full bg-blue-500" />
<span>{item}</span>
</li>
))}
</ul>
</section>
)}
<section>
{bodyTitle !== null && (
<h2 className="mb-4 text-h3 text-text-primary">{bodyTitle ?? '代码演示'}</h2>
)}
{children}
</section>
</article>
);
}
/* ------------------------------------------------------------------ *
* Example group — a titled cluster of examples within a page.
* ------------------------------------------------------------------ */
export function ExampleGroup({
title,
subtitle,
children,
}: {
id?: string;
title: string;
title?: string;
subtitle?: ReactNode;
children: ReactNode;
}) {
return (
<section id={id} className="mb-16 scroll-mt-6">
<div className="mb-4 border-b border-border-light pb-3">
<h2 className="text-xl font-semibold text-text-primary">{title}</h2>
{subtitle && <p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>}
</div>
<div className="mb-8">
{title && <h3 className="mb-1 text-h4 text-text-primary">{title}</h3>}
{subtitle && <p className="mb-3 text-body-sm text-muted-foreground">{subtitle}</p>}
{!subtitle && title && <div className="mb-3" />}
{children}
</section>
</div>
);
}
/** A labeled example cell — shows one variant/state with a caption underneath. */
export function Demo({
label,
note,
/* ------------------------------------------------------------------ *
* Example card — antd demo cell: live demo on top, meta below a divider.
* ------------------------------------------------------------------ */
export function ExampleCard({
title,
description,
className,
children,
}: {
label: string;
note?: string;
title: string;
description?: ReactNode;
className?: string;
children: ReactNode;
}) {
return (
<div
className={cn(
'flex flex-col gap-3 rounded-xl border border-border-light bg-background p-5',
className,
)}
>
<div className="flex min-h-[64px] flex-wrap items-center gap-3">{children}</div>
<div className="mt-auto">
<div className="text-sm font-medium text-text-primary">{label}</div>
{note && <div className="mt-0.5 text-xs text-muted-foreground">{note}</div>}
<div className={cn('overflow-hidden rounded-xl border border-border-light bg-background', className)}>
<div className="flex min-h-[88px] flex-wrap items-center gap-3 p-6">{children}</div>
<div className="border-t border-dashed border-border-light px-5 py-3">
<div className="text-body-sm font-medium text-text-primary">{title}</div>
{description && <div className="mt-0.5 text-caption text-muted-foreground">{description}</div>}
</div>
</div>
);
}
/** Responsive grid for laying out Demo cells. */
export function DemoGrid({ children, cols = 3 }: { children: ReactNode; cols?: 2 | 3 | 4 }) {
/** Responsive grid for laying out ExampleCard / Demo cells. */
export function ExampleGrid({ children, cols = 3 }: { children: ReactNode; cols?: 1 | 2 | 3 | 4 }) {
const colClass =
cols === 2
? 'sm:grid-cols-2'
: cols === 4
? 'sm:grid-cols-2 lg:grid-cols-4'
: 'sm:grid-cols-2 lg:grid-cols-3';
cols === 1
? ''
: cols === 2
? 'sm:grid-cols-2'
: cols === 4
? 'sm:grid-cols-2 lg:grid-cols-4'
: 'sm:grid-cols-2 lg:grid-cols-3';
return <div className={cn('grid grid-cols-1 gap-4', colClass)}>{children}</div>;
}
/** A small comparison table for documenting differences between variants. */
/* ------------------------------------------------------------------ *
* A small comparison table for documenting differences between variants.
* ------------------------------------------------------------------ */
export function CompareTable({
head,
rows,
@@ -81,11 +146,11 @@ export function CompareTable({
}) {
return (
<div className="overflow-x-auto rounded-xl border border-border-light">
<table className="w-full text-left text-sm">
<table className="w-full text-left text-body-sm">
<thead className="bg-muted/40">
<tr>
{head.map((h, i) => (
<th key={i} className="px-4 py-2.5 font-medium text-text-primary">
<th key={i} className="whitespace-nowrap px-4 py-2.5 font-medium text-text-primary">
{h}
</th>
))}
@@ -106,3 +171,52 @@ export function CompareTable({
</div>
);
}
/* ------------------------------------------------------------------ *
* Legacy aliases — Demo / DemoGrid are still used by migrated pages
* (they now forward to ExampleCard / ExampleGrid). Section is unused by
* gallery pages (all migrated to ComponentPage) but kept as a safety net.
* ------------------------------------------------------------------ */
/** @deprecated use ExampleGroup inside a ComponentPage. */
export function Section({
id,
title,
subtitle,
children,
}: {
id?: string;
title: string;
subtitle?: ReactNode;
children: ReactNode;
}) {
return (
<section id={id} className="mx-auto max-w-4xl px-8 py-10">
<div className="mb-6">
<h1 className="text-h1 text-text-primary">{title}</h1>
{subtitle && <p className="mt-3 max-w-3xl text-body text-text-primary">{subtitle}</p>}
</div>
{children}
</section>
);
}
/** @deprecated use ExampleCard. */
export function Demo({
label,
note,
className,
children,
}: {
label: string;
note?: string;
className?: string;
children: ReactNode;
}) {
return <ExampleCard title={label} description={note} className={className}>{children}</ExampleCard>;
}
/** @deprecated use ExampleGrid. */
export function DemoGrid({ children, cols = 3 }: { children: ReactNode; cols?: 1 | 2 | 3 | 4 }) {
return <ExampleGrid cols={cols}>{children}</ExampleGrid>;
}
@@ -0,0 +1,118 @@
/**
* Button migration ledger — DEV-ONLY.
* The dual-axis spec lives in the 「设计规范 → Button」 page; this page keeps the
* legacy-API mapping table (docs-ui-refactor/组件-Button按钮.md §6.3) — every button
* below renders THROUGH the old API to verify the automatic mapping.
*/
import { Outlined } from 'bisheng-icons';
import { Button } from '~/components/ui/Button';
import { ComponentPage, ExampleGroup, CompareTable } from '../components/kit';
export function ButtonProgress() {
return (
<ComponentPage
title="Button · 迁移"
eng="Button Progress"
description={
<>
2026-07-14
deprecated
退 <code>btn</code> Generations/Button
</>
}
whenToUse={[
<>
<code>h-9</code>(36px) medium(32px)<b> 4px</b>
§6.6
</>,
<> API </>,
]}
bodyTitle="迁移台账"
>
<ExampleGroup title="旧 API 兼容映射(迁移台账,§6.3)">
<CompareTable
head={['旧写法(用量)', '映射为', '旧 API 实渲染']}
rows={[
[
<code key="o"> / variant=&quot;default&quot;116 </code>,
<code key="n">primary solid</code>,
<Button key="b" variant="default">
</Button>,
],
[
<code key="o">variant=&quot;submit&quot;11 </code>,
<code key="n">primary solid ChatGPT 绿</code>,
<Button key="b" variant="submit">
</Button>,
],
[
<code key="o">variant=&quot;outline&quot;78 </code>,
<code key="n">default outlined</code>,
<Button key="b" variant="outline">
</Button>,
],
[
<code key="o">variant=&quot;secondary&quot;17 18 primary filled</code>,
<code key="n">default filled</code>,
<Button key="b" variant="secondary">
</Button>,
],
[
<code key="o">variant=&quot;secondaryBrand&quot;0 </code>,
<code key="n">primary filled</code>,
<Button key="b" variant="secondaryBrand">
</Button>,
],
[
<code key="o">variant=&quot;ghost&quot;40 </code>,
<code key="n">default text</code>,
<Button key="b" variant="ghost">
</Button>,
],
[
<code key="o">variant=&quot;destructive&quot;6 </code>,
<code key="n">danger solid</code>,
<Button key="b" variant="destructive">
</Button>,
],
[
<code key="o">variant=&quot;link&quot;0 </code>,
<code key="n">primary link</code>,
<Button key="b" variant="link">
</Button>,
],
[
<code key="o">size / &quot;sm&quot;249 h-9</code>,
<code key="n">medium32px</code>,
<Button key="b" variant="outline" size="sm">
</Button>,
],
[
<code key="o">size=&quot;lg&quot;2 </code>,
<code key="n">large40px</code>,
<Button key="b" size="lg">
</Button>,
],
[
<code key="o">size=&quot;icon&quot;18 </code>,
<code key="n">medium + iconOnly</code>,
<Button key="b" variant="outline" size="icon" aria-label="搜索">
<Outlined.Search />
</Button>,
],
]}
/>
</ExampleGroup>
</ComponentPage>
);
}
@@ -0,0 +1,53 @@
/**
* Color migration ledger — DEV-ONLY.
* The spec itself lives in the 「设计规范 → 色彩」 page; this page tracks the
* bare-hex → semantic-token replacement work (docs-ui-refactor/基础-色彩规范.md).
*/
import { ComponentPage, ExampleGroup, CompareTable } from '../components/kit';
export function ColorProgress() {
return (
<ComponentPage
title="色彩 · 迁移"
eng="Color Progress"
description={
<>
token + Tailwind 线2026-07-15 hex <b>2469 / 215 </b>
2026-07-14 token
</>
}
whenToUse={[
<>
<b></b> LibreChat token<code>--text-primary</code> Arco
A.3 #5
</>,
<> <code>#EBECF0</code> gray-3 2/255</>,
<> token tag </>,
<>
<code>border</code> shadcn <code>--border</code>HSL
Tailwind <code>border-base</code>{' '}
<code>border-border-base</code> <code>border-border-light</code>
</>,
]}
bodyTitle="迁移台账"
>
<ExampleGroup title="迁移速查" subtitle="旧写法 → 新语义类。">
<CompareTable
head={['旧写法', '新语义类', '说明']}
rows={[
[<code key="o">#1D2129 / #212121</code>, <code key="n">text-text-1</code>, 'Arco gray-10 / LibreChat gray-800 → 主文字'],
[<code key="o">#4E5969</code>, <code key="n">text-text-2</code>, 'Arco gray-8 → 次文字(×197'],
[<code key="o">#86909C / #999999 / text-gray-500</code>, <code key="n">text-text-3</code>, '辅助文字(#86909C 全站最高频 ×263'],
[<code key="o">#C9CDD4</code>, <code key="n">text-text-4 / bg-fill-4</code>, '禁用文字 / 重填充,按语境选'],
[<code key="o">#E5E6EB / #EBECF0 / #ECECEC</code>, <code key="n">border-border-base</code>, '#EBECF0(×81) 折叠进 gray-3,视觉差 ≤2/255,逐处目检'],
[<code key="o">#F7F8FA / #F2F3F5</code>, <code key="n">bg-fill-1 / bg-fill-2</code>, 'hover 底 / filled 控件底'],
[<code key="o">#00C650 / #06B84C / #007D1A</code>, <code key="n">success </code>, '野生成功绿折叠(集中在 Files/VectorStore 旧组件)'],
[<code key="o">red-500 / --destructive / surface-destructive</code>, <code key="n">danger </code>, '危险红 4 写法收敛到 #F53F3F 三态'],
[<code key="o">#FFF2F0</code>, <code key="n">bg-danger-tint</code>, '旧浅红 tag 底 → Arco red-1 #FFECE8'],
[<code key="o">#335CFF / #0072FF / #1B61E6 / #165DFF </code>, <code key="n">blue-* / --brand-*</code>, ' token'],
]}
/>
</ExampleGroup>
</ComponentPage>
);
}
@@ -0,0 +1,244 @@
/**
* Confirm dialog migration ledger — DEV-ONLY. See docs-ui-refactor/组件-Modal弹窗.md.
*
* Tracks the OGDialogTemplate-selection → useConfirm() convergence: population
* table, the 9 historical selectClasses variants, and old-API demos that should
* now render with the unified look (step-1 convergence acceptance). The finalized
* spec lives in 「设计规范 → 二次确认弹窗」.
*/
import { ReactNode } from 'react';
import { Button } from '~/components/ui/Button';
import { OGDialog, OGDialogTrigger } from '~/components/ui/OriginalDialog';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { Label } from '~/components/ui/Label';
import { ComponentPage, ExampleGroup, Demo, DemoGrid, CompareTable } from '../components/kit';
/** One confirm-dialog demo replicating a real business usage. */
function ConfirmDemo({
label,
note,
title,
body,
selectText,
selectClasses,
isLoading,
showCloseButton = false,
}: {
label: string;
note?: string;
title: string;
body: ReactNode;
selectText: string;
selectClasses?: string;
isLoading?: boolean;
showCloseButton?: boolean;
}) {
return (
<Demo label={label} note={note}>
<OGDialog>
<OGDialogTrigger asChild>
<Button variant="outline"></Button>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={showCloseButton}
title={title}
className="max-w-[450px]"
main={
<Label className="text-left text-body font-medium">{body}</Label>
}
selection={{
selectHandler: () => undefined,
selectClasses,
selectText,
isLoading,
}}
/>
</OGDialog>
</Demo>
);
}
export function ConfirmProgress() {
return (
<ComponentPage
title="二次确认 · 迁移"
eng="Confirm Progress"
description={
<>
<code>OGDialogTemplate selection</code> 13 7 UI +
6 <code>useConfirm()</code>26 10
<b></b> UI SidePanel +
Chat/Header Modal <b></b>B
C 9 selectClasses danger / primary
9 {' '}
<code>AlertDialog</code> Modal
</>
}
whenToUse={[
<> UI selectClasses Loading </>,
<>0 N</>,
]}
bodyTitle="现状盘点"
>
{/* The two coexisting confirm-dialog systems */}
<ExampleGroup title="现状:两套确认体系">
<CompareTable
head={['体系', '实现', '业务文件数', '用在哪', '样式一致性']}
rows={[
[
'B 套模板',
<>
<code>OGDialogTemplate</code> + <code>selection</code>
</>,
'13(原 21;剩余全是死 UI 或表单)',
'旧页面(会话/书签/Agent/设置/Prompt…LibreChat 血统)',
'差 · 确认按钮 9 种写法',
],
[
'C 套服务',
<>
<code>useConfirm()</code>ConfirmContext + AlertDialog
</>,
'26(收敛完成,含已迁入 10 处)',
'新页面(知识空间 / 订阅频道 / 权限)',
'好 · 样式集中在一个文件,destructive/default 两档',
],
]}
/>
</ExampleGroup>
{/* Inventory table: every selectClasses variant found in business code */}
<ExampleGroup title="确认按钮 selectClasses 清单(9 种历史写法)">
<CompareTable
head={['#', '确认按钮 selectClasses(原文)', '用处', '文件数']}
rows={[
[
'1',
<code key="c">bg-red-700 dark:bg-red-600 hover:bg-red-800 </code>,
'可达的 4 处已迁 C;剩 4 处全是死 UI(书签/分享弹窗/两个工具移除)',
'4(原 8)· 全死 UI',
],
[
'2',
<code key="c">bg-red-600 hover:bg-red-700 dark:hover:bg-red-800</code>,
'删除 Agent / Assistant —— 死 UISidePanel 被注释)',
'2(原 3)· 全死 UI',
],
[
'3',
<code key="c">bg-red-600 hover:bg-red-700 dark:hover:bg-red-600</code>,
'清空预设 —— 死 UIChat/Header 无人引用)',
'1 · 死 UI',
],
[
'4',
<code key="c">bg-destructive hover:bg-destructive/80</code>,
'清空聊天 / 删缓存 / 撤销密钥 —— ✅ 已全部迁 C 套',
'0(原 3',
],
[
'5',
<code key="c">bg-surface-destructive hover:bg-surface-destructive-hover</code>,
'删除版本 / 管理员确认 —— ✅ 已全部迁 C 套',
'0(原 2',
],
[
'6',
<code key="c">bg-green-500 hover:bg-green-600 text-white</code>,
'保存预设 / 保存 API Key(确认=绿色?!',
'2',
],
[
'7',
<>
<code>btn btn-primary</code> CSS
</>,
'提交密钥',
'1',
],
[
'8',
<code key="c">bg-surface-submit hover:bg-surface-submit-hover</code>,
'重命名保存',
'1',
],
[
'9',
<>
<code>bg-gray-800 dark:bg-gray-200</code>
</>,
'OGDialogTemplate 内置 defaultSelect',
'—',
],
]}
/>
</ExampleGroup>
<ExampleGroup title="逐个打开对比(旧写法现应呈现统一外观)">
<DemoGrid cols={3}>
<ConfirmDemo
label="① red-700 系(6 处,原 8"
note="删除书签 — Bookmarks/DeleteBookmarkButton.tsx(原例子删会话已迁 C 套)"
title="删除书签"
body={
<>
<strong></strong>
</>
}
selectText="删除"
selectClasses="bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white"
/>
<ConfirmDemo
label="② red-600 系(2 处,原 3"
note="删除 Agent — SidePanel/Agents/DeleteButton.tsx"
title="删除助手"
body="确定要删除这个助手吗?此操作不可撤销。"
selectText="删除"
selectClasses="bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white"
/>
<ConfirmDemo
label="⑥ 绿色确认(2 处)"
note="保存预设 — Endpoints/SaveAsPresetDialog.tsx"
title="另存为预设"
body="将当前配置保存为预设?"
selectText="保存"
selectClasses="bg-green-500 hover:bg-green-600 dark:hover:bg-green-600 text-white"
showCloseButton
/>
<ConfirmDemo
label="⑨ 模板默认(不传 selectClasses"
note="OGDialogTemplate 内置 defaultSelect:黑底/暗色反白"
title="确认操作"
body="这是不传 selectClasses 时的默认确认按钮。"
selectText="确认"
/>
<ConfirmDemo
label="Loading 态(isLoading: true"
note="模板内置 Spinner · 各页自塞 Spinner 的写法已随迁移清零(原 4 处)"
title="删除会话"
body="确认按钮处于加载中。"
selectText="删除"
selectClasses="bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white"
isLoading
/>
<ConfirmDemo
label="⑧ surface-submit1 处)"
note="重命名保存 — Prompts/Groups/DashGroupItem.tsx"
title="重命名"
body="保存新的名称?"
selectText="保存"
selectClasses="bg-surface-submit hover:bg-surface-submit-hover text-white disabled:hover:bg-surface-submit"
/>
<ConfirmDemo
label="⑦ btn btn-primary1 处)"
note="提交密钥 — SetKeyDialog.tsx · 走全局 CSS 类"
title="设置密钥"
body="提交这个 API Key"
selectText="提交"
selectClasses="btn btn-primary"
/>
</DemoGrid>
</ExampleGroup>
</ComponentPage>
);
}
@@ -0,0 +1,334 @@
/**
* Modal migration ledger — DEV-ONLY. See docs-ui-refactor/组件-Modal弹窗.md.
*
* Fresh survey (2026-07-09): FIVE coexisting modal populations. The same demo
* content (title + description + input + cancel/confirm footer) is mounted into
* each shell so the shell differences (overlay / radius / padding / title /
* buttons) are the only variable when opening them side by side.
* The (draft) spec view lives in 「设计规范 → Modal」.
*/
import { Button } from '~/components/ui/Button';
import { Input } from '~/components/ui/Input';
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from '~/components/ui/Dialog';
import {
OGDialog,
OGDialogTrigger,
OGDialogContent,
OGDialogHeader,
OGDialogTitle,
OGDialogDescription,
} from '~/components/ui/OriginalDialog';
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
} from '~/components/ui/AlertDialog';
import DialogTemplate from '~/components/ui/DialogTemplate';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useConfirm } from '~/Providers';
import { ComponentPage, ExampleGroup, Demo, DemoGrid, CompareTable } from '../components/kit';
/** Identical body for every shell so only the shell itself differs. */
const demoBody = (
<div className="flex flex-col gap-3">
<p className="text-body text-text-primary">
/
</p>
<Input placeholder="示例输入框" />
</div>
);
/** Small reference demo of the finalized confirm dialog, for shell comparison. */
function ConfirmReferenceDemo() {
const confirm = useConfirm();
return (
<Button
variant="outline"
onClick={() =>
confirm({
variant: 'destructive',
title: '确认删除',
description: '弹窗壳视觉参照:圆角16 / p-5 / 灰底毛玻璃遮罩。',
confirmText: '确认删除',
})
}
>
</Button>
);
}
export function ModalProgress() {
return (
<ComponentPage
title="Modal · 迁移"
eng="Modal Progress"
description={
<>
2026-07-09 <b>5 64 </b>
B C A 22 + 3
AlertDialog7
</>
}
whenToUse={[
<><b></b>稿"④ 待设计师定夺"</>,
<>C 16 / p-5 / B </>,
<>A 22 / / </>,
]}
bodyTitle="现状盘点"
>
{/* ① Population overview */}
<ExampleGroup title="① 用量盘子(当前精确扫描,排除 ui/ 与画廊)">
<CompareTable
head={['体系', '实现', '业务文件数', '用在哪 / 备注']}
rows={[
[
'A 套 · 原语直接拼',
'Dialog + DialogContent 手拼',
<b key="a1">22</b>,
'新页面为主:知识库 8、订阅 2、审批/通知/账号/分享/appChat…(含 MainLayout 全局弹窗)',
],
[
'A 套 · 模板',
'DialogTemplate',
'3',
'EditPresetDialog、PresetItems、ContextButton(后者在 SidePanel 死树)',
],
[
'B 套 · 模板',
'OGDialogTemplate',
'16(原 25,确认迁移后)',
'书签/导出/SetKey/归档/Agent 面板…(其中 SidePanel 死树约 6 处);壳已对齐 C 套',
],
[
'B 套 · 原语直接拼',
'OGDialog + OGDialogContent 手拼',
'16',
'设置(账号/数据)、Prompts、文件预览、ShareAgent…;壳同上(已对齐 C 套)',
],
[
'手拼 AlertDialog',
'AlertDialogContent + 自拼头尾',
'7',
'频道成员 2、爬取系 4、灵思 TaskModeInput(部分带确认性质,本期一并处理)',
],
[
'C 套 · useConfirm(参照)',
'ConfirmContextAlertDialog 底层)',
'26(已收敛 ✅)',
'二次确认已定稿的视觉基准:圆角16 / p-5 / 灰底毛玻璃 —— Modal 壳的天然候选',
],
]}
/>
</ExampleGroup>
{/* ② Shell anatomy */}
<ExampleGroup title="② 壳样式解剖(源码当前真实值)">
<CompareTable
head={['维度', 'A 套 Dialog', 'B 套 OriginalDialog(已对齐 C 套)', 'AlertDialog(手拼底座)']}
rows={[
[
'遮罩',
'bg-black/40 + blur(浅黑毛玻璃)',
'bg-gray-500/90 + blur(灰白毛玻璃)',
'bg-gray-500/90 + blur(同 B',
],
['层级 z-index', 'z-[100]', 'z-50', 'z-[110]'],
['圆角', 'sm:rounded-lg8px,移动端直角)', 'rounded-2xl16px', 'sm:rounded-lg8px,移动端直角)'],
['内边距', 'p-520px', 'p-520px', 'p-624px'],
[
'边框 / 阴影',
'border + shadow-lg',
'border #ebebeb + 淡投影',
'无边框、无阴影',
],
[
'标题',
'text-base font-semibold',
'text-base font-medium',
'组件是 text-lg semibold,但各页多自拼标题行',
],
[
'关闭按钮 ×',
'内置右上(可关)',
'内置右上(可关)',
'无内置,各页自拼',
],
[
'深色模式底色',
'dark:bg-[#303134](写死)',
'bg-background(跟主题)',
'dark:bg-gray-900',
],
[
'移动端行为',
'居中缩放出现',
'居中缩放出现',
'从底部滑入、贴底',
],
[
'footer 按钮',
'各页自拼(多为 Button outline + default',
'模板:取消白底描边 + 确认 danger/primary 档;直接拼的各页自理',
'各页自拼(红 #F53F3F 等)',
],
]}
/>
</ExampleGroup>
{/* ③ Side-by-side demos — identical content, different shells */}
<ExampleGroup title="③ 同一内容装进各个壳(逐个打开对比)">
<DemoGrid cols={3}>
{/* A-set raw primitives — the largest population */}
<Demo label="A 套 · 原语直接拼(22 处)" note="ui/Dialog.tsx · 浅黑毛玻璃 · 圆角8 · p-5">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline"></Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{demoBody}
<DialogFooter>
<DialogClose asChild>
<Button variant="outline"></Button>
</DialogClose>
<Button></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Demo>
{/* A-set template — legacy default black confirm button */}
<Demo label="A 套 · DialogTemplate3 处)" note="旧模板 · 默认黑底确认钮 · 正文额外 px-6">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline"></Button>
</DialogTrigger>
<DialogTemplate
title="弹窗标题"
description="标题下的说明文字。"
main={demoBody}
selection={{ selectHandler: () => null, selectText: '确定' }}
/>
</Dialog>
</Demo>
{/* B-set template — shell already aligned with C-set */}
<Demo label="B 套 · OGDialogTemplate16 处)" note="壳已对齐 C 套 · 圆角16 · 取消/确认已统一">
<OGDialog>
<OGDialogTrigger asChild>
<Button variant="outline"></Button>
</OGDialogTrigger>
<OGDialogTemplate
title="弹窗标题"
description="标题下的说明文字。"
className="max-w-md"
main={demoBody}
selection={{
selectHandler: () => null,
selectVariant: 'primary',
selectText: '确定',
}}
/>
</OGDialog>
</Demo>
{/* B-set raw primitives — same shell, hand-rolled body/footer */}
<Demo label="B 套 · OG 原语直接拼(16 处)" note="壳同左 · 头尾各页自拼(设置/Prompts 老页面)">
<OGDialog>
<OGDialogTrigger asChild>
<Button variant="outline"></Button>
</OGDialogTrigger>
<OGDialogContent className="max-w-md">
<OGDialogHeader>
<OGDialogTitle></OGDialogTitle>
<OGDialogDescription></OGDialogDescription>
</OGDialogHeader>
{demoBody}
<div className="flex justify-end gap-2">
<Button variant="outline"></Button>
<Button></Button>
</div>
</OGDialogContent>
</OGDialog>
</Demo>
{/* Hand-rolled AlertDialog population */}
<Demo
label="手拼 AlertDialog7 处)"
note="p-6 · 圆角8 · 无边框阴影 · z-110 · 移动端贴底滑入"
>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline"></Button>
</AlertDialogTrigger>
<AlertDialogContent className="max-w-md">
{/* Business pages hand-roll header/footer like this (ChannelMemberDialog etc.) */}
<h3 className="text-h4 text-text-primary"></h3>
{demoBody}
<div className="flex justify-end gap-2">
<Button variant="outline"></Button>
<Button></Button>
</div>
</AlertDialogContent>
</AlertDialog>
</Demo>
{/* C-set reference — the finalized confirm shell */}
<Demo
label="C 套 · useConfirm 参照(已定稿)"
note="二次确认基准壳:圆角16 / p-5 / 灰底毛玻璃 —— Modal 壳候选"
>
<ConfirmReferenceDemo />
</Demo>
</DemoGrid>
</ExampleGroup>
{/* ④ Decision checklist */}
<ExampleGroup title="④ 待设计师定夺">
<div className="rounded-xl border border-border-light bg-muted/20 p-5 text-body text-text-primary">
<ol className="list-decimal space-y-1 pl-5">
<li>
<b></b>A black/40+blur vs B/C gray-500/90+blur
black/80
</li>
<li>
<b></b>8pxA / AlertDialog vs 16pxB/C /
</li>
<li>
<b></b>p-520pxA/B/C vs p-624pxAlertDialogheader/body/footer gap-4
</li>
<li>
<b></b>font-semiboldA vs font-mediumB/C
</li>
<li>
<b> ×</b> A/B ×AlertDialog
</li>
<li>
<b>footer </b>/ Button outline+default C
+ danger/primary gap-2 vs gap-3
</li>
<li>
<b></b>z-50 / z-[100] / z-[110] Drawer/Sheet/Popover
</li>
<li>
<b></b>A 22 A B
</li>
</ol>
</div>
</ExampleGroup>
</ComponentPage>
);
}
@@ -0,0 +1,101 @@
/**
* Progress overview — DEV-ONLY. The migration dashboard for the unification
* effort's owner. Spec consumers (designers/engineers/PMs) should use the
* 「设计规范」 mode instead; this board mirrors docs-ui-refactor/00-总纲.md §四.
*/
import { ComponentPage, ExampleGroup, CompareTable } from '../components/kit';
export function ProgressOverview() {
return (
<ComponentPage
title="迁移进度"
eng="Progress"
description={
<>
/
/
</>
}
whenToUse={[
<> <code>docs-ui-refactor/00-.md</code></>,
<>
<b> demo </b>0 N
退"现状病历"
</>,
<>🟨 · · Modal Select</>,
]}
bodyTitle={null}
>
<ExampleGroup title="进度看板">
<CompareTable
head={['组件', '状态', '现状', '基准(收敛目标)与剩余工作']}
rows={[
[
'字体 Typography',
'🟨 进行中',
'Tailwind 默认档',
'九档 semantic token + 系统字体栈已落地;剩:删死字体 / typography.css / 逐步迁移',
],
[
'色彩 Colors',
'🟨 进行中',
'裸 hex 2469 处 / 215 值(2026-07-14 扫描)',
'Arco 两层 token 已落地;剩:逐批迁移(第一优先 = LibreChat 语义 token 重指向 Arco 值)',
],
[
'多端适配 Responsive',
'🟨 v1 已建',
'移动端处理散落各组件',
'双判定口径 + 4 原则已定;组件细则随各组件文档补充',
],
[
'滚动条 Scrollbar',
'✅ 完成',
'原全局强制细滚动条(已移除)',
'跟随系统设置已落地(2026-07-15);剩:.scrollbar-os 空类 ~33 处随手清、灵思深色面板例外待定夺',
],
[
'Button 按钮',
'🟨 进行中',
'5 路并行(旧 API 已自动映射)',
'color×variant 双轴 v1 已落地;剩:设计师验收 → 逐批迁移 → 清退 btn 系全局类',
],
[
'Modal 弹窗',
'🟨 进行中',
'5 个并行体系 · 约 64 个业务文件',
'标准待定 · C 套壳为基准候选(见 Modal 迁移页「待设计师定夺」)',
],
[
'二次确认弹窗',
'🟨 进行中',
'2 套体系(真确认已全部迁 C 套)',
'B 套壳与按钮已对齐 C 套;剩:死 UI 清理、表单弹窗归 Modal 期',
],
['点赞 / 点踩', '✅ 完成', '1(已统一)', 'MessageFeedbackButtons 全 6 类回答界面共用'],
['Select / 下拉', '⬜ 待办', '多个', '待定(下一个优先)'],
['Dropdown 菜单', '⬜ 待办', '2Dropdown / DropdownMenu', '待定'],
['Input 输入框', '⬜ 待办', '待扫描', '待定'],
['Tabs 标签页', '⬜ 待办', '待扫描', '待定'],
]}
/>
</ExampleGroup>
<ExampleGroup title="规范索引" subtitle="各规范页的成熟度与源文档;已定稿的直接照用,未定稿的以页内说明为准。">
<CompareTable
head={['规范页', '内容', '成熟度', '源文档(docs-ui-refactor/']}
rows={[
['字体 Typography', '系统字体栈 + 九档 semantic 字号 + 双字重', '✅ 已定稿落地', '基础-字体规范.md'],
['色彩 Colors', '两层 tokenArco primitive → semantic(文字/填充/边框/语义色)', '✅ v1 已定稿落地', '基础-色彩规范.md'],
['多端适配 Responsive', '双判定口径 + 四条核心原则 + 窄屏布局惯例', '✅ v1 已建', '基础-多端适配原则.md'],
['滚动条(并入总览 · 设计原则 ⑦)', '显隐跟随系统设置,默认不自定义 + 三个减显 utility', '✅ 已定稿落地', '基础-滚动条规范.md'],
['Button 按钮', 'color × variant 双轴 + 三档尺寸 + 内容形态与状态', '✅ v1 已定稿落地', '组件-Button按钮.md'],
['Modal 弹窗', '普通弹窗壳标准', '🟨 未定稿 · C 套壳候选', '组件-Modal弹窗.md'],
['二次确认弹窗', 'useConfirm() 服务 + 壳与按钮两档标准', '✅ 已定稿', '组件-Modal弹窗.md'],
['点赞 / 点踩', 'MessageFeedbackButtons 统一控件 + 延迟提交交互', '✅ 已定稿', '—'],
]}
/>
</ExampleGroup>
</ComponentPage>
);
}
@@ -0,0 +1,42 @@
/**
* Typography migration ledger — DEV-ONLY.
* The spec itself lives in the 「设计规范 → 字体」 page; this page tracks the
* old-classname → semantic-token replacement work (docs-ui-refactor/基础-字体规范.md §8).
*/
import { ComponentPage, ExampleGroup, CompareTable } from '../components/kit';
export function TypographyProgress() {
return (
<ComponentPage
title="字体 · 迁移"
eng="Typography Progress"
description={
<>
semantic token + 2026-07-14 Tailwind
<b></b>Inter / /
Roboto Mono typography.css
</>
}
whenToUse={[
<></>,
<><code>font-bold / font-semibold</code>157 <code>font-medium</code></>,
]}
bodyTitle="迁移台账"
>
<ExampleGroup title="迁移速查" subtitle="旧写法 → 新语义类,以及两者的实际差异。">
<CompareTable
head={['旧写法(Tailwind 默认档)', '新语义类', '差异']}
rows={[
['text-xs (12/16)', 'text-caption (12/20)', '行高 16→20'],
['text-sm (14/20)', 'text-body (14/22)', '行高 20→22,规范化 lh = size + 8'],
['text-base (16/24)', 'text-h4 (16/24, 500) 或正文场景 text-body', '按语义选择'],
['text-lg (18/28)', 'text-h3 (18/26, 500)', '行高 28→26,自带 500 字重'],
['text-xl (20/28)', 'text-h2 (20/28, 500)', '自带 500 字重'],
['text-2xl (24/32)', 'text-h1 (24/32, 500)', '自带 500 字重'],
['font-bold / font-semibold (157 处)', 'font-medium', '独立迁移,非本次范围'],
]}
/>
</ExampleGroup>
</ComponentPage>
);
}
@@ -1,55 +1,247 @@
/**
* Button gallery — DEV-ONLY.
* Button.tsx is already cva-based; it serves as the reference pattern for how other
* components should expose variants/sizes. See docs-ui-refactor/01-设计规范.md.
* Standard-usage documentation for the refactored dual-axis Button
* (docs-ui-refactor/组件-Button按钮.md v1): color × variant × size matrix,
* states and content forms. Legacy-API mapping ledger: progress/ButtonProgress.tsx.
*/
import { Outlined } from 'bisheng-icons';
import { Button } from '~/components/ui/Button';
import { Section, Demo, DemoGrid } from '../components/kit';
import {
ComponentPage,
ExampleGroup,
ExampleGrid,
ExampleCard,
CompareTable,
} from '../components/kit';
const VARIANTS = [
'default',
'secondary',
'secondaryBrand',
'outline',
'ghost',
'destructive',
'submit',
'link',
] as const;
const COLORS = ['primary', 'default', 'danger'] as const;
const VARIANTS = ['solid', 'outlined', 'filled', 'text', 'link'] as const;
const SIZES = ['small', 'medium', 'large'] as const;
const SIZES = ['sm', 'default', 'lg', 'icon'] as const;
const SIZE_META: Record<(typeof SIZES)[number], string> = {
small: '高 24px · 字号 14/22 · 圆角 4px — 表格行内、紧凑工具条',
medium: '高 32px · 字号 14/22 · 圆角 6px — 默认,绝大多数场景',
large: '高 40px · 字号 16/24 · 圆角 8px — 登录页、大表单提交',
};
export function ButtonSection() {
return (
<Section
id="button"
<ComponentPage
title="Button 按钮"
subtitle={
eng="Button"
description={
<>
<code>cva</code> "留改动余地" + {' '}
<code>className</code>
antd <code>color × variant</code> color primary / default /
dangervariant solid / outlined / filled / text / link3×5
</>
}
whenToUse={[
<>
<code>primary solid</code>
<code>color=&quot;default&quot;</code>
</>,
<>
<code>small / medium / large</code> 24/32/40px
</>,
<>
footer 12px
8px
</>,
<>
icon Tooltip <code>aria-label</code>
<code>shape=&quot;circle&quot;</code> icon bisheng-icons
lucide
</>,
<>
loading <code>loading</code> Spinner
</>,
<>
<code>className</code>
APIoutline/ghost/submit
Button
</>,
]}
>
<DemoGrid cols={4}>
{VARIANTS.map((v) => (
<Demo key={v} label={`variant="${v}"`}>
<Button variant={v}>{v}</Button>
<Button variant={v} disabled>
disabled
</Button>
</Demo>
))}
</DemoGrid>
<ExampleGroup title="常用类型" subtitle="六个常用别名,其余组合按双轴推导。">
<h3 className="mb-3 mt-8 text-sm font-medium text-text-primary"> size</h3>
<DemoGrid cols={4}>
{SIZES.map((s) => (
<Demo key={s} label={`size="${s}"`}>
<Button size={s}>{s === 'icon' ? '★' : s}</Button>
</Demo>
))}
</DemoGrid>
</Section>
<ExampleGrid cols={3}>
<ExampleCard title="Primary 主按钮" description="primary × solid — 主行动点,一个区域只放一个">
<Button color="primary" variant="solid">
</Button>
</ExampleCard>
<ExampleCard title="Secondary 次强调" description="primary × filled — 品牌浅底,弱于主按钮">
<Button color="primary" variant="filled">
</Button>
</ExampleCard>
<ExampleCard title="Default 默认按钮" description="default × outlined — 最常用的次级按钮(取消/返回)">
<Button color="default" variant="outlined">
</Button>
</ExampleCard>
<ExampleCard title="Text 文字按钮" description="default × text — 最次级、表格行内、工具栏">
<Button color="default" variant="text">
</Button>
</ExampleCard>
<ExampleCard title="Link 链接按钮" description="primary × link — 导航型操作,hover 不加底">
<Button color="primary" variant="link">
</Button>
</ExampleCard>
<ExampleCard title="Danger 危险按钮" description="danger × solid / outlined / text — 一般配二次确认">
<Button color="danger" variant="solid">
</Button>
<Button color="danger" variant="outlined">
</Button>
<Button color="danger" variant="text">
</Button>
</ExampleCard>
</ExampleGrid>
</ExampleGroup>
<ExampleGroup title="color × variant 全矩阵">
<CompareTable
head={['variant \\ color', 'primary 品牌', 'default 中性', 'danger 危险']}
rows={VARIANTS.map((v) => [
<code key="v">{v}</code>,
...COLORS.map((c) => (
<Button key={c} color={c} variant={v}>
</Button>
)),
])}
/>
</ExampleGroup>
<ExampleGroup title="尺寸 size" subtitle="三档 24 / 32 / 40px,圆角 4 / 6 / 8px。">
<ExampleGrid cols={3}>
{SIZES.map((s) => (
<ExampleCard key={s} title={`size="${s}"`} description={SIZE_META[s]}>
<Button color="primary" size={s}>
</Button>
<Button color="default" size={s}>
</Button>
<Button color="default" size={s} iconOnly aria-label="搜索">
<Outlined.Search />
</Button>
</ExampleCard>
))}
</ExampleGrid>
</ExampleGroup>
<ExampleGroup title="内容形态" subtitle="纯文字 / 纯 icon / 文字 + icon;一个按钮最多一个 icon。">
<ExampleGrid cols={2}>
<ExampleCard
title="纯文字"
description="不换行不省略;两个汉字不加中间空格;字重 400(全尺寸全类型一致)"
>
<Button></Button>
<Button color="default"></Button>
</ExampleCard>
<ExampleCard
title="纯 iconshape square / circle"
description="circle 正圆仅限纯 icon;必须带 Tooltip + aria-label;触屏热区 ≥44px。"
>
<Button color="default" variant="outlined" size="small" iconOnly aria-label="编辑">
<Outlined.Edit />
</Button>
<Button color="default" variant="outlined" size="medium" iconOnly aria-label="编辑">
<Outlined.Edit />
</Button>
<Button color="default" variant="outlined" size="large" iconOnly aria-label="编辑">
<Outlined.Edit />
</Button>
<Button
color="default"
variant="outlined"
shape="circle"
iconOnly
aria-label="搜索"
>
<Outlined.Search />
</Button>
<Button shape="circle" iconOnly aria-label="发送">
<Outlined.Send />
</Button>
<Button color="default" variant="text" iconOnly aria-label="删除">
<Outlined.Delete />
</Button>
<Button color="danger" variant="text" iconOnly aria-label="删除">
<Outlined.Delete />
</Button>
</ExampleCard>
<ExampleCard
title="文字 + iconicon 属性,默认在左)"
description="icon 与纯 icon 同一套 14/16/18px,间距 8pxsmall 4px"
>
<Button icon={<Outlined.Plus />}></Button>
<Button color="default" icon={<Outlined.Download />}>
</Button>
<Button color="danger" variant="outlined" icon={<Outlined.Delete />}>
</Button>
</ExampleCard>
<ExampleCard
title="方向语义 icon 在右"
description="“下一步 →”类方向语义可放右侧:icon 走 children 尾部"
>
<Button>
<Outlined.ArrowRight />
</Button>
</ExampleCard>
</ExampleGrid>
</ExampleGroup>
<ExampleGroup title="状态 state" subtitle="disabled / loading 全类型统一;focus 环仅键盘可见。">
<ExampleGrid cols={2}>
<ExampleCard title="disabled(全类型统一)" description="灰底、不可点。">
<Button disabled></Button>
<Button color="primary" variant="filled" disabled>
</Button>
<Button color="default" disabled>
</Button>
<Button color="default" variant="text" disabled>
</Button>
<Button color="danger" disabled>
</Button>
</ExampleCard>
<ExampleCard title="loading(内置 spinner" description="期间不可点;禁止业务页自塞 Spinner。">
<Button loading></Button>
<Button color="default" loading>
</Button>
<Button color="danger" loading>
</Button>
<Button loading icon={<Outlined.Plus />}>
</Button>
</ExampleCard>
</ExampleGrid>
</ExampleGroup>
</ComponentPage>
);
}
@@ -0,0 +1,374 @@
/**
* 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 (
<div className="flex items-center gap-2">
<span
className={cn('inline-block h-6 w-10 shrink-0 rounded border border-black/10', className)}
style={hex ? { background: hex } : style}
/>
{(label ?? hex) && (
<code className="whitespace-nowrap text-caption text-muted-foreground">{label ?? hex}</code>
)}
</div>
);
}
/** A light-bg + strong-text tag chip (§4 tag pairs). */
function TagChip({
className,
style,
children,
}: {
className?: string;
style?: CSSProperties;
children: ReactNode;
}) {
return (
<span className={cn('inline-flex items-center rounded px-2 py-0.5 text-body-sm', className)} style={style}>
{children}
</span>
);
}
/* ------------------------------------------------------------------ *
* antd-style vertical palette column (基础色板 look): seamless stacked
* swatches, step + hex overlaid, contrasting text, primary shade marked.
* ------------------------------------------------------------------ */
/** Perceived-luminance pick of a readable overlay text color for a hex bg. */
function overlayText(hex: string): string {
const h = hex.replace('#', '');
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.6 ? 'rgba(0,0,0,0.85)' : 'rgba(255,255,255,0.95)';
}
interface Swatch {
/** Shade label shown on the left (step number, or 'muted'). */
label: string;
/** Documentation hex — used for the overlaid value AND text-contrast math. */
hex: string;
/** Optional live background (e.g. rgb(var(--arco-gray-N))); defaults to hex. */
bg?: string;
/** Mark this shade as the primary (主色). */
primary?: boolean;
/** Bottom-accent color for the primary marker (a darker shade of the ramp). */
accent?: string;
/** Purpose text — revealed on hover as a tooltip. */
usage?: string;
}
/**
* Horizontal palette strip — tall, flat, sharp-cornered, seamless. Each swatch
* carries its own info (role / step / hex); hovering lifts it (scale + shadow,
* still square-cornered), no tooltip, no caption line.
*/
function PaletteRow({ title, swatches }: { title?: ReactNode; swatches: Swatch[] }) {
return (
<div>
{title && <div className="mb-3 text-body-sm font-medium text-text-primary">{title}</div>}
<div className="flex">
{swatches.map((s) => {
const fg = overlayText(s.hex);
return (
<div
key={s.label}
className="group relative flex h-36 min-w-0 flex-1 cursor-default flex-col justify-between p-3.5 transition-transform duration-200 ease-out hover:z-10 hover:scale-[1.06] hover:shadow-2xl"
style={{ background: s.bg ?? s.hex, color: fg }}
>
{/* role — hidden by default, fades in on hover (arco-style clean default) */}
{s.usage && (
<span
className="text-[11px] leading-snug opacity-0 transition-opacity duration-200 group-hover:opacity-100"
style={{ color: fg }}
>
{s.usage}
</span>
)}
<div>
<div className={cn('text-body-sm leading-tight', s.primary && 'font-medium')}>
{s.label}
</div>
<div className="mt-1 font-mono text-caption leading-none" style={{ opacity: 0.8 }}>
{s.hex}
</div>
</div>
{/* primary marker — a thin bottom accent bar in a darker shade of the ramp */}
{s.primary && (
<span className="absolute inset-x-0 bottom-0 h-[3px]" style={{ background: s.accent ?? fg }} />
)}
</div>
);
})}
</div>
</div>
);
}
/* ------------------------------------------------------------------ *
* Spec data
* ------------------------------------------------------------------ */
/** §1.1 brand ramp — blue/green documentation values. */
const BRAND_RAMP = [
{ step: '50', blue: '#E8F3FF', green: '#E4F1E7', usage: '浅底一档:选中背景;按钮 filled 底 / outlined·text hover' },
{ step: '100', blue: '#BEDAFF', green: '#CCE4D2', usage: '浅底二档:filled hover / outlined·text 触屏 active' },
{ step: '200', blue: '#94BFFF', green: '#A3D2B0', usage: '浅底三档:filled 触屏 active' },
{ step: '300', blue: '#6AA1FF', green: '#6FBA85', usage: '浅色过渡档 · 暂无固定用途' },
{ step: '400', blue: '#4080FF', green: '#3D9B5C', usage: 'hover(比主色亮一档)' },
{ step: '500', blue: '#165DFF', green: '#169C47', usage: '主色(= --brand-main' },
{ step: '600', blue: '#024DE3', green: '#098B35', usage: 'active / 按下(深一档)' },
{ step: '700', blue: '#0239AB', green: '#076929', usage: '深色档 · 暂无固定用途' },
{ step: '800', blue: '#042B80', green: '#074E20', usage: '深色档 · 暂无固定用途' },
{ step: '900', blue: '#051D52', green: '#063216', 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: '中间档 · 无 semantic 映射' },
{ n: 6, hex: '#86909C', note: '辅助文字 → text-3(全站最高频 hex ×263' },
{ n: 7, hex: '#6B7785', note: '中间档 · 无 semantic 映射' },
{ n: 8, hex: '#4E5969', note: '次要文字 → text-2' },
{ n: 9, hex: '#272E3B', note: '中间档 · 无 semantic 映射' },
{ n: 10, hex: '#1D2129', note: '主文字 → text-1' },
];
/** Concise in-card role labels (the palette shows these, not the verbose usage). */
const BRAND_ROLE: Record<string, string> = {
'50': '选中背景',
'100': 'filled hover',
'200': '触屏 active',
'300': '过渡档',
'400': 'hover 态',
'500': '主色',
'600': '按下 active',
'700': '深色档',
'800': '深色档',
'900': '深色档',
muted: '低饱和点缀',
};
const ARCO_ROLE: Record<number, string> = {
1: 'hover 底',
2: 'filled 底',
3: '边框',
4: '禁用 / 占位',
5: '过渡',
6: '辅助文字',
7: '过渡',
8: '次文字',
9: '过渡',
10: '主文字',
};
/** §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: <span className="text-body text-text-1"></span> },
{ token: '--text-2', cls: 'text-text-2', value: 'gray-8 #4E5969', usage: '次文字:次要说明、默认按钮文字', demo: <span className="text-body text-text-2"></span> },
{ token: '--text-3', cls: 'text-text-3', value: 'gray-6 #86909C', usage: '辅助文字:弱提示、时间戳、占位符', demo: <span className="text-body text-text-3"> · 10 </span> },
{ token: '--text-4', cls: 'text-text-4', value: 'gray-4 #C9CDD4', usage: '禁用文字', demo: <span className="text-body text-text-4"></span> },
{ token: '--fill-1', cls: 'bg-fill-1', value: 'gray-1 #F7F8FA', usage: '浅填充:hover 底、页面浅灰背景', demo: <ColorCell className="bg-fill-1" /> },
{ token: '--fill-2', cls: 'bg-fill-2', value: 'gray-2 #F2F3F5', usage: '填充:active 底、filled 控件底', demo: <ColorCell className="bg-fill-2" /> },
{ token: '--fill-3', cls: 'bg-fill-3', value: 'gray-3 #E5E6EB', usage: '深填充:filled hover', demo: <ColorCell className="bg-fill-3" /> },
{ token: '--fill-4', cls: 'bg-fill-4', value: 'gray-4 #C9CDD4', usage: '重填充:filled active', demo: <ColorCell className="bg-fill-4" /> },
{ token: '--border-base', cls: 'border-border-base', value: 'gray-3 #E5E6EB', usage: '常规边框:输入框、卡片、分割线(规范里的 border)', demo: <span className="inline-block h-8 w-24 rounded-md border border-border-base bg-white" /> },
{ token: '--border-deep', cls: 'border-border-deep', value: 'gray-4 #C9CDD4', usage: '深边框:强调分割、hover 边框', demo: <span className="inline-block h-8 w-24 rounded-md border border-border-deep bg-white" /> },
];
/** Functional colors — fixed hex (不换肤); 主色 marked with a darker (active) accent. */
const FUNCTIONAL: { title: string; swatches: Swatch[] }[] = [
{
title: '成功 Success',
swatches: [
{ label: '主色', hex: '#00B42A', primary: true, accent: '#009A29', usage: 'bg-success' },
{ label: 'hover', hex: '#23C343', usage: 'bg-success-hover' },
{ label: 'active', hex: '#009A29', usage: 'bg-success-active' },
{ label: 'tint 浅底', hex: '#E8FFEA', usage: 'bg-success-tint' },
],
},
{
title: '警告 Warning',
swatches: [
{ label: '主色', hex: '#FF7D00', primary: true, accent: '#D25F00', usage: 'bg-warning' },
{ label: 'hover', hex: '#FF9A2E', usage: 'bg-warning-hover' },
{ label: 'active', hex: '#D25F00', usage: 'bg-warning-active' },
{ label: 'tint 浅底', hex: '#FFF7E8', usage: 'bg-warning-tint' },
],
},
{
title: '危险 Danger',
swatches: [
{ label: '主色', hex: '#F53F3F', primary: true, accent: '#D02F33', usage: 'bg-danger' },
{ label: 'hover', hex: '#D6373A', usage: 'bg-danger-hover' },
{ label: 'active', hex: '#D02F33', usage: 'bg-danger-active' },
{ label: 'tint 浅底', hex: '#FFECE8', usage: 'bg-danger-tint(仅 tag' },
],
},
{
title: '链接 / 信息 Info= 品牌色,跟随主题)',
swatches: [
{ label: '主色', hex: '#165DFF', primary: true, accent: '#024DE3', usage: 'blue-500' },
{ label: 'hover', hex: '#4080FF', usage: 'blue-400' },
{ label: 'active', hex: '#024DE3', usage: 'blue-600' },
{ label: '浅底', hex: '#E8F3FF', usage: 'blue-50' },
],
},
];
/* ------------------------------------------------------------------ */
export function ColorSection() {
return (
<ComponentPage
title="色彩 Colors"
eng="Color"
description={
<>
Arco tokenprimitive<code>--arco-gray-1~10</code>
semantic / / / semantic
</>
}
whenToUse={[
<>
semantic <code>text-text-1</code> / <code>bg-fill-1</code> /{' '}
<code>border-border-base</code> / <code>bg-success</code> hex
</>,
<>
<code>blue-*</code> 绿 hex/{' '}
<code>bg-blue-500/[0.07]</code>
</>,
<>
//<strong></strong>{' '}
<code>bg-danger/10~20</code>tag <code>bg-danger-tint</code>#FFECE8
</>,
<> <code>text-text-2</code></>,
]}
>
<ExampleGroup
title="品牌色 Brand"
subtitle="主色 = 500 档,跟随蓝⇄绿主题。"
>
<div className="space-y-8">
<PaletteRow
title="蓝(默认 = arcoblue"
swatches={[
...BRAND_RAMP.map((r) => ({
label: r.step,
hex: r.blue,
primary: r.step === '500',
accent: BRAND_RAMP.find((x) => x.step === '700')!.blue,
usage: BRAND_ROLE[r.step],
})),
{ label: 'muted', hex: '#5773B4', usage: BRAND_ROLE.muted },
]}
/>
<PaletteRow
title="绿(theme-green"
swatches={[
...BRAND_RAMP.map((r) => ({
label: r.step,
hex: r.green,
primary: r.step === '500',
accent: BRAND_RAMP.find((x) => x.step === '700')!.green,
usage: BRAND_ROLE[r.step],
})),
{ label: 'muted', hex: '#5C8A77', usage: BRAND_ROLE.muted },
]}
/>
</div>
<p className="mt-3 text-body-sm text-muted-foreground">
tag logo muted
</p>
</ExampleGroup>
<ExampleGroup
title="中性色 · primitiveArco gray 110"
subtitle="中性色数值源;组件不直接用这层,走下方 semantic。"
>
<PaletteRow
title="Arco gray 110"
swatches={ARCO_GRAYS.map((g) => ({
label: `gray-${g.n}`,
hex: g.hex,
bg: `rgb(var(--arco-gray-${g.n}))`,
usage: ARCO_ROLE[g.n],
}))}
/>
</ExampleGroup>
<ExampleGroup
title="中性色 · semantic(组件用这层)"
subtitle="文字 / 填充 / 边框,同一语义只有一个值。"
>
<CompareTable
head={['Token', 'Tailwind 类', '取值', '用途', '实时示例']}
rows={NEUTRAL_SEMANTIC.map((s) => [
<code key="t" className="whitespace-nowrap">{s.token}</code>,
<code key="c" className="whitespace-nowrap">{s.cls}</code>,
<span key="v" className="whitespace-nowrap">{s.value}</span>,
s.usage,
<div key="d">{s.demo}</div>,
])}
/>
</ExampleGroup>
<ExampleGroup title="语义色 Functional(不换肤)">
<div className="space-y-8">
{FUNCTIONAL.map((f) => (
<PaletteRow key={f.title} title={f.title} swatches={f.swatches} />
))}
</div>
</ExampleGroup>
<ExampleGroup title="标签色 Tag">
<ExampleGrid cols={1}>
<ExampleCard title="Tag 配对一览" description="橙 / 绿 / 红三对走 token;技能紫、审批蓝为固定例外色。">
<TagChip style={{ background: '#F5E8FF', color: '#722ED1' }}> · token </TagChip>
<TagChip className="bg-warning-tint text-warning"> = warning </TagChip>
<TagChip className="bg-success-tint text-success"></TagChip>
<TagChip className="bg-danger-tint text-danger"></TagChip>
<TagChip style={{ background: '#E8F3FF', color: '#165DFF' }}></TagChip>
</ExampleCard>
</ExampleGrid>
</ExampleGroup>
</ComponentPage>
);
}
@@ -1,27 +1,22 @@
/**
* Confirm dialog gallery — DEV-ONLY. See docs-ui-refactor/组件-Modal弹窗.md.
* Confirm dialog spec page — DEV-ONLY. See docs-ui-refactor/组件-Modal弹窗.md.
*
* "二次确认弹窗" = OGDialogTemplate with the `selection` prop (delete / dangerous
* action confirmations). Every demo below reproduces an exact selectClasses string
* found in business code, so the designer can compare the current zoo of confirm
* button styles and pick one standard.
* Standard usage of the finalized confirm dialog: the app-wide useConfirm()
* service (ConfirmContext, AlertDialog-based) with its two variants, plus the
* finalized shell/button anatomy. Migration ledger (legacy OGDialogTemplate
* selection populations, selectClasses inventory): progress/ConfirmProgress.tsx.
*/
import { ReactNode } from 'react';
import { Button } from '~/components/ui/Button';
import { OGDialog, OGDialogTrigger } from '~/components/ui/OriginalDialog';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { Label } from '~/components/ui/Label';
import { useConfirm } from '~/Providers';
import { Section, Demo, DemoGrid, CompareTable } from '../components/kit';
import { ComponentPage, ExampleGroup, ExampleGrid, ExampleCard, CompareTable } from '../components/kit';
/** Demos for the app-wide `useConfirm()` service (ConfirmContext, AlertDialog-based). */
function UseConfirmDemos() {
const confirm = useConfirm();
return (
<>
<Demo
label="C 套 · useConfirm 危险态"
note="ConfirmContext.tsx · variant: destructive · 红图标+红标题+暂不/确认删除"
<ExampleCard
title="危险态(variant: destructive"
description="红图标 + 红标题 + 暂不 / 确认删除 —— 删除等不可逆操作"
>
<Button
variant="outline"
@@ -35,10 +30,10 @@ function UseConfirmDemos() {
>
</Button>
</Demo>
<Demo
label="C 套 · useConfirm 普通态"
note="ConfirmContext.tsx · variant: default · 橙色警示图标+主色确认"
</ExampleCard>
<ExampleCard
title="普通态(variant: default"
description="橙色警示图标 + 主色确认 —— 可逆但需用户知情的操作"
>
<Button
variant="outline"
@@ -51,272 +46,72 @@ function UseConfirmDemos() {
>
</Button>
</Demo>
</ExampleCard>
</>
);
}
/** One confirm-dialog demo replicating a real business usage. */
function ConfirmDemo({
label,
note,
title,
body,
selectText,
selectClasses,
isLoading,
showCloseButton = false,
}: {
label: string;
note?: string;
title: string;
body: ReactNode;
selectText: string;
selectClasses?: string;
isLoading?: boolean;
showCloseButton?: boolean;
}) {
return (
<Demo label={label} note={note}>
<OGDialog>
<OGDialogTrigger asChild>
<Button variant="outline"></Button>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={showCloseButton}
title={title}
className="max-w-[450px]"
main={
<Label className="text-left text-sm font-medium">{body}</Label>
}
selection={{
selectHandler: () => undefined,
selectClasses,
selectText,
isLoading,
}}
/>
</OGDialog>
</Demo>
);
}
export function ConfirmDialogSection() {
return (
<Section
id="confirm"
<ComponentPage
title="二次确认弹窗"
subtitle={
eng="Confirm Dialog"
description={
<>
/ / {' '}
<code>OGDialogTemplate selection</code> 13 7 UI + 6 {' '}
<code>useConfirm()</code>26 10
<b></b> UI SidePanel + Chat/Header Modal <b></b>B C 9 selectClasses
danger / primary
9 <code>AlertDialog</code> Modal
/ / {' '}
<code>useConfirm()</code> destructive / default <b>稿</b>
</>
}
whenToUse={[
<>
<code>useConfirm()</code><code>variant: destructive</code>
<code>default</code>
</>,
<>danger <code>#f53f3f</code> / primary </>,
<> ModalModal </>,
]}
>
{/* The three coexisting confirm-dialog systems */}
<div className="mb-6">
<CompareTable
head={['体系', '实现', '业务文件数', '用在哪', '样式一致性']}
rows={[
[
'B 套模板',
<>
<code>OGDialogTemplate</code> + <code>selection</code>
</>,
'13(原 21;剩余全是死 UI 或表单)',
'旧页面(会话/书签/Agent/设置/Prompt…LibreChat 血统)',
'差 · 确认按钮 9 种写法',
],
[
'C 套服务',
<>
<code>useConfirm()</code>ConfirmContext + AlertDialog
</>,
'26(收敛完成,含已迁入 10 处)',
'新页面(知识空间 / 订阅频道 / 权限)',
'好 · 样式集中在一个文件,destructive/default 两档',
],
]}
/>
</div>
<ExampleGroup title="两个变体">
<ExampleGrid cols={2}>
<UseConfirmDemos />
</ExampleGrid>
</ExampleGroup>
{/* Inventory table: every selectClasses variant found in business code */}
<div className="mb-6">
<ExampleGroup title="规格 Anatomy">
<CompareTable
head={['#', '确认按钮 selectClasses(原文)', '用处', '文件数']}
rows={[
[
'1',
<code key="c">bg-red-700 dark:bg-red-600 hover:bg-red-800 </code>,
'可达的 4 处已迁 C;剩 4 处全是死 UI(书签/分享弹窗/两个工具移除)',
'4(原 8)· 全死 UI',
],
[
'2',
<code key="c">bg-red-600 hover:bg-red-700 dark:hover:bg-red-800</code>,
'删除 Agent / Assistant —— 死 UISidePanel 被注释)',
'2(原 3)· 全死 UI',
],
[
'3',
<code key="c">bg-red-600 hover:bg-red-700 dark:hover:bg-red-600</code>,
'清空预设 —— 死 UIChat/Header 无人引用)',
'1 · 死 UI',
],
[
'4',
<code key="c">bg-destructive hover:bg-destructive/80</code>,
'清空聊天 / 删缓存 / 撤销密钥 —— ✅ 已全部迁 C 套',
'0(原 3',
],
[
'5',
<code key="c">bg-surface-destructive hover:bg-surface-destructive-hover</code>,
'删除版本 / 管理员确认 —— ✅ 已全部迁 C 套',
'0(原 2',
],
[
'6',
<code key="c">bg-green-500 hover:bg-green-600 text-white</code>,
'保存预设 / 保存 API Key(确认=绿色?!',
'2',
],
[
'7',
<>
<code>btn btn-primary</code> CSS
</>,
'提交密钥',
'1',
],
[
'8',
<code key="c">bg-surface-submit hover:bg-surface-submit-hover</code>,
'重命名保存',
'1',
],
[
'9',
<>
<code>bg-gray-800 dark:bg-gray-200</code>
</>,
'OGDialogTemplate 内置 defaultSelect',
'—',
],
]}
/>
</div>
{/* Anatomy after step-1 convergence — B shell/buttons now mirror the C look */}
<div className="mb-6">
<CompareTable
head={['部位', '对齐后的值(B 套 = C 套)', '备注']}
head={['部位', '值', '备注']}
rows={[
[
'弹窗容器',
<code key="c">rounded-2xl p-5 gap-4 border #ebebeb + </code>,
'圆角 16 / padding 20,与 C 套一致',
'圆角 16 / padding 20',
],
[
'遮罩',
<>
<code>bg-gray-500/90</code> + <code>backdrop-blur-md</code>
</>,
'灰底毛玻璃,与 C 套一致',
'灰底毛玻璃',
],
['标题', <code key="c">text-base font-medium leading-6</code>, '与 C 套一致'],
['标题', <code key="c">text-base font-medium leading-6</code>, ''],
[
'确认按钮',
<>
danger <code>#f53f3f</code> / primary <code>selectVariant</code>{' '}
selectClasses
</>,
'cva 档位 · 特例走 selectClasses 口子',
'特例可自定义',
],
[
'取消按钮',
<>
<code>hover:bg-[#f7f8fa]</code><code>focus-visible</code>
</>,
'与 C 套一致(含知识空间同款 hover)',
],
[
'待定项',
'宽度仍由各页传(max-w-[450px]/max-w-lg vs C 套 400px);Loading 仍有 4 处页面自塞 Spinner',
'随第三步迁移一并清理',
'',
],
]}
/>
</div>
<DemoGrid cols={3}>
<UseConfirmDemos />
<ConfirmDemo
label="① red-700 系(6 处,原 8"
note="删除书签 — Bookmarks/DeleteBookmarkButton.tsx(原例子删会话已迁 C 套)"
title="删除书签"
body={
<>
<strong></strong>
</>
}
selectText="删除"
selectClasses="bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white"
/>
<ConfirmDemo
label="② red-600 系(2 处,原 3"
note="删除 Agent — SidePanel/Agents/DeleteButton.tsx"
title="删除助手"
body="确定要删除这个助手吗?此操作不可撤销。"
selectText="删除"
selectClasses="bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white"
/>
<ConfirmDemo
label="⑥ 绿色确认(2 处)"
note="保存预设 — Endpoints/SaveAsPresetDialog.tsx"
title="另存为预设"
body="将当前配置保存为预设?"
selectText="保存"
selectClasses="bg-green-500 hover:bg-green-600 dark:hover:bg-green-600 text-white"
showCloseButton
/>
<ConfirmDemo
label="⑨ 模板默认(不传 selectClasses"
note="OGDialogTemplate 内置 defaultSelect:黑底/暗色反白"
title="确认操作"
body="这是不传 selectClasses 时的默认确认按钮。"
selectText="确认"
/>
<ConfirmDemo
label="Loading 态(isLoading: true"
note="模板内置 Spinner · 各页自塞 Spinner 的写法已随迁移清零(原 4 处)"
title="删除会话"
body="确认按钮处于加载中。"
selectText="删除"
selectClasses="bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white"
isLoading
/>
<ConfirmDemo
label="⑧ surface-submit1 处)"
note="重命名保存 — Prompts/Groups/DashGroupItem.tsx"
title="重命名"
body="保存新的名称?"
selectText="保存"
selectClasses="bg-surface-submit hover:bg-surface-submit-hover text-white disabled:hover:bg-surface-submit"
/>
<ConfirmDemo
label="⑦ btn btn-primary1 处)"
note="提交密钥 — SetKeyDialog.tsx · 走全局 CSS 类"
title="设置密钥"
body="提交这个 API Key"
selectText="提交"
selectClasses="btn btn-primary"
/>
</DemoGrid>
</Section>
</ExampleGroup>
</ComponentPage>
);
}
@@ -10,47 +10,52 @@
*/
import { useState } from 'react';
import { MessageFeedbackButtons } from '~/components/Chat/MessageFeedbackButtons';
import { Section, Demo, DemoGrid } from '../components/kit';
import { ComponentPage, ExampleGroup, ExampleGrid, ExampleCard } from '../components/kit';
function LoggedDemo({ liked }: { liked?: number }) {
const [last, setLast] = useState<string>('—');
return (
<div className="flex items-center gap-4">
<MessageFeedbackButtons
liked={liked}
onLike={(l) => setLast(`onLike(${l})`)}
onDislikeComment={(c) => setLast(`onDislikeComment("${c}")`)}
/>
<span className="text-xs text-muted-foreground">{last}</span>
</div>
);
const [last, setLast] = useState<string>('—');
return (
<div className="flex items-center gap-4">
<MessageFeedbackButtons
liked={liked}
onLike={(l) => setLast(`onLike(${l})`)}
onDislikeComment={(c) => setLast(`onDislikeComment("${c}")`)}
/>
<span className="text-caption text-muted-foreground">{last}</span>
</div>
);
}
export function FeedbackSection() {
return (
<Section
id="feedback"
title="点赞 / 点踩反馈"
subtitle={
<>
<code>MessageFeedbackButtons</code> 6 AI / /
3 / / appChat<b></b>
= 12 / 20 / 32 · 14px ·
400 · 6
</>
}
>
<DemoGrid cols={3}>
<Demo label="初始未评价" note="点踩先弹窗,提交后才高亮;取消不留痕">
<LoggedDemo />
</Demo>
<Demo label="已点赞态(liked=1" note="点踩弹窗取消后应保持点赞高亮">
<LoggedDemo liked={1} />
</Demo>
<Demo label="已点踩态(liked=2" note="再点踩=直接取消(onLike(0)),不弹窗">
<LoggedDemo liked={2} />
</Demo>
</DemoGrid>
</Section>
);
return (
<ComponentPage
title="点赞 / 点踩反馈"
eng="Message Feedback"
description={
<>
<code>MessageFeedbackButtons</code> 6 AI / /
3 / / appChat
</>
}
whenToUse={[
<><b></b>= </>,
<> = </>,
<> 12 / 20 / 32 · 14px · 400 · 6</>,
]}
>
<ExampleGroup title="三种初始状态">
<ExampleGrid cols={3}>
<ExampleCard title="初始未评价" description="点踩先弹窗,提交后才高亮;取消不留痕">
<LoggedDemo />
</ExampleCard>
<ExampleCard title="已点赞态(liked=1" description="点踩弹窗取消后应保持点赞高亮">
<LoggedDemo liked={1} />
</ExampleCard>
<ExampleCard title="已点踩态(liked=2" description="再点踩 = 直接取消,不弹窗">
<LoggedDemo liked={2} />
</ExampleCard>
</ExampleGrid>
</ExampleGroup>
</ComponentPage>
);
}
@@ -0,0 +1,185 @@
/**
* Illustration section — all themed inline-SVG illustrations from
* src/components/illustrations/, rendered side by side in the three color
* modes (blue theme / green theme / grey draft) so the designer can audit the
* whole set at a glance. Spec source: docs-ui-refactor/基础-色彩规范.md §5 +
* BRAND-THEME-HANDOFF.md §3.1.
*
* DEV-ONLY internal tooling, never shipped (route gated by import.meta.env.DEV).
* NOTE on literal RGB values in this file: DISPLAY-ONLY spec pins (same
* precedent as ColorSection's swatches) — a descendant can't un-inherit
* html.theme-green, so the blue tab pins the :root palette to stay
* deterministic whatever global theme the viewer has switched to.
* Business code must never hardcode illustration colors — always --illus-*.
*/
import { ComponentType, CSSProperties, useState } from 'react';
import { cn } from '~/utils';
import {
ArticleQAIllustration,
CrawlingIllustration,
EmptyStateIllustration,
ListWebLinkIllustration,
NoPermissionIllustration,
SuccessIllustration,
SystemMaintenanceIllustration,
} from '~/components/illustrations';
import { ComponentPage, ExampleGroup, ExampleGrid, ExampleCard, CompareTable } from '../components/kit';
/* ------------------------------------------------------------------ *
* Color modes
* ------------------------------------------------------------------ */
type ColorMode = 'blue' | 'green' | 'grey';
const MODES: { id: ColorMode; label: string; hint: string }[] = [
{ id: 'blue', label: '蓝主题', hint: '默认主题。' },
{ id: 'green', label: '绿主题', hint: '全局切换绿主题时,插画自动变绿。' },
{ id: 'grey', label: '灰稿', hint: '主题无关的灰稿,整图降至 80% 不透明度。' },
];
/** Mirrors :root in style.css — display-only pin for the blue tab (see header note). */
const BLUE_ILLUS_VARS = {
'--illus-100': '190 218 255',
'--illus-300': '106 161 255',
'--illus-500': '22 93 255',
} as CSSProperties;
/* ------------------------------------------------------------------ *
* Illustration inventory — everything exported from ~/components/illustrations
* ------------------------------------------------------------------ */
/* KnowledgeSpaceIcon is deliberately NOT shown here: it's a themed icon with two
bespoke drawings switched by the Recoil brand theme, not an --illus-* illustration. */
interface IllustrationDef {
name: string;
usage: string;
Comp: ComponentType<{ className?: string; grey?: boolean }>;
}
const ILLUSTRATIONS: IllustrationDef[] = [
{
name: 'EmptyStateIllustration',
usage: '通用空状态 — 列表 / 成员 / 频道等无数据',
Comp: EmptyStateIllustration,
},
{
name: 'NoPermissionIllustration',
usage: '无权限访问 / 内容待审核不可见',
Comp: NoPermissionIllustration,
},
{
name: 'ListWebLinkIllustration',
usage: '列表网页链接 — 按名称搜索无收录空态',
Comp: ListWebLinkIllustration,
},
{
name: 'CrawlingIllustration',
usage: '爬取中 — 网页爬取等待态',
Comp: CrawlingIllustration,
},
{
name: 'SuccessIllustration',
usage: '成功态 — 跟随品牌主题',
Comp: SuccessIllustration,
},
{
name: 'ArticleQAIllustration',
usage: '文章问答场景',
Comp: ArticleQAIllustration,
},
{
name: 'SystemMaintenanceIllustration',
usage: '系统维护 — 后端 500 全屏维护弹层',
Comp: SystemMaintenanceIllustration,
},
];
/* ------------------------------------------------------------------ *
* Page
* ------------------------------------------------------------------ */
export function IllustrationSection() {
const [mode, setMode] = useState<ColorMode>('blue');
const activeMode = MODES.find((m) => m.id === mode)!;
return (
<ComponentPage
title="插画"
eng="Illustration"
description={
<>
/ UI
绿稿
</>
}
whenToUse={[
<> / / / PNG</>,
<>稿 <code>grey</code> 80% </>,
<>UI </>,
]}
>
<ExampleGroup title="调色板三态">
<CompareTable
head={['档', '蓝主题(默认)', '绿主题', '灰稿']}
rows={[
['浅底', <Swatch hex="#BEDAFF" key="b" />, <Swatch hex="#DDF0E8" key="g" />, <Swatch hex="#E5E5E5" key="y" />],
['中间调', <Swatch hex="#6AA1FF" key="b" />, <Swatch hex="#A2D7B5" key="g" />, <Swatch hex="#FFFFFF" key="y" />],
['主体', <Swatch hex="#165DFF" key="b" />, <Swatch hex="#169C47" key="g" />, <Swatch hex="#BCBCBC" key="y" />],
['整图不透明度', '100%', '100%', '80%'],
]}
/>
</ExampleGroup>
<ExampleGroup title="全部插画">
{/* Local segmented control — same recipe as the gallery sidebar's mode switch:
36px total = 1px border + 2px padding + 30px options; 2px gap; radius 8 outer / 6 inner. */}
<div className="mb-2 inline-grid h-9 grid-cols-3 gap-0.5 rounded-lg border border-border-base p-0.5">
{MODES.map((m) => (
<button
key={m.id}
onClick={() => setMode(m.id)}
className={cn(
'h-[30px] rounded-md px-4 text-body-sm transition-colors',
mode === m.id
? 'bg-blue-500/[0.08] font-medium text-blue-500'
: 'text-muted-foreground hover:bg-muted/60',
)}
>
{m.label}
</button>
))}
</div>
<p className="mb-4 text-body-sm text-muted-foreground">{activeMode.hint}</p>
<div
className={mode === 'green' ? 'theme-green' : undefined}
style={mode === 'blue' ? BLUE_ILLUS_VARS : undefined}
>
<ExampleGrid cols={4}>
{ILLUSTRATIONS.map(({ name, usage, Comp }) => (
<ExampleCard key={name} title={name} description={usage}>
<div className="flex w-full justify-center">
<Comp className="size-[120px]" grey={mode === 'grey'} />
</div>
</ExampleCard>
))}
</ExampleGrid>
</div>
</ExampleGroup>
</ComponentPage>
);
}
/** Inline swatch + hex label for the palette table (display-only spec values). */
function Swatch({ hex }: { hex: string }) {
return (
<div className="flex items-center gap-2">
<span
className="inline-block h-6 w-10 shrink-0 rounded border border-black/10"
style={{ background: hex }}
/>
<code className="whitespace-nowrap text-caption text-muted-foreground">{hex}</code>
</div>
);
}
@@ -1,234 +1,50 @@
/**
* Modal / Dialog gallery — DEV-ONLY. See docs-ui-refactor/组件-Modal弹窗.md.
* Modal spec page — DEV-ONLY. See docs-ui-refactor/组件-Modal弹窗.md.
*
* Fresh survey (2026-07-09): FIVE coexisting modal populations. The same demo
* content (title + description + input + cancel/confirm footer) is mounted into
* each shell so the shell differences (overlay / radius / padding / title /
* buttons) are the only variable when opening them side by side.
* The unified modal standard is NOT finalized yet — this page documents what IS
* decided (the C-set shell candidate) plus interim guidance, and clearly marks
* the open questions. The full population survey and side-by-side shell
* comparison live in progress/ModalProgress.tsx (「迁移进度 → Modal」).
*/
import { Button } from '~/components/ui/Button';
import { Input } from '~/components/ui/Input';
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from '~/components/ui/Dialog';
import {
OGDialog,
OGDialogTrigger,
OGDialogContent,
OGDialogHeader,
OGDialogTitle,
OGDialogDescription,
} from '~/components/ui/OriginalDialog';
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
} from '~/components/ui/AlertDialog';
import DialogTemplate from '~/components/ui/DialogTemplate';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useConfirm } from '~/Providers';
import { Section, Demo, DemoGrid, CompareTable } from '../components/kit';
/** Identical body for every shell so only the shell itself differs. */
const demoBody = (
<div className="flex flex-col gap-3">
<p className="text-sm text-text-primary">
/
</p>
<Input placeholder="示例输入框" />
</div>
);
/** Small reference demo of the finalized confirm dialog, for shell comparison. */
function ConfirmReferenceDemo() {
const confirm = useConfirm();
return (
<Button
variant="outline"
onClick={() =>
confirm({
variant: 'destructive',
title: '确认删除',
description: '弹窗壳视觉参照:圆角16 / p-5 / 灰底毛玻璃遮罩。',
confirmText: '确认删除',
})
}
>
</Button>
);
}
import { ComponentPage, ExampleGroup, ExampleGrid, ExampleCard, CompareTable } from '../components/kit';
export function ModalSection() {
return (
<Section
id="modal"
<ComponentPage
title="Modal 弹窗"
subtitle={
eng="Modal"
description={
<>
2026-07-09 <b>5 64 </b>
B C A 22 + 3
AlertDialog7
<b>稿</b>
稿"已定" Modal
</>
}
whenToUse={[
<>
<b> · </b> 16<code>rounded-2xl</code>/ p-5 /
<code>bg-gray-500/90</code> + blur/ <code>font-medium</code>
</>,
<>
<b></b> <code>OGDialogTemplate</code>
/ <code>AlertDialog</code>
</>,
<> / <code>useConfirm()</code></>,
<>footer </>,
]}
>
{/* ① Population overview */}
<h3 className="mb-3 mt-2 text-base font-semibold text-text-primary">
ui/
</h3>
<div className="mb-8">
<CompareTable
head={['体系', '实现', '业务文件数', '用在哪 / 备注']}
rows={[
[
'A 套 · 原语直接拼',
'Dialog + DialogContent 手拼',
<b key="a1">22</b>,
'新页面为主:知识库 8、订阅 2、审批/通知/账号/分享/appChat…(含 MainLayout 全局弹窗)',
],
[
'A 套 · 模板',
'DialogTemplate',
'3',
'EditPresetDialog、PresetItems、ContextButton(后者在 SidePanel 死树)',
],
[
'B 套 · 模板',
'OGDialogTemplate',
'16(原 25,确认迁移后)',
'书签/导出/SetKey/归档/Agent 面板…(其中 SidePanel 死树约 6 处);壳已对齐 C 套',
],
[
'B 套 · 原语直接拼',
'OGDialog + OGDialogContent 手拼',
'16',
'设置(账号/数据)、Prompts、文件预览、ShareAgent…;壳同上(已对齐 C 套)',
],
[
'手拼 AlertDialog',
'AlertDialogContent + 自拼头尾',
'7',
'频道成员 2、爬取系 4、灵思 TaskModeInput(部分带确认性质,本期一并处理)',
],
[
'C 套 · useConfirm(参照)',
'ConfirmContextAlertDialog 底层)',
'26(已收敛 ✅)',
'二次确认已定稿的视觉基准:圆角16 / p-5 / 灰底毛玻璃 —— Modal 壳的天然候选',
],
]}
/>
</div>
{/* ② Shell anatomy */}
<h3 className="mb-3 text-base font-semibold text-text-primary">
</h3>
<div className="mb-8">
<CompareTable
head={['维度', 'A 套 Dialog', 'B 套 OriginalDialog(已对齐 C 套)', 'AlertDialog(手拼底座)']}
rows={[
[
'遮罩',
'bg-black/40 + blur(浅黑毛玻璃)',
'bg-gray-500/90 + blur(灰白毛玻璃)',
'bg-gray-500/90 + blur(同 B',
],
['层级 z-index', 'z-[100]', 'z-50', 'z-[110]'],
['圆角', 'sm:rounded-lg8px,移动端直角)', 'rounded-2xl16px', 'sm:rounded-lg8px,移动端直角)'],
['内边距', 'p-520px', 'p-520px', 'p-624px'],
[
'边框 / 阴影',
'border + shadow-lg',
'border #ebebeb + 淡投影',
'无边框、无阴影',
],
[
'标题',
'text-base font-semibold',
'text-base font-medium',
'组件是 text-lg semibold,但各页多自拼标题行',
],
[
'关闭按钮 ×',
'内置右上(可关)',
'内置右上(可关)',
'无内置,各页自拼',
],
[
'深色模式底色',
'dark:bg-[#303134](写死)',
'bg-background(跟主题)',
'dark:bg-gray-900',
],
[
'移动端行为',
'居中缩放出现',
'居中缩放出现',
'从底部滑入、贴底',
],
[
'footer 按钮',
'各页自拼(多为 Button outline + default',
'模板:取消白底描边 + 确认 danger/primary 档;直接拼的各页自理',
'各页自拼(红 #F53F3F 等)',
],
]}
/>
</div>
{/* ③ Side-by-side demos — identical content, different shells */}
<h3 className="mb-3 text-base font-semibold text-text-primary">
</h3>
<div className="mb-8">
<DemoGrid cols={3}>
{/* A-set raw primitives — the largest population */}
<Demo label="A 套 · 原语直接拼(22 处)" note="ui/Dialog.tsx · 浅黑毛玻璃 · 圆角8 · p-5">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline"></Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{demoBody}
<DialogFooter>
<DialogClose asChild>
<Button variant="outline"></Button>
</DialogClose>
<Button></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Demo>
{/* A-set template — legacy default black confirm button */}
<Demo label="A 套 · DialogTemplate3 处)" note="旧模板 · 默认黑底确认钮 · 正文额外 px-6">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline"></Button>
</DialogTrigger>
<DialogTemplate
title="弹窗标题"
description="标题下的说明文字。"
main={demoBody}
selection={{ selectHandler: () => null, selectText: '确定' }}
/>
</Dialog>
</Demo>
{/* B-set template — shell already aligned with C-set */}
<Demo label="B 套 · OGDialogTemplate16 处)" note="壳已对齐 C 套 · 圆角16 · 取消/确认已统一">
<ExampleGroup title="基准候选壳" subtitle="当前推荐的新弹窗写法。">
<ExampleGrid cols={2}>
<ExampleCard
title="标准结构:标题 + 正文 + 取消/确定"
description="圆角 16 / p-5 / 灰底毛玻璃。"
>
<OGDialog>
<OGDialogTrigger asChild>
<Button variant="outline"></Button>
@@ -237,7 +53,14 @@ export function ModalSection() {
title="弹窗标题"
description="标题下的说明文字。"
className="max-w-md"
main={demoBody}
main={
<div className="flex flex-col gap-3">
<p className="text-body text-text-primary">
/
</p>
<Input placeholder="示例输入框" />
</div>
}
selection={{
selectHandler: () => null,
selectVariant: 'primary',
@@ -245,91 +68,26 @@ export function ModalSection() {
}}
/>
</OGDialog>
</Demo>
</ExampleCard>
</ExampleGrid>
</ExampleGroup>
{/* B-set raw primitives — same shell, hand-rolled body/footer */}
<Demo label="B 套 · OG 原语直接拼(16 处)" note="壳同左 · 头尾各页自拼(设置/Prompts 老页面)">
<OGDialog>
<OGDialogTrigger asChild>
<Button variant="outline"></Button>
</OGDialogTrigger>
<OGDialogContent className="max-w-md">
<OGDialogHeader>
<OGDialogTitle></OGDialogTitle>
<OGDialogDescription></OGDialogDescription>
</OGDialogHeader>
{demoBody}
<div className="flex justify-end gap-2">
<Button variant="outline"></Button>
<Button></Button>
</div>
</OGDialogContent>
</OGDialog>
</Demo>
{/* Hand-rolled AlertDialog population */}
<Demo
label="手拼 AlertDialog7 处)"
note="p-6 · 圆角8 · 无边框阴影 · z-110 · 移动端贴底滑入"
>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline"></Button>
</AlertDialogTrigger>
<AlertDialogContent className="max-w-md">
{/* Business pages hand-roll header/footer like this (ChannelMemberDialog etc.) */}
<h3 className="text-base font-medium text-text-primary"></h3>
{demoBody}
<div className="flex justify-end gap-2">
<Button variant="outline"></Button>
<Button></Button>
</div>
</AlertDialogContent>
</AlertDialog>
</Demo>
{/* C-set reference — the finalized confirm shell */}
<Demo
label="C 套 · useConfirm 参照(已定稿)"
note="二次确认基准壳:圆角16 / p-5 / 灰底毛玻璃 —— Modal 壳候选"
>
<ConfirmReferenceDemo />
</Demo>
</DemoGrid>
</div>
{/* ④ Decision checklist */}
<h3 className="mb-3 text-base font-semibold text-text-primary"> </h3>
<div className="rounded-xl border border-border-light bg-muted/20 p-5 text-sm leading-7 text-text-primary">
<ol className="list-decimal space-y-1 pl-5">
<li>
<b></b>A black/40+blur vs B/C gray-500/90+blur
black/80
</li>
<li>
<b></b>8pxA / AlertDialog vs 16pxB/C /
</li>
<li>
<b></b>p-520pxA/B/C vs p-624pxAlertDialogheader/body/footer gap-4
</li>
<li>
<b></b>font-semiboldA vs font-mediumB/C
</li>
<li>
<b> ×</b> A/B ×AlertDialog
</li>
<li>
<b>footer </b>/ Button outline+default C
+ danger/primary gap-2 vs gap-3
</li>
<li>
<b></b>z-50 / z-[100] / z-[110] Drawer/Sheet/Popover
</li>
<li>
<b></b>A 22 A B
</li>
</ol>
</div>
</Section>
<ExampleGroup title="壳规格(已定部分)" subtitle="与二次确认弹窗同一套壳。">
<CompareTable
head={['部位', '值', '备注']}
rows={[
['圆角', <code key="c">rounded-2xl16px</code>, '移动端是否保留直角/贴底待定'],
['内边距', <code key="c">p-520px· gap-4</code>, ''],
['遮罩', <><code>bg-gray-500/90</code> + <code>backdrop-blur-md</code></>, '灰底毛玻璃'],
['边框 / 阴影', <code key="c">border #ebebeb + </code>, ''],
['标题', <code key="c">text-base font-medium leading-6</code>, ''],
['取消 / 确认按钮', '白底描边 + danger/primary 两档', '与二次确认一致'],
]}
/>
<p className="mt-3 text-body-sm text-muted-foreground">
/ / z-indexfooter
</p>
</ExampleGroup>
</ComponentPage>
);
}
@@ -1,26 +1,120 @@
/** Gallery overview — DEV-ONLY. High-level status of the unification effort. */
import { Section, CompareTable } from '../components/kit';
/**
* Spec overview — DEV-ONLY. The entry page of the 「设计规范」 mode: what this
* design system is, the design principles behind it, and an index of every spec
* page with its maturity. Migration status lives in the 「迁移进度」 mode.
*
* The 设计原则 block follows ant.design/docs/spec/values-cn's editorial layout:
* a dashed derivation strip (原则 → 基础规范 → 组件规范) on top, then each
* principle as a numbered heading + prose paragraph — no bordered cards.
*/
import { ComponentType } from 'react';
import { Outlined } from 'bisheng-icons';
import { ComponentPage, ExampleGroup } from '../components/kit';
/** The cross-cutting principles every individual spec derives from. */
const PRINCIPLES: { title: string; body: string }[] = [
{
title: 'Semantic token 优先',
body: '字号、颜色、尺寸一律用语义类(text-body / text-text-2 / size="medium"),不写裸值、不写 hex。同一语义只允许一个值——新颜色先在色板找替代,确需新增走 token 评审。',
},
{
title: '档位化,不自由定制',
body: '组件用档位(size / color / variant)承载常见变化,页面按需选档;极特殊的一次性情况用 className 覆盖,不为它新增变体、不改源码。',
},
{
title: '一套组件一条代码路径',
body: '桌面与移动共用同一组件,适配全走 media query 与既有 hooks;不引入移动端组件库、不做第二套移动端组件。',
},
{
title: '品牌跟主题,语义色恒定',
body: '品牌色永远走 blue-* 类(已重指向 --brand-*,自动蓝⇄绿换肤),禁止硬编码品牌 hex;成功/警告/危险等语义色不参与换肤。',
},
{
title: '触屏与窄屏分口径',
body: 'hover 禁用与触达面积跟"输入方式"pointer: coarse,跟手不跟屏);布局重排与字号重映射跟"屏幕宽度"(768px)。两种判定别混用。',
},
{
title: '状态即规范',
body: 'hover / active / disabled / loading / focus 是组件规范的一部分,由组件内置(如 Button 的 loading 属性),业务页不自拼状态样式。',
},
{
title: '滚动条跟随系统',
body: '任何滚动区域默认不写 ::-webkit-scrollbar / scrollbar-width——原生滚动条天然跟随系统的显隐设置(始终显示 / 滚动时显示),写了就会强制常驻。例外只允许"更少可见"的三个既有 utilityno-scrollbar / scroll-on-scroll / scroll-no-hover),详见 基础-滚动条规范.md。',
},
];
/** 原则 → 基础规范 → 组件规范 derivation strip, antd-values style. */
const DERIVATION: { Icon: ComponentType; title: string; caption: string }[] = [
{ Icon: Outlined.Bulb, title: '设计原则', caption: '取舍与评价的统一依据' },
{ Icon: Outlined.Book, title: '基础规范', caption: '字体 / 色彩 / 多端适配 token' },
{ Icon: Outlined.ViewGridCard, title: '组件规范', caption: '各组件的档位、用法与状态' },
];
function DerivationStrip() {
return (
<div className="mb-12 flex flex-col items-center gap-4 rounded-xl bg-muted/20 px-8 py-7 sm:flex-row sm:gap-0">
{DERIVATION.map(({ Icon, title, caption }, i) => (
<div key={title} className="contents">
{i > 0 && (
<div className="hidden min-w-8 flex-1 items-center px-3 sm:flex" aria-hidden>
<span className="h-px flex-1 border-t border-dashed border-border-deep" />
<span className="-ml-px text-text-4">
<Outlined.Right />
</span>
</div>
)}
<div className="flex w-40 flex-col items-center text-center">
<span className="flex size-11 items-center justify-center rounded-full bg-blue-500/[0.08] text-h3 text-blue-500 [&_svg]:size-5">
<Icon />
</span>
<div className="mt-2.5 text-h4 text-text-primary">{title}</div>
<div className="mt-0.5 text-caption text-muted-foreground">{caption}</div>
</div>
</div>
))}
</div>
);
}
export function OverviewSection() {
return (
<Section
id="overview"
title="组件统一化 · 总览"
subtitle="本页仅在开发环境可见,用户看不到。改这里用到的组件 = 改真实业务组件,全站同步生效。"
<ComponentPage
title="设计规范"
eng="Overview"
description={
<>
BiSheng client token / /
<b></b>
</>
}
whenToUse={[
<> = 使使+ /token + </>,
<>稿 Modal"已定/待定"</>,
<> / 使</>,
]}
bodyTitle={null}
>
<p className="mb-4 max-w-3xl text-sm text-text-primary">
client
<code>docs-ui-refactor/00-.md</code>
</p>
<CompareTable
head={['组件', '状态', '现有版本', '收敛基准']}
rows={[
['Modal 弹窗', '🟨 进行中', '4(含 1 死代码)', '待定 · OGDialogTemplate 候选'],
['Select / 下拉', '⬜ 待办', '多个', '待定'],
['Button 按钮', '⬜ 待办', '1(已 cva 化,较好)', 'Button.tsx'],
['Dropdown 菜单', '⬜ 待办', '2', '待定'],
]}
/>
</Section>
<ExampleGroup
title="设计原则"
subtitle="原则推导出基础规范,基础规范支撑组件规范;遇到规范没覆盖的场景,回到这七条判断。"
>
<DerivationStrip />
<div className="space-y-9">
{PRINCIPLES.map((p, i) => (
<div key={p.title} className="flex gap-5">
<span className="w-9 shrink-0 pt-0.5 text-h2 font-medium tabular-nums leading-7 text-blue-500">
{String(i + 1).padStart(2, '0')}
</span>
<div>
<h4 className="text-h3 text-text-primary">{p.title}</h4>
<p className="mt-1.5 max-w-2xl text-body text-text-2">{p.body}</p>
</div>
</div>
))}
</div>
</ExampleGroup>
</ComponentPage>
);
}
@@ -0,0 +1,102 @@
/**
* Multi-device adaptation principles — DEV-ONLY.
* Mirrors docs-ui-refactor/基础-多端适配原则.md: cross-component responsive rules
* (touch vs width criteria, hover/active, hit-area, one code path). Principles-only
* page — no live component demos, so it's table/text driven.
*/
import { ComponentPage, ExampleGroup, CompareTable } from '../components/kit';
export function ResponsiveSection() {
return (
<ComponentPage
title="多端适配"
eng="Responsive"
description={
<>
Web + /H5<b> App</b>
</>
}
whenToUse={[
<>
<b></b><code></code> hover
<code></code>
</>,
<> antd-mobile CSS hooks</>,
<> 1416 16px</>,
]}
bodyTitle={null}
>
<ExampleGroup title="① 两种判定口径">
<CompareTable
head={['口径', '写法', '判定什么', '用于']}
rows={[
[
'输入方式',
<code key="a">@media (hover: none) and (pointer: coarse)</code>,
'是不是触屏(无悬停、手指粗)',
'hover 禁用、触达面积 — 跟手不跟屏(触屏笔记本/平板横屏也生效)',
],
[
'屏幕宽度',
<code key="b">@media (max-width: 768px)</code>,
'屏幕窄不窄',
'布局重排(等宽平铺、block、贴底弹层)、字号重映射',
],
]}
/>
</ExampleGroup>
<ExampleGroup title="② 四条核心原则">
<CompareTable
head={['原则', '怎么做', '依据']}
rows={[
[
'触屏禁 hover,用 active 补偿',
'触屏下禁用 hover 态,按下反馈交给 activeactive 深档只在触屏生效。',
'移动组件库共识',
],
[
'触达面积 ≥ 44×44px',
'视觉尺寸不变,用透明伪元素 / padding 撑热区;小控件高频触屏场景直接升档。',
'WCAG · iOS HIG · Material',
],
[
'控件字号不随正文抬升',
'按钮 / 标签 / 菜单项保持桌面值(≥14);唯一例外输入框 ≥16px。',
'字体规范',
],
[
'一套组件一条代码路径',
'适配全走 media query / sm: 前缀,在同一组件内完成。',
'字体规范同思路',
],
]}
/>
</ExampleGroup>
<ExampleGroup title="③ 窄屏布局惯例">
<CompareTable
head={['场景', '窄屏', '桌面']}
rows={[
['弹层 footer 按钮', <> <code key="a">flex-1</code></>, <> <code key="b">sm:flex-none</code></>],
['主操作', '占满整行 block', '自适应宽'],
['弹层容器', <> <code key="c">calc(100% - 2rem)</code> </>, '居中固定宽'],
]}
/>
</ExampleGroup>
<ExampleGroup title="④ 组件适配细则索引" subtitle="组件专属细则在各组件文档的「移动端适配」节。">
<CompareTable
head={['组件', '细则位置']}
rows={[
['Button', '组件-Button按钮.md'],
['Modal / 字体 / Select …', '各组件文档「移动端适配」节'],
]}
/>
</ExampleGroup>
</ComponentPage>
);
}
@@ -0,0 +1,150 @@
/**
* Typography section — showcases the semantic type scale from
* docs-ui-refactor/基础-字体规范.md so the designer can visually verify it.
*
* DEV-ONLY internal tooling, never shipped (route gated by import.meta.env.DEV).
* The classes shown here (text-caption ... text-metric) are real Tailwind
* classes generated from theme.extend.fontSize + CSS vars in src/style.css.
* Resize the window below 768px to see the mobile remap (same classNames).
*/
import { ComponentPage, ExampleGroup, CompareTable } from '../components/kit';
interface TypeStep {
/** Tailwind className, which is also the semantic token name. */
cls: string;
/** Desktop size / line-height in px. */
desktop: string;
/** Mobile (≤768px) size / line-height in px. */
mobile: string;
weight: 400 | 500;
usage: string;
}
/** Ordered small → large, matching the spec's §2 semantic table. */
const SCALE: TypeStep[] = [
{ cls: 'text-caption', desktop: '12 / 20', mobile: '12 / 20', weight: 400, usage: '时间戳、标签、水印' },
{ cls: 'text-body-sm', desktop: '13 / 21', mobile: '14 / 22', weight: 400, usage: '密集表格、侧栏次要项' },
{ cls: 'text-body', desktop: '14 / 22', mobile: '16 / 24', weight: 400, usage: '正文基准,表单、表格默认' },
{ cls: 'text-h4', desktop: '16 / 24', mobile: '16 / 24', weight: 500, usage: '强调正文、四级标题' },
{ cls: 'text-h3', desktop: '18 / 26', mobile: '17 / 25', weight: 500, usage: '卡片标题' },
{ cls: 'text-h2', desktop: '20 / 28', mobile: '18 / 26', weight: 500, usage: '区块标题' },
{ cls: 'text-h1', desktop: '24 / 32', mobile: '22 / 30', weight: 500, usage: '页面标题' },
{ cls: 'text-display', desktop: '30 / 38', mobile: '26 / 34', weight: 500, usage: '大标题、营销场景' },
{ cls: 'text-metric', desktop: '36 / 44', mobile: '30 / 38', weight: 500, usage: 'Dashboard 核心指标数字' },
];
const SAMPLE_ZH = '毕昇平台让大模型应用触手可及';
const SAMPLE_EN = 'The quick brown fox jumps over the lazy dog 0123456789';
interface FontStack {
/** Tailwind className (also usable in plain CSS via the stack value). */
cls: string;
/** Design token name in the spec (§1). */
token: string;
usage: string;
/** The actual comma-joined stack, for display. */
stack: string;
/** Live sample lines rendered with this stack. */
samples: string[];
}
/** §1 font stacks — pure system fonts, zero webfont loading. */
const FONT_STACKS: FontStack[] = [
{
cls: 'font-sans',
token: 'font-family-base',
usage: '全局默认(已写入 body/html,无需显式加类)',
stack:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif',
samples: [SAMPLE_ZH, SAMPLE_EN],
},
{
cls: 'font-mono',
token: 'font-family-mono',
usage: 'ID、代码、日志',
stack: 'ui-monospace, "SF Mono", "Cascadia Mono", Consolas, "Liberation Mono", monospace',
samples: ['wf_a1b2c3 · 0123456789', 'const answer = 42; // code sample'],
},
];
export function TypographySection() {
return (
<ComponentPage
title="字体 Typography"
eng="Typography"
description="九档 semantic 字号(自带字重)+ 两档字重,纯系统字体栈。窗口缩到 ≤768px 可看移动端重映射。"
whenToUse={[
<> semantic <code>text-body</code> / <code>text-h1</code> <code>text-sm</code> </>,
<> <code>font-normal</code>(400) / <code>font-medium</code>(500) 600/700</>,
<>使h1h2h3</>,
<>/ <code>tabular-nums</code>ID <code>font-mono</code></>,
]}
>
<ExampleGroup title="字体栈 Font Family" subtitle="纯系统字体栈,零加载成本。">
<CompareTable
head={['Token / 类名', '实时示例 + 完整字体栈', '用途']}
rows={FONT_STACKS.map((f) => [
<div key="t" className="whitespace-nowrap">
<code className="text-body-sm text-text-primary">{f.token}</code>
<div className="mt-0.5 text-caption">
<code>{f.cls}</code>
</div>
</div>,
<div key="s" className="min-w-[20rem]">
<div className="text-text-primary">
{f.samples.map((s) => (
<div key={s} className={`${f.cls} truncate text-h4 font-normal`}>
{s}
</div>
))}
</div>
<div className="mt-1.5 break-all font-mono text-caption">{f.stack}</div>
</div>,
f.usage,
])}
/>
</ExampleGroup>
<ExampleGroup title="字号阶梯 Semantic" subtitle="移动列在窗口 ≤768px 时生效。">
<CompareTable
head={['Token', '桌面 (size/line)', '移动 (size/line)', '字重', '示例']}
rows={SCALE.map((step) => [
<code key="t" className="whitespace-nowrap text-body-sm text-text-primary">
{step.cls}
</code>,
<span key="d" className="whitespace-nowrap tabular-nums">{step.desktop}</span>,
<span key="m" className="whitespace-nowrap tabular-nums">{step.mobile}</span>,
String(step.weight),
<div key="s" className={`${step.cls} text-text-primary`}>{step.usage}</div>,
])}
/>
</ExampleGroup>
{/* Font weights — the only two allowed steps */}
<ExampleGroup title="字重 Font Weight" subtitle="只用 400 / 500,禁用 600 / 700。">
<CompareTable
head={['Token / 类名', '值', '示例']}
rows={[
{ cls: 'font-normal', token: 'font-weight-regular', weight: 400, usage: '正文、说明' },
{ cls: 'font-medium', token: 'font-weight-medium', weight: 500, usage: '标题、强调、按钮' },
].map((w) => [
<div key="t" className="whitespace-nowrap">
<code className="text-body-sm text-text-primary">{w.token}</code>
<div className="mt-0.5 text-caption">
<code>{w.cls}</code>
</div>
</div>,
String(w.weight),
/* text-h3 carries fontWeight 500; inline style deterministically overrides it */
<div key="s" className="text-h3 text-text-primary" style={{ fontWeight: w.weight }}>
{w.usage}
</div>,
])}
/>
</ExampleGroup>
</ComponentPage>
);
}
@@ -11,7 +11,7 @@ export function AppEmptyState() {
const localize = useLocalize();
return (
<div className="flex flex-col items-center">
<EmptyStateIllustration className="size-[120px] mb-4 opacity-90" />
<EmptyStateIllustration className="size-[120px] mb-4" />
<div className="text-[14px] font-normal text-[#999999] flex items-center gap-1">
{localize('com_app.empty_go_explore')}
<Link
@@ -259,7 +259,7 @@ export default function ExplorePlaza() {
)}
{!loading && agents.length === 0 && (
<div className="flex flex-col items-center">
<EmptyStateIllustration className="size-[120px] mb-4 opacity-90" />
<EmptyStateIllustration className="size-[120px] mb-4" />
<p className="text-[#a9aeb8] text-[14px] font-['PingFang_SC']">{localize('com_app_explore_no_agents')}</p>
</div>
)}
@@ -495,7 +495,7 @@ const ExcelPreview = ({ filePath, fileExt: fileExtProp }: { filePath: string; fi
<div className="flex flex-col relative flex-1 min-h-0">
<div
ref={tableContainerRef}
className="excel-scroll flex-1 min-h-0 border border-gray-200 bg-white relative overflow-auto"
className="flex-1 min-h-0 border border-gray-200 bg-white relative overflow-auto"
style={{
width: "100%",
overflowX: "auto",
@@ -559,7 +559,7 @@ export function KnowledgeSpacePreviewDrawer({
</div>
) : filesPreview.length === 0 ? (
<div className="flex flex-1 flex-col items-center justify-center text-center">
<EmptyStateIllustration className="size-[120px] mb-4 opacity-90" />
<EmptyStateIllustration className="size-[120px] mb-4" />
<p className="text-[14px] font-normal text-[#999999]">
{localize("com_knowledge.no_files")}
</p>
@@ -603,14 +603,14 @@ export function KnowledgeSpacePreviewDrawer({
</div>
) : space?.visibility === VisibilityType.APPROVAL ? (
<div className="flex flex-col items-center justify-center h-full min-h-[360px]">
<NoPermissionIllustration className="size-[140px] mb-4" />
<NoPermissionIllustration className="size-[120px] mb-4" />
<div className="text-[14px] font-normal text-[#999999]">
{localize("com_knowledge.space_view_requires_approval")}
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center h-full min-h-[360px]">
<NoPermissionIllustration className="size-[140px] mb-4" />
<NoPermissionIllustration className="size-[120px] mb-4" />
<div className="text-[14px] font-normal text-[#999999]">
{localize("com_knowledge.space_view_requires_join")}
</div>
@@ -303,7 +303,7 @@ export default function KnowledgeSquare({
<div className="flex-1 flex items-center justify-center text-[#86909C]">{localize("com_knowledge.loading")}</div>
) : visibleSpaces.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center text-[#86909c]">
<EmptyStateIllustration className="size-[120px] mb-4 opacity-90" />
<EmptyStateIllustration className="size-[120px] mb-4" />
<p className="text-[14px] font-normal text-[#999999]">{tEmptyText}</p>
</div>
) : (
@@ -1368,7 +1368,7 @@ export function KnowledgeSpaceContent({
// pb-[112px] reserves room for the floating AI dock so the empty state
// centers in the visible region above it, matching the card grid.
<div className="flex h-full flex-1 flex-col items-center justify-center pb-[112px] pt-10 text-center">
<EmptyStateIllustration className="size-[120px] mb-4 opacity-90" />
<EmptyStateIllustration className="size-[120px] mb-4" />
<p className="text-[14px] font-normal leading-6 text-[#999999]">
{searchQuery ? localize("com_knowledge.no_matched_file") : canUploadFile ? localize("com_knowledge.no_file_here_please") : localize("com_knowledge.no_file_here")}
{canUploadFile && !searchQuery && (
@@ -923,7 +923,7 @@ export default function Knowledge() {
bottom bar and center an illustration with two actions
(mirrors the subscription channel empty state). */
<div className="flex min-h-0 flex-1 flex-col items-center justify-center px-6 pb-16">
<EmptyStateIllustration className="size-[120px] opacity-90" />
<EmptyStateIllustration className="size-[120px]" />
<p className="mt-6 text-[14px] leading-6 text-[#999999]">
{localize("com_knowledge.no_related_content_you_can")}
</p>
@@ -979,7 +979,7 @@ export default function Knowledge() {
) : (
/* PC empty state when no space is selected — illustration + two actions */
<div className="flex flex-1 flex-col items-center justify-center py-10 text-center">
<EmptyStateIllustration className="size-[120px] opacity-90" />
<EmptyStateIllustration className="size-[120px]" />
<p className="mt-6 text-[14px] font-normal leading-6 text-[#999999]">
{localize("com_knowledge.no_related_content_you_can")}
</p>
@@ -584,11 +584,12 @@ export function KnowledgeSpaceSidebar({
{localize("com_knowledge.go_to_knowledge_square")}
</Button>
<Button
variant="secondary"
color="primary"
variant="filled"
icon={<Outlined.Plus />}
onClick={onCreateSpace}
className="h-8 w-full gap-1 rounded-[6px] bg-blue-100 px-3 py-[5px] text-sm font-normal leading-[22px] text-blue-main hover:bg-blue-200"
className="w-full"
>
<Plus className="size-4" />
{localize("com_knowledge.create_knowledge_space")}
</Button>
</div>
+181 -61
View File
@@ -48,6 +48,107 @@
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
/* ============================================================
* Typography tokens (docs-ui-refactor/基础-字体规范.md)
* Two layers: primitive (--font-size-1..9, numeric source) and
* semantic (--text-*, what components use via Tailwind classes
* text-caption / text-body / text-h1 ...). Line height = size + 8px.
* Components must reference the semantic layer only.
* ============================================================ */
/* Primitive — desktop scale */
--font-size-1: 0.75rem; --line-height-1: 1.25rem; /* 12 / 20 */
--font-size-2: 0.8125rem; --line-height-2: 1.3125rem; /* 13 / 21 */
--font-size-3: 0.875rem; --line-height-3: 1.375rem; /* 14 / 22 */
--font-size-4: 1rem; --line-height-4: 1.5rem; /* 16 / 24 */
--font-size-5: 1.125rem; --line-height-5: 1.625rem; /* 18 / 26 */
--font-size-6: 1.25rem; --line-height-6: 1.75rem; /* 20 / 28 */
--font-size-7: 1.5rem; --line-height-7: 2rem; /* 24 / 32 */
--font-size-8: 1.875rem; --line-height-8: 2.375rem; /* 30 / 38 */
--font-size-9: 2.25rem; --line-height-9: 2.75rem; /* 36 / 44 */
/* Semantic — referenced by tailwind.config.cjs theme.extend.fontSize */
--text-caption: var(--font-size-1); --leading-caption: var(--line-height-1);
--text-body-sm: var(--font-size-2); --leading-body-sm: var(--line-height-2);
--text-body: var(--font-size-3); --leading-body: var(--line-height-3);
--text-h4: var(--font-size-4); --leading-h4: var(--line-height-4);
--text-h3: var(--font-size-5); --leading-h3: var(--line-height-5);
--text-h2: var(--font-size-6); --leading-h2: var(--line-height-6);
--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 `/<alpha>` 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:
* body raised (14→16), headings lowered (24→22) — a flatter ladder.
* Component classNames stay unchanged; caption / h4 are identical on
* both breakpoints so they are not remapped here.
* Some mobile steps (17/25, 22/30, 26/34) are not on the primitive
* scale, so they are literal rem values by design. */
@media (max-width: 768px) {
:root {
--text-body-sm: var(--font-size-3); --leading-body-sm: var(--line-height-3); /* 13→14 */
--text-body: var(--font-size-4); --leading-body: var(--line-height-4); /* 14→16 */
--text-h3: 1.0625rem; --leading-h3: 1.5625rem; /* 18→17 / 25 */
--text-h2: var(--font-size-5); --leading-h2: var(--line-height-5); /* 20→18 */
--text-h1: 1.375rem; --leading-h1: 1.875rem; /* 24→22 / 30 */
--text-display: 1.625rem; --leading-display: 2.125rem; /* 30→26 / 34 */
--text-metric: var(--font-size-8); --leading-metric: var(--line-height-8); /* 36→30 */
}
}
html {
@@ -128,6 +229,21 @@ html {
--brand-main: 22 93 255; /* #165DFF */
--brand-muted: 87 115 180; /* #5773B4 — muted/low-sat brand accent (e.g. pin) */
/* Button semantic tokens (docs-ui-refactor/组件-Button按钮.md §5.1).
Neutral grays follow the Arco ramp; danger red is FIXED — never themed.
RGB channels so the Tailwind `btn-*` colors keep `/<alpha>` working.
Light-mode values only for now (dark mode inherits them — known debt). */
--btn-gray-text: 78 89 105; /* #4E5969 — default-color button text */
--btn-gray-border: 229 230 235; /* #E5E6EB — default-color button border */
--btn-fill-1: 247 248 250; /* #F7F8FA — neutral fill ramp: hover bg */
--btn-fill-2: 242 243 245; /* #F2F3F5 — active bg / filled base */
--btn-fill-3: 229 230 235; /* #E5E6EB — filled hover */
--btn-fill-4: 201 205 212; /* #C9CDD4 — filled active */
--btn-danger: 245 63 63; /* #F53F3F */
--btn-danger-hover: 214 55 58; /* #D6373A */
--btn-danger-active: 208 47 51; /* #D02F33 */
--btn-disabled-border: 217 217 217; /* #D9D9D9 — bordered variants, disabled */
/* Illustration palette — empty-state SVG illustrations use --illus-* instead
of --brand-* so the green theme can render the illustrations' own vivid
green ramp (#19B476 / #BDE6D3 / #7CD0B1) rather than the darker UI brand
@@ -139,33 +255,38 @@ html {
/* Green brand theme — toggled via `theme-green` class on <html>.
Re-points the brand palette + the few brand-tinted tokens. Frontend-only.
Mirrors the blue ramp: 500 = main #187C54, 600 = pressed, 50 = #E4F1EC tint. */
Ramp re-derived 2026-07-15 anchored on the FIXED primary #169C47 (the old
steps sat at hue ~157° teal while 500 is 142° green, so 500 jumped out):
light side keeps the OLD ramp's saturation/brightness envelope (the Arco-
algorithm version hit v=100 and read neon; designer nixed it) with only the
hue rotated onto the 500 family (2°/step, same drift as the blue lights);
dark side mirrors the blue ramp's dark-side HSV geometry. */
.theme-green {
--brand-50: 228 241 236; /* #E4F1EC — light tint / selection bg */
--brand-100: 204 228 218; /* #CCE4DA */
--brand-200: 163 210 192; /* #A3D2C0 */
--brand-300: 111 186 160; /* #6FBAA0 */
--brand-400: 61 155 120; /* #3D9B78 */
--brand-500: 24 124 84; /* #187C54 — main brand green */
--brand-600: 19 99 69; /* #136345 — button / pressed */
--brand-700: 15 77 54; /* #0F4D36 */
--brand-800: 10 56 38; /* #0A3826 */
--brand-900: 6 38 25; /* #062619 */
--brand-main: 24 124 84; /* #187C54 */
--brand-50: 228 241 231; /* #E4F1E7 — light tint / selection bg */
--brand-100: 204 228 210; /* #CCE4D2 */
--brand-200: 163 210 176; /* #A3D2B0 */
--brand-300: 111 186 133; /* #6FBA85 */
--brand-400: 61 155 92; /* #3D9B5C */
--brand-500: 22 156 71; /* #169C47 — main brand green (FIXED, do not touch) */
--brand-600: 9 139 53; /* #098B35 — button / pressed */
--brand-700: 7 105 41; /* #076929 */
--brand-800: 7 78 32; /* #074E20 */
--brand-900: 6 50 22; /* #063216 */
--brand-main: 22 156 71; /* #169C47 */
--brand-muted: 92 138 119; /* #5C8A77 — muted/low-sat brand accent (e.g. pin) */
/* Illustration palette — green mode keeps the source SVGs' vivid greens so the
illustrations match their original artwork (not the darker UI brand green). */
--illus-100: 189 230 211; /* #BDE6D3 */
--illus-300: 124 208 177; /* #7CD0B1 */
--illus-500: 25 180 118; /* #19B476 */
--illus-100: 221 240 232; /* #DDF0E8 */
--illus-300: 162 215 181; /* #A2D7B5 */
--illus-500: 22 156 71; /* #169C47 */
/* Brand-tinted surfaces driven by their own vars (not the blue palette). */
/* HSL of the brand green #187C54 — 156 68% 29% rounds to rgb(24,124,84), exactly
matching --brand-500. The old 154 67% 29% rounded to rgb(24,123,81), so
bg-primary (e.g. picker checkboxes) drifted ~3/255 off the brand green. */
--primary: 156 68% 29%;
--surface-active-alt: rgb(228 241 236);
--primary: 142 75% 35%;
--surface-active-alt: rgb(228 241 231); /* = --brand-50 (keep in sync) */
--surface-primary-alt: #f4faf7;
--search-input: hsla(152, 40%, 99%, 1);
}
@@ -180,7 +301,7 @@ html {
opacity, e.g. 60%, still applies). */
.illus-grey {
--illus-100: 229 229 229; /* #E5E5E5 */
--illus-300: 255 255 255; /* #FFFFFF */
--illus-300: 229 229 229; /* #E5E5E5 — was #FFFFFF, but pure white washed out on white bg (e.g. CrawlingIllustration magnifier halo); grey-only override, green/blue keep the mid tone */
--illus-500: 188 188 188; /* #BCBCBC */
}
.illus-grey.brand-illustration,
@@ -199,14 +320,19 @@ html {
<Button> default `bg-primary` — those win on the default state otherwise. Hover/active
step to progressively darker greens so the state change is visible. */
.theme-green .btn-brand-primary:not(:disabled) {
background-color: #19b476 !important; /* default: green fill */
background-color: #169c47 !important; /* default: green fill (matches --primary) */
color: #fff !important; /* default: white text / icon */
}
.theme-green .btn-brand-primary:not(:disabled):hover {
background-color: #17a66d !important; /* hover: slightly darker green */
background-color: #148f41 !important; /* hover: slightly darker green */
}
.theme-green .btn-brand-primary:not(:disabled):active {
background-color: #159964 !important; /* active: deeper green on press */
/* Press feedback is TOUCH-ONLY (组件-Button按钮.md §5.2): on hover-capable
devices a pressed button keeps its hover color (no click flash); on touch,
where hover is disabled, the deeper step is the only press feedback. */
@media not ((hover: hover) and (pointer: fine)) {
.theme-green .btn-brand-primary:not(:disabled):active {
background-color: #12813b !important; /* active: deeper green on press */
}
}
/* Force a direct-child icon white too — some send buttons pin `[&>svg]:text-white`
already, but others inherit; this keeps every primary-button icon white on the fill. */
@@ -214,6 +340,22 @@ html {
color: #fff !important;
}
/* Touch hit-area expansion (组件-Button按钮.md §5.5): medium and icon-only
buttons get an invisible ≥44×44 hot zone on touch devices — "跟手不跟屏"
(基础-多端适配原则.md §0/§2) — while the visual size stays unchanged.
The base Button sets `relative` + this class; never hand-roll per page. */
@media (hover: none) and (pointer: coarse) {
.btn-touch-hit::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: max(100%, 44px);
height: max(100%, 44px);
transform: translate(-50%, -50%);
}
}
.dark {
--presentation: var(--gray-800);
--text-primary: var(--gray-100);
@@ -1561,42 +1703,13 @@ button {
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
/* Webkit scrollbar — `.scrollbar-os` opts out so the OS scrollbar setting
(auto-hide vs always-on) is respected instead of a forced custom scrollbar.
`html`/`body` (the document scroller) are always excluded so the page-level
scrollbar follows the OS setting rather than becoming a forced always-on bar. */
:not(.scrollbar-os):not(html):not(body)::-webkit-scrollbar {
height: 0.1em;
width: 0.5rem;
}
:not(.scrollbar-os):not(html):not(body)::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 9999px;
}
.dark :not(.scrollbar-os):not(html):not(body)::-webkit-scrollbar-thumb {
background-color: hsla(0, 0%, 100%, 0.1);
}
:not(.scrollbar-os):not(html):not(body)::-webkit-scrollbar-track {
background-color: transparent;
border-radius: 9999px;
}
/* Excel preview: horizontal scrollbar same thickness as vertical (override global 0.1em height) */
.excel-scroll::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
}
.scrollbar-transparent::-webkit-scrollbar-thumb {
background-color: transparent;
}
.dark .scrollbar-transparent::-webkit-scrollbar-thumb {
background-color: transparent;
}
/* Scrollbar policy (设计规范 · 基础-滚动条规范.md, 2026-07-15):
scrollbars follow the OS setting — auto-hide stays auto-hide, always-on stays
always-on. DO NOT style ::-webkit-scrollbar / scrollbar-width by default:
any such rule kills the OS overlay behavior and forces an always-visible bar.
(The old global forced-slim rule was removed; the `.scrollbar-os` opt-out class
found in ~33 business files is now a no-op — clean it up as files get touched.)
Sanctioned exceptions below only make bars LESS visible, never force them on. */
/* Hide scrollbar entirely (used by horizontal tab strips) */
.no-scrollbar {
@@ -1945,17 +2058,24 @@ html {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
/* Global font stack = font-family-base in 基础-字体规范.md §1.
* Pure system stack (zero webfont loading): SF Pro + PingFang on Apple,
* Segoe UI + Microsoft YaHei on Windows, Roboto + Noto Sans CJK on Android.
* Step ① of the two-step swap: @font-face blocks and font files
* (Inter / AlibabaPuHuiTi / Roboto Mono / Söhne) are intentionally KEPT
* until the Windows regression pass confirms rendering, then removed. */
body,
html {
height: 100%;
font-family:
-apple-system,
BlinkMacSystemFont,
"AlibabaPuHuiTi-3-55-Regular",
"Segoe UI",
Roboto,
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Helvetica Neue",
Arial,
"Noto Sans CJK SC",
sans-serif;
}
+24 -1
View File
@@ -1,6 +1,29 @@
import { twMerge } from 'tailwind-merge';
import { extendTailwindMerge } from 'tailwind-merge';
import { type ClassValue, clsx } from 'clsx';
/**
* twMerge taught about the project's custom semantic font-size tokens
* (see docs -.md §7 / theme.extend.fontSize in tailwind.config.cjs).
*
* Why this is required: tailwind-merge's `text-*` handling treats any UNKNOWN
* `text-X` as a text COLOR (catch-all). So `text-body` / `text-h1` / `text-caption`
* are mis-grouped as colors, and when a real color (e.g. `text-text-primary`)
* appears in the SAME cn() call, the font size is silently dropped element
* falls back to the inherited 16px. Registering them in the `font-size` group
* makes cn() preserve size + color together, and collapse size-vs-size correctly.
*
* NOTE: this uses the tailwind-merge **v1.x** config shape (`classGroups` at the
* top level). v2+ nests it under `extend` do not copy a v2 snippet here while
* the dependency is pinned to ^1.14.0.
*/
const twMerge = extendTailwindMerge({
classGroups: {
'font-size': [
{ text: ['caption', 'body-sm', 'body', 'h1', 'h2', 'h3', 'h4', 'display', 'metric'] },
],
},
});
/**
* Merges the tailwind clases (using twMerge). Conditionally removes false values
* @param inputs The tailwind classes to merge
+9
View File
@@ -0,0 +1,9 @@
/**
* Browser stub for Node's `url` module (docs site / rspress build only).
*
* `src/api/chat/actions.ts` (reached transitively through the `~/utils` barrel)
* does `import { URL } from 'url'`. In the browser the global URL is identical,
* so re-export it to satisfy strict ESM linking. Never executed beyond that.
*/
export const URL = globalThis.URL;
export default { URL };
+90 -6
View File
@@ -3,23 +3,58 @@ const plugin = require('tailwindcss/plugin');
/** @type {import('tailwindcss').Config} */
module.exports = {
// 基础-多端适配原则.md §1: hover states are disabled on touch APP-WIDE — every
// `hover:` utility compiles wrapped in a hover-capable media query. Press
// feedback on touch comes from `active:` styles instead (no sticky hover).
// NOTE: components must keep using plain `hover:` (never a custom variant),
// so tailwind-merge can still dedupe business-page hover overrides.
future: {
hoverOnlyWhenSupported: true,
},
content: ['./src/**/*.{js,jsx,ts,tsx}'],
// darkMode: 'class',
darkMode: ['class'],
theme: {
fontFamily: {
// -apple-system / BlinkMacSystemFont 在 macOS / iOS / Apple 设备的浏览器里
// 解析为系统字体(San Francisco / SF Pro 等),同时 Apple System 字体在
// 中文系统下会自动联动 PingFang SC,所以放最前面体验最好。
// 非 Apple 系统再回退到 Inter / 系统默认 sans-serif。
sans: ['-apple-system', 'BlinkMacSystemFont', 'Inter', 'sans-serif'],
mono: ['Roboto Mono', 'monospace'],
// font-family-base / font-family-mono in docs-ui-refactor/基础-字体规范.md §1.
// Pure system stack, kept in sync with the global body/html rule in src/style.css
// (that hardcoded rule is what actually sets the app-wide font; this config only
// affects explicit font-sans / font-mono usages).
sans: [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"PingFang SC"',
'"Hiragino Sans GB"',
'"Microsoft YaHei"',
'"Noto Sans CJK SC"',
'sans-serif',
],
mono: ['ui-monospace', '"SF Mono"', '"Cascadia Mono"', 'Consolas', '"Liberation Mono"', 'monospace'],
},
// fontFamily: {
// sans: ['Söhne', 'sans-serif'],
// mono: ['Söhne Mono', 'monospace'],
// },
extend: {
// Semantic type scale (docs-ui-refactor/基础-字体规范.md §2/§7) — MUST live in
// `extend` so Tailwind's default text-xs/sm/base/... classes stay available
// (900+ existing usages). Values reference semantic CSS vars defined in
// src/style.css :root, which remap under 768px for the mobile ladder, so
// classNames never change per breakpoint. Each entry carries its own
// font-weight (400 body tier / 500 heading tier) — no extra font-medium needed.
fontSize: {
caption: ['var(--text-caption)', { lineHeight: 'var(--leading-caption)', fontWeight: '400' }],
'body-sm': ['var(--text-body-sm)', { lineHeight: 'var(--leading-body-sm)', fontWeight: '400' }],
body: ['var(--text-body)', { lineHeight: 'var(--leading-body)', fontWeight: '400' }],
h4: ['var(--text-h4)', { lineHeight: 'var(--leading-h4)', fontWeight: '500' }],
h3: ['var(--text-h3)', { lineHeight: 'var(--leading-h3)', fontWeight: '500' }],
h2: ['var(--text-h2)', { lineHeight: 'var(--leading-h2)', fontWeight: '500' }],
h1: ['var(--text-h1)', { lineHeight: 'var(--leading-h1)', fontWeight: '500' }],
display: ['var(--text-display)', { lineHeight: 'var(--leading-display)', fontWeight: '500' }],
metric: ['var(--text-metric)', { lineHeight: 'var(--leading-metric)', fontWeight: '500' }],
},
width: {
authPageWidth: '370px',
},
@@ -132,6 +167,55 @@ module.exports = {
900: 'rgb(var(--brand-900) / <alpha-value>)',
},
'brand-purple': '#ab68ff',
// Button semantic tokens (docs-ui-refactor/组件-Button按钮.md §5.1) —
// RGB-channel vars defined in src/style.css :root; channel form keeps
// `/<alpha>` modifiers working. Neutral fill ramp is shared Arco grays.
'btn-gray-text': 'rgb(var(--btn-gray-text) / <alpha-value>)',
'btn-gray-border': 'rgb(var(--btn-gray-border) / <alpha-value>)',
'btn-fill-1': 'rgb(var(--btn-fill-1) / <alpha-value>)',
'btn-fill-2': 'rgb(var(--btn-fill-2) / <alpha-value>)',
'btn-fill-3': 'rgb(var(--btn-fill-3) / <alpha-value>)',
'btn-fill-4': 'rgb(var(--btn-fill-4) / <alpha-value>)',
'btn-danger': 'rgb(var(--btn-danger) / <alpha-value>)',
'btn-danger-hover': 'rgb(var(--btn-danger-hover) / <alpha-value>)',
'btn-danger-active': 'rgb(var(--btn-danger-active) / <alpha-value>)',
'btn-disabled-border': 'rgb(var(--btn-disabled-border) / <alpha-value>)',
// 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 `/<alpha>` 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) / <alpha-value>)',
'text-2': 'rgb(var(--text-2) / <alpha-value>)',
'text-3': 'rgb(var(--text-3) / <alpha-value>)',
'text-4': 'rgb(var(--text-4) / <alpha-value>)',
'fill-1': 'rgb(var(--fill-1) / <alpha-value>)',
'fill-2': 'rgb(var(--fill-2) / <alpha-value>)',
'fill-3': 'rgb(var(--fill-3) / <alpha-value>)',
'fill-4': 'rgb(var(--fill-4) / <alpha-value>)',
'border-base': 'rgb(var(--border-base) / <alpha-value>)',
'border-deep': 'rgb(var(--border-deep) / <alpha-value>)',
success: {
DEFAULT: 'rgb(var(--success) / <alpha-value>)',
hover: 'rgb(var(--success-hover) / <alpha-value>)',
active: 'rgb(var(--success-active) / <alpha-value>)',
tint: 'rgb(var(--success-tint) / <alpha-value>)',
},
warning: {
DEFAULT: 'rgb(var(--warning) / <alpha-value>)',
hover: 'rgb(var(--warning-hover) / <alpha-value>)',
active: 'rgb(var(--warning-active) / <alpha-value>)',
tint: 'rgb(var(--warning-tint) / <alpha-value>)',
},
danger: {
DEFAULT: 'rgb(var(--danger) / <alpha-value>)',
hover: 'rgb(var(--danger-hover) / <alpha-value>)',
active: 'rgb(var(--danger-active) / <alpha-value>)',
tint: 'rgb(var(--danger-tint) / <alpha-value>)',
},
'presentation': 'var(--presentation)',
'text-primary': 'var(--text-primary)',
'text-secondary': 'var(--text-secondary)',