iter(g129): 新增排版文本组件 typography

This commit is contained in:
HIsheR
2026-10-26 15:18:27 +08:00
parent 6158f49583
commit ea2a846734
13 changed files with 451 additions and 10 deletions
+1
View File
@@ -60,3 +60,4 @@ export { affix } from './affix.js';
export { anchor } from './anchor.js';
export { popconfirm } from './popconfirm.js';
export { calendar } from './calendar.js';
export { typography } from './typography.js';
+16
View File
@@ -741,4 +741,20 @@ export interface CalendarProps {
onMonthChange?: (month: string) => void; // 翻月回调,参数为 YYYY-MM
}
/*** 排版文本入参(variant 决定语义标签:title 出 h1-h5、paragraph 出 p、text 出 span) ***/
export interface TypographyProps {
content: string | Node; // 文本内容:字符串按纯文本渲染,传 DOM 则原样插入
variant?: 'title' | 'text' | 'paragraph'; // 排版形态;缺省 text
level?: 1 | 2 | 3 | 4 | 5; // 标题层级,仅 variant=title 生效;缺省 3
type?: 'secondary' | 'success' | 'warning' | 'danger'; // 语义色调;缺省主文本色
strong?: boolean; // 加粗强调;缺省 false
underline?: boolean; // 下划线;缺省 false
delete?: boolean; // 删除线(作废文案);缺省 false
mark?: boolean; // 高亮底纹;缺省 false
code?: boolean; // 等宽代码样式;缺省 false
align?: 'left' | 'center' | 'right'; // 水平对齐;缺省左对齐
ellipsis?: number; // 截断行数(1 起),超出显示省略号且悬停给全文;缺省不截断
copyable?: boolean; // 附复制按钮:点后写剪贴板并短暂回显结果;缺省 false
}
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
+55
View File
@@ -0,0 +1,55 @@
import { h } from '@ui-arch/frame';
import type { TypographyProps } from './types.js';
// 标题层级 → 语义标签(level 已在入口夹到 1-5)
const HEADING = ['h1', 'h2', 'h3', 'h4', 'h5'] as const;
// 形态 → 缺省标签
const TAG: Record<string, string> = { title: 'h3', text: 'span', paragraph: 'p' };
/*** 复制到剪贴板:优先异步 API,非安全上下文退回复选式复制,两者都失败返回 false ***/
function copyText(text: string): Promise<boolean> {
if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(text).then(() => true, () => false);
const box = h('textarea', { class: 'ua-type-copybox', readonly: true }, text) as HTMLTextAreaElement;
document.body.append(box);
box.select(); // 不可见文本框选中后再走兼容接口
const ok = document.execCommand('copy');
box.remove();
return Promise.resolve(ok);
}
/*** 类名拼装:形态 + 语义色 + 装饰位,均为纯类名,颜色留给样式表与令牌 ***/
function classOf(props: TypographyProps): string {
const variant = props.variant ?? 'text';
return ['ua-type', `ua-type-${variant}`, props.type ? `is-${props.type}` : '', props.strong ? 'is-strong' : '',
props.underline ? 'is-underline' : '', props.delete ? 'is-delete' : '', props.mark ? 'is-mark' : '',
props.code ? 'is-code' : '', props.align ? `is-${props.align}` : '', props.ellipsis ? 'is-clamped' : '']
.filter(Boolean).join(' ');
}
/*** 复制按钮:点击后短暂回显结果,1.2 秒复原;按钮自身不参与文本截断 ***/
function copyButton(el: HTMLElement): HTMLElement {
const btn = h('button', { class: 'ua-type-copy', type: 'button', 'aria-label': '复制文本' }, '复制');
btn.addEventListener('click', async () => {
const ok = await copyText(el.textContent ?? '');
btn.textContent = ok ? '已复制' : '复制失败';
btn.classList.toggle('is-ok', ok); // 成功态换色,失败保持中性
setTimeout(() => {
btn.textContent = '复制';
btn.classList.remove('is-ok');
}, 1200); // 回显窗口
});
return btn;
}
/*** 排版文本:variant 决定语义标签(title 出 h1-h5、paragraph 出 p、text 出 span),ellipsis 走多行截断,copyable 附复制按钮 ***/
export function typography(props: TypographyProps): HTMLElement {
const variant = props.variant ?? 'text';
const level = Math.min(Math.max(props.level ?? 3, 1), 5); // 层级夹取,避免越界索引
const tag = variant === 'title' ? HEADING[level - 1] : TAG[variant];
const plain = typeof props.content === 'string' ? props.content : null; // 只有字符串可安全截断
const el = h(tag, { class: classOf(props) }, props.content);
el.style.setProperty('--ua-type-clamp', String(props.ellipsis ?? 0)); // 行数交给 CSS 变量,样式表统一 clamp
if (plain && props.ellipsis) el.title = plain; // 截断后悬停可读全文
if (!props.copyable) return el;
return h('span', { class: 'ua-type-wrap' }, el, copyButton(el));
}
+87 -1
View File
@@ -74,7 +74,8 @@
"multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用",
"anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器",
"popconfirm(props, trigger):二次确认气泡,挂在任意触发器上,就地确认危险操作;确定后先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」",
"calendar(props):月历,整月网格内嵌页面(排期 / 值班 / 日程),每日可带 marker 标记文案,方向键漫游 + Enter 选中,翻月走 onMonthChange"
"calendar(props):月历,整月网格内嵌页面(排期 / 值班 / 日程),每日可带 marker 标记文案,方向键漫游 + Enter 选中,翻月走 onMonthChange",
"typography(props):排版文本,variant 决定语义(标题出 h1-h5 / 段落出 p / 行内出 span),支持语义色、装饰位、对齐、多行截断与复制按钮"
],
"keyTypes": [
"ButtonProps",
@@ -2946,6 +2947,91 @@
"需要显示每天有几件事就传 marker:返回非空文案即渲染小徽标,并拼进 aria-label 让读屏也听得到",
"键盘:Tab 进入网格一次,←→ 逐日、↑↓ 逐周、Enter / 空格 选中;跨月会自动翻页并交接焦点"
]
},
{
"id": "typography",
"title": "排版文本 Typography",
"demo": "sec-typography",
"props": [
{
"n": "content",
"t": "string | Node",
"r": 1,
"d": "文本内容:字符串按纯文本渲染;传 DOM 则原样插入(此时不参与截断与复制)"
},
{
"n": "variant",
"t": "'title' | 'text' | 'paragraph'",
"df": "text",
"d": "排版形态,决定语义标签与字号:title 出 h1-h5、paragraph 出 p、text 出 span"
},
{
"n": "level",
"t": "1 | 2 | 3 | 4 | 5",
"df": "3",
"d": "标题层级,仅 variant=title 生效;越界值会被夹到 1-5"
},
{
"n": "type",
"t": "'secondary' | 'success' | 'warning' | 'danger'",
"d": "语义色调;缺省主文本色"
},
{
"n": "strong",
"t": "boolean",
"df": "false",
"d": "加粗强调"
},
{
"n": "underline",
"t": "boolean",
"df": "false",
"d": "下划线(可为链接式文案保留指引视觉)"
},
{
"n": "delete",
"t": "boolean",
"df": "false",
"d": "删除线,用于作废文案;会同时弱化文字颜色"
},
{
"n": "mark",
"t": "boolean",
"df": "false",
"d": "高亮底纹(警示色 24% 混透明),用于搜索命中片段"
},
{
"n": "code",
"t": "boolean",
"df": "false",
"d": "等宽代码样式,用于行内字段名 / 命令"
},
{
"n": "align",
"t": "'left' | 'center' | 'right'",
"d": "水平对齐;缺省左对齐"
},
{
"n": "ellipsis",
"t": "number",
"d": "截断行数(1 起):超出显示省略号,纯文本自动挂 title 供悬停看全文;缺省不截断"
},
{
"n": "copyable",
"t": "boolean",
"df": "false",
"d": "右侧附复制按钮:点击写剪贴板并回显「已复制 / 复制失败」,1.2 秒后复原"
}
],
"example": "typography({ variant: 'title', level: 2, content: '部署说明' }); typography({ content: 'RB-2048', code: true, copyable: true }); typography({ variant: 'paragraph', ellipsis: 2, content: '一段很长的说明……' })",
"steps": [
"正文一律用 variant='text' 或 'paragraph',标题用 variant='title' + level,标签自动出 h1-h5,别自己写 h 标签再叠样式",
"文内强调用装饰位组合:strong 加粗、mark 高亮、code 等宽、underline / delete 表示链接与作废,一个组件内可叠加",
"长描述(卡片摘要、表格说明)用 ellipsis 给行数:超出显示省略号并自动挂 title,悬停即看全文,不必额外做展开按钮",
"订单号 / 密钥 / 命令这类要复制的内容传 copyable:按钮独立于文本节点,复制失败会回显「复制失败」而不是静默",
"content 传 DOM 时组件只负责语义标签与类名:截断与复制对 DOM 内容不生效(取不到稳定文本)",
"段落连续排列时相邻 .ua-type-paragraph 自动留出间距,单个段落不额外撑高,嵌进卡片也不会有空隙"
]
}
]
}
+84
View File
@@ -3961,3 +3961,87 @@ button.ua-step-dot:hover {
pointer-events: auto;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.18);
}
/* —— 排版文本:层级靠字号与字重区分,语义色与装饰位只换颜色,不引布局 —— */
.ua-type {
color: var(--ua-color-text);
font-size: var(--ua-font-size-md);
line-height: 1.65;
margin: 0;
}
.ua-type-paragraph + .ua-type-paragraph {
margin-top: var(--ua-space-2); /* 段落之间留呼吸,单段不额外撑高 */
}
.ua-type-title {
font-weight: 600;
line-height: 1.35;
letter-spacing: -0.01em;
}
h1.ua-type-title { font-size: calc(var(--ua-font-size-lg) * 1.6); }
h2.ua-type-title { font-size: calc(var(--ua-font-size-lg) * 1.32); }
h3.ua-type-title { font-size: calc(var(--ua-font-size-lg) * 1.12); }
h4.ua-type-title { font-size: var(--ua-font-size-lg); }
h5.ua-type-title { font-size: var(--ua-font-size-md); }
.ua-type.is-secondary { color: var(--ua-color-text-muted); }
.ua-type.is-success { color: var(--ua-color-success); }
.ua-type.is-warning { color: var(--ua-color-warning); }
.ua-type.is-danger { color: var(--ua-color-danger); }
.ua-type.is-strong { font-weight: 600; }
.ua-type.is-underline { text-decoration: underline; text-underline-offset: 2px; }
.ua-type.is-delete { text-decoration: line-through; color: var(--ua-color-text-muted); }
.ua-type.is-underline.is-delete { text-decoration: underline line-through; }
.ua-type.is-mark {
background: color-mix(in srgb, var(--ua-color-warning) 24%, transparent);
border-radius: var(--ua-radius-sm);
padding: 0 2px;
}
.ua-type.is-code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.92em;
background: color-mix(in srgb, var(--ua-color-border) 45%, transparent);
border-radius: var(--ua-radius-sm);
padding: 1px var(--ua-space-1);
}
.ua-type.is-center { text-align: center; }
.ua-type.is-right { text-align: right; }
.ua-type.is-clamped {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--ua-type-clamp, 1);
overflow: hidden;
text-overflow: ellipsis;
}
.ua-type-wrap {
display: inline-flex;
align-items: baseline;
gap: var(--ua-space-1);
max-width: 100%;
}
.ua-type-copy {
border: 0;
background: transparent;
color: var(--ua-color-text-muted);
font: inherit;
font-size: var(--ua-font-size-sm);
padding: 0 2px;
border-radius: var(--ua-radius-sm);
cursor: pointer;
transition: color 0.15s ease, background 0.15s ease;
}
.ua-type-copy:hover {
color: var(--ua-color-accent);
background: color-mix(in srgb, var(--ua-color-accent) 10%, transparent);
}
.ua-type-copy.is-ok { color: var(--ua-color-success); }
.ua-type-copybox {
position: fixed;
left: -9999px;
top: 0;
opacity: 0; /* 兼容复制用的离屏文本框,不参与视觉布局 */
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.ua-type-copy {
transition: none;
}
}