@plitzi/plitzi-sdk 0.37.9 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,647 @@
1
1
  # @plitzi/plitzi-sdk
2
2
 
3
+ ## 0.38.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 266e691: ## A shutdown that does not wait on idle connections
8
+
9
+ A server shutting down while it answered a request waited, once the answer was sent, for the client to let go of the
10
+ keep-alive connection it came on — three seconds for Node's own `fetch`, longer for others — before it could stop. The
11
+ connection is now closed as soon as its answer is finished.
12
+
13
+ - Updated dependencies [266e691]
14
+ - @plitzi/sdk-auth@0.38.1
15
+ - @plitzi/sdk-dev-tools@0.38.1
16
+ - @plitzi/sdk-elements@0.38.1
17
+ - @plitzi/sdk-event-bridge@0.38.1
18
+ - @plitzi/sdk-interactions@0.38.1
19
+ - @plitzi/sdk-navigation@0.38.1
20
+ - @plitzi/sdk-plugins@0.38.1
21
+ - @plitzi/sdk-schema@0.38.1
22
+ - @plitzi/sdk-shared@0.38.1
23
+ - @plitzi/sdk-style@0.38.1
24
+ - @plitzi/sdk-variables@0.38.1
25
+
26
+ ## 0.38.0
27
+
28
+ ### Minor Changes
29
+
30
+ - v0.38.0
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies
35
+ - @plitzi/sdk-auth@0.38.0
36
+ - @plitzi/sdk-dev-tools@0.38.0
37
+ - @plitzi/sdk-elements@0.38.0
38
+ - @plitzi/sdk-event-bridge@0.38.0
39
+ - @plitzi/sdk-interactions@0.38.0
40
+ - @plitzi/sdk-navigation@0.38.0
41
+ - @plitzi/sdk-plugins@0.38.0
42
+ - @plitzi/sdk-schema@0.38.0
43
+ - @plitzi/sdk-shared@0.38.0
44
+ - @plitzi/sdk-style@0.38.0
45
+ - @plitzi/sdk-variables@0.38.0
46
+
47
+ ## 0.37.10
48
+
49
+ ### Patch Changes
50
+
51
+ - 8d1cc02: ## The builder's sidebar, one entry per subject
52
+
53
+ From 21 entries to 9. **Elements** ends with the space's **Components**, one search for both — each is dragged
54
+ onto the canvas the same way. **Server** gathers Actions, Functions, Connectors, Credentials and Runtime behind tabs, with
55
+ the warning that a space has no server-rendered deployment said once above them. **Variables** holds Feature Flags,
56
+ **Assets** holds files and fonts, **Settings** holds Visitors, the Pages panel opens the **Sitemap** in place of the
57
+ canvas, and **History** moved to the header beside undo and redo. Each grouped entry remembers the tab left open.
58
+
59
+ The builder is drawn with the website's design system: Geist and Geist Mono, the `#5b3df5` violet, cool neutrals and
60
+ 8 / 10 / 14px radii, from the tokens `@plitzi/plitzi-ui/theme.css` now ships. The published SDK stylesheet does not
61
+ take them, so a space's own elements look as they did.
62
+
63
+ The Sitemap is drawn by `TreeCanvas`, a new `@plitzi/plitzi-ui` component: the site laid out as a tree on its own, panned
64
+ and zoomed like a design canvas, a page moved by dropping it onto a folder or onto the top level. `@xyflow/react` — and
65
+ zustand and d3 with it — is no longer a dependency of the builder.
66
+ From the map a page is found (search lights it and the folders leading to it), opened in the canvas (double click,
67
+ Enter or its card), and created inside a folder; folders fold away what they hold, remembered between visits; arrows walk
68
+ it. Each card says who may open the page, its layout, the flag it exists under, where it sends somebody it refuses, and
69
+ marks its dynamic segments and the page being edited. Fixed on the way: a page with no access level was labelled
70
+ "Public", which in Plitzi means guests only — it is open to everyone, and now says so.
71
+
72
+ ## Feature flags
73
+ - **What they are:** `schema.flags`, a space's switches by name — a default and rules over the environment, the host,
74
+ the URL and the visitor. Read with the document, stored apart from its snapshots: one set per environment, turned
75
+ without a new revision; a snapshot keeps a copy, read only when the environment's flags (and their Redis copy)
76
+ cannot be. See `docs/en/feature-flags.md`.
77
+ - **Caches follow them:** `SSRSpaceDeployment.flagsVersion` (the flags' hash) keys the HTML, RSC and `offlineData`
78
+ caches of `@plitzi/sdk-server`; `createCloudAdapters` probes `flagsHash` and fetches `SpaceFlags` only when it moved
79
+ — a pinned revision included — and keeps the last flags in its shared cache for a cold start with Plitzi down.
80
+ - **Who decides:** the space, then the server rendering it (`createServer({ flags })`), then the SDK embedding it (the
81
+ `flags` prop), then a tester (the dev tools' Flags tab, only where debugging is authorized) — each only for flags the
82
+ space declares.
83
+ - **Gating:** `definition.flag: { name, is }` renders an element only while the flag agrees — not a visibility: gated
84
+ off, none of it is rendered, on the server or in the browser, and RSC resolves no data for it. A gated page is not
85
+ found. Its declaration still ships with the space's document: a flag switches a feature off, it does not hide it.
86
+ - **Reading:** the `flags` global source (`{{ flags.x }}`), `useFlag(name)` for plugins, and `flags` in a server
87
+ action's scope (the `getFlags` action lookup).
88
+ - **Builder:** Feature Flags beside the variables (declare, rule, force in the canvas, publish), the gate in an element's tools, a
89
+ marker in the tree. Its own flags come from the platform (`PlatformFlags`) instead of a constant.
90
+ - **Authoring and MCP:** `SpaceSpec.flags`, `flag: 'name' | '!name'` on elements and pages, linter codes
91
+ `flag-undeclared`, `flag-unknown`, `flag-unused`, `flag-rule-empty`; MCP `upsertFlag`, `deleteFlag`, `flag` on
92
+ element and page ops, `plitzi://flags/{env}`.
93
+ - **Global sources** are one list now (`@plitzi/sdk-shared/dataSource/globalSources`), read by the runtime and the
94
+ authoring validator alike.
95
+
96
+ ## Element templates are Snippets
97
+
98
+ What the builder saves from a subtree and drops into a page was called a template, the word a space's own starting
99
+ point already goes by. It is a **snippet** now, everywhere, with no alias for the old names:
100
+
101
+ - **Builder:** "Save as snippet" on an element, **Snippets** in the resources list. A snippet has an icon of its own
102
+ (an object group) beside the component's cube, in the canvas overlay and the context menu alike, and each says on
103
+ hover what sets it apart: a component stays linked, a snippet is a copy.
104
+ - **CDN:** a snippet is uploaded to `snippets/` in the space's folder, with the resource type `snippet`. A file already
105
+ in `templates/` is no longer listed as one: upload it again.
106
+ - **Authoring:** `authorSnippet`, `validateSnippet`, `SnippetSpec` and `AuthoredSnippet`, which returns `{ snippet,
107
+ warnings }`. The validator codes are `SNIPPET_*`.
108
+ - **Shared and schema:** the `Snippet` type (`@plitzi/sdk-shared/types/SnippetTypes`), `SpaceAddSnippet` and
109
+ `SPACE_ADD_SNIPPET`, `SCHEMA_ADD_SNIPPET` and `STYLE_ADD_SNIPPET`, `schemaAddSnippet` / `styleAddSnippet` on the
110
+ event bridge, and `FlatMap.flatAsSnippet`.
111
+ - **Plugins:** the builder config key `canTemplate` is `canSnippet`.
112
+ - **One document, whoever writes it:** a snippet's `schema` is only what travels — `flat` and `variables` — whether
113
+ `authorSnippet` wrote it or the builder saved it. Until now an authored one carried a whole space (`pages: []`, its
114
+ settings), which the builder's preview laid over the space being edited, and one the builder saved failed
115
+ `validateSnippet` (`INVALID_PAGES`).
116
+ - **The same snippet, dropped twice:** the second drop renamed its elements in the editor while the server was sent
117
+ the names it arrived with, refused them as taken, and the drop was undone. The names are now fitted where the
118
+ snippet is dropped (`fitSnippet`, `@plitzi/sdk-schema/helpers/fitSnippet`) and carried by the insert to the server
119
+ and every collaborator; `SCHEMA_ADD_SNIPPET` inserts under them and refuses a name taken since, as the server does.
120
+ `SpaceAddSnippet` checks what it is sent against the `SPACE_ADD_SNIPPET` event before applying it.
121
+ - **A snippet never restyles the space it lands in:** a class it brings under a name the space uses for something
122
+ else is renamed (`card` → `card-2`) on its rule, its elements and the rules naming it as an ancestor, and its CSS
123
+ recompiled; one that says the same is shared. The space keeps its rules for element types and its tokens, and a
124
+ snippet's tokens the space lacks are now added — the editor merged its rules only, and the server neither
125
+ (`mergeSnippetStyle`, `@plitzi/sdk-shared/style/snippetStyle`; `SnippetStyle`; `STYLE_ADD_SNIPPET` carries `style`).
126
+ - **A snippet is known by what it holds:** `isSnippet` (`@plitzi/sdk-shared/schema/snippet`). A JSON uploaded in
127
+ **Assets** that is a snippet goes among the snippets — before, an authored one landed as a plain file — and an upload
128
+ declared a snippet that is not one is refused. A file among the snippets the builder cannot read is shown as such,
129
+ with a way to remove it, instead of breaking the panel.
130
+ - **Saving says how it went:** "Save as snippet" announces the snippet once the upload answered, and says why when it
131
+ did not — it used to report it created before knowing, and from the context menu said nothing at all.
132
+
133
+ Space templates — what a new space starts as — keep their name.
134
+
135
+ ## A link to a section of a page
136
+ - **`anchor`** on any element is its `id` in the DOM — the element's own id only ever reached it as `data-id` — so
137
+ `/page#plans` has somewhere to land. Written by `authorSpace` (`anchor: 'plans'`), the builder (the element's
138
+ **Anchor** field), the MCP (`upsertElement` / `patchElement`) and read back by `specFromSpace`.
139
+ - **`link` takes `hash`**: `link({ href: 'home', hash: 'plans' })` goes to `/#plans`; a flow's `navigate('home#plans')`
140
+ resolves the page and keeps the fragment.
141
+ - **The router scrolls to the fragment** after every client-side navigation and on arrival, and waits up to 3 s for a
142
+ section that renders once its data arrives — a visitor who scrolls first is left where they are.
143
+ - **Refused while authoring:** an anchor that is not lowercase letters, digits and `-` (`anchor-invalid`), one on an
144
+ element with no tag (`anchor-no-tag`), inside a list row or a component (`anchor-repeated`), twice on one page with
145
+ its layouts (`anchor-duplicate`), and a link to a section the page does not have (`anchor-missing`).
146
+
147
+ ## Every problem in one run
148
+ - **`authorSpace` reports everything it cannot write at once**, as a `SpaceRefusedError` whose `refusals` list each
149
+ one — instead of stopping at the first. An element it cannot write is left out and its siblings carry on; a class
150
+ worn with `css`, `states` or a `selector` of its own is reported and written with the class, so the linter still
151
+ reads the rest of the space and its findings join the same report.
152
+ - **Each problem says where:** the line of your own code that called the factory (`src/site/home.ts:417`) and the
153
+ nearest named element with the steps from it (`"store-footer" › container[1]`) — not a path of indices.
154
+ - **The project's `npm run author`** prints one line on success, the numbered problems (no stack from inside the
155
+ package) on failure, and one JSON object with `--json`.
156
+
157
+ ## A class, plus one thing
158
+
159
+ `class: [cover, { opacity: '0.25' }]` puts rules of the element's own on top of the classes it wears — the commonest
160
+ shape there is, which until now took a class of its own every time. The rules become the class `<id>--own`, declared
161
+ after every shared class so it wins over them, editable in the builder like any class; they take states and
162
+ breakpoints as `styles()` does, need the element's `id`, and read back from a document as the same inline object.
163
+
164
+ ## Every few seconds, with no plugin
165
+
166
+ `onInterval(ms)` is a trigger every element has: a flow that repeats every `ms` milliseconds — an autoplay, a clock, a
167
+ refresh — while the element is mounted and the tab is in view, and never in the builder outside preview. Each flow
168
+ names its own interval and counts its own ticks (`{{ <step>.count }}`); one below 250 ms is refused (`trigger-interval`).
169
+ It is in the builder's flow editor beside `onKey`, and `@plitzi/sdk-shared/helpers/interval` holds the rule all of them
170
+ read.
171
+
172
+ ## Data in `public/`, in the page from the first byte
173
+ - **`plitzi create --mode server`** writes `public/data/` and passes `publicDir` to `createServer`: `public/data/*.json`
174
+ is served with no change to `src/main.ts`.
175
+ - **A server provider reads a file of the server's own:** an `apiContainer` with `runtime: 'server'` and a `query`
176
+ that is a plain path in `publicDir` (`/data/home.json`) is resolved by the page server from disk — no connector or
177
+ action configured — so the page arrives with the section in it rather than fetching it after load. A URL, a path
178
+ outside `publicDir` or a `query` with `{{tokens}}` is left to the browser as before.
179
+
180
+ ## A project's server, findable
181
+ - **`npm start` starts beside whatever holds 8080** while developing: the next free port (`freePort`, from
182
+ `@plitzi/sdk-server`), printed and written to `.plitzi/dev-server.json`. With `PORT` set, that port or an error.
183
+ - **`/health` answers with the space's name**, and `npm run shot` checks it before taking a picture — another server
184
+ on the port is reported, instead of photographed. `playwright.config.ts` and `shot` read the port from `PORT`, else
185
+ from `.plitzi/dev-server.json`.
186
+
187
+ ## Every document type, from the package you author with
188
+
189
+ `@plitzi/sdk-authoring` exports the types of what it writes — `Schema`, `Style`, `SpaceFont`, `Element` and the rest
190
+ of the documents, plus `SchemaValidationError`, `SchemaValidationOptions` and `SchemaValidationResult` — so a project
191
+ types its own helpers without reaching into `@plitzi/sdk-shared`.
192
+
193
+ ## Breakpoint warnings that read `display: none`
194
+
195
+ `tablet-rule-skips-mobile` no longer warns about a tablet rule the phone hides anyway: an element with
196
+ `display: none` under `mobile` never shows the rule it skips. When it does warn, it points to `compact`, which reaches
197
+ both.
198
+
199
+ ## An inline container
200
+
201
+ `container({ subType: 'span' })` renders a `<span>`: a dot before a title, a word dressed apart, a badge in a line of
202
+ text — inline by default, and in the builder's container settings as "Span (inline)". A span holding a heading, a
203
+ paragraph, a list, a form or prose is warned about (`span-holds-block`).
204
+
205
+ ## Every problem has a code, and a page that cannot miss one
206
+ - **Every refusal and warning carries a code** — `[class-and-css]`, `[id-taken]`, `[tablet-rule-skips-mobile]` — in
207
+ the message and in `SpaceRefusedError.refusals[].code`. `AUTHORING_CODES` (exported) is the one table they are raised
208
+ from: whether each is refused or warned, what was wrong and what to write instead. A check raised with a code that is
209
+ not in it does not compile.
210
+ - **The skill's `authoring-errors.md` is generated from that table**, as is the website's Authoring errors page
211
+ (`authoringCodesTable`), so neither can miss a code; a test fails when the page and the table disagree.
212
+ - `AuthoringError` is what a factory or `authorSpace` throws for one problem on its own, with its `code` and `reason`.
213
+
214
+ ## The skills, packaged for a project
215
+ - **A cheatsheet to start from** (`CHEATSHEET.md`): the factories, fields, steps and the problems met most, on one
216
+ page. `SKILL.md` starts there, says what to read for each kind of task, and what never to read whole.
217
+ - **Recipes by intent, as files** (`recipes/*.ts`): show data from a file, filter a list, a detail page, link to a
218
+ section, something every few seconds (a carousel), a marquee, a link built from a row, forms and modals, a feature
219
+ flag, a plugin, controls usable without sight, styling, and an embed or an SVG. CI authors each one: a recipe that
220
+ stops authoring fails the build.
221
+ - **Every link in a skill resolves inside a project**, and none names a file only the workspace has; a test walks them
222
+ as `plitzi create` copies them. Realtime channels have a reference of their own.
223
+ - **Each skill file has a budget** — about 4k tokens for a `SKILL.md`, 3k for a reference — held by a test.
224
+ - **When to use the MCP and when the CLI**, in both skills: the MCP for a space that lives on Plitzi, the CLI for one
225
+ that lives in code; a sign-in nobody can give never blocks the second.
226
+ - New guidance: animations (keyframes, entering with `visible`, pausing on hover, staggering), a list's `index` as
227
+ text, rows kept by position, a provider around a layout's slot, checking motion in a test.
228
+
229
+ ## A project an agent finds its way around
230
+ - **`AGENTS.md` says the port, where data goes, how to look at a page, and what not to read**: the generated
231
+ `space/offline-data.json`, `.sdk-plugins/`, a large `public/data/*.json` and the bundles in `node_modules`.
232
+ - **`plitzi data describe <file>`** prints a JSON file's shape — every field, its type, how many rows have it — and one
233
+ row of its longest list; `--json` for a tool.
234
+ - **Quiet by default:** the server prints only what goes wrong (`npm start -- --verbose` for every request), and
235
+ `typecheck` one line per error.
236
+ - **The welcome space follows the skill's rules:** a box one class owns whole is a shorthand; a test holds it to that.
237
+ - `.claude` is left out of a project's lint and formatting.
238
+
239
+ ## Problems over MCP come with their code and their fix
240
+
241
+ An agent editing a space over MCP has no skill page to look a code up in: each problem a batch meets now leads with its
242
+ `[code]` and its hint says what to write instead, from the same `AUTHORING_CODES` row `authorSpace` raises it from
243
+ (`authoringCodeEntry(code)` looks one up).
244
+
245
+ ## Scrolling, as steps
246
+ - **`scrollBy`, `scrollTo` and `scrollIntoView`** are callbacks every element answers: `scrollBy('cards', { x: '80%' })`
247
+ moves a box by most of what it shows, `scrollTo('cards', { x: 'end' })` to an end or a place, `scrollIntoView` brings
248
+ an element into view. In the builder's flow editor, in authoring and over MCP.
249
+ - **`onScroll`** fires as an element's box moves — at most once a frame, and once on mount — with
250
+ `{ x, y, atStart, atEnd }`, so an arrow can hide at the end already reached (recipe: `recipes/scroll-a-row.ts`).
251
+
252
+ ## A list names its rows by the field you choose
253
+
254
+ `list({ itemKey: 'slug' })` keys each row by that field of its item, so a row's state follows its item when the list is
255
+ filtered or reordered, and a row whose item changes mounts again — a one-row list showing the current slide replays its
256
+ entrance. Left out, rows follow the items' `id` (when every item has its own), else their position. In the builder it
257
+ is the list's "Row key"; `list-item-key-missing` warns when fixed items lack it or share one.
258
+
259
+ ## A skeleton while a provider loads
260
+
261
+ `apiContainer({ loadingSlot: 'catalog-skeleton' })` names one of its children to show in place of the others until the
262
+ first answer arrives — the shape of what is coming — and to drop after it. In the builder every child shows, so the
263
+ slot is edited beside what it stands for; `loading-slot-unknown` refuses a slot no child answers to.
264
+
265
+ ## `plitzi create --template blank`
266
+
267
+ A space written in the project can start empty: tokens for both themes, a layout whose `site-main` the pages render in,
268
+ one page, `public/data/` and no example plugin — for a project that is about to be a specific site, where the welcome
269
+ tour is the first thing that would be deleted. `emptySpaceSpec` / `emptySpaceSource` in `@plitzi/sdk-authoring`.
270
+
271
+ ## `plitzi add plugin` writes the shape it is told
272
+
273
+ `--prop interval:number=5000`, `--trigger onTick:count`, `--callback reset` and `--headless` write an element in its
274
+ final shape — typed props with defaults, bindable and with a control each in its panel; a `use<Name>Events()` hook that
275
+ fires its events with typed payloads; a function per action; and, headless, hidden on a page and a badge in the builder
276
+ — instead of the counter example, which is what it writes without them. Flags that cannot make an element (a type that
277
+ is not one, an event every element already fires, a name used twice) are refused with how to write them, and the files
278
+ are written as the project's Prettier writes them.
279
+
280
+ ## Skills that say their version, and `plitzi skills update`
281
+
282
+ The skills `plitzi create` copies into a project carry the version of the package they came from (`version:` in
283
+ `SKILL.md`). `npm run author` says when the authoring skill is older than the `@plitzi/sdk-authoring` installed, and
284
+ `plitzi skills update` replaces each Plitzi skill with the installed package's — whole, leaving any other skill alone.
285
+ The CLI skill's references for `create --from` / `pull` and for a space's functions are files of their own now, read
286
+ when a task names them.
287
+
288
+ ## `plitzi explain`
289
+
290
+ What a name means when authoring, in a few lines: an element (its attributes and their values, what it fires and
291
+ answers, its slots), a step (its params and the function that writes it), a trigger (what it hands its flow, what fires
292
+ it), a problem's code (what was wrong, what to write instead) or a transformer. `--list steps` names every one of a
293
+ kind, `--json` answers in one object, and over MCP it is the resource `plitzi://explain/{name}`. Read from the same
294
+ catalogues the checks use (`explain`, `explainList`, `explanationText` in `@plitzi/sdk-authoring`).
295
+
296
+ ## `plitzi check` and `plitzi shot`: a page in numbers before pictures
297
+ - **`plitzi check / --width 1440,390`** says whether a page of the running project is whole, in text: every element the
298
+ space owes it on screen (or why not), no broken image, no sideways scroll, no text in the colour behind it, no console
299
+ error, no refused request — per width, `--json` for a tool. A space in code is checked against what each page owes;
300
+ any other, against what every page does.
301
+ - **`plitzi shot`** takes the picture, and `--compare <url>` puts the same page of another site beside it — the
302
+ differences in red, and the share that differs in each landmark (`section#plans 14%`); `--frames 4 --every 500` says
303
+ what moves; `--wait-for` and `--reduced-motion` set it up. The comparison runs in the browser
304
+ (`comparePictures`, `pageRegions` in `@plitzi/sdk-authoring`), so nothing is installed for it.
305
+ - Both use the project's own Playwright and refuse a port that answers as another project. A project `create` writes
306
+ installs `@plitzi/cli` and runs them as `npm run check` / `npm run shot`, which replaces its `scripts/shot.ts`.
307
+
308
+ ## The builder's problems panel says the fix
309
+
310
+ Each problem in the builder's panel shows what to write instead, from the same `AUTHORING_CODES` row `authorSpace` and
311
+ the MCP use — `SpaceIssue.fix` over GraphQL, its code set apart.
312
+
313
+ ## Scroll snap, and anchors under a fixed header
314
+
315
+ `scroll-snap-type`, `scroll-snap-align` and `scroll-snap-stop` — a row of cards that comes to rest on a card — and
316
+ `scroll-padding-top` / `scroll-margin-top` — an anchor that lands below a fixed header rather than under it — are part
317
+ of the style vocabulary, with a "Scroll snap" section in the style editor.
318
+
319
+ ## CSS as a React style object, and a value per breakpoint in place
320
+ - **camelCase keys and numbers:** `{ paddingTop: 8, fontWeight: 800, WebkitLineClamp: 2 }` is `padding-top: 8px`,
321
+ `font-weight: 800`, `-webkit-line-clamp: 2` — a bare number on a length is pixels. The document keeps kebab-case; one
322
+ property written under both spellings is refused (`css-property-twice`).
323
+ - **One property per breakpoint:** `{ fontSize: { desktop: '24px', compact: '18px' }, fontWeight: 700 }` changes the
324
+ size on tablet and phone without splitting the rule set; it mixes with the per-breakpoint form.
325
+
326
+ ## A link's mode follows from its href
327
+
328
+ `link({ href: '/games/nebula' })` is internal, `link({ href: 'https://…' })` (or `mailto:`, `tel:`) external, and
329
+ `link({ href: 'about' })` a page — `mode` is written only to say otherwise. A link that opens another tab gets
330
+ `rel="noopener noreferrer"`.
331
+
332
+ ## `from` and `as`: an element shows its data in a line
333
+ - **`from`** binds the attribute a type shows its data in — a text's or heading's `content`, an image's `src`, a link's
334
+ `href`, a list's `items` — and leaves it empty until the data answers: `heading({ from: 'site.data.hero.title' })`.
335
+ It writes the same binding `bind` does; `bind` keeps the other attributes.
336
+ - **`as`** shows it through a template (`as: '{{ source }} left'`) or a format the space names once:
337
+ `formats: { price: "{{ source|currency('USD', 'en', { trimZeros: true }) }}" }`, then `as: 'price'` anywhere.
338
+ - **New template filters:** `currency('USD', locale?, { trimZeros })` and `percent(decimals?)`, in `en` unless told,
339
+ so a server and a browser in different locales write the same text.
340
+ - Refused: `from` on a type with no main attribute, the main attribute bound twice, a format nobody declared, `as`
341
+ without `from`.
342
+
343
+ ## A list in one line: `items` and `row`
344
+
345
+ `list({ id: 'grid', items: 'catalog.data.products', row: 'product-card' })` is a controlled list fed by that source,
346
+ placing the component once per item with the row bound to its `item` prop (or its only prop). `row` can also be a
347
+ function handed the row's names — `row: r => text({ from: `${r.item}.title` })`, `r.inTemplate.item` for a template.
348
+ `items` (an array, or a source's name) and `from` make a list controlled without saying so. Refused: a row and
349
+ children at once, a function row on a list with no `id`, a row naming a component that cannot take it.
350
+
351
+ ## `activeWhen`, and a list's index is a number
352
+ - **`activeWhen(dot, '{{ list_dots.index == state.slide }}')`** wears a class's `active` variant while a condition
353
+ holds (`idle` otherwise) — the general form of `activeOn`, without a hand-written ternary.
354
+ - **`list_<id>.index` is a number** from 0, so a template counts with it (`index + 1`). `==` compares text and numbers
355
+ alike, so templates that compared it as text read the same.
356
+
357
+ ## `cycleState` and `stepState`
358
+
359
+ `cycleState({ key: 'slide', length: 4 })` moves a number in state round a cycle — after the last the first, `by: -1`
360
+ before the first the last — and `stepState({ key: 'shown', by: 40, max: 'apiContainer_site.data.total' })` adds and
361
+ stops at its bounds. Both are the `setState` they stand for, with the arithmetic written once; a length or a bound is a
362
+ number or a template expression for one.
363
+
364
+ ## `scope()` for what a helper builds
365
+
366
+ `scope('promos', ref => container({ id: 'panel', … }))` prefixes every `id` given inside it (`promos-panel`), so a
367
+ helper that builds the same block twice writes two sets of names instead of being refused `id-taken`. `ref('slides')` is
368
+ the full name, for a binding, a step's target or a template; scopes nest. `id-taken` now points at it.
369
+
370
+ ## Typed tokens, and `unknown-variable`
371
+ - **`tokens(variables)`** turns the space's variables into the values a rule writes — `t.surface === 'var(--surface)'`
372
+ — so a token that does not exist is a type error where it is written.
373
+ - **`unknown-variable`** warns of a bare `var(--x)` nothing declares (a space variable, a selector's variable, a custom
374
+ property a rule sets, or `customCss`): the browser drops the property and the page shows what it inherits. A
375
+ `var(--x, fallback)` is taken as meant.
376
+
377
+ ## Typed sources: `source()` and `twig`
378
+
379
+ `const site = source('site', home)` names a provider's source from a sample of its answer — the JSON file it reads,
380
+ imported — so every path is completed by the editor and checked: `site.data.hero.titel` is a type error, and refused
381
+ `source-field-unknown` where the types were not looking. A path is the full source name, so it goes in `from`, `items`,
382
+ `bind` and `visible` as it is, and into a template through `` twig`{{ ${site.data.total} + 1 }}` ``. A list fed by a
383
+ path hands its `row` the item typed (`row: g => text({ from: g.item.title })`). Nothing of the sample is written into
384
+ the space; inside a `scope()` the id is the scoped one.
385
+
386
+ ## `tw()`: Tailwind classes as Plitzi styles
387
+
388
+ `styles('pill', tw('inline-flex items-center gap-2 px-5 rounded-full bg-slate-950/90 hover:scale-105 md:text-sm'))`
389
+ writes the rules the classes mean when the space is authored — Tailwind v4's scales and palette, arbitrary values,
390
+ `[property:value]`. Its breakpoints become Plitzi's ranges (`md:` tablet and desktop, `lg:` desktop, `max-md:`,
391
+ `max-lg:`), its states the class's states, `group-hover/<class>:` an ancestor's. Composed properties (transform,
392
+ filter, gradient, ring and shadow) are put together once. `createTw({ colors: tokens(variables) })` names the space's
393
+ tokens. What has no exact equivalent (`sm:`, `dark:`, `space-x-*`, `animate-*`) is refused with what to write instead.
394
+
395
+ ## `create --template catalog`, and a file per part
396
+
397
+ `npx @plitzi/cli create shop --template catalog` writes a complete small site to read and change: a layout with a menu,
398
+ a product card component, `public/data/products.json` read as a typed source, a catalog filtered by category and a page
399
+ per product — each part a short file of its own under `src/site/`, assembled by `src/space.ts`. The template is
400
+ authored by sdk-authoring's tests, and a generated project authors, typechecks and lints clean. The skills point to it,
401
+ and `AGENTS.md` asks for a file per part.
402
+
403
+ ## `embed` and `svg` elements
404
+ - **`embed({ src, title })`** puts another page in a frame — a map, a video player — lazily loaded, with `allow`,
405
+ `sandbox` and `referrerPolicy` when it needs them. Only a web address or a path of the site is loaded, and in the
406
+ builder the frame does not swallow the click that selects it. `embed-without-title` warns of one a screen reader
407
+ cannot describe.
408
+ - **`svg('<svg …>…</svg>', { label })`** draws SVG markup inside a box its class colours (`currentColor`): checked to be
409
+ one `<svg>` (`svg-not-svg` otherwise) and sanitised as rich text is, plus what only SVG can carry (`foreignObject`,
410
+ animations writing a `javascript:` link). Decorative unless it has a `label`, then `role="img"`. `{{ }}` tokens
411
+ resolve in it like in any attribute.
412
+
413
+ ## Pictures resized by the page server
414
+
415
+ `createServer({ images: { domains: ['images.example.com'] } })` resizes other sites' pictures at `/_plitzi/img`: an
416
+ `image` whose `src` is one of them offers a `srcset` from 320 to 1920 px, in AVIF or WebP when the browser takes them.
417
+ Each original is downloaded once and each size made once, both kept on disk; a week on they keep answering while the
418
+ original is revalidated with its `ETag` / `Last-Modified`, so an unchanged picture is never resized again. Only the listed hosts (`*.example.com` for subdomains) are fetched, every redirect is
419
+ held to the list and to the outbound guard, and SVG is refused. `sharp` is an optional peer: without it pictures are
420
+ passed through and kept. `image` also takes `sizes`, and `width`/`height` so the browser keeps the picture's space.
421
+
422
+ ## `plitzi check` says what the page holds
423
+
424
+ Every `plitzi check` now lists the flows that failed while the page loaded, each with the step that failed and why.
425
+ `--state` adds the page's state and every source by its full name (with the shape of what it holds), and `--element
426
+ <id>` one element: what it reads, its own state, how many copies, whether it is on screen and its box. Read from the
427
+ page's dev tools, which keep the flows a page runs before they mount; in a test, `readDevTools(page)` from
428
+ `@plitzi/sdk-authoring` answers the same.
429
+
430
+ ## `carousel`
431
+
432
+ A structure element for slides that change, a marquee and a row that swipes: `carousel({ id: 'hero', items, row,
433
+ autoplay: 5000, children: [arrows, dots] })`. Its `row` is written into a `carouselTrack`, and its other children are
434
+ its controls, reading `carousel_<id>.index`, `.count`, `.item` and `.items`. `mode: 'slide'` shows one at a time
435
+ (`transition` `slide`, `fade` or `none`, back entering from the left); `'marquee'` scrolls the items past at `speed`
436
+ px/s with no seam; `'scroll'` is a snapping row a visitor swipes. Steps `carouselNext`, `carouselPrevious`,
437
+ `carouselGoTo`, `carouselPlay` and `carouselPause`; trigger `onChange`. Autoplay holds still under the pointer, with
438
+ keyboard focus inside, in a hidden tab and for reduced motion, and never in the builder. Slides are announced as "2
439
+ of 5" in a region named by `label`.
440
+
441
+ ## `plitzi fix`: the fixes authoring knows, written in your source
442
+
443
+ `plitzi fix` shows what authoring would fix — a key the element never reads, `'true'` where a boolean goes, a URL in
444
+ page mode, a `state.` prefix on a state key, a binding nothing reads — as a diff of the project's own source: each
445
+ edit made in the call that wrote the element (found by the line and column it remembers), and only where the value is
446
+ a literal. `--write` writes them, formatted as the project formats, authors the space again in a fresh process and
447
+ keeps them only if every fix is gone and no problem was added; one that would add a problem is put back and said with
448
+ the reason. `npm run author` says how many of its problems have one fix. From `@plitzi/sdk-authoring`, `planFixes`
449
+ returns the plan: every problem, and each fix with its place and edit.
450
+
451
+ `fixSpace` now reads an element with the linter's own context, so a component instance's props and slot are no longer
452
+ taken for attributes nobody reads — it used to remove the binding of a list row to its component's `item`.
453
+
454
+ `plitzi import <url>` measures a page you own in the project's Playwright and writes it into the project as a place to
455
+ start from: `tokens.ts` (the page's custom properties by name, then its dominant colours, each with the value the same
456
+ place shows in the dark scheme; repeated corners and shadows; Google fonts), `outline.ts` (`container()`s for its
457
+ landmarks and blocks, with their layout per breakpoint as what each narrower width changes), its repeated lists as
458
+ `data/*.json`, `assets.json`, a screenshot per width and `IMPORT.md`. Never its words. It imports only a site that is
459
+ the person's: a verified domain of one of their spaces covering the host (the `_plitzi` TXT record, asked of the
460
+ platform's new `GET /account/domains/covering`), or one served from this machine; `--out`, `--widths`, `--force`,
461
+ `--json`, `--api`. From `@plitzi/sdk-authoring`: `importProbe` (runs
462
+ in the page), `importedFiles` and `darkScheme`.
463
+
464
+ A compound element without the part it shows its content through — a `carousel` with no `carouselTrack`, a
465
+ `tabContainer` missing its header or body, a `dropdown` with no `dropdownPopup` — is now refused (`part-missing`):
466
+ it rendered nothing of what it held, without a word. The parts are read off the declarations (`elementPartTypes`, the
467
+ `partTypes` catalog), so the builder's issues and publish gate, the MCP's validate and `authorSpace` all hold to it.
468
+ The MCP guide says how a compound element is written.
469
+
470
+ The dev tools catch up. **Elements** shows what is on screen as the trees it comes from — the layouts around the page,
471
+ outermost first, the page, and every component an instance places — at their depth, searched together (a match is kept
472
+ with what holds it), each element outlined on the page while it is pointed at. Selected, its **Runtime** tab is the
473
+ report `window.__plitzi.element()` and `plitzi check --element` give: own state, what it reads, the component it places
474
+ or sits in, copies and box, read again every second. That report now finds an element inside a component, which it
475
+ missed. **Logs** has a `realtime` category: the page's connection opening and dropping, each message in (←) and out
476
+ (→), and every topic the server refused, with why. One hook outlines an element for every tab.
477
+
478
+ A `webHook` step that gets no answer at all — offline, refused, blocked by CORS — now fails, with the request and the
479
+ reason, so the flow stops and its `onFailure` runs. It used to succeed with an empty response, and the flow carried on
480
+ as if the request had been made. Any answer is still the step's result, an error status included: a flow reads
481
+ `{{ <step>.response.status }}` to tell a 401 from a 200. `response.data` is typed as what the body parsed to.
482
+
483
+ The server checks what a deployment's config hands it in one place (`configSeam`): each action document and connector
484
+ manifest a lookup returns goes through the validator the builder saves with, as it is read — an action that is not a
485
+ document is refused by name, and one in a list is left out and said rather than failing the space's schedule — and the
486
+ database drivers and functions config are checked once, as the server starts. Four unchecked casts are gone.
487
+
488
+ `@plitzi/sdk-shared/helpers/eventTarget` reads an event's target as a node (`nodeOf`, `isNodeTarget`) or an element
489
+ (`elementOf`, `isElementTarget`) by its own type rather than `instanceof`, which is false for a target in the builder's
490
+ canvas iframe. The builder, the style inspector and the dev tools use it, and type their change and key handlers by
491
+ the input they listen on; the casts of `event.target` are gone.
492
+
493
+ The builder's flag form checks each rule's `when` is a group of conditions before it saves, rather than passing
494
+ whatever the field held.
495
+
496
+ The CLI's commands now work the same way: the answer on stdout and errors and sign-in prompts on stderr, so `--json`
497
+ is only the answer (`import --json` printed the sign-in prompt into it); exit 1 whenever a command did not do what it
498
+ was asked (`fix --json` with a problem exited 0); and a flag's value checked where the flag is declared, refused with
499
+ what it takes — `--width abc` or `--scheme darkk` used to check at no width or in light, saying nothing. One flag means
500
+ one thing everywhere: `import --widths` is `--width`, like `check`, and `-o`, `-f` and `-e` are the short forms on
501
+ every command that writes, overwrites or names an environment. `import` no longer repeats a sign-in error. `whoami`
502
+ and `runtime status` take `--json` too; `functions` and `runtime push` refuse outside a project, as every other command
503
+ that works on one does, instead of writing `functions/` into whatever folder they were run from; and every command
504
+ says a finished action the same way, in green.
505
+
506
+ The SDK's production build keeps `console.warn` and `console.error`, and drops only `log`, `info` and `debug`. It used
507
+ to drop them all: an override of a flag the space does not declare, or a render that failed, said nothing on a
508
+ published site — exactly where nobody can attach a debugger.
509
+
510
+ A link to the page being shown says so: it carries `aria-current="page"` — read from the address the page was rendered
511
+ at, so the first paint has it — and a class dresses it with the new `current` style state (`states: { current: … }`,
512
+ a tab in the style editor, folded from `[aria-current="page"]` rules when a space is exported). A site's header now
513
+ goes in a layout once, instead of a copy per page to mark the right navigation item.
514
+
515
+ `notifications` dresses the whole toast, not only its colours: `font`, `fontSize`, `border`, `shadow` and `padding`
516
+ join `radius` — the library's variables where it has them, a rule on the toast where it has none. A space no longer
517
+ writes `.Toastify__toast { … }` into its `customCss` for them.
518
+
519
+ A visitor whose machine asks for less motion gets it on every space: the SDK's base layer cuts animations and
520
+ transitions to an instant (each still ends where it would) and turns smooth scrolling off. Spaces used to copy that
521
+ rule into their own custom CSS, and one that did not moved anyway.
522
+
523
+ Authoring suggests, beside what it refuses and warns: `authorSpace` returns `suggestions` — a shorter way to the same
524
+ page, each with its code, the elements it is about and how many it would save, the largest first. The same header in
525
+ every page is a layout (`repeated-on-pages`, which also names the `current` state when the copies differ only in the
526
+ active link), one structure copied with other words a component or a list (`repeated-shape`), a `text` alone inside a
527
+ button or a link the element's own `content` (`content-attribute`), and `customCss` that a class's states, the SDK or
528
+ `notifications` already say (`custom-css-*`). Copies are compared by what they read too — a block reading its own
529
+ provider is still one block, two pagers over two lists are not — a block only some pages of a layout carry is offered as
530
+ a component rather than a layout of its own, and a text that is a shape drawn inside a button is left alone.
531
+ `suggestSpace({ schema, style })` gives them for any document; they never block. `plitzi_validate` and `plitzi_apply`
532
+ answer with the ones a batch opened up, `npm run author` prints them under the warnings (and `--json` carries them), and
533
+ the authoring skill's new `reference/efficiency.md` teaches the short way first.
534
+
535
+ A `link` has words of its own: `content`, drawn before or after its children (`contentPlacement`), as a button's — a
536
+ link with only a label no longer needs a `text` inside it.
537
+
538
+ A style that writes its rules beside `states`, `variants` or `ancestors` — `{ desktop: { … }, ancestors: { … } }` — is
539
+ refused with what to write (`rule-set-mixed`: the rules go under `css`), instead of reporting `desktop: [object Object]`
540
+ as a CSS value it could not read.
541
+
542
+ `plitzi explain` and `plitzi://explain` name a suggestion's code `suggested`, with its short way, instead of calling it
543
+ `warned`. The MCP server's guide teaches the short way first — a link's own `content`, the `current` state for the link
544
+ to the page being shown, a component for a block only some pages of a layout carry — and how to read the
545
+ `suggestions` `plitzi_validate` and `plitzi_apply` answer with.
546
+
547
+ The MCP server dresses the notifications too: `patchSettings { notifications: { background, border, … } }`, merged
548
+ field by field (`null` removes one), and `plitzi://settings` reads them apart from the space's own `customCss` — the
549
+ same rule authoring writes for a space's `notifications`, kept when either changes. The builder's Export gives that
550
+ rule back as `notifications` instead of leaving it in `customCss` (`splitNotificationsCss`, `withNotificationsCss`).
551
+
552
+ A `button` and a `link` draw an icon beside their words: `icon` (Font Awesome classes, `'fa-solid fa-arrow-right'`) and
553
+ `iconPlacement` (`before` or `after` the words), dressed through a new `icon` slot; it takes the size of the words and
554
+ no space of its own — the box's `gap`, or a margin on the slot, separates them. One element instead of the element, a
555
+ `text` and a `fontAwesome`; alone, with a `title`, it is an icon button. The builder offers the icon picker in both
556
+ elements' settings (shared now with `fontAwesome`'s), and the `content-attribute` suggestion points at a plain
557
+ `fontAwesome` beside the words too.
558
+
559
+ The builder lists the suggestions: `SpaceIssues` answers `suggestions` beside `errors` and `warnings`, the problems
560
+ panel shows them last — the short way, how many elements it saves, and each element it is about, a link to it — and
561
+ the header's issues button, with nothing wrong, shows a light bulb and how many there are.
562
+
563
+ The builder edits the notifications' look in the space settings (Notifications: surface, text, accents, radius, font,
564
+ size, border, shadow, padding), checked as it is typed; the custom CSS editor shows the space's own CSS without the
565
+ rule they are stored as, and keeps it. The mechanism moved to `@plitzi/sdk-shared/style/notifications`
566
+ (`notificationsCss`, `notificationsProblem`, `splitNotificationsCss`, `withNotificationsCss`); `@plitzi/sdk-authoring`
567
+ re-exports it, refusing a spec that is not sound as before.
568
+
569
+ ## Motion that waits for the page to wake
570
+
571
+ The SDK's root carries `data-hydrated` once the page is hydrated, so a space can hold decorative motion that runs on
572
+ the main thread — a custom property, a `background-position`, a `top` — until then, when it would stutter behind the
573
+ hydration's long tasks: `animation-play-state: paused` on the element, and `[data-hydrated] .x { animation-play-state:
574
+ running }`. Opacity and transform animations run on the compositor and need no gate.
575
+
576
+ Hydration is lighter on the way: the fonts' Google URLs sort their families by code point instead of `localeCompare`,
577
+ whose first call built a collator in the middle of the page's first render, and twig's compiled-template cache holds
578
+ 1024 templates instead of a number a large space outgrew, compiling the same ones again on every render.
579
+
580
+ Good practices for motion, said wherever a space is written: `docs/en/motion.md`, the authoring skill (a rule in
581
+ `SKILL.md` and `reference/colours-and-motion.md`), the MCP server's guide and quickstart, and the website's styling
582
+ docs — animate `opacity` and `transform`, never blur, a shadow or a size; fake the expensive ones with the cheap ones;
583
+ hold main-thread decoration until `data-hydrated`; short entrances, one slow loop per screen, no `transition: all`.
584
+
585
+ `heavy-animation`, a new suggestion: keyframes that something runs and that animate a size or a position, a blur or a
586
+ shadow — or, in a loop, a colour, a gradient or a custom property — are named with each property and the way out of
587
+ its cost. A loop that starts `paused` and runs under `[data-hydrated]` is let through, a property that only switches
588
+ (`visibility`) is not counted, and keyframes nothing runs are not mentioned. It reaches `authorSpace`, `npm run
589
+ author`, `plitzi_validate`/`plitzi_apply`, `plitzi explain` and the builder's problems panel like every suggestion. The
590
+ stylesheet scanner `customCss` folding used is shared now (`style/stylesheet`), and reads at-rules' blocks.
591
+
592
+ `authorSpace` suggests `heavy-animation` for keyframes something runs off the compositor — a size, a position, a
593
+ blur, a shadow every time; a colour, a gradient or a custom property in a loop — naming each property and the way out
594
+ of its cost. A loop that starts `paused` and runs under `[data-hydrated]` is let through; a property that only
595
+ switches (`visibility`) is not counted. It reaches `plitzi_validate`, `npm run author` and the builder's problems
596
+ panel like every suggestion, and never blocks. The scanner `customCss` is read with is shared now
597
+ (`style/stylesheet`), so the fold and this read the same segments.
598
+
599
+ `@plitzi/plitzi-ui` 1.6.29: every field is labelled by its `label` and described by its error message, the code
600
+ editor included.
601
+
602
+ ## A layout grid over the canvas
603
+
604
+ The builder's header has a layout grid switch beside the element outlines: the columns a page is laid out on, drawn
605
+ over the canvas — twelve on a desktop, eight on a tablet, four on a phone, with their gutters and margins — so an
606
+ element is lined up by eye with the rest of the page — switching at the page's own breakpoints. It follows the canvas
607
+ zoom, lets every click through, and is remembered between visits like the outlines.
608
+
609
+ ## A QA tab in the dev tools
610
+
611
+ The dev tools — wherever debugging is authorized, a pre-production deployment included — have a **QA** tab for whoever
612
+ checks a build in the browser it will be used in. A bar of tools over the page: an **inspector** — point at any element
613
+ for its box model, type, colours and their contrast; click to keep it in the tab with its computed box, type, classes
614
+ and every CSS rule that reaches it as written (copyable); hold Alt over another to measure the distance between them —
615
+ the builder's layout grid, every element's outline, the order the Tab key walks the controls in, the viewport's size
616
+ and the breakpoint showing, animations paused, the page with reduced motion, and the page as seen without one kind of
617
+ colour, in grayscale or out of focus. Beside it, six checks that outline what they find and scroll to it: whatever
618
+ sticks out past the page's sides, controls and pictures with no name, text under AA contrast, pictures stretched past
619
+ their pixels or far bigger than shown, a heading outline with no h1 or a skipped level, and touch targets under 24 × 24
620
+ px with another too close (WCAG 2.2). The tab lays the checks and the inspector side by side when the panel is wide.
621
+ Folding the panel away stops the inspector and the checks; the views — grid, outlines, vision — stay, in this browser.
622
+ The tab is offered where `DevToolsContainer` is given `qa` — the SDK does, over its page; an application shell such as
623
+ the builder does not, so its own UI is never walked by the checks. Its settings are kept under a key of their own
624
+ (`plitzi-sdk-dev-tools-qa`), the checks run when the page is idle, and the tab order is worked out again only when the
625
+ page changes.
626
+
627
+ The layout grid and the breakpoints are one definition in `@plitzi/sdk-shared/style` (`LAYOUT_GRIDS`,
628
+ `layoutGridLook`, `layoutGridCss`, `DISPLAY_MODE_MIN_WIDTH`, `displayModeAt`), which `@plitzi/sdk-style` compiles its
629
+ media queries from and the builder's grid draws with. The SDK's stylesheet applies its reduced-motion rule under the
630
+ class `plitzi-reduced-motion` on the document too, from the same mixin as the media query.
631
+
632
+ - Updated dependencies [8d1cc02]
633
+ - @plitzi/sdk-auth@0.37.10
634
+ - @plitzi/sdk-dev-tools@0.37.10
635
+ - @plitzi/sdk-elements@0.37.10
636
+ - @plitzi/sdk-event-bridge@0.37.10
637
+ - @plitzi/sdk-interactions@0.37.10
638
+ - @plitzi/sdk-navigation@0.37.10
639
+ - @plitzi/sdk-plugins@0.37.10
640
+ - @plitzi/sdk-schema@0.37.10
641
+ - @plitzi/sdk-shared@0.37.10
642
+ - @plitzi/sdk-style@0.37.10
643
+ - @plitzi/sdk-variables@0.37.10
644
+
3
645
  ## 0.37.9
4
646
 
5
647
  ### Patch Changes