diff --git a/src/frontend/client/src/pages/_gallery/GalleryApp.tsx b/src/frontend/client/src/pages/_gallery/GalleryApp.tsx index 0b1b70cd8..6286845bf 100644 --- a/src/frontend/client/src/pages/_gallery/GalleryApp.tsx +++ b/src/frontend/client/src/pages/_gallery/GalleryApp.tsx @@ -6,85 +6,96 @@ * tree-shaken out of the production build. Changing a component shown here changes the * real shared component, so every business page updates too. * + * Layout mirrors ant.design/components: left sidebar navigates between component + * pages; the main area shows ONE component page at a time (not a long scroll). * 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 { TypographySection } from './sections/TypographySection'; import { ModalSection } from './sections/ModalSection'; import { ConfirmDialogSection } from './sections/ConfirmDialogSection'; import { ButtonSection } from './sections/ButtonSection'; import { FeedbackSection } from './sections/FeedbackSection'; -interface NavItem { +type Status = 'wip' | 'todo' | 'done'; + +interface PageDef { id: string; label: string; - status?: 'wip' | 'todo' | 'done'; + group: string; + status?: Status; + 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' }, +/** Registry — one entry per component page. Grouped like antd's sidebar. */ +const PAGES: PageDef[] = [ + { id: 'overview', label: '总览', group: '开始', Page: OverviewSection }, + { id: 'typography', label: '字体 Typography', group: '基础 Foundation', status: 'wip', Page: TypographySection }, + { id: 'button', label: 'Button 按钮', group: '通用 General', status: 'todo', Page: ButtonSection }, + { id: 'modal', label: 'Modal 弹窗', group: '反馈 Feedback', status: 'wip', Page: ModalSection }, + { id: 'confirm', label: '二次确认弹窗', group: '反馈 Feedback', status: 'wip', Page: ConfirmDialogSection }, + { id: 'feedback', label: '点赞 / 点踩', group: '反馈 Feedback', status: 'done', Page: FeedbackSection }, ]; -const STATUS_DOT: Record, string> = { +const STATUS_DOT: Record = { 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 PAGES. */ +const GROUPS = PAGES.reduce((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 [activeId, setActiveId] = useState('overview'); + const active = PAGES.find((p) => p.id === activeId) ?? PAGES[0]; + const ActivePage = active.Page; return (
{/* Sidebar */} -
); diff --git a/src/frontend/client/src/pages/_gallery/components/kit.tsx b/src/frontend/client/src/pages/_gallery/components/kit.tsx index 1e34786bf..db113c73b 100644 --- a/src/frontend/client/src/pages/_gallery/components/kit.tsx +++ b/src/frontend/client/src/pages/_gallery/components/kit.tsx @@ -1,77 +1,137 @@ /** - * 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, + children, +}: { + title: string; + eng?: string; + description?: ReactNode; + /** 何时使用 / 使用规则 — rendered as a bordered bullet list, antd-style. */ + whenToUse?: ReactNode[]; + children: ReactNode; +}) { + return ( +
+
+

+ {title} + {eng && {eng}} +

+ {description && ( +

{description}

+ )} +
+ + {whenToUse && whenToUse.length > 0 && ( +
+

何时使用 / 规则

+
    + {whenToUse.map((item, i) => ( +
  • + + {item} +
  • + ))} +
+
+ )} + +
+

代码演示 / 状态

+ {children} +
+
+ ); +} + +/* ------------------------------------------------------------------ * + * 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 ( -
-
-

{title}

- {subtitle &&

{subtitle}

} -
+
+ {title &&

{title}

} + {subtitle &&

{subtitle}

} + {!subtitle && title &&
} {children} -
+ ); } -/** 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 ( -
-
{children}
-
-
{label}
- {note &&
{note}
} +
+
{children}
+
+
{title}
+ {description &&
{description}
}
); } -/** 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
{children}
; } -/** 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 +141,11 @@ export function CompareTable({ }) { return (
- +
{head.map((h, i) => ( - ))} @@ -106,3 +166,52 @@ export function CompareTable({ ); } + +/* ------------------------------------------------------------------ * + * Legacy aliases — kept so not-yet-migrated sections still compile. + * New pages should use ComponentPage / ExampleGroup / ExampleCard / ExampleGrid. + * TODO(gallery): migrate Modal / ConfirmDialog sections to the new template. + * ------------------------------------------------------------------ */ + +/** @deprecated use ExampleGroup inside a ComponentPage. */ +export function Section({ + id, + title, + subtitle, + children, +}: { + id?: string; + title: string; + subtitle?: ReactNode; + children: ReactNode; +}) { + return ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {children} +
+ ); +} + +/** @deprecated use ExampleCard. */ +export function Demo({ + label, + note, + className, + children, +}: { + label: string; + note?: string; + className?: string; + children: ReactNode; +}) { + return {children}; +} + +/** @deprecated use ExampleGrid. */ +export function DemoGrid({ children, cols = 3 }: { children: ReactNode; cols?: 1 | 2 | 3 | 4 }) { + return {children}; +} diff --git a/src/frontend/client/src/pages/_gallery/sections/ButtonSection.tsx b/src/frontend/client/src/pages/_gallery/sections/ButtonSection.tsx index d79d2cb41..a14661926 100644 --- a/src/frontend/client/src/pages/_gallery/sections/ButtonSection.tsx +++ b/src/frontend/client/src/pages/_gallery/sections/ButtonSection.tsx @@ -4,7 +4,7 @@ * components should expose variants/sizes. See docs-ui-refactor/01-设计规范.md. */ import { Button } from '~/components/ui/Button'; -import { Section, Demo, DemoGrid } from '../components/kit'; +import { ComponentPage, ExampleGroup, ExampleGrid, ExampleCard } from '../components/kit'; const VARIANTS = [ 'default', @@ -21,35 +21,44 @@ const SIZES = ['sm', 'default', 'lg', 'icon'] as const; export function ButtonSection() { return ( -
- 已是 cva 变体写法,作为其它组件"留改动余地"的范本:预设档位 + 允许{' '} - className 覆盖。 + 已是 cva 变体写法,作为其它组件"留改动余地"的范本:预设档位 + + 允许 className 覆盖。 } + whenToUse={[ + <>主操作用 default(已跟随蓝⇄绿主题);危险操作用 destructive。, + <>次要操作用 outline / secondary;弱操作用 ghost / link。, + <>尺寸只用 sm / default / lg / icon 四档,不要手写高度/内边距。, + <>特殊一次性样式用 className 覆盖,不要新增变体。, + ]} > - - {VARIANTS.map((v) => ( - - - - - ))} - + + + {VARIANTS.map((v) => ( + + + + + ))} + + -

尺寸 size

- - {SIZES.map((s) => ( - - - - ))} - -
+ + + {SIZES.map((s) => ( + + + + ))} + + + ); } diff --git a/src/frontend/client/src/pages/_gallery/sections/FeedbackSection.tsx b/src/frontend/client/src/pages/_gallery/sections/FeedbackSection.tsx index 7ea46412a..c5a25e940 100644 --- a/src/frontend/client/src/pages/_gallery/sections/FeedbackSection.tsx +++ b/src/frontend/client/src/pages/_gallery/sections/FeedbackSection.tsx @@ -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('—'); - return ( -
- setLast(`onLike(${l})`)} - onDislikeComment={(c) => setLast(`onDislikeComment("${c}")`)} - /> - 最近调用:{last} -
- ); + const [last, setLast] = useState('—'); + return ( +
+ setLast(`onLike(${l})`)} + onDislikeComment={(c) => setLast(`onDislikeComment("${c}")`)} + /> + 最近调用:{last} +
+ ); } export function FeedbackSection() { - return ( -
- MessageFeedbackButtons — 全部 6 类 AI 回答界面共用(首页对话 / 知源 / - 订阅 3 面板 / 灵思 / appChat)。点踩为延迟提交:弹窗点「提交」才落库并高亮, - 原因选填;「取消」= 彻底放弃点踩。弹窗规格:圆角 12 / 边距 20 / 按钮 32 高 · 14px · - 字重 400 · 圆角 6。 - - } - > - - - - - - - - - - - -
- ); + return ( + + MessageFeedbackButtons — 全部 6 类 AI 回答界面共用(首页对话 / 知源 / + 订阅 3 面板 / 灵思 / appChat)。 + + } + whenToUse={[ + <>点踩为延迟提交:弹窗点「提交」才落库并高亮,原因选填;「取消」= 彻底放弃点踩。, + <>已点踩态再点踩 = 直接取消(onLike(0)),不再弹窗。, + <>弹窗规格:圆角 12 / 边距 20 / 按钮 32 高 · 14px · 字重 400 · 圆角 6。, + ]} + > + + + + + + + + + + + + + + + ); } diff --git a/src/frontend/client/src/pages/_gallery/sections/OverviewSection.tsx b/src/frontend/client/src/pages/_gallery/sections/OverviewSection.tsx index 4b177e8c6..6ec269960 100644 --- a/src/frontend/client/src/pages/_gallery/sections/OverviewSection.tsx +++ b/src/frontend/client/src/pages/_gallery/sections/OverviewSection.tsx @@ -1,26 +1,28 @@ /** Gallery overview — DEV-ONLY. High-level status of the unification effort. */ -import { Section, CompareTable } from '../components/kit'; +import { ComponentPage, CompareTable } from '../components/kit'; export function OverviewSection() { return ( -
改这里用到的组件 = 改真实业务组件,全站同步生效(组件是共享的)。, + <>左侧按分组选择组件,查看其现状、各档位与状态。, + <>工作方式、提交规则见 docs-ui-refactor/00-总纲.md。, + ]} > -

- 目标:把 client 前台重复、样式不一致的高频组件逐个统一,最终抽成可复用的设计组件库。 - 工作方式见 docs-ui-refactor/00-总纲.md。左侧选择组件查看现状与各档位。 -

-
+ ); } diff --git a/src/frontend/client/src/pages/_gallery/sections/TypographySection.tsx b/src/frontend/client/src/pages/_gallery/sections/TypographySection.tsx new file mode 100644 index 000000000..889765f34 --- /dev/null +++ b/src/frontend/client/src/pages/_gallery/sections/TypographySection.tsx @@ -0,0 +1,169 @@ +/** + * 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 ( + 组件与业务代码只用 semantic 类(text-body / text-h1…),不写裸 text-sm 或数值。, + <>字重只用 font-normal(400) / font-medium(500) 两档,禁用 600/700。, + <>标题层级连续使用(h1→h2→h3),不跳级取字号。, + <>数字/金额加 tabular-nums;ID、代码用 font-mono。, + ]} + > + + [ +
+ {f.token} +
+ 类名 {f.cls} +
+
, +
+
+ {f.samples.map((s) => ( +
+ {s} +
+ ))} +
+
{f.stack}
+
, + f.usage, + ])} + /> +
+ + + [ + + {step.cls} + , + {step.desktop}, + {step.mobile}, + String(step.weight), +
{step.usage}
, + ])} + /> +
+ + {/* Font weights — the only two allowed steps */} + + [ +
+ {w.token} +
+ 类名 {w.cls} +
+
, + String(w.weight), + /* text-h3 carries fontWeight 500; inline style deterministically overrides it */ +
+ {w.usage} +
, + ])} + /> +
+ + {/* Quick reference for migration */} + + + +
+ ); +} diff --git a/src/frontend/client/src/style.css b/src/frontend/client/src/style.css index b73954c19..06c6c0d95 100644 --- a/src/frontend/client/src/style.css +++ b/src/frontend/client/src/style.css @@ -48,6 +48,53 @@ --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); +} + +/* 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 { @@ -1945,17 +1992,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; } diff --git a/src/frontend/client/tailwind.config.cjs b/src/frontend/client/tailwind.config.cjs index 6dfaec83d..9285a4a6d 100644 --- a/src/frontend/client/tailwind.config.cjs +++ b/src/frontend/client/tailwind.config.cjs @@ -8,18 +8,45 @@ module.exports = { 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', },
+ {h}