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:
shawn
2026-10-23 14:58:10 +08:00
committed by HIsheR
parent a186d71412
commit 0de9b6e66e
59 changed files with 5293 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
import { h } from '@ui-arch/frame';
import type { AlertProps } from './types.js';
/*** 提示条:variant 决定语义色,纯展示不持有状态 ***/
export function alert(props: AlertProps): HTMLElement {
return h('div', { class: `ua-alert ua-alert-${props.variant ?? 'info'}` }, props.message);
}
+7
View File
@@ -0,0 +1,7 @@
import { h } from '@ui-arch/frame';
import type { BadgeProps } from './types.js';
/*** 徽标:variant 决定语义色,纯展示不持有状态 ***/
export function badge(props: BadgeProps): HTMLElement {
return h('span', { class: `ua-badge ua-badge-${props.variant ?? 'default'}` }, props.label);
}
+11
View File
@@ -0,0 +1,11 @@
import { h } from '@ui-arch/frame';
import type { ButtonProps } from './types.js';
/*** 按钮:variant 控制视觉强度,onClick 为点击回调 ***/
export function button(props: ButtonProps): HTMLElement {
return h(
'button',
{ class: `ua-btn ua-btn-${props.variant ?? 'primary'}`, onClick: props.onClick },
props.label,
);
}
+12
View File
@@ -0,0 +1,12 @@
import { h } from '@ui-arch/frame';
import type { CardProps } from './types.js';
/*** 卡片容器:可选标题 + 任意子内容 ***/
export function card(props: CardProps, ...children: unknown[]): HTMLElement {
return h(
'div',
{ class: 'ua-card' },
props.title ? h('h3', { class: 'ua-card-title' }, props.title) : null,
...children,
);
}
+18
View File
@@ -0,0 +1,18 @@
import { effect, h } from '@ui-arch/frame';
import type { CheckboxProps } from './types.js';
/*** 复选框:checked 信号双向绑定,onChange 为变更回调 ***/
export function checkbox(props: CheckboxProps): HTMLElement {
const el = h('input', { type: 'checkbox', class: 'ua-checkbox' }) as HTMLInputElement;
if (props.checked) {
const checked = props.checked;
el.addEventListener('change', () => checked(el.checked)); // 用户勾选写回信号
effect(() => {
if (el.checked !== checked()) el.checked = checked(); // 外部变化写回勾选框
});
}
if (props.onChange) el.addEventListener('change', () => props.onChange!(el.checked));
if (!props.label) return el;
const wrap = h('label', { class: 'ua-check-row' }, el, props.label);
return wrap;
}
+133
View File
@@ -0,0 +1,133 @@
import {
createTable,
filterPlugin,
paginationPlugin,
selectionPlugin,
sortPlugin,
} from '@ui-arch/table-core';
import type { PageInfo, RowKey, TableInstance } from '@ui-arch/table-core';
import { computed, effect, h, renderList, signal } from '@ui-arch/frame';
import { button } from './button.js';
import { textField } from './textField.js';
import type { DataTableProps } from './types.js';
/*** 数据表格:粘性表头 + 行选择 + 排序 + 筛选 + 分页 + 空状态;状态由 table-core 持有,信号只做桥接渲染 ***/
export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): HTMLElement {
// —— 状态集中声明 ——
const selectable = props.selectable ?? false; // 行选择开关
const table = createTable({
data: props.data,
columns: props.columns,
rowKey: 'id',
plugins: [filterPlugin<T>(), sortPlugin<T>(), paginationPlugin<T>(), selectionPlugin<T>()],
initialState: { pagination: { page: 1, pageSize: props.pageSize ?? 5 } },
});
const api = table as unknown as TableInstance<T> & {
getPageInfo(): PageInfo;
selectAllVisible(): void;
};
const version = signal(0); // 桥接信号:table-core 订阅通知 → 递增触发信号系统
table.subscribe(() => version(version() + 1));
// —— 派生:全部由 version 单一来源推导,语义与内核一致 ——
const visible = computed(() => {
version();
return table.getVisibleRows();
});
const state = computed(() => {
version();
return table.getState();
});
const info = computed(() => {
visible();
return api.getPageInfo();
});
const selectedCount = computed(() => state().selection.length);
const filterKey = props.filterKey ?? props.columns[0].key;
const colspan = props.columns.length + (selectable ? 1 : 0);
// —— 表头全选框:checked / indeterminate 三态跟随当页选中情况 ——
const headCheck = h('input', { type: 'checkbox', class: 'ua-checkbox' }) as HTMLInputElement;
headCheck.addEventListener('change', () => {
if (headCheck.checked) api.selectAllVisible();
else table.dispatch({ type: 'clearSelection' });
});
effect(() => {
const keys = visible().keys;
const sel = state().selection;
const onPage = keys.filter((k) => sel.includes(k)).length;
headCheck.checked = keys.length > 0 && onPage === keys.length;
headCheck.indeterminate = onPage > 0 && onPage < keys.length;
});
/*** 排序标记:升 ↑ / 降 ↓ / 未排序 ↕(可点击提示) ***/
const sortMark = (key: string) =>
h(
'span',
{ class: 'ua-sort-mark' },
() => {
const rule = state().sorting.find((r) => r.key === key);
return rule ? (rule.dir === 'asc' ? '↑' : '↓') : '↕';
},
);
const th = (col: (typeof props.columns)[number]) =>
h(
'th',
{
class: () => (col.sortable ? 'ua-sortable' : ''),
onClick: () => col.sortable && table.dispatch({ type: 'toggleSort', key: col.key }),
},
col.header ?? col.key,
col.sortable ? sortMark(col.key) : null,
);
const tbody = h('tbody');
renderList(tbody, () => {
const sel = state().selection; // 读入依赖:选择变化即整表重绘(行数 = pageSize,代价可控)
const rows = visible().rows;
if (!rows.length) {
return [h('tr', { class: 'ua-empty-row' }, h('td', { colspan }, '暂无匹配数据'))];
}
return rows.map((row, i) => {
const key = visible().keys[i];
const selected = sel.includes(key);
const cells = props.columns.map((col) => h('td', null, String(table.getCellValue(row, col.key))));
if (selectable) {
const check = h('input', { type: 'checkbox', class: 'ua-checkbox' }) as HTMLInputElement;
check.checked = selected;
check.addEventListener('change', () => table.dispatch({ type: 'toggleSelect', key }));
cells.unshift(h('td', { class: 'ua-cell-check' }, check));
}
return h('tr', selected ? { class: 'ua-row-selected' } : null, ...cells);
});
});
const filterInput = textField({
placeholder: `筛选 ${filterKey}…`,
onInput: (value) => {
table.dispatch({ type: 'setFilter', key: filterKey, value });
table.dispatch({ type: 'setPage', page: 1 }); // 筛选变化回第 1 页
},
});
return h(
'section',
{ class: 'ua-table' },
h(
'div',
{ class: 'ua-table-toolbar' },
filterInput,
selectable ? h('span', { class: 'ua-sel-count' }, () => (selectedCount() ? `已选 ${selectedCount()} 行` : '')) : null,
),
h('div', { class: 'ua-table-scroll' }, h('table', null, h('thead', null, h('tr', null, selectable ? headCheck : null, ...props.columns.map(th))), tbody)),
h(
'div',
{ class: 'ua-pager' },
button({ label: '上一页', variant: 'ghost', onClick: () => table.dispatch({ type: 'setPage', page: info().page - 1 }) }),
h('span', { class: 'ua-page-info' }, () => `第 ${info().page} / ${info().totalPages} 页 · 共 ${visible().total} 行`),
button({ label: '下一页', variant: 'ghost', onClick: () => table.dispatch({ type: 'setPage', page: info().page + 1 }) }),
),
);
}
+78
View File
@@ -0,0 +1,78 @@
import { h } from '@ui-arch/frame';
import { button } from './button.js';
import { textField } from './textField.js';
import type { FormItemHandle, FormItemProps, FormProps, FormRule } from './types.js';
/*** 执行单条规则:null 通过,字符串为错误消息 ***/
function checkRule(value: string, rule: FormRule): string | null {
if (rule.required && !value.trim()) return rule.message ?? '该项为必填项';
if (!value) return null; // 非必填且为空:其余规则跳过
if (rule.min !== undefined && value.length < rule.min) return rule.message ?? `至少 ${rule.min} 个字符`;
if (rule.max !== undefined && value.length > rule.max) return rule.message ?? `最多 ${rule.max} 个字符`;
if (rule.pattern && !rule.pattern.test(value)) return rule.message ?? '格式不正确';
if (rule.validator) {
const r = rule.validator(value);
if (r !== true) return typeof r === 'string' ? r : rule.message ?? '校验未通过';
}
return null;
}
/*** 表单字段:label + 控件插槽 + 错误区;返回句柄供 form 聚合校验 ***/
export function formItem(props: FormItemProps, control: HTMLElement): FormItemHandle {
// —— 状态集中声明 ——
const mode = props.validateOn ?? 'blur'; // 校验时机;缺省失焦校验
const rules = [...(props.required ? [{ required: true }] : []), ...(props.rules ?? [])];
const error = h('div', { class: 'ua-form-error' });
const label = h('label', { class: 'ua-form-label' }, props.label, props.required ? h('i', { class: 'ua-form-req' }, '*') : null);
const el = h('div', { class: 'ua-form-item' }, label, h('div', { class: 'ua-form-control' }, control, error));
/*** 执行全部规则:返回首个错误消息,同时同步错误区与控件态 ***/
const validate = () => {
const value = props.value();
let msg: string | null = null;
for (const rule of rules) {
msg = checkRule(value, rule);
if (msg) break; // 首个失败即停止
}
error.textContent = msg ?? '';
control.classList.toggle('ua-invalid', !!msg);
return msg;
};
control.addEventListener('input', () => {
if (mode === 'input' || error.textContent) validate(); // input 模式即时校验;其余模式仅清已报错误
});
control.addEventListener('blur', () => {
if (mode !== 'submit') validate();
});
return { el, validate };
}
/*** 表单容器:布局 + 原生 submit 拦截 + 聚合校验(全部通过才触发 onSubmit) ***/
export function form(props: FormProps, items: FormItemHandle[]): HTMLElement {
// —— 状态集中声明 ——
/*** 整体提交:逐项校验,聚焦首个出错项并中止 ***/
const submit = () => {
for (const item of items) {
if (item.validate()) {
(item.el.querySelector('input, textarea, select') as HTMLElement | null)?.focus();
return; // 卫语句:有错误不提交
}
}
props.onSubmit?.();
};
const submitBtn = button({ label: props.submitText ?? '提交', onClick: submit });
const resetBtn = props.resetText ? button({ label: props.resetText, variant: 'ghost', onClick: props.onReset }) : null;
const actions = h('div', { class: 'ua-form-actions' }, submitBtn, resetBtn);
const el = h(
'form',
{ class: props.layout === 'horizontal' ? 'ua-form ua-form-h' : 'ua-form' },
...items.map((i) => i.el),
actions,
);
el.addEventListener('submit', (e) => {
e.preventDefault(); // 拦截原生提交,走校验流程
submit();
});
return el;
}
+17
View File
@@ -0,0 +1,17 @@
export * from './types.js';
export { button } from './button.js';
export { textField } from './textField.js';
export { textarea } from './textarea.js';
export { checkbox } from './checkbox.js';
export { switchButton } from './switch.js';
export { select } from './select.js';
export { radioGroup } from './radioGroup.js';
export { card } from './card.js';
export { badge } from './badge.js';
export { alert } from './alert.js';
export { spinner } from './spinner.js';
export { progress } from './progress.js';
export { tabs } from './tabs.js';
export { modal } from './modal.js';
export { form, formItem } from './form.js';
export { dataTable } from './dataTable.js';
+33
View File
@@ -0,0 +1,33 @@
import { effect, h } from '@ui-arch/frame';
import type { ModalProps } from './types.js';
/*** 弹窗:open 信号驱动显隐,点遮罩、× 或 Esc 触发 onClose ***/
export function modal(props: ModalProps, ...children: unknown[]): HTMLElement {
const close = () => {
if (!props.open()) return; // 卫语句:已关闭不重复触发回调
props.open(false); // 写回信号触发隐藏
props.onClose?.();
};
const overlay = h('div', {
class: 'ua-modal-overlay',
onClick: (e: Event) => {
if (e.target === overlay) close(); // 卫语句:仅点遮罩本身关闭
},
});
const box = h(
'div',
{ class: 'ua-modal' },
h('div', { class: 'ua-modal-head' }, h('h3', { class: 'ua-modal-title' }, props.title), h('button', { class: 'ua-modal-close', onClick: close }, '×')),
h('div', { class: 'ua-modal-body' }, ...children),
);
overlay.append(box);
effect(() => {
const open = props.open();
overlay.classList.toggle('ua-open', open); // 显隐跟随信号
if (open) (box.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素
});
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape' && props.open()) close(); // Esc 关闭当前打开的弹窗
});
return overlay;
}
+22
View File
@@ -0,0 +1,22 @@
import { effect, h } from '@ui-arch/frame';
import type { ProgressProps } from './types.js';
/*** 进度条:轨道 + 渐变流动填充,value 信号驱动 0-100;满格切换 success 色并停止条纹 ***/
export function progress(props: ProgressProps): HTMLElement {
const fill = h('div', { class: 'ua-progress-fill' });
const track = h('div', { class: 'ua-progress-track' }, fill);
const text = h('span', { class: 'ua-progress-text' });
effect(() => {
const v = clamp(props.value()); // 依赖收集:信号变化自动写回
fill.style.width = `${v}%`;
fill.classList.toggle('ua-done', v >= 100); // 满格切语义色
text.textContent = `${Math.round(v)}%`;
});
return h('div', { class: 'ua-progress' }, track, props.showText === false ? null : text);
}
/*** 进度钳制到 0-100 ***/
function clamp(v: number): number {
if (Number.isNaN(v)) return 0; // 卫语句:非法值归零
return Math.min(100, Math.max(0, v));
}
+25
View File
@@ -0,0 +1,25 @@
import { effect, h } from '@ui-arch/frame';
import type { RadioGroupProps } from './types.js';
/*** 单选组:value 信号双向绑定,options 同组互斥,name 固定保证原生互斥行为 ***/
export function radioGroup(props: RadioGroupProps): HTMLElement {
const groupName = `ua-radio-${Math.random().toString(36).slice(2, 8)}`; // 组内唯一 name
const radios: HTMLInputElement[] = [];
for (const opt of props.options) {
const input = h('input', { type: 'radio', name: groupName, class: 'ua-radio' }) as HTMLInputElement;
input.value = opt.value;
radios.push(input);
input.addEventListener('change', () => {
if (props.value) props.value(input.value); // 用户选择写回信号
props.onChange?.(input.value);
});
}
if (props.value) {
const value = props.value;
effect(() => {
for (const input of radios) input.checked = input.value === value(); // 外部变化同步勾选态
});
}
const rows = props.options.map((opt, i) => h('label', { class: 'ua-check-row' }, radios[i], opt.label));
return h('div', { class: 'ua-radio-group' }, ...rows);
}
+60
View File
@@ -0,0 +1,60 @@
import { effect, h, signal } from '@ui-arch/frame';
import type { SelectProps } from './types.js';
/*** 下拉选择:自绘弹层替代原生 select,value 信号双向绑定;点选项 / 点外部 / Esc 关闭 ***/
export function select(props: SelectProps): HTMLElement {
// —— 状态集中声明 ——
const open = signal(false); // 弹层开合
const value = props.value ?? signal(''); // 选中值;未传信号则内部持有
const labelOf = (v: string) => props.options.find((o) => o.value === v)?.label ?? '';
const current = () => labelOf(value()); // 触发器显示文案(响应式)
const pick = (v: string) => {
value(v);
open(false);
props.onChange?.(v);
};
// —— 触发器 ——
const trigger = h(
'button',
{
type: 'button',
class: 'ua-select-trigger',
'aria-haspopup': 'listbox',
'aria-expanded': () => String(open()),
onClick: () => open(!open()),
},
h('span', { class: () => `ua-select-value${value() ? '' : ' is-empty'}` }, () => current() || props.placeholder || ''),
h('span', { class: 'ua-select-arrow' }, '▾'),
);
// —— 选项弹层 ——
const options = props.options.map((opt) =>
h(
'button',
{
type: 'button',
class: () => `ua-option${opt.value === value() ? ' ua-option-selected' : ''}`,
role: 'option',
'aria-selected': () => String(opt.value === value()),
onClick: () => pick(opt.value),
},
opt.label,
),
);
const pop = h('div', { class: () => `ua-select-pop${open() ? ' ua-open' : ''}`, role: 'listbox' }, ...options);
const root = h('div', { class: 'ua-select' }, trigger, pop);
root.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') open(false); // Esc 关闭弹层
});
document.addEventListener('click', (e) => {
if (root.contains(e.target as Node)) return; // 卫语句:组件内点击不处理
open(false); // 点外部关闭
});
effect(() => open()); // 建立依赖,保证开合信号生效
return root;
}
+11
View File
@@ -0,0 +1,11 @@
import { h } from '@ui-arch/frame';
import type { SpinnerProps } from './types.js';
/*** 加载指示器:纯 CSS 旋转动画,size 控制直径 ***/
export function spinner(props: SpinnerProps = {}): HTMLElement {
const size = `${props.size ?? 18}px`;
const el = h('span', { class: 'ua-spinner' });
el.style.width = size;
el.style.height = size;
return el;
}
+19
View File
@@ -0,0 +1,19 @@
import { effect, h } from '@ui-arch/frame';
import type { SwitchProps } from './types.js';
/*** 开关:checked 信号双向绑定,轨道 + 滑块的纯 CSS 实现 ***/
export function switchButton(props: SwitchProps): HTMLElement {
const input = h('input', { type: 'checkbox', class: 'ua-switch-input' }) as HTMLInputElement;
if (props.checked) {
const checked = props.checked;
input.addEventListener('change', () => checked(input.checked)); // 用户切换写回信号
effect(() => {
if (input.checked !== checked()) input.checked = checked(); // 外部变化写回开关
});
}
if (props.onChange) input.addEventListener('change', () => props.onChange!(input.checked));
const track = h('span', { class: 'ua-switch-track' }, h('span', { class: 'ua-switch-thumb' }));
const el = h('label', { class: 'ua-switch' }, input, track);
if (props.label) el.append(h('span', { class: 'ua-switch-label' }, props.label));
return el;
}
+54
View File
@@ -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);
}
+16
View File
@@ -0,0 +1,16 @@
import { effect, h } from '@ui-arch/frame';
import type { TextFieldProps } from './types.js';
/*** 文本输入:value 信号双向绑定,onInput 为输入回调 ***/
export function textField(props: TextFieldProps): HTMLInputElement {
const el = h('input', { class: 'ua-input', placeholder: props.placeholder ?? '' }) as HTMLInputElement;
if (props.value) {
const signal = props.value;
el.addEventListener('input', () => signal(el.value)); // 用户输入写回信号
effect(() => {
if (el.value !== signal()) el.value = signal(); // 外部变化写回输入框
});
}
if (props.onInput) el.addEventListener('input', () => props.onInput!(el.value));
return el;
}
+16
View File
@@ -0,0 +1,16 @@
import { effect, h } from '@ui-arch/frame';
import type { TextareaProps } from './types.js';
/*** 多行输入:value 信号双向绑定,onInput 为输入回调 ***/
export function textarea(props: TextareaProps): HTMLTextAreaElement {
const el = h('textarea', { class: 'ua-textarea', rows: props.rows ?? 3, placeholder: props.placeholder ?? '' }) as HTMLTextAreaElement;
if (props.value) {
const signal = props.value;
el.addEventListener('input', () => signal(el.value)); // 用户输入写回信号
effect(() => {
if (el.value !== signal()) el.value = signal(); // 外部变化写回输入框
});
}
if (props.onInput) el.addEventListener('input', () => props.onInput!(el.value));
return el;
}
+147
View File
@@ -0,0 +1,147 @@
import type { ColumnDef, RowKey } from '@ui-arch/table-core';
import type { Signal } from '@ui-arch/frame';
/*** 按钮入参 ***/
export interface ButtonProps {
label: string; // 按钮文案
variant?: 'primary' | 'ghost' | 'danger'; // 视觉强度;缺省 primary
onClick?: () => void; // 点击回调
}
/*** 文本输入入参 ***/
export interface TextFieldProps {
placeholder?: string; // 占位文案
value?: Signal<string>; // 双向绑定的信号
onInput?: (value: string) => void; // 输入回调
}
/*** 卡片入参 ***/
export interface CardProps {
title?: string; // 标题;缺省不渲染标题行
}
/*** 数据表格入参 ***/
export interface DataTableProps<T> {
data: T[]; // 全量行数据,行须含 id 字段作 rowKey
columns: ColumnDef<T>[]; // 列定义
filterKey?: string; // 工具栏筛选的列;缺省取第一列
pageSize?: number; // 每页行数;缺省 5
selectable?: boolean; // 是否启用行选择列(含全选三态与已选计数);缺省 false
}
/*** 选项条目:select / radioGroup / tabs 共用的选项形状 ***/
export interface OptionItem {
value: string; // 选项值(提交/匹配用)
label: string; // 选项文案(展示用)
disabled?: boolean; // 仅 tabs 消费:禁用该页签
}
/*** 复选框入参 ***/
export interface CheckboxProps {
label?: string; // 右侧文案;缺省不渲染
checked?: Signal<boolean>; // 勾选状态信号,双向绑定
onChange?: (checked: boolean) => void; // 变更回调
}
/*** 开关入参 ***/
export interface SwitchProps {
label?: string; // 左侧文案;缺省不渲染
checked?: Signal<boolean>; // 开关状态信号,双向绑定
onChange?: (checked: boolean) => void; // 变更回调
}
/*** 下拉选择入参 ***/
export interface SelectProps {
options: OptionItem[]; // 选项列表
value?: Signal<string>; // 选中值信号,双向绑定
placeholder?: string; // 未选中时的占位项文案
onChange?: (value: string) => void; // 变更回调
}
/*** 单选组入参 ***/
export interface RadioGroupProps {
options: OptionItem[]; // 选项列表
value?: Signal<string>; // 选中值信号,双向绑定
onChange?: (value: string) => void; // 变更回调
}
/*** 多行输入入参 ***/
export interface TextareaProps {
placeholder?: string; // 占位文案
rows?: number; // 行数;缺省 3
value?: Signal<string>; // 双向绑定的信号
onInput?: (value: string) => void; // 输入回调
}
/*** 徽标入参 ***/
export interface BadgeProps {
label: string; // 徽标文案
variant?: 'default' | 'success' | 'warning' | 'danger'; // 语义色;缺省 default
}
/*** 提示条入参 ***/
export interface AlertProps {
message: string; // 提示内容
variant?: 'info' | 'success' | 'warning' | 'danger'; // 语义色;缺省 info
}
/*** 加载指示器入参 ***/
export interface SpinnerProps {
size?: number; // 直径像素;缺省 18
}
/*** 进度条入参 ***/
export interface ProgressProps {
value: Signal<number>; // 当前进度 0-100 的信号
showText?: boolean; // 是否右侧显示百分比;缺省 true
}
/*** 标签页入参 ***/
export interface TabsProps {
tabs: OptionItem[]; // 页签列表
active?: Signal<string>; // 当前激活页签的 value 信号,双向绑定
onChange?: (value: string) => void; // 切换回调
}
/*** 弹窗入参 ***/
export interface ModalProps {
title: string; // 标题
open: Signal<boolean>; // 开关信号:true 显示 / false 隐藏
onClose?: () => void; // 关闭回调(点遮罩、× 或 Esc 触发)
}
/*** 表单校验规则:按序执行,首个失败即停止 ***/
export interface FormRule {
required?: boolean; // 非空校验(trim 后判定)
min?: number; // 最小长度
max?: number; // 最大长度
pattern?: RegExp; // 正则格式校验
validator?: (value: string) => boolean | string; // 自定义校验:true 通过 / 字符串为错误消息
message?: string; // 该规则的错误消息;缺省按规则类型给默认文案
}
/*** 表单字段入参 ***/
export interface FormItemProps {
label: string; // 字段标签
value: Signal<string>; // 字段值信号(校验数据源)
required?: boolean; // 必填:显示红星并自动追加非空规则
rules?: FormRule[]; // 附加校验规则,按序执行
validateOn?: 'input' | 'blur' | 'submit'; // 校验时机;缺省 blur
}
/*** 表单字段句柄:form 聚合校验用 ***/
export interface FormItemHandle {
el: HTMLElement; // 字段根元素(label + 控件 + 错误区)
validate(): string | null; // 执行校验:null 通过 / 字符串为错误消息
}
/*** 表单容器入参 ***/
export interface FormProps {
layout?: 'vertical' | 'horizontal'; // 布局方向;缺省 vertical
submitText?: string; // 提交按钮文案;缺省「提交」
resetText?: string; // 重置按钮文案;缺省不渲染重置按钮
onSubmit?: () => void; // 全部字段校验通过后的提交回调
onReset?: () => void; // 重置回调
}
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方