Compare commits
No commits in common. "main" and "v1.0.1" have entirely different histories.
48
AI.yaml
Normal file
48
AI.yaml
Normal 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
|
||||
@ -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).
|
||||
|
||||
@ -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 包的版本独立于前置框架包。
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() })
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
name: Toast
|
||||
purpose: Show transient notifications and lightweight asynchronous confirmations.
|
||||
|
||||
methods:
|
||||
show: 'Toast.show(message, options) -> Promise<Toast element>'
|
||||
|
||||
42
components/data/Chart.yaml
Normal file
42
components/data/Chart.yaml
Normal 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
|
||||
@ -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' } },
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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/
|
||||
|
||||
@ -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/
|
||||
|
||||
91
llms.txt
91
llms.txt
@ -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.
|
||||
@ -14,7 +14,7 @@
|
||||
"utilities",
|
||||
"ui.js",
|
||||
"ui.test.html",
|
||||
"llms.txt",
|
||||
"AI.yaml",
|
||||
"README.md",
|
||||
"README.zh-CN.md",
|
||||
"test",
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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
2
ui.js
@ -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
2
ui.min.js
vendored
@ -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)})();
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
name: HTTP
|
||||
purpose: Send browser HTTP requests with JSON, form, upload, timeout, and retained-header handling.
|
||||
|
||||
methods:
|
||||
request:
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user