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
@@ -0,0 +1,67 @@
import type {
Action,
DeriveContext,
FilterRule,
RowKey,
RowModel,
TablePlugin,
TableState,
} from '../types.js';
/*** 筛选插件入参 ***/
export interface FilterPluginOptions<T> {
predicate?: (cellValue: unknown, filterValue: unknown, row: T) => boolean; // 自定义匹配谓词;缺省大小写不敏感子串匹配
}
/*** 列筛选插件:'' / null 表示清除该列筛选;按(输入模型, filters 切片)memo ***/
export function filterPlugin<T = unknown>(
opts: FilterPluginOptions<T> = {},
): TablePlugin<T> {
const matches =
opts.predicate ??
((value: unknown, filterValue: unknown) =>
String(value ?? '')
.toLowerCase()
.includes(String(filterValue ?? '').toLowerCase()));
let lastInput: RowModel<T> | null = null;
let lastFilters: FilterRule[] | null = null;
let cached: RowModel<T> | null = null;
return {
name: 'filter',
patchState(state: TableState, action: Action): TableState {
switch (action.type) {
case 'setFilter': {
const filters = state.filters.filter((f) => f.key !== action.key);
if (action.value !== '' && action.value != null) filters.push({ key: action.key, value: action.value });
return { ...state, filters };
}
case 'clearFilters':
return state.filters.length ? { ...state, filters: [] } : state;
default:
return state;
}
},
derive(model: RowModel<T>, state: TableState, ctx: DeriveContext<T>): RowModel<T> {
if (model === lastInput && state.filters === lastFilters && cached) return cached;
const { filters } = state;
let out = model;
if (filters.length > 0) {
const rows: T[] = [];
const keys: RowKey[] = [];
for (let i = 0; i < model.rows.length; i++) {
const row = model.rows[i];
if (!filters.every((f) => matches(ctx.getCellValue(row, f.key), f.value, row))) continue;
rows.push(row);
keys.push(model.keys[i]);
}
out = { rows, keys, total: rows.length }; // total = 筛选后行数,供分页算页数
}
cached = out;
lastInput = model;
lastFilters = state.filters;
return cached;
},
};
}
@@ -0,0 +1,61 @@
import type {
Action,
RowModel,
TableInstance,
TablePlugin,
TableState,
} from '../types.js';
/*** 分页快照:由上游 total 推导的派生量,语义与 RowModel.total 一致 ***/
export interface PageInfo {
page: number; // 当前页(钳制后实际生效值)
pageSize: number; // 每页行数
totalPages: number; // 总页数 = ceil(total / pageSize)
}
/*** 客户端分页插件:读入参 total 算页数,越界页在派生时钳制;按(输入模型, pagination 切片)memo ***/
export function paginationPlugin<T = unknown>(): TablePlugin<T> {
let lastInput: RowModel<T> | null = null;
let lastPage: { page: number; pageSize: number } | null = null;
let cached: RowModel<T> | null = null;
let pageInfo: PageInfo = { page: 1, pageSize: 20, totalPages: 1 };
return {
name: 'pagination',
patchState(state: TableState, action: Action): TableState {
switch (action.type) {
case 'setPage':
return { ...state, pagination: { ...state.pagination, page: action.page } };
case 'setPageSize':
return { ...state, pagination: { page: 1, pageSize: action.pageSize } }; // 改页大小回到第 1 页
default:
return state;
}
},
derive(model: RowModel<T>, state: TableState): RowModel<T> {
if (model === lastInput && state.pagination === lastPage && cached) return cached;
const { page, pageSize } = state.pagination;
const totalPages = Math.max(1, Math.ceil(model.total / pageSize));
const safePage = Math.min(Math.max(1, page), totalPages); // 越界钳制,视图不空白
const start = (safePage - 1) * pageSize;
cached = {
rows: model.rows.slice(start, start + pageSize),
keys: model.keys.slice(start, start + pageSize),
total: model.total,
};
pageInfo = { page: safePage, pageSize, totalPages };
lastInput = model;
lastPage = state.pagination;
return cached;
},
api(table: TableInstance<T>) {
return {
setPage: (page: number) => table.dispatch({ type: 'setPage', page }),
nextPage: () => table.dispatch({ type: 'setPage', page: pageInfo.page + 1 }),
prevPage: () => table.dispatch({ type: 'setPage', page: pageInfo.page - 1 }),
setPageSize: (pageSize: number) => table.dispatch({ type: 'setPageSize', pageSize }),
getPageInfo: (): PageInfo => ({ ...pageInfo }),
};
},
};
}
@@ -0,0 +1,34 @@
import type { Action, TableInstance, TablePlugin, TableState } from '../types.js';
/*** 行选择插件:一律存 rowKey 而非索引,跨页、排序、异步刷新都不丢失 ***/
export function selectionPlugin<T = unknown>(): TablePlugin<T> {
return {
name: 'selection',
patchState(state: TableState, action: Action): TableState {
switch (action.type) {
case 'setSelection':
return { ...state, selection: [...action.keys] }; // 整体替换语义
case 'toggleSelect': {
const has = state.selection.includes(action.key);
return {
...state,
selection: has
? state.selection.filter((k) => k !== action.key)
: [...state.selection, action.key],
};
}
case 'clearSelection':
return state.selection.length ? { ...state, selection: [] } : state;
default:
return state;
}
},
api(table: TableInstance<T>) {
return {
selectAllVisible: () => {
table.dispatch({ type: 'setSelection', keys: [...table.getVisibleRows().keys] });
},
};
},
};
}
@@ -0,0 +1,81 @@
import type {
Action,
DeriveContext,
RowModel,
SortRule,
TablePlugin,
TableState,
} from '../types.js';
/*** 值比较:数值按大小、其余按本地化字符串序,null 恒排末尾 ***/
function compareValues(a: unknown, b: unknown): number {
if (a == null && b == null) return 0;
if (a == null) return 1;
if (b == null) return -1;
if (typeof a === 'number' && typeof b === 'number') return a - b;
return String(a).localeCompare(String(b), undefined, {
numeric: true,
sensitivity: 'base',
});
}
/*** 排序插件入参 ***/
export interface SortPluginOptions {
initialDir?: 'asc' | 'desc'; // 首次点击列头时的排序方向,默认升序
}
/*** 多列排序插件:点击循环 asc → desc → 取消;按(输入模型, sorting 切片)memo,翻页不重排 ***/
export function sortPlugin<T = unknown>(
opts: SortPluginOptions = {},
): TablePlugin<T> {
const initialDir = opts.initialDir ?? 'asc';
let lastInput: RowModel<T> | null = null;
let lastRules: SortRule[] | null = null;
let cached: RowModel<T> | null = null;
return {
name: 'sort',
patchState(state: TableState, action: Action): TableState {
switch (action.type) {
case 'setSorting':
return { ...state, sorting: action.sorting };
case 'toggleSort': {
const rules = action.multi ? [...state.sorting] : []; // 非 multi 直接单列重置
const idx = rules.findIndex((r) => r.key === action.key);
if (idx === -1) rules.push({ key: action.key, dir: initialDir });
else if (rules[idx].dir === 'asc') rules[idx] = { key: action.key, dir: 'desc' };
else rules.splice(idx, 1); // desc 再点即取消
return { ...state, sorting: rules };
}
default:
return state;
}
},
derive(model: RowModel<T>, state: TableState, ctx: DeriveContext<T>): RowModel<T> {
if (model === lastInput && state.sorting === lastRules && cached) return cached;
const rules = state.sorting;
let rows = model.rows;
let keys = model.keys;
if (rules.length > 0) {
const order = rows.map((_, i) => i); // 只排索引,避免行对象来回搬
order.sort((a, b) => {
for (const rule of rules) {
const cmp = compareValues(
ctx.getCellValue(rows[a], rule.key),
ctx.getCellValue(rows[b], rule.key),
);
if (cmp !== 0) return rule.dir === 'asc' ? cmp : -cmp;
}
return 0;
});
rows = order.map((i) => model.rows[i]);
keys = order.map((i) => model.keys[i]);
}
cached = { rows, keys, total: model.total };
lastInput = model;
lastRules = state.sorting;
return cached;
},
};
}