@jxsuite/studio 2.0.0 → 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-0hzentwq.js → components-2bpqp4mq.js} +5 -7
  7. package/dist/chunks/{components-0hzentwq.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-2c188n41.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-97qcgvdn.js → studio-adn0z1k3.js} +14 -8
  74. package/dist/chunks/{studio-97qcgvdn.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-hhj92gcr.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-ygzav7z5.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-n9zmpxjg.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 +23711 -35185
  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 +34 -9
  214. package/src/new-project/location-fields.ts +9 -1
  215. package/src/new-project/new-project-modal.ts +270 -262
  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 -120
  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 +75 -2
  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 +113 -0
  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-qct0gt8f.js +0 -531
  363. package/dist/chunks/extension-sections-qct0gt8f.js.map +0 -12
  364. package/dist/chunks/files-02zn5y24.js +0 -50
  365. package/dist/chunks/files-02zn5y24.js.map +0 -9
  366. package/dist/chunks/git-panel-mbb0y0hz.js +0 -28
  367. package/dist/chunks/git-panel-mbb0y0hz.js.map +0 -9
  368. package/dist/chunks/layers-panel-9qekmqk1.js +0 -26
  369. package/dist/chunks/layers-panel-9qekmqk1.js.map +0 -9
  370. package/dist/chunks/settings-modal-gj0hrah4.js +0 -29
  371. package/dist/chunks/settings-modal-gj0hrah4.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-4k0m4fmp.js.map +0 -13
  381. package/dist/chunks/studio-568nx4t5.js +0 -469
  382. package/dist/chunks/studio-568nx4t5.js.map +0 -14
  383. package/dist/chunks/studio-9smfzqhc.js +0 -19
  384. package/dist/chunks/studio-9smfzqhc.js.map +0 -10
  385. package/dist/chunks/studio-asvf0j9e.js +0 -429
  386. package/dist/chunks/studio-asvf0j9e.js.map +0 -13
  387. package/dist/chunks/studio-d77qm3pk.js +0 -3427
  388. package/dist/chunks/studio-d77qm3pk.js.map +0 -26
  389. package/dist/chunks/studio-j6gqyjf4.js +0 -1854
  390. package/dist/chunks/studio-j6gqyjf4.js.map +0 -18
  391. package/dist/chunks/studio-n9zmpxjg.js.map +0 -115
  392. package/dist/chunks/studio-qkvt37c0.js.map +0 -9
  393. package/dist/chunks/studio-rq0vxb19.js +0 -3744
  394. package/dist/chunks/studio-rq0vxb19.js.map +0 -22
  395. package/dist/chunks/studio-wd6ewdvd.js +0 -853
  396. package/dist/chunks/studio-wd6ewdvd.js.map +0 -12
  397. package/dist/chunks/studio-xq0t7n5t.js +0 -140
  398. package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
  399. package/dist/chunks/studio-xze4zrs1.js.map +0 -10
  400. package/dist/chunks/studio-ygzav7z5.js.map +0 -46
  401. package/dist/chunks/studio-zmy7241e.js +0 -399
  402. package/dist/chunks/studio-zmy7241e.js.map +0 -12
  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-2c188n41.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-hhj92gcr.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,3427 +0,0 @@
1
- import {
2
- UPLOAD_ACCEPT,
3
- flushCanvasEdits,
4
- isImage,
5
- rectOf,
6
- setEditZoom,
7
- uploadAssets,
8
- validateComponentSlots
9
- } from "./studio-ygzav7z5.js";
10
- import {
11
- loadComponentRegistry
12
- } from "./studio-hhj92gcr.js";
13
- import {
14
- classMap
15
- } from "./studio-cx91mnh8.js";
16
- import {
17
- autoSyncProjectOnOpen,
18
- maybePromptJxsuiteUpdate,
19
- showProgressModal
20
- } from "./studio-568nx4t5.js";
21
- import {
22
- applyPanelCollapse,
23
- errorMessage,
24
- openModal,
25
- renderPopover,
26
- showConfirmDialog,
27
- showPromptDialog,
28
- statusMessage,
29
- view
30
- } from "./studio-asvf0j9e.js";
31
- import {
32
- activateTab,
33
- activeTab,
34
- closeAllTabs,
35
- collabSave,
36
- collabState,
37
- createState,
38
- effect,
39
- findContentTypeSchema,
40
- isCollabPath,
41
- onScopeDispose,
42
- openTab,
43
- projectState,
44
- reactive,
45
- renameTab,
46
- renderOnly,
47
- replaceAllTabs,
48
- requireProjectState,
49
- setHistoryDelegate,
50
- setProjectState,
51
- setWorkspaceProject,
52
- toRaw,
53
- updateUi,
54
- workspace
55
- } from "./studio-rq0vxb19.js";
56
- import {
57
- ref
58
- } from "./studio-4k0m4fmp.js";
59
- import {
60
- html,
61
- nothing
62
- } from "./studio-5x5774ak.js";
63
- import {
64
- defaultContentFormat,
65
- documentExtensions,
66
- formatByName,
67
- formatForPath,
68
- formatParse,
69
- formatSerialize,
70
- loadFormats,
71
- noFormatError,
72
- refreshExtensionUi,
73
- refreshFormats,
74
- splitFormatDocument
75
- } from "./studio-97qcgvdn.js";
76
- import {
77
- getPlatform,
78
- hasPlatform
79
- } from "./studio-9smfzqhc.js";
80
- import {
81
- combine,
82
- draggable,
83
- dropTargetForElements,
84
- monitorForElements
85
- } from "./studio-kr88ryr7.js";
86
- import {
87
- __require
88
- } from "./studio-md4gps17.js";
89
-
90
- // src/project-list.ts
91
- var cache = [];
92
- function platformListsProjects() {
93
- return hasPlatform() && typeof getPlatform().listProjects === "function";
94
- }
95
- async function hydrateProjectList() {
96
- if (!platformListsProjects()) {
97
- cache = [];
98
- return;
99
- }
100
- try {
101
- cache = await getPlatform().listProjects?.() ?? [];
102
- } catch {
103
- cache = [];
104
- }
105
- }
106
- function getProjectList() {
107
- return cache;
108
- }
109
- function seedProjectList(entries) {
110
- cache = entries;
111
- }
112
-
113
- // src/services/automation.ts
114
- function shouldInstallAutomation(search) {
115
- return new URLSearchParams(search).get("automation") === "1";
116
- }
117
- function createAutomationApi(deps) {
118
- return {
119
- editDef(defName) {
120
- updateUi("editingFunction", { defName, type: "def" });
121
- deps.render();
122
- },
123
- editFunction(path, eventKey) {
124
- updateUi("editingFunction", { eventKey, path, type: "event" });
125
- deps.render();
126
- },
127
- openBrowse() {
128
- deps.openBrowseModal();
129
- },
130
- openDataGrid(options) {
131
- deps.openConnectorGrid(options.connection, options.table);
132
- },
133
- openNewProject() {
134
- deps.openNewProjectModal();
135
- },
136
- openQuickSearch() {
137
- deps.openQuickSearchPalette();
138
- },
139
- openSettings(section) {
140
- deps.openSettingsModal(section);
141
- },
142
- seedAssistant(options) {
143
- deps.seedAssistantMessages(options.messages);
144
- },
145
- seedCollab(options) {
146
- const tab = activeTab.value;
147
- if (!tab) {
148
- return;
149
- }
150
- const state = collabState(tab);
151
- state.status = "synced";
152
- state.active = true;
153
- state.peers = options.peers.map((peer) => ({
154
- ...peer,
155
- state: {
156
- ...peer.state,
157
- focusedPath: peer.state.focusedPath ?? tab.documentPath
158
- }
159
- }));
160
- deps.render();
161
- },
162
- seedPublish(options) {
163
- deps.seedPublishConnected(options);
164
- },
165
- showWelcome(options) {
166
- closeAllTabs();
167
- setProjectState(null);
168
- if (options?.projects) {
169
- seedProjectList(options.projects);
170
- }
171
- view.chatPanelCollapsed = true;
172
- applyPanelCollapse();
173
- deps.render();
174
- },
175
- getState() {
176
- return {
177
- activeTabId: workspace.activeTabId,
178
- canvasMode: deps.getCanvasMode(),
179
- canvasStatus: activeTab.value?.session.canvas.status ?? null,
180
- leftTab: view.leftTab
181
- };
182
- },
183
- select(path) {
184
- const tab = activeTab.value;
185
- if (tab) {
186
- tab.session.selection = path;
187
- }
188
- deps.render();
189
- },
190
- setActivity(tab) {
191
- view.leftTab = tab;
192
- view.leftPanelCollapsed = false;
193
- applyPanelCollapse();
194
- renderOnly("leftPanel");
195
- deps.renderActivityBar();
196
- },
197
- setCanvasMode(mode) {
198
- deps.setCanvasMode(mode);
199
- deps.render();
200
- },
201
- setRightTab(tab) {
202
- if (tab === "assistant") {
203
- view.chatPanelCollapsed = false;
204
- applyPanelCollapse();
205
- renderOnly("chatPanel");
206
- deps.render();
207
- return;
208
- }
209
- updateUi("rightTab", tab);
210
- deps.render();
211
- },
212
- setStatus(text) {
213
- deps.statusMessage(text);
214
- },
215
- setTheme(color) {
216
- document.querySelector("sp-theme")?.setAttribute("color", color);
217
- },
218
- setZoom(zoom) {
219
- updateUi("zoom", zoom);
220
- deps.render();
221
- },
222
- setEditZoom(zoom) {
223
- setEditZoom(zoom);
224
- },
225
- waitForCanvasReady(timeoutMs = 30000) {
226
- return new Promise((resolve, reject) => {
227
- const started = Date.now();
228
- const tick = () => {
229
- if (activeTab.value?.session.canvas.status === "ready") {
230
- resolve();
231
- return;
232
- }
233
- if (Date.now() - started > timeoutMs) {
234
- reject(new Error(`canvas not ready after ${timeoutMs}ms`));
235
- return;
236
- }
237
- setTimeout(tick, 50);
238
- };
239
- tick();
240
- });
241
- }
242
- };
243
- }
244
- function installAutomationHook(deps) {
245
- if (!shouldInstallAutomation(location.search)) {
246
- return false;
247
- }
248
- globalThis.__jxAutomation = createAutomationApi(deps);
249
- return true;
250
- }
251
-
252
- // src/packages/ensure-deps.ts
253
- async function ensureDependenciesInstalled() {
254
- if (shouldInstallAutomation(location.search)) {
255
- return;
256
- }
257
- const platform = getPlatform();
258
- if (!platform.dependenciesNeedInstall || !platform.installDependencies) {
259
- return;
260
- }
261
- let needs = false;
262
- try {
263
- needs = await platform.dependenciesNeedInstall();
264
- } catch {
265
- return;
266
- }
267
- if (!needs) {
268
- return;
269
- }
270
- const progress = showProgressModal({
271
- status: "Running bun install…",
272
- title: "Installing dependencies"
273
- });
274
- try {
275
- const result = await platform.installDependencies();
276
- if (result.ok) {
277
- progress.done();
278
- } else {
279
- progress.fail(result.log ?? "bun install failed");
280
- }
281
- } catch (error) {
282
- progress.fail(error instanceof Error ? error.message : String(error));
283
- }
284
- }
285
-
286
- // src/files/fs-events.ts
287
- var RECENT_MS = 1500;
288
- var recentLocal = new Map;
289
- var norm = (p) => p.replaceAll("\\", "/");
290
- function markLocalMutation(...paths) {
291
- const expiry = Date.now() + RECENT_MS;
292
- for (const p of paths) {
293
- if (p) {
294
- recentLocal.set(norm(p), expiry);
295
- }
296
- }
297
- }
298
- function isRecentLocal(path) {
299
- const key = norm(path);
300
- const expiry = recentLocal.get(key);
301
- if (expiry === undefined) {
302
- return false;
303
- }
304
- if (Date.now() > expiry) {
305
- recentLocal.delete(key);
306
- return false;
307
- }
308
- return true;
309
- }
310
- function parentDir(path) {
311
- const i = path.lastIndexOf("/");
312
- return i === -1 ? "." : path.slice(0, i);
313
- }
314
- function baseName(path) {
315
- const i = path.lastIndexOf("/");
316
- return i === -1 ? path : path.slice(i + 1);
317
- }
318
- function applyFsEvents(dirs, expanded, events) {
319
- const changedDirs = new Set;
320
- for (const ev of events) {
321
- const parent = parentDir(ev.path);
322
- if (ev.type === "add" || ev.type === "addDir") {
323
- const entries = dirs.get(parent);
324
- if (entries && !entries.some((e) => e.path === ev.path)) {
325
- entries.push({
326
- name: baseName(ev.path),
327
- path: ev.path,
328
- type: ev.isDir ? "directory" : "file"
329
- });
330
- changedDirs.add(parent);
331
- }
332
- if (ev.type === "addDir" && !dirs.has(ev.path)) {
333
- dirs.set(ev.path, []);
334
- }
335
- } else if (ev.type === "unlink" || ev.type === "unlinkDir") {
336
- const entries = dirs.get(parent);
337
- if (entries) {
338
- const idx = entries.findIndex((e) => e.path === ev.path);
339
- if (idx !== -1) {
340
- entries.splice(idx, 1);
341
- changedDirs.add(parent);
342
- }
343
- }
344
- if (ev.type === "unlinkDir") {
345
- dirs.delete(ev.path);
346
- const nestedDirs = [];
347
- for (const key of dirs.keys()) {
348
- if (key.startsWith(`${ev.path}/`)) {
349
- nestedDirs.push(key);
350
- }
351
- }
352
- for (const key of nestedDirs) {
353
- dirs.delete(key);
354
- }
355
- const staleExpanded = [];
356
- for (const key of expanded) {
357
- if (key === ev.path || key.startsWith(`${ev.path}/`)) {
358
- staleExpanded.push(key);
359
- }
360
- }
361
- for (const key of staleExpanded) {
362
- expanded.delete(key);
363
- }
364
- }
365
- }
366
- }
367
- return changedDirs;
368
- }
369
- function startFsSync(ctx) {
370
- const platform = getPlatform();
371
- if (!platform.subscribeFileEvents) {
372
- return () => {};
373
- }
374
- let pending = [];
375
- let timer = null;
376
- const flush = () => {
377
- timer = null;
378
- const state = projectState;
379
- const batch = pending.filter((e) => !isRecentLocal(e.path));
380
- pending = [];
381
- if (!state || batch.length === 0) {
382
- return;
383
- }
384
- const changedDirs = applyFsEvents(state.dirs, state.expanded, batch);
385
- if (ctx.onContentChange) {
386
- for (const ev of batch) {
387
- if (ev.type === "change") {
388
- ctx.onContentChange(ev.path);
389
- }
390
- }
391
- }
392
- if (changedDirs.size > 0) {
393
- ctx.renderLeftPanel();
394
- }
395
- };
396
- return platform.subscribeFileEvents((events) => {
397
- pending.push(...events);
398
- if (timer) {
399
- clearTimeout(timer);
400
- }
401
- timer = setTimeout(flush, 50);
402
- });
403
- }
404
-
405
- // src/services/data-service.ts
406
- function platformOrNull() {
407
- try {
408
- return getPlatform();
409
- } catch {
410
- return null;
411
- }
412
- }
413
- function dataSurfaceAvailable() {
414
- return typeof platformOrNull()?.dataRows === "function";
415
- }
416
- async function fetchConnections() {
417
- const platform = platformOrNull();
418
- if (typeof platform?.dataConnections !== "function") {
419
- return null;
420
- }
421
- return platform.dataConnections();
422
- }
423
- async function testConnection(connection) {
424
- const platform = platformOrNull();
425
- if (typeof platform?.dataConnectionTest !== "function") {
426
- return { error: "Connection tests are not supported by this backend", ok: false };
427
- }
428
- try {
429
- return await platform.dataConnectionTest(connection);
430
- } catch (error) {
431
- return { error: error instanceof Error ? error.message : String(error), ok: false };
432
- }
433
- }
434
- async function pushSchema(opts) {
435
- const platform = platformOrNull();
436
- if (typeof platform?.dataPush !== "function") {
437
- return { applied: false, errors: ["Schema push is not supported by this backend"], plan: [] };
438
- }
439
- try {
440
- return await platform.dataPush(opts);
441
- } catch (error) {
442
- return {
443
- applied: false,
444
- errors: [error instanceof Error ? error.message : String(error)],
445
- plan: []
446
- };
447
- }
448
- }
449
- function requireMember(name) {
450
- const platform = platformOrNull();
451
- const member = platform?.[name];
452
- if (member === undefined || member === null) {
453
- throw new Error(`The ${String(name)} surface is not supported by this backend`);
454
- }
455
- return member;
456
- }
457
- function fetchRows(query) {
458
- return requireMember("dataRows")(query);
459
- }
460
- function insertRow(req) {
461
- return requireMember("dataInsertRow")(req);
462
- }
463
- function updateRow(req) {
464
- return requireMember("dataUpdateRow")(req);
465
- }
466
- function deleteRow(req) {
467
- return requireMember("dataDeleteRow")(req);
468
- }
469
- function deriveSecretEnvName(sectionKey, entryKey, fieldKey) {
470
- const constant = (raw) => raw.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").replaceAll(/[^A-Za-z0-9]+/g, "_").replaceAll(/^_+|_+$/g, "").toUpperCase();
471
- const parts = [entryKey ?? sectionKey, fieldKey.replace(/Env$/, "")].map((part) => constant(part)).filter(Boolean);
472
- const name = parts.join("_") || "SECRET";
473
- return /^\d/.test(name) ? `JX_${name}` : name;
474
- }
475
-
476
- // src/grid/grid-source.ts
477
- function cellValuesEqual(a, b) {
478
- if (a === b) {
479
- return true;
480
- }
481
- if (Array.isArray(a) && Array.isArray(b)) {
482
- return a.length === b.length && a.every((v, i) => v === b[i]);
483
- }
484
- if (a !== null && b !== null && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
485
- return a.$ref === b.$ref;
486
- }
487
- return false;
488
- }
489
- var GRID_SCHEME = "grid://";
490
- function makeGridTabId(ref2) {
491
- switch (ref2.kind) {
492
- case "pages": {
493
- return `${GRID_SCHEME}pages`;
494
- }
495
- case "collection": {
496
- return `${GRID_SCHEME}collection/${encodeURIComponent(ref2.name)}`;
497
- }
498
- default: {
499
- return `${GRID_SCHEME}data/${encodeURIComponent(ref2.connection)}/${encodeURIComponent(ref2.table)}`;
500
- }
501
- }
502
- }
503
- function isGridTabId(id) {
504
- return id.startsWith(GRID_SCHEME);
505
- }
506
- function parseGridTabId(id) {
507
- if (!isGridTabId(id)) {
508
- return null;
509
- }
510
- const parts = id.slice(GRID_SCHEME.length).split("/");
511
- if (parts[0] === "pages" && parts.length === 1) {
512
- return { kind: "pages" };
513
- }
514
- if (parts[0] === "collection" && parts.length === 2 && parts[1]) {
515
- return { kind: "collection", name: decodeURIComponent(parts[1]) };
516
- }
517
- if (parts[0] === "data" && parts.length === 3 && parts[1] && parts[2]) {
518
- return {
519
- connection: decodeURIComponent(parts[1]),
520
- kind: "data",
521
- table: decodeURIComponent(parts[2])
522
- };
523
- }
524
- return null;
525
- }
526
- function gridTabLabel(id) {
527
- const ref2 = parseGridTabId(id);
528
- if (!ref2) {
529
- return null;
530
- }
531
- switch (ref2.kind) {
532
- case "pages": {
533
- return "Pages · grid";
534
- }
535
- case "collection": {
536
- return `${ref2.name} · grid`;
537
- }
538
- default: {
539
- return `${ref2.table} @ ${ref2.connection} · grid`;
540
- }
541
- }
542
- }
543
-
544
- // src/grid/edit-buffer.ts
545
- var _tempCounter = 0;
546
- function createEditBuffer(opts) {
547
- const coalesceMs = opts.coalesceMs ?? 800;
548
- const now = opts.now ?? (() => Date.now());
549
- const state = reactive({
550
- deletes: new Set,
551
- errors: new Map,
552
- inserts: new Map,
553
- pending: new Map,
554
- redoStack: [],
555
- stale: new Set,
556
- undoStack: []
557
- });
558
- let groupOps = null;
559
- const setError = (rowKey, field, message) => {
560
- const row = state.errors.get(rowKey);
561
- if (row) {
562
- row.set(field, message);
563
- } else {
564
- state.errors.set(rowKey, new Map([[field, message]]));
565
- }
566
- };
567
- const clearCellError = (rowKey, field) => {
568
- const row = state.errors.get(rowKey);
569
- row?.delete(field);
570
- if (row?.size === 0) {
571
- state.errors.delete(rowKey);
572
- }
573
- };
574
- const pushOp = (op) => {
575
- state.redoStack.length = 0;
576
- if (groupOps) {
577
- groupOps.push(op);
578
- return;
579
- }
580
- const last = state.undoStack.at(-1);
581
- if (op.kind === "cell" && last?.kind === "cell" && last.rowKey === op.rowKey && last.field === op.field && op.at - last.at <= coalesceMs) {
582
- last.after = op.after;
583
- last.at = op.at;
584
- return;
585
- }
586
- state.undoStack.push(op);
587
- };
588
- const writeCell = (rowKey, field, value) => {
589
- const insertCells = state.inserts.get(rowKey);
590
- if (insertCells) {
591
- if (value === null) {
592
- delete insertCells[field];
593
- } else {
594
- insertCells[field] = value;
595
- }
596
- return;
597
- }
598
- if (cellValuesEqual(value, opts.resolveBaseline(rowKey, field))) {
599
- const row2 = state.pending.get(rowKey);
600
- row2?.delete(field);
601
- if (row2?.size === 0) {
602
- state.pending.delete(rowKey);
603
- }
604
- return;
605
- }
606
- const row = state.pending.get(rowKey);
607
- if (row) {
608
- row.set(field, { value });
609
- } else {
610
- state.pending.set(rowKey, new Map([[field, { value }]]));
611
- }
612
- };
613
- const effectiveValue = (rowKey, field) => {
614
- const insertCells = state.inserts.get(rowKey);
615
- if (insertCells) {
616
- return insertCells[field] ?? null;
617
- }
618
- const pending = state.pending.get(rowKey)?.get(field);
619
- return pending ? pending.value : opts.resolveBaseline(rowKey, field);
620
- };
621
- const cellError = (rowKey, field) => {
622
- const row = state.errors.get(rowKey);
623
- return row?.get(field) ?? row?.get("*") ?? null;
624
- };
625
- const applyOp = (op, direction) => {
626
- switch (op.kind) {
627
- case "cell": {
628
- writeCell(op.rowKey, op.field, direction === "undo" ? op.before : op.after);
629
- break;
630
- }
631
- case "insert": {
632
- if (direction === "undo") {
633
- state.inserts.delete(op.tempKey);
634
- } else {
635
- state.inserts.set(op.tempKey, { ...op.cells });
636
- }
637
- break;
638
- }
639
- case "drop-insert": {
640
- if (direction === "undo") {
641
- state.inserts.set(op.tempKey, { ...op.cells });
642
- } else {
643
- state.inserts.delete(op.tempKey);
644
- }
645
- break;
646
- }
647
- case "delete": {
648
- if (direction === "undo") {
649
- state.deletes.delete(op.rowKey);
650
- } else {
651
- state.deletes.add(op.rowKey);
652
- }
653
- break;
654
- }
655
- default: {
656
- const ops = direction === "undo" ? op.ops.toReversed() : op.ops;
657
- for (const child of ops) {
658
- applyOp(child, direction);
659
- }
660
- break;
661
- }
662
- }
663
- };
664
- const beginGroup = () => {
665
- if (groupOps) {
666
- throw new Error("edit-buffer: groups may not nest");
667
- }
668
- groupOps = [];
669
- };
670
- const endGroup = () => {
671
- const ops = groupOps;
672
- groupOps = null;
673
- if (!ops) {
674
- return;
675
- }
676
- if (ops.length === 1) {
677
- state.undoStack.push(ops[0]);
678
- } else if (ops.length > 1) {
679
- state.undoStack.push({ kind: "group", ops });
680
- }
681
- };
682
- const pruneHistory = (dropRow) => {
683
- const keep = (op) => {
684
- switch (op.kind) {
685
- case "cell": {
686
- return dropRow(op.rowKey) ? null : op;
687
- }
688
- case "insert":
689
- case "drop-insert": {
690
- return dropRow(op.tempKey) ? null : op;
691
- }
692
- case "delete": {
693
- return dropRow(op.rowKey) ? null : op;
694
- }
695
- default: {
696
- const ops = op.ops.map((child) => keep(child)).filter((o) => o !== null);
697
- return ops.length > 0 ? { kind: "group", ops } : null;
698
- }
699
- }
700
- };
701
- state.undoStack = state.undoStack.map(keep).filter((o) => o !== null);
702
- state.redoStack = state.redoStack.map(keep).filter((o) => o !== null);
703
- };
704
- return {
705
- applyCommitResult(result) {
706
- const committedRows = new Set;
707
- for (const cell of result.cells) {
708
- if (cell.ok) {
709
- const row = state.pending.get(cell.rowKey);
710
- row?.delete(cell.field);
711
- if (row?.size === 0) {
712
- state.pending.delete(cell.rowKey);
713
- }
714
- clearCellError(cell.rowKey, cell.field);
715
- } else {
716
- setError(cell.rowKey, cell.field, cell.error ?? "Save failed");
717
- if (cell.stale) {
718
- state.stale.add(cell.rowKey);
719
- }
720
- }
721
- }
722
- for (const ins of result.inserts) {
723
- if (ins.ok) {
724
- state.inserts.delete(ins.tempKey);
725
- state.errors.delete(ins.tempKey);
726
- committedRows.add(ins.tempKey);
727
- } else {
728
- setError(ins.tempKey, "*", ins.error ?? "Insert failed");
729
- }
730
- }
731
- for (const del of result.deletes) {
732
- if (del.ok) {
733
- state.deletes.delete(del.rowKey);
734
- state.pending.delete(del.rowKey);
735
- state.errors.delete(del.rowKey);
736
- committedRows.add(del.rowKey);
737
- } else {
738
- setError(del.rowKey, "*", del.error ?? "Delete failed");
739
- if (del.stale) {
740
- state.stale.add(del.rowKey);
741
- }
742
- }
743
- }
744
- for (const row of committedRows) {
745
- state.stale.delete(row);
746
- }
747
- if (committedRows.size > 0) {
748
- pruneHistory((rowKey) => committedRows.has(rowKey));
749
- }
750
- },
751
- buildBatch() {
752
- const cells = [];
753
- for (const [rowKey, fields] of state.pending) {
754
- if (state.deletes.has(rowKey)) {
755
- continue;
756
- }
757
- for (const [field, cell] of fields) {
758
- cells.push({
759
- baseline: opts.resolveBaseline(rowKey, field),
760
- field,
761
- rowKey,
762
- value: cell.value
763
- });
764
- }
765
- }
766
- return {
767
- cells,
768
- deletes: [...state.deletes].map((rowKey) => ({ rowKey })),
769
- inserts: [...state.inserts].map(([tempKey, insertCells]) => ({
770
- cells: { ...insertCells },
771
- tempKey
772
- }))
773
- };
774
- },
775
- canRedo: () => state.redoStack.length > 0,
776
- canUndo: () => state.undoStack.length > 0,
777
- cellError,
778
- cellState(rowKey, field) {
779
- if (cellError(rowKey, field) !== null) {
780
- return "error";
781
- }
782
- if (state.stale.has(rowKey)) {
783
- return "stale";
784
- }
785
- if (state.inserts.has(rowKey) || state.pending.get(rowKey)?.has(field)) {
786
- return "dirty";
787
- }
788
- return "clean";
789
- },
790
- deleteRow(rowKey) {
791
- if (state.deletes.has(rowKey)) {
792
- return;
793
- }
794
- const insertCells = state.inserts.get(rowKey);
795
- if (insertCells) {
796
- const cells = { ...insertCells };
797
- state.inserts.delete(rowKey);
798
- state.errors.delete(rowKey);
799
- state.stale.delete(rowKey);
800
- pushOp({ cells, kind: "drop-insert", tempKey: rowKey });
801
- return;
802
- }
803
- state.deletes.add(rowKey);
804
- pushOp({ kind: "delete", rowKey });
805
- },
806
- dirtyCount() {
807
- let count = state.inserts.size + state.deletes.size;
808
- for (const [rowKey, fields] of state.pending) {
809
- if (!state.deletes.has(rowKey)) {
810
- count += fields.size;
811
- }
812
- }
813
- return count;
814
- },
815
- discardRow(rowKey) {
816
- state.pending.delete(rowKey);
817
- state.deletes.delete(rowKey);
818
- state.inserts.delete(rowKey);
819
- state.errors.delete(rowKey);
820
- state.stale.delete(rowKey);
821
- pruneHistory((key) => key === rowKey);
822
- },
823
- effectiveValue,
824
- beginGroup,
825
- endGroup,
826
- group(fn) {
827
- beginGroup();
828
- try {
829
- fn();
830
- } finally {
831
- endGroup();
832
- }
833
- },
834
- insertRow(cells = {}) {
835
- _tempCounter += 1;
836
- const tempKey = `__new-${_tempCounter}`;
837
- state.inserts.set(tempKey, { ...cells });
838
- pushOp({ cells: { ...cells }, kind: "insert", tempKey });
839
- return tempKey;
840
- },
841
- isDirty: () => state.pending.size > 0 || state.inserts.size > 0 || state.deletes.size > 0,
842
- isInsertKey: (rowKey) => state.inserts.has(rowKey),
843
- markStale(rowKeys) {
844
- for (const rowKey of rowKeys) {
845
- state.stale.add(rowKey);
846
- }
847
- },
848
- redo() {
849
- const op = state.redoStack.pop();
850
- if (!op) {
851
- return false;
852
- }
853
- applyOp(op, "redo");
854
- state.undoStack.push(op);
855
- return true;
856
- },
857
- reset() {
858
- state.pending.clear();
859
- state.inserts.clear();
860
- state.deletes.clear();
861
- state.errors.clear();
862
- state.stale.clear();
863
- state.undoStack.length = 0;
864
- state.redoStack.length = 0;
865
- },
866
- rowState(rowKey) {
867
- if (state.deletes.has(rowKey)) {
868
- return "pending-delete";
869
- }
870
- if (state.inserts.has(rowKey)) {
871
- return "pending-insert";
872
- }
873
- if (state.stale.has(rowKey)) {
874
- return "stale";
875
- }
876
- if (state.pending.has(rowKey) || state.errors.has(rowKey)) {
877
- return "dirty";
878
- }
879
- return "clean";
880
- },
881
- setCell(rowKey, field, value) {
882
- const before = effectiveValue(rowKey, field);
883
- if (cellValuesEqual(before, value)) {
884
- return;
885
- }
886
- writeCell(rowKey, field, value);
887
- pushOp({ after: value, at: now(), before, field, kind: "cell", rowKey });
888
- },
889
- state,
890
- undo() {
891
- const op = state.undoStack.pop();
892
- if (!op) {
893
- return false;
894
- }
895
- applyOp(op, "undo");
896
- state.redoStack.push(op);
897
- return true;
898
- }
899
- };
900
- }
901
-
902
- // src/grid/grid-controller.ts
903
- var ROW_KEY_FIELD = "__key";
904
- var _controllers = new WeakMap;
905
- function getGridController(tab) {
906
- return tab ? _controllers.get(toRaw(tab)) ?? null : null;
907
- }
908
- function createGridController(tab, source) {
909
- const state = reactive({
910
- columns: [],
911
- error: null,
912
- loading: false,
913
- query: {},
914
- rows: [],
915
- saving: false,
916
- total: 0
917
- });
918
- let rowByKey = new Map;
919
- const indexRows = (rows) => {
920
- rowByKey = new Map(rows.map((row) => [row.key, row]));
921
- };
922
- const buffer = createEditBuffer({
923
- resolveBaseline: (rowKey, field) => rowByKey.get(rowKey)?.cells[field] ?? null
924
- });
925
- const reloadRows = async () => {
926
- const result = await source.rows(state.query);
927
- state.rows = result.rows;
928
- state.total = result.total;
929
- indexRows(result.rows);
930
- };
931
- const requiredViolations = (batch) => {
932
- const required = state.columns.filter((c) => c.required && c.editable);
933
- if (required.length === 0) {
934
- return null;
935
- }
936
- const cells = batch.cells.filter((cell) => cell.value === null && required.some((c) => c.field === cell.field)).map((cell) => ({ error: "Required", field: cell.field, ok: false, rowKey: cell.rowKey }));
937
- const inserts = batch.inserts.filter((insert) => required.some((c) => insert.cells[c.field] === null || insert.cells[c.field] === undefined)).map((insert) => {
938
- const missing = required.filter((c) => insert.cells[c.field] === null || insert.cells[c.field] === undefined).map((c) => c.field).join(", ");
939
- return { error: `Missing required: ${missing}`, ok: false, tempKey: insert.tempKey };
940
- });
941
- if (cells.length === 0 && inserts.length === 0) {
942
- return null;
943
- }
944
- return { cells, deletes: [], inserts };
945
- };
946
- let viewBinding = null;
947
- const syncView = () => viewBinding?.refreshData();
948
- const controller = {
949
- addRow(cells = {}) {
950
- const tempKey = buffer.insertRow(cells);
951
- syncView();
952
- return tempKey;
953
- },
954
- bindView(view2) {
955
- viewBinding = view2;
956
- },
957
- buffer,
958
- deleteRows(rowKeys) {
959
- if (rowKeys.length === 0) {
960
- return;
961
- }
962
- buffer.group(() => {
963
- for (const rowKey of rowKeys) {
964
- buffer.deleteRow(rowKey);
965
- }
966
- });
967
- syncView();
968
- },
969
- discardRow(rowKey) {
970
- buffer.discardRow(rowKey);
971
- syncView();
972
- },
973
- replaceAll(find, replace) {
974
- if (find === "") {
975
- return 0;
976
- }
977
- const textColumns = state.columns.filter((column) => column.editable && (column.kind === "string" || column.kind === "text"));
978
- const rowKeys = [...state.rows.map((row) => row.key), ...buffer.state.inserts.keys()];
979
- let changed = 0;
980
- buffer.group(() => {
981
- for (const rowKey of rowKeys) {
982
- if (buffer.rowState(rowKey) === "pending-delete") {
983
- continue;
984
- }
985
- for (const column of textColumns) {
986
- const value = buffer.effectiveValue(rowKey, column.field);
987
- if (typeof value === "string" && value.includes(find)) {
988
- buffer.setCell(rowKey, column.field, value.replaceAll(find, replace));
989
- changed += 1;
990
- }
991
- }
992
- }
993
- });
994
- if (changed > 0) {
995
- syncView();
996
- }
997
- return changed;
998
- },
999
- effectiveRows() {
1000
- const plain = (value) => Array.isArray(value) ? [...value] : value !== null && typeof value === "object" ? { $ref: value.$ref } : value;
1001
- const rowObject = (key) => {
1002
- const cells = { [ROW_KEY_FIELD]: key };
1003
- for (const column of state.columns) {
1004
- cells[column.field] = plain(buffer.effectiveValue(key, column.field));
1005
- }
1006
- return cells;
1007
- };
1008
- const rows = state.rows.map((row) => rowObject(row.key));
1009
- for (const [tempKey] of buffer.state.inserts) {
1010
- rows.push(rowObject(tempKey));
1011
- }
1012
- return rows;
1013
- },
1014
- async load() {
1015
- state.loading = true;
1016
- state.error = null;
1017
- try {
1018
- state.columns = await source.columns();
1019
- await reloadRows();
1020
- } catch (error) {
1021
- state.error = errorMessage(error);
1022
- } finally {
1023
- state.loading = false;
1024
- }
1025
- syncView();
1026
- },
1027
- async refresh() {
1028
- if (buffer.isDirty()) {
1029
- const confirmed = await showConfirmDialog("Refresh Grid", `Discard ${buffer.dirtyCount()} pending change(s) and reload?`, { confirmLabel: "Discard & Reload", destructive: true });
1030
- if (!confirmed) {
1031
- return;
1032
- }
1033
- }
1034
- buffer.reset();
1035
- await source.refresh?.();
1036
- await this.load();
1037
- },
1038
- async save() {
1039
- if (state.saving) {
1040
- return;
1041
- }
1042
- const batch = buffer.buildBatch();
1043
- const changeCount = batch.cells.length + batch.inserts.length + batch.deletes.length;
1044
- if (changeCount === 0) {
1045
- statusMessage("No grid changes to save");
1046
- return;
1047
- }
1048
- const violations = requiredViolations(batch);
1049
- if (violations) {
1050
- buffer.applyCommitResult(violations);
1051
- statusMessage("Required cells are empty — fix the marked rows and save again", 5000);
1052
- return;
1053
- }
1054
- if (batch.deletes.length > 0) {
1055
- const confirmed = await showConfirmDialog("Delete Rows", `Save will permanently delete ${batch.deletes.length} row(s). Continue?`, { confirmLabel: "Delete & Save", destructive: true });
1056
- if (!confirmed) {
1057
- return;
1058
- }
1059
- }
1060
- const affectedRows = new Set(batch.cells.map((c) => c.rowKey)).size + batch.inserts.length + batch.deletes.length;
1061
- const progress = affectedRows > 5 ? showProgressModal({ title: "Saving grid" }) : null;
1062
- state.saving = true;
1063
- try {
1064
- const result = await source.commit(batch);
1065
- const cellKey = (rowKey, field) => `${rowKey}\x00${field}`;
1066
- const batchCells = new Map(batch.cells.map((c) => [cellKey(c.rowKey, c.field), c]));
1067
- for (const outcome of result.cells) {
1068
- const cell = batchCells.get(cellKey(outcome.rowKey, outcome.field));
1069
- if (outcome.ok && cell) {
1070
- const row = rowByKey.get(outcome.rowKey);
1071
- if (row) {
1072
- row.cells[outcome.field] = cell.value;
1073
- }
1074
- }
1075
- }
1076
- buffer.applyCommitResult(result);
1077
- const structural = result.inserts.some((r) => r.ok) || result.deletes.some((r) => r.ok);
1078
- if (structural) {
1079
- await reloadRows();
1080
- }
1081
- const okCount = result.cells.filter((r) => r.ok).length + result.inserts.filter((r) => r.ok).length + result.deletes.filter((r) => r.ok).length;
1082
- const failed = changeCount - okCount;
1083
- const staleCount = result.cells.filter((r) => r.stale).length + result.deletes.filter((r) => r.stale).length;
1084
- statusMessage(failed === 0 ? `Saved ${okCount} change(s)` : `Saved ${okCount} · ${failed} failed${staleCount ? ` (${staleCount} stale)` : ""} — kept pending`, failed === 0 ? 3000 : 6000);
1085
- } catch (error) {
1086
- statusMessage(`Save error: ${errorMessage(error)}`, 6000);
1087
- } finally {
1088
- state.saving = false;
1089
- progress?.done();
1090
- syncView();
1091
- }
1092
- },
1093
- serializeForSource() {
1094
- if (!source.serializeForSource) {
1095
- return null;
1096
- }
1097
- return source.serializeForSource(buffer.buildBatch());
1098
- },
1099
- async setQuery(query) {
1100
- state.query = query;
1101
- state.loading = true;
1102
- try {
1103
- await reloadRows();
1104
- } catch (error) {
1105
- state.error = errorMessage(error);
1106
- } finally {
1107
- state.loading = false;
1108
- }
1109
- syncView();
1110
- },
1111
- source,
1112
- state,
1113
- tab
1114
- };
1115
- tab.scope.run(() => {
1116
- effect(() => {
1117
- tab.doc.dirty = buffer.isDirty();
1118
- });
1119
- setHistoryDelegate(tab, {
1120
- canRedo: () => buffer.canRedo(),
1121
- canUndo: () => buffer.canUndo(),
1122
- redo: () => {
1123
- buffer.redo();
1124
- syncView();
1125
- },
1126
- undo: () => {
1127
- buffer.undo();
1128
- syncView();
1129
- }
1130
- });
1131
- const platform = getPlatform();
1132
- if (platform.subscribeFileEvents && source.backingPaths) {
1133
- const unsubscribe = platform.subscribeFileEvents((events) => {
1134
- const backing = source.backingPaths();
1135
- const hitKeys = new Set;
1136
- for (const event of events) {
1137
- if (event.isDir) {
1138
- continue;
1139
- }
1140
- const path = event.path.replaceAll("\\", "/");
1141
- const rowKey = backing.get(path);
1142
- if (rowKey !== undefined && !isRecentLocal(path)) {
1143
- hitKeys.add(rowKey);
1144
- }
1145
- }
1146
- if (hitKeys.size === 0) {
1147
- return;
1148
- }
1149
- if (buffer.isDirty()) {
1150
- const keys = hitKeys.has("*") ? state.rows.map((row) => row.key) : [...hitKeys];
1151
- buffer.markStale(keys);
1152
- statusMessage("Grid rows changed on disk — marked stale (refresh to reload)", 5000);
1153
- syncView();
1154
- } else {
1155
- controller.refresh();
1156
- }
1157
- });
1158
- onScopeDispose(unsubscribe);
1159
- }
1160
- onScopeDispose(() => {
1161
- setHistoryDelegate(tab, null);
1162
- _controllers.delete(toRaw(tab));
1163
- source.dispose?.();
1164
- });
1165
- });
1166
- _controllers.set(toRaw(tab), controller);
1167
- return controller;
1168
- }
1169
-
1170
- // src/grid/csv-codec.ts
1171
- function splitRecords(text) {
1172
- const records = [];
1173
- let fields = [];
1174
- let field = "";
1175
- let inQuotes = false;
1176
- let fieldStarted = false;
1177
- const endField = () => {
1178
- fields.push(field);
1179
- field = "";
1180
- fieldStarted = false;
1181
- };
1182
- const endRecord = () => {
1183
- endField();
1184
- if (fields.length === 1 && fields[0] === "") {
1185
- fields = [];
1186
- return;
1187
- }
1188
- records.push(fields);
1189
- fields = [];
1190
- };
1191
- for (let i = 0;i < text.length; i++) {
1192
- const ch = text[i];
1193
- if (inQuotes) {
1194
- if (ch === '"') {
1195
- if (text[i + 1] === '"') {
1196
- field += '"';
1197
- i += 1;
1198
- } else {
1199
- inQuotes = false;
1200
- }
1201
- } else {
1202
- field += ch;
1203
- }
1204
- continue;
1205
- }
1206
- if (ch === '"' && !fieldStarted) {
1207
- inQuotes = true;
1208
- fieldStarted = true;
1209
- } else if (ch === '"') {
1210
- field += ch;
1211
- } else if (ch === ",") {
1212
- endField();
1213
- } else if (ch === `
1214
- `) {
1215
- endRecord();
1216
- } else if (ch === "\r") {
1217
- if (text[i + 1] === `
1218
- `) {
1219
- i += 1;
1220
- }
1221
- endRecord();
1222
- } else {
1223
- field += ch;
1224
- fieldStarted = true;
1225
- }
1226
- }
1227
- if (field !== "" || fields.length > 0) {
1228
- endRecord();
1229
- }
1230
- return records;
1231
- }
1232
- function parseCsv(text) {
1233
- const crlf = (text.match(/\r\n/g) ?? []).length;
1234
- const bareLf = (text.match(/(?<!\r)\n/g) ?? []).length;
1235
- const eol = crlf > bareLf ? `\r
1236
- ` : `
1237
- `;
1238
- const trailingNewline = text.endsWith(`
1239
- `);
1240
- const records = splitRecords(text);
1241
- const headers = records[0] ?? [];
1242
- const rows = records.slice(1);
1243
- const width = Math.max(headers.length, ...rows.map((r) => r.length), 0);
1244
- while (headers.length < width) {
1245
- headers.push("");
1246
- }
1247
- for (const row of rows) {
1248
- while (row.length < width) {
1249
- row.push("");
1250
- }
1251
- }
1252
- return { eol, headers, rows, trailingNewline };
1253
- }
1254
- function encodeField(value) {
1255
- if (/[",\n\r]/.test(value)) {
1256
- return `"${value.replaceAll('"', '""')}"`;
1257
- }
1258
- return value;
1259
- }
1260
- function serializeCsv(doc) {
1261
- const lines = [doc.headers, ...doc.rows].map((fields) => fields.map((field) => encodeField(field)).join(","));
1262
- return lines.join(doc.eol) + (doc.trailingNewline ? doc.eol : "");
1263
- }
1264
-
1265
- // src/grid/schema-columns.ts
1266
- var KIND_WIDTHS = {
1267
- array: 220,
1268
- boolean: 70,
1269
- date: 130,
1270
- enum: 140,
1271
- image: 180,
1272
- number: 110,
1273
- readonly: 200,
1274
- reference: 200,
1275
- string: 180,
1276
- text: 340
1277
- };
1278
- function clampWidth(maxLen) {
1279
- return Math.min(340, Math.max(90, Math.round(maxLen * 7.5)));
1280
- }
1281
- function kindForProp(prop) {
1282
- if (prop.$ref) {
1283
- return "reference";
1284
- }
1285
- if (prop.enum !== undefined && Array.isArray(prop.enum)) {
1286
- return "enum";
1287
- }
1288
- const format = prop.type === "array" ? undefined : prop.format;
1289
- if (format === "image") {
1290
- return "image";
1291
- }
1292
- if (format === "date" || format === "date-time") {
1293
- return "date";
1294
- }
1295
- switch (prop.type) {
1296
- case "number":
1297
- case "integer": {
1298
- return "number";
1299
- }
1300
- case "boolean": {
1301
- return "boolean";
1302
- }
1303
- case "array": {
1304
- return "array";
1305
- }
1306
- case "object": {
1307
- return "readonly";
1308
- }
1309
- default: {
1310
- return (prop.maxLength ?? 0) > 200 || prop.format === "markdown" ? "text" : "string";
1311
- }
1312
- }
1313
- }
1314
- function titleForField(field) {
1315
- const spaced = field.replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2").replaceAll(/[_-]+/g, " ").trim();
1316
- return spaced.charAt(0).toUpperCase() + spaced.slice(1);
1317
- }
1318
- function columnsFromSchema(schema, opts = {}) {
1319
- const props = Object.entries(schema?.properties ?? {});
1320
- const required = new Set(schema?.required);
1321
- const readonly = new Set(opts.readonlyFields);
1322
- const columns = props.map(([field, prop]) => {
1323
- const kind = readonly.has(field) ? "readonly" : kindForProp(prop);
1324
- const enumLen = Array.isArray(prop.enum) ? Math.max(...prop.enum.map((v) => String(v).length), 4) : 0;
1325
- return {
1326
- editable: kind !== "readonly",
1327
- field,
1328
- kind,
1329
- pk: field === opts.idField,
1330
- required: required.has(field),
1331
- schema: prop,
1332
- title: titleForField(field),
1333
- widthHint: kind === "enum" ? clampWidth(enumLen + 3) : KIND_WIDTHS[kind]
1334
- };
1335
- });
1336
- columns.sort((a, b) => {
1337
- const rank = (c) => c.pk ? 0 : c.required ? 1 : 2;
1338
- const diff = rank(a) - rank(b);
1339
- return diff === 0 ? 0 : diff;
1340
- });
1341
- return columns;
1342
- }
1343
- function inferColumnsFromRows(rows, sampleLimit = 50) {
1344
- const sample = rows.slice(0, sampleLimit);
1345
- const fields = [];
1346
- for (const row of sample) {
1347
- for (const key of Object.keys(row)) {
1348
- if (!fields.includes(key)) {
1349
- fields.push(key);
1350
- }
1351
- }
1352
- }
1353
- return fields.map((field) => {
1354
- const values = sample.map((row) => row[field]).filter((v) => v !== null && v !== undefined && v !== "");
1355
- const kind = inferKind(values);
1356
- const maxLen = Math.max(...values.map((v) => cellToText(v).length), field.length, 4);
1357
- return {
1358
- editable: true,
1359
- field,
1360
- kind,
1361
- title: titleForField(field),
1362
- widthHint: kind === "string" || kind === "text" ? clampWidth(maxLen) : KIND_WIDTHS[kind]
1363
- };
1364
- });
1365
- }
1366
- function inferKind(values) {
1367
- if (values.length === 0) {
1368
- return "string";
1369
- }
1370
- if (values.some((v) => Array.isArray(v))) {
1371
- return "array";
1372
- }
1373
- const texts = values.map(String);
1374
- if (values.every((v) => typeof v === "boolean") || texts.every((t) => /^(true|false)$/i.test(t))) {
1375
- return "boolean";
1376
- }
1377
- if (values.every((v) => typeof v === "number") || texts.every((t) => t.trim() !== "" && !Number.isNaN(Number(t.replaceAll(/[$€£¥,\s]/g, ""))))) {
1378
- return "number";
1379
- }
1380
- if (texts.every((t) => /^\d{4}-\d{2}-\d{2}([T ].*)?$/.test(t))) {
1381
- return "date";
1382
- }
1383
- if (texts.some((t) => t.length > 200)) {
1384
- return "text";
1385
- }
1386
- return "string";
1387
- }
1388
- function coerceCellInput(raw, col) {
1389
- if (raw === null || raw === undefined) {
1390
- return null;
1391
- }
1392
- switch (col.kind) {
1393
- case "number": {
1394
- if (typeof raw === "number") {
1395
- return Number.isNaN(raw) ? null : raw;
1396
- }
1397
- const cleaned = String(raw).trim().replaceAll(/[$€£¥,\s]/g, "");
1398
- if (cleaned === "") {
1399
- return null;
1400
- }
1401
- const n = Number(cleaned);
1402
- return Number.isNaN(n) ? null : n;
1403
- }
1404
- case "boolean": {
1405
- if (typeof raw === "boolean") {
1406
- return raw;
1407
- }
1408
- return String(raw).trim().toLowerCase() === "true";
1409
- }
1410
- case "array": {
1411
- if (Array.isArray(raw)) {
1412
- return raw.map(String);
1413
- }
1414
- const text = String(raw).trim();
1415
- return text === "" ? [] : text.split(",").map((s) => s.trim()).filter(Boolean);
1416
- }
1417
- case "reference": {
1418
- if (typeof raw === "object" && raw !== null && "$ref" in raw) {
1419
- const ref2 = raw.$ref;
1420
- return typeof ref2 === "string" && ref2 !== "" ? ref2 : null;
1421
- }
1422
- const text = String(raw).trim();
1423
- return text === "" ? null : text;
1424
- }
1425
- default: {
1426
- const text = String(raw);
1427
- return text === "" ? null : text;
1428
- }
1429
- }
1430
- }
1431
- function cellToText(value) {
1432
- if (value === null || value === undefined) {
1433
- return "";
1434
- }
1435
- if (Array.isArray(value)) {
1436
- return value.join(", ");
1437
- }
1438
- if (typeof value === "object") {
1439
- return value.$ref;
1440
- }
1441
- return String(value);
1442
- }
1443
-
1444
- // src/grid/sources/csv-file-source.ts
1445
- var ID_FIELDS = ["id", "sku", "slug", "Slug"];
1446
- function fieldNamesForHeaders(headers) {
1447
- const used = new Set;
1448
- return headers.map((header, i) => {
1449
- let name = header.trim() === "" ? `column${i + 1}` : header;
1450
- if (used.has(name)) {
1451
- name = `${name}__${i + 1}`;
1452
- }
1453
- used.add(name);
1454
- return name;
1455
- });
1456
- }
1457
- function pickIdField(fields, doc) {
1458
- for (const candidate of ID_FIELDS) {
1459
- const idx = fields.indexOf(candidate);
1460
- if (idx === -1) {
1461
- continue;
1462
- }
1463
- const values = doc.rows.map((row) => row[idx] ?? "");
1464
- if (values.every((v) => v !== "") && new Set(values).size === values.length) {
1465
- return candidate;
1466
- }
1467
- }
1468
- return null;
1469
- }
1470
- function buildModel(text, path) {
1471
- const doc = parseCsv(text);
1472
- const fields = fieldNamesForHeaders(doc.headers);
1473
- const idField = pickIdField(fields, doc);
1474
- const idIndex = idField ? fields.indexOf(idField) : -1;
1475
- const keys = doc.rows.map((row, i) => idIndex === -1 ? String(i) : row[idIndex]);
1476
- const keyToIndex = new Map(keys.map((key, i) => [key, i]));
1477
- const contentType = findContentTypeSchema(path, projectState?.projectConfig);
1478
- const schemaProps = contentType?.schema?.properties ?? {};
1479
- const sampleRows = doc.rows.slice(0, 50).map((row) => {
1480
- const cells = {};
1481
- for (const [i, field] of fields.entries()) {
1482
- cells[field] = row[i] ?? "";
1483
- }
1484
- return cells;
1485
- });
1486
- const inferred = new Map(inferColumnsFromRows(sampleRows).map((c) => [c.field, c]));
1487
- const typed = new Map(columnsFromSchema(contentType?.schema, { idField: idField ?? undefined }).map((c) => [
1488
- c.field,
1489
- c
1490
- ]));
1491
- const columns = fields.map((field, i) => {
1492
- const fromSchema = field in schemaProps ? typed.get(field) : undefined;
1493
- const column = fromSchema ?? inferred.get(field) ?? {
1494
- editable: true,
1495
- field,
1496
- kind: "string",
1497
- title: doc.headers[i] || field
1498
- };
1499
- return { ...column, pk: field === idField, title: doc.headers[i]?.trim() || column.title };
1500
- });
1501
- return { columns, doc, fields, keys, keyToIndex, loadText: text };
1502
- }
1503
- function rowCells(model, rowIndex) {
1504
- const cells = {};
1505
- const row = model.doc.rows[rowIndex] ?? [];
1506
- for (const [i, field] of model.fields.entries()) {
1507
- const column = model.columns[i];
1508
- const raw = row[i] ?? "";
1509
- cells[field] = column.kind === "string" || column.kind === "text" || column.kind === "readonly" ? raw : coerceCellInput(raw, column);
1510
- }
1511
- return cells;
1512
- }
1513
- function applyBatch(model, batch) {
1514
- const rows = model.doc.rows.map((row) => [...row]);
1515
- const colFor = (field) => model.fields.indexOf(field);
1516
- for (const cell of batch.cells) {
1517
- const rowIndex = model.keyToIndex.get(cell.rowKey);
1518
- const colIndex = colFor(cell.field);
1519
- if (rowIndex === undefined || colIndex === -1) {
1520
- continue;
1521
- }
1522
- rows[rowIndex][colIndex] = cellToText(cell.value);
1523
- }
1524
- for (const insert of batch.inserts) {
1525
- const row = model.fields.map((field) => cellToText(insert.cells[field] ?? null));
1526
- rows.push(row);
1527
- }
1528
- const dropIndexes = batch.deletes.map((del) => model.keyToIndex.get(del.rowKey)).filter((i) => i !== undefined).toSorted((a, b) => b - a);
1529
- for (const index of dropIndexes) {
1530
- rows.splice(index, 1);
1531
- }
1532
- return { ...model.doc, rows };
1533
- }
1534
- function uniformResult(batch, outcome) {
1535
- return {
1536
- cells: batch.cells.map((c) => ({ field: c.field, rowKey: c.rowKey, ...outcome })),
1537
- deletes: batch.deletes.map((d) => ({ rowKey: d.rowKey, ...outcome })),
1538
- inserts: batch.inserts.map((i) => ({
1539
- error: outcome.error,
1540
- ok: outcome.ok,
1541
- tempKey: i.tempKey
1542
- }))
1543
- };
1544
- }
1545
- function createCsvFileSource(path) {
1546
- let loadPromise = null;
1547
- const load = (force = false) => {
1548
- if (!force && loadPromise) {
1549
- return loadPromise;
1550
- }
1551
- loadPromise = getPlatform().readFile(path).then((text) => buildModel(text, path));
1552
- return loadPromise;
1553
- };
1554
- return {
1555
- backingPaths: () => new Map([[path, "*"]]),
1556
- capabilities: { delete: true, insert: true, remotePaging: false, remoteSort: false },
1557
- async columns() {
1558
- const current = await load();
1559
- return current.columns;
1560
- },
1561
- async commit(batch) {
1562
- const current = await load();
1563
- const platform = getPlatform();
1564
- let onDisk = null;
1565
- try {
1566
- onDisk = await platform.readFile(path);
1567
- } catch {
1568
- onDisk = null;
1569
- }
1570
- if (onDisk !== current.loadText && !isRecentLocal(path)) {
1571
- return uniformResult(batch, {
1572
- error: "File changed on disk — refresh to reload",
1573
- ok: false,
1574
- stale: true
1575
- });
1576
- }
1577
- const nextDoc = applyBatch(current, batch);
1578
- const text = serializeCsv(nextDoc);
1579
- markLocalMutation(path);
1580
- await platform.writeFile(path, text);
1581
- loadPromise = Promise.resolve(buildModel(text, path));
1582
- return uniformResult(batch, { ok: true });
1583
- },
1584
- id: path,
1585
- label: path.split("/").pop() ?? path,
1586
- async refresh() {
1587
- await load(true);
1588
- },
1589
- async rows() {
1590
- const current = await load();
1591
- const rows = current.doc.rows.map((_, i) => ({
1592
- cells: rowCells(current, i),
1593
- key: current.keys[i]
1594
- }));
1595
- return { rows, total: rows.length };
1596
- },
1597
- async serializeForSource(batch) {
1598
- const current = await load();
1599
- return serializeCsv(applyBatch(current, batch));
1600
- }
1601
- };
1602
- }
1603
-
1604
- // src/files/file-ops.ts
1605
- async function parseFormatSource(format, source) {
1606
- const doc = await formatParse(format.name, source);
1607
- return splitFormatDocument(format, doc);
1608
- }
1609
- async function parseSourceForPath(path, source) {
1610
- await loadFormats();
1611
- const format = formatForPath(path);
1612
- if (!format || !format.capabilities.parse) {
1613
- throw noFormatError(path);
1614
- }
1615
- const result = await parseFormatSource(format, source);
1616
- return { ...result, format };
1617
- }
1618
- function savedMessage(tab, savedMsg) {
1619
- const doc = tab.doc.document;
1620
- const warning = typeof doc.tagName === "string" && doc.tagName.includes("-") ? validateComponentSlots(doc) : null;
1621
- if (warning) {
1622
- statusMessage(`${savedMsg} — Warning: ${warning}`, 6000);
1623
- } else {
1624
- statusMessage(savedMsg);
1625
- }
1626
- }
1627
- async function saveFile() {
1628
- const tab = activeTab.value;
1629
- if (!tab) {
1630
- return;
1631
- }
1632
- await flushCanvasEdits(tab.id);
1633
- const grid = getGridController(tab);
1634
- if (grid) {
1635
- await grid.save();
1636
- return;
1637
- }
1638
- try {
1639
- if (await collabSave(tab)) {
1640
- savedMessage(tab, "Synced");
1641
- return;
1642
- }
1643
- const output = await serializeDocument(tab);
1644
- if (tab.documentPath) {
1645
- const platform = getPlatform();
1646
- await platform.writeFile(tab.documentPath, output);
1647
- tab.doc.dirty = false;
1648
- savedMessage(tab, "Saved");
1649
- } else if (tab.fileHandle && "createWritable" in tab.fileHandle) {
1650
- const writable = await tab.fileHandle.createWritable();
1651
- await writable.write(output);
1652
- await writable.close();
1653
- tab.doc.dirty = false;
1654
- savedMessage(tab, "Saved");
1655
- } else {
1656
- statusMessage("No save target — use Export");
1657
- }
1658
- } catch (error) {
1659
- if (!(error instanceof Error && error.name === "AbortError")) {
1660
- statusMessage(`Save error: ${errorMessage(error)}`);
1661
- }
1662
- }
1663
- }
1664
- function tabFormat(tab) {
1665
- return formatByName(tab.doc.sourceFormat) ?? (tab.doc.mode === "content" ? defaultContentFormat() : undefined);
1666
- }
1667
- async function exportFile() {
1668
- const tab = activeTab.value;
1669
- if (!tab) {
1670
- return;
1671
- }
1672
- try {
1673
- await loadFormats();
1674
- const format = tabFormat(tab);
1675
- const output = await serializeDocument(tab);
1676
- const mimeType = format ? format.mediaType ?? "text/plain" : "application/json";
1677
- const ext = format ? format.extensions[0] : ".json";
1678
- const description = format ? format.name : "Jx Component";
1679
- const fallbackName = format ? `content${ext}` : "component.json";
1680
- if ("showSaveFilePicker" in window) {
1681
- const suggestedName = tab.documentPath ? tab.documentPath.split("/").pop() : fallbackName;
1682
- const handle = await window.showSaveFilePicker({
1683
- suggestedName,
1684
- types: [{ accept: { [mimeType]: [ext] }, description }]
1685
- });
1686
- const writable = await handle.createWritable();
1687
- await writable.write(output);
1688
- await writable.close();
1689
- tab.doc.dirty = false;
1690
- savedMessage(tab, `Exported as ${handle.name}`);
1691
- } else {
1692
- const blob = new Blob([output], { type: mimeType });
1693
- const url = URL.createObjectURL(blob);
1694
- const a = document.createElement("a");
1695
- a.href = url;
1696
- a.download = fallbackName;
1697
- a.click();
1698
- URL.revokeObjectURL(url);
1699
- tab.doc.dirty = false;
1700
- savedMessage(tab, "Downloaded");
1701
- }
1702
- } catch (error) {
1703
- if (!(error instanceof Error && error.name === "AbortError")) {
1704
- statusMessage(`Export error: ${errorMessage(error)}`);
1705
- }
1706
- }
1707
- }
1708
- async function serializeDocument(tab) {
1709
- const grid = getGridController(tab);
1710
- const gridText = grid?.serializeForSource();
1711
- if (gridText) {
1712
- return gridText;
1713
- }
1714
- await loadFormats();
1715
- const sourceFormat = formatByName(tab.doc.sourceFormat);
1716
- if (sourceFormat?.capabilities.serialize) {
1717
- const fm = tab.doc.content?.frontmatter || {};
1718
- const doc = tab.doc.document;
1719
- const fullDoc = { ...fm, ...doc, children: doc.children ?? [] };
1720
- return formatSerialize(sourceFormat.name, fullDoc, { mode: "roundtrip" });
1721
- }
1722
- if (tab.doc.mode === "content") {
1723
- const format = defaultContentFormat();
1724
- if (format) {
1725
- const fm = tab.doc.content?.frontmatter ?? {};
1726
- const hasFrontmatter = Object.keys(fm).length > 0;
1727
- const fullDoc = { ...fm, ...tab.doc.document };
1728
- return formatSerialize(format.name, fullDoc, {
1729
- frontmatter: hasFrontmatter,
1730
- mode: "roundtrip"
1731
- });
1732
- }
1733
- }
1734
- return JSON.stringify(tab.doc.document, null, 2);
1735
- }
1736
-
1737
- // src/grid/sources/content-source.ts
1738
- var PATH_FIELD = "__path";
1739
- function collectionInfo(typeName) {
1740
- const content = projectState?.projectConfig?.content ?? {};
1741
- const def = content[typeName];
1742
- if (!def?.source) {
1743
- return null;
1744
- }
1745
- const dir = def.source.replace(/^\.\//, "").replace(/\/$/, "");
1746
- const ext = def.format === "json" ? ".json" : formatByName(def.format)?.extensions[0] ?? defaultContentFormat()?.extensions[0] ?? ".json";
1747
- return { def, dir, ext, name: typeName };
1748
- }
1749
- function collectionDirs() {
1750
- const content = projectState?.projectConfig?.content ?? {};
1751
- return Object.entries(content).filter(([, def]) => {
1752
- const source = def.source?.replace(/^\.\//, "") ?? "";
1753
- return source !== "" && !/\.[a-z0-9]+$/i.test(source);
1754
- }).map(([name, def]) => ({ dir: def.source.replace(/^\.\//, "").replace(/\/$/, ""), name }));
1755
- }
1756
- async function listEntryFiles(dir, exts) {
1757
- const platform = getPlatform();
1758
- const results = [];
1759
- const walk = async (current) => {
1760
- let children;
1761
- try {
1762
- children = await platform.listDirectory(current);
1763
- } catch {
1764
- return;
1765
- }
1766
- for (const entry of children) {
1767
- if (entry.type === "directory") {
1768
- await walk(entry.path);
1769
- } else if (exts.some((ext) => entry.path.endsWith(ext))) {
1770
- results.push(entry.path);
1771
- }
1772
- }
1773
- };
1774
- await walk(dir);
1775
- return results.toSorted();
1776
- }
1777
- async function listCollectionEntryIds(typeName) {
1778
- await loadFormats().catch(() => {});
1779
- const info = collectionInfo(typeName);
1780
- if (!info) {
1781
- return [];
1782
- }
1783
- const files = await listEntryFiles(info.dir, [info.ext]);
1784
- return files.map((path) => {
1785
- const base = path.split("/").pop() ?? path;
1786
- return base.slice(0, base.length - info.ext.length);
1787
- });
1788
- }
1789
- async function mapLimit(items, limit, fn) {
1790
- const results = Array.from({ length: items.length });
1791
- let next = 0;
1792
- const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
1793
- while (next < items.length) {
1794
- const index = next;
1795
- next += 1;
1796
- results[index] = await fn(items[index]);
1797
- }
1798
- });
1799
- await Promise.all(workers);
1800
- return results;
1801
- }
1802
- function toCellValue(value) {
1803
- if (value === null || value === undefined) {
1804
- return null;
1805
- }
1806
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1807
- return value;
1808
- }
1809
- if (Array.isArray(value) && value.every((v) => typeof v !== "object" || v === null)) {
1810
- return value.map(String);
1811
- }
1812
- return JSON.stringify(value);
1813
- }
1814
- function openTabFor(path) {
1815
- for (const tab of workspace.tabs.values()) {
1816
- if (tab.documentPath === path) {
1817
- return tab;
1818
- }
1819
- }
1820
- return null;
1821
- }
1822
- function extraRowsAreStrings(rows, field) {
1823
- return rows.every((row) => {
1824
- const value = row[field];
1825
- return value === null || value === undefined || typeof value !== "string" ? true : !value.startsWith("{") && !value.startsWith("[");
1826
- });
1827
- }
1828
- function createEntryFileSource(opts) {
1829
- const entries = new Map;
1830
- let loadPromise = null;
1831
- let columnsCache = null;
1832
- const load = (force = false) => {
1833
- if (!force && loadPromise) {
1834
- return loadPromise;
1835
- }
1836
- loadPromise = (async () => {
1837
- await loadFormats().catch(() => {});
1838
- const platform = getPlatform();
1839
- const files = await opts.list();
1840
- entries.clear();
1841
- columnsCache = null;
1842
- await mapLimit(files, 8, async (path) => {
1843
- try {
1844
- const text = await platform.readFile(path);
1845
- const parsed = await parseSourceForPath(path, text);
1846
- entries.set(path, {
1847
- document: parsed.document,
1848
- formatName: parsed.format.name,
1849
- frontmatter: parsed.frontmatter ?? {},
1850
- rawText: text
1851
- });
1852
- } catch {}
1853
- });
1854
- })();
1855
- return loadPromise;
1856
- };
1857
- const buildColumns = () => {
1858
- if (columnsCache) {
1859
- return columnsCache;
1860
- }
1861
- const pathColumn = {
1862
- editable: false,
1863
- field: PATH_FIELD,
1864
- insertOnly: true,
1865
- kind: "string",
1866
- pk: true,
1867
- required: true,
1868
- title: "Path",
1869
- widthHint: 220
1870
- };
1871
- const schemaColumns = columnsFromSchema(opts.schema());
1872
- const known = new Set([PATH_FIELD, ...schemaColumns.map((c) => c.field)]);
1873
- const extraRows = [...entries.values()].map((entry) => {
1874
- const cells = {};
1875
- for (const [key, value] of Object.entries(entry.frontmatter)) {
1876
- if (!known.has(key) && !key.startsWith("$")) {
1877
- cells[key] = toCellValue(value);
1878
- }
1879
- }
1880
- return cells;
1881
- });
1882
- const extraColumns = inferColumnsFromRows(extraRows);
1883
- for (const column of extraColumns) {
1884
- column.editable = column.kind !== "text" || extraRowsAreStrings(extraRows, column.field);
1885
- }
1886
- if (opts.priorityFields?.length) {
1887
- const rank = (c) => {
1888
- const index = opts.priorityFields.indexOf(c.field);
1889
- return index === -1 ? opts.priorityFields.length : index;
1890
- };
1891
- extraColumns.sort((a, b) => rank(a) - rank(b));
1892
- }
1893
- columnsCache = [pathColumn, ...schemaColumns, ...extraColumns];
1894
- return columnsCache;
1895
- };
1896
- const rowFor = (path, entry) => {
1897
- const cells = { [PATH_FIELD]: path };
1898
- for (const column of buildColumns()) {
1899
- if (column.field === PATH_FIELD) {
1900
- continue;
1901
- }
1902
- cells[column.field] = toCellValue(entry.frontmatter[column.field]);
1903
- }
1904
- return { cells, fingerprint: entry.rawText, key: path };
1905
- };
1906
- const writeEntry = async (path, entry, patches) => {
1907
- const platform = getPlatform();
1908
- const frontmatter = { ...entry.frontmatter };
1909
- for (const patch of patches) {
1910
- if (patch.value === null) {
1911
- delete frontmatter[patch.field];
1912
- } else {
1913
- frontmatter[patch.field] = patch.value;
1914
- }
1915
- }
1916
- const doc = entry.document;
1917
- const fullDoc = { ...frontmatter, ...doc, children: doc.children ?? [] };
1918
- const text = await formatSerialize(entry.formatName, fullDoc, { mode: "roundtrip" });
1919
- markLocalMutation(path);
1920
- await platform.writeFile(path, text);
1921
- entries.set(path, { ...entry, frontmatter, rawText: text });
1922
- };
1923
- return {
1924
- backingPaths: () => new Map([...entries.keys()].map((path) => [path, path])),
1925
- capabilities: { delete: true, insert: true, remotePaging: false, remoteSort: false },
1926
- async columns() {
1927
- await load();
1928
- return buildColumns();
1929
- },
1930
- async commit(batch) {
1931
- await load();
1932
- const platform = getPlatform();
1933
- const result = { cells: [], deletes: [], inserts: [] };
1934
- const byRow = new Map;
1935
- for (const cell of batch.cells) {
1936
- const list = byRow.get(cell.rowKey) ?? [];
1937
- list.push({ field: cell.field, value: cell.value });
1938
- byRow.set(cell.rowKey, list);
1939
- }
1940
- for (const [path, patches] of byRow) {
1941
- const outcome = (ok, error, stale) => {
1942
- for (const patch of patches) {
1943
- result.cells.push({ error, field: patch.field, ok, rowKey: path, stale });
1944
- }
1945
- };
1946
- const entry = entries.get(path);
1947
- if (!entry) {
1948
- outcome(false, "Entry no longer loaded — refresh the grid");
1949
- continue;
1950
- }
1951
- const openTab2 = openTabFor(path);
1952
- if (openTab2?.doc.dirty) {
1953
- outcome(false, "Open in a tab with unsaved changes — save or close it first");
1954
- continue;
1955
- }
1956
- let onDisk = null;
1957
- try {
1958
- onDisk = await platform.readFile(path);
1959
- } catch {
1960
- onDisk = null;
1961
- }
1962
- if (onDisk !== entry.rawText && !isRecentLocal(path)) {
1963
- outcome(false, "File changed on disk — refresh to reload", true);
1964
- continue;
1965
- }
1966
- try {
1967
- await writeEntry(path, entry, patches);
1968
- outcome(true);
1969
- if (openTab2) {
1970
- const { reloadFileInTab } = await import("./files-02zn5y24.js");
1971
- reloadFileInTab(path);
1972
- }
1973
- } catch (error) {
1974
- outcome(false, error instanceof Error ? error.message : String(error));
1975
- }
1976
- }
1977
- for (const insert of batch.inserts) {
1978
- const rawPath = insert.cells[PATH_FIELD];
1979
- if (typeof rawPath !== "string" || rawPath.trim() === "") {
1980
- result.inserts.push({ error: "Path is required", ok: false, tempKey: insert.tempKey });
1981
- continue;
1982
- }
1983
- const path = opts.resolveInsertPath(rawPath);
1984
- let exists = entries.has(path);
1985
- if (!exists) {
1986
- try {
1987
- await platform.readFile(path);
1988
- exists = true;
1989
- } catch {
1990
- exists = false;
1991
- }
1992
- }
1993
- if (exists) {
1994
- result.inserts.push({
1995
- error: `${path} already exists`,
1996
- ok: false,
1997
- tempKey: insert.tempKey
1998
- });
1999
- continue;
2000
- }
2001
- const formatName = opts.insertFormatName(path);
2002
- if (!formatName) {
2003
- result.inserts.push({
2004
- error: `No format can serialize ${path}`,
2005
- ok: false,
2006
- tempKey: insert.tempKey
2007
- });
2008
- continue;
2009
- }
2010
- try {
2011
- const frontmatter = {};
2012
- for (const [field, value] of Object.entries(insert.cells)) {
2013
- if (field !== PATH_FIELD && value !== null) {
2014
- frontmatter[field] = value;
2015
- }
2016
- }
2017
- const text = await formatSerialize(formatName, { ...frontmatter, children: [] }, { frontmatter: Object.keys(frontmatter).length > 0, mode: "roundtrip" });
2018
- markLocalMutation(path);
2019
- await platform.writeFile(path, text);
2020
- const parsed = await parseSourceForPath(path, text);
2021
- entries.set(path, {
2022
- document: parsed.document,
2023
- formatName: parsed.format.name,
2024
- frontmatter: parsed.frontmatter ?? {},
2025
- rawText: text
2026
- });
2027
- result.inserts.push({ newKey: path, ok: true, tempKey: insert.tempKey });
2028
- } catch (error) {
2029
- result.inserts.push({
2030
- error: error instanceof Error ? error.message : String(error),
2031
- ok: false,
2032
- tempKey: insert.tempKey
2033
- });
2034
- }
2035
- }
2036
- for (const del of batch.deletes) {
2037
- if (openTabFor(del.rowKey)) {
2038
- result.deletes.push({
2039
- error: "Open in a tab — close it first",
2040
- ok: false,
2041
- rowKey: del.rowKey
2042
- });
2043
- continue;
2044
- }
2045
- try {
2046
- markLocalMutation(del.rowKey);
2047
- await platform.deleteFile(del.rowKey);
2048
- entries.delete(del.rowKey);
2049
- result.deletes.push({ ok: true, rowKey: del.rowKey });
2050
- } catch (error) {
2051
- result.deletes.push({
2052
- error: error instanceof Error ? error.message : String(error),
2053
- ok: false,
2054
- rowKey: del.rowKey
2055
- });
2056
- }
2057
- }
2058
- return result;
2059
- },
2060
- id: opts.id,
2061
- label: opts.label,
2062
- async refresh() {
2063
- await load(true);
2064
- },
2065
- async rows() {
2066
- await load();
2067
- const rows = [...entries.entries()].map(([path, entry]) => rowFor(path, entry));
2068
- return { rows, total: rows.length };
2069
- }
2070
- };
2071
- }
2072
- function createCollectionSource(typeName) {
2073
- const requireInfo = () => {
2074
- const info = collectionInfo(typeName);
2075
- if (!info) {
2076
- throw new Error(`No content collection named "${typeName}" in project.json`);
2077
- }
2078
- return info;
2079
- };
2080
- return createEntryFileSource({
2081
- id: makeGridTabId({ kind: "collection", name: typeName }),
2082
- insertFormatName: (path) => {
2083
- const info = requireInfo();
2084
- return (info.def.format ? formatByName(info.def.format)?.name : undefined) ?? formatForPath(path)?.name ?? defaultContentFormat()?.name ?? null;
2085
- },
2086
- label: typeName,
2087
- list: () => {
2088
- const info = requireInfo();
2089
- return listEntryFiles(info.dir, [info.ext]);
2090
- },
2091
- resolveInsertPath: (raw) => {
2092
- const info = requireInfo();
2093
- let path = raw.trim().replace(/^\.\//, "").replace(/^\//, "");
2094
- if (!path.includes("/")) {
2095
- path = `${info.dir}/${path}`;
2096
- }
2097
- if (!path.endsWith(info.ext)) {
2098
- path = `${path}${info.ext}`;
2099
- }
2100
- return path;
2101
- },
2102
- schema: () => requireInfo().def.schema
2103
- });
2104
- }
2105
- function createPagesSource() {
2106
- const pageExts = () => {
2107
- const exts = documentExtensions("page").filter((ext) => ext !== ".json");
2108
- return exts.length > 0 ? exts : [".md"];
2109
- };
2110
- return createEntryFileSource({
2111
- id: makeGridTabId({ kind: "pages" }),
2112
- insertFormatName: (path) => formatForPath(path)?.name ?? null,
2113
- label: "Pages",
2114
- list: () => listEntryFiles("pages", pageExts()),
2115
- priorityFields: ["title", "description"],
2116
- resolveInsertPath: (raw) => {
2117
- let path = raw.trim().replace(/^\.\//, "").replace(/^\//, "");
2118
- if (!path.startsWith("pages/")) {
2119
- path = `pages/${path}`;
2120
- }
2121
- if (!pageExts().some((ext) => path.endsWith(ext))) {
2122
- path = `${path}${pageExts()[0]}`;
2123
- }
2124
- return path;
2125
- },
2126
- schema: () => null
2127
- });
2128
- }
2129
-
2130
- // src/grid/sources/connector-source.ts
2131
- var DATA_PAGE_SIZE = 50;
2132
- function kindForSqlType(type) {
2133
- const t = type.toLowerCase();
2134
- if (/(int|real|floa|doub|num|dec)/.test(t)) {
2135
- return "number";
2136
- }
2137
- if (t.includes("bool")) {
2138
- return "boolean";
2139
- }
2140
- if (/(timestamp|datetime|date)/.test(t)) {
2141
- return "date";
2142
- }
2143
- return "string";
2144
- }
2145
- function editableColumn(column) {
2146
- return !column.pk && column.name !== "created_at" && column.name !== "updated_at";
2147
- }
2148
- function toCellValue2(value) {
2149
- if (value === null || value === undefined) {
2150
- return null;
2151
- }
2152
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
2153
- return value;
2154
- }
2155
- return JSON.stringify(value);
2156
- }
2157
- function createConnectorSource(connection, table) {
2158
- let columnsMeta = [];
2159
- let pkName = "id";
2160
- const pkValues = new Map;
2161
- const applyMeta = (columns) => {
2162
- if (columns.length > 0) {
2163
- columnsMeta = columns;
2164
- pkName = columns.find((c) => c.pk)?.name ?? "id";
2165
- }
2166
- };
2167
- const toQuery = (query) => ({
2168
- table,
2169
- ...connection ? { connection } : {},
2170
- limit: query?.limit ?? DATA_PAGE_SIZE,
2171
- offset: query?.offset ?? 0,
2172
- ...query?.orderBy ? { dir: query.dir ?? "asc", orderBy: query.orderBy } : {}
2173
- });
2174
- const rowKeyOf = (row, index) => {
2175
- const pk = row[pkName];
2176
- if (typeof pk === "string" || typeof pk === "number") {
2177
- pkValues.set(String(pk), pk);
2178
- return String(pk);
2179
- }
2180
- return `row-${index}`;
2181
- };
2182
- return {
2183
- capabilities: { delete: true, insert: true, remotePaging: true, remoteSort: true },
2184
- async columns() {
2185
- if (columnsMeta.length === 0) {
2186
- const result = await fetchRows(toQuery({ limit: 1, offset: 0 }));
2187
- applyMeta(result.columns);
2188
- }
2189
- return columnsMeta.map((meta) => {
2190
- const kind = editableColumn(meta) ? kindForSqlType(meta.type) : "readonly";
2191
- return {
2192
- editable: kind !== "readonly",
2193
- field: meta.name,
2194
- kind,
2195
- pk: meta.pk === true,
2196
- title: meta.name,
2197
- widthHint: meta.pk ? 80 : undefined
2198
- };
2199
- });
2200
- },
2201
- async commit(batch) {
2202
- const result = { cells: [], deletes: [], inserts: [] };
2203
- const byRow = new Map;
2204
- for (const cell of batch.cells) {
2205
- const list = byRow.get(cell.rowKey) ?? [];
2206
- list.push({ field: cell.field, value: cell.value });
2207
- byRow.set(cell.rowKey, list);
2208
- }
2209
- for (const [rowKey, patches] of byRow) {
2210
- const pk = pkValues.get(rowKey);
2211
- const outcome = (ok, error) => {
2212
- for (const patch of patches) {
2213
- result.cells.push({ error, field: patch.field, ok, rowKey });
2214
- }
2215
- };
2216
- if (pk === undefined) {
2217
- outcome(false, "Row has no primary key — refresh the grid");
2218
- continue;
2219
- }
2220
- try {
2221
- const set = {};
2222
- for (const patch of patches) {
2223
- set[patch.field] = patch.value;
2224
- }
2225
- await updateRow({ pk, set, table, ...connection ? { connection } : {} });
2226
- outcome(true);
2227
- } catch (error) {
2228
- outcome(false, error instanceof Error ? error.message : String(error));
2229
- }
2230
- }
2231
- for (const insert of batch.inserts) {
2232
- try {
2233
- const values = {};
2234
- for (const [field, value] of Object.entries(insert.cells)) {
2235
- if (value !== null) {
2236
- values[field] = value;
2237
- }
2238
- }
2239
- const { row } = await insertRow({ table, values, ...connection ? { connection } : {} });
2240
- const pk = row[pkName];
2241
- const newKey = typeof pk === "string" || typeof pk === "number" ? String(pk) : undefined;
2242
- if (newKey !== undefined && (typeof pk === "string" || typeof pk === "number")) {
2243
- pkValues.set(newKey, pk);
2244
- }
2245
- result.inserts.push({ newKey, ok: true, tempKey: insert.tempKey });
2246
- } catch (error) {
2247
- result.inserts.push({
2248
- error: error instanceof Error ? error.message : String(error),
2249
- ok: false,
2250
- tempKey: insert.tempKey
2251
- });
2252
- }
2253
- }
2254
- for (const del of batch.deletes) {
2255
- const pk = pkValues.get(del.rowKey);
2256
- if (pk === undefined) {
2257
- result.deletes.push({
2258
- error: "Row has no primary key — refresh the grid",
2259
- ok: false,
2260
- rowKey: del.rowKey
2261
- });
2262
- continue;
2263
- }
2264
- try {
2265
- await deleteRow({ pk, table, ...connection ? { connection } : {} });
2266
- result.deletes.push({ ok: true, rowKey: del.rowKey });
2267
- } catch (error) {
2268
- result.deletes.push({
2269
- error: error instanceof Error ? error.message : String(error),
2270
- ok: false,
2271
- rowKey: del.rowKey
2272
- });
2273
- }
2274
- }
2275
- return result;
2276
- },
2277
- id: makeGridTabId({ connection: connection ?? "default", kind: "data", table }),
2278
- label: table,
2279
- async refresh() {
2280
- columnsMeta = [];
2281
- pkValues.clear();
2282
- },
2283
- async rows(query) {
2284
- const result = await fetchRows(toQuery(query));
2285
- applyMeta(result.columns);
2286
- const rows = result.rows.map((row, index) => {
2287
- const key = rowKeyOf(row, index);
2288
- const cells = {};
2289
- for (const meta of columnsMeta) {
2290
- cells[meta.name] = toCellValue2(row[meta.name]);
2291
- }
2292
- return { cells, key };
2293
- });
2294
- return { rows, total: result.total };
2295
- }
2296
- };
2297
- }
2298
-
2299
- // src/grid/grid-open.ts
2300
- var GRID_STUB_DOCUMENT = { children: [], tagName: "div" };
2301
- async function openCsvGridTab(path) {
2302
- const existing = workspace.tabs.get(path);
2303
- if (existing) {
2304
- activateTab(path);
2305
- return existing;
2306
- }
2307
- try {
2308
- await loadFormats();
2309
- } catch {}
2310
- const source = createCsvFileSource(path);
2311
- const tab = openTab({
2312
- capabilities: { modes: ["grid", "source"] },
2313
- document: structuredClone(GRID_STUB_DOCUMENT),
2314
- documentPath: path,
2315
- id: path,
2316
- sourceFormat: formatForPath(path)?.name ?? null
2317
- });
2318
- const controller = createGridController(tab, source);
2319
- controller.load();
2320
- return tab;
2321
- }
2322
- function openVirtualGridTab(source) {
2323
- const tab = openTab({
2324
- capabilities: { modes: ["grid"] },
2325
- document: structuredClone(GRID_STUB_DOCUMENT),
2326
- documentPath: null,
2327
- id: source.id
2328
- });
2329
- const controller = createGridController(tab, source);
2330
- controller.load();
2331
- return tab;
2332
- }
2333
- function openCollectionGrid(typeName) {
2334
- const id = makeGridTabId({ kind: "collection", name: typeName });
2335
- const existing = workspace.tabs.get(id);
2336
- if (existing) {
2337
- activateTab(id);
2338
- return existing;
2339
- }
2340
- return openVirtualGridTab(createCollectionSource(typeName));
2341
- }
2342
- function openPagesGrid() {
2343
- const id = makeGridTabId({ kind: "pages" });
2344
- const existing = workspace.tabs.get(id);
2345
- if (existing) {
2346
- activateTab(id);
2347
- return existing;
2348
- }
2349
- return openVirtualGridTab(createPagesSource());
2350
- }
2351
- function openConnectorGrid(connection, table) {
2352
- const id = makeGridTabId({ connection: connection ?? "default", kind: "data", table });
2353
- const existing = workspace.tabs.get(id);
2354
- if (existing) {
2355
- activateTab(id);
2356
- return existing;
2357
- }
2358
- return openVirtualGridTab(createConnectorSource(connection, table));
2359
- }
2360
- async function openGridSourcePicker() {
2361
- const collections = collectionDirs();
2362
- let connections = [];
2363
- if (dataSurfaceAvailable()) {
2364
- const response = await fetchConnections().catch(() => null);
2365
- connections = response?.connections ?? [];
2366
- }
2367
- const handle = openModal(html`<sp-dialog-wrapper
2368
- open
2369
- dismissable
2370
- underlay
2371
- headline="Open Grid"
2372
- @close=${() => handle.close()}
2373
- >
2374
- <sp-menu class="jx-grid-picker">
2375
- <sp-menu-group>
2376
- <span slot="header">Project</span>
2377
- <sp-menu-item
2378
- @click=${() => {
2379
- handle.close();
2380
- openPagesGrid();
2381
- }}
2382
- >Pages</sp-menu-item
2383
- >
2384
- ${collections.map(({ name }) => html`<sp-menu-item
2385
- @click=${() => {
2386
- handle.close();
2387
- openCollectionGrid(name);
2388
- }}
2389
- >Collection: ${name}</sp-menu-item
2390
- >`)}
2391
- </sp-menu-group>
2392
- ${connections.map((conn) => html`<sp-menu-group>
2393
- <span slot="header">Data · ${conn.name}</span>
2394
- ${conn.tables.length === 0 ? html`<sp-menu-item disabled>No tables — push a schema first</sp-menu-item>` : conn.tables.map((table) => html`<sp-menu-item
2395
- @click=${() => {
2396
- handle.close();
2397
- openConnectorGrid(conn.name, table);
2398
- }}
2399
- >${table}</sp-menu-item
2400
- >`)}
2401
- </sp-menu-group>`)}
2402
- </sp-menu>
2403
- </sp-dialog-wrapper>`);
2404
- }
2405
-
2406
- // src/recent-projects.ts
2407
- var STORAGE_KEY = "jx-studio-recent-projects";
2408
- var FILES_STORAGE_KEY = "jx-studio-recent-files";
2409
- var MAX_RECENT = 8;
2410
- var MAX_RECENT_FILES = 10;
2411
- var cache2 = [];
2412
- function backend() {
2413
- if (!hasPlatform()) {
2414
- return null;
2415
- }
2416
- const platform = getPlatform();
2417
- return platform.getRecentProjects && platform.saveRecentProjects ? platform : null;
2418
- }
2419
- function loadFromLocalStorage() {
2420
- try {
2421
- const raw = localStorage.getItem(STORAGE_KEY);
2422
- if (!raw) {
2423
- return [];
2424
- }
2425
- const parsed = JSON.parse(raw);
2426
- return Array.isArray(parsed) ? parsed : [];
2427
- } catch {
2428
- return [];
2429
- }
2430
- }
2431
- function currentList() {
2432
- return backend() ? cache2 : loadFromLocalStorage();
2433
- }
2434
- function commit(list) {
2435
- cache2 = list;
2436
- const store = backend();
2437
- if (store) {
2438
- store.saveRecentProjects(list);
2439
- } else {
2440
- try {
2441
- localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
2442
- } catch {}
2443
- }
2444
- }
2445
- async function hydrateRecentProjects() {
2446
- const store = backend();
2447
- if (!store) {
2448
- return;
2449
- }
2450
- try {
2451
- cache2 = await store.getRecentProjects();
2452
- } catch {
2453
- cache2 = [];
2454
- }
2455
- }
2456
- function getRecentProjects() {
2457
- return currentList().toSorted((a, b) => b.timestamp - a.timestamp);
2458
- }
2459
- function addRecentProject(name, root) {
2460
- const projects = currentList().filter((p) => p.root !== root);
2461
- projects.unshift({ name, root, timestamp: Date.now() });
2462
- if (projects.length > MAX_RECENT) {
2463
- projects.length = MAX_RECENT;
2464
- }
2465
- commit(projects);
2466
- }
2467
- function removeRecentProject(root) {
2468
- commit(currentList().filter((p) => p.root !== root));
2469
- }
2470
- function clearRecentProjects() {
2471
- cache2 = [];
2472
- const store = backend();
2473
- if (store) {
2474
- store.saveRecentProjects([]);
2475
- } else {
2476
- localStorage.removeItem(STORAGE_KEY);
2477
- }
2478
- }
2479
- function loadRecentFiles() {
2480
- try {
2481
- const raw = localStorage.getItem(FILES_STORAGE_KEY);
2482
- if (!raw) {
2483
- return [];
2484
- }
2485
- const parsed = JSON.parse(raw);
2486
- return Array.isArray(parsed) ? parsed : [];
2487
- } catch {
2488
- return [];
2489
- }
2490
- }
2491
- function getRecentFiles(root) {
2492
- const all = loadRecentFiles().toSorted((a, b) => b.timestamp - a.timestamp);
2493
- return root == null ? all : all.filter((f) => f.root === root);
2494
- }
2495
- function trackRecentFile(file) {
2496
- const all = loadRecentFiles().filter((f) => !(f.root === file.root && f.path === file.path));
2497
- all.unshift({ name: file.name, path: file.path, root: file.root, timestamp: Date.now() });
2498
- const perRoot = new Map;
2499
- const kept = [];
2500
- for (const f of all.toSorted((a, b) => b.timestamp - a.timestamp)) {
2501
- const n = (perRoot.get(f.root) ?? 0) + 1;
2502
- perRoot.set(f.root, n);
2503
- if (n <= MAX_RECENT_FILES) {
2504
- kept.push(f);
2505
- }
2506
- }
2507
- localStorage.setItem(FILES_STORAGE_KEY, JSON.stringify(kept));
2508
- }
2509
-
2510
- // src/files/files.ts
2511
- var fileIconMap = {
2512
- "sp-icon-document": html`<sp-icon-document></sp-icon-document>`,
2513
- "sp-icon-file-code": html`<sp-icon-file-code></sp-icon-file-code>`,
2514
- "sp-icon-file-txt": html`<sp-icon-file-txt></sp-icon-file-txt>`,
2515
- "sp-icon-folder": html`<sp-icon-folder></sp-icon-folder>`,
2516
- "sp-icon-folder-open": html`<sp-icon-folder-open></sp-icon-folder-open>`,
2517
- "sp-icon-image": html`<sp-icon-image></sp-icon-image>`
2518
- };
2519
- async function loadDirectory(dirPath) {
2520
- if (!projectState) {
2521
- return;
2522
- }
2523
- try {
2524
- const platform = getPlatform();
2525
- const entries = await platform.listDirectory(dirPath);
2526
- projectState.dirs.set(dirPath, entries);
2527
- } catch {
2528
- projectState.dirs.set(dirPath, []);
2529
- }
2530
- }
2531
- async function loadProject() {
2532
- try {
2533
- const platform = getPlatform();
2534
- const result = await platform.probeRootProject();
2535
- if (!result) {
2536
- return;
2537
- }
2538
- const { meta, info } = result;
2539
- refreshFormats();
2540
- loadFormats();
2541
- refreshExtensionUi(platform);
2542
- setProjectState({
2543
- dirs: new Map,
2544
- expanded: new Set,
2545
- isSiteProject: info.isSiteProject,
2546
- name: info.isSiteProject ? info.projectConfig?.name || meta.name : meta.name,
2547
- projectConfig: (info.isSiteProject ? info.projectConfig : null) || null,
2548
- projectDirs: info.directories || [],
2549
- projectRoot: ".",
2550
- root: meta.root,
2551
- searchQuery: "",
2552
- selectedPath: null
2553
- });
2554
- if (info.isSiteProject) {
2555
- setWorkspaceProject(meta.root || ".", info.projectConfig || null);
2556
- }
2557
- if (info.isSiteProject) {
2558
- addRecentProject(requireProjectState().name, meta.root);
2559
- await autoSyncProjectOnOpen();
2560
- await ensureDependenciesInstalled();
2561
- await loadDirectory(".");
2562
- await loadComponentRegistry();
2563
- await openHomePage();
2564
- maybePromptJxsuiteUpdate(meta.root);
2565
- }
2566
- } catch {}
2567
- }
2568
- async function openProject({
2569
- renderActivityBar,
2570
- renderLeftPanel
2571
- }) {
2572
- try {
2573
- const platform = getPlatform();
2574
- const result = await platform.openProject();
2575
- if (!result) {
2576
- return;
2577
- }
2578
- const { config, handle } = result;
2579
- replaceAllTabs({
2580
- document: { children: [], tagName: "div" },
2581
- id: "initial"
2582
- });
2583
- refreshFormats();
2584
- loadFormats();
2585
- refreshExtensionUi(platform);
2586
- setProjectState({
2587
- ...projectState,
2588
- dirs: new Map,
2589
- expanded: new Set,
2590
- isSiteProject: true,
2591
- name: config.name || handle.name,
2592
- projectConfig: config,
2593
- projectRoot: handle.root,
2594
- searchQuery: "",
2595
- selectedPath: null
2596
- });
2597
- setWorkspaceProject(handle.root, config);
2598
- await autoSyncProjectOnOpen();
2599
- await ensureDependenciesInstalled();
2600
- await loadDirectory(".");
2601
- await loadComponentRegistry();
2602
- const conventionalDirs = new Set([
2603
- "pages",
2604
- "layouts",
2605
- "components",
2606
- "content",
2607
- "data",
2608
- "public",
2609
- "styles"
2610
- ]);
2611
- const entries = requireProjectState().dirs.get(".") || [];
2612
- const foundDirs = [];
2613
- for (const e of entries) {
2614
- if (e.type === "directory" && conventionalDirs.has(e.name)) {
2615
- foundDirs.push(e.name);
2616
- requireProjectState().expanded.add(e.path || e.name);
2617
- await loadDirectory(e.path || e.name);
2618
- }
2619
- }
2620
- requireProjectState().projectDirs = foundDirs;
2621
- view.leftTab = "files";
2622
- addRecentProject(requireProjectState().name, requireProjectState().projectRoot);
2623
- renderActivityBar();
2624
- renderLeftPanel();
2625
- statusMessage(`Opened project: ${requireProjectState().name}`);
2626
- await openHomePage();
2627
- maybePromptJxsuiteUpdate(requireProjectState().projectRoot);
2628
- } catch (error) {
2629
- statusMessage(`Error: ${errorMessage(error)}`);
2630
- }
2631
- }
2632
- async function findHomePage() {
2633
- await loadFormats();
2634
- const exts = [...documentExtensions("page"), ".json"];
2635
- let entries;
2636
- try {
2637
- entries = await getPlatform().listDirectory("pages");
2638
- } catch {
2639
- return null;
2640
- }
2641
- const files = new Set(entries.filter((e) => e.type === "file").map((e) => e.name));
2642
- for (const ext of exts) {
2643
- if (files.has(`index${ext}`)) {
2644
- return `pages/index${ext}`;
2645
- }
2646
- }
2647
- return null;
2648
- }
2649
- async function openHomePage() {
2650
- const home = await findHomePage();
2651
- if (home) {
2652
- await openFileInTab(home);
2653
- }
2654
- }
2655
- function fileTypeIconTpl(name, type) {
2656
- let tag;
2657
- if (type === "directory") {
2658
- tag = projectState?.expanded?.has(name) ? "sp-icon-folder-open" : "sp-icon-folder";
2659
- } else {
2660
- const ext = name.split(".").pop()?.toLowerCase();
2661
- switch (ext) {
2662
- case "json": {
2663
- tag = "sp-icon-file-code";
2664
- break;
2665
- }
2666
- case "md": {
2667
- tag = "sp-icon-file-txt";
2668
- break;
2669
- }
2670
- case "js":
2671
- case "ts": {
2672
- tag = "sp-icon-file-code";
2673
- break;
2674
- }
2675
- case "css": {
2676
- tag = "sp-icon-file-code";
2677
- break;
2678
- }
2679
- default: {
2680
- tag = ext && isImage(ext) ? "sp-icon-image" : "sp-icon-document";
2681
- break;
2682
- }
2683
- }
2684
- }
2685
- return fileIconMap[tag] || fileIconMap["sp-icon-document"];
2686
- }
2687
- function renderFilesTemplate({
2688
- openProject: openProjectFn,
2689
- openFileFromTree: openFileFn,
2690
- renderLeftPanel
2691
- }) {
2692
- if (!projectState) {
2693
- return html`<div class="file-tree-empty">No project loaded</div>`;
2694
- }
2695
- if (!projectState.isSiteProject && projectState.projectRoot === ".") {
2696
- return html`<div class="file-tree-empty">
2697
- <p style="margin:0 0 12px">Open a project folder to get started.</p>
2698
- <sp-button variant="accent" size="s" @click=${openProjectFn}>Open Project</sp-button>
2699
- </div>`;
2700
- }
2701
- return html`
2702
- ${projectState.isSiteProject ? html`
2703
- <div class="project-header">
2704
- <span class="project-name"
2705
- >${projectState.projectConfig?.name || projectState.name}</span
2706
- >
2707
- </div>
2708
- ` : nothing}
2709
- <div class="files-toolbar">
2710
- <sp-action-group size="xs" compact quiet>
2711
- <sp-action-button
2712
- size="xs"
2713
- label="New File"
2714
- @click=${() => createNewFile(".", renderLeftPanel)}
2715
- >
2716
- <sp-icon-add slot="icon"></sp-icon-add>
2717
- </sp-action-button>
2718
- <sp-action-button
2719
- size="xs"
2720
- label="Refresh"
2721
- @click=${async () => {
2722
- requireProjectState().dirs.clear();
2723
- await loadDirectory(".");
2724
- for (const dir of requireProjectState().expanded) {
2725
- await loadDirectory(dir);
2726
- }
2727
- renderLeftPanel();
2728
- }}
2729
- >
2730
- <sp-icon-refresh slot="icon"></sp-icon-refresh>
2731
- </sp-action-button>
2732
- </sp-action-group>
2733
- <sp-search
2734
- size="s"
2735
- quiet
2736
- placeholder="Filter files…"
2737
- value=${requireProjectState().searchQuery}
2738
- @input=${(e) => {
2739
- requireProjectState().searchQuery = e.target.value;
2740
- renderLeftPanel();
2741
- }}
2742
- @submit=${(e) => e.preventDefault()}
2743
- ></sp-search>
2744
- </div>
2745
- <div class="file-tree" role="tree" aria-label="Project files">
2746
- ${renderTreeLevelTemplate(".", 0, { openFileFn, renderLeftPanel })}
2747
- </div>
2748
- `;
2749
- }
2750
- function renderTreeLevelTemplate(dirPath, depth, ctx) {
2751
- const entries = requireProjectState().dirs.get(dirPath);
2752
- if (!entries) {
2753
- loadDirectory(dirPath).then(() => ctx.renderLeftPanel());
2754
- return html`<div
2755
- class="file-tree-item"
2756
- style="padding-left:${8 + depth * 16}px;color:var(--fg-dim);font-style:italic"
2757
- >
2758
- Loading…
2759
- </div>`;
2760
- }
2761
- const sorted = [...entries].toSorted((a, b) => {
2762
- if (a.type === "directory" && b.type !== "directory") {
2763
- return -1;
2764
- }
2765
- if (a.type !== "directory" && b.type === "directory") {
2766
- return 1;
2767
- }
2768
- return a.name.localeCompare(b.name);
2769
- });
2770
- const query = requireProjectState().searchQuery.toLowerCase();
2771
- const filtered = query ? sorted.filter((e) => e.type === "directory" || e.name.toLowerCase().includes(query)) : sorted;
2772
- return filtered.map((entry) => {
2773
- const isDir = entry.type === "directory";
2774
- const isExpanded = requireProjectState().expanded.has(entry.path);
2775
- const isSelected = requireProjectState().selectedPath === entry.path;
2776
- return html`
2777
- <div
2778
- class=${classMap({ "file-tree-item": true, selected: isSelected })}
2779
- style="padding-left:${8 + depth * 16}px"
2780
- role="treeitem"
2781
- aria-level=${depth + 1}
2782
- tabindex="-1"
2783
- data-path=${entry.path}
2784
- data-type=${entry.type}
2785
- aria-expanded=${isDir ? String(isExpanded) : nothing}
2786
- @click=${async (e) => {
2787
- e.stopPropagation();
2788
- if (isDir) {
2789
- if (isExpanded) {
2790
- requireProjectState().expanded.delete(entry.path);
2791
- } else {
2792
- requireProjectState().expanded.add(entry.path);
2793
- if (!requireProjectState().dirs.has(entry.path)) {
2794
- await loadDirectory(entry.path);
2795
- }
2796
- }
2797
- ctx.renderLeftPanel();
2798
- } else {
2799
- ctx.openFileFn(entry.path);
2800
- }
2801
- }}
2802
- @contextmenu=${(e) => {
2803
- e.preventDefault();
2804
- e.stopPropagation();
2805
- showFileContextMenu(e, entry, ctx);
2806
- }}
2807
- >
2808
- ${isDir ? html`<span class="file-tree-toggle">${isExpanded ? "▼" : "▶"}</span>` : html`<span class="file-tree-toggle empty"> </span>`}
2809
- <span class="file-tree-icon">${fileTypeIconTpl(entry.path, entry.type)}</span>
2810
- <span class="file-tree-name">${entry.name}</span>
2811
- </div>
2812
- ${isDir && isExpanded ? html`<div role="group">${renderTreeLevelTemplate(entry.path, depth + 1, ctx)}</div>` : nothing}
2813
- `;
2814
- });
2815
- }
2816
- function setupTreeKeyboard(tree) {
2817
- tree.addEventListener("keydown", (e) => {
2818
- const items = [...tree.querySelectorAll(".file-tree-item")];
2819
- const focused = tree.querySelector(".file-tree-item:focus");
2820
- if (!focused || items.length === 0) {
2821
- return;
2822
- }
2823
- const idx = items.indexOf(focused);
2824
- let handled = true;
2825
- switch (e.key) {
2826
- case "ArrowDown": {
2827
- if (idx < items.length - 1) {
2828
- items[idx + 1].focus();
2829
- }
2830
- break;
2831
- }
2832
- case "ArrowUp": {
2833
- if (idx > 0) {
2834
- items[idx - 1].focus();
2835
- }
2836
- break;
2837
- }
2838
- case "ArrowRight": {
2839
- if (focused.dataset.type === "directory") {
2840
- const path = focused.dataset.path;
2841
- if (!requireProjectState().expanded.has(path)) {
2842
- requireProjectState().expanded.add(path);
2843
- loadDirectory(path).then(() => {
2844
- const panel = tree.closest(".panel-body");
2845
- if (panel) {
2846
- panel.querySelector(".file-tree-item:focus")?.click();
2847
- }
2848
- });
2849
- }
2850
- }
2851
- break;
2852
- }
2853
- case "ArrowLeft": {
2854
- if (focused.dataset.type === "directory") {
2855
- const path = focused.dataset.path;
2856
- if (requireProjectState().expanded.has(path)) {
2857
- requireProjectState().expanded.delete(path);
2858
- }
2859
- }
2860
- break;
2861
- }
2862
- case "Enter": {
2863
- focused.click();
2864
- break;
2865
- }
2866
- default: {
2867
- handled = false;
2868
- }
2869
- }
2870
- if (handled) {
2871
- e.preventDefault();
2872
- }
2873
- });
2874
- const first = tree.querySelector(".file-tree-item");
2875
- if (first) {
2876
- first.setAttribute("tabindex", "0");
2877
- }
2878
- }
2879
- var _fileTreeDndCleanups = [];
2880
- function isFileDrag(e) {
2881
- return [...e.dataTransfer?.types ?? []].includes("Files");
2882
- }
2883
- function registerFileDropTarget(element, dir, activeClass, renderLeftPanel) {
2884
- const onDragOver = (e) => {
2885
- if (!isFileDrag(e)) {
2886
- return;
2887
- }
2888
- e.preventDefault();
2889
- e.stopPropagation();
2890
- if (e.dataTransfer) {
2891
- e.dataTransfer.dropEffect = "copy";
2892
- }
2893
- element.classList.add(activeClass);
2894
- };
2895
- const onDragLeave = () => element.classList.remove(activeClass);
2896
- const onDrop = (e) => {
2897
- element.classList.remove(activeClass);
2898
- const files = e.dataTransfer?.files;
2899
- if (!files?.length) {
2900
- return;
2901
- }
2902
- e.preventDefault();
2903
- e.stopPropagation();
2904
- uploadFilesToDir(files, dir, renderLeftPanel);
2905
- };
2906
- element.addEventListener("dragover", onDragOver);
2907
- element.addEventListener("dragleave", onDragLeave);
2908
- element.addEventListener("drop", onDrop);
2909
- return () => {
2910
- element.removeEventListener("dragover", onDragOver);
2911
- element.removeEventListener("dragleave", onDragLeave);
2912
- element.removeEventListener("drop", onDrop);
2913
- };
2914
- }
2915
- async function uploadFilesToDir(files, dir, renderLeftPanel) {
2916
- const uploaded = await uploadAssets([...files], { dir });
2917
- if (uploaded.length === 0) {
2918
- return;
2919
- }
2920
- if (dir !== ".") {
2921
- requireProjectState().expanded.add(dir);
2922
- }
2923
- renderLeftPanel();
2924
- }
2925
- function pickAndUploadTo(dir, renderLeftPanel) {
2926
- const input = document.createElement("input");
2927
- input.type = "file";
2928
- input.multiple = true;
2929
- input.accept = UPLOAD_ACCEPT;
2930
- input.addEventListener("change", () => {
2931
- if (input.files?.length) {
2932
- uploadFilesToDir(input.files, dir, renderLeftPanel);
2933
- }
2934
- });
2935
- input.click();
2936
- }
2937
- function registerFileTreeDnD({ renderLeftPanel }) {
2938
- for (const fn of _fileTreeDndCleanups) {
2939
- fn();
2940
- }
2941
- _fileTreeDndCleanups = [];
2942
- requestAnimationFrame(() => {
2943
- const tree = document.querySelector(".file-tree");
2944
- if (!tree) {
2945
- return;
2946
- }
2947
- const items = tree.querySelectorAll(".file-tree-item");
2948
- for (const row of items) {
2949
- const { path } = row.dataset;
2950
- const { type } = row.dataset;
2951
- if (!path) {
2952
- continue;
2953
- }
2954
- const cleanups = [
2955
- draggable({
2956
- element: row,
2957
- getInitialData() {
2958
- return { entryType: type, path, type: "file-tree" };
2959
- },
2960
- onDragStart() {
2961
- row.classList.add("dragging");
2962
- },
2963
- onDrop() {
2964
- row.classList.remove("dragging");
2965
- }
2966
- }),
2967
- registerFileDropTarget(row, type === "directory" ? path : parentDir2(path), "drag-over", renderLeftPanel)
2968
- ];
2969
- if (type === "directory") {
2970
- cleanups.push(dropTargetForElements({
2971
- canDrop({ source }) {
2972
- if (source.data.type !== "file-tree") {
2973
- return false;
2974
- }
2975
- const srcPath = source.data.path;
2976
- if (srcPath === path) {
2977
- return false;
2978
- }
2979
- if (srcPath.startsWith(`${path}/`)) {
2980
- return false;
2981
- }
2982
- const srcParent = parentDir2(srcPath);
2983
- if (srcParent === path) {
2984
- return false;
2985
- }
2986
- return true;
2987
- },
2988
- element: row,
2989
- getData() {
2990
- return { targetDir: path, type: "file-tree-target" };
2991
- },
2992
- onDrag() {
2993
- if (!row.classList.contains("drag-over")) {
2994
- row.classList.add("drag-over");
2995
- }
2996
- },
2997
- onDragEnter() {
2998
- row.classList.add("drag-over");
2999
- },
3000
- onDragLeave() {
3001
- row.classList.remove("drag-over");
3002
- },
3003
- onDrop() {
3004
- row.classList.remove("drag-over");
3005
- }
3006
- }));
3007
- }
3008
- _fileTreeDndCleanups.push(combine(...cleanups));
3009
- }
3010
- const rootCleanup = dropTargetForElements({
3011
- canDrop({ source }) {
3012
- if (source.data.type !== "file-tree") {
3013
- return false;
3014
- }
3015
- const srcPath = source.data.path;
3016
- return parentDir2(srcPath) !== ".";
3017
- },
3018
- element: tree,
3019
- getData() {
3020
- return { targetDir: ".", type: "file-tree-target" };
3021
- },
3022
- onDragEnter() {
3023
- tree.classList.add("drag-over-root");
3024
- },
3025
- onDragLeave() {
3026
- tree.classList.remove("drag-over-root");
3027
- },
3028
- onDrop() {
3029
- tree.classList.remove("drag-over-root");
3030
- }
3031
- });
3032
- _fileTreeDndCleanups.push(rootCleanup, registerFileDropTarget(tree, ".", "drag-over-root", renderLeftPanel));
3033
- const monitorCleanup = monitorForElements({
3034
- onDrop({ source, location: location2 }) {
3035
- const [target] = location2.current.dropTargets;
3036
- if (!target) {
3037
- return;
3038
- }
3039
- if (source.data.type !== "file-tree") {
3040
- return;
3041
- }
3042
- if (target.data.type !== "file-tree-target") {
3043
- return;
3044
- }
3045
- const srcPath = source.data.path;
3046
- const targetDirPath = target.data.targetDir;
3047
- const fileName = srcPath.split("/").pop();
3048
- const newPath = targetDirPath === "." ? fileName : `${targetDirPath}/${fileName}`;
3049
- if (newPath === srcPath) {
3050
- return;
3051
- }
3052
- moveFileEntry(srcPath, newPath, renderLeftPanel);
3053
- }
3054
- });
3055
- _fileTreeDndCleanups.push(monitorCleanup);
3056
- });
3057
- }
3058
- async function moveFileEntry(oldPath, newPath, renderLeftPanel) {
3059
- const platform = getPlatform();
3060
- markLocalMutation(oldPath, newPath);
3061
- try {
3062
- const report = await platform.renameFile(oldPath, newPath);
3063
- for (const [id] of workspace.tabs.entries()) {
3064
- if (id === oldPath) {
3065
- renameTab(oldPath, newPath, newPath);
3066
- } else if (id.startsWith(`${oldPath}/`)) {
3067
- const newTabPath = newPath + id.slice(oldPath.length);
3068
- renameTab(id, newTabPath, newTabPath);
3069
- }
3070
- }
3071
- const oldParent = parentDir2(oldPath);
3072
- const newParent = parentDir2(newPath);
3073
- await loadDirectory(oldParent);
3074
- if (newParent !== oldParent) {
3075
- await loadDirectory(newParent);
3076
- }
3077
- if (newParent !== ".") {
3078
- requireProjectState().expanded.add(newParent);
3079
- }
3080
- reloadRewrittenTabs(report, newPath);
3081
- renderLeftPanel();
3082
- statusMessage(`Moved to ${newPath}`);
3083
- } catch (error) {
3084
- statusMessage(`Error: ${errorMessage(error)}`);
3085
- }
3086
- }
3087
- function parentDir2(path) {
3088
- const normalized = path.replaceAll("\\", "/");
3089
- const lastSlash = normalized.lastIndexOf("/");
3090
- return lastSlash === -1 ? "." : normalized.slice(0, lastSlash);
3091
- }
3092
- var _fileCtxHandle = null;
3093
- function dismissFileContextMenu() {
3094
- if (_fileCtxHandle) {
3095
- _fileCtxHandle.dismiss();
3096
- _fileCtxHandle = null;
3097
- }
3098
- }
3099
- function showFileContextMenu(e, entry, ctx) {
3100
- e.preventDefault();
3101
- dismissFileContextMenu();
3102
- const isDir = entry.type === "directory";
3103
- const items = [];
3104
- if (!isDir) {
3105
- items.push({ action: () => ctx.openFileFn(entry.path), label: "Open" });
3106
- }
3107
- if (isDir) {
3108
- items.push({
3109
- action: () => createNewFile(entry.path, ctx.renderLeftPanel),
3110
- label: "New File…"
3111
- }, {
3112
- action: () => pickAndUploadTo(entry.path, ctx.renderLeftPanel),
3113
- label: "Upload Files…"
3114
- });
3115
- const collection = collectionDirs().find(({ dir }) => entry.path === dir || entry.path.endsWith(`/${dir}`));
3116
- if (collection) {
3117
- items.push({
3118
- action: () => {
3119
- openCollectionGrid(collection.name);
3120
- },
3121
- label: "Edit Collection in Grid"
3122
- });
3123
- }
3124
- if (entry.path === "pages" || entry.path.endsWith("/pages")) {
3125
- items.push({
3126
- action: () => {
3127
- openPagesGrid();
3128
- },
3129
- label: "Edit Pages in Grid"
3130
- });
3131
- }
3132
- }
3133
- items.push({ label: "—" }, {
3134
- action: () => renameFile(entry, ctx.renderLeftPanel),
3135
- label: "Rename…"
3136
- }, {
3137
- action: () => deleteFile(entry, ctx.renderLeftPanel),
3138
- danger: true,
3139
- label: "Delete"
3140
- });
3141
- let { clientX: x, clientY: y } = e;
3142
- _fileCtxHandle = renderPopover(html`<sp-popover
3143
- open
3144
- style="position:fixed;z-index:10000;left:${x}px;top:${y}px"
3145
- ${ref((el) => {
3146
- if (!el) {
3147
- return;
3148
- }
3149
- requestAnimationFrame(() => {
3150
- const popover = el;
3151
- const menuRect = rectOf(popover);
3152
- if (x + menuRect.width > window.innerWidth) {
3153
- x = window.innerWidth - menuRect.width - 4;
3154
- }
3155
- if (y + menuRect.height > window.innerHeight) {
3156
- y = window.innerHeight - menuRect.height - 4;
3157
- }
3158
- popover.style.left = `${x}px`;
3159
- popover.style.top = `${y}px`;
3160
- });
3161
- })}
3162
- >
3163
- <sp-menu>
3164
- ${items.map((item) => item.label === "—" ? html`<sp-menu-divider></sp-menu-divider>` : html`<sp-menu-item
3165
- style=${item.danger ? "color: var(--danger)" : ""}
3166
- @click=${() => {
3167
- dismissFileContextMenu();
3168
- item.action?.();
3169
- }}
3170
- >${item.label}</sp-menu-item
3171
- >`)}
3172
- </sp-menu>
3173
- </sp-popover>`, {
3174
- dismissOnOutsideClick: true,
3175
- onDismiss: () => {
3176
- _fileCtxHandle = null;
3177
- }
3178
- });
3179
- }
3180
- async function createNewFile(dirPath, renderLeftPanel) {
3181
- const name = await showPromptDialog("New File", {
3182
- confirmLabel: "Create",
3183
- message: dirPath === "." ? "Creating in the project root." : `Creating in ${dirPath}/`,
3184
- placeholder: "untitled.json",
3185
- select: "stem",
3186
- validate: (v) => v.trim() ? "" : "Enter a file name.",
3187
- value: "untitled.json"
3188
- });
3189
- if (!name) {
3190
- return;
3191
- }
3192
- const path = dirPath === "." ? name : `${dirPath}/${name}`;
3193
- markLocalMutation(path);
3194
- await loadFormats();
3195
- const format = formatForPath(name);
3196
- const content = format?.studio?.newFileTemplate ?? (format ? "" : JSON.stringify({ children: [{ children: [], tagName: "p" }], tagName: "div" }, null, 2));
3197
- try {
3198
- const platform = getPlatform();
3199
- await platform.writeFile(path, content);
3200
- await loadDirectory(dirPath);
3201
- renderLeftPanel();
3202
- statusMessage(`Created ${path}`);
3203
- } catch (error) {
3204
- statusMessage(`Error: ${errorMessage(error)}`);
3205
- }
3206
- }
3207
- function showRenameFileDialog(currentName) {
3208
- return showPromptDialog("Rename", {
3209
- confirmLabel: "Rename",
3210
- select: "stem",
3211
- validate: (v) => v.trim() ? "" : "Enter a file name.",
3212
- value: currentName
3213
- });
3214
- }
3215
- function renameStatus(newName, report) {
3216
- const refs = report.references;
3217
- const tagNote = report.tag ? `; tag → <${report.tag.to}> (${report.tag.refsUpdated})` : "";
3218
- if (refs && refs.refsUpdated > 0) {
3219
- return `Renamed to ${newName}; updated ${refs.refsUpdated} reference(s) in ${refs.filesChanged} file(s)${tagNote}`;
3220
- }
3221
- if (tagNote) {
3222
- return `Renamed to ${newName}${tagNote}`;
3223
- }
3224
- return `Renamed to ${newName}`;
3225
- }
3226
- function reloadRewrittenTabs(report, skipPath) {
3227
- for (const f of report.references?.files ?? []) {
3228
- if (f.path !== skipPath && workspace.tabs.has(f.path)) {
3229
- reloadFileInTab(f.path);
3230
- }
3231
- }
3232
- }
3233
- async function renameFile(entry, renderLeftPanel) {
3234
- const newName = await showRenameFileDialog(entry.name);
3235
- if (!newName || newName === entry.name) {
3236
- return;
3237
- }
3238
- const entryPath = entry.path.replaceAll("\\", "/");
3239
- const parentDirPath = entryPath.includes("/") ? entryPath.slice(0, entryPath.lastIndexOf("/")) : ".";
3240
- const newPath = parentDirPath === "." ? newName : `${parentDirPath}/${newName}`;
3241
- markLocalMutation(entry.path, newPath);
3242
- try {
3243
- const platform = getPlatform();
3244
- const report = await platform.renameFile(entry.path, newPath);
3245
- await loadDirectory(parentDirPath);
3246
- if (requireProjectState().selectedPath === entry.path) {
3247
- requireProjectState().selectedPath = newPath;
3248
- }
3249
- if (workspace.tabs.has(entry.path)) {
3250
- renameTab(entry.path, newPath, newPath);
3251
- }
3252
- reloadRewrittenTabs(report, newPath);
3253
- renderLeftPanel();
3254
- statusMessage(renameStatus(newName, report));
3255
- } catch (error) {
3256
- statusMessage(`Error: ${errorMessage(error)}`);
3257
- }
3258
- }
3259
- async function deleteFile(entry, renderLeftPanel) {
3260
- const confirmed = await showConfirmDialog("Delete File", `Delete "${entry.name}"?`, {
3261
- confirmLabel: "Delete",
3262
- destructive: true
3263
- });
3264
- if (!confirmed) {
3265
- return;
3266
- }
3267
- try {
3268
- const platform = getPlatform();
3269
- markLocalMutation(entry.path);
3270
- await platform.deleteFile(entry.path);
3271
- const delPath = entry.path.replaceAll("\\", "/");
3272
- const parentDirPath = delPath.includes("/") ? delPath.slice(0, delPath.lastIndexOf("/")) : ".";
3273
- await loadDirectory(parentDirPath);
3274
- if (requireProjectState().selectedPath === entry.path) {
3275
- requireProjectState().selectedPath = null;
3276
- }
3277
- renderLeftPanel();
3278
- statusMessage(`Deleted ${entry.name}`);
3279
- } catch (error) {
3280
- statusMessage(`Error: ${errorMessage(error)}`);
3281
- }
3282
- }
3283
- async function openFileFromTree(ctx, path) {
3284
- const platform = getPlatform();
3285
- await loadFormats();
3286
- if (ctx.S.dirty && ctx.S.documentPath) {
3287
- try {
3288
- const tabLike = {
3289
- doc: {
3290
- content: ctx.S.content,
3291
- document: ctx.S.document,
3292
- mode: ctx.S.mode,
3293
- sourceFormat: formatForPath(ctx.S.documentPath)?.name ?? null
3294
- }
3295
- };
3296
- const output = await serializeDocument(tabLike);
3297
- await platform.writeFile(ctx.S.documentPath, output);
3298
- } catch (error) {
3299
- statusMessage(`Save error: ${errorMessage(error)}`);
3300
- }
3301
- }
3302
- try {
3303
- const content = await platform.readFile(path);
3304
- if (!content) {
3305
- return;
3306
- }
3307
- if (formatForPath(path)) {
3308
- ctx.loadMarkdown(content, null);
3309
- ctx.S.documentPath = path;
3310
- ctx.S.dirty = false;
3311
- ctx.commit(ctx.S);
3312
- } else if (path.endsWith(".json")) {
3313
- const doc = JSON.parse(content);
3314
- const newS = createState(doc);
3315
- newS.documentPath = path;
3316
- newS.dirty = false;
3317
- ctx.commit(newS);
3318
- } else {
3319
- throw noFormatError(path);
3320
- }
3321
- requireProjectState().selectedPath = path;
3322
- ctx.render();
3323
- statusMessage(`Opened ${path}`);
3324
- } catch (error) {
3325
- statusMessage(`Error: ${errorMessage(error)}`);
3326
- }
3327
- }
3328
- async function openFileInTab(path) {
3329
- for (const [id, tab] of workspace.tabs.entries()) {
3330
- if (tab.documentPath === path) {
3331
- activateTab(id);
3332
- requireProjectState().selectedPath = path;
3333
- return;
3334
- }
3335
- }
3336
- if (path.toLowerCase().endsWith(".csv")) {
3337
- try {
3338
- await openCsvGridTab(path);
3339
- requireProjectState().selectedPath = path;
3340
- trackRecentFile({
3341
- name: path.split("/").pop() || path,
3342
- path,
3343
- root: requireProjectState().projectRoot
3344
- });
3345
- statusMessage(`Opened ${path.split("/").pop()}`);
3346
- } catch (error) {
3347
- statusMessage(`Error: ${errorMessage(error)}`);
3348
- }
3349
- return;
3350
- }
3351
- const platform = getPlatform();
3352
- try {
3353
- const content = await platform.readFile(path);
3354
- if (!content) {
3355
- return;
3356
- }
3357
- await loadFormats();
3358
- let document2;
3359
- let frontmatter;
3360
- const format = formatForPath(path);
3361
- if (format) {
3362
- const result = await parseSourceForPath(path, content);
3363
- ({ document: document2 } = result);
3364
- ({ frontmatter } = result);
3365
- } else if (path.endsWith(".json")) {
3366
- document2 = JSON.parse(content);
3367
- } else {
3368
- throw noFormatError(path);
3369
- }
3370
- const id = path;
3371
- openTab({
3372
- id,
3373
- documentPath: path,
3374
- document: document2,
3375
- ...frontmatter != null && { frontmatter },
3376
- sourceFormat: format?.name ?? null
3377
- });
3378
- requireProjectState().selectedPath = path;
3379
- trackRecentFile({
3380
- name: path.split("/").pop() || path,
3381
- path,
3382
- root: requireProjectState().projectRoot
3383
- });
3384
- statusMessage(`Opened ${path.split("/").pop()}`);
3385
- } catch (error) {
3386
- statusMessage(`Error: ${errorMessage(error)}`);
3387
- }
3388
- }
3389
- function reloadCleanTab(path) {
3390
- if (isCollabPath(path)) {
3391
- return;
3392
- }
3393
- for (const [, tab] of workspace.tabs.entries()) {
3394
- if (tab.documentPath === path && !tab.doc.dirty) {
3395
- reloadFileInTab(path);
3396
- return;
3397
- }
3398
- }
3399
- }
3400
- async function reloadFileInTab(path) {
3401
- for (const [, tab] of workspace.tabs.entries()) {
3402
- if (tab.documentPath === path) {
3403
- const platform = getPlatform();
3404
- try {
3405
- const content = await platform.readFile(path);
3406
- if (!content) {
3407
- return;
3408
- }
3409
- await loadFormats();
3410
- if (formatForPath(path)) {
3411
- const { document: document2, frontmatter } = await parseSourceForPath(path, content);
3412
- tab.doc.document = document2;
3413
- tab.doc.content.frontmatter = frontmatter;
3414
- } else if (path.endsWith(".json")) {
3415
- tab.doc.document = JSON.parse(content);
3416
- }
3417
- tab.doc.dirty = false;
3418
- } catch {}
3419
- return;
3420
- }
3421
- }
3422
- }
3423
-
3424
- export { hydrateProjectList, getProjectList, installAutomationHook, ensureDependenciesInstalled, startFsSync, dataSurfaceAvailable, testConnection, pushSchema, deriveSecretEnvName, cellValuesEqual, parseGridTabId, gridTabLabel, ROW_KEY_FIELD, getGridController, createGridController, coerceCellInput, cellToText, createCsvFileSource, parseSourceForPath, saveFile, exportFile, serializeDocument, listCollectionEntryIds, openConnectorGrid, openGridSourcePicker, hydrateRecentProjects, getRecentProjects, addRecentProject, removeRecentProject, clearRecentProjects, getRecentFiles, trackRecentFile, loadDirectory, loadProject, openProject, findHomePage, openHomePage, renderFilesTemplate, setupTreeKeyboard, isFileDrag, uploadFilesToDir, pickAndUploadTo, registerFileTreeDnD, openFileFromTree, openFileInTab, reloadCleanTab, reloadFileInTab };
3425
-
3426
- //# debugId=F6DA95B972A0DA6E64756E2164756E21
3427
- //# sourceMappingURL=studio-d77qm3pk.js.map