Compare commits

..

No commits in common. "main" and "v1.0.1" have entirely different histories.
main ... v1.0.1

27 changed files with 194 additions and 356 deletions

48
AI.yaml Normal file
View File

@ -0,0 +1,48 @@
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.
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).
For AI-oriented API documentation, start with [AI.yaml](AI.yaml).

View File

@ -12,6 +12,6 @@
必须把脚本放在 `<head>`,并使用普通 `<script>`。不要使用 `async``defer``type="module"`,这样 state 才能在 DOMContentLoaded 前完成组件注册,并对完整页面执行首次扫描。
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 中。
AI 使用说明从 [AI.yaml](AI.yaml) 开始;文档与源码同目录,组件示例直接写在对应 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 包的版本独立于前置框架包。

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
name: Nav
purpose: Render horizontal or vertical navigation from declarative brand and item data.
attributes:
vertical:
@ -25,8 +24,7 @@ events:
item_shape: "{ type, name?, label?, icon?, items?, bind?, class?, width? }"
examples:
basic: |
example: |
<script>
const navBrand = { image: '/res/apiGo.min.svg' }
const navItems = [
@ -46,6 +44,3 @@ rules:
- 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 the same `navItems` data for horizontal and vertical navigation when comparing layouts.
tests:
- Nav.test.html

View File

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

View File

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

View File

@ -0,0 +1,42 @@
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,5 +1,4 @@
name: DataGrid
purpose: Manage editable, virtualized data-API tables while preserving server field definitions.
attributes:
editable:
@ -9,6 +8,8 @@ attributes:
type: string
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:
table: Table name sent as request.name when api is configured.
fields: Array of data-API field definitions; assign before list.
@ -97,8 +98,7 @@ rules:
- Treat underscore-prefixed state fields as internal read-only diagnostics.
- Custom field editors must be registered in AutoForm before use.
examples:
local_and_api: |
example: |
<script>
const orderFields = [
{ id: 'f-name', tableID: 'orders', name: 'name', type: 'v100', isIndex: true, memo: '', settings: { type: 'text' } },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
name: State
purpose: Provide reactive state, declarative DOM directives, and the custom component runtime.
globals:
State: 'Reactive in-memory application state.'
@ -49,22 +48,19 @@ component:
- 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.
examples:
bindings: |
examples: |
<script>
const users = [{ id: 'ada', name: 'Ada' }]
</script>
<div $text>Hello ${users[0].name}</div>
<List $.state.list="users"></List>
<button $onclick="State.dark = !State.dark">Toggle</button>
component: |
<script>
Component.register('Counter', container => {
container.state.value = 0
container.addEventListener('bind', event => { container.state.value = event.detail })
}, Util.makeDom('<button $text="this.state.value" $onclick="this.state.value++"></button>'))
</script>
<Counter $bind="State.count"></Counter>
<!-- <Counter $bind="State.count"></Counter> -->
tests:
- https://apigo.cc/web/state/raw/branch/main/test/all.spec.js
- apigo.cc/web/state/test/

View File

@ -1,91 +0,0 @@
# @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",
"ui.js",
"ui.test.html",
"llms.txt",
"AI.yaml",
"README.md",
"README.zh-CN.md",
"test",

View File

@ -1,7 +1,6 @@
const { copyFile, readdir, readFile, stat, writeFile } = require('node:fs/promises');
const { join, relative, sep } = require('node:path');
const terser = require('terser');
const { validateDocs } = require('./validate-docs.cjs');
const root = join(__dirname, '..');
const publicDirs = ['frameworks', 'utilities', 'components'];
@ -21,19 +20,6 @@ const filesBelow = async directory => {
};
(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`))));
const sources = (await Promise.all(publicDirs.map(dir => filesBelow(join(root, dir))))).flat();
const sourceStats = await Promise.all(sources.map(async file => {

View File

@ -1,96 +0,0 @@
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.
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 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 script = document.currentScript;
if (!script || document.readyState !== 'loading') {
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:"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)})();
(()=>{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)})();

View File

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

View File

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