@plitzi/sdk-server 0.38.11 → 0.38.12
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 +108 -0
- package/dist/functions-api.d.ts +18 -2
- package/dist/modules/actions/jobs/later.js +1 -1
- package/dist/modules/actions/runtime/credentials.js +1 -1
- package/dist/modules/actions/runtime/rateLimit.d.ts +7 -0
- package/dist/modules/actions/runtime/rateLimit.js +7 -1
- package/dist/modules/actions/runtime/runAction.js +35 -8
- package/dist/modules/functions/context.js +3 -3
- package/dist/modules/functions/driver.js +9 -5
- package/dist/modules/functions/manifest.js +8 -3
- package/dist/modules/project/plugins.d.ts +1 -1
- package/package.json +3 -3
- package/skills/plitzi-authoring/CHEATSHEET.md +1 -1
- package/skills/plitzi-authoring/recipes/plugin.ts +9 -7
- package/skills/plitzi-authoring/reference/authoring-errors.md +3 -1
- package/skills/plitzi-authoring/reference/elements-and-styles.md +1 -1
- package/skills/plitzi-authoring/reference/flows.md +5 -4
- package/skills/plitzi-authoring/reference/plugins.md +40 -35
- package/skills/plitzi-authoring/reference/rules.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,113 @@
|
|
|
1
1
|
# @plitzi/sdk-server
|
|
2
2
|
|
|
3
|
+
## 0.38.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1301338: ## Builder
|
|
8
|
+
|
|
9
|
+
- **Icons in the canvas again** (`@plitzi/plitzi-builder`): the canvas inlined the SDK's Font Awesome sheet into its
|
|
10
|
+
`<style>`, where the sheet's `url(webfonts/…)` resolved against the editor's URL — a `srcdoc` frame takes it as its
|
|
11
|
+
base — so every icon was a 404 under `/spaces/<space>/webfonts/` and drew as an empty box. Only the Vite dev server,
|
|
12
|
+
which rewrites the URL, hid it. The canvas and the element preview now link the sheet from where the host serves it:
|
|
13
|
+
a new prop, `sdkIconsStylePath`, the one the host already gives the pages it renders
|
|
14
|
+
(`/sdk-assets/plitzi-sdk-icons.css` for sdk-server). Absent, the canvas links no icon sheet.
|
|
15
|
+
|
|
16
|
+
## Plugins
|
|
17
|
+
- **A plugin's other elements, hosted by `custom`** (`@plitzi/sdk-elements`): `custom({ renderType: 'jamDisc' })` on a
|
|
18
|
+
space with a plugin installed whose main element is another (`plitzi plugin pack` of several folders: the first is the
|
|
19
|
+
plugin, the rest go in it) rendered "Custom Component jamDisc Not Found" — the page registers only the plugin's main
|
|
20
|
+
element, and a `custom` host looked nowhere else. It now loads the type from the installed plugin that packs it, as
|
|
21
|
+
an element of the type itself always did, so the two ways the docs give to place a plugin's element are the same
|
|
22
|
+
everywhere: in a project `plitzi create` wrote, and once the project is on Plitzi.
|
|
23
|
+
- **One way to place a plugin's element** (`@plitzi/sdk-authoring`, `@plitzi/cli`, docs): from its declaration,
|
|
24
|
+
`const seatPicker = defineElement(declaration)` then `seatPicker({ id, … })` — typed by what it
|
|
25
|
+
declares, and the element the builder adds when somebody drops it. The welcome space `plitzi create` writes, the
|
|
26
|
+
plugin package's preview, `plitzi plugin add` and `plitzi explain` all write it so (the template's
|
|
27
|
+
`PluginHostOptions` now takes the plugin's `type` and `declaration: { from, attributes? }`; `custom`/`element` hosting
|
|
28
|
+
is gone). `pluginFolders(folder)` (`@plitzi/sdk-authoring/node`) reads each plugin with the folder it is in;
|
|
29
|
+
`pluginDeclarations` is built on it. A `custom({ renderType })` naming a plugin the space was handed the declaration
|
|
30
|
+
of is offered the declaration instead — suggestion `plugin-custom-host`. `custom` stays for a component registered
|
|
31
|
+
by name with no declaration. The skills, the CLI README, the MCP guide, `docs/en` and the website say the same.
|
|
32
|
+
- **`definePlugin`** (`@plitzi/sdk-authoring/plugin`, a new entry of 3 KB with nothing else in it): a plugin's
|
|
33
|
+
declaration written from what only it can say —
|
|
34
|
+
`definePlugin<SeatPickerAttributes>()({ type, label, attributes, triggers, callbacks })` — and every other field
|
|
35
|
+
at the default all plugins share (the builder's gestures, the catalogue entry, each attribute bindable, an empty
|
|
36
|
+
style). It answers the whole declaration, branded with its attributes, so `defineElement(declaration)` is typed
|
|
37
|
+
without a generic. `plitzi plugin add` and `create --plugin` write declarations with it (about a third of the lines).
|
|
38
|
+
- **`usePluginTrigger(declaration)`** (`@plitzi/plitzi-sdk`): fires a plugin's declared event on its element — the
|
|
39
|
+
event name and what each hands a flow typed off the declaration's `preview` — on a live page only. The hook every
|
|
40
|
+
plugin wrote for itself (and `plugin add` generated as `use<Name>Events`) is gone from the scaffold.
|
|
41
|
+
- **Attributes as the types their defaults are** (`@plitzi/sdk-elements`): a plugin's attribute bound from text — a
|
|
42
|
+
state, a row, a channel message — reaches its component as the number or the flag its declared default is (`'112'`
|
|
43
|
+
as 112, `'true'` as true). A plugin no longer types its props `number | string` and parses them.
|
|
44
|
+
|
|
45
|
+
## Server actions and functions
|
|
46
|
+
- **Task params with rules** (`@plitzi/sdk-server`, `@plitzi/sdk-shared`): a task declares `required`, a text's
|
|
47
|
+
`maxLength` and a number's `min`/`max`, and the runner refuses a call that breaks any of them — every rule broken at
|
|
48
|
+
once, in the words the page shows (`Title is required; Seconds is at most 300`) — before the code runs. A `text` param
|
|
49
|
+
bound to a number arrives as its digits. The manifest keeps the rules, so the platform's sandbox checks the same.
|
|
50
|
+
- **`taskAction(task, { trigger })`** (`@plitzi/sdk-authoring`): the action that runs one of the space's tasks and
|
|
51
|
+
answers its result, derived from the task — its id (`<namespace>-<action>`), name, description and input (the task's
|
|
52
|
+
params, once). An action of more than one step is still `defineAction`.
|
|
53
|
+
- **`ctx.rateLimit(bucket, { …, refuse })`**: past the limit the call is refused with those words instead of answering
|
|
54
|
+
`allowed: false` for the code to check — natively and in the sandbox.
|
|
55
|
+
|
|
56
|
+
## Authoring
|
|
57
|
+
- **`runServerActionOrNotify(id, params, fallback)`**: a server action run and the notice the visitor reads when it
|
|
58
|
+
fails — the action's own refusal, or the fallback — as two steps to spread into a flow.
|
|
59
|
+
- **`repeated-shape` reads what copies bind**: copies whose bindings read their data in different forms (a count, a
|
|
60
|
+
total of a quota) and copies holding a provider resolved on the server (found by its own id) are no longer offered
|
|
61
|
+
as one component.
|
|
62
|
+
- **`element-color-inherited`** (suggestion): `elements` giving a heading, a text, a link… `color: inherit` or the
|
|
63
|
+
page's own colour. **A link inherits the page's colour** (`@plitzi/plitzi-sdk`, `@plitzi/sdk-elements`) rather than
|
|
64
|
+
holding the SDK's grey — the same by default, and the space's palette inside a card that is a link. The template
|
|
65
|
+
every space starts from writes its colours once, on the page.
|
|
66
|
+
|
|
67
|
+
## Fixes
|
|
68
|
+
- **A dialog opened with an id** (`@plitzi/sdk-elements`): `openDialog('dialog', '{{ row.id }}')` reads the id as its
|
|
69
|
+
`content`, as `openModal` already did — a numeric id parsed as the number and the dialog's bindings read nothing.
|
|
70
|
+
- **A select's chevron** (`@plitzi/plitzi-sdk`): a drawn chevron in the field's own colour, sized by its type, in place
|
|
71
|
+
of the `▼` glyph — and laid out in the box, so it ends where the box's padding begins whatever that padding is, and
|
|
72
|
+
the chosen option stops short of it instead of running under it. A disabled select's is faded.
|
|
73
|
+
- **Every element has its settings** (`@plitzi/plitzi-builder`, `@plitzi/sdk-elements`): the builder found none for
|
|
74
|
+
`tabContainerHeader` (registered under a misspelt type), `themeToggle`, `dropdownPopup`, `loading` and `notFound`.
|
|
75
|
+
The theme toggle's panel sets its mode, its labels and whether it offers "System"; the registry is typed by element
|
|
76
|
+
type and must hold one for each.
|
|
77
|
+
- **A plugin's panel** (`@plitzi/cli`): `plitzi doctor` warns of a plugin with no `Settings.tsx`, or one its entry never
|
|
78
|
+
passes as `pluginSettings` (`plugin-settings-missing`); the example `create` writes has its panel, as every element
|
|
79
|
+
`plugin add` writes does, and the panels it generates are laid out as the project's Prettier lays them out.
|
|
80
|
+
- **A refused or failed run reaches `onFlowError` in every mode** (`@plitzi/sdk-interactions`): an awaited server
|
|
81
|
+
action the server refused fired no `onFlowError` on the element that started it, as `detached` and `stream` ones do
|
|
82
|
+
— a board rolling its edits back on a refusal never did; and a run answered `status: 'failed'` fired `onFlowEnd`
|
|
83
|
+
(detached) or nothing (awaited) rather than `onFlowError`.
|
|
84
|
+
- **A source renamed** (`@plitzi/sdk-shared`): the binding picker lists a provider, a form or a modal by the name its
|
|
85
|
+
element has now, not the one it had when it mounted.
|
|
86
|
+
- **A text's words in the builder** (`@plitzi/sdk-elements`): a `paragraph`, `text` or `heading` whose `content` is
|
|
87
|
+
`0` shows `0` while editing too, not its placeholder; one bound to nothing shows nothing rather than `null`.
|
|
88
|
+
- **Built-in declarations** (`@plitzi/sdk-shared`): `elementDeclaration` fills in what every element shares — the
|
|
89
|
+
builder's gestures, Plitzi's catalogue entry, a visible element with no bindings, its style named by its label —
|
|
90
|
+
from the defaults `definePlugin` uses; each element's declaration says only what is its own (a third of the lines).
|
|
91
|
+
|
|
92
|
+
## Typed for plugins and hosts
|
|
93
|
+
- **`usePlitzi()` and `PlitziProvider`** (`@plitzi/sdk-shared/hooks/usePlitzi`, `@plitzi/plitzi-sdk`): what were
|
|
94
|
+
`usePlitziServiceContext()` and `PlitziServiceProvider` (and `PlitziServiceContextValue`, now `PlitziContextValue`),
|
|
95
|
+
holding only what the host rendering a space knows — `settings`, `root`, `utils`. Its `contexts` and
|
|
96
|
+
`customContexts` are gone: every context it carried is a shared one already, imported from its own module
|
|
97
|
+
(`@plitzi/sdk-interactions/InteractionsContext`, `@plitzi/sdk-shared/network/NetworkContext`…), and
|
|
98
|
+
`PluginsContext` moves to `@plitzi/sdk-shared/plugins/PluginsContext` beside the others.
|
|
99
|
+
- **`InteractionsManagerApi`** (`@plitzi/sdk-shared`): the interactions manager as everything outside
|
|
100
|
+
`@plitzi/sdk-interactions` reaches it, declared where every package can name it and implemented by
|
|
101
|
+
`InteractionsManager`, so `use(InteractionsContext)` is typed — `interactionTrigger`, `createChildManager`… — where it
|
|
102
|
+
was `any`.
|
|
103
|
+
- **`getPathsFromObject`** (`@plitzi/sdk-shared/helpers/utils`), spelt right; `getPathsFromObeject` is gone.
|
|
104
|
+
- **`ElementWords` places itself** (`@plitzi/sdk-elements`): `contentPlacement` and the element's children, so a button
|
|
105
|
+
and a link write their words once.
|
|
106
|
+
|
|
107
|
+
- Updated dependencies [1301338]
|
|
108
|
+
- @plitzi/plitzi-sdk@0.38.12
|
|
109
|
+
- @plitzi/sdk-shared@0.38.12
|
|
110
|
+
|
|
3
111
|
## 0.38.11
|
|
4
112
|
|
|
5
113
|
### Patch Changes
|
package/dist/functions-api.d.ts
CHANGED
|
@@ -211,17 +211,28 @@ declare type InteractionCallbackParam<T extends Record<string, unknown> = Record
|
|
|
211
211
|
* space's credentials of that kind to pick from, rather than a text box somebody has to type an identifier into.
|
|
212
212
|
*/
|
|
213
213
|
credentialProvider?: SpaceCredentialProvider;
|
|
214
|
-
|
|
214
|
+
/**
|
|
215
|
+
* A space's task refuses to run without it — empty, or only spaces — saying so with the param's `label`, before its
|
|
216
|
+
* code runs: what a task otherwise checked by hand in its first lines.
|
|
217
|
+
*/
|
|
218
|
+
required?: boolean;
|
|
219
|
+
} &
|
|
220
|
+
/** Handed to a task as text, whatever was written or bound — a number as its digits — at most `maxLength` long. */
|
|
221
|
+
({
|
|
215
222
|
type: 'text';
|
|
216
223
|
defaultValue?: string | number;
|
|
224
|
+
maxLength?: number;
|
|
217
225
|
}
|
|
218
|
-
/** Written as text — a number or a template — and handed to the callback as a number
|
|
226
|
+
/** Written as text — a number or a template — and handed to the callback as a number, within `min` and `max`. */
|
|
219
227
|
| {
|
|
220
228
|
type: 'number';
|
|
221
229
|
defaultValue?: number;
|
|
230
|
+
min?: number;
|
|
231
|
+
max?: number;
|
|
222
232
|
} | {
|
|
223
233
|
type: 'textarea';
|
|
224
234
|
defaultValue?: string | number;
|
|
235
|
+
maxLength?: number;
|
|
225
236
|
} | {
|
|
226
237
|
type: 'codemirror-text';
|
|
227
238
|
defaultValue?: string;
|
|
@@ -334,6 +345,11 @@ declare type RateLimit = {
|
|
|
334
345
|
most: number;
|
|
335
346
|
perSeconds: number;
|
|
336
347
|
per?: 'caller' | 'everyone';
|
|
348
|
+
/**
|
|
349
|
+
* Over the limit, the call is refused with these words — the step's error, on the page — rather than answered
|
|
350
|
+
* `allowed: false` for the code to check: `ctx.rateLimit('make', { most: 5, perSeconds: 600, refuse: 'Wait ten minutes' })`.
|
|
351
|
+
*/
|
|
352
|
+
refuse?: string;
|
|
337
353
|
};
|
|
338
354
|
|
|
339
355
|
declare type SpaceCredentialProvider = 's3' | 'r2' | 'ssr' | 'custom' | 'smtp';
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { findTriggerNode } from "../runtime/triggers.js";
|
|
2
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
//#region src/modules/actions/jobs/later.ts
|
|
4
5
|
/** A month: past it, a run set for later is a schedule, and a cron says it better. */
|
|
5
6
|
var MAX_LATER_SECONDS = 2592e3;
|
|
6
7
|
/** A name a run set for later is known by, as a topic is written: what it is replaced and cancelled by. */
|
|
7
8
|
var KEY = /^[A-Za-z0-9:_.-]{1,128}$/;
|
|
8
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
9
|
/**
|
|
10
10
|
* Runs of a space's actions that start in a while — a turn that runs out, a bot's move, a hold that lapses — held by the
|
|
11
11
|
* same queue as its schedules: they outlive a restart where the queue does, run once across replicas, retry, and show
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
1
2
|
//#region src/modules/actions/runtime/credentials.ts
|
|
2
3
|
/** Where a project's server — and `plitzi functions dev` — reads the credentials its space's code names. */
|
|
3
4
|
var CREDENTIALS_ENV = "PLITZI_CREDENTIALS";
|
|
4
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
5
|
var EXAMPLE = "{\"google\":{\"clientId\":\"…\",\"clientSecret\":\"…\"}}";
|
|
6
6
|
/**
|
|
7
7
|
* The credentials a deployment with no credential store keeps in its environment: one JSON object, credential id → its
|
|
@@ -4,6 +4,11 @@ export type RateLimit = {
|
|
|
4
4
|
most: number;
|
|
5
5
|
perSeconds: number;
|
|
6
6
|
per?: 'caller' | 'everyone';
|
|
7
|
+
/**
|
|
8
|
+
* Over the limit, the call is refused with these words — the step's error, on the page — rather than answered
|
|
9
|
+
* `allowed: false` for the code to check: `ctx.rateLimit('make', { most: 5, perSeconds: 600, refuse: 'Wait ten minutes' })`.
|
|
10
|
+
*/
|
|
11
|
+
refuse?: string;
|
|
7
12
|
};
|
|
8
13
|
/** Where a limit stands after counting one more: whether that one was within it, and how many are left. */
|
|
9
14
|
export type RateCount = {
|
|
@@ -17,3 +22,5 @@ export type RateCount = {
|
|
|
17
22
|
* together. Refusing is the caller's: a step refuses the run, a function decides what to answer.
|
|
18
23
|
*/
|
|
19
24
|
export declare const countRate: (kv: ActionKvStore, callerId: string, bucket: string, { most, perSeconds, per }: RateLimit) => Promise<RateCount>;
|
|
25
|
+
/** A count past its limit, refused with the limit's own words when it says some; within it, or with none, as counted. */
|
|
26
|
+
export declare const refusedOver: (count: RateCount, limit: RateLimit) => RateCount;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ActionRefusal } from "./errors.js";
|
|
1
2
|
//#region src/modules/actions/runtime/rateLimit.ts
|
|
2
3
|
/**
|
|
3
4
|
* Counts one more of `bucket` in the current window and says whether it was within the limit — the one count behind
|
|
@@ -15,5 +16,10 @@ var countRate = async (kv, callerId, bucket, { most, perSeconds, per = "caller"
|
|
|
15
16
|
remaining: Math.max(0, most - count)
|
|
16
17
|
};
|
|
17
18
|
};
|
|
19
|
+
/** A count past its limit, refused with the limit's own words when it says some; within it, or with none, as counted. */
|
|
20
|
+
var refusedOver = (count, limit) => {
|
|
21
|
+
if (!count.allowed && limit.refuse) throw new ActionRefusal(limit.refuse);
|
|
22
|
+
return count;
|
|
23
|
+
};
|
|
18
24
|
//#endregion
|
|
19
|
-
export { countRate };
|
|
25
|
+
export { countRate, refusedOver };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
2
|
import "../jobs/schedules.js";
|
|
3
|
-
import { ActionRunError, isActionRefusal } from "./errors.js";
|
|
3
|
+
import { ActionRefusal, ActionRunError, isActionRefusal } from "./errors.js";
|
|
4
4
|
import { runCancelKey } from "./guards.js";
|
|
5
5
|
import { createKvStore } from "./kvStore.js";
|
|
6
6
|
import { resolveLimits } from "./limits.js";
|
|
@@ -151,10 +151,12 @@ var MAX_LOG_LINES = 100;
|
|
|
151
151
|
var MAX_LOG_LINE = 1e3;
|
|
152
152
|
/**
|
|
153
153
|
* A value written as text, read as the type its task declares: a `number` param written `5` — or bound to text that says
|
|
154
|
-
* it — arrives as 5, a `boolean` one written `'true'` as `true`, an empty number as nothing (so the default applies)
|
|
155
|
-
*
|
|
154
|
+
* it — arrives as 5, a `boolean` one written `'true'` as `true`, an empty number as nothing (so the default applies), and
|
|
155
|
+
* a `text` one bound to a number as its digits. The browser reads a callback's params the same way
|
|
156
|
+
* (`coerceDeclaredParams`): a task's author never writes `Number(…)` or `String(…)`.
|
|
156
157
|
*/
|
|
157
158
|
var asDeclared = (type, value) => {
|
|
159
|
+
if (typeof value === "number" && (type === "text" || type === "textarea")) return String(value);
|
|
158
160
|
if (typeof value !== "string") return value;
|
|
159
161
|
if (type === "number") {
|
|
160
162
|
const parsed = Number(value.trim());
|
|
@@ -162,11 +164,36 @@ var asDeclared = (type, value) => {
|
|
|
162
164
|
}
|
|
163
165
|
return type === "boolean" && (value === "true" || value === "false") ? value === "true" : value;
|
|
164
166
|
};
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Why a value does not fit the rules its param declares — `required`, a text's `maxLength`, a number's `min` and `max` —
|
|
169
|
+
* in the words the page shows: what a task's code otherwise checked by hand before doing anything. A param that
|
|
170
|
+
* declares none is never judged.
|
|
171
|
+
*/
|
|
172
|
+
var paramProblem = (key, param, value) => {
|
|
173
|
+
const name = param.label ?? key;
|
|
174
|
+
if (value === void 0 || value === null || typeof value === "string" && value.trim() === "") {
|
|
175
|
+
const defaulted = param.defaultValue !== void 0 && param.defaultValue !== "";
|
|
176
|
+
return param.required && !defaulted ? `${name} is required` : void 0;
|
|
177
|
+
}
|
|
178
|
+
if ((param.type === "text" || param.type === "textarea") && param.maxLength !== void 0) return typeof value === "string" && value.length > param.maxLength ? `${name} is longer than ${String(param.maxLength)} characters` : void 0;
|
|
179
|
+
if (param.type === "number" && (param.min !== void 0 || param.max !== void 0)) {
|
|
180
|
+
if (typeof value !== "number") return `${name} is not a number`;
|
|
181
|
+
if (param.min !== void 0 && value < param.min) return `${name} is at least ${String(param.min)}`;
|
|
182
|
+
if (param.max !== void 0 && value > param.max) return `${name} is at most ${String(param.max)}`;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var withDefaults = (task, params) => {
|
|
186
|
+
const problems = [];
|
|
187
|
+
const values = Object.entries(task.params).reduce((acum, [key, param]) => {
|
|
188
|
+
const value = asDeclared(typeof param.type === "function" ? param.type(acum) : param.type, acum[key]);
|
|
189
|
+
const problem = paramProblem(key, param, value);
|
|
190
|
+
if (problem) problems.push(problem);
|
|
191
|
+
acum[key] = value === void 0 || value === "" ? param.defaultValue ?? "" : value;
|
|
192
|
+
return acum;
|
|
193
|
+
}, { ...params });
|
|
194
|
+
if (problems.length > 0) throw new ActionRefusal(problems.join("; "));
|
|
195
|
+
return values;
|
|
196
|
+
};
|
|
170
197
|
var runNode = async (node, scope, registry, buildContext) => {
|
|
171
198
|
if (!node.action || !node.enabled) return {
|
|
172
199
|
status: "disabled",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { prefixKv } from "../actions/runtime/namespaceKv.js";
|
|
2
2
|
import { fetchOutbound } from "../../helpers/outboundGuard.js";
|
|
3
|
-
import { countRate } from "../actions/runtime/rateLimit.js";
|
|
3
|
+
import { countRate, refusedOver } from "../actions/runtime/rateLimit.js";
|
|
4
4
|
import { pluginKvPrefix, pluginSigned } from "./scope.js";
|
|
5
5
|
import { processTwigParam } from "@plitzi/sdk-shared/helpers/twigWrapper";
|
|
6
6
|
//#region src/modules/functions/context.ts
|
|
@@ -92,7 +92,7 @@ var functionContextFor = (ctx, hosts, scope) => {
|
|
|
92
92
|
if (!scope) return {
|
|
93
93
|
...shared,
|
|
94
94
|
kv: ctx.kv,
|
|
95
|
-
rateLimit: (bucket, limit) => countRate(ctx.kv, ctx.callerId, bucket, limit),
|
|
95
|
+
rateLimit: async (bucket, limit) => refusedOver(await countRate(ctx.kv, ctx.callerId, bucket, limit), limit),
|
|
96
96
|
sign: ctx.sign ?? unsigned,
|
|
97
97
|
verify: ctx.verify ?? unsigned,
|
|
98
98
|
publish: ctx.publish ?? unavailable("publish"),
|
|
@@ -109,7 +109,7 @@ var functionContextFor = (ctx, hosts, scope) => {
|
|
|
109
109
|
return {
|
|
110
110
|
...shared,
|
|
111
111
|
kv,
|
|
112
|
-
rateLimit: (bucket, limit) => countRate(kv, ctx.callerId, bucket, limit),
|
|
112
|
+
rateLimit: async (bucket, limit) => refusedOver(await countRate(kv, ctx.callerId, bucket, limit), limit),
|
|
113
113
|
sign: sign ? (value) => sign(pluginSigned(scope.plugin, value)) : unsigned,
|
|
114
114
|
verify: verify ? (value, signature) => verify(pluginSigned(scope.plugin, value), signature) : unsigned,
|
|
115
115
|
publish: noChannel,
|
|
@@ -107,11 +107,15 @@ var createFunctionsDriver = (environment) => {
|
|
|
107
107
|
op: "cancelLater",
|
|
108
108
|
key
|
|
109
109
|
}),
|
|
110
|
-
rateLimit: (bucket, limit) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
rateLimit: async (bucket, limit) => {
|
|
111
|
+
const count = await call({
|
|
112
|
+
op: "rateLimit",
|
|
113
|
+
bucket,
|
|
114
|
+
limit
|
|
115
|
+
});
|
|
116
|
+
if (isRecord(limit) && typeof limit.refuse === "string" && isRecord(count) && count.allowed === false) throw refusal(limit.refuse);
|
|
117
|
+
return count;
|
|
118
|
+
},
|
|
115
119
|
sign: (value) => call({
|
|
116
120
|
op: "sign",
|
|
117
121
|
value
|
|
@@ -47,20 +47,25 @@ var paramOf = (value) => {
|
|
|
47
47
|
const base = {
|
|
48
48
|
...typeof value.canBind === "boolean" ? { canBind: value.canBind } : {},
|
|
49
49
|
...typeof value.label === "string" ? { label: value.label } : {},
|
|
50
|
-
...typeof value.when === "boolean" ? { when: value.when } : {}
|
|
50
|
+
...typeof value.when === "boolean" ? { when: value.when } : {},
|
|
51
|
+
...typeof value.required === "boolean" ? { required: value.required } : {}
|
|
51
52
|
};
|
|
53
|
+
const bound = (key) => typeof value[key] === "number" && Number.isFinite(value[key]) ? { [key]: value[key] } : {};
|
|
52
54
|
const defaultValue = value.defaultValue;
|
|
53
55
|
switch (value.type) {
|
|
54
56
|
case "text":
|
|
55
57
|
case "textarea": return {
|
|
56
58
|
...base,
|
|
57
59
|
type: value.type,
|
|
58
|
-
...typeof defaultValue === "string" || typeof defaultValue === "number" ? { defaultValue } : {}
|
|
60
|
+
...typeof defaultValue === "string" || typeof defaultValue === "number" ? { defaultValue } : {},
|
|
61
|
+
...bound("maxLength")
|
|
59
62
|
};
|
|
60
63
|
case "number": return {
|
|
61
64
|
...base,
|
|
62
65
|
type: "number",
|
|
63
|
-
...typeof defaultValue === "number" ? { defaultValue } : {}
|
|
66
|
+
...typeof defaultValue === "number" ? { defaultValue } : {},
|
|
67
|
+
...bound("min"),
|
|
68
|
+
...bound("max")
|
|
64
69
|
};
|
|
65
70
|
case "codemirror-text":
|
|
66
71
|
case "codemirror-json": return {
|
|
@@ -5,7 +5,7 @@ import { PluginSource, SSRServer } from '@plitzi/sdk-shared';
|
|
|
5
5
|
* plugin of `vendor/plugins` run as it was built — each with its server half, when it has one.
|
|
6
6
|
*/
|
|
7
7
|
export type ProjectPlugins = {
|
|
8
|
-
/** How each is built, by the
|
|
8
|
+
/** How each is built, by the type a space's element of it is — `defineElement(declaration)` — rendered by. */
|
|
9
9
|
sources: Record<string, PluginSource>;
|
|
10
10
|
/**
|
|
11
11
|
* Which of them the space renders with — the deployment's `pluginNames`. Registering a plugin is not turning it on:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/sdk-server",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.12",
|
|
4
4
|
"license": "AGPL-3.0",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -96,8 +96,8 @@
|
|
|
96
96
|
"./dist/env.js"
|
|
97
97
|
],
|
|
98
98
|
"dependencies": {
|
|
99
|
-
"@plitzi/plitzi-sdk": "0.38.
|
|
100
|
-
"@plitzi/sdk-shared": "0.38.
|
|
99
|
+
"@plitzi/plitzi-sdk": "0.38.12",
|
|
100
|
+
"@plitzi/sdk-shared": "0.38.12",
|
|
101
101
|
"ejs": "^6.0.1",
|
|
102
102
|
"esbuild": "^0.28.2",
|
|
103
103
|
"jsonwebtoken": "^9.0.3",
|
|
@@ -55,7 +55,7 @@ form({ id, managedByInteractions: true, flows, children })
|
|
|
55
55
|
formControl({ name: 'email', label: 'Email', subType: 'email', required: true }) // optional without required; select: options: [{ label, value }]
|
|
56
56
|
modalContainer({ id, visible: false, title, children })
|
|
57
57
|
component('card', { props: { title }, children: { body: [ … ] } })
|
|
58
|
-
|
|
58
|
+
const seats = defineElement(seatsDeclaration); seats({ id }) // a plugin
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
## Styles
|
|
@@ -11,18 +11,20 @@ import {
|
|
|
11
11
|
onClick,
|
|
12
12
|
setState
|
|
13
13
|
} from '@plitzi/sdk-authoring';
|
|
14
|
+
import { definePlugin } from '@plitzi/sdk-authoring/plugin';
|
|
14
15
|
|
|
15
16
|
import type { PluginDeclarationData, SpaceSpec } from '@plitzi/sdk-authoring';
|
|
16
17
|
|
|
17
|
-
// In a project
|
|
18
|
-
const declaration = {
|
|
18
|
+
// In a project, `src/plugins/SeatPicker/declaration.ts` — `export default definePlugin<SeatPickerAttributes>()({ … })`.
|
|
19
|
+
const declaration = definePlugin<{ rows?: number }>()({
|
|
19
20
|
type: 'seatPicker',
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
21
|
+
label: 'Seat Picker',
|
|
22
|
+
attributes: { rows: 10 },
|
|
23
|
+
triggers: { onPick: { preview: { seat: '' } } },
|
|
24
|
+
callbacks: { reset: {} }
|
|
25
|
+
});
|
|
24
26
|
|
|
25
|
-
const seatPicker = defineElement
|
|
27
|
+
const seatPicker = defineElement(declaration);
|
|
26
28
|
|
|
27
29
|
// `authorSpace(recipe, { plugins })` — in a project, every `src/plugins/<Name>/declaration.ts`, found by folder.
|
|
28
30
|
export const plugins: PluginDeclarationData[] = [declaration];
|
|
@@ -234,11 +234,13 @@ Nothing is wrong: the page renders as written. Each is a shorter way to the same
|
|
|
234
234
|
| `custom-css-notifications` | the toasts or their icon, close button or progress bar dressed with `.Toastify__*` rules in `customCss` | `notifications: { font, fontSize, fontWeight, minHeight, iconSize, closeColor, progressHeight, … }` |
|
|
235
235
|
| `custom-css-sdk-default` | `customCss` repeating what the SDK already does for every space — less motion when asked, the theme toggle showing one icon | nothing: remove it |
|
|
236
236
|
| `custom-css-slot` | a part of a built-in element dressed in `customCss` by the SDK's markup — a form control's `<input>`, a pager's buttons, a heading inside a markdown | a class on the slot of that part — `slots: { field: input }`, `elements.<type>.slots` — with its states |
|
|
237
|
+
| `element-color-inherited` | `elements` giving a type — a heading, a text, a link — `color: inherit` or the page's own colour, which it takes by itself | remove it, and name the colour once on `elements.page`: every element reads in the page’s colour, a link’s words included |
|
|
237
238
|
| `heavy-animation` | keyframes animating what the browser repaints or lays out again on every frame — a size, a position, a blur, a shadow, or a colour in a loop — which stutters whenever the page is busy, most of all while it loads | `opacity` and `transform`: a size or a position is `translate`/`scale`, a blur or a shadow is the `opacity` of a layer carrying it; a loop that must animate anything else starts `paused` and runs under `[data-hydrated]` |
|
|
238
239
|
| `literal-colour` | a colour written out in a class where the space has a token of that value — it stays put in the dark scheme | `var(--token)` when it should follow the scheme; a token of one value of its own when it must stay the same in both |
|
|
239
240
|
| `not-found-page` | a space with no page of its own for an address no other page answers — a plain one was added, in the home page’s layout, sent with status 404 | a page whose slug is `'*'`: `{ id: 'not-found', name: 'Not found', slug: '*', layout: …, body: [...] }` |
|
|
241
|
+
| `plugin-custom-host` | a `custom` element naming, by `renderType`, a plugin whose declaration the space was handed | place it from its declaration — `const seatPicker = defineElement(declaration)`, then `seatPicker({ id, … })`: typed by what it declares (a `definePlugin` declaration carries its attributes), the element the builder adds too |
|
|
240
242
|
| `repeated-on-pages` | the same block — a header, a footer, a side panel — written into several pages | a layout holding it once (`layouts`, and `layout: { id, slot }` on each page) — a component when only some pages of a layout carry it; a link marks its own page with the `current` state |
|
|
241
|
-
| `repeated-shape` | the same structure written again and again with different words — cards, rows, tiles | a component with props (`components`, `component(id, { props })`), or one `list` when they are rows of data side by side — a few cards a person rewords on the canvas can stay cards |
|
|
243
|
+
| `repeated-shape` | the same structure written again and again with different words — cards, rows, tiles; copies that bind different data, read their own template or render on the server are controls, not repeats | a component with props (`components`, `component(id, { props })`), or one `list` when they are rows of data side by side — a few cards a person rewords on the canvas can stay cards |
|
|
242
244
|
| `unused-class` | a class declared in `classes` that no element, binding, flow or other class names | remove it from `classes` — or wear it where it was meant to go |
|
|
243
245
|
| `unused-component` | a component no page, layout or other component places | remove it from `components`, or place it where it was meant to go |
|
|
244
246
|
| `unused-token` | a token of `variables` no `var(--…)` reads — not a class, an element, `customCss` nor another token | remove it, or write it where its colour is written out |
|
|
@@ -73,7 +73,7 @@ indented with space below it: `{ margin: '0px', padding: '0px' }` on its class t
|
|
|
73
73
|
children sit in the parent's layout and a `class` on it styles nothing (`STYLE_WITHOUT_TAG`). Put the layout on the
|
|
74
74
|
parent, or give it `attributes: { subType: 'div' }`.
|
|
75
75
|
|
|
76
|
-
A plugin's type is authored the same way: `defineElement
|
|
76
|
+
A plugin's type is authored the same way: `defineElement(declaration)` — see [plugins](plugins.md).
|
|
77
77
|
|
|
78
78
|
**What a type takes:** `npx @plitzi/cli explain formControl`, or the exported catalogues ([validation.md](validation.md)).
|
|
79
79
|
|
|
@@ -69,8 +69,7 @@ A step's params are templates, evaluated in full (conditions, loops, filters). A
|
|
|
69
69
|
a row's button posts `{ jobId: '{{ list_jobRows.item.id }}' }` — the row that was clicked; the short name is refused.
|
|
70
70
|
|
|
71
71
|
**Pass objects, not JSON text.** `input: { title: '{{ form.values.title }}' }` rather than `input: '{"title": …}'`:
|
|
72
|
-
each value keeps its own type, and nothing depends on the quoting.
|
|
73
|
-
is escaped for it — but an object needs no punctuation.)
|
|
72
|
+
each value keeps its own type, and nothing depends on the quoting.
|
|
74
73
|
|
|
75
74
|
**A field left empty is not in `values`.** A form sends what was typed, and a field nobody typed in sends nothing —
|
|
76
75
|
so `when({ field: 'sent.values.code', operator: '=', value: '' }, …)` never holds for it. Ask
|
|
@@ -106,7 +105,7 @@ ticks while the element is mounted and the tab is in view, never in the builder
|
|
|
106
105
|
|
|
107
106
|
**A number that goes round, or stops:** `cycleState({ key: 'slide', length: 4 })` is "next" — after the last the first
|
|
108
107
|
— and `by: -1` "previous"; `stepState({ key: 'shown', by: 40, max: 'apiContainer_site.data.total' })` adds and stops
|
|
109
|
-
at the bound.
|
|
108
|
+
at the bound.
|
|
110
109
|
|
|
111
110
|
**Scrolling** is a step on the element whose box scrolls (`overflow-x: auto` on a row of cards): `scrollBy('cards',
|
|
112
111
|
{ x: '80%' })` moves it by most of what it shows (`'-80%'` back, `'240'` pixels), `scrollTo('cards', { x: 'end' })`
|
|
@@ -165,6 +164,8 @@ Pages that see each other — cursors, presence, a shared board — talk over a
|
|
|
165
164
|
asks it. `appendState({ key, value, withId: true })` stores `{ id, value }` — bind `.value`, address by `.id`.
|
|
166
165
|
`whenFailed` matches every outcome that is not `completed` (also `skipped`, `aborted`). A failed run gives `reason`,
|
|
167
166
|
and `error` only when the server or a step wrote one for the caller (a task throwing `ActionRefusal`, `flow.fail` with
|
|
168
|
-
`tellCaller`) — so show `{{ saved.error ? saved.error : "…" }}`, never the bare `error`.
|
|
167
|
+
`tellCaller`) — so show `{{ saved.error ? saved.error : "…" }}`, never the bare `error`. That pair is one call:
|
|
168
|
+
`...runServerActionOrNotify('saved', { actionId, input }, 'Could not save')` — the named run, then that toast
|
|
169
|
+
(`{ appearance, placement }` fourth).
|
|
169
170
|
|
|
170
171
|
`when(rule, step)` around a step that already has a `when` adds to it: both must hold.
|
|
@@ -1,31 +1,37 @@
|
|
|
1
1
|
# Plugins
|
|
2
2
|
|
|
3
|
-
A plugin is a React component of the project's own — a chart, a map, a game —
|
|
4
|
-
|
|
3
|
+
A plugin is a React component of the project's own — a chart, a map, a game — and an element type of its own. The
|
|
4
|
+
space places it from its `declaration.ts`, typed by what it declares:
|
|
5
5
|
|
|
6
6
|
```ts
|
|
7
|
-
|
|
7
|
+
const nebulaRun = defineElement(nebulaRunDeclaration);
|
|
8
|
+
nebulaRun({ id: 'arcade', shipColor: 'amber', bind: { best: 'state.arcadeBest' } })
|
|
8
9
|
```
|
|
9
10
|
|
|
11
|
+
(`custom({ renderType })` is for a component registered by name, with no declaration.)
|
|
12
|
+
|
|
10
13
|
## Creating one
|
|
11
14
|
|
|
12
15
|
The CLI writes it, in the shape Plitzi's own elements are written in: `npx @plitzi/cli plugin add seat-picker` (a
|
|
13
16
|
package of its own and packing it: the CLI skill).
|
|
14
17
|
|
|
15
|
-
One folder, four files: `SeatPicker.tsx` (the component), `declaration.ts` (its
|
|
16
|
-
`
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`
|
|
18
|
+
One folder, four files: `SeatPicker.tsx` (the component), `declaration.ts`, `Settings.tsx` (its panel in the builder),
|
|
19
|
+
and `index.ts` (`Object.assign(Component, declaration, { pluginSettings: Settings })`). The declaration is data only,
|
|
20
|
+
`definePlugin<SeatPickerAttributes>()({ type, label, attributes, triggers, callbacks })` from
|
|
21
|
+
`@plitzi/sdk-authoring/plugin`: the rest of what the builder needs is derived. An event or an action is declared there
|
|
22
|
+
and registered by the component from there, never only in the component.
|
|
23
|
+
|
|
24
|
+
A plugin fires its events with `const fire = usePluginTrigger(declaration)` (`@plitzi/plitzi-sdk`), typed by the
|
|
25
|
+
declaration: `fire('onPick', { seat })` — an undeclared event, or a payload missing a field its `preview` names, does
|
|
26
|
+
not compile. Nothing fires while the page is edited. Fire whenever there is news — from the first effect too: a flow
|
|
27
|
+
starts once the page has finished mounting, so the `setState` it calls is there. No `queueMicrotask` or delay of your
|
|
28
|
+
own. An event that REPORTS a state, and may fire again before its flow ends (as a
|
|
23
29
|
component reads its real value right after mounting), wants `whileRunning('queue', …)` on the flow: by default a firing
|
|
24
30
|
while the flow runs is dropped, and the stale first report would stick.
|
|
25
31
|
|
|
26
32
|
## Props
|
|
27
33
|
|
|
28
|
-
**The
|
|
34
|
+
**The element's attributes ARE the component's props.** Whatever the space writes on it arrives
|
|
29
35
|
by the same name, and so does whatever a binding writes — which is what makes a plugin live: bind an attribute to
|
|
30
36
|
`state`, a provider or a row, and the component re-renders with the answer. Prefer this to anything else.
|
|
31
37
|
|
|
@@ -34,6 +40,8 @@ panel edits for a plugin it has no controls for; from code, write attributes. A
|
|
|
34
40
|
"Settings Malformed" in the page.
|
|
35
41
|
|
|
36
42
|
Every prop is optional with a default: an attribute not authored yet, or a binding not answered yet, is `undefined`.
|
|
43
|
+
One whose declared default is a number or a boolean arrives as one, written or bound: type it `number`, never
|
|
44
|
+
`number | string`.
|
|
37
45
|
|
|
38
46
|
## The component
|
|
39
47
|
|
|
@@ -73,7 +81,7 @@ on ([kept state](kept-state.md)), what a plugin writes there — its layout, a c
|
|
|
73
81
|
## Laying out the space's elements
|
|
74
82
|
|
|
75
83
|
A plugin that arranges elements — a dock, tabs, a masonry — HOLDS them: they are its children in the space
|
|
76
|
-
(`
|
|
84
|
+
(`dock({ id: 'dock', children: [feed, tools] })`, or dropped into it in the builder), and the component
|
|
77
85
|
places each in a box of its own, by the id it was authored under:
|
|
78
86
|
|
|
79
87
|
```tsx
|
|
@@ -138,16 +146,15 @@ with thousands of shapes fast — see [drawing](drawing.md).
|
|
|
138
146
|
## Registering
|
|
139
147
|
|
|
140
148
|
A project `plitzi create` wrote registers every folder of `src/plugins` by itself, under the folder's name in
|
|
141
|
-
camelCase (`StatCard` → `statCard`). Anywhere else, the entry registers the component under its
|
|
142
|
-
third argument to `render()`, `<PlitziSdk.Plugin>` in a React application, `plugins` on a page server of your own. A
|
|
143
|
-
renders "Custom Component … Not Found", and a page server logs
|
|
144
|
-
|
|
149
|
+
camelCase (`StatCard` → `statCard`): its type. Anywhere else, the entry registers the component under its type: the
|
|
150
|
+
third argument to `render()`, `<PlitziSdk.Plugin>` in a React application, `plugins` on a page server of your own. A
|
|
151
|
+
type nothing registered renders "Custom Component … Not Found", and a page server logs it at `error` once per space:
|
|
152
|
+
on a server, register it in `plugins` AND name it in the deployment's `pluginNames`.
|
|
145
153
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
`defineElement
|
|
149
|
-
|
|
150
|
-
`element('seatPicker', { id: 'seats', start: 3 })`.
|
|
154
|
+
On Plitzi, every element loads from the plugin the space installed (`plitzi plugin pack` and `upload`: several folders
|
|
155
|
+
are one plugin). A plugin PACKAGE is placed the same way:
|
|
156
|
+
`defineElement` from its `declaration.ts`, or `elementsFromManifest<{ seatPicker: SeatPickerAttributes }>(manifest)`
|
|
157
|
+
from what it published.
|
|
151
158
|
|
|
152
159
|
## Checked like a built-in element
|
|
153
160
|
|
|
@@ -160,11 +167,10 @@ import declaration from './plugins/SeatPicker/declaration';
|
|
|
160
167
|
authorSpace(space, { plugins: [declaration] });
|
|
161
168
|
```
|
|
162
169
|
|
|
163
|
-
It is then held to what it declares
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
judged on its events at all — nothing here knows them.
|
|
170
|
+
It is then held to what it declares: a flow on an event it never fires, a step sent to an action it does not answer,
|
|
171
|
+
an attribute it does not read — each is refused with the name it should have been. (`pluginTypes: ['seatPicker']`
|
|
172
|
+
only tells the linter the type exists; nothing about how the space uses it is checked.) A plugin whose declaration is
|
|
173
|
+
NOT handed over is not judged on its events at all — nothing here knows them.
|
|
168
174
|
|
|
169
175
|
Its events and actions have builders typed from the same declaration, so a name it does not declare is a compile error:
|
|
170
176
|
|
|
@@ -175,9 +181,9 @@ button({ content: 'Clear', flows: [[onClick(), declaredCallback(declaration, 're
|
|
|
175
181
|
|
|
176
182
|
A plugin says what HAPPENED through its events (`onPick`, with the seat in the payload) and lets the space's flows
|
|
177
183
|
decide what that means — write `state`, open a modal, call a server action. Prefer that to writing `state` from inside
|
|
178
|
-
the component: the flow is visible in the space,
|
|
184
|
+
the component: the flow is visible in the space, and a button can do the same.
|
|
179
185
|
|
|
180
|
-
-
|
|
186
|
+
- An action's param declared `number` or `boolean` arrives as one, written or bound; any other arrives as written.
|
|
181
187
|
- To act when another element appears, `useElementVisible('tools')` (`@plitzi/plitzi-sdk`) — never watch classes.
|
|
182
188
|
- A name for screen readers is a prop of its own: declare `label`, write it as the root's `aria-label`.
|
|
183
189
|
|
|
@@ -186,14 +192,14 @@ the component: the flow is visible in the space, the builder shows it, and the s
|
|
|
186
192
|
The builder draws the element on its canvas while somebody edits the page: a click selects it, a drag moves it, a key
|
|
187
193
|
belongs to the editor.
|
|
188
194
|
|
|
189
|
-
- **Outside preview, do nothing on your own.** `
|
|
190
|
-
page is edited
|
|
191
|
-
|
|
195
|
+
- **Outside preview, do nothing on your own.** `usePlitzi().settings.previewMode` is `false` while the
|
|
196
|
+
page is edited: gate your own click handlers, timers, listeners, permission prompts and map gestures on it, and
|
|
197
|
+
still render something to select.
|
|
192
198
|
- **A drag lives in state, not in refs read while rendering** (the project's lint refuses that): keep the gesture
|
|
193
199
|
in `useState`, mirror the latest props into a ref inside an effect, and attach `pointermove` on `pointerdown`.
|
|
194
200
|
- **Never the global `window` or `document`.** The canvas is a frame of its own and the code runs in the builder's
|
|
195
201
|
window: listen, measure and go full screen through the node's own page (`ref.current.ownerDocument`, its
|
|
196
|
-
`defaultView`) or `
|
|
202
|
+
`defaultView`) or `usePlitzi().utils.getWindow()`. An `instanceof` check takes its class from there too.
|
|
197
203
|
|
|
198
204
|
## The plugin's stylesheet and the space's CSS
|
|
199
205
|
|
|
@@ -209,5 +215,4 @@ fonts a stylesheet names are carried the same way.
|
|
|
209
215
|
A map, a chart library, anything that positions its own markers or popups: its roots are the library's to place. Never
|
|
210
216
|
give them a class that sets `position` — the element falls into the page's flow, offset by every marker before it. Put
|
|
211
217
|
the look in the plugin's own stylesheet (imported CSS ships beside the bundle) and take colours from custom properties
|
|
212
|
-
the space sets (`--seat-accent: var(--accent)` in `customCss`)
|
|
213
|
-
element with `getComputedStyle(probe).color`, again whenever `theme.resolved` changes (bind it as a prop).
|
|
218
|
+
the space sets (`--seat-accent: var(--accent)` in `customCss`).
|
|
@@ -50,7 +50,7 @@ refuses or warns of, with the fix.
|
|
|
50
50
|
every switch through `computed` (`plates: '{{ state.platesOff ? false : true }}'`) so its default shows before
|
|
51
51
|
anybody touches it.
|
|
52
52
|
12. **Hand plugins over as their declarations**: `authorSpace(space, { plugins: [declaration] })` holds a plugin —
|
|
53
|
-
|
|
53
|
+
placed with `defineElement(declaration)` — to the events, actions and attributes it declares. See
|
|
54
54
|
[plugins](plugins.md).
|
|
55
55
|
13. **Usable without sight.** Screen readers and browser agents (Claude in Chrome) find a page's controls in its
|
|
56
56
|
accessibility tree: every button and link has words (an icon-only button a `title`), every field a `label`
|