init: @ui-arch 私人组件库 monorepo
- core/table-core: Headless 表格内核(排序/筛选/分页/选择插件 + 冒烟 9 项) - core/frame: signal/computed/effect + hyperscript 微框架(零依赖) - skin/ui: 16 个 DOM 组件(含自绘 select、滑动指示条 tabs、行选择 dataTable、form 表单校验) - design/tokens: --ua-* 设计令牌(light/dark) - tools/cli: ua 命令行(index/list/info/docs/style/mcp)+ MCP 注册表服务 - apps: demo 演示站(esbuild 单文件)+ docs 双轨文档(人类版 + llms-full.txt)
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { effect, h } from '@ui-arch/frame';
|
||||
import type { TabsProps } from './types.js';
|
||||
|
||||
/*** 标签页:active 信号双向绑定 + 滑动指示条 + 方向键环形导航;内容由调用方按 active 派生 ***/
|
||||
export function tabs(props: TabsProps): HTMLElement {
|
||||
// —— 状态集中声明 ——
|
||||
const buttons: HTMLButtonElement[] = []; // 页签按钮,顺序与 tabs 一致
|
||||
const indicator = h('span', { class: 'ua-tabs-indicator' });
|
||||
const head = h('div', { class: 'ua-tabs-head', role: 'tablist' }, indicator);
|
||||
|
||||
/*** 同步激活态与指示条几何位置 ***/
|
||||
const sync = () => {
|
||||
const value = props.active?.();
|
||||
for (const btn of buttons) {
|
||||
const on = btn.dataset.value === value;
|
||||
btn.classList.toggle('ua-tab-active', on);
|
||||
btn.setAttribute('aria-selected', String(on));
|
||||
btn.tabIndex = on ? 0 : -1; // 漫游 tabindex:仅激活页签可 Tab 聚焦
|
||||
if (on) {
|
||||
indicator.style.left = `${btn.offsetLeft}px`;
|
||||
indicator.style.width = `${btn.offsetWidth}px`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const tab of props.tabs) {
|
||||
const btn = h('button', { class: 'ua-tab', role: 'tab', type: 'button' }, tab.label) as HTMLButtonElement;
|
||||
btn.dataset.value = tab.value;
|
||||
btn.disabled = tab.disabled ?? false;
|
||||
btn.addEventListener('click', () => {
|
||||
if (btn.disabled) return; // 卫语句:禁用页签不响应
|
||||
if (props.active) props.active(tab.value); // 用户切换写回信号
|
||||
props.onChange?.(tab.value);
|
||||
});
|
||||
buttons.push(btn);
|
||||
head.append(btn);
|
||||
}
|
||||
|
||||
head.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
const delta = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
|
||||
if (!delta) return; // 卫语句:非方向键放行
|
||||
const list = buttons.filter((b) => !b.disabled);
|
||||
const i = list.indexOf(document.activeElement as HTMLButtonElement);
|
||||
if (i < 0) return; // 卫语句:焦点不在页签上
|
||||
e.preventDefault();
|
||||
const next = list[(i + delta + list.length) % list.length]; // 环形移动
|
||||
next.focus();
|
||||
next.click();
|
||||
});
|
||||
|
||||
effect(sync);
|
||||
requestAnimationFrame(sync); // 挂载后再量一次布局,否则指示条宽度为 0
|
||||
return h('div', { class: 'ua-tabs' }, head);
|
||||
}
|
||||
Reference in New Issue
Block a user