Compare commits

..

1 Commits
v1.0.1 ... main

Author SHA1 Message Date
Star
0f385f32a8 docs(ui): 建立 AI 按需文档入口(by AI) 2026-07-18 11:44:16 +08:00
27 changed files with 356 additions and 194 deletions

48
AI.yaml
View File

@ -1,48 +0,0 @@
package: apigo.cc/web/ui
rules:
- Read only documents required by the task.
- Preserve declarative bindings when extending a component.
- Do not change framework internals unless framework changes are requested.
- Add or update a regression test for every public behavior change.
- Before declaring a public API complete, compare its YAML attributes, properties, methods, events, slots, state fields, options, and rules against named test assertions and examples. Every documented item needs coverage; examples must visibly demonstrate the core behavior declaratively.
- In an ordered value list, the first value is the default.
- Treat each component test as a concise, declarative usage sample: prepare immutable top-level `const` data before DOM parsing, then bind it directly (for example `$.state.list="users"`). Do not add State or window bridge assignments unless shared mutable state is required.
- Prefer framework directives ($if, $bind, $onclick, slots) and Bootstrap classes over imperative page setup or custom CSS. Keep test actions limited to assertions and interaction verification.
- Test pages use full-height flex layouts: the component area fills remaining space and result output is a bounded, internally scrollable region.
- The outer test console keeps the selected test active and represents pass/fail with a visible status badge; detailed output belongs inside the selected test page.
- Public source and YAML use the same PascalCase basename (for example `components/base/List.js` and `components/base/List.yaml`, `frameworks/State.js` and `frameworks/State.yaml`).
loading:
example: '<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/ui@1.0.0/ui.js" components="base,form"></script>'
rules:
- Load ui.js in head without async, defer, or type="module".
- Always write components with every public component used by the page.
- Use `base` or `form` to load the documented component category; individual names and category names can be mixed.
- Do not list utilities or framework dependencies.
- ui.js loads frameworks/State.js and frameworks/Bootstrap.js beside itself.
- npm run build writes source modification versions into ui.js; ui.min.js loads matching adjacent .min.js files.
framework:
- frameworks/State.yaml
- frameworks/Bootstrap.yaml
components:
- components/base/API.yaml
- components/base/AutoForm.yaml
- components/base/List.yaml
- components/base/Modal.yaml
- components/base/Dialog.yaml
- components/base/Toast.yaml
- components/base/Nav.yaml
- components/base/Resizer.yaml
- components/form/DatePicker.yaml
- components/form/ColorPicker.yaml
- components/form/IconPicker.yaml
- components/form/TagsInput.yaml
- components/data/DataGrid.yaml
- components/data/Chart.yaml
utilities:
- utilities/HTTP.yaml
- utilities/VirtualScroll.yaml

View File

@ -16,4 +16,4 @@ Load `ui.js` in `<head>` as a normal script. Do not use `async`, `defer`, or `ty
`npm run build` does not bundle source files or create a `dist` directory. It first copies `../state/dist/state.js` and `../bootstrap/dist/bootstrap.js` into `frameworks/`, then writes their source-file modification-time cache versions into `ui.js` and produces adjacent `.min.js` files. Loading `ui.min.js` automatically loads the matching minified framework, utility, and component files. The UI package version remains independent of its framework packages. `npm run build` does not bundle source files or create a `dist` directory. It first copies `../state/dist/state.js` and `../bootstrap/dist/bootstrap.js` into `frameworks/`, then writes their source-file modification-time cache versions into `ui.js` and produces adjacent `.min.js` files. Loading `ui.min.js` automatically loads the matching minified framework, utility, and component files. The UI package version remains independent of its framework packages.
For AI-oriented API documentation, start with [AI.yaml](AI.yaml). For AI-assisted development, give the agent this single entry URL: [https://apigo.cc/web/ui/ai/llms.txt](https://apigo.cc/web/ui/ai/llms.txt). It is a task-oriented index: the agent reads it first, then opens only the linked framework, utility, and component documents required by the current task. The repository copy is [llms.txt](llms.txt).

View File

@ -12,6 +12,6 @@
必须把脚本放在 `<head>`,并使用普通 `<script>`。不要使用 `async``defer``type="module"`,这样 state 才能在 DOMContentLoaded 前完成组件注册,并对完整页面执行首次扫描。 必须把脚本放在 `<head>`,并使用普通 `<script>`。不要使用 `async``defer``type="module"`,这样 state 才能在 DOMContentLoaded 前完成组件注册,并对完整页面执行首次扫描。
AI 使用说明从 [AI.yaml](AI.yaml) 开始;文档与源码同目录,组件示例直接写在对应 YAML 中。 AI 辅助开发时,只需把统一入口 [https://apigo.cc/web/ui/ai/llms.txt](https://apigo.cc/web/ui/ai/llms.txt) 交给 AI。它会先读取这份按任务加载的索引再只读取当前任务需要的 framework、utility 和 component YAML。仓库内入口文件为 [llms.txt](llms.txt);文档与源码同目录,组件示例直接写在对应 YAML 中。
`npm run build` 不会打包源码,也不会生成 `dist`。它会先将 `../state/dist/state.js``../bootstrap/dist/bootstrap.js` 复制到 `frameworks/`,再把所有源码文件的最后修改时间缓存版本写入 `ui.js`,并在源码旁生成 `.min.js`。加载 `ui.min.js` 时,入口会自动改为加载对应的 framework、utility 和 component 的 `.min.js`。UI 包的版本独立于前置框架包。 `npm run build` 不会打包源码,也不会生成 `dist`。它会先将 `../state/dist/state.js``../bootstrap/dist/bootstrap.js` 复制到 `frameworks/`,再把所有源码文件的最后修改时间缓存版本写入 `ui.js`,并在源码旁生成 `.min.js`。加载 `ui.min.js` 时,入口会自动改为加载对应的 framework、utility 和 component 的 `.min.js`。UI 包的版本独立于前置框架包。

View File

@ -1,4 +1,5 @@
name: API name: API
purpose: Bind HTTP request configuration and response state to a declarative page element.
attributes: attributes:
auto: auto:
@ -46,12 +47,16 @@ examples:
<List $.state.list="usersApi.result.list"></List> <List $.state.list="usersApi.result.list"></List>
do: | do: |
<script> <script>
const loadUsers = async () => {
const response = await usersApi.do({ url: '/api/users', method: 'GET', noui: true }) const response = await usersApi.do({ url: '/api/users', method: 'GET', noui: true })
if (response.ok) console.log(response.result) if (response.ok) console.log(response.result)
}
</script>
<API id="usersApi"></API>
<button $onclick="loadUsers()">Load users</button>
post_default: | post_default: |
<API auto .request.url="/admin/table" .request.data.action="tables"></API> <API auto .request.url="/admin/table" .request.data.action="tables"></API>
<!-- data is present, so this request uses POST unless method is explicitly set --> <!-- data is present, so this request uses POST unless method is explicitly set -->
</script>
tests: tests:
- API.test.html - API.test.html

View File

@ -46,8 +46,10 @@ examples:
</script> </script>
<AutoForm $.state.data="profile" $.state.schema="profileSchema"></AutoForm> <AutoForm $.state.data="profile" $.state.schema="profileSchema"></AutoForm>
database_fields: | database_fields: |
<script>
const fields = [{ id: 'f-role', tableID: 'users', name: 'Role', type: 'v30', settings: { type: 'select', options: ['Admin', 'User'] } }] const fields = [{ id: 'f-role', tableID: 'users', name: 'Role', type: 'v30', settings: { type: 'select', options: ['Admin', 'User'] } }]
const user = { Role: 'Admin' } const user = { Role: 'Admin' }
</script>
<AutoForm $.state.data="user" $.state.schema="fields"></AutoForm> <AutoForm $.state.data="user" $.state.schema="fields"></AutoForm>
submit_api: | submit_api: |
<script> <script>
@ -62,11 +64,11 @@ examples:
</AutoForm> </AutoForm>
related: related:
- ../API.yaml - API.yaml
- DatePicker.yaml - ../form/DatePicker.yaml
- ColorPicker.yaml - ../form/ColorPicker.yaml
- IconPicker.yaml - ../form/IconPicker.yaml
- TagsInput.yaml - ../form/TagsInput.yaml
tests: tests:
- AutoForm.test.html - AutoForm.test.html

View File

@ -17,5 +17,5 @@ examples:
alert: "Dialog.alert('Saved successfully')" alert: "Dialog.alert('Saved successfully')"
confirm: "Dialog.confirm('Delete this record?').then(ok => { if (ok) remove() })" confirm: "Dialog.confirm('Delete this record?').then(ok => { if (ok) remove() })"
example: | tests:
Dialog.confirm('Delete this record?').then(ok => { if (ok) remove() }) - Dialog.test.html

View File

@ -1,7 +1,8 @@
name: List name: List
purpose: Render flat, grouped, tree, or virtualized item lists with declarative selection and slots.
attributes: attributes:
mode: [normal|group|tree] mode: [normal, group, tree]
fast: "boolean — virtual scrolling" fast: "boolean — virtual scrolling"
collapsible: "boolean — collapsible tree nodes" collapsible: "boolean — collapsible tree nodes"
auto-select: "boolean — clicked item id -> selectedItem" auto-select: "boolean — clicked item id -> selectedItem"

View File

@ -1,4 +1,5 @@
name: Modal name: Modal
purpose: Present slotted content in a controllable Bootstrap modal.
binding: binding:
behavior: Bind a boolean with $bind to show or hide the modal. behavior: Bind a boolean with $bind to show or hide the modal.
@ -23,9 +24,13 @@ slots:
body: Replace the modal body. body: Replace the modal body.
footer: Replace the modal footer. footer: Replace the modal footer.
example: | examples:
basic: |
<Modal id="editModal" title="Edit user"> <Modal id="editModal" title="Edit user">
<div slot="body"><input id="nameInput"></div> <div slot="body"><input id="nameInput"></div>
<div slot="footer"><button $onclick="editModal.hide()">Close</button></div> <div slot="footer"><button $onclick="editModal.hide()">Close</button></div>
</Modal> </Modal>
<button $onclick="editModal.show()">Edit</button> <button $onclick="editModal.show()">Edit</button>
tests:
- Modal.test.html

View File

@ -1,4 +1,5 @@
name: Nav name: Nav
purpose: Render horizontal or vertical navigation from declarative brand and item data.
attributes: attributes:
vertical: vertical:
@ -24,7 +25,8 @@ events:
item_shape: "{ type, name?, label?, icon?, items?, bind?, class?, width? }" item_shape: "{ type, name?, label?, icon?, items?, bind?, class?, width? }"
example: | examples:
basic: |
<script> <script>
const navBrand = { image: '/res/apiGo.min.svg' } const navBrand = { image: '/res/apiGo.min.svg' }
const navItems = [ const navItems = [
@ -44,3 +46,6 @@ rules:
- Bind `$bind="Hash.nav"` when the selected navigation item must survive a page refresh. - Bind `$bind="Hash.nav"` when the selected navigation item must survive a page refresh.
- Use nav for actions such as Logout: `$onnav="if(event.detail.name==='logout') logout()"`. Use change only for selected-page state. - Use nav for actions such as Logout: `$onnav="if(event.detail.name==='logout') logout()"`. Use change only for selected-page state.
- Use the same `navItems` data for horizontal and vertical navigation when comparing layouts. - Use the same `navItems` data for horizontal and vertical navigation when comparing layouts.
tests:
- Nav.test.html

View File

@ -1,4 +1,5 @@
name: Resizer name: Resizer
purpose: Resize a sibling or assigned target element by dragging a horizontal or vertical handle.
attributes: attributes:
vertical: Resize height instead of width. vertical: Resize height instead of width.
@ -22,7 +23,8 @@ events:
rules: rules:
- MouseMover is an internal dependency and is loaded automatically. - MouseMover is an internal dependency and is loaded automatically.
example: | examples:
horizontal_and_vertical: |
<div id="sidebar" style="width:240px"></div> <div id="sidebar" style="width:240px"></div>
<Resizer min="160" max="480" $bind="State.sidebarWidth" $onchange="State.sidebarWidth = event.detail"></Resizer> <Resizer min="160" max="480" $bind="State.sidebarWidth" $onchange="State.sidebarWidth = event.detail"></Resizer>

View File

@ -1,4 +1,5 @@
name: Toast name: Toast
purpose: Show transient notifications and lightweight asynchronous confirmations.
methods: methods:
show: 'Toast.show(message, options) -> Promise<Toast element>' show: 'Toast.show(message, options) -> Promise<Toast element>'

View File

@ -1,42 +0,0 @@
name: Chart
constructor:
signature: new Chart(canvas, options)
options:
type: line, bar, or pie. Default is line.
data: Chart.js data object or source object array.
options: Chart.js options object.
map: "{ labels, values, label } for source object arrays."
component:
attributes:
type: line, bar, or pie.
state:
data: Chart.js data object or source object array.
options: Chart.js options.
map: "{ labels, values, label }"
property:
chartInstance: Underlying Chart instance.
methods:
update: Update optional data and redraw.
destroy: Destroy the underlying Chart.js instance.
rules:
- Give the Chart container an explicit height.
- Use map only when data is an array of objects.
- Chart destroys its Chart.js instance when the component unloads.
example: |
<Chart id="sales" type="bar"></Chart>
<script>
const chart = document.querySelector('#sales')
chart.state.data = [
{ month: 'Jan', amount: 12 },
{ month: 'Feb', amount: 18 }
]
chart.state.map = { labels: 'month', values: 'amount' }
</script>
tests:
- apigo.cc/web/chart/test/all.spec.js

View File

@ -1,4 +1,5 @@
name: DataGrid name: DataGrid
purpose: Manage editable, virtualized data-API tables while preserving server field definitions.
attributes: attributes:
editable: editable:
@ -8,8 +9,6 @@ attributes:
type: string type: string
behavior: POST endpoint for the data-table action protocol. Requires state.table. behavior: POST endpoint for the data-table action protocol. Requires state.table.
purpose: Editable data-API table manager. It consumes the field definitions returned by the data API and keeps them unchanged for save events.
state: state:
table: Table name sent as request.name when api is configured. table: Table name sent as request.name when api is configured.
fields: Array of data-API field definitions; assign before list. fields: Array of data-API field definitions; assign before list.
@ -98,7 +97,8 @@ rules:
- Treat underscore-prefixed state fields as internal read-only diagnostics. - Treat underscore-prefixed state fields as internal read-only diagnostics.
- Custom field editors must be registered in AutoForm before use. - Custom field editors must be registered in AutoForm before use.
example: | examples:
local_and_api: |
<script> <script>
const orderFields = [ const orderFields = [
{ id: 'f-name', tableID: 'orders', name: 'name', type: 'v100', isIndex: true, memo: '', settings: { type: 'text' } }, { id: 'f-name', tableID: 'orders', name: 'name', type: 'v100', isIndex: true, memo: '', settings: { type: 'text' } },

View File

@ -25,4 +25,7 @@ rules:
- Use $bind for a standalone reactive value; use AutoForm schema type ColorPicker for generated forms. - Use $bind for a standalone reactive value; use AutoForm schema type ColorPicker for generated forms.
related: related:
- ../AutoForm.yaml - ../base/AutoForm.yaml
tests:
- ColorPicker.test.html

View File

@ -35,4 +35,7 @@ rules:
- In AutoForm, declare setting.rangeEnd on the start field; the end field does not need its own schema item. - In AutoForm, declare setting.rangeEnd on the start field; the end field does not need its own schema item.
related: related:
- ../AutoForm.yaml - ../base/AutoForm.yaml
tests:
- DatePicker.test.html

View File

@ -4,7 +4,8 @@ purpose: Search and choose a Bootstrap Icons name.
properties: properties:
value: string; selected icon name without the 'bi-' prefix value: string; selected icon name without the 'bi-' prefix
example: | examples:
standalone_and_form: |
<script> <script>
const selectedIcon = 'house' const selectedIcon = 'house'
const menu = { icon: 'heart' } const menu = { icon: 'heart' }
@ -18,4 +19,7 @@ events:
change: detail is the selected icon name change: detail is the selected icon name
related: related:
- ../AutoForm.yaml - ../base/AutoForm.yaml
tests:
- IconPicker.test.html

View File

@ -3,10 +3,12 @@ purpose: Enter a list of unique string tags; Enter, comma, or space commits a ta
attributes: attributes:
placeholder: string; input placeholder (the built-in default is localized) placeholder: string; input placeholder (the built-in default is localized)
properties: properties:
value: array of strings; current unique tags value: array of strings; current unique tags
example: | examples:
standalone_and_form: |
<script> <script>
const tags = ['admin', 'editor'] const tags = ['admin', 'editor']
const user = { tags: ['admin', 'editor'] } const user = { tags: ['admin', 'editor'] }
@ -20,4 +22,7 @@ events:
change: detail is the complete updated string array change: detail is the complete updated string array
related: related:
- ../AutoForm.yaml - ../base/AutoForm.yaml
tests:
- TagsInput.test.html

View File

@ -1,4 +1,5 @@
name: Bootstrap name: Bootstrap
purpose: Provide Bootstrap 5 styles, icons, JavaScript APIs, and reactive theme configuration.
provided: provided:
css: 'Bootstrap.js injects Bootstrap 5 CSS and Bootstrap Icons; do not add another Bootstrap stylesheet or runtime.' css: 'Bootstrap.js injects Bootstrap 5 CSS and Bootstrap Icons; do not add another Bootstrap stylesheet or runtime.'
@ -15,10 +16,14 @@ rules:
- Use Bootstrap classes and Icons directly in component markup. - Use Bootstrap classes and Icons directly in component markup.
- Do not load a second Bootstrap CSS file or runtime. - Do not load a second Bootstrap CSS file or runtime.
examples: | examples:
theme: |
<script>
Bootstrap.config({ primary: '#6f42c1' }) Bootstrap.config({ primary: '#6f42c1' })
Bootstrap.config({ bindDarkMode: [LocalStorage, 'darkMode'] }) Bootstrap.config({ bindDarkMode: [LocalStorage, 'darkMode'] })
bootstrap.Modal.getOrCreateInstance(document.querySelector('#settings')).show() </script>
native_api: |
<button $onclick="bootstrap.Modal.getOrCreateInstance(document.querySelector('#settings')).show()">Settings</button>
tests: tests:
- apigo.cc/web/bootstrap/test/ - https://apigo.cc/web/bootstrap/raw/branch/main/test/bootstrap.spec.js

View File

@ -1,4 +1,5 @@
name: State name: State
purpose: Provide reactive state, declarative DOM directives, and the custom component runtime.
globals: globals:
State: 'Reactive in-memory application state.' State: 'Reactive in-memory application state.'
@ -48,19 +49,22 @@ component:
- Keep required public behavior declarative; do not expose internal DOM as an application contract. - Keep required public behavior declarative; do not expose internal DOM as an application contract.
- A bindable component listens for bind and writes event.detail into its state. - A bindable component listens for bind and writes event.detail into its state.
examples: | examples:
bindings: |
<script> <script>
const users = [{ id: 'ada', name: 'Ada' }] const users = [{ id: 'ada', name: 'Ada' }]
</script> </script>
<div $text>Hello ${users[0].name}</div> <div $text>Hello ${users[0].name}</div>
<List $.state.list="users"></List> <List $.state.list="users"></List>
<button $onclick="State.dark = !State.dark">Toggle</button> <button $onclick="State.dark = !State.dark">Toggle</button>
component: |
<script>
Component.register('Counter', container => { Component.register('Counter', container => {
container.state.value = 0 container.state.value = 0
container.addEventListener('bind', event => { container.state.value = event.detail }) container.addEventListener('bind', event => { container.state.value = event.detail })
}, Util.makeDom('<button $text="this.state.value" $onclick="this.state.value++"></button>')) }, Util.makeDom('<button $text="this.state.value" $onclick="this.state.value++"></button>'))
<!-- <Counter $bind="State.count"></Counter> --> </script>
<Counter $bind="State.count"></Counter>
tests: tests:
- apigo.cc/web/state/test/ - https://apigo.cc/web/state/raw/branch/main/test/all.spec.js

91
llms.txt Normal file
View File

@ -0,0 +1,91 @@
# @apigo.cc/ui
> Build-free native JavaScript UI framework. This file is the authoritative AI entry point and documentation index.
Version: 1.0.1
Choose one version-pinned npm CDN. `ui.js` loads all adjacent framework, utility, and component files from the same origin.
- jsDelivr (primary): `https://cdn.jsdelivr.net/npm/@apigo.cc/ui@1.0.1/ui.js`
- UNPKG (alternative): `https://unpkg.com/@apigo.cc/ui@1.0.1/ui.js`
- JSDMirror (China-optimized alternative): `https://cdn.jsdmirror.com/npm/@apigo.cc/ui@1.0.1/ui.js`
Load the chosen URL in `<head>` without `async`, `defer`, or `type="module"`. A minimal complete page is:
```html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/ui@1.0.1/ui.js" components="List"></script>
<script>
const users = [{ id: 'ada', label: 'Ada', summary: 'Admin' }]
</script>
</head>
<body>
<List $.state.list="users"></List>
</body>
</html>
```
## Retrieval rules
- Read this file first, then open only the framework, utility, and component documents required by the current task.
- Resolve every relative link from the URL of this file. Linked YAML files are authoritative API contracts.
- Do not preload every linked document. Follow `related` and `tests` references only when they are relevant to the task.
- Preserve declarative bindings when extending a component.
- Do not change framework internals unless framework changes are requested.
- Add or update a regression test for every public behavior change.
- Before declaring a public API complete, compare its YAML attributes, properties, methods, events, slots, state fields, options, and rules against named test assertions and examples. Every documented item needs coverage; examples must visibly demonstrate the core behavior declaratively.
- In an ordered value list, the first value is the default.
- Treat each component test as a concise declarative usage sample: prepare immutable top-level `const` data before DOM parsing, then bind it directly, for example `$.state.list="users"`. Add State or window bridge assignments only when shared mutable state is required.
- Prefer framework directives (`$if`, `$bind`, `$onclick`, slots) and Bootstrap classes over imperative page setup or custom CSS. Keep test actions limited to assertions and interaction verification.
- Test pages use full-height flex layouts: the component area fills remaining space and result output is a bounded, internally scrollable region.
- The outer test console keeps the selected test active and represents pass/fail with a visible status badge; detailed output belongs inside the selected test page.
- Public source and YAML use the same PascalCase basename, for example `components/base/List.js` and `components/base/List.yaml`.
## Loading rules
- Always write the `components` attribute with every public component used by the page.
- Use `base`, `form`, or `data` to load a documented component category; individual names and category names can be mixed.
- Do not list utilities or framework dependencies.
- `ui.js` loads `frameworks/State.js` and `frameworks/Bootstrap.js` beside itself.
- `npm run build` writes source modification versions into `ui.js`; `ui.min.js` loads matching adjacent `.min.js` files.
## Framework
- [State and declarative directives](frameworks/State.yaml): reactive state, bindings, expressions, components, slots, and lifecycle.
- [Bootstrap integration](frameworks/Bootstrap.yaml): Bootstrap loading and framework integration rules.
## Base components
- [API](components/base/API.yaml): declarative HTTP API element.
- [AutoForm](components/base/AutoForm.yaml): schema-driven forms and submission.
- [List](components/base/List.yaml): declarative list rendering and selection.
- [Modal](components/base/Modal.yaml): modal container.
- [Dialog](components/base/Dialog.yaml): message and confirmation dialogs.
- [Toast](components/base/Toast.yaml): transient notifications.
- [Nav](components/base/Nav.yaml): declarative navigation.
- [Resizer](components/base/Resizer.yaml): resizable layout regions.
## Form components
- [DatePicker](components/form/DatePicker.yaml): date and date-range input.
- [ColorPicker](components/form/ColorPicker.yaml): color input.
- [IconPicker](components/form/IconPicker.yaml): icon selection.
- [TagsInput](components/form/TagsInput.yaml): editable tag collections.
## Data components
- [DataGrid](components/data/DataGrid.yaml): virtualized data table.
## Utilities
- [HTTP](utilities/HTTP.yaml): HTTP requests and response handling.
- [VirtualScroll](utilities/VirtualScroll.yaml): virtual scrolling behavior.
## Optional
- [README](README.md): package loading and build overview.
- [Changelog](CHANGELOG.md): read only when upgrading or investigating version differences.

View File

@ -14,7 +14,7 @@
"utilities", "utilities",
"ui.js", "ui.js",
"ui.test.html", "ui.test.html",
"AI.yaml", "llms.txt",
"README.md", "README.md",
"README.zh-CN.md", "README.zh-CN.md",
"test", "test",

View File

@ -1,6 +1,7 @@
const { copyFile, readdir, readFile, stat, writeFile } = require('node:fs/promises'); const { copyFile, readdir, readFile, stat, writeFile } = require('node:fs/promises');
const { join, relative, sep } = require('node:path'); const { join, relative, sep } = require('node:path');
const terser = require('terser'); const terser = require('terser');
const { validateDocs } = require('./validate-docs.cjs');
const root = join(__dirname, '..'); const root = join(__dirname, '..');
const publicDirs = ['frameworks', 'utilities', 'components']; const publicDirs = ['frameworks', 'utilities', 'components'];
@ -20,6 +21,19 @@ const filesBelow = async directory => {
}; };
(async () => { (async () => {
const packageMetadata = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
const llmsFile = join(root, 'llms.txt');
const llmsSource = await readFile(llmsFile, 'utf8');
const versionLine = /^Version: \S+$/m;
const cdnVersion = /(@apigo\.cc\/ui@)[^/"\s]+(?=\/ui(?:\.min)?\.js)/g;
if (!versionLine.test(llmsSource) || !cdnVersion.test(llmsSource)) throw new Error('llms.txt version markers are missing');
cdnVersion.lastIndex = 0;
const builtLlms = llmsSource
.replace(versionLine, `Version: ${packageMetadata.version}`)
.replace(cdnVersion, `$1${packageMetadata.version}`);
await writeFile(llmsFile, builtLlms);
await validateDocs();
await Promise.all(Object.entries(frameworks).map(([name, source]) => copyFile(source, join(root, 'frameworks', `${name}.js`)))); await Promise.all(Object.entries(frameworks).map(([name, source]) => copyFile(source, join(root, 'frameworks', `${name}.js`))));
const sources = (await Promise.all(publicDirs.map(dir => filesBelow(join(root, dir))))).flat(); const sources = (await Promise.all(publicDirs.map(dir => filesBelow(join(root, dir))))).flat();
const sourceStats = await Promise.all(sources.map(async file => { const sourceStats = await Promise.all(sources.map(async file => {

96
tools/validate-docs.cjs Normal file
View File

@ -0,0 +1,96 @@
const { access, readFile, readdir } = require('node:fs/promises');
const { basename, dirname, join, relative, resolve, sep } = require('node:path');
const root = join(__dirname, '..');
const publicDirs = ['frameworks', 'utilities', 'components'];
const posixPath = file => relative(root, file).split(sep).join('/');
const filesBelow = async directory => {
const entries = await readdir(directory, { withFileTypes: true });
const files = await Promise.all(entries.map(entry => {
const file = join(directory, entry.name);
return entry.isDirectory() ? filesBelow(file) : [file];
}));
return files.flat();
};
const sectionItems = (source, section) => {
const lines = source.split('\n');
const start = lines.findIndex(line => line === `${section}:`);
if (start < 0) return [];
const items = [];
for (const line of lines.slice(start + 1)) {
const match = line.match(/^ - (.+)$/);
if (!match) break;
items.push(match[1]);
}
return items;
};
const validateDocs = async () => {
const errors = [];
const [packageMetadata, llms, uiSource] = await Promise.all([
readFile(join(root, 'package.json'), 'utf8').then(JSON.parse),
readFile(join(root, 'llms.txt'), 'utf8'),
readFile(join(root, 'ui.js'), 'utf8')
]);
const localLinks = [...llms.matchAll(/\]\(([^)]+)\)/g)]
.map(match => match[1])
.filter(link => !/^[a-z]+:/i.test(link));
for (const link of localLinks) {
const file = resolve(root, link.split(/[?#]/, 1)[0]);
try { await access(file); } catch (_) { errors.push(`llms.txt links to missing file: ${link}`); }
}
const documentedComponents = new Map(
localLinks
.filter(link => /^components\/.+\.yaml$/.test(link))
.map(link => [basename(link, '.yaml'), link])
);
const modulesBlock = uiSource.match(/const modules = \{([\s\S]*?)\n \};/)?.[1] || '';
const implementedComponents = new Map(
[...modulesBlock.matchAll(/^ (\w+): \{ path: '(components\/.+)\.js'/gm)]
.map(match => [match[1], `${match[2]}.yaml`])
);
for (const [name, file] of documentedComponents) {
if (implementedComponents.get(name) !== file) errors.push(`llms.txt documents component not registered by ui.js: ${name}`);
}
for (const [name, file] of implementedComponents) {
if (documentedComponents.get(name) !== file) errors.push(`ui.js registers component missing from llms.txt: ${name}`);
}
const yamlFiles = (await Promise.all(publicDirs.map(dir => filesBelow(join(root, dir)))))
.flat()
.filter(file => file.endsWith('.yaml'));
for (const file of yamlFiles) {
const source = await readFile(file, 'utf8');
const path = posixPath(file);
const name = source.match(/^name: (.+)$/m)?.[1];
if (!name) errors.push(`${path} is missing name`);
if (name && basename(file, '.yaml') !== name) errors.push(`${path} name must match its basename`);
if (!/^purpose: .+$/m.test(source)) errors.push(`${path} is missing purpose`);
if (!/^examples:/m.test(source)) errors.push(`${path} is missing examples`);
if (/^(example|property):/m.test(source)) errors.push(`${path} must use plural examples/properties`);
for (const section of ['related', 'tests']) {
for (const item of sectionItems(source, section)) {
if (/^[a-z]+:\/\//i.test(item)) continue;
const target = resolve(dirname(file), item);
try { await access(target); } catch (_) { errors.push(`${path} ${section} points to missing file: ${item}`); }
}
}
}
const versions = [...llms.matchAll(/@apigo\.cc\/ui@([^/`"\s]+)/g)].map(match => match[1]);
if (!versions.length || versions.some(version => version !== packageMetadata.version)) {
errors.push(`llms.txt CDN versions must all equal package.json version ${packageMetadata.version}`);
}
if (errors.length) throw new Error(`AI documentation validation failed:\n- ${errors.join('\n- ')}`);
console.log(`Validated AI documentation (${localLinks.length} links; ${yamlFiles.length} YAML files; ${documentedComponents.size} components)`);
};
if (require.main === module) validateDocs().catch(error => { console.error(error); process.exit(1); });
module.exports = { validateDocs };

2
ui.js
View File

@ -1,6 +1,6 @@
(() => { (() => {
// Updated by `npm run build`. Source files stay directly executable. // Updated by `npm run build`. Source files stay directly executable.
const build = { version: 'mrjbct5d', files: {"frameworks/Bootstrap.js":"mrjbct5d","frameworks/State.js":"mrjbct5d","utilities/HTTP.js":"mrg6t8zm","utilities/MouseMover.js":"mrg6t8zm","utilities/VirtualScroll.js":"mrg6t8zn","components/base/API.js":"mrj8lfwz","components/base/AutoForm.js":"mrj10yku","components/base/Dialog.js":"mrh7tjfb","components/base/List.js":"mrge46xl","components/base/Modal.js":"mrj1zcg3","components/base/Nav.js":"mrj6haqp","components/base/Resizer.js":"mrghfzrh","components/base/Toast.js":"mrghiycy","components/data/DataGrid.js":"mrj8lxpb","components/form/ColorPicker.js":"mrgn837c","components/form/DatePicker.js":"mrgn837c","components/form/IconPicker.js":"mrgoatz5","components/form/TagsInput.js":"mrgn837b"} }; const build = { version: 'mrpt6n5q', files: {"frameworks/Bootstrap.js":"mrpt6n5q","frameworks/State.js":"mrpt6n5p","utilities/HTTP.js":"mrg6t8zm","utilities/MouseMover.js":"mrg6t8zm","utilities/VirtualScroll.js":"mrg6t8zn","components/base/API.js":"mrj8lfwz","components/base/AutoForm.js":"mrj10yku","components/base/Dialog.js":"mrh7tjfb","components/base/List.js":"mrge46xl","components/base/Modal.js":"mrj1zcg3","components/base/Nav.js":"mrj6haqp","components/base/Resizer.js":"mrghfzrh","components/base/Toast.js":"mrghiycy","components/data/DataGrid.js":"mrj8lxpb","components/form/ColorPicker.js":"mrgn837c","components/form/DatePicker.js":"mrgn837c","components/form/IconPicker.js":"mrgoatz5","components/form/TagsInput.js":"mrgn837b"} };
const script = document.currentScript; const script = document.currentScript;
if (!script || document.readyState !== 'loading') { if (!script || document.readyState !== 'loading') {
throw new Error('ui.js must be loaded by a normal <script> while the document is parsing'); throw new Error('ui.js must be loaded by a normal <script> while the document is parsing');

2
ui.min.js vendored
View File

@ -1 +1 @@
(()=>{const o={version:"mrjbct5d",files:{"frameworks/Bootstrap.js":"mrjbct5d","frameworks/State.js":"mrjbct5d","utilities/HTTP.js":"mrg6t8zm","utilities/MouseMover.js":"mrg6t8zm","utilities/VirtualScroll.js":"mrg6t8zn","components/base/API.js":"mrj8lfwz","components/base/AutoForm.js":"mrj10yku","components/base/Dialog.js":"mrh7tjfb","components/base/List.js":"mrge46xl","components/base/Modal.js":"mrj1zcg3","components/base/Nav.js":"mrj6haqp","components/base/Resizer.js":"mrghfzrh","components/base/Toast.js":"mrghiycy","components/data/DataGrid.js":"mrj8lxpb","components/form/ColorPicker.js":"mrgn837c","components/form/DatePicker.js":"mrgn837c","components/form/IconPicker.js":"mrgoatz5","components/form/TagsInput.js":"mrgn837b"}},t=document.currentScript;if(!t||"loading"!==document.readyState)throw new Error("ui.js must be loaded by a normal <script> while the document is parsing");const e=(t?.getAttribute("components")||"").split(",").map(o=>o.trim()).filter(Boolean),s={HTTP:{path:"utilities/HTTP.js"},VirtualScroll:{path:"utilities/VirtualScroll.js"},MouseMover:{path:"utilities/MouseMover.js"},API:{path:"components/base/API.js",needs:["Toast"]},AutoForm:{path:"components/base/AutoForm.js",needs:["Toast"]},List:{path:"components/base/List.js",needs:["VirtualScroll"]},Modal:{path:"components/base/Modal.js"},Dialog:{path:"components/base/Dialog.js",needs:["Modal"]},Toast:{path:"components/base/Toast.js"},Nav:{path:"components/base/Nav.js"},Resizer:{path:"components/base/Resizer.js"},DataGrid:{path:"components/data/DataGrid.js",needs:["AutoForm","Dialog","Resizer","DatePicker","ColorPicker","IconPicker","TagsInput"]},DatePicker:{path:"components/form/DatePicker.js",needs:["AutoForm"]},ColorPicker:{path:"components/form/ColorPicker.js",needs:["AutoForm"]},IconPicker:{path:"components/form/IconPicker.js",needs:["AutoForm"]},TagsInput:{path:"components/form/TagsInput.js",needs:["AutoForm"]}},r={base:["API","AutoForm","List","Modal","Dialog","Toast","Nav","Resizer"],form:["DatePicker","ColorPicker","IconPicker","TagsInput"],data:["DataGrid"]},a=Object.fromEntries(Object.entries(r).flatMap(([o,t])=>t.map(t=>[`${o}.${t}`,t]))),n=e.flatMap(o=>r[o]||[a[o]||o]);if(!t.hasAttribute("components"))throw new Error('ui.js requires components="..."');const i=new URL(".",t.src),c=/(?:^|\/)ui\.min\.js$/.test(new URL(t.src).pathname),m=t=>document.write(`<script src="${(t=>{const e=c?t.replace(/\.js$/,".min.js"):t,s=new URL(e,i),r=o.files[t];return r&&s.searchParams.set("v",r),s.href})(t)}"><\/script>`);globalThis.Component||m("frameworks/State.js"),globalThis.bootstrap||m("frameworks/Bootstrap.js"),["HTTP","VirtualScroll","MouseMover"].forEach(o=>m(s[o].path));const p=new Set(["HTTP","VirtualScroll","MouseMover"]),l=o=>{if(p.has(o))return;p.add(o);const t=s[o];if(!t)throw new Error(`Unknown UI component: ${o}`);(t.needs||[]).forEach(l),m(t.path)};n.forEach(l)})(); (()=>{const o={version:"mrpt6n5q",files:{"frameworks/Bootstrap.js":"mrpt6n5q","frameworks/State.js":"mrpt6n5p","utilities/HTTP.js":"mrg6t8zm","utilities/MouseMover.js":"mrg6t8zm","utilities/VirtualScroll.js":"mrg6t8zn","components/base/API.js":"mrj8lfwz","components/base/AutoForm.js":"mrj10yku","components/base/Dialog.js":"mrh7tjfb","components/base/List.js":"mrge46xl","components/base/Modal.js":"mrj1zcg3","components/base/Nav.js":"mrj6haqp","components/base/Resizer.js":"mrghfzrh","components/base/Toast.js":"mrghiycy","components/data/DataGrid.js":"mrj8lxpb","components/form/ColorPicker.js":"mrgn837c","components/form/DatePicker.js":"mrgn837c","components/form/IconPicker.js":"mrgoatz5","components/form/TagsInput.js":"mrgn837b"}},t=document.currentScript;if(!t||"loading"!==document.readyState)throw new Error("ui.js must be loaded by a normal <script> while the document is parsing");const e=(t?.getAttribute("components")||"").split(",").map(o=>o.trim()).filter(Boolean),s={HTTP:{path:"utilities/HTTP.js"},VirtualScroll:{path:"utilities/VirtualScroll.js"},MouseMover:{path:"utilities/MouseMover.js"},API:{path:"components/base/API.js",needs:["Toast"]},AutoForm:{path:"components/base/AutoForm.js",needs:["Toast"]},List:{path:"components/base/List.js",needs:["VirtualScroll"]},Modal:{path:"components/base/Modal.js"},Dialog:{path:"components/base/Dialog.js",needs:["Modal"]},Toast:{path:"components/base/Toast.js"},Nav:{path:"components/base/Nav.js"},Resizer:{path:"components/base/Resizer.js"},DataGrid:{path:"components/data/DataGrid.js",needs:["AutoForm","Dialog","Resizer","DatePicker","ColorPicker","IconPicker","TagsInput"]},DatePicker:{path:"components/form/DatePicker.js",needs:["AutoForm"]},ColorPicker:{path:"components/form/ColorPicker.js",needs:["AutoForm"]},IconPicker:{path:"components/form/IconPicker.js",needs:["AutoForm"]},TagsInput:{path:"components/form/TagsInput.js",needs:["AutoForm"]}},r={base:["API","AutoForm","List","Modal","Dialog","Toast","Nav","Resizer"],form:["DatePicker","ColorPicker","IconPicker","TagsInput"],data:["DataGrid"]},a=Object.fromEntries(Object.entries(r).flatMap(([o,t])=>t.map(t=>[`${o}.${t}`,t]))),n=e.flatMap(o=>r[o]||[a[o]||o]);if(!t.hasAttribute("components"))throw new Error('ui.js requires components="..."');const i=new URL(".",t.src),m=/(?:^|\/)ui\.min\.js$/.test(new URL(t.src).pathname),c=t=>document.write(`<script src="${(t=>{const e=m?t.replace(/\.js$/,".min.js"):t,s=new URL(e,i),r=o.files[t];return r&&s.searchParams.set("v",r),s.href})(t)}"><\/script>`);globalThis.Component||c("frameworks/State.js"),globalThis.bootstrap||c("frameworks/Bootstrap.js"),["HTTP","VirtualScroll","MouseMover"].forEach(o=>c(s[o].path));const p=new Set(["HTTP","VirtualScroll","MouseMover"]),l=o=>{if(p.has(o))return;p.add(o);const t=s[o];if(!t)throw new Error(`Unknown UI component: ${o}`);(t.needs||[]).forEach(l),c(t.path)};n.forEach(l)})();

View File

@ -1,4 +1,5 @@
name: HTTP name: HTTP
purpose: Send browser HTTP requests with JSON, form, upload, timeout, and retained-header handling.
methods: methods:
request: request:

View File

@ -17,8 +17,13 @@ rules:
- Treat this as an internal UI utility unless implementing a virtualized component. - Treat this as an internal UI utility unless implementing a virtualized component.
- Call update after each rendered row can be measured. - Call update after each rendered row can be measured.
example: | examples:
basic: |
const virtual = VirtualScroll({ itemHeight: 40 }) const virtual = VirtualScroll({ itemHeight: 40 })
virtual.reset(rows, container) virtual.reset(rows, container)
virtual.init(rows, () => refresh()) virtual.init(rows, () => refresh())
const view = virtual.calc(container, rows) const view = virtual.calc(container, rows)
tests:
- ../components/base/List.test.html
- ../components/data/DataGrid.test.html