@hozu/cli 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/commands/add.d.ts +1 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +3 -3
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/browse-page.d.ts.map +1 -1
- package/dist/commands/browse-page.js +6 -4
- package/dist/commands/browse-page.js.map +1 -1
- package/dist/commands/browse-tab.d.ts +5 -0
- package/dist/commands/browse-tab.d.ts.map +1 -1
- package/dist/commands/browse-tab.js +18 -3
- package/dist/commands/browse-tab.js.map +1 -1
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +11 -4
- package/dist/commands/browse.js.map +1 -1
- package/dist/commands/request.js +1 -1
- package/dist/commands/request.js.map +1 -1
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +4 -5
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/contract.d.ts +8 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -1
- package/dist/main.js.map +1 -1
- package/dist/migrate/steps.d.ts.map +1 -1
- package/dist/migrate/steps.js +13 -0
- package/dist/migrate/steps.js.map +1 -1
- package/package.json +9 -9
- package/schema/browse.schema.json +17 -0
- package/schema/inspect.schema.json +9 -1
- package/skill/SKILL.md +4 -5
- package/skill/example/features/bookmarks/views.ts +8 -12
- package/skill/example/ui/button.ts +6 -2
- package/skill/topics/auth.md +2 -2
- package/skill/topics/contracts.md +3 -1
- package/skill/topics/data.md +3 -3
- package/skill/topics/deploy.md +4 -2
- package/skill/topics/diagnostics.md +8 -8
- package/skill/topics/feature.md +5 -5
- package/skill/topics/forms.md +2 -0
- package/skill/topics/http.md +2 -1
- package/skill/topics/i18n.md +4 -2
- package/skill/topics/machine.md +9 -5
- package/skill/topics/pages.md +3 -1
- package/skill/topics/patterns.md +7 -6
- package/skill/topics/recipes.md +15 -6
- package/skill/topics/testing.md +7 -0
- package/skill/topics/views.md +26 -14
package/skill/topics/pages.md
CHANGED
|
@@ -66,4 +66,6 @@ export default project({
|
|
|
66
66
|
- A detail view: `ui.view({ route: itemPage, render: ({ params }) => ui.query(getItem, { id: params.id }, { ready,
|
|
67
67
|
failed: { NotFound: () => ui.p({}, ['Not found']), Unexpected: () => … } }) })`.
|
|
68
68
|
- A page loads JS only when a machine-bound part renders on it (`hozu plan <route or path>`). Every link loads a document;
|
|
69
|
-
|
|
69
|
+
a machine whose view both pages show (or the same address) resumes its calm state, the last state without `invoke`,
|
|
70
|
+
from `sessionStorage`; a reload or a page without that view starts from `initialContext` (or `seed`). What must
|
|
71
|
+
survive a reload lives in the URL (`seed`), on the server (queries) or in a client component's own storage.
|
package/skill/topics/patterns.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
The controls are plain elements; in an app with a kit, use its components (`ui.use(Button, …)`,
|
|
4
4
|
`hozu docs components`).
|
|
5
5
|
|
|
6
|
-
- **Busy state:** render every control once
|
|
7
|
-
`
|
|
8
|
-
- **Optimistic item:** `
|
|
6
|
+
- **Busy state:** render every control once and disable it: `disabled: is(['adding'])` (`invoke` drops repeats).
|
|
7
|
+
Progress: `is(['adding']) && ui.p({ 'aria-busy': 'true' }, ['Saving…'])`.
|
|
8
|
+
- **Optimistic item:** `is(['adding']) && ui.li({ class: 'opacity-50' }, [ctx.draft])`; leaving the state removes it
|
|
9
9
|
and the refreshed query shows the real item.
|
|
10
10
|
- **Refresh after a mutation:** tag the query, list the tag in the mutation's `invalidates`.
|
|
11
11
|
- **Go to what was just created:** `done: { target: 'idle', navigate: (r) => ui.link(itemPage, { id: r.id }) }`.
|
|
@@ -82,6 +82,7 @@ ui.each(items, 'id', (item) => ui.li({}, [ui.input({ type: 'checkbox', form: bul
|
|
|
82
82
|
`ui.each(ctx.cursors, null, (cursor) => ui.query(listPage, { cursor }, { ready: (page) => … }))`; on the last page
|
|
83
83
|
(`cursor === ctx.last && page.next !== null`) a sentinel `on: { visible: ui.send(More, { cursor: page.next }) }`;
|
|
84
84
|
`More` pushes the cursor, guarded by `e.cursor !== null && e.cursor !== ctx.last`.
|
|
85
|
-
- **
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
- **What a link keeps:** every internal link loads a document. A machine whose view both pages show (or the same
|
|
86
|
+
address) resumes its calm state, the last state without `invoke`, from `sessionStorage`; a reload or a page
|
|
87
|
+
without that view starts from `initialContext` (or `seed`). Keep what must survive a reload or a shared link in
|
|
88
|
+
the URL: put both filters in `search` and `seed` the context from it, not one in the URL and one in context.
|
package/skill/topics/recipes.md
CHANGED
|
@@ -20,8 +20,8 @@ The list is the visitor's own: it lives in their browser, so two visitors never
|
|
|
20
20
|
- **feature.ts:** `fetch: new URL('./fetch.ts', import.meta.url)`; `app.ts`: `components: bundleComponents`.
|
|
21
21
|
- **Data about the items** (quotes, prices) is public: a `runs: 'server'` (or `'either'`) query inside the list's
|
|
22
22
|
`ready` branch, `ui.query(quotes, { symbols }, …)`.
|
|
23
|
-
-
|
|
24
|
-
`RefreshNow
|
|
23
|
+
- Pause / Resume / Refresh now: `paused` is a context field (a mode), `refresh: () => [quotesTag()]` on
|
|
24
|
+
`RefreshNow`, on `Resume` (`target: 'idle'` restarts the timer) and on the guarded `after` (`hozu docs machine`).
|
|
25
25
|
- **Ask the server before saving** (normalize "2330" to "2330.TW"): a `runs: 'server'` query `resolveSymbol`, then
|
|
26
26
|
the browser mutation: `looking: { invoke: invoke(resolveSymbol, { input: { q: ctx.symbol }, done: { target:
|
|
27
27
|
'saving', assign: (r) => { ctx.symbol = r.symbol } }, failed: { … target: 'previous' } }) }`, `saving: { invoke:
|
|
@@ -89,8 +89,16 @@ const staffHead = { query: me, render: (m) => ({ title: `${m.name} · Admin` }),
|
|
|
89
89
|
const staff = (route, View) => ui.page(route, { views: [Sidebar, View], head: staffHead })
|
|
90
90
|
export default project({ /* … */ pages: [staff(orders, OrderList), staff(orderDetail, OrderPage), …] })
|
|
91
91
|
```
|
|
92
|
-
The sidebar marks its sections with `current(route)` from its render:
|
|
93
|
-
|
|
92
|
+
The sidebar marks its sections with `current(route)` from its render, one line per section:
|
|
93
|
+
```ts
|
|
94
|
+
render: ({ current }) => ui.nav({}, [
|
|
95
|
+
...[
|
|
96
|
+
[orders, 'Orders', current(orders) || current(orderDetail)],
|
|
97
|
+
[customers, 'Customers', current(customers) || current(customer)],
|
|
98
|
+
].map(([r, label, here]) => ui.a({ href: ui.link(r, null), 'aria-current': here, class: 'aria-[current]:font-bold' }, [label])),
|
|
99
|
+
])
|
|
100
|
+
```
|
|
101
|
+
A store's categories: `current(shop, { category: c })` over a constant list of categories.
|
|
94
102
|
|
|
95
103
|
## Screens with different state
|
|
96
104
|
One machine per feature: an order list (filters, selection) and an order page (shipping, refund) are two features,
|
|
@@ -99,8 +107,9 @@ One machine per feature: an order list (filters, selection) and an order page (s
|
|
|
99
107
|
## A multi-step checkout that also works without JavaScript
|
|
100
108
|
Each step is a state and each step's form posts only its own fields: after a native post the server renders the next
|
|
101
109
|
step, and every form on that page carries the machine's state in a signed hidden field, so the next post continues
|
|
102
|
-
from it (going back to edit a step too). Prefill from
|
|
103
|
-
|
|
110
|
+
from it (going back to edit a step too; the field is `__hozu_state`, sealed with `SESSION_SECRET`). Prefill from
|
|
111
|
+
the member on the view that has both `machine` and `route` (HZ048):
|
|
112
|
+
`ui.view({ machine: checkout, route: checkoutPage, seed: ({ query }) => ({ email: query(me, {}).email }), render })`.
|
|
104
113
|
|
|
105
114
|
## A notice after saving
|
|
106
115
|
A `notice` context field set in `done` and cleared by `after: [{ ms: 4000, target: 'idle' }]` on a `saved` state;
|
package/skill/topics/testing.md
CHANGED
|
@@ -80,6 +80,13 @@
|
|
|
80
80
|
`--select <css>`, `--screenshot shot.png` (after the steps), `--viewport 390x844` (a phone; default 1280x800) and
|
|
81
81
|
`--reduced-motion`.
|
|
82
82
|
- It also prints the client components on the page (mounted, failed, size, canvases).
|
|
83
|
+
- **More checks in a step:**
|
|
84
|
+
- A click that would land on another element fails the step: `the click would land on <h3>, which contains it, above
|
|
85
|
+
<a href="/x">: a person cannot click it` (an overlay, a card covering its link).
|
|
86
|
+
- A navigation shows how it arrived: `→ /x (loaded, 32 ms)` or `(prerendered, 4 ms)`; with `--js both`, per mode.
|
|
87
|
+
- `--select` prints each element's `class` too. An element moved to another parent is not a flash.
|
|
88
|
+
- A server error that `get` or `browse` lists is noted once with `a production server shows "Internal error" here`:
|
|
89
|
+
the visitor sees that text and the call id; `onError` gets the message.
|
|
83
90
|
- **`testApp`:** `app` is the default export of `app.ts`; `.post(path, fields)` submits a native form, with fields as
|
|
84
91
|
a record or as `[name, value]` pairs for repeated names. `testApp(app, { session: store })` may swap only the
|
|
85
92
|
session store (a test issuer). `testApp` reads no env files: pass `testApp(app, { env: process.env })` (or a record)
|
package/skill/topics/views.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
```ts
|
|
4
4
|
export const Board = ui.view({
|
|
5
|
-
machine: m, // optional: without it, no ctx /
|
|
5
|
+
machine: m, // optional: without it, no ctx / is / events, and 0 JS
|
|
6
6
|
route: home, // optional: render gets { params, search } typed by the route
|
|
7
7
|
seed: ({ search }) => ({ q: search.q }), // optional, with machine + route: context fields from the URL
|
|
8
|
-
render: ({ ctx,
|
|
8
|
+
render: ({ ctx, is, current, params, search, locale }) => ui.main({ class: 'mx-auto max-w-xl' }, [ /* children */ ]),
|
|
9
9
|
})
|
|
10
10
|
```
|
|
11
11
|
- **Elements:** `ui.<tag>(attrs, children)` for every HTML and SVG element; void tags (`input`, `img`) take only attrs.
|
|
@@ -13,10 +13,10 @@ export const Board = ui.view({
|
|
|
13
13
|
- **Classes:** `class` is a static string of Tailwind classes that must exist (HZ026); conditional classes:
|
|
14
14
|
`toggle: { 'bg-indigo-600 text-white': ctx.tab === t }`. No `style`.
|
|
15
15
|
- **Conditions:** `ctx.error !== null && ui.p({ role: 'alert' }, [ctx.error])`, `item.done ? 'done' : 'open'`.
|
|
16
|
-
By machine state:
|
|
17
|
-
`disabled: is(['saving'])` (
|
|
18
|
-
- **Dialogs, popovers, menus:** native
|
|
19
|
-
`ui.dialog({ id: 'd', closedby: 'any' }, [...])`, `popover` / `popovertarget`, `ui.details`.
|
|
16
|
+
By machine state: `!is(['idle']) && ui.p({}, ['Saving…'])`,
|
|
17
|
+
`disabled: is(['saving'])` (disable a control while busy, not hide it: it stays put).
|
|
18
|
+
- **Dialogs, popovers, menus:** native (bound to the machine: --more): `ui.button({ commandfor: 'd', command:
|
|
19
|
+
'show-modal' })` + `ui.dialog({ id: 'd', closedby: 'any' }, [...])`, `popover` / `popovertarget`, `ui.details`.
|
|
20
20
|
- **Lists:** `ui.each(items, 'id', (item) => ui.li({}, [item.title]))`. Never `.map` over data.
|
|
21
21
|
- **Numbers and dates:** `ui.format.number(q.price, { style: 'currency', currency: 'USD' })`, `ui.format.date(x,
|
|
22
22
|
{ dateStyle: 'medium' })`, `ui.format.relative(n, 'day')`, `ui.format.list(xs)` (Intl, the page's locale).
|
|
@@ -33,7 +33,10 @@ export const Board = ui.view({
|
|
|
33
33
|
<!-- more -->
|
|
34
34
|
|
|
35
35
|
- **Motion:** what an update adds fades in by itself (not with reduced motion); a view two pages show stays still
|
|
36
|
-
across a page change. `hozu browse` reports a flash or a layout shift: fix those.
|
|
36
|
+
across a page change. `hozu browse` reports a flash or a layout shift: fix those. `c ? a : b` whose branches are
|
|
37
|
+
one element of the same tag and shape (`ctx.paused ? resumeButton : pauseButton`) keeps the element: its text,
|
|
38
|
+
classes, attributes and listener follow `c`, and focus stays (not when a differing value computes, such as a `fn`
|
|
39
|
+
or a template string, or links elsewhere).
|
|
37
40
|
- **Attributes:** HTML names in lower case (`for`, `minlength`, `aria-pressed`, `data-x`), typed per tag. Values are
|
|
38
41
|
literals or data: `'aria-pressed': ctx.show === 'all'`, `title: ctx.error ?? 'OK'`.
|
|
39
42
|
- **Sizes and colours from data:** `vars` with an arbitrary-value class: `class: 'w-[calc(var(--pct)*1%)]'`,
|
|
@@ -42,7 +45,9 @@ export const Board = ui.view({
|
|
|
42
45
|
(runs on the server, and in the browser inside an island).
|
|
43
46
|
- **More conditions:** `list.length === 0 ? ui.p({}, ['Empty']) : ui.ul({}, [...])`; a `?:` / `&&` branch may be a list:
|
|
44
47
|
`open ? [a, b] : null`. A query branch or an each item returns one node: wrap several in an element (HZ014).
|
|
45
|
-
With an enter/leave animation: `ui.if(cond, [then], [else], 'fade')
|
|
48
|
+
With an enter/leave animation (only then): `ui.if(cond, [then], [else], 'fade')`, by machine state
|
|
49
|
+
`when(['saving'], [children], 'fade')`, and list rows `ui.each(items, 'id', row, 'fade')` (the motion name is
|
|
50
|
+
required; without a motion, `is([...]) && …`).
|
|
46
51
|
- **More lists:** `ui.each(tags, null, (t) => …)` for primitives. `.map` only over constants:
|
|
47
52
|
`['a', 'b'].map((k) => ui.option({ value: k }, [k]))`.
|
|
48
53
|
- **Text:** template strings work: `` `${n} items` ``.
|
|
@@ -50,27 +55,34 @@ export const Board = ui.view({
|
|
|
50
55
|
`row(item)`; it is inlined, so the IR equals the inline form. A plain function that receives data is HZ059.
|
|
51
56
|
- **Shared UI:** use the kit component, not a styled `ui.button` per page (`example/` uses a kit).
|
|
52
57
|
- **More events:** any DOM event name plus `visible` (entered the viewport). Payload fields also:
|
|
53
|
-
`ui.dom.formAll('name')`, `ui.dom.checked`, `ui.dom.valueAsNumber`, `ui.dom.key`.
|
|
58
|
+
`ui.dom.formAll('name')`, `ui.dom.checked`, `ui.dom.valueAsNumber`, `ui.dom.key`. Keyboard shortcuts:
|
|
59
|
+
`ui.window({ on: { keydown: ui.send(Open, {}, { keys: ['Mod+k', '/'] }) } })` sends only on those presses and
|
|
60
|
+
stops the browser's own (`Mod` is ⌘ on Apple, Ctrl elsewhere; also `Ctrl`, `Meta`, `Alt`, `Shift`); a printable
|
|
61
|
+
key without a modifier waits while the person types in a field (`Escape` does not). `ui.dom.value` / `ui.dom.form`
|
|
54
62
|
fill an enum field only from a `<select>`, radios or submit buttons whose literal values are all members (HZ033).
|
|
55
63
|
- **Search in links:** the third argument of `ui.link` is optional and exists only when the route declares `search`:
|
|
56
64
|
omitted means every default, and a search lists only the fields that differ: `ui.link(home, null, { show: 'done' })`.
|
|
57
65
|
- **More data:** `pending: ui.p({}, ['Loading…'])` is optional; a branch may return `null` to render nothing.
|
|
58
66
|
Server-fetched data is sent with the page and never fetched again; after a mutation, queries whose tags it
|
|
59
|
-
invalidates refresh in place.
|
|
67
|
+
invalidates refresh in place. When a query's input changes, the rows stay (`aria-busy` on the parent) and update
|
|
68
|
+
by key; `pending` shows only before the first answer.
|
|
60
69
|
- **From Vue or React:** `computed` → a `fn`; `ref` + `@click` → a context field + `ui.set`; `v-if` → `?:` / `&&`
|
|
61
|
-
(with `is([...])` for a machine state); `v-for` + `:key` → `ui.each(list, 'id', …)`; `setInterval` → `
|
|
62
|
-
|
|
70
|
+
(with `is([...])` for a machine state); `v-for` + `:key` → `ui.each(list, 'id', …)`; `setInterval` → `freshness: { poll: s }`
|
|
71
|
+
for data that changes on its own (`hozu docs data`), `after` with `refresh` for a refresh the visitor pauses
|
|
72
|
+
(`hozu docs machine`); `watch` → a transition's `assign`; DOM libraries (charts, maps) → a client component
|
|
63
73
|
(`hozu docs components`; `examples/showcase` has a Chart.js one).
|
|
64
74
|
- **Also:** `ui.html(post.html)` (trusted HTML from query data only, HZ030), `ui.asset(new URL('./x.png',
|
|
65
75
|
import.meta.url))`, `ui.window({ on })` / `ui.document({ on })`, `ui.embed(OtherView)`.
|
|
66
76
|
|
|
67
77
|
## Menus, dialogs, counting
|
|
68
|
-
- When a query's input changes, the rows stay (`aria-busy` on the parent) and update by key; `pending` shows only
|
|
69
|
-
before the first answer.
|
|
70
78
|
- A link to the address shown gets `aria-current="page"`. Which links mark a section is yours to say: the render's
|
|
71
79
|
`current(route)` is true on that route's pages, so a menu writes
|
|
72
80
|
`ui.a({ href: ui.link(orders, null), 'aria-current': current(orders) || current(orderDetail) }, ['Orders'])` and
|
|
73
81
|
styles `aria-[current]:font-bold`: `true` is written `"page"` on the address itself, `false` writes nothing.
|
|
82
|
+
`current(shop, { category: 'apparel' })` also compares those params (search is ignored). A menu is a constant
|
|
83
|
+
list mapped to links, one line per section:
|
|
84
|
+
`...[[orders, 'Orders'], [customers, 'Customers']].map(([r, label]) => ui.a({ href: ui.link(r, null),
|
|
85
|
+
'aria-current': current(r) }, [label]))`; `current(a) || current(b)` works there too.
|
|
74
86
|
- `ui.dialog({ open: is(['editing']), on: { close: ui.send(Cancel, {}) } }, [...])` opens as a modal and closes with
|
|
75
87
|
the machine; Escape sends `close`. It needs JavaScript: a dialog that must open without it uses the native
|
|
76
88
|
`commandfor` button (the short form) and closes when the data that shows it changes.
|