@backmatter/studio 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (439) hide show
  1. package/dist/chunks/UiKitPage-CgNMOqfE.js +172 -0
  2. package/dist/chunks/WorkspaceRoutes-DgRtdie8.js +87297 -0
  3. package/dist/chunks/abap-Y8Dl9g_6.js +6 -0
  4. package/dist/chunks/actionscript-3-DQVEcJUW.js +6 -0
  5. package/dist/chunks/ada-vP6ak0IW.js +6 -0
  6. package/dist/chunks/andromeeda-BbmzSJq1.js +4 -0
  7. package/dist/chunks/angular-html-DPB4Rf0C.js +32 -0
  8. package/dist/chunks/angular-ts-CTOR2ofC.js +21 -0
  9. package/dist/chunks/apache-BUjz-sD2.js +6 -0
  10. package/dist/chunks/apex-OyTfjCYK.js +6 -0
  11. package/dist/chunks/apl-CjNiDgad.js +16 -0
  12. package/dist/chunks/applescript-BPx7YFFu.js +6 -0
  13. package/dist/chunks/ara-Z2fSOxSw.js +6 -0
  14. package/dist/chunks/asciidoc-C1T9ziH6.js +6 -0
  15. package/dist/chunks/asm-BTWLY5ym.js +6 -0
  16. package/dist/chunks/astro-CFFExE8Q.js +18 -0
  17. package/dist/chunks/aurora-x-BwoVEUWZ.js +4 -0
  18. package/dist/chunks/awk-Fb0P9dkn.js +6 -0
  19. package/dist/chunks/ayu-dark-DJoqd4M9.js +4 -0
  20. package/dist/chunks/ayu-light-BzXEJRJ-.js +4 -0
  21. package/dist/chunks/ayu-mirage-BJ2oZGzi.js +4 -0
  22. package/dist/chunks/ballerina-oZK-YekG.js +6 -0
  23. package/dist/chunks/bat-0FvbqU9S.js +6 -0
  24. package/dist/chunks/beancount-DqJEb89h.js +6 -0
  25. package/dist/chunks/berry-DODBq_Ff.js +6 -0
  26. package/dist/chunks/bibtex-EULQRLY5.js +6 -0
  27. package/dist/chunks/bicep-BorU73w0.js +6 -0
  28. package/dist/chunks/bird2-CIjBSNj3.js +6 -0
  29. package/dist/chunks/blade-D0HyNvCw.js +20 -0
  30. package/dist/chunks/bsl-NkNNwerW.js +8 -0
  31. package/dist/chunks/c-eeMepfLm.js +6 -0
  32. package/dist/chunks/c3-BY5XW5ym.js +6 -0
  33. package/dist/chunks/cadence-02UX7mW8.js +6 -0
  34. package/dist/chunks/cairo-DM6WF2e3.js +8 -0
  35. package/dist/chunks/catppuccin-frappe-D3cH2rXe.js +4 -0
  36. package/dist/chunks/catppuccin-latte-C0LRGUW4.js +4 -0
  37. package/dist/chunks/catppuccin-macchiato-c5wQ11TT.js +4 -0
  38. package/dist/chunks/catppuccin-mocha-WMD6Qvya.js +4 -0
  39. package/dist/chunks/clarity-PKm5CwqM.js +6 -0
  40. package/dist/chunks/clojure-CXJfHrL3.js +6 -0
  41. package/dist/chunks/cmake-BJz8BOTU.js +6 -0
  42. package/dist/chunks/cobol-HihOMwht.js +10 -0
  43. package/dist/chunks/codeowners-Bt9yU6NX.js +6 -0
  44. package/dist/chunks/codeql-DHkodjjI.js +6 -0
  45. package/dist/chunks/coffee-DGBR_nr_.js +8 -0
  46. package/dist/chunks/common-lisp-EVqT9Zhp.js +6 -0
  47. package/dist/chunks/coq-B0L9upzn.js +6 -0
  48. package/dist/chunks/cpp-Cj177cuW.js +18 -0
  49. package/dist/chunks/crystal-CRJyc4G-.js +18 -0
  50. package/dist/chunks/csharp-CCgA4A09.js +6 -0
  51. package/dist/chunks/css-CECN5uSL.js +6 -0
  52. package/dist/chunks/csv-CmYOceLb.js +6 -0
  53. package/dist/chunks/cue-ZzumE7IT.js +6 -0
  54. package/dist/chunks/cypher-jpdmjtA6.js +6 -0
  55. package/dist/chunks/d-CBagWSwH.js +6 -0
  56. package/dist/chunks/dark-plus-pUHDTVV0.js +4 -0
  57. package/dist/chunks/dart-6ObCrKr9.js +6 -0
  58. package/dist/chunks/dashboard-XKKZuJGm.js +1736 -0
  59. package/dist/chunks/dax-CcDT-8dH.js +6 -0
  60. package/dist/chunks/define-studio-BYtYDGLX.js +33 -0
  61. package/dist/chunks/desktop-B93p9R9O.js +6 -0
  62. package/dist/chunks/diff-BxzP2J8R.js +6 -0
  63. package/dist/chunks/docker-CsHqm9tx.js +6 -0
  64. package/dist/chunks/dotenv-BMjVjUL1.js +6 -0
  65. package/dist/chunks/dracula-BtZx2Kac.js +4 -0
  66. package/dist/chunks/dracula-soft-BKa-aqBv.js +4 -0
  67. package/dist/chunks/dream-maker-PAx17jaB.js +6 -0
  68. package/dist/chunks/edge-BYQkOzbF.js +12 -0
  69. package/dist/chunks/elixir-Dh036GMp.js +8 -0
  70. package/dist/chunks/elm-BtNbw_Cd.js +8 -0
  71. package/dist/chunks/emacs-lisp-Cfxdx5D-.js +6 -0
  72. package/dist/chunks/erb-ScVOXVpH.js +10 -0
  73. package/dist/chunks/erlang-DNcDT8Hi.js +8 -0
  74. package/dist/chunks/everforest-dark-DMCBqXCK.js +4 -0
  75. package/dist/chunks/everforest-light-BbXl82Em.js +4 -0
  76. package/dist/chunks/fennel-N4WcXuKx.js +6 -0
  77. package/dist/chunks/fish-BTDEUgqH.js +6 -0
  78. package/dist/chunks/fluent-BMXUxfv1.js +6 -0
  79. package/dist/chunks/fortran-fixed-form-B67KFkXH.js +8 -0
  80. package/dist/chunks/fortran-free-form-CwqYvCLJ.js +6 -0
  81. package/dist/chunks/fsharp-CYOiIVNx.js +8 -0
  82. package/dist/chunks/gdresource-CvCIUjhi.js +10 -0
  83. package/dist/chunks/gdscript-DSAxJ_aS.js +6 -0
  84. package/dist/chunks/gdshader-Bk8fF6yr.js +6 -0
  85. package/dist/chunks/genie-DzUvd7U9.js +6 -0
  86. package/dist/chunks/gherkin-DHRaV0YP.js +6 -0
  87. package/dist/chunks/git-commit-Bd32YZ0K.js +8 -0
  88. package/dist/chunks/git-rebase-ZWUFO_T4.js +8 -0
  89. package/dist/chunks/github-dark-DenFmJkN.js +4 -0
  90. package/dist/chunks/github-dark-default-BJPUVz4H.js +4 -0
  91. package/dist/chunks/github-dark-dimmed-DUshB20C.js +4 -0
  92. package/dist/chunks/github-dark-high-contrast-D3aGCnF8.js +4 -0
  93. package/dist/chunks/github-light-JYsPkUQd.js +4 -0
  94. package/dist/chunks/github-light-default-D99KPAby.js +4 -0
  95. package/dist/chunks/github-light-high-contrast-BbmZE-Mp.js +4 -0
  96. package/dist/chunks/gleam-Bv284lvN.js +6 -0
  97. package/dist/chunks/glimmer-js-DkxNrfPT.js +14 -0
  98. package/dist/chunks/glimmer-ts-D6YZEmK_.js +14 -0
  99. package/dist/chunks/glsl-CkUcVZNK.js +8 -0
  100. package/dist/chunks/gn-XtJxiLa4.js +6 -0
  101. package/dist/chunks/gnuplot-yPG9-sE_.js +6 -0
  102. package/dist/chunks/go-CvFNMTvu.js +6 -0
  103. package/dist/chunks/graphql-CBu2ehBn.js +14 -0
  104. package/dist/chunks/groovy-CJQTphOW.js +6 -0
  105. package/dist/chunks/gruvbox-dark-hard-C5HOtKIh.js +4 -0
  106. package/dist/chunks/gruvbox-dark-medium-FVgwJHuz.js +4 -0
  107. package/dist/chunks/gruvbox-dark-soft-B46F314v.js +4 -0
  108. package/dist/chunks/gruvbox-light-hard-CJD38wDZ.js +4 -0
  109. package/dist/chunks/gruvbox-light-medium-BlIhMYTA.js +4 -0
  110. package/dist/chunks/gruvbox-light-soft-DoPHyLVZ.js +4 -0
  111. package/dist/chunks/hack-ItRNKiEb.js +10 -0
  112. package/dist/chunks/haml-NZLVzu_R.js +10 -0
  113. package/dist/chunks/handlebars-mxStpSE7.js +14 -0
  114. package/dist/chunks/haskell-WeIwNIP6.js +6 -0
  115. package/dist/chunks/haxe-TztHsm5T.js +6 -0
  116. package/dist/chunks/hcl-D438OF-I.js +6 -0
  117. package/dist/chunks/hjson-DYBUbqOl.js +6 -0
  118. package/dist/chunks/hlsl-Bk8TCZNL.js +6 -0
  119. package/dist/chunks/horizon-CJQ10nlf.js +4 -0
  120. package/dist/chunks/horizon-bright-CdkBZOMB.js +4 -0
  121. package/dist/chunks/houston-BDYrDoDW.js +4 -0
  122. package/dist/chunks/html-B50bmoVb.js +10 -0
  123. package/dist/chunks/html-derivative-C6bxFbFW.js +8 -0
  124. package/dist/chunks/http-DLvAkeD-.js +14 -0
  125. package/dist/chunks/hurl-CgkFN90t.js +12 -0
  126. package/dist/chunks/hxml-GbqSQCLa.js +8 -0
  127. package/dist/chunks/hy-Brt5EZ7-.js +6 -0
  128. package/dist/chunks/imba-CimUv-Uh.js +6 -0
  129. package/dist/chunks/index-CFAIR8GH.js +18924 -0
  130. package/dist/chunks/ini-BZIuRIvJ.js +6 -0
  131. package/dist/chunks/java-DY6VlHhP.js +6 -0
  132. package/dist/chunks/javascript-C25yR2R2.js +6 -0
  133. package/dist/chunks/jinja-CFW5uAwY.js +11 -0
  134. package/dist/chunks/jison-DdreslRX.js +8 -0
  135. package/dist/chunks/json-DxJze_jm.js +6 -0
  136. package/dist/chunks/json5-BT4Fjg39.js +6 -0
  137. package/dist/chunks/jsonc-CHjZD8gR.js +6 -0
  138. package/dist/chunks/jsonl-BGuvDmy9.js +6 -0
  139. package/dist/chunks/jsonnet-Bx2cfsXg.js +6 -0
  140. package/dist/chunks/jssm-BcADi6EI.js +6 -0
  141. package/dist/chunks/jsx-BtKADgXT.js +6 -0
  142. package/dist/chunks/julia-CZiKXNNP.js +16 -0
  143. package/dist/chunks/just-CEK2SJ_k.js +18 -0
  144. package/dist/chunks/kanagawa-dragon-CiKur4Hl.js +4 -0
  145. package/dist/chunks/kanagawa-lotus-BKu-smKu.js +4 -0
  146. package/dist/chunks/kanagawa-wave-CQwozSzG.js +4 -0
  147. package/dist/chunks/kdl-BNOv9TC3.js +6 -0
  148. package/dist/chunks/kotlin-ByBMgTeR.js +6 -0
  149. package/dist/chunks/kusto-DH_XitHS.js +6 -0
  150. package/dist/chunks/laserwave-6a00oqik.js +4 -0
  151. package/dist/chunks/latex-B8Egmqc5.js +8 -0
  152. package/dist/chunks/lean-BnVxaZxY.js +6 -0
  153. package/dist/chunks/less-B1GLI2Di.js +6 -0
  154. package/dist/chunks/light-plus-CZuVqSLX.js +4 -0
  155. package/dist/chunks/liquid-CHzkx3jT.js +14 -0
  156. package/dist/chunks/llvm-Bk2XctFf.js +6 -0
  157. package/dist/chunks/log-Al8wyEFV.js +6 -0
  158. package/dist/chunks/logo-DBa4JDzV.js +6 -0
  159. package/dist/chunks/lua-Bp3IRv7I.js +8 -0
  160. package/dist/chunks/luau-UQyhudEE.js +6 -0
  161. package/dist/chunks/make-CsMclxtr.js +6 -0
  162. package/dist/chunks/markdown-CrScaQ96.js +6 -0
  163. package/dist/chunks/marko-CJBA24Ri.js +14 -0
  164. package/dist/chunks/material-theme-D6KBX41T.js +4 -0
  165. package/dist/chunks/material-theme-darker-CkRroheE.js +4 -0
  166. package/dist/chunks/material-theme-lighter-BUBw43Yz.js +4 -0
  167. package/dist/chunks/material-theme-ocean-ClGX14Ja.js +4 -0
  168. package/dist/chunks/material-theme-palenight-C1RVm8K1.js +4 -0
  169. package/dist/chunks/matlab-BOAaUVP0.js +6 -0
  170. package/dist/chunks/mdc-C9YSUwVC.js +12 -0
  171. package/dist/chunks/mdx-BOhZZUJ8.js +6 -0
  172. package/dist/chunks/mermaid-C3rz4dYh.js +6 -0
  173. package/dist/chunks/min-dark-C7ak0t6c.js +4 -0
  174. package/dist/chunks/min-light-CKFxVcPp.js +4 -0
  175. package/dist/chunks/mipsasm-CTx18fBl.js +6 -0
  176. package/dist/chunks/mojo-EIZIZRTQ.js +6 -0
  177. package/dist/chunks/monokai-C1KBYcO0.js +4 -0
  178. package/dist/chunks/moonbit-B1TV0fG3.js +6 -0
  179. package/dist/chunks/move-Db4ltDq1.js +6 -0
  180. package/dist/chunks/narrat-DmhDCBs-.js +6 -0
  181. package/dist/chunks/nextflow-47kAzSl1.js +8 -0
  182. package/dist/chunks/nextflow-groovy-Drz_2V0j.js +6 -0
  183. package/dist/chunks/nginx-D7O4WoB1.js +8 -0
  184. package/dist/chunks/night-owl-Bm2rzalh.js +4 -0
  185. package/dist/chunks/night-owl-light-CBI5u5kw.js +4 -0
  186. package/dist/chunks/nim-QAjRdh9w.js +20 -0
  187. package/dist/chunks/nix-fTRXMGic.js +9 -0
  188. package/dist/chunks/nord-CC5OiUXg.js +4 -0
  189. package/dist/chunks/nushell-BGCPRlV5.js +6 -0
  190. package/dist/chunks/objective-c-Itk8tzmv.js +6 -0
  191. package/dist/chunks/objective-cpp-DGt5UKRO.js +6 -0
  192. package/dist/chunks/ocaml-eSVK32Eg.js +6 -0
  193. package/dist/chunks/odin-BItnF517.js +6 -0
  194. package/dist/chunks/one-dark-pro-DTA3VF0_.js +4 -0
  195. package/dist/chunks/one-light-LkMrt1Cf.js +4 -0
  196. package/dist/chunks/openscad-Sp5uZ6f2.js +6 -0
  197. package/dist/chunks/pascal-xy8pJNns.js +6 -0
  198. package/dist/chunks/perl-DkVrgJBg.js +16 -0
  199. package/dist/chunks/php-BIzsF_5x.js +18 -0
  200. package/dist/chunks/pkl-C-zSNmaA.js +6 -0
  201. package/dist/chunks/plastic-CSTz3KZp.js +4 -0
  202. package/dist/chunks/plsql-pVbGZfOv.js +6 -0
  203. package/dist/chunks/po-BNfHvqmm.js +6 -0
  204. package/dist/chunks/poimandres-C-VADXHD.js +4 -0
  205. package/dist/chunks/polar-CAZahv3u.js +6 -0
  206. package/dist/chunks/postcss-05aHdL-n.js +6 -0
  207. package/dist/chunks/powerquery-DI9HkTvs.js +6 -0
  208. package/dist/chunks/powershell-Clc4ydu-.js +6 -0
  209. package/dist/chunks/prisma-FZjmVtSl.js +6 -0
  210. package/dist/chunks/prolog-C5-yg4TO.js +6 -0
  211. package/dist/chunks/proto-C7QgialS.js +6 -0
  212. package/dist/chunks/pug-XOw9Q1Ho.js +12 -0
  213. package/dist/chunks/puppet-CUJHmnxV.js +6 -0
  214. package/dist/chunks/purescript-rUfGld-4.js +6 -0
  215. package/dist/chunks/python-BFNSHbwJ.js +6 -0
  216. package/dist/chunks/qml-rTNXsquU.js +8 -0
  217. package/dist/chunks/qmldir-BInDYbpo.js +6 -0
  218. package/dist/chunks/qss-AeJTysr_.js +6 -0
  219. package/dist/chunks/r-CSmzDPi7.js +6 -0
  220. package/dist/chunks/racket-B83wSAja.js +6 -0
  221. package/dist/chunks/raku-nEQ4ZJJ7.js +6 -0
  222. package/dist/chunks/razor-Ci8D0dQK.js +10 -0
  223. package/dist/chunks/red-7y8PH7HH.js +4 -0
  224. package/dist/chunks/reg-m_s_Kiip.js +6 -0
  225. package/dist/chunks/regexp-BazyLpPg.js +6 -0
  226. package/dist/chunks/rel-BcRfyd6Q.js +6 -0
  227. package/dist/chunks/riscv-Ce8MAQLP.js +6 -0
  228. package/dist/chunks/ron-DCEEQypA.js +6 -0
  229. package/dist/chunks/rose-pine-BKc3yVeu.js +4 -0
  230. package/dist/chunks/rose-pine-dawn-BulJcPZT.js +4 -0
  231. package/dist/chunks/rose-pine-moon-j6jiXKV8.js +4 -0
  232. package/dist/chunks/rosmsg-Cz0w1km8.js +6 -0
  233. package/dist/chunks/rst-DM32Wy3O.js +22 -0
  234. package/dist/chunks/ruby-BeqCs27h.js +30 -0
  235. package/dist/chunks/rust-CLzF9zIN.js +6 -0
  236. package/dist/chunks/sas-fpEvgATw.js +8 -0
  237. package/dist/chunks/sass-DxHp5rTx.js +6 -0
  238. package/dist/chunks/scala-D4grkFkl.js +6 -0
  239. package/dist/chunks/scheme-BCRWuEm4.js +6 -0
  240. package/dist/chunks/scss-B1FaCqwR.js +8 -0
  241. package/dist/chunks/sdbl-B7T8abf4.js +6 -0
  242. package/dist/chunks/shaderlab-Cr62-Wb4.js +8 -0
  243. package/dist/chunks/shellscript-InADTalH.js +6 -0
  244. package/dist/chunks/shellsession-DiDJNQdy.js +8 -0
  245. package/dist/chunks/slack-dark-i7wN4OET.js +4 -0
  246. package/dist/chunks/slack-ochin-ndHf0LoP.js +4 -0
  247. package/dist/chunks/smalltalk-BlI1_OkM.js +6 -0
  248. package/dist/chunks/snazzy-light-BlSJXAu4.js +4 -0
  249. package/dist/chunks/solarized-dark-UTmkh7lw.js +4 -0
  250. package/dist/chunks/solarized-light-BheCkDPT.js +4 -0
  251. package/dist/chunks/solidity-DUWnFhS6.js +6 -0
  252. package/dist/chunks/soy-C1-6njHa.js +8 -0
  253. package/dist/chunks/sparql-B0KWFEri.js +8 -0
  254. package/dist/chunks/splunk-CRXR8A9s.js +6 -0
  255. package/dist/chunks/sql-Cn_v3PB0.js +6 -0
  256. package/dist/chunks/ssh-config-DP-hNVbF.js +6 -0
  257. package/dist/chunks/stata-B8c5fTjX.js +8 -0
  258. package/dist/chunks/stylus-CyKEU1Ej.js +6 -0
  259. package/dist/chunks/surrealql-CQi7GQN7.js +8 -0
  260. package/dist/chunks/svelte-rYri41WT.js +14 -0
  261. package/dist/chunks/swift-DJpUqPLg.js +6 -0
  262. package/dist/chunks/synthwave-84-NU3C_KFZ.js +4 -0
  263. package/dist/chunks/system-verilog-BCm7smPJ.js +6 -0
  264. package/dist/chunks/systemd-C-4qm6XH.js +6 -0
  265. package/dist/chunks/talonscript-CFF3LF_O.js +6 -0
  266. package/dist/chunks/tasl-Cg_WBUAe.js +6 -0
  267. package/dist/chunks/tcl-DN7buRTF.js +6 -0
  268. package/dist/chunks/templ-Crs9Ui8q.js +12 -0
  269. package/dist/chunks/terraform-DGvcn9zM.js +6 -0
  270. package/dist/chunks/tex-AHxmLTL0.js +8 -0
  271. package/dist/chunks/tokyo-night-DP4TmcQl.js +4 -0
  272. package/dist/chunks/toml-DY62mUL_.js +6 -0
  273. package/dist/chunks/ts-tags-ioVe2PKw.js +41 -0
  274. package/dist/chunks/tsv-BtvSkaG0.js +6 -0
  275. package/dist/chunks/tsx-B8rCNbgL.js +6 -0
  276. package/dist/chunks/turtle-_H59FV7D.js +6 -0
  277. package/dist/chunks/twig-DwV2Fi4L.js +18 -0
  278. package/dist/chunks/typescript-RycA9KXf.js +6 -0
  279. package/dist/chunks/typespec-DWqp-kUU.js +6 -0
  280. package/dist/chunks/typst-D_1QKWns.js +6 -0
  281. package/dist/chunks/v-BPCNiyYe.js +6 -0
  282. package/dist/chunks/vala-uxaPR7d1.js +6 -0
  283. package/dist/chunks/vb-D8_c5-KN.js +6 -0
  284. package/dist/chunks/verilog-B-bybjPF.js +6 -0
  285. package/dist/chunks/vesper-BckBta1U.js +4 -0
  286. package/dist/chunks/vhdl-CUlNa8ac.js +6 -0
  287. package/dist/chunks/viml-DsfA-sWm.js +6 -0
  288. package/dist/chunks/vitesse-black-BoGvW84i.js +4 -0
  289. package/dist/chunks/vitesse-dark-Cym-eLtO.js +4 -0
  290. package/dist/chunks/vitesse-light-CcmG315c.js +4 -0
  291. package/dist/chunks/vue-DXEmyXn9.js +31 -0
  292. package/dist/chunks/vue-html-j9xNEdu3.js +8 -0
  293. package/dist/chunks/vue-vine-D41-N99t.js +18 -0
  294. package/dist/chunks/vyper-CPQuu50u.js +6 -0
  295. package/dist/chunks/wasm-BBXxrAl7.js +6 -0
  296. package/dist/chunks/wasm-DQxwEHae.js +7 -0
  297. package/dist/chunks/wenyan-pbVjoM9_.js +6 -0
  298. package/dist/chunks/wgsl-DY4iK1q1.js +6 -0
  299. package/dist/chunks/wikitext-Z-MoUasO.js +6 -0
  300. package/dist/chunks/wit-CQMQOlTg.js +6 -0
  301. package/dist/chunks/wolfram-Dz4KXISs.js +6 -0
  302. package/dist/chunks/xml-C2J0sS9M.js +8 -0
  303. package/dist/chunks/xsl-BmSZphgC.js +8 -0
  304. package/dist/chunks/yaml-DaO7k5B1.js +6 -0
  305. package/dist/chunks/zenscript-CxBjpf9c.js +6 -0
  306. package/dist/chunks/zig-Vm0PO9wB.js +6 -0
  307. package/dist/dashboard.js +8 -0
  308. package/dist/define.js +4 -0
  309. package/dist/index.js +317 -0
  310. package/dist/studio.css +2272 -0
  311. package/dist/styles.js +1 -0
  312. package/package.json +88 -0
  313. package/prose-node-shims/fs.ts +6 -0
  314. package/prose-node-shims/path.ts +16 -0
  315. package/prose-node-shims/sharp.ts +4 -0
  316. package/prose-ui-node-shim.ts +26 -0
  317. package/src/App.tsx +173 -0
  318. package/src/WorkspaceRoutes.tsx +38 -0
  319. package/src/api.ts +833 -0
  320. package/src/auth-client.ts +15 -0
  321. package/src/chat/ChangeCard.tsx +330 -0
  322. package/src/chat/ChatBar.tsx +238 -0
  323. package/src/chat/ChatMessage.tsx +478 -0
  324. package/src/chat/ChatPanel.tsx +406 -0
  325. package/src/chat/ChatProvider.tsx +661 -0
  326. package/src/chat/Composer.tsx +461 -0
  327. package/src/chat/DocListCard.tsx +322 -0
  328. package/src/chat/LiveStatus.tsx +83 -0
  329. package/src/chat/MentionPopover.tsx +164 -0
  330. package/src/chat/ModelPicker.tsx +113 -0
  331. package/src/chat/Shimmer.tsx +39 -0
  332. package/src/chat/ThreadHistory.tsx +268 -0
  333. package/src/chat/chat-context.ts +41 -0
  334. package/src/chat/chat-helpers.ts +402 -0
  335. package/src/chat/chat-state.ts +59 -0
  336. package/src/chat/icons.tsx +171 -0
  337. package/src/chat/useStickToBottom.ts +110 -0
  338. package/src/chrome/AppShell.tsx +100 -0
  339. package/src/chrome/AppSidebar.tsx +305 -0
  340. package/src/chrome/CommandPalette.tsx +559 -0
  341. package/src/chrome/ContentApiPopover.tsx +215 -0
  342. package/src/chrome/EditorActionsContext.tsx +63 -0
  343. package/src/chrome/EditorFocusContext.tsx +80 -0
  344. package/src/chrome/EditorStatusContext.tsx +84 -0
  345. package/src/chrome/NavItem.tsx +75 -0
  346. package/src/chrome/OpenSiteButton.tsx +148 -0
  347. package/src/chrome/StatusBar.tsx +73 -0
  348. package/src/chrome/UserMenu.tsx +130 -0
  349. package/src/chrome/WorkspaceNavContext.tsx +60 -0
  350. package/src/chrome/WorkspaceStatusBar.tsx +940 -0
  351. package/src/chrome/content-api-url.ts +111 -0
  352. package/src/chrome/content-type-groups.ts +32 -0
  353. package/src/chrome/hotkeys.ts +79 -0
  354. package/src/chrome/icons.tsx +191 -0
  355. package/src/chrome/open-site.ts +30 -0
  356. package/src/chrome/status-bar-action.ts +24 -0
  357. package/src/components/AppChromeContext.tsx +32 -0
  358. package/src/components/Shell.tsx +18 -0
  359. package/src/components/WorkspaceSwitcher.tsx +345 -0
  360. package/src/content-blocks/index.tsx +262 -0
  361. package/src/css.d.ts +2 -0
  362. package/src/dashboard.ts +5 -0
  363. package/src/define-studio.tsx +98 -0
  364. package/src/define.ts +12 -0
  365. package/src/index.css +123 -0
  366. package/src/index.ts +19 -0
  367. package/src/last-user.ts +40 -0
  368. package/src/last-workspace.ts +57 -0
  369. package/src/mcp/McpConnectorsPanel.tsx +872 -0
  370. package/src/mcp/parse-json.ts +52 -0
  371. package/src/pages/AcceptInvitePage.tsx +99 -0
  372. package/src/pages/BlockSettingsDialog.tsx +303 -0
  373. package/src/pages/BlocksList.tsx +402 -0
  374. package/src/pages/BodyProse.tsx +236 -0
  375. package/src/pages/DocEditorPage.tsx +1394 -0
  376. package/src/pages/EmbedChatPage.tsx +110 -0
  377. package/src/pages/EmbedPublishPage.tsx +259 -0
  378. package/src/pages/EnterPage.tsx +96 -0
  379. package/src/pages/GithubConnectPage.tsx +142 -0
  380. package/src/pages/LegacyWorkspaceRedirect.tsx +57 -0
  381. package/src/pages/LoginPage.tsx +335 -0
  382. package/src/pages/NewWorkspacePage.tsx +74 -0
  383. package/src/pages/RenderedBody.tsx +598 -0
  384. package/src/pages/ResumeHome.tsx +18 -0
  385. package/src/pages/SelectionBar.tsx +277 -0
  386. package/src/pages/SettingsGeneralPage.tsx +403 -0
  387. package/src/pages/SettingsGithubPage.tsx +277 -0
  388. package/src/pages/SettingsLayout.tsx +127 -0
  389. package/src/pages/SettingsMcpPage.tsx +30 -0
  390. package/src/pages/SettingsSchemaPage.tsx +93 -0
  391. package/src/pages/SettingsUsersPage.tsx +363 -0
  392. package/src/pages/TypeDocsPage.tsx +225 -0
  393. package/src/pages/UiKitPage.tsx +331 -0
  394. package/src/pages/UserSettingsPage.tsx +94 -0
  395. package/src/pages/WorkspaceHomeRedirect.tsx +17 -0
  396. package/src/pages/WorkspaceLayout.tsx +103 -0
  397. package/src/pages/WorkspacesPage.tsx +35 -0
  398. package/src/pages/blocks-list.ts +73 -0
  399. package/src/pages/body-blocks.ts +233 -0
  400. package/src/pages/body-mdx.tsx +82 -0
  401. package/src/pages/compile-body.ts +37 -0
  402. package/src/pages/host-env.ts +22 -0
  403. package/src/pages/html-to-markdown.ts +99 -0
  404. package/src/pages/preview-bridge.ts +46 -0
  405. package/src/pages/preview-split.ts +30 -0
  406. package/src/pages/preview-url.ts +92 -0
  407. package/src/pages/selection-format.ts +179 -0
  408. package/src/pages/type-docs.ts +9 -0
  409. package/src/replica/BootstrapSplash.tsx +80 -0
  410. package/src/replica/ReplicaProvider.tsx +267 -0
  411. package/src/replica/cache.ts +30 -0
  412. package/src/replica/idb.ts +89 -0
  413. package/src/replica/instance.ts +39 -0
  414. package/src/replica/pull.ts +23 -0
  415. package/src/replica/store.ts +497 -0
  416. package/src/replica/sync-cache.ts +51 -0
  417. package/src/replica/types.ts +119 -0
  418. package/src/replica/useReplica.ts +13 -0
  419. package/src/site.tsx +230 -0
  420. package/src/studio-config.ts +81 -0
  421. package/src/styles/themes.ts +49 -0
  422. package/src/styles/tokens.stylex.ts +58 -0
  423. package/src/styles.ts +11 -0
  424. package/src/theme/AccentOverride.tsx +29 -0
  425. package/src/theme/ThemeProvider.tsx +130 -0
  426. package/src/theme/apply-theme-class.ts +12 -0
  427. package/src/ui/BackLink.tsx +28 -0
  428. package/src/ui/Button.tsx +167 -0
  429. package/src/ui/ConfirmDialog.tsx +141 -0
  430. package/src/ui/Field.tsx +45 -0
  431. package/src/ui/Input.tsx +183 -0
  432. package/src/ui/ListRow.tsx +88 -0
  433. package/src/ui/Page.tsx +151 -0
  434. package/src/ui/Text.tsx +77 -0
  435. package/src/ui/Tooltip.tsx +85 -0
  436. package/src/ui/popup.ts +216 -0
  437. package/src/workspace/permissions.ts +17 -0
  438. package/src/workspace/types.ts +12 -0
  439. package/src/workspace-path.ts +98 -0
@@ -0,0 +1,1394 @@
1
+ // packages/studio/src/pages/DocEditorPage.tsx
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { useOutletContext, useParams, useSearchParams, Link, useNavigate } from "react-router";
4
+ import * as stylex from "@stylexjs/stylex";
5
+ import {
6
+ fetchDoc,
7
+ notifyTreeChanged,
8
+ type TreeChangeDetail,
9
+ createPreviewTicket,
10
+ resolveDoc,
11
+ revertDoc,
12
+ saveDoc,
13
+ type ApiError,
14
+ type ContentSource,
15
+ type DocStatus,
16
+ type SchemaPayload,
17
+ type SchemaField,
18
+ } from "../api";
19
+ import { resolveFieldInput, useStudioDefinition } from "../define-studio";
20
+ import { useSetEditorActions } from "../chrome/EditorActionsContext";
21
+ import {
22
+ DOC_CHANGED_EVENT,
23
+ docKey,
24
+ sameDocRef,
25
+ useEditorFocus,
26
+ type DocChangedDetail,
27
+ type EditorSelection,
28
+ } from "../chrome/EditorFocusContext";
29
+ import { useSetEditorStatus } from "../chrome/EditorStatusContext";
30
+ import { modifierSymbol } from "../chrome/hotkeys";
31
+ import { ReloadIcon } from "../chrome/icons";
32
+ import { colors, radius, space, type } from "../styles/tokens.stylex";
33
+ import { Button, ButtonLink } from "../ui/Button";
34
+ import { ConfirmDialog } from "../ui/ConfirmDialog";
35
+ import { Field } from "../ui/Field";
36
+ import { Input, Textarea } from "../ui/Input";
37
+ import { Text } from "../ui/Text";
38
+ import { Tooltip } from "../ui/Tooltip";
39
+ import {
40
+ PREVIEW_PCT_MAX,
41
+ PREVIEW_PCT_MIN,
42
+ WIDE_QUERY,
43
+ clampPreviewPct,
44
+ persistPreviewPct,
45
+ readPreviewPct,
46
+ } from "./preview-split";
47
+ import {
48
+ editorPreviewPath,
49
+ modelForType,
50
+ previewSiteUrl,
51
+ primarySiteUrl,
52
+ onSitePreviewUrl,
53
+ studioIsOnSite,
54
+ productionDocUrl,
55
+ } from "./preview-url";
56
+ import { BodyField } from "./BodyProse";
57
+ import { BlocksList } from "./BlocksList";
58
+ import { blocksFromFrontmatter } from "./blocks-list";
59
+ import type { WorkspaceOutletContext } from "../workspace/types";
60
+ import { contentDocPath, defaultLocaleOf, settingsPath } from "../workspace-path";
61
+ import { bindPreviewEditor, previewFrameTransport } from "./preview-bridge";
62
+ import { createWindowPreviewTransport, type PreviewChannelRef } from "@backmatter/core/preview";
63
+
64
+ const AUTOSAVE_MS = 400;
65
+ const hover = "@media (hover: hover)";
66
+
67
+ const spin = stylex.keyframes({
68
+ to: { transform: "rotate(360deg)" },
69
+ });
70
+
71
+ const pulse = stylex.keyframes({
72
+ "0%": { opacity: 0.4 },
73
+ "50%": { opacity: 1 },
74
+ "100%": { opacity: 0.4 },
75
+ });
76
+
77
+ function useWideLayout() {
78
+ const [wide, setWide] = useState(() => window.matchMedia(WIDE_QUERY).matches);
79
+ useEffect(() => {
80
+ const media = window.matchMedia(WIDE_QUERY);
81
+ const onChange = () => setWide(media.matches);
82
+ media.addEventListener("change", onChange);
83
+ return () => media.removeEventListener("change", onChange);
84
+ }, []);
85
+ return wide;
86
+ }
87
+
88
+ function fieldLabel(name: string) {
89
+ return name.charAt(0).toUpperCase() + name.slice(1);
90
+ }
91
+
92
+ function formatApiError(err: unknown): string {
93
+ if (!(err instanceof Error)) return "Request failed";
94
+ const apiErr = err as ApiError;
95
+ if (apiErr.code === "forbidden") return "You do not have permission to edit content.";
96
+ if (apiErr.code === "validation") {
97
+ return err.message.replace(/^validation:\s*/, "Validation failed: ");
98
+ }
99
+ return err.message;
100
+ }
101
+
102
+ function isNotFound(err: unknown): err is ApiError {
103
+ return Boolean(err && typeof err === "object" && "status" in err && (err as ApiError).status === 404);
104
+ }
105
+
106
+ function emptyFrontmatter(model: SchemaPayload["models"][number] | undefined) {
107
+ const out: Record<string, unknown> = {};
108
+ if (!model) return out;
109
+ for (const [name, field] of Object.entries(model.fields)) {
110
+ if (field.kind === "array" || field.kind === "blocks") out[name] = [];
111
+ else if (field.kind === "object") out[name] = {};
112
+ else if (field.kind === "boolean") out[name] = false;
113
+ else if (field.kind === "number") out[name] = 0;
114
+ else out[name] = "";
115
+ }
116
+ return out;
117
+ }
118
+
119
+ function JsonField({
120
+ value,
121
+ kind,
122
+ readOnly,
123
+ onValidChange,
124
+ }: {
125
+ value: unknown;
126
+ kind: "object" | "array";
127
+ readOnly?: boolean;
128
+ onValidChange: (next: unknown) => void;
129
+ }) {
130
+ const serialized = JSON.stringify(value ?? (kind === "array" ? [] : {}), null, 2);
131
+ const [draft, setDraft] = useState(serialized);
132
+ const [focused, setFocused] = useState(false);
133
+ const [invalid, setInvalid] = useState(false);
134
+ useEffect(() => {
135
+ if (!focused) {
136
+ setDraft(serialized);
137
+ setInvalid(false);
138
+ }
139
+ }, [serialized, focused]);
140
+ return (
141
+ <Textarea
142
+ value={draft}
143
+ readOnly={readOnly}
144
+ aria-invalid={invalid || undefined}
145
+ onFocus={() => setFocused(true)}
146
+ onBlur={() => setFocused(false)}
147
+ onChange={(e) => {
148
+ const next = e.target.value;
149
+ setDraft(next);
150
+ try {
151
+ onValidChange(JSON.parse(next || (kind === "array" ? "[]" : "{}")));
152
+ setInvalid(false);
153
+ } catch {
154
+ setInvalid(true);
155
+ }
156
+ }}
157
+ />
158
+ );
159
+ }
160
+
161
+ /** Status-bar label for the doc's state against the branch HEAD. */
162
+ export function docStatusLabel(status: DocStatus | null): string | null {
163
+ if (status === null) return null;
164
+ if (status === "clean") return "committed";
165
+ if (status === "conflict") return "conflict";
166
+ return "uncommitted";
167
+ }
168
+
169
+ type SaveState = "idle" | "dirty" | "saving" | "saved" | "error";
170
+
171
+ export function DocEditorPage() {
172
+ const { type = "", docSlug = "" } = useParams();
173
+ const [searchParams] = useSearchParams();
174
+ const { workspace, blocks, richtext, models } =
175
+ useOutletContext<WorkspaceOutletContext>();
176
+ const workspaceId = workspace.id;
177
+ const slug = docSlug;
178
+ const model = models.find((m) => m.type === type);
179
+ const embed = searchParams.get("embed") === "1";
180
+ const showPreview = !embed && model?.route !== false;
181
+ const locale = model?.path.includes("{locale}")
182
+ ? searchParams.get("locale") || defaultLocaleOf(workspace.locales)
183
+ : "";
184
+
185
+ const [source, setSource] = useState<ContentSource | null>(null);
186
+ const [docStatus, setDocStatus] = useState<DocStatus | null>(null);
187
+ const [branch, setBranch] = useState<string | null>(null);
188
+ const [tree, setTree] = useState<string | null>(null);
189
+ const [frontmatter, setFrontmatter] = useState<Record<string, unknown>>({});
190
+ const [body, setBody] = useState("");
191
+ const [fieldNames, setFieldNames] = useState<string[]>([]);
192
+ const [docFormat, setDocFormat] = useState<"mdx" | "page">("mdx");
193
+ const [fieldKinds, setFieldKinds] = useState<Record<string, string>>({});
194
+ const [fieldDefs, setFieldDefs] = useState<Record<string, SchemaField>>({});
195
+ const studio = useStudioDefinition();
196
+ const [parseFailed, setParseFailed] = useState(false);
197
+ const [error, setError] = useState<string | null>(null);
198
+ const [status, setStatus] = useState<string | null>(null);
199
+ const [loading, setLoading] = useState(true);
200
+ const [busy, setBusy] = useState<string | null>(null);
201
+ const [confirmRevert, setConfirmRevert] = useState(false);
202
+ const [saveState, setSaveState] = useState<SaveState>("idle");
203
+ const [previewKey, setPreviewKey] = useState(0);
204
+ const [previewLoading, setPreviewLoading] = useState(true);
205
+ const [previewFrame, setPreviewFrame] = useState(0);
206
+ const [previewOffer, setPreviewOffer] = useState<PreviewChannelRef | null>(null);
207
+ const [previewLive, setPreviewLive] = useState<"rendered" | "error" | null>(null);
208
+ const [previewPct, setPreviewPct] = useState(readPreviewPct);
209
+ const [dragging, setDragging] = useState(false);
210
+ const [editEpoch, setEditEpoch] = useState(0);
211
+ const [selection, setSelection] = useState<EditorSelection | null>(null);
212
+ const splitRef = useRef<HTMLDivElement>(null);
213
+ const previewFrameRef = useRef<HTMLIFrameElement>(null);
214
+ const previewSession = useRef<ReturnType<typeof bindPreviewEditor> | null>(null);
215
+ const navigate = useNavigate();
216
+ const wide = useWideLayout();
217
+ const { setStatus: setEditorStatus, clear: clearEditorStatus } =
218
+ useSetEditorStatus();
219
+ const editorActions = useSetEditorActions();
220
+ const { setFocus: setEditorFocus, aiEditingDocKey } = useEditorFocus();
221
+ const docRef = useMemo(() => ({ locale, type, slug }), [locale, type, slug]);
222
+ const aiEditing = aiEditingDocKey !== null && aiEditingDocKey === docKey(docRef);
223
+ const aiEditingRef = useRef(aiEditing);
224
+ aiEditingRef.current = aiEditing;
225
+ const persistRef = useRef<(reason: "auto" | "manual") => Promise<void>>(
226
+ async () => {},
227
+ );
228
+ const revertRef = useRef<() => Promise<void>>(async () => {});
229
+ const mod = modifierSymbol();
230
+
231
+ const hydrated = useRef(false);
232
+ const saveGen = useRef(0);
233
+ const latest = useRef({ frontmatter, body });
234
+ latest.current = { frontmatter, body };
235
+ // Mirrors docStatus for event handlers that must not re-subscribe on change.
236
+ const docStatusRef = useRef<DocStatus | null>(null);
237
+ docStatusRef.current = docStatus;
238
+ const channelRef = useRef<PreviewChannelRef>({ locale, type, slug });
239
+ channelRef.current = {
240
+ locale,
241
+ type,
242
+ slug,
243
+ ...(branch ? { branch } : {}),
244
+ ...(tree ? { tree } : {}),
245
+ };
246
+ const editEpochRef = useRef(editEpoch);
247
+ editEpochRef.current = editEpoch;
248
+
249
+ const [previewUrl, setPreviewUrl] = useState<string | null>(null);
250
+ const isPage = docFormat === "page";
251
+ const detailNames = fieldNames.filter((name) => fieldKinds[name] !== "blocks");
252
+ const blocksField =
253
+ fieldNames.find((name) => fieldKinds[name] === "blocks") ?? "blocks";
254
+ const siteUrl = primarySiteUrl(workspace.siteUrls ?? []);
255
+ const liveUrl = productionDocUrl(siteUrl, modelForType(models, type), locale, slug);
256
+
257
+ useEffect(() => {
258
+ if (!showPreview) {
259
+ setPreviewUrl(null);
260
+ return;
261
+ }
262
+ const redirect = editorPreviewPath(
263
+ modelForType(models, type),
264
+ slug,
265
+ locale,
266
+ branch,
267
+ tree,
268
+ );
269
+ if (!redirect || !workspaceId || !siteUrl) {
270
+ setPreviewUrl(null);
271
+ return;
272
+ }
273
+ if (studioIsOnSite(siteUrl, window.location.origin)) {
274
+ setPreviewUrl(onSitePreviewUrl(siteUrl, redirect));
275
+ return;
276
+ }
277
+ let cancelled = false;
278
+ void createPreviewTicket(workspaceId, { redirect, branch, tree })
279
+ .then((res) => {
280
+ if (!cancelled) setPreviewUrl(previewSiteUrl(workspace.siteUrls ?? [], res.ticket));
281
+ })
282
+ .catch(() => {
283
+ if (!cancelled) setPreviewUrl(null);
284
+ });
285
+ return () => {
286
+ cancelled = true;
287
+ };
288
+ }, [showPreview, workspaceId, workspace.siteUrls, siteUrl, models, type, slug, locale, branch, tree]);
289
+
290
+ useEffect(() => {
291
+ setPreviewLoading(true);
292
+ }, [previewUrl]);
293
+
294
+ useEffect(() => {
295
+ persistPreviewPct(previewPct);
296
+ }, [previewPct]);
297
+
298
+ useEffect(() => {
299
+ setPreviewOffer(null);
300
+ setPreviewLive(null);
301
+ }, [previewUrl]);
302
+
303
+ useEffect(() => {
304
+ const frame = previewFrameRef.current;
305
+ if (!frame || !siteUrl) return;
306
+ let origin: string;
307
+ try {
308
+ origin = new URL(siteUrl).origin;
309
+ } catch {
310
+ return;
311
+ }
312
+ const transport = previewFrameTransport({ frame, siteOrigin: origin });
313
+ if (!transport) return;
314
+ const session = bindPreviewEditor({
315
+ transport,
316
+ ref: () => channelRef.current,
317
+ onOffer: setPreviewOffer,
318
+ onStatus: setPreviewLive,
319
+ });
320
+ previewSession.current = session;
321
+ if (editEpochRef.current > 0) session.setDoc(latest.current);
322
+ return () => {
323
+ previewSession.current = null;
324
+ session.close();
325
+ transport.close();
326
+ };
327
+ }, [previewFrame, previewUrl, siteUrl]);
328
+
329
+ useEffect(() => {
330
+ if (!embed || window.parent === window) return;
331
+ let origin = "";
332
+ try {
333
+ origin = document.referrer ? new URL(document.referrer).origin : "";
334
+ } catch {
335
+ origin = "";
336
+ }
337
+ if (!origin) return;
338
+ const transport = createWindowPreviewTransport({
339
+ target: window.parent,
340
+ targetOrigin: origin,
341
+ allowedOrigins: [origin],
342
+ });
343
+ const session = bindPreviewEditor({
344
+ transport,
345
+ ref: () => channelRef.current,
346
+ onOffer: setPreviewOffer,
347
+ onStatus: setPreviewLive,
348
+ });
349
+ previewSession.current = session;
350
+ if (editEpochRef.current > 0) session.setDoc(latest.current);
351
+ function onMessage(event: MessageEvent) {
352
+ if (event.origin !== origin) return;
353
+ if (event.data?.type !== "backmatter:reload-doc") return;
354
+ window.dispatchEvent(
355
+ new CustomEvent(DOC_CHANGED_EVENT, { detail: { docRef: channelRef.current } }),
356
+ );
357
+ }
358
+ window.addEventListener("message", onMessage);
359
+ return () => {
360
+ window.removeEventListener("message", onMessage);
361
+ previewSession.current = null;
362
+ session.close();
363
+ transport.close();
364
+ };
365
+ }, [embed]);
366
+
367
+ useEffect(() => {
368
+ if (editEpoch === 0) return;
369
+ previewSession.current?.setDoc(latest.current);
370
+ }, [editEpoch]);
371
+
372
+ function remountPreview() {
373
+ setPreviewLoading(true);
374
+ setPreviewKey((k) => k + 1);
375
+ }
376
+
377
+ function updatePreviewPctFromPointer(clientX: number, clientY: number) {
378
+ const split = splitRef.current;
379
+ if (!split) return;
380
+ const rect = split.getBoundingClientRect();
381
+ const next = wide
382
+ ? ((clientX - rect.left) / rect.width) * 100
383
+ : ((clientY - rect.top) / rect.height) * 100;
384
+ setPreviewPct(clampPreviewPct(next));
385
+ }
386
+
387
+ function onSplitPointerDown(e: React.PointerEvent<HTMLDivElement>) {
388
+ if (e.button !== 0) return;
389
+ e.preventDefault();
390
+ e.currentTarget.focus();
391
+ e.currentTarget.setPointerCapture(e.pointerId);
392
+ setDragging(true);
393
+ updatePreviewPctFromPointer(e.clientX, e.clientY);
394
+ }
395
+
396
+ function onSplitPointerMove(e: React.PointerEvent<HTMLDivElement>) {
397
+ if (!e.currentTarget.hasPointerCapture(e.pointerId)) return;
398
+ updatePreviewPctFromPointer(e.clientX, e.clientY);
399
+ }
400
+
401
+ function onSplitPointerUp(e: React.PointerEvent<HTMLDivElement>) {
402
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
403
+ e.currentTarget.releasePointerCapture(e.pointerId);
404
+ }
405
+ setDragging(false);
406
+ }
407
+
408
+ function onSplitKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
409
+ const step = e.shiftKey ? 10 : 2;
410
+ if (wide ? e.key === "ArrowLeft" : e.key === "ArrowUp") {
411
+ e.preventDefault();
412
+ setPreviewPct((pct) => clampPreviewPct(pct - step));
413
+ return;
414
+ }
415
+ if (wide ? e.key === "ArrowRight" : e.key === "ArrowDown") {
416
+ e.preventDefault();
417
+ setPreviewPct((pct) => clampPreviewPct(pct + step));
418
+ return;
419
+ }
420
+ if (e.key === "Home") {
421
+ e.preventDefault();
422
+ setPreviewPct(PREVIEW_PCT_MIN);
423
+ return;
424
+ }
425
+ if (e.key === "End") {
426
+ e.preventDefault();
427
+ setPreviewPct(PREVIEW_PCT_MAX);
428
+ }
429
+ }
430
+
431
+ function applyModelFields(
432
+ fields: Record<string, { kind: string }> | undefined,
433
+ fallbackNames: string[],
434
+ ) {
435
+ const names = fields ? Object.keys(fields) : fallbackNames;
436
+ setFieldNames(names);
437
+ setFieldKinds(
438
+ Object.fromEntries(
439
+ Object.entries(fields ?? {}).map(([name, field]) => [name, field.kind]),
440
+ ),
441
+ );
442
+ setFieldDefs(fields ?? {});
443
+ return names;
444
+ }
445
+
446
+ async function reload() {
447
+ try {
448
+ const data = await fetchDoc(workspaceId, locale, type, slug);
449
+ hydrated.current = false;
450
+ setSource(data.doc.source);
451
+ setDocStatus(data.doc.status);
452
+ setBranch(data.doc.branch);
453
+ setTree(data.doc.tree);
454
+ setFrontmatter(data.doc.frontmatter);
455
+ setBody(data.doc.body);
456
+ latest.current = { frontmatter: data.doc.frontmatter, body: data.doc.body };
457
+ if (embed) previewSession.current?.setDoc(latest.current);
458
+ applyModelFields(
459
+ data.doc.model?.fields,
460
+ Object.keys(data.doc.frontmatter),
461
+ );
462
+ setDocFormat(data.doc.model?.format === "page" ? "page" : "mdx");
463
+ setParseFailed(
464
+ data.doc.model?.format === "page" && Boolean(data.doc.parseError),
465
+ );
466
+ setSaveState("idle");
467
+ if (data.doc.parseError && data.doc.model?.format === "page") {
468
+ setError(data.doc.parseError);
469
+ }
470
+ queueMicrotask(() => {
471
+ hydrated.current = true;
472
+ });
473
+ } catch (err) {
474
+ if (!isNotFound(err) || !model?.singleton) throw err;
475
+ const next = emptyFrontmatter(model);
476
+ hydrated.current = false;
477
+ setSource("tree");
478
+ setDocStatus("added");
479
+ setFrontmatter(next);
480
+ setBody("");
481
+ applyModelFields(model.fields, Object.keys(next));
482
+ setDocFormat(model.format === "page" ? "page" : "mdx");
483
+ setParseFailed(false);
484
+ setError(null);
485
+ setSaveState("idle");
486
+ latest.current = { frontmatter: next, body: "" };
487
+ queueMicrotask(() => {
488
+ hydrated.current = true;
489
+ });
490
+ try {
491
+ const res = await saveDoc(workspaceId, locale, type, slug, {
492
+ frontmatter: next,
493
+ body: "",
494
+ });
495
+ setDocStatus(res.doc.status);
496
+ setSource("tree");
497
+ notifyTreeChanged({
498
+ workspaceId,
499
+ locale,
500
+ type,
501
+ slug,
502
+ status: res.doc.status,
503
+ });
504
+ } catch (saveErr) {
505
+ setError(formatApiError(saveErr));
506
+ }
507
+ }
508
+ }
509
+
510
+ useEffect(() => {
511
+ if (!workspaceId || !type || !slug) return;
512
+ setLoading(true);
513
+ setPreviewLoading(true);
514
+ hydrated.current = false;
515
+ void reload()
516
+ .catch((err: Error) => setError(err.message))
517
+ .finally(() => setLoading(false));
518
+
519
+ function onChanged() {
520
+ saveGen.current += 1;
521
+ hydrated.current = false;
522
+ void reload().catch((err: Error) => setError(err.message));
523
+ }
524
+ function onDocChanged(e: Event) {
525
+ const detail = (e as CustomEvent<DocChangedDetail>).detail;
526
+ if (!sameDocRef(detail?.docRef, { locale, type, slug })) return;
527
+ onChanged();
528
+ }
529
+ // Chat publishes status with the tool result. Flip the badge before the reload returns.
530
+ function onTree(e: Event) {
531
+ const detail = (e as CustomEvent<TreeChangeDetail>).detail;
532
+ if (!detail?.status || detail.remove) return;
533
+ const ws = detail.workspaceId;
534
+ if (ws && ws !== workspace.id && ws !== workspace.slug) return;
535
+ if (detail.locale !== locale || detail.type !== type || detail.slug !== slug) return;
536
+ docStatusRef.current = detail.status;
537
+ setDocStatus(detail.status);
538
+ setSource(detail.status === "clean" ? "head" : "tree");
539
+ }
540
+ // Branch HEAD moved (teammate push / pull / commit). A clean doc reloads to
541
+ // show the new HEAD; a doc I am editing keeps my text and just refreshes its
542
+ // status, which the server reports as `conflict` when HEAD touched this file.
543
+ function onHeadChanged() {
544
+ if (docStatusRef.current === "clean") {
545
+ onChanged();
546
+ remountPreview();
547
+ return;
548
+ }
549
+ void fetchDoc(workspaceId, locale, type, slug)
550
+ .then((data) => setDocStatus(data.doc.status))
551
+ .catch(() => {});
552
+ }
553
+ window.addEventListener("backmatter:workspace-changed", onChanged);
554
+ window.addEventListener("backmatter:head-changed", onHeadChanged);
555
+ window.addEventListener("backmatter:tree-changed", onTree);
556
+ window.addEventListener(DOC_CHANGED_EVENT, onDocChanged);
557
+ return () => {
558
+ window.removeEventListener("backmatter:workspace-changed", onChanged);
559
+ window.removeEventListener("backmatter:head-changed", onHeadChanged);
560
+ window.removeEventListener("backmatter:tree-changed", onTree);
561
+ window.removeEventListener(DOC_CHANGED_EVENT, onDocChanged);
562
+ };
563
+ // eslint-disable-next-line react-hooks/exhaustive-deps
564
+ }, [workspaceId, workspace.slug, locale, type, slug]);
565
+
566
+ // Publish the focused document to the global AI chat (debounced).
567
+ useEffect(() => {
568
+ if (loading) return;
569
+ const timer = window.setTimeout(() => {
570
+ setEditorFocus({ docRef, frontmatter, body, selection });
571
+ }, 150);
572
+ return () => window.clearTimeout(timer);
573
+ }, [setEditorFocus, docRef, frontmatter, body, selection, loading]);
574
+
575
+ useEffect(() => {
576
+ return () => setEditorFocus(null);
577
+ }, [setEditorFocus]);
578
+
579
+ // When the AI finishes editing this doc, pick up its write to my tree.
580
+ const wasAiEditing = useRef(false);
581
+ useEffect(() => {
582
+ if (wasAiEditing.current && !aiEditing) {
583
+ saveGen.current += 1;
584
+ hydrated.current = false;
585
+ void reload().catch((err: Error) => setError(err.message));
586
+ }
587
+ wasAiEditing.current = aiEditing;
588
+ // eslint-disable-next-line react-hooks/exhaustive-deps
589
+ }, [aiEditing]);
590
+
591
+ async function persist(reason: "auto" | "manual") {
592
+ if (parseFailed) return;
593
+ const gen = ++saveGen.current;
594
+ const payload = latest.current;
595
+ setSaveState("saving");
596
+ if (reason === "manual") {
597
+ setBusy("save");
598
+ setError(null);
599
+ setStatus(null);
600
+ }
601
+ try {
602
+ const res = await saveDoc(workspaceId, locale, type, slug, payload);
603
+ if (gen !== saveGen.current) return;
604
+ setSource("tree");
605
+ setDocStatus(res.doc.status);
606
+ setSaveState("saved");
607
+ notifyTreeChanged({
608
+ workspaceId,
609
+ locale,
610
+ type,
611
+ slug,
612
+ status: res.doc.status,
613
+ });
614
+ if (reason === "manual") setStatus("Saved to your working tree");
615
+ } catch (err) {
616
+ if (gen !== saveGen.current) return;
617
+ setSaveState("error");
618
+ setError(formatApiError(err));
619
+ } finally {
620
+ if (reason === "manual") setBusy(null);
621
+ }
622
+ }
623
+
624
+ useEffect(() => {
625
+ if (editEpoch === 0) return;
626
+ if (busy === "revert" || busy === "resolve") return;
627
+
628
+ const timer = window.setTimeout(() => {
629
+ // The AI is writing this file right now; the editor reloads when it ends.
630
+ if (aiEditingRef.current) return;
631
+ void persist("auto");
632
+ }, AUTOSAVE_MS);
633
+
634
+ return () => window.clearTimeout(timer);
635
+ // eslint-disable-next-line react-hooks/exhaustive-deps
636
+ }, [editEpoch]);
637
+
638
+ async function onSave(e: React.FormEvent) {
639
+ e.preventDefault();
640
+ await persist("manual");
641
+ }
642
+
643
+ /** Drop my uncommitted copy; the document reads from HEAD again. */
644
+ async function onRevert() {
645
+ setConfirmRevert(false);
646
+ setBusy("revert");
647
+ setError(null);
648
+ setStatus(null);
649
+ saveGen.current += 1;
650
+ try {
651
+ await revertDoc(workspaceId, locale, type, slug);
652
+ setStatus("Reverted to the committed version");
653
+ notifyTreeChanged({ workspaceId, locale, type, slug, status: "clean" });
654
+ await reload();
655
+ remountPreview();
656
+ } catch (err) {
657
+ setError(formatApiError(err));
658
+ } finally {
659
+ setBusy(null);
660
+ }
661
+ }
662
+
663
+ /** Conflict: keep my copy (rebase onto the new HEAD) or take theirs (revert). */
664
+ async function onResolve(strategy: "mine" | "theirs") {
665
+ setBusy("resolve");
666
+ setError(null);
667
+ setStatus(null);
668
+ saveGen.current += 1;
669
+ try {
670
+ await resolveDoc(workspaceId, locale, type, slug, strategy);
671
+ notifyTreeChanged();
672
+ if (strategy === "theirs") {
673
+ setStatus("Took the committed version");
674
+ await reload();
675
+ remountPreview();
676
+ } else {
677
+ setStatus("Kept your version; it will overwrite theirs on commit");
678
+ const data = await fetchDoc(workspaceId, locale, type, slug);
679
+ setDocStatus(data.doc.status);
680
+ }
681
+ } catch (err) {
682
+ setError(formatApiError(err));
683
+ } finally {
684
+ setBusy(null);
685
+ }
686
+ }
687
+
688
+ function markEdited() {
689
+ if (!hydrated.current) return;
690
+ setSaveState("dirty");
691
+ setEditEpoch((n) => n + 1);
692
+ }
693
+
694
+ const saveLabel =
695
+ saveState === "saving"
696
+ ? "Saving…"
697
+ : saveState === "dirty"
698
+ ? "Unsaved"
699
+ : saveState === "error"
700
+ ? "Save failed"
701
+ : null;
702
+ const saveTone = saveState === "error" ? "danger" : "muted";
703
+
704
+ persistRef.current = persist;
705
+ revertRef.current = onRevert;
706
+
707
+ const statusLabel = docStatusLabel(docStatus);
708
+ useEffect(() => {
709
+ setEditorStatus({
710
+ locale,
711
+ status: statusLabel,
712
+ saveLabel,
713
+ saveTone,
714
+ });
715
+ }, [setEditorStatus, locale, saveLabel, saveTone, statusLabel]);
716
+
717
+ const hasTreeCopy = source === "tree";
718
+ useEffect(() => {
719
+ editorActions.setActions({
720
+ save: () => persistRef.current("manual"),
721
+ revert: () => revertRef.current(),
722
+ canRevert: hasTreeCopy,
723
+ });
724
+ return () => editorActions.clear();
725
+ }, [editorActions.setActions, editorActions.clear, hasTreeCopy]);
726
+
727
+ useEffect(() => {
728
+ return () => clearEditorStatus();
729
+ }, [clearEditorStatus]);
730
+
731
+ if (loading) {
732
+ return (
733
+ <div {...stylex.props(styles.pad)}>
734
+ <Text tone="muted">Loading document…</Text>
735
+ </div>
736
+ );
737
+ }
738
+
739
+ const inConflict = docStatus === "conflict";
740
+ const previewSx = stylex.props(styles.preview);
741
+
742
+ return (
743
+ <div {...stylex.props(styles.root)}>
744
+ <ConfirmDialog
745
+ open={confirmRevert}
746
+ onOpenChange={setConfirmRevert}
747
+ title="Revert this file?"
748
+ description="Your uncommitted changes to this document will be dropped and the committed version on the branch reloaded."
749
+ confirmLabel="Revert"
750
+ destructive
751
+ onConfirm={() => void onRevert()}
752
+ />
753
+ <div
754
+ ref={splitRef}
755
+ {...stylex.props(styles.split, showPreview && dragging && styles.splitDragging)}
756
+ >
757
+ {showPreview ? (
758
+ <>
759
+ <aside
760
+ className={previewSx.className}
761
+ style={{ ...previewSx.style, flexBasis: `${previewPct}%` }}
762
+ >
763
+ <div {...stylex.props(styles.previewBar)}>
764
+ <div {...stylex.props(styles.urlGroup)}>
765
+ <Tooltip label={previewLoading ? "Loading preview…" : "Reload preview"}>
766
+ <button
767
+ type="button"
768
+ aria-label={previewLoading ? "Preview loading" : "Reload preview"}
769
+ aria-busy={previewLoading || undefined}
770
+ onClick={remountPreview}
771
+ {...stylex.props(styles.reloadBtn)}
772
+ >
773
+ <span
774
+ {...stylex.props(
775
+ styles.reloadIcon,
776
+ previewLoading && styles.reloadSpinning,
777
+ )}
778
+ >
779
+ <ReloadIcon />
780
+ </span>
781
+ </button>
782
+ </Tooltip>
783
+ <span {...stylex.props(styles.url)} title={previewUrl ?? undefined}>
784
+ {previewUrl
785
+ ? previewUrl.replace(/^https?:\/\//, "")
786
+ : !siteUrl
787
+ ? "No site URL"
788
+ : "Loading checkout…"}
789
+ </span>
790
+ {previewLive === "rendered" ? (
791
+ <span {...stylex.props(styles.previewLive)}>Updated</span>
792
+ ) : null}
793
+ {previewLive === "error" ? (
794
+ <span {...stylex.props(styles.previewLiveError)}>Preview error</span>
795
+ ) : null}
796
+ {previewOffer ? (
797
+ <button
798
+ type="button"
799
+ {...stylex.props(styles.editPage)}
800
+ onClick={() => {
801
+ navigate(
802
+ contentDocPath(
803
+ workspace.slug,
804
+ previewOffer.type,
805
+ previewOffer.slug,
806
+ previewOffer.locale,
807
+ defaultLocaleOf(workspace.locales),
808
+ ),
809
+ );
810
+ }}
811
+ >
812
+ Edit this page
813
+ </button>
814
+ ) : null}
815
+ </div>
816
+ <span
817
+ {...stylex.props(
818
+ styles.badge,
819
+ inConflict && styles.badgeConflict,
820
+ )}
821
+ >
822
+ {statusLabel ?? "…"}
823
+ </span>
824
+ </div>
825
+ {previewUrl ? (
826
+ <iframe
827
+ key={`${previewKey}-${previewUrl}`}
828
+ ref={previewFrameRef}
829
+ title="Live preview"
830
+ src={previewUrl}
831
+ onLoad={() => {
832
+ setPreviewLoading(false);
833
+ setPreviewFrame((n) => n + 1);
834
+ }}
835
+ {...stylex.props(
836
+ styles.iframe,
837
+ dragging && styles.iframeDragging,
838
+ )}
839
+ />
840
+ ) : !siteUrl ? (
841
+ <div {...stylex.props(styles.iframe, styles.previewEmpty)}>
842
+ <Text as="p" size="sm" weight="medium">
843
+ No site URL
844
+ </Text>
845
+ <Link
846
+ to={settingsPath(workspace.slug, "general")}
847
+ {...stylex.props(styles.previewEmptyLink)}
848
+ >
849
+ Set a site URL in settings
850
+ </Link>
851
+ </div>
852
+ ) : (
853
+ <div
854
+ {...stylex.props(styles.iframe)}
855
+ aria-busy="true"
856
+ aria-label="Preview waiting for checkout"
857
+ />
858
+ )}
859
+ </aside>
860
+
861
+ <div
862
+ role="separator"
863
+ aria-orientation={wide ? "vertical" : "horizontal"}
864
+ aria-valuemin={PREVIEW_PCT_MIN}
865
+ aria-valuemax={PREVIEW_PCT_MAX}
866
+ aria-valuenow={Math.round(previewPct)}
867
+ aria-label="Resize preview"
868
+ tabIndex={0}
869
+ onPointerDown={onSplitPointerDown}
870
+ onPointerMove={onSplitPointerMove}
871
+ onPointerUp={onSplitPointerUp}
872
+ onPointerCancel={onSplitPointerUp}
873
+ onLostPointerCapture={() => setDragging(false)}
874
+ onKeyDown={onSplitKeyDown}
875
+ {...stylex.props(styles.handle, dragging && styles.handleActive)}
876
+ >
877
+ <span {...stylex.props(styles.handleHit)} />
878
+ </div>
879
+ </>
880
+ ) : null}
881
+
882
+ <section {...stylex.props(styles.editor)}>
883
+ <div {...stylex.props(styles.editorColumn, !showPreview && styles.editorColumnSolo)}>
884
+ <div {...stylex.props(styles.toolbar)}>
885
+ <div {...stylex.props(styles.meta)}>
886
+ <Text as="h1" size="md" weight="semibold" truncate>
887
+ {type}/{slug}
888
+ </Text>
889
+ </div>
890
+ <div {...stylex.props(styles.actions)}>
891
+ <Tooltip label={`Save (${mod}S)`}>
892
+ <Button
893
+ size="sm"
894
+ variant="secondary"
895
+ disabled={busy !== null || saveState === "saving"}
896
+ aria-keyshortcuts="Meta+S Control+S"
897
+ onClick={() => void persist("manual")}
898
+ >
899
+ Save
900
+ </Button>
901
+ </Tooltip>
902
+ {showPreview ? (
903
+ <>
904
+ <ButtonLink
905
+ size="sm"
906
+ variant="secondary"
907
+ href={previewUrl ?? undefined}
908
+ disabled={!previewUrl}
909
+ >
910
+ Preview
911
+ </ButtonLink>
912
+ <ButtonLink
913
+ size="sm"
914
+ variant="secondary"
915
+ href={liveUrl ?? undefined}
916
+ disabled={!liveUrl}
917
+ >
918
+ View live
919
+ </ButtonLink>
920
+ </>
921
+ ) : null}
922
+ {hasTreeCopy ? (
923
+ <Tooltip label="Drop your uncommitted changes to this file">
924
+ <Button
925
+ size="sm"
926
+ variant="danger"
927
+ disabled={busy !== null}
928
+ onClick={() => setConfirmRevert(true)}
929
+ >
930
+ {busy === "revert" ? "Reverting…" : "Revert"}
931
+ </Button>
932
+ </Tooltip>
933
+ ) : null}
934
+ </div>
935
+ </div>
936
+
937
+ <div {...stylex.props(styles.formWrap)}>
938
+ {inConflict ? (
939
+ <div {...stylex.props(styles.conflictBanner)} role="alert">
940
+ <div {...stylex.props(styles.conflictText)}>
941
+ <Text size="sm" weight="semibold">
942
+ This file changed on {branch ?? "the branch"} since you started editing.
943
+ </Text>
944
+ <Text size="sm" tone="muted">
945
+ Commit is blocked until you pick a side. Keep mine overwrites their
946
+ version on your next commit; take theirs drops your edits.
947
+ </Text>
948
+ </div>
949
+ <div {...stylex.props(styles.conflictActions)}>
950
+ <Button
951
+ variant="secondary"
952
+ disabled={busy !== null}
953
+ onClick={() => void onResolve("mine")}
954
+ >
955
+ {busy === "resolve" ? "Resolving…" : "Keep mine"}
956
+ </Button>
957
+ <Button
958
+ variant="danger"
959
+ disabled={busy !== null}
960
+ onClick={() => void onResolve("theirs")}
961
+ >
962
+ Take theirs
963
+ </Button>
964
+ </div>
965
+ </div>
966
+ ) : null}
967
+ {aiEditing ? (
968
+ <div {...stylex.props(styles.aiBanner)} role="status" aria-live="polite">
969
+ <span {...stylex.props(styles.aiDot)} />
970
+ <Text size="sm">AI is editing this document…</Text>
971
+ </div>
972
+ ) : null}
973
+ {error ? (
974
+ <div {...stylex.props(styles.msg)}>
975
+ <Text tone="danger" size="sm">
976
+ {error}
977
+ </Text>
978
+ </div>
979
+ ) : null}
980
+ {status ? (
981
+ <div {...stylex.props(styles.msg)}>
982
+ <Text tone="success" size="sm">
983
+ {status}
984
+ </Text>
985
+ </div>
986
+ ) : null}
987
+
988
+ <form onSubmit={onSave} {...stylex.props(styles.form)}>
989
+ <Text as="h2" size="sm" tone="muted" weight="medium">
990
+ Details
991
+ </Text>
992
+ {detailNames.length === 0 ? (
993
+ <Text tone="muted" size="sm">
994
+ {isPage
995
+ ? "No scalar fields on this model."
996
+ : "No model fields. Edit the MDX body only."}
997
+ </Text>
998
+ ) : (
999
+ detailNames.map((name) => {
1000
+ const kind = fieldKinds[name];
1001
+ const composite = kind === "object" || kind === "array";
1002
+ const CustomInput = resolveFieldInput(studio, fieldDefs[name]);
1003
+ return (
1004
+ <Field key={name} label={fieldLabel(name)}>
1005
+ {CustomInput ? (
1006
+ <CustomInput
1007
+ name={name}
1008
+ field={fieldDefs[name]!}
1009
+ value={frontmatter[name]}
1010
+ readOnly={aiEditing || parseFailed}
1011
+ onChange={(next) => {
1012
+ markEdited();
1013
+ setFrontmatter((prev) => ({ ...prev, [name]: next }));
1014
+ }}
1015
+ />
1016
+ ) : composite ? (
1017
+ <JsonField
1018
+ kind={kind === "array" ? "array" : "object"}
1019
+ value={frontmatter[name]}
1020
+ readOnly={aiEditing || parseFailed}
1021
+ onValidChange={(next) => {
1022
+ markEdited();
1023
+ setFrontmatter((prev) => ({ ...prev, [name]: next }));
1024
+ }}
1025
+ />
1026
+ ) : (
1027
+ <Input
1028
+ value={String(frontmatter[name] ?? "")}
1029
+ readOnly={aiEditing || parseFailed}
1030
+ onChange={(e) => {
1031
+ markEdited();
1032
+ setFrontmatter((prev) => ({
1033
+ ...prev,
1034
+ [name]: e.target.value,
1035
+ }));
1036
+ }}
1037
+ />
1038
+ )}
1039
+ </Field>
1040
+ );
1041
+ })
1042
+ )}
1043
+ {isPage ? (
1044
+ <BlocksList
1045
+ blocks={blocksFromFrontmatter(frontmatter[blocksField])}
1046
+ catalog={blocks}
1047
+ readOnly={aiEditing || parseFailed}
1048
+ onChange={(next) => {
1049
+ markEdited();
1050
+ setFrontmatter((prev) => ({ ...prev, [blocksField]: next }));
1051
+ }}
1052
+ />
1053
+ ) : (
1054
+ <BodyField
1055
+ value={body}
1056
+ catalog={richtext}
1057
+ readOnly={aiEditing || parseFailed}
1058
+ onChange={(value) => {
1059
+ markEdited();
1060
+ setBody(value);
1061
+ }}
1062
+ onSelect={setSelection}
1063
+ />
1064
+ )}
1065
+ </form>
1066
+ </div>
1067
+ </div>
1068
+ </section>
1069
+ </div>
1070
+ </div>
1071
+ );
1072
+ }
1073
+
1074
+ const styles = stylex.create({
1075
+ root: {
1076
+ display: "flex",
1077
+ minHeight: 0,
1078
+ flex: 1,
1079
+ flexDirection: "column",
1080
+ overflow: "hidden",
1081
+ },
1082
+ pad: {
1083
+ padding: space.xxl,
1084
+ },
1085
+ split: {
1086
+ display: "flex",
1087
+ minHeight: 0,
1088
+ flex: 1,
1089
+ flexDirection: "column",
1090
+ "@media (min-width: 1024px)": {
1091
+ flexDirection: "row",
1092
+ },
1093
+ },
1094
+ splitDragging: {
1095
+ userSelect: "none",
1096
+ cursor: "row-resize",
1097
+ "@media (min-width: 1024px)": {
1098
+ cursor: "col-resize",
1099
+ },
1100
+ },
1101
+ preview: {
1102
+ display: "flex",
1103
+ minWidth: 0,
1104
+ minHeight: 0,
1105
+ flexGrow: 0,
1106
+ flexShrink: 0,
1107
+ flexDirection: "column",
1108
+ overflow: "hidden",
1109
+ backgroundColor: colors.bgSurface,
1110
+ },
1111
+ previewBar: {
1112
+ display: "flex",
1113
+ flexShrink: 0,
1114
+ alignItems: "center",
1115
+ justifyContent: "space-between",
1116
+ gap: space.md,
1117
+ paddingInline: space.md,
1118
+ paddingBlock: space.sm,
1119
+ borderBottomWidth: 1,
1120
+ borderBottomStyle: "solid",
1121
+ borderBottomColor: colors.border,
1122
+ fontSize: type.xs,
1123
+ color: colors.textMuted,
1124
+ },
1125
+ urlGroup: {
1126
+ display: "flex",
1127
+ minWidth: 0,
1128
+ flex: 1,
1129
+ alignItems: "center",
1130
+ gap: space.sm,
1131
+ },
1132
+ reloadBtn: {
1133
+ display: "inline-flex",
1134
+ flexShrink: 0,
1135
+ alignItems: "center",
1136
+ justifyContent: "center",
1137
+ width: 22,
1138
+ height: 22,
1139
+ padding: 0,
1140
+ borderWidth: 0,
1141
+ borderRadius: radius.sm,
1142
+ backgroundColor: "transparent",
1143
+ color: colors.textMuted,
1144
+ cursor: "pointer",
1145
+ touchAction: "manipulation",
1146
+ [hover]: {
1147
+ ":hover": {
1148
+ backgroundColor: colors.bgHover,
1149
+ color: colors.text,
1150
+ },
1151
+ },
1152
+ ":focus-visible": {
1153
+ outlineWidth: 2,
1154
+ outlineStyle: "solid",
1155
+ outlineColor: colors.focus,
1156
+ outlineOffset: 1,
1157
+ },
1158
+ },
1159
+ reloadIcon: {
1160
+ display: "inline-flex",
1161
+ },
1162
+ reloadSpinning: {
1163
+ animationName: spin,
1164
+ animationDuration: "700ms",
1165
+ animationTimingFunction: "linear",
1166
+ animationIterationCount: "infinite",
1167
+ "@media (prefers-reduced-motion: reduce)": {
1168
+ animationName: "none",
1169
+ animationDuration: "0s",
1170
+ },
1171
+ },
1172
+ url: {
1173
+ overflow: "hidden",
1174
+ textOverflow: "ellipsis",
1175
+ whiteSpace: "nowrap",
1176
+ fontFamily: type.mono,
1177
+ },
1178
+ previewLive: {
1179
+ flexShrink: 0,
1180
+ color: colors.textMuted,
1181
+ },
1182
+ previewLiveError: {
1183
+ flexShrink: 0,
1184
+ color: colors.danger,
1185
+ },
1186
+ editPage: {
1187
+ flexShrink: 0,
1188
+ borderWidth: 0,
1189
+ borderRadius: 4,
1190
+ backgroundColor: colors.bgHover,
1191
+ color: colors.text,
1192
+ cursor: "pointer",
1193
+ fontSize: type.sm,
1194
+ paddingInline: space.sm,
1195
+ paddingBlock: 2,
1196
+ },
1197
+ badge: {
1198
+ flexShrink: 0,
1199
+ borderWidth: 1,
1200
+ borderStyle: "solid",
1201
+ borderColor: colors.border,
1202
+ borderRadius: 4,
1203
+ paddingInline: space.sm,
1204
+ paddingBlock: 2,
1205
+ textTransform: "uppercase",
1206
+ letterSpacing: "0.04em",
1207
+ },
1208
+ badgeConflict: {
1209
+ borderColor: colors.danger,
1210
+ color: colors.danger,
1211
+ },
1212
+ conflictBanner: {
1213
+ display: "flex",
1214
+ flexWrap: "wrap",
1215
+ alignItems: "center",
1216
+ justifyContent: "space-between",
1217
+ gap: space.md,
1218
+ marginBottom: space.md,
1219
+ paddingInline: space.md,
1220
+ paddingBlock: space.md,
1221
+ borderWidth: 1,
1222
+ borderStyle: "solid",
1223
+ borderColor: colors.danger,
1224
+ borderRadius: radius.md,
1225
+ backgroundColor: colors.bgSurface,
1226
+ },
1227
+ conflictText: {
1228
+ display: "flex",
1229
+ minWidth: 0,
1230
+ flex: 1,
1231
+ flexDirection: "column",
1232
+ gap: space.xs,
1233
+ },
1234
+ conflictActions: {
1235
+ display: "flex",
1236
+ flexShrink: 0,
1237
+ gap: space.sm,
1238
+ },
1239
+ iframe: {
1240
+ minHeight: 0,
1241
+ width: "100%",
1242
+ flex: 1,
1243
+ borderWidth: 0,
1244
+ backgroundColor: colors.bgSurface,
1245
+ },
1246
+ previewEmpty: {
1247
+ display: "flex",
1248
+ flexDirection: "column",
1249
+ alignItems: "flex-start",
1250
+ justifyContent: "center",
1251
+ gap: space.sm,
1252
+ padding: space.lg,
1253
+ },
1254
+ previewEmptyLink: {
1255
+ color: colors.text,
1256
+ fontSize: type.sm,
1257
+ textDecorationLine: "underline",
1258
+ textDecorationThickness: "1px",
1259
+ textUnderlineOffset: "2px",
1260
+ },
1261
+ iframeDragging: {
1262
+ pointerEvents: "none",
1263
+ },
1264
+ handle: {
1265
+ position: "relative",
1266
+ zIndex: 1,
1267
+ flexShrink: 0,
1268
+ width: "100%",
1269
+ height: 1,
1270
+ padding: 0,
1271
+ borderWidth: 0,
1272
+ backgroundColor: "currentColor",
1273
+ color: colors.border,
1274
+ cursor: "row-resize",
1275
+ touchAction: "none",
1276
+ userSelect: "none",
1277
+ [hover]: {
1278
+ ":hover": {
1279
+ color: colors.accent,
1280
+ },
1281
+ },
1282
+ ":focus-visible": {
1283
+ color: colors.accent,
1284
+ outlineWidth: 2,
1285
+ outlineStyle: "solid",
1286
+ outlineColor: colors.focus,
1287
+ outlineOffset: 2,
1288
+ },
1289
+ "@media (min-width: 1024px)": {
1290
+ width: 1,
1291
+ height: "auto",
1292
+ alignSelf: "stretch",
1293
+ cursor: "col-resize",
1294
+ },
1295
+ },
1296
+ handleActive: {
1297
+ color: colors.accent,
1298
+ },
1299
+ handleHit: {
1300
+ position: "absolute",
1301
+ insetInline: 0,
1302
+ insetBlock: -5,
1303
+ backgroundColor: "transparent",
1304
+ "@media (min-width: 1024px)": {
1305
+ insetBlock: 0,
1306
+ insetInline: -5,
1307
+ },
1308
+ },
1309
+ editor: {
1310
+ display: "flex",
1311
+ minWidth: 0,
1312
+ minHeight: 0,
1313
+ flex: 1,
1314
+ flexDirection: "column",
1315
+ overflow: "hidden",
1316
+ backgroundColor: colors.bgApp,
1317
+ },
1318
+ editorColumn: {
1319
+ display: "flex",
1320
+ minWidth: 0,
1321
+ minHeight: 0,
1322
+ flex: 1,
1323
+ flexDirection: "column",
1324
+ width: "100%",
1325
+ },
1326
+ editorColumnSolo: {
1327
+ maxWidth: "700px",
1328
+ marginInline: "auto",
1329
+ },
1330
+ toolbar: {
1331
+ display: "flex",
1332
+ flexShrink: 0,
1333
+ flexWrap: "wrap",
1334
+ alignItems: "center",
1335
+ justifyContent: "space-between",
1336
+ gap: space.sm,
1337
+ paddingInline: space.lg,
1338
+ paddingBlock: space.sm,
1339
+ borderBottomWidth: 1,
1340
+ borderBottomStyle: "solid",
1341
+ borderBottomColor: colors.border,
1342
+ },
1343
+ meta: {
1344
+ minWidth: 0,
1345
+ },
1346
+ actions: {
1347
+ display: "flex",
1348
+ flexWrap: "wrap",
1349
+ gap: space.sm,
1350
+ },
1351
+ formWrap: {
1352
+ minHeight: 0,
1353
+ flex: 1,
1354
+ overflowY: "auto",
1355
+ paddingInline: space.lg,
1356
+ paddingBlock: space.lg,
1357
+ },
1358
+ msg: {
1359
+ marginBottom: space.md,
1360
+ },
1361
+ aiBanner: {
1362
+ display: "flex",
1363
+ alignItems: "center",
1364
+ gap: space.sm,
1365
+ marginBottom: space.md,
1366
+ paddingInline: space.md,
1367
+ paddingBlock: space.sm,
1368
+ borderWidth: 1,
1369
+ borderStyle: "solid",
1370
+ borderColor: colors.border,
1371
+ borderRadius: radius.md,
1372
+ backgroundColor: colors.bgSurface,
1373
+ color: colors.textMuted,
1374
+ },
1375
+ aiDot: {
1376
+ width: 8,
1377
+ height: 8,
1378
+ borderRadius: 999,
1379
+ backgroundColor: colors.accent,
1380
+ animationName: pulse,
1381
+ animationDuration: "1.2s",
1382
+ animationTimingFunction: "ease-in-out",
1383
+ animationIterationCount: "infinite",
1384
+ "@media (prefers-reduced-motion: reduce)": {
1385
+ animationName: "none",
1386
+ },
1387
+ },
1388
+ form: {
1389
+ display: "flex",
1390
+ flexDirection: "column",
1391
+ gap: space.lg,
1392
+ paddingBottom: space.xxl,
1393
+ },
1394
+ });