@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.
Files changed (74) hide show
  1. package/bin/hozu.js +2 -2
  2. package/dist/commands/add.d.ts.map +1 -1
  3. package/dist/commands/add.js +10 -7
  4. package/dist/commands/add.js.map +1 -1
  5. package/dist/commands/app.d.ts +3 -0
  6. package/dist/commands/app.d.ts.map +1 -1
  7. package/dist/commands/app.js +23 -0
  8. package/dist/commands/app.js.map +1 -1
  9. package/dist/commands/browse-tab.d.ts +4 -2
  10. package/dist/commands/browse-tab.d.ts.map +1 -1
  11. package/dist/commands/browse-tab.js +36 -5
  12. package/dist/commands/browse-tab.js.map +1 -1
  13. package/dist/commands/browse-world.d.ts +3 -0
  14. package/dist/commands/browse-world.d.ts.map +1 -1
  15. package/dist/commands/browse-world.js +2 -1
  16. package/dist/commands/browse-world.js.map +1 -1
  17. package/dist/commands/browse.d.ts +1 -0
  18. package/dist/commands/browse.d.ts.map +1 -1
  19. package/dist/commands/browse.js +50 -11
  20. package/dist/commands/browse.js.map +1 -1
  21. package/dist/commands/check.d.ts.map +1 -1
  22. package/dist/commands/check.js +6 -1
  23. package/dist/commands/check.js.map +1 -1
  24. package/dist/commands/gen.d.ts +6 -0
  25. package/dist/commands/gen.d.ts.map +1 -0
  26. package/dist/commands/gen.js +53 -0
  27. package/dist/commands/gen.js.map +1 -0
  28. package/dist/commands/request.d.ts +6 -1
  29. package/dist/commands/request.d.ts.map +1 -1
  30. package/dist/commands/request.js +60 -6
  31. package/dist/commands/request.js.map +1 -1
  32. package/dist/commands/scaffold.d.ts.map +1 -1
  33. package/dist/commands/scaffold.js +6 -7
  34. package/dist/commands/scaffold.js.map +1 -1
  35. package/dist/commands/serve.d.ts.map +1 -1
  36. package/dist/commands/serve.js +11 -3
  37. package/dist/commands/serve.js.map +1 -1
  38. package/dist/contract.d.ts +40 -3
  39. package/dist/contract.d.ts.map +1 -1
  40. package/dist/gen/go.d.ts +5 -0
  41. package/dist/gen/go.d.ts.map +1 -0
  42. package/dist/gen/go.js +369 -0
  43. package/dist/gen/go.js.map +1 -0
  44. package/dist/main.d.ts +1 -0
  45. package/dist/main.d.ts.map +1 -1
  46. package/dist/main.js +28 -2
  47. package/dist/main.js.map +1 -1
  48. package/dist/migrate/steps.d.ts.map +1 -1
  49. package/dist/migrate/steps.js +17 -0
  50. package/dist/migrate/steps.js.map +1 -1
  51. package/dist/remote.d.ts +15 -0
  52. package/dist/remote.d.ts.map +1 -0
  53. package/dist/remote.js +58 -0
  54. package/dist/remote.js.map +1 -0
  55. package/package.json +8 -8
  56. package/schema/browse.schema.json +55 -4
  57. package/schema/check.schema.json +2 -1
  58. package/schema/gen.schema.json +61 -0
  59. package/schema/inspect.schema.json +20 -0
  60. package/schema/migrate.schema.json +2 -1
  61. package/schema/render.schema.json +2 -1
  62. package/schema/request.schema.json +35 -1
  63. package/skill/example/features/bookmarks/model.ts +0 -7
  64. package/skill/example/features/bookmarks/views.ts +3 -5
  65. package/skill/topics/data.md +49 -0
  66. package/skill/topics/diagnostics.md +1 -0
  67. package/skill/topics/forms.md +2 -2
  68. package/skill/topics/http.md +2 -1
  69. package/skill/topics/machine.md +5 -6
  70. package/skill/topics/pages.md +1 -1
  71. package/skill/topics/patterns.md +9 -8
  72. package/skill/topics/recipes.md +22 -0
  73. package/skill/topics/testing.md +8 -4
  74. package/skill/topics/views.md +13 -3
@@ -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.send(Search, { text:
24
- ui.dom.value }) }`, filter and test emptiness with a `fn` (see --more).
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.send(Search, { text: ui.dom.value }) } })`; `on(Search, { target: 'idle', assign: (e) =>
46
- { ctx.search = e.text } })`; filter with a `fn({ input: z.object({ items, text: z.string() }), … })`.
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.send(SetShow, { show: s.value }) } }, [s.label])`.
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): put what every mode handles the same way in
59
- `machine({ on: [on(Search, { assign: (e) => { ctx.q = e.q } })] })` (no `target`: stays in its state); each state
60
- lists only what differs.
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
@@ -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.
@@ -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>`, `select <label>=<option>`, `check` / `uncheck <label>`, `click <name>`,
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
- - **A calm page:** a step that rebuilds elements unchanged says `N elements rebuilt unchanged (a flash)` (a control
33
- hidden while busy: disable it instead), and layout that moves without input says `layout shift X`. Both are
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.
@@ -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`. When the input changes, the rows stay (`aria-busy` on the
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])))])`.