@plitzi/sdk-server 0.37.2 → 0.37.4
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 +1170 -0
- package/README.md +100 -7
- package/dist/actions.d.ts +6 -2
- package/dist/actions.js +5 -2
- package/dist/adapters/cloudAdapters.js +4 -3
- package/dist/adapters/jsonAdapters.js +28 -6
- package/dist/adapters/jsonAdapters.test.d.ts +1 -0
- package/dist/auth.d.ts +7 -4
- package/dist/auth.js +8 -6
- package/dist/core/auth/api.d.ts +29 -1
- package/dist/core/auth/api.js +47 -13
- package/dist/core/auth/createAuth.d.ts +4 -0
- package/dist/core/auth/createAuth.js +3 -3
- package/dist/core/auth/credentials.d.ts +3 -0
- package/dist/core/auth/credentials.js +1 -1
- package/dist/core/auth/csrf.d.ts +25 -1
- package/dist/core/auth/csrf.js +29 -4
- package/dist/core/auth/renewal.d.ts +26 -0
- package/dist/core/auth/renewal.js +24 -0
- package/dist/core/auth/routes.js +8 -6
- package/dist/core/auth/session.d.ts +45 -0
- package/dist/core/auth/session.js +47 -1
- package/dist/core/auth/sessionActivity.d.ts +11 -0
- package/dist/core/auth/sessionActivity.js +14 -0
- package/dist/core/auth/throttle.d.ts +15 -0
- package/dist/core/auth/throttle.js +29 -1
- package/dist/core/auth/totp.d.ts +9 -1
- package/dist/core/auth/totp.js +11 -6
- package/dist/core/createServer.actions.test.d.ts +1 -0
- package/dist/core/createServer.d.ts +14 -1
- package/dist/core/createServer.functions.test.d.ts +1 -0
- package/dist/core/createServer.js +22 -12
- package/dist/core/fontStore.js +1 -1
- package/dist/core/handlers/authRouteHandlers.d.ts +1 -1
- package/dist/core/handlers/authRouteHandlers.js +57 -21
- package/dist/core/handlers/csrfMiddleware.d.ts +23 -0
- package/dist/core/handlers/csrfMiddleware.js +31 -7
- package/dist/core/handlers/csrfMiddleware.test.d.ts +1 -0
- package/dist/core/handlers/oauthRouteHandlers.js +5 -3
- package/dist/core/handlers/socialRouteHandlers.d.ts +1 -1
- package/dist/core/handlers/socialRouteHandlers.js +2 -1
- package/dist/core/http/dispatcher.d.ts +9 -1
- package/dist/core/http/dispatcher.js +38 -11
- package/dist/core/http/navigation.d.ts +5 -2
- package/dist/core/http/navigation.js +7 -4
- package/dist/core/http/socketResponse.d.ts +18 -0
- package/dist/core/http/socketResponse.js +59 -0
- package/dist/core/http/sse.d.ts +26 -0
- package/dist/core/http/sse.js +78 -0
- package/dist/core/http/stages/authRoutes.js +3 -1
- package/dist/core/http/stages/exchangeRoute.test.d.ts +1 -0
- package/dist/core/http/stages/middlewares.d.ts +1 -1
- package/dist/core/http/stages/middlewares.js +16 -10
- package/dist/core/http/stages/middlewares.test.d.ts +1 -0
- package/dist/core/http/stages/oauth.js +2 -2
- package/dist/core/http/stages/sessionRenewal.d.ts +16 -0
- package/dist/core/http/stages/sessionRenewal.js +81 -0
- package/dist/core/http/stages/sessionRenewal.test.d.ts +1 -0
- package/dist/core/http/stages/signInRoute.d.ts +2 -0
- package/dist/core/http/stages/signInRoute.js +139 -0
- package/dist/core/http/stages/signInRoute.test.d.ts +1 -0
- package/dist/core/http/types.d.ts +9 -0
- package/dist/core/http/webExchange.d.ts +18 -0
- package/dist/core/http/webExchange.js +53 -0
- package/dist/core/oauth/authorize.js +37 -13
- package/dist/core/oauth/consentPage.js +11 -0
- package/dist/core/oauth/issueContext.d.ts +14 -0
- package/dist/core/oauth/issueContext.js +32 -0
- package/dist/core/oauth/records.d.ts +7 -0
- package/dist/core/oauth/register.d.ts +2 -0
- package/dist/core/oauth/register.js +32 -11
- package/dist/core/oauth/register.test.d.ts +1 -0
- package/dist/core/oauth/token.js +21 -6
- package/dist/core/oauth/token.test.d.ts +1 -0
- package/dist/core/requestParser.d.ts +4 -0
- package/dist/core/requestParser.js +14 -7
- package/dist/core/sdkAssets.js +1 -1
- package/dist/core/server/baseServer.d.ts +10 -0
- package/dist/core/server/baseServer.js +42 -11
- package/dist/core/server/closeOnSignals.js +4 -3
- package/dist/core/server/drain.d.ts +18 -0
- package/dist/core/server/drain.js +62 -0
- package/dist/core/server/drain.test.d.ts +1 -0
- package/dist/core/server/fleet/channel.d.ts +103 -0
- package/dist/core/server/fleet/channel.js +191 -0
- package/dist/core/server/fleet/channel.test.d.ts +1 -0
- package/dist/core/server/fleet/fixtures/fleetServer.d.ts +1 -0
- package/dist/core/server/fleet/fleet.integration.test.d.ts +1 -0
- package/dist/core/server/fleet/link.d.ts +23 -0
- package/dist/core/server/fleet/link.js +70 -0
- package/dist/core/server/fleet/relay.d.ts +11 -0
- package/dist/core/server/fleet/relay.js +104 -0
- package/dist/core/server/fleet/relay.test.d.ts +1 -0
- package/dist/core/server/fleet/role.d.ts +46 -0
- package/dist/core/server/fleet/role.js +59 -0
- package/dist/core/server/fleet/role.test.d.ts +1 -0
- package/dist/core/server/fleet/stores.d.ts +7 -0
- package/dist/core/server/fleet/stores.js +19 -0
- package/dist/core/server/fleet/supervisor.d.ts +26 -0
- package/dist/core/server/fleet/supervisor.js +108 -0
- package/dist/core/server/fleet/supervisor.test.d.ts +1 -0
- package/dist/core/server/pageServer.js +34 -5
- package/dist/core/services/action.js +10 -2
- package/dist/core/services/functionRoutes.d.ts +9 -0
- package/dist/core/services/functionRoutes.js +47 -0
- package/dist/core/services/realtime.d.ts +8 -0
- package/dist/core/services/realtime.js +57 -0
- package/dist/core/services/registry.js +6 -2
- package/dist/core/staticFiles.js +12 -4
- package/dist/core/staticFiles.test.d.ts +1 -0
- package/dist/core/transports.d.ts +13 -3
- package/dist/core/transports.js +14 -5
- package/dist/functions-api.d.ts +281 -0
- package/dist/functions.d.ts +19 -0
- package/dist/functions.js +5 -0
- package/dist/functionsRunner.d.ts +15 -0
- package/dist/functionsRunner.js +6 -0
- package/dist/handlers.d.ts +2 -2
- package/dist/handlers.js +2 -2
- package/dist/helpers/atomicFile.d.ts +10 -0
- package/dist/helpers/atomicFile.js +33 -0
- package/dist/helpers/atomicFile.test.d.ts +1 -0
- package/dist/helpers/buildResponseHelpers.js +35 -11
- package/dist/helpers/buildServerInfo.js +4 -2
- package/dist/helpers/buildServerInfo.test.d.ts +1 -0
- package/dist/helpers/cache/CompressedFileCache.d.ts +18 -0
- package/dist/helpers/cache/CompressedFileCache.js +44 -0
- package/dist/helpers/cache/CompressedFileCache.test.d.ts +1 -0
- package/dist/helpers/cache/cacheManager.d.ts +1 -1
- package/dist/helpers/cache/cacheManager.js +2 -1
- package/dist/helpers/cache/cacheManager.test.d.ts +1 -0
- package/dist/helpers/cache/index.d.ts +1 -1
- package/dist/helpers/cache/keys.d.ts +15 -1
- package/dist/helpers/cache/keys.js +47 -5
- package/dist/helpers/cache/serverCaches.d.ts +7 -1
- package/dist/helpers/compress.d.ts +3 -1
- package/dist/helpers/compress.js +11 -3
- package/dist/helpers/hydrationPayload.d.ts +11 -0
- package/dist/helpers/hydrationPayload.js +51 -0
- package/dist/helpers/hydrationPayload.test.d.ts +1 -0
- package/dist/helpers/metrics.d.ts +1 -1
- package/dist/helpers/metrics.js +3 -2
- package/dist/helpers/outboundGuard.d.ts +14 -6
- package/dist/helpers/outboundGuard.js +132 -8
- package/dist/helpers/paintedCookie.d.ts +8 -0
- package/dist/helpers/paintedCookie.js +13 -0
- package/dist/helpers/reportReactBuild.d.ts +9 -0
- package/dist/helpers/reportReactBuild.js +18 -0
- package/dist/helpers/reportReactBuild.test.d.ts +1 -0
- package/dist/helpers/serverLog.d.ts +25 -1
- package/dist/helpers/serverLog.js +68 -3
- package/dist/helpers/serverLog.test.d.ts +1 -0
- package/dist/helpers/wsFrame.d.ts +3 -0
- package/dist/helpers/wsFrame.js +5 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -2
- package/dist/kernel.d.ts +2 -1
- package/dist/kernel.js +3 -2
- package/dist/modules/actions/connectorWrite.js +2 -1
- package/dist/modules/actions/index.d.ts +29 -10
- package/dist/modules/actions/index.js +43 -6
- package/dist/modules/actions/jobs/failureStreak.d.ts +17 -0
- package/dist/modules/actions/jobs/failureStreak.js +47 -0
- package/dist/modules/actions/jobs/failureStreak.test.d.ts +1 -0
- package/dist/modules/actions/jobs/index.d.ts +2 -1
- package/dist/modules/actions/jobs/index.js +5 -3
- package/dist/modules/actions/jobs/memoryQueue.d.ts +3 -0
- package/dist/modules/actions/jobs/memoryQueue.js +17 -1
- package/dist/modules/actions/jobs/scheduler.d.ts +2 -0
- package/dist/modules/actions/jobs/scheduler.js +12 -7
- package/dist/modules/actions/jobs/worker.d.ts +3 -1
- package/dist/modules/actions/jobs/worker.js +9 -4
- package/dist/modules/actions/moduleFor.js +5 -2
- package/dist/modules/actions/runtime/errors.d.ts +12 -0
- package/dist/modules/actions/runtime/errors.js +16 -1
- package/dist/modules/actions/runtime/kvChange.d.ts +27 -0
- package/dist/modules/actions/runtime/kvChange.js +23 -0
- package/dist/modules/actions/runtime/kvList.d.ts +51 -0
- package/dist/modules/actions/runtime/kvList.js +52 -0
- package/dist/modules/actions/runtime/kvStore.d.ts +8 -0
- package/dist/modules/actions/runtime/kvStore.js +46 -20
- package/dist/modules/actions/runtime/memoryKv.d.ts +3 -0
- package/dist/modules/actions/runtime/memoryKv.js +19 -1
- package/dist/modules/actions/runtime/namespaceKv.d.ts +4 -6
- package/dist/modules/actions/runtime/namespaceKv.js +15 -3
- package/dist/modules/actions/runtime/namespaceKv.test.d.ts +1 -0
- package/dist/modules/actions/runtime/precheck.js +4 -8
- package/dist/modules/actions/runtime/rateLimit.d.ts +19 -0
- package/dist/modules/actions/runtime/rateLimit.js +19 -0
- package/dist/modules/actions/runtime/redisKv.d.ts +27 -0
- package/dist/modules/actions/runtime/redisKv.integration.test.d.ts +1 -0
- package/dist/modules/actions/runtime/redisKv.js +46 -0
- package/dist/modules/actions/runtime/report.js +2 -1
- package/dist/modules/actions/runtime/runAction.d.ts +10 -2
- package/dist/modules/actions/runtime/runAction.js +52 -116
- package/dist/modules/actions/runtime/runLogger.d.ts +5 -0
- package/dist/modules/actions/runtime/runLogger.js +8 -2
- package/dist/modules/actions/runtime/signing.d.ts +20 -0
- package/dist/modules/actions/runtime/signing.js +25 -0
- package/dist/modules/actions/runtime/signing.test.d.ts +1 -0
- package/dist/modules/actions/runtime/taskContext.d.ts +33 -0
- package/dist/modules/actions/runtime/taskContext.js +124 -0
- package/dist/modules/actions/tasks/builtins.js +5 -1
- package/dist/modules/actions/tasks/flow.js +81 -10
- package/dist/modules/actions/tasks/helpers.js +2 -2
- package/dist/modules/actions/tasks/http.js +2 -3
- package/dist/modules/actions/tasks/kv.js +39 -0
- package/dist/modules/actions/tasks/list.d.ts +2 -0
- package/dist/modules/actions/tasks/list.js +145 -0
- package/dist/modules/actions/tasks/realtime.d.ts +2 -0
- package/dist/modules/actions/tasks/realtime.js +104 -0
- package/dist/modules/actions/tasks/registry.d.ts +10 -1
- package/dist/modules/actions/tasks/registry.js +17 -6
- package/dist/modules/actions/transport/callHandler.js +5 -2
- package/dist/modules/actions/transport/catalogHandler.d.ts +5 -2
- package/dist/modules/actions/transport/catalogHandler.js +3 -2
- package/dist/modules/actions/transport/stream.d.ts +4 -5
- package/dist/modules/actions/transport/stream.js +12 -76
- package/dist/modules/actions/transport/webhookHandler.js +4 -3
- package/dist/modules/actions/types.d.ts +86 -3
- package/dist/modules/connectors/engine.js +3 -5
- package/dist/modules/functions/build.d.ts +22 -0
- package/dist/modules/functions/build.js +122 -0
- package/dist/modules/functions/build.test.d.ts +1 -0
- package/dist/modules/functions/capabilities.d.ts +22 -0
- package/dist/modules/functions/capabilities.js +228 -0
- package/dist/modules/functions/config.d.ts +34 -0
- package/dist/modules/functions/config.js +24 -0
- package/dist/modules/functions/context.d.ts +17 -0
- package/dist/modules/functions/context.js +88 -0
- package/dist/modules/functions/contract.d.ts +121 -0
- package/dist/modules/functions/contract.js +9 -0
- package/dist/modules/functions/driver.d.ts +36 -0
- package/dist/modules/functions/driver.js +150 -0
- package/dist/modules/functions/load.d.ts +15 -0
- package/dist/modules/functions/load.js +45 -0
- package/dist/modules/functions/load.test.d.ts +1 -0
- package/dist/modules/functions/local.d.ts +29 -0
- package/dist/modules/functions/local.js +61 -0
- package/dist/modules/functions/local.test.d.ts +1 -0
- package/dist/modules/functions/manifest.d.ts +24 -0
- package/dist/modules/functions/manifest.js +163 -0
- package/dist/modules/functions/manifest.test.d.ts +1 -0
- package/dist/modules/functions/native.d.ts +8 -0
- package/dist/modules/functions/native.js +21 -0
- package/dist/modules/functions/native.test.d.ts +1 -0
- package/dist/modules/functions/protocol.d.ts +216 -0
- package/dist/modules/functions/protocol.js +18 -0
- package/dist/modules/functions/routes.d.ts +36 -0
- package/dist/modules/functions/routes.js +181 -0
- package/dist/modules/functions/routes.test.d.ts +1 -0
- package/dist/modules/functions/runner/messages.d.ts +5 -0
- package/dist/modules/functions/runner/messages.js +188 -0
- package/dist/modules/functions/runner/remote.d.ts +19 -0
- package/dist/modules/functions/runner/remote.js +116 -0
- package/dist/modules/functions/runner/remote.test.d.ts +1 -0
- package/dist/modules/functions/runner/service.d.ts +32 -0
- package/dist/modules/functions/runner/service.js +209 -0
- package/dist/modules/functions/sandbox/guest.d.ts +45 -0
- package/dist/modules/functions/sandbox/guest.js +660 -0
- package/dist/modules/functions/sandbox/isolate.d.ts +37 -0
- package/dist/modules/functions/sandbox/isolate.js +347 -0
- package/dist/modules/functions/sandbox/isolate.test.d.ts +1 -0
- package/dist/modules/functions/sandbox/prelude.d.ts +2 -0
- package/dist/modules/functions/sandbox/prelude.js +49 -0
- package/dist/modules/functions/space.d.ts +40 -0
- package/dist/modules/functions/space.js +162 -0
- package/dist/modules/functions/testing/tasksOf.d.ts +4 -0
- package/dist/modules/functions/tryEntry.d.ts +9 -0
- package/dist/modules/functions/tryEntry.js +49 -0
- package/dist/modules/mongo/kv.js +31 -0
- package/dist/modules/mysql/accounts.js +19 -9
- package/dist/modules/mysql/index.d.ts +2 -1
- package/dist/modules/mysql/jobs/index.d.ts +1 -1
- package/dist/modules/mysql/jobs/kv.js +30 -0
- package/dist/modules/mysql/jobs/schema.d.ts +9 -0
- package/dist/modules/mysql/jobs/schema.js +21 -4
- package/dist/modules/mysql/schema.js +25 -0
- package/dist/modules/mysql/sessions.d.ts +1 -11
- package/dist/modules/mysql/sessions.js +6 -1
- package/dist/modules/realtime/declarations.d.ts +11 -0
- package/dist/modules/realtime/declarations.js +26 -0
- package/dist/modules/realtime/grants.d.ts +23 -0
- package/dist/modules/realtime/grants.js +61 -0
- package/dist/modules/realtime/handlers.d.ts +74 -0
- package/dist/modules/realtime/handlers.js +224 -0
- package/dist/modules/realtime/hub.d.ts +62 -0
- package/dist/modules/realtime/hub.js +141 -0
- package/dist/modules/realtime/index.d.ts +51 -0
- package/dist/modules/realtime/index.js +62 -0
- package/dist/modules/realtime/memoryPubSub.d.ts +10 -0
- package/dist/modules/realtime/memoryPubSub.js +43 -0
- package/dist/modules/realtime/realtime.test.d.ts +1 -0
- package/dist/modules/realtime/redisPubSub.d.ts +24 -0
- package/dist/modules/realtime/redisPubSub.js +36 -0
- package/dist/modules/realtime/redisPubSub.test.d.ts +1 -0
- package/dist/modules/realtime/socket.d.ts +25 -0
- package/dist/modules/realtime/socket.js +122 -0
- package/dist/modules/realtime/socket.test.d.ts +1 -0
- package/dist/modules/rsc/handler.d.ts +16 -0
- package/dist/modules/rsc/handler.js +17 -10
- package/dist/modules/rsc/handler.test.d.ts +1 -0
- package/dist/modules/rsc/resolveRscData.js +2 -1
- package/dist/modules/runtime/bundle.d.ts +24 -0
- package/dist/modules/runtime/bundle.js +102 -0
- package/dist/modules/runtime/contract.d.ts +37 -0
- package/dist/modules/runtime/contract.js +21 -0
- package/dist/modules/runtime/entry.test.d.ts +1 -0
- package/dist/modules/runtime/host.d.ts +38 -0
- package/dist/modules/runtime/host.js +68 -0
- package/dist/modules/runtime/inside.d.ts +15 -0
- package/dist/modules/runtime/inside.js +47 -0
- package/dist/modules/runtime/inside.test.d.ts +1 -0
- package/dist/modules/runtime/runner.d.ts +11 -0
- package/dist/modules/runtime/runner.js +38 -0
- package/dist/modules/runtime/runtime.test.d.ts +1 -0
- package/dist/modules/runtime/stages.d.ts +50 -0
- package/dist/modules/runtime/stages.js +117 -0
- package/dist/modules/ssr/Component.d.ts +3 -1
- package/dist/modules/ssr/Component.js +2 -1
- package/dist/modules/ssr/handler.js +10 -2
- package/dist/modules/ssr/loadPluginComponents.js +2 -1
- package/dist/modules/ssr/prepareRender.js +39 -10
- package/dist/modules/ssr/preview.d.ts +3 -0
- package/dist/modules/ssr/preview.js +17 -7
- package/dist/modules/ssr/redirect.test.d.ts +1 -0
- package/dist/modules/ssr/registerExternalPlugins.d.ts +6 -1
- package/dist/modules/ssr/registerExternalPlugins.js +19 -16
- package/dist/modules/ssr/registerExternalPlugins.test.d.ts +1 -0
- package/dist/modules/ssr/reportMissingPlugins.d.ts +9 -0
- package/dist/modules/ssr/reportMissingPlugins.js +24 -0
- package/dist/modules/ssr/reportMissingPlugins.test.d.ts +1 -0
- package/dist/modules/ssr/streamBody.d.ts +2 -2
- package/dist/modules/ssr/streamBody.js +4 -1
- package/dist/modules/ssr/template.js +1 -1
- package/dist/modules/ssr/views/template.ejs +8 -5
- package/dist/mysql.d.ts +1 -1
- package/dist/mysql.js +2 -2
- package/dist/oauth.js +2 -2
- package/dist/plugins/compile.js +9 -4
- package/dist/plugins/copy.js +3 -3
- package/dist/plugins/manager.d.ts +35 -3
- package/dist/plugins/manager.js +109 -38
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +7 -0
- package/package.json +39 -8
- package/skills/plitzi-authoring/SKILL.md +98 -14
- package/skills/plitzi-authoring/reference/accessibility.md +111 -0
- package/skills/plitzi-authoring/reference/authoring-errors.md +70 -0
- package/skills/plitzi-authoring/reference/data-and-visibility.md +84 -18
- package/skills/plitzi-authoring/reference/elements-and-styles.md +61 -7
- package/skills/plitzi-authoring/reference/flows.md +108 -17
- package/skills/plitzi-authoring/reference/lists.md +74 -0
- package/skills/plitzi-authoring/reference/performance.md +67 -0
- package/skills/plitzi-authoring/reference/plugins.md +167 -0
- package/skills/plitzi-authoring/reference/review-checklist.md +19 -2
- package/skills/plitzi-authoring/reference/templates.md +67 -27
- package/skills/plitzi-authoring/reference/testing.md +72 -5
- package/skills/plitzi-authoring/reference/validation.md +73 -0
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
button({
|
|
5
5
|
id: 'cta',
|
|
6
6
|
content: 'Get a quote',
|
|
7
|
-
flows: [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
flows: [
|
|
8
|
+
[
|
|
9
|
+
onClick(),
|
|
10
|
+
named('quote', runServerAction({ actionId: 'shipping-quote', input: { city: 'Berlin' }, mode: 'await' })),
|
|
11
|
+
whenSucceeded('quote', setState({ key: 'quote', type: 'text', value: '{{ quote.output.summary }}' })),
|
|
12
|
+
whenFailed('quote', addNotification({ content: 'Could not reach the courier', appearance: 'danger' }))
|
|
13
|
+
]
|
|
14
|
+
]
|
|
15
|
+
});
|
|
14
16
|
```
|
|
15
17
|
|
|
16
18
|
A flow is a list: a trigger, then steps in order. "Only if" is on the step (`when`, `whenSucceeded`, `whenFailed`),
|
|
@@ -22,9 +24,32 @@ never a nested tree. Use the step builders — they fill in where a step runs an
|
|
|
22
24
|
- **Which `setState`.** `setState({ key })` writes `runtime.state.<key>` and is read as `state.<key>`; never put
|
|
23
25
|
`state.` in the key. `updateElement(…)` changes one element's own attribute or state.
|
|
24
26
|
- **Flip in one step.** `toggleState({ key })` for app state, `toggleElement({ category: 'state', key: 'visibility' },
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
'panel')` to show/hide an element. Never two branches under opposite `when` guards — the second reads what the
|
|
28
|
+
first just wrote and flips it back. A key never set toggles to `true`, so for something shown by default name the
|
|
29
|
+
key for hiding it (`sidebarCollapsed`).
|
|
30
|
+
- **A trigger fired again while its flow runs is IGNORED** (`skip`, the default — no double submit). For a stream of
|
|
31
|
+
events that must each run, wrap the trigger: `whileRunning('queue', on('onArrival'))` (in order) or `'parallel'`.
|
|
32
|
+
- **Each step reads the page as it is when it runs.** A `when` or a `{{ state.x }}` after a `setState` sees the new
|
|
33
|
+
value, and one after a `delay` or a server action sees whatever changed meanwhile. To act on the value from BEFORE
|
|
34
|
+
a write, put the step that reads it first.
|
|
27
35
|
- **Keys are flat names.** A dotted key (`docsClosed.start`) is split into a path; use `docsClosedStart`.
|
|
36
|
+
- **`setState` types**: `text`, `number` (decimals kept), `boolean` (the word or a real boolean), and `json` for an
|
|
37
|
+
object or a list — `value: '{{ list_rows.item }}'` stores the row itself; JSON text is parsed, and text that is not
|
|
38
|
+
JSON fails the step (the dev tools log it) instead of storing the text.
|
|
39
|
+
|
|
40
|
+
## What a trigger hands the flow
|
|
41
|
+
|
|
42
|
+
Name the trigger (`named('changed', onChange())`) and read its payload as `{{ changed.<field> }}`:
|
|
43
|
+
|
|
44
|
+
| Trigger | Payload |
|
|
45
|
+
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
|
|
46
|
+
| `onChange` (formControl) | `value` (a boolean for a checkbox), `name` |
|
|
47
|
+
| `onSubmit` (form) | `values` (by control `name`), `actionUrl`, `method` |
|
|
48
|
+
| `onPageLoad` (page) | `pageId`, `routeParams`, `queryParams` |
|
|
49
|
+
| `onApiSuccess` / `onApiError` (apiContainer) | `url`, `method`, `status`, `data` |
|
|
50
|
+
| `onModalOpen` / `onModalClose`, `onDialogOpen` / `onDialogClose` / `onDialogAccept` / `onDialogReject` | `metadata` — what `openModal` / `openDialog` was handed |
|
|
51
|
+
| `onPageChange` (pagination) | `page` |
|
|
52
|
+
| `onThemeChange` (themeToggle) | `theme` |
|
|
28
53
|
|
|
29
54
|
## Reading what came before
|
|
30
55
|
|
|
@@ -34,8 +59,13 @@ never a nested tree. Use the step builders — they fill in where a step runs an
|
|
|
34
59
|
A step's params are templates, evaluated in full (conditions, loops, filters). A source in them is named in full:
|
|
35
60
|
a row's button posts `{ jobId: '{{ list_jobRows.item.id }}' }` — the row that was clicked; the short name is refused.
|
|
36
61
|
|
|
37
|
-
**Pass objects, not JSON text.** `input: { title: '{{ form.values.title }}' }
|
|
38
|
-
value
|
|
62
|
+
**Pass objects, not JSON text.** `input: { title: '{{ form.values.title }}' }` rather than `input: '{"title": …}'`:
|
|
63
|
+
each value keeps its own type, and nothing depends on the quoting. (JSON text works — a value inside one of its strings
|
|
64
|
+
is escaped for it — but an object says the same without the punctuation.)
|
|
65
|
+
|
|
66
|
+
**A field left empty is not in `values`.** A form sends what was typed, and a field nobody typed in sends nothing —
|
|
67
|
+
so `when({ field: 'sent.values.code', operator: '=', value: '' }, …)` never holds for it. Ask
|
|
68
|
+
`operator: 'empty'` / `'notEmpty'`, which treat missing and `''` alike.
|
|
39
69
|
|
|
40
70
|
## Writes and what they refresh
|
|
41
71
|
|
|
@@ -45,12 +75,20 @@ everything. Say what it should refresh — `invalidateQueries: 'elements', inval
|
|
|
45
75
|
|
|
46
76
|
## Triggers belong to the element that fires them
|
|
47
77
|
|
|
48
|
-
Every element fires `onClick`, `
|
|
78
|
+
Every element fires `onClick`, `onPointerDown` (the press, before it is a click — where a drag away from it starts),
|
|
79
|
+
`onLoad`, `onHover`, `onMouseEnter`/`onMouseLeave`, `onFocus`/`onBlur` and the ends of a
|
|
49
80
|
server action it started (`onFlowEnd`, `onFlowError`, `onFlowProgress`). A `page` fires `onPageLoad`, a `form`
|
|
50
81
|
`onSubmit`, a `formControl` `onChange`, an `apiContainer` `onApiSuccess`/`onApiError` (each answer, either runtime,
|
|
51
82
|
each refresh), a `modalContainer` `onModalOpen`/`onModalClose`, a `pagination` `onPageChange`. A flow on an element
|
|
52
83
|
that never fires its trigger is refused, naming the type that does.
|
|
53
84
|
|
|
85
|
+
**Keyboard shortcuts** are a trigger every element has: `onKey('f')`, `onKey('shift+f')`, `onKey('mod+k')` (⌘ on a
|
|
86
|
+
Mac, Ctrl elsewhere), several with commas (`onKey('plus, =')`). Heard on the whole page while the element is mounted,
|
|
87
|
+
so put it on the element whose flows it drives — `onKey('plus'), declaredCallback(map, 'zoomIn', { on: 'map' })` — or
|
|
88
|
+
on the page. A press while typing in a field is the field's, unless Ctrl/⌘/Alt is held or the key is Escape.
|
|
89
|
+
`{{ <step>.key }}` is the key pressed (`shift+f`). A shortcut that cannot fire (`'ctrl+shift'`, `'arrowupp'`) is refused
|
|
90
|
+
where it is written.
|
|
91
|
+
|
|
54
92
|
**A form's flow goes on the `form`**, which hands its submit over with `managedByInteractions: true`
|
|
55
93
|
(`FORM_SUBMIT_UNMANAGED` otherwise — the browser submits it natively and `onSubmit` never fires):
|
|
56
94
|
|
|
@@ -58,23 +96,76 @@ that never fires its trigger is refused, naming the type that does.
|
|
|
58
96
|
form({
|
|
59
97
|
id: 'signup',
|
|
60
98
|
managedByInteractions: true,
|
|
61
|
-
flows: [
|
|
62
|
-
|
|
63
|
-
|
|
99
|
+
flows: [
|
|
100
|
+
[named('submitted', onSubmit()), setState({ key: 'email', type: 'text', value: '{{ submitted.values.email }}' })]
|
|
101
|
+
],
|
|
102
|
+
children: [
|
|
103
|
+
formControl({ id: 'email', name: 'email', label: 'Email', subType: 'email' }),
|
|
104
|
+
button({ content: 'Sign up', subType: 'submit' })
|
|
105
|
+
]
|
|
106
|
+
});
|
|
64
107
|
```
|
|
65
108
|
|
|
66
109
|
A `formControl` is `required` by default: an optional field says `required: false`, or an empty one stops the submit
|
|
67
110
|
without a word.
|
|
68
111
|
|
|
112
|
+
## Notifications
|
|
113
|
+
|
|
114
|
+
`addNotification({ content: 'Saved', appearance: 'success' })` — `appearance` is `success`, `danger`, `warning` or
|
|
115
|
+
`info`. They follow the page's theme and font; their colours are the space's `notifications`:
|
|
116
|
+
`notifications: { background: 'var(--card)', text: 'var(--foreground)', success: 'var(--accent)', radius: '12px' }`.
|
|
117
|
+
|
|
69
118
|
## Modals, dropdowns, tabs
|
|
70
119
|
|
|
71
120
|
- A modal or dialog is declared `visible: false` and driven with `openModal('credits')` / `closeModal('credits')`
|
|
72
|
-
(`openDialog` / `closeDialog`). `openModal`'s second argument is
|
|
121
|
+
(`openDialog` / `closeDialog`). `openModal`'s second argument is the modal's data: a JSON object is read by its
|
|
122
|
+
fields (`{{ modalContainer_credits.title }}`); anything else — a row's id, a number, a word — as
|
|
123
|
+
`{{ modalContainer_credits.content }}`. The close control is a real button, reachable by keyboard.
|
|
73
124
|
- A `dropdown`'s label is a child and its `dropdownPopup` sits inside it; a `tabContainer`'s header and body are held
|
|
74
125
|
inside it too. Outside, they are refused.
|
|
75
126
|
|
|
127
|
+
## Realtime channels
|
|
128
|
+
|
|
129
|
+
Pages that see each other — cursors, presence, a shared board — need a channel. Declare its topic pattern on the space,
|
|
130
|
+
then subscribe with a `channel` element; a topic no pattern matches is refused, naming the patterns:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
channels: {
|
|
134
|
+
'board:{id}': { access: { mode: 'public' }, publish: 'server' }, // only `realtime.publish` speaks
|
|
135
|
+
'room:{id}': { access: { mode: 'public' }, presence: true } // pages speak directly
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
channel({ id: 'room', topic: 'room:{{ id }}', keep: 0, bind: { presence: 'computed.me' }, children: [...] })
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Its descendants bind `room.members`, `room.connected`, `room.last`; flows use `on('onMessage')` (`type`, `data`,
|
|
142
|
+
`from`), `onJoin`/`onLeave` (`from`, `user` and the `state` the member announced — `onJoin` only for who came after
|
|
143
|
+
this page), `publishOn('room', 'reaction', data)` and `announceOn('room', state)`. State everyone must
|
|
144
|
+
agree on goes through a server action whose last step is `realtime.publish` — validated and saved first, announced
|
|
145
|
+
after. `docs/en/realtime.md` is the whole of it.
|
|
146
|
+
|
|
147
|
+
A topic only some visitors may hear — a room behind a password, a customer's order — goes on a channel declared
|
|
148
|
+
`grant: true`. The action that decides they may be there ends with `realtime.grant` and answers the grant; the
|
|
149
|
+
element binds it, or the server refuses the topic every time (`channel-grant`). `realtime.revoke { topic, grant }`
|
|
150
|
+
takes one back — or every grant for the topic, naming none — and lets go whoever is on it with it:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
channels: { 'order:{id}': { access: { mode: 'session' }, grant: true } }
|
|
154
|
+
|
|
155
|
+
// steps: [ …decide…, { id: 'let', task: 'realtime.grant', params: { topic: 'order:{{ input.order }}' } } ]
|
|
156
|
+
channel({ id: 'order', topic: 'order:{{ id }}', bind: { grant: 'state.follow.grant' }, children: [...] })
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Two people writing the same thing at once: `kv.setIf` writes only if the value is still the one the flow read (empty
|
|
160
|
+
`expected` = only if nothing is there yet — claim a seat, a username), `list.put`/`list.range` keep an ordered list
|
|
161
|
+
(latest first, a leaderboard), and `flow.rateLimit` first in a public action that writes.
|
|
162
|
+
|
|
76
163
|
## Lists as state
|
|
77
164
|
|
|
78
165
|
`toggleInState({ key: 'picks', value })` keeps a list; `when({ field: 'state.picks', operator: 'contains', value })`
|
|
79
166
|
asks it. `appendState({ key, value, withId: true })` stores `{ id, value }` — bind `.value`, address by `.id`.
|
|
80
|
-
`whenFailed` matches every outcome that is not `completed` (also `skipped`, `aborted`).
|
|
167
|
+
`whenFailed` matches every outcome that is not `completed` (also `skipped`, `aborted`). A failed run gives `reason`,
|
|
168
|
+
and `error` only when the server or a step wrote one for the caller (a task throwing `ActionRefusal`, `flow.fail` with
|
|
169
|
+
`tellCaller`) — so show `{{ saved.error ? saved.error : "…" }}`, never the bare `error`.
|
|
170
|
+
|
|
171
|
+
`when(rule, step)` around a step that already has a `when` adds to it: both must hold.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Lists
|
|
2
|
+
|
|
3
|
+
A `list` renders its children once per item. Its rows come from its `items` — an array written or bound on the list:
|
|
4
|
+
a provider's answer, `state`, a fixed set. **Only `source: 'controlled'` reads them.** The default, `'none'`, renders
|
|
5
|
+
its children once as a plain `<ul>` and never looks at `items`, so a list with items and any other source is refused
|
|
6
|
+
(`list-items-ignored`). A CMS collection arrives the same way: a provider with its `connector` around the list, and the
|
|
7
|
+
list bound to its records (see the provider in data-and-visibility).
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
list({
|
|
11
|
+
id: 'games',
|
|
12
|
+
source: 'controlled',
|
|
13
|
+
class: gameGrid,
|
|
14
|
+
bind: { items: 'catalog.data.games' }, // an array from the provider around it
|
|
15
|
+
children: [gameCard()] // rendered once per game
|
|
16
|
+
})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- Each row publishes **`list_<id>.item`** (the row) and **`list_<id>.index`** (its position, as text). Inside the row,
|
|
20
|
+
bindings read the short form (`'games.item.title'`), templates and attribute tokens the full one
|
|
21
|
+
(`{{ list_games.item.slug }}`).
|
|
22
|
+
- A **nested list** sees the outer row: inside `list_features`, `list_games.item` is still the game.
|
|
23
|
+
- The list is a `<ul>` and its `class` styles that root; each child is rendered straight into it, with no wrapper.
|
|
24
|
+
A card grid needs `{ margin: '0px', padding: '0px', 'list-style-type': 'none' }` in its class.
|
|
25
|
+
- Fixed data is `items: [ … ]` on the list itself — no provider needed.
|
|
26
|
+
- Everything inside a row is `repeated` in the handles: a test addresses one copy with `.first()` / `.nth()`.
|
|
27
|
+
|
|
28
|
+
## Filtering and sorting
|
|
29
|
+
|
|
30
|
+
Bind `items` through a template that hands over its **value**:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
list({
|
|
34
|
+
id: 'shown',
|
|
35
|
+
source: 'controlled',
|
|
36
|
+
bind: [bindTemplate('items', 'catalog.data.games',
|
|
37
|
+
"{{ source|filter(g => (state.genre ?? '') == '' or g.genre == state.genre)|sort((a, b) => b.score - a.score) }}",
|
|
38
|
+
{ returns: 'value' })],
|
|
39
|
+
children: [gameCard()]
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Only the rows that match are rendered. A counter and an empty state read the same predicate over the same source:
|
|
44
|
+
`{{ source|filter(…)|length }}` as text, and `visible: { source: 'catalog.data.games', template: "{{ source is defined
|
|
45
|
+
and source|filter(…)|length == 0 }}" }`.
|
|
46
|
+
|
|
47
|
+
Hiding rows one by one with a visibility binding also works, but every hidden row stays in the DOM (with its whole
|
|
48
|
+
subtree unless the row says `loadStrategy: 'visible'`). Filter the items instead.
|
|
49
|
+
|
|
50
|
+
## A detail page
|
|
51
|
+
|
|
52
|
+
A page with a route param (`slug: 'games/{{slug}}'`) shows one record. With a server provider, `singleRecord` and
|
|
53
|
+
`filters` fetch just that one. In the browser — an offline project, a `query` returning the whole collection — narrow
|
|
54
|
+
the list to the record the route names:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
apiContainer({ id: 'catalog', query: '/data/games.json', cache: true, children: [
|
|
58
|
+
list({
|
|
59
|
+
id: 'game',
|
|
60
|
+
source: 'controlled',
|
|
61
|
+
bind: [bindTemplate('items', 'catalog.data.games',
|
|
62
|
+
'{{ source|filter(g => g.slug == navigation.routeParams.slug) }}', { returns: 'value' })],
|
|
63
|
+
children: [gameDossier()] // reads list_game.item
|
|
64
|
+
}),
|
|
65
|
+
text('Signal lost — no game by that name.', {
|
|
66
|
+
id: 'game-missing',
|
|
67
|
+
visible: { source: 'catalog.data.games',
|
|
68
|
+
template: "{{ source is defined and not (source|find('slug', navigation.routeParams.slug)) }}" }
|
|
69
|
+
})
|
|
70
|
+
] })
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A link to it from a card is an attribute token read inside the card's row:
|
|
74
|
+
`link({ mode: 'internal', href: '/games/{{ list_games.item.slug }}' })`.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Performance: what renders, and what it costs
|
|
2
|
+
|
|
3
|
+
A page that does the right thing can still do it by rendering everything, and nothing shows it but the frame rate — on
|
|
4
|
+
somebody else's slower machine. Measure it (`inspectRenders`, below) instead of guessing, and write the page so that a
|
|
5
|
+
change renders what shows it.
|
|
6
|
+
|
|
7
|
+
## What makes an element render
|
|
8
|
+
|
|
9
|
+
An element renders again when something it READS changes, and only then:
|
|
10
|
+
|
|
11
|
+
- **A binding, a `when` rule or an attribute template subscribes to each path it names** — `computed.tool`, not all of
|
|
12
|
+
`computed`; `state.cart.count`, not all of `state`. Name what you show: a template that reads `state.cart` renders on
|
|
13
|
+
every change to anything in the cart.
|
|
14
|
+
- **A computed value is evaluated again whenever the state, the variables, the route, the session, the host or the
|
|
15
|
+
theme changes.** A value that comes out the same keeps its object, so what reads it does not render — but the work of
|
|
16
|
+
evaluating it is done every time. Keep computed values cheap: a filter over a list of five thousand rows belongs in
|
|
17
|
+
the binding of the list that shows them (`bindTemplate(…, { returns: 'value' })`) or in the provider, where it runs
|
|
18
|
+
when the list changes and not on every keystroke anywhere on the page.
|
|
19
|
+
- **A parent rendering does not render its children** unless it hands them something new. What an element's own
|
|
20
|
+
bindings change is its own.
|
|
21
|
+
|
|
22
|
+
## What a flow costs
|
|
23
|
+
|
|
24
|
+
- **Synchronous steps are one change.** A `setState` after a `setState` after a `toggleState` renders once, when the
|
|
25
|
+
flow gives the page back — at its end, or at the first step that waits (a request, a `delay`), which renders what
|
|
26
|
+
came before it.
|
|
27
|
+
- **A trigger that fires on every pointer move or keystroke is a flow per event.** For a stream of events, keep the
|
|
28
|
+
moment-to-moment state inside a plugin and hand the space the outcome (`onCommit`, `onSelectionChange`), not every
|
|
29
|
+
step of it.
|
|
30
|
+
- **`whileRunning`** decides what a trigger fired again does while its flow runs: `skip` (the default) drops it,
|
|
31
|
+
`queue` runs it after, `parallel` alongside. A burst of events queued behind a slow request is a burst of renders
|
|
32
|
+
later.
|
|
33
|
+
|
|
34
|
+
## What mounts
|
|
35
|
+
|
|
36
|
+
- **A hidden element stays mounted** with its whole subtree — a modal's form, a closed panel's lists. `loadStrategy:
|
|
37
|
+
'visible'` mounts the contents only while it is shown, `'lazy'` from the first time it is shown. Use it for heavy
|
|
38
|
+
subtrees that start hidden.
|
|
39
|
+
- **Every list row is a subtree.** Filter the items the list is given; a row hidden by a condition is still a row.
|
|
40
|
+
|
|
41
|
+
## Measuring it: `inspectRenders`
|
|
42
|
+
|
|
43
|
+
Render the space with `debugMode` (the SDK profiles every element then), and ask what an interaction rendered:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { inspectRenders } from '@plitzi/sdk-authoring';
|
|
47
|
+
|
|
48
|
+
const report = await inspectRenders(page, () => page.click('[data-plitzi-el="save"]'), { max: 10 });
|
|
49
|
+
expect(report.problems).toEqual([]);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`report.elements` lists every element that rendered, how often, and what changed for it (`attributes`, `elementState`,
|
|
53
|
+
a prop's name); `report.causes` the store paths written meanwhile. `max` turns it into an assertion: over it,
|
|
54
|
+
`problems` names the elements that rendered most. A test that holds an interaction to its budget is how a regression
|
|
55
|
+
is caught before anybody feels it.
|
|
56
|
+
|
|
57
|
+
## When something is slow: where to look
|
|
58
|
+
|
|
59
|
+
1. **Count before changing anything.** `inspectRenders` around the interaction that feels slow. Hundreds of elements
|
|
60
|
+
for one click is the problem; three is not, and the cost is somewhere else (a plugin, a large list mounting).
|
|
61
|
+
2. **Read `changed` for the elements that rendered most.** Their own `attributes` or `elementState` changed: find what
|
|
62
|
+
they read in `causes` — a template naming more than it shows, a computed value that reads it. `nothing of its own`:
|
|
63
|
+
its context or its parent carried it; look at the element above it that did change.
|
|
64
|
+
3. **Read `causes`.** A path you did not expect written — a flow writing on every pointer move, a provider refetching —
|
|
65
|
+
is the change to stop at its source.
|
|
66
|
+
4. **Only then the browser's profiler**, for what a render costs rather than how many there are: a plugin drawing, a
|
|
67
|
+
list of thousands mounting at once.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Plugins
|
|
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`:
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
custom({ id: 'arcade', renderType: 'nebulaRun', shipColor: 'amber', bind: { best: 'state.arcadeBest' } })
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Creating one
|
|
11
|
+
|
|
12
|
+
The CLI writes it, in the shape Plitzi's own elements are written in:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @plitzi/cli add plugin seat-picker legend # elements of this project (src/plugins/<Name> in a CLI project)
|
|
16
|
+
npx @plitzi/cli create seat-picker --plugin # a package of its own, with a preview
|
|
17
|
+
npx @plitzi/cli pack plugin # built, with its manifest and the zip the builder takes
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
One folder, four files: `SeatPicker.tsx` (the component), `declaration.ts` (its `type`, the `triggers` it fires, the
|
|
21
|
+
`callbacks` it answers to, and the element the builder adds — data only), `Settings.tsx` (its panel in the builder), and
|
|
22
|
+
`index.ts` (`Object.assign(Component, declaration, { pluginSettings: Settings })`). An event or an action is declared in
|
|
23
|
+
`declaration.ts` and registered by the component from there, never only in the component.
|
|
24
|
+
|
|
25
|
+
A plugin fires its events with `interactionsManager.interactionTrigger(id, action, payload)` whenever it has news —
|
|
26
|
+
from its first effect too: a flow starts once the page has finished mounting, so the `setState` it calls is there. No
|
|
27
|
+
`queueMicrotask` or delay of your own. An event that REPORTS a state, and may fire again before its flow ends (as a
|
|
28
|
+
component reads its real value right after mounting), wants `whileRunning('queue', …)` on the flow: by default a firing
|
|
29
|
+
while the flow runs is dropped, and the stale first report would stick.
|
|
30
|
+
|
|
31
|
+
## Props
|
|
32
|
+
|
|
33
|
+
**The host element's attributes ARE the component's props.** Whatever the space writes on the `custom` element arrives
|
|
34
|
+
by the same name, and so does whatever a binding writes — which is what makes a plugin live: bind an attribute to
|
|
35
|
+
`state`, a provider or a row, and the component re-renders with the answer. Prefer this to anything else.
|
|
36
|
+
|
|
37
|
+
`settings` is the other channel: a JSON string whose keys are merged into the props too. It is what the builder's
|
|
38
|
+
panel edits for a plugin it has no controls for; from code, write attributes. A `settings` that is not JSON renders
|
|
39
|
+
"Settings Malformed" in the page.
|
|
40
|
+
|
|
41
|
+
Every prop is optional and has a default: an attribute not authored yet, or a binding whose source has not answered,
|
|
42
|
+
is `undefined`, and a plugin that renders nothing then is a hole in the page.
|
|
43
|
+
|
|
44
|
+
## The component
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { RootElement } from '@plitzi/plitzi-sdk';
|
|
48
|
+
|
|
49
|
+
const NebulaRun = ({ shipColor = 'amber', best = 0, className }: NebulaRunProps) => (
|
|
50
|
+
<RootElement className={className}>…</RootElement>
|
|
51
|
+
);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **Render `RootElement`**, not a `div`: it carries the element's id, classes and `data-plitzi-el`, so the CSS
|
|
55
|
+
authored on the element applies, the builder can select it and a test can find it.
|
|
56
|
+
- **Render the same thing on the server and on the first client pass.** A clock, a random number or anything read from
|
|
57
|
+
`window` belongs in an effect; a hydration mismatch discards the whole tree, not the plugin.
|
|
58
|
+
- **Borrow colours.** `currentColor` and the space's `var(--…)` tokens follow the theme; a hard-coded colour is
|
|
59
|
+
invisible in one of them. A canvas reads them with `getComputedStyle(element).getPropertyValue('--accent')` and
|
|
60
|
+
again when `theme.resolved` changes (bind it as a prop).
|
|
61
|
+
|
|
62
|
+
## Writing to the space
|
|
63
|
+
|
|
64
|
+
A plugin that produces something the page shows — a score, a selection — writes it to `runtime.state`, where every
|
|
65
|
+
binding reads it:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { getStateManager } from '@plitzi/plitzi-sdk';
|
|
69
|
+
|
|
70
|
+
const state = getStateManager();
|
|
71
|
+
state.setStateByKey('arcadeBest', Math.max(score, Number(state.state.arcadeBest ?? 0)));
|
|
72
|
+
const stop = state.subscribe(next => save(next)); // called after every change; call `stop()` to leave
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`state` is the current value, `setState` replaces it (or takes an updater), `setStateByKey` writes one key,
|
|
76
|
+
`clearState` empties it, `subscribe` listens. Bindings on `state.*` re-render at once.
|
|
77
|
+
|
|
78
|
+
## Talking to other pages
|
|
79
|
+
|
|
80
|
+
A plugin that moves at the speed of a cursor reads a realtime channel through `useChannel` rather than through flows —
|
|
81
|
+
messages arrive through a callback and re-render nothing:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useChannel } from '@plitzi/plitzi-sdk';
|
|
85
|
+
|
|
86
|
+
const room = useChannel(roomTopic, { onMessage: message => move(message.from, message.data) });
|
|
87
|
+
room.publish('pointer', { x, y }); // throttle it: ~20 a second, the latest position each time
|
|
88
|
+
room.members; // who is here, with the state each announced
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Take the topic as a prop (`bindTemplate('roomTopic', 'board.id', 'room:{{ source }}')`) so the space names its
|
|
92
|
+
channels, and trust a message you act on only when `message.from === 'server'`. The page has one connection and is one
|
|
93
|
+
member per topic, however many elements and plugins listen.
|
|
94
|
+
|
|
95
|
+
## Registering
|
|
96
|
+
|
|
97
|
+
A project `plitzi create` wrote registers every folder of `src/plugins` by itself, under the folder's name in
|
|
98
|
+
camelCase (`StatCard` → `statCard`). Anywhere else, the entry registers the component under its `renderType`: the
|
|
99
|
+
third argument to `render()`, `<PlitziSdk.Plugin>` in a React application, `plugins` on a page server of your own. A `renderType` nothing registered
|
|
100
|
+
renders "Custom Component … Not Found", and a page server logs the missing `renderType` at `error` once per
|
|
101
|
+
space: on a server, register it in `plugins` AND name it in the deployment's `pluginNames`.
|
|
102
|
+
|
|
103
|
+
A plugin PACKAGE, loaded by a space from its `plugin-manifest.json`, is an element TYPE of its own rather than a
|
|
104
|
+
`custom` host — it is how the builder adds one somebody dropped. Author it with a typed factory:
|
|
105
|
+
`defineElement<SeatPickerAttributes>(declaration)` from the plugin's own `declaration.ts`, or
|
|
106
|
+
`elementsFromManifest<{ seatPicker: SeatPickerAttributes }>(manifest)` from what it published — or untyped,
|
|
107
|
+
`element('seatPicker', { id: 'seats', start: 3 })`.
|
|
108
|
+
|
|
109
|
+
## Checked like a built-in element
|
|
110
|
+
|
|
111
|
+
Hand `authorSpace` the plugin's DECLARATION — the `declaration.ts` beside the component, or a manifest's
|
|
112
|
+
`pluginSchema` entry:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import declaration from './plugins/SeatPicker/declaration';
|
|
116
|
+
|
|
117
|
+
authorSpace(space, { plugins: [declaration] });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It is then held to what it declares, whether it is authored as its own type or hosted by `custom({ renderType:
|
|
121
|
+
'seatPicker' })`: a flow on an event it never fires, a step sent to an action it does not answer, an attribute it does
|
|
122
|
+
not read — each is refused with the name it should have been. (`pluginTypes: ['seatPicker']` only tells the linter the
|
|
123
|
+
type exists; nothing about how the space uses it is checked.) A `custom` host whose component is NOT handed over is not
|
|
124
|
+
judged on its events at all — nothing here knows them.
|
|
125
|
+
|
|
126
|
+
Its events and actions have builders typed from the same declaration, so a name it does not declare is a compile error:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
seats({ id: 'seats', flows: [[named('picked', declaredTrigger(declaration, 'onPick')), setState({ key: 'seat', type: 'text', value: '{{ picked.seat }}' })]] })
|
|
130
|
+
button({ content: 'Clear', flows: [[onClick(), declaredCallback(declaration, 'reset', { on: 'seats' })]] })
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A plugin says what HAPPENED through its events (`onPick`, with the seat in the payload) and lets the space's flows
|
|
134
|
+
decide what that means — write `state`, open a modal, call a server action. Prefer that to writing `state` from inside
|
|
135
|
+
the component: the flow is visible in the space, the builder shows it, and the same act can come from a button too.
|
|
136
|
+
|
|
137
|
+
## Components that draw into DOM they do not render
|
|
138
|
+
|
|
139
|
+
A map, a chart library, anything that positions its own markers or popups: its roots are the library's to place. Never
|
|
140
|
+
give them a class that sets `position` — the element falls into the page's flow, offset by every marker before it. Put
|
|
141
|
+
the look in the plugin's own stylesheet (imported CSS ships beside the bundle) and take colours from custom properties
|
|
142
|
+
the space sets (`--seat-accent: var(--accent)` in `customCss`); a canvas or WebGL layer resolves them through a probe
|
|
143
|
+
element with `getComputedStyle(probe).color`, again whenever `theme.resolved` changes (bind it as a prop).
|
|
144
|
+
|
|
145
|
+
## Components that draw a lot
|
|
146
|
+
|
|
147
|
+
A canvas with thousands of shapes on it — a whiteboard, a diagram, a map of points — is judged on a slower machine than
|
|
148
|
+
yours: sixty frames a second here says nothing about a four-year-old laptop. What keeps one fast is doing work in
|
|
149
|
+
proportion to what CHANGED, not to what is there:
|
|
150
|
+
|
|
151
|
+
- **Two layers.** Paint what stands still on one canvas and leave it; clear and draw the one over it every frame, with
|
|
152
|
+
only what moves: the cursor, the selection, what is being dragged.
|
|
153
|
+
- **Repaint the part that changed.** Something added, edited or removed repaints the area it was and is in, clipped,
|
|
154
|
+
with whatever reaches into it — not the whole board. A pan moves the picture already painted by whole device pixels
|
|
155
|
+
and paints the edges it uncovers.
|
|
156
|
+
- **Draw a dragged group once.** Everything picked up moves by the same amount at every step: draw it to a canvas of its
|
|
157
|
+
own when the drag begins and copy that into place after.
|
|
158
|
+
- **Cache per object, not per frame.** Treat elements as immutable and key what is derived from one — its bounds, its
|
|
159
|
+
shape, a resolved connector — on the object itself (a `WeakMap`), so nothing is worked out twice for the same thing.
|
|
160
|
+
- **Hand the space outcomes, not motion.** A stroke finished, a selection changed: a trigger each. Firing one at every
|
|
161
|
+
pointer move runs a flow and renders the page at every pointer move.
|
|
162
|
+
- **Measure with a bench, throttled.** A script that drives the real thing in a browser with the CPU slowed
|
|
163
|
+
(`Emulation.setCPUThrottlingRate`) and reports script time per frame, not only frames per second — and a test that
|
|
164
|
+
counts WORK (full repaints, strokes drawn during a drag), which holds on any machine where a timing does not.
|
|
165
|
+
|
|
166
|
+
Pizarra, the collaborative whiteboard on the platform (`pizarra.plitzi.app`), does all of it on a board of thousands
|
|
167
|
+
of elements.
|
|
@@ -20,11 +20,25 @@ of it; each line is something that has shipped broken before.
|
|
|
20
20
|
|
|
21
21
|
## Every screen, every theme
|
|
22
22
|
|
|
23
|
-
- [ ] Desktop, tablet and mobile — and nothing scrolls sideways on a phone.
|
|
24
|
-
|
|
23
|
+
- [ ] Desktop, tablet and mobile — and nothing scrolls sideways on a phone. A rule for tablets and phones alike is
|
|
24
|
+
written under `compact`.
|
|
25
25
|
- [ ] Light AND dark. Every colour is a token with both values; no theme-following text on a fixed background.
|
|
26
26
|
- [ ] Focus is visible on everything clickable (`focus-visible`), and `prefers-reduced-motion` stops what moves.
|
|
27
27
|
|
|
28
|
+
## Usable without sight
|
|
29
|
+
|
|
30
|
+
Screen readers and browser agents (Claude in Chrome) work a page through its accessibility tree. See
|
|
31
|
+
[accessibility](accessibility.md).
|
|
32
|
+
|
|
33
|
+
- [ ] Every button and link has words — an icon-only button a `title`, a link around a card a `label` — and every field
|
|
34
|
+
a `label` (`hideLabel: true` when the design shows what it is).
|
|
35
|
+
- [ ] Every image has an `alt` saying what it shows, or `decorative: true`.
|
|
36
|
+
- [ ] Every click is on a `button` or a `link`; the whole page can be worked from the keyboard.
|
|
37
|
+
- [ ] A toggle binds `ariaPressed`, a button that opens something `ariaExpanded`.
|
|
38
|
+
- [ ] Headings step down one level at a time; parts of the page a person jumps to are landmarks (`nav`, `main`, a
|
|
39
|
+
labelled `section`).
|
|
40
|
+
- [ ] What a canvas or a plugin draws is also there as elements, with a visible way to reach them.
|
|
41
|
+
|
|
28
42
|
## No copies
|
|
29
43
|
|
|
30
44
|
- [ ] Chrome shared by pages is a layout; the current menu entry comes from `activeOn`, not per-page styling.
|
|
@@ -39,6 +53,9 @@ of it; each line is something that has shipped broken before.
|
|
|
39
53
|
testimonials. A panel with nothing true to show is an empty state.
|
|
40
54
|
- [ ] Times are formatted with an explicit zone and say which (`… 06:00 UTC`).
|
|
41
55
|
- [ ] Logic lives in binding templates or on the server, not in an attribute (where a condition is never evaluated).
|
|
56
|
+
- [ ] A filtered or sorted list binds its `items` to a template that returns its value — its rows are not hidden one
|
|
57
|
+
by one.
|
|
58
|
+
- [ ] Every link you wrote was followed at least once: an `href` built from a row goes where it says.
|
|
42
59
|
- [ ] Writes refresh what they changed — and only that (`invalidateQueries`); a multi-step form does not refresh the
|
|
43
60
|
provider that decides whether to leave.
|
|
44
61
|
- [ ] What only an administrator may read is shown only to administrators — and the check is a real boolean, not
|