@seriousgaming/sdk-tools 0.0.0-stage → 1.0.0-alpha.1

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 (391) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +36 -2
  3. package/dist/bundling/logic-exports.d.ts +34 -0
  4. package/dist/bundling/node-builtins.d.ts +21 -0
  5. package/dist/bundling/transpiler.d.ts +7 -0
  6. package/dist/cli.js +27368 -0
  7. package/dist/client/dev-server.d.ts +59 -0
  8. package/dist/client/index.d.ts +28 -0
  9. package/dist/client/stub-services.d.ts +32 -0
  10. package/dist/client/test-session.d.ts +195 -0
  11. package/dist/client.js +16884 -0
  12. package/dist/core/asset-reference-translator.d.ts +46 -0
  13. package/dist/core/clock.d.ts +57 -0
  14. package/dist/core/faults.d.ts +65 -0
  15. package/dist/core/game-settings.d.ts +65 -0
  16. package/dist/core/history.d.ts +89 -0
  17. package/dist/core/listen-with-fallback.d.ts +30 -0
  18. package/dist/core/mutation-queue.d.ts +61 -0
  19. package/dist/core/package-scenario.d.ts +89 -0
  20. package/dist/core/participant-registry.d.ts +109 -0
  21. package/dist/core/pending-changes.d.ts +14 -0
  22. package/dist/core/protocol.d.ts +83 -0
  23. package/dist/core/scenario-drafts.d.ts +32 -0
  24. package/dist/core/session-endpoints.d.ts +108 -0
  25. package/dist/core/session-gates.d.ts +75 -0
  26. package/dist/core/session-host.d.ts +94 -0
  27. package/dist/core/session-refusals.d.ts +56 -0
  28. package/dist/core/session-rewind.d.ts +32 -0
  29. package/dist/core/session-roster.d.ts +128 -0
  30. package/dist/core/session-surfaces.d.ts +32 -0
  31. package/dist/core/session-ticks.d.ts +18 -0
  32. package/dist/core/session-timers.d.ts +84 -0
  33. package/dist/core/session-writes.d.ts +40 -0
  34. package/dist/core/session.d.ts +385 -0
  35. package/dist/core/settings-locks.d.ts +22 -0
  36. package/dist/core/team-names.d.ts +52 -0
  37. package/dist/core/team-state-store.d.ts +209 -0
  38. package/dist/core/tick-timer.d.ts +75 -0
  39. package/dist/dev/blob-links.d.ts +10 -0
  40. package/dist/dev/console/assets/index-DgFglsnA.js +204 -0
  41. package/dist/dev/console/index.html +12 -0
  42. package/dist/dev/console-path.d.ts +19 -0
  43. package/dist/dev/lockfile.d.ts +124 -0
  44. package/dist/dev/package-watcher.d.ts +21 -0
  45. package/dist/dev/routes.d.ts +67 -0
  46. package/dist/dev/shell.d.ts +260 -0
  47. package/dist/dev/surfaces.d.ts +41 -0
  48. package/dist/dev/tools-identity.d.ts +23 -0
  49. package/dist/dev/view/batch.d.ts +172 -0
  50. package/dist/dev/view/commands.d.ts +75 -0
  51. package/dist/dev/view/route.d.ts +58 -0
  52. package/dist/dev/view/view.d.ts +78 -0
  53. package/dist/dev/view/viewer.d.ts +32 -0
  54. package/dist/discovery/config.d.ts +9 -0
  55. package/dist/discovery/manifest-paths.d.ts +21 -0
  56. package/dist/discovery/removed-fields.d.ts +90 -0
  57. package/dist/docs/api/deck.md +386 -0
  58. package/dist/docs/api/index.md +67 -0
  59. package/dist/docs/api/sdk-commands.md +286 -0
  60. package/dist/docs/api/sdk-e2e.md +433 -0
  61. package/dist/docs/api/sdk-projection.md +207 -0
  62. package/dist/docs/api/sdk-testing.md +214 -0
  63. package/dist/docs/api/sdk-tools-client.md +560 -0
  64. package/dist/docs/api/sdk-ui-testing.md +231 -0
  65. package/dist/docs/api/sdk-ui.md +1269 -0
  66. package/dist/docs/api/sdk.md +2140 -0
  67. package/dist/docs/changelog-0.x.md +1156 -0
  68. package/dist/docs/changelog.md +236 -0
  69. package/dist/docs/cli-and-dev-server.md +217 -0
  70. package/dist/docs/cli-error-codes.md +154 -0
  71. package/dist/docs/cli-reference.md +2488 -0
  72. package/dist/docs/cli-simulation-testing.md +222 -0
  73. package/dist/docs/concepts/index.md +46 -0
  74. package/dist/docs/concepts/terminology.md +322 -0
  75. package/dist/docs/environment-variables.md +102 -0
  76. package/dist/docs/getting-started.md +145 -0
  77. package/dist/docs/guides/ai-facilitation.md +220 -0
  78. package/dist/docs/guides/capabilities.md +460 -0
  79. package/dist/docs/guides/endpoints.md +335 -0
  80. package/dist/docs/guides/engine-testing.md +477 -0
  81. package/dist/docs/guides/engine.md +527 -0
  82. package/dist/docs/guides/file-handling.md +405 -0
  83. package/dist/docs/guides/games.md +176 -0
  84. package/dist/docs/guides/global-settings.md +94 -0
  85. package/dist/docs/guides/guide.md +123 -0
  86. package/dist/docs/guides/hooks.md +183 -0
  87. package/dist/docs/guides/index.md +51 -0
  88. package/dist/docs/guides/internationalization.md +288 -0
  89. package/dist/docs/guides/llm-integration.md +635 -0
  90. package/dist/docs/guides/multi-team-games.md +586 -0
  91. package/dist/docs/guides/patterns.md +391 -0
  92. package/dist/docs/guides/presentation.md +196 -0
  93. package/dist/docs/guides/production-constraints.md +130 -0
  94. package/dist/docs/guides/scenario-editor.md +143 -0
  95. package/dist/docs/guides/scenarios.md +515 -0
  96. package/dist/docs/guides/session.md +972 -0
  97. package/dist/docs/guides/settings.md +458 -0
  98. package/dist/docs/guides/setup.md +193 -0
  99. package/dist/docs/guides/speech-generation.md +233 -0
  100. package/dist/docs/guides/starter-game.md +164 -0
  101. package/dist/docs/guides/testing.md +575 -0
  102. package/dist/docs/guides/tick-controller.md +569 -0
  103. package/dist/docs/guides/type-safety.md +516 -0
  104. package/dist/docs/guides/ui.md +1389 -0
  105. package/dist/docs/guides/upgrading.md +636 -0
  106. package/dist/docs/guides/validators.md +217 -0
  107. package/dist/docs/guides/waiting-room.md +244 -0
  108. package/dist/docs/index.md +41 -0
  109. package/dist/docs/recipes/act-on-the-session-from-an-endpoint.md +95 -0
  110. package/dist/docs/recipes/adapt-a-game-for-walkthroughs.md +133 -0
  111. package/dist/docs/recipes/adapt-ui-for-observers.md +74 -0
  112. package/dist/docs/recipes/call-endpoint-from-ui.md +109 -0
  113. package/dist/docs/recipes/check-a-package-from-a-script.md +117 -0
  114. package/dist/docs/recipes/create-capability.md +66 -0
  115. package/dist/docs/recipes/create-endpoint.md +127 -0
  116. package/dist/docs/recipes/create-engine.md +111 -0
  117. package/dist/docs/recipes/create-game.md +70 -0
  118. package/dist/docs/recipes/create-guide.md +75 -0
  119. package/dist/docs/recipes/create-hook.md +80 -0
  120. package/dist/docs/recipes/create-presentation.md +142 -0
  121. package/dist/docs/recipes/create-scenario-editor.md +102 -0
  122. package/dist/docs/recipes/create-scenario.md +98 -0
  123. package/dist/docs/recipes/create-session-ui.md +116 -0
  124. package/dist/docs/recipes/create-settings.md +88 -0
  125. package/dist/docs/recipes/create-setup.md +115 -0
  126. package/dist/docs/recipes/create-tick-controller.md +65 -0
  127. package/dist/docs/recipes/create-user-interface.md +96 -0
  128. package/dist/docs/recipes/create-validator.md +67 -0
  129. package/dist/docs/recipes/create-waiting-room-ui.md +109 -0
  130. package/dist/docs/recipes/declare-api-uses.md +67 -0
  131. package/dist/docs/recipes/design-state-for-concurrent-writes.md +96 -0
  132. package/dist/docs/recipes/distribute-package.md +80 -0
  133. package/dist/docs/recipes/e2e-testing.md +116 -0
  134. package/dist/docs/recipes/end-game-on-condition.md +154 -0
  135. package/dist/docs/recipes/fix-duplicate-packages.md +101 -0
  136. package/dist/docs/recipes/index.md +86 -0
  137. package/dist/docs/recipes/make-an-endpoint-external.md +101 -0
  138. package/dist/docs/recipes/negotiate-across-teams.md +164 -0
  139. package/dist/docs/recipes/observe-other-teams-state.md +119 -0
  140. package/dist/docs/recipes/publish-a-package.md +139 -0
  141. package/dist/docs/recipes/remember-a-viewers-choice.md +55 -0
  142. package/dist/docs/recipes/review-a-past-day.md +73 -0
  143. package/dist/docs/recipes/share-assets-across-scenarios.md +120 -0
  144. package/dist/docs/recipes/share-logic-between-engine-and-ui.md +130 -0
  145. package/dist/docs/recipes/show-all-teams-participants.md +109 -0
  146. package/dist/docs/recipes/start-from-an-empty-package.md +169 -0
  147. package/dist/docs/recipes/take-a-file-in-an-endpoint.md +82 -0
  148. package/dist/docs/recipes/test-a-multi-role-game.md +51 -0
  149. package/dist/docs/recipes/test-user-interface.md +95 -0
  150. package/dist/docs/recipes/test-with-engine.md +115 -0
  151. package/dist/docs/recipes/upload-and-display-files.md +148 -0
  152. package/dist/docs/recipes/use-request-play.md +65 -0
  153. package/dist/docs/recipes/use-request-tick.md +75 -0
  154. package/dist/docs/troubleshooting.md +816 -0
  155. package/dist/execution/blob-inputs.d.ts +57 -0
  156. package/dist/execution/bundle-reload.d.ts +42 -0
  157. package/dist/execution/config-resolver.d.ts +127 -0
  158. package/dist/execution/configuration-builder.d.ts +66 -0
  159. package/dist/execution/context-composition.d.ts +161 -0
  160. package/dist/execution/endpoint-actions.d.ts +9 -0
  161. package/dist/execution/endpoint-payloads.d.ts +32 -0
  162. package/dist/execution/endpoint-runner.d.ts +129 -0
  163. package/dist/execution/engine-state.d.ts +40 -0
  164. package/dist/execution/executor-callbacks.d.ts +56 -0
  165. package/dist/execution/executor.d.ts +436 -0
  166. package/dist/execution/gates.d.ts +45 -0
  167. package/dist/execution/hook-chain.d.ts +25 -0
  168. package/dist/execution/logic-calls.d.ts +190 -0
  169. package/dist/execution/logic-failure.d.ts +84 -0
  170. package/dist/execution/logic-loader.d.ts +54 -0
  171. package/dist/execution/logic-result.d.ts +76 -0
  172. package/dist/execution/tick-controller-call.d.ts +32 -0
  173. package/dist/execution/types.d.ts +48 -0
  174. package/dist/generation/ts-type.d.ts +39 -0
  175. package/dist/guide/locale.d.ts +12 -0
  176. package/dist/guide/video-registry.d.ts +42 -0
  177. package/dist/help-examples.d.ts +125 -0
  178. package/dist/host/adapter.d.ts +67 -0
  179. package/dist/host/host-channel.d.ts +222 -0
  180. package/dist/host/index.d.ts +20 -0
  181. package/dist/host/protocol.d.ts +783 -0
  182. package/dist/host/surfaces.d.ts +42 -0
  183. package/dist/host/theme.d.ts +55 -0
  184. package/dist/host.js +583 -0
  185. package/dist/prompts/index.d.ts +7 -0
  186. package/dist/prompts/schema-builder.d.ts +29 -0
  187. package/dist/prompts/types.d.ts +108 -0
  188. package/dist/prompts/validators.d.ts +32 -0
  189. package/dist/runtime/capability-scope.d.ts +6 -0
  190. package/dist/runtime/context-merge.d.ts +29 -0
  191. package/dist/runtime/context-projection.d.ts +41 -0
  192. package/dist/runtime/guarded-callbacks.d.ts +15 -0
  193. package/dist/runtime/index.d.ts +27 -0
  194. package/dist/runtime/manifest-version.d.ts +55 -0
  195. package/dist/runtime/tick-validation.d.ts +25 -0
  196. package/dist/runtime.js +346 -0
  197. package/dist/schema.json +1546 -0
  198. package/dist/server/vite-plugin.d.ts +46 -0
  199. package/dist/services/tts/client.d.ts +51 -0
  200. package/dist/services/tts/elevenlabs.d.ts +24 -0
  201. package/dist/services/tts/types.d.ts +44 -0
  202. package/dist/storage/asset-resolver.d.ts +49 -0
  203. package/dist/storage/blob-store.d.ts +41 -0
  204. package/dist/templates/endpoint/logic.test.ts.liquid +61 -0
  205. package/dist/templates/endpoint/logic.ts.liquid +25 -0
  206. package/dist/templates/engine/init.json.liquid +7 -0
  207. package/dist/templates/engine/logic-aggregate.test.ts.liquid +90 -0
  208. package/dist/templates/engine/logic-aggregate.ts.liquid +46 -0
  209. package/dist/templates/engine/logic.test.ts.liquid +61 -0
  210. package/dist/templates/engine/logic.ts.liquid +32 -0
  211. package/dist/templates/engine/run.json.liquid +7 -0
  212. package/dist/templates/guide/01-at-a-glance.md.liquid +19 -0
  213. package/dist/templates/guide/02-preparation.md.liquid +18 -0
  214. package/dist/templates/guide/03-running-the-session.md.liquid +37 -0
  215. package/dist/templates/hook/logic.test.ts.liquid +46 -0
  216. package/dist/templates/hook/logic.ts.liquid +12 -0
  217. package/dist/templates/package/CLAUDE.md.template +338 -0
  218. package/dist/templates/package/DESIGN.md.liquid +144 -0
  219. package/dist/templates/package/README.md.liquid +143 -0
  220. package/dist/templates/package/claude/rules/workflow-create-capability.md +26 -0
  221. package/dist/templates/package/claude/rules/workflow-create-endpoint.md +35 -0
  222. package/dist/templates/package/claude/rules/workflow-create-engine.md +35 -0
  223. package/dist/templates/package/claude/rules/workflow-create-guide.md +40 -0
  224. package/dist/templates/package/claude/rules/workflow-create-presentation.md +37 -0
  225. package/dist/templates/package/claude/rules/workflow-create-user-interface.md +31 -0
  226. package/dist/templates/package/claude/rules/workflow-publish-package.md +54 -0
  227. package/dist/templates/package/claude/rules/workflow-test-engine-or-ui.md +30 -0
  228. package/dist/templates/package/claude/rules/workflow-update-sdk-version.md +43 -0
  229. package/dist/templates/package/claude/settings.json.template +16 -0
  230. package/dist/templates/package/gitignore.template +15 -0
  231. package/dist/templates/package/global-setup.ts.template +21 -0
  232. package/dist/templates/package/globals.d.ts.template +2 -0
  233. package/dist/templates/package/oxfmtrc.json.template +14 -0
  234. package/dist/templates/package/oxlintrc.json.template +238 -0
  235. package/dist/templates/package/package.json.liquid +65 -0
  236. package/dist/templates/package/playwright.config.ts.liquid +19 -0
  237. package/dist/templates/package/pnpm-workspace.yaml.template +27 -0
  238. package/dist/templates/package/test-setup.ts.liquid +7 -0
  239. package/dist/templates/package/tsconfig.json.liquid +15 -0
  240. package/dist/templates/package/vite.config.ts.liquid +19 -0
  241. package/dist/templates/package/vitest.config.ts.liquid +25 -0
  242. package/dist/templates/package/vitest.engine.config.ts.liquid +12 -0
  243. package/dist/templates/presentation/index.html.liquid +20 -0
  244. package/dist/templates/presentation/src/App.tsx.liquid +49 -0
  245. package/dist/templates/presentation/src/Slides.tsx.liquid +81 -0
  246. package/dist/templates/presentation/src/i18n.ts.liquid +54 -0
  247. package/dist/templates/presentation/src/main.tsx.liquid +14 -0
  248. package/dist/templates/scenario_editor/src/App.test.tsx.liquid +30 -0
  249. package/dist/templates/scenario_editor/src/App.tsx.liquid +95 -0
  250. package/dist/templates/scenario_editor/src/i18n.ts.liquid +44 -0
  251. package/dist/templates/session/src/App.test.tsx.liquid +60 -0
  252. package/dist/templates/session/src/App.tsx.liquid +128 -0
  253. package/dist/templates/session/src/i18n.ts.liquid +53 -0
  254. package/dist/templates/session/src/main.tsx.liquid +48 -0
  255. package/dist/templates/session/src/theme.ts.liquid +46 -0
  256. package/dist/templates/settings-editor/index.html.liquid +12 -0
  257. package/dist/templates/settings-editor/src/main.tsx.liquid +48 -0
  258. package/dist/templates/settings-editor/src/theme.ts.liquid +24 -0
  259. package/dist/templates/setup/src/App.test.tsx.liquid +39 -0
  260. package/dist/templates/setup/src/App.tsx.liquid +102 -0
  261. package/dist/templates/setup/src/i18n.ts.liquid +47 -0
  262. package/dist/templates/starter-game/capabilities/briefing/capability.sdk.json +18 -0
  263. package/dist/templates/starter-game/capabilities/plan/capability.sdk.json +17 -0
  264. package/dist/templates/starter-game/capabilities/results/capability.sdk.json +61 -0
  265. package/dist/templates/starter-game/capabilities/roles/capability.sdk.json +17 -0
  266. package/dist/templates/starter-game/capabilities/weather/capability.sdk.json +23 -0
  267. package/dist/templates/starter-game/engine/defaults.ts +81 -0
  268. package/dist/templates/starter-game/engine/engine.sdk.json +12 -0
  269. package/dist/templates/starter-game/engine/logic.test.ts +221 -0
  270. package/dist/templates/starter-game/engine/logic.ts +149 -0
  271. package/dist/templates/starter-game/engine/test-helpers.ts +74 -0
  272. package/dist/templates/starter-game/engine/types.ts +139 -0
  273. package/dist/templates/starter-game/guide/content/en/01-at-a-glance.md +51 -0
  274. package/dist/templates/starter-game/guide/content/en/02-preparation.md +35 -0
  275. package/dist/templates/starter-game/guide/content/en/03-running-the-day.md +58 -0
  276. package/dist/templates/starter-game/guide/guide.sdk.json +10 -0
  277. package/dist/templates/starter-game/guide/guide.test.ts +108 -0
  278. package/dist/templates/starter-game/hooks/assignproducts/defaults.ts +37 -0
  279. package/dist/templates/starter-game/hooks/assignproducts/hook.sdk.json +13 -0
  280. package/dist/templates/starter-game/hooks/assignproducts/logic/logic.test.ts +77 -0
  281. package/dist/templates/starter-game/hooks/assignproducts/logic/logic.ts +47 -0
  282. package/dist/templates/starter-game/hooks/assignproducts/types.ts +55 -0
  283. package/dist/templates/starter-game/package.json +68 -0
  284. package/dist/templates/starter-game/playwright.config.ts +19 -0
  285. package/dist/templates/starter-game/scenarios/heatwave/scenario.sdk.json +20 -0
  286. package/dist/templates/starter-game/scenarios/ordinary_week/scenario.sdk.json +12 -0
  287. package/dist/templates/starter-game/seriousgaming.sdk.json +23 -0
  288. package/dist/templates/starter-game/session/defaults.ts +80 -0
  289. package/dist/templates/starter-game/session/index.html +12 -0
  290. package/dist/templates/starter-game/session/session.sdk.json +11 -0
  291. package/dist/templates/starter-game/session/src/App.test.tsx +307 -0
  292. package/dist/templates/starter-game/session/src/App.tsx +309 -0
  293. package/dist/templates/starter-game/session/src/Section.tsx +37 -0
  294. package/dist/templates/starter-game/session/src/TeamDoor.tsx +96 -0
  295. package/dist/templates/starter-game/session/src/format.ts +8 -0
  296. package/dist/templates/starter-game/session/src/i18n.ts +51 -0
  297. package/dist/templates/starter-game/session/src/main.tsx +48 -0
  298. package/dist/templates/starter-game/session/src/theme.ts +45 -0
  299. package/dist/templates/starter-game/session/types.ts +135 -0
  300. package/dist/templates/starter-game/settings/engine/settings.sdk.json +50 -0
  301. package/dist/templates/starter-game/settings/menu/settings.sdk.json +78 -0
  302. package/dist/templates/starter-game/settings/session/settings.sdk.json +23 -0
  303. package/dist/templates/starter-game/settings/ui/settings.sdk.json +25 -0
  304. package/dist/templates/starter-game/test-setup.ts +7 -0
  305. package/dist/templates/starter-game/testing/e2e/game.spec.ts +139 -0
  306. package/dist/templates/starter-game/testing/e2e/global-setup.ts +21 -0
  307. package/dist/templates/starter-game/testing/engine/game.engine.test.ts +171 -0
  308. package/dist/templates/starter-game/tsconfig.json +19 -0
  309. package/dist/templates/starter-game/ui/defaults.ts +60 -0
  310. package/dist/templates/starter-game/ui/index.html +12 -0
  311. package/dist/templates/starter-game/ui/src/App.test.tsx +138 -0
  312. package/dist/templates/starter-game/ui/src/App.tsx +175 -0
  313. package/dist/templates/starter-game/ui/src/DayBar.test.tsx +78 -0
  314. package/dist/templates/starter-game/ui/src/DayBar.tsx +72 -0
  315. package/dist/templates/starter-game/ui/src/HistoryPanel.test.tsx +74 -0
  316. package/dist/templates/starter-game/ui/src/HistoryPanel.tsx +120 -0
  317. package/dist/templates/starter-game/ui/src/MakeRow.tsx +129 -0
  318. package/dist/templates/starter-game/ui/src/Section.tsx +37 -0
  319. package/dist/templates/starter-game/ui/src/ThemeToggle.tsx +49 -0
  320. package/dist/templates/starter-game/ui/src/TodayPanel.test.tsx +179 -0
  321. package/dist/templates/starter-game/ui/src/TodayPanel.tsx +92 -0
  322. package/dist/templates/starter-game/ui/src/YesterdayPanel.tsx +142 -0
  323. package/dist/templates/starter-game/ui/src/format.ts +12 -0
  324. package/dist/templates/starter-game/ui/src/i18n.ts +95 -0
  325. package/dist/templates/starter-game/ui/src/main.tsx +48 -0
  326. package/dist/templates/starter-game/ui/src/menu.ts +18 -0
  327. package/dist/templates/starter-game/ui/src/test-helpers.ts +58 -0
  328. package/dist/templates/starter-game/ui/src/theme.ts +45 -0
  329. package/dist/templates/starter-game/ui/src/writes.ts +43 -0
  330. package/dist/templates/starter-game/ui/types.ts +137 -0
  331. package/dist/templates/starter-game/ui/ui.sdk.json +11 -0
  332. package/dist/templates/starter-game/validators/capacity/defaults.ts +37 -0
  333. package/dist/templates/starter-game/validators/capacity/logic/logic.test.ts +96 -0
  334. package/dist/templates/starter-game/validators/capacity/logic/logic.ts +45 -0
  335. package/dist/templates/starter-game/validators/capacity/types.ts +55 -0
  336. package/dist/templates/starter-game/validators/capacity/validator.sdk.json +11 -0
  337. package/dist/templates/starter-game/vite.config.ts +19 -0
  338. package/dist/templates/starter-game/vitest.config.ts +15 -0
  339. package/dist/templates/starter-game/vitest.engine.config.ts +19 -0
  340. package/dist/templates/starter-game/waiting_room/defaults.ts +40 -0
  341. package/dist/templates/starter-game/waiting_room/index.html +12 -0
  342. package/dist/templates/starter-game/waiting_room/src/App.test.tsx +102 -0
  343. package/dist/templates/starter-game/waiting_room/src/App.tsx +194 -0
  344. package/dist/templates/starter-game/waiting_room/src/i18n.ts +33 -0
  345. package/dist/templates/starter-game/waiting_room/src/main.tsx +48 -0
  346. package/dist/templates/starter-game/waiting_room/src/theme.ts +45 -0
  347. package/dist/templates/starter-game/waiting_room/types.ts +59 -0
  348. package/dist/templates/starter-game/waiting_room/waiting_room.sdk.json +10 -0
  349. package/dist/templates/tick-controller/logic.ts.liquid +8 -0
  350. package/dist/templates/ui/index.html.liquid +12 -0
  351. package/dist/templates/ui/src/App.test.tsx.liquid +41 -0
  352. package/dist/templates/ui/src/App.tsx.liquid +103 -0
  353. package/dist/templates/ui/src/i18n.ts.liquid +47 -0
  354. package/dist/templates/ui/src/main.tsx.liquid +48 -0
  355. package/dist/templates/ui/src/theme.ts.liquid +46 -0
  356. package/dist/templates/validator/logic.test.ts.liquid +42 -0
  357. package/dist/templates/validator/logic.ts.liquid +21 -0
  358. package/dist/templates/waiting-room/index.html.liquid +12 -0
  359. package/dist/templates/waiting-room/src/App.test.tsx.liquid +57 -0
  360. package/dist/templates/waiting-room/src/App.tsx.liquid +118 -0
  361. package/dist/templates/waiting-room/src/i18n.ts.liquid +44 -0
  362. package/dist/templates/waiting-room/src/main.tsx.liquid +48 -0
  363. package/dist/templates/waiting-room/src/theme.ts.liquid +46 -0
  364. package/dist/templating/resource-metadata.d.ts +131 -0
  365. package/dist/types.d.ts +14 -0
  366. package/dist/utils/atomic-write.d.ts +40 -0
  367. package/dist/utils/cli-error.d.ts +64 -0
  368. package/dist/utils/document-parser-client.d.ts +20 -0
  369. package/dist/utils/env-loader.d.ts +40 -0
  370. package/dist/utils/error-codes.d.ts +335 -0
  371. package/dist/utils/errors.d.ts +19 -0
  372. package/dist/utils/global-settings.d.ts +65 -0
  373. package/dist/utils/llm-client.d.ts +34 -0
  374. package/dist/utils/logger.d.ts +36 -0
  375. package/dist/utils/output.d.ts +109 -0
  376. package/dist/utils/platform.d.ts +14 -0
  377. package/dist/utils/prompt.d.ts +36 -0
  378. package/dist/utils/real-path.d.ts +6 -0
  379. package/dist/utils/schema-properties.d.ts +35 -0
  380. package/dist/validation/ajv.d.ts +17 -0
  381. package/dist/validation/facilitator-guide-validator.d.ts +27 -0
  382. package/dist/validation/findings.d.ts +40 -0
  383. package/dist/validation/game-constraints.d.ts +10 -0
  384. package/dist/validation/game-validator.d.ts +34 -0
  385. package/dist/validation/package-validation.d.ts +41 -0
  386. package/dist/validation/package-validator.d.ts +19 -0
  387. package/dist/validation/resource-validator.d.ts +79 -0
  388. package/dist/validation/schema-defaults-validator.d.ts +29 -0
  389. package/dist/validation/schema-validation.d.ts +115 -0
  390. package/dist/validation/semver.d.ts +9 -0
  391. package/package.json +122 -3
@@ -0,0 +1,307 @@
1
+ import {
2
+ createMockSharedState,
3
+ createMockTeamState,
4
+ renderSessionUserInterface,
5
+ type RenderSessionUserInterfaceOptions,
6
+ } from '@seriousgaming/sdk/ui/testing';
7
+ import { fireEvent, screen, waitFor, within } from '@testing-library/react';
8
+ import { describe, expect, it, vi } from 'vitest';
9
+
10
+ import type { SessionData, Settings, SharedContext, TeamContext } from '../types';
11
+ import App from './App';
12
+ import './i18n';
13
+
14
+ const settings: Settings = {
15
+ menu: {
16
+ Capacity: 120,
17
+ Menu: {
18
+ lemonade: { Name: 'Lemonade', Order: 1, Cost: 15, Price: 50 },
19
+ icedtea: { Name: 'Iced tea', Order: 2, Cost: 20, Price: 60 },
20
+ },
21
+ },
22
+ session: { ShowJoinLinks: true, CurrencySymbol: '£' },
23
+ };
24
+
25
+ const teamContext = (over: Partial<TeamContext> = {}): TeamContext => ({
26
+ plan: { Make: { lemonade: 40, icedtea: 20 } },
27
+ results: {
28
+ Day: 0,
29
+ Weather: 'sunny',
30
+ Lines: {},
31
+ Takings: 0,
32
+ Spent: 0,
33
+ Profit: 0,
34
+ Purse: 0,
35
+ },
36
+ roles: { LooksAfter: {} },
37
+ ...over,
38
+ });
39
+
40
+ /** A team that traded yesterday and has not touched its numbers since. */
41
+ const traded = (made: { lemonade: number; icedtea: number }, plan = made): TeamContext =>
42
+ teamContext({
43
+ plan: { Make: { ...plan } },
44
+ results: {
45
+ Day: 1,
46
+ Weather: 'sunny',
47
+ Lines: {
48
+ lemonade: {
49
+ Made: made.lemonade,
50
+ Wanted: 60,
51
+ Sold: made.lemonade,
52
+ PouredAway: 0,
53
+ TurnedAway: 0,
54
+ },
55
+ icedtea: {
56
+ Made: made.icedtea,
57
+ Wanted: 30,
58
+ Sold: made.icedtea,
59
+ PouredAway: 0,
60
+ TurnedAway: 0,
61
+ },
62
+ },
63
+ Takings: 3200,
64
+ Spent: 1000,
65
+ Profit: 2200,
66
+ Purse: 2200,
67
+ },
68
+ });
69
+
70
+ const sessionMeta = {
71
+ sessionId: 'session-1',
72
+ name: 'Tuesday cohort',
73
+ readOnly: false,
74
+ waitingRoomOpen: false,
75
+ joinPin: null,
76
+ };
77
+
78
+ const links = [
79
+ {
80
+ teamId: 'team-1',
81
+ joinUrl: 'https://example.test/join/one',
82
+ observeUrl: 'https://example.test/observe/one',
83
+ },
84
+ {
85
+ teamId: 'team-2',
86
+ joinUrl: 'https://example.test/join/two',
87
+ observeUrl: 'https://example.test/observe/two',
88
+ },
89
+ ];
90
+
91
+ const shared: SharedContext = { briefing: { Message: '' } };
92
+
93
+ const render = (options: RenderSessionUserInterfaceOptions<SessionData> = {}) =>
94
+ renderSessionUserInterface<SessionData>(<App />, {
95
+ settings,
96
+ sessionMeta,
97
+ teamLinks: links,
98
+ teams: [
99
+ { teamId: 'team-1', index: 0, name: 'Bluebird' },
100
+ { teamId: 'team-2', index: 1, name: 'Kestrel' },
101
+ ],
102
+ ...options,
103
+ // A case names only the half it is about; the other keeps its default.
104
+ sessionData: {
105
+ shared: createMockSharedState<SharedContext>({ context: shared }),
106
+ teams: {
107
+ 'team-1': createMockTeamState<TeamContext>({
108
+ context: teamContext(),
109
+ completedTicks: 0,
110
+ }),
111
+ 'team-2': createMockTeamState<TeamContext>({
112
+ context: teamContext(),
113
+ completedTicks: 2,
114
+ }),
115
+ },
116
+ ...options.sessionData,
117
+ },
118
+ });
119
+
120
+ describe('the stalls table', () => {
121
+ it('says what each stall is making against what it can make', () => {
122
+ render();
123
+
124
+ expect(screen.getByTestId('team-team-1')).toHaveTextContent('60 of 120');
125
+ });
126
+
127
+ it('counts the day each stall is about to trade', () => {
128
+ render();
129
+
130
+ expect(screen.getByTestId('team-team-2')).toHaveTextContent('3');
131
+ });
132
+
133
+ /**
134
+ * There is no 'we are ready' button on a player's screen, and adding one
135
+ * would be a commit step by another name. Comparing today's plan against
136
+ * what was actually made yesterday answers the same question for free.
137
+ */
138
+ it('flags a stall still sitting on yesterday’s numbers', () => {
139
+ render({
140
+ sessionData: {
141
+ teams: {
142
+ 'team-1': createMockTeamState<TeamContext>({
143
+ context: traded({ lemonade: 40, icedtea: 20 }),
144
+ completedTicks: 1,
145
+ }),
146
+ 'team-2': createMockTeamState<TeamContext>({
147
+ context: traded(
148
+ { lemonade: 40, icedtea: 20 },
149
+ { lemonade: 55, icedtea: 20 }
150
+ ),
151
+ completedTicks: 1,
152
+ }),
153
+ },
154
+ },
155
+ });
156
+
157
+ expect(screen.getByTestId('team-team-1')).toHaveTextContent('Same as yesterday');
158
+ expect(screen.getByTestId('team-team-2')).toHaveTextContent('Changed');
159
+ expect(screen.getByText('1 of 2 have changed their numbers today')).toBeInTheDocument();
160
+ });
161
+
162
+ // The stalls open their own days. A facilitator who could run one would end
163
+ // up running it for a group that had not finished deciding.
164
+ it('offers no way to run a stall’s day', () => {
165
+ render();
166
+
167
+ expect(screen.queryByRole('button', { name: /Run/ })).not.toBeInTheDocument();
168
+ });
169
+
170
+ it('will not undo a stall that has not traded', () => {
171
+ render();
172
+
173
+ const row = screen.getByTestId('team-team-1');
174
+ expect(within(row).getByRole('button', { name: 'Undo' })).toBeDisabled();
175
+ });
176
+ });
177
+
178
+ describe('getting into a team', () => {
179
+ /**
180
+ * Both are one click and both open a tab of their own: a session surface
181
+ * owns the whole canvas, so navigating this frame would leave the
182
+ * facilitator with no way back.
183
+ */
184
+ it('offers a way to observe a stall without joining it', () => {
185
+ render();
186
+
187
+ const observe = within(screen.getByTestId('team-team-1')).getByRole('link', {
188
+ name: 'Observe',
189
+ });
190
+
191
+ expect(observe).toHaveAttribute('href', 'https://example.test/observe/one');
192
+ expect(observe).toHaveAttribute('target', '_blank');
193
+ expect(observe).toHaveAttribute('rel', expect.stringContaining('noopener'));
194
+ });
195
+
196
+ it('offers a way to join a stall as a player', () => {
197
+ render();
198
+
199
+ const join = within(screen.getByTestId('team-team-2')).getByRole('link', { name: 'Join' });
200
+
201
+ expect(join).toHaveAttribute('href', 'https://example.test/join/two');
202
+ expect(join).toHaveAttribute('target', '_blank');
203
+ });
204
+
205
+ it('copies a join link, for handing out', async () => {
206
+ const writeText = vi.fn().mockResolvedValue(undefined);
207
+ Object.assign(navigator, { clipboard: { writeText } });
208
+ render();
209
+
210
+ const row = screen.getByTestId('team-team-1');
211
+ fireEvent.click(within(row).getByRole('button', { name: /Copy the join link/ }));
212
+
213
+ await waitFor(() =>
214
+ expect(writeText).toHaveBeenCalledWith('https://example.test/join/one')
215
+ );
216
+ expect(await within(row).findByText('Copied')).toBeInTheDocument();
217
+ });
218
+
219
+ // Permissions Policy is implemented unevenly, so a button that silently
220
+ // does nothing is a real outcome to design for.
221
+ it('shows the link to copy by hand when the clipboard refuses', async () => {
222
+ const writeText = vi.fn().mockRejectedValue(new Error('blocked'));
223
+ Object.assign(navigator, { clipboard: { writeText } });
224
+ render();
225
+
226
+ const row = screen.getByTestId('team-team-1');
227
+ fireEvent.click(within(row).getByRole('button', { name: /Copy the join link/ }));
228
+
229
+ expect(await within(row).findByText('https://example.test/join/one')).toBeInTheDocument();
230
+ });
231
+
232
+ it('offers nothing before the host has delivered the links', () => {
233
+ render({ teamLinks: [] });
234
+
235
+ expect(screen.queryByRole('link', { name: 'Observe' })).not.toBeInTheDocument();
236
+ });
237
+
238
+ it('offers nothing when the game turns the links off', () => {
239
+ render({
240
+ settings: { ...settings, session: { ...settings.session, ShowJoinLinks: false } },
241
+ });
242
+
243
+ expect(screen.queryByRole('link', { name: 'Join' })).not.toBeInTheDocument();
244
+ });
245
+ });
246
+
247
+ describe('the notice', () => {
248
+ it('is a shared write, so it goes to every stall at once', async () => {
249
+ const setData = vi.fn().mockResolvedValue(undefined);
250
+ render({ setData });
251
+
252
+ fireEvent.change(screen.getByLabelText('Message'), {
253
+ target: { value: 'Ten minutes left' },
254
+ });
255
+ fireEvent.click(screen.getByRole('button', { name: 'Send' }));
256
+
257
+ await waitFor(() =>
258
+ expect(setData).toHaveBeenCalledWith({ briefing: { Message: 'Ten minutes left' } })
259
+ );
260
+ });
261
+ });
262
+
263
+ describe('host-owned things', () => {
264
+ it('offers the guide only when the host says it has one', () => {
265
+ render();
266
+
267
+ expect(screen.queryByRole('button', { name: 'Facilitator guide' })).not.toBeInTheDocument();
268
+ });
269
+
270
+ it('asks the host to open the guide', async () => {
271
+ const openHostOverlay = vi.fn().mockResolvedValue(undefined);
272
+ render({ hostOverlays: ['facilitator-guide'], callbacks: { openHostOverlay } });
273
+
274
+ fireEvent.click(screen.getByRole('button', { name: 'Facilitator guide' }));
275
+
276
+ await waitFor(() => expect(openHostOverlay).toHaveBeenCalledWith('facilitator-guide'));
277
+ });
278
+
279
+ it('parks the session through the host rather than a flag of its own', async () => {
280
+ const setWaitingRoom = vi.fn().mockResolvedValue(undefined);
281
+ render({ callbacks: { setWaitingRoom } });
282
+
283
+ fireEvent.click(screen.getByRole('switch', { name: 'Waiting room' }));
284
+
285
+ await waitFor(() => expect(setWaitingRoom).toHaveBeenCalledWith(true));
286
+ });
287
+ });
288
+
289
+ describe('a read-only session', () => {
290
+ it('says why, instead of offering controls the host will refuse', () => {
291
+ render({
292
+ sessionMeta: { ...sessionMeta, readOnly: true },
293
+ sessionData: {
294
+ teams: {
295
+ 'team-1': createMockTeamState<TeamContext>({
296
+ context: teamContext(),
297
+ completedTicks: 1,
298
+ }),
299
+ },
300
+ },
301
+ teams: [{ teamId: 'team-1', index: 0, name: 'Bluebird' }],
302
+ });
303
+
304
+ expect(screen.getByText(/read only/)).toBeInTheDocument();
305
+ expect(screen.getByRole('button', { name: 'Undo' })).toBeDisabled();
306
+ });
307
+ });
@@ -0,0 +1,309 @@
1
+ import {
2
+ Alert,
3
+ AppBar,
4
+ Box,
5
+ Button,
6
+ Chip,
7
+ CssBaseline,
8
+ Snackbar,
9
+ Stack,
10
+ Switch,
11
+ Table,
12
+ TableBody,
13
+ TableCell,
14
+ TableHead,
15
+ TableRow,
16
+ TextField,
17
+ ThemeProvider,
18
+ Toolbar,
19
+ Tooltip,
20
+ Typography,
21
+ } from '@mui/material';
22
+ import {
23
+ useSessionUserInterfaceCallbacks,
24
+ useSessionUserInterfaceData,
25
+ useSessionUserInterfaceHostOverlays,
26
+ useSessionUserInterfaceMeta,
27
+ useSessionUserInterfaceParticipants,
28
+ useSessionUserInterfaceTeamLinks,
29
+ useSessionUserInterfaceTeams,
30
+ } from '@seriousgaming/sdk/ui';
31
+ import { useState } from 'react';
32
+ import { useTranslation } from 'react-i18next';
33
+
34
+ import type { Settings, SessionData, SharedWrites, TeamContext, TeamWrites } from '../types';
35
+ import { money } from './format';
36
+ import Section from './Section';
37
+ import TeamDoor from './TeamDoor';
38
+ import { useAppTheme } from './theme';
39
+
40
+ /**
41
+ * The facilitator's whole screen.
42
+ *
43
+ * A game that ships a session surface replaces the host's roster page outright,
44
+ * so everything a facilitator needs during play has to be here: what each stall
45
+ * is making, whether anyone has touched it today, and a way into any stall's
46
+ * screen. The stalls run their own days, so this watches rather than drives,
47
+ * and the two host-owned things it cannot draw itself, the guide and the
48
+ * waiting room, it asks the host for.
49
+ */
50
+ function Shell() {
51
+ const { sessionData, settings, setData } = useSessionUserInterfaceData<
52
+ SessionData,
53
+ Settings,
54
+ SharedWrites,
55
+ TeamWrites
56
+ >();
57
+ const teams = useSessionUserInterfaceTeams();
58
+ const participants = useSessionUserInterfaceParticipants();
59
+ const links = useSessionUserInterfaceTeamLinks();
60
+ const meta = useSessionUserInterfaceMeta();
61
+ const overlays = useSessionUserInterfaceHostOverlays();
62
+ const { undoTick, openHostOverlay, setWaitingRoom } = useSessionUserInterfaceCallbacks();
63
+ const { t } = useTranslation();
64
+
65
+ const [refusal, setRefusal] = useState<string | null>(null);
66
+ const [briefingDraft, setBriefingDraft] = useState<string | null>(null);
67
+
68
+ // One value for the session, delivered once, whether or not a team exists.
69
+ const briefing = sessionData.shared.context.briefing.Message;
70
+
71
+ // A denial is terminal, so report it rather than retrying. The alternative
72
+ // is a control that silently does nothing, which is worse than an error.
73
+ const attempt = (action: () => Promise<unknown>) => {
74
+ void action().catch((error: unknown) =>
75
+ setRefusal(error instanceof Error ? error.message : String(error))
76
+ );
77
+ };
78
+
79
+ /**
80
+ * Whether a stall is still sitting on the numbers it traded yesterday.
81
+ *
82
+ * There is nothing to press on a player's screen to say 'we are ready', and
83
+ * adding one would be a commit step by another name. Comparing today's plan
84
+ * against what was actually made yesterday costs no extra state and answers
85
+ * the only question a facilitator has before ticking.
86
+ */
87
+ const untouched = (team: TeamContext | undefined) => {
88
+ if (!team) return false;
89
+ const lines = Object.entries(team.results.Lines);
90
+ if (lines.length === 0) return false;
91
+ return lines.every(([id, line]) => (team.plan.Make[id] ?? 0) === line.Made);
92
+ };
93
+
94
+ const movedOn = teams.filter(
95
+ (team) => !untouched(sessionData.teams[team.teamId]?.context)
96
+ ).length;
97
+
98
+ return (
99
+ <Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
100
+ <AppBar position="static" color="default" elevation={0}>
101
+ <Toolbar sx={{ gap: 2 }}>
102
+ <Typography variant="h6" component="h1" sx={{ flexGrow: 1 }} noWrap>
103
+ {meta.name}
104
+ </Typography>
105
+
106
+ <Tooltip title={t('session.waitingRoomHint')}>
107
+ <Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
108
+ <Typography variant="body2">{t('session.waitingRoom')}</Typography>
109
+ <Switch
110
+ checked={meta.waitingRoomOpen}
111
+ disabled={meta.readOnly}
112
+ slotProps={{ input: { 'aria-label': t('session.waitingRoom') } }}
113
+ onChange={(event) =>
114
+ attempt(() => setWaitingRoom(event.target.checked))
115
+ }
116
+ />
117
+ </Stack>
118
+ </Tooltip>
119
+
120
+ {/* Only for an overlay this host actually offers: a button
121
+ for one it does not have is a button that cannot work. */}
122
+ {overlays.includes('facilitator-guide') && (
123
+ <Button
124
+ variant="outlined"
125
+ onClick={() => attempt(() => openHostOverlay('facilitator-guide'))}
126
+ >
127
+ {t('session.guide')}
128
+ </Button>
129
+ )}
130
+ </Toolbar>
131
+ </AppBar>
132
+
133
+ <Box sx={{ flex: 1, minHeight: 0, overflowY: 'auto', py: 3 }}>
134
+ <Stack spacing={3} sx={{ maxWidth: 1200, mx: 'auto', px: 2 }}>
135
+ {meta.readOnly && <Alert severity="warning">{t('session.readOnly')}</Alert>}
136
+
137
+ <Section
138
+ title={t('session.stalls')}
139
+ hint={t('session.movedOn', { movedOn, total: teams.length })}
140
+ >
141
+ <Box sx={{ overflowX: 'auto' }}>
142
+ <Table size="small">
143
+ <TableHead>
144
+ <TableRow>
145
+ <TableCell>{t('session.team')}</TableCell>
146
+ <TableCell align="right">{t('session.people')}</TableCell>
147
+ <TableCell align="right">{t('session.day')}</TableCell>
148
+ <TableCell align="right">{t('session.making')}</TableCell>
149
+ <TableCell>{t('session.status')}</TableCell>
150
+ <TableCell align="right">{t('session.purse')}</TableCell>
151
+ <TableCell>{t('session.wayIn')}</TableCell>
152
+ <TableCell align="right">{t('session.controls')}</TableCell>
153
+ </TableRow>
154
+ </TableHead>
155
+ <TableBody>
156
+ {teams.map((team) => {
157
+ const state = sessionData.teams[team.teamId];
158
+ const context = state?.context;
159
+ const planned = Object.values(
160
+ context?.plan.Make ?? {}
161
+ ).reduce((sum, value) => sum + value, 0);
162
+ const size = participants.filter(
163
+ (person) => person.teamId === team.teamId
164
+ ).length;
165
+ const stale = untouched(context);
166
+
167
+ return (
168
+ <TableRow
169
+ key={team.teamId}
170
+ data-testid={`team-${team.teamId}`}
171
+ >
172
+ <TableCell>{team.name}</TableCell>
173
+ <TableCell align="right">{size}</TableCell>
174
+ <TableCell align="right">
175
+ {(state?.completedTicks ?? 0) + 1}
176
+ </TableCell>
177
+ <TableCell
178
+ align="right"
179
+ sx={{ fontVariantNumeric: 'tabular-nums' }}
180
+ >
181
+ {t('session.ofCapacity', {
182
+ planned,
183
+ capacity: settings.menu.Capacity,
184
+ })}
185
+ </TableCell>
186
+ <TableCell>
187
+ {stale ? (
188
+ <Chip
189
+ size="small"
190
+ variant="outlined"
191
+ label={t('session.unchanged')}
192
+ />
193
+ ) : (
194
+ <Chip
195
+ size="small"
196
+ color="success"
197
+ label={t('session.changed')}
198
+ />
199
+ )}
200
+ </TableCell>
201
+ <TableCell align="right">
202
+ {money(
203
+ settings.session.CurrencySymbol,
204
+ context?.results.Purse ?? 0
205
+ )}
206
+ </TableCell>
207
+ <TableCell>
208
+ <TeamDoor
209
+ links={links.find(
210
+ (link) => link.teamId === team.teamId
211
+ )}
212
+ teamName={team.name}
213
+ show={settings.session.ShowJoinLinks}
214
+ />
215
+ </TableCell>
216
+ <TableCell align="right">
217
+ {/* No way to run a day from
218
+ here. The stalls open
219
+ themselves, and a
220
+ facilitator who could
221
+ run the day would end up
222
+ doing it for a group
223
+ still deciding. Undo
224
+ stays: taking a day back
225
+ is a correction, and
226
+ nobody else can make it. */}
227
+ <Button
228
+ size="small"
229
+ color="inherit"
230
+ disabled={
231
+ meta.readOnly ||
232
+ (state?.completedTicks ?? 0) === 0
233
+ }
234
+ onClick={() =>
235
+ attempt(() => undoTick(team.teamId))
236
+ }
237
+ >
238
+ {t('session.undo')}
239
+ </Button>
240
+ </TableCell>
241
+ </TableRow>
242
+ );
243
+ })}
244
+ </TableBody>
245
+ </Table>
246
+ </Box>
247
+ </Section>
248
+
249
+ <Section title={t('session.notice')} hint={t('session.noticeHint')}>
250
+ <Stack direction="row" spacing={1} sx={{ alignItems: 'flex-start' }}>
251
+ <TextField
252
+ fullWidth
253
+ multiline
254
+ minRows={2}
255
+ size="small"
256
+ label={t('session.noticeLabel')}
257
+ value={briefingDraft ?? briefing}
258
+ disabled={meta.readOnly}
259
+ onChange={(event) => setBriefingDraft(event.target.value)}
260
+ />
261
+ <Button
262
+ variant="contained"
263
+ disabled={meta.readOnly || briefingDraft === null}
264
+ onClick={() => {
265
+ // The one-argument form is a shared write:
266
+ // one value for the whole session, applied
267
+ // to every team at once. Only a session
268
+ // surface may make one, which is what makes
269
+ // a shared capability safe in a game whose
270
+ // teams tick on their own clocks.
271
+ attempt(async () => {
272
+ await setData({
273
+ briefing: { Message: briefingDraft ?? '' },
274
+ });
275
+ setBriefingDraft(null);
276
+ });
277
+ }}
278
+ >
279
+ {t('session.send')}
280
+ </Button>
281
+ </Stack>
282
+ </Section>
283
+ </Stack>
284
+ </Box>
285
+
286
+ <Snackbar
287
+ open={refusal !== null}
288
+ autoHideDuration={8000}
289
+ onClose={() => setRefusal(null)}
290
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
291
+ >
292
+ <Alert severity="error" onClose={() => setRefusal(null)}>
293
+ {refusal}
294
+ </Alert>
295
+ </Snackbar>
296
+ </Box>
297
+ );
298
+ }
299
+
300
+ export default function App() {
301
+ const theme = useAppTheme();
302
+
303
+ return (
304
+ <ThemeProvider theme={theme}>
305
+ <CssBaseline enableColorScheme />
306
+ <Shell />
307
+ </ThemeProvider>
308
+ );
309
+ }
@@ -0,0 +1,37 @@
1
+ import { Box, Card, CardContent, Stack, Typography } from '@mui/material';
2
+ import type { ReactNode } from 'react';
3
+
4
+ type Props = {
5
+ title: string;
6
+ hint?: string;
7
+ action?: ReactNode;
8
+ children: ReactNode;
9
+ };
10
+
11
+ /** One card, one heading, one job. Every panel on this surface is built of these. */
12
+ export default function Section({ title, hint, action, children }: Props) {
13
+ return (
14
+ <Card variant="outlined">
15
+ <CardContent>
16
+ <Stack
17
+ direction="row"
18
+ spacing={2}
19
+ sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}
20
+ >
21
+ <Box>
22
+ <Typography variant="h6" component="h2">
23
+ {title}
24
+ </Typography>
25
+ {hint && (
26
+ <Typography variant="body2" color="text.secondary">
27
+ {hint}
28
+ </Typography>
29
+ )}
30
+ </Box>
31
+ {action}
32
+ </Stack>
33
+ <Box sx={{ mt: 2 }}>{children}</Box>
34
+ </CardContent>
35
+ </Card>
36
+ );
37
+ }