@plitzi/sdk-server 0.38.4 → 0.38.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +350 -0
- package/README.md +126 -17
- package/dist/actions.d.ts +8 -5
- package/dist/actions.js +3 -1
- package/dist/adapters/jsonAdapters.d.ts +1 -1
- package/dist/adapters/jsonAdapters.js +16 -3
- package/dist/core/configSeam.js +20 -4
- package/dist/core/createServer.js +3 -2
- package/dist/core/handlers/socialRouteHandlers.js +6 -1
- package/dist/core/http/stages/devReload.d.ts +11 -0
- package/dist/core/http/stages/devReload.js +17 -0
- package/dist/core/server/baseServer.d.ts +2 -0
- package/dist/core/server/baseServer.js +3 -0
- package/dist/core/server/pageServer.js +18 -0
- package/dist/core/services/registry.js +2 -0
- package/dist/core/services/rsc.js +1 -1
- package/dist/core/services/spaceDocument.d.ts +2 -0
- package/dist/core/services/spaceDocument.js +11 -0
- package/dist/functions-api.d.ts +6 -1
- package/dist/functionsRunner.d.ts +5 -0
- package/dist/functionsRunner.js +2 -1
- package/dist/helpers/hydrationPayload.d.ts +25 -4
- package/dist/helpers/hydrationPayload.js +36 -30
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/modules/actions/index.d.ts +11 -2
- package/dist/modules/actions/index.js +29 -7
- package/dist/modules/actions/runtime/fileKv.d.ts +28 -0
- package/dist/modules/actions/runtime/fileKv.js +95 -0
- package/dist/modules/actions/runtime/fileKv.test.d.ts +1 -0
- package/dist/modules/actions/runtime/memoryKv.d.ts +23 -0
- package/dist/modules/actions/runtime/memoryKv.js +39 -24
- package/dist/modules/actions/runtime/namespaceKv.d.ts +2 -0
- package/dist/modules/actions/runtime/namespaceKv.js +5 -3
- package/dist/modules/actions/runtime/renderResolver.d.ts +3 -2
- package/dist/modules/actions/runtime/renderResolver.js +13 -9
- package/dist/modules/actions/runtime/renderShare.d.ts +7 -24
- package/dist/modules/actions/runtime/renderShare.js +47 -13
- package/dist/modules/actions/runtime/runAction.js +15 -1
- package/dist/modules/actions/runtime/scope.js +16 -1
- package/dist/modules/actions/runtime/signing.js +4 -1
- package/dist/modules/actions/runtime/taskContext.d.ts +7 -1
- package/dist/modules/actions/runtime/taskContext.js +6 -2
- package/dist/modules/actions/tasks/registry.d.ts +3 -1
- package/dist/modules/actions/tasks/registry.js +15 -1
- package/dist/modules/actions/types.d.ts +15 -2
- package/dist/modules/functions/config.d.ts +6 -0
- package/dist/modules/functions/context.d.ts +4 -2
- package/dist/modules/functions/context.js +43 -21
- package/dist/modules/functions/load.d.ts +8 -1
- package/dist/modules/functions/load.js +8 -4
- package/dist/modules/functions/manifest.d.ts +4 -1
- package/dist/modules/functions/manifest.js +14 -2
- package/dist/modules/functions/native.d.ts +2 -1
- package/dist/modules/functions/native.js +2 -2
- package/dist/modules/functions/plugins.test.d.ts +1 -0
- package/dist/modules/functions/routes.d.ts +8 -3
- package/dist/modules/functions/routes.js +41 -21
- package/dist/modules/functions/scope.d.ts +30 -0
- package/dist/modules/functions/scope.js +28 -0
- package/dist/modules/functions/space.d.ts +10 -5
- package/dist/modules/functions/space.js +37 -15
- package/dist/modules/rsc/connectorRscData.d.ts +3 -1
- package/dist/modules/rsc/connectorRscData.js +8 -4
- package/dist/modules/rsc/handler.d.ts +3 -16
- package/dist/modules/rsc/handler.js +10 -4
- package/dist/modules/rsc/publicFileResolver.d.ts +15 -12
- package/dist/modules/rsc/publicFileResolver.js +56 -24
- package/dist/modules/rsc/resolveRscData.d.ts +3 -1
- package/dist/modules/rsc/resolveRscData.js +8 -5
- package/dist/modules/runtime/bundle.d.ts +5 -0
- package/dist/modules/runtime/bundle.js +20 -5
- package/dist/modules/sqlite/kv.d.ts +34 -0
- package/dist/modules/sqlite/kv.js +99 -0
- package/dist/modules/sqlite/kv.test.d.ts +1 -0
- package/dist/modules/ssr/Component.d.ts +2 -1
- package/dist/modules/ssr/Component.js +2 -1
- package/dist/modules/ssr/prepareRender.js +48 -6
- package/dist/modules/ssr/spaceDocument.d.ts +25 -0
- package/dist/modules/ssr/spaceDocument.js +72 -0
- package/dist/modules/ssr/spaceDocument.test.d.ts +1 -0
- package/dist/modules/ssr/testing/offlineData.d.ts +5 -0
- package/dist/modules/ssr/views/template.ejs +122 -20
- package/dist/plugins/compile.js +6 -21
- package/dist/plugins/compile.test.d.ts +1 -0
- package/dist/plugins/copy.d.ts +5 -0
- package/dist/plugins/copy.js +16 -1
- package/dist/plugins/copy.test.d.ts +1 -0
- package/dist/plugins/manager.d.ts +13 -0
- package/dist/plugins/manager.js +40 -11
- package/dist/plugins/watch.d.ts +12 -0
- package/dist/plugins/watch.js +79 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +2 -2
- package/dist/sqlite.d.ts +17 -0
- package/dist/sqlite.js +2 -0
- package/package.json +8 -3
- package/skills/plitzi-authoring/CHEATSHEET.md +4 -2
- package/skills/plitzi-authoring/SKILL.md +20 -19
- package/skills/plitzi-authoring/recipes/plugin.ts +1 -1
- package/skills/plitzi-authoring/recipes/server-data.ts +3 -3
- package/skills/plitzi-authoring/recipes/show-data.ts +3 -2
- package/skills/plitzi-authoring/recipes/typed-data.ts +1 -1
- package/skills/plitzi-authoring/reference/authoring-errors.md +8 -2
- package/skills/plitzi-authoring/reference/colours-and-motion.md +3 -1
- package/skills/plitzi-authoring/reference/data-and-visibility.md +24 -22
- package/skills/plitzi-authoring/reference/drawing.md +24 -0
- package/skills/plitzi-authoring/reference/efficiency.md +19 -2
- package/skills/plitzi-authoring/reference/flows.md +3 -0
- package/skills/plitzi-authoring/reference/layouts.md +8 -23
- package/skills/plitzi-authoring/reference/performance.md +2 -2
- package/skills/plitzi-authoring/reference/plugins.md +51 -38
- package/skills/plitzi-authoring/reference/review-checklist.md +2 -0
- package/skills/plitzi-authoring/reference/structure.md +32 -1
- package/skills/plitzi-authoring/reference/templates.md +5 -2
- package/skills/plitzi-authoring/reference/testing.md +9 -2
- package/skills/plitzi-authoring/reference/typed-sources.md +3 -1
- 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 };
|
package/dist/sqlite.d.ts
ADDED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/sdk-server",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.6",
|
|
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.
|
|
83
|
-
"@plitzi/sdk-shared": "0.38.
|
|
87
|
+
"@plitzi/plitzi-sdk": "0.38.6",
|
|
88
|
+
"@plitzi/sdk-shared": "0.38.6",
|
|
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
|
-
|
|
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
|
|
@@ -46,6 +47,7 @@ carousel({ id: 'hero', items: 'p.data.slides', autoplay: 5000, row: 'slide-card'
|
|
|
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
|
|
48
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
|
|
49
51
|
// loadingSlot: 'p-skeleton' — a child shown until it answers
|
|
50
52
|
form({ id, managedByInteractions: true, flows, children })
|
|
51
53
|
formControl({ name: 'email', label: 'Email', subType: 'email' }) // select: options: [{ label, value }]
|
|
@@ -109,7 +111,7 @@ A flow is `[trigger, …steps]`, in an element's `flows`.
|
|
|
109
111
|
[onClick(), navigate({ urlType: 'page', url: 'about' })]
|
|
110
112
|
[onClick(), runServerAction({ actionId: 'checkout', input: { id: '{{ list_rows.item.id }}' } })]
|
|
111
113
|
[onClick(), when({ field: 'state.step', operator: '=', value: '2' }, setState({ … }))]
|
|
112
|
-
[whileRunning('queue', onClick()), delay(1000), …]
|
|
114
|
+
[whileRunning('queue', onClick()), delay(1000), …] // 'latest': a new firing stops the running one
|
|
113
115
|
[named('picked', declaredTrigger(declaration, 'onPick')), …]; declaredCallback(declaration, 'reset', { on: 'seats' })
|
|
114
116
|
```
|
|
115
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
|
|
44
|
-
|
|
45
|
-
do exactly that. Never work around a check, cast past it, or move the logic into a plugin to escape
|
|
46
|
-
|
|
47
|
-
|
|
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]
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
54
|
-
|
|
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
|
|
|
@@ -66,10 +66,11 @@ second into the first. If the MCP asks for a sign-in nobody can give, a project
|
|
|
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
|
|
70
|
-
|
|
71
|
-
its own `content`, not through a `text` inside it.
|
|
72
|
-
[components](reference/components.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
|
|
@@ -94,9 +95,9 @@ second into the first. If the MCP asks for a sign-in nobody can give, a project
|
|
|
94
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
|
|
98
|
-
|
|
99
|
-
|
|
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).
|
|
100
101
|
10. **Never hand-write** `flat`, derived ids, `styleSelectors`, `beforeNode`/`afterNode`/`flowId`, or a
|
|
101
102
|
`styleVariant` binding's key — use the factories, `variantFrom` and `activeOn`.
|
|
102
103
|
11. **A switch is named for how it leaves its default.** `toggleState` turns a key nobody has set yet ON, so a key
|
|
@@ -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
|
|
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 = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Data in a server project (`create --mode server`): a JSON file the project
|
|
3
|
-
* by the page server before it answers — so the page arrives with the plans
|
|
4
|
-
* nothing to fetch once the browser has it.
|
|
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
5
|
*
|
|
6
6
|
* - `runtime: 'server'` on the provider is the whole switch: server data is on unless a space turns it off.
|
|
7
7
|
* - The answer is read exactly as in the browser — `plans.data.…` — so moving a provider between runtimes changes no
|
|
@@ -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
|
|
4
|
-
* card per product, a skeleton while it loads, a count, a computed value, and an
|
|
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
|
|
|
@@ -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 '
|
|
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
|
|
|
@@ -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/
|
|
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
|
|
|
@@ -119,6 +119,7 @@ The space is not written until these are fixed.
|
|
|
119
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 |
|
|
120
120
|
| `selector-invalid` | a `selector` that is not a CSS class name | letters, digits, `-` and `_` |
|
|
121
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 |
|
|
122
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 |
|
|
123
124
|
| `setting-misplaced` | `settings.computed` or `settings.channels` written inside `settings` | `computed` and `channels` at the top of the space |
|
|
124
125
|
| `slot-children` | children handed to an instance outside its slots | `children: { slotName: [ … ] }`; a component with no slots takes no children |
|
|
@@ -151,7 +152,7 @@ The space is not written until these are fixed.
|
|
|
151
152
|
| `utility-module` | a utility sent to a module — a utility takes none | drop the `on` |
|
|
152
153
|
| `visibility-as-attribute` | `visibility` bound as an attribute, which no element reads | `visible: { source, template }` |
|
|
153
154
|
| `visitor-roles` | `settings.visitorRoles` written in a shape the runtime does not read | the shape the message gives |
|
|
154
|
-
| `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()), …]` |
|
|
155
156
|
|
|
156
157
|
## Warned
|
|
157
158
|
|
|
@@ -159,6 +160,7 @@ The space renders, and renders something you probably did not mean. Fix every on
|
|
|
159
160
|
|
|
160
161
|
| Code | What was wrong | Write instead |
|
|
161
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` |
|
|
162
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` |
|
|
163
165
|
| `colour-without-dark` | a colour token with no dark value | `{ light, dark, default }` |
|
|
164
166
|
| `condition-starts-visible` | a computed visibility that shows until its data answers | `visible: { source, template }` |
|
|
@@ -211,8 +213,12 @@ Nothing is wrong: the page renders as written. Each is a shorter way to the same
|
|
|
211
213
|
| `custom-css-notifications` | the toasts dressed with `.Toastify__toast` rules in `customCss` | `notifications: { font, fontSize, border, shadow, padding, background, text, radius }` |
|
|
212
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 |
|
|
213
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 |
|
|
214
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 |
|
|
215
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 |
|
|
216
222
|
|
|
217
223
|
## UPPER_CASE codes
|
|
218
224
|
|
|
@@ -62,7 +62,9 @@ variables: {
|
|
|
62
62
|
- **`tokens(variables)`** names them for a rule: declare the variables once (`satisfies SpaceSpec['variables']`), hand
|
|
63
63
|
the same object to the space, and `const t = tokens(variables)` gives `t.primary === 'var(--primary)'` — a name the
|
|
64
64
|
space does not declare is a type error. A `var(--x)` written by hand that nothing declares is warned
|
|
65
|
-
`unknown-variable`; one that may be missing on purpose takes a fallback, `var(--x, 8px)`.
|
|
65
|
+
`unknown-variable`; one that may be missing on purpose takes a fallback, `var(--x, 8px)`. The other way round, a token
|
|
66
|
+
nothing reads is suggested `unused-token`, and a class painted from the palette that types one of its colours out
|
|
67
|
+
`literal-colour` — see [efficiency](efficiency.md#what-it-declares-and-never-uses).
|
|
66
68
|
- Choose each value against its own background: a colour picked on white is not the same colour on near-black.
|
|
67
69
|
- Fixed colours are only for surfaces that are fixed in both themes (a brand panel that is always dark) — and then
|
|
68
70
|
the text on them is fixed too. Theme-following text on a fixed background is the bug.
|
|
@@ -13,8 +13,8 @@ text({ bind: [{ to: 'content', source: 'posts.title', when: { … } }] })
|
|
|
13
13
|
|
|
14
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
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') }}" }` (
|
|
17
|
-
`currency` and `percent`
|
|
16
|
+
the value), or a format the space names once in `formats: { price: "{{ source|currency('USD') }}" }` (the
|
|
17
|
+
`currency` and `percent` filters). `bind` is for every other attribute;
|
|
18
18
|
`bindTemplate(to, source, template, { category?, returns? })` is the computed attribute — a template over the value
|
|
19
19
|
at `source`. `returns: 'value'` hands over what a single `{{ expression }}` evaluates to instead of its text: a list's
|
|
20
20
|
`items`, a number, a flag (see [lists](lists.md)).
|
|
@@ -27,7 +27,8 @@ completed from what the element publishes (`apiContainer_posts`, `list_postList`
|
|
|
27
27
|
The binding's `source` is completed for you; its template is read as written, so the short name there is refused.
|
|
28
28
|
|
|
29
29
|
An element re-renders when anything it reads changes: the first part of each binding's `source` and every source its
|
|
30
|
-
templates name. The globals are `variables`, `navigation` (`routeParams`, `queryParams`, `origin`, `currentPageId
|
|
30
|
+
templates name. The globals are `variables`, `navigation` (`routeParams`, `queryParams`, `origin`, `currentPageId`,
|
|
31
|
+
`pending` / `pendingLocation` while a link waits for its page's server data),
|
|
31
32
|
`auth` (who is signed in, `status` while that is being found out), `state` (what flows wrote), `theme`
|
|
32
33
|
(`mode`, `resolved`), `host` (the `hostData` an application embedding the space hands the SDK) and `computed` (below).
|
|
33
34
|
|
|
@@ -60,14 +61,19 @@ apiContainer({ id: 'board', runtime: 'server', action: 'queue-board' })
|
|
|
60
61
|
```
|
|
61
62
|
|
|
62
63
|
- A provider's source is readable by its **descendants** only. Wrap what reads it; a sibling cannot see it.
|
|
63
|
-
- It publishes
|
|
64
|
-
A `query`'s body is `data` in **either runtime** (`p.data.plans`), so moving a provider to the server
|
|
65
|
-
binding. A connector publishes `records` (or `record`)
|
|
64
|
+
- It publishes its answer plus `isLoading`, `isEmpty`, `hasError`, `errorMessage` — bind a state to those, not to
|
|
65
|
+
guesses. A `query`'s body is `data` in **either runtime** (`p.data.plans`), so moving a provider to the server
|
|
66
|
+
changes no binding. A connector publishes `records` (or `record`). **An action publishes its output at the root**:
|
|
67
|
+
`apiContainer_board.columns`, never `.data.columns` (warned `action-output-path`).
|
|
66
68
|
`isEmpty` reads what arrived: a `query`'s body when it is missing, `null`, `''`, `[]` or `{}`; a connector list by
|
|
67
69
|
its `records`; a `singleRecord` provider by its `record`. It is also true before the first answer, so pair it with
|
|
68
70
|
`not isLoading` for an empty state.
|
|
69
71
|
- A query that depends on state is a binding on `query`; it answers `''` (and fetches nothing) until the state exists:
|
|
70
|
-
`"{{ source ? apiUrl ~ '/workspaces/' ~ source ~ '/stats' : '' }}"` with `source: 'state.workspace.id'`.
|
|
72
|
+
`"{{ source ? apiUrl ~ '/workspaces/' ~ source ~ '/stats' : '' }}"` with `source: 'state.workspace.id'`. A server
|
|
73
|
+
provider's bound `input` (`bindTemplate('input', 'state.c', '{{ { country: source } }}', { returns: 'value' })`)
|
|
74
|
+
asks again on every change; `reloadApi('p', { … })` asks once.
|
|
75
|
+
- `isLoading` is true while it is asked again, in either runtime. A newer ask drops the older one; `cancelApi('p')` is a
|
|
76
|
+
STOP: what is shown stays.
|
|
71
77
|
- `runtime: 'server'` is resolved by the page server — server data is on unless a space says `rsc: { enabled: false }`.
|
|
72
78
|
Only a page and its layouts are resolved — never inside a component: keep the provider on the page and hand the
|
|
73
79
|
component its rows as a prop ([recipes/server-data.ts](../recipes/server-data.ts)).
|
|
@@ -79,21 +85,17 @@ see [typed-sources.md](typed-sources.md).
|
|
|
79
85
|
|
|
80
86
|
### Data in a project with no backend
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
and so is everything the space's documents hold (pages, variables, attributes,
|
|
90
|
-
|
|
91
|
-
credential
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
**In a server project (`create --mode server`), put the provider on the server** — `runtime: 'server'` — and the page
|
|
95
|
-
server reads the file from `public/` itself: the page arrives with those sections (and their anchors) in it. A `query`
|
|
96
|
-
with `{{tokens}}` is still read in the browser, against the visitor's route and state.
|
|
88
|
+
Content lives in JSON files read like any API — `apiContainer({ id: 'catalog', query: '/data/games.json', children:
|
|
89
|
+
[ … ] })` — so a real endpoint later changes one `query`. `mockData` is only what the BUILDER shows. Say demo is demo.
|
|
90
|
+
|
|
91
|
+
- **Server project:** `src/data/games.json` answers `/data/games.json`, read by the server, never served. The provider
|
|
92
|
+
is `runtime: 'server'` (the page arrives with it); a browser one is refused (`server-data-in-browser`).
|
|
93
|
+
- **No server:** `public/data/`, fetched — public.
|
|
94
|
+
|
|
95
|
+
**`public/` is on the internet**, and so is everything the space's documents hold (pages, variables, attributes,
|
|
96
|
+
`mockData`) and what a provider reads into a page: never a secret, a key or data only some visitors may read. A secret
|
|
97
|
+
is a credential an action or a connector names; data for some visitors, or fields a page must not carry, comes from a
|
|
98
|
+
server action answering only what is shown, its `access` checking who asks.
|
|
97
99
|
|
|
98
100
|
### Live, cached, refreshed
|
|
99
101
|
|
|
@@ -20,3 +20,27 @@ compile — GLSL ES 1.0 wants `196.0`, not `196` — throws a `ShaderError` with
|
|
|
20
20
|
`[plugin <type> "<id>"] fragment shader failed: …` (`plitzi check` lists it under the console) and `error` says it;
|
|
21
21
|
show the still image meanwhile, as above. `useAnimationFrame(onFrame, { target })` is the same loop for motion that
|
|
22
22
|
is not a canvas.
|
|
23
|
+
|
|
24
|
+
## Drawing a lot
|
|
25
|
+
|
|
26
|
+
A canvas with thousands of shapes on it — a whiteboard, a diagram, a map of points — is judged on a slower machine than
|
|
27
|
+
yours: sixty frames a second here says nothing about a four-year-old laptop. What keeps one fast is doing work in
|
|
28
|
+
proportion to what CHANGED, not to what is there:
|
|
29
|
+
|
|
30
|
+
- **Two layers.** Paint what stands still on one canvas and leave it; clear and draw the one over it every frame, with
|
|
31
|
+
only what moves: the cursor, the selection, what is being dragged.
|
|
32
|
+
- **Repaint the part that changed.** Something added, edited or removed repaints the area it was and is in, clipped,
|
|
33
|
+
with whatever reaches into it — not the whole board. A pan moves the picture already painted by whole device pixels
|
|
34
|
+
and paints the edges it uncovers.
|
|
35
|
+
- **Draw a dragged group once.** Everything picked up moves by the same amount at every step: draw it to a canvas of its
|
|
36
|
+
own when the drag begins and copy that into place after.
|
|
37
|
+
- **Cache per object, not per frame.** Treat elements as immutable and key what is derived from one — its bounds, its
|
|
38
|
+
shape, a resolved connector — on the object itself (a `WeakMap`), so nothing is worked out twice for the same thing.
|
|
39
|
+
- **Hand the space outcomes, not motion.** A stroke finished, a selection changed: a trigger each. Firing one at every
|
|
40
|
+
pointer move runs a flow and renders the page at every pointer move.
|
|
41
|
+
- **Measure with a bench, throttled.** A script that drives the real thing in a browser with the CPU slowed
|
|
42
|
+
(`Emulation.setCPUThrottlingRate`) and reports script time per frame, not only frames per second — and a test that
|
|
43
|
+
counts WORK (full repaints, strokes drawn during a drag), which holds on any machine where a timing does not.
|
|
44
|
+
|
|
45
|
+
Pizarra, the collaborative whiteboard on the platform (`pizarra.plitzi.app`), does all of it on a board of thousands
|
|
46
|
+
of elements.
|
|
@@ -19,8 +19,9 @@ const { warnings, suggestions } = authorSpace(space);
|
|
|
19
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
20
|
reason is usually that the copies are about to diverge. `npx @plitzi/cli explain repeated-on-pages` explains any code.
|
|
21
21
|
|
|
22
|
-
Left on purpose, say so where it is written — `quiet` on an element it is about
|
|
23
|
-
builder or over MCP either (it is kept in the
|
|
22
|
+
Left on purpose, say so where it is written — `quiet` on an element it is about (a component's instance too:
|
|
23
|
+
`component(id, { quiet })`), and it is not offered again, in the builder or over MCP either (it is kept in the
|
|
24
|
+
document):
|
|
24
25
|
|
|
25
26
|
```ts
|
|
26
27
|
const includes = (words: string) => container({ class: row, quiet: ['repeated-shape'], children: [tick, text(words)] });
|
|
@@ -42,6 +43,22 @@ Only a suggestion's code: a problem is never quieted (`quiet-unknown`).
|
|
|
42
43
|
| `.Toastify__toast { font-family: …; border: … }` in `customCss` | `notifications: { font, fontSize, border, shadow, padding, … }` | `custom-css-notifications` |
|
|
43
44
|
| Keyframes animating `width`, `top`, `filter: blur()`, `box-shadow`, a colour in a loop | `opacity` and `transform`; decoration held until `[data-hydrated]` — [motion](colours-and-motion.md) | `heavy-animation` |
|
|
44
45
|
|
|
46
|
+
## What it declares and never uses
|
|
47
|
+
|
|
48
|
+
A big space gathers what nothing reads any more: the class kept after the last element wearing it went, the token
|
|
49
|
+
nobody points at, the component no page places — and the colour typed out where its token was meant. Each is one more
|
|
50
|
+
thing the next reader has to check is safe to change. `authorSpace` names them too:
|
|
51
|
+
|
|
52
|
+
| What it found | What to do | Suggestion |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| A class in `classes` that no element, binding, flow or other class's `ancestors` names | Remove it — or wear it where it was meant to go | `unused-class` |
|
|
55
|
+
| A token in `variables` that no `var(--…)` reads — not a class, an element, `customCss` nor another token | Remove it, or write it where its colour is typed out | `unused-token` |
|
|
56
|
+
| A component that no page, layout or other component places | Remove it, or place it where it was meant to go | `unused-component` |
|
|
57
|
+
| A colour typed out in a class painted from the palette, equal to a token's light value | `var(--token)` when it should follow the scheme; a token of one value of its own when it must stay the same in both (dark words on a light chip) | `literal-colour` |
|
|
58
|
+
|
|
59
|
+
They are read generously, so they are never wrong: a name counts as used wherever it appears as a word, and a class
|
|
60
|
+
written all in literals — a sticker's paper and ink, a swatch — is a palette of its own and not named.
|
|
61
|
+
|
|
45
62
|
And a few the suggestions do not count, because they cost styles rather than elements:
|
|
46
63
|
|
|
47
64
|
- **A look used twice is a class**, written once with `styles()`; every element of a TYPE dressed the same way is the
|
|
@@ -31,6 +31,9 @@ same flags, decided on the server. Use the step builders — they fill in where
|
|
|
31
31
|
key for hiding it (`sidebarCollapsed`).
|
|
32
32
|
- **A trigger fired again while its flow runs is IGNORED** (`skip`, the default — no double submit). For a stream of
|
|
33
33
|
events that must each run, wrap the trigger: `whileRunning('queue', on('onArrival'))` (in order) or `'parallel'`.
|
|
34
|
+
Where only the newest firing matters — a search as you type — `whileRunning('latest', …)`: the run in progress
|
|
35
|
+
stops (no further step; its server action or request is cancelled) and the new one runs. With a `delay(450)` first
|
|
36
|
+
it is a debounce: each firing stops the one still waiting, and only the last gets past the wait.
|
|
34
37
|
- **Each step reads the page as it is when it runs.** A `when` or a `{{ state.x }}` after a `setState` sees the new
|
|
35
38
|
value, and one after a `delay` or a server action sees whatever changed meanwhile. To act on the value from BEFORE
|
|
36
39
|
a write, put the step that reads it first.
|
|
@@ -113,31 +113,16 @@ this replaces.
|
|
|
113
113
|
|
|
114
114
|
## Pages from a factory
|
|
115
115
|
|
|
116
|
-
When pages share a shape — a heading, a lede, content, a footer — the shape is
|
|
116
|
+
When pages share a shape — a heading, a lede, content, a footer — the shape is written once and each page is its data,
|
|
117
|
+
with `pageFamily`: it gives every page its id, slug, titles, folder and this layout, and prefixes the ids each body
|
|
118
|
+
gives (see [structure](structure.md#pages-of-one-shape-are-a-family)):
|
|
117
119
|
|
|
118
120
|
```ts
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
name: entry.title,
|
|
125
|
-
slug: entry.slug,
|
|
126
|
-
seoTitle: `${entry.title} — Docs`,
|
|
127
|
-
seoDescription: entry.lede,
|
|
128
|
-
folder: 'docs',
|
|
129
|
-
layout: { id: 'docs-layout', slot: 'docs-slot' },
|
|
130
|
-
body: [
|
|
131
|
-
container({
|
|
132
|
-
id: `${id}-main`, // prefixed: this function runs once per page
|
|
133
|
-
class: main,
|
|
134
|
-
children: [heading(entry.title, { id: `${id}-title`, class: title }), markdown(markdownSource, { id: `${id}-md` }), ...footer(id)]
|
|
135
|
-
})
|
|
136
|
-
]
|
|
137
|
-
};
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
export const quickstart = docsPage('docs-quickstart', `## One command …`);
|
|
121
|
+
pages: pageFamily(
|
|
122
|
+
{ folder: 'docs', layout: { id: 'docs-layout', slot: 'docs-slot' }, seoTitle: entry => `${entry.title} — Docs`,
|
|
123
|
+
body: entry => [container({ id: 'main', class: main, children: [heading({ id: 'title', content: entry.title, class: title }), markdown(entry.source, { id: 'md' })] })] },
|
|
124
|
+
DOCS // [{ id: 'docs-quickstart', title, slug, description, source }] — the same list the sidebar reads
|
|
125
|
+
)
|
|
141
126
|
```
|
|
142
127
|
|
|
143
128
|
Every page is now its content and nothing else, and every page is guaranteed the same structure.
|
|
@@ -28,8 +28,8 @@ An element renders again when something it READS changes, and only then:
|
|
|
28
28
|
moment-to-moment state inside a plugin and hand the space the outcome (`onCommit`, `onSelectionChange`), not every
|
|
29
29
|
step of it.
|
|
30
30
|
- **`whileRunning`** decides what a trigger fired again does while its flow runs: `skip` (the default) drops it,
|
|
31
|
-
`queue` runs it after, `parallel` alongside. A burst of events queued behind a slow
|
|
32
|
-
later
|
|
31
|
+
`queue` runs it after, `parallel` alongside, `latest` stops the one running. A burst of events queued behind a slow
|
|
32
|
+
request is a burst of renders later — for a search, `latest`.
|
|
33
33
|
|
|
34
34
|
## What mounts
|
|
35
35
|
|