@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
@@ -1,31 +1,372 @@
1
1
  /// <reference lib="dom" />
2
+ /**
3
+ * The Palette — one omnibox, several modes (UX-REDESIGN-PLAN §5.4).
4
+ *
5
+ * This file used to be a filename-substring finder with one hidden domain swap in it: with no
6
+ * project open the same trigger, the same chrome and the same placeholder silently listed recent
7
+ * PROJECTS instead. It is now the surface the whole shell shrinks into — the place a capability
8
+ * goes when it is retired from the chrome (§2 principle 9), which only works if everything is
9
+ * reachable here by name, with its chord printed beside it.
10
+ *
11
+ * Four properties earn that:
12
+ *
13
+ * 1. **Modes are named, prefixed and enumerable.** `>` commands, `@` document nodes, plain text files,
14
+ * and `?` lists them. The mode is echoed as a REMOVABLE CHIP, so the no-project case is a stated
15
+ * `Recent Projects` mode rather than a swap you have to infer. `PALETTE_MODES` is the namespace:
16
+ * P4's Problems and P7's content search are new members, not a new widget.
17
+ * 2. **Files are matched by fuzzy subsequence over the FULL PATH.** `pgblog` finds
18
+ * `pages/blog/index.md`. The backend glob (`**\/*q*.{json,md}`) can only do a basename
19
+ * substring, so the palette asks it once for the document set and ranks locally — which is also
20
+ * what lets the ranking prefer a basename hit, a word boundary and a consecutive run, in that
21
+ * order.
22
+ * 3. **Every row prints its binding.** That is how a large surface stays discoverable without adding
23
+ * chrome, and it is the mechanism by which the palette teaches the keyboard it replaced.
24
+ * 4. **An unavailable command is GREYED, not hidden**, with its `requires` sentence as the subtitle
25
+ * (§2 principle 4). "Why can't I" is the question a palette is uniquely good at answering, and a
26
+ * row that vanishes answers it with silence.
27
+ *
28
+ * The exported names still read `…QuickSearch` because `src/studio.ts` and `src/panels/empty-state.
29
+ * ts` import them and neither is this workstream's file. They are the palette's; the rename is one
30
+ * find-replace in those two call sites.
31
+ */
32
+
2
33
  import { html, render as litRender, nothing } from "lit-html";
3
34
  import { classMap } from "lit-html/directives/class-map.js";
4
35
  import { live } from "lit-html/directives/live.js";
5
36
  import { ref } from "lit-html/directives/ref.js";
6
37
  import { getPlatform } from "../platform";
7
- import { projectState } from "../store";
38
+ import { flattenTree, nodeLabel, projectState } from "../store";
8
39
  import { documentExtensions, formatByExtension, loadFormats } from "../format/format-host";
9
40
  import { openFileInTab } from "../files/files";
10
41
  import { getRecentFiles, getRecentProjects, trackRecentFile } from "../recent-projects";
11
42
  import { getLayerSlot } from "../ui/layers";
43
+ import { activeTab } from "../workspace/workspace";
44
+ import { activeRegistry } from "../commands/active-registry";
45
+ import type { PaletteMode } from "../commands/defaults";
46
+ import type { AnyCommand, CommandRegistry } from "../commands/registry";
47
+ import type { JxMutableNode } from "@jxsuite/schema/types";
48
+ import type { JxPath } from "../state";
49
+
50
+ export type { PaletteMode } from "../commands/defaults";
51
+
52
+ // ─── The mode namespace ───────────────────────────────────────────────────────
53
+
54
+ /** One palette mode: its prefix, the chip it echoes, and what it says when it is empty. */
55
+ export interface PaletteModeSpec {
56
+ mode: PaletteMode;
57
+ /** The one character that switches into this mode from any other. `""` = the plain mode. */
58
+ prefix: string;
59
+ /** The removable chip's text, and the row label in `?`. */
60
+ chip: string;
61
+ placeholder: string;
62
+ /** What `?` prints under the mode's name. One sentence, no noun phrases (§2 principle 6). */
63
+ description: string;
64
+ }
65
+
66
+ /**
67
+ * Every mode, in the order `?` lists them.
68
+ *
69
+ * Declared as data rather than as a switch because three consumers read it: the prefix parser, the
70
+ * chip, and the `?` listing itself — which is what makes the palette able to teach its own
71
+ * namespace instead of hiding it in a docs page.
72
+ */
73
+ export const PALETTE_MODES: readonly PaletteModeSpec[] = [
74
+ {
75
+ mode: "files",
76
+ prefix: "",
77
+ chip: "Files",
78
+ placeholder: "Go to a file…",
79
+ description: "Open a file by any part of its path — pgblog finds pages/blog/index.md.",
80
+ },
81
+ {
82
+ mode: "commands",
83
+ prefix: ">",
84
+ chip: "Commands",
85
+ placeholder: "Run a command…",
86
+ description: "Run anything Studio can do, with its keyboard shortcut printed beside it.",
87
+ },
88
+ {
89
+ mode: "nodes",
90
+ prefix: "@",
91
+ chip: "Symbols",
92
+ placeholder: "Go to an element in this document…",
93
+ description: "Select an element in the open document by its Outline name.",
94
+ },
95
+ {
96
+ mode: "projects",
97
+ prefix: "",
98
+ chip: "Recent Projects",
99
+ placeholder: "Open a recent project…",
100
+ description: "Reopen a project you have had open before.",
101
+ },
102
+ {
103
+ mode: "picker",
104
+ prefix: "?",
105
+ chip: "Modes",
106
+ placeholder: "Type ? for modes, > for commands, @ for elements…",
107
+ description: "List these modes.",
108
+ },
109
+ ];
110
+
111
+ const MODE_SPEC = new Map(PALETTE_MODES.map((spec) => [spec.mode, spec]));
112
+
113
+ /** The modes a prefix character switches into. `files` and `projects` have no prefix. */
114
+ const PREFIXED_MODES = PALETTE_MODES.filter((spec) => spec.prefix !== "");
115
+
116
+ /** The spec for a mode. Total: every {@link PaletteMode} is declared above. */
117
+ export function modeSpec(mode: PaletteMode): PaletteModeSpec {
118
+ return MODE_SPEC.get(mode) ?? PALETTE_MODES[0]!;
119
+ }
120
+
121
+ /**
122
+ * The mode a raw input actually addresses.
123
+ *
124
+ * Three rules, in order:
125
+ *
126
+ * - A leading prefix character wins, whatever the palette was opened as — that is what `>` and `@`
127
+ * are FOR, and it is why the prefix is stripped out of the query and into the chip.
128
+ * - `picker` with an empty query lists the modes; `picker` with anything typed means files, because
129
+ * ⌘K then typing a filename is the gesture every omnibox has trained people to expect.
130
+ * - Files with no project open is `Recent Projects`. This is the one substitution the old widget made
131
+ * silently; here the chip says so, and `Project: Open Recent…` names it from the outside.
132
+ */
133
+ export function resolvePaletteMode(
134
+ requested: PaletteMode,
135
+ raw: string,
136
+ projectOpen: boolean,
137
+ ): { mode: PaletteMode; query: string } {
138
+ const prefixed = PREFIXED_MODES.find((spec) => raw.startsWith(spec.prefix));
139
+ if (prefixed) {
140
+ return resolvePaletteMode(prefixed.mode, raw.slice(1), projectOpen);
141
+ }
142
+ let mode = requested;
143
+ if (mode === "picker" && raw.trim() !== "") {
144
+ mode = "files";
145
+ }
146
+ if (mode === "files" && !projectOpen) {
147
+ mode = "projects";
148
+ }
149
+ return { mode, query: raw };
150
+ }
151
+
152
+ // ─── Fuzzy subsequence ranking ────────────────────────────────────────────────
153
+
154
+ /** Characters after which a match counts as starting a word — the boundaries a path has. */
155
+ const BOUNDARY = new Set(["/", "-", "_", ".", " "]);
156
+
157
+ /**
158
+ * Score `text` against `query` as a subsequence, or `null` when it is not one.
159
+ *
160
+ * Higher is better. The weights encode what a person means when they type six characters at a file
161
+ * tree: a consecutive run is worth much more than scattered letters, a match that starts a path
162
+ * segment is worth more than one mid-word, and — decisively — a match inside the BASENAME beats one
163
+ * in a directory, so typing `index` does not bury `index.md` under `pages/index-partials/x.md`.
164
+ *
165
+ * An empty query scores 0 and matches everything, which is what makes "show me the recents" and
166
+ * "show me every file" the same code path.
167
+ */
168
+ export function fuzzyScore(text: string, query: string): number | null {
169
+ if (query === "") {
170
+ return 0;
171
+ }
172
+ const haystack = text.toLowerCase();
173
+ const needle = query.toLowerCase();
174
+ const basenameStart = haystack.lastIndexOf("/") + 1;
175
+ let score = 0;
176
+ let cursor = 0;
177
+ let previousIndex = -2;
178
+ for (const char of needle) {
179
+ const index = haystack.indexOf(char, cursor);
180
+ if (index === -1) {
181
+ return null;
182
+ }
183
+ if (index === previousIndex + 1) {
184
+ score += 8;
185
+ }
186
+ if (index === 0 || BOUNDARY.has(haystack[index - 1] ?? "")) {
187
+ score += 6;
188
+ }
189
+ if (index >= basenameStart) {
190
+ score += 4;
191
+ }
192
+ previousIndex = index;
193
+ cursor = index + 1;
194
+ }
195
+ // Two tie-breaks, both small enough never to outrank a real structural signal: an earlier first
196
+ // Match, and a shorter path — `pages/index.md` over `pages/blog/drafts/index.md`.
197
+ return score - previousIndex * 0.01 - haystack.length * 0.001;
198
+ }
199
+
200
+ /** Rank candidates by {@link fuzzyScore}, dropping non-matches. Stable within equal scores. */
201
+ export function rankBy<T>(items: readonly T[], query: string, key: (item: T) => string): T[] {
202
+ const scored: { item: T; score: number; index: number }[] = [];
203
+ for (const [index, item] of items.entries()) {
204
+ const score = fuzzyScore(key(item), query);
205
+ if (score !== null) {
206
+ scored.push({ index, item, score });
207
+ }
208
+ }
209
+ return scored
210
+ .toSorted((a, b) => b.score - a.score || a.index - b.index)
211
+ .map((entry) => entry.item);
212
+ }
213
+
214
+ // ─── Argument prompts ─────────────────────────────────────────────────────────
215
+
216
+ /** One offered value for a command's single argument. */
217
+ export interface PaletteArgChoice {
218
+ value: unknown;
219
+ label: string;
220
+ }
221
+
222
+ /** What the palette can do with a command's `args` schema. */
223
+ export type PaletteArgs =
224
+ | { kind: "none" }
225
+ | { kind: "choice"; name: string; choices: PaletteArgChoice[] }
226
+ | { kind: "unsupported" };
227
+
228
+ interface ArgsSchemaShape {
229
+ properties?: Record<string, { type?: string; enum?: readonly unknown[]; description?: string }>;
230
+ required?: readonly string[];
231
+ }
232
+
233
+ /**
234
+ * How a command's arguments are asked for.
235
+ *
236
+ * The palette can prompt for exactly one closed value space — an `enum` or a boolean — because that
237
+ * is a LIST, and a list is the one thing this widget is. `canvas.setZoom { zoom: number }` and
238
+ * `selection.set { path }` are `"unsupported"`, so command mode does not render them: a row that
239
+ * cannot be completed is worse than an absent one, and both are reachable from the surface that
240
+ * owns them (the zoom cluster, the canvas). This is a property of the SCHEMA, not a list of ids, so
241
+ * a command that gains an enum argument becomes promptable without anyone editing this file.
242
+ */
243
+ export function paletteArgs(command: AnyCommand): PaletteArgs {
244
+ const schema = command.args as ArgsSchemaShape | undefined;
245
+ const properties = schema?.properties;
246
+ if (!schema || !properties) {
247
+ return { kind: "none" };
248
+ }
249
+ const names = Object.keys(properties);
250
+ if (names.length !== 1) {
251
+ return { kind: "unsupported" };
252
+ }
253
+ const name = names[0]!;
254
+ const property = properties[name]!;
255
+ if (property.enum) {
256
+ return {
257
+ kind: "choice",
258
+ name,
259
+ choices: property.enum.map((value) => ({ label: String(value), value })),
260
+ };
261
+ }
262
+ if (property.type === "boolean") {
263
+ return {
264
+ kind: "choice",
265
+ name,
266
+ choices: [
267
+ { label: "on", value: true },
268
+ { label: "off", value: false },
269
+ ],
270
+ };
271
+ }
272
+ return { kind: "unsupported" };
273
+ }
274
+
275
+ // ─── Recently-used commands ───────────────────────────────────────────────────
12
276
 
13
277
  /**
14
- * A row in the Quick Access modal. With a project open the modal lists/searches that project's
15
- * files; with no project open it lists recent projects to re-open. The two never mix — the modal
16
- * only ever shows files from the current project.
278
+ * The ids the combobox relationship is built from (WAI-ARIA APG, combobox with listbox popup).
279
+ *
280
+ * The input already said `role="combobox"`, but nothing connected it to the list beneath: no
281
+ * `aria-controls`, so a screen reader could not find the popup, and no `aria-activedescendant`, so
282
+ * arrowing through the results moved a visual highlight and announced nothing. `aria-expanded` was
283
+ * the literal string `"true"`, which claims a popup is showing even when the query matched
284
+ * nothing.
17
285
  */
18
- interface FileItem {
286
+ const QUICK_SEARCH_LISTBOX_ID = "quick-search-listbox";
287
+
288
+ /** A stable per-row id, so `aria-activedescendant` has something to point at. */
289
+ function optionId(index: number): string {
290
+ return `quick-search-option-${index}`;
291
+ }
292
+
293
+ const RECENT_COMMANDS_KEY = "jx-studio-recent-commands";
294
+ const RECENT_COMMANDS_MAX = 6;
295
+
296
+ /** The ids most recently run FROM THE PALETTE, newest first. Pinned above the rest of the list. */
297
+ export function getRecentCommands(): string[] {
298
+ try {
299
+ const raw: unknown = JSON.parse(localStorage.getItem(RECENT_COMMANDS_KEY) || "[]");
300
+ return Array.isArray(raw) ? raw.filter((id) => typeof id === "string") : [];
301
+ } catch {
302
+ return [];
303
+ }
304
+ }
305
+
306
+ function trackRecentCommand(id: string): void {
307
+ const next = [id, ...getRecentCommands().filter((known) => known !== id)].slice(
308
+ 0,
309
+ RECENT_COMMANDS_MAX,
310
+ );
311
+ try {
312
+ localStorage.setItem(RECENT_COMMANDS_KEY, JSON.stringify(next));
313
+ } catch {
314
+ // Storage full or unavailable — the command still ran, it is just not remembered.
315
+ }
316
+ }
317
+
318
+ // ─── Rows ─────────────────────────────────────────────────────────────────────
319
+
320
+ interface FileRow {
19
321
  kind: "file";
20
322
  path: string;
21
323
  name: string;
324
+ /** Directory part, shown as the subtitle. */
325
+ detail: string;
326
+ recent: boolean;
22
327
  }
23
- interface ProjectItem {
328
+ interface ProjectRow {
24
329
  kind: "project";
25
330
  root: string;
26
331
  name: string;
332
+ detail: string;
333
+ }
334
+ interface CommandRow {
335
+ kind: "command";
336
+ id: string;
337
+ /** `"View: Zen Mode"` — the palette's own naming convention (§5.1). */
338
+ name: string;
339
+ /** The `requires` sentence when disabled; otherwise empty. */
340
+ detail: string;
341
+ chord: string;
342
+ enabled: boolean;
343
+ recent: boolean;
344
+ }
345
+ interface NodeRow {
346
+ kind: "node";
347
+ path: JxPath;
348
+ name: string;
349
+ detail: string;
350
+ }
351
+ interface ModeRow {
352
+ kind: "mode";
353
+ mode: PaletteMode;
354
+ name: string;
355
+ detail: string;
356
+ chord: string;
27
357
  }
28
- type QuickItem = FileItem | ProjectItem;
358
+ interface ArgRow {
359
+ kind: "arg";
360
+ commandId: string;
361
+ argName: string;
362
+ value: unknown;
363
+ name: string;
364
+ detail: string;
365
+ }
366
+
367
+ type PaletteRow = FileRow | ProjectRow | CommandRow | NodeRow | ModeRow | ArgRow;
368
+
369
+ // ─── State ────────────────────────────────────────────────────────────────────
29
370
 
30
371
  interface QuickCtx {
31
372
  openRecentProject: (root: string) => void | Promise<void>;
@@ -33,108 +374,279 @@ interface QuickCtx {
33
374
 
34
375
  let _ctx: QuickCtx | null = null;
35
376
  let _open = false;
377
+ /** The mode the palette was OPENED as; the effective one is derived with the query. */
378
+ let _requested: PaletteMode = "picker";
36
379
  let _query = "";
37
- let _results: FileItem[] = [];
38
380
  let _selectedIndex = 0;
39
- let _debounceTimer = 0;
381
+ /** The project's document set, fetched once per open. `null` while it has never been asked for. */
382
+ let _files: FileRow[] | null = null;
383
+ let _loading = false;
384
+ /** The command awaiting its one argument, or `null`. */
385
+ let _pending: { command: AnyCommand; name: string; choices: PaletteArgChoice[] } | null = null;
40
386
 
41
- /** @returns {HTMLElement} */
42
387
  function getContainer() {
43
388
  return getLayerSlot("popover", "quick-search");
44
389
  }
45
390
 
46
- /** @param {QuickCtx} [ctx] */
47
391
  export function initQuickSearch(ctx?: QuickCtx) {
48
392
  _ctx = ctx ?? null;
49
393
  }
50
394
 
51
- export function openQuickSearch() {
395
+ /**
396
+ * Open the palette in `mode`.
397
+ *
398
+ * The default is `"files"`, not the picker: every caller that names a mode names one (⌘K passes
399
+ * `"picker"`, the pill's segments pass theirs), so the bare call is the "open a page…" gesture an
400
+ * empty state offers — and that has to land on files rather than on a list of modes.
401
+ *
402
+ * @param mode Which mode to open in. `"picker"` (⌘K) lists the modes until something is typed.
403
+ */
404
+ export function openQuickSearch(mode: PaletteMode = "files") {
52
405
  _open = true;
406
+ _requested = mode;
53
407
  _query = "";
54
- _results = [];
55
408
  _selectedIndex = 0;
409
+ _pending = null;
410
+ _files = null;
56
411
  renderOverlay();
412
+ void ensureFiles();
57
413
  }
58
414
 
59
415
  export function closeQuickSearch() {
60
416
  _open = false;
417
+ _pending = null;
61
418
  renderOverlay();
62
419
  }
63
420
 
64
- /** The project root scoping the modal, or null when no project is open. */
421
+ /** Whether the palette is on screen — the fact the Command Bar's ⌘K affordance reflects. */
422
+ export function isQuickSearchOpen(): boolean {
423
+ return _open;
424
+ }
425
+
426
+ /** The project root scoping the palette, or null when no project is open. */
65
427
  function scopeRoot(): string | null {
66
428
  return projectState ? (projectState.projectRoot ?? null) : null;
67
429
  }
68
430
 
69
- /**
70
- * Resolve the rows to display for the current query/mode. File search is async (populates
71
- * `_results`); recent files and recent-project filtering are synchronous.
72
- */
73
- function currentItems(): { items: QuickItem[]; showingRecent: boolean } {
74
- const q = _query.trim();
75
- if (!projectState) {
76
- // No project open → offer recent projects to re-open, filtered by the query.
77
- const needle = q.toLowerCase();
78
- const projects = getRecentProjects().filter(
79
- (p) =>
80
- !needle || p.name.toLowerCase().includes(needle) || p.root.toLowerCase().includes(needle),
81
- );
82
- return {
83
- items: projects.map((p) => ({ kind: "project", name: p.name, root: p.root })),
84
- showingRecent: !q,
85
- };
86
- }
87
- if (!q) {
88
- const recent = getRecentFiles(scopeRoot() ?? undefined);
89
- return {
90
- items: recent.map((f) => ({ kind: "file", name: f.name, path: f.path })),
91
- showingRecent: true,
92
- };
431
+ function hasProject(): boolean {
432
+ return projectState != null;
433
+ }
434
+
435
+ /** The mode and query the current input resolves to. */
436
+ function current(): { mode: PaletteMode; query: string } {
437
+ return resolvePaletteMode(_requested, _query, hasProject());
438
+ }
439
+
440
+ // ─── Row sources ──────────────────────────────────────────────────────────────
441
+
442
+ function dirPart(path: string) {
443
+ const parts = path.split("/");
444
+ parts.pop();
445
+ return parts.length > 0 ? parts.join("/") : "";
446
+ }
447
+
448
+ /** Collapse a home-prefixed absolute path for compact display. */
449
+ function shortenPath(path: string) {
450
+ if (path.startsWith("/home/")) {
451
+ return `~/${path.split("/").slice(3).join("/")}`;
93
452
  }
94
- return { items: _results, showingRecent: false };
453
+ return path;
95
454
  }
96
455
 
97
- async function doSearch(query: string) {
98
- if (!query.trim()) {
99
- _results = [];
100
- _selectedIndex = 0;
101
- renderOverlay();
456
+ /**
457
+ * Fetch the project's document set once per open.
458
+ *
459
+ * The empty query is deliberate: `searchFiles`'s glob is `**\/*<query>*.{json,md,…}`, so an empty
460
+ * one IS "every document", and asking for it once buys a client-side fuzzy match over full paths
461
+ * that the glob could never express.
462
+ */
463
+ async function ensureFiles(): Promise<void> {
464
+ if (!_open || _files !== null || _loading || !hasProject()) {
102
465
  return;
103
466
  }
467
+ _loading = true;
468
+ renderOverlay();
104
469
  try {
105
- const platform = getPlatform();
106
470
  await loadFormats();
107
- const hits = await platform.searchFiles(query.trim().toLowerCase(), documentExtensions());
108
- _results = hits.map((h) => ({
109
- kind: "file",
110
- name: h.name ?? h.path.split("/").pop() ?? "",
111
- path: h.path,
471
+ const hits = await getPlatform().searchFiles("", documentExtensions());
472
+ _files = hits.map((hit) => ({
473
+ kind: "file" as const,
474
+ path: hit.path,
475
+ name: hit.name ?? hit.path.split("/").pop() ?? "",
476
+ detail: dirPart(hit.path),
477
+ recent: false,
112
478
  }));
113
- _selectedIndex = 0;
114
- renderOverlay();
115
479
  } catch {
116
- _results = [];
117
- renderOverlay();
480
+ // A search backend that is down leaves the palette usable in every other mode; the files mode
481
+ // Says "No results" rather than pretending the project is empty.
482
+ _files = [];
483
+ }
484
+ _loading = false;
485
+ renderOverlay();
486
+ }
487
+
488
+ function fileRows(query: string): FileRow[] {
489
+ if (query.trim() === "") {
490
+ return getRecentFiles(scopeRoot() ?? undefined).map((file) => ({
491
+ kind: "file" as const,
492
+ path: file.path,
493
+ name: file.name,
494
+ detail: dirPart(file.path),
495
+ recent: true,
496
+ }));
497
+ }
498
+ return rankBy(_files ?? [], query.trim(), (row) => row.path);
499
+ }
500
+
501
+ function projectRows(query: string): ProjectRow[] {
502
+ const rows = getRecentProjects().map((project) => ({
503
+ kind: "project" as const,
504
+ root: project.root,
505
+ name: project.name,
506
+ detail: shortenPath(project.root),
507
+ }));
508
+ return rankBy(rows, query.trim(), (row) => `${row.name}/${row.root}`);
509
+ }
510
+
511
+ /**
512
+ * Every command the registry holds, ranked — including the ones that cannot run right now.
513
+ *
514
+ * Visibility (`when`) still hides; enablement does not. That split is the whole of §2 principle 4:
515
+ * a command that does not apply to this app at all is absent, and one that does not apply to this
516
+ * MOMENT is greyed and says why.
517
+ */
518
+ function commandRows(registry: CommandRegistry, query: string): CommandRow[] {
519
+ const recents = getRecentCommands();
520
+ const rows: CommandRow[] = [];
521
+ for (const command of registry.visible()) {
522
+ if (paletteArgs(command).kind === "unsupported") {
523
+ continue;
524
+ }
525
+ if (!(command.menus ?? ["palette"]).includes("palette")) {
526
+ continue;
527
+ }
528
+ rows.push({
529
+ kind: "command",
530
+ id: command.id,
531
+ name: `${command.category}: ${command.title}`,
532
+ detail: registry.disabledReason(command.id) ?? "",
533
+ chord: registry.keymap.formatBinding(command.id) ?? "",
534
+ enabled: registry.isEnabled(command.id),
535
+ recent: recents.includes(command.id),
536
+ });
537
+ }
538
+ const ranked = rankBy(rows, query.trim(), (row) => row.name);
539
+ if (query.trim() !== "") {
540
+ return ranked;
541
+ }
542
+ // Recents pin above the rest, newest first — the one ordering a palette owes a returning user.
543
+ const pinned = recents
544
+ .map((id) => ranked.find((row) => row.id === id))
545
+ .filter((row): row is CommandRow => row !== undefined);
546
+ return [...pinned, ...ranked.filter((row) => !pinned.includes(row))];
547
+ }
548
+
549
+ function nodeRows(query: string): NodeRow[] {
550
+ const tab = activeTab.value;
551
+ if (!tab) {
552
+ return [];
553
+ }
554
+ const rows: NodeRow[] = flattenTree(tab.doc.document).map((row) => ({
555
+ kind: "node" as const,
556
+ path: row.path,
557
+ name:
558
+ row.nodeType === "text"
559
+ ? String(row.node).slice(0, 60)
560
+ : nodeLabel(row.node as JxMutableNode),
561
+ detail: row.nodeType,
562
+ }));
563
+ return rankBy(rows, query.trim(), (row) => row.name);
564
+ }
565
+
566
+ function modeRows(): ModeRow[] {
567
+ return PALETTE_MODES.filter((spec) => spec.mode !== "picker").map((spec) => ({
568
+ kind: "mode" as const,
569
+ mode: spec.mode,
570
+ name: spec.chip,
571
+ detail: spec.description,
572
+ chord: spec.prefix,
573
+ }));
574
+ }
575
+
576
+ function argRows(query: string): ArgRow[] {
577
+ const pending = _pending!;
578
+ const rows: ArgRow[] = pending.choices.map((choice) => ({
579
+ kind: "arg" as const,
580
+ commandId: pending.command.id,
581
+ argName: pending.name,
582
+ value: choice.value,
583
+ name: choice.label,
584
+ detail: `${pending.command.title} → ${pending.name}`,
585
+ }));
586
+ return rankBy(rows, query.trim(), (row) => row.name);
587
+ }
588
+
589
+ /** The rows for the current input, and whether they are a "recent" listing rather than a search. */
590
+ function currentRows(): { rows: PaletteRow[]; showingRecent: boolean } {
591
+ const { mode, query } = current();
592
+ if (_pending) {
593
+ return { rows: argRows(query), showingRecent: false };
594
+ }
595
+ switch (mode) {
596
+ case "commands": {
597
+ const registry = activeRegistry();
598
+ return {
599
+ rows: registry ? commandRows(registry, query) : [],
600
+ showingRecent: query.trim() === "",
601
+ };
602
+ }
603
+ case "nodes": {
604
+ return { rows: nodeRows(query), showingRecent: false };
605
+ }
606
+ case "projects": {
607
+ return { rows: projectRows(query), showingRecent: query.trim() === "" };
608
+ }
609
+ case "picker": {
610
+ return { rows: modeRows(), showingRecent: false };
611
+ }
612
+ default: {
613
+ return { rows: fileRows(query), showingRecent: query.trim() === "" };
614
+ }
118
615
  }
119
616
  }
120
617
 
618
+ // ─── Interaction ──────────────────────────────────────────────────────────────
619
+
121
620
  function onInput(e: Event) {
122
- _query = (e.target as HTMLInputElement).value;
123
- clearTimeout(_debounceTimer);
124
- // Only file search needs the backend (and debouncing); recent-project filtering is synchronous.
125
- if (projectState) {
126
- _debounceTimer = setTimeout(() => doSearch(_query), 150) as unknown as number;
621
+ const raw = (e.target as HTMLInputElement).value;
622
+ const prefixed = PREFIXED_MODES.find((spec) => raw.startsWith(spec.prefix));
623
+ if (prefixed) {
624
+ // The prefix moves OUT of the input and into the chip, so the query the user reads back is the
625
+ // Text they are searching for and Backspace has one obvious meaning at position zero.
626
+ _requested = prefixed.mode;
627
+ _query = raw.slice(1);
628
+ } else {
629
+ _query = raw;
127
630
  }
128
631
  _selectedIndex = 0;
129
632
  renderOverlay();
633
+ void ensureFiles();
634
+ }
635
+
636
+ /** Drop the mode chip: back to the neutral picker, keeping what was typed. */
637
+ function clearMode() {
638
+ _requested = "picker";
639
+ _pending = null;
640
+ _selectedIndex = 0;
641
+ renderOverlay();
130
642
  }
131
643
 
132
644
  function onKeydown(e: KeyboardEvent) {
133
- const { items } = currentItems();
645
+ const { rows } = currentRows();
134
646
  switch (e.key) {
135
647
  case "ArrowDown": {
136
648
  e.preventDefault();
137
- _selectedIndex = Math.min(_selectedIndex + 1, items.length - 1);
649
+ _selectedIndex = Math.min(_selectedIndex + 1, rows.length - 1);
138
650
  renderOverlay();
139
651
  break;
140
652
  }
@@ -146,8 +658,17 @@ function onKeydown(e: KeyboardEvent) {
146
658
  }
147
659
  case "Enter": {
148
660
  e.preventDefault();
149
- if (items[_selectedIndex]) {
150
- selectItem(items[_selectedIndex]!);
661
+ if (rows[_selectedIndex]) {
662
+ selectRow(rows[_selectedIndex]!);
663
+ }
664
+ break;
665
+ }
666
+ case "Backspace": {
667
+ // At position zero the chip is the thing to the left of the caret, so this is the same
668
+ // Gesture as deleting a character — and it is why the chip is removable at all.
669
+ if (_query === "" && (_pending || _requested !== "picker")) {
670
+ e.preventDefault();
671
+ clearMode();
151
672
  }
152
673
  break;
153
674
  }
@@ -162,16 +683,88 @@ function onKeydown(e: KeyboardEvent) {
162
683
  }
163
684
  }
164
685
 
165
- function selectItem(item: QuickItem) {
686
+ function enterMode(mode: PaletteMode) {
687
+ _requested = mode;
688
+ _query = "";
689
+ _selectedIndex = 0;
690
+ renderOverlay();
691
+ void ensureFiles();
692
+ }
693
+
694
+ function runCommand(command: AnyCommand, args?: Record<string, unknown>) {
695
+ const registry = activeRegistry();
696
+ if (!registry) {
697
+ return;
698
+ }
699
+ trackRecentCommand(command.id);
166
700
  closeQuickSearch();
167
- if (item.kind === "project") {
168
- void _ctx?.openRecentProject(item.root);
701
+ const report = (error: unknown) => {
702
+ console.error(`palette: command "${command.id}" failed`, error);
703
+ };
704
+ // Both shapes, deliberately: `run` may throw synchronously (a coercion refusal from
705
+ // `command-args.ts` does) or reject later. A palette that let either escape would take the
706
+ // Keydown listener down with it, and the overlay is already closed by then.
707
+ try {
708
+ void Promise.resolve(registry.run(command.id, args)).catch(report);
709
+ } catch (error) {
710
+ report(error);
711
+ }
712
+ }
713
+
714
+ function selectRow(row: PaletteRow) {
715
+ switch (row.kind) {
716
+ case "project": {
717
+ closeQuickSearch();
718
+ void _ctx?.openRecentProject(row.root);
719
+ return;
720
+ }
721
+ case "file": {
722
+ closeQuickSearch();
723
+ trackRecentFile({ name: row.name, path: row.path, root: scopeRoot() ?? "" });
724
+ void openFileInTab(row.path);
725
+ return;
726
+ }
727
+ case "mode": {
728
+ enterMode(row.mode);
729
+ return;
730
+ }
731
+ case "node": {
732
+ const registry = activeRegistry();
733
+ closeQuickSearch();
734
+ void registry?.run("selection.set", { path: row.path });
735
+ return;
736
+ }
737
+ case "arg": {
738
+ runCommand(_pending!.command, { [row.argName]: row.value });
739
+ return;
740
+ }
741
+ default: {
742
+ selectCommandRow(row);
743
+ }
744
+ }
745
+ }
746
+
747
+ function selectCommandRow(row: CommandRow) {
748
+ const registry = activeRegistry();
749
+ const command = registry?.get(row.id);
750
+ if (!registry || !command || !row.enabled) {
751
+ // A greyed row is not a dead one: it stays on screen with its reason, and pressing Enter on it
752
+ // Does nothing rather than closing the palette on a refusal the user never asked to see.
753
+ return;
754
+ }
755
+ const args = paletteArgs(command);
756
+ if (args.kind === "choice") {
757
+ _pending = { choices: args.choices, command, name: args.name };
758
+ _query = "";
759
+ _selectedIndex = 0;
760
+ renderOverlay();
169
761
  return;
170
762
  }
171
- trackRecentFile({ name: item.name, path: item.path, root: scopeRoot() ?? "" });
172
- void openFileInTab(item.path);
763
+ runCommand(command);
173
764
  }
174
765
 
766
+ // ─── Rendering ────────────────────────────────────────────────────────────────
767
+
175
768
  function fileIcon(name: string) {
176
769
  const ext = name.split(".").pop()?.toLowerCase();
177
770
  if (ext === "json") {
@@ -183,18 +776,84 @@ function fileIcon(name: string) {
183
776
  return html`<sp-icon-document size="s"></sp-icon-document>`;
184
777
  }
185
778
 
186
- function dirPart(path: string) {
187
- const parts = path.split("/");
188
- parts.pop();
189
- return parts.length > 0 ? parts.join("/") : "";
779
+ function rowIcon(row: PaletteRow) {
780
+ switch (row.kind) {
781
+ case "project": {
782
+ return html`<sp-icon-folder-open size="s"></sp-icon-folder-open>`;
783
+ }
784
+ case "file": {
785
+ return fileIcon(row.name);
786
+ }
787
+ case "command": {
788
+ return html`<sp-icon-play size="s"></sp-icon-play>`;
789
+ }
790
+ case "node": {
791
+ return html`<sp-icon-layers size="s"></sp-icon-layers>`;
792
+ }
793
+ default: {
794
+ return html`<sp-icon-chevron-right size="s"></sp-icon-chevron-right>`;
795
+ }
796
+ }
190
797
  }
191
798
 
192
- /** Collapse a home-prefixed absolute path for compact display. */
193
- function shortenPath(path: string) {
194
- if (path.startsWith("/home/")) {
195
- return `~/${path.split("/").slice(3).join("/")}`;
799
+ /** The right-aligned cell: a chord, a prefix, or the recent badge. */
800
+ function rowTrailing(row: PaletteRow, showingRecent: boolean) {
801
+ if (row.kind === "command" && row.chord) {
802
+ return html`<kbd class="palette-chord">${row.chord}</kbd>`;
803
+ }
804
+ if (row.kind === "mode" && row.chord) {
805
+ return html`<kbd class="palette-chord">${row.chord}</kbd>`;
806
+ }
807
+ if (row.kind === "command" && row.recent) {
808
+ return html`<span class="quick-search-badge">recent</span>`;
809
+ }
810
+ return showingRecent ? html`<span class="quick-search-badge">recent</span>` : nothing;
811
+ }
812
+
813
+ /** The chip echoing the active mode (or the command awaiting an argument), with its remove button. */
814
+ function chipTemplate(mode: PaletteMode) {
815
+ const label = _pending ? _pending.command.title : modeSpec(mode).chip;
816
+ if (!_pending && mode === "picker") {
817
+ return nothing;
818
+ }
819
+ return html`
820
+ <span class="palette-chip">
821
+ ${label}
822
+ <button
823
+ class="palette-chip-remove"
824
+ type="button"
825
+ title="Clear mode (Backspace)"
826
+ aria-label="Clear mode"
827
+ @click=${clearMode}
828
+ >
829
+ ×
830
+ </button>
831
+ </span>
832
+ `;
833
+ }
834
+
835
+ /** The sentence an empty result set shows. Never a noun phrase (§2 principle 6). */
836
+ function emptyHint(mode: PaletteMode, query: string): string {
837
+ if (_loading) {
838
+ return "Reading the project's files…";
839
+ }
840
+ if (query.trim() !== "") {
841
+ return "No results";
842
+ }
843
+ switch (mode) {
844
+ case "projects": {
845
+ return "No recent projects — open one to get started";
846
+ }
847
+ case "nodes": {
848
+ return "Open a document to jump to its elements";
849
+ }
850
+ case "commands": {
851
+ return "Type to find any command in Studio";
852
+ }
853
+ default: {
854
+ return modeSpec(mode).placeholder;
855
+ }
196
856
  }
197
- return path;
198
857
  }
199
858
 
200
859
  function renderOverlay() {
@@ -204,73 +863,83 @@ function renderOverlay() {
204
863
  return;
205
864
  }
206
865
 
207
- const hasProject = projectState != null;
208
- const { items, showingRecent } = currentItems();
209
- const hasQuery = _query.trim().length > 0;
210
-
211
- const placeholder = hasProject ? "Search project files…" : "Open a recent project…";
212
- const sectionLabel = hasProject ? "Recently opened" : "Recent projects";
213
- const emptyHint = hasProject
214
- ? "Type to search project files"
215
- : "No recent projects — open one to get started";
866
+ const { mode, query } = current();
867
+ const { rows, showingRecent } = currentRows();
868
+ const spec = modeSpec(mode);
869
+ const placeholder = _pending ? `Choose a ${_pending.name}…` : spec.placeholder;
870
+ const sectionLabel = _pending
871
+ ? `${_pending.command.title} — ${_pending.name}`
872
+ : mode === "projects"
873
+ ? "Recent projects"
874
+ : mode === "commands"
875
+ ? "Recently used"
876
+ : "Recently opened";
216
877
 
217
878
  const tpl = html`
218
879
  <div class="quick-search-overlay" @click=${closeQuickSearch}>
219
880
  <div class="quick-search-panel" @click=${(e: Event) => e.stopPropagation()}>
220
- <input
221
- class="quick-search-input"
222
- type="text"
223
- placeholder=${placeholder}
224
- .value=${live(_query)}
225
- @input=${onInput}
226
- @keydown=${onKeydown}
227
- ${ref((el) => {
228
- if (el) {
229
- requestAnimationFrame(() => (el as HTMLInputElement).focus());
230
- }
231
- })}
232
- />
233
- <div class="quick-search-results">
234
- ${
235
- items.length === 0 && hasQuery
236
- ? html`<div class="quick-search-empty">No results</div>`
237
- : nothing
238
- }
881
+ <div class="palette-input-row">
882
+ ${chipTemplate(mode)}
883
+ <input
884
+ class="quick-search-input"
885
+ type="text"
886
+ role="combobox"
887
+ aria-controls=${QUICK_SEARCH_LISTBOX_ID}
888
+ aria-expanded=${rows.length > 0}
889
+ aria-activedescendant=${rows.length > 0 ? optionId(_selectedIndex) : nothing}
890
+ aria-autocomplete="list"
891
+ aria-label=${placeholder}
892
+ placeholder=${placeholder}
893
+ .value=${live(_query)}
894
+ @input=${onInput}
895
+ @keydown=${onKeydown}
896
+ ${ref((el) => {
897
+ if (el) {
898
+ requestAnimationFrame(() => (el as HTMLInputElement).focus());
899
+ }
900
+ })}
901
+ />
902
+ </div>
903
+ <div class="quick-search-results" role="listbox" id=${QUICK_SEARCH_LISTBOX_ID}>
239
904
  ${
240
- items.length === 0 && !hasQuery
241
- ? html`<div class="quick-search-empty">${emptyHint}</div>`
905
+ rows.length === 0
906
+ ? html`<div class="quick-search-empty">${emptyHint(mode, query)}</div>`
242
907
  : nothing
243
908
  }
244
909
  ${
245
- showingRecent && items.length > 0
910
+ showingRecent && rows.length > 0
246
911
  ? html`<div class="quick-search-section-label">${sectionLabel}</div>`
247
912
  : nothing
248
913
  }
249
- ${items.map((item, i) => {
250
- const icon =
251
- item.kind === "project"
252
- ? html`<sp-icon-folder-open size="s"></sp-icon-folder-open>`
253
- : fileIcon(item.name);
254
- const pathText = item.kind === "project" ? shortenPath(item.root) : dirPart(item.path);
255
- return html`
914
+ ${rows.map(
915
+ (row, i) => html`
256
916
  <div
257
917
  class=${classMap({
258
918
  "quick-search-item": true,
919
+ disabled: row.kind === "command" && !row.enabled,
259
920
  selected: i === _selectedIndex,
260
921
  })}
261
- @click=${() => selectItem(item)}
922
+ role="option"
923
+ id=${optionId(i)}
924
+ aria-selected=${i === _selectedIndex}
925
+ aria-disabled=${row.kind === "command" && !row.enabled}
926
+ @click=${() => selectRow(row)}
262
927
  @mouseenter=${() => {
263
928
  _selectedIndex = i;
264
929
  renderOverlay();
265
930
  }}
266
931
  >
267
- <span class="quick-search-icon">${icon}</span>
268
- <span class="quick-search-name">${item.name}</span>
269
- <span class="quick-search-path">${pathText}</span>
270
- ${showingRecent ? html`<span class="quick-search-badge">recent</span>` : nothing}
932
+ <span class="quick-search-icon">${rowIcon(row)}</span>
933
+ <span class="quick-search-name">${row.name}</span>
934
+ <span class="quick-search-path">${row.detail}</span>
935
+ ${rowTrailing(row, showingRecent)}
271
936
  </div>
272
- `;
273
- })}
937
+ `,
938
+ )}
939
+ </div>
940
+ <div class="palette-hint">
941
+ <kbd>↑↓</kbd> move · <kbd>↵</kbd> run · <kbd>?</kbd> modes · <kbd>&gt;</kbd> commands ·
942
+ <kbd>@</kbd> elements
274
943
  </div>
275
944
  </div>
276
945
  </div>