@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.
- package/CHANGELOG.md +530 -0
- package/README.md +142 -24
- 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/http/stages/images.js +1 -1
- 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/images/imageProxy.js +19 -1
- package/dist/modules/rsc/connectorRscData.d.ts +3 -1
- package/dist/modules/rsc/connectorRscData.js +9 -5
- 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 -9
- package/dist/modules/rsc/publicFileResolver.js +61 -23
- 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/prepareRender.js +45 -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 +41 -12
- 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 +9 -5
- package/skills/plitzi-authoring/SKILL.md +24 -20
- package/skills/plitzi-authoring/recipes/accordion.ts +58 -0
- package/skills/plitzi-authoring/recipes/link-to-a-section.ts +1 -1
- package/skills/plitzi-authoring/recipes/plugin.ts +1 -1
- package/skills/plitzi-authoring/recipes/server-data.ts +65 -0
- package/skills/plitzi-authoring/recipes/show-data.ts +3 -2
- package/skills/plitzi-authoring/recipes/style-a-page.ts +1 -1
- package/skills/plitzi-authoring/recipes/typed-data.ts +1 -1
- package/skills/plitzi-authoring/reference/accessibility.md +6 -2
- package/skills/plitzi-authoring/reference/authoring-errors.md +16 -5
- package/skills/plitzi-authoring/reference/colours-and-motion.md +8 -2
- package/skills/plitzi-authoring/reference/data-and-visibility.md +38 -26
- package/skills/plitzi-authoring/reference/drawing.md +24 -0
- package/skills/plitzi-authoring/reference/efficiency.md +27 -1
- package/skills/plitzi-authoring/reference/elements-and-styles.md +5 -9
- package/skills/plitzi-authoring/reference/flows.md +3 -0
- package/skills/plitzi-authoring/reference/layouts.md +8 -23
- package/skills/plitzi-authoring/reference/lists.md +1 -1
- 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 +16 -2
- package/skills/plitzi-authoring/reference/snippets-and-export.md +2 -2
- package/skills/plitzi-authoring/reference/structure.md +32 -1
- package/skills/plitzi-authoring/reference/templates.md +6 -3
- package/skills/plitzi-authoring/reference/testing.md +10 -3
- package/skills/plitzi-authoring/reference/typed-sources.md +3 -1
- package/skills/plitzi-authoring/reference/validation.md +1 -1
|
@@ -20,7 +20,9 @@ Only `opacity` and `transform` (`translate`, `scale`, `rotate`) animate off the
|
|
|
20
20
|
the page hydrates and on a slow phone. Everything else repaints, or lays the page out again, on every frame.
|
|
21
21
|
|
|
22
22
|
- **A preset first:** `motion: { enter: 'fade-up', on: 'view' }` on any element, `stagger: 60` for children one by one,
|
|
23
|
-
`loop: 'float'` — played by the SDK, cheap and stilled for reduced motion by construction (motion
|
|
23
|
+
`loop: 'float'` — played by the SDK, cheap and stilled for reduced motion by construction (`npx plitzi explain motion` lists them; `MOTION_ENTERS` and `MOTION_LOOPS` in code).
|
|
24
|
+
`on: 'view'` plays once, the first time the element comes into view, and it stays; `on: 'scroll'` follows the scroll
|
|
25
|
+
both ways — it goes back out as the reader scrolls up past it — so keep it for an effect that is about the scroll.
|
|
24
26
|
|
|
25
27
|
- **Slide, grow, fade** with `transform` and `opacity` — never `left`, `top`, `width`, `height` or `margin`. Something
|
|
26
28
|
that travels across a box is `transform: translate(%)` on an element as large as the box.
|
|
@@ -31,6 +33,8 @@ the page hydrates and on a slow phone. Everything else repaints, or lays the pag
|
|
|
31
33
|
- **Main-thread decoration waits for hydration:** a sweep on a custom property, a `background-position` seam — start
|
|
32
34
|
it paused, run it once the SDK's root says `data-hydrated`:
|
|
33
35
|
`'.glow { animation: glow 8s linear infinite paused; } [data-hydrated] .glow { animation-play-state: running; }'`.
|
|
36
|
+
That is for loops. An entrance of your own that plays once — `transform`/`opacity` only — plays from the first paint
|
|
37
|
+
with `backwards`, as the presets do: gated on `[data-hydrated]` it jumps when the page goes live.
|
|
34
38
|
- **Entrances:** `opacity` + a small `translateY`, under ~600ms, staggered by tens of ms.
|
|
35
39
|
- **One ambient loop per screen**, slow — not one per card. `infinite` runs on every visitor's battery for as long
|
|
36
40
|
as the page is open.
|
|
@@ -58,7 +62,9 @@ variables: {
|
|
|
58
62
|
- **`tokens(variables)`** names them for a rule: declare the variables once (`satisfies SpaceSpec['variables']`), hand
|
|
59
63
|
the same object to the space, and `const t = tokens(variables)` gives `t.primary === 'var(--primary)'` — a name the
|
|
60
64
|
space does not declare is a type error. A `var(--x)` written by hand that nothing declares is warned
|
|
61
|
-
`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).
|
|
62
68
|
- Choose each value against its own background: a colour picked on white is not the same colour on near-black.
|
|
63
69
|
- Fixed colours are only for surfaces that are fixed in both themes (a brand panel that is always dark) — and then
|
|
64
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
|
|
|
@@ -50,7 +51,7 @@ text('', { bind: [bindTemplate('content', 'computed.level', 'Level {{ source }}'
|
|
|
50
51
|
|
|
51
52
|
A computed value reads the globals (`state`, `auth`, `navigation`, `variables`, `theme`, `host`) and the ones declared
|
|
52
53
|
above it — not an element's source: bind that on the element. One `{{ expression }}` gives its value (a number, a
|
|
53
|
-
list); anything else gives text. `authorSpace` refuses a name read before it is declared
|
|
54
|
+
list); anything else gives text. `authorSpace` refuses a name read before it is declared.
|
|
54
55
|
|
|
55
56
|
## Providers
|
|
56
57
|
|
|
@@ -60,13 +61,22 @@ 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
|
+
- 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`).
|
|
64
68
|
`isEmpty` reads what arrived: a `query`'s body when it is missing, `null`, `''`, `[]` or `{}`; a connector list by
|
|
65
69
|
its `records`; a `singleRecord` provider by its `record`. It is also true before the first answer, so pair it with
|
|
66
70
|
`not isLoading` for an empty state.
|
|
67
71
|
- A query that depends on state is a binding on `query`; it answers `''` (and fetches nothing) until the state exists:
|
|
68
|
-
`"{{ source ? apiUrl ~ '/workspaces/' ~ source ~ '/stats' : '' }}"` with `source: 'state.workspace.id'`.
|
|
69
|
-
|
|
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.
|
|
77
|
+
- `runtime: 'server'` is resolved by the page server — server data is on unless a space says `rsc: { enabled: false }`.
|
|
78
|
+
Only a page and its layouts are resolved — never inside a component: keep the provider on the page and hand the
|
|
79
|
+
component its rows as a prop ([recipes/server-data.ts](../recipes/server-data.ts)).
|
|
70
80
|
|
|
71
81
|
### Typed by a sample
|
|
72
82
|
|
|
@@ -75,17 +85,17 @@ see [typed-sources.md](typed-sources.md).
|
|
|
75
85
|
|
|
76
86
|
### Data in a project with no backend
|
|
77
87
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
'/data/games.json', cache: true, children: [ … ] })`. The page binds to `catalog.data.…` exactly as it would to a live
|
|
81
|
-
backend, so swapping the file for a real endpoint later changes one `query`. `mockData` is what the BUILDER shows while
|
|
82
|
-
editing; it is not a data source for the running page. Say on the page that demo content is demo content.
|
|
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.
|
|
83
90
|
|
|
84
|
-
**
|
|
85
|
-
`
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
89
99
|
|
|
90
100
|
### Live, cached, refreshed
|
|
91
101
|
|
|
@@ -116,7 +126,7 @@ modalContainer({ visible: false, … }) // starts hidden; a flow opens
|
|
|
116
126
|
| The logic… | Example | Write |
|
|
117
127
|
| --- | --- | --- |
|
|
118
128
|
| reveals it: hidden until the data says so | empty state, "get started", admin-only panel, error note | `visible: 'src'`, or `visible: false` + a computed binding |
|
|
119
|
-
| hides it: shown unless a flag says otherwise | sidebar labels until the sidebar is folded, a banner until dismissed |
|
|
129
|
+
| hides it: shown unless a flag says otherwise | sidebar labels until the sidebar is folded, a banner until dismissed | `visible: '!state.folded'` — on screen while nobody has set it |
|
|
120
130
|
|
|
121
131
|
`visible` starts the element **hidden**, and it appears when its data says so. A revealing condition you have to
|
|
122
132
|
compute is `visible: { source, template }` — it starts hidden too, and the template's value is read as a yes or a
|
|
@@ -135,17 +145,19 @@ container({
|
|
|
135
145
|
|
|
136
146
|
Rules for a condition's template:
|
|
137
147
|
|
|
138
|
-
- **A
|
|
139
|
-
|
|
140
|
-
condition on data. `authorSpace` warns `condition-starts-visible` when a computed one on a provider starts on screen.
|
|
148
|
+
- **A visibility binding written by hand in `bind` keeps the element on screen until it answers** — a flash for a
|
|
149
|
+
condition on data (`condition-starts-visible` warns); `visible` starts it hidden.
|
|
141
150
|
- **A hidden element stays in the DOM** (`plitzi-component--hidden`), its subtree mounted. `loadStrategy: 'visible'`
|
|
142
151
|
mounts the subtree only while it is shown, and `'lazy'` from the first time it is shown (modals do that already).
|
|
143
152
|
Use it on heavy panels that are usually hidden; leave the default on what is usually on screen.
|
|
144
|
-
-
|
|
145
|
-
|
|
146
|
-
- **
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
- **Once its source has a value, the answer is a yes or a no**: `false`, `0`, `''` and an empty list hide it,
|
|
154
|
+
anything else shows it.
|
|
155
|
+
- **While its source has no value, only a definite answer is written.** `!source` is `true` — shown; a template's
|
|
156
|
+
`'true'` or `'false'` is written as it says. A plain source, or a template that answers nothing (`''`), writes
|
|
157
|
+
nothing: the element stays as it started — hidden for `visible`, shown for a binding written by hand.
|
|
158
|
+
- **So a hiding flag is `visible: '!state.folded'`** (or a binding with no `visible`): on screen before anybody has set
|
|
159
|
+
it. A condition that answers "not known yet" with `'false'` from a value that IS there — a "folded" read off
|
|
160
|
+
something always set — hid every label of a sidebar for everybody.
|
|
149
161
|
- **Hide the element itself.** Never wrap it in a container that is shown while the inner one is hidden: an empty
|
|
150
162
|
visible wrapper still takes a slot in its parent's `gap`, and leaves a hole in the page.
|
|
151
163
|
|
|
@@ -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,6 +19,16 @@ 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 (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):
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const includes = (words: string) => container({ class: row, quiet: ['repeated-shape'], children: [tick, text(words)] });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Only a suggestion's code: a problem is never quieted (`quiet-unknown`).
|
|
31
|
+
|
|
22
32
|
## The long way, and the short one
|
|
23
33
|
|
|
24
34
|
| Written the long way | The short way | Suggestion |
|
|
@@ -26,13 +36,29 @@ reason is usually that the copies are about to diverge. `npx @plitzi/cli explain
|
|
|
26
36
|
| The same header, footer or sidebar in every page | A **layout** holding it once; each page names it (`layout: { id, slot }`) — [layouts](layouts.md) | `repeated-on-pages` |
|
|
27
37
|
| A header copied per page so one link can be styled "active" | The link marks its own page: `states: { current: { … } }` on its class; `activeOn` for an entry lit on several pages | `repeated-on-pages` |
|
|
28
38
|
| The same card, row or tile written again with other words | A **component** with props, placed with `component(id, { props })` — [components](components.md) | `repeated-shape` |
|
|
29
|
-
| Rows of data written one by one, side by side | One `list` over the rows — `items: [ … ]` fixed, or bound to a source — with its row written once — [lists](lists.md). Data is the test: three cards a person rewords on the canvas read better as three cards | `repeated-shape` |
|
|
39
|
+
| Rows of data written one by one, side by side | One `list` over the rows — `items: [ … ]` fixed, or bound to a source — with its row written once — [lists](lists.md). Data is the test: three cards a person rewords on the canvas read better as three cards. Copies that read different sources or write different state keys are controls written alike, and not offered | `repeated-shape` |
|
|
30
40
|
| `button({ children: [text('Save')] })`, `link({ children: [text('Docs'), fontAwesome({ icon })] })` | `button({ content: 'Save' })`, `link({ href, content: 'Docs', icon, iconPlacement: 'after' })` — the icon's class on the `icon` slot; what the text's class adds (`whiteSpace: 'nowrap'`) moves to the box's class — never the class itself, whose `inherit` would then point past the box and whose `pointerEvents: 'none'` would switch the box off | `content-attribute` |
|
|
31
41
|
| `.card:hover { … }`, `.card .icon { … }` in `customCss` | The class's own `states` and `ancestors` | `custom-css-class` |
|
|
32
42
|
| A reduced-motion reset, or rules showing one icon of the theme toggle, in `customCss` | Nothing: the SDK does both for every space | `custom-css-sdk-default` |
|
|
33
43
|
| `.Toastify__toast { font-family: …; border: … }` in `customCss` | `notifications: { font, fontSize, border, shadow, padding, … }` | `custom-css-notifications` |
|
|
34
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` |
|
|
35
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
|
+
|
|
36
62
|
And a few the suggestions do not count, because they cost styles rather than elements:
|
|
37
63
|
|
|
38
64
|
- **A look used twice is a class**, written once with `styles()`; every element of a TYPE dressed the same way is the
|
|
@@ -25,7 +25,7 @@ container([hero, grid]) // an array is the children
|
|
|
25
25
|
| `visible` | a condition (`'src'`, `'!src'`, `{ source, template }`), or `false` to start hidden |
|
|
26
26
|
| `flows` | what happens on click, on submit, on load |
|
|
27
27
|
| `runtime` | `'server'` resolves this provider's data on the server |
|
|
28
|
-
| `loadStrategy` | when the contents mount relative to visibility: `eager`
|
|
28
|
+
| `loadStrategy` | when the contents mount relative to visibility: `eager`, `lazy`, `visible`. Left out, the type decides — `eager`, `lazy` for a modal or a dialog |
|
|
29
29
|
| `children` | the tree |
|
|
30
30
|
| `meta` | `meta.label` is the element's name in the builder's tree |
|
|
31
31
|
|
|
@@ -44,7 +44,7 @@ another link — make the card the link, or the button, not both.
|
|
|
44
44
|
link to the page being shown is `aria-current="page"`; its class's `current` state styles it, so one header in a
|
|
45
45
|
layout lights the right item everywhere.
|
|
46
46
|
|
|
47
|
-
**An element's `id` is not its HTML id.** It reaches the DOM as `data-
|
|
47
|
+
**An element's `id` is not its HTML id.** It reaches the DOM as `data-plitzi-el`. For a section a URL can land on, give it
|
|
48
48
|
an `anchor` (`anchor: 'plans'` → `id="plans"`: lowercase, digits, `-`) and link to it with `hash`:
|
|
49
49
|
`link({ href: 'home', hash: 'plans' })` goes to `/#plans` and scrolls there, waiting for a section that renders once
|
|
50
50
|
its data arrives. One anchor per page, layouts included; never inside a list row or a component, which would repeat
|
|
@@ -78,12 +78,8 @@ A type this SDK does not ship — a plugin — is authored the same way: `define
|
|
|
78
78
|
|
|
79
79
|
**What a type takes, answered by the package** instead of the 7,000-line `.d.ts`: every catalogue is exported, keyed by
|
|
80
80
|
type — `elementCatalog` (what it is for), `elementDefaultAttributes`, `elementAttributeNames`, `elementTriggers`,
|
|
81
|
-
`elementCallbacks`, `elementSlots`, `elementSourceTypes`, `elementLeafTypes`, plus `BUILTIN_TRANSFORMERS
|
|
82
|
-
prints
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
node --input-type=module -e "import * as a from '@plitzi/sdk-authoring'; const t = 'formControl'; console.log({ attributes: a.elementDefaultAttributes[t], triggers: a.elementTriggers[t], slots: a.elementSlots[t] })"
|
|
86
|
-
```
|
|
81
|
+
`elementCallbacks`, `elementSlots`, `elementSourceTypes`, `elementLeafTypes`, plus `BUILTIN_TRANSFORMERS` — and
|
|
82
|
+
`npx @plitzi/cli explain formControl` prints them for one type.
|
|
87
83
|
|
|
88
84
|
## CSS
|
|
89
85
|
|
|
@@ -92,7 +88,7 @@ kebab-case or camelCase (`paddingTop`, as a React style object), and a bare numb
|
|
|
92
88
|
`fontWeight: 800`, `opacity: 0.5` stay numbers). A property outside the vocabulary is refused.
|
|
93
89
|
|
|
94
90
|
- `column(gap, extra?)`, `row(gap, extra?)`, `grid(columns, gap, extra?)` for the three layouts every space repeats.
|
|
95
|
-
- **Breakpoints are ranges, not a cascade.** `tablet` is 48–64rem, `mobile`
|
|
91
|
+
- **Breakpoints are ranges, not a cascade.** `tablet` is 48–64rem, `mobile` up to 48rem, and each inherits only from
|
|
96
92
|
`desktop` — a rule for `tablet` never reaches a phone (`authorSpace` warns `tablet-rule-skips-mobile`). A rule for
|
|
97
93
|
everything narrower than a desktop is `compact`, written to both: `css: { desktop: { … }, compact: { 'grid-template-columns':
|
|
98
94
|
'1fr' }, mobile: { gap: '8px' } }` — what `tablet` or `mobile` say for themselves wins.
|
|
@@ -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.
|
|
@@ -12,7 +12,7 @@ list({ id: 'games', items: 'catalog.data.games', row: r => listItem({ children:
|
|
|
12
12
|
list({ id: 'plans', items: ['Free', 'Pro'], children: [listItem({ children: [text({ from: 'plans.item' })] })] }) // fixed
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
`items` makes a list controlled: an array of its own, or the source its rows come from. `row` is what it renders once
|
|
15
|
+
`items` (or `from`) makes a list controlled: an array of its own, or the source its rows come from. `row` is what it renders once
|
|
16
16
|
per item — a component's id (placed with the row bound to its `item` prop, or its only prop), or a function handed the
|
|
17
17
|
row's names: `r.item` and `r.index` for a binding or `from`, `r.inTemplate.item` for a template
|
|
18
18
|
(`` `/games/{{ ${r.inTemplate.item}.slug }}` ``). A function needs the list's `id`, which names those sources. With
|
|
@@ -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
|
|
|
@@ -9,13 +9,8 @@ custom({ id: 'arcade', renderType: 'nebulaRun', shipColor: 'amber', bind: { best
|
|
|
9
9
|
|
|
10
10
|
## Creating one
|
|
11
11
|
|
|
12
|
-
The CLI writes it, in the shape Plitzi's own elements are written in:
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
npx @plitzi/cli add plugin seat-picker legend # elements of this project (src/plugins/<Name> in a CLI project)
|
|
16
|
-
npx @plitzi/cli create seat-picker --plugin # a package of its own, with a preview
|
|
17
|
-
npx @plitzi/cli pack plugin # built, with its manifest and the zip the builder takes
|
|
18
|
-
```
|
|
12
|
+
The CLI writes it, in the shape Plitzi's own elements are written in: `npx @plitzi/cli add plugin seat-picker` (a
|
|
13
|
+
package of its own and packing it: the CLI skill).
|
|
19
14
|
|
|
20
15
|
One folder, four files: `SeatPicker.tsx` (the component), `declaration.ts` (its `type`, the `triggers` it fires, the
|
|
21
16
|
`callbacks` it answers to, and the element the builder adds — data only), `Settings.tsx` (its panel in the builder), and
|
|
@@ -38,8 +33,7 @@ by the same name, and so does whatever a binding writes — which is what makes
|
|
|
38
33
|
panel edits for a plugin it has no controls for; from code, write attributes. A `settings` that is not JSON renders
|
|
39
34
|
"Settings Malformed" in the page.
|
|
40
35
|
|
|
41
|
-
Every prop is optional
|
|
42
|
-
is `undefined`, and a plugin that renders nothing then is a hole in the page.
|
|
36
|
+
Every prop is optional with a default: an attribute not authored yet, or a binding not answered yet, is `undefined`.
|
|
43
37
|
|
|
44
38
|
## The component
|
|
45
39
|
|
|
@@ -73,7 +67,36 @@ const stop = state.subscribe(next => save(next)); // called after every cha
|
|
|
73
67
|
```
|
|
74
68
|
|
|
75
69
|
`state` is the current value, `setState` replaces it (or takes an updater), `setStateByKey` writes one key,
|
|
76
|
-
`clearState` empties it, `subscribe` listens. Bindings on `state.*` re-render at once.
|
|
70
|
+
`clearState` empties it, `subscribe` listens. Bindings on `state.*` re-render at once. With the space's `keepState`
|
|
71
|
+
on ([kept state](kept-state.md)), what a plugin writes there — its layout, a choice — is back after a reload too.
|
|
72
|
+
|
|
73
|
+
## Laying out the space's elements
|
|
74
|
+
|
|
75
|
+
A plugin that arranges elements — a dock, tabs, a masonry — HOLDS them: they are its children in the space
|
|
76
|
+
(`custom({ renderType: 'dock', children: [feed, tools] })`, or dropped into it in the builder), and the component
|
|
77
|
+
places each in a box of its own, by the id it was authored under:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { elementChildren } from '@plitzi/plitzi-sdk';
|
|
81
|
+
|
|
82
|
+
{elementChildren(children).map(({ id, node }) => <section key={id} style={placed[id]}>{node}</section>)}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Never write `style` onto an element the plugin does not render: nothing promises to keep it. Its boxes share one
|
|
86
|
+
stacking context, the plugin's. `useDisplayMode()` names the breakpoint showing — `desktop`, `tablet` or `mobile` — at
|
|
87
|
+
the widths the space's styles use, instead of a width of the plugin's own.
|
|
88
|
+
|
|
89
|
+
## Its own server code
|
|
90
|
+
|
|
91
|
+
`plitzi add plugin board --server` writes `functions/index.ts` in its folder (the CLI skill). Its routes answer
|
|
92
|
+
under `/fn/plugins/board/`; the component names them, with nothing for the space to wire:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
const route = usePluginRoute('board'); // from '@plitzi/plitzi-sdk'
|
|
96
|
+
const url = route('/layout'); // undefined where no server runs code (the canvas)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Its `ctx.kv` is the plugin's own; the space's credentials and channels are not.
|
|
77
100
|
|
|
78
101
|
## Talking to other pages
|
|
79
102
|
|
|
@@ -104,13 +127,12 @@ import { useFlag } from '@plitzi/plitzi-sdk';
|
|
|
104
127
|
const newChart = useFlag('newChart'); // false for a flag the space does not declare
|
|
105
128
|
```
|
|
106
129
|
|
|
107
|
-
Gating the whole plugin needs no code: give its element `flag: 'newChart'`.
|
|
108
|
-
inside — a new mode, an extra panel.
|
|
130
|
+
Gating the whole plugin needs no code: give its element `flag: 'newChart'`.
|
|
109
131
|
|
|
110
132
|
## Drawing and animating
|
|
111
133
|
|
|
112
|
-
A canvas, WebGL or a loop of its own: `useCanvas2d`, `useWebGL`, `useWebGL2` and `useAnimationFrame` —
|
|
113
|
-
[drawing](drawing.md).
|
|
134
|
+
A canvas, WebGL or a loop of its own: `useCanvas2d`, `useWebGL`, `useWebGL2` and `useAnimationFrame` — and keeping one
|
|
135
|
+
with thousands of shapes fast — see [drawing](drawing.md).
|
|
114
136
|
|
|
115
137
|
## Registering
|
|
116
138
|
|
|
@@ -154,6 +176,10 @@ A plugin says what HAPPENED through its events (`onPick`, with the seat in the p
|
|
|
154
176
|
decide what that means — write `state`, open a modal, call a server action. Prefer that to writing `state` from inside
|
|
155
177
|
the component: the flow is visible in the space, the builder shows it, and the same act can come from a button too.
|
|
156
178
|
|
|
179
|
+
- A param declared `number` or `boolean` arrives as one, written or bound; any other arrives as written.
|
|
180
|
+
- To act when another element appears, `useElementVisible('tools')` (`@plitzi/plitzi-sdk`) — never watch classes.
|
|
181
|
+
- A name for screen readers is a prop of its own: declare `label`, write it as the root's `aria-label`.
|
|
182
|
+
|
|
157
183
|
## Behaving in the builder
|
|
158
184
|
|
|
159
185
|
The builder draws the element on its canvas while somebody edits the page: a click selects it, a drag moves it, a key
|
|
@@ -162,10 +188,21 @@ belongs to the editor.
|
|
|
162
188
|
- **Outside preview, do nothing on your own.** `usePlitziServiceContext().settings.previewMode` is `false` while the
|
|
163
189
|
page is edited. Declared interactions are already held back then; your own click handlers, timers, global listeners,
|
|
164
190
|
permission prompts and map gestures are not — gate them on `previewMode`, and still render something to select.
|
|
191
|
+
- **A drag lives in state, not in refs read while rendering** (the project's lint refuses that): keep the gesture
|
|
192
|
+
in `useState`, mirror the latest props into a ref inside an effect, and attach `pointermove` on `pointerdown`.
|
|
165
193
|
- **Never the global `window` or `document`.** The canvas is a frame of its own and the code runs in the builder's
|
|
166
194
|
window: listen, measure and go full screen through the node's own page (`ref.current.ownerDocument`, its
|
|
167
195
|
`defaultView`) or `usePlitziServiceContext().utils.getWindow()`. An `instanceof` check takes its class from there too.
|
|
168
196
|
|
|
197
|
+
## The plugin's stylesheet and the space's CSS
|
|
198
|
+
|
|
199
|
+
A plugin's stylesheet ships in a layer below the space's (`plitzi-sdk-plugin`, written by the build): the space's
|
|
200
|
+
classes and `customCss` win over it whatever their specificity, as over a built-in element's defaults.
|
|
201
|
+
|
|
202
|
+
A file a library needs whole is imported as Vite imports it, and travels inside the bundle: `worker.js?raw` is its
|
|
203
|
+
text (a worker, started from `URL.createObjectURL(new Blob([source]))`), `engine.wasm?inline` a data URI. Images and
|
|
204
|
+
fonts a stylesheet names are carried the same way.
|
|
205
|
+
|
|
169
206
|
## Components that draw into DOM they do not render
|
|
170
207
|
|
|
171
208
|
A map, a chart library, anything that positions its own markers or popups: its roots are the library's to place. Never
|
|
@@ -173,27 +210,3 @@ give them a class that sets `position` — the element falls into the page's flo
|
|
|
173
210
|
the look in the plugin's own stylesheet (imported CSS ships beside the bundle) and take colours from custom properties
|
|
174
211
|
the space sets (`--seat-accent: var(--accent)` in `customCss`); a canvas or WebGL layer resolves them through a probe
|
|
175
212
|
element with `getComputedStyle(probe).color`, again whenever `theme.resolved` changes (bind it as a prop).
|
|
176
|
-
|
|
177
|
-
## Components that draw a lot
|
|
178
|
-
|
|
179
|
-
A canvas with thousands of shapes on it — a whiteboard, a diagram, a map of points — is judged on a slower machine than
|
|
180
|
-
yours: sixty frames a second here says nothing about a four-year-old laptop. What keeps one fast is doing work in
|
|
181
|
-
proportion to what CHANGED, not to what is there:
|
|
182
|
-
|
|
183
|
-
- **Two layers.** Paint what stands still on one canvas and leave it; clear and draw the one over it every frame, with
|
|
184
|
-
only what moves: the cursor, the selection, what is being dragged.
|
|
185
|
-
- **Repaint the part that changed.** Something added, edited or removed repaints the area it was and is in, clipped,
|
|
186
|
-
with whatever reaches into it — not the whole board. A pan moves the picture already painted by whole device pixels
|
|
187
|
-
and paints the edges it uncovers.
|
|
188
|
-
- **Draw a dragged group once.** Everything picked up moves by the same amount at every step: draw it to a canvas of its
|
|
189
|
-
own when the drag begins and copy that into place after.
|
|
190
|
-
- **Cache per object, not per frame.** Treat elements as immutable and key what is derived from one — its bounds, its
|
|
191
|
-
shape, a resolved connector — on the object itself (a `WeakMap`), so nothing is worked out twice for the same thing.
|
|
192
|
-
- **Hand the space outcomes, not motion.** A stroke finished, a selection changed: a trigger each. Firing one at every
|
|
193
|
-
pointer move runs a flow and renders the page at every pointer move.
|
|
194
|
-
- **Measure with a bench, throttled.** A script that drives the real thing in a browser with the CPU slowed
|
|
195
|
-
(`Emulation.setCPUThrottlingRate`) and reports script time per frame, not only frames per second — and a test that
|
|
196
|
-
counts WORK (full repaints, strokes drawn during a drag), which holds on any machine where a timing does not.
|
|
197
|
-
|
|
198
|
-
Pizarra, the collaborative whiteboard on the platform (`pizarra.plitzi.app`), does all of it on a board of thousands
|
|
199
|
-
of elements.
|
|
@@ -7,7 +7,10 @@ of it; each line is something that has shipped broken before.
|
|
|
7
7
|
|
|
8
8
|
- [ ] `authorSpace` returns **zero warnings**. Each one is written code that will not do what it says.
|
|
9
9
|
- [ ] Every **suggestion** is taken, or left for a reason you can say (the copies are about to diverge; three cards a
|
|
10
|
-
person rewords on the canvas)
|
|
10
|
+
person rewords on the canvas) — and then quieted where it is written, `quiet: ['repeated-shape']`, so it stops
|
|
11
|
+
burying the ones that matter. The ones your change opened up first — [efficiency.md](efficiency.md).
|
|
12
|
+
- [ ] `npm run lint:space` is clean — no file past 400 lines, no rows of data inline, no colour that is not a token —
|
|
13
|
+
or each departure says why where it is ([structure.md](structure.md#checked-for-you)).
|
|
11
14
|
- [ ] No console errors on any page you touched.
|
|
12
15
|
|
|
13
16
|
## Nothing flashes, nothing jumps
|
|
@@ -39,7 +42,8 @@ Screen readers and browser agents (Claude in Chrome) work a page through its acc
|
|
|
39
42
|
a `label` (`hideLabel: true` when the design shows what it is).
|
|
40
43
|
- [ ] Every image has an `alt` saying what it shows, or `decorative: true`.
|
|
41
44
|
- [ ] Every click is on a `button` or a `link`; the whole page can be worked from the keyboard.
|
|
42
|
-
- [ ] A toggle binds `ariaPressed`, a button that opens something `ariaExpanded
|
|
45
|
+
- [ ] A toggle binds `ariaPressed`, a button that opens something `ariaExpanded` and names it with `controls`; words
|
|
46
|
+
that change on their own are in a `live` container.
|
|
43
47
|
- [ ] Headings step down one level at a time; parts of the page a person jumps to are landmarks (`nav`, `main`, a
|
|
44
48
|
labelled `section`).
|
|
45
49
|
- [ ] What a canvas or a plugin draws is also there as elements, with a visible way to reach them.
|
|
@@ -66,6 +70,16 @@ Screen readers and browser agents (Claude in Chrome) work a page through its acc
|
|
|
66
70
|
- [ ] What only an administrator may read is shown only to administrators — and the check is a real boolean, not
|
|
67
71
|
a template left as text.
|
|
68
72
|
|
|
73
|
+
## Nothing left behind
|
|
74
|
+
|
|
75
|
+
- [ ] Nothing you made and no longer use is still there — a page, a component, a class, a token, a data file, an
|
|
76
|
+
import, a plugin folder. No commented-out code, no debug `console.log`, no copy of a file kept "just in case".
|
|
77
|
+
- [ ] Scratch work — a one-off script, a dump, a picture to look at — went in `tmp/` (never committed) and is gone from
|
|
78
|
+
everywhere else.
|
|
79
|
+
- [ ] Nothing secret, and nothing only some visitors may read, is in `public/` or in the space's documents: both reach
|
|
80
|
+
every visitor.
|
|
81
|
+
- [ ] `typecheck`, `lint` and `format` are clean.
|
|
82
|
+
|
|
69
83
|
## Proven
|
|
70
84
|
|
|
71
85
|
- [ ] You looked at it: the visual check ran, on the pages you changed, in both themes.
|
|
@@ -31,8 +31,8 @@ Never rewrite an exported space by hand. `specFromSpace({ schema, style })` read
|
|
|
31
31
|
A server action reads back the same way: `actionSpecFromEntry({ id, document })` gives the `defineAction` spec that writes
|
|
32
32
|
it — only when that spec writes the very same document, otherwise `reason` says why and it stays JSON — and
|
|
33
33
|
`actionToSource(spec)` writes the module. For the whole space as a project — pages, actions, functions, the source of
|
|
34
|
-
its plugins and runtime, its files — use `plitzi create --from <space>` (
|
|
35
|
-
the parts by hand.
|
|
34
|
+
its plugins and runtime, its files — use `plitzi create --from <space>` (then `plitzi pull` to bring the space's changes in, and
|
|
35
|
+
`plitzi push` to put the project's back) rather than exporting the parts by hand.
|
|
36
36
|
|
|
37
37
|
For a document you did not author — an export, a JSON edited by hand — run the gate before serving it:
|
|
38
38
|
`validateSpace({ schema, style })`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Structuring a space that grows
|
|
2
2
|
|
|
3
|
-
A one-screen space fits in `src/space.ts`. Past a few pages, split it by what changes together — a file per part, short
|
|
3
|
+
A one-screen space fits in `src/space/index.ts`. Past a few pages, split it by what changes together — a file per part, short
|
|
4
4
|
enough to read whole — and keep every repeated thing in exactly one place. `npx @plitzi/cli create shop --template
|
|
5
5
|
catalog` writes a complete small site in this shape (tokens, a layout, a card component, typed data, a page per file)
|
|
6
6
|
to read before writing one.
|
|
@@ -71,6 +71,30 @@ container({ id: 'quick', class: quickGrid, children: QUICK.map(item => quickCard
|
|
|
71
71
|
When items differ in ONE detail (one link needs a binding), put that detail in the item's data as an optional field
|
|
72
72
|
— never an `if (item.id === 'team')` inside the map.
|
|
73
73
|
|
|
74
|
+
## Pages of one shape are a family
|
|
75
|
+
|
|
76
|
+
Docs pages, a menu's sections, a team's profiles: the same shape, different words. Write the shape once and the pages
|
|
77
|
+
as data with `pageFamily` — it gives each page its id, slug, titles, folder and layout, and builds each body inside
|
|
78
|
+
`scope(entry.id, …)`, so the ids two pages of the family give are never one:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { pageFamily, heading, markdown } from '@plitzi/sdk-authoring';
|
|
82
|
+
import { SECTIONS } from '../data/sections'; // [{ id: 'starters', title: 'Starters', slug: 'starters', description, body }]
|
|
83
|
+
|
|
84
|
+
pages: [
|
|
85
|
+
home,
|
|
86
|
+
...pageFamily(
|
|
87
|
+
{ folder: 'menu', layout: { id: 'site', slot: 'site-slot' }, seoTitle: entry => `${entry.title} — Menu`,
|
|
88
|
+
body: entry => [heading({ id: 'title', content: entry.title }), markdown(entry.body, { id: 'body' })] },
|
|
89
|
+
SECTIONS
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Adding a page is adding an entry; two entries of one id or slug are refused as any two pages are. For one shape over
|
|
95
|
+
records a visitor browses — a product, a post — a single page with a route param (`slug: 'posts/:postId'`) reading
|
|
96
|
+
the record is the answer instead: one page, as many addresses as records.
|
|
97
|
+
|
|
74
98
|
## Names
|
|
75
99
|
|
|
76
100
|
- **Ids:** `area-role` in kebab-case — `hm-traffic-title`, `docs-nav-docs-data`. Never the builder's positional
|
|
@@ -90,3 +114,10 @@ per page. It renders; it is not maintainable. When you work on such a file, leav
|
|
|
90
114
|
|
|
91
115
|
Each of those is safe to do on its own: authoring refuses anything that would not render, and the visual check shows
|
|
92
116
|
the rest.
|
|
117
|
+
|
|
118
|
+
## Checked for you
|
|
119
|
+
|
|
120
|
+
`npm run lint:space` (`plitzi lint`) reads the source for what this page asks — a file past 400 lines, pages in one
|
|
121
|
+
file, rows of data inline, a colour that is not a token, CSS copied three times, a row singled out by its id inside a
|
|
122
|
+
`map`, a minted id, a file nothing imports — at file:line, beside authoring's suggestions. Leave it clean; a departure
|
|
123
|
+
on purpose says why where it is: `// plitzi-lint-disable-next-line <code> -- why`.
|