fix(client): register semantic font-size tokens in tailwind-merge (cn)

extendTailwindMerge teaches cn() the custom text-caption/body/h1... tokens as a font-size group (v1.x config shape) so a size + a color in the same cn() call no longer drop the size; gallery Modal/ConfirmDialog sections + kit updates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kinyoo
2026-07-15 10:36:45 +08:00
co-authored by Claude Opus 4.8
parent 9faf41afd5
commit 6bfedd60f4
5 changed files with 71 additions and 44 deletions
@@ -77,7 +77,7 @@ export default function GalleryApp() {
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',
'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-body transition-colors',
activeId === p.id
? 'bg-blue-500/[0.08] font-medium text-text-primary'
: 'text-muted-foreground hover:bg-muted/60',
@@ -49,7 +49,7 @@ export function ComponentPage({
<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">
<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>
@@ -168,9 +168,9 @@ 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.
* 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. */
@@ -12,7 +12,7 @@ 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, Demo, DemoGrid, CompareTable } from '../components/kit';
/** Demos for the app-wide `useConfirm()` service (ConfirmContext, AlertDialog-based). */
function UseConfirmDemos() {
@@ -87,7 +87,7 @@ function ConfirmDemo({
title={title}
className="max-w-[450px]"
main={
<Label className="text-left text-sm font-medium">{body}</Label>
<Label className="text-left text-body font-medium">{body}</Label>
}
selection={{
selectHandler: () => undefined,
@@ -103,10 +103,10 @@ function ConfirmDemo({
export function ConfirmDialogSection() {
return (
<Section
id="confirm"
<ComponentPage
title="二次确认弹窗"
subtitle={
eng="Confirm Dialog"
description={
<>
/ / {' '}
<code>OGDialogTemplate selection</code> 13 7 UI + 6 {' '}
@@ -116,9 +116,14 @@ export function ConfirmDialogSection() {
9 <code>AlertDialog</code> Modal
</>
}
whenToUse={[
<> <code>useConfirm()</code><code>variant: destructive</code>/ <code>default</code></>,
<>danger <code>#f53f3f</code> / primary <code>selectVariant</code> <code>selectClasses</code> </>,
<> 16 / p-5 / B C </>,
]}
>
{/* The three coexisting confirm-dialog systems */}
<div className="mb-6">
{/* The two coexisting confirm-dialog systems */}
<ExampleGroup title="现状:两套确认体系">
<CompareTable
head={['体系', '实现', '业务文件数', '用在哪', '样式一致性']}
rows={[
@@ -142,10 +147,10 @@ export function ConfirmDialogSection() {
],
]}
/>
</div>
</ExampleGroup>
{/* Inventory table: every selectClasses variant found in business code */}
<div className="mb-6">
<ExampleGroup title="确认按钮 selectClasses 清单(9 种历史写法)">
<CompareTable
head={['#', '确认按钮 selectClasses(原文)', '用处', '文件数']}
rows={[
@@ -209,10 +214,10 @@ export function ConfirmDialogSection() {
],
]}
/>
</div>
</ExampleGroup>
{/* Anatomy after step-1 convergence — B shell/buttons now mirror the C look */}
<div className="mb-6">
<ExampleGroup title="对齐后的规格(B 套 = C 套)">
<CompareTable
head={['部位', '对齐后的值(B 套 = C 套)', '备注']}
rows={[
@@ -251,9 +256,10 @@ export function ConfirmDialogSection() {
],
]}
/>
</div>
</ExampleGroup>
<DemoGrid cols={3}>
<ExampleGroup title="逐个打开对比(旧写法现应呈现统一外观)">
<DemoGrid cols={3}>
<UseConfirmDemos />
<ConfirmDemo
label="① red-700 系(6 处,原 8"
@@ -317,6 +323,7 @@ export function ConfirmDialogSection() {
selectClasses="btn btn-primary"
/>
</DemoGrid>
</Section>
</ExampleGroup>
</ComponentPage>
);
}
@@ -34,12 +34,12 @@ import {
import DialogTemplate from '~/components/ui/DialogTemplate';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useConfirm } from '~/Providers';
import { Section, Demo, DemoGrid, CompareTable } from '../components/kit';
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-sm text-text-primary">
<p className="text-body text-text-primary">
/
</p>
<Input placeholder="示例输入框" />
@@ -68,22 +68,24 @@ function ConfirmReferenceDemo() {
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
</>
}
whenToUse={[
<><b></b>稿"④ 待设计师定夺"</>,
<>C 16 / p-5 / B </>,
<>A 22 / / </>,
]}
>
{/* ① Population overview */}
<h3 className="mb-3 mt-2 text-base font-semibold text-text-primary">
ui/
</h3>
<div className="mb-8">
<ExampleGroup title="① 用量盘子(当前精确扫描,排除 ui/ 与画廊)">
<CompareTable
head={['体系', '实现', '业务文件数', '用在哪 / 备注']}
rows={[
@@ -125,13 +127,10 @@ export function ModalSection() {
],
]}
/>
</div>
</ExampleGroup>
{/* ② Shell anatomy */}
<h3 className="mb-3 text-base font-semibold text-text-primary">
</h3>
<div className="mb-8">
<ExampleGroup title="② 壳样式解剖(源码当前真实值)">
<CompareTable
head={['维度', 'A 套 Dialog', 'B 套 OriginalDialog(已对齐 C 套)', 'AlertDialog(手拼底座)']}
rows={[
@@ -182,13 +181,10 @@ export function ModalSection() {
],
]}
/>
</div>
</ExampleGroup>
{/* ③ Side-by-side demos — identical content, different shells */}
<h3 className="mb-3 text-base font-semibold text-text-primary">
</h3>
<div className="mb-8">
<ExampleGroup title="③ 同一内容装进各个壳(逐个打开对比)">
<DemoGrid cols={3}>
{/* A-set raw primitives — the largest population */}
<Demo label="A 套 · 原语直接拼(22 处)" note="ui/Dialog.tsx · 浅黑毛玻璃 · 圆角8 · p-5">
@@ -278,7 +274,7 @@ export function ModalSection() {
</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>
<h3 className="text-h4 text-text-primary"></h3>
{demoBody}
<div className="flex justify-end gap-2">
<Button variant="outline"></Button>
@@ -296,11 +292,11 @@ export function ModalSection() {
<ConfirmReferenceDemo />
</Demo>
</DemoGrid>
</div>
</ExampleGroup>
{/* ④ 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">
<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
@@ -330,6 +326,7 @@ export function ModalSection() {
</li>
</ol>
</div>
</Section>
</ExampleGroup>
</ComponentPage>
);
}
+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