import { computed, effect, h, mount, renderList, signal } from '@ui-arch/frame'; /*** 演示用内联 SVG 图片(data URI,离线可用) ***/ const IMG_DEMO = 'data:image/svg+xml;utf8,' + encodeURIComponent( '' + '' + '' + 'IMAGE DEMO', ); import { accordion, affix, alert, anchor, autoComplete, avatarGroup, avatar, backTop, badge, breadcrumb, button, calendar, card, cascader, checkbox, checkboxGroup, codeBlock, colorPicker, timePicker, commandPalette, copyButton, countdown, dataTable, layout, jsonViewer, kbd, heatmap, highlight, datePicker, transfer, descriptions, diffView, divider, drawer, empty, floatButton, image, form, formItem, inputNumber, inputTag, grid, link, loading, list, carousel, treeSelect, tree, notification, noticeBar, result, upload, watermark, menu, masonry, editable, theme, modal, mentions, multiSelect, pagination, pinInput, popconfirm, popover, progress, progressRing, radioGroup, relativeTime, rate, segmented, select, showToast, signaturePad, skeleton, sortableList, splitter, slider, sparkline, space, spinner, stepper, statistic, switchButton, tabs, tag, textarea, textField, timeline, toast, tour, tooltip, typography, virtualList, } from '@ui-arch/ui'; import type { RowKey } from '@ui-arch/ui'; /*** 侧边目录:挂载后从 .sec 反推锚点,IntersectionObserver 高亮视口内区块(清单改动自动跟随);顶部搜索框按标题过滤 ***/ function buildToc(toc: HTMLElement, content: HTMLElement): void { const secs = Array.from(content.querySelectorAll('section.sec')); if (!secs.length) return; // 卫语句:无区块不建目录 const links = secs.map((sec) => h('a', { class: 'toc-link', href: `#${sec.id}`, 'data-for': sec.id }, sec.querySelector('h2')?.textContent ?? sec.id), ); const search = h('input', { class: 'toc-search', type: 'search', placeholder: '过滤组件…', 'aria-label': '过滤组件目录' }) as HTMLInputElement; const empty = h('p', { class: 'toc-empty' }, '无匹配项'); // 过滤后一条不剩时的提示 empty.hidden = true; /*** 按标题子串过滤目录项;空关键字恢复全量 ***/ const apply = (): void => { const q = search.value.trim().toLowerCase(); let hit = 0; for (const link of links) { const on = !q || (link.textContent ?? '').toLowerCase().includes(q); link.hidden = !on; // 隐藏项同时退出 Tab 序(hidden 自带) if (on) hit++; } empty.hidden = hit > 0; }; search.addEventListener('input', apply); search.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key !== 'Escape' || !search.value) return; // 卫语句:无内容时 Esc 交给默认行为 search.value = ''; apply(); }); toc.append(search, ...links, empty); const io = new IntersectionObserver( (entries) => { for (const en of entries) { if (!en.isIntersecting) continue; // 只处理刚进入视口的区块 const id = (en.target as HTMLElement).id; for (const link of links) link.classList.toggle('is-active', link.getAttribute('data-for') === id); } }, { rootMargin: '-10% 0px -80% 0px' }, // 顶部窄条判定,滚动时高亮更跟手 ); for (const sec of secs) io.observe(sec); } /*** 生成演示区块;id 供文档站 iframe 锚点定位 ***/ function section(id: string, title: string, desc: string, ...body: unknown[]): HTMLElement { return h( 'section', { class: 'sec', id }, h('h2', null, title), h('p', { class: 'desc' }, desc), h('div', { class: 'stage' }, ...body), ); } // —— 演示数据 —— const cities = ['北京', '上海', '深圳', '杭州']; const users = [ 'Alice', 'Bob', 'Charlie', 'Dave', 'Eve', 'Frank', 'Grace', 'Hank', 'Ivy', 'Jack', 'Kate', 'Liam', 'Mia', 'Noah', 'Olivia', 'Paul', 'Quinn', 'Rose', 'Sam', 'Tina', 'Uma', 'Victor', 'Wendy', 'Xavier', ].map((name, i) => ({ id: i + 1, name, age: 22 + ((i * 7) % 20), city: cities[i % cities.length] })); // —— 月历标记:以今天为基准前后几天,避免把日期写死 —— const dayKey = (n: number): string => { const d = new Date(); d.setDate(d.getDate() + n); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; const calendarMarks: Record = { [dayKey(0)]: '2 项', [dayKey(2)]: '1 项', [dayKey(-3)]: '5 项' }; const calDay = signal(dayKey(0)); // —— 响应式状态演示 —— const count = signal(0); const saving = signal(false); // 按钮 loading 态演示(模拟异步保存) // —— 双向绑定演示 —— const name = signal(''); // —— 表单控件状态集中声明 —— const agreed = signal(false); const nightMode = signal(true); const sheetOpen = signal(false); // 底部抽屉开合 const stickyOpen = signal(false); // 防误关弹窗开合 const sound = signal(true); // 无可见文案的开关演示 const city = signal(''); const stackPick = signal(''); // 分组下拉的选中值 const size = signal('m'); const bio = signal(''); // —— 反馈组件状态 —— const percent = signal(0); setInterval(() => percent((percent() + 5) % 105), 400); // 自动推进演示进度 // —— 标签页 / 弹窗状态 —— const activeTab = signal('a'); // —— 可关闭页签:数据源为信号数组,onClose 里摘掉元素即复现 —— const closableTabs = signal([ { value: 'n1', label: '订单 #1024', closable: true }, { value: 'n2', label: '订单 #1025', closable: true }, { value: 'n3', label: '订单 #1026', closable: true }, ]); const closableActive = signal('n1'); // —— 溢出页签:容器装不下时,切到被裁掉的页签应自动拉回可视区 —— const wideTab = signal('t0'); const modalOpen = signal(false); const modalPick = signal('a'); // 弹窗内下拉的取值,用于演示 Esc 逐层关闭 // —— 表单校验状态集中声明 —— const inviteCode = signal(''); // 异步校验演示:邀请码 const note = signal(''); // 表单跨列字段的值 const username = signal(''); const email = signal(''); const formResult = signal(''); // —— 分页联动状态:dataTable 与独立 pagination 共用同一页码信号 —— const tablePage = signal(1); // —— 抽屉开关 —— const drawerOpen = signal(false); const inspectorOpen = signal(false); // 非模态抽屉:无遮罩,页面仍可操作 // —— 标签列表:关闭即写回信号,容器按信号重渲染 —— const tags = signal(['设计', '前端', '组件库']); const chipOn = { 全部: signal(true), 进行中: signal(false), 已完成: signal(false), 已归档: signal(false) }; // 筛选片选中态:每片独立信号,供 tag 双向绑定 // —— 滑块:音量与禁用态演示 —— const volume = signal(40); const priceLo = signal(200); // 区间滑块下限 const priceHi = signal(800); // 区间滑块上限 // —— 数字输入:购买数量演示,与禁用态实例共用同一信号 —— const qty = signal(1); const price = signal('9.9'); // 演示 suffix 附加块 const pwd = signal(''); // 演示 password 显隐切换 const amount = signal(2); // 演示 inputNumber 前后缀 // —— 步骤条:下单流程演示,按钮推进、点已完成步骤回跳 —— const stepIdx = signal(0); // —— 评分:满意度演示,与只读实例共用同一信号 —— const score = signal(0); const score2 = signal(3); // 演示 allowClear 可清除评分 // —— 空状态:搜索无结果演示 —— const noResult = signal(true); // —— 行内编辑:标题与备注各一份信号,提交才落值(Esc 取消可肉眼验证没被改) —— const titleText = signal('季度增长看板'); const remark = signal('点这里就地改;Esc 丢弃草稿,失焦自动保存。'); // —— 受控选中:dataTable 内部勾选实时写回,外部可清空 —— const picked = signal([]); // —— 表格加载态:切换后数据行变骨架行,筛选与翻页临时禁用 —— const tableLoading = signal(false); // —— 菜单选中结果:演示 onSelect 回调 —— const menuPick = signal(''); const listPick = signal(''); // 列表点击选中的条目标题 const slideIndex = signal(0); // 轮播当前下标(外部受控演示) const cascaderPath = signal(['east', 'sh']); // 级联选择路径(外部受控演示) const splitSizes = signal([35, 65]); // 分栏比例(外部受控演示) const tagPick = signal(['ts']); // 多选下拉选中值(外部受控演示) const tagFold = signal(['ts', 'dom', 'css']); // 多选下拉折叠演示:预置 3 项看溢出 const permPick = signal(['read']); // 多选组选中值(外部受控演示) const colorVal = signal('var(--ua-color-accent)'); // 颜色选择值(外部受控演示) const atTime = signal('09:30'); // 时间选择值:给定初值即受控,组件选完写回 const dateVal = signal('2026-10-26'); // 日期选择值(外部受控演示) const rangeFrom = signal(''); // 区间起点 const rangeTo = signal(''); // 区间终点 const acValue = signal(''); // 自动补全输入值(外部受控演示) const pinCode = signal(''); // 验证码输入值(外部受控演示) const pinMask = signal(''); // 密文验证码输入值(mask 演示) const resendCd = countdown({ seconds: 90, running: false, format: (s) => `重发(${s}s)` }); // 重发倒计时(手动启动演示) const highlightText = signal('数据不出端、预测低于 50ms、习惯本地学习是输入法增强系统的三条底线'); // 高亮演示原文 const highlightKw = signal('本地'); // 高亮演示关键词(与搜索框信号联动) const demoTags = signal(['本地推理', '隐私优先']); // 标签输入演示集合(成标签 / 删 chip 双向写回) const tagRows = [ { id: 'r1', name: '本地 ASR 推理', tags: ['本地推理', '隐私优先'] }, { id: 'r2', name: '云端补全兜底', tags: ['云端'] }, { id: 'r3', name: '习惯冷启动加速', tags: ['本地推理'] }, ]; // 标签筛选演示数据:每行带 tags 供「且」关系匹配 const tagMatched = computed(() => tagRows.filter((row) => demoTags().every((tag) => row.tags.includes(tag)))); // 派生行集合:标签变化即重算,喂给 dataTable 的信号数据源 const treeChecked = signal(['core']); // 多选树勾选集合(外部受控演示) const treePick = signal('core'); // 树形选中节点 key const transferKeys = signal(['a11y']); // 穿梭框右侧集合(外部受控演示) const cityPick = signal(''); // 可搜索下拉的选中城市 const pagerPage = signal(1); // 独立分页器当前页 const simplePage = signal(2); // 极简分页演示用页码 const pickedFiles = signal(''); // 文件选择结果(名称串) // —— 禁用态演示:开关信号同时绑定 switch 的 checked 与 checkbox 的 disabled —— const locked = signal(true); // —— 统计数值:两个指标共用同一组按钮刷新,演示滚动过渡 —— const gmv = signal(128000); const orders = signal(2048); // —— 分段控制器:两个实例共用同一信号,演示信号驱动同步与整组禁用 —— const range = signal('week'); // —— 时间线:false 时末尾显示「进行中」虚线节点,true 表示流程已结束 —— const flowDone = signal(false); // —— @提及:受控文本信号,选中的人写进评论正文 —— const comment = signal(''); const teamMembers = [ { name: 'zhangsan', label: '张三', description: '前端 · 组件库', keywords: ['zs', '1001'] }, { name: 'lisi', label: '李四', description: '后端 · 订单服务', keywords: ['ls', '1002'] }, { name: 'wangwu', label: '王五', description: '设计 · 交互', keywords: ['ww', '1003'] }, { name: 'zhaoliu', label: '赵六', description: 'QA · 回归', keywords: ['zl', '1004'] }, ]; // —— 树形选择:单选存 key、多选存 key 集合,两个实例共用同一棵部门树 —— const dept = signal(''); const deptChecked = signal([]); const orgTree = [ { key: 'tech', label: '技术中心', children: [ { key: 'fe', label: '前端组', children: [{ key: 'ui', label: '组件库' }, { key: 'web', label: '业务前端' }] }, { key: 'be', label: '后端组', children: [{ key: 'order', label: '订单服务' }, { key: 'pay', label: '支付服务' }] }, ] }, { key: 'design', label: '设计中心', children: [{ key: 'ix', label: '交互' }, { key: 'vd', label: '视觉' }] }, { key: 'qa', label: '质量中心', children: [{ key: 'reg', label: '回归测试', disabled: true }] }, ]; // —— 主题:URL 参数(文档站 iframe 联动)> 上次选择 > 跟随系统;优先级、落盘与系统偏好监听都收在 theme() 里 —— const th = theme({ storageKey: 'ua-demo-theme' }); // 与文档站 iframe 共用 ?theme=dark|light 入口 const dark = th.dark; // 实际生效是否暗色:system 档下也跟随系统,控件直接绑它 // —— 拖拽排序演示:顺序写回信号,下方摘要随之刷新 —— const sortableOrder = signal(['需求评审', '接口联调', '灰度发布', '线上回归']); const sortableDemo = sortableList({ items: ['需求评审', '接口联调', '灰度发布', '线上回归'], value: sortableOrder }); const content = h( 'div', { class: 'page' }, h( 'header', null, h('div', null, h('h1', null, '@ui-arch 组件库 · 现场演示'), h('p', { class: 'sub' }, '驱动:@ui-arch/frame(自研微框架) + @ui-arch/ui(组件库) + @ui-arch/table-core(表格内核)')), switchButton({ label: '暗色主题', checked: dark, onChange: (v) => th.set(v ? 'dark' : 'light') }), // 切换 html[data-theme],令牌整套换色(记忆写回由 theme 负责) ), section( 'sec-counter', '响应式状态', 'signal 驱动:按钮修改信号,文本自动更新,无虚拟 DOM。', button({ label: 'count +1', onClick: () => count(count() + 1) }), button({ label: '重置', variant: 'ghost', onClick: () => count(0) }), button({ label: '保存(模拟请求)', loading: saving, onClick: () => { saving(true); setTimeout(() => saving(false), 1500); } }), h('span', { class: 'out' }, () => `count = ${count()}`), ), section( 'sec-bind', '双向绑定', 'textField 与信号双向同步:输入即回显;prefix/suffix 渲染为输入框内侧附加块;clearable 开启后有值即显一键清空;password 原生打点并内置显隐切换。', textField({ placeholder: '输入名字…', value: name }), textField({ placeholder: '搜索…', value: name, prefix: searchIcon(), clearable: true }), textField({ value: price, suffix: '元', clearable: true }), textField({ placeholder: '密码…', value: pwd, password: true, clearable: true }), h('span', { class: 'out' }, () => (name() ? `你好,${name()}!` : '等待输入…')), ), section( 'sec-form', '表单控件', 'checkbox / switch / select / radioGroup / textarea:状态一律由调用方信号持有;select 弹层在视口下方空间不足时自动翻到上方,展开后键入字母可快跳选项;textarea 传 maxCount 显示实时字数。', h('div', { class: 'row' }, checkbox({ label: '同意条款', checked: agreed }), h('span', { class: 'out' }, () => (agreed() ? '已同意' : '未同意'))), h('div', { class: 'row' }, switchButton({ label: '夜间模式', checked: nightMode }), h('span', { class: 'out' }, () => (nightMode() ? '开' : '关'))), h('div', { class: 'row' }, switchButton({ checked: sound, ariaLabel: '通知声音' }), h('span', { class: 'out' }, () => `无可见文案的开关(ariaLabel 兜底,读屏念「通知声音」):${sound() ? '开' : '关'}`)), h('div', { class: 'row' }, select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '选择城市…', clearable: true }), h('span', { class: 'out' }, () => (city() ? `城市:${city()}(点 × 或按 Delete 清空)` : '未选择'))), h('div', { class: 'row' }, select({ // 分组选项:相邻同组合成一段,过滤后空组标题自动隐藏 options: [ { value: 'vue', label: 'Vue 3', group: '前端框架' }, { value: 'react', label: 'React 19', group: '前端框架' }, { value: 'svelte', label: 'Svelte 5', group: '前端框架' }, { value: 'node', label: 'Node.js 22', group: '服务端运行时' }, { value: 'deno', label: 'Deno 2', group: '服务端运行时' }, { value: 'bun', label: 'Bun 1.2', group: '服务端运行时' }, ], value: stackPick, placeholder: '按分类选技术栈…', }), h('span', { class: 'out' }, () => (stackPick() ? `技术栈:${stackPick()}` : '分组下拉:试试搜索「d」'))), h('div', { class: 'row' }, radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }, { value: 'l', label: '大' }], value: size })), textarea({ placeholder: '一句话介绍…', value: bio, rows: 2, maxCount: 30 }), textarea({ placeholder: '自适应高度(1-4 行钳制)…', value: signal(''), autosize: { minRows: 1, maxRows: 4 } }), h('span', { class: 'out' }, () => `简介 ${bio().length} 字`), h( 'div', { class: 'row' }, button({ label: '禁用按钮', variant: 'ghost', disabled: true }), checkbox({ label: '信号禁用的勾选', disabled: locked }), switchButton({ label: '锁定', checked: locked }), // 关掉开关即解锁上面的勾选框与输入框 textField({ placeholder: '锁定后不可输入…', disabled: locked }), ), h( 'div', { class: 'row' }, select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '信号禁用的下拉…', disabled: locked }), radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }], value: size, disabled: locked }), radioGroup({ options: [{ value: 'x', label: '静态禁用' }], value: size, disabled: true }), ), ), section( 'sec-drawer', '抽屉', 'drawer:open 信号驱动侧滑面板,保留主视图上下文;四向滑出(左右按宽度、上下按高度),遮罩 / × / Esc 关闭。', h('div', { class: 'row' }, button({ label: '从右侧打开', onClick: () => drawerOpen(true) }), button({ label: '从底部打开', variant: 'ghost', onClick: () => sheetOpen(true) }), button({ label: '非模态打开', variant: 'ghost', onClick: () => inspectorOpen(true) }), ), drawer( { title: '订单详情', open: drawerOpen, width: 360, onClose: () => drawerOpen(false), footer: h('div', { class: 'row' }, button({ label: '关闭', variant: 'ghost', onClick: () => drawerOpen(false) })), }, h('p', null, '这里是抽屉内容,可放任意 DOM。'), h('div', { class: 'row' }, tag({ label: '#A1024', variant: 'info' }), badge({ label: '已发货', variant: 'success' })), ), drawer( { title: '快捷操作', open: sheetOpen, placement: 'bottom', width: 220, onClose: () => sheetOpen(false) }, h('p', null, '从底部滑出的操作面板:适合移动端底部弹层,宽度自动铺满、高度由 width 指定。'), ), drawer( { title: '非模态检查器', open: inspectorOpen, modal: false, width: 300, onClose: () => inspectorOpen(false), footer: h('div', { class: 'row' }, button({ label: '关闭', variant: 'ghost', onClick: () => inspectorOpen(false) })), }, h('p', null, 'modal: false —— 没有遮罩、不锁背景滚动、不困焦点:左边页面照常能点能滚,适合「一边看一边改」的检查器 / 详情面板。'), ), ), section( 'sec-tag', '标签', 'tag:可关闭标签(badge 的交互版),关闭回调里改信号即从列表移除。', h( 'div', { class: 'row' }, tag({ label: '纯展示', variant: 'default' }), tag({ label: '通过', variant: 'success' }), tag({ label: '待处理', variant: 'warning' }), tag({ label: '已失效', variant: 'danger' }), ), (() => { const box = h('div', { class: 'row' }); renderList(box, () => tags().map((t) => tag({ label: t, variant: 'info', closable: true, onClose: () => tags(tags().filter((x) => x !== t)) }))); return box; })(), (() => { const chips = h('div', { class: 'row' }); renderList(chips, () => Object.entries(chipOn).map(([name, on]) => tag({ label: name, checkable: true, checked: on }))); const out = h('span', { class: 'out' }, () => `已选:${Object.entries(chipOn).filter(([, on]) => on()).map(([name]) => name).join(' / ') || '无'}`); return h('div', {}, chips, out); })(), ), section( 'sec-avatar', '头像', 'avatar:有 src 走图片(alt 用名称),无 src 用首字占位;size 与语义底色都可调。', h( 'div', { class: 'row' }, avatar({ name: '张三' }), avatar({ name: 'Alice', size: 40, variant: 'success' }), avatar({ name: 'Bob', size: 24, variant: 'warning' }), avatarGroup({ items: [{ name: '张三' }, { name: '李四', variant: 'success' }, { name: '王五', variant: 'warning' }, { name: '赵六' }, { name: '钱七' }, { name: '孙八' }], max: 4 }), avatar({ name: '李四', size: 36, variant: 'danger' }), ), ), section( 'sec-slider', '滑块', 'slider:原生 range 承载交互(含键盘 ←→),value 信号双向绑定,右侧可显示当前值;range 模式叠两条原生轨做双滑块区间。', h( 'div', { class: 'row' }, slider({ value: volume, min: 0, max: 100, step: 5, onChange: (v) => toast.info(`音量 ${v}`) }), slider({ value: volume, disabled: locked }), slider({ value: volume, marks: [{ value: 0, label: '静音' }, { value: 50, label: '中' }, { value: 100, label: '最大' }], onChange: (v) => toast.info(`音量 ${v}`) }), slider({ value: priceLo, end: priceHi, range: true, min: 0, max: 1000, step: 50, onRangeChange: (a, b) => toast.info(`价格 ${a} ~ ${b}`) }), ), ), section( 'sec-inputnumber', '数字输入', 'inputNumber:−/+ 按钮与键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取;长按按钮可连发步进。', h( 'div', { class: 'row' }, inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => toast.info(`数量 ${v}`) }), inputNumber({ value: qty, disabled: locked }), inputNumber({ value: amount, min: 0, prefix: '¥', suffix: '件' }), ), h('div', { class: 'out' }, '↓ 控件高度三档并排(sm / md / lg):button / textField / select / inputNumber 统一走 --ua-control-h-*'), h( 'div', { class: 'row' }, button({ label: '小号', size: 'sm' }), button({ label: '默认' }), button({ label: '大号', size: 'lg' }), textField({ placeholder: 'sm', size: 'sm' }), textField({ placeholder: 'md' }), textField({ placeholder: 'lg', size: 'lg' }), select({ options: [{ value: 'sm', label: 'sm' }, { value: 'md', label: 'md' }, { value: 'lg', label: 'lg' }], value: signal('md'), size: 'sm' }), select({ options: [{ value: 'a', label: '选项 A' }, { value: 'b', label: '选项 B' }], value: signal('a') }), select({ options: [{ value: 'a', label: '选项 A' }, { value: 'b', label: '选项 B' }], value: signal('a'), size: 'lg' }), inputNumber({ value: qty, size: 'sm' }), inputNumber({ value: qty }), inputNumber({ value: qty, size: 'lg' }), ), ), section( 'sec-feedback', '反馈与提示', 'badge / alert / spinner / progress:语义色全部来自令牌,暗色自动切换。', badge({ label: '默认' }), badge({ label: '成功', variant: 'success' }), badge({ label: '警告', variant: 'warning' }), badge({ label: '危险', variant: 'danger' }), badge({ count: 5, label: '未读' }), badge({ count: 128, max: 99, label: '未读' }), badge({ dot: true, label: '有未读消息' }), alert({ message: '信息提示:这是一条 info 提示条。' }), alert({ message: '警告提示:操作可能存在风险。', variant: 'warning' }), h('div', { class: 'row' }, spinner(), spinner({ size: 26 }), spinner({ size: 22, label: '正在加载订单列表' })), progress({ value: percent, label: '上传进度' }), h('div', { class: 'row' }, progress({ value: percent, type: 'circle', label: '上传进度(环形)' }), progress({ value: percent, type: 'circle', variant: 'success', label: '同值换语义色' })), h('div', { class: 'row' }, h('span', { class: 'out' }, '语义色:'), progress({ value: percent, variant: 'danger', label: '同步失败重试进度' })), h('div', { class: 'row' }, h('span', { class: 'out' }, '不确定态:'), progress({ label: '正在连接' })), ), section( 'sec-alert', '提示条', 'alert:四档语义色 + 可选标题与关闭按钮,role=alert 由读屏播报。', alert({ message: '这是一条中性提示(无标题)', variant: 'info' }), alert({ title: '保存失败', message: '网络波动,请稍后重试;本次修改已保留在本地草稿。', variant: 'danger', closable: true, onClose: () => toast.warning('提示已关闭') }), alert({ title: '发布成功', message: '构建产物已上传,5 分钟后全网生效。', variant: 'success', showIcon: true }), alert({ message: '配额即将用尽:本月剩余 8%,超出后新请求会被限流。', variant: 'warning', showIcon: true, banner: true }), ), section( 'sec-tooltip', '悬浮提示', 'tooltip 包裹触发器:hover / 聚焦延迟 150ms 出场,移出、失焦或 Esc 收起,气泡不抢指针;方位支持上下左右四向,视口该侧空间不足时自动翻到对侧。', h( 'div', { class: 'row' }, tooltip({ content: '保存后立即生效' }, button({ label: '保存' })), tooltip({ content: '该操作不可撤销', placement: 'bottom' }, button({ label: '删除', variant: 'danger' })), tooltip({ content: '聚焦输入框同样会显示' }, textField({ placeholder: '聚焦我…' })), tooltip({ content: '左侧方位:竖排工具栏常用', placement: 'left' }, button({ label: '左侧气泡', variant: 'ghost' })), tooltip({ content: '右侧方位,贴右边时自动翻回左侧', placement: 'right' }, button({ label: '右侧气泡', variant: 'ghost' })), ), ), section( 'sec-toast', '消息提示', 'toast 右上角堆叠弹出,默认 3s 自动淡出移除(悬停可暂停倒计时);语义色四档,出入场微动画。', h( 'div', { class: 'row' }, button({ label: '成功', onClick: () => toast.success('保存成功') }), button({ label: '错误', variant: 'danger', onClick: () => toast.error('网络请求失败') }), button({ label: '警告', onClick: () => toast.warning('磁盘空间不足') }), button({ label: '信息(1.5s)', onClick: () => toast.info('已同步云端', 1500) }), button({ label: '可关闭', variant: 'ghost', onClick: () => showToast({ message: '这条可以手动关掉', variant: 'warning', closable: true }) }), button({ label: '带撤销', variant: 'ghost', onClick: () => showToast({ message: '已删除 3 条记录', variant: 'success', actionLabel: '撤销', duration: 5000, onAction: () => toast.info('已撤销删除') }) }), ), ), section( 'sec-skeleton', '骨架屏', 'skeleton 占位:text 多行段落 / rect 块 / circle 圆,扫光微动画,reduced-motion 自动静态化。', card( { title: '加载中的用户资料' }, h('div', { class: 'row' }, skeleton({ variant: 'circle' }), skeleton({ variant: 'text', rows: 2 })), skeleton({ variant: 'rect' }), ), ), section( 'sec-formlab', '表单校验', 'form + formItem:必填 / 长度 / 正则同步校验,validator 返回 Promise 即异步规则(提交会等待);全部通过才触发提交。校验不通过时顶部出现「有 N 项需要修改」汇总条(role=alert 会被读屏播报)并聚焦第一处——自绘控件(下拉 / 评分 / 开关)没有原生 input,焦点退到它的触发器,整块不可聚焦时落在字段容器上;onError 拿到不通字段数。本例 columns: 2 走两列栅格,窄屏自动堆叠,按钮区始终整行;字段传 span 可跨列(备注整宽)。', form( { columns: 2, submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()} / ${city()}`) }, [ formItem({ label: '用户名', required: true, value: username, rules: [{ min: 2, max: 12, message: '用户名需 2-12 个字符' }] }, textField({ placeholder: '2-12 个字符', value: username })), formItem({ label: '邮箱', required: true, value: email, hint: '用于登录与找回密码,不会公开', rules: [{ pattern: /^[\w.-]+@[\w-]+\.[\w.]+$/, message: '邮箱格式不正确' }] }, textField({ placeholder: 'you@example.com', value: email })), formItem({ label: '城市', required: true, value: city }, select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '请选择城市…' })), formItem( { label: '邀请码', required: true, value: inviteCode, rules: [{ validator: async (v) => { await new Promise((r) => setTimeout(r, 400)); return v === 'USED' ? '该邀请码已被使用' : true; }, message: '校验未通过' }] }, textField({ placeholder: '异步规则:输入 USED 会校验失败', value: inviteCode }), ), formItem({ label: '备注', value: note, span: 2, hint: 'span: 2 —— 多列表单里整行铺开,单列时自动退回整行' }, textarea({ placeholder: '多行备注…', value: note, rows: 2, maxCount: 120 })), ], ), h('span', { class: 'out' }, () => formResult() || '等待提交…'), ), section( 'sec-tabs-modal', '标签页与弹窗', 'tabs 滑动指示条 + 方向键切换(←→),禁用页签置灰,closable 页签带 ×(聚焦后 Delete 也能关,关闭只回调、由数据源摘除后重绘);modal 支持 Esc / 遮罩 / × 关闭(closeOnOverlay: false 时点遮罩不关、防填一半误触),弹窗内的下拉展开时 Esc 先收下拉、再按才关弹窗(逐层关闭)。', tabs({ tabs: [{ value: 'a', label: '概览' }, { value: 'b', label: '明细' }, { value: 'c', label: '设置' }, { value: 'd', label: '已停用', disabled: true }], active: activeTab, extra: button({ label: '新建页签', variant: 'ghost', onClick: () => toast.info('右侧操作区与页签同一行') }), // extra 放页签栏右侧 }), h('span', { class: 'out' }, () => `当前页签:${activeTab()}`), renderList(h('div', { class: 'row' }), () => { const items = closableTabs(); if (!items.length) return [h('span', { class: 'desc' }, '页签全关掉了 —— onClose 里从数据源摘掉,重绘自然只剩空态')]; return [tabs({ tabs: items, active: closableActive, onClose: (v) => { const left = closableTabs().filter((t) => t.value !== v); // 数据源唯一来源:摘掉即重绘 closableTabs(left); if (closableActive() === v) closableActive(left[0]?.value ?? ''); // 关掉的正好是当前页:顺位激活下一个 }, })]; }), h('div', { style: 'max-width:320px;width:100%' }, tabs({ tabs: Array.from({ length: 8 }, (_, i) => ({ value: `t${i}`, label: `选项 ${i + 1}` })), // 八个页签必然溢出,用来验证自动回拉 active: wideTab, })), h('span', { class: 'out' }, () => `溢出演示当前页签:${wideTab()}(点最右侧的页签,标题条会自动滚过去)`), button({ label: '打开弹窗', onClick: () => modalOpen(true) }), button({ label: '打开防误关弹窗', variant: 'ghost', onClick: () => stickyOpen(true) }), modal( { title: '填写中,点遮罩不关闭', open: stickyOpen, closeOnOverlay: false, onClose: () => stickyOpen(false), footer: h('div', { class: 'row' }, button({ label: '关闭', variant: 'ghost', onClick: () => stickyOpen(false) })), }, h('p', null, 'closeOnOverlay: false —— 点遮罩不会关闭,防止填到一半误触丢失内容;× 与 Esc 仍然有效。'), ), modal( { title: '确认操作(内置页脚 · 异步确认)', open: modalOpen, size: 'lg', confirmText: '确认提交', cancelText: '再想想', onClose: () => modalOpen(false), onConfirm: async () => { await new Promise((r) => setTimeout(r, 900)); toast.success('已确认,窗口自动关闭'); }, // 成功后组件自己收窗 }, h('p', null, '不传 footer 时走内置按钮组:onConfirm 返回 Promise 期间按钮变「处理中…」并禁用,成功才收窗,失败留在窗口重试(提示交给调用方的 toast)。'), h('div', { class: 'row' }, badge({ label: 'modal', variant: 'success' }), badge({ label: 'onConfirm', variant: 'default' })), select({ options: [{ value: 'a', label: '优先处理' }, { value: 'b', label: '排队等待' }, { value: 'c', label: '转交他人' }], value: modalPick }), ), ), section( 'sec-stepper', '步骤条', 'stepper:done/current/upcoming 三态随 active 信号流转,clickable 时点已完成步骤(圆点或标题)回跳;某步 status=error 标失败;direction=vertical 走竖排。', stepper({ steps: [{ label: '确认订单', description: '收货信息与商品清单' }, { label: '支付', description: '选择支付方式' }, { label: '完成', description: '等待发货' }], active: stepIdx, clickable: true, onChange: (i) => toast.info(`回到第 ${i + 1} 步`), }), divider({ dashed: true }), stepper({ // 竖向:侧栏 / 移动端场景,连接线落在圆点下方 steps: [{ label: '提交申请', description: '10-20 09:12 已提交' }, { label: '资料初审', description: '10-21 14:03 初审通过' }, { label: '终审', description: '等待风控终审' }], active: signal(2), // 独立信号:演示停在第三步 direction: 'vertical', }), divider({ dashed: true }), stepper({ // 失败态:提交没过的那一步单独标红,既不算已完成也不顶替当前步 steps: [ { label: '填写资料', description: '4 项必填已齐' }, { label: '人脸核身', description: '活体检测未通过,请重试', status: 'error' }, { label: '提交审核', description: '核身通过后自动提交' }, ], active: signal(2), // 当前步仍在第三步:失败步不是「前进到的位置」 }), h( 'div', { class: 'row' }, button({ label: '上一步', variant: 'ghost', disabled: computed(() => stepIdx() === 0), onClick: () => stepIdx(Math.max(0, stepIdx() - 1)) }), button({ label: '下一步', disabled: computed(() => stepIdx() === 2), onClick: () => stepIdx(Math.min(2, stepIdx() + 1)) }), ), ), section( 'sec-rate', '评分', 'rate:点选星标打分,悬停预览移出还原,键盘 →/↑ 加分 ←/↓ 减分,disabled 只读展示,allowClear 再点当前分清零。', h( 'div', { class: 'row' }, rate({ value: score, onChange: (v) => toast.info(`评分 ${v} 星`) }), h('span', { class: 'out' }, () => `当前:${score() || '未评分'}`), ), h('div', { class: 'row' }, rate({ value: score2, allowClear: true }), h('span', { class: 'out' }, () => `可清除(再点当前分归零):${score2() || '已清'}`)), h('div', { class: 'row' }, rate({ value: score, disabled: true })), ), section( 'sec-image', '图片', 'image:缩略图点击 / 回车打开遮罩大图,Esc 或点击遮罩关闭,焦点归还缩略图;加载失败自动落虚线占位(第三张故意给错地址)。', h( 'div', { class: 'row' }, image({ src: IMG_DEMO, alt: '演示图一', width: 180 }), image({ src: IMG_DEMO, alt: '演示图二', width: 120 }), image({ src: './no-such-image.png', alt: '演示图三(地址无效)', width: 120, fallback: '图片走丢了' }), ), ), section( 'sec-empty', '空状态', 'empty:无数据 / 无结果的统一占位,action 放引导按钮,点击按钮切回有数据态可对比。', h('div', { class: 'row' }, h('span', { class: 'out' }, () => (noResult() ? '当前:无结果' : '当前:有数据'))), h( 'div', { class: 'row' }, button({ label: '切换数据态', onClick: () => noResult(!noResult()) }), ), () => (noResult() ? empty({ title: '暂无匹配结果', description: '试试放宽筛选条件,或清空关键词重新搜索。', icon: searchIcon(), action: button({ label: '重置筛选', variant: 'ghost', onClick: () => noResult(false) }) }) : card({ title: '搜索结果' }, h('p', null, '这里展示命中的数据列表。'))), ), section( 'sec-divider', '分隔线', 'divider:水平线可带居中文案,dashed 切虚线;vertical 行内竖线用于工具栏。', divider({ label: '更多信息' }), h('p', null, '分割线上下的内容各自成组。'), divider({ dashed: true }), h( 'div', { class: 'row' }, button({ label: '保存', variant: 'ghost' }), divider({ vertical: true }), button({ label: '另存为', variant: 'ghost' }), divider({ vertical: true, dashed: true }), button({ label: '导出', variant: 'ghost' }), ), ), section( 'sec-space', '间距容器', 'space:用 flex gap 托管一排子元素的间距(不再各自写 margin),size 三档走令牌或直接给像素,direction 切竖排,justify + block 让工具栏两端撑开。', space( { justify: 'between', block: true }, h('span', { class: 'out' }, '左侧标题'), space({ size: 'sm' }, button({ label: '取消', variant: 'ghost' }), button({ label: '保存' })), ), space( { size: 'lg', wrap: true }, badge({ label: '默认' }), badge({ label: '成功', variant: 'success' }), badge({ label: '警告', variant: 'warning' }), tag({ label: '#A1024', variant: 'info' }), ), space({ direction: 'vertical', align: 'start', size: 6 }, h('span', { class: 'out' }, '竖排第一行'), h('span', { class: 'out' }, '竖排第二行(6px 精确间距)')), space( { size: 'sm', split: '·' }, // 分隔符由容器插入,读屏跳过 h('span', { class: 'out' }, '全部订单'), h('span', { class: 'out' }, '待发货'), h('span', { class: 'out' }, '已完成'), ), ), section( 'sec-statistic', '统计数值', 'statistic:大号等宽数字 + 前后缀 + 环比趋势;value 变化时 400ms 缓出滚动(减弱动效偏好下直落终值);传 countdown 目标时间戳即变倒计时,到点自动停表。', h( 'div', { class: 'row' }, statistic({ title: '本月 GMV', value: gmv, prefix: '¥', precision: 2, trend: 'up' }), statistic({ title: '订单数', value: orders, suffix: '单', trend: 'down' }), statistic({ title: '活动倒计时', countdown: Date.now() + 95 * 60 * 1000 }), ), h( 'div', { class: 'row' }, button({ label: '刷新数据', variant: 'ghost', onClick: () => { gmv(gmv() + Math.round(Math.random() * 20000 - 8000)); // 随机涨跌,演示双向滚动 orders(orders() + Math.round(Math.random() * 200 - 80)); } }), ), ), section( 'sec-descriptions', '描述列表', 'descriptions:label : value 网格,column 定列数(窄屏自动单列);bordered 切表格形态,span 让长字段占整行;layout=vertical 把字段名挪到值上方,extra 给标题行加操作区。', descriptions({ title: '订单信息(纯网格 · 3 列)', column: 3, items: [ { label: '订单号', value: 'RB-2048' }, { label: '下单人', value: '王小明' }, { label: '金额', value: '¥1,280.00' }, { label: '状态', value: tag({ label: '待复核', variant: 'warning' }) }, { label: '备注', value: '需要开专用发票,抬头与税号见附件。', span: 2 }, ], }), divider({ dashed: true }), descriptions({ title: '物流信息(表格形态 · 2 列)', column: 2, bordered: true, size: 'sm', items: [ { label: '承运商', value: '顺丰速运' }, { label: '运单号', value: 'SF1234567890' }, { label: '收件地址', value: '浙江省杭州市余杭区文一西路 969 号', span: 2 }, ], }), divider({ dashed: true }), descriptions({ title: '竖排 + 标题操作区(窄栏场景)', column: 2, layout: 'vertical', extra: [h('span', {}, '更新于 15:20'), button({ label: '刷新', variant: 'ghost', onClick: () => toast.info('已刷新') })], items: [ { label: '合同编号', value: 'HT-2026-0913' }, { label: '签署日期', value: '2026-09-13' }, { label: '备注', value: '续签两年,价格按季度结算;此列用 span 占满整行以演示长文本换行。', span: 2 }, ], }), ), section( 'sec-segmented', '分段控制器', 'segmented:胶囊分段 + 滑动滑块,value 信号双向绑定;两个实例共用信号即自动同步,整组禁用走信号。', h( 'div', { class: 'row' }, segmented({ options: [ { value: 'day', label: '日' }, { value: 'week', label: '周' }, { value: 'month', label: '月' }, { value: 'year', label: '年', disabled: true }, ], value: range, }), h('span', { class: 'out' }, () => `当前粒度:${range()}`), ), h( 'div', { class: 'row' }, segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }, { value: 'month', label: '月' }], value: range, size: 'sm' }), select({ options: [{ value: 'bj', label: '北京' }, { value: 'sh', label: '上海' }, { value: 'gz', label: '广州' }, { value: 'sz', label: '深圳' }, { value: 'hz', label: '杭州' }, { value: 'cd', label: '成都' }], value: cityPick, searchable: true, placeholder: '可搜索城市' }), segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }], value: range, size: 'sm', disabled: locked }), h('span', { class: 'out' }, () => (locked() ? '第二个分段已禁用(同上「锁定」开关)' : '第二个分段可用')), ), ), section( 'sec-timeline', '时间线', 'timeline:items 自左向右排事件流,variant 给节点语义色;pending 追加虚线「进行中」节点,点按钮切换。', h( 'div', { class: 'row' }, button({ label: '流程继续/结束', variant: 'ghost', onClick: () => flowDone(!flowDone()) }), ), () => timeline({ items: [ { title: '提交审批', time: '09:12', content: '王小明提交了报销单 RB-2048', variant: 'success' }, { title: '财务复核', time: '10:30', content: '金额与发票一致,待张工确认', variant: 'warning' }, { title: '打款失败', time: '14:05', content: '收款账户信息有误,已退回修改', variant: 'danger' }, ], pending: !flowDone(), }), divider({ dashed: true }), timeline({ // 双侧交替:轴线居中,奇偶左右分列,窄屏退回单列 mode: 'alternate', items: [ { title: '立项评审通过', time: '10-01', content: '范围与里程碑确定', variant: 'success' }, { title: '设计定稿', time: '10-09', content: '视觉与令牌同步交付', variant: 'success' }, { title: '一期上线', time: '10-22', content: '灰度 10% → 全量', variant: 'warning' }, ], }), ), section( 'sec-list', '列表', 'list:header/footer 外壳 + 条目 avatar/title/description/extra;传 onItemClick 后条目可点击并有 hover 反馈,items 为空自动回落空状态。', h( 'div', { class: 'row' }, list({ header: '审批待办(3)', footer: '共 3 条,点击条目查看详情', bordered: true, items: [ { title: '王小明 · 报销单 RB-2048', description: '差旅费 ¥1,280,等待财务复核', avatar: avatar({ name: '王' }), extra: tag({ label: '待审', variant: 'warning' }) }, { title: '李静 · 采购申请 CG-112', description: '显示器 2 台,预算 ¥3,600', avatar: avatar({ name: '李', variant: 'success' }), extra: tag({ label: '已通过', variant: 'success' }) }, { title: '张伟 · 加班调休', description: '10-24 加班 3 小时,申请调休', avatar: avatar({ name: '张', variant: 'danger' }), extra: tag({ label: '已驳回', variant: 'danger' }) }, ], onItemClick: (item) => listPick(item.title), // 点击回传整条数据 }), h( 'div', { class: 'stack' }, h('span', { class: 'out' }, () => (listPick() ? '已选:' + listPick() : '未点击条目')), list({ items: [], bordered: true, emptyText: '暂无待办事项' }), ), ), ), section( 'sec-carousel', '轮播', 'carousel:items 横排位移,指示点 / 箭头 / ←→ 键切换,autoplay 毫秒自动前进,悬停或焦点进入即暂停;index 传信号即可外部受控。', h( 'div', { class: 'row' }, carousel({ height: 150, autoplay: 4000, index: slideIndex, onChange: (i) => slideIndex(i), items: [ h('div', { style: 'flex:1;display:flex;align-items:center;justify-content:center;height:100%;font-size:15px;background:var(--ua-color-surface);color:var(--ua-color-text)' }, '① 令牌驱动的幻灯片 —— 悬停自动暂停'), h('div', { style: 'flex:1;display:flex;align-items:center;justify-content:center;height:100%;font-size:15px;background:color-mix(in srgb, var(--ua-color-accent) 12%, transparent);color:var(--ua-color-text)' }, '② 指示点与箭头均可点击,←→ 也能切'), h('div', { style: 'flex:1;display:flex;align-items:center;justify-content:center;height:100%;font-size:15px;background:color-mix(in srgb, var(--ua-color-success) 14%, transparent);color:var(--ua-color-text)' }, '③ 环形回绕:末张的下一张回到首张'), ], }), h('span', { class: 'out' }, () => `第 ${slideIndex() + 1} / 3 张`), ), ), section( 'sec-tree', '树形', 'tree:递归节点,箭头展开/收起、点标签选中;键盘 Enter 选中、→ 展开、← 收起、↑↓/Home/End 行间移动,整棵树只占一个 Tab 停点(roving tabindex);顶部搜索按标签过滤并保留命中父链。', h( 'div', { class: 'row' }, tree({ searchable: true, nodes: [ { key: 'src', label: 'src', children: [ { key: 'core', label: 'core', children: [{ key: 'frame', label: 'frame' }, { key: 'table', label: 'table-core' }] }, { key: 'skin', label: 'skin', children: [{ key: 'ui', label: 'ui' }, { key: 'css', label: 'ui.css' }] }, ] }, { key: 'apps', label: 'apps', children: [{ key: 'demo', label: 'demo' }, { key: 'docs', label: 'docs' }] }, { key: 'lock', label: 'package-lock.json(禁用示例)', disabled: true }, ], selected: treePick, onSelect: (n) => treePick(n.key), }), h('span', { class: 'out' }, () => (treePick() ? `已选节点:${treePick()}` : '未选中节点')), h( 'div', { class: 'col' }, tree({ multi: true, checked: treeChecked, defaultExpandAll: true, nodes: [ { key: 'perm', label: '权限包', children: [ { key: 'read', label: '读取' }, { key: 'write', label: '写入' }, { key: 'admin', label: '管理(不可授予)', disabled: true }, ] }, { key: 'bill', label: '计费模块', children: [{ key: 'bill-view', label: '查看账单' }] }, ], onCheck: (keys) => toast.info(`已勾选 ${keys.length} 项`), }), h('span', { class: 'out' }, () => `已勾选:${treeChecked().join(' / ') || '无'}`), ), ), ), section( 'sec-upload', '文件选择', 'upload:点击或拖拽入列,清单可逐个移除;只负责选文件并回传 onChange,上传请求交给调用方。', h( 'div', { class: 'row' }, upload({ multiple: true, maxCount: 3, maxSize: 2 * 1024 * 1024, hint: '多选(上限 3 个 / 单个 2 MB)', onChange: (fs2) => pickedFiles(fs2.map((f) => f.name).join('、')) }), h('span', { class: 'out' }, () => (pickedFiles() ? `已选:${pickedFiles()}` : '未选择文件')), ), h('div', { class: 'row' }, upload({ accept: 'image/*', hint: '仅图片(accept: image/*,拖拽同样拦)' }), upload({ disabled: true, hint: '禁用态' })), ), section( 'sec-notification', '通知卡片', 'notification:右下角堆叠(与右上角 toast 分区),标题 + 描述 + 操作区,缺省常驻需手动关闭;duration 传毫秒可自动消失,悬停或聚焦会暂停倒计时、离开按剩余时间续计;带 key 的第二次调用原地改同一张卡(进度刷新不再堆条)。', h( 'div', { class: 'row' }, button({ label: '常驻通知', variant: 'primary', onClick: () => notification({ title: '有 3 条待审报销', description: '王小明 / 李静 / 张伟提交的报销单等待复核', action: button({ label: '去处理', onClick: () => toast.success('已跳转待办') }) }) }), button({ label: '成功(4s 自动关)', onClick: () => notification({ title: '导出完成', description: 'report-2026-10.csv 已开始下载', variant: 'success', duration: 4000 }) }), button({ label: '警告(常驻)', variant: 'ghost', onClick: () => notification({ title: '连接不稳定', description: '正在重试第 2 次,请检查网络', variant: 'warning' }) }), button({ label: '错误(可关)', variant: 'danger', onClick: () => notification({ title: '保存失败', description: '服务端返回 500,请稍后重试', variant: 'danger' }) }), button({ label: '左上角通知', variant: 'ghost', onClick: () => notification({ title: '后台任务已排队', description: '四个角各自独立堆叠,这条去左上角', placement: 'top-left' }) }), button({ label: '同 key 原地更新', variant: 'ghost', onClick: () => { // 三拍演示:一条卡原地改三次,不会堆成三条 const job = notification({ key: 'sync-job', title: '同步中 0%', description: '正在上传附件' }); window.setTimeout(() => job.update({ title: '同步中 60%', description: '已上传 3 / 5 个文件' }), 900); window.setTimeout(() => job.update({ title: '同步完成', description: '5 个文件已全部就绪', variant: 'success' }), 1800); window.setTimeout(() => job.close(), 4000); } }), ), ), section( 'sec-multiselect', '多选下拉', 'multiSelect:标签式回显 + 可搜索弹层,点选不收起弹层(连续多选),空输入退格删末个标签,max 到限后未选项禁用,maxTagCount 折叠溢出标签,clearable 一键清空;selectAll 在弹层顶部给「全选(已选/可选)」,半选显 –,只作用于当前过滤结果。', h( 'div', { class: 'row' }, multiSelect({ value: tagPick, max: 3, clearable: true, placeholder: '选择技术栈(最多 3 项)', onChange: (v) => toast.info(`已选 ${v.length} 项`), options: [ { value: 'ts', label: 'TypeScript' }, { value: 'dom', label: '原生 DOM' }, { value: 'css', label: 'CSS 令牌' }, { value: 'test', label: '测试' }, { value: 'wasm', label: 'WASM(暂未支持)', disabled: true }, ], }), h('span', { class: 'out' }, () => `已选:${tagPick().join(' / ') || '无'}`), multiSelect({ value: tagFold, maxTagCount: 2, selectAll: true, // 全选行:先搜索再全选即「按条件批量选」 placeholder: '标签折叠演示', options: [ { value: 'ts', label: 'TypeScript' }, { value: 'dom', label: '原生 DOM' }, { value: 'css', label: 'CSS 令牌' }, { value: 'test', label: '测试' }, ], }), h('span', { class: 'out' }, () => `折叠后仅露出 2 枚标签,其余折成 +N(共 ${tagFold().length} 项)`), ), ), section( 'sec-checkgroup', '多选组', 'checkboxGroup:数组信号双向绑定,min/max 用「禁用边界项」代替报错,checkAll 带半选,只统计未禁用项。', h( 'div', { class: 'col' }, checkboxGroup({ value: permPick, min: 1, max: 3, checkAll: true, onChange: (v) => toast.info(`已选 ${v.length} 项`), options: [ { value: 'read', label: '读取' }, { value: 'write', label: '写入' }, { value: 'delete', label: '删除' }, { value: 'share', label: '分享' }, { value: 'audit', label: '审计日志(不可授予)', disabled: true }, ], }), checkboxGroup({ direction: 'vertical', options: [{ value: 'a', label: '纵向 A' }, { value: 'b', label: '纵向 B' }] }), h('span', { class: 'out' }, () => `已选:${permPick().join(' / ') || '无'}(下限 1、上限 3)`), ), ), section( 'sec-watermark', '水印', 'watermark:canvas 生成平铺单元,颜色从 --ua-* 令牌解析成实色,覆盖层 pointer-events:none 不挡交互。', h( 'div', { class: 'row' }, watermark( { text: 'UI-ARCH · 内部资料\n仅限内网查看', gap: 160, fontSize: 13 }, card( { title: '受保护内容' }, h('p', { class: 'desc' }, '水印覆盖在内容之上、交互穿透到下方,适合内部系统防截图外传。颜色缺省取 --ua-color-text-muted,暗色主题下自动变浅。'), h('div', { class: 'row' }, button({ label: '可点击(验证穿透)', variant: 'primary', onClick: () => toast.success('水印层没有挡住点击') })), ), ), ), ), section( 'sec-tour', '引导漫游', 'tour({ steps }) 返回句柄:start() 从第一步播放,聚光框按目标选择器定位、卡片自动选上下方位,Esc / × 跳过走 onClose。', h( 'div', { class: 'row' }, button({ label: '开始引导(3 步)', variant: 'primary', onClick: () => demoTour.start() }), h('span', { class: 'desc' }, '三步分别指向本段、分栏段与返回顶部段,滚动后位置会实时重算。'), ), ), section( 'sec-colorpicker', '颜色选择', 'colorPicker:预设色板 + 原生取色器 + 手输 CSS 颜色串;缺省色板直接取语义令牌,暗色主题下自动跟着变。', h( 'div', { class: 'row' }, colorPicker({ value: colorVal, onChange: (c) => toast.success(`已选 ${c}`) }), colorPicker({ placeholder: '只给两个预设', presets: ['var(--ua-color-success)', 'var(--ua-color-danger)'] }), h('span', { class: 'out' }, () => `当前颜色:${colorVal()}`), h('span', { class: 'ua-colorpicker-swatch', style: () => `background:${colorVal()}` }), ), ), section( 'sec-timepicker', '时间选择', 'timePicker:时 / 分两列弹层,value 双向绑定 HH:mm;minuteStep 控制分的粒度,选完分自动收起并归还焦点。', h( 'div', { class: 'row' }, timePicker({ value: atTime, minuteStep: 15, clearable: true, onChange: (t) => toast.success(t ? `定在 ${t}` : '已清空时间') }), timePicker({ placeholder: '小号', size: 'sm' }), timePicker({ placeholder: '禁用态', disabled: true }), h('span', { class: 'out' }, () => `当前时间:${atTime() || '未选'}`), ), ), section( 'sec-pininput', '验证码输入', 'pinInput:N 格单字符数字输入,输入自动进格、退格回删、←/→ 移格、粘贴自动分发;mask 密文显示,disabled 禁用。', h( 'div', { class: 'row' }, pinInput({ value: pinCode, onChange: (v) => { if (v.length === 6) toast.success(`验证码 ${v} 已就绪`); } }), h('span', { class: 'out' }, () => `当前:${pinCode() || '未输入'}(试试直接粘贴整段)`), ), h( 'div', { class: 'row' }, pinInput({ value: pinMask, length: 4, mask: true }), h('span', { class: 'out' }, () => `密文 4 格:${pinMask().length}/4`), ), h('div', { class: 'row' }, pinInput({ value: signal(''), length: 6, disabled: true })), ), section( 'sec-copybutton', '复制按钮', 'copyButton:点击写入剪贴板,成功后短暂换成「已复制」再还原;text 可传函数取实时值,剪贴板 API 不可用时自动降级 execCommand。', h( 'div', { class: 'row' }, copyButton({ text: 'npm install @ui-arch/ui', onCopied: (t) => toast.success(`已复制:${t}`) }), copyButton({ text: () => pinCode() || '尚未输入验证码', label: '复制上方验证码' }), copyButton({ text: '禁用态示例', disabled: true }), ), ), section( 'sec-countdown', '倒计时', 'countdown:秒级递减到 0 自动停表并触发 onFinish,缺省 mm:ss 展示;句柄 start / stop / reset,验证码重发场景与 pinInput 搭配。', h( 'div', { class: 'row' }, countdown({ seconds: 10, onFinish: () => toast.info('倒计时结束') }).el, resendCd.el, button({ label: '重置 90s', variant: 'ghost', onClick: () => { resendCd.reset(); resendCd.start(); } }), h('span', { class: 'out' }, '第二个示例初始暂停,点按钮重开'), ), ), section( 'sec-kbd', '键盘按键', 'kbd:键帽样式展示按键提示,单键直传字符串,组合键传数组以 + 连接(+ 对读屏隐藏)。', h( 'div', { class: 'row' }, kbd({ keys: 'Esc' }), kbd({ keys: ['Ctrl', 'K'] }), kbd({ keys: ['Shift', 'Enter'] }), h('span', { class: 'out' }, '按 ', kbd({ keys: ['Ctrl', 'K'] }), ' 唤起演示页命令面板'), ), ), section( 'sec-highlight', '关键词高亮', 'highlight:命中片段包 mark(大小写不敏感全部命中),text / keyword 接受信号实时联动,空关键词原样展示。', h( 'div', { class: 'row' }, highlight({ text: highlightText, keyword: highlightKw }), h('span', { class: 'out' }, () => `关键词:${highlightKw() || '(空)'}`), ), h( 'div', { class: 'row' }, textField({ placeholder: '输入关键词试试实时联动', value: highlightKw, size: 'sm' }), ), ), section( 'sec-inputtag', '标签输入', 'inputTag:回车 / 逗号成标签,chip 可单个删除,空框退格删末个,失焦自动入列;max 满额自动禁输。', h( 'div', { class: 'row' }, inputTag({ value: demoTags, max: 5, onChange: (list) => toast.info(`当前 ${list.length} 个标签`) }), h('span', { class: 'out' }, () => `已选:${demoTags().join(' / ') || '(空)'}`), ), h( 'div', { class: 'row' }, dataTable({ data: tagMatched, // 信号数据源:删标签后表格自动换源,排序 / 每页行数不被重置 columns: [ { key: 'name', header: '能力' }, { key: 'tags', header: '标签', format: (value) => (value as string[]).join(' / ') }, ], pageSize: 3, }), ), ), section( 'sec-affix', '固钉', 'affix:内容滚过阈值后转 fixed 贴顶,外层保留占位高度不塌陷;固定后宽度随容器自适应。', h( 'div', { class: 'row' }, h('div', { style: 'flex:1;min-height:320px;display:flex;gap:16px' }, affix({ offset: 8 }, card({ title: '滚动时我会贴顶' }, h('p', { class: 'desc' }, '向下滚动本页,这块卡片会固定在视口顶部(距顶 8px),左侧占位不会塌。'))), h('div', { class: 'desc', style: 'flex:1' }, '右侧是普通内容,用来撑高这一行,方便观察固钉效果。'.repeat(6)), ), ), ), section( 'sec-backtop', '返回顶部', 'backTop:滚动越过阈值浮现(右下角悬浮),外环弧长即已滚比例,点击平滑回顶并尊重减少动效偏好。', h( 'div', { class: 'row' }, h('span', { class: 'desc' }, '本页已挂载一个实例:向下滚动超过 300px 后右下角出现,外环显示滚动进度。'), backTop({ threshold: 300, onClick: () => toast.info('已回到顶部') }), ), ), section( 'sec-datepicker', '日期选择', 'datePicker:单月网格,翻月 / 今天 / ←→↑↓ 逐日逐周 / PageUp↓ 逐月,Esc 收起;min/max 闭区间置灰越界日;区间模式可配 presets 快捷按钮。', h( 'div', { class: 'row' }, datePicker({ value: dateVal, onChange: (d) => toast.success(`已选 ${d}`) }), datePicker({ placeholder: '只能选 10 月下旬', min: '2026-10-20', max: '2026-10-31' }), datePicker({ range: true, value: rangeFrom, end: rangeTo, presets: [{ label: '最近 7 天', days: 7 }, { label: '最近 30 天', days: 30 }, { label: '10 月上旬', from: '2026-10-01', to: '2026-10-15' }], onRangeChange: (a, b) => toast.success(`区间 ${a} ~ ${b}`) }), h('span', { class: 'out' }, () => (dateVal() ? `当前日期:${dateVal()}` : '未选择')), h('span', { class: 'out' }, () => (rangeFrom() ? `区间:${rangeFrom()} ~ ${rangeTo() || '待定'}` : '区间未选择')), ), ), section( 'sec-autocomplete', '自动补全', 'autoComplete:输入即过滤候选,↑↓ 环形高亮 / Enter 选中 / Esc 收起;options 传函数可做远程联想,空结果给提示行。', h( 'div', { class: 'row' }, autoComplete({ value: acValue, placeholder: '输入组件名,如 table', emptyText: '没有匹配的组件', onSelect: (o) => toast.success(`已选 ${o.value}`), options: [ { value: 'dataTable', hint: '表格' }, { value: 'transfer', hint: '穿梭框' }, { value: 'cascader', hint: '级联' }, { value: 'splitter', hint: '分栏' }, { value: 'autoComplete', hint: '联想' }, { value: 'pagination', hint: '分页' }, ], }), autoComplete({ placeholder: '按关键字动态算候选', options: (q) => [{ value: `${q}-alpha` }, { value: `${q}-beta` }] }), h('span', { class: 'out' }, () => (acValue() ? `输入框当前值:${acValue()}` : '未输入')), ), ), section( 'sec-splitter', '可拖拽分栏', 'splitter:分隔条可拖拽、可聚焦用方向键微调(Shift 加速)、双击复位均分;sizes 传信号即受控,每栏不低于 min。', h( 'div', { class: 'col' }, splitter({ sizes: splitSizes, onChange: (p) => toast.info(`比例:${p.map((n) => n + '%').join(' / ')}`), panes: [ h('div', {}, h('div', { class: 'desc' }, '左侧栏'), list({ items: [{ title: '待办 A' }, { title: '待办 B' }], bordered: true })), h('div', {}, h('div', { class: 'desc' }, '右侧栏:拖动中间分隔条试试'), h('p', { class: 'desc' }, '分隔条支持键盘:Tab 聚焦后 ← → 每次 2%,Shift + ← → 每次 10%,双击复位。')), ], }), h('span', { class: 'out' }, () => `当前比例:${splitSizes().map((n) => n + '%').join(' / ')}`), splitter({ direction: 'vertical', min: 20, panes: [h('div', { class: 'desc' }, '上栏'), h('div', { class: 'desc' }, '下栏(纵向拖拽)')] }), ), ), section( 'sec-cascader', '级联选择', 'cascader:多列联动,悬停即下钻、点叶子提交;changeOnSelect 可停在中间层,Esc 收起,↑↓/→/←/Enter 全键盘可用。', h( 'div', { class: 'row' }, cascader({ value: cascaderPath, placeholder: '请选择所属区域', changeOnSelect: true, onChange: (p) => toast.success(`已选:${p.join(' / ')}`), options: [ { value: 'east', label: '华东', children: [ { value: 'sh', label: '上海', children: [{ value: 'pd', label: '浦东新区' }, { value: 'xh', label: '徐汇区' }] }, { value: 'hz', label: '杭州', children: [{ value: 'xh-hz', label: '西湖区' }, { value: 'yh', label: '余杭区' }] }, ] }, { value: 'south', label: '华南', children: [ { value: 'gz', label: '广州', children: [{ value: 'th', label: '天河区' }] }, { value: 'sz', label: '深圳', children: [{ value: 'nsh', label: '南山区' }] }, ] }, { value: 'overseas', label: '海外(暂不开放)', disabled: true }, ], }), cascader({ separator: ' · ', placeholder: '必须选到末级', options: [ { value: 'a', label: '研发中心', children: [{ value: 'a1', label: '前端组' }, { value: 'a2', label: '后端组' }] }, { value: 'b', label: '设计中心', children: [{ value: 'b1', label: '交互组' }, { value: 'b2', label: '视觉组' }] }, ] }), h('span', { class: 'out' }, () => (cascaderPath().length ? `当前路径:${cascaderPath().join(' / ')}` : '未选择')), ), ), section( 'sec-transfer', '穿梭框', 'transfer:左右两栏由同一份 dataSource 按归属派生,勾选后点箭头整体搬运;两栏各自搜索、全选支持半选,禁用条目既不参与全选也不可搬运。', h( 'div', { class: 'col' }, transfer({ targetKeys: transferKeys, titles: ['待选能力', '已启用能力'], height: 180, onChange: (keys) => toast.info(`已启用 ${keys.length} 项`), dataSource: [ { key: 'a11y', label: '无障碍语义(aria)' }, { key: 'theme', label: '暗色主题令牌' }, { key: 'motion', label: '微交互动效' }, { key: 'virtual', label: '虚拟滚动' }, { key: 'ssr', label: 'SSR 水合(暂不可用)', disabled: true }, { key: 'i18n', label: '多语言文案' }, ], }), h('span', { class: 'out' }, () => `已启用 ${transferKeys().length} 项:${transferKeys().join(' / ') || '无'}`), ), ), section( 'sec-result', '结果页', 'result:终态页(成功 / 警告 / 失败 / 中性),状态符号 + 标题 + 说明 + 操作区,适合提交完成、权限不足等场景。', h( 'div', { class: 'row' }, result({ status: 'success', title: '提交成功', description: '报销单 RB-2049 已进入财务复核,预计 2 个工作日内出结果。', extra: h('div', { class: 'row' }, button({ label: '查看单据', variant: 'primary', onClick: () => toast.success('打开单据') }), button({ label: '继续提交', variant: 'ghost', onClick: () => toast.info('已新建草稿') })) }), result({ status: 'danger', title: '校验未通过', description: '发票金额与报销金额不一致,请核对后重新提交。' }), ), ), section( 'sec-pagination', '分页', 'pagination:受控页码信号,>7 页自动省略号折叠;jumpable 跳页输入、pageSizes 切每页条数(切换后回第 1 页)。', h( 'div', { class: 'row' }, pagination({ page: pagerPage, pageSize: 10, total: 137, jumpable: true, pageSizes: [10, 20, 50], onChange: (p) => pagerPage(p) }), h('span', { class: 'out' }, () => `第 ${pagerPage()} 页`), ), h( 'div', { class: 'row' }, pagination({ page: simplePage, pageSize: 2, total: 9, simple: true }), // 极简:只留前后翻页与 3 / 5 h('span', { class: 'out' }, '极简模式:工具栏窄位只留箭头与进度'), pagination({ page: simplePage, pageSize: 20, total: 9, hideOnSinglePage: true }), // 单页:整块不渲染 h('span', { class: 'out' }, '单页隐藏:上面那块 9 条 / 每页 20 已自动消失'), ), (() => { // 总量走信号:点「筛到 25 条」后页码从 14 自动钳到 3,父级 state 同步收到回调 const liveTotal = signal(137); const livePage = signal(14); return h( 'div', { class: 'row' }, pagination({ page: livePage, pageSize: 10, total: liveTotal, onChange: (p) => livePage(p) }), button({ label: '总量 137 条', variant: 'ghost', onClick: () => liveTotal(137) }), button({ label: '筛到 25 条', variant: 'ghost', onClick: () => liveTotal(25) }), h('span', { class: 'out' }, () => `第 ${livePage()} 页 / 共 ${Math.max(1, Math.ceil(liveTotal() / 10))} 页`), ); })(), ), section( 'sec-menu', '下拉菜单', 'menu 触发器 + 自绘弹层:条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部或 Esc 收起;视口下方空间不足时弹层自动翻到上方,展开后键入字母可快跳菜单项。', h( 'div', { class: 'row' }, menu({ label: '文件操作', items: [ { value: 'rename', label: '重命名', shortcut: 'F2' }, { value: 'copy', label: '创建副本', shortcut: '⌘D' }, { value: 'archive', label: '归档', disabled: true }, { divider: true, value: '', label: '' }, { value: 'delete', label: '删除', danger: true }, ], onSelect: (v) => menuPick(v), }), menu({ label: '右对齐', align: 'end', variant: 'primary', items: [ { value: 'profile', label: '个人资料' }, { value: 'settings', label: '偏好设置' }, ], onSelect: (v) => menuPick(v), }), h('span', { class: 'out' }, () => (menuPick() ? `已选:${menuPick()}` : '未选择')), ), ), section( 'sec-popover', '气泡卡片', 'popover:可交互弹层(tooltip 只展示,popover 里能点),点外部 / Esc 收起;视口下方空间不足时弹层自动翻到上方。', h( 'div', { class: 'row' }, popover( { title: '筛选条件', content: h( 'div', null, h('div', { class: 'row' }, checkbox({ label: '只看我的', checked: agreed })), h('div', { class: 'row' }, button({ label: '应用', onClick: () => toast.success('已应用筛选') })), ), }, button({ label: '打开气泡卡片' }), ), popover({ placement: 'top', trigger: 'hover', content: '悬停出场,移出收起(可放链接)' }, badge({ label: 'hover 触发' })), ), ), section( 'sec-breadcrumb', '面包屑', 'breadcrumb:nav + ol 语义结构,末项自动标为当前页,无 href 的条目走 onNavigate;条目超过 maxItems 时中段折叠成「…」,点开后完整展开。', breadcrumb({ items: [{ label: '首页', href: '#sec-counter' }, { label: '组件库' }, { label: '导航类' }, { label: '面包屑' }], separator: '›', onNavigate: (item) => toast.info(`跳转到 ${item.label}`), }), breadcrumb({ items: [ { label: '首页', href: '#sec-counter' }, { label: '控制台' }, { label: '订单管理' }, { label: '华东区' }, { label: '2026 Q4' }, { label: '待发货' }, { label: '订单 #1024' }, ], separator: '›', maxItems: 4, onNavigate: (item) => toast.info(`跳转到 ${item.label}`), }), ), section( 'sec-accordion', '折叠面板', 'accordion:标题按钮 + grid 行高过渡,默认手风琴单开,multiple 可多开,支持禁用面板。', accordion({ multiple: true, items: [ { value: 'a', title: '账户设置', content: '修改昵称、头像与绑定邮箱。' }, { value: 'b', title: '通知偏好', content: alert({ message: '通知偏好:每日摘要已开启', variant: 'info' }) }, { value: 'c', title: '危险操作', content: '删除账号后数据不可恢复。' }, { value: 'd', title: '企业版专属(禁用)', content: '升级后可配置 SSO。', disabled: true }, ], }), ), section( 'sec-table', '数据表格', '粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序(按住 Shift 点第二个表头即多列叠加,标记带优先级 ↑1 / ↓2)、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选(0 行时按「有没有筛选词」分别提示「暂无数据」或「没有符合…的行」并给一键清除)、底部分页;工具栏「列」按钮勾选即可隐藏/恢复列(至少留一列,隐藏列不占表头也不进导出)、「导出 CSV」导的是筛选 + 排序后的全量行(不含勾选,与当前页无关)。', card({ title: '用户表' }, dataTable({ data: users, selectable: true, exportable: true, columnToggle: true, loading: tableLoading, columns: [ { key: 'name', header: '姓名', sortable: true, width: 160 }, { key: 'age', header: '年龄', sortable: true, align: 'right', width: 100, format: (v) => `${v} 岁` }, { key: 'city', header: '城市', align: 'center', width: 'flex', render: (row) => tag({ label: row.city, variant: row.city === '上海' ? 'info' : 'default' }) }, // 自定义单元格:本列画标签而非纯文本,格内点击不触发整行回调 ], pageSize: 5, selected: picked, renderDetail: (row) => h('div', null, `${row.name} · ${row.age} 岁 · ${row.city}:展开行放表里塞不下的长内容(备注、标签、二级操作),点行首的 ▸ 开合,状态按 rowKey 记,筛选与翻页都不丢。`), // 详情行:多在表里加一列展开钮 onRowClick: (row) => toast.info(`打开 ${row.name} 的详情`), // 整行可点:Tab 进入 + Enter 等价 }), h( 'div', { class: 'row' }, switchButton({ label: '加载态', checked: tableLoading }), badge({ label: '受控选中', variant: 'default' }), h('span', { class: 'out' }, () => `已选 ${picked().length} 行`), button({ label: '清空选择', variant: 'ghost', onClick: () => picked([]) }), )), card( { title: '独立分页器联动(pageSize 3,共用页码信号)' }, dataTable({ data: users, columns: [ { key: 'name', header: '姓名' }, { key: 'city', header: '城市' }, ], pageSize: 3, page: tablePage, }), pagination({ page: tablePage, pageSize: 3, total: users.length, jumpable: true, onChange: (p) => tablePage(p) }), ), ), section( 'sec-card', '卡片容器', 'card 组件:标题行(可带右侧操作区)+ 任意子内容 + 可选底部操作区;bordered / hoverable 控制外观,样式全部来自设计令牌。', card({ title: '令牌驱动' }, h('p', { class: 'desc' }, '这里没有硬编码颜色 —— 全部消费 --ua-* CSS 变量,暗色模式自动切换。')), card( { title: '带操作区的卡片', extra: h('span', { class: 'desc' }, '共 3 项'), hoverable: true, footer: h('div', { class: 'row' }, button({ label: '取消', variant: 'ghost', onClick: () => toast.info('已取消') }), button({ label: '保存', onClick: () => toast.success('已保存') })), }, h('p', { class: 'desc' }, '标题行右侧放操作或统计,底部 footer 放主次按钮;hoverable 时整卡悬停浮起。'), ), ), section( 'sec-anchor', '锚点导航', '竖排区块链接:滚动时按目标位置高亮当前区块并同步 aria-current,点击按 offset 偏移量平滑定位、hash 用 replaceState 回写(不堆历史);禁用项留在列表里不响应,目标缺失的项在判定时自动跳过。', card( { title: '跳到本页区块' }, anchor({ items: [ { href: '#sec-anchor', title: '锚点导航(本节)' }, { href: '#sec-table', title: '数据表格' }, { href: '#sec-card', title: '卡片容器' }, { href: '#sec-missing-zone', title: '不存在的区块(自动跳过)' }, { href: '#sec-affix', title: '吸顶容器(禁用示例)', disabled: true }, ], offset: 12, onChange: (href) => toast.info(`已跳到 ${href}`), }), ), ), section( 'sec-popconfirm', '二次确认', 'popconfirm 把确认放在触发点旁边而不是弹窗里:确定先收起再回调(回调里改数据不会看到弹层还挂着),取消 / 点外部 / Esc 一律走 onCancel;展开时焦点落在「取消」上,误按回车不会删东西。', popconfirm({ title: '删除这条记录?', description: '删除后不可恢复,关联的审批记录会一并清空。', variant: 'danger', confirmText: '删除', cancelText: '再想想', onConfirm: () => toast.success('已删除(弹层先收起再回调)'), onCancel: () => toast.info('已取消'), }, button({ label: '删除记录', variant: 'danger' })), popconfirm({ title: '发布到生产?', description: '发布后 1 分钟内全量生效。', confirmText: '发布', onConfirm: () => toast.success('已发布') }, button({ label: '发布', variant: 'ghost' })), popconfirm({ // 异步确认:Promise 期间按钮转「处理中…」并禁用,成功才收起 title: '归档这批任务?', description: '调用后端需要约 1 秒,期间按钮保持 loading,失败则留在气泡里可重试。', confirmText: '归档', onConfirm: async () => { await new Promise((r) => setTimeout(r, 1000)); toast.success('已归档'); }, }, button({ label: '归档(异步)', variant: 'ghost' })), ), section( 'sec-calendar', '月历', 'calendar 把整月网格内嵌进页面(排期 / 值班 / 日程):每日可带标记文案,标记同时写进 aria-label 让读屏也听得到;←→ 逐日、↑↓ 逐周、Enter 选中,跨月自动翻页并交接焦点。', calendar({ value: calDay, marker: (date) => calendarMarks[date] ?? null, onSelect: (date) => toast.info(`选中 ${date}`), }), h('span', { class: 'out' }, () => `当前选中:${calDay() || '无'}`), ), section( 'sec-typography', '排版文本', 'typography 一个入口覆盖标题 / 正文 / 行内三种形态:variant 决定语义标签(标题出 h1-h5,读屏可按层级跳读),装饰位可叠加,长描述用 ellipsis 截断并自动挂 title 看全文,copyable 给订单号这类要复制的字段。', card({ title: '标题层级' }, typography({ variant: 'title', level: 3, content: '订单详情' }), typography({ variant: 'title', level: 5, content: '第三节:配送与售后' }), typography({ variant: 'paragraph', type: 'secondary', content: '标题按 level 出 h1-h5,字号由样式表按标签给,这里没写任何内联字号。' }), ), card({ title: '行内装饰' }, typography({ variant: 'paragraph', content: h('span', null, '本周新增 ', typography({ content: '2', strong: true, type: 'success' }), ' 个组件,门禁命令 ', typography({ content: 'npm run check', code: true }), ',搜索命中 ', typography({ content: '排版', mark: true }), ',已作废 ', typography({ content: '旧接口', delete: true }), '。'), }), ), card({ title: '截断与复制' }, typography({ variant: 'paragraph', ellipsis: 2, content: '这一段故意写得很长用来演示多行截断:组件把行数写进 CSS 变量交给 -webkit-line-clamp,超出的部分显示省略号,同时把全文挂到 title 上,鼠标停留即可读完,调用方不需要自己算宽度或加「展开」按钮。', }), space({ size: 'sm' }, typography({ content: 'RB-2048-0917-3355', code: true, copyable: true }), typography({ content: '点右侧复制可写进剪贴板', type: 'secondary' }), ), ), ), section( 'sec-grid', '栅格容器', 'grid 铺页面骨架:columns 给固定列数,min 走 auto-fit 让列数随可用宽度自然增减(比手写断点稳),列间距统一走 gutter 不产生双倍 margin,窄屏一律降为单列。', grid({ columns: 3, gutter: 'md' }, card({ title: '实例数' }, statistic({ value: signal(128), suffix: '台' })), card({ title: '错误率' }, statistic({ value: signal(0.42), suffix: '%', precision: 2 })), card({ title: '平均延迟' }, statistic({ value: signal(38), suffix: 'ms' })), ), h('div', { class: 'out' }, '↓ min: 220px 的 auto-fit:拉窄窗口可见列数自动减少'), grid({ min: '220px', align: 'start' }, ...['ap-east-1', 'ap-south-1', 'eu-west-1', 'us-east-1'].map((zone) => card({ title: zone }, typography({ content: '健康检查通过', type: 'success', variant: 'paragraph' }))), ), ), section( 'sec-mentions', '@提及输入', 'mentions 在 textarea 里键入 @ 就弹候选:↑↓ 环形移动、Enter/Tab 选中、Esc 只收这一层弹层;选中后按光标位置替换 @片段并写回 value 信号。输入拼音或工号也能命中(keywords 只参与过滤不显示)。', grid({ min: '320px' }, card({ title: '评论框' }, mentions({ items: teamMembers, value: comment, rows: 3, placeholder: '输入 @ 叫人来评审……', onSelect: (item) => toast.info(`已提及 ${item.label}`), }), ), card({ title: '当前正文' }, h('div', { class: 'out' }, () => comment() || '(还没写)'), typography({ variant: 'paragraph', type: 'secondary', content: '值由 value 信号驱动,外部改信号输入框同步;这里就是同一个信号。' }), ), ), ), section( 'sec-treeselect', '树形选择', 'treeSelect = 触发器 + 弹层里嵌的 tree:搜索、多选、父子级联全复用 tree,Esc 与其他弹层共享同一个栈(只关最上层),点外部自动收起;单选选中即收起,多选显示已选数量。', grid({ min: '280px' }, card({ title: '单选(可搜索)' }, treeSelect({ nodes: orgTree, value: dept, searchable: true, placeholder: '选择归属部门', onSelect: (n) => toast.info(`已选 ${n.label}`) }), ), card({ title: '多选(保持展开)' }, treeSelect({ nodes: orgTree, multi: true, checked: deptChecked, onChange: (keys) => toast.info(`已勾选 ${keys.length} 项`) }), typography({ variant: 'paragraph', type: 'secondary', content: '勾选父节点会级联到子节点,「回归测试」是禁用叶子,不可选。' }), ), ), ), section( 'sec-link', '链接', 'link 统一站内 / 站外跳转:external 自动补 ↗、target=_blank 与 rel=noopener noreferrer,并给读屏播报「在新窗口打开」;muted 用于页脚次要链接,disabled 去掉 href 但保留可聚焦与 aria-disabled。', space({ wrap: true, size: 'md' }, link({ href: '#sec-link', label: '站内锚点' }), link({ href: 'https://gitea.yinyuanjingling.icu/temp/UI', label: '仓库地址', external: true }), link({ href: '#sec-link', label: '次要说明', muted: true }), link({ href: '#sec-link', label: '已停用', disabled: true }), link({ href: '#sec-link', label: '带埋点的链接', onClick: () => toast.info('点击已上报') }), ), ), section( 'sec-floatbutton', '悬浮操作按钮', 'floatButton 停靠左下 / 右下角:只给 icon 是圆形,配 label 变胶囊,badge 走角标,tooltip 兼作 aria-label。它返回的是 fixed 包裹层,挂到 body 才不会被容器 overflow 裁掉。', grid({ min: '240px' }, card({ title: '本页已挂两个' }, typography({ variant: 'paragraph', type: 'secondary', content: '左下角「+ 新建工单」是图标 + 文字胶囊,点击弹提示;右下角「?」是纯图标圆形并带角标 3,悬停看 title。' }), typography({ variant: 'paragraph', content: h('span', null, '右下角同时有 backTop,所以这里改用 ', typography({ content: "position: 'left-bottom'", code: true }), ' 错开。') }), ), ), ), section( 'sec-vlist', '虚拟列表', 'virtualList 固定行高虚拟化:DOM 里只保留可视窗口 + 上下缓冲条(本例 10,000 行常驻约 20 行),滚动 rAF 合帧整窗重建;句柄 scrollTo 按全量下标回跳,越界自动钳制。', grid({ min: '320px' }, card({ title: '10,000 行订单流水' }, (() => { const rows = Array.from({ length: 10000 }, (_, i) => ({ id: `SO-${10000 + i}`, amount: 88 + (i % 97) * 13 })); // 金额按下标推导,回看稳定 return virtualList({ items: rows, itemHeight: 44, height: 320, renderItem: (raw, i) => `#${i + 1} · ${(raw as { id: string }).id} · ¥${(raw as { amount: number }).amount}` }).el; })(), typography({ variant: 'paragraph', type: 'secondary', content: '滚动条是真实全长,但 DOM 只渲染可视窗口——开 DevTools 数一数 .ua-vlist-item 就知道。' }), ), card({ title: '滚回第 500 条' }, (() => { const rows = Array.from({ length: 10000 }, (_, i) => ({ id: `SO-${10000 + i}`, amount: 88 + (i % 97) * 13 })); const vlist = virtualList({ items: rows, itemHeight: 44, height: 320, renderItem: (raw, i) => `#${i + 1} · ${(raw as { id: string }).id} · ¥${(raw as { amount: number }).amount}` }); return h('div', null, vlist.el, space({ size: 'sm' }, button({ label: '跳到 #500', onClick: () => vlist.scrollTo(499) }))); })(), typography({ variant: 'paragraph', type: 'secondary', content: 'scrollTo(index) 接全量下标(从 0 计),组件同步重渲,不等下一次滚动事件。' }), ), ), ), section( 'sec-cmdk', '命令面板', 'commandPalette 是 ⌘K / Ctrl+K 唤起的高频入口:输入即过滤(主文案 / 说明 / 关键词三层),↑↓ 环形选择、回车执行、Esc 收起,选中或关闭都归还焦点;组标题按 items 顺序合并,组件不替你排序。', grid({ min: '320px' }, card({ title: '⌘K 命令面板' }, (() => { const open = signal(false); // 显隐信号:按钮与快捷键共用同一份状态 const cmdk = commandPalette({ open, hotkey: false, // 全局 Ctrl+K 已让给页级跳转面板(多实例约定:只有一个实例注册快捷键),此卡用按钮唤起 items: [ { id: 'save', label: '保存当前页面', group: '文件', hint: '⌘S' }, { id: 'export', label: '导出为 Markdown', group: '文件', hint: '⌘E' }, { id: 'invite', label: '邀请成员', group: '协作', hint: '⌘I', keywords: ['yaoqing'] }, { id: 'role', label: '调整成员权限', group: '协作' }, { id: 'theme', label: '切换暗色主题', group: '偏好', keywords: ['dark', 'anse'] }, { id: 'lang', label: '切换界面语言', group: '偏好', hint: '⌘L' }, ], onSelect: (it) => toast.info(`执行:${it.label}`), }); return h('div', null, button({ label: '打开命令面板(⌘/Ctrl + K)', onClick: () => open(true) }), cmdk.el); })(), typography({ variant: 'paragraph', type: 'secondary', content: '试试输入「协」或拼音 yaoqing / dark:keywords 让中文别名与拼音也能命中,不用改组件。多实例时只有一处注册 ⌘K——本页的 Ctrl+K 属于页级跳转面板,这里用按钮唤起。' }), ), card({ title: '为什么不用原生 input + datalist' }, typography({ variant: 'paragraph', type: 'secondary', content: 'datalist 无法分组、不搜说明文字、也控制不了高亮与焦点归还;命令面板要的是「输入即缩小 + 键盘走完整个流程」,所以自绘列表并用 aria-activedescendant 把高亮交给读屏。' }), ), ), ), section( 'sec-layout', '布局骨架', 'layout:页头 / 侧栏 / 内容 / 页脚一次成型,页头左侧开关可折叠侧栏;内容区自己滚动,页头能吸顶。骨架上不放圆角与投影——那是卡片的事。', h('div', { style: 'height:280px;overflow:hidden;border-radius:var(--ua-radius-md)' }, layout( { header: h('strong', null, '控制台'), sider: h('div', null, h('div', { class: 'out' }, '概览'), h('div', { class: 'out' }, '订单'), h('div', { class: 'out' }, '设置')), footer: '© 2026 @ui-arch', collapsible: true, sticky: true, }, card({ title: '内容区' }, typography({ variant: 'paragraph', type: 'secondary', content: '省略 header / sider / footer 时内容区独占整行;长内容页打开 sticky,页头随滚动留在顶部。' })), ), ), ), section( 'sec-jsonviewer', 'JSON 树查看', 'jsonViewer:接口返回的原始对象直接丢进去即可——默认展开两层、分支显示「N 项 / N 个键」,叶子按类型着色;只读,不带编辑。', jsonViewer({ value: { code: 0, message: 'ok', data: { total: 3, page: 1, items: users.slice(0, 3).map((u) => ({ id: u.id, name: u.name, city: u.city, tags: ['活跃'], vip: u.id === 1 })) }, }, rootLabel: 'response', depth: 2, maxHeight: 260, }), ), section( 'sec-diff', '文本差异对比', 'diffView:改前 / 改后整串丢进去——先剪公共前后缀再对中间段做 LCS,所以改两行不会把整篇标红;增删行同时有底色与 + / − 符号两条通道。', diffView({ before: [ 'export function sum(a, b) {', ' return a + b;', '}', '', '// 用法', 'sum(1, 2);', ].join('\n'), after: [ 'export function sum(a, b) {', " if (typeof a !== 'number') throw new TypeError('a 必须是数字');", ' return a + b;', '}', '', '// 用法', 'sum(1, 2);', "sum(1, '2');", ].join('\n'), }), diffView({ before: 'if (a) {\n return 1;\n}', after: 'if (a) {\n return 1;\n}', ignoreWhitespace: true, // 只差缩进:开启后不再报差异,但显示仍保留原文缩进 }), ), section( 'sec-code', '代码块', 'codeBlock:等宽多行 + 行号列 + 头部标题,右上角一键复制原文;行号是装饰、不进剪贴板,只做展示不高亮。', codeBlock({ title: 'sum.ts', lang: 'ts', maxHeight: 200, wrap: true, // 窄屏下长行折行,不横向滚 code: [ 'export function sum(a: number, b: number): number {', ' if (typeof a !== \'number\') throw new TypeError(\'a 必须是数字\');', ' return a + b;', '}', '', '// 用法:sum(1, 2) === 3', ].join('\n'), }), ), section( 'sec-sign', '手写签名', 'signaturePad:指针手写、位图按 DPR 放大不糊,收笔即把 PNG dataURL 写回信号;底部清空在未落笔时自动禁用。', (() => { const sign = signal(''); const pad = signaturePad({ value: sign, height: 140 }); return h('div', null, pad.el, h('div', { class: 'out' }, () => (sign() ? '已签字(dataURL 长度 ' + sign().length + ')' : '尚未签字'))); })(), ), section( 'sec-loading', '区域加载遮罩', 'loading:把要遮挡的块交进 children,active 为真时盖上遮罩;默认延迟 120ms 出场——接口快于这个时间就不闪,慢于它必有反馈。', (() => { const busy = signal(false); const box = loading({ active: busy, text: '正在刷新订单', minHeight: 120, }, card({ title: '订单概览' }, typography({ variant: 'paragraph', type: 'secondary', content: '这里是一块会被整体刷新的内容:遮罩盖住时它仍在原位、只是变淡。' }))); return h('div', { style: 'display:flex;flex-direction:column;gap:8px' }, button({ label: '切换刷新状态', onClick: () => busy(!busy()) }), box, ); })(), ), section( 'sec-reltime', '相对时间', 'relativeTime:把时间戳渲染成「3 分钟前」并按间隔自动刷新,超阈值改显示绝对日期;精确时间在 title 里,悬停即见。', h('div', { class: 'row' }, h('span', null, '刚刚:'), relativeTime({ value: Date.now() - 20 * 1000 }), h('span', null, '半小时前:'), relativeTime({ value: Date.now() - 30 * 60 * 1000 }), h('span', null, '三天前:'), relativeTime({ value: Date.now() - 3 * 86400 * 1000 }), h('span', null, '两个月前:'), relativeTime({ value: Date.now() - 60 * 86400 * 1000 }), h('span', null, '脏数据:'), relativeTime({ value: 'not-a-date' }), ), ), section( 'sec-spark', '迷你趋势线', 'sparkline:把数值序列压成一条折线,可选面积与末点标记;不画坐标轴、不带交互,只表达走向——主数值交给 statistic。', h('div', { class: 'row' }, card({ title: '订单量(7 天)' }, h('div', { style: 'display:flex;align-items:center;gap:12px' }, statistic({ value: signal(1284) }), sparkline({ values: [820, 932, 901, 934, 1290, 1330, 1284], width: 140, height: 36 }))), card({ title: '转化率(30 天)' }, h('div', { style: 'display:flex;align-items:center;gap:12px' }, statistic({ value: signal(3.62), precision: 2, suffix: '%' }), sparkline({ values: [4.1, 3.9, 4.0, 3.8, 3.7, 3.9, 3.6], area: false }))), ), ), section( 'sec-heat', '日历热力图', 'heatmap:一周一列、七天一行,格子浓度表示当天数量(按最大值归一四档);末列对齐到本周,悬停看具体日期与次数。', (() => { const today = new Date(); const values: { date: string; count: number }[] = []; for (let i = 0; i < 84; i += 1) { const d = new Date(today.getTime() - i * 86400000); const key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); const count = (i * 7) % 5 === 0 ? 0 : (i * 3) % 7; // 伪随机但确定性:每次渲染同一图案 values.push({ date: key, count }); } return heatmap({ values, weeks: 12, formatValue: (n, d) => d + ' 提交 ' + n + ' 次' }); })(), ), ); // 环形进度:value 传信号,按钮改信号 → 弧长与中心文字一起重绘 content.append( section( 'sec-ring', '环形进度', 'progressRing:SVG 双圆靠 stroke-dasharray 画弧(0 从 12 点方向顺时针长),中心文字等宽;value 可传信号,弧长随之重绘。', (() => { const pct = signal(64); const bump = (delta: number): void => pct(Math.min(100, Math.max(0, pct() + delta))); // 夹在 0-100,越界按键不产生负进度 return h( 'div', { class: 'row' }, progressRing({ value: pct }), progressRing({ value: pct, size: 56, thickness: 6, showText: false }), // 无文案小环:适合塞进表格单元格 button({ label: '+10%', variant: 'ghost', onClick: () => bump(10) }), button({ label: '-10%', variant: 'ghost', onClick: () => bump(-10) }), button({ label: '清零', variant: 'ghost', onClick: () => pct(0) }), ); })(), ), ); // 通知条:页顶播报的「一行版 alert」,scroll 让超宽文案自己走过去 content.append( section( 'sec-notice', '通知条', 'noticeBar:单行横条,语义色 + 左侧符号 + 关闭;scroll 开启后正文跑马灯(悬停暂停),减少动效偏好下停成静态首屏。', h( 'div', { class: 'col' }, noticeBar({ text: '系统将于今晚 23:00 维护约 10 分钟,期间提交会排队重试。', onClose: () => toast.info('已关闭维护公告') }), noticeBar({ text: '长公告示例:本季度将完成表单校验规则的可视化配置,期间旧配置继续有效;滚动模式下悬停会自动暂停。', variant: 'warning', scroll: true }), noticeBar({ text: '构建成功:12 项门禁全部通过。', variant: 'success', closable: false }), noticeBar({ text: '连接已断开,正在重试第 3 次…', variant: 'danger', icon: '⚠' }), ), ), section( 'sec-sortable', '拖拽排序', 'sortableList:整行可拖(越过邻行中线即换位),Alt+↑↓ 上下搬一格、Alt+Home/End 搬首尾;松手写回 value 信号,摘要行跟着刷新。', h( 'div', { class: 'col' }, sortableDemo.el, h('p', { class: 'desc' }, () => `当前顺序:${sortableOrder().join(' → ')}`), h('p', { class: 'desc' }, '禁用态:拖不动也不骗人——光标回默认、文字退次级色、悬停与按下都不再给反馈'), sortableList({ items: ['已冻结:需求评审', '已冻结:接口联调'], disabled: true }).el, ), ), section( 'sec-masonry', '瀑布流', 'masonry:CSS 多列布局,列宽按 minColumnWidth 自适应(窄屏自动降列)、也能固定列数;每项包一层才不被列边界拆断,间距一处传值同时管列间距与行间距。', masonry({ minColumnWidth: 200, items: [ card({ title: '图片墙 ①' }, h('p', { class: 'desc' }, '列高不同才是瀑布流的价值:内容自己决定卡片高度,浏览器负责找最短列。')), card({ title: '图片墙 ②' }, h('p', { class: 'desc' }, '短卡片。')), card({ title: '图片墙 ③' }, h('p', { class: 'desc' }, '这一张内容偏长,用来制造高度差:文字越多卡片越高,多列布局才会错落起来,而不是排成整齐的行。')), card({ title: '图片墙 ④' }, h('p', { class: 'desc' }, '中等长度的一段说明,两行左右。')), card({ title: '图片墙 ⑤' }, h('p', { class: 'desc' }, '再一条短的。')), ], }), ), section( 'sec-editable', '行内编辑', 'editable:静止态是文字、点一下就地变输入框;Enter 提交、Esc 取消(不落值)、失焦提交;validate 拦下时保持编辑态并标红,多行模式用 Ctrl+Enter 提交。', h( 'div', { class: 'row' }, editable({ value: titleText, label: '标题', placeholder: '未命名看板', onCommit: (v) => toast.success(`标题改成「${v}」`) }), editable({ value: remark, label: '备注', multiline: true, validate: (v) => (v.length > 40 ? `最多 40 字(当前 ${v.length})` : null), onCommit: () => toast.info('备注已保存'), }), editable({ value: signal('只读字段'), label: '只读', disabled: true }), ), ), ); // 悬浮按钮:fixed 定位,挂 body 不参与页面布局 document.body.append( floatButton({ icon: '+', label: '新建工单', tooltip: '新建工单', position: 'left-bottom', onClick: () => toast.success('已新建工单') }), floatButton({ icon: '?', tooltip: '帮助中心(3 条待读)', badge: 3, onClick: () => toast.info('打开帮助中心') }), ); // 引导漫游实例:根节点需挂到 body(fixed 定位,不参与布局) const demoTour = tour({ steps: [ { target: '#sec-tour', title: '引导漫游', description: '聚光框按 target 选择器定位,卡片会自动挑空间更大的一侧。' }, { target: '#sec-splitter', title: '可拖拽分栏', description: '分隔条支持指针拖拽与方向键微调,双击复位。' }, { target: '#sec-backtop', title: '返回顶部', description: '滚过 300px 后右下角浮现,外环是已滚比例。' }, ], onFinish: () => toast.success('引导完成'), onClose: () => toast.info('已跳过引导'), }); document.body.append(demoTour.el); // 页级命令面板:Ctrl+K 跳转任意演示区块;items 与侧边目录同源(从 .sec 反推,清单改动自动跟随) const demoPalette = commandPalette({ open: signal(false), placeholder: '跳转到组件…', emptyText: '没有匹配的组件', items: Array.from(content.querySelectorAll('section.sec')).map((sec) => ({ id: sec.id, label: sec.querySelector('h2')?.textContent ?? sec.id, group: '跳转', keywords: [sec.id.replace(/^sec-/, '')], // 锚点 id 去 sec- 前缀当拼音外的第二检索路 })), onSelect: (it) => document.getElementById(it.id)?.scrollIntoView({ behavior: 'smooth' }), }); document.body.append(demoPalette.el); const toc = h('aside', { class: 'toc' }, h('div', { class: 'toc-title' }, '目录')); // 目录项由 buildToc 填充 const app = h('div', { class: 'shell' }, toc, content); mount('#app', () => app); buildToc(toc, content); /*** 放大镜图标:demo 内联 SVG,避免 emoji 在不同系统渲染成彩色位图 ***/ function searchIcon(): HTMLElement { const ns = 'http://www.w3.org/2000/svg'; const holder = document.createElement('span'); holder.style.display = 'inline-flex'; const svg = document.createElementNS(ns, 'svg'); svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('width', '14'); svg.setAttribute('height', '14'); svg.setAttribute('fill', 'none'); svg.setAttribute('stroke', 'currentColor'); svg.setAttribute('stroke-width', '1.5'); svg.setAttribute('stroke-linecap', 'round'); const lens = document.createElementNS(ns, 'circle'); lens.setAttribute('cx', '7'); lens.setAttribute('cy', '7'); lens.setAttribute('r', '4.5'); const handle = document.createElementNS(ns, 'line'); handle.setAttribute('x1', '10.5'); handle.setAttribute('y1', '10.5'); handle.setAttribute('x2', '14'); handle.setAttribute('y2', '14'); svg.append(lens, handle); holder.append(svg); return holder; }