create-hozu 0.4.1 → 0.5.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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +68 -154
- package/skill/changing.md +15 -18
- package/skill/example/features/bookmarks/model.ts +51 -23
- package/skill/example/features/bookmarks/views.ts +10 -14
- package/skill/topics/auth.md +12 -0
- package/skill/topics/content.md +14 -0
- package/skill/topics/contracts.md +21 -0
- package/skill/topics/data.md +35 -0
- package/skill/topics/deploy.md +12 -0
- package/skill/{diagnostics.md → topics/diagnostics.md} +7 -4
- package/skill/topics/endpoints.md +12 -0
- package/skill/topics/env.md +5 -0
- package/skill/topics/forms.md +24 -0
- package/skill/topics/http.md +17 -0
- package/skill/topics/i18n.md +11 -0
- package/skill/topics/machine.md +40 -0
- package/skill/topics/pages.md +39 -0
- package/skill/topics/patterns.md +45 -0
- package/skill/topics/testing.md +12 -0
- package/skill/topics/views.md +34 -0
- package/skill/topics/widgets.md +25 -0
- package/skill/patterns.md +0 -57
- package/skill/reference.md +0 -172
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AAChD,MAAM,MAAM,MAAM,GAAG,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAA;AAE1D,eAAO,MAAM,MAAM,EAAE,SAAS,KAAK,EAAiC,CAAA;AAGpE,eAAO,MAAM,YAAY,QAAsB,CAAA;AAiB/C,eAAO,MAAM,QAAQ,cAAe,MAAM,GAAG,SAAS,KAAG,MAGxD,CAAA;AAWD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAwBzG;AAYD,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,KAAK,EAAE,KAAK,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,eAAO,MAAM,WAAW,SAAU,MAAM,WAAW,MAAM;;;;;QAKrD,KAAK;QACL,KAAK;QACL,KAAK;;;QAGL,oBAAoB;QACpB,YAAY;QACZ,WAAW;QACX,YAAY;QACZ,sBAAsB;QACtB,kBAAkB;QAClB,GAAG;;;QAGH,WAAW;QACX,eAAe;QACf,aAAa;QACb,UAAU;;;QAED,IAAI;;CACf,CAAA;AAEF,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBtF"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AAChD,MAAM,MAAM,MAAM,GAAG,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAA;AAE1D,eAAO,MAAM,MAAM,EAAE,SAAS,KAAK,EAAiC,CAAA;AAGpE,eAAO,MAAM,YAAY,QAAsB,CAAA;AAiB/C,eAAO,MAAM,QAAQ,cAAe,MAAM,GAAG,SAAS,KAAG,MAGxD,CAAA;AAWD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAwBzG;AAYD,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,KAAK,EAAE,KAAK,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,eAAO,MAAM,WAAW,SAAU,MAAM,WAAW,MAAM;;;;;QAKrD,KAAK;QACL,KAAK;QACL,KAAK;;;QAGL,oBAAoB;QACpB,YAAY;QACZ,WAAW;QACX,YAAY;QACZ,sBAAsB;QACtB,kBAAkB;QAClB,iBAAiB;QACjB,GAAG;;;QAGH,WAAW;QACX,eAAe;QACf,aAAa;QACb,UAAU;;;QAED,IAAI;;CACf,CAAA;AAEF,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBtF"}
|
package/dist/index.js
CHANGED
|
@@ -65,7 +65,7 @@ export const packageJson = (name, version) => ({
|
|
|
65
65
|
type: 'module',
|
|
66
66
|
scripts: {
|
|
67
67
|
check: 'hozu check',
|
|
68
|
-
start: 'node serve.ts',
|
|
68
|
+
start: 'node --import @hozu/transform/register serve.ts',
|
|
69
69
|
build: 'hozu build',
|
|
70
70
|
},
|
|
71
71
|
dependencies: {
|
|
@@ -75,6 +75,7 @@ export const packageJson = (name, version) => ({
|
|
|
75
75
|
'@hozu/data': `^${version}`,
|
|
76
76
|
'@hozu/runtime-server': `^${version}`,
|
|
77
77
|
'@hozu/schema-zod': `^${version}`,
|
|
78
|
+
'@hozu/transform': `^${version}`,
|
|
78
79
|
zod: '^4.6.5',
|
|
79
80
|
},
|
|
80
81
|
devDependencies: {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACpF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAKxC,MAAM,CAAC,MAAM,MAAM,GAAqB,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA;AAEpE,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,KAAK,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AAC3D,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAC/C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;AAEzC,MAAM,OAAO,GAAG,CAAC,KAAY,EAAE,EAAE,CAC/B;IACE;QACE,KAAK,EAAE,WAAW;QAClB,KAAK,EAAE,qBAAqB;QAC5B,IAAI,EAAE,8CAA8C;KACrD;IACD;QACE,KAAK,EAAE,WAAW;QAClB,KAAK,EAAE,qBAAqB;QAC5B,IAAI,EAAE,qEAAqE;KAC5E;CACF,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAElF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,SAA6B,EAAU,EAAE;IAChE,MAAM,IAAI,GAAG,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IACrC,OAAO,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAA;AAClG,CAAC,CAAA;AAED,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,EAAE,CAC9B,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CACb,GAAG,EAAE,CAAC,IAAI,EACV,GAAG,EAAE,CAAC,KAAK,CACZ,CAAA;AAEH,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,MAA8B,EAAE,EAAE,CAC5D,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,CAAA;AAQjG,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAW,EAAE,KAAY,EAAE,OAAqB;IACpF,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,CAAA;IACjE,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAA;QAChC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACjD,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAChD,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,IAAI,YAAY,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACzE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAA;QAC/B,IAAI,MAAM,MAAM,CAAC,IAAI,CAAC;YAAE,SAAQ;QAChC,MAAM,MAAM,GAAG;YACb,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,GAAG,EAAE,OAAO,CAAC,MAAM;YACnB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,IAAI,EACF,OAAO,CAAC,MAAM,KAAK,WAAW;gBAC5B,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,qEAAqE,OAAO,CAAC,MAAM,SAAS;SACnG,CAAA;QACD,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAA;QAClE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,KAAK,UAAU,KAAK,CAAC,GAAW;IAC9B,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QAClC,IAAI,KAAK,CAAC,WAAW,EAAE;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;;YACpD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAOD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAE,OAAe,EAAE,EAAE,CAAC,CAAC;IAC7D,IAAI;IACJ,OAAO,EAAE,IAAI;IACb,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE;QACP,KAAK,EAAE,YAAY;QACnB,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -3,190 +3,104 @@ name: hozu
|
|
|
3
3
|
description: Build or change an app with the Hozu framework (packages @hozu/*, files like hozu.config.ts, features/*/model.ts, views.ts). Use it before writing any Hozu code. It is the complete authoring reference, so you do not need to read the framework source.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Hozu
|
|
6
|
+
# Hozu
|
|
7
7
|
|
|
8
|
-
Hozu is not in your training data
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
|
|
12
|
-
- **`reference.md`** when the task needs it: routes, DOM fields, no-JS forms, `head`, 404/500, field errors,
|
|
13
|
-
sessions, languages, env, HTTP, Markdown, images, preview, PWA, page tests, deployment.
|
|
14
|
-
- **A diagnostic you do not understand:** `diagnostics.md`.
|
|
8
|
+
Hozu is not in your training data; this file and `hozu docs <topic>` are the whole API (skip `node_modules/@hozu`).
|
|
9
|
+
- **Building:** read this file, run `hozu add feature <name> --page / --with …`, then edit what it lists.
|
|
10
|
+
- **Changing:** read `changing.md`, then only the lines `hozu map` points to.
|
|
11
|
+
- **Anything else:** `hozu docs <topic>` prints one short topic (index below). Diagnostics name the topic too.
|
|
15
12
|
|
|
16
|
-
##
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
`ui.link(route, params, search)` spell "none" as `null`.
|
|
13
|
+
## How it works
|
|
14
|
+
- A feature is **declarations**: events, queries / mutations (the only side effects), one machine, views,
|
|
15
|
+
contracts. Builders record them as data (an IR) that is validated, then rendered on the server. Only views bound
|
|
16
|
+
to the machine ship JS.
|
|
17
|
+
- **Callbacks are ordinary TypeScript** (`render`, `guard`, `assign`, `navigate`, `ui.each` / `ui.query`
|
|
18
|
+
callbacks): `===`, `!==`, `<`, `&&`, `||`, `!`, `??`, `c ? a : b`, template strings, `+`, `-`, `.length`, and in
|
|
19
|
+
`assign`, `ctx.x = v`, `ctx.n += 1`, `ctx.list.push(v)`, `ctx.list = ctx.list.filter((i) => i.id !== e.id)`.
|
|
20
|
+
Methods on data (`.map`, `.toUpperCase()`…) are not: use `ui.each` for lists and a `fn()` for computation.
|
|
21
|
+
- A mutation runs when the machine **enters** a state whose `invoke` calls it; that state drops other events, and
|
|
22
|
+
`done` / `failed` leave it. Contracts are needed only where a transition decides (a guard, `navigate`, a `fn`).
|
|
27
23
|
|
|
28
|
-
## Files
|
|
24
|
+
## Files and commands
|
|
29
25
|
```
|
|
30
|
-
hozu.config.ts project(
|
|
31
|
-
|
|
32
|
-
server.ts resolvers(
|
|
33
|
-
serve.ts createServer({ build, styles, resolvers }).listen(PORT)
|
|
34
|
-
app.css @import "tailwindcss";
|
|
35
|
-
features/<name>/
|
|
36
|
-
model.ts schemas, events, query / mutation / tag / fn, the machine
|
|
37
|
-
views.ts views, contracts, feature()
|
|
26
|
+
hozu.config.ts project({ schema, site, routes, pages, features }) routes.ts route() declarations
|
|
27
|
+
features/<name>/model.ts schemas, events, effects, machine views.ts views, contracts, feature()
|
|
28
|
+
features/<name>/server.ts implement(...) resolvers serve.ts createServer(...)
|
|
38
29
|
```
|
|
39
|
-
|
|
40
|
-
## Commands (from the app directory)
|
|
41
30
|
```
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
npx hozu check # after every edit: types, rules, contracts
|
|
32
|
+
npx hozu check --update-lock # accept an intended behaviour change
|
|
33
|
+
npx hozu add feature items --page / --with auth,detail,toggle,filter,remove
|
|
34
|
+
npx hozu map # outline with file:line
|
|
35
|
+
npx hozu get / --select button --forms # a page, no server needed
|
|
36
|
+
npx hozu post / --field title=A --next / # a no-JS form post, cookies shown
|
|
37
|
+
npx hozu docs views # one topic
|
|
48
38
|
```
|
|
49
|
-
|
|
50
|
-
chain steps with `--next`. Run `node serve.ts` only to use the app. Relative imports end in `.ts`.
|
|
39
|
+
`get` / `post` replace a running server for checks; `npm start` runs the app. Relative imports end in `.ts`.
|
|
51
40
|
|
|
52
|
-
##
|
|
41
|
+
## A feature in one screen
|
|
53
42
|
```ts
|
|
54
|
-
|
|
43
|
+
// model.ts
|
|
44
|
+
export const Item = z.object({ id: z.string(), title: z.string(), done: z.boolean() })
|
|
55
45
|
export const Add = event({ payload: z.object({ title: z.string() }) })
|
|
56
|
-
|
|
57
|
-
export const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
tags: () => [itemsTag()], // optional; (input) => [...]
|
|
63
|
-
})
|
|
64
|
-
export const getItem = query({ input: Key, output: Item, errors: { NotFound: Key }, … })
|
|
65
|
-
export const addItem = mutation({
|
|
66
|
-
input: z.object({ title: z.string().min(2, 'Use at least 2 characters') }), output: Item,
|
|
67
|
-
errors: { Duplicate: z.object({ title: z.string() }) }, // optional: declared failures
|
|
68
|
-
invalidates: () => [itemsTag()], // refreshes queries with these tags
|
|
69
|
-
})
|
|
70
|
-
export const unread = fn({ // pure JS, self-contained: no imports or closures
|
|
71
|
-
input: z.object({ items: z.array(Item) }), output: z.array(Item),
|
|
72
|
-
impl: ({ items }) => items.filter((i) => !i.read),
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
export const m = machine({
|
|
46
|
+
export const itemsTag = tag({ param: null })
|
|
47
|
+
export const listItems = query({ input: z.object({}), output: z.array(Item), scope: 'public',
|
|
48
|
+
freshness: 'static', tags: () => [itemsTag()] })
|
|
49
|
+
export const addItem = mutation({ input: z.object({ title: z.string().min(2, 'Too short') }), output: Item,
|
|
50
|
+
errors: { Duplicate: z.object({ title: z.string() }) }, invalidates: () => [itemsTag()] })
|
|
51
|
+
export const items = machine({
|
|
76
52
|
context: z.object({ draft: z.string(), error: z.string().nullable() }),
|
|
77
53
|
initialContext: { draft: '', error: null },
|
|
78
54
|
initial: 'idle',
|
|
79
55
|
states: ({ ctx }) => ({
|
|
80
|
-
idle: {
|
|
81
|
-
on: [
|
|
82
|
-
on(Draft, { target: 'idle', assign: (e) => [op.set(ctx.draft, e.text)] }),
|
|
83
|
-
on(Add, { target: 'adding', guard: (e) => op.neq(e.title, ''), // first matching guard wins
|
|
84
|
-
assign: (e) => [op.set(ctx.draft, e.title), op.set(ctx.error, null)] }),
|
|
85
|
-
],
|
|
86
|
-
},
|
|
56
|
+
idle: { on: [on(Add, { target: 'adding', assign: (e) => { ctx.draft = e.title; ctx.error = null } })] },
|
|
87
57
|
adding: {
|
|
88
|
-
|
|
89
|
-
invoke: invoke(addItem, { // runs on entering the state
|
|
58
|
+
invoke: invoke(addItem, {
|
|
90
59
|
input: { title: ctx.draft },
|
|
91
|
-
done:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Unexpected: [{ target: 'idle', assign: (e) => [op.set(ctx.error, e.message)] }],
|
|
60
|
+
done: { target: 'idle', assign: () => { ctx.draft = '' } },
|
|
61
|
+
failed: {
|
|
62
|
+
Duplicate: { target: 'idle', assign: () => { ctx.error = 'Already listed' } },
|
|
63
|
+
Unexpected: { target: 'idle', assign: (e) => { ctx.error = e.message } },
|
|
96
64
|
},
|
|
97
65
|
}),
|
|
98
66
|
},
|
|
99
|
-
flash: { after: [{ ms: 3000, target: 'idle' }] }, // timers; `final: true` for terminal states
|
|
100
67
|
}),
|
|
101
68
|
})
|
|
102
|
-
|
|
103
|
-
- `op.set`, `op.append(list, item)`, `op.inc(n, by)`, `op.removeWhere(list, 'key', value)`.
|
|
104
|
-
- Guards: `op.eq / neq / lt / lte / gt / gte`, `op.and(...)`, `op.or(...)`, `op.not(g)`, or a boolean `fn`. The
|
|
105
|
-
reference goes on the left: `op.eq(ctx.tab, 'design')`.
|
|
106
|
-
- A transition to the same state re-enters it and re-runs its `invoke`; busy states `ignore` instead.
|
|
107
|
-
|
|
108
|
-
## views.ts
|
|
109
|
-
```ts
|
|
69
|
+
// views.ts
|
|
110
70
|
export const Board = ui.view({
|
|
111
|
-
machine:
|
|
112
|
-
|
|
113
|
-
render: ({ ctx, when, search }) =>
|
|
71
|
+
machine: items,
|
|
72
|
+
render: ({ ctx, when }) =>
|
|
114
73
|
ui.main({ class: 'mx-auto max-w-xl' }, [
|
|
115
74
|
ui.form({ on: { submit: ui.send(Add, { title: ui.dom.form('title') }) } }, [
|
|
116
|
-
ui.input({ name: 'title', required: true, value: ctx.draft,
|
|
117
|
-
on: { input: ui.send(Draft, { text: ui.dom.value }) } }),
|
|
75
|
+
ui.input({ name: 'title', required: true, value: ctx.draft }),
|
|
118
76
|
ui.button({ type: 'submit' }, ['Add']),
|
|
119
77
|
]),
|
|
120
|
-
|
|
121
|
-
when(['adding'], [ui.p({ 'aria-busy': 'true' }, [
|
|
78
|
+
ctx.error !== null && ui.p({ role: 'alert' }, [ctx.error]),
|
|
79
|
+
when(['adding'], [ui.p({ 'aria-busy': 'true' }, [`Adding ${ctx.draft}…`])]),
|
|
122
80
|
ui.query(listItems, {}, {
|
|
123
|
-
ready: (
|
|
124
|
-
ui.li({}, [ui.a({ href: ui.link(itemPage, { id: i.id }) }, [i.title])]))]),
|
|
125
|
-
pending: ui.p({}, ['Loading…']), // or null
|
|
81
|
+
ready: (list) => ui.ul({}, [ui.each(list, 'id', (i) => ui.li({}, [i.title, i.done ? ' ✓' : '']))]),
|
|
126
82
|
failed: { Unexpected: () => ui.p({ role: 'alert' }, ['Unavailable']) },
|
|
127
83
|
}),
|
|
128
84
|
]),
|
|
129
85
|
})
|
|
86
|
+
export const todos = feature({ id: 'todos', intent: { summary: 'A to-do list' },
|
|
87
|
+
declarations: { Add, itemsTag, listItems, addItem, items, Board } })
|
|
130
88
|
```
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
`toggle: { 'bg-indigo-600 text-white': op.eq(ctx.tab, t) }`. CSS variables: `vars: { '--hue': item.hue }`.
|
|
134
|
-
- Events: `on: { click: ui.send(Event, payload) }`. Payload fields: literals, references, `ui.dom.value`,
|
|
135
|
-
`ui.dom.form('name')`, `ui.dom.checked`, `ui.dom.valueAsNumber`, `ui.dom.key`.
|
|
136
|
-
- Links: `ui.link(route, params, search)`, never a string path (HZ032). The third argument exists only when the
|
|
137
|
-
route declares `search` (`null` = all defaults). Filters that belong in the URL are `search` links, not context.
|
|
138
|
-
- A form whose submit reads only `ui.dom.form(...)`, literals, context, params and search also works without JS.
|
|
139
|
-
|
|
140
|
-
### Contracts
|
|
141
|
-
```ts
|
|
142
|
-
export const adds = contract(m, {
|
|
143
|
-
given: { state: 'idle' }, // context: optional, defaults to initialContext
|
|
144
|
-
when: [
|
|
145
|
-
{ send: Add, payload: { title: 'A' } },
|
|
146
|
-
{ done: addItem, result: { id: 'i9', title: 'A', read: false } },
|
|
147
|
-
], // or { failed: addItem, error: 'Duplicate', data } / { elapse: ms }
|
|
148
|
-
expect: {
|
|
149
|
-
state: 'idle',
|
|
150
|
-
changes: { error: null }, // only what changes; every other field must stay equal
|
|
151
|
-
effects: [{ effect: addItem, input: { title: 'A' } }, { navigate: '/items/i9' }], // optional: none
|
|
152
|
-
},
|
|
153
|
-
})
|
|
154
|
-
```
|
|
155
|
-
Cover each `on`, `done`, `failed` and `after` once. `given.context` sets up a full context; nested objects in
|
|
156
|
-
`changes` are patches too, arrays are replaced. HZ016 prints each missing contract ready to paste.
|
|
157
|
-
|
|
158
|
-
### Feature
|
|
159
|
-
```ts
|
|
160
|
-
export const items = feature({
|
|
161
|
-
id: 'items',
|
|
162
|
-
intent: { summary: 'A reading list', invariants: ['Titles are unique'] }, // invariants optional
|
|
163
|
-
declarations: { Add, Draft, itemsTag, listItems, getItem, addItem, unread, m, Board, Detail, adds },
|
|
164
|
-
})
|
|
165
|
-
```
|
|
166
|
-
Every declaration goes in `declarations` once, under its name. Optional: `imports: [otherFeature]`,
|
|
167
|
-
`exports: [Event, query, …]` (all other features may use), `styles: [new URL('./x.css', import.meta.url)]`.
|
|
168
|
-
|
|
169
|
-
## hozu.config.ts
|
|
170
|
-
`project({ schema: zodAdapter, styles, site: { url, name, lang }, routes: { home, itemPage }, pages, features })`;
|
|
171
|
-
`hozu add feature --page` adds static pages. A page with params:
|
|
172
|
-
```ts
|
|
173
|
-
ui.page(itemPage, {
|
|
174
|
-
views: [Detail],
|
|
175
|
-
head: {
|
|
176
|
-
query: getItem, // its failure sets the status (NotFound → 404)
|
|
177
|
-
input: (params) => ({ id: params.id }),
|
|
178
|
-
render: (item) => ({ title: item.title, description: item.title, type: 'article' }),
|
|
179
|
-
},
|
|
180
|
-
entries: { query: listItems, input: {}, params: (item) => ({ id: item.id }) }, // sitemap
|
|
181
|
-
}),
|
|
182
|
-
```
|
|
183
|
-
```ts
|
|
184
|
-
// routes.ts
|
|
185
|
-
export const home = route({ path: '/', params: null, search: z.object({ show: Show.default('all') }) })
|
|
186
|
-
export const itemPage = route({ path: '/items/:id', params: z.object({ id: z.string() }), search: null })
|
|
187
|
-
```
|
|
89
|
+
Every declaration goes in `declarations` once, under its name. Resolvers:
|
|
90
|
+
`implement(addItem, ({ title }, { fail }) => exists ? fail('Duplicate', { title }) : save(title))`.
|
|
188
91
|
|
|
189
|
-
##
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
92
|
+
## Topics (`hozu docs <topic>`)
|
|
93
|
+
| Task | Topic |
|
|
94
|
+
|---|---|
|
|
95
|
+
| elements, attributes, classes, events, DOM fields, lists, links | `views` |
|
|
96
|
+
| states, events, invoke, timers, guards, updates | `machine` |
|
|
97
|
+
| queries, mutations, tags, errors, `fn()`, resolvers | `data` |
|
|
98
|
+
| writing and checking contracts | `contracts` |
|
|
99
|
+
| routes, params, search, pages, `head`, 404, sitemap | `pages` |
|
|
100
|
+
| forms without JS, field errors, selects | `forms` |
|
|
101
|
+
| sign-in, sessions, per-user data | `auth` |
|
|
102
|
+
| common UI: filters, empty states, per-item actions, load more, optimistic | `patterns` |
|
|
103
|
+
| webhooks and JSON APIs | `endpoints` |
|
|
104
|
+
| browser APIs and DOM libraries (maps, charts) | `widgets` |
|
|
105
|
+
| languages, env, HTTP, Markdown, images, preview, PWA, tests, deployment | `i18n`, `env`, `http`, `content`, `testing`, `deploy` |
|
|
106
|
+
| a diagnostic code | `diagnostics` |
|
package/skill/changing.md
CHANGED
|
@@ -7,7 +7,7 @@ Keep the loop short: map once, edit everything, check once, verify once.
|
|
|
7
7
|
- `pnpm exec hozu map`: every route, query, mutation, event, state, view and contract, each with its `file:line`.
|
|
8
8
|
Open only the lines the change touches. Below, *model* is where the app keeps schemas, events, effects and
|
|
9
9
|
the machine (`model.ts`), and *views* where it keeps views, contracts and `feature()`.
|
|
10
|
-
- The API is in `SKILL.md`. Use a recipe below when one fits;
|
|
10
|
+
- The API is in `SKILL.md`. Use a recipe below when one fits; run `hozu docs <topic>` only for
|
|
11
11
|
something else.
|
|
12
12
|
|
|
13
13
|
## 2. Recipes
|
|
@@ -20,33 +20,28 @@ Names follow `hozu add feature items`: `Item`, `NewItem`, `Add`, `addItem`, `ite
|
|
|
20
20
|
- context: `priority: Priority`, with `priority: 'normal'` in `initialContext`;
|
|
21
21
|
- `fields` gets `priority: z.string().nullable()`, with `priority: null` in `initialContext` and in the `Add`
|
|
22
22
|
assign that resets it;
|
|
23
|
-
- the `Add` assign also gets `
|
|
23
|
+
- the `Add` assign also gets `ctx.priority = e.priority`, and the add `invoke` input becomes
|
|
24
24
|
`{ title: ctx.draft, priority: ctx.priority }`.
|
|
25
25
|
- **views:**
|
|
26
26
|
- the form's submit sends `{ title: ui.dom.form('title'), priority: ui.dom.form('priority') }`;
|
|
27
27
|
- inside the form add
|
|
28
28
|
`ui.select({ name: 'priority', 'aria-label': 'Priority', class: 'rounded border px-2' }, ['low', 'normal', 'high'].map((p) => ui.option({ value: p, selected: p === 'normal' }, [p])))`;
|
|
29
29
|
- in the item: `ui.span({ class: 'text-xs' }, [item.priority])`.
|
|
30
|
-
- **Contracts:**
|
|
31
|
-
|
|
32
|
-
- `rejectsInvalid`'s `data.fields` and `changes.fields` get `priority: null`.
|
|
30
|
+
- **Contracts:** if the app has contracts that send `Add` or return an item, add `priority` to their payloads,
|
|
31
|
+
inputs and results. These transitions only copy values, so they need no new contract.
|
|
33
32
|
- **server:** store `priority` (seed items included) and return it.
|
|
34
33
|
|
|
35
34
|
### An action button that works on many items (e.g. "Clear done")
|
|
36
35
|
- **model:**
|
|
37
36
|
- `export const ClearDone = event({ payload: z.object({}) })`;
|
|
38
37
|
- `export const clearDone = mutation({ input: z.object({}), output: z.object({ removed: z.number() }), invalidates: () => [itemsTag()] })`;
|
|
39
|
-
- in `idle`: `on(ClearDone, { target: 'clearing', assign: () =>
|
|
38
|
+
- in `idle`: `on(ClearDone, { target: 'clearing', assign: () => { ctx.error = null } })`;
|
|
40
39
|
- a state
|
|
41
|
-
`clearing: {
|
|
42
|
-
|
|
40
|
+
`clearing: { invoke: invoke(clearDone, { input: {}, done: 'idle', failed: { Unexpected: { target: 'idle', assign: (e) => { ctx.error = e.message } } } }) }`
|
|
41
|
+
(busy states drop events they do not handle, so no `ignore`).
|
|
43
42
|
- **views:** the control
|
|
44
43
|
`ui.form({ on: { submit: ui.send(ClearDone, {}) } }, [ui.button({ type: 'submit', class: 'text-sm underline' }, ['Clear done'])])`.
|
|
45
|
-
-
|
|
46
|
-
- `given: { state: 'idle' }`, when `[{ send: ClearDone, payload: {} }, { done: clearDone, result: { removed: 1 } }]`,
|
|
47
|
-
expect `{ state: 'idle', effects: [{ effect: clearDone, input: {} }] }`;
|
|
48
|
-
- a second one for `failed … 'Unexpected'` from `clearing`;
|
|
49
|
-
- add `ClearDone`, `clearDone` and both contracts to `declarations`.
|
|
44
|
+
- Add `ClearDone` and `clearDone` to `declarations`. The new transitions only copy values, so they need no contract.
|
|
50
45
|
- **server:**
|
|
51
46
|
`implement(clearDone, () => { const before = items.length; items.splice(0, items.length, ...items.filter((i) => !i.done)); return { removed: before - items.length } })`.
|
|
52
47
|
- **Try it:** `hozu post / --button 'Clear done' --next /`.
|
|
@@ -66,20 +61,22 @@ Run a fresh scaffold into a scratch app with `--with detail`, and copy the parts
|
|
|
66
61
|
### Other changes
|
|
67
62
|
| Change | Touch |
|
|
68
63
|
|---|---|
|
|
69
|
-
| New UI-only state (a tab) | model: the context field and its initial value, an event, an `on`
|
|
64
|
+
| New UI-only state (a tab) | model: the context field and its initial value, an event, an `on` whose `assign` sets it → views: the control, the event in `declarations` (no contract: it only copies a value). |
|
|
70
65
|
| Filter / sort / page in the URL | the route's `search` schema (with a default) → links with `ui.link(route, params, { key: value })` → read `search.key` in the view. No machine change. |
|
|
71
66
|
| New page | `routes.ts` → a view with `route`, in `declarations` → `ui.page(...)` in `hozu.config.ts` (`head`, and `entries` when the route has params). |
|
|
72
67
|
|
|
73
68
|
Whenever the machine changes:
|
|
74
|
-
-
|
|
75
|
-
|
|
69
|
+
- A transition that decides something (a guard, `navigate`, or a `fn` in its values) needs a contract; HZ016
|
|
70
|
+
prints each missing one ready to paste. Transitions that only copy values are reviewed through the lock.
|
|
71
|
+
- States with `invoke` drop unhandled events by themselves. Other states must handle or `ignore` every event their
|
|
72
|
+
visible controls send; HZ005 prints the missing entries.
|
|
76
73
|
|
|
77
74
|
## 3. Check (once, after all edits)
|
|
78
75
|
```
|
|
79
76
|
pnpm exec hozu check
|
|
80
77
|
```
|
|
81
|
-
Fix what it reports. When the behaviour change is intended
|
|
82
|
-
`
|
|
78
|
+
Fix what it reports. When the behaviour change is intended, run `pnpm exec hozu check --update-lock`: HZ018 shows
|
|
79
|
+
each changed transition as `was: … now: …`, and accepting it updates the lock.
|
|
83
80
|
|
|
84
81
|
## 4. Verify (once, no server needed)
|
|
85
82
|
- **Pages:** `pnpm exec hozu get / /items/i1` prints the status, title, alerts and visible text.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { event, fn, invoke, machine, mutation, on,
|
|
1
|
+
import { event, fn, invoke, machine, mutation, on, query, tag, ui } from '@hozu/core'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { bookmarkPage, Show } from '../../routes.ts'
|
|
4
4
|
|
|
@@ -83,45 +83,73 @@ export const bookmarksMachine = machine({
|
|
|
83
83
|
states: ({ ctx }) => ({
|
|
84
84
|
idle: {
|
|
85
85
|
on: [
|
|
86
|
-
on(Draft, {
|
|
86
|
+
on(Draft, {
|
|
87
|
+
target: 'idle',
|
|
88
|
+
assign: (e) => {
|
|
89
|
+
ctx.draft = e.text
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
87
92
|
on(Add, {
|
|
88
93
|
target: 'adding',
|
|
89
|
-
assign: (e) =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
assign: (e) => {
|
|
95
|
+
ctx.draft = e.title
|
|
96
|
+
ctx.kind = e.kind
|
|
97
|
+
ctx.error = null
|
|
98
|
+
ctx.fields = { title: null, kind: null }
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
on(ToggleRead, {
|
|
102
|
+
target: 'toggling',
|
|
103
|
+
assign: (e) => {
|
|
104
|
+
ctx.target = e.id
|
|
105
|
+
},
|
|
95
106
|
}),
|
|
96
|
-
on(ToggleRead, { target: 'toggling', assign: (e) => [op.set(ctx.target, e.id)] }),
|
|
97
107
|
],
|
|
98
108
|
},
|
|
99
109
|
adding: {
|
|
100
|
-
ignore: [Draft, Add, ToggleRead],
|
|
101
110
|
invoke: invoke(addBookmark, {
|
|
102
111
|
input: { title: ctx.draft, kind: ctx.kind },
|
|
103
|
-
done:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
navigate: (b) => ui.link(bookmarkPage, { id: b.id }),
|
|
112
|
+
done: {
|
|
113
|
+
target: 'idle',
|
|
114
|
+
assign: () => {
|
|
115
|
+
ctx.draft = ''
|
|
108
116
|
},
|
|
109
|
-
|
|
117
|
+
navigate: (b) => ui.link(bookmarkPage, { id: b.id }),
|
|
118
|
+
},
|
|
110
119
|
failed: {
|
|
111
|
-
Duplicate:
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
Duplicate: {
|
|
121
|
+
target: 'idle',
|
|
122
|
+
assign: () => {
|
|
123
|
+
ctx.error = DUPLICATE
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
Invalid: {
|
|
127
|
+
target: 'idle',
|
|
128
|
+
assign: (e) => {
|
|
129
|
+
ctx.fields = e.fields
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
Unexpected: {
|
|
133
|
+
target: 'idle',
|
|
134
|
+
assign: (e) => {
|
|
135
|
+
ctx.error = e.message
|
|
136
|
+
},
|
|
137
|
+
},
|
|
114
138
|
},
|
|
115
139
|
}),
|
|
116
140
|
},
|
|
117
141
|
toggling: {
|
|
118
|
-
ignore: [Draft, Add, ToggleRead],
|
|
119
142
|
invoke: invoke(toggleRead, {
|
|
120
143
|
input: { id: ctx.target },
|
|
121
|
-
done:
|
|
144
|
+
done: 'idle',
|
|
122
145
|
failed: {
|
|
123
|
-
NotFound:
|
|
124
|
-
Unexpected:
|
|
146
|
+
NotFound: 'idle',
|
|
147
|
+
Unexpected: {
|
|
148
|
+
target: 'idle',
|
|
149
|
+
assign: (e) => {
|
|
150
|
+
ctx.error = e.message
|
|
151
|
+
},
|
|
152
|
+
},
|
|
125
153
|
},
|
|
126
154
|
}),
|
|
127
155
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { contract, feature,
|
|
1
|
+
import { contract, feature, ui } from '@hozu/core'
|
|
2
2
|
import { bookmarkPage, home } from '../../routes.ts'
|
|
3
3
|
import {
|
|
4
4
|
Add,
|
|
@@ -41,20 +41,20 @@ export const Board = ui.view({
|
|
|
41
41
|
minlength: 2,
|
|
42
42
|
maxlength: 80,
|
|
43
43
|
value: ctx.draft,
|
|
44
|
-
'aria-invalid':
|
|
44
|
+
'aria-invalid': ctx.fields.title !== null,
|
|
45
45
|
'aria-describedby': 'title-error',
|
|
46
46
|
class: 'flex-1 rounded border px-3 py-2',
|
|
47
47
|
on: { input: ui.send(Draft, { text: ui.dom.value }) },
|
|
48
48
|
}),
|
|
49
49
|
ui.select(
|
|
50
50
|
{ name: 'kind', 'aria-label': 'Kind', class: 'rounded border px-2' },
|
|
51
|
-
kinds.map((k) => ui.option({ value: k, selected:
|
|
51
|
+
kinds.map((k) => ui.option({ value: k, selected: ctx.kind === k }, [k])),
|
|
52
52
|
),
|
|
53
53
|
ui.button({ type: 'submit', class: 'rounded bg-indigo-600 px-4 py-2 text-white' }, ['Add']),
|
|
54
54
|
],
|
|
55
55
|
),
|
|
56
56
|
ui.p({ id: 'title-error', class: 'text-sm text-rose-600' }, [ctx.fields.title]),
|
|
57
|
-
|
|
57
|
+
ctx.error !== null && ui.p({ role: 'alert', class: 'text-rose-600' }, [ctx.error]),
|
|
58
58
|
when(
|
|
59
59
|
['adding'],
|
|
60
60
|
[
|
|
@@ -71,7 +71,7 @@ export const Board = ui.view({
|
|
|
71
71
|
ui.a(
|
|
72
72
|
{
|
|
73
73
|
href: ui.link(home, null, { show: s.value }),
|
|
74
|
-
'aria-current':
|
|
74
|
+
'aria-current': search.show === s.value,
|
|
75
75
|
class:
|
|
76
76
|
'rounded-full border px-3 py-1 aria-[current=true]:bg-indigo-600 aria-[current=true]:text-white',
|
|
77
77
|
},
|
|
@@ -84,11 +84,9 @@ export const Board = ui.view({
|
|
|
84
84
|
{},
|
|
85
85
|
{
|
|
86
86
|
ready: (items) =>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
[
|
|
91
|
-
ui.ul({ class: 'divide-y rounded border' }, [
|
|
87
|
+
isEmpty({ items, show: search.show })
|
|
88
|
+
? ui.p({ class: 'text-slate-500' }, ['No bookmarks'])
|
|
89
|
+
: ui.ul({ class: 'divide-y rounded border' }, [
|
|
92
90
|
ui.each(visible({ items, show: search.show }), 'id', (b) =>
|
|
93
91
|
ui.li({ class: 'flex items-center gap-3 px-4 py-3' }, [
|
|
94
92
|
ui.a({ href: ui.link(bookmarkPage, { id: b.id }), class: 'flex-1 underline' }, [
|
|
@@ -101,13 +99,11 @@ export const Board = ui.view({
|
|
|
101
99
|
class: 'text-sm',
|
|
102
100
|
on: { click: ui.send(ToggleRead, { id: b.id }) },
|
|
103
101
|
},
|
|
104
|
-
[
|
|
102
|
+
[b.read ? 'Mark unread' : 'Mark read'],
|
|
105
103
|
),
|
|
106
104
|
]),
|
|
107
105
|
),
|
|
108
106
|
]),
|
|
109
|
-
],
|
|
110
|
-
),
|
|
111
107
|
pending: ui.p({}, ['Loading…']),
|
|
112
108
|
failed: { Unexpected: () => ui.p({ role: 'alert' }, ['Bookmarks are unavailable']) },
|
|
113
109
|
},
|
|
@@ -127,7 +123,7 @@ export const Detail = ui.view({
|
|
|
127
123
|
ui.article({}, [
|
|
128
124
|
ui.h1({ class: 'text-3xl font-bold' }, [b.title]),
|
|
129
125
|
ui.p({}, ['Kind: ', b.kind]),
|
|
130
|
-
ui.p({}, [
|
|
126
|
+
ui.p({}, [b.read ? 'Read' : 'Unread']),
|
|
131
127
|
]),
|
|
132
128
|
pending: null,
|
|
133
129
|
failed: {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Sign-in, sessions, per-user data
|
|
2
|
+
|
|
3
|
+
- **Start from the scaffold:** `hozu add feature notes --page / --with auth` writes `features/account` (sign-in page,
|
|
4
|
+
sign-out, `me`), the session cookie in `serve.ts`, per-user resolvers, and a redirect to `/login` when signed out.
|
|
5
|
+
Replace the name-only sign-in with real credentials before production; set `SESSION_SECRET` (and
|
|
6
|
+
`SESSION_SECURE=true` behind HTTPS).
|
|
7
|
+
- `project({ session: z.object({ user: z.string() }) })` declares the identity. Queries with `scope: 'user'` and all
|
|
8
|
+
mutations receive `session`; public queries never do. A mutation calls `setSession(value)` (or `null` to sign out).
|
|
9
|
+
- `createServer({ session: sessionCookie({ name: 'sid', secret }) })` (from `@hozu/runtime-server`): a signed,
|
|
10
|
+
HttpOnly cookie. The session never reaches browser JavaScript.
|
|
11
|
+
- Guard pages: `head: { query: me, redirects: { Unauthorized: login }, … }`.
|
|
12
|
+
- Calling another API with a token: keep the token in the session (server side) and read it in the resolver.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Markdown, images, share images, fonts, preview, offline
|
|
2
|
+
|
|
3
|
+
- **Markdown:** `@hozu/content`: `const posts = await loadCollection({ dir: new URL('./content/posts/', import.meta.url), schema })`
|
|
4
|
+
in `server.ts` gives `{ slug, data, html, headings }`; return it from query resolvers and render `ui.html(post.html)`.
|
|
5
|
+
- **Images:** `ui.img({ src: ui.asset(new URL('./hero.jpg', import.meta.url)), alt, width, height })` (HZ028 without
|
|
6
|
+
dimensions). With `@hozu/image`, `hozu build` adds WebP `srcset` widths.
|
|
7
|
+
- **Share images:** `head.render → image: ui.og({ title, subtitle })` (needs `og: ogImage` from `@hozu/image` in
|
|
8
|
+
`createServer`); on a static host use `image: ui.asset(new URL('./share.png', import.meta.url))`.
|
|
9
|
+
- **Fonts:** a local `@font-face` gets a size-matched fallback automatically.
|
|
10
|
+
- **Page transitions:** links cross-fade (CSS view transitions, no JS); turn off with
|
|
11
|
+
`@view-transition { navigation: none; }` in `app.css`.
|
|
12
|
+
- **Preview:** `createServer({ preview: { secret } })`; `/_hozu/preview?secret=…&path=/posts/a` turns it on; resolvers
|
|
13
|
+
read `ctx.preview`; preview responses are never cached.
|
|
14
|
+
- **Offline:** `site.offline: route` (a static page) makes a service worker (HZ043).
|