@plitzi/sdk-server 0.37.9 → 0.38.1

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 (102) hide show
  1. package/CHANGELOG.md +615 -0
  2. package/README.md +17 -0
  3. package/dist/adapters/cloudAdapters.d.ts +5 -3
  4. package/dist/adapters/cloudAdapters.js +120 -34
  5. package/dist/adapters/space/types.d.ts +3 -0
  6. package/dist/core/configSeam.d.ts +14 -0
  7. package/dist/core/configSeam.js +114 -0
  8. package/dist/core/configSeam.test.d.ts +1 -0
  9. package/dist/core/createServer.js +11 -5
  10. package/dist/core/createServer.publicData.test.d.ts +1 -0
  11. package/dist/core/freePort.d.ts +10 -0
  12. package/dist/core/freePort.js +36 -0
  13. package/dist/core/freePort.test.d.ts +1 -0
  14. package/dist/core/http/stages/images.d.ts +6 -0
  15. package/dist/core/http/stages/images.js +47 -0
  16. package/dist/core/http/stages/images.test.d.ts +1 -0
  17. package/dist/core/server/drain.js +9 -4
  18. package/dist/core/server/fleet/channel.js +1 -1
  19. package/dist/core/server/fleet/relay.js +1 -1
  20. package/dist/core/services/action.js +5 -4
  21. package/dist/core/services/registry.js +2 -0
  22. package/dist/helpers/cache/keys.d.ts +23 -10
  23. package/dist/helpers/cache/keys.js +16 -2
  24. package/dist/helpers/flagOverrides.d.ts +27 -0
  25. package/dist/helpers/flagOverrides.js +25 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +2 -1
  28. package/dist/modules/actions/moduleFor.js +5 -3
  29. package/dist/modules/actions/runtime/flags.d.ts +16 -0
  30. package/dist/modules/actions/runtime/flags.js +49 -0
  31. package/dist/modules/actions/runtime/flags.test.d.ts +1 -0
  32. package/dist/modules/actions/runtime/kvList.js +1 -1
  33. package/dist/modules/actions/runtime/runAction.js +2 -0
  34. package/dist/modules/actions/transport/callHandler.js +8 -0
  35. package/dist/modules/actions/types.d.ts +14 -1
  36. package/dist/modules/connectors/engine.js +1 -1
  37. package/dist/modules/functions/capabilities.js +1 -1
  38. package/dist/modules/functions/load.js +2 -2
  39. package/dist/modules/functions/manifest.js +1 -1
  40. package/dist/modules/functions/runner/messages.js +1 -1
  41. package/dist/modules/images/allowlist.d.ts +5 -0
  42. package/dist/modules/images/allowlist.js +18 -0
  43. package/dist/modules/images/imageProxy.d.ts +36 -0
  44. package/dist/modules/images/imageProxy.js +273 -0
  45. package/dist/modules/images/imageProxy.test.d.ts +1 -0
  46. package/dist/modules/images/transform.d.ts +10 -0
  47. package/dist/modules/images/transform.js +28 -0
  48. package/dist/modules/images/transform.test.d.ts +1 -0
  49. package/dist/modules/rsc/connectorRscData.d.ts +12 -7
  50. package/dist/modules/rsc/connectorRscData.js +12 -8
  51. package/dist/modules/rsc/handler.js +18 -4
  52. package/dist/modules/rsc/publicFileResolver.d.ts +13 -0
  53. package/dist/modules/rsc/publicFileResolver.js +39 -0
  54. package/dist/modules/rsc/publicFileResolver.test.d.ts +1 -0
  55. package/dist/modules/rsc/resolveRscData.d.ts +4 -2
  56. package/dist/modules/rsc/resolveRscData.js +10 -2
  57. package/dist/modules/runtime/bundle.js +2 -2
  58. package/dist/modules/ssr/Component.d.ts +5 -1
  59. package/dist/modules/ssr/Component.js +3 -1
  60. package/dist/modules/ssr/prepareRender.js +36 -8
  61. package/package.json +19 -14
  62. package/skills/plitzi-authoring/CHEATSHEET.md +135 -0
  63. package/skills/plitzi-authoring/SKILL.md +71 -71
  64. package/skills/plitzi-authoring/recipes/carousel.ts +114 -0
  65. package/skills/plitzi-authoring/recipes/detail-page.ts +70 -0
  66. package/skills/plitzi-authoring/recipes/embed-and-svg.ts +48 -0
  67. package/skills/plitzi-authoring/recipes/every-few-seconds.ts +119 -0
  68. package/skills/plitzi-authoring/recipes/feature-flag.ts +24 -0
  69. package/skills/plitzi-authoring/recipes/filter-a-list.ts +63 -0
  70. package/skills/plitzi-authoring/recipes/forms-and-modals.ts +64 -0
  71. package/skills/plitzi-authoring/recipes/from-tailwind.ts +52 -0
  72. package/skills/plitzi-authoring/recipes/link-to-a-section.ts +35 -0
  73. package/skills/plitzi-authoring/recipes/link-with-data.ts +40 -0
  74. package/skills/plitzi-authoring/recipes/marquee.ts +57 -0
  75. package/skills/plitzi-authoring/recipes/plugin.ts +53 -0
  76. package/skills/plitzi-authoring/recipes/scroll-a-row.ts +79 -0
  77. package/skills/plitzi-authoring/recipes/show-data.ts +73 -0
  78. package/skills/plitzi-authoring/recipes/style-a-page.ts +45 -0
  79. package/skills/plitzi-authoring/recipes/typed-data.ts +64 -0
  80. package/skills/plitzi-authoring/recipes/usable-without-sight.ts +39 -0
  81. package/skills/plitzi-authoring/reference/accessibility.md +2 -2
  82. package/skills/plitzi-authoring/reference/authoring-errors.md +195 -53
  83. package/skills/plitzi-authoring/reference/colours-and-motion.md +63 -0
  84. package/skills/plitzi-authoring/reference/components.md +9 -1
  85. package/skills/plitzi-authoring/reference/data-and-visibility.md +29 -29
  86. package/skills/plitzi-authoring/reference/efficiency.md +53 -0
  87. package/skills/plitzi-authoring/reference/elements-and-styles.md +39 -24
  88. package/skills/plitzi-authoring/reference/feature-flags.md +78 -0
  89. package/skills/plitzi-authoring/reference/flows.md +25 -36
  90. package/skills/plitzi-authoring/reference/kept-state.md +28 -0
  91. package/skills/plitzi-authoring/reference/layouts.md +20 -3
  92. package/skills/plitzi-authoring/reference/lists.md +45 -16
  93. package/skills/plitzi-authoring/reference/plugins.md +15 -0
  94. package/skills/plitzi-authoring/reference/realtime.md +35 -0
  95. package/skills/plitzi-authoring/reference/review-checklist.md +8 -3
  96. package/skills/plitzi-authoring/reference/{templates-and-export.md → snippets-and-export.md} +9 -8
  97. package/skills/plitzi-authoring/reference/structure.md +29 -20
  98. package/skills/plitzi-authoring/reference/tailwind.md +19 -0
  99. package/skills/plitzi-authoring/reference/templates.md +6 -5
  100. package/skills/plitzi-authoring/reference/testing.md +11 -0
  101. package/skills/plitzi-authoring/reference/typed-sources.md +20 -0
  102. package/skills/plitzi-authoring/reference/validation.md +6 -2
@@ -1,70 +1,212 @@
1
- # When `authorSpace` refuses
1
+ # When `authorSpace` refuses, warns or suggests
2
2
 
3
- `authorSpace` checks the whole declaration before it writes anything, and stops at the first thing that would not
4
- render as written. The message names the element (`Element "text" (price) at shop/home/0/2`) and what to write
5
- instead. **Do what the message says.** Never cast past a check, silence it, or move the logic into a plugin to avoid
6
- it — the check exists because that declaration renders something other than what it says.
3
+ <!-- Generated from AUTHORING_CODES (src/schema/codes.ts) by `yarn generate:authoring-errors`. Do not edit. -->
4
+
5
+ Search this page for the code in brackets — `[class-and-css]` — rather than reading it whole, or ask for one:
6
+ `npx @plitzi/cli explain class-and-css`.
7
+
8
+ `authorSpace` reports everything it cannot write in one run, as a `SpaceRefusedError` whose `refusals` each carry
9
+ a `code`, the line of your code that wrote the element (`src/site/home.ts:417`) and the nearest named element
10
+ (`"store-footer" › container[1]`). **Do what the message says.** Never cast past a check, silence it, or move the
11
+ logic into a plugin to avoid it — the check exists because that declaration renders something other than what it says.
7
12
 
8
13
  The one exception is a TEST fixture whose subject is the break itself — how the runtime copes with a document no author
9
14
  would write. It names that break, and only that one: `authorSpace(spec, { allow: [{ code, element, why }] })`. See
10
15
  [testing](testing.md#spaces-written-for-a-test). A space anybody visits never has an `allow`.
11
16
 
17
+ More on a topic: [templates](templates.md), [feature flags](feature-flags.md), [accessibility](accessibility.md),
18
+ [components](components.md), [flows](flows.md).
19
+
12
20
  ## Refused
13
21
 
14
- | The message says | What was wrong | Write instead |
22
+ The space is not written until these are fixed.
23
+
24
+ | Code | What was wrong | Write instead |
15
25
  | --- | --- | --- |
16
- | `has "x", which it does not take` | a field the space, a page, an element, a binding or a step does not have | the field it suggests; an element's attribute goes inside the factory's props (`button({ content })`) |
17
- | `sets "x", which a "button" never reads` | an attribute the element does not have | one it lists; an event (`onClick`) is a flow: `flows: [[onClick(), …]]` |
18
- | `` `subType` is "h7" … It is one of `` | a value outside the attribute's list | one of the listed values |
19
- | `reads it as true or false` / `as a list` | text where a flag or a list is read | `disabled: true`, not `'true'`; `items: [ … ]` |
20
- | `holds none` | children on a type that renders only its own attributes | a `container` (for a heading made of parts, `container({ subType: 'h1', children })`) |
21
- | `the template … cannot be read as written` | a template with an operator, filter or function that does not exist, or broken syntax | see [templates](templates.md) for what exists; `matches` never does |
22
- | `reads "x", which nothing here answers to` | a name in a template that is not a source, a variable or a route param | the full source name (`list_rows.item`), `navigation.queryParams.x` for a query param, `source` for the bound value |
23
- | `is not around it` | a source read by an element that is not inside the element publishing it | move the element inside it, or share the value through `state` |
24
- | `lands on "x", which a "text" never reads` | a binding onto an attribute the element does not have — the value arrives and nothing shows it | one it lists (`content`); to follow data with a class, bind `styleSelectors.base` in `initialState` |
25
- | `no element answers to the name "x"` | a binding whose source names an element that does not exist | the id of the element that publishes it |
26
- | `renders text — and "items" holds a list` | a text template feeding a list | `bindTemplate('items', src, '{{ … }}', { returns: 'value' })` |
27
- | `which a "x" never fires` / `never answers to it` | a flow on an event, or a step to an action, the element does not have | the element that fires it (the message names it); for a plugin, a name its declaration has — `declaredTrigger` / `declaredCallback` make a wrong one a compile error |
28
- | `does not start with its trigger` | a flow whose first step is not the event that runs it | `[onClick(), setState(…)]` |
29
- | `step "x" has the param "y"` / `"type" is "string"` | a step param that does not exist or a value outside its options | the params and values it lists |
30
- | `step "setState" sets "x" on "y", which a "z" never reads` | an element `setState`/`toggleState` writing a field the element does not have | an attribute it lists, or for `category: 'state'` `visibility` / `styleSelectors.<selector>` |
31
- | `names the page "x", and no page has that id` | a link or `navigate` to a page id that does not exist | an existing page id, or a path with its slash (`'/about'`) |
32
- | `a full URL, in page mode` | a URL, `mailto:` or `tel:` in a link left in page mode | `mode: 'external'` |
33
- | `is a controlled list with no items` | a list with nothing to render | `items: [ … ]` or `bind: { items: 'provider.data.rows' }` |
34
- | `` has items, but its `source` is "none" `` | a list whose items nothing reads — it renders its children once | `source: 'controlled'` |
35
- | `answers at /x for the same visitors as page` | two pages at one address | another slug — or `accessLevel` `'public'` on one and `'authenticated'` on the other |
36
- | `is not one CSS value` | an empty CSS value, or one with `;` or `{}` | one value per property; leave a property out instead of writing it empty |
37
- | `computed … declared after it` / `does not compute` | a computed value read before it is declared, or never declared | declare it in `computed`, above the one that reads it |
26
+ | `action-step-params` | a step with no params in an action whose ways in declare different inputs | the step's params |
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
+ | `action-without-steps` | an action with no steps | the steps it runs |
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 |
31
+ | `anchor-duplicate` | one anchor on two elements of the same page (layouts included) | rename all but one |
32
+ | `anchor-invalid` | an `anchor` or a link's `hash` that is not lowercase letters, digits and `-` | `anchor: 'plans'`, `hash: 'plans'` — no `#` |
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` |
34
+ | `anchor-no-tag` | an anchor on an element that renders no element of its own | the anchor on the element around it, or `subType: 'div'` on the provider |
35
+ | `anchor-repeated` | an anchor inside a list row or a component — it would be in the page once per row or instance | the anchor on the list, or on the element around the instance |
36
+ | `as-without-from` | `as` with no `from` — `as` is how the source `from` names is shown | `from: 'products.item.price', as: 'price'` |
37
+ | `attribute-kind` | text where a flag or a list is read | `disabled: true`, not `'true'`; `items: [ … ]` |
38
+ | `attribute-value` | a value outside the attribute's list (`subType: 'h7'`) | one of the listed values |
39
+ | `binding-category` | a binding `category` that does not exist | one of the categories it lists |
40
+ | `binding-shape` | a binding with no `to` or no `source` | `bind: { content: 'posts.item.title' }`, or `bindTemplate(to, source, template)` |
41
+ | `binding-source-out-of-scope` | a source read by an element that is not inside the element publishing it | move the element inside it, or share the value through `state` |
42
+ | `binding-source-unknown` | a binding whose source names an element that does not exist, or with its prefix written wrong | the id of the element that publishes it, written alone — the prefix is filled in |
43
+ | `binding-target-unknown` | a binding onto an attribute the element does not have — the value arrives and nothing shows it | one it lists (`content`); to follow data with a class, `variantFrom` |
44
+ | `callback-key-unknown` | a `setState` / `toggleState` on an element writing a field it does not have | an attribute it lists, or for `category: 'state'` `visibility` / `styleSelectors.<selector>` |
45
+ | `callback-not-answered` | a step sending an action the target element never answers to | the element that answers it; for a plugin, `declaredCallback(declaration, 'reset', { on })` |
46
+ | `channel-declaration` | a channel whose pattern or access the server cannot read | what the message says: `{ access: { mode: 'public' } }`, a pattern like `room:{id}` |
47
+ | `channel-grant` | a topic of a private channel opened with no grant to it | the grant the message names, or a public channel |
48
+ | `channel-topic` | a `channel` element with no topic | a topic a channel of the space covers |
49
+ | `children-in-leaf` | children on a type that renders only its own attributes (`heading`, `text`, `image`…) | a `container` — for a heading made of parts, `container({ subType: 'h1', children })` |
50
+ | `class-and-css` | an element wearing a shared class AND `css` or `states` of its own — it has one base selector | the rules on top of the class: `class: [card, { opacity: '0.5' }]` (needs an `id`) — or into the class |
51
+ | `class-and-selector` | an element with a shared class and a `selector` of its own | drop one: a shared class IS its selector |
52
+ | `class-conflict` | two different declarations for one class name | rename one, or make them agree |
53
+ | `class-listed-under-other-name` | a `styles()` declaration listed in `classes` under another name | list it under its own name |
54
+ | `class-undeclared` | a class name the space does not declare | declare it in `classes`, hand the `styles()` declaration itself, or write the rules with `css` |
55
+ | `component-duplicate` | two components with one id | a name each |
56
+ | `component-id` | a component whose `id` is not a name it can be placed by | a letter first, then letters, digits, `-` and `_` |
57
+ | `component-undeclared` | an instance of a component the space does not declare | the id it suggests, or declare it in `components` |
58
+ | `computed-name` | a computed value whose name a template cannot read as `computed.x` | letters, digits and `_` |
59
+ | `computed-not-template` | a computed value that is not a template | `'{{ state.x * 2 }}'` |
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
+ | `computed-unknown` | a computed value read before it is declared, or never declared | declare it in `computed`, above the one that reads it |
62
+ | `css-property-twice` | one property written twice in a rule set — `paddingTop` beside `'padding-top'` — so one would silently win | keep one |
63
+ | `css-property-unknown` | a CSS property that does not exist | the property it suggests; a custom property starts with `--` |
64
+ | `css-value` | an empty CSS value, or one with `;` or `{}` | one value per property; leave a property out instead of writing it empty |
65
+ | `element-load-strategy` | a `loadStrategy` the element does not take | one of the values the message lists |
66
+ | `element-rejected` | an element the schema refused to hold | the problem listed with it |
67
+ | `element-runtime` | a `runtime` the element does not run in | one of the values the message lists |
68
+ | `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(…)` |
69
+ | `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" |
70
+ | `flag-name` | a flag whose name a template cannot read as `flags.x` | letters, digits and `_` |
71
+ | `flag-rule-shape` | a flag rule that is not `{ when, value }`, or whose `value` is not `true` or `false` | `{ when: { … }, value: true }` |
72
+ | `flag-shape` | a flag whose `value` is not `true` or `false` | `{ value: false, rules: [] }` |
73
+ | `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 |
74
+ | `flag-unknown` | a template reading a flag the space does not declare | the declared name it suggests, or declare it |
75
+ | `flow-empty` | a flow with no steps | `[onClick(), setState({ … })]` |
76
+ | `flow-without-trigger` | a flow whose first step is not the event that runs it | `[onClick(), setState(…)]` |
77
+ | `folder-cycle` | a page folder inside itself | a `parent` that leads to the top |
78
+ | `folder-undeclared` | a page, a layout or a folder filed in a folder the space does not declare | declare the folder, or the name it suggests |
79
+ | `font-invalid` | a font the space cannot load as declared | the field the message names |
80
+ | `format-unknown` | an `as` that names no format of the space's `formats` | declare it once — `formats: { price: "{{ source\|currency('USD') }}" }` — or write the template in its place |
81
+ | `from-and-bind` | the main attribute bound twice, with `from` and in `bind` | keep `from`, and give `bind` the other attributes |
82
+ | `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' }` |
83
+ | `global-callback-module` | a global callback sent to a module other than the one that registers it | the step builder, which knows the module |
84
+ | `global-callback-undeclared` | a global-callback step builder naming an action no source declares | one of the actions it lists |
85
+ | `id-invalid` | an id a binding, a template or a test cannot name | a letter first, then letters, digits, `-` and `_`: `'hero-title'` |
86
+ | `id-shadows-global` | an element named like a global data source (`state`, `navigation`, `auth`…) | another id |
87
+ | `id-taken` | two elements with one id — ids are one namespace for the whole space | wrap what a helper builds in `scope('promos', ref => …)`: every id inside is prefixed, `ref('slides')` names one |
88
+ | `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 |
89
+ | `layout-undeclared` | a page or a layout inside a layout the space does not declare | the layout's id, or declare it in `layouts` |
90
+ | `list-items-ignored` | a list whose items nothing reads — `source: 'none'` renders its children once | `source: 'controlled'` |
91
+ | `list-without-items` | a controlled list with nothing to render | `items: [ … ]` or `bind: { items: 'provider.data.rows' }` |
92
+ | `loading-slot-unknown` | an `apiContainer`'s `loadingSlot` that is not the id of one of its children | the id of the child to show until the first answer — `loadingSlot: 'catalog-skeleton'` beside that child |
93
+ | `modifier-count` | more than one set of rules in a class list | one set, after the classes: `class: [card, { opacity: '0.5', 'margin-top': '8px' }]` |
94
+ | `modifier-without-id` | rules on top of a class on an element with no `id` — the id names the class they become | `id: 'hero-bg'` |
95
+ | `no-pages` | a space with no pages | `pages: [{ id: 'home', name: 'Home', slug: '', body: [] }]` |
96
+ | `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' |
97
+ | `outside-ancestor` | an element that reads the state of an element it is not inside | nest it in the element the message names |
98
+ | `page-access-level` | an `accessLevel` that is not one the router reads | one of the values the message lists |
99
+ | `page-route-reserved` | a page under `/fn`, where the space's functions answer | another slug |
100
+ | `page-route-taken` | two pages at one address for the same visitors | another slug — or `accessLevel` `'public'` on one and `'authenticated'` on the other |
101
+ | `page-target-unknown` | a link or `navigate` to a page id that does not exist | an existing page id, or a path with its slash (`'/about'`) |
102
+ | `page-target-url` | a URL, `mailto:` or `tel:` in a link left in page mode | `mode: 'external'` |
103
+ | `page-without-slug` | a page with no `slug` | `slug: ''` for the home page, its path for any other: `'about'`, `'blog/:slug'` |
104
+ | `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) |
105
+ | `prop-missing` | an instance without a prop its component requires | `component('card', { props: { name: … } })`, or bind it |
106
+ | `prop-name` | a prop whose name a template cannot read as `props.x` | letters, digits and `_` |
107
+ | `prop-unknown` | a prop the component does not declare, handed in or read | the prop it suggests, or declare it: `props: { name: { type: 'text' } }` |
108
+ | `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 |
109
+ | `props-outside-component` | `props.x` read outside a component | read the source it would have come from |
110
+ | `redirect-target-unknown` | a page that sends visitors it is not for to a page the space does not have | a page's id or slug (`''` is the home page), or a full URL |
111
+ | `row-and-children` | a list with a `row` and `children` — the row is what it renders | one of the two |
112
+ | `row-component` | a list's `row` naming a component the space does not declare, or one with no prop to take the row | a declared component with an `item` prop — or a single prop — for the row |
113
+ | `row-outside-list` | `row` on an element that is not a list | a `list` around it |
114
+ | `row-without-id` | a list whose `row` is a function, with no `id` — the row's sources are named after it | `list({ id: 'products', items, row: r => … })` |
115
+ | `rule-set-mixed` | a style that writes its rules beside `states`, `variants` or `ancestors` instead of under `css` | `{ css: { desktop: { … } }, states: { hover: { … } } }` — the rules under `css`, each of the others beside it |
116
+ | `selector-invalid` | a `selector` that is not a CSS class name | letters, digits, `-` and `_` |
117
+ | `selector-taken` | a `selector` that is a declared class, or another element's | `class` to share rules; a selector of an element's own is its alone |
118
+ | `setting-misplaced` | `settings.computed` or `settings.channels` written inside `settings` | `computed` and `channels` at the top of the space |
119
+ | `slot-children` | children handed to an instance outside its slots | `children: { slotName: [ … ] }`; a component with no slots takes no children |
120
+ | `slot-unknown` | a component slot that is not an element of its tree | the id of an element inside the component — usually an empty container |
121
+ | `source-field-unknown` | a path into a typed source that its sample does not have | read a field the sample has, or add the field to the sample |
122
+ | `state-key-list` | `settings.transientState` / `paintedState` that is not a list of top-level state keys | `transientState: ['demoStep']` — a dotted path names its top key |
123
+ | `state-painted-and-transient` | a state key both painted (kept for the server) and transient (never kept) | remove it from one of the two |
124
+ | `step-duplicate` | two steps with one name in a flow or an action | a name each |
125
+ | `step-name` | a step name a later step cannot read as `{{ name.field }}` | a letter first, then letters, digits, `-` and `_` |
126
+ | `step-params` | a step param that does not exist, or a value outside its options | the params and values it lists |
127
+ | `step-type` | a step `type` that does not exist | the step builders — `setState(…)`, `navigate(…)` — write it |
128
+ | `style-state-unknown` | a state a selector does not react to | one of the states it lists (`hover`, `focus`, `active`…) |
129
+ | `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` |
130
+ | `template-short-source` | a source named without its prefix inside a template or a step | the full name: `apiContainer_stats`, `list_rows` |
131
+ | `template-source-out-of-scope` | a template reading the source of an element it is not inside | move the element inside it, or share the value through `state` |
132
+ | `template-text-into-value` | a template, which renders text, feeding an attribute that holds a list or an object | `bindTemplate('items', source, '{{ … }}', { returns: 'value' })` |
133
+ | `template-unknown-name` | a name in a template that is not a source, a variable or a route param | the full source name (`list_rows.item`), `navigation.queryParams.x`, or `source` for the bound value |
134
+ | `template-unreadable` | a template with an operator, a filter or a function that does not exist, or broken syntax | an operator, a filter or a function the interpreter has — the message names the one it could not read; `matches` never exists |
135
+ | `transformer-params` | a transformer param that does not exist, or a value outside its options | the params and values it lists |
136
+ | `trigger-interval` | an `onInterval` that never ticks: not a whole number of milliseconds, or below 250 | `onInterval(5000)` |
137
+ | `trigger-keys` | an `onKey` shortcut that cannot fire: two keys at once, only modifiers, or a name that is not a key | `onKey('f')`, `onKey('shift+f')`, `onKey('mod+k, escape')` |
138
+ | `trigger-never-fired` | a flow on an event the element never fires | the element that fires it; for a plugin, `declaredTrigger(declaration, 'onPick')` |
139
+ | `tw-class-conflict` | two classes set the same property and neither is the narrower one (`p-2 p-4`) | keep one |
140
+ | `tw-no-equivalent` | a Tailwind class or variant with no exact equivalent in a Plitzi style (`sm:`, `dark:`, `space-x-4`) | what the message names: `md:`/`lg:`/`max-md:`/`max-lg:`, a token with both themes, `gap` |
141
+ | `tw-unknown-class` | a class `tw()` does not know as Tailwind | the class it suggests; a colour of the space through `createTw({ colors })`; any CSS as `[property:value]` |
142
+ | `UNKNOWN_CSS_PROPERTY` | a property in the style document the style editor cannot read back | the property it suggests |
143
+ | `unknown-attribute` | an attribute the element does not have | one it lists; an event (`onClick`) is a flow: `flows: [[onClick(), …]]` |
144
+ | `unknown-field` | a field the space, a page, an element, a binding or a step does not have | the field it suggests; an element's attribute goes inside the factory's props (`button({ content })`) |
145
+ | `unknown-transformer` | a transformer that does not exist | one of the transformers it lists |
146
+ | `utility-module` | a utility sent to a module — a utility takes none | drop the `on` |
147
+ | `visibility-as-attribute` | `visibility` bound as an attribute, which no element reads | `visible: { source, template }` |
148
+ | `visitor-roles` | `settings.visitorRoles` written in a shape the runtime does not read | the shape the message gives |
149
+ | `while-running` | `whileRunning` on a step that is not the trigger, or a mode other than `skip`, `parallel`, `queue` | `[whileRunning('queue', onClick()), …]` |
38
150
 
39
151
  ## Warned
40
152
 
41
- A warning means the space renders, and renders something you probably did not mean. Fix every one.
153
+ The space renders, and renders something you probably did not mean. Fix every one.
42
154
 
43
- | Code | Meaning | Fix |
155
+ | Code | What was wrong | Write instead |
44
156
  | --- | --- | --- |
45
- | `condition-starts-visible` | a computed visibility shows until its data answers | `visible: { source, template }` |
46
- | `template-never-resolved` | a condition in an attribute, used as written | move it into `bindTemplate` |
47
- | `tablet-rule-skips-mobile` | a tablet rule phones never get | write it under `compact` |
48
- | `default-content-beside-children` | a button prints "Button" beside its children | `content: ''` |
49
- | `overlay-starts-open` | a modal or dialog is open when the page loads | `visible: false`, opened by `openModal` |
50
- | `provider-without-source` | an `apiContainer` asks nothing | give it a `query` (or `action`, `connector`, `resource`) |
51
- | `unknown-element-type` | a type no built-in element has | the built-in it suggests; a plugin's DECLARATION goes in `authorSpace(space, { plugins: [declaration] })`, which also checks its events, actions and attributes |
157
+ | `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` |
52
158
  | `colour-without-dark` | a colour token with no dark value | `{ light, dark, default }` |
53
- | `FORM_SUBMIT_UNMANAGED` | a form the browser would submit itself | `managedByInteractions: true` |
54
- | `STYLE_WITHOUT_TAG` | style on a provider that renders no element | `subType: 'div'` on the provider, or style its parent |
55
- | `server-data-without-rsc` | a `runtime: 'server'` provider with a `connector` or `action` in a space that does not turn server data on — it renders its mock data | `rsc: { enabled: true }` on the space |
56
- | `route-param-undeclared` | `navigation.routeParams.x` read on a page whose slug has no `:x` — always empty | add `:x` to the slug, or read `navigation.queryParams.x` |
57
- | `form-control-unnamed` | a control in a form with no `name` — its value never reaches `values` | `formControl({ name: 'email', … })` |
159
+ | `condition-starts-visible` | a computed visibility that shows until its data answers | `visible: { source, template }` |
160
+ | `control-in-decorative` | a control inside a `decorative` container — Tab lands on something nothing announces | move it out of the illustration |
161
+ | `control-without-name` | a button or a link with no words, or a field nothing names | `title` on the button, `label` on the link, `label` (with `hideLabel: true`) on the field |
162
+ | `default-content-beside-children` | a button printing its default "Button" beside its children | `content: ''` |
163
+ | `dropdown-without-control` | a dropdown opened from a box or an icon — no keyboard opens it | a `button` as what opens it (`title` if it is only an icon) |
164
+ | `embed-without-title` | an `embed` with no `title` — a frame a screen reader cannot describe | say what it shows in `title` |
165
+ | `flag-rule-empty` | a flag rule with no conditions — skipped, never read as "always" | give it a condition, or set the flag's `value` instead |
166
+ | `flag-unused` | a declared flag nothing gates on and no template reads | gate what it switches (`flag: 'x'`), or remove it once the feature has shipped |
167
+ | `FORM_SUBMIT_UNMANAGED` | a form the browser would submit itself, reloading the page | `managedByInteractions: true` |
58
168
  | `form-control-name-taken` | two controls in one form with one name — one overwrites the other | a name each |
59
- | `while-running` | `whileRunning` on a step that is not the trigger, or a value other than `skip`, `parallel`, `queue` | `[whileRunning('queue', onClick()), …]` |
60
- | `trigger-keys` | an `onKey` flow whose `keys` cannot fire: two keys in one shortcut, only modifiers, or a name that is not a key | `onKey('f')`, `onKey('shift+f')`, `onKey('mod+k, escape')` |
61
- | `state-toggled-in-branches` | two `setState` steps of one key, each under a `when` on that key — the second flips back what the first wrote | `toggleState({ key })`; for something shown by default, a key named for hiding it |
62
- | `form-value-compared-to-blank` | a `when` asking whether a submitted field (`….values.x`) `=` or `!=` `""` — a field nobody typed in is not sent, so it never matches | `operator: 'empty'` / `'notEmpty'` |
63
- | `overlay-never-opened` | a modal or dialog that starts hidden and that no step opens | a flow with `openModal('id')` / `openDialog('id')` |
64
- | `control-without-name` | a button or link with no words (an icon, a picture without `alt`, nothing), or a field nothing names — a screen reader says just "button", a browser agent cannot find it | `title` on the button, `label` on the link, `label` (with `hideLabel: true`) on the field — see [accessibility](accessibility.md) |
169
+ | `form-control-unnamed` | a control in a form with no `name` — its value never reaches `values` | `formControl({ name: 'email', … })` |
170
+ | `form-value-compared-to-blank` | a `when` comparing a submitted field to `""` — a field nobody typed in is not sent | `operator: 'empty'` / `'notEmpty'` |
171
+ | `heading-level-skipped` | an `h4` right after an `h2` — the outline misses a level | the next level down; size it with its class |
65
172
  | `image-without-alt` | an image that is not `decorative` and has no `alt` | say what it shows, or `decorative: true` |
66
- | `click-on-static-element` | a click flow on a container, text, heading, image or list item — no keyboard reaches it, no agent finds it | the flow on a `button` (it holds children) or a `link` |
67
- | `heading-level-skipped` | an `h4` right after an `h2` on a page — the outline misses a level | the next level down; size it with its class |
68
- | `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`, …) |
69
- | `dropdown-without-control` | a dropdown opened from a box or an icon — no keyboard opens it | a `button` as what opens it (`title` if it is only an icon) |
70
- | `control-in-decorative` | a control inside a `decorative` container — Tab lands on something nothing announces | move it out of the illustration |
173
+ | `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`…) |
174
+ | `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 |
175
+ | `overlay-never-opened` | a modal or a dialog that starts hidden and that no step opens | a flow with `openModal('id')` / `openDialog('id')` |
176
+ | `overlay-starts-open` | a modal or a dialog open when the page loads | `visible: false`, opened by `openModal` |
177
+ | `painted-state-without-keep-state` | `paintedState` without `keepState` — nothing is kept for the server to draw with | `settings.keepState: true`, or remove `paintedState` |
178
+ | `provider-without-source` | an `apiContainer` that asks nothing | a `query` (or `action`, `connector`, `resource`) |
179
+ | `route-param-undeclared` | `navigation.routeParams.x` read on a page whose slug has no `:x` — always empty | add `:x` to the slug, or read `navigation.queryParams.x` |
180
+ | `server-data-without-rsc` | a `runtime: 'server'` provider with a `connector` or `action` in a space that does not turn server data on | `rsc: { enabled: true }` on the space |
181
+ | `span-holds-block` | a `container({ subType: 'span' })` holding a heading, a paragraph, a list, a form or prose | a `div` (leave `subType` out), or words: a `text` with `display: inline` in its class |
182
+ | `state-key-has-runtime-prefix` | a state key written with `runtime.state.` in front — the state callbacks already write below it | `key: 'cart'`, not `'runtime.state.cart'` |
183
+ | `state-toggled-in-branches` | two `setState` of one key, each under a `when` on that key — the second flips back what the first wrote | `toggleState({ key })`; for something shown by default, a key named for hiding it |
184
+ | `STYLE_WITHOUT_TAG` | style on a provider that renders no element of its own | `subType: 'div'` on the provider, or style its parent |
185
+ | `tablet-rule-skips-mobile` | a tablet rule phones never get — mobile inherits desktop, not tablet | write it under `compact` (tablet and mobile together) |
186
+ | `template-never-resolved` | a condition in an attribute, which only resolves `{{ name\|filter }}` — used as written | move it into `bindTemplate` |
187
+ | `transient-state-without-keep-state` | `transientState` without `keepState` — nothing is kept in the first place | `settings.keepState: true`, or remove `transientState` |
188
+ | `unknown-element-type` | a type no built-in element has | the built-in it suggests; a plugin's declaration goes in `authorSpace(space, { plugins: [declaration] })` |
189
+ | `unknown-global-callback` | a global callback no built-in source declares — it runs only if something registers it | the built-in it suggests, or make sure a plugin or module of the space registers it |
190
+ | `unknown-utility` | a utility that is not one of the built-in ones | one of the built-in utilities |
191
+ | `unknown-variable` | a `var(--x)` nothing in the space declares — the property it is in is dropped | declare it under `variables`, fix the name, or give it a fallback: `var(--x, …)` |
192
+
193
+ ## Suggested
194
+
195
+ Nothing is wrong: the page renders as written. Each is a shorter way to the same page — fewer elements, less CSS — or a lighter one, and `authorSpace` returns them in `suggestions`, the ones that save the most first. Take them: a space written the short way is the one an editor, an agent and the next person can read. More in [efficiency](efficiency.md) and [motion](colours-and-motion.md).
196
+
197
+ | Code | Written the long way | The short way |
198
+ | --- | --- | --- |
199
+ | `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 |
200
+ | `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 |
201
+ | `custom-css-notifications` | the toasts dressed with `.Toastify__toast` rules in `customCss` | `notifications: { font, fontSize, border, shadow, padding, background, text, radius }` |
202
+ | `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 |
203
+ | `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]` |
204
+ | `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 |
205
+ | `repeated-shape` | the same structure written again and again with different words — cards, rows, tiles | a component with props (`components`, `component(id, { props })`), or one `list` when they are rows of data side by side — a few cards a person rewords on the canvas can stay cards |
206
+
207
+ ## UPPER_CASE codes
208
+
209
+ `ORPHANED_ELEMENT`, `DUPLICATE_ELEMENT_ID`, `BROKEN_FLOW_LINK` and the rest of the UPPER_CASE codes come from the
210
+ documents' own structure — what `validateSchema` checks of any schema. Authoring derives every id, parent link and
211
+ flow chain, so only a hand-written or hand-edited JSON has them: author it instead (see
212
+ [snippets and export](snippets-and-export.md) to turn a JSON into code).
@@ -0,0 +1,63 @@
1
+ # Colours, themes and motion
2
+
3
+ ## Animations
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'`.
7
+ - **An element hidden by `visible` is `display: none`**, so an animation in its class runs again every time it is
8
+ shown — the way to animate a slide in (see recipes/every-few-seconds.ts).
9
+ - **Pause on hover** from the element around it: `ancestors: { [band.name]: { states: { hover:
10
+ { 'animation-play-state': 'paused' } } } }`.
11
+ - **A stagger** — rows arriving one after another — is `animation-delay` per `:nth-child` in `customCss`, against the
12
+ row's class.
13
+ - Keep motion for meaning. Whoever asks their machine for less motion gets it without a word from you: the SDK cuts
14
+ every animation and transition to an instant for them. Write a `prefers-reduced-motion` rule only for what that does
15
+ not cover — a hover that moves a card (`transform: none`), a decoration better not shown at all.
16
+
17
+ ## Motion — good practices
18
+
19
+ Only `opacity` and `transform` (`translate`, `scale`, `rotate`) animate off the main thread: they stay smooth while
20
+ the page hydrates and on a slow phone. Everything else repaints, or lays the page out again, on every frame.
21
+
22
+ - **Slide, grow, fade** with `transform` and `opacity` — never `left`, `top`, `width`, `height` or `margin`. Something
23
+ that travels across a box is `transform: translate(%)` on an element as large as the box.
24
+ - **No animated blur:** no `filter: blur()` in an entrance or a loop, no `backdrop-filter` over moving content. A soft
25
+ shape is a `radial-gradient` fading to transparent.
26
+ - **Fake the expensive ones:** a growing shadow is a second shadow on a pseudo-element whose `opacity` changes; a
27
+ pulsing glow is a static gradient whose `opacity` or `scale` changes.
28
+ - **Main-thread decoration waits for hydration:** a sweep on a custom property, a `background-position` seam — start
29
+ it paused, run it once the SDK's root says `data-hydrated`:
30
+ `'.glow { animation: glow 8s linear infinite paused; } [data-hydrated] .glow { animation-play-state: running; }'`.
31
+ - **Entrances:** `opacity` + a small `translateY`, under ~600ms, staggered by tens of ms.
32
+ - **One ambient loop per screen**, slow — not one per card. `infinite` runs on every visitor's battery for as long
33
+ as the page is open.
34
+ - **Name what transitions:** `transition: 'transform 200ms, opacity 200ms'`, never `all` (a hover that changes
35
+ `padding` becomes a layout animation).
36
+ - **`will-change`** only on the few elements about to move, never on every item of a list.
37
+
38
+ `authorSpace` suggests `heavy-animation` for keyframes a class or `customCss` runs off the compositor, naming each
39
+ property and the way out.
40
+
41
+ ## Colours and themes
42
+
43
+ Every colour is a **variable** with both values, and elements say `var(--name)`:
44
+
45
+ ```ts
46
+ variables: {
47
+ color: {
48
+ foreground: { light: '#0c0c14', dark: '#ededf3', default: '#0c0c14' },
49
+ card: { light: '#ffffff', dark: '#101019', default: '#ffffff' },
50
+ primary: { light: '#5b3df5', dark: '#6e52f7', default: '#5b3df5' }
51
+ }
52
+ }
53
+ ```
54
+
55
+ - **`tokens(variables)`** names them for a rule: declare the variables once (`satisfies SpaceSpec['variables']`), hand
56
+ the same object to the space, and `const t = tokens(variables)` gives `t.primary === 'var(--primary)'` — a name the
57
+ space does not declare is a type error. A `var(--x)` written by hand that nothing declares is warned
58
+ `unknown-variable`; one that may be missing on purpose takes a fallback, `var(--x, 8px)`.
59
+ - Choose each value against its own background: a colour picked on white is not the same colour on near-black.
60
+ - Fixed colours are only for surfaces that are fixed in both themes (a brand panel that is always dark) — and then
61
+ the text on them is fixed too. Theme-following text on a fixed background is the bug.
62
+ - A tint of a token is `color-mix(in srgb, var(--primary) 12%, transparent)`, which follows the theme for free.
63
+ - `themeToggle()` is the switch; `theme.resolved` (`light`/`dark`) is a global source for anything else that needs it.
@@ -6,6 +6,14 @@ the document holds every copy, and a person who later edits one in the builder e
6
6
  block written ONCE: the document holds one tree, every placement is an instance of it, and an edit to it is an edit to
7
7
  all of them, in code and in the builder alike.
8
8
 
9
+ `authorSpace` points them out: `repeated-shape` names copies of one structure saying different things, and
10
+ `repeated-on-pages` a block only some pages of a layout carry — both are a component ([efficiency.md](efficiency.md)).
11
+
12
+ Not a **snippet**. A snippet (`authorSnippet`, [snippets-and-export.md](snippets-and-export.md)) is a block published as
13
+ a JSON file for any space to drop in: each drop is a COPY, the page's own from then on, and nothing links it back. Write
14
+ a component for what must stay the same everywhere in this space; a snippet for a starting point people reshape, or
15
+ carry to another space.
16
+
9
17
  ```ts
10
18
  import { component, container, heading, text } from '@plitzi/sdk-authoring';
11
19
 
@@ -71,5 +79,5 @@ pages: [{ name: 'Shop', slug: 'shop', body: [
71
79
  | Many blocks of DIFFERENT structure built by the same logic | a **helper** returning `ElementSpec` |
72
80
  | A list of the same block over data | a **list** whose row is the block — or places the component ([lists.md](lists.md)) |
73
81
 
74
- A component is also what a person editing the space in the builder will find in its Components panel, open and edit
82
+ A component is also what a person editing the space in the builder will find under Components, at the foot of its Elements panel, open and edit
75
83
  once; a helper's output is, to them, a pile of copies.
@@ -3,12 +3,18 @@
3
3
  ## Bindings
4
4
 
5
5
  ```ts
6
- heading({ bind: { content: 'posts.title' } }) // short form: attributes
7
- text({ bind: [bindTemplate('content', 'stats.data.total', "{{ source|number_format(0, '.', ',') }} views")] })
6
+ heading({ from: 'posts.title' }) // the element's main attribute
7
+ text({ from: 'stats.data.total', as: "{{ source|number_format(0, '.', ',') }} views" }) // through a template
8
+ text({ from: 'products.item.price', as: 'price' }) // a format of the space's, by name
9
+ image({ alt: '', bind: { alt: 'posts.title' }, from: 'posts.cover' }) // `bind`: the other attributes
8
10
  container({ bind: [bindTemplate('width', 'state.xp', '{{ source / 10 }}%', { category: 'style' })] }) // a style
9
11
  text({ bind: [{ to: 'content', source: 'posts.title', when: { … } }] }) // full form: anything else
10
12
  ```
11
13
 
14
+ `from` binds what the type shows — a text's, heading's, paragraph's or button's `content`, an image's `src`, a link's
15
+ `href`, a list's `items` — and leaves it empty until the data answers. `as` shows it through a template (`source` is
16
+ the value), or a format the space names once in `formats: { price: "{{ source|currency('USD') }}" }` (filters
17
+ `currency` and `percent` are there for exactly this). `bind` is for every other attribute;
12
18
  `bindTemplate(to, source, template, { category?, returns? })` is the computed attribute — a template over the value
13
19
  at `source`. `returns: 'value'` hands over what a single `{{ expression }}` evaluates to instead of its text: a list's
14
20
  `items`, a number, a flag (see [lists](lists.md)).
@@ -62,14 +68,25 @@ apiContainer({ id: 'board', runtime: 'server', action: 'queue-board' })
62
68
  `"{{ source ? apiUrl ~ '/workspaces/' ~ source ~ '/stats' : '' }}"` with `source: 'state.workspace.id'`.
63
69
  - A space with any `runtime: 'server'` provider needs `rsc: { enabled: true }`.
64
70
 
71
+ ### Typed by a sample
72
+
73
+ `source('site', home)` types a provider's source from a sample of its answer, so a misspelt path is a type error —
74
+ see [typed-sources.md](typed-sources.md).
75
+
65
76
  ### Data in a project with no backend
66
77
 
67
78
  An offline project (`offlineMode`) still never invents data: the content lives in JSON files the project serves
68
- (`public/data/games.json` with Vite) and a provider reads them like any API — `apiContainer({ id: 'catalog', query:
79
+ (`public/data/games.json`, served in either mode) and a provider reads them like any API — `apiContainer({ id: 'catalog', query:
69
80
  '/data/games.json', cache: true, children: [ … ] })`. The page binds to `catalog.data.…` exactly as it would to a live
70
81
  backend, so swapping the file for a real endpoint later changes one `query`. `mockData` is what the BUILDER shows while
71
82
  editing; it is not a data source for the running page. Say on the page that demo content is demo content.
72
83
 
84
+ **In a server project (`create --mode server`), put the provider on the server** — `runtime: 'server'`, with
85
+ `rsc: { enabled: true }` on the space — and the page server reads the file from `public/` itself: the page arrives
86
+ with those sections in it (and their anchors in place) instead of fetching them once the browser has the page. A
87
+ browser provider leaves the server-rendered HTML without them. A `query` with `{{tokens}}` is still read in the
88
+ browser, against the visitor's route and state.
89
+
73
90
  ### Live, cached, refreshed
74
91
 
75
92
  - **Live:** `refreshSeconds: 15` asks again on its own (either runtime); it pauses in a hidden tab and never
@@ -85,6 +102,9 @@ editing; it is not a data source for the running page. Say on the page that demo
85
102
 
86
103
  ## Visibility
87
104
 
105
+ What the page's own data or state shows and hides. What a person switches on — a feature in beta, the old version
106
+ kept during a rollout — is a [feature flag](feature-flags.md): gated off, an element is not rendered at all.
107
+
88
108
  ```ts
89
109
  container({ visible: 'posts.hasPosts', … }) // shown while true
90
110
  container({ visible: '!posts.hasPosts', … }) // its inverse
@@ -131,6 +151,11 @@ Rules for a condition's template:
131
151
 
132
152
  ### Loading, empty, error
133
153
 
154
+ A provider renders nothing inside it until its first answer. A skeleton of what is coming is a child named by its
155
+ `loadingSlot` — `apiContainer({ loadingSlot: 'catalog-skeleton', children: [container({ id: 'catalog-skeleton' }),
156
+ …] })` — shown alone until then and gone after (`loading-slot-unknown` if no child has that id). So a provider goes
157
+ around the section that needs it, never around a layout's slot: there every page would wait on it.
158
+
134
159
  Four states, and each has its own element:
135
160
 
136
161
  | State | How to tell |
@@ -152,29 +177,4 @@ Both sides of one question are `visible: 'x'` and `visible: '!x'`, not an `x` an
152
177
 
153
178
  ## State that outlives a reload
154
179
 
155
- `render(…, { state })` seeds `runtime.state` when the page starts — the way a host hands a space what it already knows.
156
- `settings: { keepState: true, stateStorage: 'localStorage' }` keeps `runtime.state` across reloads, filed under
157
- whoever is signed in (a guest's state is the browser's; another account never sees it). Keep only what a person would
158
- expect back — favourites, a chosen theme. Everything else goes in `transientState`, which is never written and never
159
- brought back:
160
-
161
- ```ts
162
- settings: { keepState: true, transientState: ['filter', 'tourStep', 'panelOpen'] }
163
- ```
164
-
165
- Do not reset kept state from `onPageLoad` instead: what was kept is restored late — after hydration, once auth knows
166
- who this is — and lands in the middle of that flow, so half of it is undone. A filter restored on the next visit is a
167
- page that looks broken. Keeping state is the SPACE's setting; a page does not take `keepState`.
168
-
169
- That lateness shows. Kept state is restored after the server's first paint, so anything kept that changes what is
170
- DRAWN — the tool a toolbar shows as last picked, a name in an avatar, a panel left off — is drawn with its default and
171
- then swapped. List those keys in `paintedState`: they are kept in a cookie too, the server renders with them, and the
172
- page starts from the same values.
173
-
174
- ```ts
175
- settings: { keepState: true, paintedState: ['shapesPick', 'name', 'color'], transientState: ['panelOpen'] }
176
- ```
177
-
178
- Only what the first paint shows, and small values: the cookie travels with every request and holds a few kilobytes
179
- (over that, the page falls back to the defaults and the dev tools say so). Never a secret — a key, a token — and never
180
- a key that is also in `transientState` (`authorSpace` refuses it).
180
+ `keepState`, what is never kept, and what the first paint needs from it — see [kept-state.md](kept-state.md).
@@ -0,0 +1,53 @@
1
+ # Efficiency: the same page with fewer elements
2
+
3
+ Every element is a node the page renders, the builder lists, the document stores and the next reader has to read. A
4
+ space written the long way still renders — nothing in it is wrong — but it is two or three times the size it needs,
5
+ and every copy is a place the next edit has to be made again. The platform has a short way for each of the long ones
6
+ below. Use it from the start; `authorSpace` points out the ones you missed.
7
+
8
+ ## Read the suggestions
9
+
10
+ `authorSpace` returns `suggestions` beside `warnings` — the ones that save the most elements first, each with its code,
11
+ what to write instead, the elements it is about and how many it would save:
12
+
13
+ ```ts
14
+ const { warnings, suggestions } = authorSpace(space);
15
+ // [{ code: 'repeated-on-pages', saves: 57, at: 'src/pages/docs.ts:291', elementIds: [...], message: '4 pages carry…' }]
16
+ ```
17
+
18
+ `npm run author` prints them under the warnings (`[suggest]`), and `npm run author -- --json` carries them. A warning
19
+ is a bug to fix; a suggestion is a shorter way to the same page — take it unless you have a reason not to, and the
20
+ reason is usually that the copies are about to diverge. `npx @plitzi/cli explain repeated-on-pages` explains any code.
21
+
22
+ ## The long way, and the short one
23
+
24
+ | Written the long way | The short way | Suggestion |
25
+ | --- | --- | --- |
26
+ | 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` |
27
+ | 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` |
28
+ | 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` |
29
+ | 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 | `repeated-shape` |
30
+ | `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` |
31
+ | `.card:hover { … }`, `.card .icon { … }` in `customCss` | The class's own `states` and `ancestors` | `custom-css-class` |
32
+ | 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` |
33
+ | `.Toastify__toast { font-family: …; border: … }` in `customCss` | `notifications: { font, fontSize, border, shadow, padding, … }` | `custom-css-notifications` |
34
+ | 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` |
35
+
36
+ And a few the suggestions do not count, because they cost styles rather than elements:
37
+
38
+ - **A look used twice is a class**, written once with `styles()`; every element of a TYPE dressed the same way is the
39
+ space's `elements` defaults (`elements: { heading: { color: 'var(--fg)' } }`), not a class on each.
40
+ - **Inner parts are slots.** A form control's input, a modal's backdrop, a theme toggle's icons: style them with
41
+ `slots: { input: inputClass }` on the element, or the type's `slots` in `elements` — never a `customCss` rule reaching
42
+ into the element.
43
+ - **A container with one child and nothing of its own** — no class, no flow, no condition — is an element for nothing:
44
+ put what it was for on the child.
45
+ - **A wrapper that only marks where something goes** inside a parent that lays its children out takes
46
+ `css: { display: 'contents' }` (a layout's slot, a layout itself in a page that is a column): no box, so the layout
47
+ around it is what it was.
48
+
49
+ ## Measured
50
+
51
+ On the seeded spaces, taking these suggestions removed a third of a restaurant site (2,533 → 1,697 elements: header
52
+ and footer into a layout), two fifths of a blog (353 → 210: the per-page header provider into the layout) and of a
53
+ sign-in space (848 → 530: the split screen into a layout) — and rendered every page pixel for pixel the same.