@plitzi/sdk-interactions 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,838 @@
|
|
|
1
1
|
# @plitzi/sdk-interactions
|
|
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-event-bridge@0.37.10
|
|
592
|
+
- @plitzi/sdk-navigation@0.37.10
|
|
593
|
+
- @plitzi/sdk-schema@0.37.10
|
|
594
|
+
- @plitzi/sdk-shared@0.37.10
|
|
595
|
+
|
|
596
|
+
## 0.37.9
|
|
597
|
+
|
|
598
|
+
### Patch Changes
|
|
599
|
+
|
|
600
|
+
- 3ae61a4: ## Components replace segments
|
|
601
|
+
|
|
602
|
+
- **What a component is:** a reusable block written once and placed anywhere as an instance. An edit to the component
|
|
603
|
+
is an edit to every instance.
|
|
604
|
+
- **Where it lives:** in the space document, as `schema.components`, one tree per component and never in
|
|
605
|
+
`schema.flat`. It is published, rolled back, copied by templates and exported with the space.
|
|
606
|
+
- **Props and slots:**
|
|
607
|
+
- A component declares props (`type`, `description`, `required`, `default`, `options`). An instance hands them in
|
|
608
|
+
as its own attributes, so templates and bindings reach them. Inside, they are read as `{{ props.<name> }}`.
|
|
609
|
+
- Slots are elements an instance fills; each child names its slot in `attributes.slot`.
|
|
610
|
+
- **A prop an instance leaves out prints nothing.** It is its `default`, or `null`. `props` is a settled source:
|
|
611
|
+
- `processTwig` takes `{ settled }` in place of `keepEmptyTokens: true`.
|
|
612
|
+
- Every other empty token is still kept for a later pass.
|
|
613
|
+
- `COMPONENT_PROPS_SOURCE` names the source.
|
|
614
|
+
- **Fixed: a flow in a list row or a component instance acts on its own copy.** A step's target resolves in the
|
|
615
|
+
replica the flow fired in first, then outwards. It used to reach whichever copy registered last.
|
|
616
|
+
- **Fixed: an instance's own `visible` hides it in preview.** The same goes for an element reference.
|
|
617
|
+
- **Fixed: a list row keeps its state with its record.** Rows are keyed by each record's unique `id`, so filtering no
|
|
618
|
+
longer moves one row's state onto another.
|
|
619
|
+
- **Closed scope:** inside, a component reads only its props and the globals. The validator, `lintSpace` and
|
|
620
|
+
`authorSpace` each refuse a read of the page around an instance. Components nest, and a cycle is refused.
|
|
621
|
+
- **Where to use them:**
|
|
622
|
+
- In code (`@plitzi/sdk-authoring`): `SpaceSpec.components` and `component(id, { props, children })`.
|
|
623
|
+
`specFromSpace` and `specToSource` read and write both.
|
|
624
|
+
- In the builder:
|
|
625
|
+
- a Components panel lists them, and an open component becomes the canvas;
|
|
626
|
+
- an element becomes a component with **Save as component**;
|
|
627
|
+
- in an instance's settings, an instance gets its props and slots, or is **detached** back into a copy.
|
|
628
|
+
- In the MCP: `upsertComponent` and `deleteComponent`. Element ops work inside a component through `pageRef`.
|
|
629
|
+
- **Schema helpers:** `@plitzi/sdk-schema` gains `addComponent`, `updateComponent`, `removeComponent`,
|
|
630
|
+
`detachInstance`, `renameElement`, `treeOf`, `flatMapOf` and `documentIds`.
|
|
631
|
+
- **GraphQL and live events:** `SpaceAddComponent`, `SpaceUpdateComponent`, `SpaceRemoveComponent` and
|
|
632
|
+
`SpaceDetachInstance`, each with a live event. History records a declaration change as a `component` entry.
|
|
633
|
+
- **Breaking: segments are removed.** This covers:
|
|
634
|
+
- `@plitzi/sdk-shared`'s segment types, queries, mutations, context and `SEGMENT_*` events;
|
|
635
|
+
- `referenceType: 'segment'`;
|
|
636
|
+
- the `Segments` builder module;
|
|
637
|
+
- `Space.segments` and the `Segment`/`Segments` queries;
|
|
638
|
+
- `CommonState.prevSchema`.
|
|
639
|
+
- **Breaking: `ElementLayout` and `LayoutBody` change shape.**
|
|
640
|
+
- `ElementLayout` is `{ slots, rootId, type }`; it was `{ containerId }`.
|
|
641
|
+
- `LayoutBody` takes `bodies` keyed by slot.
|
|
642
|
+
- `reference`'s `referenceContainer` attribute is removed.
|
|
643
|
+
- Guide: `docs/en/components.md`.
|
|
644
|
+
|
|
645
|
+
## Functions ask for the time they need
|
|
646
|
+
- **What changes:** a task can ask for more CPU or wall time than the default with `limits`, in milliseconds. Example:
|
|
647
|
+
`limits: { cpuMs: 1000, wallMs: 20_000 }`. `defineFunctions({ limits })` asks it for every task and route at once,
|
|
648
|
+
and a task's own limits win over those.
|
|
649
|
+
- **What a run is given:** what it asked for, or the default (100 ms of CPU, 10 s) when it asked for nothing. Never
|
|
650
|
+
above the space's plan or the deployment's ceiling.
|
|
651
|
+
- **Deployment ceilings:** `functions.limits` sets them. `DEFAULT_FUNCTION_CEILINGS` covers each unset one, at 2 s of
|
|
652
|
+
CPU and 30 s.
|
|
653
|
+
- **Asking for more than the ceiling** is a problem when the functions are saved; it is never quietly cut down.
|
|
654
|
+
- **Manifest:** carries what each task asked for, and the builder shows it beside the task.
|
|
655
|
+
|
|
656
|
+
## Functions answer under `/fn`, not `/api`
|
|
657
|
+
|
|
658
|
+
A space's routes are served at `/fn/<path>` (`FUNCTION_ROUTES_PREFIX`). `/api` is a slug a site wants for a page of its
|
|
659
|
+
own. A page under `/fn` is refused instead (`page-route-reserved`).
|
|
660
|
+
|
|
661
|
+
## The builder's Functions panel
|
|
662
|
+
|
|
663
|
+
Rebuilt around the code. The panel reads `defineFunctions` as it is typed and writes into it, so the code stays the one
|
|
664
|
+
place a function is declared.
|
|
665
|
+
|
|
666
|
+
- **Layout:** the tasks, routes and files on the left, the code in the middle, the selected task on the right.
|
|
667
|
+
- **Live list:** tasks and routes are listed as the code declares them, including tasks imported from another file.
|
|
668
|
+
A task you have written but not saved says so. Tasks built by calling something are counted, and listed once saved.
|
|
669
|
+
- **Code and panel follow each other:** clicking a task or a route opens its file at its line. Putting the cursor inside
|
|
670
|
+
a task's code selects that task.
|
|
671
|
+
- **New task:** + in Tasks asks for its namespace, action and title. The task is written into `defineFunctions` with a
|
|
672
|
+
`run` to start from, in the file's own quotes, and the editor opens on it.
|
|
673
|
+
- **Time limit:** each task gets a slider from 100 ms to 1 s of CPU per run, with presets. The value is written into
|
|
674
|
+
the task as `limits: { cpuMs }`. "Use default" takes it out. `DEFAULT_FUNCTION_TIME_LIMITS` in
|
|
675
|
+
`@plitzi/sdk-shared/actions` is the default both the panel and the server use.
|
|
676
|
+
- **Test:** fills a task's params the way its step does, from their defaults: a select, a switch or text. JSON stays a
|
|
677
|
+
toggle away. With unsaved changes, the button reads "Save & run": it saves, then runs. If the save fails, it says why.
|
|
678
|
+
- **Header:** shows Saved, Unsaved (and in how many files) or the number of problems the last save found. ⌘S saves.
|
|
679
|
+
**Discard** asks first, then puts every file back to what was last saved, or back to nothing for functions never
|
|
680
|
+
saved. Removing the functions is a quiet button beside Save.
|
|
681
|
+
- **Problems:** clicking one opens its file at its line.
|
|
682
|
+
- **Editor:** each file has its own editor and undo history. Opening another file no longer marks the one you left as
|
|
683
|
+
changed. Before this, it could also write the newly opened file's text into the one you left. The cause was in
|
|
684
|
+
`@plitzi/plitzi-ui`'s CodeMirror, fixed in 1.6.25, which every package now depends on. That release also sets code
|
|
685
|
+
editors (several lines) in a monospaced face again. Long lines scroll inside the editor, and the line numbers stay
|
|
686
|
+
in place.
|
|
687
|
+
|
|
688
|
+
## A space's own functions are their own category of steps
|
|
689
|
+
|
|
690
|
+
In the action editor's step picker, the space's own functions are listed under **Functions**, apart from the
|
|
691
|
+
platform's **Tasks**. The other headings now read Callbacks, Global callbacks and Utilities. The saved step is still a
|
|
692
|
+
`task` node.
|
|
693
|
+
|
|
694
|
+
- `@plitzi/sdk-server`: every registered task has an `origin`, `'deployment'` (shipped with the server or a native
|
|
695
|
+
function) or `'space'` (from the space's functions). `describeCatalog`, `/_action/catalog` and the builder's
|
|
696
|
+
`SpaceActionTasks` carry it (`ActionTaskDescriptor.origin`).
|
|
697
|
+
- `@plitzi/sdk-shared`: an `InteractionCallback` may name the `group` the picker lists it under.
|
|
698
|
+
|
|
699
|
+
## Fixed: preview no longer breaks a builder that is embedded in a page
|
|
700
|
+
|
|
701
|
+
When the builder is mounted inside a Plitzi page (the platform's `/spaces/:id/update`), going to preview with a page
|
|
702
|
+
that has SEO turned on left the builder unstyled. The previewed page wrote its title and description through a head
|
|
703
|
+
manager of the builder's own, and that manager rewrote the host page's head, removing the builder's own stylesheet.
|
|
704
|
+
|
|
705
|
+
- `@plitzi/sdk-shared`: a new render setting, `ownsHead`, says whether the page may write the document head. It is on
|
|
706
|
+
by default (`DEFAULT_RENDER_SETTINGS`).
|
|
707
|
+
- `@plitzi/sdk-elements`: `Page` writes its SEO only where `ownsHead` is on.
|
|
708
|
+
- Builder: sets `ownsHead: false` for its canvas. A page drawn there is in a frame, and the document head is the
|
|
709
|
+
editor's. The canvas's own `HelmetProvider` is gone.
|
|
710
|
+
|
|
711
|
+
## Fixed: the builder's plan usage panel shows the space being edited
|
|
712
|
+
- **The 404 is gone.** Opening Plan usage said "The account breakdown could not be read (The server answered 404.)".
|
|
713
|
+
The panel asked `/account/usage`, which went away when accounts became workspaces.
|
|
714
|
+
- **Only this space.** The panel listed every space of the workspace. It now reads `/spaces/:spaceId/usage`, which
|
|
715
|
+
now also answers the space's own `pages` (the heaviest ten), `pagesTotal` and `periodEndsAt`. The plan's ceilings
|
|
716
|
+
stay on top. Anyone who can edit the space can read it, including a guest of another workspace.
|
|
717
|
+
- **It scrolls.** A long breakdown scrolls inside the modal, where it used to overflow.
|
|
718
|
+
- **`getKeyDecoded(webKey, true)` reads the token's subject.** The function, in `@plitzi/sdk-shared`, looked for
|
|
719
|
+
`data.spaceId`, which space tokens no longer carry, so every space decoded as 0. The builder asked about space 0, and
|
|
720
|
+
the builder and the SDK kept every space's persisted state under the same key on a host. Pages served without a
|
|
721
|
+
`webKey` (SSR) still decode as 0, which is what their painted-state cookie is named after.
|
|
722
|
+
|
|
723
|
+
## Fixed: a remote plugin in the builder reads the canvas it sits in
|
|
724
|
+
|
|
725
|
+
The builder carries its own copy of the Plitzi runtime. A remote plugin imports `@plitzi/plitzi-sdk`, which the page's
|
|
726
|
+
import map resolves to the SDK's copy. Each copy made its own React contexts, so on the builder's canvas a plugin read
|
|
727
|
+
none of the canvas's providers. In the builder embedded in Plitzi's site, it read the site around the builder instead:
|
|
728
|
+
the site's element as its own, the site's live mode (so it stayed interactive while being edited), and the site's store
|
|
729
|
+
and interactions.
|
|
730
|
+
|
|
731
|
+
- `@plitzi/sdk-shared` gains `sharedContext(name, default)`: a context made once per page and handed to every copy of
|
|
732
|
+
the runtime that asks for it.
|
|
733
|
+
- The runtime's contexts now go through it, so a provider from any copy reaches a consumer from any copy:
|
|
734
|
+
- `@plitzi/sdk-shared`: service, component, schema, network, theme scope, dev tools, builder.
|
|
735
|
+
- `@plitzi/sdk-elements`: element, element parent, layout body.
|
|
736
|
+
- interactions, plugins, event bridge, auth, variables and style.
|
|
737
|
+
- The store's contexts need `@plitzi/nexus` 1.4.0, which does the same. Every `@plitzi/*` package now asks for
|
|
738
|
+
`^1.4.0`, which is published.
|
|
739
|
+
|
|
740
|
+
## The source of plugins and runtimes is kept on the space
|
|
741
|
+
|
|
742
|
+
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
|
|
743
|
+
(`plitzi create --from`, below).
|
|
744
|
+
|
|
745
|
+
- `@plitzi/cli`:
|
|
746
|
+
- `plitzi pack plugin` writes the plugin's source beside its zip: every file of the project its elements import,
|
|
747
|
+
`import type` included, and the packages they need. `--source-root` names the project those paths are relative to.
|
|
748
|
+
- `plitzi upload plugin` and `plitzi runtime push` keep that source on the space, in its private bucket.
|
|
749
|
+
- `plitzi pack source` writes it to a file.
|
|
750
|
+
- A source that cannot be kept (a file outside the project, an undeclared package, a credential in the code) never
|
|
751
|
+
stops the upload or the push: they say why, and the artifact is kept built only.
|
|
752
|
+
- `@plitzi/sdk-shared/source`: the snapshot's format, the paths it may hold and the credential check, shared by the CLI
|
|
753
|
+
and the platform.
|
|
754
|
+
|
|
755
|
+
## `plitzi create --from` and `plitzi pull`: a space on Plitzi as a project of your own
|
|
756
|
+
|
|
757
|
+
The way back from everything the CLI puts on Plitzi. `plitzi create my-board --from pizarra` writes a server project
|
|
758
|
+
holding what the space is made of, and runs it with nothing of Plitzi's: neither its servers nor its CDN. `plitzi pull`
|
|
759
|
+
keeps it in step with the space. See `docs/en/projects-from-spaces.md`.
|
|
760
|
+
|
|
761
|
+
- **What lands in the project:**
|
|
762
|
+
- its pages as authoring code;
|
|
763
|
+
- its server actions as `defineAction` code — JSON, with the reason said, for one that would not read back exactly;
|
|
764
|
+
- its plugins and runtime as the source they were uploaded from, and its functions;
|
|
765
|
+
- its files, downloaded into `public/`, with every CDN address rewritten to the project's.
|
|
766
|
+
- **`src/main.ts`** serves all of it, the runtime in the same process.
|
|
767
|
+
- **`.env`** gets a key made for the project's actions to sign with, and the names of the variables and credentials
|
|
768
|
+
the space had. Their values stay on Plitzi.
|
|
769
|
+
- **Who may run it:** the person must be signed in and able to change the space (owner, administrator or writer).
|
|
770
|
+
- **Plugins** are rebuilt against the project's SDK. One uploaded before sources were kept runs as it was built, from
|
|
771
|
+
`vendor/plugins/`, and the report says to upload it again.
|
|
772
|
+
- **The end of `create`** says what came across differently, including a space whose visitors sign in with Plitzi.
|
|
773
|
+
- `--source cloud` keeps the pages on Plitzi and runs the rest locally.
|
|
774
|
+
- **Any version:** `--environment` and `--revision` take out a published snapshot — its latest, or one revision pinned
|
|
775
|
+
— instead of the draft, with the source its plugins and runtime were built from then. A cloud project serves that
|
|
776
|
+
revision pinned.
|
|
777
|
+
- **`plitzi pull`** writes what changed on the space, keeps what changed in the project, and writes nothing when a
|
|
778
|
+
file changed on both — naming them, with `--force` to take the space's copy. It never touches `.env`, only adds to
|
|
779
|
+
`package.json`, and keeps `plitzi functions push` working from the project. It follows the version the project was
|
|
780
|
+
made from, and `--environment`/`--revision` move it.
|
|
781
|
+
- `@plitzi/sdk-authoring`:
|
|
782
|
+
- `actionSpecFromEntry` reads an action document back into its `defineAction` declaration, only when the round trip
|
|
783
|
+
is exact, and `actionToSource` writes it as a module.
|
|
784
|
+
- `defineAction` takes `limits`.
|
|
785
|
+
- `specToSource` takes `importExtension: '.ts'`, for split files that Node imports as they are.
|
|
786
|
+
|
|
787
|
+
## The builder shows what a snapshot holds
|
|
788
|
+
|
|
789
|
+
**Make Snapshot** lists what it will freeze — pages, layouts and elements, server actions, connectors, functions, the
|
|
790
|
+
runtime and the plugins, each with whether its source is kept — and what no snapshot freezes: the space's files, its
|
|
791
|
+
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.
|
|
792
|
+
|
|
793
|
+
## Fixed: a space read from Plitzi kept its server elements
|
|
794
|
+
- **What happened:** a page server reading its space from Plitzi (`createCloudAdapters`) never ran an element's
|
|
795
|
+
`render` action, and a browser-rendered space ignored `loadStrategy`. The space's GraphQL answered neither
|
|
796
|
+
`runtime` nor `loadStrategy` of an element, nor the space's `rsc` settings.
|
|
797
|
+
- **Now:** the platform answers them, and the SDK, the builder and the cloud adapters ask for them.
|
|
798
|
+
|
|
799
|
+
## A plugin the deployment registers is not looked for elsewhere
|
|
800
|
+
|
|
801
|
+
`@plitzi/sdk-server` used to fetch the manifest of every plugin the space lists on its CDN, even one the deployment
|
|
802
|
+
registers itself, and logged a warning when the CDN was out of reach. It now asks only for the ones it does not have.
|
|
803
|
+
|
|
804
|
+
## Fixed: inline code in Markdown carries nothing of the syntax tree
|
|
805
|
+
|
|
806
|
+
A `markdown` element wrote every inline `` `code` `` as `<code node="[object Object]">`: `react-markdown` hands its
|
|
807
|
+
renderers the syntax-tree node as a prop, and the inline branch spread it onto the tag. Fixed in
|
|
808
|
+
`@plitzi/plitzi-ui` 1.6.26 (with a test), which every package now asks for.
|
|
809
|
+
|
|
810
|
+
## Dev tools hear about the render run the page stopped waiting for
|
|
811
|
+
|
|
812
|
+
When a server element's action ran past the section's budget, the page was answered without it. The run ended a moment
|
|
813
|
+
later, after the page's runs had been sent, so the dev tools never showed the one run that needed debugging. It is now
|
|
814
|
+
told the moment the page stops waiting, as `aborted`, with the reason.
|
|
815
|
+
|
|
816
|
+
## Fixed: a tab you come back to no longer loses its session
|
|
817
|
+
- **What happened:** a page left in another tab past its access token's life signed its visitor out on return. A
|
|
818
|
+
reload put them right back in.
|
|
819
|
+
- **Why:** the browser drops the cookie carrying the access token the moment the token expires, and the background
|
|
820
|
+
tab's renewal timer had not run. The first check on return was told `missing`, which the client took as no
|
|
821
|
+
session at all.
|
|
822
|
+
- **Now:** a `missing` refusal is renewed instead whenever the browser can still renew, meaning it holds a refresh
|
|
823
|
+
token or a session hint whose renewal window is open. The session ends only if that renewal fails.
|
|
824
|
+
- **Requests made in that moment:** `reportAuthFailure` now answers whether it renewed the session. A read refused
|
|
825
|
+
as the tab came back is asked again, once, once the session is renewed. This covers an api container's read and a
|
|
826
|
+
server section's refresh, so they no longer show a 401 that only a reload cleared.
|
|
827
|
+
|
|
828
|
+
- Updated dependencies [3ae61a4]
|
|
829
|
+
- @plitzi/sdk-auth@0.37.9
|
|
830
|
+
- @plitzi/sdk-dev-tools@0.37.9
|
|
831
|
+
- @plitzi/sdk-event-bridge@0.37.9
|
|
832
|
+
- @plitzi/sdk-navigation@0.37.9
|
|
833
|
+
- @plitzi/sdk-schema@0.37.9
|
|
834
|
+
- @plitzi/sdk-shared@0.37.9
|
|
835
|
+
|
|
3
836
|
## 0.37.8
|
|
4
837
|
|
|
5
838
|
### Patch Changes
|
|
@@ -34,6 +34,14 @@ declare class InteractionsManager {
|
|
|
34
34
|
private getSubscriptorInternal;
|
|
35
35
|
getSubscriptor(subscriptorId: string): Subscriptor | undefined;
|
|
36
36
|
private getCallbacksAvailablesInternal;
|
|
37
|
+
/**
|
|
38
|
+
* Every callback a flow fired here can reach, by the id of the element it belongs to.
|
|
39
|
+
*
|
|
40
|
+
* A replica — a list row, a component instance — renders the same ids as every other copy of it, so an id alone
|
|
41
|
+
* names several elements. The one a flow means is the nearest: in the replica the flow fired in, else in the
|
|
42
|
+
* replicas around it, else anywhere on the page. Merged the other way, the copy that registered last answered for
|
|
43
|
+
* all of them, and a card's "Details" opened another card's panel.
|
|
44
|
+
*/
|
|
37
45
|
getCallbacksAvailables(): {
|
|
38
46
|
[x: string]: Record<string, InteractionCallback> | Record<string, InteractionCallback<any>>;
|
|
39
47
|
};
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { flowTrigger as e } from "./InteractionsHelper.mjs";
|
|
2
2
|
import { get as t, set as n } from "@plitzi/plitzi-ui/helpers";
|
|
3
3
|
import r from "@plitzi/sdk-event-bridge";
|
|
4
|
-
import {
|
|
4
|
+
import { INTERVAL_TRIGGER as i, intervalOf as a } from "@plitzi/sdk-shared/helpers/interval";
|
|
5
|
+
import { KEY_TRIGGER as o } from "@plitzi/sdk-shared/helpers/keys";
|
|
5
6
|
//#region src/InteractionsManager.ts
|
|
6
|
-
var
|
|
7
|
-
if (e.action
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
7
|
+
var s = (e, t) => {
|
|
8
|
+
if (e.action === o) {
|
|
9
|
+
let { shortcuts: n } = t;
|
|
10
|
+
return Array.isArray(n) && n.includes(e.params.keys);
|
|
11
|
+
}
|
|
12
|
+
return e.action !== i || a(e.params.interval) === t.interval;
|
|
13
|
+
}, c = class i {
|
|
11
14
|
eventBridge;
|
|
12
15
|
parentManager;
|
|
13
16
|
childManagers;
|
|
@@ -24,15 +27,15 @@ var a = (e, t) => {
|
|
|
24
27
|
...n
|
|
25
28
|
}, this.lastUpdate = Date.now();
|
|
26
29
|
}
|
|
27
|
-
eventBridgeCallback = (n) => async (r, i,
|
|
30
|
+
eventBridgeCallback = (n) => async (r, i, a) => {
|
|
28
31
|
if (!n || !i || !r) return;
|
|
29
|
-
let
|
|
30
|
-
if (await Promise.resolve(), !
|
|
32
|
+
let o = this.subscriptors[r];
|
|
33
|
+
if (await Promise.resolve(), !o || this.subscriptors[r] !== o) return;
|
|
31
34
|
let c = t(this.subscriptors, `${r}.getAdditionalParams`, void 0), l = () => ({
|
|
32
35
|
...this.interactionsData,
|
|
33
36
|
...typeof c == "function" ? c().dataSource : void 0
|
|
34
|
-
}), u = Object.values(n).filter((e) => e.type === "trigger" && e.action === i && e.enabled &&
|
|
35
|
-
await Promise.all(u.map((t) => this.runFlow(`${r}.${t.id}`, t.whileRunning ?? "skip", () => e(t, n, this.getCallbacksAvailables(), { [t.id]:
|
|
37
|
+
}), u = Object.values(n).filter((e) => e.type === "trigger" && e.action === i && e.enabled && s(e, a));
|
|
38
|
+
await Promise.all(u.map((t) => this.runFlow(`${r}.${t.id}`, t.whileRunning ?? "skip", () => e(t, n, this.getCallbacksAvailables(), { [t.id]: a }, l, r))));
|
|
36
39
|
};
|
|
37
40
|
runFlow(e, t, n) {
|
|
38
41
|
let r = this.flowsRunning.get(e);
|
|
@@ -101,7 +104,18 @@ var a = (e, t) => {
|
|
|
101
104
|
};
|
|
102
105
|
}
|
|
103
106
|
getCallbacksAvailables() {
|
|
104
|
-
|
|
107
|
+
if (!this.parentManager) return this.getCallbacksAvailablesInternal();
|
|
108
|
+
let e = [];
|
|
109
|
+
for (let t = this.parentManager; t; t = t.parentManager) e.unshift(t);
|
|
110
|
+
let t = this.getRootManager()?.getCallbacksAvailablesInternal() ?? {};
|
|
111
|
+
for (let n of e) t = {
|
|
112
|
+
...t,
|
|
113
|
+
...n.callbacksAvailables
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
...t,
|
|
117
|
+
...this.getCallbacksAvailablesInternal()
|
|
118
|
+
};
|
|
105
119
|
}
|
|
106
120
|
interactionTrigger(e, t, n = {}) {
|
|
107
121
|
if (e) return this.eventBridge.emit("interaction", e, e, t, n);
|
|
@@ -125,4 +139,4 @@ var a = (e, t) => {
|
|
|
125
139
|
}
|
|
126
140
|
};
|
|
127
141
|
//#endregion
|
|
128
|
-
export {
|
|
142
|
+
export { c as default };
|
package/dist/utility/webHook.mjs
CHANGED
|
@@ -10,37 +10,34 @@ var a = /* @__PURE__ */ new Set(["GET", "HEAD"]), o = /* @__PURE__ */ new Set([
|
|
|
10
10
|
let n = Number(t);
|
|
11
11
|
return (Number.isFinite(n) && n >= 0 ? n : Number(e.params.staleTime.default)) * 1e3;
|
|
12
12
|
}, c = (e) => e ? { Authorization: `Bearer ${e}` } : {}, l = (e) => typeof e == "object" && e ? e : {}, u = /* @__PURE__ */ new Set(["authorization", "content-type"]), d = (e) => typeof e == "object" && e ? Object.fromEntries(Object.entries(e).flatMap(([e, t]) => (typeof t == "string" || typeof t == "number") && t !== "" && !u.has(e.toLowerCase()) ? [[e, String(t)]] : [])) : {}, f = async ({ url: e, authorizationToken: t, headers: n, body: r, credentials: i }, a) => {
|
|
13
|
-
let s = l(r), u = Object.values(s).some((e) => e instanceof Blob)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
e.append(t, n);
|
|
30
|
-
}), r.body = e;
|
|
31
|
-
}
|
|
13
|
+
let s = l(r), u = Object.values(s).some((e) => e instanceof Blob), f = {
|
|
14
|
+
method: a,
|
|
15
|
+
headers: {
|
|
16
|
+
...d(n),
|
|
17
|
+
...u ? {} : { "Content-Type": "application/json" },
|
|
18
|
+
...c(t)
|
|
19
|
+
},
|
|
20
|
+
credentials: i
|
|
21
|
+
};
|
|
22
|
+
if (!o.has(a)) {
|
|
23
|
+
if (!u) f.body = JSON.stringify(s);
|
|
24
|
+
else {
|
|
25
|
+
let e = new FormData();
|
|
26
|
+
Object.entries(s).forEach(([t, n]) => {
|
|
27
|
+
e.append(t, n);
|
|
28
|
+
}), f.body = e;
|
|
32
29
|
}
|
|
33
|
-
let l = await fetch(e, r), f = "";
|
|
34
|
-
try {
|
|
35
|
-
f = await l.json();
|
|
36
|
-
} catch {}
|
|
37
|
-
return {
|
|
38
|
-
status: l.status,
|
|
39
|
-
data: f
|
|
40
|
-
};
|
|
41
|
-
} catch (e) {
|
|
42
|
-
return console.error(e), {};
|
|
43
30
|
}
|
|
31
|
+
let p = await fetch(e, f).catch((t) => {
|
|
32
|
+
throw Error(`${a} ${e} got no answer: ${t instanceof Error ? t.message : String(t)}`);
|
|
33
|
+
}), m = "";
|
|
34
|
+
try {
|
|
35
|
+
m = await p.json();
|
|
36
|
+
} catch {}
|
|
37
|
+
return {
|
|
38
|
+
status: p.status,
|
|
39
|
+
data: m
|
|
40
|
+
};
|
|
44
41
|
}, p = t("webHook", e, async (e) => {
|
|
45
42
|
let t = (e.method || "get").toUpperCase();
|
|
46
43
|
if (a.has(t)) {
|
|
@@ -58,11 +55,11 @@ var a = /* @__PURE__ */ new Set(["GET", "HEAD"]), o = /* @__PURE__ */ new Set([
|
|
|
58
55
|
meta: { url: e.url },
|
|
59
56
|
fetcher: () => f(e, t),
|
|
60
57
|
staleTime: s(e.staleTime),
|
|
61
|
-
isCacheable: (e) => e.status
|
|
62
|
-
})
|
|
58
|
+
isCacheable: (e) => e.status < 400
|
|
59
|
+
}) };
|
|
63
60
|
}
|
|
64
61
|
let o = await f(e, t);
|
|
65
|
-
return o.status
|
|
62
|
+
return o.status >= 200 && o.status < 300 && n({
|
|
66
63
|
mode: e.invalidateQueries,
|
|
67
64
|
fallback: "origin",
|
|
68
65
|
elements: e.invalidateElements,
|
|
@@ -5,5 +5,8 @@ import { BuiltinActionSpec } from '@plitzi/sdk-shared/authoring/builder';
|
|
|
5
5
|
* A read may be served from the page's query cache — the same one api containers use, under the same key, so a
|
|
6
6
|
* flow reading a URL a container already loaded does not ask again. A write says what it refreshes once it
|
|
7
7
|
* succeeded, which by default is every cached request to the site it wrote to.
|
|
8
|
+
*
|
|
9
|
+
* Any answer is the step's result, an error status included — `{{ <step>.response.status }}` is how a flow tells a 401
|
|
10
|
+
* from a 200. No answer at all fails the step, so the flow stops and its `onFailure` runs.
|
|
8
11
|
*/
|
|
9
12
|
export declare const webHookSpec: BuiltinActionSpec;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/sdk-interactions",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.10",
|
|
4
4
|
"license": "AGPL-3.0",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -189,12 +189,12 @@
|
|
|
189
189
|
"vitest": "^5.0.1"
|
|
190
190
|
},
|
|
191
191
|
"dependencies": {
|
|
192
|
-
"@plitzi/plitzi-ui": "^1.6.
|
|
193
|
-
"@plitzi/sdk-auth": "0.37.
|
|
194
|
-
"@plitzi/sdk-dev-tools": "0.37.
|
|
195
|
-
"@plitzi/sdk-event-bridge": "0.37.
|
|
196
|
-
"@plitzi/sdk-navigation": "0.37.
|
|
197
|
-
"@plitzi/sdk-schema": "0.37.
|
|
198
|
-
"@plitzi/sdk-shared": "0.37.
|
|
192
|
+
"@plitzi/plitzi-ui": "^1.6.29",
|
|
193
|
+
"@plitzi/sdk-auth": "0.37.10",
|
|
194
|
+
"@plitzi/sdk-dev-tools": "0.37.10",
|
|
195
|
+
"@plitzi/sdk-event-bridge": "0.37.10",
|
|
196
|
+
"@plitzi/sdk-navigation": "0.37.10",
|
|
197
|
+
"@plitzi/sdk-schema": "0.37.10",
|
|
198
|
+
"@plitzi/sdk-shared": "0.37.10"
|
|
199
199
|
}
|
|
200
200
|
}
|