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