@plitzi/sdk-server 0.38.6 → 0.38.8

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 (88) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +102 -16
  3. package/dist/adapters/jsonAdapters.d.ts +8 -4
  4. package/dist/adapters/jsonAdapters.js +5 -9
  5. package/dist/core/auth/api.d.ts +4 -1
  6. package/dist/core/auth/api.js +14 -5
  7. package/dist/core/auth/createAuth.d.ts +7 -1
  8. package/dist/core/auth/createAuth.js +14 -0
  9. package/dist/core/auth/credentials.d.ts +2 -0
  10. package/dist/core/auth/credentials.js +4 -2
  11. package/dist/core/auth/session.d.ts +2 -0
  12. package/dist/core/auth/session.js +6 -5
  13. package/dist/core/createServer.js +25 -4
  14. package/dist/core/http/stages/devReload.d.ts +2 -2
  15. package/dist/core/http/stages/devReload.js +2 -2
  16. package/dist/env.d.ts +1 -0
  17. package/dist/env.js +19 -0
  18. package/dist/env.test.d.ts +1 -0
  19. package/dist/functions-api.d.ts +5 -0
  20. package/dist/helpers/buildServerInfo.js +6 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/modules/actions/runtime/projectData.d.ts +18 -0
  23. package/dist/modules/actions/runtime/projectData.js +69 -0
  24. package/dist/modules/actions/runtime/taskContext.d.ts +1 -1
  25. package/dist/modules/actions/runtime/taskContext.js +6 -3
  26. package/dist/modules/actions/types.d.ts +7 -1
  27. package/dist/modules/functions/build.js +10 -0
  28. package/dist/modules/functions/capabilities.js +5 -0
  29. package/dist/modules/functions/context.js +10 -2
  30. package/dist/modules/functions/contract.d.ts +5 -0
  31. package/dist/modules/functions/driver.js +4 -0
  32. package/dist/modules/functions/local.d.ts +3 -1
  33. package/dist/modules/functions/local.js +4 -2
  34. package/dist/modules/functions/protocol.d.ts +3 -0
  35. package/dist/modules/functions/scope.d.ts +1 -1
  36. package/dist/modules/images/imageProxy.js +1 -1
  37. package/dist/modules/mysql/accounts.js +1 -5
  38. package/dist/modules/project/plugins.d.ts +31 -0
  39. package/dist/modules/project/plugins.js +162 -0
  40. package/dist/modules/project/serveProject.d.ts +78 -0
  41. package/dist/modules/project/serveProject.js +200 -0
  42. package/dist/modules/project/serveProject.test.d.ts +1 -0
  43. package/dist/modules/project/watchSpace.d.ts +14 -0
  44. package/dist/modules/project/watchSpace.js +77 -0
  45. package/dist/modules/realtime/failure.d.ts +6 -0
  46. package/dist/modules/realtime/failure.js +11 -0
  47. package/dist/modules/realtime/handlers.js +13 -3
  48. package/dist/modules/realtime/hub.js +4 -1
  49. package/dist/modules/realtime/socket.js +3 -2
  50. package/dist/modules/rsc/connectorRscData.d.ts +7 -3
  51. package/dist/modules/rsc/connectorRscData.js +3 -4
  52. package/dist/modules/rsc/handler.d.ts +1 -0
  53. package/dist/modules/rsc/handler.js +11 -1
  54. package/dist/modules/rsc/publicFileResolver.d.ts +4 -8
  55. package/dist/modules/rsc/publicFileResolver.js +6 -10
  56. package/dist/modules/runtime/bundle.d.ts +1 -1
  57. package/dist/modules/runtime/bundle.js +2 -2
  58. package/dist/modules/ssr/prepareRender.js +10 -2
  59. package/dist/modules/ssr/registerExternalPlugins.js +1 -1
  60. package/dist/modules/ssr/vendor.test.d.ts +1 -0
  61. package/dist/modules/ssr/views/template.ejs +7 -3
  62. package/dist/plugins/compile.js +1 -1
  63. package/dist/plugins/copy.js +1 -1
  64. package/dist/plugins/manager.js +1 -1
  65. package/dist/project.d.ts +9 -0
  66. package/dist/project.js +2 -0
  67. package/package.json +16 -4
  68. package/skills/plitzi-authoring/CHEATSHEET.md +2 -2
  69. package/skills/plitzi-authoring/SKILL.md +34 -148
  70. package/skills/plitzi-authoring/recipes/plugin.ts +1 -1
  71. package/skills/plitzi-authoring/reference/accessibility.md +3 -4
  72. package/skills/plitzi-authoring/reference/auth.md +75 -0
  73. package/skills/plitzi-authoring/reference/authoring-errors.md +19 -4
  74. package/skills/plitzi-authoring/reference/colours-and-motion.md +3 -2
  75. package/skills/plitzi-authoring/reference/drawing.md +1 -1
  76. package/skills/plitzi-authoring/reference/efficiency.md +5 -2
  77. package/skills/plitzi-authoring/reference/elements-and-styles.md +23 -23
  78. package/skills/plitzi-authoring/reference/flows.md +15 -8
  79. package/skills/plitzi-authoring/reference/index.md +33 -0
  80. package/skills/plitzi-authoring/reference/layouts.md +15 -18
  81. package/skills/plitzi-authoring/reference/plugins.md +3 -3
  82. package/skills/plitzi-authoring/reference/recipes.md +25 -0
  83. package/skills/plitzi-authoring/reference/review-checklist.md +2 -1
  84. package/skills/plitzi-authoring/reference/rules.md +61 -0
  85. package/skills/plitzi-authoring/reference/snippets-and-export.md +2 -2
  86. package/skills/plitzi-authoring/reference/structure.md +4 -3
  87. package/skills/plitzi-authoring/reference/testing.md +1 -1
  88. package/skills/plitzi-authoring/reference/validation.md +1 -1
@@ -0,0 +1,75 @@
1
+ # Signing people in
2
+
3
+ ## Where they sign in
4
+
5
+ A page signs in with one provider: the space's `settings.userProvider`, or — when it declares none — the one the server
6
+ that rendered it serves.
7
+
8
+ | The space runs… | Declare |
9
+ | --- | --- |
10
+ | on the Plitzi platform, visitors signing in with their Plitzi account | `userProvider: 'server'` — and `visitorRoles` |
11
+ | on a self-hosted server with `auth` (`createServer({ auth })`; in a project, `src/config/serverOptions.ts`) | nothing: the server tells its pages where they sign in |
12
+ | against an API of your own over HTTP+JSON | `userProvider: 'basic'`, `loginUrl`, `userUrl`, `refreshUrl`, `logoutUrl`, and `sessionHintCookie` when it writes one |
13
+
14
+ What the space declares wins, setting by setting; a space naming another provider than the server's ignores the
15
+ server's altogether. A page with no provider at all offers no auth actions, and an `authLogin` there says why in the
16
+ console (`[plitzi] auth.login: …`) — read it before suspecting the form.
17
+
18
+ On the platform, `userProvider: 'server'` signs in by leaving the page: a `link` with `mode: 'external'` to
19
+ `/auth/sign-in?return=/` sends the visitor to sign in and back, signed in on the space's own host.
20
+
21
+ ## The steps
22
+
23
+ ```ts
24
+ form({
25
+ id: 'signIn',
26
+ managedByInteractions: true,
27
+ flows: [
28
+ [
29
+ named('sent', onSubmit()),
30
+ named('signedIn', authLogin({ username: '{{ sent.values.username }}', password: '{{ sent.values.password }}' })),
31
+ when({ field: 'signedIn.ok', operator: '=', value: true }, navigate({ urlType: 'page', url: 'dashboard' })),
32
+ when(
33
+ { field: 'signedIn.reason', operator: '=', value: 'unverified' },
34
+ addNotification({ content: 'Confirm your address first — the link is in your inbox', appearance: 'warning' })
35
+ )
36
+ ]
37
+ ],
38
+ children: [
39
+ formControl({ name: 'username', label: 'Username' }),
40
+ formControl({ name: 'password', label: 'Password', subType: 'password' }),
41
+ button({ content: 'Sign in', subType: 'submit' })
42
+ ]
43
+ });
44
+ ```
45
+
46
+ - `authLogin(params)` — `{ username, password }`; `{ mode: 'token', token }` adopts a credential obtained elsewhere,
47
+ checked against `userUrl`; `{ mode: 'mfa', mfaToken, code }` completes a sign-in that owed a second factor.
48
+ - What it answers: `ok`; when not, `reason` (`unverified`, `inactive`, `network`, `missing`…) and, for `reason: 'mfa'`,
49
+ `mfaToken` — the challenge `mode: 'mfa'` completes. Test it on `<step>.ok` with `when`, never `whenSucceeded` /
50
+ `whenFailed`, which are a server action's ([flows](flows.md)).
51
+ - `authLogout()` ends the session; `authRefreshDetails()` reloads who is signed in.
52
+
53
+ ## Who is signed in
54
+
55
+ The `auth` source: `{{ auth.isAuthenticated }}`, `{{ auth.status }}` (`init` … `authenticated` | `guest`, while it is
56
+ being found out), `{{ auth.details.username }}` — `details` carries `id`, `username`, `email`, `verified`, `roles` and
57
+ `permissions`. Show a signed-in part with `visible: 'auth.isAuthenticated'`; never bind a token into the page.
58
+
59
+ ## Pages for one kind of visitor
60
+
61
+ A page's `accessLevel`: `'authenticated'` (signed in), `'public'` (signed OUT only — the sign-in page), or nothing
62
+ (everybody). A lone `public` page vanishes the moment anyone signs in, so leave it out unless the page has a signed-in
63
+ twin on the same slug. `unauthorizedRedirect` names where a visitor the page is not for goes — a page's id or slug
64
+ (`''` is the home page), or a URL; without it they are answered 403.
65
+
66
+ ## What a visitor may do
67
+
68
+ `settings.visitorRoles` — `{ author: ['postPublish'] }` — is what each role gives. A visitor holds exactly the
69
+ permissions of their roles in this space; who holds a role is given by email in the builder, never written here. A page
70
+ reads them in `auth.details.permissions`, a plugin asks `can('postPublish')` of the auth context.
71
+
72
+ A server action's trigger says who may run it: `access: { mode: 'public' }`, `{ mode: 'session' }` (signed in) or
73
+ `{ mode: 'role', permissions: ['postPublish'] }`. The page hiding a button is not the check — the trigger is. Inside,
74
+ its steps read who asked as `{{ user.id }}`, and a function as `ctx.user` (`id`, `username`, `email`, `verified`,
75
+ `roles`, `permissions`) — never their session.
@@ -27,7 +27,7 @@ The space is not written until these are fixed.
27
27
  | `action-without-entry` | an action with no way in, so nothing can start it | a `trigger` — `{ type: 'call', access }`, or `render`, `webhook`, `custom`, `schedule` |
28
28
  | `action-without-steps` | an action with no steps | the steps it runs |
29
29
  | `active-when-constant` | an `activeWhen` condition that reads nothing — always or never true | name what it depends on: `'{{ list_dots.index == state.slide }}'` |
30
- | `ancestor-not-class` | an ancestor condition keyed by something that is not a class name | `[card.name]` for a `styles()` declaration |
30
+ | `ancestor-not-class` | an ancestor condition keyed by something that is neither a class name nor `'>'` (the parent) | `[card.name]` for a `styles()` declaration, or `'>'` for the parent whatever it wears |
31
31
  | `anchor-duplicate` | one anchor on two elements of the same page (layouts included) | rename all but one |
32
32
  | `anchor-invalid` | an `anchor` or a link's `hash` that is not lowercase letters, digits and `-` | `anchor: 'plans'`, `hash: 'plans'` — no `#` |
33
33
  | `anchor-missing` | a link whose `hash` no element on the page it goes to carries — it would land at the top | the anchor the message lists, or give the section that `anchor` |
@@ -59,20 +59,23 @@ The space is not written until these are fixed.
59
59
  | `computed-not-template` | a computed value that is not a template | `'{{ state.x * 2 }}'` |
60
60
  | `computed-reads-element` | a computed value reading an element's source — no element is around the whole space | compute it from the globals and the variables, or bind it on the element |
61
61
  | `computed-unknown` | a computed value read before it is declared, or never declared | declare it in `computed`, above the one that reads it |
62
+ | `condition-field-unpublished` | a `when` asking a step of its flow for a key it never publishes — `whenSucceeded` / `whenFailed` on anything but `runServerAction` | a key the step publishes (`explain <step>`): after `authLogin`, `when({ field: 'signedIn.ok', operator: '=', value: true }, …)` |
62
63
  | `controls-unknown` | a button's `controls` naming no element of the space | the id of the element it shows and hides: `controls: 'faq-answer'` |
63
64
  | `css-property-twice` | one property written twice in a rule set — `paddingTop` beside `'padding-top'` — so one would silently win | keep one |
64
65
  | `css-property-unknown` | a CSS property that does not exist | the property it suggests; a custom property starts with `--` |
65
66
  | `css-value` | an empty CSS value, or one with `;` or `{}` | one value per property; leave a property out instead of writing it empty |
67
+ | `element-ids-target` | a step refreshing containers by id (`invalidateElements`, `invalidateQueries({ elements })`) naming one the space does not have, or one that is not an `apiContainer` | the id of the `apiContainer` whose data the step changes: `invalidateElements: ['posts']` |
66
68
  | `element-load-strategy` | a `loadStrategy` the element does not take | one of the values the message lists |
67
69
  | `element-rejected` | an element the schema refused to hold | the problem listed with it |
68
70
  | `element-runtime` | a `runtime` the element does not run in | one of the values the message lists |
69
71
  | `element-shape` | an element that is not an object with a `type`, `attributes` and a list of `children` | build elements with their factories — `text(…)`, `container(…)` |
72
+ | `element-slot-unknown` | a `slots` key the element type does not have — its class would dress nothing | one of the slots `plitzi explain <type>` lists; the element itself takes `class` |
70
73
  | `flag-gate` | an element's or a page's `flag` that is not a flag name | `flag: 'newCheckout'`, or `flag: '!newCheckout'` for "only while off" |
71
74
  | `flag-name` | a flag whose name a template cannot read as `flags.x` | letters, digits and `_` |
72
75
  | `flag-rule-shape` | a flag rule that is not `{ when, value }`, or whose `value` is not `true` or `false` | `{ when: { … }, value: true }` |
73
76
  | `flag-shape` | a flag whose `value` is not `true` or `false` | `{ value: false, rules: [] }` |
74
77
  | `flag-undeclared` | a gate on a flag the space does not declare — it reads as off, so the element never (or always) renders | declare it in `flags`, or remove the gate |
75
- | `flag-unknown` | a template reading a flag the space does not declare | the declared name it suggests, or declare it |
78
+ | `flag-unknown` | a template, a binding or a `when` reading a flag the space does not declare | the declared name it suggests, or declare it |
76
79
  | `flow-empty` | a flow with no steps | `[onClick(), setState({ … })]` |
77
80
  | `flow-without-trigger` | a flow whose first step is not the event that runs it | `[onClick(), setState(…)]` |
78
81
  | `folder-cycle` | a page folder inside itself | a `parent` that leads to the top |
@@ -83,9 +86,11 @@ The space is not written until these are fixed.
83
86
  | `from-without-attribute` | `from` on a type with no one attribute that shows its data (a container, a form) | bind the attribute you mean: `bind: { attribute: 'source' }` |
84
87
  | `global-callback-module` | a global callback sent to a module other than the one that registers it | the step builder, which knows the module |
85
88
  | `global-callback-undeclared` | a global-callback step builder naming an action no source declares | one of the actions it lists |
89
+ | `global-field-unknown` | a binding, a template or a `when` reading a field `navigation`, `auth` or `theme` never has — `auth.authenticated` | the field it suggests: `auth.isAuthenticated`, `auth.details.username`, `navigation.queryParams.next`, `theme.resolved` |
86
90
  | `id-invalid` | an id a binding, a template or a test cannot name | a letter first, then letters, digits, `-` and `_`: `'hero-title'` |
87
91
  | `id-shadows-global` | an element named like a global data source (`state`, `navigation`, `auth`…) | another id |
88
92
  | `id-taken` | two elements with one id — ids are one namespace for the whole space | a name taken by a page or a layout: a name of its own; a helper called twice: `scope('promos', ref => …)`, every id inside prefixed |
93
+ | `keyframes-shape` | keyframes with a name CSS does not accept or the SDK keeps for itself, or a frame that is not an offset | a plain name (`slide-in`), frames keyed `from`, `to` or a percentage (`'50%'`, `'0%, 100%'`) |
89
94
  | `layout-slot-unknown` | a layout `slot` that is not an element inside that layout | the id of the element in the shell where the body goes |
90
95
  | `layout-undeclared` | a page or a layout inside a layout the space does not declare | the layout's id, or declare it in `layouts` |
91
96
  | `list-items-ignored` | a list whose items nothing reads — `source: 'none'` renders its children once | `source: 'controlled'` |
@@ -96,7 +101,7 @@ The space is not written until these are fixed.
96
101
  | `motion-invalid` | a `motion` with a preset, a trigger or a timing the page cannot play | `motion: { enter: 'fade-up', on: 'view' }` — the message lists the presets |
97
102
  | `motion-no-tag` | a `motion` on an element with no tag of its own — nothing to move | the motion on what it wraps, or a `subType` on it |
98
103
  | `no-pages` | a space with no pages | `pages: [{ id: 'home', name: 'Home', slug: '', body: [] }]` |
99
- | `notifications-shape` | `notifications` with a field it does not have, or a value that is not one CSS value | the fields it lists; a colour or a length, like 'var(--card)' or '12px' |
104
+ | `notifications-shape` | `notifications` with a field it does not have, or a value that is not one CSS value | the field it suggests, or one it lists; one CSS value, like 'var(--card)', '12px' or '500' |
100
105
  | `outside-ancestor` | an element that reads the state of an element it is not inside | nest it in the element the message names |
101
106
  | `page-access-level` | an `accessLevel` that is not one the router reads | one of the values the message lists |
102
107
  | `page-route-reserved` | a page under `/fn`, where the space's functions answer | another slug |
@@ -107,6 +112,7 @@ The space is not written until these are fixed.
107
112
  | `part-missing` | a compound element — a carousel, a tab container, a dropdown — without a part it renders through | the part the message names, inside it (`carousel()` writes its own track) |
108
113
  | `prop-missing` | an instance without a prop its component requires | `component('card', { props: { name: … } })`, or bind it |
109
114
  | `prop-name` | a prop whose name a template cannot read as `props.x` | letters, digits and `_` |
115
+ | `prop-type-unknown` | a prop declared with a type no editor offers and nothing checks a value against | one of text, textarea, select, boolean, number, scalar, json, elementIds — words are `text` |
110
116
  | `prop-unknown` | a prop the component does not declare, handed in or read | the prop it suggests, or declare it: `props: { name: { type: 'text' } }` |
111
117
  | `prop-value` | a prop handed in with a value its declaration does not take | a value of the declared type, or one of its options |
112
118
  | `props-outside-component` | `props.x` read outside a component | read the source it would have come from |
@@ -131,6 +137,10 @@ The space is not written until these are fixed.
131
137
  | `step-name` | a step name a later step cannot read as `{{ name.field }}` | a letter first, then letters, digits, `-` and `_` |
132
138
  | `step-params` | a step param that does not exist, or a value outside its options | the params and values it lists |
133
139
  | `step-type` | a step `type` that does not exist | the step builders — `setState(…)`, `navigate(…)` — write it |
140
+ | `style-condition-unknown` | a condition a class's rules cannot hold under | `motion-reduce`, `motion-safe`, or a container width — `container (max-width: 30rem)`, `container card (min-width: 480px)` |
141
+ | `style-pseudo-content` | a `content` CSS cannot read — text without its quotes draws nothing — or a `before`/`after` with no `content`, which is not there at all | the text in quotes inside the string — `content: '"→"'`, `'""'` for an empty box — or a function such as `counter(step)` |
142
+ | `style-pseudo-property` | a property the pseudo-element ignores — the browser drops it without a word | one it honours (`::selection` paints, `::marker` and `::placeholder` take text and font), or style the element |
143
+ | `style-pseudo-unknown` | a pseudo-element a class cannot dress | one of `before`, `after`, `marker`, `placeholder`, `first-letter`, `first-line`, `selection` — without the `::` |
134
144
  | `style-state-unknown` | a state a selector does not react to | one of the states it lists (`hover`, `focus`, `active`…) |
135
145
  | `svg-not-svg` | an `svg` whose `content` is not one `<svg>…</svg>` — it draws nothing | the SVG markup alone; HTML around it goes in a `blockHtml` |
136
146
  | `template-short-source` | a source named without its prefix inside a template or a step | the full name: `apiContainer_stats`, `list_rows` |
@@ -161,6 +171,7 @@ The space renders, and renders something you probably did not mean. Fix every on
161
171
  | Code | What was wrong | Write instead |
162
172
  | --- | --- | --- |
163
173
  | `action-output-path` | a read of `.data` on a provider fed by a server action — it publishes the action’s output at its root, and `.data` is a `query` provider’s answer | the output’s own field — `apiContainer_feed.stories`, not `apiContainer_feed.data.stories` |
174
+ | `animation-name-unknown` | `animation-name` naming keyframes the space does not declare — nothing plays | declare them in the space's `keyframes`, or name one it has |
164
175
  | `click-on-static-element` | a click flow on a container, text, heading, image or list item — no keyboard reaches it | the flow on a `button` (it holds children) or a `link` |
165
176
  | `colour-without-dark` | a colour token with no dark value | `{ light, dark, default }` |
166
177
  | `condition-starts-visible` | a computed visibility that shows until its data answers | `visible: { source, template }` |
@@ -176,9 +187,11 @@ The space renders, and renders something you probably did not mean. Fix every on
176
187
  | `form-control-name-taken` | two controls in one form with one name — one overwrites the other | a name each |
177
188
  | `form-control-unnamed` | a control in a form with no `name` — its value never reaches `values` | `formControl({ name: 'email', … })` |
178
189
  | `form-value-compared-to-blank` | a `when` comparing a submitted field to `""` — a field nobody typed in is not sent | `operator: 'empty'` / `'notEmpty'` |
190
+ | `heading-level-overridden` | a markdown's or richText's `heading` slot and a level's (`heading3`) both setting one property, the general class written later in the stylesheet — the level's value is lost | set the property on one of the two: the level slot for that level alone, `heading` for every heading |
179
191
  | `heading-level-skipped` | an `h4` right after an `h2` — the outline misses a level | the next level down; size it with its class |
180
192
  | `image-without-alt` | an image that is not `decorative` and has no `alt` | say what it shows, or `decorative: true` |
181
193
  | `label-ignored` | a `label` on a container whose tag is named by what it holds (`li`, a heading) | the words inside, or a landmark tag (`nav`, `section`…) |
194
+ | `link-current-section` | `current: 'section'` on a link that is never current (an external one) or current everywhere (to `/`) | leave `current` out — `'section'` is for a link to a section's own page, `/automations/runs` |
182
195
  | `list-item-key-missing` | a list's `itemKey` that some of its items lack, or two of them share — the rows fall back to `id`, then position | a field every item has, once each |
183
196
  | `list-row-not-li` | a row of a list with `items` that is not an `<li>` — that list is a `<ul>` (or `<ol>`), so the row is a box inside a list, no item to a screen reader | `container({ subType: 'li' })` — or the component's root one, or a wrapper for a link or a button |
184
197
  | `overlay-never-opened` | a modal or a dialog that starts hidden and that no step opens | a flow with `openModal('id')` / `openDialog('id')` |
@@ -208,10 +221,12 @@ Nothing is wrong: the page renders as written. Each is a shorter way to the same
208
221
 
209
222
  | Code | Written the long way | The short way |
210
223
  | --- | --- | --- |
224
+ | `class-overrides-class` | one class's shorthand erasing the longhand another class on the same element writes out — `padding` over `padding-top` — because the stylesheet writes it later: classes in the order they are first met, a breakpoint's rules after the base, never the order of a class list | the longhands the shorthand means instead of it (`padding-left`, `padding-right`), so it leaves the other class's alone — or `quiet: ['class-overrides-class']` when the shorthand is meant to win |
211
225
  | `content-attribute` | a button or a link whose children are only its words and an icon — a `text`, a `fontAwesome` — elements more than it needs | its own `content` and `icon` (`link({ href, content: 'Docs', icon: 'fa-solid fa-arrow-right', iconPlacement: 'after' })`); the icon's class goes on the `icon` slot, and what the text's class adds on the box's class, never the class itself |
212
226
  | `custom-css-class` | a `customCss` rule a class can hold — `.card:hover`, `.panel .icon` | the class's own `states` or `ancestors`, where the style editor reads it back and a breakpoint can change it |
213
- | `custom-css-notifications` | the toasts dressed with `.Toastify__toast` rules in `customCss` | `notifications: { font, fontSize, border, shadow, padding, background, text, radius }` |
227
+ | `custom-css-notifications` | the toasts or their icon, close button or progress bar dressed with `.Toastify__*` rules in `customCss` | `notifications: { font, fontSize, fontWeight, minHeight, iconSize, closeColor, progressHeight, … }` |
214
228
  | `custom-css-sdk-default` | `customCss` repeating what the SDK already does for every space — less motion when asked, the theme toggle showing one icon | nothing: remove it |
229
+ | `custom-css-slot` | a part of a built-in element dressed in `customCss` by the SDK's markup — a form control's `<input>`, a pager's buttons, a heading inside a markdown | a class on the slot of that part — `slots: { field: input }`, `elements.<type>.slots` — with its states |
215
230
  | `heavy-animation` | keyframes animating what the browser repaints or lays out again on every frame — a size, a position, a blur, a shadow, or a colour in a loop — which stutters whenever the page is busy, most of all while it loads | `opacity` and `transform`: a size or a position is `translate`/`scale`, a blur or a shadow is the `opacity` of a layer carrying it; a loop that must animate anything else starts `paused` and runs under `[data-hydrated]` |
216
231
  | `literal-colour` | a colour written out in a class where the space has a token of that value — it stays put in the dark scheme | `var(--token)` when it should follow the scheme; a token of one value of its own when it must stay the same in both |
217
232
  | `repeated-on-pages` | the same block — a header, a footer, a side panel — written into several pages | a layout holding it once (`layouts`, and `layout: { id, slot }` on each page) — a component when only some pages of a layout carry it; a link marks its own page with the `current` state |
@@ -2,8 +2,9 @@
2
2
 
3
3
  ## Animations
4
4
 
5
- - **Keyframes go in the space's `customCss`** (`'@keyframes marquee { to { transform: translateX(-50%); } }'`) and
6
- the class names them: `animation: 'marquee 30s linear infinite'`.
5
+ - **Keyframes go in the space's `keyframes`** (`keyframes: { marquee: { to: { transform: 'translateX(-50%)' } } }`)
6
+ and the class names them: `animation: 'marquee 30s linear infinite'`. A name no keyframes declare is warned
7
+ (`animation-name-unknown`).
7
8
  - **An element hidden by `visible` is `display: none`**, so an animation in its class runs again every time it is
8
9
  shown — the way to animate a slide in (see recipes/every-few-seconds.ts).
9
10
  - **Pause on hover** from the element around it: `ancestors: { [band.name]: { states: { hover:
@@ -17,7 +17,7 @@ It sizes the canvas to the device (at most 2× — `maxPixelRatio`), follows its
17
17
  can see it move: on a live page (in the builder it draws one still frame), for a visitor who did not ask for less
18
18
  motion, with the tab in front and the canvas on screen; `paused` holds it (bind it to a hover). A shader that does not
19
19
  compile — GLSL ES 1.0 wants `196.0`, not `196` — throws a `ShaderError` with the driver's log, printed as
20
- `[plugin <type> "<id>"] fragment shader failed: …` (`plitzi check` lists it under the console) and `error` says it;
20
+ `[plugin <type> "<id>"] fragment shader failed: …` (`plitzi page check` lists it under the console) and `error` says it;
21
21
  show the still image meanwhile, as above. `useAnimationFrame(onFrame, { target })` is the same loop for motion that
22
22
  is not a canvas.
23
23
 
@@ -34,13 +34,13 @@ Only a suggestion's code: a problem is never quieted (`quiet-unknown`).
34
34
  | Written the long way | The short way | Suggestion |
35
35
  | --- | --- | --- |
36
36
  | The same header, footer or sidebar in every page | A **layout** holding it once; each page names it (`layout: { id, slot }`) — [layouts](layouts.md) | `repeated-on-pages` |
37
- | A header copied per page so one link can be styled "active" | The link marks its own page: `states: { current: { … } }` on its class; `activeOn` for an entry lit on several pages | `repeated-on-pages` |
37
+ | A header copied per page so one link can be styled "active" | The link marks its own page: `states: { current: { … } }` on its class; `current: 'section'` for an entry lit on the pages under it | `repeated-on-pages` |
38
38
  | The same card, row or tile written again with other words | A **component** with props, placed with `component(id, { props })` — [components](components.md) | `repeated-shape` |
39
39
  | Rows of data written one by one, side by side | One `list` over the rows — `items: [ … ]` fixed, or bound to a source — with its row written once — [lists](lists.md). Data is the test: three cards a person rewords on the canvas read better as three cards. Copies that read different sources or write different state keys are controls written alike, and not offered | `repeated-shape` |
40
40
  | `button({ children: [text('Save')] })`, `link({ children: [text('Docs'), fontAwesome({ icon })] })` | `button({ content: 'Save' })`, `link({ href, content: 'Docs', icon, iconPlacement: 'after' })` — the icon's class on the `icon` slot; what the text's class adds (`whiteSpace: 'nowrap'`) moves to the box's class — never the class itself, whose `inherit` would then point past the box and whose `pointerEvents: 'none'` would switch the box off | `content-attribute` |
41
41
  | `.card:hover { … }`, `.card .icon { … }` in `customCss` | The class's own `states` and `ancestors` | `custom-css-class` |
42
42
  | A reduced-motion reset, or rules showing one icon of the theme toggle, in `customCss` | Nothing: the SDK does both for every space | `custom-css-sdk-default` |
43
- | `.Toastify__toast { font-family: …; border: … }` in `customCss` | `notifications: { font, fontSize, border, shadow, padding, … }` | `custom-css-notifications` |
43
+ | `.Toastify__toast { font-family: …; border: … }`, `.Toastify__toast-icon { width: … }` in `customCss` | `notifications: { font, fontSize, border, iconSize, closeColor, progressHeight, … }` | `custom-css-notifications` |
44
44
  | Keyframes animating `width`, `top`, `filter: blur()`, `box-shadow`, a colour in a loop | `opacity` and `transform`; decoration held until `[data-hydrated]` — [motion](colours-and-motion.md) | `heavy-animation` |
45
45
 
46
46
  ## What it declares and never uses
@@ -63,6 +63,9 @@ And a few the suggestions do not count, because they cost styles rather than ele
63
63
 
64
64
  - **A look used twice is a class**, written once with `styles()`; every element of a TYPE dressed the same way is the
65
65
  space's `elements` defaults (`elements: { heading: { color: 'var(--fg)' } }`), not a class on each.
66
+ - **A shorthand in a class says every side.** On an element wearing two classes, one's `padding` erases the
67
+ `padding-top` the other writes out wherever the stylesheet writes it later; write the longhands it means
68
+ (`class-overrides-class` — a suggestion, quieted like the rest).
66
69
  - **Inner parts are slots.** A form control's input, a modal's backdrop, a theme toggle's icons: style them with
67
70
  `slots: { input: inputClass }` on the element, or the type's `slots` in `elements` — never a `customCss` rule reaching
68
71
  into the element.
@@ -18,7 +18,7 @@ container([hero, grid]) // an array is the children
18
18
  | `id` | the ONE name this element answers to — its key in the document, a binding's source, a step's target. Unique across the WHOLE space |
19
19
  | `class` | a shared class: a `styles()` declaration or a name from the space's `classes`; a list for several. Exclusive with `css` |
20
20
  | `css` | rules of this element's own: one set, or one per breakpoint (`{ desktop, tablet, mobile }`) |
21
- | `states` | `hover`, `focus-visible`, `current` (a link to the page shown), `hidden`… beside the element's own `css` |
21
+ | `states` | `hover`, `focus-visible`, `current` (the chosen link, toggle, tab), `expanded`, `first`/`last`/`odd`/`even`, `hidden`… |
22
22
  | `variant` | a variant of the class it wears (when the class declares it) or of its type |
23
23
  | `slots` | a class for one of the element's OTHER selectors — a form control's `input` |
24
24
  | `bind` | where a value comes from |
@@ -73,13 +73,9 @@ indented with space below it: `{ margin: '0px', padding: '0px' }` on its class t
73
73
  children sit in the parent's layout and a `class` on it styles nothing (`STYLE_WITHOUT_TAG`). Put the layout on the
74
74
  parent, or give it `attributes: { subType: 'div' }`.
75
75
 
76
- A type this SDK does not ship — a plugin — is authored the same way: `defineElement<Props>({ type, … })`,
77
- `element<Props>('type', props)`, or `elementsFromManifest(manifest)`. See [plugins](plugins.md).
76
+ A plugin's type is authored the same way: `defineElement<Props>({ type, … })` — see [plugins](plugins.md).
78
77
 
79
- **What a type takes, answered by the package** instead of the 7,000-line `.d.ts`: every catalogue is exported, keyed by
80
- type — `elementCatalog` (what it is for), `elementDefaultAttributes`, `elementAttributeNames`, `elementTriggers`,
81
- `elementCallbacks`, `elementSlots`, `elementSourceTypes`, `elementLeafTypes`, plus `BUILTIN_TRANSFORMERS` — and
82
- `npx @plitzi/cli explain formControl` prints them for one type.
78
+ **What a type takes:** `npx @plitzi/cli explain formControl`, or the exported catalogues ([validation.md](validation.md)).
83
79
 
84
80
  ## CSS
85
81
 
@@ -111,15 +107,20 @@ const card = styles('card', {
111
107
  container({ class: card, children: [ … ] });
112
108
  ```
113
109
 
114
- - A flat object is the base rules; `{ css, states, variants, ancestors }` when there is more. Each part may be written
110
+ - A flat object is the base rules; `{ css, states, variants, ancestors, pseudos, conditions }` when there is more. Each part may be written
115
111
  per breakpoint.
116
- - **Share a rule as a class, never as a spread.** Spreading a `const` into each element's `css` shares the source and
117
- duplicates the document: one selector per element, so restyling one card in the builder restyles one card.
112
+ - **Share a rule as a class, never as a spread** into each element's `css`: that is one selector per element, so
113
+ restyling one card in the builder restyles one card.
118
114
  - **Build a family from a plain object**, not by layering classes: `styles('button-primary', { ...buttonBase, … })`.
115
+ - Of two classes on an element, the one the stylesheet writes later wins (first met; a breakpoint's after the base),
116
+ not the list's last: write the longhands a class means (`class-overrides-class`).
119
117
  - One name declared twice with rules that disagree is refused. Declare a class once, in the module that owns it, and
120
118
  import it.
121
119
  - `ancestors: { [sidebar.name]: { variants: { collapsed: { display: 'none' } } } }` styles an element by the state or
122
- variant of an ancestor class — the way to react to a parent's hover or collapse without a flow.
120
+ variant of an ancestor class — the way to react to a parent's hover or collapse without a flow; `'>'` is the parent,
121
+ whatever it wears (a component's part: `{ '>': { states: { expanded: … } } }`).
122
+ - `pseudos: { after: { content: '"→"' } }` (`before`/`after` need a quoted `content`; also `selection`, `marker`,
123
+ `placeholder`, `first-letter`, `first-line`). `conditions: { 'motion-reduce': …, 'container (max-width: 30rem)': … }`.
123
124
  - **`hidden` is how an element looks while its `visible` says no** — where it goes as it hides, and where it comes
124
125
  from as it shows. Give the class a transition that includes `display` with `allow-discrete`, and a panel fades or
125
126
  slides instead of blinking; the transition on the base is the way in, the one in `hidden` the way out:
@@ -142,16 +143,15 @@ container({ class: card, children: [ … ] });
142
143
  What is inside follows its container with `ancestors: { [panel.name]: { states: { hidden: { … } } } }` — a
143
144
  different `transition-delay` per item staggers them. A container whose children mount only while it shows
144
145
  (`loadStrategy: 'visible'`) empties before it can leave: use `'lazy'` on one that animates out.
145
- - **The navigation of a site goes in a layout, once.** A link to the page being shown marks itself, and its class
146
- dresses it with `states: { current: { … } }` — never a class picked per page, which forces a copy of the header into
147
- every page.
148
- - **`:hover` is part of the selector, not a rule in `customCss`.** `customCss` is for what no class can say:
149
- keyframes, pseudo-elements, a rule across two unrelated elements, the inside of a third-party widget.
150
- - Per element TYPE defaults go in `elements: { heading: { base, states, variants, slots } }` — a whole type at once.
151
- - **Slots** are an element's other selectors: a `formControl`'s `input`, `label`, `error`; a `modalContainer`'s
152
- `backgroundContainer`, `rootContainer`, `headerContainer`, `headerTitle`, `headerCloseButton`, `bodyContainer`; a
153
- `dialogContainer`'s footer and buttons. `elementSlots` lists them all by type. Style one element's with `slots`, a
154
- whole type's with `elements.<type>.slots`.
146
+ - **The navigation goes in a layout, once:** a link to the page shown dresses itself with its class's
147
+ `states: { current: { … } }` — never a class picked per page, which copies the header into every page.
148
+ - **`:hover` is part of the selector, not a rule in `customCss`.** `customCss` is for what no class can say: a rule
149
+ across two unrelated elements, the inside of a third-party widget.
150
+ - Per element TYPE defaults go in `elements: { heading: { base, states, variants, pseudos, slots } }` — a whole type.
151
+ - **Slots** are an element's parts (not `customCss`): a `formControl`'s `input` (the box), `field` (its `<input>`),
152
+ `label`, `requiredMark`, `error`; a `markdown`'s or `richText`'s `heading`, `heading2`, `link`, `codeBlockFrame`…;
153
+ a `pagination`'s `page` (the one shown: its `current` state). `plitzi explain <type>` lists them. Style one
154
+ element's with `slots`, a type's with `elements.<type>.slots`.
155
155
  - A `modalContainer` / `dialogContainer` is fixed to the viewport, as tall as its content up to the screen, and 500px
156
156
  wide up to the screen; theme its look through its slots.
157
157
 
@@ -159,7 +159,7 @@ container({ class: card, children: [ … ] });
159
159
  already speak in variant names — `variantFrom(pill, 'runs.item.status', { template: "{{ source == 'completed' ?
160
160
  'ok' : 'failed' }}" })`. The binding's key names the CLASS; never write it by hand. A variant while a condition holds
161
161
  — the dot of the slide on screen — is `activeWhen(dot, '{{ list_dots.index == state.slide }}')` (`active`, else
162
- `idle`); for "the current page", `activeOn` in [layouts.md](layouts.md).
162
+ `idle`); for "the current page", `current` in [layouts.md](layouts.md).
163
163
 
164
164
  ## From Tailwind classes
165
165
 
@@ -169,4 +169,4 @@ A design written in Tailwind classes becomes classes the builder edits with `sty
169
169
  ## Colours, themes and motion
170
170
 
171
171
  Every colour is a token with a light and a dark value, read as `var(--name)` (or `tokens(variables)`); keyframes go in
172
- `customCss` — see [colours-and-motion.md](colours-and-motion.md).
172
+ the space's `keyframes` — see [colours-and-motion.md](colours-and-motion.md).
@@ -21,8 +21,8 @@ source — `when({ field: 'flags.newCheckout', operator: '=', value: true }, …
21
21
  same flags, decided on the server. Use the step builders — they fill in where a step runs and what it takes:
22
22
 
23
23
  - **Where it runs.** A global callback registers under its source MODULE (`state`, `auth`, `actions`), an element
24
- callback under an element's id, a utility under nothing. Naming either half wrong is a control that does nothing,
25
- with no error — which is why the builders exist.
24
+ callback under an element's id, a utility under nothing. Either half named wrong is a control that silently does
25
+ nothing.
26
26
  - **Which `setState`.** `setState({ key })` writes `runtime.state.<key>` and is read as `state.<key>`; never put
27
27
  `state.` in the key. `updateElement(…)` changes one element's own attribute or state.
28
28
  - **Flip in one step.** `toggleState({ key })` for app state, `toggleElement({ category: 'state', key: 'visibility' },
@@ -48,7 +48,7 @@ Name the trigger (`named('changed', on('onChange'))`) and read its payload as `{
48
48
 
49
49
  | Trigger | Payload |
50
50
  | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
51
- | `onChange` (formControl) | `value` (a boolean for a checkbox), `name` |
51
+ | `onChange` (formControl) | `value` (a boolean for a checkbox or switch), `name` |
52
52
  | `onSubmit` (form) | `values` (by control `name`), `actionUrl`, `method` |
53
53
  | `onPageLoad` (page) | `pageId`, `routeParams`, `queryParams` |
54
54
  | `onApiSuccess` / `onApiError` (apiContainer) | `url`, `method`, `status`, `data` |
@@ -61,6 +61,10 @@ Name the trigger (`named('changed', on('onChange'))`) and read its payload as `{
61
61
  `named('quote', step)` is how a later step reads an earlier one: `{{ quote.output.total }}`. The trigger too:
62
62
  `named('submitted', onSubmit())` → `{{ submitted.values.email }}`. Unnamed steps get ids nothing can refer to.
63
63
 
64
+ `whenSucceeded` / `whenFailed` read a server action's `status` (`runServerAction` only); any other step is asked what
65
+ it publishes (`npx @plitzi/cli explain login` lists it) — after `named('signedIn', authLogin(…))`,
66
+ `when({ field: 'signedIn.ok', operator: '=', value: true }, …)`.
67
+
64
68
  A step's params are templates, evaluated in full (conditions, loops, filters). A source in them is named in full:
65
69
  a row's button posts `{ jobId: '{{ list_jobRows.item.id }}' }` — the row that was clicked; the short name is refused.
66
70
 
@@ -75,7 +79,7 @@ so `when({ field: 'sent.values.code', operator: '=', value: '' }, …)` never ho
75
79
  ## Writes and what they refresh
76
80
 
77
81
  `webHook` (not GET) refreshes every request to its own site when it succeeds; `runServerAction` refreshes
78
- everything. Say what it should refresh — `invalidateQueries: 'elements', invalidateElements: ['orders']` — or
82
+ everything, server-driven providers included. Say what it should refresh — `invalidateQueries: 'elements', invalidateElements: ['orders']` — or
79
83
  `'none'` for a step that only reads, or when a refresh would make the page act on the new answer mid-flow.
80
84
 
81
85
  ## Triggers belong to the element that fires them
@@ -134,10 +138,13 @@ without a word.
134
138
 
135
139
  `addNotification({ content: 'Saved', appearance: 'success' })` — `appearance` is `success`, `danger`, `warning` or
136
140
  `info`. They follow the page's theme; how they look is the space's `notifications` — colours (`background`, `text`,
137
- `success`, `danger`, `warning`, `info`) and shape (`radius`, `font`, `fontSize`, `border`, `shadow`, `padding`):
138
- `notifications: { background: 'var(--card)', text: 'var(--foreground)', success: 'var(--accent)', radius: '12px',
139
- font: 'var(--font-sans)', border: '1px solid var(--border)', shadow: 'var(--shadow-lg)' }`. The toast carries no class
140
- of the space's, so this — not a `.Toastify__toast` rule in `customCss` — is where it is dressed.
141
+ `success`, `danger`, `warning`, `info`), shape (`radius`, `font`, `fontSize`, `fontWeight`, `lineHeight`, `minHeight`,
142
+ `border`, `shadow`, `padding`) and the parts inside (`iconSize`, `iconGap`, `closeColor`, `closeOpacity`,
143
+ `progressHeight`), one CSS value each: `notifications: { background: 'var(--card)', radius: '12px', iconSize: '18px' }`.
144
+ The toast carries no class of the space's, so this — not a `.Toastify__*` rule in `customCss` — is where it is
145
+ dressed.
146
+
147
+ Copy link: `copyToClipboard('{{ navigation.href }}')`, then the toast — it fails with no clipboard.
141
148
 
142
149
  ## Modals, dropdowns, tabs
143
150
 
@@ -0,0 +1,33 @@
1
+ # Every reference, and when to read it
2
+
3
+ Read one only when the task names its subject.
4
+
5
+ | Read | When |
6
+ | --- | --- |
7
+ | [rules.md](rules.md) | Every rule of writing a space, with its why |
8
+ | [recipes.md](recipes.md) | A whole working file for a common task, to copy and rename |
9
+ | [elements-and-styles.md](elements-and-styles.md) | Any element or CSS: factories, fields, classes, states, variants, tokens, fonts, lists, links |
10
+ | [colours-and-motion.md](colours-and-motion.md) | Colours for both themes, tokens, keyframes, and motion that stays smooth (good practices) |
11
+ | [tailwind.md](tailwind.md) | A design written in Tailwind classes: `tw()`, its breakpoints and states, what it refuses |
12
+ | [layouts.md](layouts.md) | Anything shown on more than one page; menus; reducing duplication of elements and styles |
13
+ | [efficiency.md](efficiency.md) | The same page with fewer elements: the suggestions `authorSpace` makes, and the short way for each long one |
14
+ | [components.md](components.md) | One block placed many times — a card, a tier, a testimonial: props, slots, binding a row into one, why it is closed |
15
+ | [data-and-visibility.md](data-and-visibility.md) | Bindings, providers, offline data, loading/empty/error states, live data, caching, showing and hiding, kept state |
16
+ | [kept-state.md](kept-state.md) | State that outlives a reload: `keepState`, transient and painted keys |
17
+ | [auth.md](auth.md) | Signing people in: providers, `authLogin`, `auth.*`, visitor roles |
18
+ | [feature-flags.md](feature-flags.md) | Switching a part of the space on or off — a beta, a rollout, the old version kept until the new one ships |
19
+ | [lists.md](lists.md) | Rendering rows, filtering and sorting them, a detail page for one record, carousels |
20
+ | [typed-sources.md](typed-sources.md) | Data typed by a sample of it: `source()`, typed rows, `twig` for templates |
21
+ | [validation.md](validation.md) | How `authorSpace` checks, the loop that wastes no attempts, and what it cannot see |
22
+ | [authoring-errors.md](authoring-errors.md) | What `authorSpace` refuses or warns about, and what to write instead |
23
+ | [templates.md](templates.md) | Any `{{ … }}` or `{% … %}`: where it runs, naming sources, filters, tests, dates |
24
+ | [flows.md](flows.md) | Clicks, submits, page loads, every few seconds, server actions, modals, state |
25
+ | [realtime.md](realtime.md) | Pages that see each other: channels, presence, cursors, a shared board, who may hear a topic |
26
+ | [plugins.md](plugins.md) | A component of your own: props, binding them, writing state, channels, registering, behaving in the builder |
27
+ | [drawing.md](drawing.md) | A plugin that draws or animates: a canvas sized to the device, WebGL, a loop that stops when unseen |
28
+ | [structure.md](structure.md) | A space bigger than one screen: files, helpers, naming, keeping it short |
29
+ | [testing.md](testing.md) | Any test: `inspectPage`, handles, fixtures, catching a flash from the first frame, shortcuts, counting renders |
30
+ | [performance.md](performance.md) | A page with many elements, a busy flow, something that feels slow: what renders, what it costs, how to measure it |
31
+ | [snippets-and-export.md](snippets-and-export.md) | Publishing a snippet; turning an exported JSON — or a server action — into code |
32
+ | [accessibility.md](accessibility.md) | Icon buttons, fields, images, clickable cards, toggles, headings, landmarks, a canvas — anything a screen reader or a browser agent has to use |
33
+ | [review-checklist.md](review-checklist.md) | Before you say it is done |
@@ -74,7 +74,6 @@ const navGroup = (group: DocsGroup): ElementSpec =>
74
74
  id: `docs-nav-${entry.id}`,
75
75
  href: entry.id,
76
76
  class: navLink,
77
- bind: [activeOn(navLink, entry.id)],
78
77
  children: [text(entry.title, { id: `docs-nav-label-${entry.id}` })]
79
78
  })
80
79
  )
@@ -83,33 +82,31 @@ const navGroup = (group: DocsGroup): ElementSpec =>
83
82
 
84
83
  Adding a page is one entry in the list: the menu, the index and the neighbours' "next" follow.
85
84
 
86
- ## The current entry: `activeOn`
85
+ ## The current entry
87
86
 
88
- A menu in a layout is the same nodes on every page, so it cannot have the right entry styled by hand. It asks which
89
- page is showing:
87
+ A menu in a layout is the same nodes on every page, so it cannot have the right entry styled by hand — and does not
88
+ need to. A link to the page being shown marks itself: `aria-current="page"`, which a screen reader announces, and its
89
+ class's `current` state dresses it:
90
90
 
91
91
  ```ts
92
- const navLink = styles('nav-link', {
93
- css: { color: 'var(--muted)' },
94
- variants: { active: { color: 'var(--foreground)', 'font-weight': '600' } }
95
- });
92
+ const navLink = styles('nav-link', { css: { color: 'var(--muted)' }, states: { current: { color: 'var(--foreground)' } } });
96
93
 
97
- link({ href: 'spaces', class: navLink, bind: [activeOn(navLink, ['spaces', 'space-record'])] });
94
+ link({ href: '/pricing', mode: 'internal', class: navLink });
98
95
  ```
99
96
 
100
- A link to ONE page needs nothing of this: it marks itself — `aria-current="page"`, which a screen reader announces — and
101
- its class dresses it with the `current` state:
97
+ An entry lit for a SECTION — a journal and its articles, `/automations/runs` while on `/automations/runs/42` — says
98
+ `current: 'section'`: current on its page and on every page under its path, at a `/` (`/runs` holds `/runs/42`, not
99
+ `/runsx`), announced there as the current entry (`aria-current="true"`); the same `current` state dresses it. A link to
100
+ `/` holds every page, so authoring warns (`link-current-section`):
102
101
 
103
102
  ```ts
104
- const navLink = styles('nav-link', { css: { color: 'var(--muted)' }, states: { current: { color: 'var(--foreground)' } } });
105
-
106
- link({ href: '/pricing', mode: 'internal', class: navLink });
103
+ link({ href: 'runs', class: navLink, current: 'section' });
107
104
  ```
108
105
 
109
- `activeOn(class, pageIds, { variant?, slot? })` is for an entry lit on SEVERAL pages — a section and the pages under it,
110
- a journal and its articles: it binds the class's variant to `navigation.currentPageId`, the `active` variant on those
111
- pages and `idle` everywhere else. Do not write the binding by hand with a `when` rule per page; that is the long form
112
- this replaces.
106
+ `activeOn(class, pageIds, { variant?, slot? })` is for an entry lit on pages NOT under its path — `['spaces',
107
+ 'space-record']` when a record lives at `/s/:id`: it binds the class's variant to `navigation.currentPageId`, the
108
+ `active` variant on those pages and `idle` everywhere else. Do not write the binding by hand with a `when` rule per
109
+ page; that is the long form this replaces.
113
110
 
114
111
  ## Pages from a factory
115
112
 
@@ -9,7 +9,7 @@ custom({ id: 'arcade', renderType: 'nebulaRun', shipColor: 'amber', bind: { best
9
9
 
10
10
  ## Creating one
11
11
 
12
- The CLI writes it, in the shape Plitzi's own elements are written in: `npx @plitzi/cli add plugin seat-picker` (a
12
+ The CLI writes it, in the shape Plitzi's own elements are written in: `npx @plitzi/cli plugin add seat-picker` (a
13
13
  package of its own and packing it: the CLI skill).
14
14
 
15
15
  One folder, four files: `SeatPicker.tsx` (the component), `declaration.ts` (its `type`, the `triggers` it fires, the
@@ -88,7 +88,7 @@ the widths the space's styles use, instead of a width of the plugin's own.
88
88
 
89
89
  ## Its own server code
90
90
 
91
- `plitzi add plugin board --server` writes `functions/index.ts` in its folder (the CLI skill). Its routes answer
91
+ `plitzi plugin add board --server` writes `functions/index.ts` in its folder (the CLI skill). Its routes answer
92
92
  under `/fn/plugins/board/`; the component names them, with nothing for the space to wire:
93
93
 
94
94
  ```tsx
@@ -96,7 +96,7 @@ const route = usePluginRoute('board'); // from '@plitzi/plitzi-sdk'
96
96
  const url = route('/layout'); // undefined where no server runs code (the canvas)
97
97
  ```
98
98
 
99
- Its `ctx.kv` is the plugin's own; the space's credentials and channels are not.
99
+ Its `ctx.kv` is the plugin's own; the space's credentials, channels and data are not.
100
100
 
101
101
  ## Talking to other pages
102
102
 
@@ -0,0 +1,25 @@
1
+ # Recipes
2
+
3
+ Each is a whole file that authors with no warning — CI holds it to that. Copy the one that fits, then change the names.
4
+
5
+ | To… | Open |
6
+ | --- | --- |
7
+ | Show data with no backend: a JSON file, a card per row, a count, an empty state, a computed value | [recipes/show-data.ts](../recipes/show-data.ts) |
8
+ | Data typed by a sample of it: completed and checked paths, typed rows | [recipes/typed-data.ts](../recipes/typed-data.ts) |
9
+ | Data read by the page server (`--mode server`) | [recipes/server-data.ts](../recipes/server-data.ts) |
10
+ | An accordion, one answer open at a time | [recipes/accordion.ts](../recipes/accordion.ts) |
11
+ | Filter and sort a list from a select | [recipes/filter-a-list.ts](../recipes/filter-a-list.ts) |
12
+ | A page per record (`/products/:slug`), and "not found" | [recipes/detail-page.ts](../recipes/detail-page.ts) |
13
+ | Link to a section of a page (`/#plans`) | [recipes/link-to-a-section.ts](../recipes/link-to-a-section.ts) |
14
+ | A carousel: slides with arrows, dots and autoplay; a marquee; a row that swipes | [recipes/carousel.ts](../recipes/carousel.ts) |
15
+ | Do something every few seconds — a ticker, a poll, a slideshow by hand | [recipes/every-few-seconds.ts](../recipes/every-few-seconds.ts) |
16
+ | A marquee that scrolls for ever and stops under the pointer | [recipes/marquee.ts](../recipes/marquee.ts) |
17
+ | A row that scrolls sideways, with arrows that hide at its ends | [recipes/scroll-a-row.ts](../recipes/scroll-a-row.ts) |
18
+ | A link built from a row: WhatsApp with the product's name, `mailto:` | [recipes/link-with-data.ts](../recipes/link-with-data.ts) |
19
+ | React to a click or a submit; a modal; a switch | [recipes/forms-and-modals.ts](../recipes/forms-and-modals.ts) |
20
+ | A feature behind a flag | [recipes/feature-flag.ts](../recipes/feature-flag.ts) |
21
+ | An element of your own (a plugin), checked | [recipes/plugin.ts](../recipes/plugin.ts) |
22
+ | Controls a screen reader and a browser agent can use | [recipes/usable-without-sight.ts](../recipes/usable-without-sight.ts) |
23
+ | Tokens, a shared class, "the class plus one thing", tablet and phone at once | [recipes/style-a-page.ts](../recipes/style-a-page.ts) |
24
+ | A design in Tailwind classes, kept as classes the builder edits | [recipes/from-tailwind.ts](../recipes/from-tailwind.ts) |
25
+ | A page from elsewhere in a frame (`embed`: a map, a player) and a drawing of your own (`svg`) | [recipes/embed-and-svg.ts](../recipes/embed-and-svg.ts) |
@@ -50,7 +50,8 @@ Screen readers and browser agents (Claude in Chrome) work a page through its acc
50
50
 
51
51
  ## No copies
52
52
 
53
- - [ ] Chrome shared by pages is a layout; the current menu entry comes from `activeOn`, not per-page styling.
53
+ - [ ] Chrome shared by pages is a layout; the current menu entry marks itself (`current` state, `current: 'section'`),
54
+ not per-page styling.
54
55
  - [ ] A look used twice is a class; a tree used twice is a function or a `map` over data; a list of pages, links or
55
56
  plans is ONE array everything reads.
56
57
  - [ ] No positional ids (`container-45`) in what you wrote; every referenced element is named, and a helper that