@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.
Files changed (359) hide show
  1. package/CHANGELOG.md +1170 -0
  2. package/README.md +100 -7
  3. package/dist/actions.d.ts +6 -2
  4. package/dist/actions.js +5 -2
  5. package/dist/adapters/cloudAdapters.js +4 -3
  6. package/dist/adapters/jsonAdapters.js +28 -6
  7. package/dist/adapters/jsonAdapters.test.d.ts +1 -0
  8. package/dist/auth.d.ts +7 -4
  9. package/dist/auth.js +8 -6
  10. package/dist/core/auth/api.d.ts +29 -1
  11. package/dist/core/auth/api.js +47 -13
  12. package/dist/core/auth/createAuth.d.ts +4 -0
  13. package/dist/core/auth/createAuth.js +3 -3
  14. package/dist/core/auth/credentials.d.ts +3 -0
  15. package/dist/core/auth/credentials.js +1 -1
  16. package/dist/core/auth/csrf.d.ts +25 -1
  17. package/dist/core/auth/csrf.js +29 -4
  18. package/dist/core/auth/renewal.d.ts +26 -0
  19. package/dist/core/auth/renewal.js +24 -0
  20. package/dist/core/auth/routes.js +8 -6
  21. package/dist/core/auth/session.d.ts +45 -0
  22. package/dist/core/auth/session.js +47 -1
  23. package/dist/core/auth/sessionActivity.d.ts +11 -0
  24. package/dist/core/auth/sessionActivity.js +14 -0
  25. package/dist/core/auth/throttle.d.ts +15 -0
  26. package/dist/core/auth/throttle.js +29 -1
  27. package/dist/core/auth/totp.d.ts +9 -1
  28. package/dist/core/auth/totp.js +11 -6
  29. package/dist/core/createServer.actions.test.d.ts +1 -0
  30. package/dist/core/createServer.d.ts +14 -1
  31. package/dist/core/createServer.functions.test.d.ts +1 -0
  32. package/dist/core/createServer.js +22 -12
  33. package/dist/core/fontStore.js +1 -1
  34. package/dist/core/handlers/authRouteHandlers.d.ts +1 -1
  35. package/dist/core/handlers/authRouteHandlers.js +57 -21
  36. package/dist/core/handlers/csrfMiddleware.d.ts +23 -0
  37. package/dist/core/handlers/csrfMiddleware.js +31 -7
  38. package/dist/core/handlers/csrfMiddleware.test.d.ts +1 -0
  39. package/dist/core/handlers/oauthRouteHandlers.js +5 -3
  40. package/dist/core/handlers/socialRouteHandlers.d.ts +1 -1
  41. package/dist/core/handlers/socialRouteHandlers.js +2 -1
  42. package/dist/core/http/dispatcher.d.ts +9 -1
  43. package/dist/core/http/dispatcher.js +38 -11
  44. package/dist/core/http/navigation.d.ts +5 -2
  45. package/dist/core/http/navigation.js +7 -4
  46. package/dist/core/http/socketResponse.d.ts +18 -0
  47. package/dist/core/http/socketResponse.js +59 -0
  48. package/dist/core/http/sse.d.ts +26 -0
  49. package/dist/core/http/sse.js +78 -0
  50. package/dist/core/http/stages/authRoutes.js +3 -1
  51. package/dist/core/http/stages/exchangeRoute.test.d.ts +1 -0
  52. package/dist/core/http/stages/middlewares.d.ts +1 -1
  53. package/dist/core/http/stages/middlewares.js +16 -10
  54. package/dist/core/http/stages/middlewares.test.d.ts +1 -0
  55. package/dist/core/http/stages/oauth.js +2 -2
  56. package/dist/core/http/stages/sessionRenewal.d.ts +16 -0
  57. package/dist/core/http/stages/sessionRenewal.js +81 -0
  58. package/dist/core/http/stages/sessionRenewal.test.d.ts +1 -0
  59. package/dist/core/http/stages/signInRoute.d.ts +2 -0
  60. package/dist/core/http/stages/signInRoute.js +139 -0
  61. package/dist/core/http/stages/signInRoute.test.d.ts +1 -0
  62. package/dist/core/http/types.d.ts +9 -0
  63. package/dist/core/http/webExchange.d.ts +18 -0
  64. package/dist/core/http/webExchange.js +53 -0
  65. package/dist/core/oauth/authorize.js +37 -13
  66. package/dist/core/oauth/consentPage.js +11 -0
  67. package/dist/core/oauth/issueContext.d.ts +14 -0
  68. package/dist/core/oauth/issueContext.js +32 -0
  69. package/dist/core/oauth/records.d.ts +7 -0
  70. package/dist/core/oauth/register.d.ts +2 -0
  71. package/dist/core/oauth/register.js +32 -11
  72. package/dist/core/oauth/register.test.d.ts +1 -0
  73. package/dist/core/oauth/token.js +21 -6
  74. package/dist/core/oauth/token.test.d.ts +1 -0
  75. package/dist/core/requestParser.d.ts +4 -0
  76. package/dist/core/requestParser.js +14 -7
  77. package/dist/core/sdkAssets.js +1 -1
  78. package/dist/core/server/baseServer.d.ts +10 -0
  79. package/dist/core/server/baseServer.js +42 -11
  80. package/dist/core/server/closeOnSignals.js +4 -3
  81. package/dist/core/server/drain.d.ts +18 -0
  82. package/dist/core/server/drain.js +62 -0
  83. package/dist/core/server/drain.test.d.ts +1 -0
  84. package/dist/core/server/fleet/channel.d.ts +103 -0
  85. package/dist/core/server/fleet/channel.js +191 -0
  86. package/dist/core/server/fleet/channel.test.d.ts +1 -0
  87. package/dist/core/server/fleet/fixtures/fleetServer.d.ts +1 -0
  88. package/dist/core/server/fleet/fleet.integration.test.d.ts +1 -0
  89. package/dist/core/server/fleet/link.d.ts +23 -0
  90. package/dist/core/server/fleet/link.js +70 -0
  91. package/dist/core/server/fleet/relay.d.ts +11 -0
  92. package/dist/core/server/fleet/relay.js +104 -0
  93. package/dist/core/server/fleet/relay.test.d.ts +1 -0
  94. package/dist/core/server/fleet/role.d.ts +46 -0
  95. package/dist/core/server/fleet/role.js +59 -0
  96. package/dist/core/server/fleet/role.test.d.ts +1 -0
  97. package/dist/core/server/fleet/stores.d.ts +7 -0
  98. package/dist/core/server/fleet/stores.js +19 -0
  99. package/dist/core/server/fleet/supervisor.d.ts +26 -0
  100. package/dist/core/server/fleet/supervisor.js +108 -0
  101. package/dist/core/server/fleet/supervisor.test.d.ts +1 -0
  102. package/dist/core/server/pageServer.js +34 -5
  103. package/dist/core/services/action.js +10 -2
  104. package/dist/core/services/functionRoutes.d.ts +9 -0
  105. package/dist/core/services/functionRoutes.js +47 -0
  106. package/dist/core/services/realtime.d.ts +8 -0
  107. package/dist/core/services/realtime.js +57 -0
  108. package/dist/core/services/registry.js +6 -2
  109. package/dist/core/staticFiles.js +12 -4
  110. package/dist/core/staticFiles.test.d.ts +1 -0
  111. package/dist/core/transports.d.ts +13 -3
  112. package/dist/core/transports.js +14 -5
  113. package/dist/functions-api.d.ts +281 -0
  114. package/dist/functions.d.ts +19 -0
  115. package/dist/functions.js +5 -0
  116. package/dist/functionsRunner.d.ts +15 -0
  117. package/dist/functionsRunner.js +6 -0
  118. package/dist/handlers.d.ts +2 -2
  119. package/dist/handlers.js +2 -2
  120. package/dist/helpers/atomicFile.d.ts +10 -0
  121. package/dist/helpers/atomicFile.js +33 -0
  122. package/dist/helpers/atomicFile.test.d.ts +1 -0
  123. package/dist/helpers/buildResponseHelpers.js +35 -11
  124. package/dist/helpers/buildServerInfo.js +4 -2
  125. package/dist/helpers/buildServerInfo.test.d.ts +1 -0
  126. package/dist/helpers/cache/CompressedFileCache.d.ts +18 -0
  127. package/dist/helpers/cache/CompressedFileCache.js +44 -0
  128. package/dist/helpers/cache/CompressedFileCache.test.d.ts +1 -0
  129. package/dist/helpers/cache/cacheManager.d.ts +1 -1
  130. package/dist/helpers/cache/cacheManager.js +2 -1
  131. package/dist/helpers/cache/cacheManager.test.d.ts +1 -0
  132. package/dist/helpers/cache/index.d.ts +1 -1
  133. package/dist/helpers/cache/keys.d.ts +15 -1
  134. package/dist/helpers/cache/keys.js +47 -5
  135. package/dist/helpers/cache/serverCaches.d.ts +7 -1
  136. package/dist/helpers/compress.d.ts +3 -1
  137. package/dist/helpers/compress.js +11 -3
  138. package/dist/helpers/hydrationPayload.d.ts +11 -0
  139. package/dist/helpers/hydrationPayload.js +51 -0
  140. package/dist/helpers/hydrationPayload.test.d.ts +1 -0
  141. package/dist/helpers/metrics.d.ts +1 -1
  142. package/dist/helpers/metrics.js +3 -2
  143. package/dist/helpers/outboundGuard.d.ts +14 -6
  144. package/dist/helpers/outboundGuard.js +132 -8
  145. package/dist/helpers/paintedCookie.d.ts +8 -0
  146. package/dist/helpers/paintedCookie.js +13 -0
  147. package/dist/helpers/reportReactBuild.d.ts +9 -0
  148. package/dist/helpers/reportReactBuild.js +18 -0
  149. package/dist/helpers/reportReactBuild.test.d.ts +1 -0
  150. package/dist/helpers/serverLog.d.ts +25 -1
  151. package/dist/helpers/serverLog.js +68 -3
  152. package/dist/helpers/serverLog.test.d.ts +1 -0
  153. package/dist/helpers/wsFrame.d.ts +3 -0
  154. package/dist/helpers/wsFrame.js +5 -0
  155. package/dist/index.d.ts +6 -1
  156. package/dist/index.js +5 -2
  157. package/dist/kernel.d.ts +2 -1
  158. package/dist/kernel.js +3 -2
  159. package/dist/modules/actions/connectorWrite.js +2 -1
  160. package/dist/modules/actions/index.d.ts +29 -10
  161. package/dist/modules/actions/index.js +43 -6
  162. package/dist/modules/actions/jobs/failureStreak.d.ts +17 -0
  163. package/dist/modules/actions/jobs/failureStreak.js +47 -0
  164. package/dist/modules/actions/jobs/failureStreak.test.d.ts +1 -0
  165. package/dist/modules/actions/jobs/index.d.ts +2 -1
  166. package/dist/modules/actions/jobs/index.js +5 -3
  167. package/dist/modules/actions/jobs/memoryQueue.d.ts +3 -0
  168. package/dist/modules/actions/jobs/memoryQueue.js +17 -1
  169. package/dist/modules/actions/jobs/scheduler.d.ts +2 -0
  170. package/dist/modules/actions/jobs/scheduler.js +12 -7
  171. package/dist/modules/actions/jobs/worker.d.ts +3 -1
  172. package/dist/modules/actions/jobs/worker.js +9 -4
  173. package/dist/modules/actions/moduleFor.js +5 -2
  174. package/dist/modules/actions/runtime/errors.d.ts +12 -0
  175. package/dist/modules/actions/runtime/errors.js +16 -1
  176. package/dist/modules/actions/runtime/kvChange.d.ts +27 -0
  177. package/dist/modules/actions/runtime/kvChange.js +23 -0
  178. package/dist/modules/actions/runtime/kvList.d.ts +51 -0
  179. package/dist/modules/actions/runtime/kvList.js +52 -0
  180. package/dist/modules/actions/runtime/kvStore.d.ts +8 -0
  181. package/dist/modules/actions/runtime/kvStore.js +46 -20
  182. package/dist/modules/actions/runtime/memoryKv.d.ts +3 -0
  183. package/dist/modules/actions/runtime/memoryKv.js +19 -1
  184. package/dist/modules/actions/runtime/namespaceKv.d.ts +4 -6
  185. package/dist/modules/actions/runtime/namespaceKv.js +15 -3
  186. package/dist/modules/actions/runtime/namespaceKv.test.d.ts +1 -0
  187. package/dist/modules/actions/runtime/precheck.js +4 -8
  188. package/dist/modules/actions/runtime/rateLimit.d.ts +19 -0
  189. package/dist/modules/actions/runtime/rateLimit.js +19 -0
  190. package/dist/modules/actions/runtime/redisKv.d.ts +27 -0
  191. package/dist/modules/actions/runtime/redisKv.integration.test.d.ts +1 -0
  192. package/dist/modules/actions/runtime/redisKv.js +46 -0
  193. package/dist/modules/actions/runtime/report.js +2 -1
  194. package/dist/modules/actions/runtime/runAction.d.ts +10 -2
  195. package/dist/modules/actions/runtime/runAction.js +52 -116
  196. package/dist/modules/actions/runtime/runLogger.d.ts +5 -0
  197. package/dist/modules/actions/runtime/runLogger.js +8 -2
  198. package/dist/modules/actions/runtime/signing.d.ts +20 -0
  199. package/dist/modules/actions/runtime/signing.js +25 -0
  200. package/dist/modules/actions/runtime/signing.test.d.ts +1 -0
  201. package/dist/modules/actions/runtime/taskContext.d.ts +33 -0
  202. package/dist/modules/actions/runtime/taskContext.js +124 -0
  203. package/dist/modules/actions/tasks/builtins.js +5 -1
  204. package/dist/modules/actions/tasks/flow.js +81 -10
  205. package/dist/modules/actions/tasks/helpers.js +2 -2
  206. package/dist/modules/actions/tasks/http.js +2 -3
  207. package/dist/modules/actions/tasks/kv.js +39 -0
  208. package/dist/modules/actions/tasks/list.d.ts +2 -0
  209. package/dist/modules/actions/tasks/list.js +145 -0
  210. package/dist/modules/actions/tasks/realtime.d.ts +2 -0
  211. package/dist/modules/actions/tasks/realtime.js +104 -0
  212. package/dist/modules/actions/tasks/registry.d.ts +10 -1
  213. package/dist/modules/actions/tasks/registry.js +17 -6
  214. package/dist/modules/actions/transport/callHandler.js +5 -2
  215. package/dist/modules/actions/transport/catalogHandler.d.ts +5 -2
  216. package/dist/modules/actions/transport/catalogHandler.js +3 -2
  217. package/dist/modules/actions/transport/stream.d.ts +4 -5
  218. package/dist/modules/actions/transport/stream.js +12 -76
  219. package/dist/modules/actions/transport/webhookHandler.js +4 -3
  220. package/dist/modules/actions/types.d.ts +86 -3
  221. package/dist/modules/connectors/engine.js +3 -5
  222. package/dist/modules/functions/build.d.ts +22 -0
  223. package/dist/modules/functions/build.js +122 -0
  224. package/dist/modules/functions/build.test.d.ts +1 -0
  225. package/dist/modules/functions/capabilities.d.ts +22 -0
  226. package/dist/modules/functions/capabilities.js +228 -0
  227. package/dist/modules/functions/config.d.ts +34 -0
  228. package/dist/modules/functions/config.js +24 -0
  229. package/dist/modules/functions/context.d.ts +17 -0
  230. package/dist/modules/functions/context.js +88 -0
  231. package/dist/modules/functions/contract.d.ts +121 -0
  232. package/dist/modules/functions/contract.js +9 -0
  233. package/dist/modules/functions/driver.d.ts +36 -0
  234. package/dist/modules/functions/driver.js +150 -0
  235. package/dist/modules/functions/load.d.ts +15 -0
  236. package/dist/modules/functions/load.js +45 -0
  237. package/dist/modules/functions/load.test.d.ts +1 -0
  238. package/dist/modules/functions/local.d.ts +29 -0
  239. package/dist/modules/functions/local.js +61 -0
  240. package/dist/modules/functions/local.test.d.ts +1 -0
  241. package/dist/modules/functions/manifest.d.ts +24 -0
  242. package/dist/modules/functions/manifest.js +163 -0
  243. package/dist/modules/functions/manifest.test.d.ts +1 -0
  244. package/dist/modules/functions/native.d.ts +8 -0
  245. package/dist/modules/functions/native.js +21 -0
  246. package/dist/modules/functions/native.test.d.ts +1 -0
  247. package/dist/modules/functions/protocol.d.ts +216 -0
  248. package/dist/modules/functions/protocol.js +18 -0
  249. package/dist/modules/functions/routes.d.ts +36 -0
  250. package/dist/modules/functions/routes.js +181 -0
  251. package/dist/modules/functions/routes.test.d.ts +1 -0
  252. package/dist/modules/functions/runner/messages.d.ts +5 -0
  253. package/dist/modules/functions/runner/messages.js +188 -0
  254. package/dist/modules/functions/runner/remote.d.ts +19 -0
  255. package/dist/modules/functions/runner/remote.js +116 -0
  256. package/dist/modules/functions/runner/remote.test.d.ts +1 -0
  257. package/dist/modules/functions/runner/service.d.ts +32 -0
  258. package/dist/modules/functions/runner/service.js +209 -0
  259. package/dist/modules/functions/sandbox/guest.d.ts +45 -0
  260. package/dist/modules/functions/sandbox/guest.js +660 -0
  261. package/dist/modules/functions/sandbox/isolate.d.ts +37 -0
  262. package/dist/modules/functions/sandbox/isolate.js +347 -0
  263. package/dist/modules/functions/sandbox/isolate.test.d.ts +1 -0
  264. package/dist/modules/functions/sandbox/prelude.d.ts +2 -0
  265. package/dist/modules/functions/sandbox/prelude.js +49 -0
  266. package/dist/modules/functions/space.d.ts +40 -0
  267. package/dist/modules/functions/space.js +162 -0
  268. package/dist/modules/functions/testing/tasksOf.d.ts +4 -0
  269. package/dist/modules/functions/tryEntry.d.ts +9 -0
  270. package/dist/modules/functions/tryEntry.js +49 -0
  271. package/dist/modules/mongo/kv.js +31 -0
  272. package/dist/modules/mysql/accounts.js +19 -9
  273. package/dist/modules/mysql/index.d.ts +2 -1
  274. package/dist/modules/mysql/jobs/index.d.ts +1 -1
  275. package/dist/modules/mysql/jobs/kv.js +30 -0
  276. package/dist/modules/mysql/jobs/schema.d.ts +9 -0
  277. package/dist/modules/mysql/jobs/schema.js +21 -4
  278. package/dist/modules/mysql/schema.js +25 -0
  279. package/dist/modules/mysql/sessions.d.ts +1 -11
  280. package/dist/modules/mysql/sessions.js +6 -1
  281. package/dist/modules/realtime/declarations.d.ts +11 -0
  282. package/dist/modules/realtime/declarations.js +26 -0
  283. package/dist/modules/realtime/grants.d.ts +23 -0
  284. package/dist/modules/realtime/grants.js +61 -0
  285. package/dist/modules/realtime/handlers.d.ts +74 -0
  286. package/dist/modules/realtime/handlers.js +224 -0
  287. package/dist/modules/realtime/hub.d.ts +62 -0
  288. package/dist/modules/realtime/hub.js +141 -0
  289. package/dist/modules/realtime/index.d.ts +51 -0
  290. package/dist/modules/realtime/index.js +62 -0
  291. package/dist/modules/realtime/memoryPubSub.d.ts +10 -0
  292. package/dist/modules/realtime/memoryPubSub.js +43 -0
  293. package/dist/modules/realtime/realtime.test.d.ts +1 -0
  294. package/dist/modules/realtime/redisPubSub.d.ts +24 -0
  295. package/dist/modules/realtime/redisPubSub.js +36 -0
  296. package/dist/modules/realtime/redisPubSub.test.d.ts +1 -0
  297. package/dist/modules/realtime/socket.d.ts +25 -0
  298. package/dist/modules/realtime/socket.js +122 -0
  299. package/dist/modules/realtime/socket.test.d.ts +1 -0
  300. package/dist/modules/rsc/handler.d.ts +16 -0
  301. package/dist/modules/rsc/handler.js +17 -10
  302. package/dist/modules/rsc/handler.test.d.ts +1 -0
  303. package/dist/modules/rsc/resolveRscData.js +2 -1
  304. package/dist/modules/runtime/bundle.d.ts +24 -0
  305. package/dist/modules/runtime/bundle.js +102 -0
  306. package/dist/modules/runtime/contract.d.ts +37 -0
  307. package/dist/modules/runtime/contract.js +21 -0
  308. package/dist/modules/runtime/entry.test.d.ts +1 -0
  309. package/dist/modules/runtime/host.d.ts +38 -0
  310. package/dist/modules/runtime/host.js +68 -0
  311. package/dist/modules/runtime/inside.d.ts +15 -0
  312. package/dist/modules/runtime/inside.js +47 -0
  313. package/dist/modules/runtime/inside.test.d.ts +1 -0
  314. package/dist/modules/runtime/runner.d.ts +11 -0
  315. package/dist/modules/runtime/runner.js +38 -0
  316. package/dist/modules/runtime/runtime.test.d.ts +1 -0
  317. package/dist/modules/runtime/stages.d.ts +50 -0
  318. package/dist/modules/runtime/stages.js +117 -0
  319. package/dist/modules/ssr/Component.d.ts +3 -1
  320. package/dist/modules/ssr/Component.js +2 -1
  321. package/dist/modules/ssr/handler.js +10 -2
  322. package/dist/modules/ssr/loadPluginComponents.js +2 -1
  323. package/dist/modules/ssr/prepareRender.js +39 -10
  324. package/dist/modules/ssr/preview.d.ts +3 -0
  325. package/dist/modules/ssr/preview.js +17 -7
  326. package/dist/modules/ssr/redirect.test.d.ts +1 -0
  327. package/dist/modules/ssr/registerExternalPlugins.d.ts +6 -1
  328. package/dist/modules/ssr/registerExternalPlugins.js +19 -16
  329. package/dist/modules/ssr/registerExternalPlugins.test.d.ts +1 -0
  330. package/dist/modules/ssr/reportMissingPlugins.d.ts +9 -0
  331. package/dist/modules/ssr/reportMissingPlugins.js +24 -0
  332. package/dist/modules/ssr/reportMissingPlugins.test.d.ts +1 -0
  333. package/dist/modules/ssr/streamBody.d.ts +2 -2
  334. package/dist/modules/ssr/streamBody.js +4 -1
  335. package/dist/modules/ssr/template.js +1 -1
  336. package/dist/modules/ssr/views/template.ejs +8 -5
  337. package/dist/mysql.d.ts +1 -1
  338. package/dist/mysql.js +2 -2
  339. package/dist/oauth.js +2 -2
  340. package/dist/plugins/compile.js +9 -4
  341. package/dist/plugins/copy.js +3 -3
  342. package/dist/plugins/manager.d.ts +35 -3
  343. package/dist/plugins/manager.js +109 -38
  344. package/dist/runtime.d.ts +15 -0
  345. package/dist/runtime.js +7 -0
  346. package/package.json +39 -8
  347. package/skills/plitzi-authoring/SKILL.md +98 -14
  348. package/skills/plitzi-authoring/reference/accessibility.md +111 -0
  349. package/skills/plitzi-authoring/reference/authoring-errors.md +70 -0
  350. package/skills/plitzi-authoring/reference/data-and-visibility.md +84 -18
  351. package/skills/plitzi-authoring/reference/elements-and-styles.md +61 -7
  352. package/skills/plitzi-authoring/reference/flows.md +108 -17
  353. package/skills/plitzi-authoring/reference/lists.md +74 -0
  354. package/skills/plitzi-authoring/reference/performance.md +67 -0
  355. package/skills/plitzi-authoring/reference/plugins.md +167 -0
  356. package/skills/plitzi-authoring/reference/review-checklist.md +19 -2
  357. package/skills/plitzi-authoring/reference/templates.md +67 -27
  358. package/skills/plitzi-authoring/reference/testing.md +72 -5
  359. 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
- onClick(),
9
- named('quote', runServerAction({ actionId: 'shipping-quote', input: { city: 'Berlin' }, mode: 'await' })),
10
- whenSucceeded('quote', setState({ key: 'quote', type: 'text', value: '{{ quote.output.summary }}' })),
11
- whenFailed('quote', addNotification({ content: 'Could not reach the courier', appeareance: 'danger' }))
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
- 'panel')` to show/hide an element. Never two branches under opposite `when` guards — they read the state as it was
26
- when the flow started, a click behind.
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 }}' }`, never `input: '{"title": …}'` — a
38
- value with a quotation mark or a newline makes the text unparseable, and unparseable input posts `{}`.
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`, `onLoad`, `onHover`, `onMouseEnter`/`onMouseLeave`, `onFocus`/`onBlur` and the ends of a
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: [[named('submitted', onSubmit()), setState({ key: 'email', type: 'text', value: '{{ submitted.values.email }}' })]],
62
- children: [formControl({ id: 'email', name: 'email', label: 'Email', subType: 'email' }), button({ content: 'Sign up', subType: 'submit' })]
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 read inside as `{{ modalContainer_credits.content }}`.
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. Tablet rules are repeated for mobile when
24
- phones should keep them.
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