@plitzi/sdk-server 0.38.3 → 0.38.5

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 (125) hide show
  1. package/CHANGELOG.md +530 -0
  2. package/README.md +142 -24
  3. package/dist/actions.d.ts +8 -5
  4. package/dist/actions.js +3 -1
  5. package/dist/adapters/jsonAdapters.d.ts +1 -1
  6. package/dist/adapters/jsonAdapters.js +16 -3
  7. package/dist/core/configSeam.js +20 -4
  8. package/dist/core/createServer.js +3 -2
  9. package/dist/core/handlers/socialRouteHandlers.js +6 -1
  10. package/dist/core/http/stages/devReload.d.ts +11 -0
  11. package/dist/core/http/stages/devReload.js +17 -0
  12. package/dist/core/http/stages/images.js +1 -1
  13. package/dist/core/server/baseServer.d.ts +2 -0
  14. package/dist/core/server/baseServer.js +3 -0
  15. package/dist/core/server/pageServer.js +18 -0
  16. package/dist/core/services/registry.js +2 -0
  17. package/dist/core/services/rsc.js +1 -1
  18. package/dist/core/services/spaceDocument.d.ts +2 -0
  19. package/dist/core/services/spaceDocument.js +11 -0
  20. package/dist/functions-api.d.ts +6 -1
  21. package/dist/functionsRunner.d.ts +5 -0
  22. package/dist/functionsRunner.js +2 -1
  23. package/dist/helpers/hydrationPayload.d.ts +25 -4
  24. package/dist/helpers/hydrationPayload.js +36 -30
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +2 -2
  27. package/dist/modules/actions/index.d.ts +11 -2
  28. package/dist/modules/actions/index.js +29 -7
  29. package/dist/modules/actions/runtime/fileKv.d.ts +28 -0
  30. package/dist/modules/actions/runtime/fileKv.js +95 -0
  31. package/dist/modules/actions/runtime/fileKv.test.d.ts +1 -0
  32. package/dist/modules/actions/runtime/memoryKv.d.ts +23 -0
  33. package/dist/modules/actions/runtime/memoryKv.js +39 -24
  34. package/dist/modules/actions/runtime/namespaceKv.d.ts +2 -0
  35. package/dist/modules/actions/runtime/namespaceKv.js +5 -3
  36. package/dist/modules/actions/runtime/renderResolver.d.ts +3 -2
  37. package/dist/modules/actions/runtime/renderResolver.js +13 -9
  38. package/dist/modules/actions/runtime/renderShare.d.ts +7 -24
  39. package/dist/modules/actions/runtime/renderShare.js +47 -13
  40. package/dist/modules/actions/runtime/runAction.js +15 -1
  41. package/dist/modules/actions/runtime/scope.js +16 -1
  42. package/dist/modules/actions/runtime/signing.js +4 -1
  43. package/dist/modules/actions/runtime/taskContext.d.ts +7 -1
  44. package/dist/modules/actions/runtime/taskContext.js +6 -2
  45. package/dist/modules/actions/tasks/registry.d.ts +3 -1
  46. package/dist/modules/actions/tasks/registry.js +15 -1
  47. package/dist/modules/actions/types.d.ts +15 -2
  48. package/dist/modules/functions/config.d.ts +6 -0
  49. package/dist/modules/functions/context.d.ts +4 -2
  50. package/dist/modules/functions/context.js +43 -21
  51. package/dist/modules/functions/load.d.ts +8 -1
  52. package/dist/modules/functions/load.js +8 -4
  53. package/dist/modules/functions/manifest.d.ts +4 -1
  54. package/dist/modules/functions/manifest.js +14 -2
  55. package/dist/modules/functions/native.d.ts +2 -1
  56. package/dist/modules/functions/native.js +2 -2
  57. package/dist/modules/functions/plugins.test.d.ts +1 -0
  58. package/dist/modules/functions/routes.d.ts +8 -3
  59. package/dist/modules/functions/routes.js +41 -21
  60. package/dist/modules/functions/scope.d.ts +30 -0
  61. package/dist/modules/functions/scope.js +28 -0
  62. package/dist/modules/functions/space.d.ts +10 -5
  63. package/dist/modules/functions/space.js +37 -15
  64. package/dist/modules/images/imageProxy.js +19 -1
  65. package/dist/modules/rsc/connectorRscData.d.ts +3 -1
  66. package/dist/modules/rsc/connectorRscData.js +9 -5
  67. package/dist/modules/rsc/handler.d.ts +3 -16
  68. package/dist/modules/rsc/handler.js +10 -4
  69. package/dist/modules/rsc/publicFileResolver.d.ts +15 -9
  70. package/dist/modules/rsc/publicFileResolver.js +61 -23
  71. package/dist/modules/rsc/resolveRscData.d.ts +3 -1
  72. package/dist/modules/rsc/resolveRscData.js +8 -5
  73. package/dist/modules/runtime/bundle.d.ts +5 -0
  74. package/dist/modules/runtime/bundle.js +20 -5
  75. package/dist/modules/sqlite/kv.d.ts +34 -0
  76. package/dist/modules/sqlite/kv.js +99 -0
  77. package/dist/modules/sqlite/kv.test.d.ts +1 -0
  78. package/dist/modules/ssr/prepareRender.js +45 -6
  79. package/dist/modules/ssr/spaceDocument.d.ts +25 -0
  80. package/dist/modules/ssr/spaceDocument.js +72 -0
  81. package/dist/modules/ssr/spaceDocument.test.d.ts +1 -0
  82. package/dist/modules/ssr/testing/offlineData.d.ts +5 -0
  83. package/dist/modules/ssr/views/template.ejs +122 -20
  84. package/dist/plugins/compile.js +6 -21
  85. package/dist/plugins/compile.test.d.ts +1 -0
  86. package/dist/plugins/copy.d.ts +5 -0
  87. package/dist/plugins/copy.js +16 -1
  88. package/dist/plugins/copy.test.d.ts +1 -0
  89. package/dist/plugins/manager.d.ts +13 -0
  90. package/dist/plugins/manager.js +41 -12
  91. package/dist/plugins/watch.d.ts +12 -0
  92. package/dist/plugins/watch.js +79 -0
  93. package/dist/runtime.d.ts +1 -1
  94. package/dist/runtime.js +2 -2
  95. package/dist/sqlite.d.ts +17 -0
  96. package/dist/sqlite.js +2 -0
  97. package/package.json +8 -3
  98. package/skills/plitzi-authoring/CHEATSHEET.md +9 -5
  99. package/skills/plitzi-authoring/SKILL.md +24 -20
  100. package/skills/plitzi-authoring/recipes/accordion.ts +58 -0
  101. package/skills/plitzi-authoring/recipes/link-to-a-section.ts +1 -1
  102. package/skills/plitzi-authoring/recipes/plugin.ts +1 -1
  103. package/skills/plitzi-authoring/recipes/server-data.ts +65 -0
  104. package/skills/plitzi-authoring/recipes/show-data.ts +3 -2
  105. package/skills/plitzi-authoring/recipes/style-a-page.ts +1 -1
  106. package/skills/plitzi-authoring/recipes/typed-data.ts +1 -1
  107. package/skills/plitzi-authoring/reference/accessibility.md +6 -2
  108. package/skills/plitzi-authoring/reference/authoring-errors.md +16 -5
  109. package/skills/plitzi-authoring/reference/colours-and-motion.md +8 -2
  110. package/skills/plitzi-authoring/reference/data-and-visibility.md +38 -26
  111. package/skills/plitzi-authoring/reference/drawing.md +24 -0
  112. package/skills/plitzi-authoring/reference/efficiency.md +27 -1
  113. package/skills/plitzi-authoring/reference/elements-and-styles.md +5 -9
  114. package/skills/plitzi-authoring/reference/flows.md +3 -0
  115. package/skills/plitzi-authoring/reference/layouts.md +8 -23
  116. package/skills/plitzi-authoring/reference/lists.md +1 -1
  117. package/skills/plitzi-authoring/reference/performance.md +2 -2
  118. package/skills/plitzi-authoring/reference/plugins.md +51 -38
  119. package/skills/plitzi-authoring/reference/review-checklist.md +16 -2
  120. package/skills/plitzi-authoring/reference/snippets-and-export.md +2 -2
  121. package/skills/plitzi-authoring/reference/structure.md +32 -1
  122. package/skills/plitzi-authoring/reference/templates.md +6 -3
  123. package/skills/plitzi-authoring/reference/testing.md +10 -3
  124. package/skills/plitzi-authoring/reference/typed-sources.md +3 -1
  125. package/skills/plitzi-authoring/reference/validation.md +1 -1
@@ -0,0 +1,12 @@
1
+ import { PluginManager } from './manager';
2
+ /**
3
+ * Calls `onChange(key)` when a file a built plugin was built from changes: what a development server rebuilds and
4
+ * hands to the open pages (`devReload`), without restarting the process that serves them.
5
+ *
6
+ * The FOLDERS are watched, not the files. An editor that saves through a temporary file renames it over the old one,
7
+ * and a watcher on the old file hears nothing after the first save. Which files matter comes from the build itself
8
+ * (`PluginManager.onSources`), so a file one plugin imports from another's folder belongs to both.
9
+ *
10
+ * Returns the function that stops watching.
11
+ */
12
+ export declare const watchPluginSources: (manager: PluginManager, onChange: (key: string) => void) => (() => void);
@@ -0,0 +1,79 @@
1
+ import path from "node:path";
2
+ import { watch } from "node:fs";
3
+ //#region src/plugins/watch.ts
4
+ /** How long a burst of writes — an editor saving, formatting, saving again — is waited out before one rebuild. */
5
+ var SETTLE_MS = 80;
6
+ /**
7
+ * Calls `onChange(key)` when a file a built plugin was built from changes: what a development server rebuilds and
8
+ * hands to the open pages (`devReload`), without restarting the process that serves them.
9
+ *
10
+ * The FOLDERS are watched, not the files. An editor that saves through a temporary file renames it over the old one,
11
+ * and a watcher on the old file hears nothing after the first save. Which files matter comes from the build itself
12
+ * (`PluginManager.onSources`), so a file one plugin imports from another's folder belongs to both.
13
+ *
14
+ * Returns the function that stops watching.
15
+ */
16
+ var watchPluginSources = (manager, onChange) => {
17
+ /** File → the plugins built from it. */
18
+ const owners = /* @__PURE__ */ new Map();
19
+ /** Plugin → the files it was built from, to take its claim back when a build names others. */
20
+ const claims = /* @__PURE__ */ new Map();
21
+ const folders = /* @__PURE__ */ new Map();
22
+ const pending = /* @__PURE__ */ new Map();
23
+ const changed = (key) => {
24
+ clearTimeout(pending.get(key));
25
+ pending.set(key, setTimeout(() => {
26
+ pending.delete(key);
27
+ onChange(key);
28
+ }, SETTLE_MS));
29
+ };
30
+ const watchFolder = (folder) => {
31
+ if (folders.has(folder)) return;
32
+ try {
33
+ const watcher = watch(folder, { persistent: false }, (_event, name) => {
34
+ if (!name) return;
35
+ owners.get(path.join(folder, name))?.forEach(changed);
36
+ });
37
+ watcher.on("error", () => {
38
+ watcher.close();
39
+ folders.delete(folder);
40
+ });
41
+ folders.set(folder, watcher);
42
+ } catch {}
43
+ };
44
+ const unwatchUnused = () => {
45
+ const used = new Set([...owners.keys()].map((file) => path.dirname(file)));
46
+ folders.forEach((watcher, folder) => {
47
+ if (!used.has(folder)) {
48
+ watcher.close();
49
+ folders.delete(folder);
50
+ }
51
+ });
52
+ };
53
+ const stopListening = manager.onSources((key, inputs) => {
54
+ (claims.get(key) ?? []).forEach((file) => {
55
+ const keys = owners.get(file);
56
+ keys?.delete(key);
57
+ if (keys?.size === 0) owners.delete(file);
58
+ });
59
+ claims.set(key, inputs);
60
+ inputs.forEach((file) => {
61
+ owners.set(file, (owners.get(file) ?? /* @__PURE__ */ new Set()).add(key));
62
+ watchFolder(path.dirname(file));
63
+ });
64
+ unwatchUnused();
65
+ });
66
+ return () => {
67
+ stopListening();
68
+ pending.forEach((timer) => {
69
+ clearTimeout(timer);
70
+ });
71
+ pending.clear();
72
+ folders.forEach((watcher) => {
73
+ watcher.close();
74
+ });
75
+ folders.clear();
76
+ };
77
+ };
78
+ //#endregion
79
+ export { watchPluginSources };
package/dist/runtime.d.ts CHANGED
@@ -8,7 +8,7 @@ export { defineRuntime, endpointFor, endpointProblem } from './modules/runtime/c
8
8
  export type { SpaceRuntime, SpaceRuntimeContext, SpaceRuntimeEndpoint, SpaceRuntimeParts } from './modules/runtime/contract';
9
9
  export { RUNTIME_DESCRIBE_PATH, startSpaceRuntime } from './modules/runtime/host';
10
10
  export type { SpaceRuntimeDescription, SpaceRuntimeHost, SpaceRuntimeHostOptions } from './modules/runtime/host';
11
- export { inspectRuntime, loadRuntime, MAX_RUNTIME_BUNDLE_BYTES, packRuntime, runtimeBundleId } from './modules/runtime/bundle';
11
+ export { inspectRuntime, loadRuntime, loadRuntimeModule, MAX_RUNTIME_BUNDLE_BYTES, packRuntime, runtimeBundleId } from './modules/runtime/bundle';
12
12
  export { reachSpaceInside } from './modules/runtime/inside';
13
13
  export { createRuntimeRunner } from './modules/runtime/runner';
14
14
  export { createRuntimeProxyStage, runtimeEndpointsStage, serveRuntime } from './modules/runtime/stages';
package/dist/runtime.js CHANGED
@@ -2,6 +2,6 @@ import { defineRuntime, endpointFor, endpointProblem } from "./modules/runtime/c
2
2
  import { reachSpaceInside } from "./modules/runtime/inside.js";
3
3
  import { createRuntimeRunner } from "./modules/runtime/runner.js";
4
4
  import { RUNTIME_DESCRIBE_PATH, startSpaceRuntime } from "./modules/runtime/host.js";
5
- import { MAX_RUNTIME_BUNDLE_BYTES, inspectRuntime, loadRuntime, packRuntime, runtimeBundleId } from "./modules/runtime/bundle.js";
5
+ import { MAX_RUNTIME_BUNDLE_BYTES, inspectRuntime, loadRuntime, loadRuntimeModule, packRuntime, runtimeBundleId } from "./modules/runtime/bundle.js";
6
6
  import { createRuntimeProxyStage, runtimeEndpointsStage, serveRuntime } from "./modules/runtime/stages.js";
7
- export { MAX_RUNTIME_BUNDLE_BYTES, RUNTIME_DESCRIBE_PATH, createRuntimeProxyStage, createRuntimeRunner, defineRuntime, endpointFor, endpointProblem, inspectRuntime, loadRuntime, packRuntime, reachSpaceInside, runtimeBundleId, runtimeEndpointsStage, serveRuntime, startSpaceRuntime };
7
+ export { MAX_RUNTIME_BUNDLE_BYTES, RUNTIME_DESCRIBE_PATH, createRuntimeProxyStage, createRuntimeRunner, defineRuntime, endpointFor, endpointProblem, inspectRuntime, loadRuntime, loadRuntimeModule, packRuntime, reachSpaceInside, runtimeBundleId, runtimeEndpointsStage, serveRuntime, startSpaceRuntime };
@@ -0,0 +1,17 @@
1
+ /** A key/value store in a SQLite file, for a deployment with no database. Import it as `@plitzi/sdk-server/sqlite`.
2
+ *
3
+ * What server actions keep in `kv` — saved layouts, counters, the single-flight keys a run guard takes — lives in one
4
+ * process's memory by default, and goes with it. `createFileKv` (`@plitzi/sdk-server/actions`) keeps it in a JSON
5
+ * file for ONE process; this keeps it in SQLite, which every process on the file shares safely:
6
+ *
7
+ * ```ts
8
+ * import { createServer } from '@plitzi/sdk-server';
9
+ * import { createSqliteKv } from '@plitzi/sdk-server/sqlite';
10
+ *
11
+ * createServer({ adapters, action: { lookups, kv: createSqliteKv({ file: 'data/kv.sqlite' }) } });
12
+ * ```
13
+ *
14
+ * Its own entry because it loads `node:sqlite`, which needs Node 22.13 or later and prints an `ExperimentalWarning`
15
+ * when it loads: a deployment that does not use it never loads it. */
16
+ export { createSqliteKv } from './modules/sqlite/kv';
17
+ export type { SqliteKvOptions } from './modules/sqlite/kv';
package/dist/sqlite.js ADDED
@@ -0,0 +1,2 @@
1
+ import { createSqliteKv } from "./modules/sqlite/kv.js";
2
+ export { createSqliteKv };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plitzi/sdk-server",
3
- "version": "0.38.3",
3
+ "version": "0.38.5",
4
4
  "license": "AGPL-3.0",
5
5
  "files": [
6
6
  "dist",
@@ -40,6 +40,11 @@
40
40
  "import": "./dist/mongo.js",
41
41
  "default": "./dist/mongo.js"
42
42
  },
43
+ "./sqlite": {
44
+ "types": "./dist/sqlite.d.ts",
45
+ "import": "./dist/sqlite.js",
46
+ "default": "./dist/sqlite.js"
47
+ },
43
48
  "./handlers": {
44
49
  "types": "./dist/handlers.d.ts",
45
50
  "import": "./dist/handlers.js",
@@ -79,8 +84,8 @@
79
84
  "type": "module",
80
85
  "sideEffects": false,
81
86
  "dependencies": {
82
- "@plitzi/plitzi-sdk": "0.38.3",
83
- "@plitzi/sdk-shared": "0.38.3",
87
+ "@plitzi/plitzi-sdk": "0.38.5",
88
+ "@plitzi/sdk-shared": "0.38.5",
84
89
  "ejs": "^6.0.1",
85
90
  "esbuild": "^0.28.2",
86
91
  "jsonwebtoken": "^9.0.3",
@@ -18,7 +18,8 @@ const space: SpaceSpec = {
18
18
  components: [{ id, props, slots, root }], // a block placed many times with different content
19
19
  pages: [{ id, name, slug: '', layout: { id: 'shell', slot: 'main' }, body: [] }] // slot: where the body goes
20
20
  }; // slug '' is home; 'products/:slug' takes a param
21
- const { schema, style, warnings } = authorSpace(space, { plugins: [declaration] });
21
+ pageFamily({ folder, layout, seoTitle, body: entry => [...] }, ENTRIES) // pages of one shape, from data; ids scoped per page
22
+ const { schema, style, warnings, suggestions } = authorSpace(space, { plugins: [declaration] });
22
23
  ```
23
24
 
24
25
  ## Elements
@@ -33,7 +34,7 @@ paragraph('A paragraph.')
33
34
  container([children], { subType: 'section' }) // div, section, nav, header, footer, main, article, li, h1–h6, span…
34
35
  link({ href: 'about', content: 'About' }) // a page id, a '/path', or a URL — the mode follows from the href
35
36
  link({ href: 'home', hash: 'plans' }) // → /#plans, onto the element with anchor: 'plans'
36
- container({ motion: { enter: 'fade-up', on: 'view' }, children }) // arrives as it scrolls in; stagger, loop too
37
+ container({ motion: { enter: 'fade-up', on: 'view' }, children }) // arrives once as it comes into view; stagger, loop too
37
38
  button({ content: 'Save', flows }) // holds children too: then content: ''
38
39
  image({ src, alt }) // or decorative: true
39
40
  embed({ src, title }) // a map, a player: another page in a frame
@@ -45,6 +46,8 @@ list({ id: 'rows', items: 'p.data.rows', row: r => listItem({ children: [text({
45
46
  carousel({ id: 'hero', items: 'p.data.slides', autoplay: 5000, row: 'slide-card', children: [/* carouselNext('hero') arrows */] })
46
47
  // rows follow their item's `id`, or `itemKey: 'slug'`
47
48
  apiContainer({ id: 'p', query: '/data/x.json', cache: true, children }) // publishes p.data; no box of its own
49
+ apiContainer({ id: 'p', query: '/data/x.json', runtime: 'server', children }) // read by the page server: still p.data
50
+ apiContainer({ id: 'p', action: 'report', runtime: 'server', children }) // the action's output at p's root: p.rows
48
51
  // loadingSlot: 'p-skeleton' — a child shown until it answers
49
52
  form({ id, managedByInteractions: true, flows, children })
50
53
  formControl({ name: 'email', label: 'Email', subType: 'email' }) // select: options: [{ label, value }]
@@ -64,7 +67,7 @@ container({ id: 'hero', class: [card, { opacity: '0.5' }] }); // …plus one th
64
67
  container({ css: { padding: '8px' } }); // rules of its own — never with `class`
65
68
  ```
66
69
 
67
- Breakpoints: `desktop` (base), `tablet` (48–64rem), `mobile` (<48rem), `compact` (tablet AND mobile). Each inherits
70
+ Breakpoints: `desktop` (base), `tablet` (48–64rem), `mobile` (≤48rem), `compact` (tablet AND mobile). Each inherits
68
71
  from `desktop` alone. States: `hover`, `focus`, `focus-visible`, `active`, `disabled`…. An ancestor's state:
69
72
  `ancestors: { [card.name]: { states: { hover: { … } } } }`.
70
73
 
@@ -87,7 +90,8 @@ visible: false // hidden until a
87
90
 
88
91
  Inside a list row: `rows.item` in a binding, `{{ list_rows.item.x }}` in a template or an attribute,
89
92
  `list_rows.index` is a number from 0. A provider's source in a template is `apiContainer_p`… —
90
- spelled in full, as the element publishes it. Globals: `state`, `navigation` (`routeParams`, `queryParams`),
93
+ spelled in full, as the element publishes it; an id with a `-` keeps it (`list_study-plans`: a minus takes spaces,
94
+ `a - b`). Globals: `state`, `navigation` (`routeParams`, `queryParams`),
91
95
  `auth`, `variables`, `computed`, `flags`, `theme`, `host`.
92
96
 
93
97
  ## Flows
@@ -107,7 +111,7 @@ A flow is `[trigger, …steps]`, in an element's `flows`.
107
111
  [onClick(), navigate({ urlType: 'page', url: 'about' })]
108
112
  [onClick(), runServerAction({ actionId: 'checkout', input: { id: '{{ list_rows.item.id }}' } })]
109
113
  [onClick(), when({ field: 'state.step', operator: '=', value: '2' }, setState({ … }))]
110
- [whileRunning('queue', onClick()), delay(1000), …]
114
+ [whileRunning('queue', onClick()), delay(1000), …] // 'latest': a new firing stops the running one
111
115
  [named('picked', declaredTrigger(declaration, 'onPick')), …]; declaredCallback(declaration, 'reset', { on: 'seats' })
112
116
  ```
113
117
 
@@ -38,21 +38,21 @@ second into the first. If the MCP asks for a sign-in nobody can give, a project
38
38
 
39
39
  ## How to work
40
40
 
41
- 1. **Read before you write.** Open `src/space.ts` (and whatever it imports) and find the layout, the classes and the
41
+ 1. **Read before you write.** Open `src/space/index.ts` (and whatever it imports) and find the layout, the classes and the
42
42
  helpers already there. Extend them; do not add a second way of doing something the space already does.
43
- 2. **Change the declaration, then author it.** `npm run author` (or restart the server) runs `authorSpace`. It checks
44
- everything — every field, value, template, name, param and page link — and a refusal says what to write instead:
45
- do exactly that. Never work around a check, cast past it, or move the logic into a plugin to escape it. The first
46
- refusals come one at a time; the linter's come as ONE list — fix every line of it before running again. How it
47
- checks, a one-file author script for any project, and what it cannot see: [validation](reference/validation.md).
43
+ 2. **Change the declaration, then author it.** `npm run author` (or a restart) runs `authorSpace` — not `doctor`, which
44
+ is the project's. It checks every field, value, template, name, param and page link, and a refusal says what to
45
+ write instead: do exactly that. Never work around a check, cast past it, or move the logic into a plugin to escape
46
+ it. The linter's problems come as ONE list — fix every line before running again. How it checks, and what it
47
+ cannot see: [validation](reference/validation.md).
48
48
  3. **Read every warning, then every suggestion.** A warning names something written that will not do what it says —
49
- zero warnings is the bar. A suggestion (`[suggest]`, `suggestions` in `authorSpace`'s result) names a shorter way to
50
- the same page: a layout for chrome on every page, a component or a `list` for copies, a button's own `content`. Take
51
- them, the ones that save the most first. See [efficiency](reference/efficiency.md).
49
+ zero warnings is the bar. A suggestion (`[suggest]`) names a shorter way to the same page — a layout, a component,
50
+ a `list`, a button's own `content` — or what nothing uses. Take them, the ones that save the most first
51
+ ([efficiency](reference/efficiency.md)). Then `npm run lint:space`: how the source is written — long files, rows of
52
+ data inline, colours that are not tokens — at file:line. Leave both clean.
52
53
  4. **Look at it.** `npm run check -- /about --width 1440,390` says in text whether the page is whole — every element on
53
- screen, nothing overflowing, a clean console; `npm run shot -- /about --width 390 --scheme dark` is the picture, for
54
- when it says something is wrong (`--frames 4` shows what moves); `npm run visual`
55
- runs the checks. Look at desktop, tablet and mobile, light and dark, and the page while its data is still loading.
54
+ screen, nothing overflowing, a clean console; `npm run shot -- /about --width 390 --scheme dark` is the picture
55
+ (`--frames 4` shows what moves); `npm run visual` runs the checks. Look at desktop, tablet and mobile, light and dark, and the page while its data is still loading.
56
56
  5. **Go through the [review checklist](reference/review-checklist.md) before saying it is done.** It is the feedback a
57
57
  reviewer gives on every change, written down so you do not need to hear it.
58
58
 
@@ -61,15 +61,16 @@ second into the first. If the MCP asks for a sign-in nobody can give, a project
61
61
  1. **Name what is referred to.** Give an `id` to every element a binding, a flow or a test addresses. Ids are ONE
62
62
  namespace for the whole space — layouts and every page share it — so a helper that runs more than once builds inside
63
63
  `scope('promos', ref => …)`: every id in it is prefixed (`promos-panel`), and `ref('slides')` names one in full. An `id` is the element's name in the
64
- space (`data-id` in the page); the DOM id a URL's `#fragment` lands on is its `anchor`.
64
+ space (`data-plitzi-el` in the page); the DOM id a URL's `#fragment` lands on is its `anchor`.
65
65
  2. **Share with classes, layouts and components, never with copies.** A look used twice is a `styles()` class.
66
66
  Chrome shown on several pages — a header, a sidebar, a footer — is a **layout** the pages name, written once. A block
67
67
  placed many times with different content — a product card, a testimonial — is a **component** placed with
68
68
  `component(id, { props })`. A menu, a card grid, a list of steps is DATA, never a block pasted per item: a short
69
- menu mapped in code (`entries.map(entry => link({ href, content: entry.title }))`) is fine; rows — cards, dishes,
70
- tiles — are one `list` over them (`items: [ … ]`) whose row is written once. A link or a button says its words as
71
- its own `content`, not through a `text` inside it. See [layouts and duplication](reference/layouts.md),
72
- [components](reference/components.md) and [efficiency](reference/efficiency.md).
69
+ menu mapped in code is fine; rows — cards, dishes, tiles — are one `list` (`items: [ … ]`) whose row is written
70
+ once. A link or a button says its words as
71
+ its own `content`, not through a `text` inside it. Pages of one shape are one `pageFamily` over data. See
72
+ [layouts](reference/layouts.md), [components](reference/components.md), [structure](reference/structure.md) and
73
+ [efficiency](reference/efficiency.md).
73
74
  3. **One element, one base selector.** An element takes a shared `class` OR its own `css`, never both — authoring
74
75
  refuses the pair. "This class plus one thing" is the class with rules on top, last in the list:
75
76
  `class: [cover, { opacity: '0.25' }]` (needs the element's `id`; it becomes the class `<id>--own`). A look that
@@ -91,11 +92,12 @@ second into the first. If the MCP asks for a sign-in nobody can give, a project
91
92
  checked from the first commit.
92
93
  7. **Times in UTC, and say so.** Format with an explicit zone (`|date('j M · H:i', 'UTC')`) and print "UTC" beside it:
93
94
  a page rendered on a server and hydrated in a browser in another zone must agree on the hour.
94
- 8. **Breakpoints are ranges.** `tablet` (48–64rem) and `mobile` (below 48rem) each inherit only from `desktop`; a rule
95
+ 8. **Breakpoints are ranges.** `tablet` (48–64rem) and `mobile` (up to 48rem) each inherit only from `desktop`; a rule
95
96
  meant for both is written under `compact`.
96
97
  9. **Never invent data.** Numbers, names and states on screen come from a source. A panel with nothing true to say is
97
- an empty state, not a placeholder figure. With no backend, the data is JSON the project serves
98
- (`public/data/*.json`) read by an `apiContainer` — see [data and visibility](reference/data-and-visibility.md).
98
+ an empty state, not a placeholder figure. With no backend, the data is JSON read by an `apiContainer`: a server
99
+ project's `src/data/*.json`, read on the server and never served; with no server, `public/data/*.json`, which is
100
+ public — see [data and visibility](reference/data-and-visibility.md).
99
101
  10. **Never hand-write** `flat`, derived ids, `styleSelectors`, `beforeNode`/`afterNode`/`flowId`, or a
100
102
  `styleVariant` binding's key — use the factories, `variantFrom` and `activeOn`.
101
103
  11. **A switch is named for how it leaves its default.** `toggleState` turns a key nobody has set yet ON, so a key
@@ -121,6 +123,8 @@ Each is a whole file that authors with no warning — CI holds it to that. Copy
121
123
  | --- | --- |
122
124
  | 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) |
123
125
  | Data typed by a sample of it: completed and checked paths, typed rows | [recipes/typed-data.ts](recipes/typed-data.ts) |
126
+ | Data read by the page server (`--mode server`) | [recipes/server-data.ts](recipes/server-data.ts) |
127
+ | An accordion, one answer open at a time | [recipes/accordion.ts](recipes/accordion.ts) |
124
128
  | Filter and sort a list from a select | [recipes/filter-a-list.ts](recipes/filter-a-list.ts) |
125
129
  | A page per record (`/products/:slug`), and "not found" | [recipes/detail-page.ts](recipes/detail-page.ts) |
126
130
  | Link to a section of a page (`/#plans`) | [recipes/link-to-a-section.ts](recipes/link-to-a-section.ts) |
@@ -0,0 +1,58 @@
1
+ /**
2
+ * An accordion — questions whose answers open one at a time — written once and mapped over the questions: the open one
3
+ * is a key in `state`, and each question reads it the same way. The button says whether its answer is open
4
+ * (`ariaExpanded`) and which panel it opens (`controls`, by the panel's id), so a screen reader follows it; the panel is
5
+ * revealed by the state, so it starts hidden.
6
+ */
7
+ import { bindTemplate, button, container, onClick, paragraph, setState, styles } from '@plitzi/sdk-authoring';
8
+
9
+ import type { ElementSpec, SpaceSpec } from '@plitzi/sdk-authoring';
10
+
11
+ const QUESTIONS = [
12
+ { id: 'shipping', question: 'How long does shipping take?', answer: 'Three working days, anywhere in Europe.' },
13
+ { id: 'returns', question: 'Can I return an order?', answer: 'Within thirty days, unopened, at no cost.' },
14
+ { id: 'warranty', question: 'Is there a warranty?', answer: 'Two years on every product we sell.' }
15
+ ];
16
+
17
+ const question = styles('faq-question', {
18
+ css: {
19
+ display: 'flex',
20
+ 'justify-content': 'space-between',
21
+ width: '100%',
22
+ padding: '16px 0px',
23
+ border: 'none',
24
+ 'background-color': 'transparent',
25
+ 'font-size': '16px',
26
+ 'font-weight': '600',
27
+ cursor: 'pointer'
28
+ },
29
+ states: { 'focus-visible': { outline: '2px solid currentColor', 'outline-offset': '2px' } }
30
+ });
31
+
32
+ const answer = styles('faq-answer', { 'padding-bottom': '16px', margin: '0px' });
33
+
34
+ /** One question: the button that opens it — closing the one open, or this one again — and the answer it opens. */
35
+ const faqItem = ({ id, question: words, answer: text }: (typeof QUESTIONS)[number]): ElementSpec[] => [
36
+ button({
37
+ id: `${id}-question`,
38
+ content: words,
39
+ class: question,
40
+ // Named by the panel's id: authoring gives the panel the anchor the page needs for `aria-controls`.
41
+ controls: `${id}-answer`,
42
+ ariaExpanded: false,
43
+ bind: [bindTemplate('ariaExpanded', 'state.faq', `{{ source == '${id}' }}`, { returns: 'value' })],
44
+ flows: [[onClick(), setState({ key: 'faq', type: 'text', value: `{{ state.faq == '${id}' ? '' : '${id}' }}` })]]
45
+ }),
46
+ // Revealed by the state, so it starts hidden: nothing flashes open before the page knows which one is.
47
+ container({
48
+ id: `${id}-answer`,
49
+ visible: { source: 'state.faq', template: `{{ source == '${id}' }}` },
50
+ children: [paragraph({ content: text, class: answer })]
51
+ })
52
+ ];
53
+
54
+ export const recipe: SpaceSpec = {
55
+ name: 'Questions',
56
+ permanentUrl: 'questions',
57
+ pages: [{ id: 'home', name: 'Home', slug: '', body: QUESTIONS.flatMap(faqItem) }]
58
+ };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Link to a section: the section carries an `anchor` — its id in the page; an element's `id` is only its name in the
3
- * space, `data-id` in the DOM — and a link's `hash` lands on it, from any page, even when the section renders late.
3
+ * space, `data-plitzi-el` in the DOM — and a link's `hash` lands on it, from any page, even when the section renders late.
4
4
  */
5
5
  import { container, heading, link, text } from '@plitzi/sdk-authoring';
6
6
 
@@ -24,7 +24,7 @@ const declaration = {
24
24
 
25
25
  const seatPicker = defineElement<{ rows?: number }>(declaration);
26
26
 
27
- // `authorSpace(recipe, { plugins })` — in a project, `src/plugins/declarations.ts` lists them all.
27
+ // `authorSpace(recipe, { plugins })` — in a project, every `src/plugins/<Name>/declaration.ts`, found by folder.
28
28
  export const plugins: PluginDeclarationData[] = [declaration];
29
29
 
30
30
  export const recipe: SpaceSpec = {
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Data in a server project (`create --mode server`): a JSON file of the project's own (`src/data/plans.json`, answering
3
+ * `/data/plans.json`), read by the page server before it answers and never served — so the page arrives with the plans
4
+ * in its HTML, their anchors in place, and nothing to fetch once the browser has it.
5
+ *
6
+ * - `runtime: 'server'` on the provider is the whole switch: server data is on unless a space turns it off.
7
+ * - The answer is read exactly as in the browser — `plans.data.…` — so moving a provider between runtimes changes no
8
+ * binding.
9
+ * - The page server resolves what a page and its layouts hold, never inside a component: the provider stays on the page
10
+ * and each row reaches the card component as a prop.
11
+ * - `mockData` is what the builder shows while editing; the running page always reads the file.
12
+ */
13
+ import { apiContainer, heading, list, listItem, styles, text } from '@plitzi/sdk-authoring';
14
+
15
+ import type { SpaceSpec } from '@plitzi/sdk-authoring';
16
+
17
+ const grid = styles('plan-grid', {
18
+ display: 'grid',
19
+ 'grid-template-columns': 'repeat(auto-fill, minmax(220px, 1fr))',
20
+ gap: '16px',
21
+ margin: '0px',
22
+ padding: '0px'
23
+ });
24
+
25
+ const card = styles('plan-card', { padding: '16px', 'border-radius': '12px', 'background-color': 'var(--card)' });
26
+
27
+ export const recipe: SpaceSpec = {
28
+ name: 'Pricing',
29
+ permanentUrl: 'pricing',
30
+ variables: { color: { card: { light: '#ffffff', dark: '#16161d', default: '#ffffff' } } },
31
+ components: [
32
+ {
33
+ id: 'plan-card',
34
+ props: { item: { type: 'json', description: 'The plan: name and price' } },
35
+ // The row of the list: an `<li>` reading the plan it was handed.
36
+ root: listItem({
37
+ id: 'plan-item',
38
+ class: card,
39
+ children: [heading({ subType: 'h3', from: 'props.item.name' }), text({ from: 'props.item.price' })]
40
+ })
41
+ }
42
+ ],
43
+ pages: [
44
+ {
45
+ id: 'home',
46
+ name: 'Home',
47
+ slug: '',
48
+ body: [
49
+ apiContainer({
50
+ id: 'plans',
51
+ query: '/data/plans.json',
52
+ runtime: 'server',
53
+ mockData: { plans: [{ name: 'Starter', price: '$0' }] },
54
+ children: [
55
+ // `plans.data.plans` — the file's `plans` — in either runtime.
56
+ list({ id: 'plan-list', class: grid, items: 'plans.data.plans', row: 'plan-card' }),
57
+ text('No plans yet.', {
58
+ visible: { source: 'plans.data.plans', template: '{{ source is defined and source|length == 0 }}' }
59
+ })
60
+ ]
61
+ })
62
+ ]
63
+ }
64
+ ]
65
+ };
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable quotes -- templates quote their own strings, and read best in the other quotes */
2
2
  /**
3
- * Show data with no backend: a JSON file the project serves (`public/data/products.json`), read by a provider — one
4
- * card per product, a skeleton while it loads, a count, a computed value, and an empty state that never flashes.
3
+ * Show data with no backend, in a project with no server: a JSON file the browser fetches (`public/data/products.json`,
4
+ * public), read by a provider — one card per product, a skeleton while it loads, a count, a computed value, and an
5
+ * empty state that never flashes. A server project reads its `src/data/` on the server instead (`server-data.ts`).
5
6
  */
6
7
  import { apiContainer, container, heading, list, styles, text, listItem } from '@plitzi/sdk-authoring';
7
8
 
@@ -10,7 +10,7 @@ import type { SpaceSpec } from '@plitzi/sdk-authoring';
10
10
  const card = styles('card', {
11
11
  css: {
12
12
  desktop: { display: 'flex', 'flex-direction': 'column', gap: '8px', padding: '24px', 'border-radius': '16px' },
13
- // Tablet (48–64rem) and mobile (below 48rem) each inherit from desktop alone: `compact` is both.
13
+ // Tablet (48–64rem) and mobile (up to 48rem) each inherit from desktop alone: `compact` is both.
14
14
  compact: { padding: '16px' }
15
15
  },
16
16
  states: { hover: { 'border-color': 'var(--primary)' } }
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Data typed by a sample of it: `source()` turns the JSON the page reads into names the editor completes and checks —
4
4
  * `site.data.hero.titel` is a type error, not an empty heading. In a project the sample is the file itself:
5
- * `import home from '../../public/data/home.json' with { type: 'json' }`.
5
+ * `import home from '../data/home.json' with { type: 'json' }` — `src/data/`, or `public/data/` with no server.
6
6
  */
7
7
  import { apiContainer, heading, list, source, styles, text, twig, listItem } from '@plitzi/sdk-authoring';
8
8
 
@@ -4,7 +4,8 @@ A screen reader and a browser agent (Claude in Chrome) read a page the same way.
4
4
  tree** the browser builds from the markup, where every control is a role and a name. If a control has no name, or a
5
5
  click sits on a box that is not a control, those readers can't find it, even though the page looks fine. The linter
6
6
  warns about each case below (`control-without-name`, `image-without-alt`, `click-on-static-element`,
7
- `heading-level-skipped`, `label-ignored`). Fix them the same way you fix any other warning.
7
+ `dropdown-without-control`, `heading-level-skipped`, `label-ignored`, `control-in-decorative`, `embed-without-title`,
8
+ `controls-no-anchor`). Fix them the same way you fix any other warning.
8
9
 
9
10
  ## Controls have words
10
11
 
@@ -75,7 +76,10 @@ button({ content: 'Grid', bind: { ariaPressed: 'computed.gridOn' }, class: chip
75
76
  button({ content: 'Menu', bind: { ariaExpanded: 'state.menuOpen' }, flows: [[onClick(), toggleState({ key: 'menuOpen' })]] })
76
77
  ```
77
78
 
78
- An "active" variant is for the eyes. `ariaPressed` and `ariaExpanded` are for everyone else. Use both.
79
+ An "active" variant is for the eyes. `ariaPressed` and `ariaExpanded` are for everyone else. Use both. A button that
80
+ opens a panel also names it — `controls: 'menu-panel'`, the panel's id (authoring gives the panel the anchor
81
+ `aria-controls` needs; [recipes/accordion.ts](../recipes/accordion.ts)). Words that change while somebody is on the page
82
+ — a count, a total, a status — sit in `container({ live: 'polite', … })`, so a screen reader says them when they change.
79
83
 
80
84
  ## The page has an outline
81
85
 
@@ -6,7 +6,7 @@ Search this page for the code in brackets — `[class-and-css]` — rather than
6
6
  `npx @plitzi/cli explain class-and-css`.
7
7
 
8
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
9
+ a `code`, the line of your code that wrote the element (`src/space/pages/home.ts:417`) and the nearest named element
10
10
  (`"store-footer" › container[1]`). **Do what the message says.** Never cast past a check, silence it, or move the
11
11
  logic into a plugin to avoid it — the check exists because that declaration renders something other than what it says.
12
12
 
@@ -59,6 +59,7 @@ 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
+ | `controls-unknown` | a button's `controls` naming no element of the space | the id of the element it shows and hides: `controls: 'faq-answer'` |
62
63
  | `css-property-twice` | one property written twice in a rule set — `paddingTop` beside `'padding-top'` — so one would silently win | keep one |
63
64
  | `css-property-unknown` | a CSS property that does not exist | the property it suggests; a custom property starts with `--` |
64
65
  | `css-value` | an empty CSS value, or one with `;` or `{}` | one value per property; leave a property out instead of writing it empty |
@@ -84,7 +85,7 @@ The space is not written until these are fixed.
84
85
  | `global-callback-undeclared` | a global-callback step builder naming an action no source declares | one of the actions it lists |
85
86
  | `id-invalid` | an id a binding, a template or a test cannot name | a letter first, then letters, digits, `-` and `_`: `'hero-title'` |
86
87
  | `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
+ | `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 |
88
89
  | `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
90
  | `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
91
  | `list-items-ignored` | a list whose items nothing reads — `source: 'none'` renders its children once | `source: 'controlled'` |
@@ -109,6 +110,7 @@ The space is not written until these are fixed.
109
110
  | `prop-unknown` | a prop the component does not declare, handed in or read | the prop it suggests, or declare it: `props: { name: { type: 'text' } }` |
110
111
  | `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 |
111
112
  | `props-outside-component` | `props.x` read outside a component | read the source it would have come from |
113
+ | `quiet-unknown` | an element's `quiet` naming something that is not a suggestion's code — a problem is never quieted | the code of the suggestion it was offered: `quiet: ['repeated-shape']` |
112
114
  | `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 |
113
115
  | `row-and-children` | a list with a `row` and `children` — the row is what it renders | one of the two |
114
116
  | `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 |
@@ -117,6 +119,8 @@ The space is not written until these are fixed.
117
119
  | `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 |
118
120
  | `selector-invalid` | a `selector` that is not a CSS class name | letters, digits, `-` and `_` |
119
121
  | `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 |
122
+ | `server-data-in-browser` | a provider asking for the project's own data (`/data/…`, read by its server from `src/data/` and never served) from the browser | `runtime: 'server'` on the provider: the page arrives with the data in it |
123
+ | `server-provider-in-component` | a `runtime: 'server'` element inside a component: the page server resolves a page's and its layouts', never a component's | put the provider on the page, around the instance, and hand the component its rows as a prop |
120
124
  | `setting-misplaced` | `settings.computed` or `settings.channels` written inside `settings` | `computed` and `channels` at the top of the space |
121
125
  | `slot-children` | children handed to an instance outside its slots | `children: { slotName: [ … ] }`; a component with no slots takes no children |
122
126
  | `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 |
@@ -148,7 +152,7 @@ The space is not written until these are fixed.
148
152
  | `utility-module` | a utility sent to a module — a utility takes none | drop the `on` |
149
153
  | `visibility-as-attribute` | `visibility` bound as an attribute, which no element reads | `visible: { source, template }` |
150
154
  | `visitor-roles` | `settings.visitorRoles` written in a shape the runtime does not read | the shape the message gives |
151
- | `while-running` | `whileRunning` on a step that is not the trigger, or a mode other than `skip`, `parallel`, `queue` | `[whileRunning('queue', onClick()), …]` |
155
+ | `while-running` | `whileRunning` on a step that is not the trigger, or a mode other than `skip`, `parallel`, `queue`, `latest` | `[whileRunning('queue', onClick()), …]` |
152
156
 
153
157
  ## Warned
154
158
 
@@ -156,11 +160,13 @@ The space renders, and renders something you probably did not mean. Fix every on
156
160
 
157
161
  | Code | What was wrong | Write instead |
158
162
  | --- | --- | --- |
163
+ | `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` |
159
164
  | `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` |
160
165
  | `colour-without-dark` | a colour token with no dark value | `{ light, dark, default }` |
161
166
  | `condition-starts-visible` | a computed visibility that shows until its data answers | `visible: { source, template }` |
162
167
  | `control-in-decorative` | a control inside a `decorative` container — Tab lands on something nothing announces | move it out of the illustration |
163
168
  | `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 |
169
+ | `controls-no-anchor` | a button's `controls` naming no anchor — `aria-controls` points at nothing | an `anchor` on the element it shows and hides, and `controls` naming it |
164
170
  | `default-content-beside-children` | a button printing its default "Button" beside its children | `content: ''` |
165
171
  | `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) |
166
172
  | `embed-without-title` | an `embed` with no `title` — a frame a screen reader cannot describe | say what it shows in `title` |
@@ -178,11 +184,12 @@ The space renders, and renders something you probably did not mean. Fix every on
178
184
  | `overlay-never-opened` | a modal or a dialog that starts hidden and that no step opens | a flow with `openModal('id')` / `openDialog('id')` |
179
185
  | `overlay-starts-open` | a modal or a dialog open when the page loads | `visible: false`, opened by `openModal` |
180
186
  | `painted-state-without-keep-state` | `paintedState` without `keepState` — nothing is kept for the server to draw with | `settings.keepState: true`, or remove `paintedState` |
187
+ | `path-not-in-data` | a binding onto a provider whose answer the author could read (`data`), through a path that answer does not have | the path the message lists the keys for — `p.data.plans`, not `p.data.landing.plans` |
181
188
  | `plugin-attribute-reserved` | a plugin attribute named as one of the element’s own fields — a factory never hands it to the plugin | rename it in the plugin (`variant` → `kind`) |
182
189
  | `provider-without-source` | an `apiContainer` that asks nothing | a `query` (or `action`, `connector`, `resource`) |
183
190
  | `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` |
184
- | `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 |
185
- | `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 |
191
+ | `server-data-without-rsc` | a `runtime: 'server'` provider — `connector`, `action` or `query` — in a space that turns server data off (`rsc: { enabled: false }`) | drop `rsc: { enabled: false }`: server data is on unless a space turns it off |
192
+ | `span-holds-block` | a `container` with `subType: 'span'` or `'p'` holding a heading, a paragraph, a list, a form or prose | a `div` (leave `subType` out), or words and inline elements: a `text`, a `link` |
186
193
  | `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'` |
187
194
  | `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 |
188
195
  | `STYLE_WITHOUT_TAG` | style on a provider that renders no element of its own | `subType: 'div'` on the provider, or style its parent |
@@ -206,8 +213,12 @@ Nothing is wrong: the page renders as written. Each is a shorter way to the same
206
213
  | `custom-css-notifications` | the toasts dressed with `.Toastify__toast` rules in `customCss` | `notifications: { font, fontSize, border, shadow, padding, background, text, radius }` |
207
214
  | `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 |
208
215
  | `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
+ | `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 |
209
217
  | `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 |
210
218
  | `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 |
219
+ | `unused-class` | a class declared in `classes` that no element, binding, flow or other class names | remove it from `classes` — or wear it where it was meant to go |
220
+ | `unused-component` | a component no page, layout or other component places | remove it from `components`, or place it where it was meant to go |
221
+ | `unused-token` | a token of `variables` no `var(--…)` reads — not a class, an element, `customCss` nor another token | remove it, or write it where its colour is written out |
211
222
 
212
223
  ## UPPER_CASE codes
213
224