@hozu/cli 0.21.0 → 0.22.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/bin/hozu.js +2 -2
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +10 -7
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/app.d.ts +3 -0
- package/dist/commands/app.d.ts.map +1 -1
- package/dist/commands/app.js +23 -0
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/browse-tab.d.ts +4 -2
- package/dist/commands/browse-tab.d.ts.map +1 -1
- package/dist/commands/browse-tab.js +36 -5
- package/dist/commands/browse-tab.js.map +1 -1
- package/dist/commands/browse-world.d.ts +3 -0
- package/dist/commands/browse-world.d.ts.map +1 -1
- package/dist/commands/browse-world.js +2 -1
- package/dist/commands/browse-world.js.map +1 -1
- package/dist/commands/browse.d.ts +1 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +50 -11
- package/dist/commands/browse.js.map +1 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +6 -1
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/gen.d.ts +6 -0
- package/dist/commands/gen.d.ts.map +1 -0
- package/dist/commands/gen.js +53 -0
- package/dist/commands/gen.js.map +1 -0
- package/dist/commands/request.d.ts +6 -1
- package/dist/commands/request.d.ts.map +1 -1
- package/dist/commands/request.js +60 -6
- package/dist/commands/request.js.map +1 -1
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +6 -7
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +11 -3
- package/dist/commands/serve.js.map +1 -1
- package/dist/contract.d.ts +40 -3
- package/dist/contract.d.ts.map +1 -1
- package/dist/gen/go.d.ts +5 -0
- package/dist/gen/go.d.ts.map +1 -0
- package/dist/gen/go.js +369 -0
- package/dist/gen/go.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +28 -2
- package/dist/main.js.map +1 -1
- package/dist/migrate/steps.d.ts.map +1 -1
- package/dist/migrate/steps.js +17 -0
- package/dist/migrate/steps.js.map +1 -1
- package/dist/remote.d.ts +15 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +58 -0
- package/dist/remote.js.map +1 -0
- package/package.json +8 -8
- package/schema/browse.schema.json +55 -4
- package/schema/check.schema.json +2 -1
- package/schema/gen.schema.json +61 -0
- package/schema/inspect.schema.json +20 -0
- package/schema/migrate.schema.json +2 -1
- package/schema/render.schema.json +2 -1
- package/schema/request.schema.json +35 -1
- package/skill/example/features/bookmarks/model.ts +0 -7
- package/skill/example/features/bookmarks/views.ts +3 -5
- package/skill/topics/data.md +49 -0
- package/skill/topics/diagnostics.md +1 -0
- package/skill/topics/forms.md +2 -2
- package/skill/topics/http.md +2 -1
- package/skill/topics/machine.md +5 -6
- package/skill/topics/pages.md +1 -1
- package/skill/topics/patterns.md +9 -8
- package/skill/topics/recipes.md +22 -0
- package/skill/topics/testing.md +8 -4
- package/skill/topics/views.md +13 -3
package/skill/topics/patterns.md
CHANGED
|
@@ -20,8 +20,8 @@ ui.form({ on: { submit: ui.send(Toggle, { id: ui.dom.form('id') }) } }, [
|
|
|
20
20
|
```
|
|
21
21
|
- **Filter in the URL** (shareable, no JS): `search` on the route, options as
|
|
22
22
|
`ui.a({ href: ui.link(home, null, { show: s.value }), 'aria-current': search.show === s.value }, [s.label])`.
|
|
23
|
-
- **Filter as you type, empty state:** context `search: z.string()`, `on: { input: ui.
|
|
24
|
-
|
|
23
|
+
- **Filter as you type, empty state:** context `search: z.string()`, `on: { input: ui.set(ctx.search, ui.dom.value)
|
|
24
|
+
}`, filter and test emptiness with a `fn` (see --more).
|
|
25
25
|
- **Detail page with a 404:** `hozu docs pages`.
|
|
26
26
|
|
|
27
27
|
<!-- more -->
|
|
@@ -42,10 +42,10 @@ isEmpty({ items, show: ctx.show })
|
|
|
42
42
|
: ui.ul({}, [ui.each(visible({ items, show: ctx.show }), 'id', (i) => ui.li({}, [i.title]))])
|
|
43
43
|
```
|
|
44
44
|
- **Search as you type:** context `search: z.string()`; `ui.input({ type: 'search', 'aria-label': 'Search', value:
|
|
45
|
-
ctx.search, on: { input: ui.
|
|
46
|
-
|
|
45
|
+
ctx.search, on: { input: ui.set(ctx.search, ui.dom.value) } })`; filter with a
|
|
46
|
+
`fn({ input: z.object({ items, text: z.string() }), … })`.
|
|
47
47
|
- **Toggle buttons:** for each option of a constant list,
|
|
48
|
-
`ui.button({ type: 'button', 'aria-pressed': ctx.show === s.value, on: { click: ui.
|
|
48
|
+
`ui.button({ type: 'button', 'aria-pressed': ctx.show === s.value, on: { click: ui.set(ctx.show, s.value) } }, [s.label])`.
|
|
49
49
|
- **In the URL and as you type** (`/?q=park` is a link to share, typing filters live): seed the machine from the URL,
|
|
50
50
|
read only the context, and write the address back with `replace` on the typing transition
|
|
51
51
|
(`replace: () => ui.link(home, null, { q: ctx.q })`). A GET form with `name="q"` sets it without JS.
|
|
@@ -55,9 +55,10 @@ export const Board = ui.view({ machine: m, route: home, seed: ({ search }) => ({
|
|
|
55
55
|
ui.input({ type: 'search', name: 'q', 'aria-label': 'Search', value: ctx.q, on: { input: ui.send(Search, { q: ui.dom.value }) } }),
|
|
56
56
|
/* … */ ui.each(visible({ items, q: ctx.q, district: ctx.district }), 'id', (s) => …) ]) })
|
|
57
57
|
```
|
|
58
|
-
- **A mode with shared controls** (a tour, an edit mode):
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
- **A mode with shared controls** (a tour, an edit mode): a `ui.set` already works in every state without `invoke`;
|
|
59
|
+
put other transitions every mode handles the same way in
|
|
60
|
+
`machine({ on: [on(Search, { assign: (e) => { ctx.q = e.q; ctx.page = 1 } })] })` (no `target`: stays in its
|
|
61
|
+
state); each state lists only what differs.
|
|
61
62
|
- **Select many, then act** (bulk delete): checkboxes in the list join one form through a formRef; the invoke
|
|
62
63
|
state drops events, so the checkboxes are disabled while it runs:
|
|
63
64
|
```ts
|
package/skill/topics/recipes.md
CHANGED
|
@@ -81,3 +81,25 @@ Run a fresh scaffold into a scratch app with `--with detail`, and copy the parts
|
|
|
81
81
|
- Following the system needs no code: Tailwind's `dark:` classes (`bg-white dark:bg-slate-900`).
|
|
82
82
|
- A switch the visitor chooses: a client component (`hozu docs components`) puts `dark` on `<html>` and keeps the
|
|
83
83
|
choice in `localStorage`; `app.css` adds `@custom-variant dark (&:where(.dark, .dark *));`.
|
|
84
|
+
|
|
85
|
+
## A shell shared by many pages (a back office)
|
|
86
|
+
Pages are config, so a helper is the layout:
|
|
87
|
+
```ts
|
|
88
|
+
const staffHead = { query: me, render: (m) => ({ title: `${m.name} · Admin` }), failed: { Forbidden: signIn } }
|
|
89
|
+
const staff = (route, View) => ui.page(route, { views: [Sidebar, View], head: staffHead })
|
|
90
|
+
export default project({ /* … */ pages: [staff(orders, OrderList), staff(orderDetail, OrderPage), …] })
|
|
91
|
+
```
|
|
92
|
+
The sidebar's links mark the page shown with `aria-current` by themselves (`aria-[current]:font-bold`).
|
|
93
|
+
|
|
94
|
+
## Screens with different state
|
|
95
|
+
One machine per feature: an order list (filters, selection) and an order page (shipping, refund) are two features,
|
|
96
|
+
`orders` and `order`, sharing declarations through `exports`. Each machine stays small and its contracts few.
|
|
97
|
+
|
|
98
|
+
## A multi-step checkout that also works without JavaScript
|
|
99
|
+
Each step is a state; the server runs the machine per request, so without JS a step's form posts every earlier field
|
|
100
|
+
again as hidden inputs (`ui.input({ type: 'hidden', name: 'line1', value: ctx.line1 })`), and the last step's
|
|
101
|
+
mutation receives them all. Prefill from the member with `seed: ({ query }) => ({ email: query(me, {}).email })`.
|
|
102
|
+
|
|
103
|
+
## A notice after saving
|
|
104
|
+
A `notice` context field set in `done` and cleared by `after: [{ ms: 4000, target: 'idle' }]` on a `saved` state;
|
|
105
|
+
the view shows `ctx.notice !== null && ui.p({ role: 'status' }, [ctx.notice])`. There is no global toast store.
|
package/skill/topics/testing.md
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
`hozu call api.who --input '{"room":"a"}' --header 'Authorization: Bearer t'` (a POST needs `--write`).
|
|
8
8
|
- **Drive the app in a real browser, still without a server:**
|
|
9
9
|
`hozu browse / --session '{"user":"ada"}' --do 'fill New note=Milk' --do 'press Enter' --do 'click Pin in "Milk"'`.
|
|
10
|
-
- Steps: `fill <label>=<value
|
|
11
|
-
`submit "<form>"`, `press <key>`, `wait <ms>`, `goto <path>`, `post <path> a=1&b=2`,
|
|
10
|
+
- Steps: `fill <label>=<value>` (`\n`, `\t` work), `select <label>=<option>`, `check` / `uncheck <label>`,
|
|
11
|
+
`click <name>`, `submit "<form>"`, `press <key>`, `wait <ms>`, `goto <path>`, `post <path> a=1&b=2`,
|
|
12
12
|
`remember <name> from url|<selector> [@attr]` (later steps read `$name`); a target may end with `in "<text>"`
|
|
13
13
|
(for fill and select, before or after `=value`).
|
|
14
14
|
- Labels are what `hozu get <page> --forms` lists; a missing one prints `Did you mean "…"?`. One `--do` may hold
|
|
@@ -29,8 +29,12 @@
|
|
|
29
29
|
|
|
30
30
|
<!-- more -->
|
|
31
31
|
|
|
32
|
-
- **
|
|
33
|
-
|
|
32
|
+
- **Server errors:** what the app's `onError` receives (a resolver that threw, an invalid input) is listed under the
|
|
33
|
+
step or the `get` request that caused it, `server error: <message> (<feature.effect>)`; `--json` `serverErrors`.
|
|
34
|
+
- **A calm page:** a step that rebuilds elements unchanged says `N elements rebuilt unchanged (a flash: main > form >
|
|
35
|
+
button[type=submit])`, naming up to five (`--json` `flashes.elements` has all; a control hidden while busy: disable
|
|
36
|
+
it instead). Equal means tag, class, text, `name`, `id`, `href`, `src`, `type` and parent path; a node that moved is
|
|
37
|
+
no flash. Layout that moves without input says `layout shift X`. Both are
|
|
34
38
|
problems to fix; a calm step prints neither. An address changed with `replace` stays `in place`.
|
|
35
39
|
- **`hozu get`** prints the status, redirect, `set-cookie` attributes (`HttpOnly`, `SameSite`), title, alerts and
|
|
36
40
|
visible text.
|
package/skill/topics/views.md
CHANGED
|
@@ -23,10 +23,10 @@ export const Board = ui.view({
|
|
|
23
23
|
- **Events:** `on: { click: ui.send(Event, payload) }`; payload fields are literals, data, `ui.dom.value`,
|
|
24
24
|
`ui.dom.form('name')` (submit; `hozu docs forms`). A control that only sets a context field:
|
|
25
25
|
`on: { click: ui.set(ctx.open, !ctx.open) }`, `on: { input: ui.set(ctx.q, ui.dom.value) }` (no event to declare).
|
|
26
|
-
- **Links:** `ui.a({ href: ui.link(itemPage, { id: item.id }) }, [...])`; never a string path (HZ032).
|
|
26
|
+
- **Links:** `ui.a({ href: ui.link(itemPage, { id: item.id }) }, [...])`; never a string path (HZ032). Menus,
|
|
27
|
+
dialogs, plurals: --more.
|
|
27
28
|
- **Data:** `ui.query(listItems, input, { ready: (items) => …, failed: { NotFound: () => …, Unexpected: () => … } })`;
|
|
28
|
-
`failed` lists every declared error plus `Unexpected`.
|
|
29
|
-
parent) and update by key; `pending` shows only before the first answer.
|
|
29
|
+
`failed` lists every declared error plus `Unexpected`.
|
|
30
30
|
- **Shared UI** (buttons, inputs, fields): `ui.use(Button, { variant, props, on }, ['Save'])` of a kit component
|
|
31
31
|
(`hozu docs components`).
|
|
32
32
|
|
|
@@ -63,3 +63,13 @@ export const Board = ui.view({
|
|
|
63
63
|
(`hozu docs components`; `examples/showcase` has a Chart.js one).
|
|
64
64
|
- **Also:** `ui.html(post.html)` (trusted HTML from query data only, HZ030), `ui.asset(new URL('./x.png',
|
|
65
65
|
import.meta.url))`, `ui.window({ on })` / `ui.document({ on })`, `ui.embed(OtherView)`.
|
|
66
|
+
|
|
67
|
+
## 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
|
+
- A link to the page shown gets `aria-current="page"` (`"true"` for its section): `aria-[current]:font-bold`.
|
|
71
|
+
- `ui.dialog({ open: is(['editing']), on: { close: ui.send(Cancel, {}) } }, [...])` opens as a modal and closes with
|
|
72
|
+
the machine; Escape sends `close`.
|
|
73
|
+
- `ui.format.plural(n, { one: '# item', other: '# items' })` picks the case for the page's language (`=0` works).
|
|
74
|
+
- `null` and `false` render nothing, also inside a constant list:
|
|
75
|
+
`ui.ul({}, [...kinds.map((k) => (k === 'draft' ? null : ui.li({}, [k])))])`.
|