@plitzi/sdk-server 0.37.1 → 0.37.3
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 +1296 -0
- package/README.md +126 -7
- package/dist/actions.d.ts +20 -8
- package/dist/actions.js +11 -4
- package/dist/adapters/cloudAdapters.js +4 -3
- package/dist/adapters/jsonAdapters.js +28 -6
- 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 +14 -1
- package/dist/core/server/baseServer.js +43 -11
- package/dist/core/server/closeOnSignals.d.ts +28 -0
- package/dist/core/server/closeOnSignals.js +47 -0
- package/dist/core/server/closeOnSignals.test.d.ts +1 -0
- 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 +36 -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 +8 -1
- package/dist/index.js +6 -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 +41 -13
- package/dist/modules/actions/index.js +52 -9
- 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 +33 -0
- package/dist/modules/actions/jobs/index.js +59 -0
- package/dist/modules/actions/jobs/memoryQueue.d.ts +33 -0
- package/dist/modules/actions/jobs/memoryQueue.js +194 -0
- package/dist/modules/actions/jobs/memoryStores.test.d.ts +1 -0
- package/dist/modules/actions/jobs/scheduler.d.ts +64 -0
- package/dist/modules/actions/jobs/scheduler.js +130 -0
- package/dist/modules/actions/jobs/scheduler.test.d.ts +1 -0
- package/dist/modules/actions/jobs/schedules.d.ts +23 -0
- package/dist/modules/actions/jobs/schedules.js +47 -0
- package/dist/modules/actions/jobs/storeClock.d.ts +16 -0
- package/dist/modules/actions/jobs/storeClock.js +32 -0
- package/dist/modules/actions/jobs/testing/jobQueueContract.d.ts +25 -0
- package/dist/modules/actions/jobs/worker.d.ts +64 -0
- package/dist/modules/actions/jobs/worker.js +267 -0
- package/dist/modules/actions/jobs/worker.test.d.ts +1 -0
- package/dist/modules/actions/moduleFor.js +6 -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 +5 -9
- 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/renderResolver.js +2 -2
- 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 +6 -3
- 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 +5 -4
- package/dist/modules/actions/types.d.ts +162 -4
- 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/index.d.ts +4 -0
- package/dist/modules/mongo/jobQueue.d.ts +38 -0
- package/dist/modules/mongo/jobQueue.js +335 -0
- package/dist/modules/mongo/kv.d.ts +15 -0
- package/dist/modules/mongo/kv.js +129 -0
- package/dist/modules/mongo/stores.integration.test.d.ts +1 -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 +5 -0
- package/dist/modules/mysql/jobs/jobQueue.d.ts +40 -0
- package/dist/modules/mysql/jobs/jobQueue.js +375 -0
- package/dist/modules/mysql/jobs/kv.d.ts +18 -0
- package/dist/modules/mysql/jobs/kv.js +108 -0
- package/dist/modules/mysql/jobs/schema.d.ts +33 -0
- package/dist/modules/mysql/jobs/schema.js +113 -0
- package/dist/modules/mysql/jobs/stores.integration.test.d.ts +1 -0
- 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 +4 -5
- package/dist/modules/ssr/Component.js +2 -2
- package/dist/modules/ssr/handler.js +10 -2
- package/dist/modules/ssr/loadPluginComponents.js +2 -1
- package/dist/modules/ssr/prepareRender.js +39 -13
- 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/mongo.d.ts +25 -0
- package/dist/mongo.js +3 -0
- package/dist/mysql.d.ts +11 -0
- package/dist/mysql.js +4 -1
- 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 +52 -11
- package/skills/plitzi-authoring/SKILL.md +153 -263
- 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 +180 -0
- package/skills/plitzi-authoring/reference/elements-and-styles.md +161 -0
- package/skills/plitzi-authoring/reference/flows.md +171 -0
- package/skills/plitzi-authoring/reference/layouts.md +140 -0
- 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 +67 -0
- package/skills/plitzi-authoring/reference/structure.md +83 -0
- package/skills/plitzi-authoring/reference/templates-and-export.md +54 -0
- package/skills/plitzi-authoring/reference/templates.md +109 -0
- package/skills/plitzi-authoring/reference/testing.md +119 -0
- package/skills/plitzi-authoring/reference/validation.md +73 -0
- package/dist/modules/actions/runtime/schedule.d.ts +0 -35
- package/dist/modules/actions/runtime/schedule.js +0 -76
- /package/dist/{modules/actions/runtime/schedule.test.d.ts → adapters/jsonAdapters.test.d.ts} +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Data and visibility
|
|
2
|
+
|
|
3
|
+
## Bindings
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
heading({ bind: { content: 'posts.title' } }) // short form: attributes
|
|
7
|
+
text({ bind: [bindTemplate('content', 'stats.data.total', "{{ source|number_format(0, '.', ',') }} views")] })
|
|
8
|
+
container({ bind: [bindTemplate('width', 'state.xp', '{{ source / 10 }}%', { category: 'style' })] }) // a style
|
|
9
|
+
text({ bind: [{ to: 'content', source: 'posts.title', when: { … } }] }) // full form: anything else
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`bindTemplate(to, source, template, { category?, returns? })` is the computed attribute — a template over the value
|
|
13
|
+
at `source`. `returns: 'value'` hands over what a single `{{ expression }}` evaluates to instead of its text: a list's
|
|
14
|
+
`items`, a number, a flag (see [lists](lists.md)).
|
|
15
|
+
|
|
16
|
+
**A source names the id you gave the element** — `'posts.title'`, `'postList.item.cover'` — and the prefix is
|
|
17
|
+
completed from what the element publishes (`apiContainer_posts`, `list_postList`; a `form` publishes under
|
|
18
|
+
`apiContainer`). A name nothing answers to, or a prefix that does not match, is refused.
|
|
19
|
+
|
|
20
|
+
**Inside a template the name is spelled in full**: `{{ apiContainer_stats.data.total }}`, `{{ list_rows.item.id }}`.
|
|
21
|
+
The binding's `source` is completed for you; its template is read as written, so the short name there is refused.
|
|
22
|
+
|
|
23
|
+
An element re-renders when anything it reads changes: the first part of each binding's `source` and every source its
|
|
24
|
+
templates name. The globals are `variables`, `navigation` (`routeParams`, `queryParams`, `origin`, `currentPageId`),
|
|
25
|
+
`auth` (who is signed in, `status` while that is being found out), `state` (what flows wrote), `theme`
|
|
26
|
+
(`mode`, `resolved`), `host` (the `hostData` an application embedding the space hands the SDK) and `computed` (below).
|
|
27
|
+
|
|
28
|
+
## Values computed once
|
|
29
|
+
|
|
30
|
+
A value many elements show — a score, a level, a count of what the visitor picked — is declared once on the space and
|
|
31
|
+
read everywhere as `computed.<name>`, instead of repeating its expression in every binding:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
export const space = {
|
|
35
|
+
…,
|
|
36
|
+
computed: {
|
|
37
|
+
xp: '{{ (state.favourites|length) * 10 + (state.runs ?? 0) * 5 }}',
|
|
38
|
+
level: '{{ computed.xp // 100 + 1 }}'
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
text('', { bind: [bindTemplate('content', 'computed.level', 'Level {{ source }}')] })
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A computed value reads the globals (`state`, `auth`, `navigation`, `variables`, `theme`, `host`) and the ones declared
|
|
46
|
+
above it — not an element's source: bind that on the element. One `{{ expression }}` gives its value (a number, a
|
|
47
|
+
list); anything else gives text. `authorSpace` refuses a name read before it is declared, or never.
|
|
48
|
+
|
|
49
|
+
## Providers
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
apiContainer({ id: 'orders', query: '{{apiUrl}}/orders', credentials: 'include', children: [ … ] })
|
|
53
|
+
apiContainer({ id: 'board', runtime: 'server', action: 'queue-board' }) // resolved before the HTML
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- A provider's source is readable by its **descendants** only. Wrap what reads it; a sibling cannot see it.
|
|
57
|
+
- It publishes `data` plus `isLoading`, `isEmpty`, `hasError`, `errorMessage` — bind a state to those, not to guesses.
|
|
58
|
+
`isEmpty` reads what arrived: a `query`'s body when it is missing, `null`, `''`, `[]` or `{}`; a connector list by
|
|
59
|
+
its `records`; a `singleRecord` provider by its `record`. It is also true before the first answer, so pair it with
|
|
60
|
+
`not isLoading` for an empty state.
|
|
61
|
+
- A query that depends on state is a binding on `query`; it answers `''` (and fetches nothing) until the state exists:
|
|
62
|
+
`"{{ source ? apiUrl ~ '/workspaces/' ~ source ~ '/stats' : '' }}"` with `source: 'state.workspace.id'`.
|
|
63
|
+
- A space with any `runtime: 'server'` provider needs `rsc: { enabled: true }`.
|
|
64
|
+
|
|
65
|
+
### Data in a project with no backend
|
|
66
|
+
|
|
67
|
+
An offline project (`offlineMode`) still never invents data: the content lives in JSON files the project serves
|
|
68
|
+
(`public/data/games.json` with Vite) and a provider reads them like any API — `apiContainer({ id: 'catalog', query:
|
|
69
|
+
'/data/games.json', cache: true, children: [ … ] })`. The page binds to `catalog.data.…` exactly as it would to a live
|
|
70
|
+
backend, so swapping the file for a real endpoint later changes one `query`. `mockData` is what the BUILDER shows while
|
|
71
|
+
editing; it is not a data source for the running page. Say on the page that demo content is demo content.
|
|
72
|
+
|
|
73
|
+
### Live, cached, refreshed
|
|
74
|
+
|
|
75
|
+
- **Live:** `refreshSeconds: 15` asks again on its own (either runtime); it pauses in a hidden tab and never
|
|
76
|
+
overlaps. Use it for queues, feeds, status boards — then mark the panel as live so the reader knows it moves.
|
|
77
|
+
- **Cached:** `cache: true` (and `staleTime` seconds, 30 by default) shares an answer between providers asking the
|
|
78
|
+
same thing and across page changes.
|
|
79
|
+
- **Refreshed by writes:** a `webHook` that is not a GET refreshes every request to its own site by default
|
|
80
|
+
(`invalidateQueries: 'origin'`); a `runServerAction` refreshes everything (`'all'`). Narrow it with
|
|
81
|
+
`'elements'` + `invalidateElements: ['orders']`, or stop it with `'none'`.
|
|
82
|
+
- **Say `'none'` when a refresh would move the page on.** A page that decides where to go from a provider's answer
|
|
83
|
+
("profile complete → leave") will act on the refreshed answer in the middle of a multi-step form that saves as it
|
|
84
|
+
goes, and skip the last step.
|
|
85
|
+
|
|
86
|
+
## Visibility
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
container({ visible: 'posts.hasPosts', … }) // shown while true
|
|
90
|
+
container({ visible: '!posts.hasPosts', … }) // its inverse
|
|
91
|
+
modalContainer({ visible: false, … }) // starts hidden; a flow opens it
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**An element is visible by default.** Which way its condition flips decides how it is written:
|
|
95
|
+
|
|
96
|
+
| The logic… | Example | Write |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| reveals it: hidden until the data says so | empty state, "get started", admin-only panel, error note | `visible: 'src'`, or `visible: false` + a computed binding |
|
|
99
|
+
| hides it: shown unless a flag says otherwise | sidebar labels until the sidebar is folded, a banner until dismissed | a binding with no `visible`; an absent flag must leave it shown |
|
|
100
|
+
|
|
101
|
+
`visible` starts the element **hidden**, and it appears when its data says so. A revealing condition you have to
|
|
102
|
+
compute is `visible: { source, template }` — it starts hidden too, and the template's value is read as a yes or a
|
|
103
|
+
no: `false`, `0`, an empty text, an empty list and nothing at all are a no, anything else a yes. Write the condition
|
|
104
|
+
itself; `? 'true' : 'false'` is not needed:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
container({
|
|
108
|
+
id: 'first-steps',
|
|
109
|
+
visible: {
|
|
110
|
+
source: 'stats.data.totals',
|
|
111
|
+
template: '{{ source and not (source.spaces > 0 and source.published > 0) }}'
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Rules for a condition's template:
|
|
117
|
+
|
|
118
|
+
- **A revealing condition starts hidden** — `visible: { source, template }` does that for you. A visibility binding
|
|
119
|
+
written by hand in `bind` keeps the element on screen until it answers — right for a hiding flag, a flash for a
|
|
120
|
+
condition on data. `authorSpace` warns `condition-starts-visible` when a computed one on a provider starts on screen.
|
|
121
|
+
- **A hidden element stays in the DOM** (`plitzi-component--hidden`), its subtree mounted. `loadStrategy: 'visible'`
|
|
122
|
+
mounts the subtree only while it is shown, and `'lazy'` from the first time it is shown (modals do that already).
|
|
123
|
+
Use it on heavy panels that are usually hidden; leave the default on what is usually on screen.
|
|
124
|
+
- **`'true'` shows, `'false'` hides, `''` writes nothing** and leaves the element as it is. Visibility hides only on
|
|
125
|
+
an explicit false.
|
|
126
|
+
- **Answer what "not known yet" should mean.** For a revealing condition that is `'false'` (`source ? … : 'false'`);
|
|
127
|
+
for a hiding flag it is `''` — nothing written, the default stays. A sidebar that answered `'false'` for a
|
|
128
|
+
"folded" state nobody had set yet hid every label of the menu for everybody.
|
|
129
|
+
- **Hide the element itself.** Never wrap it in a container that is shown while the inner one is hidden: an empty
|
|
130
|
+
visible wrapper still takes a slot in its parent's `gap`, and leaves a hole in the page.
|
|
131
|
+
|
|
132
|
+
### Loading, empty, error
|
|
133
|
+
|
|
134
|
+
Four states, and each has its own element:
|
|
135
|
+
|
|
136
|
+
| State | How to tell |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| Loading | `apiContainer_x.isLoading` — or simply nothing: every condition is hidden until data arrives |
|
|
139
|
+
| Empty | the answer ARRIVED and is empty: `{{ source is defined and source is empty }}` over the list |
|
|
140
|
+
| Error | `apiContainer_x.hasError` |
|
|
141
|
+
| Data | the list itself |
|
|
142
|
+
|
|
143
|
+
`visible: '!provider.data.items'` is NOT an empty state: before the answer, `!undefined` is true and "Nothing here yet"
|
|
144
|
+
shows on every load. Test for "arrived and empty" (`is defined and … is empty`), or bind to `isEmpty` together with
|
|
145
|
+
`not isLoading`.
|
|
146
|
+
|
|
147
|
+
## Never ask the data for an opposite
|
|
148
|
+
|
|
149
|
+
Both sides of one question are `visible: 'x'` and `visible: '!x'`, not an `x` and a `notX` in the server's answer.
|
|
150
|
+
`!` reads a boolean that travelled as text (`"false"`, `"0"`) correctly. A three-state condition
|
|
151
|
+
(`Boolean(post) && !canEdit`) still belongs where the data is made.
|
|
152
|
+
|
|
153
|
+
## State that outlives a reload
|
|
154
|
+
|
|
155
|
+
`render(…, { state })` seeds `runtime.state` when the page starts — the way a host hands a space what it already knows.
|
|
156
|
+
`settings: { keepState: true, stateStorage: 'localStorage' }` keeps `runtime.state` across reloads, filed under
|
|
157
|
+
whoever is signed in (a guest's state is the browser's; another account never sees it). Keep only what a person would
|
|
158
|
+
expect back — favourites, a chosen theme. Everything else goes in `transientState`, which is never written and never
|
|
159
|
+
brought back:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
settings: { keepState: true, transientState: ['filter', 'tourStep', 'panelOpen'] }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Do not reset kept state from `onPageLoad` instead: what was kept is restored late — after hydration, once auth knows
|
|
166
|
+
who this is — and lands in the middle of that flow, so half of it is undone. A filter restored on the next visit is a
|
|
167
|
+
page that looks broken. Keeping state is the SPACE's setting; a page does not take `keepState`.
|
|
168
|
+
|
|
169
|
+
That lateness shows. Kept state is restored after the server's first paint, so anything kept that changes what is
|
|
170
|
+
DRAWN — the tool a toolbar shows as last picked, a name in an avatar, a panel left off — is drawn with its default and
|
|
171
|
+
then swapped. List those keys in `paintedState`: they are kept in a cookie too, the server renders with them, and the
|
|
172
|
+
page starts from the same values.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
settings: { keepState: true, paintedState: ['shapesPick', 'name', 'color'], transientState: ['panelOpen'] }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Only what the first paint shows, and small values: the cookie travels with every request and holds a few kilobytes
|
|
179
|
+
(over that, the page falls back to the defaults and the dev tools say so). Never a secret — a key, a token — and never
|
|
180
|
+
a key that is also in `transientState` (`authorSpace` refuses it).
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Elements and styles
|
|
2
|
+
|
|
3
|
+
## Elements
|
|
4
|
+
|
|
5
|
+
One factory per element, named after it. Attributes and the authoring fields go in ONE flat object:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
heading({ content: 'Fieldnotes', subType: 'h2', class: title })
|
|
9
|
+
image({ src: '/fox.jpg', alt: 'A fox', css: { 'aspect-ratio': '3/2' } })
|
|
10
|
+
container({ class: card, children: [ … ] })
|
|
11
|
+
text('Wildlife, close up') // a string is the content
|
|
12
|
+
container([hero, grid]) // an array is the children
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Authoring field | What it does |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `id` | the ONE name this element answers to — its key in the document, a binding's source, a step's target. Unique across the WHOLE space |
|
|
18
|
+
| `class` | a shared class: a `styles()` declaration or a name from the space's `classes`; a list for several. Exclusive with `css` |
|
|
19
|
+
| `css` | rules of this element's own: one set, or one per breakpoint (`{ desktop, tablet, mobile }`) |
|
|
20
|
+
| `states` | `hover`, `focus-visible`, `hidden`… beside the element's own `css` |
|
|
21
|
+
| `variant` | a variant of the class it wears (when the class declares it) or of its type |
|
|
22
|
+
| `slots` | a class for one of the element's OTHER selectors — a form control's `input` |
|
|
23
|
+
| `bind` | where a value comes from |
|
|
24
|
+
| `visible` | a condition (`'src'`, `'!src'`, `{ source, template }`), or `false` to start hidden |
|
|
25
|
+
| `flows` | what happens on click, on submit, on load |
|
|
26
|
+
| `runtime` | `'server'` resolves this provider's data on the server |
|
|
27
|
+
| `loadStrategy` | when the contents mount relative to visibility: `eager` (default), `lazy`, `visible` |
|
|
28
|
+
| `children` | the tree |
|
|
29
|
+
| `meta` | `meta.label` is the element's name in the builder's tree |
|
|
30
|
+
|
|
31
|
+
Anything else in the object is an **attribute** (`authorSpace` warns `unknown-attribute` for one the element never
|
|
32
|
+
reads).
|
|
33
|
+
|
|
34
|
+
**A link's `href` is a page id.** `link({ href: 'reports' })` finds the page by id, folder included. `mode: 'internal'`
|
|
35
|
+
for a path within the space carrying `{{tokens}}` — a row's own slug (`'/games/{{ list_games.item.slug }}'`), a route
|
|
36
|
+
param, state; `mode: 'external'` for a full URL passed through untouched. A link is an `<a>`: never put a link inside
|
|
37
|
+
another link — make the card the link, or the button, not both.
|
|
38
|
+
|
|
39
|
+
**Some types hold no children.** A `heading`, `text`, `paragraph`, `image`, `formControl`… renders its own attributes
|
|
40
|
+
and nothing nested in it — `authorSpace` refuses children there. A heading made of parts (a word in another colour, an
|
|
41
|
+
icon, a badge) is a `container` with the heading's tag: `container({ subType: 'h1', class: title, children: [text('Space
|
|
42
|
+
'), text('Gamer', { class: accent })] })` — give the texts `display: inline` in their class.
|
|
43
|
+
|
|
44
|
+
**A `button` shows its `content` AND its children.** `content` defaults to "Button": a button whose words are children
|
|
45
|
+
says `content: ''` (`default-content-beside-children` warns otherwise). Its accessible name is everything it shows.
|
|
46
|
+
|
|
47
|
+
**A `formControl` works on its own.** Inside a `form` its value is the form's and it validates on submit; outside one it
|
|
48
|
+
keeps its own value and fires `onChange` with `{ value, name }` on every change — a search box or a select that
|
|
49
|
+
filters a screen needs no form. Bind `defaultValue` to the state it writes and a reset of that state reaches it.
|
|
50
|
+
|
|
51
|
+
**A `list` is a `<ul>`.** The browser indents it and puts bullets on it; a list of cards needs
|
|
52
|
+
`{ margin: '0px', padding: '0px', 'list-style-type': 'none' }` in its class.
|
|
53
|
+
|
|
54
|
+
**A provider has no box unless you give it a tag.** An `apiContainer` renders no element of its own by default, so its
|
|
55
|
+
children sit in the parent's layout and a `class` on it styles nothing (`STYLE_WITHOUT_TAG`). Put the layout on the
|
|
56
|
+
parent, or give it `attributes: { subType: 'div' }`.
|
|
57
|
+
|
|
58
|
+
A type this SDK does not ship — a plugin — is authored the same way: `defineElement<Props>({ type, … })`,
|
|
59
|
+
`element<Props>('type', props)`, or `elementsFromManifest(manifest)`. See [plugins](plugins.md).
|
|
60
|
+
|
|
61
|
+
**What a type takes, answered by the package** instead of the 7,000-line `.d.ts`: every catalogue is exported, keyed by
|
|
62
|
+
type — `elementCatalog` (what it is for), `elementDefaultAttributes`, `elementAttributeNames`, `elementTriggers`,
|
|
63
|
+
`elementCallbacks`, `elementSlots`, `elementSourceTypes`, `elementLeafTypes`, plus `BUILTIN_TRANSFORMERS`. One line
|
|
64
|
+
prints any of them:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
node --input-type=module -e "import * as a from '@plitzi/sdk-authoring'; const t = 'formControl'; console.log({ attributes: a.elementDefaultAttributes[t], triggers: a.elementTriggers[t], slots: a.elementSlots[t] })"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## CSS
|
|
71
|
+
|
|
72
|
+
Write CSS as you write CSS, shorthands included — they are expanded to the longhands Plitzi's editor reads. A property
|
|
73
|
+
outside the vocabulary is refused with the key it should have been (`paddingTop` → `padding-top`).
|
|
74
|
+
|
|
75
|
+
- `column(gap, extra?)`, `row(gap, extra?)`, `grid(columns, gap, extra?)` for the three layouts every space repeats.
|
|
76
|
+
- **Breakpoints are ranges, not a cascade.** `tablet` is 48–64rem, `mobile` below 48rem, and each inherits only from
|
|
77
|
+
`desktop` — a rule for `tablet` never reaches a phone (`authorSpace` warns `tablet-rule-skips-mobile`). A rule for
|
|
78
|
+
everything narrower than a desktop is `compact`, written to both: `css: { desktop: { … }, compact: { 'grid-template-columns':
|
|
79
|
+
'1fr' }, mobile: { gap: '8px' } }` — what `tablet` or `mobile` say for themselves wins.
|
|
80
|
+
- **`backdrop-filter`, `filter` and `transform` make an element the containing block of its `position: fixed`
|
|
81
|
+
children.** A glass header with a fixed phone dock inside puts the dock at the top of the header, not the screen.
|
|
82
|
+
- **Fonts are declared.** A `font-family` loads only if the space lists the face in `fonts`
|
|
83
|
+
(`{ source: 'google', family: 'Geist', fallback: 'system-ui, sans-serif', weights: [400, 600], styles: ['normal'] }`).
|
|
84
|
+
|
|
85
|
+
## Classes
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const card = styles('card', {
|
|
89
|
+
css: { padding: '24px', 'border-radius': '12px', 'background-color': 'var(--card)' },
|
|
90
|
+
states: { hover: { 'border-color': 'var(--primary)' }, 'focus-visible': { outline: '2px solid var(--primary)' } },
|
|
91
|
+
variants: { active: { 'border-color': 'var(--primary)' } }
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
container({ class: card, children: [ … ] });
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- A flat object is the base rules; `{ css, states, variants, ancestors }` when there is more. Each part may be written
|
|
98
|
+
per breakpoint.
|
|
99
|
+
- **Share a rule as a class, never as a spread.** Spreading a `const` into each element's `css` shares the source and
|
|
100
|
+
duplicates the document: one selector per element, so restyling one card in the builder restyles one card.
|
|
101
|
+
- **Build a family from a plain object**, not by layering classes: `styles('button-primary', { ...buttonBase, … })`.
|
|
102
|
+
- One name declared twice with rules that disagree is refused. Declare a class once, in the module that owns it, and
|
|
103
|
+
import it.
|
|
104
|
+
- `ancestors: { [sidebar.name]: { variants: { collapsed: { display: 'none' } } } }` styles an element by the state or
|
|
105
|
+
variant of an ancestor class — the way to react to a parent's hover or collapse without a flow.
|
|
106
|
+
- **`hidden` is how an element looks while its `visible` says no** — where it goes as it hides, and where it comes
|
|
107
|
+
from as it shows. Give the class a transition that includes `display` with `allow-discrete`, and a panel fades or
|
|
108
|
+
slides instead of blinking; the transition on the base is the way in, the one in `hidden` the way out:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const panel = styles('panel', {
|
|
112
|
+
css: {
|
|
113
|
+
transition: 'opacity 220ms cubic-bezier(0.2, 0.9, 0.3, 1.2), transform 220ms cubic-bezier(0.2, 0.9, 0.3, 1.2), display 220ms allow-discrete'
|
|
114
|
+
},
|
|
115
|
+
states: {
|
|
116
|
+
hidden: {
|
|
117
|
+
opacity: '0',
|
|
118
|
+
transform: 'translateY(-6px) scale(0.98)',
|
|
119
|
+
transition: 'opacity 120ms ease-in, transform 120ms ease-in, display 120ms allow-discrete'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
What is inside follows its container with `ancestors: { [panel.name]: { states: { hidden: { … } } } }` — a
|
|
126
|
+
different `transition-delay` per item staggers them. A container whose children mount only while it shows
|
|
127
|
+
(`loadStrategy: 'visible'`) empties before it can leave: use `'lazy'` on one that animates out.
|
|
128
|
+
- **`:hover` is part of the selector, not a rule in `customCss`.** `customCss` is for what no class can say:
|
|
129
|
+
keyframes, pseudo-elements, a rule across two unrelated elements, the inside of a third-party widget.
|
|
130
|
+
- Per element TYPE defaults go in `elements: { heading: { base, states, variants, slots } }` — a whole type at once.
|
|
131
|
+
- **Slots** are an element's other selectors: a `formControl`'s `input`, `label`, `error`; a `modalContainer`'s
|
|
132
|
+
`backgroundContainer`, `rootContainer`, `headerContainer`, `headerTitle`, `headerCloseButton`, `bodyContainer`; a
|
|
133
|
+
`dialogContainer`'s footer and buttons. `elementSlots` lists them all by type. Style one element's with `slots`, a
|
|
134
|
+
whole type's with `elements.<type>.slots`.
|
|
135
|
+
- A `modalContainer` / `dialogContainer` is fixed to the viewport, as tall as its content up to the screen, and 500px
|
|
136
|
+
wide up to the screen; theme its look through its slots.
|
|
137
|
+
|
|
138
|
+
**A variant chosen by the data:** `variantFrom(pill, 'jobs.item.status')`, or with a template when the data does not
|
|
139
|
+
already speak in variant names — `variantFrom(pill, 'runs.item.status', { template: "{{ source == 'completed' ?
|
|
140
|
+
'ok' : 'failed' }}" })`. The binding's key names the CLASS; never write it by hand. For "the current page", see
|
|
141
|
+
`activeOn` in [layouts.md](layouts.md).
|
|
142
|
+
|
|
143
|
+
## Colours and themes
|
|
144
|
+
|
|
145
|
+
Every colour is a **variable** with both values, and elements say `var(--name)`:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
variables: {
|
|
149
|
+
color: {
|
|
150
|
+
foreground: { light: '#0c0c14', dark: '#ededf3', default: '#0c0c14' },
|
|
151
|
+
card: { light: '#ffffff', dark: '#101019', default: '#ffffff' },
|
|
152
|
+
primary: { light: '#5b3df5', dark: '#6e52f7', default: '#5b3df5' }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- Choose each value against its own background: a colour picked on white is not the same colour on near-black.
|
|
158
|
+
- Fixed colours are only for surfaces that are fixed in both themes (a brand panel that is always dark) — and then
|
|
159
|
+
the text on them is fixed too. Theme-following text on a fixed background is the bug.
|
|
160
|
+
- A tint of a token is `color-mix(in srgb, var(--primary) 12%, transparent)`, which follows the theme for free.
|
|
161
|
+
- `themeToggle()` is the switch; `theme.resolved` (`light`/`dark`) is a global source for anything else that needs it.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Flows
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
button({
|
|
5
|
+
id: 'cta',
|
|
6
|
+
content: 'Get a quote',
|
|
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
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
A flow is a list: a trigger, then steps in order. "Only if" is on the step (`when`, `whenSucceeded`, `whenFailed`),
|
|
19
|
+
never a nested tree. Use the step builders — they fill in where a step runs and what it takes:
|
|
20
|
+
|
|
21
|
+
- **Where it runs.** A global callback registers under its source MODULE (`state`, `auth`, `actions`), an element
|
|
22
|
+
callback under an element's id, a utility under nothing. Naming either half wrong is a control that does nothing,
|
|
23
|
+
with no error — which is why the builders exist.
|
|
24
|
+
- **Which `setState`.** `setState({ key })` writes `runtime.state.<key>` and is read as `state.<key>`; never put
|
|
25
|
+
`state.` in the key. `updateElement(…)` changes one element's own attribute or state.
|
|
26
|
+
- **Flip in one step.** `toggleState({ key })` for app state, `toggleElement({ category: 'state', key: 'visibility' },
|
|
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.
|
|
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` |
|
|
53
|
+
|
|
54
|
+
## Reading what came before
|
|
55
|
+
|
|
56
|
+
`named('quote', step)` is how a later step reads an earlier one: `{{ quote.output.total }}`. The trigger too:
|
|
57
|
+
`named('submitted', onSubmit())` → `{{ submitted.values.email }}`. Unnamed steps get ids nothing can refer to.
|
|
58
|
+
|
|
59
|
+
A step's params are templates, evaluated in full (conditions, loops, filters). A source in them is named in full:
|
|
60
|
+
a row's button posts `{ jobId: '{{ list_jobRows.item.id }}' }` — the row that was clicked; the short name is refused.
|
|
61
|
+
|
|
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.
|
|
69
|
+
|
|
70
|
+
## Writes and what they refresh
|
|
71
|
+
|
|
72
|
+
`webHook` (not GET) refreshes every request to its own site when it succeeds; `runServerAction` refreshes
|
|
73
|
+
everything. Say what it should refresh — `invalidateQueries: 'elements', invalidateElements: ['orders']` — or
|
|
74
|
+
`'none'` for a step that only reads, or when a refresh would make the page act on the new answer mid-flow.
|
|
75
|
+
|
|
76
|
+
## Triggers belong to the element that fires them
|
|
77
|
+
|
|
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
|
|
80
|
+
server action it started (`onFlowEnd`, `onFlowError`, `onFlowProgress`). A `page` fires `onPageLoad`, a `form`
|
|
81
|
+
`onSubmit`, a `formControl` `onChange`, an `apiContainer` `onApiSuccess`/`onApiError` (each answer, either runtime,
|
|
82
|
+
each refresh), a `modalContainer` `onModalOpen`/`onModalClose`, a `pagination` `onPageChange`. A flow on an element
|
|
83
|
+
that never fires its trigger is refused, naming the type that does.
|
|
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
|
+
|
|
92
|
+
**A form's flow goes on the `form`**, which hands its submit over with `managedByInteractions: true`
|
|
93
|
+
(`FORM_SUBMIT_UNMANAGED` otherwise — the browser submits it natively and `onSubmit` never fires):
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
form({
|
|
97
|
+
id: 'signup',
|
|
98
|
+
managedByInteractions: true,
|
|
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
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A `formControl` is `required` by default: an optional field says `required: false`, or an empty one stops the submit
|
|
110
|
+
without a word.
|
|
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
|
+
|
|
118
|
+
## Modals, dropdowns, tabs
|
|
119
|
+
|
|
120
|
+
- A modal or dialog is declared `visible: false` and driven with `openModal('credits')` / `closeModal('credits')`
|
|
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.
|
|
124
|
+
- A `dropdown`'s label is a child and its `dropdownPopup` sits inside it; a `tabContainer`'s header and body are held
|
|
125
|
+
inside it too. Outside, they are refused.
|
|
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
|
+
|
|
163
|
+
## Lists as state
|
|
164
|
+
|
|
165
|
+
`toggleInState({ key: 'picks', value })` keeps a list; `when({ field: 'state.picks', operator: 'contains', value })`
|
|
166
|
+
asks it. `appendState({ key, value, withId: true })` stores `{ id, value }` — bind `.value`, address by `.id`.
|
|
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,140 @@
|
|
|
1
|
+
# Layouts, and not writing anything twice
|
|
2
|
+
|
|
3
|
+
A space grows by pages, and most of what a page shows is the same on its neighbours: the top bar, the sidebar, the
|
|
4
|
+
footer, the menu, the "ask for help" card. Written into each page, that chrome is a copy per page — sixteen docs pages
|
|
5
|
+
carried the same sidebar sixteen times, around 900 elements whose only difference was which link was highlighted, and
|
|
6
|
+
the copies disagreed the first time one was edited. The tools below remove every one of those copies.
|
|
7
|
+
|
|
8
|
+
## A layout is the shell, written once
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
export const docsLayout: LayoutSpec = {
|
|
12
|
+
id: 'docs-layout',
|
|
13
|
+
label: 'Docs Layout',
|
|
14
|
+
folder: 'docs', // files it beside its pages in the builder; it routes nothing
|
|
15
|
+
body: [
|
|
16
|
+
container({
|
|
17
|
+
id: 'docs-shell',
|
|
18
|
+
class: shell,
|
|
19
|
+
children: [
|
|
20
|
+
topbar(), // helpers returning ElementSpec keep the layout readable
|
|
21
|
+
container({ id: 'docs-body', class: body, children: [sidebar(), container({ id: 'docs-slot', class: slot })] })
|
|
22
|
+
]
|
|
23
|
+
})
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// in the space
|
|
28
|
+
layouts: [appLayout, docsLayout],
|
|
29
|
+
pages: [{ name: 'Quickstart', slug: 'quickstart', folder: 'docs', layout: { id: 'docs-layout', slot: 'docs-slot' }, body: [ … ] }]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **The slot** is the element in the layout where the page's body goes. It must be inside the layout (checked).
|
|
33
|
+
- **Not re-mounted on navigation.** Moving between pages of one layout keeps the shell — its scroll, its open menus,
|
|
34
|
+
its providers, its state — and swaps only the slot. That is also why a layout is cheaper than a copy.
|
|
35
|
+
- **Layouts nest.** `layout: { id: 'app-shell', slot: 'main' }` on a LAYOUT puts a section shell (analytics tabs, docs
|
|
36
|
+
sidebar) inside the application shell. The page resolves the chain from the outside in.
|
|
37
|
+
- **A provider in a layout serves every page** — the plan in a sidebar, a star count in a top bar. A server provider
|
|
38
|
+
(`runtime: 'server'`) in a layout is resolved with the page like one in the page.
|
|
39
|
+
- **Ids are shared** between a layout and all its pages. Name layout elements after the layout (`docs-topbar`), and
|
|
40
|
+
let pages prefix theirs with the page id.
|
|
41
|
+
|
|
42
|
+
## The menu is data
|
|
43
|
+
|
|
44
|
+
The pages a menu lists, their titles, their summaries and their order are ONE list, and everything that shows them
|
|
45
|
+
reads it — the sidebar, the page title, the meta description, "previous / next", an index of cards:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// nav.ts — the single source
|
|
49
|
+
export const DOCS_NAV = [
|
|
50
|
+
{
|
|
51
|
+
id: 'start',
|
|
52
|
+
title: 'Getting started',
|
|
53
|
+
icon: 'fas fa-rocket',
|
|
54
|
+
entries: [
|
|
55
|
+
{ id: 'docs-index', slug: '', title: 'Plitzi for developers', lede: 'What a space is…', icon: 'fas fa-book-open' },
|
|
56
|
+
{ id: 'docs-quickstart', slug: 'quickstart', title: 'Quickstart', lede: 'One command…', icon: 'fas fa-bolt' }
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
// layout.ts — the sidebar, generated
|
|
62
|
+
const navGroup = (group: DocsGroup): ElementSpec =>
|
|
63
|
+
container({
|
|
64
|
+
id: `docs-group-${group.id}`,
|
|
65
|
+
children: group.entries.map(entry =>
|
|
66
|
+
link({
|
|
67
|
+
id: `docs-nav-${entry.id}`,
|
|
68
|
+
href: entry.id,
|
|
69
|
+
class: navLink,
|
|
70
|
+
bind: [activeOn(navLink, entry.id)],
|
|
71
|
+
children: [text(entry.title, { id: `docs-nav-label-${entry.id}` })]
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Adding a page is one entry in the list: the menu, the index and the neighbours' "next" follow.
|
|
78
|
+
|
|
79
|
+
## The current entry: `activeOn`
|
|
80
|
+
|
|
81
|
+
A menu in a layout is the same nodes on every page, so it cannot have the right entry styled by hand. It asks which
|
|
82
|
+
page is showing:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const navLink = styles('nav-link', {
|
|
86
|
+
css: { color: 'var(--muted)' },
|
|
87
|
+
variants: { active: { color: 'var(--foreground)', 'font-weight': '600' } }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
link({ href: 'spaces', class: navLink, bind: [activeOn(navLink, ['spaces', 'space-record'])] });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`activeOn(class, pageIds, { variant?, slot? })` binds the class's variant to `navigation.currentPageId`: the `active`
|
|
94
|
+
variant on those pages, `idle` everywhere else. Several ids for one entry — a section and the pages under it. Do not
|
|
95
|
+
write the binding by hand with a `when` rule per page; that is the long form this replaces.
|
|
96
|
+
|
|
97
|
+
## Pages from a factory
|
|
98
|
+
|
|
99
|
+
When pages share a shape — a heading, a lede, content, a footer — the shape is a function and each page is its data:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
export const docsPage = (id: string, markdownSource: string): PageSpec => {
|
|
103
|
+
const entry = docsEntry(id); // title, slug, lede from nav.ts
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
id,
|
|
107
|
+
name: entry.title,
|
|
108
|
+
slug: entry.slug,
|
|
109
|
+
seoTitle: `${entry.title} — Docs`,
|
|
110
|
+
seoDescription: entry.lede,
|
|
111
|
+
folder: 'docs',
|
|
112
|
+
layout: { id: 'docs-layout', slot: 'docs-slot' },
|
|
113
|
+
body: [
|
|
114
|
+
container({
|
|
115
|
+
id: `${id}-main`, // prefixed: this function runs once per page
|
|
116
|
+
class: main,
|
|
117
|
+
children: [heading(entry.title, { id: `${id}-title`, class: title }), markdown(markdownSource, { id: `${id}-md` }), ...footer(id)]
|
|
118
|
+
})
|
|
119
|
+
]
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const quickstart = docsPage('docs-quickstart', `## One command …`);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Every page is now its content and nothing else, and every page is guaranteed the same structure.
|
|
127
|
+
|
|
128
|
+
## Styles follow the same rule
|
|
129
|
+
|
|
130
|
+
- A role used on several pages is ONE class (`panelCard`, `pageTitle`), declared in the module that owns the area and
|
|
131
|
+
imported — never re-declared per page, never spread into `css`.
|
|
132
|
+
- Layout classes live with the layout; page classes with their page; shared ones in one `styles.ts` per area.
|
|
133
|
+
- A family of looks is a shared base object and one class per member; a state of one element is a variant.
|
|
134
|
+
- If you find yourself copying a `css` block, it is a class. If you find yourself copying an element tree, it is a
|
|
135
|
+
function, a layout, or a `map` over data.
|
|
136
|
+
|
|
137
|
+
## When NOT to use a layout
|
|
138
|
+
|
|
139
|
+
A block that appears on one page belongs to that page. A block on several pages but in different places is a
|
|
140
|
+
function (`communityLinks('docs-help', 'on-surface')`), not a layout. A layout is for what frames the page.
|