iter(g129): 新增排版文本组件 typography
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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 }; // 再导出常用内核类型,方便调用方
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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 自动留出间距,单个段落不额外撑高,嵌进卡片也不会有空隙"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user