@akairos/pipeline 0.18.1 → 0.19.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 (217) hide show
  1. package/ARTICLE-THEMES.md +2 -0
  2. package/README.md +27 -4
  3. package/package.json +7 -5
  4. package/src/adapter-loader.ts +24 -4
  5. package/src/adapter-types.ts +14 -0
  6. package/src/article-theme.test.ts +55 -0
  7. package/src/article-theme.ts +10 -2
  8. package/src/commands/article-theme.ts +1 -1
  9. package/src/commands/init.ts +2 -0
  10. package/src/commands/publish.ts +1 -1
  11. package/src/commands/typeset-assets-sync.test.ts +12 -1
  12. package/src/commands/wechat-export.ts +9 -4
  13. package/src/plugin-runner.ts +10 -4
  14. package/src/plugin-runtime.test.ts +19 -0
  15. package/src/plugin-runtime.ts +11 -2
  16. package/src/providers/index.ts +4 -2
  17. package/src/runtime-package.ts +13 -6
  18. package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +1280 -5
  19. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  20. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  21. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  22. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  23. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  24. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  25. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  26. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  27. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  28. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  29. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  30. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  31. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  32. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  33. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  34. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  35. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  36. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  37. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  38. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  39. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  40. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  41. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  42. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  43. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  44. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  45. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  46. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  47. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  48. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  49. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  50. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  51. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  52. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  53. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  54. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  55. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  56. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  57. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  58. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  59. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  60. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  61. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  62. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  63. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  64. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  65. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  66. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  67. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  68. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  69. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  70. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  71. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  72. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  73. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  74. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  75. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  76. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  77. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  78. package/src/wechat-preview/assets/browser-dist/assets/apl-B4CMkyY2.js +1 -0
  79. package/src/wechat-preview/assets/browser-dist/assets/asciiarmor-Df11BRmG.js +1 -0
  80. package/src/wechat-preview/assets/browser-dist/assets/asn1-EdZsLKOL.js +1 -0
  81. package/src/wechat-preview/assets/browser-dist/assets/asterisk-B-8jnY81.js +1 -0
  82. package/src/wechat-preview/assets/browser-dist/assets/brainfuck-C4LP7Hcl.js +1 -0
  83. package/src/wechat-preview/assets/browser-dist/assets/clike-B9uivgTg.js +1 -0
  84. package/src/wechat-preview/assets/browser-dist/assets/clojure-BMjYHr_A.js +1 -0
  85. package/src/wechat-preview/assets/browser-dist/assets/cmake-BQqOBYOt.js +1 -0
  86. package/src/wechat-preview/assets/browser-dist/assets/cobol-CWcv1MsR.js +1 -0
  87. package/src/wechat-preview/assets/browser-dist/assets/coffeescript-S37ZYGWr.js +1 -0
  88. package/src/wechat-preview/assets/browser-dist/assets/commonlisp-DBKNyK5s.js +1 -0
  89. package/src/wechat-preview/assets/browser-dist/assets/crystal-SjHAIU92.js +1 -0
  90. package/src/wechat-preview/assets/browser-dist/assets/css-BnMrqG3P.js +1 -0
  91. package/src/wechat-preview/assets/browser-dist/assets/cypher-C_CwsFkJ.js +1 -0
  92. package/src/wechat-preview/assets/browser-dist/assets/d-pRatUO7H.js +1 -0
  93. package/src/wechat-preview/assets/browser-dist/assets/diff-DbItnlRl.js +1 -0
  94. package/src/wechat-preview/assets/browser-dist/assets/dockerfile-BKs6k2Af.js +1 -0
  95. package/src/wechat-preview/assets/browser-dist/assets/dtd-DF_7sFjM.js +1 -0
  96. package/src/wechat-preview/assets/browser-dist/assets/dylan-DwRh75JA.js +1 -0
  97. package/src/wechat-preview/assets/browser-dist/assets/ebnf-CDyGwa7X.js +1 -0
  98. package/src/wechat-preview/assets/browser-dist/assets/ecl-Cabwm37j.js +1 -0
  99. package/src/wechat-preview/assets/browser-dist/assets/eiffel-CnydiIhH.js +1 -0
  100. package/src/wechat-preview/assets/browser-dist/assets/elm-vLlmbW-K.js +1 -0
  101. package/src/wechat-preview/assets/browser-dist/assets/erlang-BNw1qcRV.js +1 -0
  102. package/src/wechat-preview/assets/browser-dist/assets/factor-kuTfRLto.js +1 -0
  103. package/src/wechat-preview/assets/browser-dist/assets/fcl-Kvtd6kyn.js +1 -0
  104. package/src/wechat-preview/assets/browser-dist/assets/forth-Ffai-XNe.js +1 -0
  105. package/src/wechat-preview/assets/browser-dist/assets/fortran-DYz_wnZ1.js +1 -0
  106. package/src/wechat-preview/assets/browser-dist/assets/gas-Bneqetm1.js +1 -0
  107. package/src/wechat-preview/assets/browser-dist/assets/gherkin-heZmZLOM.js +1 -0
  108. package/src/wechat-preview/assets/browser-dist/assets/groovy-D9Dt4D0W.js +1 -0
  109. package/src/wechat-preview/assets/browser-dist/assets/haskell-Cw1EW3IL.js +1 -0
  110. package/src/wechat-preview/assets/browser-dist/assets/haxe-H-WmDvRZ.js +1 -0
  111. package/src/wechat-preview/assets/browser-dist/assets/http-DBlCnlav.js +1 -0
  112. package/src/wechat-preview/assets/browser-dist/assets/idl-BEugSyMb.js +1 -0
  113. package/src/wechat-preview/assets/browser-dist/assets/index-B13lCSuc.js +1 -0
  114. package/src/wechat-preview/assets/browser-dist/assets/index-BDdU9pNh.js +1 -0
  115. package/src/wechat-preview/assets/browser-dist/assets/index-BFLGVgsp.js +1 -0
  116. package/src/wechat-preview/assets/browser-dist/assets/index-BHJY0B4E.js +8 -0
  117. package/src/wechat-preview/assets/browser-dist/assets/index-BOnKEJxh.js +7 -0
  118. package/src/wechat-preview/assets/browser-dist/assets/index-BPmBcGI-.js +1 -0
  119. package/src/wechat-preview/assets/browser-dist/assets/index-BTVIa90Y.js +1 -0
  120. package/src/wechat-preview/assets/browser-dist/assets/index-BYV6TA8f.js +1 -0
  121. package/src/wechat-preview/assets/browser-dist/assets/index-BlVpb0dF.js +1 -0
  122. package/src/wechat-preview/assets/browser-dist/assets/index-BqKARoyW.js +1 -0
  123. package/src/wechat-preview/assets/browser-dist/assets/index-BzHql5Rp.js +20 -0
  124. package/src/wechat-preview/assets/browser-dist/assets/index-CPFEJ9fX.js +1 -0
  125. package/src/wechat-preview/assets/browser-dist/assets/index-CdxKyTX6.js +1 -0
  126. package/src/wechat-preview/assets/browser-dist/assets/index-CyXDvcwY.js +1 -0
  127. package/src/wechat-preview/assets/browser-dist/assets/index-DA7Z4paG.js +13 -0
  128. package/src/wechat-preview/assets/browser-dist/assets/index-DDQGWU_D.js +3 -0
  129. package/src/wechat-preview/assets/browser-dist/assets/index-DENMoE5l.js +1 -0
  130. package/src/wechat-preview/assets/browser-dist/assets/index-D_EI5vFS.js +1 -0
  131. package/src/wechat-preview/assets/browser-dist/assets/index-DpuVrwze.js +1 -0
  132. package/src/wechat-preview/assets/browser-dist/assets/index-IBlhKHQr.js +8 -0
  133. package/src/wechat-preview/assets/browser-dist/assets/index-ifI5svkD.js +2 -0
  134. package/src/wechat-preview/assets/browser-dist/assets/index-vRiJxvj2.js +1 -0
  135. package/src/wechat-preview/assets/browser-dist/assets/javascript-iXu5QeM3.js +1 -0
  136. package/src/wechat-preview/assets/browser-dist/assets/julia-DuME0IfC.js +1 -0
  137. package/src/wechat-preview/assets/browser-dist/assets/livescript-BwQOo05w.js +1 -0
  138. package/src/wechat-preview/assets/browser-dist/assets/lua-VAEuO923.js +1 -0
  139. package/src/wechat-preview/assets/browser-dist/assets/mathematica-DTrFuWx2.js +1 -0
  140. package/src/wechat-preview/assets/browser-dist/assets/mbox-CNhZ1qSd.js +1 -0
  141. package/src/wechat-preview/assets/browser-dist/assets/mirc-CjQqDB4T.js +1 -0
  142. package/src/wechat-preview/assets/browser-dist/assets/mllike-CXdrOF99.js +1 -0
  143. package/src/wechat-preview/assets/browser-dist/assets/modelica-Dc1JOy9r.js +1 -0
  144. package/src/wechat-preview/assets/browser-dist/assets/mscgen-BA5vi2Kp.js +1 -0
  145. package/src/wechat-preview/assets/browser-dist/assets/mumps-BT43cFF4.js +1 -0
  146. package/src/wechat-preview/assets/browser-dist/assets/nginx-DdIZxoE0.js +1 -0
  147. package/src/wechat-preview/assets/browser-dist/assets/nsis-LY0VT_u5.js +1 -0
  148. package/src/wechat-preview/assets/browser-dist/assets/ntriples-BfvgReVJ.js +1 -0
  149. package/src/wechat-preview/assets/browser-dist/assets/octave-Ck1zUtKM.js +1 -0
  150. package/src/wechat-preview/assets/browser-dist/assets/oz-BzwKVEFT.js +1 -0
  151. package/src/wechat-preview/assets/browser-dist/assets/pascal--L3eBynH.js +1 -0
  152. package/src/wechat-preview/assets/browser-dist/assets/perl-CdXCOZ3F.js +1 -0
  153. package/src/wechat-preview/assets/browser-dist/assets/pig-CevX1Tat.js +1 -0
  154. package/src/wechat-preview/assets/browser-dist/assets/powershell-CFHJl5sT.js +1 -0
  155. package/src/wechat-preview/assets/browser-dist/assets/properties-C78fOPTZ.js +1 -0
  156. package/src/wechat-preview/assets/browser-dist/assets/protobuf-ChK-085T.js +1 -0
  157. package/src/wechat-preview/assets/browser-dist/assets/pug-DeIclll2.js +1 -0
  158. package/src/wechat-preview/assets/browser-dist/assets/puppet-DMA9R1ak.js +1 -0
  159. package/src/wechat-preview/assets/browser-dist/assets/python-BuPzkPfP.js +1 -0
  160. package/src/wechat-preview/assets/browser-dist/assets/q-pXgVlZs6.js +1 -0
  161. package/src/wechat-preview/assets/browser-dist/assets/r-B6wPVr8A.js +1 -0
  162. package/src/wechat-preview/assets/browser-dist/assets/rpm-CTu-6PCP.js +1 -0
  163. package/src/wechat-preview/assets/browser-dist/assets/ruby-BK5aLJev.js +1 -0
  164. package/src/wechat-preview/assets/browser-dist/assets/sas-B4kiWyti.js +1 -0
  165. package/src/wechat-preview/assets/browser-dist/assets/scheme-C41bIUwD.js +1 -0
  166. package/src/wechat-preview/assets/browser-dist/assets/shell-CjFT_Tl9.js +1 -0
  167. package/src/wechat-preview/assets/browser-dist/assets/sieve-C3Gn_uJK.js +1 -0
  168. package/src/wechat-preview/assets/browser-dist/assets/simple-mode-GW_nhZxv.js +1 -0
  169. package/src/wechat-preview/assets/browser-dist/assets/smalltalk-CnHTOXQT.js +1 -0
  170. package/src/wechat-preview/assets/browser-dist/assets/solr-DehyRSwq.js +1 -0
  171. package/src/wechat-preview/assets/browser-dist/assets/sparql-DkYu6x3z.js +1 -0
  172. package/src/wechat-preview/assets/browser-dist/assets/spreadsheet-BCZA_wO0.js +1 -0
  173. package/src/wechat-preview/assets/browser-dist/assets/sql-D0XecflT.js +1 -0
  174. package/src/wechat-preview/assets/browser-dist/assets/stex-C3f8Ysf7.js +1 -0
  175. package/src/wechat-preview/assets/browser-dist/assets/style-Cif_gYUW.css +1 -0
  176. package/src/wechat-preview/assets/browser-dist/assets/stylus-B533Al4x.js +1 -0
  177. package/src/wechat-preview/assets/browser-dist/assets/swift-BzpIVaGY.js +1 -0
  178. package/src/wechat-preview/assets/browser-dist/assets/tcl-DVfN8rqt.js +1 -0
  179. package/src/wechat-preview/assets/browser-dist/assets/textile-CnDTJFAw.js +1 -0
  180. package/src/wechat-preview/assets/browser-dist/assets/tiddlywiki-DO-Gjzrf.js +1 -0
  181. package/src/wechat-preview/assets/browser-dist/assets/tiki-DGYXhP31.js +1 -0
  182. package/src/wechat-preview/assets/browser-dist/assets/toml-Bm5Em-hy.js +1 -0
  183. package/src/wechat-preview/assets/browser-dist/assets/troff-wAsdV37c.js +1 -0
  184. package/src/wechat-preview/assets/browser-dist/assets/ttcn-CfJYG6tj.js +1 -0
  185. package/src/wechat-preview/assets/browser-dist/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  186. package/src/wechat-preview/assets/browser-dist/assets/turtle-B1tBg_DP.js +1 -0
  187. package/src/wechat-preview/assets/browser-dist/assets/vb-CmGdzxic.js +1 -0
  188. package/src/wechat-preview/assets/browser-dist/assets/vbscript-BuJXcnF6.js +1 -0
  189. package/src/wechat-preview/assets/browser-dist/assets/velocity-D8B20fx6.js +1 -0
  190. package/src/wechat-preview/assets/browser-dist/assets/verilog-C6RDOZhf.js +1 -0
  191. package/src/wechat-preview/assets/browser-dist/assets/vhdl-lSbBsy5d.js +1 -0
  192. package/src/wechat-preview/assets/browser-dist/assets/webidl-ZXfAyPTL.js +1 -0
  193. package/src/wechat-preview/assets/browser-dist/assets/{wechat-renderer-GgNrlSLi.js → wechat-renderer-CcHuq7M-.js} +83 -61
  194. package/src/wechat-preview/assets/browser-dist/assets/xquery-DzFWVndE.js +1 -0
  195. package/src/wechat-preview/assets/browser-dist/assets/yacas-BJ4BC0dw.js +1 -0
  196. package/src/wechat-preview/assets/browser-dist/assets/z80-Hz9HOZM7.js +1 -0
  197. package/src/wechat-preview/assets/browser-dist/editor-export.js +1 -1
  198. package/src/wechat-preview/assets/browser-dist/src/wechat-preview/browser/studio/studio.html +215 -165
  199. package/src/wechat-preview/assets/browser-dist/studio.js +1148 -237
  200. package/src/wechat-preview/browser/studio/color-palettes.ts +28 -0
  201. package/src/wechat-preview/browser/studio/config-manager.ts +49 -0
  202. package/src/wechat-preview/browser/studio/milkdown-article-menu.ts +251 -0
  203. package/src/wechat-preview/browser/studio/myno-editor.ts +122 -0
  204. package/src/wechat-preview/browser/studio/studio.css +493 -250
  205. package/src/wechat-preview/browser/studio/studio.html +213 -163
  206. package/src/wechat-preview/browser/studio/studio.ts +684 -211
  207. package/src/wechat-preview/browser/studio/style-packs.ts +201 -0
  208. package/src/wechat-preview/browser/studio/syntax-presets.test.ts +10 -32
  209. package/src/wechat-preview/browser/studio/syntax-presets.ts +19 -357
  210. package/src/wechat-preview/fixtures/all-nodes.md +2 -11
  211. package/src/wechat-preview/index.ts +35 -14
  212. package/src/wechat-preview/server/http.ts +64 -8
  213. package/src/wechat-preview/server/studio.test.ts +60 -1
  214. package/src/wechat-preview/wechat-export.integration.test.ts +49 -8
  215. package/src/wechat-preview/wechat-renderer.ts +134 -11
  216. package/src/workflow.test.ts +2 -0
  217. package/src/wechat-preview/assets/browser-dist/assets/style-C9-YtTIm.css +0 -1
@@ -1,16 +1,16 @@
1
+ import articleCss from "@akairos/milkdown-article-style/article.css?raw";
1
2
  import { Editor, defaultValueCtx, rootCtx } from "@milkdown/kit/core";
2
3
  import { commonmark } from "@milkdown/kit/preset/commonmark";
3
4
  import { gfm } from "@milkdown/kit/preset/gfm";
4
5
  import { getHTML } from "@milkdown/kit/utils";
5
- import articleCss from "@akairos/milkdown-article-style/article.css?raw";
6
- import { getCombinedMilkdownPlugins, type WechatPluginDoc } from "@akairos/milkdown-article-plugins";
7
- import { renderWechatHtml } from "../../wechat-renderer";
8
- import type { ArticleStructure } from "../../wechat-renderer";
9
- import type { WechatExportTheme } from "../../themes";
6
+ import { getCombinedMilkdownPlugins, getCombinedDefaultCss, type WechatPluginDoc } from "@akairos/milkdown-article-plugins";
7
+ import { MynoEditor } from "./myno-editor";
8
+ import { STYLE_PACKS, type StylePack } from "./style-packs";
10
9
  import { SYNTAX_CATEGORIES, buildCombinedPresetsCss } from "./syntax-presets";
11
10
  import { COLOR_PALETTES } from "./color-palettes";
12
- import { extractFrontmatter } from "../../frontmatter-handler";
13
- import { stripLeadingH1 } from "../../../text";
11
+ import { parseStyleConfig, exportStyleConfigBlob, type StandaloneStyleConfig } from "./config-manager";
12
+ import { renderWechatHtml, buildWechatThemeCss, type ArticleStructure } from "../../wechat-renderer";
13
+ import type { WechatExportTheme } from "../../themes";
14
14
 
15
15
  interface AccountData {
16
16
  name: string;
@@ -24,6 +24,7 @@ interface AccountData {
24
24
  };
25
25
  articleThemeCss?: string;
26
26
  articleTheme?: string | null;
27
+ articleThemeStandalone?: boolean;
27
28
  }
28
29
 
29
30
  interface StudioConfigResponse {
@@ -40,21 +41,24 @@ let latestWechatHtml = "";
40
41
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
41
42
  let showingHtmlSource = false;
42
43
  let currentSyntaxPresets: Record<string, string> = {};
43
- // 串行渲染,旧结果不能覆盖最新编辑或账号。
44
44
  let renderRevision = 0;
45
45
  let rendering = false;
46
+ let mynoEditor: MynoEditor | null = null;
47
+ let currentEditorMode: "visual" | "source" = "visual";
48
+ let standaloneThemeName = "";
46
49
 
47
- // DOM Elements
50
+ // ── DOM Elements ────────────────────────────────────────────────────────
48
51
  const accountSelect = document.getElementById("account-select") as HTMLSelectElement;
52
+ const configModeBadge = document.getElementById("config-mode-badge") as HTMLElement;
49
53
  const editorMarkdown = document.getElementById("editor-markdown") as HTMLTextAreaElement;
54
+ const milkdownEditorRoot = document.getElementById("milkdown-editor-root") as HTMLElement;
50
55
  const previewScreen = document.getElementById("preview-screen") as HTMLElement;
51
56
  const editorStats = document.getElementById("editor-stats") as HTMLElement;
52
57
  const htmlSourceContainer = document.getElementById("html-source-container") as HTMLElement;
53
58
  const htmlSourceCode = document.getElementById("html-source-code") as HTMLTextAreaElement;
54
59
  const toastContainer = document.getElementById("toast-container") as HTMLElement;
55
- const hiddenHost = document.getElementById("studio-editor-host") as HTMLElement;
56
60
 
57
- // Controls
61
+ // Sliders & Text Inputs
58
62
  const inputFontSize = document.getElementById("input-font-size") as HTMLInputElement;
59
63
  const valFontSize = document.getElementById("val-font-size") as HTMLElement;
60
64
  const inputLineHeight = document.getElementById("input-line-height") as HTMLInputElement;
@@ -85,9 +89,24 @@ const inputHeadingLabel = document.getElementById("input-heading-label") as HTML
85
89
  const inputQuoteLabel = document.getElementById("input-quote-label") as HTMLInputElement;
86
90
  const inputCustomCss = document.getElementById("input-custom-css") as HTMLTextAreaElement;
87
91
 
92
+ const btnModeVisual = document.getElementById("btn-mode-visual") as HTMLButtonElement;
93
+ const btnModeSource = document.getElementById("btn-mode-source") as HTMLButtonElement;
94
+
95
+ const fileImportConfig = document.getElementById("file-import-config") as HTMLInputElement;
96
+ const btnImportConfig = document.getElementById("btn-import-config") as HTMLButtonElement;
97
+ const btnExportConfig = document.getElementById("btn-export-config") as HTMLButtonElement;
98
+ const btnSave = document.getElementById("btn-save") as HTMLButtonElement;
99
+ const btnReset = document.getElementById("btn-reset") as HTMLButtonElement;
100
+ const btnCopy = document.getElementById("btn-copy") as HTMLButtonElement;
101
+ const btnToggleView = document.getElementById("btn-toggle-view") as HTMLButtonElement;
102
+ const btnLoadSample = document.getElementById("btn-load-sample") as HTMLButtonElement;
103
+ const btnClearMd = document.getElementById("btn-clear-md") as HTMLButtonElement;
104
+
105
+ // ── Toast System ────────────────────────────────────────────────────────
88
106
  function showToast(message: string, type: "info" | "success" | "error" = "info") {
107
+ if (!toastContainer) return;
89
108
  const toast = document.createElement("div");
90
- toast.className = `toast ${type === "success" ? "toast-success" : ""}`;
109
+ toast.className = `toast ${type === "success" ? "toast-success" : type === "error" ? "toast-error" : ""}`;
91
110
  toast.textContent = message;
92
111
  toastContainer.appendChild(toast);
93
112
  requestAnimationFrame(() => toast.classList.add("show"));
@@ -97,6 +116,7 @@ function showToast(message: string, type: "info" | "success" | "error" = "info")
97
116
  }, 3000);
98
117
  }
99
118
 
119
+ // ── Color Picker Pairing ────────────────────────────────────────────────
100
120
  function bindColorPair(picker: HTMLInputElement, hex: HTMLInputElement, onChange: () => void) {
101
121
  picker.addEventListener("input", () => {
102
122
  hex.value = picker.value;
@@ -111,15 +131,45 @@ function bindColorPair(picker: HTMLInputElement, hex: HTMLInputElement, onChange
111
131
  }
112
132
 
113
133
  function updateStats() {
114
- const text = editorMarkdown.value;
134
+ const text = getActiveMarkdown();
115
135
  const chars = text.replace(/\s+/g, "").length;
116
136
  const minutes = Math.max(1, Math.round(chars / 400));
117
- editorStats.textContent = `字数: ${chars.toLocaleString()} 字 | 预计阅读: ${minutes} 分钟`;
137
+ if (editorStats) {
138
+ editorStats.textContent = `字数: ${chars.toLocaleString()} 字 | 预计阅读: ${minutes} 分钟`;
139
+ }
118
140
  }
119
141
 
120
- /** 预览与保存共用同一份样式参数。 */
142
+ function getActiveMarkdown(): string {
143
+ if (currentEditorMode === "visual" && mynoEditor) {
144
+ return mynoEditor.getMarkdown();
145
+ }
146
+ return editorMarkdown.value;
147
+ }
148
+
149
+ function stripLeadingH1(markdown: string): string {
150
+ return markdown.replace(/^#\s+[^\r\n]*(?:\r?\n|$)/, "");
151
+ }
152
+
153
+ function extractFrontmatter(markdown: string): { frontmatter: Record<string, unknown> | null; content: string } {
154
+ const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
155
+ if (!match) return { frontmatter: null, content: markdown };
156
+ return { frontmatter: {}, content: markdown.slice(match[0].length) };
157
+ }
158
+
159
+ /** 品牌色衍生的卡片底色(--brand-bg 系),非 6 位 hex 时返回 null 以保留账号默认值。 */
160
+ function brandTint(hex: string, alpha: number): string | null {
161
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
162
+ if (!m) return null;
163
+ const n = parseInt(m[1], 16);
164
+ return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
165
+ }
166
+
167
+ // ── Read Style Controls ─────────────────────────────────────────────────
121
168
  function readStyleControls() {
122
- const account = accounts[currentAccount];
169
+ const account = accounts[currentAccount] || {
170
+ theme: { exportTheme: {}, editorVars: {}, structure: {} },
171
+ };
172
+
123
173
  const exportTheme: WechatExportTheme = {
124
174
  ...account.theme.exportTheme as WechatExportTheme,
125
175
  bodyFontSize: `${inputFontSize.value}px`,
@@ -133,52 +183,186 @@ function readStyleControls() {
133
183
  bodyLineHeight: inputLineHeight.value,
134
184
  bodyLetterSpacing: `${inputLetterSpacing.value}em`,
135
185
  };
186
+
187
+ const brandBg = brandTint(hexBrandColor.value, 0.08);
188
+ const brandBgDeep = brandBg && brandTint(hexBrandColor.value, 0.12);
136
189
  const editorVars = {
137
190
  ...account.theme.editorVars,
138
191
  "--brand": hexBrandColor.value,
192
+ ...(brandBg && brandBgDeep ? { "--brand-bg": brandBg, "--brand-bg-deep": brandBgDeep } : {}),
139
193
  "--text": hexTextColor.value,
140
194
  "--divider": hexDividerColor.value,
141
195
  };
196
+
142
197
  const structure: ArticleStructure = {
143
198
  ...account.theme.structure,
144
199
  numberedHeadings: switchNumberedHeadings.checked,
145
200
  headingLabel: inputHeadingLabel.value.trim(),
146
201
  quoteLabel: inputQuoteLabel.value.trim(),
147
202
  };
203
+
148
204
  return { editorVars, exportTheme, structure };
149
205
  }
150
206
 
207
+ // ── Live Editor Style Synchronization ───────────────────────────────────
208
+ function applyLiveEditorStyles(
209
+ style: ReturnType<typeof readStyleControls>,
210
+ customCss: string,
211
+ ) {
212
+ let styleTag = document.getElementById("studio-live-editor-css") as HTMLStyleElement | null;
213
+ if (!styleTag) {
214
+ styleTag = document.createElement("style");
215
+ styleTag.id = "studio-live-editor-css";
216
+ document.head.appendChild(styleTag);
217
+ }
218
+
219
+ const themeCss = buildWechatThemeCss(style.editorVars, style.exportTheme);
220
+ const pluginCss = getCombinedDefaultCss();
221
+
222
+ // Structure decorations for editor (simulating heading labels / counters)
223
+ let structureCss = "";
224
+ if (style.structure.numberedHeadings) {
225
+ const labelPrefix = style.structure.headingLabel ? `"${style.structure.headingLabel} " ` : "";
226
+ structureCss += `
227
+ .milkdown .editor {
228
+ counter-reset: wx-heading-counter;
229
+ }
230
+ .milkdown .editor h2::before {
231
+ counter-increment: wx-heading-counter;
232
+ content: ${labelPrefix}counter(wx-heading-counter, decimal-leading-zero) " ";
233
+ margin-right: 0.3em;
234
+ opacity: 0.7;
235
+ font-weight: 600;
236
+ }
237
+ `;
238
+ } else if (style.structure.headingLabel) {
239
+ structureCss += `
240
+ .milkdown .editor h2::before {
241
+ content: "${style.structure.headingLabel} ";
242
+ margin-right: 0.3em;
243
+ opacity: 0.7;
244
+ font-weight: 600;
245
+ }
246
+ `;
247
+ }
248
+
249
+ if (style.structure.quoteLabel) {
250
+ structureCss += `
251
+ .milkdown .editor blockquote::before {
252
+ content: "${style.structure.quoteLabel}";
253
+ display: block;
254
+ font-size: 11px;
255
+ font-weight: 600;
256
+ letter-spacing: 0.05em;
257
+ color: var(--wx-brand-ink, var(--wx-brand-accent, #ca6093));
258
+ margin-bottom: 6px;
259
+ text-transform: uppercase;
260
+ }
261
+ `;
262
+ }
263
+
264
+ styleTag.textContent = `
265
+ /* ── Crepe resets: 后置于运行时,同特异性即可覆盖 Crepe 主题;不可用 !important,否则压过下方预设 ── */
266
+ .milkdown .ProseMirror {
267
+ padding: 0;
268
+ }
269
+ .milkdown .ProseMirror h1,
270
+ .milkdown .ProseMirror h2,
271
+ .milkdown .ProseMirror h3,
272
+ .milkdown .ProseMirror h4,
273
+ .milkdown .ProseMirror h5,
274
+ .milkdown .ProseMirror h6 {
275
+ padding-top: 0;
276
+ padding-bottom: 0;
277
+ }
278
+ .milkdown .ProseMirror p {
279
+ padding: 0;
280
+ }
281
+ .milkdown .ProseMirror blockquote {
282
+ position: static;
283
+ box-sizing: border-box;
284
+ }
285
+ .milkdown .ProseMirror blockquote::before {
286
+ display: none;
287
+ }
288
+
289
+ /* ── Dynamic WeChat Theme Variables & Container Styling ── */
290
+ ${themeCss}
291
+
292
+ /* ── Structure Enhancements (Numbered Headings, Chapter / Quote Labels) ── */
293
+ ${structureCss}
294
+
295
+ /* ── Article Plugins Styling (Callouts, Badges, Kbd, Article Cards, etc.) ── */
296
+ ${pluginCss}
297
+
298
+ /* ── Active Syntax Presets & Custom CSS Overrides ── */
299
+ ${customCss}
300
+ `;
301
+ }
302
+
303
+ function refreshLiveEditorStyles() {
304
+ const style = readStyleControls();
305
+ const account = accounts[currentAccount];
306
+ const customCss = [
307
+ buildCombinedPresetsCss(currentSyntaxPresets),
308
+ account?.articleThemeCss,
309
+ inputCustomCss.value,
310
+ ].filter(Boolean).join("\n\n");
311
+ applyLiveEditorStyles(style, customCss);
312
+ }
313
+
314
+ // ── Inlined WeChat Render Pass ──────────────────────────────────────────
151
315
  async function doRender() {
152
- if (rendering || !accounts[currentAccount]) return;
316
+ if (rendering) return;
153
317
  rendering = true;
154
318
  const revision = renderRevision;
155
- const markdown = stripLeadingH1(extractFrontmatter(editorMarkdown.value).content).trimStart();
319
+ const rawMd = getActiveMarkdown();
320
+ const markdown = stripLeadingH1(extractFrontmatter(rawMd).content).trimStart();
156
321
  const style = readStyleControls();
322
+ const account = accounts[currentAccount];
323
+
157
324
  const customCss = [
158
325
  buildCombinedPresetsCss(currentSyntaxPresets),
159
- accounts[currentAccount].articleThemeCss,
326
+ account?.articleThemeCss,
160
327
  inputCustomCss.value,
161
328
  ].filter(Boolean).join("\n\n");
329
+
330
+ applyLiveEditorStyles(style, customCss);
331
+
162
332
  updateStats();
163
333
 
164
334
  try {
165
- hiddenHost.innerHTML = "";
166
- const editor = Editor.make()
167
- .config((ctx) => {
168
- ctx.set(rootCtx, hiddenHost);
169
- ctx.set(defaultValueCtx, markdown);
170
- })
171
- .use(commonmark)
172
- .use(gfm)
173
- .use(getCombinedMilkdownPlugins());
174
-
175
- let semanticHtml: string;
176
- try {
177
- await editor.create();
178
- semanticHtml = editor.action(getHTML());
179
- } finally {
180
- await editor.destroy();
335
+ let semanticHtml = "";
336
+ if (currentEditorMode === "visual" && mynoEditor) {
337
+ const crepe = mynoEditor.getCrepe();
338
+ if (crepe) {
339
+ try {
340
+ semanticHtml = crepe.editor.action(getHTML());
341
+ } catch {
342
+ // ignore and fallback
343
+ }
344
+ }
181
345
  }
346
+
347
+ if (!semanticHtml) {
348
+ const hiddenHost = document.getElementById("studio-editor-host") || document.createElement("div");
349
+ hiddenHost.innerHTML = "";
350
+ const editor = Editor.make()
351
+ .config((ctx) => {
352
+ ctx.set(rootCtx, hiddenHost);
353
+ ctx.set(defaultValueCtx, markdown);
354
+ })
355
+ .use(commonmark)
356
+ .use(gfm)
357
+ .use(getCombinedMilkdownPlugins());
358
+ try {
359
+ await editor.create();
360
+ semanticHtml = editor.action(getHTML());
361
+ } finally {
362
+ await editor.destroy();
363
+ }
364
+ }
365
+
182
366
  const inlinedHtml = await renderWechatHtml({
183
367
  semanticHtml,
184
368
  baseCss: articleCss,
@@ -186,17 +370,19 @@ async function doRender() {
186
370
  structure: style.structure,
187
371
  editorVars: style.editorVars,
188
372
  theme: style.exportTheme,
373
+ strictImageDecode: false,
189
374
  });
375
+
190
376
  if (revision !== renderRevision) return;
191
377
  latestWechatHtml = inlinedHtml;
192
- previewScreen.innerHTML = inlinedHtml;
193
- htmlSourceCode.value = inlinedHtml;
378
+ if (previewScreen) previewScreen.innerHTML = inlinedHtml;
379
+ if (htmlSourceCode) htmlSourceCode.value = inlinedHtml;
194
380
  } catch (error) {
195
381
  if (revision !== renderRevision) return;
196
- console.error("Studio render error:", error);
382
+ console.error("myno render error:", error);
197
383
  const message = error instanceof Error ? error.message : String(error);
198
- previewScreen.textContent = `渲染错误: ${message}`;
199
- htmlSourceCode.value = "";
384
+ if (previewScreen) previewScreen.textContent = `渲染错误: ${message}`;
385
+ if (htmlSourceCode) htmlSourceCode.value = "";
200
386
  } finally {
201
387
  rendering = false;
202
388
  if (revision !== renderRevision) {
@@ -209,210 +395,351 @@ async function doRender() {
209
395
  function scheduleRender(delay = 60) {
210
396
  renderRevision += 1;
211
397
  latestWechatHtml = "";
398
+ refreshLiveEditorStyles();
212
399
  if (debounceTimer) clearTimeout(debounceTimer);
213
400
  debounceTimer = setTimeout(() => {
214
401
  void doRender();
215
402
  }, delay);
216
403
  }
217
404
 
218
- function renderSyntaxCards(plugins: WechatPluginDoc[]) {
219
- const container = document.getElementById("syntax-card-list");
405
+ // ── Style Packs (One-Click Application) ──────────────────────────────────
406
+ function renderStylePacks() {
407
+ const container = document.getElementById("style-packs-grid");
220
408
  if (!container) return;
221
409
  container.innerHTML = "";
222
410
 
223
- for (const doc of plugins) {
411
+ for (const pack of STYLE_PACKS) {
224
412
  const card = document.createElement("div");
225
- card.className = "syntax-card";
226
-
227
- const header = document.createElement("div");
228
- header.className = "syntax-card-header";
229
-
230
- const title = document.createElement("span");
231
- title.className = "syntax-card-title";
232
- title.textContent = doc.name;
233
-
234
- const badge = document.createElement("span");
235
- badge.className = "syntax-card-badge";
236
- badge.textContent = doc.category === "inline" ? "行内扩展" : "块级组件";
413
+ card.className = "style-pack-card";
414
+ card.dataset.packId = pack.id;
415
+
416
+ const head = document.createElement("div");
417
+ head.className = "style-pack-head";
418
+
419
+ const titleWrap = document.createElement("div");
420
+ const name = document.createElement("span");
421
+ name.className = "style-pack-name";
422
+ name.textContent = pack.name;
423
+ const sub = document.createElement("span");
424
+ sub.className = "style-pack-subtitle";
425
+ sub.textContent = ` · ${pack.subtitle}`;
426
+ titleWrap.appendChild(name);
427
+ titleWrap.appendChild(sub);
428
+
429
+ const swatches = document.createElement("div");
430
+ swatches.className = "style-pack-swatches";
431
+ for (const color of pack.swatch) {
432
+ const dot = document.createElement("span");
433
+ dot.className = "swatch-dot";
434
+ dot.style.background = color;
435
+ swatches.appendChild(dot);
436
+ }
237
437
 
238
- header.appendChild(title);
239
- header.appendChild(badge);
240
- card.appendChild(header);
438
+ head.appendChild(titleWrap);
439
+ head.appendChild(swatches);
440
+ card.appendChild(head);
241
441
 
242
- const desc = document.createElement("p");
243
- desc.className = "syntax-card-desc";
244
- desc.textContent = doc.description;
442
+ const desc = document.createElement("div");
443
+ desc.className = "style-pack-desc";
444
+ desc.textContent = pack.description;
245
445
  card.appendChild(desc);
246
446
 
247
- const code = document.createElement("pre");
248
- code.className = "syntax-code-snippet";
249
- code.textContent = doc.sampleMarkdown;
250
- card.appendChild(code);
251
-
252
- const insertBtn = document.createElement("button");
253
- insertBtn.className = "syntax-btn-insert";
254
- insertBtn.textContent = "+ 插入示例到文章中";
255
- insertBtn.addEventListener("click", () => {
256
- insertTextAtCursor(doc.sampleMarkdown);
257
- showToast(`已插入「${doc.name}」示例`, "info");
447
+ card.addEventListener("click", () => {
448
+ applyStylePack(pack);
449
+ document.querySelectorAll(".style-pack-card").forEach((c) => c.classList.remove("active"));
450
+ card.classList.add("active");
451
+ showToast(`已套用「${pack.name}」风格套系`, "info");
258
452
  });
259
- card.appendChild(insertBtn);
260
453
 
261
454
  container.appendChild(card);
262
455
  }
263
456
  }
264
457
 
265
- function insertTextAtCursor(text: string) {
266
- const start = editorMarkdown.selectionStart;
267
- const end = editorMarkdown.selectionEnd;
268
- const original = editorMarkdown.value;
269
- const insertText = `\n\n${text}\n\n`;
270
- editorMarkdown.value = original.slice(0, start) + insertText + original.slice(end);
271
- editorMarkdown.focus();
272
- editorMarkdown.selectionStart = editorMarkdown.selectionEnd = start + insertText.length;
458
+ function applyStylePack(pack: StylePack) {
459
+ const t = pack.theme;
460
+ // Font Size
461
+ inputFontSize.value = t.fontSize;
462
+ valFontSize.textContent = `${t.fontSize}px`;
463
+ syncSegmentedState("seg-font-size", t.fontSize);
464
+
465
+ // Line Height
466
+ inputLineHeight.value = t.lineHeight;
467
+ valLineHeight.textContent = t.lineHeight;
468
+ syncSegmentedState("seg-line-height", t.lineHeight);
469
+
470
+ // Colors
471
+ pickerBrandColor.value = hexBrandColor.value = t.brandColor;
472
+ pickerTextColor.value = hexTextColor.value = t.textColor;
473
+ pickerH2Color.value = hexH2Color.value = t.h2Color;
474
+ pickerH3Color.value = hexH3Color.value = t.h3Color;
475
+ pickerQuoteColor.value = hexQuoteColor.value = t.quoteColor;
476
+ pickerDividerColor.value = hexDividerColor.value = t.dividerColor;
477
+ syncSwatchActiveState(t.brandColor);
478
+
479
+ // H2 Preset
480
+ currentSyntaxPresets.h2 = t.h2Preset;
481
+ syncPillActiveState("seg-h2-preset", t.h2Preset);
482
+
483
+ // H3 Preset
484
+ if (t.h3Preset) currentSyntaxPresets.h3 = t.h3Preset;
485
+
486
+ // Numbered Headings
487
+ switchNumberedHeadings.checked = t.numberedHeadings;
488
+
273
489
  scheduleRender(20);
274
490
  }
275
491
 
492
+ function syncSegmentedState(containerId: string, value: string) {
493
+ const container = document.getElementById(containerId);
494
+ if (!container) return;
495
+ container.querySelectorAll(".seg-btn").forEach((btn) => {
496
+ const el = btn as HTMLElement;
497
+ if (el.dataset.val === value) {
498
+ el.classList.add("active");
499
+ } else {
500
+ el.classList.remove("active");
501
+ }
502
+ });
503
+ }
504
+
505
+ function syncPillActiveState(containerId: string, presetId: string) {
506
+ const container = document.getElementById(containerId);
507
+ if (!container) return;
508
+ container.querySelectorAll(".pill-btn").forEach((btn) => {
509
+ const el = btn as HTMLElement;
510
+ if (el.dataset.preset === presetId) {
511
+ el.classList.add("active");
512
+ } else {
513
+ el.classList.remove("active");
514
+ }
515
+ });
516
+ }
517
+
518
+ function syncSwatchActiveState(hexColor: string) {
519
+ document.querySelectorAll(".swatch-btn").forEach((btn) => {
520
+ const el = btn as HTMLElement;
521
+ if (el.dataset.color?.toLowerCase() === hexColor.toLowerCase()) {
522
+ el.style.transform = "scale(1.2)";
523
+ el.style.boxShadow = "0 0 0 2px var(--brand)";
524
+ } else {
525
+ el.style.transform = "";
526
+ el.style.boxShadow = "0 0 0 1px var(--border)";
527
+ }
528
+ });
529
+ }
530
+
531
+ // ── Bind Segmented Controls ─────────────────────────────────────────────
532
+ function initSegmentedControls() {
533
+ // Font Size
534
+ const segFontSize = document.getElementById("seg-font-size");
535
+ segFontSize?.querySelectorAll(".seg-btn").forEach((btn) => {
536
+ btn.addEventListener("click", () => {
537
+ const val = (btn as HTMLElement).dataset.val || "16";
538
+ inputFontSize.value = val;
539
+ valFontSize.textContent = `${val}px`;
540
+ syncSegmentedState("seg-font-size", val);
541
+ scheduleRender(20);
542
+ });
543
+ });
544
+
545
+ // Line Height
546
+ const segLineHeight = document.getElementById("seg-line-height");
547
+ segLineHeight?.querySelectorAll(".seg-btn").forEach((btn) => {
548
+ btn.addEventListener("click", () => {
549
+ const val = (btn as HTMLElement).dataset.val || "1.84";
550
+ inputLineHeight.value = val;
551
+ valLineHeight.textContent = val;
552
+ syncSegmentedState("seg-line-height", val);
553
+ scheduleRender(20);
554
+ });
555
+ });
556
+
557
+ // Quick Swatches
558
+ document.querySelectorAll(".swatch-btn").forEach((btn) => {
559
+ btn.addEventListener("click", () => {
560
+ const color = (btn as HTMLElement).dataset.color || "#ca6093";
561
+ pickerBrandColor.value = hexBrandColor.value = color;
562
+ syncSwatchActiveState(color);
563
+ scheduleRender(20);
564
+ });
565
+ });
566
+
567
+ // H2 Pills
568
+ const segH2 = document.getElementById("seg-h2-preset");
569
+ segH2?.querySelectorAll(".pill-btn").forEach((btn) => {
570
+ btn.addEventListener("click", () => {
571
+ const preset = (btn as HTMLElement).dataset.preset || "pillar";
572
+ currentSyntaxPresets.h2 = preset;
573
+ syncPillActiveState("seg-h2-preset", preset);
574
+ scheduleRender(20);
575
+ });
576
+ });
577
+ }
578
+
579
+ // ── Syntax Presets in Accordion ─────────────────────────────────────────
276
580
  function renderSyntaxPresetControls() {
277
581
  const container = document.getElementById("syntax-preset-container");
278
582
  if (!container) return;
279
583
  container.innerHTML = "";
280
584
 
281
585
  for (const cat of SYNTAX_CATEGORIES) {
586
+ if (cat.key === "h2") continue; // H2 has its own pills at top level
282
587
  const group = document.createElement("div");
283
- group.className = "preset-group";
588
+ group.className = "control-group";
284
589
 
285
590
  const header = document.createElement("div");
286
- header.className = "preset-header";
287
-
591
+ header.className = "control-label";
288
592
  const title = document.createElement("span");
289
- title.className = "preset-title";
290
593
  title.textContent = `${cat.icon} ${cat.label}`;
291
594
  header.appendChild(title);
292
595
 
293
596
  const hasMultiplePresets = Boolean(cat.presets && cat.presets.length > 1);
294
-
295
- const extractBtn = document.createElement("button");
296
- extractBtn.type = "button";
297
- extractBtn.className = "btn-extract-css";
298
- extractBtn.textContent = "📋 提取 CSS";
299
- extractBtn.title = "将此语法的 CSS 追加到底部自定义编辑器,方便微调覆盖";
300
- extractBtn.addEventListener("click", () => {
301
- let snippet = "";
302
- if (hasMultiplePresets && cat.presets) {
303
- const activeId = currentSyntaxPresets[cat.key] || cat.defaultPresetId || cat.presets[0].id;
304
- const p = cat.presets.find((preset) => preset.id === activeId) || cat.presets[0];
305
- snippet = `/* [${cat.label}] - ${p.name} */\n${p.css.trim()}\n`;
306
- } else {
307
- const cssContent = cat.css || (cat.presets && cat.presets[0]?.css) || "";
308
- snippet = `/* [${cat.label}] 基础样式 */\n${cssContent.trim()}\n`;
309
- }
310
- if (!snippet) return;
311
- inputCustomCss.value = inputCustomCss.value ? `${inputCustomCss.value.trim()}\n\n${snippet}` : snippet;
312
- inputCustomCss.focus();
313
- inputCustomCss.scrollTop = inputCustomCss.scrollHeight;
314
- showToast(`已将「${cat.label}」CSS 追加到底部编辑框!`, "info");
315
- scheduleRender(20);
316
- });
317
- header.appendChild(extractBtn);
318
-
319
- group.appendChild(header);
320
-
321
597
  if (hasMultiplePresets && cat.presets) {
322
598
  const select = document.createElement("select");
323
- select.className = "preset-select";
324
- select.dataset.category = cat.key;
325
-
599
+ select.className = "select-control full-width";
326
600
  for (const preset of cat.presets) {
327
601
  const opt = document.createElement("option");
328
602
  opt.value = preset.id;
329
603
  opt.textContent = preset.name;
330
604
  select.appendChild(opt);
331
605
  }
332
-
333
606
  const activePresetId = currentSyntaxPresets[cat.key] || cat.defaultPresetId || cat.presets[0].id;
334
607
  select.value = activePresetId;
335
- group.appendChild(select);
336
-
337
- const desc = document.createElement("div");
338
- desc.className = "preset-desc";
339
- const activePreset = cat.presets.find((p) => p.id === activePresetId) || cat.presets[0];
340
- desc.textContent = activePreset?.description || "";
341
- group.appendChild(desc);
342
-
343
608
  select.addEventListener("change", () => {
344
609
  currentSyntaxPresets[cat.key] = select.value;
345
- const p = cat.presets!.find((preset) => preset.id === select.value);
346
- desc.textContent = p?.description || "";
347
610
  scheduleRender(20);
348
611
  });
349
- } else if (cat.description) {
350
- const desc = document.createElement("div");
351
- desc.className = "preset-desc";
352
- desc.textContent = cat.description;
353
- group.appendChild(desc);
612
+ group.appendChild(header);
613
+ group.appendChild(select);
614
+ } else {
615
+ group.appendChild(header);
354
616
  }
355
617
 
356
618
  container.appendChild(group);
357
619
  }
358
620
  }
359
621
 
622
+ // ── Tab 2: Syntax Cards ─────────────────────────────────────────────────
623
+ function renderSyntaxCards(plugins: WechatPluginDoc[]) {
624
+ const container = document.getElementById("syntax-card-list");
625
+ if (!container) return;
626
+ container.innerHTML = "";
627
+
628
+ for (const doc of plugins) {
629
+ const card = document.createElement("div");
630
+ card.className = "syntax-card";
631
+
632
+ const header = document.createElement("div");
633
+ header.className = "syntax-card-header";
634
+
635
+ const title = document.createElement("span");
636
+ title.className = "syntax-card-title";
637
+ title.textContent = doc.name;
638
+
639
+ const badge = document.createElement("span");
640
+ badge.className = "syntax-card-badge";
641
+ badge.textContent = doc.category === "inline" ? "行内扩展" : "块级组件";
642
+
643
+ header.appendChild(title);
644
+ header.appendChild(badge);
645
+ card.appendChild(header);
646
+
647
+ const desc = document.createElement("p");
648
+ desc.className = "syntax-card-desc";
649
+ desc.textContent = doc.description;
650
+ card.appendChild(desc);
651
+
652
+ const code = document.createElement("pre");
653
+ code.className = "syntax-code-snippet";
654
+ code.textContent = doc.sampleMarkdown;
655
+ card.appendChild(code);
656
+
657
+ const insertBtn = document.createElement("button");
658
+ insertBtn.className = "syntax-btn-insert";
659
+ insertBtn.textContent = "+ 插入示例到当前文章";
660
+ insertBtn.addEventListener("click", () => {
661
+ insertTextAtCursor(doc.sampleMarkdown);
662
+ showToast(`已插入「${doc.name}」示例`, "info");
663
+ });
664
+ card.appendChild(insertBtn);
665
+
666
+ container.appendChild(card);
667
+ }
668
+ }
669
+
670
+ function insertTextAtCursor(text: string) {
671
+ if (currentEditorMode === "visual" && mynoEditor) {
672
+ const current = mynoEditor.getMarkdown();
673
+ const updated = `${current.trimEnd()}\n\n${text}\n\n`;
674
+ mynoEditor.setMarkdown(updated);
675
+ editorMarkdown.value = updated;
676
+ } else {
677
+ const start = editorMarkdown.selectionStart;
678
+ const end = editorMarkdown.selectionEnd;
679
+ const original = editorMarkdown.value;
680
+ const insertText = `\n\n${text}\n\n`;
681
+ editorMarkdown.value = original.slice(0, start) + insertText + original.slice(end);
682
+ editorMarkdown.focus();
683
+ editorMarkdown.selectionStart = editorMarkdown.selectionEnd = start + insertText.length;
684
+ }
685
+ scheduleRender(20);
686
+ }
687
+
688
+ // ── Account & Config Loading ────────────────────────────────────────────
360
689
  function applyAccountConfig(accountKey: string) {
361
690
  const account = accounts[accountKey];
362
691
  if (!account) return;
363
692
 
693
+ const standalone = Boolean(account.articleThemeStandalone);
694
+ configModeBadge.textContent = standalone ? "独立排版文件" : "账号默认配置";
695
+ configModeBadge.className = `badge ${standalone ? "badge-primary" : "badge-subtle"}`;
696
+
697
+ btnSave.disabled = Boolean(account.articleThemeStandalone);
698
+ btnSave.title = account.articleThemeStandalone
699
+ ? "当前账号引用独立配置,请通过「导出配置」保存调整后替换原文件"
700
+ : "保存当前调整至本地 config.json 的当前账号";
701
+
364
702
  const exportTheme = account.theme?.exportTheme || {};
365
703
  const editorVars = account.theme?.editorVars || {};
366
704
 
367
- // 账号切换与重置必须恢复全部排版控件。
368
- inputFontSize.value = exportTheme.bodyFontSize?.replace(/px$/, "")
369
- || exportTheme.containerStyle?.match(/font-size:\s*(\d+(?:\.\d+)?)px/)?.[1] || "16";
370
- valFontSize.textContent = `${inputFontSize.value}px`;
371
- inputLineHeight.value = exportTheme.bodyLineHeight || "1.84";
372
- valLineHeight.textContent = inputLineHeight.value;
705
+ // Font Size
706
+ const fontSize = exportTheme.bodyFontSize?.replace(/px$/, "") || "16";
707
+ inputFontSize.value = fontSize;
708
+ valFontSize.textContent = `${fontSize}px`;
709
+ syncSegmentedState("seg-font-size", fontSize);
710
+
711
+ // Line Height
712
+ const lineHeight = exportTheme.bodyLineHeight || "1.84";
713
+ inputLineHeight.value = lineHeight;
714
+ valLineHeight.textContent = lineHeight;
715
+ syncSegmentedState("seg-line-height", lineHeight);
716
+
717
+ // Letter & Paragraph Spacing
373
718
  inputLetterSpacing.value = String(parseFloat(exportTheme.bodyLetterSpacing || "0.012em"));
374
719
  valLetterSpacing.textContent = `${inputLetterSpacing.value}em`;
375
720
  inputParaSpacing.value = String(parseFloat(exportTheme.bodyParagraphSpacing || "1.18em"));
376
721
  valParaSpacing.textContent = `${inputParaSpacing.value}em`;
377
- paletteSelect.value = "";
378
- paletteDesc.textContent = "";
379
-
380
- // 4. Colors
381
- const brand = editorVars["--brand"] || exportTheme.primaryColor;
382
- if (brand) {
383
- pickerBrandColor.value = brand;
384
- hexBrandColor.value = brand;
385
- }
386
- const textColor = exportTheme.bodyColor || editorVars["--text"];
387
- if (textColor) {
388
- pickerTextColor.value = textColor;
389
- hexTextColor.value = textColor;
390
- }
391
- if (exportTheme.h2Color) {
392
- pickerH2Color.value = exportTheme.h2Color;
393
- hexH2Color.value = exportTheme.h2Color;
394
- }
395
- if (exportTheme.h3Color) {
396
- pickerH3Color.value = exportTheme.h3Color;
397
- hexH3Color.value = exportTheme.h3Color;
398
- }
399
- const quoteColor = exportTheme.blockquoteBorderColor || brand;
400
- if (quoteColor) {
401
- pickerQuoteColor.value = quoteColor;
402
- hexQuoteColor.value = quoteColor;
403
- }
404
- const dividerColor = exportTheme.dividerColor || editorVars["--divider"];
405
- if (dividerColor) {
406
- pickerDividerColor.value = dividerColor;
407
- hexDividerColor.value = dividerColor;
408
- }
409
- if (account.customCssContent) {
410
- inputCustomCss.value = account.customCssContent;
411
- } else {
412
- inputCustomCss.value = "";
413
- }
414
722
 
415
- // 5. Syntax Presets
723
+ // Colors
724
+ const brand = editorVars["--brand"] || exportTheme.primaryColor || "#ca6093";
725
+ pickerBrandColor.value = hexBrandColor.value = brand;
726
+ syncSwatchActiveState(brand);
727
+
728
+ const text = exportTheme.bodyColor || editorVars["--text"] || "#292526";
729
+ pickerTextColor.value = hexTextColor.value = text;
730
+
731
+ if (exportTheme.h2Color) pickerH2Color.value = hexH2Color.value = exportTheme.h2Color;
732
+ if (exportTheme.h3Color) pickerH3Color.value = hexH3Color.value = exportTheme.h3Color;
733
+
734
+ const quote = exportTheme.blockquoteBorderColor || brand;
735
+ pickerQuoteColor.value = hexQuoteColor.value = quote;
736
+
737
+ const divider = exportTheme.dividerColor || editorVars["--divider"] || "#e2dcdf";
738
+ pickerDividerColor.value = hexDividerColor.value = divider;
739
+
740
+ inputCustomCss.value = account.customCssContent || "";
741
+
742
+ // Syntax Presets
416
743
  const savedPresets = account.theme?.syntaxPresets || {};
417
744
  currentSyntaxPresets = {};
418
745
  for (const cat of SYNTAX_CATEGORIES) {
@@ -420,9 +747,10 @@ function applyAccountConfig(accountKey: string) {
420
747
  currentSyntaxPresets[cat.key] = savedPresets[cat.key] || cat.defaultPresetId;
421
748
  }
422
749
  }
750
+ syncPillActiveState("seg-h2-preset", currentSyntaxPresets.h2 || "pillar");
423
751
  renderSyntaxPresetControls();
424
752
 
425
- // 6. Structure
753
+ // Structure
426
754
  const structure = account.theme?.structure || {};
427
755
  switchNumberedHeadings.checked = Boolean(structure.numberedHeadings);
428
756
  inputHeadingLabel.value = structure.headingLabel || "";
@@ -431,6 +759,7 @@ function applyAccountConfig(accountKey: string) {
431
759
  scheduleRender(20);
432
760
  }
433
761
 
762
+ // ── Initialize Color Palettes Dropdown ───────────────────────────────────
434
763
  function initColorPalettes() {
435
764
  if (!paletteSelect) return;
436
765
  paletteSelect.innerHTML = `<option value="">-- 选择推荐调色盘 (一键应用) --</option>`;
@@ -440,6 +769,7 @@ function initColorPalettes() {
440
769
  opt.textContent = `${p.name} · ${p.category}`;
441
770
  paletteSelect.appendChild(opt);
442
771
  }
772
+
443
773
  paletteSelect.addEventListener("change", () => {
444
774
  const pal = COLOR_PALETTES.find((p) => p.id === paletteSelect.value);
445
775
  if (!pal) return;
@@ -449,14 +779,30 @@ function initColorPalettes() {
449
779
  pickerH3Color.value = hexH3Color.value = pal.colors.h3;
450
780
  pickerQuoteColor.value = hexQuoteColor.value = pal.colors.quote;
451
781
  pickerDividerColor.value = hexDividerColor.value = pal.colors.divider;
782
+ syncSwatchActiveState(pal.colors.brand);
452
783
  if (paletteDesc) paletteDesc.textContent = pal.description;
453
- showToast(`已应用「${pal.name}」高级配色方案!`, "info");
784
+ showToast(`已套用「${pal.name}」色盘`, "info");
454
785
  scheduleRender(20);
455
786
  });
456
787
  }
457
788
 
789
+ // ── Studio Main Initialization ──────────────────────────────────────────
458
790
  async function initStudio() {
459
791
  initColorPalettes();
792
+ initSegmentedControls();
793
+ renderStylePacks();
794
+
795
+ // Bind Color Pairs
796
+ bindColorPair(pickerBrandColor, hexBrandColor, () => {
797
+ syncSwatchActiveState(pickerBrandColor.value);
798
+ scheduleRender(20);
799
+ });
800
+ bindColorPair(pickerTextColor, hexTextColor, () => scheduleRender(20));
801
+ bindColorPair(pickerH2Color, hexH2Color, () => scheduleRender(20));
802
+ bindColorPair(pickerH3Color, hexH3Color, () => scheduleRender(20));
803
+ bindColorPair(pickerQuoteColor, hexQuoteColor, () => scheduleRender(20));
804
+ bindColorPair(pickerDividerColor, hexDividerColor, () => scheduleRender(20));
805
+
460
806
  try {
461
807
  const res = await fetch("/api/studio/config");
462
808
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
@@ -466,7 +812,7 @@ async function initStudio() {
466
812
  currentAccount = data.defaultAccount || "default";
467
813
  pluginsList = data.plugins || [];
468
814
 
469
- // Populate account selector
815
+ // Account selector
470
816
  accountSelect.innerHTML = "";
471
817
  for (const [key, acc] of Object.entries(accounts)) {
472
818
  const opt = document.createElement("option");
@@ -476,36 +822,83 @@ async function initStudio() {
476
822
  accountSelect.appendChild(opt);
477
823
  }
478
824
 
479
- // Prefill Markdown
480
- editorMarkdown.value = data.sampleMarkdown || "# 微信排版标题\n\n欢迎使用 WeChat Visual Studio!";
825
+ const initialMd = data.sampleMarkdown || "# 微信排版标题\n\n欢迎使用 myno 微信排版工作台!";
826
+ editorMarkdown.value = initialMd;
827
+
828
+ // Mount MynoEditor (Milkdown Crepe)
829
+ if (milkdownEditorRoot) {
830
+ mynoEditor = new MynoEditor({
831
+ root: milkdownEditorRoot,
832
+ defaultValue: initialMd,
833
+ onMarkdownChange: (md) => {
834
+ editorMarkdown.value = md;
835
+ scheduleRender(40);
836
+ },
837
+ });
838
+ await mynoEditor.create();
839
+ }
481
840
 
482
- // Render plugins cheatsheet
483
841
  renderSyntaxCards(pluginsList);
484
-
485
- // Apply active account config
486
842
  applyAccountConfig(currentAccount);
487
843
  } catch (err) {
488
844
  console.error("Failed to load studio config:", err);
489
- showToast("无法加载配置,使用默认设置", "error");
845
+ showToast("无法加载服务器配置,使用本地默认设置", "error");
490
846
  scheduleRender(20);
491
847
  }
492
848
  }
493
849
 
494
- // Event Listeners
850
+ // ── Event Handlers ──────────────────────────────────────────────────────
495
851
  accountSelect.addEventListener("change", () => {
496
852
  currentAccount = accountSelect.value;
497
853
  applyAccountConfig(currentAccount);
498
854
  });
499
855
 
500
- editorMarkdown.addEventListener("input", () => scheduleRender(40));
856
+ editorMarkdown.addEventListener("input", () => {
857
+ // 源码模式下 textarea 是编辑面;视觉模式下它隐藏,但仍可能被脚本/按钮更新,需回同步给 Crepe
858
+ if (mynoEditor) {
859
+ mynoEditor.setMarkdown(editorMarkdown.value);
860
+ }
861
+ scheduleRender(40);
862
+ });
863
+
864
+ // Dual Mode Switcher
865
+ btnModeVisual.addEventListener("click", () => {
866
+ if (currentEditorMode === "visual") return;
867
+ currentEditorMode = "visual";
868
+ btnModeVisual.classList.add("active");
869
+ btnModeSource.classList.remove("active");
870
+ milkdownEditorRoot.style.display = "block";
871
+ editorMarkdown.style.display = "none";
872
+ if (mynoEditor) {
873
+ mynoEditor.setMarkdown(editorMarkdown.value);
874
+ }
875
+ refreshLiveEditorStyles();
876
+ showToast("已切换至所见即所得编辑模式", "info");
877
+ });
878
+
879
+ btnModeSource.addEventListener("click", () => {
880
+ if (currentEditorMode === "source") return;
881
+ currentEditorMode = "source";
882
+ btnModeSource.classList.add("active");
883
+ btnModeVisual.classList.remove("active");
884
+ if (mynoEditor) {
885
+ editorMarkdown.value = mynoEditor.getMarkdown();
886
+ }
887
+ milkdownEditorRoot.style.display = "none";
888
+ editorMarkdown.style.display = "block";
889
+ editorMarkdown.focus();
890
+ showToast("已切换至 Markdown 源码模式", "info");
891
+ });
501
892
 
502
- // Sliders
893
+ // Sliders and Text Inputs
503
894
  inputFontSize.addEventListener("input", () => {
504
895
  valFontSize.textContent = `${inputFontSize.value}px`;
896
+ syncSegmentedState("seg-font-size", inputFontSize.value);
505
897
  scheduleRender(20);
506
898
  });
507
899
  inputLineHeight.addEventListener("input", () => {
508
900
  valLineHeight.textContent = inputLineHeight.value;
901
+ syncSegmentedState("seg-line-height", inputLineHeight.value);
509
902
  scheduleRender(20);
510
903
  });
511
904
  inputLetterSpacing.addEventListener("input", () => {
@@ -517,15 +910,6 @@ inputParaSpacing.addEventListener("input", () => {
517
910
  scheduleRender(20);
518
911
  });
519
912
 
520
- // Color Pickers
521
- bindColorPair(pickerBrandColor, hexBrandColor, () => scheduleRender(20));
522
- bindColorPair(pickerTextColor, hexTextColor, () => scheduleRender(20));
523
- bindColorPair(pickerH2Color, hexH2Color, () => scheduleRender(20));
524
- bindColorPair(pickerH3Color, hexH3Color, () => scheduleRender(20));
525
- bindColorPair(pickerQuoteColor, hexQuoteColor, () => scheduleRender(20));
526
- bindColorPair(pickerDividerColor, hexDividerColor, () => scheduleRender(20));
527
-
528
- // Switches & text inputs
529
913
  switchNumberedHeadings.addEventListener("change", () => scheduleRender(20));
530
914
  inputHeadingLabel.addEventListener("input", () => scheduleRender(40));
531
915
  inputQuoteLabel.addEventListener("input", () => scheduleRender(40));
@@ -537,14 +921,14 @@ const tabBtnSyntax = document.getElementById("tab-btn-syntax") as HTMLButtonElem
537
921
  const tabContentStyle = document.getElementById("tab-content-style") as HTMLElement;
538
922
  const tabContentSyntax = document.getElementById("tab-content-syntax") as HTMLElement;
539
923
 
540
- tabBtnStyle.addEventListener("click", () => {
924
+ tabBtnStyle?.addEventListener("click", () => {
541
925
  tabBtnStyle.classList.add("active");
542
926
  tabBtnSyntax.classList.remove("active");
543
927
  tabContentStyle.style.display = "block";
544
928
  tabContentSyntax.style.display = "none";
545
929
  });
546
930
 
547
- tabBtnSyntax.addEventListener("click", () => {
931
+ tabBtnSyntax?.addEventListener("click", () => {
548
932
  tabBtnSyntax.classList.add("active");
549
933
  tabBtnStyle.classList.remove("active");
550
934
  tabContentStyle.style.display = "none";
@@ -552,44 +936,45 @@ tabBtnSyntax.addEventListener("click", () => {
552
936
  });
553
937
 
554
938
  // Load sample / clear
555
- document.getElementById("btn-load-sample")?.addEventListener("click", async () => {
939
+ btnLoadSample?.addEventListener("click", async () => {
556
940
  const res = await fetch("/api/studio/config");
557
941
  const data = (await res.json()) as StudioConfigResponse;
558
- editorMarkdown.value = data.sampleMarkdown || "";
942
+ const sample = data.sampleMarkdown || "";
943
+ editorMarkdown.value = sample;
944
+ if (mynoEditor) mynoEditor.setMarkdown(sample);
559
945
  scheduleRender(20);
560
- showToast("已加载官方全语法测试稿", "info");
946
+ showToast("已加载全语法官方测试稿", "info");
561
947
  });
562
948
 
563
- document.getElementById("btn-clear-md")?.addEventListener("click", () => {
949
+ btnClearMd?.addEventListener("click", () => {
564
950
  editorMarkdown.value = "";
951
+ if (mynoEditor) mynoEditor.setMarkdown("");
565
952
  scheduleRender(20);
566
953
  });
567
954
 
568
955
  // Toggle HTML view
569
- document.getElementById("btn-toggle-view")?.addEventListener("click", (e) => {
570
- const btn = e.currentTarget as HTMLButtonElement;
956
+ btnToggleView?.addEventListener("click", () => {
571
957
  showingHtmlSource = !showingHtmlSource;
572
958
  if (showingHtmlSource) {
573
959
  htmlSourceContainer.style.display = "block";
574
- btn.textContent = "📱 返回视图";
960
+ btnToggleView.textContent = "📱 返回视图";
575
961
  } else {
576
962
  htmlSourceContainer.style.display = "none";
577
- btn.textContent = "🔍 查看 HTML 源码";
963
+ btnToggleView.textContent = "🔍 HTML 源码";
578
964
  }
579
965
  });
580
966
 
581
967
  // Reset
582
- document.getElementById("btn-reset")?.addEventListener("click", () => {
968
+ btnReset?.addEventListener("click", () => {
583
969
  applyAccountConfig(currentAccount);
584
970
  showToast("已重置为当前账号默认样式", "info");
585
971
  });
586
972
 
587
973
  // Save to config.json
588
- document.getElementById("btn-save")?.addEventListener("click", async () => {
974
+ btnSave?.addEventListener("click", async () => {
589
975
  const account = accounts[currentAccount];
590
976
  if (!account) return;
591
977
  const { structure, ...theme } = readStyleControls();
592
- // 页首素材由正文模板提供,不写入账号结构配置。
593
978
  const { headerImageUrl: _headerImageUrl, ...savedStructure } = structure;
594
979
  const payload = {
595
980
  account: currentAccount,
@@ -618,8 +1003,97 @@ document.getElementById("btn-save")?.addEventListener("click", async () => {
618
1003
  }
619
1004
  });
620
1005
 
1006
+ // Export Config (wechat-style.json)
1007
+ btnExportConfig?.addEventListener("click", () => {
1008
+ const account = accounts[currentAccount];
1009
+ if (!account) return;
1010
+ const styleConfig: StandaloneStyleConfig = {
1011
+ schemaVersion: 1,
1012
+ engineVersion: 1,
1013
+ id: standaloneThemeName || "wechat-style",
1014
+ name: "myno WeChat 排版配置",
1015
+ version: "1.0.0",
1016
+ ...readStyleControls(),
1017
+ syntaxPresets: { ...currentSyntaxPresets },
1018
+ css: [account.articleThemeCss, inputCustomCss.value].filter(Boolean).join("\n\n"),
1019
+ };
1020
+
1021
+ const { url, filename } = exportStyleConfigBlob(styleConfig);
1022
+ const link = document.createElement("a");
1023
+ link.href = url;
1024
+ link.download = filename;
1025
+ link.click();
1026
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
1027
+ showToast("已导出独立排版文件,CLI 使用 --article-theme 引用该文件", "success");
1028
+ });
1029
+
1030
+ // Import Config (wechat-style.json)
1031
+ btnImportConfig?.addEventListener("click", () => {
1032
+ fileImportConfig.click();
1033
+ });
1034
+
1035
+ fileImportConfig?.addEventListener("change", () => {
1036
+ const file = fileImportConfig.files?.[0];
1037
+ if (!file) return;
1038
+ const reader = new FileReader();
1039
+ reader.onload = () => {
1040
+ const content = reader.result as string;
1041
+ const result = parseStyleConfig(content);
1042
+ if (!result.ok) {
1043
+ showToast(`导入失败: ${result.error}`, "error");
1044
+ return;
1045
+ }
1046
+ const cfg = result.config;
1047
+ standaloneThemeName = cfg.id || file.name.replace(/\.json$/i, "");
1048
+
1049
+ // Apply values to UI
1050
+ if (cfg.exportTheme.bodyFontSize) {
1051
+ const fs = cfg.exportTheme.bodyFontSize.replace(/px$/, "");
1052
+ inputFontSize.value = fs;
1053
+ valFontSize.textContent = `${fs}px`;
1054
+ syncSegmentedState("seg-font-size", fs);
1055
+ }
1056
+ if (cfg.exportTheme.bodyLineHeight) {
1057
+ inputLineHeight.value = cfg.exportTheme.bodyLineHeight;
1058
+ valLineHeight.textContent = cfg.exportTheme.bodyLineHeight;
1059
+ syncSegmentedState("seg-line-height", cfg.exportTheme.bodyLineHeight);
1060
+ }
1061
+ if (cfg.exportTheme.primaryColor || cfg.editorVars["--brand"]) {
1062
+ const brand = cfg.exportTheme.primaryColor || cfg.editorVars["--brand"];
1063
+ pickerBrandColor.value = hexBrandColor.value = brand;
1064
+ syncSwatchActiveState(brand);
1065
+ }
1066
+ if (cfg.exportTheme.bodyColor || cfg.editorVars["--text"]) {
1067
+ pickerTextColor.value = hexTextColor.value = cfg.exportTheme.bodyColor || cfg.editorVars["--text"];
1068
+ }
1069
+ if (cfg.exportTheme.h2Color) pickerH2Color.value = hexH2Color.value = cfg.exportTheme.h2Color;
1070
+ if (cfg.exportTheme.h3Color) pickerH3Color.value = hexH3Color.value = cfg.exportTheme.h3Color;
1071
+ if (cfg.exportTheme.blockquoteBorderColor) pickerQuoteColor.value = hexQuoteColor.value = cfg.exportTheme.blockquoteBorderColor;
1072
+ if (cfg.exportTheme.dividerColor) pickerDividerColor.value = hexDividerColor.value = cfg.exportTheme.dividerColor;
1073
+
1074
+ if (cfg.syntaxPresets) {
1075
+ currentSyntaxPresets = { ...cfg.syntaxPresets };
1076
+ if (currentSyntaxPresets.h2) syncPillActiveState("seg-h2-preset", currentSyntaxPresets.h2);
1077
+ }
1078
+ if (cfg.structure) {
1079
+ switchNumberedHeadings.checked = Boolean(cfg.structure.numberedHeadings);
1080
+ inputHeadingLabel.value = cfg.structure.headingLabel || "";
1081
+ inputQuoteLabel.value = cfg.structure.quoteLabel || "";
1082
+ }
1083
+ if (cfg.css) {
1084
+ inputCustomCss.value = cfg.css;
1085
+ }
1086
+
1087
+ configModeBadge.textContent = `独立配置: ${file.name}`;
1088
+ configModeBadge.className = "badge badge-primary";
1089
+ showToast(`✅ 成功导入独立配置: ${file.name}`, "success");
1090
+ scheduleRender(20);
1091
+ };
1092
+ reader.readAsText(file);
1093
+ });
1094
+
621
1095
  // Copy to WeChat
622
- document.getElementById("btn-copy")?.addEventListener("click", async () => {
1096
+ btnCopy?.addEventListener("click", async () => {
623
1097
  if (!latestWechatHtml) {
624
1098
  showToast("文章内容为空", "error");
625
1099
  return;
@@ -637,7 +1111,6 @@ document.getElementById("btn-copy")?.addEventListener("click", async () => {
637
1111
  showToast("📋 已复制富文本!可在微信公众号后台直接粘贴 (Cmd+V)", "success");
638
1112
  } catch (err) {
639
1113
  console.error("Clipboard write error:", err);
640
- // Fallback: execCommand copy
641
1114
  const listener = (e: ClipboardEvent) => {
642
1115
  e.clipboardData?.setData("text/html", latestWechatHtml);
643
1116
  e.clipboardData?.setData("text/plain", latestWechatHtml);