@plitzi/sdk-server 0.37.8 → 0.37.10

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 (127) hide show
  1. package/CHANGELOG.md +825 -0
  2. package/README.md +24 -2
  3. package/dist/actions.d.ts +1 -1
  4. package/dist/adapters/cloudAdapters.d.ts +5 -3
  5. package/dist/adapters/cloudAdapters.js +123 -63
  6. package/dist/adapters/space/types.d.ts +3 -0
  7. package/dist/core/configSeam.d.ts +14 -0
  8. package/dist/core/configSeam.js +114 -0
  9. package/dist/core/configSeam.test.d.ts +1 -0
  10. package/dist/core/createServer.js +11 -5
  11. package/dist/core/createServer.publicData.test.d.ts +1 -0
  12. package/dist/core/freePort.d.ts +10 -0
  13. package/dist/core/freePort.js +36 -0
  14. package/dist/core/freePort.test.d.ts +1 -0
  15. package/dist/core/http/stages/images.d.ts +6 -0
  16. package/dist/core/http/stages/images.js +47 -0
  17. package/dist/core/http/stages/images.test.d.ts +1 -0
  18. package/dist/core/server/fleet/channel.js +1 -1
  19. package/dist/core/server/fleet/relay.js +1 -1
  20. package/dist/core/services/action.js +5 -4
  21. package/dist/core/services/functionRoutes.d.ts +2 -2
  22. package/dist/core/services/functionRoutes.js +2 -2
  23. package/dist/core/services/registry.js +2 -0
  24. package/dist/functions-api.d.ts +23 -2
  25. package/dist/functions.d.ts +1 -1
  26. package/dist/functions.js +2 -2
  27. package/dist/helpers/cache/keys.d.ts +23 -10
  28. package/dist/helpers/cache/keys.js +16 -2
  29. package/dist/helpers/flagOverrides.d.ts +27 -0
  30. package/dist/helpers/flagOverrides.js +25 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +2 -1
  33. package/dist/modules/actions/index.d.ts +1 -1
  34. package/dist/modules/actions/index.js +3 -2
  35. package/dist/modules/actions/moduleFor.js +5 -3
  36. package/dist/modules/actions/runtime/flags.d.ts +16 -0
  37. package/dist/modules/actions/runtime/flags.js +49 -0
  38. package/dist/modules/actions/runtime/flags.test.d.ts +1 -0
  39. package/dist/modules/actions/runtime/kvList.js +1 -1
  40. package/dist/modules/actions/runtime/renderResolver.js +30 -4
  41. package/dist/modules/actions/runtime/runAction.js +2 -0
  42. package/dist/modules/actions/taskCatalog.d.ts +3 -1
  43. package/dist/modules/actions/taskCatalog.js +1 -0
  44. package/dist/modules/actions/tasks/registry.js +4 -2
  45. package/dist/modules/actions/transport/callHandler.js +8 -0
  46. package/dist/modules/actions/types.d.ts +20 -1
  47. package/dist/modules/connectors/engine.js +1 -1
  48. package/dist/modules/functions/capabilities.js +1 -1
  49. package/dist/modules/functions/config.d.ts +19 -3
  50. package/dist/modules/functions/config.js +30 -14
  51. package/dist/modules/functions/contract.d.ts +10 -3
  52. package/dist/modules/functions/driver.js +4 -2
  53. package/dist/modules/functions/load.js +2 -2
  54. package/dist/modules/functions/manifest.d.ts +5 -3
  55. package/dist/modules/functions/manifest.js +37 -6
  56. package/dist/modules/functions/routes.d.ts +1 -1
  57. package/dist/modules/functions/routes.js +3 -3
  58. package/dist/modules/functions/runner/messages.js +1 -1
  59. package/dist/modules/functions/space.d.ts +7 -5
  60. package/dist/modules/functions/space.js +17 -10
  61. package/dist/modules/images/allowlist.d.ts +5 -0
  62. package/dist/modules/images/allowlist.js +18 -0
  63. package/dist/modules/images/imageProxy.d.ts +36 -0
  64. package/dist/modules/images/imageProxy.js +273 -0
  65. package/dist/modules/images/imageProxy.test.d.ts +1 -0
  66. package/dist/modules/images/transform.d.ts +10 -0
  67. package/dist/modules/images/transform.js +28 -0
  68. package/dist/modules/images/transform.test.d.ts +1 -0
  69. package/dist/modules/rsc/connectorRscData.d.ts +12 -7
  70. package/dist/modules/rsc/connectorRscData.js +12 -8
  71. package/dist/modules/rsc/handler.js +18 -4
  72. package/dist/modules/rsc/publicFileResolver.d.ts +13 -0
  73. package/dist/modules/rsc/publicFileResolver.js +39 -0
  74. package/dist/modules/rsc/publicFileResolver.test.d.ts +1 -0
  75. package/dist/modules/rsc/resolveRscData.d.ts +4 -2
  76. package/dist/modules/rsc/resolveRscData.js +10 -2
  77. package/dist/modules/runtime/bundle.js +2 -2
  78. package/dist/modules/runtime/contract.d.ts +1 -1
  79. package/dist/modules/runtime/stages.d.ts +1 -1
  80. package/dist/modules/runtime/stages.js +1 -1
  81. package/dist/modules/ssr/Component.d.ts +5 -1
  82. package/dist/modules/ssr/Component.js +3 -1
  83. package/dist/modules/ssr/prepareRender.js +38 -10
  84. package/dist/modules/ssr/registerExternalPlugins.d.ts +5 -1
  85. package/dist/modules/ssr/registerExternalPlugins.js +6 -2
  86. package/package.json +9 -4
  87. package/skills/plitzi-authoring/CHEATSHEET.md +135 -0
  88. package/skills/plitzi-authoring/SKILL.md +77 -74
  89. package/skills/plitzi-authoring/recipes/carousel.ts +114 -0
  90. package/skills/plitzi-authoring/recipes/detail-page.ts +70 -0
  91. package/skills/plitzi-authoring/recipes/embed-and-svg.ts +48 -0
  92. package/skills/plitzi-authoring/recipes/every-few-seconds.ts +119 -0
  93. package/skills/plitzi-authoring/recipes/feature-flag.ts +24 -0
  94. package/skills/plitzi-authoring/recipes/filter-a-list.ts +63 -0
  95. package/skills/plitzi-authoring/recipes/forms-and-modals.ts +64 -0
  96. package/skills/plitzi-authoring/recipes/from-tailwind.ts +52 -0
  97. package/skills/plitzi-authoring/recipes/link-to-a-section.ts +35 -0
  98. package/skills/plitzi-authoring/recipes/link-with-data.ts +40 -0
  99. package/skills/plitzi-authoring/recipes/marquee.ts +57 -0
  100. package/skills/plitzi-authoring/recipes/plugin.ts +53 -0
  101. package/skills/plitzi-authoring/recipes/scroll-a-row.ts +79 -0
  102. package/skills/plitzi-authoring/recipes/show-data.ts +73 -0
  103. package/skills/plitzi-authoring/recipes/style-a-page.ts +45 -0
  104. package/skills/plitzi-authoring/recipes/typed-data.ts +64 -0
  105. package/skills/plitzi-authoring/recipes/usable-without-sight.ts +39 -0
  106. package/skills/plitzi-authoring/reference/accessibility.md +2 -2
  107. package/skills/plitzi-authoring/reference/authoring-errors.md +195 -53
  108. package/skills/plitzi-authoring/reference/colours-and-motion.md +63 -0
  109. package/skills/plitzi-authoring/reference/components.md +83 -0
  110. package/skills/plitzi-authoring/reference/data-and-visibility.md +29 -29
  111. package/skills/plitzi-authoring/reference/efficiency.md +53 -0
  112. package/skills/plitzi-authoring/reference/elements-and-styles.md +39 -24
  113. package/skills/plitzi-authoring/reference/feature-flags.md +78 -0
  114. package/skills/plitzi-authoring/reference/flows.md +26 -37
  115. package/skills/plitzi-authoring/reference/kept-state.md +28 -0
  116. package/skills/plitzi-authoring/reference/layouts.md +20 -3
  117. package/skills/plitzi-authoring/reference/lists.md +45 -16
  118. package/skills/plitzi-authoring/reference/plugins.md +27 -0
  119. package/skills/plitzi-authoring/reference/realtime.md +35 -0
  120. package/skills/plitzi-authoring/reference/review-checklist.md +8 -3
  121. package/skills/plitzi-authoring/reference/{templates-and-export.md → snippets-and-export.md} +15 -8
  122. package/skills/plitzi-authoring/reference/structure.md +29 -20
  123. package/skills/plitzi-authoring/reference/tailwind.md +19 -0
  124. package/skills/plitzi-authoring/reference/templates.md +6 -5
  125. package/skills/plitzi-authoring/reference/testing.md +11 -0
  126. package/skills/plitzi-authoring/reference/typed-sources.md +20 -0
  127. package/skills/plitzi-authoring/reference/validation.md +6 -2
@@ -55,7 +55,7 @@ var NOT_ANSWERED = /* @__PURE__ */ new Set([...HOP_BY_HOP, "set-cookie"]);
55
55
  * The page server forwarding a space's endpoints to its runtime: the request as it came, streamed both ways — an agent's
56
56
  * event stream included — without the visitor's cookies or `Authorization`, and with who asked in `X-Forwarded-*`.
57
57
  *
58
- * A data stage: after the auth chain, so the space is known, and before the space's `/api/` routes and its pages.
58
+ * A data stage: after the auth chain, so the space is known, and before the space's `/fn/` routes and its pages.
59
59
  */
60
60
  var createRuntimeProxyStage = ({ lookup, onForward }) => async (ctx) => {
61
61
  const { spaceId, environment = "main" } = ctx.req.ctx.spaceDeployment ?? {};
@@ -15,6 +15,10 @@ export type ComponentProps = {
15
15
  theme?: Theme;
16
16
  /** The kept state the first paint depends on (`settings.paintedState`), from the visitor's cookie. */
17
17
  state?: Record<string, unknown>;
18
+ /** The flags this deployment decides (`config.flags`). */
19
+ serverFlags?: Record<string, boolean>;
20
+ /** The flags a tester forced, for a page allowed to debug. See `prepareRender`. */
21
+ forcedFlags?: Record<string, boolean>;
18
22
  };
19
- declare const Component: ({ server, renderMode, previewMode, offlineData, environment, plugins, ssrResult, sdkDevToolsStylePath, debugMode, overQuota, theme, state }: ComponentProps) => import('react').JSX.Element;
23
+ declare const Component: ({ server, renderMode, previewMode, offlineData, environment, plugins, ssrResult, sdkDevToolsStylePath, debugMode, overQuota, theme, state, serverFlags, forcedFlags }: ComponentProps) => import('react').JSX.Element;
20
24
  export default Component;
@@ -1,7 +1,7 @@
1
1
  import PlitziSdk from "@plitzi/plitzi-sdk";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  //#region src/modules/ssr/Component.tsx
4
- var Component = ({ server, renderMode = "raw", previewMode = true, offlineData, environment = "main", plugins, ssrResult, sdkDevToolsStylePath, debugMode = false, overQuota, theme, state }) => {
4
+ var Component = ({ server, renderMode = "raw", previewMode = true, offlineData, environment = "main", plugins, ssrResult, sdkDevToolsStylePath, debugMode = false, overQuota, theme, state, serverFlags, forcedFlags }) => {
5
5
  const serverWithResult = {
6
6
  ...server,
7
7
  ssr: {
@@ -21,6 +21,8 @@ var Component = ({ server, renderMode = "raw", previewMode = true, offlineData,
21
21
  ...overQuota === void 0 ? {} : { overQuota },
22
22
  ...theme === void 0 ? {} : { theme },
23
23
  ...state === void 0 ? {} : { state },
24
+ ...serverFlags === void 0 ? {} : { serverFlags },
25
+ ...forcedFlags === void 0 ? {} : { forcedFlags },
24
26
  children: plugins && Object.keys(plugins).map((key) => /* @__PURE__ */ jsx(PlitziSdk.Plugin, {
25
27
  renderType: key,
26
28
  component: plugins[key].component,
@@ -1,3 +1,4 @@
1
+ import { requestFlagOverrides } from "../../helpers/flagOverrides.js";
1
2
  import { authorizesDebugging } from "../../helpers/debugAuthorization.js";
2
3
  import { realtimeModuleFor } from "../realtime/index.js";
3
4
  import { matchRscPage } from "../rsc/matchRscPage.js";
@@ -11,10 +12,12 @@ import { createOfflineDataLoader } from "../../helpers/offlineDataLoader.js";
11
12
  import { registerExternalPlugins } from "./registerExternalPlugins.js";
12
13
  import { reportMissingPlugins } from "./reportMissingPlugins.js";
13
14
  import { resolvePageSeo } from "./resolvePageSeo.js";
15
+ import { imagesPathOf } from "../../core/http/stages/images.js";
14
16
  import { sdkAssetVersion } from "../../core/sdkAssets.js";
15
17
  import { buildServerInfo } from "../../helpers/buildServerInfo.js";
16
18
  import { hydrationPayload } from "../../helpers/hydrationPayload.js";
17
19
  import { resolveDebugMode } from "../../helpers/resolveDebugMode.js";
20
+ import { flagUserFromSSR, flagValues, resolveFlags } from "@plitzi/sdk-shared/flags";
18
21
  import { hasServerElements } from "@plitzi/sdk-shared/schema/serverElements";
19
22
  import { debugCookieName } from "@plitzi/sdk-shared/devTools";
20
23
  import { themeFromCookies } from "@plitzi/sdk-shared/theme";
@@ -35,7 +38,7 @@ var declaredTheme = (style) => {
35
38
  };
36
39
  var prepareRender = async (req, config, spaceId, environment, revision, pluginManager, offlineDataCache, metrics, offlineDataOverride) => {
37
40
  const m = (name, fn) => metrics ? metrics.measure(name, fn) : Promise.resolve(fn());
38
- const offlineCacheKey = environment !== "main" ? buildOfflineDataCacheKey(spaceId, environment, revision) : void 0;
41
+ const offlineCacheKey = environment !== "main" ? buildOfflineDataCacheKey(spaceId, environment, revision, req.ctx.spaceDeployment?.flagsVersion) : void 0;
39
42
  const cachedOfflineStr = offlineDataOverride === void 0 && offlineCacheKey ? offlineDataCache?.get(offlineCacheKey) : void 0;
40
43
  const loadOfflineData = createOfflineDataLoader(() => {
41
44
  if (offlineDataOverride !== void 0) return Promise.resolve(offlineDataOverride);
@@ -46,13 +49,27 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
46
49
  const rscPath = resolveRscEndpoint(config);
47
50
  const schema = offlineData?.schema;
48
51
  const pageMatch = schema !== void 0 ? matchRscPage(schema, req.path, req.ctx.user) : void 0;
49
- const rscData = rscPath !== void 0 && schema !== void 0 && schema.rsc?.enabled !== false && pageMatch !== void 0 && hasServerElements(schema, pageMatch.pageId) && config.adapters.getRscData ? await m("rsc", () => config.adapters.getRscData?.({
52
+ const rscEnabled = rscPath !== void 0 && schema !== void 0 && schema.rsc?.enabled !== false;
53
+ const debugAuthorized = !Boolean(req.query["__pt"]) && authorizesDebugging(config, schema?.settings);
54
+ const flagOverrides = await requestFlagOverrides(config, req, {
55
+ spaceId: req.ctx.spaceDeployment?.spaceId ?? spaceId,
56
+ environment: req.ctx.spaceDeployment?.environment ?? environment
57
+ }, debugAuthorized);
58
+ const pageFlags = schema !== void 0 && pageMatch !== void 0 ? flagValues(resolveFlags(schema.flags, {
59
+ environment: req.ctx.spaceDeployment?.environment ?? environment,
60
+ hostname: req.hostname,
61
+ routeParams: pageMatch.routeParams,
62
+ queryParams: req.query,
63
+ user: flagUserFromSSR(req.ctx.user)
64
+ }, flagOverrides)) : void 0;
65
+ const rscData = rscEnabled && pageMatch !== void 0 && hasServerElements(schema, pageMatch.pageId, pageFlags) && config.adapters.getRscData ? await m("rsc", () => config.adapters.getRscData?.({
50
66
  req,
51
67
  spaceId: req.ctx.spaceDeployment?.spaceId ?? spaceId,
52
68
  environment: req.ctx.spaceDeployment?.environment ?? environment,
53
69
  revision: req.ctx.spaceDeployment?.revision ?? revision,
54
70
  user: req.ctx.user,
55
- loadOfflineData
71
+ loadOfflineData,
72
+ flagOverrides
56
73
  })) : rscPath ? { serverData: {} } : void 0;
57
74
  const pageSeo = resolvePageSeo(schema, pageMatch?.pageId);
58
75
  const server = buildServerInfo(req, config, {
@@ -60,7 +77,8 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
60
77
  rscData,
61
78
  actionPath: resolveActionEndpoint(config),
62
79
  realtimePath: realtimeModuleFor(config)?.path,
63
- realtimeTransport: realtimeModuleFor(config)?.transport
80
+ realtimeTransport: realtimeModuleFor(config)?.transport,
81
+ imagePath: imagesPathOf(config)
64
82
  });
65
83
  if (offlineDataOverride === void 0 && !cachedOfflineStr && offlineCacheKey && offlineData !== void 0) offlineDataCache?.set(offlineCacheKey, JSON.stringify(offlineData));
66
84
  const version = config.assetVersion ?? sdkAssetVersion();
@@ -81,7 +99,6 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
81
99
  * so does the space itself (`settings.debugMode`) — an owner inspecting their own published site. The space is read
82
100
  * from what this server loaded, never from the request, so a visitor has no say in it.
83
101
  */
84
- const debugAuthorized = !Boolean(req.query["__pt"]) && authorizesDebugging(config, schema?.settings);
85
102
  const debugRendered = resolveDebugMode(debugAuthorized, readCookie(req.headers.cookie, debugCookieName(req.headers.host)));
86
103
  /**
87
104
  * The runs this render's server elements started, handed to a page that may debug them — and to no other.
@@ -123,6 +140,13 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
123
140
  * would hydrate other markup. Only the keys the space declares, and only with `keepState` on.
124
141
  */
125
142
  const paintedState = schema?.settings.keepState ? paintedStateFor(req.headers.cookie, ssrPaintedCookieName(req.headers.host), paintedKeys(schema.settings)) : void 0;
143
+ /**
144
+ * The two layers of the space's flags this server answers for. Its own (`config.flags`), asked per space when it
145
+ * serves several; and the ones a tester forced from the dev tools, read from their cookie — only for a page allowed
146
+ * to debug, or any visitor could switch on a feature still behind a flag. Both travel to the browser beside the
147
+ * page, so it hydrates with the flags it was drawn with.
148
+ */
149
+ const { server: serverFlags, qa: forcedFlags } = flagOverrides;
126
150
  const offlineDataStr = hydrationPayload(offlineData, {
127
151
  offlineMode: true,
128
152
  environment,
@@ -133,7 +157,9 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
133
157
  ...paintedState ? { state: paintedState } : {},
134
158
  ...clientAnalytics ? { analytics: clientAnalytics } : {},
135
159
  ...overQuota ? { overQuota } : {},
136
- ...actionRuns ? { actionRuns } : {}
160
+ ...actionRuns ? { actionRuns } : {},
161
+ ...serverFlags ? { serverFlags } : {},
162
+ ...forcedFlags ? { forcedFlags } : {}
137
163
  });
138
164
  const pluginNames = req.ctx.spaceDeployment?.pluginNames ?? [];
139
165
  const pluginSources = req.ctx.spaceDeployment?.pluginSources;
@@ -143,11 +169,11 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
143
169
  const key = pluginManager.ensure(pluginName, pluginSource);
144
170
  if (!pluginBaseNames.has(pluginName)) dynamicNames.push(key);
145
171
  }
146
- const externalNamesFiltered = (config.autoLoadSchemaPlugins !== false ? await m("extPlugins", () => registerExternalPlugins(pluginManager, offlineData, { allowPrivateHosts: config.allowPrivatePluginHosts === true })) : []).filter((k) => !pluginBaseNames.has(k.replace(/@[^@]*$/, "")));
172
+ const externalNames = config.autoLoadSchemaPlugins !== false ? await m("extPlugins", () => registerExternalPlugins(pluginManager, offlineData, { allowPrivateHosts: config.allowPrivatePluginHosts === true }, pluginBaseNames)) : [];
147
173
  const allPluginNames = [
148
174
  ...pluginNames,
149
175
  ...dynamicNames,
150
- ...externalNamesFiltered
176
+ ...externalNames
151
177
  ];
152
178
  const entries = allPluginNames.length > 0 ? await pluginManager.getEntries(allPluginNames) : [];
153
179
  const pluginComponents = await m("plugins", () => loadPluginComponents(entries, pluginManager.getComponents()));
@@ -167,7 +193,9 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
167
193
  sdkDevToolsStylePath,
168
194
  overQuota,
169
195
  theme,
170
- ...paintedState ? { state: paintedState } : {}
196
+ ...paintedState ? { state: paintedState } : {},
197
+ ...serverFlags ? { serverFlags } : {},
198
+ ...forcedFlags ? { forcedFlags } : {}
171
199
  },
172
200
  entries,
173
201
  templateParams: {
@@ -202,7 +230,7 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
202
230
  ssrOnly: config.ssrOnly === true,
203
231
  offlineData: offlineDataStr
204
232
  },
205
- cacheable: actionRuns === void 0
233
+ cacheable: actionRuns === void 0 && forcedFlags === void 0
206
234
  };
207
235
  };
208
236
  //#endregion
@@ -7,6 +7,10 @@ type ManifestReach = {
7
7
  /**
8
8
  * Reads plugins listed in offlineData.plugins, fetches their manifests, downloads and caches
9
9
  * JS/CSS via the PluginManager, and returns the effective plugin keys to pass to getEntries().
10
+ *
11
+ * `registered`: the types this deployment registers itself — a project's own build of a plugin the space also lists
12
+ * on its CDN. Those are never looked for elsewhere: the deployment's copy is the one rendered, and asking the CDN for a
13
+ * manifest nobody will use is a request, and a warning, for nothing.
10
14
  */
11
- export declare const registerExternalPlugins: (pluginManager: PluginManager, offlineData: OfflineDataRaw | undefined, reach?: ManifestReach) => Promise<string[]>;
15
+ export declare const registerExternalPlugins: (pluginManager: PluginManager, offlineData: OfflineDataRaw | undefined, reach?: ManifestReach, registered?: ReadonlySet<string>) => Promise<string[]>;
12
16
  export {};
@@ -97,9 +97,13 @@ var registerPlugin = async (pluginManager, plugin, reach) => {
97
97
  /**
98
98
  * Reads plugins listed in offlineData.plugins, fetches their manifests, downloads and caches
99
99
  * JS/CSS via the PluginManager, and returns the effective plugin keys to pass to getEntries().
100
+ *
101
+ * `registered`: the types this deployment registers itself — a project's own build of a plugin the space also lists
102
+ * on its CDN. Those are never looked for elsewhere: the deployment's copy is the one rendered, and asking the CDN for a
103
+ * manifest nobody will use is a request, and a warning, for nothing.
100
104
  */
101
- var registerExternalPlugins = async (pluginManager, offlineData, reach = { allowPrivateHosts: false }) => {
102
- const plugins = offlineData?.plugins;
105
+ var registerExternalPlugins = async (pluginManager, offlineData, reach = { allowPrivateHosts: false }, registered = /* @__PURE__ */ new Set()) => {
106
+ const plugins = offlineData?.plugins?.filter((plugin) => !registered.has(plugin.type));
103
107
  if (!plugins || plugins.length === 0) return [];
104
108
  return (await Promise.all(plugins.map((p) => registerPlugin(pluginManager, p, reach)))).filter((k) => k !== null);
105
109
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plitzi/sdk-server",
3
- "version": "0.37.8",
3
+ "version": "0.37.10",
4
4
  "license": "AGPL-3.0",
5
5
  "files": [
6
6
  "dist",
@@ -79,8 +79,8 @@
79
79
  "type": "module",
80
80
  "sideEffects": false,
81
81
  "dependencies": {
82
- "@plitzi/plitzi-sdk": "0.37.8",
83
- "@plitzi/sdk-shared": "0.37.8",
82
+ "@plitzi/plitzi-sdk": "0.37.10",
83
+ "@plitzi/sdk-shared": "0.37.10",
84
84
  "ejs": "^6.0.1",
85
85
  "esbuild": "^0.28.2",
86
86
  "jsonwebtoken": "^9.0.3",
@@ -94,7 +94,8 @@
94
94
  "mongodb": "^6 || ^7",
95
95
  "mysql2": "^3",
96
96
  "react": "^19",
97
- "react-dom": "^19"
97
+ "react-dom": "^19",
98
+ "sharp": "^0.34.0"
98
99
  },
99
100
  "peerDependenciesMeta": {
100
101
  "core-js": {
@@ -108,6 +109,9 @@
108
109
  },
109
110
  "mysql2": {
110
111
  "optional": true
112
+ },
113
+ "sharp": {
114
+ "optional": true
111
115
  }
112
116
  },
113
117
  "devDependencies": {
@@ -129,6 +133,7 @@
129
133
  "mysql2": "^3.24.4",
130
134
  "react": "^19.3.0",
131
135
  "react-dom": "^19.3.0",
136
+ "sharp": "^0.34.5",
132
137
  "tsx": "^4.23.15",
133
138
  "typescript": "^6.0.3",
134
139
  "vite": "^8.3.1",
@@ -0,0 +1,135 @@
1
+ # Plitzi authoring — cheatsheet
2
+
3
+ What most changes need, on one page. Everything is imported from `@plitzi/sdk-authoring`. For the rest, ask by name —
4
+ `npx @plitzi/cli explain navigate` (an element, a step, a trigger, a problem's code), `--list steps` for every one of a
5
+ kind — rather than reading its `.d.ts`.
6
+
7
+ ## The space
8
+
9
+ ```ts
10
+ const space: SpaceSpec = {
11
+ name, permanentUrl, // permanentUrl: lowercase, digits, `-`
12
+ variables: { color: { card: { light, dark, default } } }, // every colour a token, both themes; tokens(variables).card
13
+ classes: { card }, // styles() declarations, by their own name; styles('card', tw('p-4 rounded-xl'))
14
+ computed: { total: '{{ … }}' }, // values read anywhere as computed.total
15
+ flags: { beta: { description, value: false, rules: [] } },
16
+ customCss: '@keyframes …', // keyframes and what no property says
17
+ layouts: [{ id: 'shell', body }], // chrome shared by pages
18
+ components: [{ id, props, slots, root }], // a block placed many times with different content
19
+ pages: [{ id, name, slug: '', layout: { id: 'shell', slot: 'main' }, body: [] }] // slot: where the body goes
20
+ }; // slug '' is home; 'products/:slug' takes a param
21
+ const { schema, style, warnings } = authorSpace(space, { plugins: [declaration] });
22
+ ```
23
+
24
+ ## Elements
25
+
26
+ Every factory takes the element's attributes and these fields: `id`, `class`, `css`, `states`, `slots`, `bind`,
27
+ `visible`, `flows`, `anchor`, `flag`, `runtime`, `loadStrategy`, `children`.
28
+
29
+ ```ts
30
+ text('Words', { class }) // also text({ content }); a <div> — `display: inline` to flow
31
+ heading('Title', { subType: 'h2' }) // h1–h6; holds no children
32
+ paragraph('A paragraph.')
33
+ container([children], { subType: 'section' }) // div, section, nav, header, footer, main, article, li, h1–h6, span…
34
+ link({ href: 'about', children }) // a page id, a '/path', or a URL — the mode follows from the href
35
+ link({ href: 'home', hash: 'plans' }) // → /#plans, onto the element with anchor: 'plans'
36
+ button({ content: 'Save', flows }) // holds children too: then content: ''
37
+ image({ src, alt }) // or decorative: true
38
+ embed({ src, title }) // a map, a player: another page in a frame
39
+ svg('<svg …>…</svg>', { label }) // checked, sanitised; currentColor follows the class
40
+ fontAwesome({ icon: 'fa-solid fa-xmark' })
41
+ list({ id: 'rows', items: 'p.data.rows', row: 'row-card' }) // a <ul>; a component per row (its `item` prop)
42
+ list({ id: 'rows', items: 'p.data.rows', row: r => text({ from: `${r.item}.title` }) }) // or a tree per row
43
+ carousel({ id: 'hero', items: 'p.data.slides', autoplay: 5000, row: 'slide-card', children: [/* carouselNext('hero') arrows */] })
44
+ // rows follow their item's `id`, or `itemKey: 'slug'`
45
+ apiContainer({ id: 'p', query: '/data/x.json', cache: true, children }) // publishes p.data; no box of its own
46
+ // loadingSlot: 'p-skeleton' — a child shown until it answers
47
+ form({ id, managedByInteractions: true, flows, children })
48
+ formControl({ name: 'email', label: 'Email', subType: 'email' }) // select: options: [{ label, value }]
49
+ modalContainer({ id, visible: false, title, children })
50
+ component('card', { props: { title }, children: { body: [ … ] } })
51
+ custom({ renderType: 'myPlugin' }); defineElement<Props>(declaration) // a plugin
52
+ ```
53
+
54
+ ## Styles
55
+
56
+ ```ts
57
+ const card = styles('card', { padding: 24, borderRadius: 16 }); // shorthands, camelCase, numbers as px
58
+ styles('title', { fontSize: { desktop: '48px', compact: '32px' }, fontWeight: 700 }); // one property per breakpoint
59
+ styles('card', { css: { desktop: { … }, compact: { … } }, states: { hover: { … } }, variants: { active: { … } } });
60
+ container({ class: card }); // a shared class…
61
+ container({ id: 'hero', class: [card, { opacity: '0.5' }] }); // …plus one thing (needs an id)
62
+ container({ css: { padding: '8px' } }); // rules of its own — never with `class`
63
+ ```
64
+
65
+ Breakpoints: `desktop` (base), `tablet` (48–64rem), `mobile` (<48rem), `compact` (tablet AND mobile). Each inherits
66
+ from `desktop` alone. States: `hover`, `focus`, `focus-visible`, `active`, `disabled`…. An ancestor's state:
67
+ `ancestors: { [card.name]: { states: { hover: { … } } } }`.
68
+
69
+ ## Data, bindings, visibility
70
+
71
+ ```ts
72
+ from: 'p.data.title' // the main attribute: content, src, href, items
73
+ from: 'rows.item.price', as: 'price' // shown as a format (space `formats`) or a template
74
+ from: site.data.title // const site = source('p', sampleJson): typed, checked
75
+ bind: { alt: 'p.data.title' } // other attributes; short source names
76
+ bind: [bindTemplate('content', 'p.data.items', '{{ source|length }} items')]
77
+ bind: [bindTemplate('items', 'p.data.rows', '{{ source|filter(r => r.on) }}', { returns: 'value' })]
78
+ bind: [variantFrom(pill, 'rows.item.status')] // a variant chosen by the data
79
+ bind: [activeOn(navLink, 'about')] // the current page's link
80
+ bind: [activeWhen(dot, '{{ list_dots.index == state.slide }}')] // a variant while a condition holds
81
+ visible: 'p.data.signedIn' // shown while true; '!p.data.signedIn' inverts
82
+ visible: { source: 'p.data.rows', template: '{{ source is defined and source|length == 0 }}' }
83
+ visible: false // hidden until a step shows it
84
+ ```
85
+
86
+ Inside a list row: `rows.item` in a binding, `{{ list_rows.item.x }}` in a template or an attribute,
87
+ `list_rows.index` is a number from 0. A provider's source in a template is `apiContainer_p`… —
88
+ spelled in full, as the element publishes it. Globals: `state`, `navigation` (`routeParams`, `queryParams`),
89
+ `auth`, `variables`, `computed`, `flags`, `theme`, `host`.
90
+
91
+ ## Flows
92
+
93
+ A flow is `[trigger, …steps]`, in an element's `flows`.
94
+
95
+ ```ts
96
+ [onClick(), setState({ key: 'saved', type: 'boolean', value: true })] // types: boolean, number, text, json
97
+ [named('sent', onSubmit()), setState({ key: 'email', type: 'text', value: '{{ sent.values.email }}' })]
98
+ [on('onChange'), …]; [on('onMouseEnter'), …]; [onPageLoad(), …]; [onKey('mod+k, escape'), …]
99
+ [onInterval(5000), …] // every 5 s, while the tab is in view
100
+ [onClick(), scrollBy('cards', { x: '80%' })]; scrollTo('cards', { x: 'end' }); scrollIntoView('answer')
101
+ [named('row', onScroll()), …] // { x, y, atStart, atEnd } of its own box
102
+ [onClick(), toggleState({ key: 'menuOpen' })]
103
+ [onClick(), cycleState({ key: 'slide', length: 3, by: -1 })] // goes round; stepState({ key, by, min, max }) stops
104
+ [onClick(), openModal('details')]; closeModal('details')
105
+ [onClick(), navigate({ urlType: 'page', url: 'about' })]
106
+ [onClick(), runServerAction({ actionId: 'checkout', input: { id: '{{ list_rows.item.id }}' } })]
107
+ [onClick(), when({ field: 'state.step', operator: '=', value: '2' }, setState({ … }))]
108
+ [whileRunning('queue', onClick()), delay(1000), …]
109
+ [named('picked', declaredTrigger(declaration, 'onPick')), …]; declaredCallback(declaration, 'reset', { on: 'seats' })
110
+ ```
111
+
112
+ ## The problems met most
113
+
114
+ Each comes with its code in brackets; reference/authoring-errors.md has a row for every one — search it by code.
115
+
116
+ | Code | Write instead |
117
+ | --- | --- |
118
+ | `class-and-css` | `class: [card, { … }]` with an `id`, or the rules in the class |
119
+ | `id-taken` | ids are one namespace for the whole space, pages included: build a repeated helper inside `scope()` |
120
+ | `unknown-attribute` | the attribute it lists; an event is a flow, not an attribute |
121
+ | `children-in-leaf` | a `container` — a heading made of parts is `container({ subType: 'h1' })` |
122
+ | `template-short-source` | the source in full in templates: `list_rows`, `apiContainer_p` |
123
+ | `template-unknown-name` | a source, a variable, `navigation.queryParams.x` or `source` |
124
+ | `template-text-into-value` | `{ returns: 'value' }` on a template feeding `items` |
125
+ | `list-items-ignored` | `source: 'controlled'` |
126
+ | `condition-starts-visible` | `visible: { source, template }`, which starts hidden |
127
+ | `tablet-rule-skips-mobile` | the rule under `compact` |
128
+ | `colour-without-dark` | `{ light, dark, default }` |
129
+ | `click-on-static-element` | the flow on a `button` (it holds children) or a `link` |
130
+ | `image-without-alt` / `control-without-name` | `alt` or `decorative: true`; `title` on an icon button |
131
+ | `anchor-missing` | the `anchor` on the section the `hash` names |
132
+
133
+ ## Recipes
134
+
135
+ Whole, authoring examples by intent — copy one, then change the names: see the Recipes table in SKILL.md.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: plitzi-authoring
3
3
  description: >-
4
- Write or change a Plitzi space or template in TypeScript with @plitzi/sdk-authoring — pages, layouts, elements,
5
- CSS classes, data bindings, providers and flows — instead of hand-writing schema JSON. Use whenever the task is to
4
+ Write or change a Plitzi space or snippet in TypeScript with @plitzi/sdk-authoring — pages, layouts, components,
5
+ elements, CSS classes, data bindings, providers and flows — instead of hand-writing schema JSON. Use whenever the task is to
6
6
  create, extend, restyle or fix a space: adding a page or a section, sharing a header across pages, binding an
7
7
  element to data, showing or hiding something, wiring what happens on click, or turning an exported JSON into code.
8
8
  ---
@@ -27,10 +27,14 @@ export const space = {
27
27
  const { schema, style, warnings } = authorSpace(space);
28
28
  ```
29
29
 
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. For one element
32
- type, the exported catalogues answer faster (`elementDefaultAttributes`, `elementTriggers`, `elementSlots`… — see
33
- [elements and styles](reference/elements-and-styles.md)).
30
+ **Start with [the cheatsheet](CHEATSHEET.md)** — the factories, fields, steps and the problems met most, on one page —
31
+ and open a reference below only when the task names its subject. What an element takes, a step's params, what a
32
+ trigger hands its flow: `npx @plitzi/cli explain <name>` answers in a few lines (over MCP, `plitzi://explain/<name>`).
33
+ The published `.d.ts` documents the rest — search it by name, never read it whole.
34
+
35
+ This skill writes a space that lives in code. One that lives on Plitzi — edited in the builder, published from there —
36
+ is edited through the Plitzi MCP server instead, never both on one space; `plitzi create --from <space>` turns the
37
+ second into the first. If the MCP asks for a sign-in nobody can give, a project in code needs no account.
34
38
 
35
39
  ## How to work
36
40
 
@@ -41,8 +45,13 @@ type, the exported catalogues answer faster (`elementDefaultAttributes`, `elemen
41
45
  do exactly that. Never work around a check, cast past it, or move the logic into a plugin to escape it. The first
42
46
  refusals come one at a time; the linter's come as ONE list — fix every line of it before running again. How it
43
47
  checks, a one-file author script for any project, and what it cannot see: [validation](reference/validation.md).
44
- 3. **Read every warning.** Each one names something written that will not do what it says. Zero warnings is the bar.
45
- 4. **Look at it.** `npm run shot -- /about --width 390 --scheme dark` saves a picture of one page; `npm run visual`
48
+ 3. **Read every warning, then every suggestion.** A warning names something written that will not do what it says —
49
+ zero warnings is the bar. A suggestion (`[suggest]`, `suggestions` in `authorSpace`'s result) names a shorter way to
50
+ the same page: a layout for chrome on every page, a component or a `list` for copies, a button's own `content`. Take
51
+ them, the ones that save the most first. See [efficiency](reference/efficiency.md).
52
+ 4. **Look at it.** `npm run check -- /about --width 1440,390` says in text whether the page is whole — every element on
53
+ screen, nothing overflowing, a clean console; `npm run shot -- /about --width 390 --scheme dark` is the picture, for
54
+ when it says something is wrong (`--frames 4` shows what moves); `npm run visual`
46
55
  runs the checks. Look at desktop, tablet and mobile, light and dark, and the page while its data is still loading.
47
56
  5. **Go through the [review checklist](reference/review-checklist.md) before saying it is done.** It is the feedback a
48
57
  reviewer gives on every change, written down so you do not need to hear it.
@@ -50,20 +59,28 @@ type, the exported catalogues answer faster (`elementDefaultAttributes`, `elemen
50
59
  ## The rules that matter most
51
60
 
52
61
  1. **Name what is referred to.** Give an `id` to every element a binding, a flow or a test addresses. Ids are ONE
53
- namespace for the whole space — layouts and every page share it — so an element built by a helper that runs more
54
- than once gets its id prefixed by what it is for: `` `${pageId}-foot` ``.
55
- 2. **Share with classes and layouts, never with copies.** A look used twice is a `styles()` class. Chrome shown on
56
- several pages — a header, a sidebar, a footer — is a **layout** the pages name, written once. A menu, a card grid,
57
- a list of steps is DATA mapped to elements (`entries.map(entry => link(…))`), not a block pasted per item. See
58
- [layouts and duplication](reference/layouts.md).
62
+ namespace for the whole space — layouts and every page share it — so a helper that runs more than once builds inside
63
+ `scope('promos', ref => …)`: every id in it is prefixed (`promos-panel`), and `ref('slides')` names one in full. An `id` is the element's name in the
64
+ space (`data-id` in the page); the DOM id a URL's `#fragment` lands on is its `anchor`.
65
+ 2. **Share with classes, layouts and components, never with copies.** A look used twice is a `styles()` class.
66
+ Chrome shown on several pages — a header, a sidebar, a footer — is a **layout** the pages name, written once. A block
67
+ placed many times with different content — a product card, a testimonial — is a **component** placed with
68
+ `component(id, { props })`. A menu, a card grid, a list of steps is DATA, never a block pasted per item: a short
69
+ menu mapped in code (`entries.map(entry => link({ href, content: entry.title }))`) is fine; rows — cards, dishes,
70
+ tiles — are one `list` over them (`items: [ … ]`) whose row is written once. A link or a button says its words as
71
+ its own `content`, not through a `text` inside it. See [layouts and duplication](reference/layouts.md),
72
+ [components](reference/components.md) and [efficiency](reference/efficiency.md).
59
73
  3. **One element, one base selector.** An element takes a shared `class` OR its own `css`, never both — authoring
60
- refuses the pair. A look that never changes is its own class.
74
+ refuses the pair. "This class plus one thing" is the class with rules on top, last in the list:
75
+ `class: [cover, { opacity: '0.25' }]` (needs the element's `id`; it becomes the class `<id>--own`). A look that
76
+ never changes is its own class.
61
77
  4. **Visible by default; decide which way the logic flips.** An element is on screen unless something hides it. When
62
78
  the logic REVEALS it (hidden → shown: an empty state, a "get started" card, an admin-only panel), it starts hidden —
63
79
  `visible: 'source'`, or `visible: false` plus a binding for a computed condition — so it never flashes while data
64
80
  loads (`condition-starts-visible` warns). When a flag HIDES it (shown → hidden: a sidebar label until the sidebar
65
81
  is folded), it keeps the default and an absent flag must leave it shown. See
66
- [data and visibility](reference/data-and-visibility.md).
82
+ [data and visibility](reference/data-and-visibility.md). What a PERSON switches on — a feature in beta, a rollout —
83
+ is a feature flag, not a visibility: gated off, it is not rendered at all. See [feature flags](reference/feature-flags.md).
67
84
  5. **Know where a template is evaluated.** A binding's template (`bindTemplate`) and a step's params evaluate full
68
85
  Twig. An ATTRIBUTE resolves a name with filters (`{{ list_games.item.slug }}`, `{{ post.slug|url_encode }}`) against
69
86
  the sources around the element; a condition there is used as written. Inside any template a source is spelled in
@@ -93,81 +110,67 @@ type, the exported catalogues answer faster (`elementDefaultAttributes`, `elemen
93
110
  (`hideLabel: true` hides it and keeps the name), every image an `alt` or `decorative: true`. Clicks go on a
94
111
  `button` — it holds children, so a whole card can be one — or a `link`, never on a container. See
95
112
  [accessibility](reference/accessibility.md).
113
+ 14. **Motion that stays smooth.** Animate `opacity` and `transform` only; no animated blur, shadow or size. See
114
+ [motion](reference/colours-and-motion.md).
96
115
 
97
116
  ## Recipes
98
117
 
99
- Copy the one that fits, then change the names. Each is the shape `authorSpace` accepts on the first try.
118
+ Each is a whole file that authors with no warning — CI holds it to that. Copy the one that fits, then change the names.
100
119
 
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
- ```
120
+ | To… | Open |
121
+ | --- | --- |
122
+ | Show data with no backend: a JSON file, a card per row, a count, an empty state, a computed value | [recipes/show-data.ts](recipes/show-data.ts) |
123
+ | Data typed by a sample of it: completed and checked paths, typed rows | [recipes/typed-data.ts](recipes/typed-data.ts) |
124
+ | Filter and sort a list from a select | [recipes/filter-a-list.ts](recipes/filter-a-list.ts) |
125
+ | A page per record (`/products/:slug`), and "not found" | [recipes/detail-page.ts](recipes/detail-page.ts) |
126
+ | Link to a section of a page (`/#plans`) | [recipes/link-to-a-section.ts](recipes/link-to-a-section.ts) |
127
+ | A carousel: slides with arrows, dots and autoplay; a marquee; a row that swipes | [recipes/carousel.ts](recipes/carousel.ts) |
128
+ | Do something every few seconds — a ticker, a poll, a slideshow by hand | [recipes/every-few-seconds.ts](recipes/every-few-seconds.ts) |
129
+ | A marquee that scrolls for ever and stops under the pointer | [recipes/marquee.ts](recipes/marquee.ts) |
130
+ | A row that scrolls sideways, with arrows that hide at its ends | [recipes/scroll-a-row.ts](recipes/scroll-a-row.ts) |
131
+ | A link built from a row: WhatsApp with the product's name, `mailto:` | [recipes/link-with-data.ts](recipes/link-with-data.ts) |
132
+ | React to a click or a submit; a modal; a switch | [recipes/forms-and-modals.ts](recipes/forms-and-modals.ts) |
133
+ | A feature behind a flag | [recipes/feature-flag.ts](recipes/feature-flag.ts) |
134
+ | An element of your own (a plugin), checked | [recipes/plugin.ts](recipes/plugin.ts) |
135
+ | Controls a screen reader and a browser agent can use | [recipes/usable-without-sight.ts](recipes/usable-without-sight.ts) |
136
+ | Tokens, a shared class, "the class plus one thing", tablet and phone at once | [recipes/style-a-page.ts](recipes/style-a-page.ts) |
137
+ | A design in Tailwind classes, kept as classes the builder edits | [recipes/from-tailwind.ts](recipes/from-tailwind.ts) |
138
+ | A page from elsewhere in a frame (`embed`: a map, a player) and a drawing of your own (`svg`) | [recipes/embed-and-svg.ts](recipes/embed-and-svg.ts) |
154
139
 
155
140
  ## References
156
141
 
157
142
  | Read | When |
158
143
  | --- | --- |
159
144
  | [elements-and-styles.md](reference/elements-and-styles.md) | Any element or CSS: factories, fields, classes, states, variants, tokens, fonts, lists, links |
145
+ | [colours-and-motion.md](reference/colours-and-motion.md) | Colours for both themes, tokens, keyframes, and motion that stays smooth (good practices) |
146
+ | [tailwind.md](reference/tailwind.md) | A design written in Tailwind classes: `tw()`, its breakpoints and states, what it refuses |
160
147
  | [layouts.md](reference/layouts.md) | Anything shown on more than one page; menus; reducing duplication of elements and styles |
148
+ | [efficiency.md](reference/efficiency.md) | The same page with fewer elements: the suggestions `authorSpace` makes, and the short way for each long one |
149
+ | [components.md](reference/components.md) | One block placed many times — a card, a tier, a testimonial: props, slots, binding a row into one, why it is closed |
161
150
  | [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 |
151
+ | [kept-state.md](reference/kept-state.md) | State that outlives a reload: `keepState`, transient and painted keys |
152
+ | [feature-flags.md](reference/feature-flags.md) | Switching a part of the space on or off — a beta, a rollout, the old version kept until the new one ships |
153
+ | [lists.md](reference/lists.md) | Rendering rows, filtering and sorting them, a detail page for one record, carousels |
154
+ | [typed-sources.md](reference/typed-sources.md) | Data typed by a sample of it: `source()`, typed rows, `twig` for templates |
163
155
  | [validation.md](reference/validation.md) | How `authorSpace` checks, the loop that wastes no attempts, and what it cannot see |
164
156
  | [authoring-errors.md](reference/authoring-errors.md) | What `authorSpace` refuses or warns about, and what to write instead |
165
157
  | [templates.md](reference/templates.md) | Any `{{ … }}` or `{% … %}`: where it runs, naming sources, filters, tests, dates |
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 |
158
+ | [flows.md](reference/flows.md) | Clicks, submits, page loads, every few seconds, server actions, modals, state |
159
+ | [realtime.md](reference/realtime.md) | Pages that see each other: channels, presence, cursors, a shared board, who may hear a topic |
160
+ | [plugins.md](reference/plugins.md) | A component of your own: props, binding them, writing state, channels, registering, behaving in the builder |
168
161
  | [structure.md](reference/structure.md) | A space bigger than one screen: files, helpers, naming, keeping it short |
169
162
  | [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
163
  | [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 |
171
- | [templates-and-export.md](reference/templates-and-export.md) | Publishing a template; turning an exported JSON into code |
164
+ | [snippets-and-export.md](reference/snippets-and-export.md) | Publishing a snippet; turning an exported JSON — or a server action — into code |
172
165
  | [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 |
173
166
  | [review-checklist.md](reference/review-checklist.md) | Before you say it is done |
167
+
168
+ **What to read for a kind of task** — the cheatsheet, then only these:
169
+
170
+ - A site — landing, catalogue, blog: elements-and-styles, layouts, components, lists.
171
+ - An app with state and actions: data-and-visibility, templates, flows — realtime when pages see each other.
172
+ - A component of your own: plugins.
173
+ - A refusal or a warning: search authoring-errors.md for its code — never read it whole.
174
+ - Tests: testing.
175
+
176
+ Nothing else needs reading up front.