@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
@@ -28,17 +28,22 @@ const { schema, style, warnings } = authorSpace(space);
28
28
  ```
29
29
 
30
30
  The published `.d.ts` is the reference for every factory, field and step builder
31
- (`node_modules/@plitzi/sdk-authoring/dist/index.d.ts`) — read it instead of guessing a name or a param.
31
+ (`node_modules/@plitzi/sdk-authoring/dist/index.d.ts`) — read it instead of guessing a name or a param. For one element
32
+ type, the exported catalogues answer faster (`elementDefaultAttributes`, `elementTriggers`, `elementSlots`… — see
33
+ [elements and styles](reference/elements-and-styles.md)).
32
34
 
33
35
  ## How to work
34
36
 
35
37
  1. **Read before you write.** Open `src/space.ts` (and whatever it imports) and find the layout, the classes and the
36
38
  helpers already there. Extend them; do not add a second way of doing something the space already does.
37
- 2. **Change the declaration, then author it.** `npm run author` (or restart the server) runs `authorSpace`. A refusal
38
- is an error that names the element and the fix — fix the declaration, never work around the check.
39
+ 2. **Change the declaration, then author it.** `npm run author` (or restart the server) runs `authorSpace`. It checks
40
+ everything — every field, value, template, name, param and page link — and a refusal says what to write instead:
41
+ do exactly that. Never work around a check, cast past it, or move the logic into a plugin to escape it. The first
42
+ refusals come one at a time; the linter's come as ONE list — fix every line of it before running again. How it
43
+ checks, a one-file author script for any project, and what it cannot see: [validation](reference/validation.md).
39
44
  3. **Read every warning.** Each one names something written that will not do what it says. Zero warnings is the bar.
40
- 4. **Look at it.** `npm run visual` opens the page in a browser. Check desktop, tablet and mobile, light and dark,
41
- and the page while its data is still loading — not only the finished frame.
45
+ 4. **Look at it.** `npm run shot -- /about --width 390 --scheme dark` saves a picture of one page; `npm run visual`
46
+ runs the checks. Look at desktop, tablet and mobile, light and dark, and the page while its data is still loading.
42
47
  5. **Go through the [review checklist](reference/review-checklist.md) before saying it is done.** It is the feedback a
43
48
  reviewer gives on every change, written down so you do not need to hear it.
44
49
 
@@ -59,20 +64,93 @@ The published `.d.ts` is the reference for every factory, field and step builder
59
64
  loads (`condition-starts-visible` warns). When a flag HIDES it (shown → hidden: a sidebar label until the sidebar
60
65
  is folded), it keeps the default and an absent flag must leave it shown. See
61
66
  [data and visibility](reference/data-and-visibility.md).
62
- 5. **Know where a template is evaluated.** A binding's `twigTemplate` and a step's params evaluate full Twig. An
63
- ATTRIBUTE only resolves a name with filters (`{{ post.slug|url_encode }}`); a condition there is used as written.
64
- Inside any template a source is spelled in full — `apiContainer_stats`, `list_rows` — never by the element's short
65
- id. See [templates](reference/templates.md).
67
+ 5. **Know where a template is evaluated.** A binding's template (`bindTemplate`) and a step's params evaluate full
68
+ Twig. An ATTRIBUTE resolves a name with filters (`{{ list_games.item.slug }}`, `{{ post.slug|url_encode }}`) against
69
+ the sources around the element; a condition there is used as written. Inside any template a source is spelled in
70
+ full — `apiContainer_stats`, `list_rows` — and a query param is `navigation.queryParams.x`. A template that feeds a
71
+ list its `items` hands over its value (`returns: 'value'`). `authorSpace` refuses a template it cannot read and a
72
+ name nothing answers to. See [templates](reference/templates.md).
66
73
  6. **Tokens, not colours.** Every colour is a variable of the space with a `light` and a `dark` value; both themes are
67
74
  checked from the first commit.
68
75
  7. **Times in UTC, and say so.** Format with an explicit zone (`|date('j M · H:i', 'UTC')`) and print "UTC" beside it:
69
76
  a page rendered on a server and hydrated in a browser in another zone must agree on the hour.
70
77
  8. **Breakpoints are ranges.** `tablet` (48–64rem) and `mobile` (below 48rem) each inherit only from `desktop`; a rule
71
- meant for both is written in both.
78
+ meant for both is written under `compact`.
72
79
  9. **Never invent data.** Numbers, names and states on screen come from a source. A panel with nothing true to say is
73
- an empty state, not a placeholder figure.
80
+ an empty state, not a placeholder figure. With no backend, the data is JSON the project serves
81
+ (`public/data/*.json`) read by an `apiContainer` — see [data and visibility](reference/data-and-visibility.md).
74
82
  10. **Never hand-write** `flat`, derived ids, `styleSelectors`, `beforeNode`/`afterNode`/`flowId`, or a
75
83
  `styleVariant` binding's key — use the factories, `variantFrom` and `activeOn`.
84
+ 11. **A switch is named for how it leaves its default.** `toggleState` turns a key nobody has set yet ON, so a key
85
+ named for the default (`showPlates`, on by default) takes a first click to do nothing: name it `platesOff`, and read
86
+ every switch through `computed` (`plates: '{{ state.platesOff ? false : true }}'`) so its default shows before
87
+ anybody touches it.
88
+ 12. **Hand plugins over as their declarations**: `authorSpace(space, { plugins: [declaration] })` holds a plugin —
89
+ its own type, or a `custom({ renderType })` host — to the events, actions and attributes it declares. See
90
+ [plugins](reference/plugins.md).
91
+ 13. **Usable without sight.** Screen readers and browser agents (Claude in Chrome) find a page's controls in its
92
+ accessibility tree: every button and link has words (an icon-only button a `title`), every field a `label`
93
+ (`hideLabel: true` hides it and keeps the name), every image an `alt` or `decorative: true`. Clicks go on a
94
+ `button` — it holds children, so a whole card can be one — or a `link`, never on a container. See
95
+ [accessibility](reference/accessibility.md).
96
+
97
+ ## Recipes
98
+
99
+ Copy the one that fits, then change the names. Each is the shape `authorSpace` accepts on the first try.
100
+
101
+ ```ts
102
+ // Data with no backend: a JSON file the project serves (public/data/games.json), read by a provider.
103
+ apiContainer({ id: 'catalog', query: '/data/games.json', cache: true, children: [ /* reads catalog.data.… */ ] })
104
+
105
+ // A list of rows from that data. Inside a row: short form in bindings, full name in templates and attributes.
106
+ list({ id: 'games', source: 'controlled', bind: { items: 'catalog.data.games' }, children: [
107
+ link({ mode: 'internal', href: '/games/{{ list_games.item.slug }}', children: [
108
+ text('', { bind: { content: 'games.item.title' } })
109
+ ] })
110
+ ] })
111
+
112
+ // A filtered list: a template that hands over its VALUE.
113
+ list({ id: 'shown', source: 'controlled',
114
+ bind: [bindTemplate('items', 'catalog.data.games', "{{ source|filter(g => g.genre == state.genre) }}", { returns: 'value' })],
115
+ children: [ … ] })
116
+
117
+ // Text computed from data.
118
+ text('', { bind: [bindTemplate('content', 'catalog.data.games', '{{ source|length }} games')] })
119
+
120
+ // Shown only when a computed condition says so (starts hidden, no flash).
121
+ text('Nothing here yet', { visible: { source: 'catalog.data.games', template: '{{ source is defined and source|length == 0 }}' } })
122
+
123
+ // A value used in many places: declare it once in the space, read it anywhere as computed.xp.
124
+ computed: { xp: '{{ (state.favourites|length) * 10 }}' }
125
+
126
+ // A button that does something: a flow is [trigger, steps…].
127
+ button({ content: 'Save', flows: [[onClick(), setState({ key: 'saved', type: 'boolean', value: true })]] })
128
+
129
+ // A form: the flow goes on the form, which manages its own submit.
130
+ form({ id: 'signup', managedByInteractions: true,
131
+ flows: [[named('sent', onSubmit()), setState({ key: 'email', type: 'text', value: '{{ sent.values.email }}' })]],
132
+ children: [formControl({ name: 'email', label: 'Email', subType: 'email' }), button({ content: 'Sign up', subType: 'submit' })] })
133
+
134
+ // A modal: hidden until a flow opens it.
135
+ modalContainer({ id: 'details', visible: false, title: 'Details', children: [ … ] })
136
+ button({ content: 'Open', flows: [[onClick(), openModal('details')]] })
137
+
138
+ // A plugin: author it from its own declaration, flow on its events, call its actions — all checked.
139
+ const seats = defineElement<SeatPickerAttributes>(declaration);
140
+ seats({ id: 'seats', flows: [[named('picked', declaredTrigger(declaration, 'onPick')), setState({ key: 'seat', type: 'text', value: '{{ picked.seat }}' })]] })
141
+ button({ content: 'Clear', flows: [[onClick(), declaredCallback(declaration, 'reset', { on: 'seats' })]] })
142
+ authorSpace(space, { plugins: [declaration] })
143
+
144
+ // Usable without sight: an icon-only button says what it does; a clickable card IS a button; a field keeps its label
145
+ // out of sight rather than dropping it; a picture says what it shows, or that it only decorates.
146
+ button({ content: '', title: 'Close', children: [fontAwesome({ icon: 'fa-solid fa-xmark' })], flows: [[onClick(), closeModal('details')]] })
147
+ button({ content: '', class: card, children: [text('Pro'), text('12 € a month')], flows: [[onClick(), setState({ key: 'plan', type: 'text', value: 'pro' })]] })
148
+ formControl({ name: 'q', label: 'Search the docs', hideLabel: true, placeholder: 'Search…' })
149
+ image({ src: '/team.jpg', alt: 'The team at the 2026 offsite' }); image({ src: '/grain.png', decorative: true })
150
+
151
+ // A link: to a page by its id, to a path with mode 'internal', to anything else with mode 'external'.
152
+ link({ href: 'about' }); link({ href: '/games/nebula', mode: 'internal' }); link({ href: 'mailto:hi@x.com', mode: 'external' })
153
+ ```
76
154
 
77
155
  ## References
78
156
 
@@ -80,10 +158,16 @@ The published `.d.ts` is the reference for every factory, field and step builder
80
158
  | --- | --- |
81
159
  | [elements-and-styles.md](reference/elements-and-styles.md) | Any element or CSS: factories, fields, classes, states, variants, tokens, fonts, lists, links |
82
160
  | [layouts.md](reference/layouts.md) | Anything shown on more than one page; menus; reducing duplication of elements and styles |
83
- | [data-and-visibility.md](reference/data-and-visibility.md) | Bindings, providers, loading/empty/error states, live data, caching, showing and hiding |
161
+ | [data-and-visibility.md](reference/data-and-visibility.md) | Bindings, providers, offline data, loading/empty/error states, live data, caching, showing and hiding, kept state |
162
+ | [lists.md](reference/lists.md) | Rendering rows, filtering and sorting them, a detail page for one record |
163
+ | [validation.md](reference/validation.md) | How `authorSpace` checks, the loop that wastes no attempts, and what it cannot see |
164
+ | [authoring-errors.md](reference/authoring-errors.md) | What `authorSpace` refuses or warns about, and what to write instead |
84
165
  | [templates.md](reference/templates.md) | Any `{{ … }}` or `{% … %}`: where it runs, naming sources, filters, tests, dates |
85
- | [flows.md](reference/flows.md) | Clicks, submits, page loads, server actions, modals, state |
166
+ | [flows.md](reference/flows.md) | Clicks, submits, page loads, server actions, realtime channels, modals, state |
167
+ | [plugins.md](reference/plugins.md) | A component of your own: props, binding them, writing state, channels, registering |
86
168
  | [structure.md](reference/structure.md) | A space bigger than one screen: files, helpers, naming, keeping it short |
87
- | [testing.md](reference/testing.md) | Handles, visual checks, catching a flash from the first frame |
169
+ | [testing.md](reference/testing.md) | Any test: `inspectPage` (one call, every problem), handles, fixtures, catching a flash from the first frame, shortcuts, counting renders |
170
+ | [performance.md](reference/performance.md) | A page with many elements, a busy flow, something that feels slow: what renders, what it costs, how to measure it |
88
171
  | [templates-and-export.md](reference/templates-and-export.md) | Publishing a template; turning an exported JSON into code |
172
+ | [accessibility.md](reference/accessibility.md) | Icon buttons, fields, images, clickable cards, toggles, headings, landmarks, a canvas — anything a screen reader or a browser agent has to use |
89
173
  | [review-checklist.md](reference/review-checklist.md) | Before you say it is done |
@@ -0,0 +1,111 @@
1
+ # Usable without sight
2
+
3
+ A screen reader and a browser agent (Claude in Chrome) read a page the same way. They use the **accessibility
4
+ tree** the browser builds from the markup, where every control is a role and a name. If a control has no name, or a
5
+ click sits on a box that is not a control, those readers can't find it, even though the page looks fine. The linter
6
+ warns about each case below (`control-without-name`, `image-without-alt`, `click-on-static-element`,
7
+ `heading-level-skipped`, `label-ignored`). Fix them the same way you fix any other warning.
8
+
9
+ ## Controls have words
10
+
11
+ ```ts
12
+ // Words of its own: nothing to add.
13
+ button({ content: 'Save' })
14
+
15
+ // An icon only: `title` is its name, and also its tooltip. The icon is hidden from the tree by itself.
16
+ button({ content: '', title: 'Close', children: [fontAwesome({ icon: 'fa-solid fa-xmark' })] })
17
+
18
+ // Words that do not say what it does — a key hint, a count: `label` is read in their place.
19
+ button({ content: '', label: 'Select (V)', title: 'Select — V', children: [fontAwesome({ icon: 'fa-solid fa-arrow-pointer' }), text('V')] })
20
+
21
+ // A link around a whole card: without `label` it is announced as every word inside the card.
22
+ link({ href: '/posts/field-notes', mode: 'internal', label: 'Read “Field notes”', children: [ /* cover, topic, title, byline */ ] })
23
+
24
+ // A field whose design says what it is: the label stays, out of sight.
25
+ formControl({ name: 'q', label: 'Search the docs', hideLabel: true, placeholder: 'Search…' })
26
+ formControl({ name: 'tint', subType: 'color', label: 'Highlight colour', hideLabel: true })
27
+ ```
28
+
29
+ A `placeholder` names only a field you type into, and only until something is typed. A swatch, a date, a select or a
30
+ checkbox always needs its `label`.
31
+
32
+ ## Pictures say what they show, or that they only decorate
33
+
34
+ ```ts
35
+ image({ src: '/team.jpg', alt: 'The team at the 2026 offsite' }) // what it shows, for this page
36
+ image({ src: '/grain.png', decorative: true }) // an ornament: left out of the tree
37
+ image({ decorative: true, bind: { src: 'post.record.cover' } }) // a cover under its own headline adds nothing
38
+ image({ bind: { src: 'row.item.cover', alt: 'row.item.title' } }) // …unless it is alone in a link: then it names it
39
+ fontAwesome({ icon: 'fa-solid fa-triangle-exclamation', label: 'Overdue' }) // an icon that says something alone
40
+ ```
41
+
42
+ ## Clicks go on controls
43
+
44
+ A `button` holds children, so a clickable card is a button with the card's class:
45
+
46
+ ```ts
47
+ button({
48
+ content: '',
49
+ class: planCard,
50
+ children: [text('Pro', { class: planName }), text('12 € a month', { class: planPrice })],
51
+ flows: [[onClick(), setState({ key: 'plan', type: 'text', value: 'pro' })]]
52
+ })
53
+ ```
54
+
55
+ Never put a click on a `container`, `text`, `image` or list item. A keyboard can't reach it, and a browser agent
56
+ won't find it among the page's controls. A button's children are its name, so keep headings, links and other
57
+ buttons out of it. If a card needs any of those, make its title a link and let the rest of the card be plain
58
+ content. An empty backdrop that closes a panel is fine, because the panel's close button and Escape are the ways
59
+ everyone has.
60
+
61
+ A filter chip, a tab you built yourself, a row that selects: all of them are buttons. Remember that a class starts
62
+ with the SDK button look (padding, radius), so reset what you don't want.
63
+
64
+ A dropdown opens from a button too: put one among its children, outside the popup. The dropdown marks it
65
+ (`aria-haspopup`, `aria-expanded`) and moves the focus into the menu and back out.
66
+
67
+ ```ts
68
+ dropdown({ children: [dropdownPopup({ children: [ /* menu entries: buttons or links */ ] }), button({ content: '', title: 'Account', children: [avatar] })] })
69
+ ```
70
+
71
+ ## State is said, not only styled
72
+
73
+ ```ts
74
+ button({ content: 'Grid', bind: { ariaPressed: 'computed.gridOn' }, class: chip }) // a toggle
75
+ button({ content: 'Menu', bind: { ariaExpanded: 'state.menuOpen' }, flows: [[onClick(), toggleState({ key: 'menuOpen' })]] })
76
+ ```
77
+
78
+ An "active" variant is for the eyes. `ariaPressed` and `ariaExpanded` are for everyone else. Use both.
79
+
80
+ ## The page has an outline
81
+
82
+ - Headings go down one level at a time: h1, then h2, then h3. The class sets the size, so an `h2` can look small.
83
+ - Landmarks are containers with a tag: `header`, `nav`, `main`, `aside`, `footer`. When a page has two of one kind,
84
+ give each a `label`: `container({ subType: 'nav', label: 'Docs sections' })`. A `section` with a `label` becomes a
85
+ region an assistant can jump to.
86
+
87
+ ## Illustrations
88
+
89
+ A mock of a page, a diagram, some art built from elements: `container({ decorative: true, children: [ … ] })`. The
90
+ readers skip it, and the linter asks nothing of what is inside, except that nothing in it can be tabbed to
91
+ (`control-in-decorative`). Whatever it lets a mouse do needs a real control somewhere else.
92
+
93
+ ## Built in, nothing to add
94
+
95
+ Modals and dialogs (named by their title; they take the focus, keep Tab inside and close on Escape), tabs (arrow
96
+ keys), a field's error (marked invalid and described by its message), the pager's current page, and the theme
97
+ toggle's pressed option.
98
+
99
+ ## A canvas or a plugin
100
+
101
+ Anything drawn on a canvas is one picture to these readers. Render what it shows as elements too: a list with a
102
+ named button per action. Keep that list visible when someone asks for it, because an agent clicks by position and
103
+ can't click something that only appears on focus. See `docs/en/accessibility.md` and Pizarra's List view.
104
+
105
+ ## Checking
106
+
107
+ - `authorSpace` warnings: zero, as always.
108
+ - In a test, `page.getByRole('button', { name: 'Close' })` fails when the name is missing.
109
+ `expect(locator).toMatchAriaSnapshot(…)` pins the whole outline.
110
+ - Through MCP, `plitzi_screenshot` with `view: "accessibility"` returns the page's tree and lists everything that has
111
+ no name.
@@ -0,0 +1,70 @@
1
+ # When `authorSpace` refuses
2
+
3
+ `authorSpace` checks the whole declaration before it writes anything, and stops at the first thing that would not
4
+ render as written. The message names the element (`Element "text" (price) at shop/home/0/2`) and what to write
5
+ instead. **Do what the message says.** Never cast past a check, silence it, or move the logic into a plugin to avoid
6
+ it — the check exists because that declaration renders something other than what it says.
7
+
8
+ The one exception is a TEST fixture whose subject is the break itself — how the runtime copes with a document no author
9
+ would write. It names that break, and only that one: `authorSpace(spec, { allow: [{ code, element, why }] })`. See
10
+ [testing](testing.md#spaces-written-for-a-test). A space anybody visits never has an `allow`.
11
+
12
+ ## Refused
13
+
14
+ | The message says | What was wrong | Write instead |
15
+ | --- | --- | --- |
16
+ | `has "x", which it does not take` | a field the space, a page, an element, a binding or a step does not have | the field it suggests; an element's attribute goes inside the factory's props (`button({ content })`) |
17
+ | `sets "x", which a "button" never reads` | an attribute the element does not have | one it lists; an event (`onClick`) is a flow: `flows: [[onClick(), …]]` |
18
+ | `` `subType` is "h7" … It is one of `` | a value outside the attribute's list | one of the listed values |
19
+ | `reads it as true or false` / `as a list` | text where a flag or a list is read | `disabled: true`, not `'true'`; `items: [ … ]` |
20
+ | `holds none` | children on a type that renders only its own attributes | a `container` (for a heading made of parts, `container({ subType: 'h1', children })`) |
21
+ | `the template … cannot be read as written` | a template with an operator, filter or function that does not exist, or broken syntax | see [templates](templates.md) for what exists; `matches` never does |
22
+ | `reads "x", which nothing here answers to` | a name in a template that is not a source, a variable or a route param | the full source name (`list_rows.item`), `navigation.queryParams.x` for a query param, `source` for the bound value |
23
+ | `is not around it` | a source read by an element that is not inside the element publishing it | move the element inside it, or share the value through `state` |
24
+ | `lands on "x", which a "text" never reads` | a binding onto an attribute the element does not have — the value arrives and nothing shows it | one it lists (`content`); to follow data with a class, bind `styleSelectors.base` in `initialState` |
25
+ | `no element answers to the name "x"` | a binding whose source names an element that does not exist | the id of the element that publishes it |
26
+ | `renders text — and "items" holds a list` | a text template feeding a list | `bindTemplate('items', src, '{{ … }}', { returns: 'value' })` |
27
+ | `which a "x" never fires` / `never answers to it` | a flow on an event, or a step to an action, the element does not have | the element that fires it (the message names it); for a plugin, a name its declaration has — `declaredTrigger` / `declaredCallback` make a wrong one a compile error |
28
+ | `does not start with its trigger` | a flow whose first step is not the event that runs it | `[onClick(), setState(…)]` |
29
+ | `step "x" has the param "y"` / `"type" is "string"` | a step param that does not exist or a value outside its options | the params and values it lists |
30
+ | `step "setState" sets "x" on "y", which a "z" never reads` | an element `setState`/`toggleState` writing a field the element does not have | an attribute it lists, or for `category: 'state'` `visibility` / `styleSelectors.<selector>` |
31
+ | `names the page "x", and no page has that id` | a link or `navigate` to a page id that does not exist | an existing page id, or a path with its slash (`'/about'`) |
32
+ | `a full URL, in page mode` | a URL, `mailto:` or `tel:` in a link left in page mode | `mode: 'external'` |
33
+ | `is a controlled list with no items` | a list with nothing to render | `items: [ … ]` or `bind: { items: 'provider.data.rows' }` |
34
+ | `` has items, but its `source` is "none" `` | a list whose items nothing reads — it renders its children once | `source: 'controlled'` |
35
+ | `answers at /x for the same visitors as page` | two pages at one address | another slug — or `accessLevel` `'public'` on one and `'authenticated'` on the other |
36
+ | `is not one CSS value` | an empty CSS value, or one with `;` or `{}` | one value per property; leave a property out instead of writing it empty |
37
+ | `computed … declared after it` / `does not compute` | a computed value read before it is declared, or never declared | declare it in `computed`, above the one that reads it |
38
+
39
+ ## Warned
40
+
41
+ A warning means the space renders, and renders something you probably did not mean. Fix every one.
42
+
43
+ | Code | Meaning | Fix |
44
+ | --- | --- | --- |
45
+ | `condition-starts-visible` | a computed visibility shows until its data answers | `visible: { source, template }` |
46
+ | `template-never-resolved` | a condition in an attribute, used as written | move it into `bindTemplate` |
47
+ | `tablet-rule-skips-mobile` | a tablet rule phones never get | write it under `compact` |
48
+ | `default-content-beside-children` | a button prints "Button" beside its children | `content: ''` |
49
+ | `overlay-starts-open` | a modal or dialog is open when the page loads | `visible: false`, opened by `openModal` |
50
+ | `provider-without-source` | an `apiContainer` asks nothing | give it a `query` (or `action`, `connector`, `resource`) |
51
+ | `unknown-element-type` | a type no built-in element has | the built-in it suggests; a plugin's DECLARATION goes in `authorSpace(space, { plugins: [declaration] })`, which also checks its events, actions and attributes |
52
+ | `colour-without-dark` | a colour token with no dark value | `{ light, dark, default }` |
53
+ | `FORM_SUBMIT_UNMANAGED` | a form the browser would submit itself | `managedByInteractions: true` |
54
+ | `STYLE_WITHOUT_TAG` | style on a provider that renders no element | `subType: 'div'` on the provider, or style its parent |
55
+ | `server-data-without-rsc` | a `runtime: 'server'` provider with a `connector` or `action` in a space that does not turn server data on — it renders its mock data | `rsc: { enabled: true }` on the space |
56
+ | `route-param-undeclared` | `navigation.routeParams.x` read on a page whose slug has no `:x` — always empty | add `:x` to the slug, or read `navigation.queryParams.x` |
57
+ | `form-control-unnamed` | a control in a form with no `name` — its value never reaches `values` | `formControl({ name: 'email', … })` |
58
+ | `form-control-name-taken` | two controls in one form with one name — one overwrites the other | a name each |
59
+ | `while-running` | `whileRunning` on a step that is not the trigger, or a value other than `skip`, `parallel`, `queue` | `[whileRunning('queue', onClick()), …]` |
60
+ | `trigger-keys` | an `onKey` flow whose `keys` cannot fire: two keys in one shortcut, only modifiers, or a name that is not a key | `onKey('f')`, `onKey('shift+f')`, `onKey('mod+k, escape')` |
61
+ | `state-toggled-in-branches` | two `setState` steps of one key, each under a `when` on that key — the second flips back what the first wrote | `toggleState({ key })`; for something shown by default, a key named for hiding it |
62
+ | `form-value-compared-to-blank` | a `when` asking whether a submitted field (`….values.x`) `=` or `!=` `""` — a field nobody typed in is not sent, so it never matches | `operator: 'empty'` / `'notEmpty'` |
63
+ | `overlay-never-opened` | a modal or dialog that starts hidden and that no step opens | a flow with `openModal('id')` / `openDialog('id')` |
64
+ | `control-without-name` | a button or link with no words (an icon, a picture without `alt`, nothing), or a field nothing names — a screen reader says just "button", a browser agent cannot find it | `title` on the button, `label` on the link, `label` (with `hideLabel: true`) on the field — see [accessibility](accessibility.md) |
65
+ | `image-without-alt` | an image that is not `decorative` and has no `alt` | say what it shows, or `decorative: true` |
66
+ | `click-on-static-element` | a click flow on a container, text, heading, image or list item — no keyboard reaches it, no agent finds it | the flow on a `button` (it holds children) or a `link` |
67
+ | `heading-level-skipped` | an `h4` right after an `h2` on a page — the outline misses a level | the next level down; size it with its class |
68
+ | `label-ignored` | a `label` on a container whose tag is named by what it holds (`li`, a heading) | the words inside, or a landmark tag (`nav`, `section`, …) |
69
+ | `dropdown-without-control` | a dropdown opened from a box or an icon — no keyboard opens it | a `button` as what opens it (`title` if it is only an icon) |
70
+ | `control-in-decorative` | a control inside a `decorative` container — Tab lands on something nothing announces | move it out of the illustration |
@@ -3,11 +3,16 @@
3
3
  ## Bindings
4
4
 
5
5
  ```ts
6
- heading({ bind: { content: 'posts.title' } }) // short form: attributes
7
- text({ bind: [{ to: 'content', source: 'stats.data.total', // full form: anything else
8
- transformers: [{ action: 'twigTemplate', params: { template: "{{ source|number_format(0, '.', ',') }} views" } }] }] })
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
9
10
  ```
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
+
11
16
  **A source names the id you gave the element** — `'posts.title'`, `'postList.item.cover'` — and the prefix is
12
17
  completed from what the element publishes (`apiContainer_posts`, `list_postList`; a `form` publishes under
13
18
  `apiContainer`). A name nothing answers to, or a prefix that does not match, is refused.
@@ -17,8 +22,29 @@ The binding's `source` is completed for you; its template is read as written, so
17
22
 
18
23
  An element re-renders when anything it reads changes: the first part of each binding's `source` and every source its
19
24
  templates name. The globals are `variables`, `navigation` (`routeParams`, `queryParams`, `origin`, `currentPageId`),
20
- `auth` (who is signed in, `status` while that is being found out), `state` (what flows wrote) and `theme`
21
- (`mode`, `resolved`).
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.
22
48
 
23
49
  ## Providers
24
50
 
@@ -29,10 +55,21 @@ apiContainer({ id: 'board', runtime: 'server', action: 'queue-board' })
29
55
 
30
56
  - A provider's source is readable by its **descendants** only. Wrap what reads it; a sibling cannot see it.
31
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.
32
61
  - A query that depends on state is a binding on `query`; it answers `''` (and fetches nothing) until the state exists:
33
62
  `"{{ source ? apiUrl ~ '/workspaces/' ~ source ~ '/stats' : '' }}"` with `source: 'state.workspace.id'`.
34
63
  - A space with any `runtime: 'server'` provider needs `rsc: { enabled: true }`.
35
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
+
36
73
  ### Live, cached, refreshed
37
74
 
38
75
  - **Live:** `refreshSeconds: 15` asks again on its own (either runtime); it pauses in a hidden tab and never
@@ -62,28 +99,28 @@ modalContainer({ visible: false, … }) // starts hidden; a flow opens
62
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 |
63
100
 
64
101
  `visible` starts the element **hidden**, and it appears when its data says so. A revealing condition you have to
65
- compute is `visible: false` — so it waits hidden — plus a visibility binding with a template:
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:
66
105
 
67
106
  ```ts
68
107
  container({
69
108
  id: 'first-steps',
70
- visible: false,
71
- bind: [{
72
- to: 'visibility',
109
+ visible: {
73
110
  source: 'stats.data.totals',
74
- category: 'initialState',
75
- transformers: [{ action: 'twigTemplate', params: {
76
- template: "{{ source ? (source.spaces > 0 and source.published > 0 ? 'false' : 'true') : 'false' }}"
77
- } }]
78
- }]
111
+ template: '{{ source and not (source.spaces > 0 and source.published > 0) }}'
112
+ }
79
113
  })
80
114
  ```
81
115
 
82
116
  Rules for a condition's template:
83
117
 
84
- - **A revealing condition starts hidden with `visible: false`.** A visibility binding alone keeps the element on
85
- screen until it answers — right for a hiding flag, a flash for a condition on data. `authorSpace` warns
86
- `condition-starts-visible` when a computed one on a provider starts on screen.
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.
87
124
  - **`'true'` shows, `'false'` hides, `''` writes nothing** and leaves the element as it is. Visibility hides only on
88
125
  an explicit false.
89
126
  - **Answer what "not known yet" should mean.** For a revealing condition that is `'false'` (`source ? … : 'false'`);
@@ -99,7 +136,7 @@ Four states, and each has its own element:
99
136
  | State | How to tell |
100
137
  | --- | --- |
101
138
  | Loading | `apiContainer_x.isLoading` — or simply nothing: every condition is hidden until data arrives |
102
- | Empty | the answer ARRIVED and is empty: `{{ source is defined and source is empty ? 'true' : 'false' }}` over the list |
139
+ | Empty | the answer ARRIVED and is empty: `{{ source is defined and source is empty }}` over the list |
103
140
  | Error | `apiContainer_x.hasError` |
104
141
  | Data | the list itself |
105
142
 
@@ -112,3 +149,32 @@ shows on every load. Test for "arrived and empty" (`is defined and … is empty`
112
149
  Both sides of one question are `visible: 'x'` and `visible: '!x'`, not an `x` and a `notX` in the server's answer.
113
150
  `!` reads a boolean that travelled as text (`"false"`, `"0"`) correctly. A three-state condition
114
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).
@@ -17,13 +17,14 @@ container([hero, grid]) // an array is the children
17
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
18
  | `class` | a shared class: a `styles()` declaration or a name from the space's `classes`; a list for several. Exclusive with `css` |
19
19
  | `css` | rules of this element's own: one set, or one per breakpoint (`{ desktop, tablet, mobile }`) |
20
- | `states` | `hover`, `focus-visible`… beside the element's own `css` |
20
+ | `states` | `hover`, `focus-visible`, `hidden`… beside the element's own `css` |
21
21
  | `variant` | a variant of the class it wears (when the class declares it) or of its type |
22
22
  | `slots` | a class for one of the element's OTHER selectors — a form control's `input` |
23
23
  | `bind` | where a value comes from |
24
- | `visible` | a condition (`'src'`, `'!src'`), or `false` to start hidden |
24
+ | `visible` | a condition (`'src'`, `'!src'`, `{ source, template }`), or `false` to start hidden |
25
25
  | `flows` | what happens on click, on submit, on load |
26
26
  | `runtime` | `'server'` resolves this provider's data on the server |
27
+ | `loadStrategy` | when the contents mount relative to visibility: `eager` (default), `lazy`, `visible` |
27
28
  | `children` | the tree |
28
29
  | `meta` | `meta.label` is the element's name in the builder's tree |
29
30
 
@@ -31,8 +32,21 @@ Anything else in the object is an **attribute** (`authorSpace` warns `unknown-at
31
32
  reads).
32
33
 
33
34
  **A link's `href` is a page id.** `link({ href: 'reports' })` finds the page by id, folder included. `mode: 'internal'`
34
- for a path within the space carrying `{{tokens}}`; `mode: 'external'` for a full URL passed through untouched. A link
35
- is an `<a>`: never put a link inside another link — make the card the link, or the button, not both.
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.
36
50
 
37
51
  **A `list` is a `<ul>`.** The browser indents it and puts bullets on it; a list of cards needs
38
52
  `{ margin: '0px', padding: '0px', 'list-style-type': 'none' }` in its class.
@@ -42,7 +56,16 @@ children sit in the parent's layout and a `class` on it styles nothing (`STYLE_W
42
56
  parent, or give it `attributes: { subType: 'div' }`.
43
57
 
44
58
  A type this SDK does not ship — a plugin — is authored the same way: `defineElement<Props>({ type, … })`,
45
- `element<Props>('type', props)`, or `elementsFromManifest(manifest)`.
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
+ ```
46
69
 
47
70
  ## CSS
48
71
 
@@ -51,8 +74,11 @@ outside the vocabulary is refused with the key it should have been (`paddingTop`
51
74
 
52
75
  - `column(gap, extra?)`, `row(gap, extra?)`, `grid(columns, gap, extra?)` for the three layouts every space repeats.
53
76
  - **Breakpoints are ranges, not a cascade.** `tablet` is 48–64rem, `mobile` below 48rem, and each inherits only from
54
- `desktop` — a rule for `tablet` never reaches a phone. Write it in both; `authorSpace` warns
55
- `tablet-rule-skips-mobile`.
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.
56
82
  - **Fonts are declared.** A `font-family` loads only if the space lists the face in `fonts`
57
83
  (`{ source: 'google', family: 'Geist', fallback: 'system-ui, sans-serif', weights: [400, 600], styles: ['normal'] }`).
58
84
 
@@ -77,9 +103,37 @@ container({ class: card, children: [ … ] });
77
103
  import it.
78
104
  - `ancestors: { [sidebar.name]: { variants: { collapsed: { display: 'none' } } } }` styles an element by the state or
79
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.
80
128
  - **`:hover` is part of the selector, not a rule in `customCss`.** `customCss` is for what no class can say:
81
129
  keyframes, pseudo-elements, a rule across two unrelated elements, the inside of a third-party widget.
82
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.
83
137
 
84
138
  **A variant chosen by the data:** `variantFrom(pill, 'jobs.item.status')`, or with a template when the data does not
85
139
  already speak in variant names — `variantFrom(pill, 'runs.item.status', { template: "{{ source == 'completed' ?