@jxsuite/studio 2.0.1 → 2.1.0

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 (419) hide show
  1. package/canvas.html +21 -0
  2. package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
  3. package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
  4. package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
  5. package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
  6. package/dist/chunks/{components-799jpvhm.js → components-2bpqp4mq.js} +5 -7
  7. package/dist/chunks/{components-799jpvhm.js.map → components-2bpqp4mq.js.map} +1 -1
  8. package/dist/chunks/content-source-34gnkbnn.js +38 -0
  9. package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
  10. package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
  11. package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
  12. package/dist/chunks/extension-sections-6mqydz35.js +301 -0
  13. package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
  14. package/dist/chunks/files-8kn321h1.js +70 -0
  15. package/dist/chunks/files-8kn321h1.js.map +9 -0
  16. package/dist/chunks/{format-host-8jxwts2p.js → format-host-mkwsmt8d.js} +3 -3
  17. package/dist/chunks/git-panel-tjbr191d.js +51 -0
  18. package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
  19. package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
  20. package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
  21. package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
  22. package/dist/chunks/layers-panel-87pb25km.js +58 -0
  23. package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
  24. package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
  25. package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
  26. package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
  27. package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
  28. package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
  29. package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
  30. package/dist/chunks/right-panel-zpdxd98t.js +37 -0
  31. package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
  32. package/dist/chunks/section-registry-zmckaxf4.js +30 -0
  33. package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
  34. package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
  35. package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
  36. package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
  37. package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
  38. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
  39. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
  40. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
  41. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
  42. package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
  43. package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
  44. package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
  45. package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
  46. package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
  47. package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
  48. package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
  49. package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
  50. package/dist/chunks/studio-41exgzhq.js +6690 -0
  51. package/dist/chunks/studio-41exgzhq.js.map +38 -0
  52. package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
  53. package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
  54. package/dist/chunks/studio-50k77cca.js +1787 -0
  55. package/dist/chunks/studio-50k77cca.js.map +18 -0
  56. package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
  57. package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
  58. package/dist/chunks/studio-6f0am8f2.js +53 -0
  59. package/dist/chunks/studio-6f0am8f2.js.map +10 -0
  60. package/dist/chunks/studio-6nmjc5qk.js +7002 -0
  61. package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
  62. package/dist/chunks/studio-7afd21nw.js +67 -0
  63. package/dist/chunks/studio-7afd21nw.js.map +10 -0
  64. package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
  65. package/dist/chunks/studio-7bmdvzne.js.map +10 -0
  66. package/dist/chunks/studio-8jpzqk5s.js +128 -0
  67. package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
  68. package/dist/chunks/studio-94m77tgk.js +1482 -0
  69. package/dist/chunks/studio-94m77tgk.js.map +13 -0
  70. package/dist/chunks/studio-9je6w5sq.js +1000 -0
  71. package/dist/chunks/studio-9je6w5sq.js.map +16 -0
  72. package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
  73. package/dist/chunks/{studio-gs6kqdt1.js → studio-adn0z1k3.js} +14 -8
  74. package/dist/chunks/{studio-gs6kqdt1.js.map → studio-adn0z1k3.js.map} +3 -3
  75. package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
  76. package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
  77. package/dist/chunks/studio-bw5dz6km.js +608 -0
  78. package/dist/chunks/studio-bw5dz6km.js.map +21 -0
  79. package/dist/chunks/studio-c6s5getc.js +4232 -0
  80. package/dist/chunks/studio-c6s5getc.js.map +39 -0
  81. package/dist/chunks/studio-cjm7fdjf.js +451 -0
  82. package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
  83. package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
  84. package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
  85. package/dist/chunks/studio-dg7zyt5x.js +28 -0
  86. package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
  87. package/dist/chunks/studio-dzcyvvvz.js +166 -0
  88. package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
  89. package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
  90. package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
  91. package/dist/chunks/studio-f12h6177.js +411 -0
  92. package/dist/chunks/studio-f12h6177.js.map +13 -0
  93. package/dist/chunks/studio-hbp1s8m9.js +1817 -0
  94. package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
  95. package/dist/chunks/studio-k2sxjph5.js +15295 -0
  96. package/dist/chunks/studio-k2sxjph5.js.map +235 -0
  97. package/dist/chunks/studio-myje51t3.js +385 -0
  98. package/dist/chunks/studio-myje51t3.js.map +12 -0
  99. package/dist/chunks/studio-na6rwzhw.js +353 -0
  100. package/dist/chunks/studio-na6rwzhw.js.map +11 -0
  101. package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
  102. package/dist/chunks/studio-p471gg5n.js.map +14 -0
  103. package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
  104. package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
  105. package/dist/chunks/{studio-esrewxap.js → studio-s519zc99.js} +3 -3
  106. package/dist/chunks/studio-sas7mt3k.js +75 -0
  107. package/dist/chunks/studio-sas7mt3k.js.map +10 -0
  108. package/dist/chunks/studio-t5wqwzdh.js +74 -0
  109. package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
  110. package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
  111. package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
  112. package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
  113. package/dist/chunks/studio-tr20m8g8.js.map +9 -0
  114. package/dist/chunks/{studio-v7jkjqng.js → studio-v96qfnww.js} +5514 -4291
  115. package/dist/chunks/studio-v96qfnww.js.map +54 -0
  116. package/dist/chunks/studio-vcetc91k.js +659 -0
  117. package/dist/chunks/studio-vcetc91k.js.map +10 -0
  118. package/dist/chunks/{studio-jm0z592w.js → studio-vmymjefp.js} +2972 -4937
  119. package/dist/chunks/studio-vmymjefp.js.map +98 -0
  120. package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
  121. package/dist/chunks/studio-x9hmy5kr.js +390 -0
  122. package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
  123. package/dist/chunks/studio-xa0ce0n7.js +335 -0
  124. package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
  125. package/dist/chunks/studio-xm3dbxvn.js +378 -0
  126. package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
  127. package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
  128. package/dist/chunks/studio-zv37yrfa.js +1054 -0
  129. package/dist/chunks/studio-zv37yrfa.js.map +13 -0
  130. package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
  131. package/dist/chunks/ws-client-654ts66j.js.map +11 -0
  132. package/dist/iframe-entry.js +668 -230
  133. package/dist/studio.js +21080 -32585
  134. package/index.html +35 -4685
  135. package/package.json +17 -15
  136. package/src/about/about-modal.ts +42 -11
  137. package/src/browse/library-commands.ts +240 -0
  138. package/src/browse/library-layouts.ts +369 -0
  139. package/src/browse/library-model.ts +396 -0
  140. package/src/browse/library-pane.ts +1049 -0
  141. package/src/browse/library-preview.ts +252 -0
  142. package/src/browse/library-source.ts +150 -0
  143. package/src/canvas/canvas-helpers.ts +33 -9
  144. package/src/canvas/canvas-live-render.ts +45 -29
  145. package/src/canvas/canvas-patcher.ts +151 -31
  146. package/src/canvas/canvas-perf.ts +46 -7
  147. package/src/canvas/canvas-render.ts +1092 -220
  148. package/src/canvas/canvas-surface.ts +240 -0
  149. package/src/canvas/canvas-utils.ts +1166 -124
  150. package/src/canvas/iframe-drop.ts +2 -1
  151. package/src/canvas/iframe-entry.ts +241 -4
  152. package/src/canvas/iframe-host.ts +1226 -143
  153. package/src/canvas/iframe-inline-edit.ts +31 -0
  154. package/src/canvas/iframe-interaction.ts +76 -16
  155. package/src/canvas/iframe-keys.ts +107 -45
  156. package/src/canvas/iframe-overlay.ts +38 -1
  157. package/src/canvas/iframe-protocol.ts +137 -2
  158. package/src/canvas/iframe-render.ts +70 -2
  159. package/src/canvas/iframe-slash.ts +6 -0
  160. package/src/canvas/path-mapping.ts +56 -6
  161. package/src/canvas/surface-registry.ts +292 -0
  162. package/src/collab/collab-commands.ts +244 -0
  163. package/src/collab/collab-session.ts +198 -32
  164. package/src/collab/collab-state.ts +28 -2
  165. package/src/collab/presence-chips.ts +100 -9
  166. package/src/commands/active-registry.ts +42 -0
  167. package/src/commands/app-commands.ts +176 -0
  168. package/src/commands/budget.ts +158 -0
  169. package/src/commands/command-args.ts +253 -0
  170. package/src/commands/context.ts +387 -0
  171. package/src/commands/defaults.ts +580 -0
  172. package/src/commands/keymap.ts +571 -0
  173. package/src/commands/levels.ts +345 -0
  174. package/src/commands/live-context.ts +193 -0
  175. package/src/commands/reference.ts +227 -0
  176. package/src/commands/registry.ts +293 -0
  177. package/src/component-props.ts +3 -2
  178. package/src/content/draft-state.ts +106 -0
  179. package/src/content/entry-commands.ts +282 -0
  180. package/src/content/entry-editor.ts +327 -0
  181. package/src/content/entry-fields.ts +116 -0
  182. package/src/content/entry-model.ts +175 -0
  183. package/src/editor/canvas-context-menu.ts +17 -11
  184. package/src/editor/canvas-slash-bridge.ts +11 -4
  185. package/src/editor/context-menu.ts +804 -204
  186. package/src/editor/convert-to-component.ts +31 -10
  187. package/src/editor/convert-to-repeater.ts +3 -2
  188. package/src/editor/inline-edit-apply.ts +5 -5
  189. package/src/editor/inline-edit.ts +86 -50
  190. package/src/editor/insert-zone-action.ts +1 -1
  191. package/src/editor/shortcuts.ts +1043 -329
  192. package/src/editor/slash-menu.ts +1 -0
  193. package/src/files/elements.ts +166 -0
  194. package/src/files/file-ops.ts +178 -31
  195. package/src/files/files.ts +898 -268
  196. package/src/files/fs-events.ts +21 -0
  197. package/src/files/media-meta.ts +203 -0
  198. package/src/files/media-paths.ts +141 -0
  199. package/src/files/media-upload.ts +7 -3
  200. package/src/files/media-usage.ts +189 -0
  201. package/src/github/github-auth.ts +235 -33
  202. package/src/github/github-publish.ts +80 -21
  203. package/src/grid/grid-controller.ts +152 -12
  204. package/src/grid/grid-layout.ts +225 -20
  205. package/src/grid/grid-open.ts +120 -0
  206. package/src/grid/grid-panel.ts +554 -39
  207. package/src/grid/grid-source.ts +27 -1
  208. package/src/grid/redirects-grid.ts +599 -0
  209. package/src/grid/redirects.ts +398 -0
  210. package/src/grid/sources/content-source.ts +108 -2
  211. package/src/i18n/i18n-commands.ts +322 -0
  212. package/src/new-project/add-repo-modal.ts +8 -10
  213. package/src/new-project/import-tab.ts +7 -0
  214. package/src/new-project/location-fields.ts +9 -1
  215. package/src/new-project/new-project-modal.ts +221 -245
  216. package/src/packages/jxsuite-update.ts +97 -48
  217. package/src/packages/pull-package-sync.ts +18 -9
  218. package/src/page-params.ts +2 -1
  219. package/src/panels/activity-bar.ts +152 -91
  220. package/src/panels/activity-panel.ts +481 -0
  221. package/src/panels/ai-chat/chat-markdown.ts +8 -3
  222. package/src/panels/ai-chat/chat-view.ts +245 -18
  223. package/src/panels/ai-chat/composer.ts +31 -5
  224. package/src/panels/ai-chat/sessions-view.ts +17 -10
  225. package/src/panels/ai-panel.ts +279 -53
  226. package/src/panels/block-action-bar.ts +1115 -224
  227. package/src/panels/bottom-dock.ts +337 -0
  228. package/src/panels/canvas-dnd-bridge.ts +37 -8
  229. package/src/panels/chat-panel.ts +29 -12
  230. package/src/panels/data-explorer.ts +254 -112
  231. package/src/panels/data-grid.ts +10 -4
  232. package/src/panels/dnd.ts +28 -51
  233. package/src/panels/editors.ts +434 -107
  234. package/src/panels/elements-panel.ts +66 -2
  235. package/src/panels/empty-state.ts +112 -0
  236. package/src/panels/events-panel.ts +596 -82
  237. package/src/panels/formula-workspace.ts +408 -218
  238. package/src/panels/frontmatter-fields.ts +51 -10
  239. package/src/panels/frontmatter-panel.ts +357 -79
  240. package/src/panels/git-panel.ts +375 -164
  241. package/src/panels/head-panel.ts +791 -85
  242. package/src/panels/i18n-panel.ts +592 -0
  243. package/src/panels/imports-panel.ts +95 -85
  244. package/src/panels/jump-bar.ts +584 -0
  245. package/src/panels/layers-panel.ts +1106 -328
  246. package/src/panels/left-panel.ts +109 -204
  247. package/src/panels/navigator-panels.ts +131 -0
  248. package/src/panels/overlays.ts +7 -2
  249. package/src/panels/pane-context.ts +1477 -0
  250. package/src/panels/pane-grid.ts +406 -0
  251. package/src/panels/panel-registry.ts +340 -0
  252. package/src/panels/panel-scheduler.ts +75 -0
  253. package/src/panels/problems-panel.ts +234 -0
  254. package/src/panels/properties-panel.ts +946 -1139
  255. package/src/panels/provenance.ts +258 -0
  256. package/src/panels/quick-search.ts +791 -122
  257. package/src/panels/right-panel.ts +228 -102
  258. package/src/panels/seo-modal.ts +395 -0
  259. package/src/panels/settings-pane.ts +203 -0
  260. package/src/panels/signals-panel.ts +354 -137
  261. package/src/panels/statement-editor.ts +41 -25
  262. package/src/panels/statusbar.ts +342 -106
  263. package/src/panels/style-panel.ts +974 -463
  264. package/src/panels/style-utils.ts +3 -3
  265. package/src/panels/stylebook-doc.ts +4 -2
  266. package/src/panels/stylebook-layers-panel.ts +3 -2
  267. package/src/panels/stylebook-panel.ts +51 -26
  268. package/src/panels/tab-strip.ts +1058 -78
  269. package/src/panels/target-line.ts +274 -0
  270. package/src/panels/toolbar.ts +631 -484
  271. package/src/panels/welcome-screen.ts +301 -198
  272. package/src/platform-errors.ts +26 -3
  273. package/src/platform.ts +96 -2
  274. package/src/platforms/cloud.ts +71 -8
  275. package/src/platforms/devserver.ts +92 -29
  276. package/src/publish/deploy-checklist.ts +383 -0
  277. package/src/publish/pages-service.ts +10 -1
  278. package/src/publish/publish-commands.ts +218 -0
  279. package/src/publish/publish-panel.ts +110 -16
  280. package/src/recent-projects.ts +3 -2
  281. package/src/services/a11y-report.ts +505 -0
  282. package/src/services/ai-models.ts +30 -1
  283. package/src/services/ai-project-tools.ts +29 -10
  284. package/src/services/ai-system-prompt.ts +53 -14
  285. package/src/services/ai-tools.ts +39 -3
  286. package/src/services/ai-writes.ts +154 -0
  287. package/src/services/announce.ts +97 -0
  288. package/src/services/automation.ts +424 -175
  289. package/src/services/cem-export.ts +9 -4
  290. package/src/services/clock.ts +61 -0
  291. package/src/services/document-assistant.ts +36 -8
  292. package/src/services/idle.ts +179 -0
  293. package/src/services/import-client.ts +26 -0
  294. package/src/services/monaco-buffer.ts +627 -0
  295. package/src/services/monaco-lazy.ts +36 -0
  296. package/src/services/notify.ts +271 -0
  297. package/src/services/profile.ts +155 -0
  298. package/src/services/references.ts +0 -0
  299. package/src/services/token-lint.ts +3 -2
  300. package/src/services/tool-executor.ts +53 -7
  301. package/src/services/trusted-types.ts +123 -0
  302. package/src/settings/contexts-section.ts +427 -0
  303. package/src/settings/contributed-section.ts +149 -11
  304. package/src/settings/css-vars-editor.ts +306 -349
  305. package/src/settings/defs-editor.ts +93 -34
  306. package/src/settings/dependencies-editor.ts +7 -7
  307. package/src/settings/extension-sections.ts +37 -5
  308. package/src/settings/general-settings.ts +204 -121
  309. package/src/settings/locales-section.ts +347 -0
  310. package/src/settings/preferences-accounts.ts +140 -0
  311. package/src/settings/preferences-dialog.ts +620 -0
  312. package/src/settings/preferences-keymap.ts +270 -0
  313. package/src/settings/project-sections.ts +260 -0
  314. package/src/settings/section-registry.ts +169 -0
  315. package/src/settings/settings-document.ts +385 -0
  316. package/src/shell.ts +1549 -0
  317. package/src/site-context.ts +45 -20
  318. package/src/state.ts +14 -275
  319. package/src/store.ts +88 -43
  320. package/src/studio.ts +790 -346
  321. package/src/style/live-preview.ts +36 -0
  322. package/src/style/project-styles.ts +349 -0
  323. package/src/style/token-ref.ts +123 -0
  324. package/src/tabs/patch-ops.ts +16 -4
  325. package/src/tabs/project-config.ts +416 -0
  326. package/src/tabs/selection.ts +323 -0
  327. package/src/tabs/tab.ts +182 -47
  328. package/src/tabs/transact.ts +209 -28
  329. package/src/types.ts +68 -13
  330. package/src/ui/ai-credentials-form.ts +39 -35
  331. package/src/ui/ai-managed-connect.ts +7 -1
  332. package/src/ui/dynamic-slot.ts +224 -81
  333. package/src/ui/expression-editor.ts +71 -63
  334. package/src/ui/field-input.ts +4 -6
  335. package/src/ui/field-row.ts +215 -38
  336. package/src/ui/form-controls.ts +194 -7
  337. package/src/ui/layers.ts +492 -69
  338. package/src/ui/media-picker.ts +115 -25
  339. package/src/ui/panel-resize.ts +109 -91
  340. package/src/ui/progress-modal.ts +131 -50
  341. package/src/ui/regions.ts +462 -0
  342. package/src/ui/schema-form.ts +229 -45
  343. package/src/ui/spectrum.ts +24 -8
  344. package/src/ui/timing.ts +49 -0
  345. package/src/ui/value-selector.ts +20 -0
  346. package/src/ui/value-source.ts +348 -0
  347. package/src/ui/virtual-window.ts +400 -0
  348. package/src/utils/edit-display.ts +3 -2
  349. package/src/utils/inherited-style.ts +67 -37
  350. package/src/view.ts +64 -78
  351. package/src/workspace/pane-derive.ts +1258 -0
  352. package/src/workspace/session.ts +317 -0
  353. package/src/workspace/workspace.ts +1345 -23
  354. package/styles/canvas.css +653 -0
  355. package/styles/forced-colors.css +106 -0
  356. package/styles/inspector.css +1454 -0
  357. package/styles/overlays.css +2704 -0
  358. package/styles/panels.css +1701 -0
  359. package/styles/shell.css +1423 -0
  360. package/styles/tokens.css +151 -0
  361. package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
  362. package/dist/chunks/extension-sections-qr3drkkw.js +0 -531
  363. package/dist/chunks/extension-sections-qr3drkkw.js.map +0 -12
  364. package/dist/chunks/files-j8a1t06k.js +0 -50
  365. package/dist/chunks/files-j8a1t06k.js.map +0 -9
  366. package/dist/chunks/git-panel-xtq5z81x.js +0 -28
  367. package/dist/chunks/git-panel-xtq5z81x.js.map +0 -9
  368. package/dist/chunks/layers-panel-ancn2wpq.js +0 -26
  369. package/dist/chunks/layers-panel-ancn2wpq.js.map +0 -9
  370. package/dist/chunks/settings-modal-17ttr96g.js +0 -29
  371. package/dist/chunks/settings-modal-17ttr96g.js.map +0 -9
  372. package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
  373. package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
  374. package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
  375. package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
  376. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
  377. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
  378. package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
  379. package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
  380. package/dist/chunks/studio-0xgs7xrs.js +0 -429
  381. package/dist/chunks/studio-0xgs7xrs.js.map +0 -13
  382. package/dist/chunks/studio-4d8hzcz5.js +0 -3427
  383. package/dist/chunks/studio-4d8hzcz5.js.map +0 -26
  384. package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
  385. package/dist/chunks/studio-9smfzqhc.js +0 -19
  386. package/dist/chunks/studio-9smfzqhc.js.map +0 -10
  387. package/dist/chunks/studio-jm0z592w.js.map +0 -115
  388. package/dist/chunks/studio-m2t4mdg5.js +0 -1854
  389. package/dist/chunks/studio-m2t4mdg5.js.map +0 -18
  390. package/dist/chunks/studio-n4p11dfy.js +0 -853
  391. package/dist/chunks/studio-n4p11dfy.js.map +0 -12
  392. package/dist/chunks/studio-qkvt37c0.js.map +0 -9
  393. package/dist/chunks/studio-sjh1wt1a.js +0 -3744
  394. package/dist/chunks/studio-sjh1wt1a.js.map +0 -22
  395. package/dist/chunks/studio-v7jkjqng.js.map +0 -46
  396. package/dist/chunks/studio-xq0t7n5t.js +0 -140
  397. package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
  398. package/dist/chunks/studio-xze4zrs1.js.map +0 -10
  399. package/dist/chunks/studio-yfcstdzy.js +0 -399
  400. package/dist/chunks/studio-yfcstdzy.js.map +0 -12
  401. package/dist/chunks/studio-zp7jncs8.js +0 -469
  402. package/dist/chunks/studio-zp7jncs8.js.map +0 -14
  403. package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
  404. package/src/browse/browse-modal.ts +0 -69
  405. package/src/browse/browse.ts +0 -904
  406. package/src/canvas/canvas-diff.ts +0 -216
  407. package/src/canvas/nested-site-style.ts +0 -58
  408. package/src/new-project/design-fields.ts +0 -264
  409. package/src/new-project/templates.ts +0 -61
  410. package/src/panels/tab-bar.ts +0 -548
  411. package/src/settings/settings-modal.ts +0 -203
  412. /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
  413. /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
  414. /package/dist/chunks/{format-host-8jxwts2p.js.map → format-host-mkwsmt8d.js.map} +0 -0
  415. /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
  416. /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
  417. /package/dist/chunks/{studio-esrewxap.js.map → studio-s519zc99.js.map} +0 -0
  418. /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
  419. /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
@@ -0,0 +1,571 @@
1
+ /**
2
+ * Keymap.ts — chords in, command ids out.
3
+ *
4
+ * Three jobs, deliberately in one file because they must agree on one canonical chord string:
5
+ *
6
+ * 1. **Normalisation.** `"Cmd+Shift+P"`, `"meta+shift+p"` and `"mod+shift+P"` are the same chord.
7
+ * Everything is stored as `"mod+shift+p"` — modifiers in a fixed order, key lowercase.
8
+ * 2. **Formatting.** ONE function turns a chord into what the user reads: `⌘⇧P` on mac, `Ctrl+Shift+P`
9
+ * everywhere else. Today `toolbar.ts:301` and `:555` hardcode `⌘P` and show it to Windows and
10
+ * Linux users; `formatChord` is what kills that everywhere at once.
11
+ * 3. **Resolution through a scope stack.** `caret > grid/code engine > focused dock > global`. A chord
12
+ * bound in a narrower scope shadows the same chord in a wider one — that is the mechanism, not
13
+ * an accident, and it is why `keyScope` exists as a field separate from `level`.
14
+ * {@link overlappingScopes} is the same fact read the other way round: which scopes can be live
15
+ * at one instant, and therefore which pairs cannot both answer one chord.
16
+ *
17
+ * Conflicts fail loudly at registration: two commands claiming one chord in ONE scope throws. That
18
+ * is the class of bug that let ⌘W (`editor/shortcuts.ts:192`, refuses to close the last tab) and
19
+ * the tab strip's × (`panels/tab-strip.ts:182`, closes it happily) disagree for a year.
20
+ *
21
+ * 4. **The user's layer.** A rebinding is an OVERRIDE (`setOverrides`), never an edit to a record:
22
+ * `declaredFor(id)` keeps answering what the registry declared, which is what a reset restores
23
+ * and what `specs/studio.md` §15 rule 2 means by the Keyboard sheet being a projection. The two
24
+ * layers meet by one rule — **an override outranks a default, and a default never evicts an
25
+ * override** — so the outcome does not depend on registration order, and a stored override that
26
+ * a later release's new default happens to collide with cannot throw the bootstrap. Two defaults
27
+ * colliding still throws, because that one is a bug in the app and nobody's preference.
28
+ */
29
+
30
+ import { KEY_SCOPES } from "./levels";
31
+ import type { KeyScope } from "./levels";
32
+
33
+ /** Canonical modifier order. `mod` is ⌘ on mac and Ctrl elsewhere — one token, one meaning. */
34
+ const MODIFIER_ORDER = ["mod", "ctrl", "alt", "shift"] as const;
35
+
36
+ type Modifier = (typeof MODIFIER_ORDER)[number];
37
+
38
+ /** Spellings people actually write, mapped onto the canonical four. */
39
+ const MODIFIER_ALIASES: Readonly<Record<string, Modifier>> = {
40
+ mod: "mod",
41
+ cmd: "mod",
42
+ command: "mod",
43
+ meta: "mod",
44
+ super: "mod",
45
+ ctrl: "ctrl",
46
+ control: "ctrl",
47
+ alt: "alt",
48
+ opt: "alt",
49
+ option: "alt",
50
+ shift: "shift",
51
+ };
52
+
53
+ /** Key aliases — arrow/space spellings and the `KeyboardEvent.key` values that differ. */
54
+ const KEY_ALIASES: Readonly<Record<string, string>> = {
55
+ up: "arrowup",
56
+ down: "arrowdown",
57
+ left: "arrowleft",
58
+ right: "arrowright",
59
+ esc: "escape",
60
+ space: " ",
61
+ spacebar: " ",
62
+ del: "delete",
63
+ plus: "=",
64
+ return: "enter",
65
+ };
66
+
67
+ /** How each modifier prints, per platform. */
68
+ const MAC_GLYPHS: Readonly<Record<Modifier, string>> = {
69
+ mod: "⌘",
70
+ ctrl: "⌃",
71
+ alt: "⌥",
72
+ shift: "⇧",
73
+ };
74
+
75
+ const PC_LABELS: Readonly<Record<Modifier, string>> = {
76
+ mod: "Ctrl",
77
+ ctrl: "Ctrl",
78
+ alt: "Alt",
79
+ shift: "Shift",
80
+ };
81
+
82
+ /** Keys whose printed name is not just the uppercased token. */
83
+ const KEY_LABELS: Readonly<Record<string, string>> = {
84
+ arrowup: "↑",
85
+ arrowdown: "↓",
86
+ arrowleft: "←",
87
+ arrowright: "→",
88
+ " ": "Space",
89
+ escape: "Esc",
90
+ enter: "Enter",
91
+ tab: "Tab",
92
+ delete: "Delete",
93
+ backspace: "Backspace",
94
+ };
95
+
96
+ /**
97
+ * The five fields the keymap reads off a keyboard event.
98
+ *
99
+ * Declared structurally rather than as `KeyboardEvent` so this module — and the registry that
100
+ * imports it — type without the DOM lib. That is what lets `scripts/check-command-levels.ts` load
101
+ * the command set in a bare Bun process, and it costs nothing: a real `KeyboardEvent` satisfies
102
+ * it.
103
+ */
104
+ export interface KeyChordEvent {
105
+ key: string;
106
+ /**
107
+ * The physical key, when the event carries one.
108
+ *
109
+ * Optional, and read for exactly one purpose: with ⇧ held, `key` is the SHIFTED glyph, so ⌘⇧2
110
+ * arrives as `"@"` on a US layout, `"\""` on a UK one and `"é"` on a French one. A chord table
111
+ * that spelled `mod+shift+2` would then never fire — which is precisely the run of chords ⌘⇧1–4
112
+ * (the Inspector tabs) needs. `code` is `"Digit2"` on all three, so the digit row resolves by
113
+ * position, the way a user counting tabs means it.
114
+ */
115
+ code?: string;
116
+ metaKey: boolean;
117
+ ctrlKey: boolean;
118
+ altKey: boolean;
119
+ shiftKey: boolean;
120
+ }
121
+
122
+ /** Enough of `navigator` to tell a mac from everything else. */
123
+ export interface PlatformInfo {
124
+ platform?: string;
125
+ userAgent?: string;
126
+ }
127
+
128
+ /** A chord broken into its parts. */
129
+ export interface ParsedChord {
130
+ mod: boolean;
131
+ ctrl: boolean;
132
+ alt: boolean;
133
+ shift: boolean;
134
+ /** Lowercased key token, e.g. "p", "arrowup", "," or a lone space for Space. */
135
+ key: string;
136
+ }
137
+
138
+ /** The subset of a command record the keymap indexes. */
139
+ export interface BindableRecord {
140
+ id: string;
141
+ keybinding?: string | readonly string[] | undefined;
142
+ keyScope?: KeyScope | undefined;
143
+ }
144
+
145
+ /** Parse `"Cmd+Shift+P"` into its parts. Throws on an empty or modifier-only chord. */
146
+ export function parseChord(chord: string): ParsedChord {
147
+ const parsed: ParsedChord = { mod: false, ctrl: false, alt: false, shift: false, key: "" };
148
+ const tokens = chord.split("+");
149
+ for (const [index, token] of tokens.entries()) {
150
+ if (token === "") {
151
+ // "mod++".split("+") is ["mod", "", ""] — a pair of empty tokens is a literal "+" key, and
152
+ // A lone empty token is just the slack in "mod + s".
153
+ if (index > 0 && tokens[index - 1] === "") {
154
+ parsed.key = "+";
155
+ }
156
+ continue;
157
+ }
158
+ // A whitespace-only token is the Space key; everything else is trimmed so "Cmd + Shift + P"
159
+ // Parses the same as "Cmd+Shift+P".
160
+ const trimmed = token.trim();
161
+ const lower = trimmed === "" ? " " : trimmed.toLowerCase();
162
+ const modifier = MODIFIER_ALIASES[lower];
163
+ if (modifier) {
164
+ parsed[modifier] = true;
165
+ continue;
166
+ }
167
+ parsed.key = KEY_ALIASES[lower] ?? lower;
168
+ }
169
+ if (!parsed.key) {
170
+ throw new Error(`keybinding "${chord}" has no key — a chord needs more than modifiers`);
171
+ }
172
+ return parsed;
173
+ }
174
+
175
+ /** Render a parsed chord back to its canonical string. */
176
+ export function serializeChord(parsed: ParsedChord): string {
177
+ const parts = MODIFIER_ORDER.filter((modifier) => parsed[modifier]);
178
+ return [...parts, parsed.key].join("+");
179
+ }
180
+
181
+ /** `"Cmd+Shift+P"` → `"mod+shift+p"`. Idempotent; the storage form for every binding. */
182
+ export function normalizeChord(chord: string): string {
183
+ return serializeChord(parseChord(chord));
184
+ }
185
+
186
+ /** Whether the current platform is a mac. `navigator` is injectable so tests never touch globals. */
187
+ export function isMacPlatform(
188
+ nav: PlatformInfo = (globalThis as { navigator?: PlatformInfo }).navigator ?? {},
189
+ ): boolean {
190
+ return /mac/i.test(nav.platform ?? nav.userAgent ?? "");
191
+ }
192
+
193
+ /**
194
+ * The chord a keyboard event represents, in canonical form — or `""` for a bare modifier press.
195
+ *
196
+ * `mod` is the platform's primary modifier: ⌘ on mac (where a separate ⌃ can still be held and
197
+ * reads as `ctrl`), Ctrl elsewhere (where Ctrl is `mod` and never also `ctrl`).
198
+ */
199
+ /** `"Digit4"` → `"4"`; anything else → `undefined`. See {@link KeyChordEvent.code}. */
200
+ function digitFromCode(code: string | undefined): string | undefined {
201
+ const match = /^Digit(\d)$/.exec(code ?? "");
202
+ return match?.[1];
203
+ }
204
+
205
+ export function chordFromEvent(event: KeyChordEvent, mac: boolean): string {
206
+ const key = digitFromCode(event.code) ?? event.key.toLowerCase();
207
+ /* A modifier ALONE is not a chord, and holding one is how every chord starts. Serializing the
208
+ keydown for a lone Ctrl produced "mod+control", which `parseChord` refuses by contract ("a
209
+ chord needs more than modifiers") — so every bare modifier press threw out of `resolveEvent`,
210
+ into the keydown listener, and onto the console. `""` is the honest answer, and
211
+ {@link Keymap.resolveEvent} reads it as "no chord yet". */
212
+ if (MODIFIER_ALIASES[key]) {
213
+ return "";
214
+ }
215
+ const parsed: ParsedChord = {
216
+ mod: mac ? event.metaKey : event.ctrlKey,
217
+ ctrl: mac ? event.ctrlKey : false,
218
+ alt: event.altKey,
219
+ // Shift is implied by an uppercase letter but a chord table cannot rely on that: with ⇧ held
220
+ // `e.key` is "Z" for ⌘⇧Z and "?" for ⇧/, so the flag is the only stable signal.
221
+ shift: event.shiftKey,
222
+ key: KEY_ALIASES[key] ?? key,
223
+ };
224
+ return serializeChord(parsed);
225
+ }
226
+
227
+ /** `"mod+shift+p"` → `⌘⇧P` on mac, `Ctrl+Shift+P` elsewhere. The only place a chord is styled. */
228
+ export function formatChord(chord: string, mac: boolean): string {
229
+ const parsed = parseChord(chord);
230
+ const held = MODIFIER_ORDER.filter((modifier) => parsed[modifier]);
231
+ const key =
232
+ KEY_LABELS[parsed.key] ??
233
+ (parsed.key.length === 1
234
+ ? parsed.key.toUpperCase()
235
+ : parsed.key.charAt(0).toUpperCase() + parsed.key.slice(1));
236
+ return mac
237
+ ? held.map((modifier) => MAC_GLYPHS[modifier]).join("") + key
238
+ : [...held.map((modifier) => PC_LABELS[modifier]), key].join("+");
239
+ }
240
+
241
+ /** Raised when two commands claim one chord in one scope. Carries both ids for the message. */
242
+ export class KeybindingConflictError extends Error {
243
+ readonly chord: string;
244
+ readonly scope: KeyScope;
245
+ readonly existingId: string;
246
+ readonly incomingId: string;
247
+
248
+ constructor(chord: string, scope: KeyScope, existingId: string, incomingId: string) {
249
+ super(
250
+ `keybinding conflict: "${chord}" in scope "${scope}" is already bound to "${existingId}"; ` +
251
+ `"${incomingId}" cannot claim it. Rebind one, or move one to a narrower keyScope.`,
252
+ );
253
+ this.name = "KeybindingConflictError";
254
+ this.chord = chord;
255
+ this.scope = scope;
256
+ this.existingId = existingId;
257
+ this.incomingId = incomingId;
258
+ }
259
+ }
260
+
261
+ /** The overlay's stack, and the only one `palette` appears in. */
262
+ const PALETTE_SCOPES: readonly KeyScope[] = ["palette"];
263
+
264
+ /** Every scope a dispatch stack can reach outside an overlay, `global` last-resort but named first. */
265
+ const DISPATCHABLE_SCOPES: readonly KeyScope[] = [
266
+ "global",
267
+ ...KEY_SCOPES.filter((scope) => scope !== "global" && scope !== "palette"),
268
+ ];
269
+
270
+ /**
271
+ * Every scope that can be live at the same instant as `scope` — including `scope` itself.
272
+ *
273
+ * Scopes are NOT independent namespaces, and treating them as one is how a rebinding steals a
274
+ * chord. Dispatch walks a stack (`commands/context.ts` `keyScopeStack`), and every stack Studio can
275
+ * be in is either the overlay's `["palette"]` or `[<one engine scope>, "global"]`. So:
276
+ *
277
+ * - **`global` overlaps every engine scope.** Binding a canvas command to ⌘S does not "also" bind ⌘S
278
+ * — it SHADOWS Save for as long as the canvas has focus, and Save stops responding with no trace.
279
+ * Whichever of the two is the newcomer, the pair cannot both answer that chord.
280
+ * - **Two engine scopes never overlap.** `caret` and `grid` are never on one stack, so one chord may
281
+ * mean two things there for ever; refusing that would forbid the shadowing ladder's whole point.
282
+ * - **`palette` overlaps nothing but itself.** An overlay owns the keyboard outright: nothing else
283
+ * resolves while one is up, and a palette binding resolves nowhere else.
284
+ *
285
+ * Ordered so that {@link Keymap.resolveChord} over the result names the most direct holder first:
286
+ * the same scope, then the wider one.
287
+ */
288
+ export function overlappingScopes(scope: KeyScope): readonly KeyScope[] {
289
+ if (scope === "palette") {
290
+ return PALETTE_SCOPES;
291
+ }
292
+ if (scope === "global") {
293
+ return DISPATCHABLE_SCOPES;
294
+ }
295
+ return [scope, "global"];
296
+ }
297
+
298
+ /** One resolved chord. */
299
+ export interface KeymapMatch {
300
+ commandId: string;
301
+ chord: string;
302
+ scope: KeyScope;
303
+ }
304
+
305
+ /**
306
+ * The user's keybinding layer: command id → the chords it holds INSTEAD of the ones it declared.
307
+ *
308
+ * An entry with an empty list is a command the user unbound. An id with no entry is a command that
309
+ * still has whatever the registry declared — absence is what makes a default a default.
310
+ */
311
+ export type KeybindingOverrides = ReadonlyMap<string, readonly string[]>;
312
+
313
+ export interface Keymap {
314
+ /**
315
+ * Whether chords print with mac glyphs.
316
+ *
317
+ * Exposed because the platform decision is made ONCE, here, and every surface that prints a chord
318
+ * reads it rather than re-detecting: the hardcoded `⌘P` that `toolbar.ts` showed Windows and
319
+ * Linux users existed because there was nowhere else to ask.
320
+ */
321
+ readonly mac: boolean;
322
+ /** Format an arbitrary chord for this platform. {@link formatBinding} is this over a command's. */
323
+ format: (chord: string) => string;
324
+ /** Index a record's bindings. Throws {@link KeybindingConflictError} on a same-scope clash. */
325
+ add: (record: BindableRecord) => void;
326
+ /** Drop a record's bindings — used when a registry is rebuilt, not in normal operation. */
327
+ remove: (id: string) => void;
328
+ /** Canonical chords LIVE for `id` — the user's, when they have overridden it. */
329
+ bindingsFor: (id: string) => readonly string[];
330
+ /**
331
+ * Canonical chords `id` DECLARED, whatever the user layer says.
332
+ *
333
+ * This is what a reset restores, and it is why a rebinding is a layer rather than an edit: the
334
+ * registry stays the one place a default chord is written down.
335
+ */
336
+ declaredFor: (id: string) => readonly string[];
337
+ /** Replace the user's layer and re-index every record against it. */
338
+ setOverrides: (overrides: KeybindingOverrides) => void;
339
+ /** The user's layer, as applied. A copy: the layer is replaced, never mutated in place. */
340
+ overrides: () => KeybindingOverrides;
341
+ /** The first binding of `id`, formatted for this platform, or `undefined` if unbound. */
342
+ formatBinding: (id: string) => string | undefined;
343
+ /** Walk `scopeStack` narrowest-first and return the first scope that binds `chord`. */
344
+ resolveChord: (chord: string, scopeStack: readonly KeyScope[]) => KeymapMatch | undefined;
345
+ /** {@link resolveChord} over a live keyboard event. */
346
+ resolveEvent: (event: KeyChordEvent, scopeStack: readonly KeyScope[]) => KeymapMatch | undefined;
347
+ /** Every (scope, chord, id) triple — the generated shortcut sheet reads this. */
348
+ entries: () => readonly KeymapMatch[];
349
+ /**
350
+ * Subscribe to "the live bindings changed". Returns an unsubscribe.
351
+ *
352
+ * One listener exists today and it is the reason this hook does: the canvas iframe holds a COPY
353
+ * of the chord table (`canvas/iframe-keys.ts`), and a copy with no invalidation is a second
354
+ * authority that drifts the moment someone rebinds a key. Fired by {@link setOverrides}, which is
355
+ * the only operation that changes what is live after boot.
356
+ */
357
+ onChange: (listener: () => void) => () => void;
358
+ }
359
+
360
+ /**
361
+ * Build an empty keymap.
362
+ *
363
+ * @param options.mac Whether to format chords with mac glyphs. Defaults to platform detection.
364
+ */
365
+ /** Which layer a live claim came from — the whole of the precedence rule is this one field. */
366
+ interface Claim {
367
+ id: string;
368
+ source: "default" | "override";
369
+ }
370
+
371
+ export function createKeymap(options: { mac?: boolean } = {}): Keymap {
372
+ const mac = options.mac ?? isMacPlatform();
373
+ /** Scope → chord → who holds it. */
374
+ const byScope = new Map<KeyScope, Map<string, Claim>>();
375
+ /** Command id → the chords it holds LIVE, in declaration order. */
376
+ const byId = new Map<string, string[]>();
377
+ /** Command id → what it declared. Never written by a rebinding. */
378
+ const declared = new Map<string, { chords: string[]; scope: KeyScope }>();
379
+ /** The user's layer. Replaced wholesale by `setOverrides`; empty until one is applied. */
380
+ let userLayer = new Map<string, readonly string[]>();
381
+ /** Who to tell when the live bindings change. */
382
+ const listeners = new Set<() => void>();
383
+
384
+ function chordsOf(record: BindableRecord): string[] {
385
+ const raw = record.keybinding;
386
+ if (!raw) {
387
+ return [];
388
+ }
389
+ return (typeof raw === "string" ? [raw] : [...raw]).map((chord) => normalizeChord(chord));
390
+ }
391
+
392
+ function tableFor(scope: KeyScope): Map<string, Claim> {
393
+ let table = byScope.get(scope);
394
+ if (!table) {
395
+ table = new Map<string, Claim>();
396
+ byScope.set(scope, table);
397
+ }
398
+ return table;
399
+ }
400
+
401
+ /**
402
+ * Index one record's LIVE chords.
403
+ *
404
+ * `strict` is registration: two defaults colliding is an app bug and throws, and the throw
405
+ * happens before anything is written so a rejected record leaves nothing behind. A rebuild is not
406
+ * strict — by then every collision involves the user's layer, which has an answer.
407
+ */
408
+ function claim(id: string, strict: boolean): void {
409
+ const record = declared.get(id);
410
+ if (!record) {
411
+ return;
412
+ }
413
+ const override = userLayer.get(id);
414
+ const source: Claim["source"] = override === undefined ? "default" : "override";
415
+ const chords = override ?? record.chords;
416
+ const table = tableFor(record.scope);
417
+ if (strict) {
418
+ for (const chord of chords) {
419
+ const held = table.get(chord);
420
+ if (held && held.id !== id && held.source === "default" && source === "default") {
421
+ throw new KeybindingConflictError(chord, record.scope, held.id, id);
422
+ }
423
+ }
424
+ }
425
+ const taken: string[] = [];
426
+ for (const chord of chords) {
427
+ const held = table.get(chord);
428
+ if (held && held.id !== id) {
429
+ if (source === "default") {
430
+ // A default never evicts an override: the user asked for this chord by name, and a
431
+ // Command that ships with it later is the one that goes unbound.
432
+ continue;
433
+ }
434
+ // An override always evicts. (Two overrides on one chord can only come from a hand-edited
435
+ // Store — `rebindCommand` refuses it — and then the later record wins, deterministically.)
436
+ byId.set(
437
+ held.id,
438
+ (byId.get(held.id) ?? []).filter((bound) => bound !== chord),
439
+ );
440
+ }
441
+ table.set(chord, { id, source });
442
+ taken.push(chord);
443
+ }
444
+ byId.set(id, taken);
445
+ }
446
+
447
+ /** Re-index everything: overridden records first, so their claims are the ones that survive. */
448
+ function rebuild(): void {
449
+ byScope.clear();
450
+ byId.clear();
451
+ for (const id of declared.keys()) {
452
+ if (userLayer.has(id)) {
453
+ claim(id, false);
454
+ }
455
+ }
456
+ for (const id of declared.keys()) {
457
+ if (!userLayer.has(id)) {
458
+ claim(id, false);
459
+ }
460
+ }
461
+ }
462
+
463
+ return {
464
+ mac,
465
+ format(chord) {
466
+ return formatChord(chord, mac);
467
+ },
468
+ add(record) {
469
+ const scope: KeyScope = record.keyScope ?? "global";
470
+ declared.set(record.id, { chords: chordsOf(record), scope });
471
+ try {
472
+ claim(record.id, true);
473
+ } catch (error) {
474
+ declared.delete(record.id);
475
+ throw error;
476
+ }
477
+ },
478
+ remove(id) {
479
+ declared.delete(id);
480
+ // A rebuild rather than a targeted delete, so anything this record was displacing gets its
481
+ // Chord back — the same reason `setOverrides` rebuilds.
482
+ rebuild();
483
+ },
484
+ bindingsFor(id) {
485
+ return byId.get(id) ?? [];
486
+ },
487
+ declaredFor(id) {
488
+ return declared.get(id)?.chords ?? [];
489
+ },
490
+ setOverrides(overrides) {
491
+ userLayer = new Map(
492
+ [...overrides].map(([id, chords]) => [id, chords.map((chord) => normalizeChord(chord))]),
493
+ );
494
+ rebuild();
495
+ for (const listener of listeners) {
496
+ listener();
497
+ }
498
+ },
499
+ onChange(listener) {
500
+ listeners.add(listener);
501
+ return () => listeners.delete(listener);
502
+ },
503
+ overrides() {
504
+ return new Map(userLayer);
505
+ },
506
+ formatBinding(id) {
507
+ const first = byId.get(id)?.[0];
508
+ return first === undefined ? undefined : formatChord(first, mac);
509
+ },
510
+ resolveChord(chord, scopeStack) {
511
+ const normalized = normalizeChord(chord);
512
+ // Narrowest scope first: the stack IS the precedence order, so the first hit wins and a
513
+ // Caret binding shadows the same chord bound globally.
514
+ const scope = scopeStack.find((candidate) => byScope.get(candidate)?.has(normalized));
515
+ const held = scope === undefined ? undefined : byScope.get(scope)?.get(normalized);
516
+ return scope === undefined || held === undefined
517
+ ? undefined
518
+ : { chord: normalized, commandId: held.id, scope };
519
+ },
520
+ resolveEvent(event, scopeStack) {
521
+ const chord = chordFromEvent(event, mac);
522
+ // A bare modifier press resolves to nothing rather than throwing on a keyless chord.
523
+ return chord === "" ? undefined : this.resolveChord(chord, scopeStack);
524
+ },
525
+ entries() {
526
+ const all: KeymapMatch[] = [];
527
+ for (const [scope, table] of byScope) {
528
+ for (const [chord, held] of table) {
529
+ all.push({ chord, commandId: held.id, scope });
530
+ }
531
+ }
532
+ return all;
533
+ },
534
+ };
535
+ }
536
+
537
+ /**
538
+ * Every live (chord, scope) pair in `scopes`, deduplicated — the table a second realm can resolve
539
+ * against.
540
+ *
541
+ * Written for the canvas iframe, which cannot see the registry at all: it is handed this and
542
+ * forwards a keystroke iff some scope on its own stack claims the chord (`canvas/iframe-keys.ts`).
543
+ * The command id is deliberately dropped — the frame decides whether to forward, never what runs —
544
+ * and the projection is a pure function of the keymap so the host can recompute and repost it
545
+ * whenever a rebinding lands.
546
+ */
547
+ export function chordsInScopes<S extends KeyScope>(
548
+ keymap: Keymap,
549
+ scopes: readonly S[],
550
+ ): { chord: string; scope: S }[] {
551
+ const seen = new Set<string>();
552
+ const out: { chord: string; scope: S }[] = [];
553
+ for (const entry of keymap.entries()) {
554
+ /* `find` rather than a `Set.has` test: the narrowed member is what makes the result carry the
555
+ CALLER's scope union. The canvas frame's table is three scopes wide (`FRAME_KEY_SCOPES`) and
556
+ has to stay that way across the bridge, where `SyncedChord` declares exactly those three —
557
+ widening back to every `KeyScope` here would push a `grid` chord into a message that cannot
558
+ hold one. */
559
+ const scope = scopes.find((candidate) => candidate === entry.scope);
560
+ if (scope === undefined) {
561
+ continue;
562
+ }
563
+ const key = `${scope} ${entry.chord}`;
564
+ if (seen.has(key)) {
565
+ continue;
566
+ }
567
+ seen.add(key);
568
+ out.push({ chord: entry.chord, scope });
569
+ }
570
+ return out;
571
+ }