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,28 @@
|
||||
{
|
||||
"name": "@ui-arch/frame",
|
||||
"version": "0.1.0",
|
||||
"description": "Minimal reactive DOM framework: signal / computed / effect + hyperscript, zero dependency",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"signal",
|
||||
"reactive",
|
||||
"dom",
|
||||
"micro-framework"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { effect } from './signal.js';
|
||||
|
||||
type Child = Node | string | number | boolean | null | undefined | (() => unknown);
|
||||
type Props = Record<string, unknown>;
|
||||
|
||||
/*** 响应式属性绑定:函数值属性在依赖变化时自动写回 DOM ***/
|
||||
function bindProp(el: HTMLElement, key: string, fn: () => unknown): void {
|
||||
effect(() => {
|
||||
const v = fn();
|
||||
if (key === 'value') (el as unknown as HTMLInputElement).value = String(v);
|
||||
else if (key === 'checked') (el as unknown as HTMLInputElement).checked = Boolean(v);
|
||||
else if (v === false || v == null) el.removeAttribute(key);
|
||||
else el.setAttribute(key, v === true ? '' : String(v));
|
||||
});
|
||||
}
|
||||
|
||||
/*** 静态属性写入:false / null 移除属性,true 置空标记属性 ***/
|
||||
function bindStatic(el: HTMLElement, key: string, value: unknown): void {
|
||||
if (value === false || value == null) el.removeAttribute(key);
|
||||
else if (value === true) el.setAttribute(key, '');
|
||||
else el.setAttribute(key, String(value));
|
||||
}
|
||||
|
||||
/*** 挂载子节点:函数型子节点转为响应式文本节点 ***/
|
||||
function appendChild(el: HTMLElement, child: unknown): void {
|
||||
if (child == null || child === false) return;
|
||||
if (typeof child === 'function') {
|
||||
const node = document.createTextNode('');
|
||||
effect(() => {
|
||||
node.textContent = String(child());
|
||||
});
|
||||
el.append(node);
|
||||
} else if (typeof child === 'object') {
|
||||
el.append(child as Node);
|
||||
} else {
|
||||
el.append(String(child));
|
||||
}
|
||||
}
|
||||
|
||||
/*** hyperscript 入口:h(tag, props, ...children);on* 为事件,函数值为响应式属性 ***/
|
||||
export function h(tag: string, props?: Props | null, ...children: unknown[]): HTMLElement {
|
||||
const el = document.createElement(tag);
|
||||
for (const [key, value] of Object.entries(props ?? {})) {
|
||||
if (key.startsWith('on') && typeof value === 'function') {
|
||||
el.addEventListener(key.slice(2).toLowerCase(), value as EventListener);
|
||||
} else if (typeof value === 'function') {
|
||||
bindProp(el, key, value as () => unknown);
|
||||
} else if (key === 'class') {
|
||||
el.className = String(value);
|
||||
} else {
|
||||
bindStatic(el, key, value);
|
||||
}
|
||||
}
|
||||
for (const child of children.flat(Infinity)) appendChild(el, child);
|
||||
return el;
|
||||
}
|
||||
|
||||
/*** 列表渲染:信号变化时整体替换容器子节点(tbody / 列表等场景) ***/
|
||||
export function renderList(el: HTMLElement, fn: () => Node[]): void {
|
||||
effect(() => {
|
||||
el.replaceChildren(...fn());
|
||||
});
|
||||
}
|
||||
|
||||
/*** 挂载视图到容器;容器不存在时静默跳过 ***/
|
||||
export function mount(target: Element | string, view: () => Node): void {
|
||||
const root = typeof target === 'string' ? document.querySelector(target) : target;
|
||||
if (!root) return; // 卫语句:容器不存在
|
||||
root.replaceChildren(view());
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './signal.js';
|
||||
export * from './dom.js';
|
||||
@@ -0,0 +1,49 @@
|
||||
// 当前正在收集依赖的 effect;null 表示非追踪态
|
||||
let active: (() => void) | null = null;
|
||||
|
||||
/*** 响应式信号:无参调用读值并收集依赖,传参写值并触发订阅 ***/
|
||||
export interface Signal<T> {
|
||||
(): T;
|
||||
(value: T): void;
|
||||
}
|
||||
|
||||
/*** 创建信号:框架状态的最小单元 ***/
|
||||
export function signal<T>(initial: T): Signal<T> {
|
||||
let value = initial;
|
||||
const subs = new Set<() => void>();
|
||||
const read = (...args: unknown[]) => {
|
||||
if (args.length === 0) {
|
||||
if (active) subs.add(active); // 依赖收集
|
||||
return value;
|
||||
}
|
||||
const v = args[0] as T;
|
||||
if (Object.is(v, value)) return; // 值未变不触发
|
||||
value = v;
|
||||
for (const fn of [...subs]) fn(); // 拷贝后触发,防遍历中修改
|
||||
};
|
||||
return read as Signal<T>;
|
||||
}
|
||||
|
||||
/*** 副作用:立即执行一次并追踪内部读取的信号,依赖变化自动重跑 ***/
|
||||
export function effect(fn: () => void): () => void {
|
||||
const run = () => {
|
||||
const prev = active;
|
||||
active = run;
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
active = prev;
|
||||
}
|
||||
};
|
||||
run();
|
||||
return () => {
|
||||
/* 演示级实现:不做依赖清理 */
|
||||
};
|
||||
}
|
||||
|
||||
/*** 计算值:由信号派生的只读视图,同样参与上层依赖收集 ***/
|
||||
export function computed<T>(fn: () => T): () => T {
|
||||
const out = signal<T>(undefined as T);
|
||||
effect(() => out(fn()));
|
||||
return () => out();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@ui-arch/frame",
|
||||
"type": "headless-core",
|
||||
"description": "自研微框架:signal / computed / effect 信号系统 + hyperscript(h)DOM 层。函数值属性与函数型子节点自动响应式;renderList 做列表整体替换;零依赖、无虚拟 DOM。",
|
||||
"tags": [
|
||||
"framework",
|
||||
"signal",
|
||||
"reactive",
|
||||
"hyperscript"
|
||||
],
|
||||
"api": {
|
||||
"entry": "src/index.ts",
|
||||
"exports": [
|
||||
"signal(initial) -> Signal<T>:无参读值并收集依赖,传参写值并触发订阅",
|
||||
"effect(fn):立即执行并追踪依赖,信号变化自动重跑",
|
||||
"computed(fn):由信号派生的只读计算值",
|
||||
"h(tag, props, ...children):hyperscript;on* 绑事件,函数值属性/子节点自动响应式",
|
||||
"renderList(el, fn):信号变化时整体替换容器子节点",
|
||||
"mount(target, view):挂载视图"
|
||||
],
|
||||
"keyTypes": [
|
||||
"Signal"
|
||||
]
|
||||
},
|
||||
"usage": "const count = signal(0); mount('#app', () => h('button', { onClick: () => count(count() + 1) }, () => `count = ${count()}`))",
|
||||
"conventions": [
|
||||
"组件 = 返回 DOM 节点的普通函数,不做类组件与生命周期抽象",
|
||||
"跨系统状态(如 table-core)用桥接信号:subscribe 里递增 version 信号"
|
||||
],
|
||||
"docs": {
|
||||
"intro": "零依赖微框架:组件 = 返回 DOM 的普通函数。signal/effect/computed 管状态,h() 管 DOM,无虚拟 DOM 无生命周期抽象。",
|
||||
"steps": [
|
||||
"引入:import { signal, computed, effect, h, mount } from '@ui-arch/frame'",
|
||||
"声明状态:const count = signal(0);无参调用读值 count(),传参写值 count(1)",
|
||||
"渲染视图:mount('#app', () => h('div', null, () => `count = ${count()}`))——函数型子节点即响应式文本",
|
||||
"绑定事件:props 里 on 开头且值为函数的键(onClick 等)自动 addEventListener",
|
||||
"派生数据:const double = computed(() => count() * 2),视图里直接调用 double(),依赖自动追踪",
|
||||
"列表渲染:renderList(tbody, () => rows.map(r => h('tr', null, r.name))),信号变化整体替换子节点"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user