mirror of
https://github.com/dataelement/bisheng.git
synced 2026-09-21 12:43:36 +08:00
feat(client): add semantic typography scale + gallery typography section
Two-layer type tokens (primitive --font-size-N + semantic --text-*) in style.css with a mobile remap, wired to Tailwind fontSize; system font stack; new gallery TypographySection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
715ebaf991
commit
324f299ffd
@@ -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<NonNullable<NavItem['status']>, string> = {
|
||||
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 PAGES. */
|
||||
const GROUPS = 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 [activeId, setActiveId] = useState('overview');
|
||||
const active = PAGES.find((p) => p.id === activeId) ?? PAGES[0];
|
||||
const ActivePage = active.Page;
|
||||
|
||||
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>
|
||||
<nav className="flex-1 overflow-y-auto p-3">
|
||||
{GROUPS.map((group) => (
|
||||
<div key={group} className="mb-4">
|
||||
<div className="px-3 pb-1 text-caption font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{group}
|
||||
</div>
|
||||
{PAGES.filter((p) => p.group === group).map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => setActiveId(p.id)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-body-sm transition-colors',
|
||||
activeId === p.id
|
||||
? 'bg-blue-500/[0.08] font-medium text-text-primary'
|
||||
: 'text-muted-foreground hover:bg-muted/60',
|
||||
)}
|
||||
>
|
||||
{p.status && <span className={cn('size-1.5 rounded-full', STATUS_DOT[p.status])} />}
|
||||
<span className="truncate">{p.label}</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 component page at a time */}
|
||||
<main key={activeId} className="flex-1 overflow-y-auto">
|
||||
<ActivePage />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<article className="mx-auto max-w-4xl px-8 py-10">
|
||||
<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-sm text-text-primary">
|
||||
<span className="mt-2 size-1.5 shrink-0 rounded-full bg-blue-500" />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h2 className="mb-4 text-h3 text-text-primary">代码演示 / 状态</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 +141,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 +166,52 @@ export function CompareTable({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* 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 (
|
||||
<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>;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Section
|
||||
id="button"
|
||||
<ComponentPage
|
||||
title="Button 按钮"
|
||||
subtitle={
|
||||
eng="Button"
|
||||
description={
|
||||
<>
|
||||
已是 <code>cva</code> 变体写法,作为其它组件"留改动余地"的范本:预设档位 + 允许{' '}
|
||||
<code>className</code> 覆盖。
|
||||
已是 <code>cva</code> 变体写法,作为其它组件"留改动余地"的范本:预设档位 +
|
||||
允许 <code>className</code> 覆盖。
|
||||
</>
|
||||
}
|
||||
whenToUse={[
|
||||
<>主操作用 <code>default</code>(已跟随蓝⇄绿主题);危险操作用 <code>destructive</code>。</>,
|
||||
<>次要操作用 <code>outline</code> / <code>secondary</code>;弱操作用 <code>ghost</code> / <code>link</code>。</>,
|
||||
<>尺寸只用 <code>sm / default / lg / icon</code> 四档,不要手写高度/内边距。</>,
|
||||
<>特殊一次性样式用 <code>className</code> 覆盖,不要新增变体。</>,
|
||||
]}
|
||||
>
|
||||
<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="变体 variant" subtitle="每格含常态与 disabled 态">
|
||||
<ExampleGrid cols={4}>
|
||||
{VARIANTS.map((v) => (
|
||||
<ExampleCard key={v} title={`variant="${v}"`}>
|
||||
<Button variant={v}>{v}</Button>
|
||||
<Button variant={v} disabled>
|
||||
disabled
|
||||
</Button>
|
||||
</ExampleCard>
|
||||
))}
|
||||
</ExampleGrid>
|
||||
</ExampleGroup>
|
||||
|
||||
<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>
|
||||
<ExampleGroup title="尺寸 size">
|
||||
<ExampleGrid cols={4}>
|
||||
{SIZES.map((s) => (
|
||||
<ExampleCard key={s} title={`size="${s}"`}>
|
||||
<Button size={s}>{s === 'icon' ? '★' : s}</Button>
|
||||
</ExampleCard>
|
||||
))}
|
||||
</ExampleGrid>
|
||||
</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>:弹窗点「提交」才落库并高亮,原因选填;「取消」= 彻底放弃点踩。</>,
|
||||
<>已点踩态再点踩 = 直接取消(<code>onLike(0)</code>),不再弹窗。</>,
|
||||
<>弹窗规格:圆角 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="再点踩=直接取消(onLike(0)),不弹窗">
|
||||
<LoggedDemo liked={2} />
|
||||
</ExampleCard>
|
||||
</ExampleGrid>
|
||||
</ExampleGroup>
|
||||
</ComponentPage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Section
|
||||
id="overview"
|
||||
title="组件统一化 · 总览"
|
||||
subtitle="本页仅在开发环境可见,用户看不到。改这里用到的组件 = 改真实业务组件,全站同步生效。"
|
||||
<ComponentPage
|
||||
title="组件统一化"
|
||||
eng="Overview"
|
||||
description="把 client 前台重复、样式不一致的高频组件逐个统一,最终抽成可复用的设计组件库。本页仅开发环境可见,用户看不到。"
|
||||
whenToUse={[
|
||||
<>改这里用到的组件 = 改<b>真实业务组件</b>,全站同步生效(组件是共享的)。</>,
|
||||
<>左侧按分组选择组件,查看其现状、各档位与状态。</>,
|
||||
<>工作方式、提交规则见 <code>docs-ui-refactor/00-总纲.md</code>。</>,
|
||||
]}
|
||||
>
|
||||
<p className="mb-4 max-w-3xl text-sm text-text-primary">
|
||||
目标:把 client 前台重复、样式不一致的高频组件逐个统一,最终抽成可复用的设计组件库。
|
||||
工作方式见 <code>docs-ui-refactor/00-总纲.md</code>。左侧选择组件查看现状与各档位。
|
||||
</p>
|
||||
<CompareTable
|
||||
head={['组件', '状态', '现有版本', '收敛基准']}
|
||||
rows={[
|
||||
['字体 Typography', '🟨 进行中', 'Tailwind 默认档', '九档 semantic token(已落地)'],
|
||||
['Modal 弹窗', '🟨 进行中', '4(含 1 死代码)', '待定 · OGDialogTemplate 候选'],
|
||||
['Select / 下拉', '⬜ 待办', '多个', '待定'],
|
||||
['Button 按钮', '⬜ 待办', '1(已 cva 化,较好)', 'Button.tsx'],
|
||||
['Select / 下拉', '⬜ 待办', '多个', '待定'],
|
||||
['Dropdown 菜单', '⬜ 待办', '2', '待定'],
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
</ComponentPage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<ComponentPage
|
||||
title="字体 Typography"
|
||||
eng="Typography"
|
||||
description="九档 semantic 字号(自带字重)+ 两档字重。系统字体栈已全量切换;窗口缩到 ≤768px 可看移动端重映射(className 不变)。"
|
||||
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。</>,
|
||||
<>标题层级连续使用(h1→h2→h3),不跳级取字号。</>,
|
||||
<>数字/金额加 <code>tabular-nums</code>;ID、代码用 <code>font-mono</code>。</>,
|
||||
]}
|
||||
>
|
||||
<ExampleGroup
|
||||
title="字体栈 Font Family"
|
||||
subtitle="纯系统字体栈,零加载成本:Mac/iOS 命中 SF + 苹方,Windows 命中 Segoe UI + 微软雅黑,Android 命中 Roboto + 思源黑体。两步法第①步已切栈,旧字体文件(Inter / 阿里普惠体 / Roboto Mono)保留待回归后删除。"
|
||||
>
|
||||
<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="与规范 §2 Semantic 表同构,末列为该档实时渲染。移动列在窗口 ≤768px 时生效(className 不变)。"
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Quick reference for migration */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user