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
+75
View File
@@ -0,0 +1,75 @@
/**
* 演示站打包:esbuild 从 TS 源码打包(alias 直连各包 src),
* 连同 tokens.css / ui.css 内嵌进单文件 apps/demo/dist/demo.html。
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, '..', '..'); // apps/demo 上两级 = 仓库根
// esbuild 装在托管 node workspace,用 createRequire 指过去
const require = createRequire('C:/Users/shawn/.workbuddy/binaries/node/workspace/index.js');
const esbuild = require('esbuild');
const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');
const result = await esbuild.build({
entryPoints: [path.join(root, 'apps/demo/main.ts')],
bundle: true,
format: 'iife',
target: 'es2020',
charset: 'utf8', // 中文保留原文,不转义为 \uXXXX
write: false,
legalComments: 'none',
alias: {
'@ui-arch/frame': path.join(root, 'packages/core/frame/src/index.ts'),
'@ui-arch/ui': path.join(root, 'packages/skin/ui/src/index.ts'),
'@ui-arch/table-core': path.join(root, 'packages/core/table-core/src/index.ts'),
},
});
const js = result.outputFiles[0].text.replace(/<\/script>/gi, '<\\/script>'); // 防提前闭合
const css = [read('packages/design/tokens/tokens.css'), read('packages/skin/ui/ui.css')].join('\n');
const html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@ui-arch 组件库 · 现场演示</title>
<style>
${css}
:root{--page-bg:#ffffff;--page-text:#1c1e21;--page-muted:#6b7280;--page-border:rgba(0,0,0,.14)}
@media (prefers-color-scheme:dark){:root{--page-bg:#101216;--page-text:#e7e9ec;--page-muted:#888780;--page-border:rgba(255,255,255,.15)}}
*{box-sizing:border-box}
body{margin:0;background:var(--page-bg);color:var(--page-text);font:14px/1.7 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif}
.page{max-width:880px;margin:0 auto;padding:36px 20px 80px}
header{margin-bottom:30px}
h1{font-size:20px;font-weight:500;margin:0}
.sub{color:var(--page-muted);font-size:13px;margin:8px 0 0}
.sec{margin-bottom:40px}
h2{font-size:16px;font-weight:500;margin:0 0 4px}
.desc{color:var(--page-muted);font-size:13px;margin:0 0 14px}
.stage{display:flex;flex-wrap:wrap;gap:12px;align-items:center;border:1px dashed var(--page-border);border-radius:12px;padding:18px 20px}
.row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.stage .ua-table,.stage .ua-card{width:100%}
.out{color:var(--ua-color-accent,#185fa5);font-size:13px}
footer{color:var(--page-muted);font-size:12px;text-align:center;margin-top:50px}
</style>
</head>
<body>
<div id="app"></div>
<footer>由 @ui-arch/frame + @ui-arch/ui 驱动 · 构建产物单文件,无运行时依赖</footer>
<script>
${js}
</script>
</body>
</html>
`;
const outDir = path.join(root, 'apps/demo/dist');
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, 'demo.html'), html);
console.log(`demo built -> apps/demo/dist/demo.html (${Math.round(html.length / 1024)} KB)`);
+164
View File
@@ -0,0 +1,164 @@
import { h, mount, signal } from '@ui-arch/frame';
import {
alert,
badge,
button,
card,
checkbox,
dataTable,
form,
formItem,
modal,
progress,
radioGroup,
select,
spinner,
switchButton,
tabs,
textarea,
textField,
} from '@ui-arch/ui';
/*** 生成演示区块;id 供文档站 iframe 锚点定位 ***/
function section(id: string, title: string, desc: string, ...body: unknown[]): HTMLElement {
return h(
'section',
{ class: 'sec', id },
h('h2', null, title),
h('p', { class: 'desc' }, desc),
h('div', { class: 'stage' }, ...body),
);
}
// —— 演示数据 ——
const cities = ['北京', '上海', '深圳', '杭州'];
const users = [
'Alice', 'Bob', 'Charlie', 'Dave', 'Eve', 'Frank', 'Grace', 'Hank',
'Ivy', 'Jack', 'Kate', 'Liam', 'Mia', 'Noah', 'Olivia', 'Paul',
'Quinn', 'Rose', 'Sam', 'Tina', 'Uma', 'Victor', 'Wendy', 'Xavier',
].map((name, i) => ({ id: i + 1, name, age: 22 + ((i * 7) % 20), city: cities[i % cities.length] }));
// —— 响应式状态演示 ——
const count = signal(0);
// —— 双向绑定演示 ——
const name = signal('');
// —— 表单控件状态集中声明 ——
const agreed = signal(false);
const nightMode = signal(true);
const city = signal('');
const size = signal('m');
const bio = signal('');
// —— 反馈组件状态 ——
const percent = signal(0);
setInterval(() => percent((percent() + 5) % 105), 400); // 自动推进演示进度
// —— 标签页 / 弹窗状态 ——
const activeTab = signal('a');
const modalOpen = signal(false);
// —— 表单校验状态集中声明 ——
const username = signal('');
const email = signal('');
const formResult = signal('');
const app = h(
'div',
{ class: 'page' },
h(
'header',
null,
h('h1', null, '@ui-arch 组件库 · 现场演示'),
h('p', { class: 'sub' }, '驱动:@ui-arch/frame(自研微框架) + @ui-arch/ui(组件库) + @ui-arch/table-core(表格内核)'),
),
section(
'sec-counter',
'响应式状态',
'signal 驱动:按钮修改信号,文本自动更新,无虚拟 DOM。',
button({ label: 'count +1', onClick: () => count(count() + 1) }),
button({ label: '重置', variant: 'ghost', onClick: () => count(0) }),
h('span', { class: 'out' }, () => `count = ${count()}`),
),
section(
'sec-bind',
'双向绑定',
'textField 与信号双向同步:输入即回显。',
textField({ placeholder: '输入名字…', value: name }),
h('span', { class: 'out' }, () => (name() ? `你好,${name()}!` : '等待输入…')),
),
section(
'sec-form',
'表单控件',
'checkbox / switch / select / radioGroup / textarea:状态一律由调用方信号持有。',
h('div', { class: 'row' }, checkbox({ label: '同意条款', checked: agreed }), h('span', { class: 'out' }, () => (agreed() ? '已同意' : '未同意'))),
h('div', { class: 'row' }, switchButton({ label: '夜间模式', checked: nightMode }), h('span', { class: 'out' }, () => (nightMode() ? '开' : '关'))),
h('div', { class: 'row' }, select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '选择城市…' }), h('span', { class: 'out' }, () => (city() ? `城市:${city()}` : '未选择'))),
h('div', { class: 'row' }, radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }, { value: 'l', label: '大' }], value: size })),
textarea({ placeholder: '一句话介绍…', value: bio, rows: 2 }),
h('span', { class: 'out' }, () => `简介 ${bio().length} 字`),
),
section(
'sec-feedback',
'反馈与提示',
'badge / alert / spinner / progress:语义色全部来自令牌,暗色自动切换。',
badge({ label: '默认' }),
badge({ label: '成功', variant: 'success' }),
badge({ label: '警告', variant: 'warning' }),
badge({ label: '危险', variant: 'danger' }),
alert({ message: '信息提示:这是一条 info 提示条。' }),
alert({ message: '警告提示:操作可能存在风险。', variant: 'warning' }),
h('div', { class: 'row' }, spinner(), spinner({ size: 26 })),
progress({ value: percent }),
),
section(
'sec-formlab',
'表单校验',
'form + formItem:必填 / 长度 / 正则校验,全部通过才触发提交;失焦校验、输入清错。',
form(
{ submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()}`) },
[
formItem({ label: '用户名', required: true, value: username, rules: [{ min: 2, max: 12, message: '用户名需 2-12 个字符' }] }, textField({ placeholder: '2-12 个字符', value: username })),
formItem({ label: '邮箱', required: true, value: email, rules: [{ pattern: /^[\w.-]+@[\w-]+\.[\w.]+$/, message: '邮箱格式不正确' }] }, textField({ placeholder: 'you@example.com', value: email })),
],
),
h('span', { class: 'out' }, () => formResult() || '等待提交…'),
),
section(
'sec-tabs-modal',
'标签页与弹窗',
'tabs 滑动指示条 + 方向键切换(←→),禁用页签置灰;modal 支持 Esc / 遮罩 / × 关闭。',
tabs({ tabs: [{ value: 'a', label: '概览' }, { value: 'b', label: '明细' }, { value: 'c', label: '设置' }, { value: 'd', label: '已停用', disabled: true }], active: activeTab }),
h('span', { class: 'out' }, () => `当前页签:${activeTab()}`),
button({ label: '打开弹窗', onClick: () => modalOpen(true) }),
modal(
{ title: '确认操作', open: modalOpen, onClose: () => modalOpen(false) },
h('p', null, '这是一个由信号驱动的弹窗组件。'),
h('div', { class: 'row' }, button({ label: '知道了', onClick: () => modalOpen(false) }), badge({ label: 'modal', variant: 'success' })),
),
),
section(
'sec-table',
'数据表格',
'粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头排序、工具栏筛选、底部分页。',
card({ title: '用户表' }, dataTable({
data: users,
selectable: true,
columns: [
{ key: 'name', header: '姓名', sortable: true },
{ key: 'age', header: '年龄', sortable: true },
{ key: 'city', header: '城市' },
],
pageSize: 5,
})),
),
section(
'sec-card',
'卡片容器',
'card 组件:标题 + 任意子内容,样式全部来自设计令牌。',
card({ title: '令牌驱动' }, h('p', { class: 'desc' }, '这里没有硬编码颜色 —— 全部消费 --ua-* CSS 变量,暗色模式自动切换。')),
),
);
mount('#app', () => app);
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@ui-arch/demo",
"version": "0.1.0",
"private": true,
"description": "Interactive demo app for the @ui-arch component library, bundled to a single HTML file",
"type": "module",
"scripts": {
"build": "node build.mjs",
"typecheck": "tsc -p tsconfig.json"
},
"dependencies": {
"@ui-arch/frame": "workspace:*",
"@ui-arch/ui": "workspace:*"
},
"license": "MIT"
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"paths": {
"@ui-arch/frame": ["../../packages/core/frame/src/index.ts"],
"@ui-arch/ui": ["../../packages/skin/ui/src/index.ts"],
"@ui-arch/table-core": ["../../packages/core/table-core/src/index.ts"]
}
},
"include": ["."]
}