@plitzi/plitzi-sdk 0.37.8 → 0.37.10

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