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
+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));
}