@plitzi/sdk-server 0.38.11 → 0.38.13

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,122 @@
1
1
  # @plitzi/sdk-server
2
2
 
3
+ ## 0.38.13
4
+
5
+ ### Patch Changes
6
+
7
+ - v0.38.13
8
+ - Updated dependencies
9
+ - @plitzi/plitzi-sdk@0.38.13
10
+ - @plitzi/sdk-shared@0.38.13
11
+
12
+ ## 0.38.12
13
+
14
+ ### Patch Changes
15
+
16
+ - 1301338: ## Builder
17
+
18
+ - **Icons in the canvas again** (`@plitzi/plitzi-builder`): the canvas inlined the SDK's Font Awesome sheet into its
19
+ `<style>`, where the sheet's `url(webfonts/…)` resolved against the editor's URL — a `srcdoc` frame takes it as its
20
+ base — so every icon was a 404 under `/spaces/<space>/webfonts/` and drew as an empty box. Only the Vite dev server,
21
+ which rewrites the URL, hid it. The canvas and the element preview now link the sheet from where the host serves it:
22
+ a new prop, `sdkIconsStylePath`, the one the host already gives the pages it renders
23
+ (`/sdk-assets/plitzi-sdk-icons.css` for sdk-server). Absent, the canvas links no icon sheet.
24
+
25
+ ## Plugins
26
+ - **A plugin's other elements, hosted by `custom`** (`@plitzi/sdk-elements`): `custom({ renderType: 'jamDisc' })` on a
27
+ space with a plugin installed whose main element is another (`plitzi plugin pack` of several folders: the first is the
28
+ plugin, the rest go in it) rendered "Custom Component jamDisc Not Found" — the page registers only the plugin's main
29
+ element, and a `custom` host looked nowhere else. It now loads the type from the installed plugin that packs it, as
30
+ an element of the type itself always did, so the two ways the docs give to place a plugin's element are the same
31
+ everywhere: in a project `plitzi create` wrote, and once the project is on Plitzi.
32
+ - **One way to place a plugin's element** (`@plitzi/sdk-authoring`, `@plitzi/cli`, docs): from its declaration,
33
+ `const seatPicker = defineElement(declaration)` then `seatPicker({ id, … })` — typed by what it
34
+ declares, and the element the builder adds when somebody drops it. The welcome space `plitzi create` writes, the
35
+ plugin package's preview, `plitzi plugin add` and `plitzi explain` all write it so (the template's
36
+ `PluginHostOptions` now takes the plugin's `type` and `declaration: { from, attributes? }`; `custom`/`element` hosting
37
+ is gone). `pluginFolders(folder)` (`@plitzi/sdk-authoring/node`) reads each plugin with the folder it is in;
38
+ `pluginDeclarations` is built on it. A `custom({ renderType })` naming a plugin the space was handed the declaration
39
+ of is offered the declaration instead — suggestion `plugin-custom-host`. `custom` stays for a component registered
40
+ by name with no declaration. The skills, the CLI README, the MCP guide, `docs/en` and the website say the same.
41
+ - **`definePlugin`** (`@plitzi/sdk-authoring/plugin`, a new entry of 3 KB with nothing else in it): a plugin's
42
+ declaration written from what only it can say —
43
+ `definePlugin<SeatPickerAttributes>()({ type, label, attributes, triggers, callbacks })` — and every other field
44
+ at the default all plugins share (the builder's gestures, the catalogue entry, each attribute bindable, an empty
45
+ style). It answers the whole declaration, branded with its attributes, so `defineElement(declaration)` is typed
46
+ without a generic. `plitzi plugin add` and `create --plugin` write declarations with it (about a third of the lines).
47
+ - **`usePluginTrigger(declaration)`** (`@plitzi/plitzi-sdk`): fires a plugin's declared event on its element — the
48
+ event name and what each hands a flow typed off the declaration's `preview` — on a live page only. The hook every
49
+ plugin wrote for itself (and `plugin add` generated as `use<Name>Events`) is gone from the scaffold.
50
+ - **Attributes as the types their defaults are** (`@plitzi/sdk-elements`): a plugin's attribute bound from text — a
51
+ state, a row, a channel message — reaches its component as the number or the flag its declared default is (`'112'`
52
+ as 112, `'true'` as true). A plugin no longer types its props `number | string` and parses them.
53
+
54
+ ## Server actions and functions
55
+ - **Task params with rules** (`@plitzi/sdk-server`, `@plitzi/sdk-shared`): a task declares `required`, a text's
56
+ `maxLength` and a number's `min`/`max`, and the runner refuses a call that breaks any of them — every rule broken at
57
+ once, in the words the page shows (`Title is required; Seconds is at most 300`) — before the code runs. A `text` param
58
+ bound to a number arrives as its digits. The manifest keeps the rules, so the platform's sandbox checks the same.
59
+ - **`taskAction(task, { trigger })`** (`@plitzi/sdk-authoring`): the action that runs one of the space's tasks and
60
+ answers its result, derived from the task — its id (`<namespace>-<action>`), name, description and input (the task's
61
+ params, once). An action of more than one step is still `defineAction`.
62
+ - **`ctx.rateLimit(bucket, { …, refuse })`**: past the limit the call is refused with those words instead of answering
63
+ `allowed: false` for the code to check — natively and in the sandbox.
64
+
65
+ ## Authoring
66
+ - **`runServerActionOrNotify(id, params, fallback)`**: a server action run and the notice the visitor reads when it
67
+ fails — the action's own refusal, or the fallback — as two steps to spread into a flow.
68
+ - **`repeated-shape` reads what copies bind**: copies whose bindings read their data in different forms (a count, a
69
+ total of a quota) and copies holding a provider resolved on the server (found by its own id) are no longer offered
70
+ as one component.
71
+ - **`element-color-inherited`** (suggestion): `elements` giving a heading, a text, a link… `color: inherit` or the
72
+ page's own colour. **A link inherits the page's colour** (`@plitzi/plitzi-sdk`, `@plitzi/sdk-elements`) rather than
73
+ holding the SDK's grey — the same by default, and the space's palette inside a card that is a link. The template
74
+ every space starts from writes its colours once, on the page.
75
+
76
+ ## Fixes
77
+ - **A dialog opened with an id** (`@plitzi/sdk-elements`): `openDialog('dialog', '{{ row.id }}')` reads the id as its
78
+ `content`, as `openModal` already did — a numeric id parsed as the number and the dialog's bindings read nothing.
79
+ - **A select's chevron** (`@plitzi/plitzi-sdk`): a drawn chevron in the field's own colour, sized by its type, in place
80
+ of the `▼` glyph — and laid out in the box, so it ends where the box's padding begins whatever that padding is, and
81
+ the chosen option stops short of it instead of running under it. A disabled select's is faded.
82
+ - **Every element has its settings** (`@plitzi/plitzi-builder`, `@plitzi/sdk-elements`): the builder found none for
83
+ `tabContainerHeader` (registered under a misspelt type), `themeToggle`, `dropdownPopup`, `loading` and `notFound`.
84
+ The theme toggle's panel sets its mode, its labels and whether it offers "System"; the registry is typed by element
85
+ type and must hold one for each.
86
+ - **A plugin's panel** (`@plitzi/cli`): `plitzi doctor` warns of a plugin with no `Settings.tsx`, or one its entry never
87
+ passes as `pluginSettings` (`plugin-settings-missing`); the example `create` writes has its panel, as every element
88
+ `plugin add` writes does, and the panels it generates are laid out as the project's Prettier lays them out.
89
+ - **A refused or failed run reaches `onFlowError` in every mode** (`@plitzi/sdk-interactions`): an awaited server
90
+ action the server refused fired no `onFlowError` on the element that started it, as `detached` and `stream` ones do
91
+ — a board rolling its edits back on a refusal never did; and a run answered `status: 'failed'` fired `onFlowEnd`
92
+ (detached) or nothing (awaited) rather than `onFlowError`.
93
+ - **A source renamed** (`@plitzi/sdk-shared`): the binding picker lists a provider, a form or a modal by the name its
94
+ element has now, not the one it had when it mounted.
95
+ - **A text's words in the builder** (`@plitzi/sdk-elements`): a `paragraph`, `text` or `heading` whose `content` is
96
+ `0` shows `0` while editing too, not its placeholder; one bound to nothing shows nothing rather than `null`.
97
+ - **Built-in declarations** (`@plitzi/sdk-shared`): `elementDeclaration` fills in what every element shares — the
98
+ builder's gestures, Plitzi's catalogue entry, a visible element with no bindings, its style named by its label —
99
+ from the defaults `definePlugin` uses; each element's declaration says only what is its own (a third of the lines).
100
+
101
+ ## Typed for plugins and hosts
102
+ - **`usePlitzi()` and `PlitziProvider`** (`@plitzi/sdk-shared/hooks/usePlitzi`, `@plitzi/plitzi-sdk`): what were
103
+ `usePlitziServiceContext()` and `PlitziServiceProvider` (and `PlitziServiceContextValue`, now `PlitziContextValue`),
104
+ holding only what the host rendering a space knows — `settings`, `root`, `utils`. Its `contexts` and
105
+ `customContexts` are gone: every context it carried is a shared one already, imported from its own module
106
+ (`@plitzi/sdk-interactions/InteractionsContext`, `@plitzi/sdk-shared/network/NetworkContext`…), and
107
+ `PluginsContext` moves to `@plitzi/sdk-shared/plugins/PluginsContext` beside the others.
108
+ - **`InteractionsManagerApi`** (`@plitzi/sdk-shared`): the interactions manager as everything outside
109
+ `@plitzi/sdk-interactions` reaches it, declared where every package can name it and implemented by
110
+ `InteractionsManager`, so `use(InteractionsContext)` is typed — `interactionTrigger`, `createChildManager`… — where it
111
+ was `any`.
112
+ - **`getPathsFromObject`** (`@plitzi/sdk-shared/helpers/utils`), spelt right; `getPathsFromObeject` is gone.
113
+ - **`ElementWords` places itself** (`@plitzi/sdk-elements`): `contentPlacement` and the element's children, so a button
114
+ and a link write their words once.
115
+
116
+ - Updated dependencies [1301338]
117
+ - @plitzi/plitzi-sdk@0.38.12
118
+ - @plitzi/sdk-shared@0.38.12
119
+
3
120
  ## 0.38.11
4
121
 
5
122
  ### Patch Changes
@@ -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). The
155
- * browser reads a callback's params the same way (`coerceDeclaredParams`): a task's author never writes `Number(…)`.
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
- var withDefaults = (task, params) => Object.entries(task.params).reduce((acum, [key, param]) => {
166
- const value = asDeclared(typeof param.type === "function" ? param.type(acum) : param.type, acum[key]);
167
- acum[key] = value === void 0 || value === "" ? param.defaultValue ?? "" : value;
168
- return acum;
169
- }, { ...params });
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) => call({
111
- op: "rateLimit",
112
- bucket,
113
- limit
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 name a space's `custom({ renderType })` renders it by. */
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.11",
3
+ "version": "0.38.13",
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.11",
100
- "@plitzi/sdk-shared": "0.38.11",
99
+ "@plitzi/plitzi-sdk": "0.38.13",
100
+ "@plitzi/sdk-shared": "0.38.13",
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
- custom({ renderType: 'myPlugin' }); defineElement<Props>(declaration) // a plugin
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: `import declaration from './plugins/seatPicker/declaration';`
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
- triggers: { onPick: { action: 'onPick', title: 'On Pick', type: 'trigger', params: {}, preview: { seat: '' } } },
21
- callbacks: { reset: { action: 'reset', title: 'Reset', type: 'callback', params: {} } },
22
- content: { attributes: { rows: 10 }, definition: { label: 'Seat Picker' } }
23
- } as const;
21
+ label: 'Seat Picker',
22
+ attributes: { rows: 10 },
23
+ triggers: { onPick: { preview: { seat: '' } } },
24
+ callbacks: { reset: {} }
25
+ });
24
26
 
25
- const seatPicker = defineElement<{ rows?: number }>(declaration);
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<Props>({ type, … })` — see [plugins](plugins.md).
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. (JSON text works — a value inside one of its strings
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. Each is the `setState` it stands for, with the arithmetic written once.
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 — hosted in the space by a `custom`
4
- element that names it by `renderType`:
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
- custom({ id: 'arcade', renderType: 'nebulaRun', shipColor: 'amber', bind: { best: 'state.arcadeBest' } })
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 `type`, the `triggers` it fires, the
16
- `callbacks` it answers to, and the element the builder adds — data only), `Settings.tsx` (its panel in the builder), and
17
- `index.ts` (`Object.assign(Component, declaration, { pluginSettings: Settings })`). An event or an action is declared in
18
- `declaration.ts` and registered by the component from there, never only in the component.
19
-
20
- A plugin fires its events with `interactionsManager.interactionTrigger(id, action, payload)` whenever it has news —
21
- from its first effect too: a flow starts once the page has finished mounting, so the `setState` it calls is there. No
22
- `queueMicrotask` or delay of your own. An event that REPORTS a state, and may fire again before its flow ends (as a
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 host element's attributes ARE the component's props.** Whatever the space writes on the `custom` element arrives
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
- (`custom({ renderType: 'dock', children: [feed, tools] })`, or dropped into it in the builder), and the component
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 `renderType`: the
142
- third argument to `render()`, `<PlitziSdk.Plugin>` in a React application, `plugins` on a page server of your own. A `renderType` nothing registered
143
- renders "Custom Component … Not Found", and a page server logs the missing `renderType` at `error` once per
144
- space: on a server, register it in `plugins` AND name it in the deployment's `pluginNames`.
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
- A plugin PACKAGE, loaded by a space from its `plugin-manifest.json`, is an element TYPE of its own rather than a
147
- `custom` host — it is how the builder adds one somebody dropped. Author it with a typed factory:
148
- `defineElement<SeatPickerAttributes>(declaration)` from the plugin's own `declaration.ts`, or
149
- `elementsFromManifest<{ seatPicker: SeatPickerAttributes }>(manifest)` from what it published — or untyped,
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, whether it is authored as its own type or hosted by `custom({ renderType:
164
- 'seatPicker' })`: a flow on an event it never fires, a step sent to an action it does not answer, an attribute it does
165
- not read — each is refused with the name it should have been. (`pluginTypes: ['seatPicker']` only tells the linter the
166
- type exists; nothing about how the space uses it is checked.) A `custom` host whose component is NOT handed over is not
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, the builder shows it, and the same act can come from a button too.
184
+ the component: the flow is visible in the space, and a button can do the same.
179
185
 
180
- - A param declared `number` or `boolean` arrives as one, written or bound; any other arrives as written.
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.** `usePlitziServiceContext().settings.previewMode` is `false` while the
190
- page is edited. Declared interactions are already held back then; your own click handlers, timers, global listeners,
191
- permission prompts and map gestures are not — gate them on `previewMode`, and still render something to select.
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 `usePlitziServiceContext().utils.getWindow()`. An `instanceof` check takes its class from there too.
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`); a canvas or WebGL layer resolves them through a probe
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
- its own type, or a `custom({ renderType })` host — to the events, actions and attributes it declares. See
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`