@jant/core 0.3.38 → 0.3.40

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 (1181) hide show
  1. package/LICENSE +189 -189
  2. package/README.md +6 -4
  3. package/bin/commands/assets/prepare.js +55 -0
  4. package/bin/commands/db/execute-file.js +113 -0
  5. package/bin/commands/db/export.js +5 -0
  6. package/bin/commands/db/rehearse.js +65 -0
  7. package/bin/commands/deploy.js +162 -0
  8. package/bin/commands/export.js +73 -75
  9. package/bin/commands/import-site.js +1531 -267
  10. package/bin/commands/migrate.js +79 -0
  11. package/bin/commands/reset-password.js +80 -7
  12. package/bin/commands/site/export.js +415 -0
  13. package/bin/commands/site/import.js +5 -0
  14. package/bin/commands/site/localize-media.js +118 -0
  15. package/bin/commands/site/snapshot/export.js +357 -0
  16. package/bin/commands/site/snapshot/import.js +363 -0
  17. package/bin/commands/start.js +46 -0
  18. package/bin/commands/uploads/cleanup.js +119 -0
  19. package/bin/jant.js +55 -15
  20. package/bin/lib/cli-api-token.js +6 -0
  21. package/bin/lib/d1-query.js +194 -0
  22. package/bin/lib/load-node-runtime.js +10 -0
  23. package/bin/lib/migration-artifacts.js +99 -0
  24. package/bin/lib/migration-rehearsal.js +438 -0
  25. package/bin/lib/migration-runner.js +311 -0
  26. package/bin/lib/node-database.js +48 -0
  27. package/bin/lib/node-sqlite.js +158 -0
  28. package/bin/lib/public-assets.js +66 -0
  29. package/bin/lib/r2-query.js +170 -0
  30. package/bin/lib/runtime-target.js +33 -0
  31. package/bin/lib/site-localize-media.js +427 -0
  32. package/bin/lib/site-media-parser.js +1150 -0
  33. package/bin/lib/site-selection.js +222 -0
  34. package/bin/lib/site-snapshot.js +484 -0
  35. package/bin/lib/site-url.js +84 -0
  36. package/bin/lib/sql-export.js +194 -0
  37. package/bin/lib/wrangler-cli.js +41 -0
  38. package/bin/lib/wrangler-config.js +86 -0
  39. package/dist/app-CAtsuLLh.js +30644 -0
  40. package/dist/client/_assets/chunks/0041f681602cc834bb5c55ced0155b8e-BNpHLJgt.woff2 +0 -0
  41. package/dist/client/_assets/chunks/008ea9091e332c639ceb18874eacd60c-CygB704Q.woff2 +0 -0
  42. package/dist/client/_assets/chunks/00c9ac960d866ffaf8a866e5939024e2-CqRrlxSi.woff2 +0 -0
  43. package/dist/client/_assets/chunks/02629e5d0a9860b7fe32ec1f0563213a-YvgSIv_V.woff2 +0 -0
  44. package/dist/client/_assets/chunks/02e48e353415a00e0f556608cab33a43-YFgu4zi3.woff2 +0 -0
  45. package/dist/client/_assets/chunks/02faf6bb0ab4d56ada037c0bbaf9b9f7-DUuUM2eo.woff2 +0 -0
  46. package/dist/client/_assets/chunks/031089da45fbfb7dc18ac827bef4c56e-D4ahCOTO.woff2 +0 -0
  47. package/dist/client/_assets/chunks/03ac785139320b7b13bac9c150bf72bf-BG7zubxs.woff2 +0 -0
  48. package/dist/client/_assets/chunks/053bd3d7aec0040d0cc50c261a1f4e3e-BdG4yI0n.woff2 +0 -0
  49. package/dist/client/_assets/chunks/057a3d44d7fc606f113d863376d0ecf0-BsEMbuKV.woff2 +0 -0
  50. package/dist/client/_assets/chunks/0713613227cc4c686c45a279f8bdc166-BzLoJcLo.woff2 +0 -0
  51. package/dist/client/_assets/chunks/0b1f83a3c7e715560a55ad9eb0fb1c94-asGrAhxW.woff2 +0 -0
  52. package/dist/client/_assets/chunks/0c055db157e7a13f3103cc2a6b67fec3-1T6huY_I.woff2 +0 -0
  53. package/dist/client/_assets/chunks/0d3f5cc265cb6c439c517f2c4cebbddf-CmkfpVHX.woff2 +0 -0
  54. package/dist/client/_assets/chunks/0e97f44ebc65384c346fe19bcc52fa20-BJTzmZzt.woff2 +0 -0
  55. package/dist/client/_assets/chunks/10f2b44b3711d3f5bdcc30d373b543d1-BRTxD_6-.woff2 +0 -0
  56. package/dist/client/_assets/chunks/1139d32ae2bdeb26c0c8f31330aa9a9f-B7k4Da0E.woff2 +0 -0
  57. package/dist/client/_assets/chunks/1259e5825b314fe2b8bb96d6e8069ee5-Ba8VoZCg.woff2 +0 -0
  58. package/dist/client/_assets/chunks/12c518ebfe62818af550c08947e359e7-B7eDp07N.woff2 +0 -0
  59. package/dist/client/_assets/chunks/145831a59caa06d894022fe60212ed21-B3xSIk4b.woff2 +0 -0
  60. package/dist/client/_assets/chunks/14b040a2dda256936bc7b3470e548394-CF7zgNWs.woff2 +0 -0
  61. package/dist/client/_assets/chunks/14c1506106d92621bafb11016735194e-cbk_paoP.woff2 +0 -0
  62. package/dist/client/_assets/chunks/154a2c266902003bd8b7449386b10776-CYgd0apt.woff2 +0 -0
  63. package/dist/client/_assets/chunks/15f8c0df47fd639d1b0d9bd5cf507c9b-B11bYwtn.woff2 +0 -0
  64. package/dist/client/_assets/chunks/1668bd859ffe15bed7d5563117d8d5fb-DJotYU-j.woff2 +0 -0
  65. package/dist/client/_assets/chunks/169a096e61d38a773216f51d1ec2cc06-CzS4CZjy.woff2 +0 -0
  66. package/dist/client/_assets/chunks/1884a2b22d314c7d57707f03aec348e0-BdBJ4_JO.woff2 +0 -0
  67. package/dist/client/_assets/chunks/189f272ea2600c74d576b7b15c014922-CylwUmhO.woff2 +0 -0
  68. package/dist/client/_assets/chunks/190e3f8632494e7c095117f26b1c811e-DT09KhZe.woff2 +0 -0
  69. package/dist/client/_assets/chunks/19ad151c22ce1befe0a9ea643fbee570-BOjbPzxL.woff2 +0 -0
  70. package/dist/client/_assets/chunks/19e39850472250bfdbce654d30859879-Db8MVQ-5.woff2 +0 -0
  71. package/dist/client/_assets/chunks/1b5d0d740450fb996749464c9b882025-BG0yepI2.woff2 +0 -0
  72. package/dist/client/_assets/chunks/1bca0a2a8840ad0ee9414940593db144-CfiRZ0Fs.woff2 +0 -0
  73. package/dist/client/_assets/chunks/1c820b5295868008ca7c78afa5b7655d-rGkaVY0Z.woff2 +0 -0
  74. package/dist/client/_assets/chunks/1cda27dcaab977ae4ef5d5ab2a10ae03-3n1CbajX.woff2 +0 -0
  75. package/dist/client/_assets/chunks/1e2640116bbba817f43c43cc69371cf1-BLnekPNy.woff2 +0 -0
  76. package/dist/client/_assets/chunks/1f4bc38a1c50f55f335f5411cae47696-3J1JFXeu.woff2 +0 -0
  77. package/dist/client/_assets/chunks/1fbccc182322b513f57cd156a9a491b0-bRVH28a5.woff2 +0 -0
  78. package/dist/client/_assets/chunks/1fbe225742c69f4ba9ea5f74922f0ca1-BHRIiKDB.woff2 +0 -0
  79. package/dist/client/_assets/chunks/2022cf097cb952d9fe75b53b4587d2c3-Btdq5ZEB.woff2 +0 -0
  80. package/dist/client/_assets/chunks/21e0a71d86be7b8a9e812e7af09dd061-SSZaQU5Z.woff2 +0 -0
  81. package/dist/client/_assets/chunks/2240a3c43ca5ef59ae3c348c7884792f-DQgT8QLe.woff2 +0 -0
  82. package/dist/client/_assets/chunks/2573703213da30d3ba18925b100b2c2b-D61uHyrJ.woff2 +0 -0
  83. package/dist/client/_assets/chunks/265e048cc9f2f8a711ba585a534d5351-geHDzSNf.woff2 +0 -0
  84. package/dist/client/_assets/chunks/26839c0e47c73514b8d8f660d24d6b19-D2gmzZl-.woff2 +0 -0
  85. package/dist/client/_assets/chunks/280e3d2b58e9ad3501816072e01b0c13-Dx5yDwjz.woff2 +0 -0
  86. package/dist/client/_assets/chunks/2874d07e228da9583b0e73646dacd498-26mUURU-.woff2 +0 -0
  87. package/dist/client/_assets/chunks/29d49891713a2785a3a383001cf58c59-DVb6JpZh.woff2 +0 -0
  88. package/dist/client/_assets/chunks/2a22e14a9ad53f2abb3c7e85017b7d12-CU_4APNf.woff2 +0 -0
  89. package/dist/client/_assets/chunks/2a7cedfcd6e4c7cec36f4fd7b0f329c2-BUp7BYdq.woff2 +0 -0
  90. package/dist/client/_assets/chunks/2acea04a920f6af31e7db97052f563c6-KdL2n7-Y.woff2 +0 -0
  91. package/dist/client/_assets/chunks/2ae2ca951489c9d50cde5b36a2a5515b-CWV12jwD.woff2 +0 -0
  92. package/dist/client/_assets/chunks/2b3e8c5703b91f39f6027f43f0da6f4b-FHYDvFqX.woff2 +0 -0
  93. package/dist/client/_assets/chunks/2ba802b14f21a58fc61606c88fa93373-BmrW6szo.woff2 +0 -0
  94. package/dist/client/_assets/chunks/2d81eb6ab0ebbc0cabfb3a3341ba8800-Ds2YcMj0.woff2 +0 -0
  95. package/dist/client/_assets/chunks/2deb444546774c3a3ab38c75eb69cdfb-Dm9USQXk.woff2 +0 -0
  96. package/dist/client/_assets/chunks/2e98b666924b8e0a09d1aeeefd24bdd2-Cmh3DrKG.woff2 +0 -0
  97. package/dist/client/_assets/chunks/2f27ee4fb2cf6a280e110e09c18ef73e-D_yYWYMZ.woff2 +0 -0
  98. package/dist/client/_assets/chunks/2fbccf9a3853eb59db1a825e044515fd-CiShE3Wo.woff2 +0 -0
  99. package/dist/client/_assets/chunks/2fd3fceb6faed5e3db768e88d7614dca-D74stxc-.woff2 +0 -0
  100. package/dist/client/_assets/chunks/2ff009fa8701505d7f3dc6c83763f019-nkOuUfA-.woff2 +0 -0
  101. package/dist/client/_assets/chunks/31342cebfa5ea7fac06b4ea372d96bc5-DdIH_TkU.woff2 +0 -0
  102. package/dist/client/_assets/chunks/31424fe5d54692e7c8b38021ccb8597c-CfNu45zJ.woff2 +0 -0
  103. package/dist/client/_assets/chunks/3179006d1c7ebfa50d27482a2859d9a0-e7PNUH9y.woff2 +0 -0
  104. package/dist/client/_assets/chunks/34c2edb3c37f71258f5c4a31091f0c6c-Cyr8VjPf.woff2 +0 -0
  105. package/dist/client/_assets/chunks/35cf5dd04315e0b906e1a413d7905a2f-A-QPUrVX.woff2 +0 -0
  106. package/dist/client/_assets/chunks/360c190344c26278bbc50e2f4d6a2b3f-B7OhzvgR.woff2 +0 -0
  107. package/dist/client/_assets/chunks/375329ba0b50b94b35006498e555867c-CDc5XB7_.woff2 +0 -0
  108. package/dist/client/_assets/chunks/387c811226f303af62f1e21aae6f5c83-BmPYUAdb.woff2 +0 -0
  109. package/dist/client/_assets/chunks/389a950f2a1211946d294716e679e381-Diuxk8JN.woff2 +0 -0
  110. package/dist/client/_assets/chunks/39b0bbc910af9d2d6dcd8bd4abd6387d-NhvZf71d.woff2 +0 -0
  111. package/dist/client/_assets/chunks/3adc8f6350cf5067bcb6dc5e44c45d41-DIO3oy0b.woff2 +0 -0
  112. package/dist/client/_assets/chunks/3b41385fc27419c19822060daa0b5cb3-Dh5JVnsu.woff2 +0 -0
  113. package/dist/client/_assets/chunks/3bed5bd57de8f738e53cddaea88983d9-BAGVbLtM.woff2 +0 -0
  114. package/dist/client/_assets/chunks/3c201fd8d1bb20abe7d06b940e83a4d9-Bz0dCLvM.woff2 +0 -0
  115. package/dist/client/_assets/chunks/3cbe4a697fd595ef42c899de7d3e5445-zWwFRRI_.woff2 +0 -0
  116. package/dist/client/_assets/chunks/3d385ea0880df7204258e290648ec012-BQqoFH8R.woff2 +0 -0
  117. package/dist/client/_assets/chunks/3d83dacbbec3d8532ae9afede21f3aab-BH06SEJj.woff2 +0 -0
  118. package/dist/client/_assets/chunks/3dc1a4f0d7af59e16b5162a2b077a442-CkXZW3CY.woff2 +0 -0
  119. package/dist/client/_assets/chunks/3dc68e473fe23bd076dd46785cd23583-Cv6HZ0Dr.woff2 +0 -0
  120. package/dist/client/_assets/chunks/427577dcb707d1d35eebd155b4222aa7-D7-mHA6o.woff2 +0 -0
  121. package/dist/client/_assets/chunks/42a74b6a625bbf0a9616ed4db3152c88-CQ58KTMS.woff2 +0 -0
  122. package/dist/client/_assets/chunks/435b7dca567809813fcb395a27ed83a0-C6E3YXBr.woff2 +0 -0
  123. package/dist/client/_assets/chunks/43693195e775d515689fa035394067fd-DLMrklI6.woff2 +0 -0
  124. package/dist/client/_assets/chunks/43fb49e5b79ee7e553869d84e6e08b1e-BgOVrd59.woff2 +0 -0
  125. package/dist/client/_assets/chunks/44dcbbc3cc8f22e613b342d691511ab6-dRyYwGGi.woff2 +0 -0
  126. package/dist/client/_assets/chunks/450a5b53be0a8a778bb0b623e86b652f-qwII2gXW.woff2 +0 -0
  127. package/dist/client/_assets/chunks/457485e72835364662dfead6281638c1-Mv279UyL.woff2 +0 -0
  128. package/dist/client/_assets/chunks/47479c470fae70f10b7c964a7ecbf274-D9VNBclD.woff2 +0 -0
  129. package/dist/client/_assets/chunks/474fac21b12b7efd71f7c321578878b0-BJRz_Yd3.woff2 +0 -0
  130. package/dist/client/_assets/chunks/477866c8396474a17317dcac3e7a014f-e90LWIFy.woff2 +0 -0
  131. package/dist/client/_assets/chunks/478ebdaadda7775c391c5dcab4e697df-RfXJJ1gl.woff2 +0 -0
  132. package/dist/client/_assets/chunks/488846410760fe128dae939836ca5423-D0YbXQOX.woff2 +0 -0
  133. package/dist/client/_assets/chunks/48d6a97a185c799be4fe67aaf7edf213-CqLkiJik.woff2 +0 -0
  134. package/dist/client/_assets/chunks/48eb0a91e50c7f026e248c64145e72af-D8LB2Dye.woff2 +0 -0
  135. package/dist/client/_assets/chunks/490edb9fc8a4356aea556eed32287464-Bxk-XtyT.woff2 +0 -0
  136. package/dist/client/_assets/chunks/4a23fe6e82fd496b5eb20401b6164efe-CfhsfLd6.woff2 +0 -0
  137. package/dist/client/_assets/chunks/4b0e79ba18b2ce424fa93e84996d7cba-B0dS4UvF.woff2 +0 -0
  138. package/dist/client/_assets/chunks/4bc743968cf1c3ce5711de67ef1ccc4d-Dmn20LlT.woff2 +0 -0
  139. package/dist/client/_assets/chunks/4c4bdd0b3f3a52e28f3b643c1c5d43be-BLqyuzyK.woff2 +0 -0
  140. package/dist/client/_assets/chunks/4c96411f3693a9a8657a9c1190f82bce-BhP7AS2j.woff2 +0 -0
  141. package/dist/client/_assets/chunks/4c9aa12aba2a6a57410eacaff7427916-CLDZgzM7.woff2 +0 -0
  142. package/dist/client/_assets/chunks/4cca7233bf8ce5dec2e5d146b993d626-Bf7OgJe6.woff2 +0 -0
  143. package/dist/client/_assets/chunks/4cf0f292f3358bd2f73b1cf4ec1476f3-CIfjw85D.woff2 +0 -0
  144. package/dist/client/_assets/chunks/4d0a9128d06ea857f203bf5d007b1ab9-BZ-BuqS9.woff2 +0 -0
  145. package/dist/client/_assets/chunks/4dc0728df0f2ba70796f45f05654c7ba-DGd3NDaT.woff2 +0 -0
  146. package/dist/client/_assets/chunks/4dc2bc2c55b47f57d13b63aa6b1c8bd4-g3WbclK2.woff2 +0 -0
  147. package/dist/client/_assets/chunks/4e1cc6aafb411b572c8d3511e925ecf1-BUf15jYj.woff2 +0 -0
  148. package/dist/client/_assets/chunks/4e5384920bbb155d9d8d74887b09ea5b-2KLteQXT.woff2 +0 -0
  149. package/dist/client/_assets/chunks/501f66f24bce8234441954de1b568403-CIe4N6pO.woff2 +0 -0
  150. package/dist/client/_assets/chunks/50cfd672bfa62512ba090420acf35c87-CibXrwCT.woff2 +0 -0
  151. package/dist/client/_assets/chunks/5227dbe9933760a48baff21ebd13fc98-v8CBITMX.woff2 +0 -0
  152. package/dist/client/_assets/chunks/526b263e72c189f4b065738aaa6d423a-Dj5Ns6Ev.woff2 +0 -0
  153. package/dist/client/_assets/chunks/53a88404451448cd2e620a0ca0e45a20-CI0P5NCv.woff2 +0 -0
  154. package/dist/client/_assets/chunks/54da934819a917f561b439bfd10f88b6-CpoNRhz7.woff2 +0 -0
  155. package/dist/client/_assets/chunks/54e301f412730f391225db59dae1c8d5-CxTAz9F1.woff2 +0 -0
  156. package/dist/client/_assets/chunks/551b1d7a0b80c8d42af09863cdca7f01-BIGKH8GS.woff2 +0 -0
  157. package/dist/client/_assets/chunks/555d990ab3fd7d3d66c6d1fa9a82fec5-BndAe6f2.woff2 +0 -0
  158. package/dist/client/_assets/chunks/557cd00c5d6827e13d72a0c71b23587b-C6gpr-g-.woff2 +0 -0
  159. package/dist/client/_assets/chunks/563fa31542d553f25abab65cf7f81e1d-Cl_1me5X.woff2 +0 -0
  160. package/dist/client/_assets/chunks/56e1c4734bbbb38af2fbc262bf6e98f2-DZ32amUx.woff2 +0 -0
  161. package/dist/client/_assets/chunks/5947f5da5da9a352a2b534ee64bfc29a-DT0Fj-SL.woff2 +0 -0
  162. package/dist/client/_assets/chunks/5979c33a7eb5963bf8e83e46931b5fb5--4f9yv1z.woff2 +0 -0
  163. package/dist/client/_assets/chunks/597d69d0710e0178b162afb0a0c20401-CpThXFdo.woff2 +0 -0
  164. package/dist/client/_assets/chunks/59966ee0b069b577510fe68c350da0ee-Bs_HDvyP.woff2 +0 -0
  165. package/dist/client/_assets/chunks/5a10741e41259e235841440394c0763d-uCZGOg0A.woff2 +0 -0
  166. package/dist/client/_assets/chunks/5bfc7a121c35ae42623ef804fb525e0e-mS7vyFFz.woff2 +0 -0
  167. package/dist/client/_assets/chunks/5cc23a76e122d0ad2f7cede41bc35b27-4T1ZVQJu.woff2 +0 -0
  168. package/dist/client/_assets/chunks/5d48855bed5f3554eff91b573d7376ac-CPzsdNDF.woff2 +0 -0
  169. package/dist/client/_assets/chunks/5ddcbe564b29ef08632e1aeb33455435-CQzDKa-z.woff2 +0 -0
  170. package/dist/client/_assets/chunks/5f90024544c2907c6c0203c6210c50be-C_1uxIky.woff2 +0 -0
  171. package/dist/client/_assets/chunks/605667a998e91e2b6a4a3cd7c31fe5a9-Co2lOjER.woff2 +0 -0
  172. package/dist/client/_assets/chunks/60a14064ed334f0155795d795e926abe-C2HuVyw5.woff2 +0 -0
  173. package/dist/client/_assets/chunks/60d8b0805a0a8c54a6cca216004beff5-CV1GX5br.woff2 +0 -0
  174. package/dist/client/_assets/chunks/611b62d5fd9698d9b5ce495ba6f14c93-CPi2xRKF.woff2 +0 -0
  175. package/dist/client/_assets/chunks/61bf4287453da4025d03fa6b2dba66ca-CMh4XzzB.woff2 +0 -0
  176. package/dist/client/_assets/chunks/638369541268ed5a10af97ad77498c73-DrpvGu-L.woff2 +0 -0
  177. package/dist/client/_assets/chunks/649b12d7cee7bb981842946e4547e6ca-EACt8KkC.woff2 +0 -0
  178. package/dist/client/_assets/chunks/653bef2ed891ae48d8ed712283080649-C3d1ZTAc.woff2 +0 -0
  179. package/dist/client/_assets/chunks/67d2a81f06ba352f17fbdc3a5e6ea59e-DC8Iostn.woff2 +0 -0
  180. package/dist/client/_assets/chunks/67f32ceea9e78e5109f87724ad886010-C_3-9sSn.woff2 +0 -0
  181. package/dist/client/_assets/chunks/68304f3229cf763465f044fccb5892c0-CTvPlN8c.woff2 +0 -0
  182. package/dist/client/_assets/chunks/687d0f0f90a9b23e40102e16ad8e9836-Bzlx1W-7.woff2 +0 -0
  183. package/dist/client/_assets/chunks/688a88911e4da17b609196a959b8b930-BgwP4zIQ.woff2 +0 -0
  184. package/dist/client/_assets/chunks/68f2fab82ec8e9291f08c3145111549c-CXqagiiH.woff2 +0 -0
  185. package/dist/client/_assets/chunks/69519ada3f3f74ca20aacb8af48ab6b4-DncDOElk.woff2 +0 -0
  186. package/dist/client/_assets/chunks/6a6e884fb2b65ec5b4a3d5ecd0d01a6a-DBNJo4jq.woff2 +0 -0
  187. package/dist/client/_assets/chunks/6db6ddf72c38a78ce44c1327701152e1-CFUWMaQQ.woff2 +0 -0
  188. package/dist/client/_assets/chunks/6e1a8b45b01939088c3a8cfcf8c10681-BemqZSaI.woff2 +0 -0
  189. package/dist/client/_assets/chunks/6e2164fad867d166de2e5b274f04a563-_YD_QMym.woff2 +0 -0
  190. package/dist/client/_assets/chunks/6e83fe0b6e708eaf1c3003d6dee11488-CRqY8z2g.woff2 +0 -0
  191. package/dist/client/_assets/chunks/6eefc9d430171c1e1e4034ecadee31c8-d3VCnQ01.woff2 +0 -0
  192. package/dist/client/_assets/chunks/70861376e5d4f92f8aa7aa1b2749b617-BqwxmSo2.woff2 +0 -0
  193. package/dist/client/_assets/chunks/70adaf50c56d5ff859c64d35e0f1e34e-BCmTRExx.woff2 +0 -0
  194. package/dist/client/_assets/chunks/7124d150570d39ced8d45507dc11ca1e-Co_mUkqP.woff2 +0 -0
  195. package/dist/client/_assets/chunks/71eafb8fbe3a734283517e230ad8b6db-CFmZ-qAS.woff2 +0 -0
  196. package/dist/client/_assets/chunks/72ee453ac0e19bd2c631c8921c44e3de-CEidyyfI.woff2 +0 -0
  197. package/dist/client/_assets/chunks/751f54dbb115140d5b645a6ba4aff5d3-DY-S96zI.woff2 +0 -0
  198. package/dist/client/_assets/chunks/753b5f6fb254bacb6618ace25af3df60-uBG6c32o.woff2 +0 -0
  199. package/dist/client/_assets/chunks/7609e7e74dd4d916a7abc7ecc7d95f7e-CzrNhght.woff2 +0 -0
  200. package/dist/client/_assets/chunks/76b9d6fe838ae4151d95ce7200aa2bf6-BfQWDykU.woff2 +0 -0
  201. package/dist/client/_assets/chunks/76d4244186d118eea245d1385a4de2ec-CEoql1pw.woff2 +0 -0
  202. package/dist/client/_assets/chunks/77a7533bd21ccd33192d142a93555aa8-BzSb8ker.woff2 +0 -0
  203. package/dist/client/_assets/chunks/78ce29fed872e44fc9014d94875d2aac-D1BDuJXp.woff2 +0 -0
  204. package/dist/client/_assets/chunks/79a7fdf7d9c722b5723ae25e6ff8e203-5rPAnIr7.woff2 +0 -0
  205. package/dist/client/_assets/chunks/79a85a253e9b3f12d2e2cb15e16b3003-DTjWGv2X.woff2 +0 -0
  206. package/dist/client/_assets/chunks/7a86b155111ba20f3e87306ff6beac77-DchSNxJ1.woff2 +0 -0
  207. package/dist/client/_assets/chunks/7b1e76975b0984e6f83e3f9f8069e784-OB_rGzsR.woff2 +0 -0
  208. package/dist/client/_assets/chunks/7b6c60131822a0e4d36d980d52509d4e-8dQVuUiW.woff2 +0 -0
  209. package/dist/client/_assets/chunks/7caa14a095a6bc313aab780fe4ff7999-BJ-80Ois.woff2 +0 -0
  210. package/dist/client/_assets/chunks/7cbda564cb2dd4799ab9e89d51286aa7-MlT7YRSz.woff2 +0 -0
  211. package/dist/client/_assets/chunks/7d138084cf03c14116b11297fce0e3e3-CdWCcyam.woff2 +0 -0
  212. package/dist/client/_assets/chunks/7d65a3d6a65050eb5e6eca43398aeba4-CYB359u-.woff2 +0 -0
  213. package/dist/client/_assets/chunks/7dfc711962c8771f97e7c8898a6bcb65-Yv69yrg4.woff2 +0 -0
  214. package/dist/client/_assets/chunks/7ef123b62d530fcba73974fa265e0aae-CD7IMlBc.woff2 +0 -0
  215. package/dist/client/_assets/chunks/7f60eefa15956d6f06dd92404887d58c-LqioDjlc.woff2 +0 -0
  216. package/dist/client/_assets/chunks/7f8c15e0ecb102738981d9fa4cb6b921-8qiw7Fmn.woff2 +0 -0
  217. package/dist/client/_assets/chunks/80466082a896fd328f30a78593c7c568-BMAVDyAy.woff2 +0 -0
  218. package/dist/client/_assets/chunks/812b5a4b87f3a7b4afc1cfebc864f413-B4_mWnLK.woff2 +0 -0
  219. package/dist/client/_assets/chunks/812dfb7f8144d01b3cc9d5ce0b472f40-DxhfKDhh.woff2 +0 -0
  220. package/dist/client/_assets/chunks/84742b1ede4f0bb6d27131298eba21b4-4_QYQ-wj.woff2 +0 -0
  221. package/dist/client/_assets/chunks/8555f0285e3d28e95e2fc0ccccd9caff-CEnfoaRy.woff2 +0 -0
  222. package/dist/client/_assets/chunks/880162ae92cd9e120eb4e4e11fae459d-Cj61LKOn.woff2 +0 -0
  223. package/dist/client/_assets/chunks/8a3c84b0df36f851f5fea75ee8757951-CfHrA-x8.woff2 +0 -0
  224. package/dist/client/_assets/chunks/8b0c8c9f8cfa9fa090d97c5a5efb1f4c-DZc3EGF4.woff2 +0 -0
  225. package/dist/client/_assets/chunks/8c8393bc875f1ee36697a2113f4421ea-CnBugeCf.woff2 +0 -0
  226. package/dist/client/_assets/chunks/8dc035a34c76e6515ca203e2df182588-B1reQPCB.woff2 +0 -0
  227. package/dist/client/_assets/chunks/8e04e64c8f68d292a18d4160fbde8671-DkT7v-sd.woff2 +0 -0
  228. package/dist/client/_assets/chunks/8e6c9bb43afb8cbbff7cf1055e67c9bd-c7wzPI7v.woff2 +0 -0
  229. package/dist/client/_assets/chunks/8eb06109812cb80be44f47b8179c2709-DOaU0xD8.woff2 +0 -0
  230. package/dist/client/_assets/chunks/8f2b960c2823670e94f5b08aa65657e6-CHT-Gunr.woff2 +0 -0
  231. package/dist/client/_assets/chunks/8f89f57230d184f92a36e241874229d7-D6P0FMCn.woff2 +0 -0
  232. package/dist/client/_assets/chunks/904324af375d5fd370af1054355a050e-CHiXmgO7.woff2 +0 -0
  233. package/dist/client/_assets/chunks/90ac4f9d2aa02afdace2843b49fc18bb-CWArmWzF.woff2 +0 -0
  234. package/dist/client/_assets/chunks/90b6f57d77847f512fd11db74fa912f1-DzYbq8hW.woff2 +0 -0
  235. package/dist/client/_assets/chunks/911a2092d64d6d6494b254d819af2b91-C0xmZ-Cu.woff2 +0 -0
  236. package/dist/client/_assets/chunks/913759e6690f9fc0746a20b96f4bdcb4-B0k9hqx0.woff2 +0 -0
  237. package/dist/client/_assets/chunks/9154e26efe532a85a27d80902f5a2d6c-DJQmzPmB.woff2 +0 -0
  238. package/dist/client/_assets/chunks/9180de34b48b325200a97e267befff32-ClJx20hD.woff2 +0 -0
  239. package/dist/client/_assets/chunks/94e7ed67f1557b76fead6b6e456a0415-CjAhhvRn.woff2 +0 -0
  240. package/dist/client/_assets/chunks/95127a92346c04fec1fa81d6295b0a28-DNbeXPGm.woff2 +0 -0
  241. package/dist/client/_assets/chunks/958efb9b2fa2ea0008ffef009885f9f8-CNOwBDS1.woff2 +0 -0
  242. package/dist/client/_assets/chunks/95df3b9f681d9df411c30aea5b24f2e0-Dv7GHFVj.woff2 +0 -0
  243. package/dist/client/_assets/chunks/975af5a496e8d87d821910aa9fe4d598-CG5KDMOI.woff2 +0 -0
  244. package/dist/client/_assets/chunks/97a874bbf55ce89a4ab7cd27c7e938b1-fhost-bL.woff2 +0 -0
  245. package/dist/client/_assets/chunks/9ca9b71010a5faeee7047ef97aeee13b-K6NP7P3o.woff2 +0 -0
  246. package/dist/client/_assets/chunks/9cd0b77920b9d6c64eb686493123fc76-Db6wX5BE.woff2 +0 -0
  247. package/dist/client/_assets/chunks/9de02d745b8e25c6411fb152fb067748-CGxZtq8G.woff2 +0 -0
  248. package/dist/client/_assets/chunks/9eb33a430058d839ebbe2af4b2e0daa9-Cjd-WSds.woff2 +0 -0
  249. package/dist/client/_assets/chunks/9ebd27835ffcbd794e67151ab046ce68-DmxtZVr5.woff2 +0 -0
  250. package/dist/client/_assets/chunks/9f5a73aa8ba417688019d628f334db07-P03uxox_.woff2 +0 -0
  251. package/dist/client/_assets/chunks/9fbc06b2e3ff16b9d705c76db563ef17-XDHSv5Te.woff2 +0 -0
  252. package/dist/client/_assets/chunks/9fd53607094e329fa8e5c785b3ff0f1a-B-9qgOdM.woff2 +0 -0
  253. package/dist/client/_assets/chunks/a077f51cfb5cffb4ff4d8e229c0e9e79-BaPgyZ5H.woff2 +0 -0
  254. package/dist/client/_assets/chunks/a0f0c06d5c7a3ffa97706178cce212a8-DN_NE4Ic.woff2 +0 -0
  255. package/dist/client/_assets/chunks/a38c1830367f784181b6f544b0b11bbd-BmPGDXn1.woff2 +0 -0
  256. package/dist/client/_assets/chunks/a397997b579d3945c9c70a979c17a8ad-DXa54wGz.woff2 +0 -0
  257. package/dist/client/_assets/chunks/a3b929542e6c5a0644b73a7c8a8b6c03-B5i2p-Hj.woff2 +0 -0
  258. package/dist/client/_assets/chunks/a578742770fcd2226e3c45b5b6efdcb0-C1QhQdHg.woff2 +0 -0
  259. package/dist/client/_assets/chunks/a68d9d5027803832bb28e78cdcd04949-D_bwWhwg.woff2 +0 -0
  260. package/dist/client/_assets/chunks/a8857f5d478f101c053ba02d2f223e90-CHOaRPpq.woff2 +0 -0
  261. package/dist/client/_assets/chunks/a904b05966368bcf90b632c7c2e5f76b-BbTXYQVh.woff2 +0 -0
  262. package/dist/client/_assets/chunks/a9cf85e27428c14351d30eac8cbc8d91-DMaVc3-8.woff2 +0 -0
  263. package/dist/client/_assets/chunks/aa0ce6740f301351761a0615cc8b2e99-4RL3pitV.woff2 +0 -0
  264. package/dist/client/_assets/chunks/aa218a2c45f3749537ce876201e5152b-DndHqA-p.woff2 +0 -0
  265. package/dist/client/_assets/chunks/aa28db16818f9eaa8c817f289e1c3270-Drm1Jwhe.woff2 +0 -0
  266. package/dist/client/_assets/chunks/aa64c9953af43ca65832f413895bb433-CR32RteD.woff2 +0 -0
  267. package/dist/client/_assets/chunks/aa96d698491c2540e2dcf7009c65c456-BXlPVR0v.woff2 +0 -0
  268. package/dist/client/_assets/chunks/ada8f0241244c60ec8d3d59ad37f20a5-BFQsznEm.woff2 +0 -0
  269. package/dist/client/_assets/chunks/ae25c41034ddc1a9e0b41f5034c9aa4b-COQJkHOc.woff2 +0 -0
  270. package/dist/client/_assets/chunks/ae289ae3f8cdb54a3a6c07174517afec-DFGIoTY6.woff2 +0 -0
  271. package/dist/client/_assets/chunks/ae401fb4db80d5ff5cd3f8d9bc811070-Byt9AwoT.woff2 +0 -0
  272. package/dist/client/_assets/chunks/b02dfa2aa52cbdb1b2f11a9f44335469-DVWsuPSL.woff2 +0 -0
  273. package/dist/client/_assets/chunks/b0ab3a7f319ce6dd3c9a4de2674e7c72-CksmA6Hr.woff2 +0 -0
  274. package/dist/client/_assets/chunks/b159deb135e9946eea0572d52778170b-BZrtOTn0.woff2 +0 -0
  275. package/dist/client/_assets/chunks/b2e326f7f9b807451bf9c745df747efe-BvlONCPB.woff2 +0 -0
  276. package/dist/client/_assets/chunks/b341de0bc0bfe194a6c28dcfb566029e-DJAj3r6U.woff2 +0 -0
  277. package/dist/client/_assets/chunks/b846c293981ca5429eabaa967f222f26-GS-TAaO6.woff2 +0 -0
  278. package/dist/client/_assets/chunks/b91450304d9ac44f5c6e0da0792e055d-vS0S7kl0.woff2 +0 -0
  279. package/dist/client/_assets/chunks/baa325551b381c5e035ef143e56d4abe-DJg3GhKJ.woff2 +0 -0
  280. package/dist/client/_assets/chunks/bc3f0cb8b55ee11d32b94ca488976f8d-Dp5KZMSD.woff2 +0 -0
  281. package/dist/client/_assets/chunks/bcb3307527d6d0033bf0f17660b91e71-BMgRW_eW.woff2 +0 -0
  282. package/dist/client/_assets/chunks/be64f9379412876e00fd3a0bfa6b6fe9-BJ3FMWFM.woff2 +0 -0
  283. package/dist/client/_assets/chunks/befed8a4fa817773fa7109db6fe07f56-3sAmDsOO.woff2 +0 -0
  284. package/dist/client/_assets/chunks/bf1acc86e17b4229c548828a9d6f455d-JNuPCGDM.woff2 +0 -0
  285. package/dist/client/_assets/chunks/c09ee2b219982f8d46ad9968b7e6e0ba-qVb8yJHb.woff2 +0 -0
  286. package/dist/client/_assets/chunks/c0c7836749e585cee24ab2f8457c5b01-Ds0003oT.woff2 +0 -0
  287. package/dist/client/_assets/chunks/c2ac4ef1860812036ca2b8c4e4089bdc-DsziO8Du.woff2 +0 -0
  288. package/dist/client/_assets/chunks/c31019c08bd22464f7a88f090281404c-CUC0jMW1.woff2 +0 -0
  289. package/dist/client/_assets/chunks/c33c59feccf391f0c5f1f5d24e36d1fe-BXTVggad.woff2 +0 -0
  290. package/dist/client/_assets/chunks/c39ec937c6a8d124e8b68cf829ea5ad4-CarrkdmQ.woff2 +0 -0
  291. package/dist/client/_assets/chunks/c3fbc1f2557c343863a10698f8c966a2-BiOC1yo2.woff2 +0 -0
  292. package/dist/client/_assets/chunks/c3fd21315345ae541f6e98067059fa19-rHtO7vK5.woff2 +0 -0
  293. package/dist/client/_assets/chunks/c568a16e3168ceb1f191b70022c492ea-BpAiyU80.woff2 +0 -0
  294. package/dist/client/_assets/chunks/c57ee3b49b7e45b995539a6b2c51f138-IHDXeVRU.woff2 +0 -0
  295. package/dist/client/_assets/chunks/c5c1c0be944ea39a3f50a02d32f5b759-DAwbrvTL.woff2 +0 -0
  296. package/dist/client/_assets/chunks/c5e66d60be3375835bbd8d6b797f6eac-DWp4tpvN.woff2 +0 -0
  297. package/dist/client/_assets/chunks/c5f1075caf6d1344ee720de85114a521-DTTbeLhd.woff2 +0 -0
  298. package/dist/client/_assets/chunks/c82fd9456d7465b5e5bd3659e9b14c55-Cs6h6hE6.woff2 +0 -0
  299. package/dist/client/_assets/chunks/c90b7b65d2b9696fbf3a506738f94d68-DsvgiuQQ.woff2 +0 -0
  300. package/dist/client/_assets/chunks/cae29b3f8951eaf20d2f61c2206e28d9-CoHXmhmG.woff2 +0 -0
  301. package/dist/client/_assets/chunks/cba6ad3981cb7861428d4be169ee8124-CBdFsPAF.woff2 +0 -0
  302. package/dist/client/_assets/chunks/cc26525aa2af1f0b929af32ce50a7fba-D55LVtW9.woff2 +0 -0
  303. package/dist/client/_assets/chunks/cd0e7b51eddb22a77a09b025c0281434-ByI_TSTq.woff2 +0 -0
  304. package/dist/client/_assets/chunks/cd10a3af2133805d8c92104d1ee6ff18-Dtr5mBXe.woff2 +0 -0
  305. package/dist/client/_assets/chunks/cd6d074f3957d58bac58437fc97e5e33-CrVZuSbw.woff2 +0 -0
  306. package/dist/client/_assets/chunks/cd75ca47da9ae4c0899e37d4c543319b-DXU1s4Ja.woff2 +0 -0
  307. package/dist/client/_assets/chunks/cef249b6d013fb0cc0d574176bc23811-BL7WfNHA.woff2 +0 -0
  308. package/dist/client/_assets/chunks/d043b8d7a48bb0ac59ee1f1477d88eee-d5CwY2I8.woff2 +0 -0
  309. package/dist/client/_assets/chunks/d0bd387fda28e58d3c9b3efa2468dd8a-CjDFowFS.woff2 +0 -0
  310. package/dist/client/_assets/chunks/d12ce1d8445213317f9163283e58a05d-4RtGQnak.woff2 +0 -0
  311. package/dist/client/_assets/chunks/d15a3317942b7d31978a759fbf2222c8-DQ9QFQdS.woff2 +0 -0
  312. package/dist/client/_assets/chunks/d28fb13acf9ced9f0657fd4012c81cd2-D8Bgk6sd.woff2 +0 -0
  313. package/dist/client/_assets/chunks/d320b000b5978c7251148a6a154741b8-CcGZjeFx.woff2 +0 -0
  314. package/dist/client/_assets/chunks/d3714e6b90de8e2085dfb2514464dd6a-8LsVAcmb.woff2 +0 -0
  315. package/dist/client/_assets/chunks/d3beff96216c8af1aa79246476b6a323-hQysoMK3.woff2 +0 -0
  316. package/dist/client/_assets/chunks/d3e311f30c811dc339c262a79a51877e-CkUTbS74.woff2 +0 -0
  317. package/dist/client/_assets/chunks/d51f4cdc83711e510f5d25f03235597e-BY3VAGvr.woff2 +0 -0
  318. package/dist/client/_assets/chunks/d5df4a8dfd4328c67d933b3912c6ad0f-A2fccswC.woff2 +0 -0
  319. package/dist/client/_assets/chunks/d740dc2e854aaa7b3dcdd3ed25455eeb-Cw8LrB06.woff2 +0 -0
  320. package/dist/client/_assets/chunks/d8325ba7ae651bc30440905bd67b95f1-Dg8M_ijr.woff2 +0 -0
  321. package/dist/client/_assets/chunks/da13b136efb1d1e4c76575af8f79a698-BbfzLGVB.woff2 +0 -0
  322. package/dist/client/_assets/chunks/da2cf0ec56bf69374ee37764c7e3ea3d-fyVBKE2g.woff2 +0 -0
  323. package/dist/client/_assets/chunks/da93ae099ff3b7aae27b3f674d3fc721-BDG5ywtd.woff2 +0 -0
  324. package/dist/client/_assets/chunks/daf62255dd60679946f28c442ca62533-BnIjYSn3.woff2 +0 -0
  325. package/dist/client/_assets/chunks/dccda6a2e2db3b530788bdfa2acd1979-ByL7eZcn.woff2 +0 -0
  326. package/dist/client/_assets/chunks/dd01a1035345f6921a48525b8ce08f06-vkP6t4fC.woff2 +0 -0
  327. package/dist/client/_assets/chunks/e2204cf85edcb96c5de5c3dcf240da9d-Ch3zKjD-.woff2 +0 -0
  328. package/dist/client/_assets/chunks/e264213b9e102dabc603adb6e4fda5e6-CyxmQBHB.woff2 +0 -0
  329. package/dist/client/_assets/chunks/e3e913e145ddcd9323b2a0972967feb6-3sjEdzck.woff2 +0 -0
  330. package/dist/client/_assets/chunks/e4fb59479cedc87ba79785590bf861ca-sx7RZ8Vu.woff2 +0 -0
  331. package/dist/client/_assets/chunks/e5d00355f73293d40b61299459d17ca5-D8Vt3D-z.woff2 +0 -0
  332. package/dist/client/_assets/chunks/e647b8d2efc501c0cc0e407249cc7135-BrSW9DU4.woff2 +0 -0
  333. package/dist/client/_assets/chunks/e6e60ffb2ebd1828628764b507060aea-Dqyf48QC.woff2 +0 -0
  334. package/dist/client/_assets/chunks/e7c7ef3669ae48c0a736f06ca471e1d7-DNCebEB-.woff2 +0 -0
  335. package/dist/client/_assets/chunks/e81a742cacef744130c40de1b90837d8-DtAIFBk_.woff2 +0 -0
  336. package/dist/client/_assets/chunks/e8b755172122d1d0a5dd453e96b0ff24-BNTSyPYU.woff2 +0 -0
  337. package/dist/client/_assets/chunks/e99280299c305402eaa5271a3e36c49b-BpqXwl7U.woff2 +0 -0
  338. package/dist/client/_assets/chunks/e9c66b085052ece66bfadf45f711d3e1-DVza3Uob.woff2 +0 -0
  339. package/dist/client/_assets/chunks/ed7c6dafaa6d8bcf015ef0ca574837df-C7bcndJl.woff2 +0 -0
  340. package/dist/client/_assets/chunks/f00eb499abb94fa7b799d6d8c9b050e9-D_XvVTI9.woff2 +0 -0
  341. package/dist/client/_assets/chunks/f2900a1d30c3a33129f4e2225669bd0e-in84LilX.woff2 +0 -0
  342. package/dist/client/_assets/chunks/f2fb1f1fbf7e44afb53c672ec286a22e-tFk0sTQc.woff2 +0 -0
  343. package/dist/client/_assets/chunks/f372129c60aaece937cf7b91ee75c9b8-DiP_JYOt.woff2 +0 -0
  344. package/dist/client/_assets/chunks/f5237486197aeff59341a1ff38b8eff8-D_uuqrNZ.woff2 +0 -0
  345. package/dist/client/_assets/chunks/f553d54ef931066712d8f3f0ce018e1b-D8K983U5.woff2 +0 -0
  346. package/dist/client/_assets/chunks/f5d7487963d43c89da63aaf10f2e6fb7-B7GIrfPU.woff2 +0 -0
  347. package/dist/client/_assets/chunks/f649cba8e14c33d6bf2265483b14b895-Nm5wSov8.woff2 +0 -0
  348. package/dist/client/_assets/chunks/f6839df1bf7cb4dc8d27e5ea55bbe633-BIjWKN84.woff2 +0 -0
  349. package/dist/client/_assets/chunks/f6b7304e028980f77a7f7007bb540abd-CIlBt6sf.woff2 +0 -0
  350. package/dist/client/_assets/chunks/f75496953a40ff241178240209f56990-MOSKd67d.woff2 +0 -0
  351. package/dist/client/_assets/chunks/f7d36ffff7a75c9c6216d576a57dd00d-CaXQA5A8.woff2 +0 -0
  352. package/dist/client/_assets/chunks/f7f3f63e7a149cd89eccab3b52171d05-oNOOSdDV.woff2 +0 -0
  353. package/dist/client/_assets/chunks/f8db8bef0a6e1178835d350ae0d384a1-ZAiKM369.woff2 +0 -0
  354. package/dist/client/_assets/chunks/f92d74d1d217d21b39075ff23f79f7fd-BlTe0_IX.woff2 +0 -0
  355. package/dist/client/_assets/chunks/f9d6d981d8b87b3e469027277f585741-DQqIv77a.woff2 +0 -0
  356. package/dist/client/_assets/chunks/fa7d3b99744d7f2dc9e00864a97a62d6-Pfww07DK.woff2 +0 -0
  357. package/dist/client/_assets/chunks/fa8ed469ef290bfeb571418fe0abb628-D5v-Z7kY.woff2 +0 -0
  358. package/dist/client/_assets/chunks/fb0e90665980954719c2eb685b130bc0-CiWRvo58.woff2 +0 -0
  359. package/dist/client/_assets/chunks/fb4649a82c50620773d79820e2e5ff13-dw14czgx.woff2 +0 -0
  360. package/dist/client/_assets/chunks/fb61b690208eff56e6d8432951270901-CUAnVvWa.woff2 +0 -0
  361. package/dist/client/_assets/chunks/fb9402d6c6357a825affc402f14d5a7e-CIp4G53L.woff2 +0 -0
  362. package/dist/client/_assets/chunks/fcc41f6a067ddd658bba5c9dff234a32-BtZTNo2r.woff2 +0 -0
  363. package/dist/client/_assets/chunks/fd6ad889fcf3583bd9b0b6db53aad434-DqiKyYmz.woff2 +0 -0
  364. package/dist/client/_assets/chunks/ff0937ad63cda71ff420945ead55ab4d-CXGPdnOz.woff2 +0 -0
  365. package/dist/client/_assets/chunks/heic-to-XcUDQvtx.js +1 -0
  366. package/dist/client/_assets/chunks/literata-cyrillic-ext-wght-normal-CGKlZYBf.woff2 +0 -0
  367. package/dist/client/_assets/chunks/literata-cyrillic-wght-normal-DLqwHbi6.woff2 +0 -0
  368. package/dist/client/_assets/chunks/literata-greek-ext-wght-normal-e3e57Shi.woff2 +0 -0
  369. package/dist/client/_assets/chunks/literata-greek-wght-normal-CO1l-giJ.woff2 +0 -0
  370. package/dist/client/_assets/chunks/literata-latin-ext-wght-normal-BnEbWgdZ.woff2 +0 -0
  371. package/dist/client/_assets/chunks/literata-latin-wght-normal-DLxlUchJ.woff2 +0 -0
  372. package/dist/client/_assets/chunks/literata-vietnamese-wght-normal-LcSrhZ7T.woff2 +0 -0
  373. package/dist/client/_assets/chunks/module-ChVQstFd.js +716 -0
  374. package/dist/client/_assets/chunks/native-CR5HLOyf.js +1 -0
  375. package/dist/client/_assets/chunks/news-cycle-cyrillic-400-normal-9BSXki1I.woff +0 -0
  376. package/dist/client/_assets/chunks/news-cycle-cyrillic-400-normal-CUYmhJri.woff2 +0 -0
  377. package/dist/client/_assets/chunks/news-cycle-cyrillic-ext-400-normal-CKyb9yaQ.woff2 +0 -0
  378. package/dist/client/_assets/chunks/news-cycle-cyrillic-ext-400-normal-Llxpm7uO.woff +0 -0
  379. package/dist/client/_assets/chunks/news-cycle-greek-400-normal-BiAZ91Zl.woff2 +0 -0
  380. package/dist/client/_assets/chunks/news-cycle-greek-400-normal-D88oNngQ.woff +0 -0
  381. package/dist/client/_assets/chunks/news-cycle-greek-ext-400-normal-Ckclqjrc.woff2 +0 -0
  382. package/dist/client/_assets/chunks/news-cycle-greek-ext-400-normal-oz-bBDWj.woff +0 -0
  383. package/dist/client/_assets/chunks/news-cycle-latin-400-normal-BLgpQ3uo.woff +0 -0
  384. package/dist/client/_assets/chunks/news-cycle-latin-400-normal-BgW97ttO.woff2 +0 -0
  385. package/dist/client/_assets/chunks/news-cycle-latin-ext-400-normal-DmDltzLi.woff2 +0 -0
  386. package/dist/client/_assets/chunks/news-cycle-latin-ext-400-normal-lewDoXxP.woff +0 -0
  387. package/dist/client/_assets/chunks/news-cycle-vietnamese-400-normal-QPSuG-pc.woff +0 -0
  388. package/dist/client/_assets/chunks/news-cycle-vietnamese-400-normal-t2mxI9x9.woff2 +0 -0
  389. package/dist/client/_assets/chunks/newsreader-latin-ext-wght-normal-C-3rgBeH.woff2 +0 -0
  390. package/dist/client/_assets/chunks/newsreader-latin-wght-normal-CCVVNp6i.woff2 +0 -0
  391. package/dist/client/_assets/chunks/newsreader-vietnamese-wght-normal-Czsa-EzN.woff2 +0 -0
  392. package/dist/client/_assets/chunks/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  393. package/dist/client/_assets/chunks/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  394. package/dist/client/_assets/chunks/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  395. package/dist/client/_assets/chunks/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  396. package/dist/client/_assets/chunks/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  397. package/dist/client/_assets/chunks/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  398. package/dist/client/_assets/chunks/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  399. package/dist/client/_assets/chunks/url-CG0eolsk.js +1 -0
  400. package/dist/client/_assets/client-auth.js +3260 -0
  401. package/dist/client/_assets/client-cjk-tc.css +1 -0
  402. package/dist/client/_assets/client-cjk.css +1 -0
  403. package/dist/client/_assets/client.css +2 -0
  404. package/dist/client/_assets/client.js +380 -0
  405. package/dist/index.js +2 -20359
  406. package/dist/node.js +479 -0
  407. package/package.json +67 -40
  408. package/src/__tests__/bin/uploads-cleanup.test.ts +77 -0
  409. package/src/__tests__/export-service.test.ts +550 -0
  410. package/src/__tests__/helpers/app.ts +60 -27
  411. package/src/__tests__/helpers/db.ts +14 -24
  412. package/src/__tests__/helpers/lingui-core-macro-mock.ts +15 -2
  413. package/src/__tests__/import-site-command.test.ts +406 -0
  414. package/src/__tests__/site-localize-media.test.ts +150 -0
  415. package/src/__tests__/site-media-parser.test.ts +216 -0
  416. package/src/app.tsx +277 -175
  417. package/src/assets/branding/generated/README.txt +15 -0
  418. package/src/assets/branding/generated/jant-apple-touch-icon.png +0 -0
  419. package/src/assets/branding/generated/jant-brand-assets.zip +0 -0
  420. package/src/assets/branding/generated/jant-brand-tile-512.png +0 -0
  421. package/src/assets/branding/generated/jant-brand-tile.svg +1 -0
  422. package/src/assets/branding/generated/jant-circle-tile-512.png +0 -0
  423. package/src/assets/branding/generated/jant-circle-tile.svg +1 -0
  424. package/src/assets/branding/generated/jant-favicon.ico +0 -0
  425. package/src/assets/branding/generated/jant-logo-negative.svg +1 -0
  426. package/src/assets/branding/generated/jant-logo-positive-512.png +0 -0
  427. package/src/assets/branding/generated/jant-logo-positive.svg +1 -0
  428. package/src/assets/branding/generated/jant-social-preview.png +0 -0
  429. package/src/assets/branding/generated/jant-square-tile-512.png +0 -0
  430. package/src/assets/branding/generated/jant-square-tile.svg +1 -0
  431. package/src/assets/branding/jant-logo-positive.svg +3 -0
  432. package/src/auth.ts +47 -6
  433. package/src/client/__tests__/avatar-upload.test.ts +186 -0
  434. package/src/client/__tests__/collection-form-bridge.test.ts +127 -0
  435. package/src/client/__tests__/collection-page-actions.test.ts +112 -0
  436. package/src/client/__tests__/collection-sort-menu.test.ts +78 -0
  437. package/src/client/__tests__/compose-bridge.test.ts +260 -0
  438. package/src/client/__tests__/compose-discovery.test.ts +155 -0
  439. package/src/client/__tests__/compose-shortcuts.test.ts +133 -0
  440. package/src/client/__tests__/confirm.test.ts +98 -0
  441. package/src/client/__tests__/custom-url-menu.test.ts +104 -0
  442. package/src/client/__tests__/form-enter-submit.test.ts +110 -0
  443. package/src/client/__tests__/post-form-bridge.test.ts +82 -0
  444. package/src/client/__tests__/sortable-list.test.ts +91 -0
  445. package/src/client/__tests__/toast.test.ts +46 -0
  446. package/src/client/archive-nav.js +1 -3
  447. package/src/client/avatar-upload.ts +66 -25
  448. package/src/client/collection-form-bridge.ts +32 -15
  449. package/src/client/collection-page-actions.ts +160 -0
  450. package/src/client/collection-sort-menu.ts +105 -0
  451. package/src/client/components/__tests__/jant-collection-form.test.ts +199 -51
  452. package/src/client/components/__tests__/jant-collection-sidebar.test.ts +306 -0
  453. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2507 -342
  454. package/src/client/components/__tests__/jant-compose-editor.test.ts +484 -20
  455. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +298 -0
  456. package/src/client/components/__tests__/jant-post-form.test.ts +9 -8
  457. package/src/client/components/__tests__/jant-post-menu.test.ts +325 -0
  458. package/src/client/components/__tests__/jant-settings-avatar.test.ts +27 -3
  459. package/src/client/components/__tests__/jant-settings-general.test.ts +312 -84
  460. package/src/client/components/collection-manager-types.ts +51 -0
  461. package/src/client/components/collection-types.ts +12 -10
  462. package/src/client/components/compose-types.ts +80 -13
  463. package/src/client/components/jant-collection-form.ts +340 -509
  464. package/src/client/components/jant-collection-sidebar.ts +474 -463
  465. package/src/client/components/jant-compose-dialog.ts +1566 -479
  466. package/src/client/components/jant-compose-editor.ts +531 -276
  467. package/src/client/components/jant-compose-fullscreen.ts +184 -112
  468. package/src/client/components/jant-confirm-dialog.ts +249 -0
  469. package/src/client/components/jant-nav-manager.ts +79 -103
  470. package/src/client/components/jant-post-form.ts +4 -1
  471. package/src/client/components/jant-post-menu.ts +583 -306
  472. package/src/client/components/jant-settings-avatar.ts +15 -9
  473. package/src/client/components/jant-settings-general.ts +444 -137
  474. package/src/client/components/nav-manager-types.ts +6 -3
  475. package/src/client/components/post-form-template.ts +2 -9
  476. package/src/client/components/post-form-types.ts +2 -4
  477. package/src/client/components/settings-types.ts +34 -2
  478. package/src/client/compose-bridge.ts +297 -171
  479. package/src/client/compose-discovery-bridge.ts +9 -0
  480. package/src/client/compose-discovery.ts +254 -0
  481. package/src/client/compose-launch.ts +98 -0
  482. package/src/client/compose-shortcuts.ts +61 -0
  483. package/src/client/confirm.ts +40 -0
  484. package/src/client/custom-url-menu.ts +128 -0
  485. package/src/client/form-enter-submit.ts +87 -0
  486. package/src/client/json.ts +37 -0
  487. package/src/client/multipart-upload.ts +51 -18
  488. package/src/client/post-form-bridge.ts +20 -14
  489. package/src/client/runtime-paths.ts +72 -0
  490. package/src/client/settings-bridge.ts +54 -10
  491. package/src/client/site-header-nav.js +84 -0
  492. package/src/client/sortable-list.ts +152 -0
  493. package/src/client/thread-context.ts +11 -5
  494. package/src/client/tiptap/__tests__/floating-position.test.ts +74 -0
  495. package/src/client/tiptap/__tests__/link-toolbar.test.ts +94 -0
  496. package/src/client/tiptap/__tests__/paste-media.test.ts +39 -0
  497. package/src/client/tiptap/bubble-menu.ts +98 -13
  498. package/src/client/tiptap/create-editor.ts +6 -0
  499. package/src/client/tiptap/extensions.ts +15 -4
  500. package/src/client/tiptap/floating-position.ts +167 -0
  501. package/src/client/tiptap/inline-image-upload.ts +73 -0
  502. package/src/client/tiptap/link-input-rules.ts +56 -0
  503. package/src/client/tiptap/link-toolbar.ts +60 -14
  504. package/src/client/tiptap/paste-media.ts +120 -0
  505. package/src/client/tiptap/slash-commands.ts +205 -52
  506. package/src/client/tiptap/toolbar-mode.ts +31 -0
  507. package/src/client/toast.ts +136 -16
  508. package/src/client/types/sortablejs.d.ts +4 -4
  509. package/src/client/upload-session.ts +270 -0
  510. package/src/client/upload-with-metadata.ts +10 -17
  511. package/src/client-auth.ts +38 -0
  512. package/src/client.ts +10 -29
  513. package/src/db/__tests__/d1-query.test.ts +84 -0
  514. package/src/db/__tests__/demo-canonical-snapshot.test.ts +78 -0
  515. package/src/db/__tests__/demo-env-loader.test.ts +114 -0
  516. package/src/db/__tests__/dialect.test.ts +42 -0
  517. package/src/db/__tests__/migration-artifacts.test.ts +82 -0
  518. package/src/db/__tests__/migration-rehearsal.test.ts +73 -0
  519. package/src/db/__tests__/migration-runner.test.ts +72 -0
  520. package/src/db/__tests__/migrations.test.ts +293 -8
  521. package/src/db/__tests__/r2-query.test.ts +102 -0
  522. package/src/db/__tests__/wrangler-config.test.ts +86 -0
  523. package/src/db/backfills/0000_normalize_legacy_time_zone_values.sql +38 -0
  524. package/src/db/backfills/README.md +11 -0
  525. package/src/db/dialect.ts +71 -0
  526. package/src/db/index.ts +84 -2
  527. package/src/db/migrations/0000_baseline.sql +35 -57
  528. package/src/db/migrations/0002_site_aware_core.sql +354 -0
  529. package/src/db/migrations/0003_fts_site_aware.sql +38 -0
  530. package/src/db/migrations/0004_perpetual_eternity.sql +29 -0
  531. package/src/db/migrations/meta/0000_snapshot.json +134 -138
  532. package/src/db/migrations/meta/0001_snapshot.json +135 -139
  533. package/src/db/migrations/meta/0002_snapshot.json +1717 -0
  534. package/src/db/migrations/meta/0003_snapshot.json +1717 -0
  535. package/src/db/migrations/meta/0004_snapshot.json +1897 -0
  536. package/src/db/migrations/meta/_journal.json +23 -2
  537. package/src/db/migrations/pg/0000_pg_site_aware_baseline.sql +306 -0
  538. package/src/db/migrations/pg/0001_pg_search_support.sql +8 -0
  539. package/src/db/migrations/pg/0002_breezy_lockjaw.sql +29 -0
  540. package/src/db/migrations/pg/README.md +8 -0
  541. package/src/db/migrations/pg/meta/0000_snapshot.json +2195 -0
  542. package/src/db/migrations/pg/meta/0001_snapshot.json +2248 -0
  543. package/src/db/migrations/pg/meta/0002_snapshot.json +2476 -0
  544. package/src/db/migrations/pg/meta/_journal.json +27 -0
  545. package/src/db/pg/__tests__/node.test.ts +58 -0
  546. package/src/db/pg/node.ts +151 -0
  547. package/src/db/pg/schema.ts +739 -0
  548. package/src/db/raw-query.ts +15 -0
  549. package/src/db/rehearsal-fixtures/demo-current.json +25 -0
  550. package/src/db/rehearsal-fixtures/demo-current.sql +142 -0
  551. package/src/db/schema-bundle.ts +12 -0
  552. package/src/db/schema.ts +309 -128
  553. package/src/db/sqlite/schema.ts +1 -0
  554. package/src/i18n/__tests__/context.test.tsx +35 -0
  555. package/src/i18n/context.tsx +10 -2
  556. package/src/i18n/locales/en.po +1792 -231
  557. package/src/i18n/locales/en.ts +1 -1
  558. package/src/i18n/locales/glossary.zh-Hans.yml +116 -0
  559. package/src/i18n/locales/glossary.zh-Hant.yml +116 -0
  560. package/src/i18n/locales/zh-Hans.po +1831 -270
  561. package/src/i18n/locales/zh-Hans.ts +1 -1
  562. package/src/i18n/locales/zh-Hant.po +1835 -274
  563. package/src/i18n/locales/zh-Hant.ts +1 -1
  564. package/src/index.ts +4 -0
  565. package/src/lib/__tests__/asset-path.test.ts +54 -0
  566. package/src/lib/__tests__/blurhash-placeholder.test.ts +25 -1
  567. package/src/lib/__tests__/collection-groups.test.ts +63 -0
  568. package/src/lib/__tests__/collection-sort.test.ts +45 -0
  569. package/src/lib/__tests__/constants.test.ts +24 -1
  570. package/src/lib/__tests__/favicon.test.ts +1 -1
  571. package/src/lib/__tests__/feed.test.ts +147 -0
  572. package/src/lib/__tests__/hosted-control-plane.test.ts +106 -0
  573. package/src/lib/__tests__/hosted-domain.test.ts +78 -0
  574. package/src/lib/__tests__/hosted-signin.test.ts +103 -0
  575. package/src/lib/__tests__/icons.test.ts +14 -172
  576. package/src/lib/__tests__/image.test.ts +23 -4
  577. package/src/lib/__tests__/jant-branding.test.ts +160 -0
  578. package/src/lib/__tests__/markdown-to-tiptap.test.ts +27 -0
  579. package/src/lib/__tests__/page-title.test.ts +20 -0
  580. package/src/lib/__tests__/password.test.ts +60 -0
  581. package/src/lib/__tests__/post-display.test.ts +99 -0
  582. package/src/lib/__tests__/post-meta.test.ts +126 -0
  583. package/src/lib/__tests__/public-storage.test.ts +75 -0
  584. package/src/lib/__tests__/resolve-config.test.ts +156 -28
  585. package/src/lib/__tests__/schemas.test.ts +286 -22
  586. package/src/lib/__tests__/search-snippet.test.ts +43 -0
  587. package/src/lib/__tests__/site-resolution.test.ts +39 -0
  588. package/src/lib/__tests__/slug-format.test.ts +54 -0
  589. package/src/lib/__tests__/startup-config.test.ts +87 -0
  590. package/src/lib/__tests__/storage.test.ts +218 -1
  591. package/src/lib/__tests__/theme.test.ts +50 -13
  592. package/src/lib/__tests__/time.test.ts +44 -0
  593. package/src/lib/__tests__/timeline.test.ts +492 -4
  594. package/src/lib/__tests__/timezones.test.ts +51 -18
  595. package/src/lib/__tests__/tiptap-to-markdown.test.ts +23 -0
  596. package/src/lib/__tests__/url.test.ts +79 -1
  597. package/src/lib/__tests__/view.test.ts +74 -13
  598. package/src/lib/asset-path.ts +87 -0
  599. package/src/lib/blurhash-placeholder.ts +56 -1
  600. package/src/lib/collection-groups.ts +69 -0
  601. package/src/lib/collection-sort.ts +61 -0
  602. package/src/lib/confirm.ts +37 -0
  603. package/src/lib/constants.ts +20 -3
  604. package/src/lib/crypto.ts +47 -0
  605. package/src/lib/env.ts +245 -0
  606. package/src/lib/errors.ts +25 -9
  607. package/src/lib/favicon.ts +2 -1
  608. package/src/lib/featured-icons.ts +33 -0
  609. package/src/lib/feed.ts +73 -24
  610. package/src/lib/hosted-control-plane-sync.ts +27 -0
  611. package/src/lib/hosted-control-plane.ts +99 -0
  612. package/src/lib/hosted-domain-check.ts +98 -0
  613. package/src/lib/hosted-domain.ts +61 -0
  614. package/src/lib/hosted-signin.ts +115 -0
  615. package/src/lib/hosted-sso.ts +110 -0
  616. package/src/lib/icons.ts +10 -175
  617. package/src/lib/ids.ts +54 -0
  618. package/src/lib/image.ts +18 -7
  619. package/src/lib/jant-branding-generated.ts +947 -0
  620. package/src/lib/jant-branding.ts +520 -0
  621. package/src/lib/markdown-to-tiptap.ts +57 -0
  622. package/src/lib/media-helpers.ts +6 -2
  623. package/src/lib/navigation.ts +9 -2
  624. package/src/lib/page-title.ts +23 -0
  625. package/src/lib/pagination.ts +34 -0
  626. package/src/lib/password.ts +164 -0
  627. package/src/lib/post-display.ts +162 -0
  628. package/src/lib/post-meta.ts +95 -0
  629. package/src/lib/public-storage.ts +57 -0
  630. package/src/lib/render.tsx +22 -0
  631. package/src/lib/resolve-config.ts +78 -27
  632. package/src/lib/schemas.ts +271 -73
  633. package/src/lib/search-snippet.ts +110 -8
  634. package/src/lib/site-resolution.ts +28 -0
  635. package/src/lib/slug-format.ts +92 -0
  636. package/src/lib/slug.ts +1 -1
  637. package/src/lib/startup-config.ts +244 -0
  638. package/src/lib/storage.ts +722 -28
  639. package/src/lib/theme.ts +13 -6
  640. package/src/lib/time.ts +59 -15
  641. package/src/lib/timeline.ts +394 -63
  642. package/src/lib/timezones.ts +120 -50
  643. package/src/lib/tiptap-render.ts +1 -3
  644. package/src/lib/tiptap-to-markdown.ts +49 -0
  645. package/src/lib/upload.ts +216 -11
  646. package/src/lib/url.ts +210 -0
  647. package/src/lib/view.ts +87 -31
  648. package/src/middleware/__tests__/auth.test.ts +216 -14
  649. package/src/middleware/__tests__/error-handler.test.ts +37 -0
  650. package/src/middleware/__tests__/onboarding.test.ts +9 -0
  651. package/src/middleware/__tests__/secure-headers.test.ts +111 -0
  652. package/src/middleware/auth.ts +156 -28
  653. package/src/middleware/config.ts +29 -9
  654. package/src/middleware/error-handler.ts +16 -1
  655. package/src/middleware/onboarding.ts +15 -6
  656. package/src/middleware/secure-headers.ts +136 -25
  657. package/src/node/__tests__/cli-db-execute-file.test.ts +109 -0
  658. package/src/node/__tests__/cli-deploy.test.ts +124 -0
  659. package/src/node/__tests__/cli-export.test.ts +66 -0
  660. package/src/node/__tests__/cli-reset-password.test.ts +91 -0
  661. package/src/node/__tests__/cli-runtime-target.test.ts +37 -0
  662. package/src/node/__tests__/cli-site-selection.test.ts +133 -0
  663. package/src/node/__tests__/cli-site-snapshot.test.ts +636 -0
  664. package/src/node/__tests__/cli-site-token-env.test.ts +137 -0
  665. package/src/node/__tests__/runtime.test.ts +346 -0
  666. package/src/node/index.ts +18 -0
  667. package/src/node/request-handler.ts +668 -0
  668. package/src/node/runtime.ts +88 -0
  669. package/src/preset.css +73 -16
  670. package/src/routes/__tests__/compose.test.ts +77 -4
  671. package/src/routes/api/__tests__/attachments.test.ts +92 -0
  672. package/src/routes/api/__tests__/collections.test.ts +138 -24
  673. package/src/routes/api/__tests__/nav-items.test.ts +81 -20
  674. package/src/routes/api/__tests__/posts.test.ts +315 -57
  675. package/src/routes/api/__tests__/search.test.ts +25 -1
  676. package/src/routes/api/__tests__/settings.test.ts +380 -2
  677. package/src/routes/api/__tests__/upload-multipart.test.ts +216 -26
  678. package/src/routes/api/__tests__/uploads.test.ts +420 -0
  679. package/src/routes/api/attachments.ts +24 -0
  680. package/src/routes/api/collections.ts +59 -28
  681. package/src/routes/api/custom-urls.ts +6 -5
  682. package/src/routes/api/export.ts +37 -7
  683. package/src/routes/api/internal/__tests__/api-tokens.test.ts +55 -0
  684. package/src/routes/api/internal/__tests__/sites.test.ts +472 -0
  685. package/src/routes/api/internal/__tests__/uploads.test.ts +131 -0
  686. package/src/routes/api/internal/api-tokens.ts +17 -0
  687. package/src/routes/api/internal/sites.ts +241 -0
  688. package/src/routes/api/internal/uploads.ts +37 -0
  689. package/src/routes/api/nav-items.ts +24 -15
  690. package/src/routes/api/posts.ts +222 -74
  691. package/src/routes/api/search.ts +50 -9
  692. package/src/routes/api/settings.ts +141 -7
  693. package/src/routes/api/upload-multipart.ts +105 -16
  694. package/src/routes/api/upload.ts +90 -21
  695. package/src/routes/api/uploads.ts +247 -0
  696. package/src/routes/auth/__tests__/dev.test.ts +135 -0
  697. package/src/routes/auth/__tests__/hosted-sso.test.ts +136 -0
  698. package/src/routes/auth/__tests__/setup.test.ts +73 -49
  699. package/src/routes/auth/dev.ts +72 -0
  700. package/src/routes/auth/hosted-sso.ts +64 -0
  701. package/src/routes/auth/reset.tsx +41 -7
  702. package/src/routes/auth/setup.tsx +37 -38
  703. package/src/routes/auth/signin.tsx +36 -6
  704. package/src/routes/compose.tsx +25 -38
  705. package/src/routes/dash/__tests__/font-theme.test.ts +45 -34
  706. package/src/routes/dash/__tests__/settings-avatar.test.ts +24 -11
  707. package/src/routes/dash/custom-urls.tsx +245 -67
  708. package/src/routes/dash/settings.tsx +530 -139
  709. package/src/routes/feed/__tests__/rss.test.ts +200 -51
  710. package/src/routes/feed/__tests__/sitemap.test.ts +64 -0
  711. package/src/routes/feed/rss.ts +112 -48
  712. package/src/routes/feed/sitemap.ts +16 -6
  713. package/src/routes/hosted/__tests__/domain-check.test.ts +94 -0
  714. package/src/routes/hosted/domain-check.ts +39 -0
  715. package/src/routes/pages/__tests__/collections.test.ts +27 -21
  716. package/src/routes/pages/__tests__/featured.test.ts +9 -2
  717. package/src/routes/pages/archive.tsx +79 -33
  718. package/src/routes/pages/brand.tsx +119 -0
  719. package/src/routes/pages/collection.tsx +201 -65
  720. package/src/routes/pages/collections.tsx +46 -20
  721. package/src/routes/pages/featured.tsx +31 -39
  722. package/src/routes/pages/home.tsx +50 -25
  723. package/src/routes/pages/latest.tsx +20 -4
  724. package/src/routes/pages/new.tsx +10 -6
  725. package/src/routes/pages/page.tsx +27 -53
  726. package/src/routes/pages/partials.tsx +83 -0
  727. package/src/routes/pages/search.tsx +11 -9
  728. package/src/routes/pages/theme-sample.tsx +98 -0
  729. package/src/runtime/__tests__/node.test.ts +258 -0
  730. package/src/runtime/__tests__/readiness.test.ts +89 -0
  731. package/src/runtime/cloudflare.ts +102 -0
  732. package/src/runtime/index.ts +14 -0
  733. package/src/runtime/node.ts +187 -0
  734. package/src/runtime/readiness.ts +129 -0
  735. package/src/runtime/site.ts +169 -0
  736. package/src/services/__tests__/api-token.test.ts +5 -2
  737. package/src/services/__tests__/auth.test.ts +348 -0
  738. package/src/services/__tests__/collection.test.ts +199 -16
  739. package/src/services/__tests__/custom-url.test.ts +10 -3
  740. package/src/services/__tests__/hosted-handoff.test.ts +123 -0
  741. package/src/services/__tests__/media.test.ts +256 -33
  742. package/src/services/__tests__/navigation.test.ts +89 -4
  743. package/src/services/__tests__/post-timeline.test.ts +169 -2
  744. package/src/services/__tests__/post.test.ts +498 -39
  745. package/src/services/__tests__/search.test.ts +145 -14
  746. package/src/services/__tests__/settings.test.ts +162 -2
  747. package/src/services/__tests__/site-admin.test.ts +48 -0
  748. package/src/services/__tests__/site-profile.test.ts +175 -0
  749. package/src/services/api-token.ts +44 -11
  750. package/src/services/auth.ts +211 -9
  751. package/src/services/bootstrap.ts +68 -0
  752. package/src/services/collection.ts +556 -100
  753. package/src/services/custom-url.ts +42 -12
  754. package/src/services/export.ts +2269 -320
  755. package/src/services/hosted-handoff.ts +184 -0
  756. package/src/services/index.ts +110 -12
  757. package/src/services/media.ts +309 -39
  758. package/src/services/navigation.ts +141 -20
  759. package/src/services/path.ts +45 -9
  760. package/src/services/post.ts +1640 -248
  761. package/src/services/search.ts +222 -29
  762. package/src/services/settings.ts +202 -48
  763. package/src/services/site-admin.ts +825 -0
  764. package/src/services/site-member.ts +74 -0
  765. package/src/services/site-profile.ts +52 -0
  766. package/src/services/site.ts +250 -0
  767. package/src/services/upload-session.ts +707 -0
  768. package/src/style-cjk-tc.css +3 -0
  769. package/src/style-cjk.css +3 -0
  770. package/src/styles/components.css +647 -16
  771. package/src/styles/fonts/latin.css +4 -0
  772. package/src/styles/fonts/noto-serif-sc/400/02629e5d0a9860b7fe32ec1f0563213a.woff2 +0 -0
  773. package/src/styles/fonts/noto-serif-sc/400/031089da45fbfb7dc18ac827bef4c56e.woff2 +0 -0
  774. package/src/styles/fonts/noto-serif-sc/400/03ac785139320b7b13bac9c150bf72bf.woff2 +0 -0
  775. package/src/styles/fonts/noto-serif-sc/400/0b1f83a3c7e715560a55ad9eb0fb1c94.woff2 +0 -0
  776. package/src/styles/fonts/noto-serif-sc/400/14b040a2dda256936bc7b3470e548394.woff2 +0 -0
  777. package/src/styles/fonts/noto-serif-sc/400/189f272ea2600c74d576b7b15c014922.woff2 +0 -0
  778. package/src/styles/fonts/noto-serif-sc/400/1fbccc182322b513f57cd156a9a491b0.woff2 +0 -0
  779. package/src/styles/fonts/noto-serif-sc/400/21e0a71d86be7b8a9e812e7af09dd061.woff2 +0 -0
  780. package/src/styles/fonts/noto-serif-sc/400/265e048cc9f2f8a711ba585a534d5351.woff2 +0 -0
  781. package/src/styles/fonts/noto-serif-sc/400/2ae2ca951489c9d50cde5b36a2a5515b.woff2 +0 -0
  782. package/src/styles/fonts/noto-serif-sc/400/2ba802b14f21a58fc61606c88fa93373.woff2 +0 -0
  783. package/src/styles/fonts/noto-serif-sc/400/2deb444546774c3a3ab38c75eb69cdfb.woff2 +0 -0
  784. package/src/styles/fonts/noto-serif-sc/400/2fbccf9a3853eb59db1a825e044515fd.woff2 +0 -0
  785. package/src/styles/fonts/noto-serif-sc/400/2ff009fa8701505d7f3dc6c83763f019.woff2 +0 -0
  786. package/src/styles/fonts/noto-serif-sc/400/31424fe5d54692e7c8b38021ccb8597c.woff2 +0 -0
  787. package/src/styles/fonts/noto-serif-sc/400/360c190344c26278bbc50e2f4d6a2b3f.woff2 +0 -0
  788. package/src/styles/fonts/noto-serif-sc/400/39b0bbc910af9d2d6dcd8bd4abd6387d.woff2 +0 -0
  789. package/src/styles/fonts/noto-serif-sc/400/3adc8f6350cf5067bcb6dc5e44c45d41.woff2 +0 -0
  790. package/src/styles/fonts/noto-serif-sc/400/3bed5bd57de8f738e53cddaea88983d9.woff2 +0 -0
  791. package/src/styles/fonts/noto-serif-sc/400/3c201fd8d1bb20abe7d06b940e83a4d9.woff2 +0 -0
  792. package/src/styles/fonts/noto-serif-sc/400/3d385ea0880df7204258e290648ec012.woff2 +0 -0
  793. package/src/styles/fonts/noto-serif-sc/400/3dc1a4f0d7af59e16b5162a2b077a442.woff2 +0 -0
  794. package/src/styles/fonts/noto-serif-sc/400/3dc68e473fe23bd076dd46785cd23583.woff2 +0 -0
  795. package/src/styles/fonts/noto-serif-sc/400/435b7dca567809813fcb395a27ed83a0.woff2 +0 -0
  796. package/src/styles/fonts/noto-serif-sc/400/43693195e775d515689fa035394067fd.woff2 +0 -0
  797. package/src/styles/fonts/noto-serif-sc/400/43fb49e5b79ee7e553869d84e6e08b1e.woff2 +0 -0
  798. package/src/styles/fonts/noto-serif-sc/400/44dcbbc3cc8f22e613b342d691511ab6.woff2 +0 -0
  799. package/src/styles/fonts/noto-serif-sc/400/474fac21b12b7efd71f7c321578878b0.woff2 +0 -0
  800. package/src/styles/fonts/noto-serif-sc/400/4a23fe6e82fd496b5eb20401b6164efe.woff2 +0 -0
  801. package/src/styles/fonts/noto-serif-sc/400/4bc743968cf1c3ce5711de67ef1ccc4d.woff2 +0 -0
  802. package/src/styles/fonts/noto-serif-sc/400/4cf0f292f3358bd2f73b1cf4ec1476f3.woff2 +0 -0
  803. package/src/styles/fonts/noto-serif-sc/400/501f66f24bce8234441954de1b568403.woff2 +0 -0
  804. package/src/styles/fonts/noto-serif-sc/400/53a88404451448cd2e620a0ca0e45a20.woff2 +0 -0
  805. package/src/styles/fonts/noto-serif-sc/400/557cd00c5d6827e13d72a0c71b23587b.woff2 +0 -0
  806. package/src/styles/fonts/noto-serif-sc/400/563fa31542d553f25abab65cf7f81e1d.woff2 +0 -0
  807. package/src/styles/fonts/noto-serif-sc/400/56e1c4734bbbb38af2fbc262bf6e98f2.woff2 +0 -0
  808. package/src/styles/fonts/noto-serif-sc/400/5947f5da5da9a352a2b534ee64bfc29a.woff2 +0 -0
  809. package/src/styles/fonts/noto-serif-sc/400/5a10741e41259e235841440394c0763d.woff2 +0 -0
  810. package/src/styles/fonts/noto-serif-sc/400/60d8b0805a0a8c54a6cca216004beff5.woff2 +0 -0
  811. package/src/styles/fonts/noto-serif-sc/400/61bf4287453da4025d03fa6b2dba66ca.woff2 +0 -0
  812. package/src/styles/fonts/noto-serif-sc/400/638369541268ed5a10af97ad77498c73.woff2 +0 -0
  813. package/src/styles/fonts/noto-serif-sc/400/687d0f0f90a9b23e40102e16ad8e9836.woff2 +0 -0
  814. package/src/styles/fonts/noto-serif-sc/400/6e2164fad867d166de2e5b274f04a563.woff2 +0 -0
  815. package/src/styles/fonts/noto-serif-sc/400/6eefc9d430171c1e1e4034ecadee31c8.woff2 +0 -0
  816. package/src/styles/fonts/noto-serif-sc/400/71eafb8fbe3a734283517e230ad8b6db.woff2 +0 -0
  817. package/src/styles/fonts/noto-serif-sc/400/751f54dbb115140d5b645a6ba4aff5d3.woff2 +0 -0
  818. package/src/styles/fonts/noto-serif-sc/400/7609e7e74dd4d916a7abc7ecc7d95f7e.woff2 +0 -0
  819. package/src/styles/fonts/noto-serif-sc/400/76b9d6fe838ae4151d95ce7200aa2bf6.woff2 +0 -0
  820. package/src/styles/fonts/noto-serif-sc/400/78ce29fed872e44fc9014d94875d2aac.woff2 +0 -0
  821. package/src/styles/fonts/noto-serif-sc/400/79a85a253e9b3f12d2e2cb15e16b3003.woff2 +0 -0
  822. package/src/styles/fonts/noto-serif-sc/400/7caa14a095a6bc313aab780fe4ff7999.woff2 +0 -0
  823. package/src/styles/fonts/noto-serif-sc/400/7d138084cf03c14116b11297fce0e3e3.woff2 +0 -0
  824. package/src/styles/fonts/noto-serif-sc/400/880162ae92cd9e120eb4e4e11fae459d.woff2 +0 -0
  825. package/src/styles/fonts/noto-serif-sc/400/8f2b960c2823670e94f5b08aa65657e6.woff2 +0 -0
  826. package/src/styles/fonts/noto-serif-sc/400/8f89f57230d184f92a36e241874229d7.woff2 +0 -0
  827. package/src/styles/fonts/noto-serif-sc/400/9180de34b48b325200a97e267befff32.woff2 +0 -0
  828. package/src/styles/fonts/noto-serif-sc/400/95df3b9f681d9df411c30aea5b24f2e0.woff2 +0 -0
  829. package/src/styles/fonts/noto-serif-sc/400/975af5a496e8d87d821910aa9fe4d598.woff2 +0 -0
  830. package/src/styles/fonts/noto-serif-sc/400/97a874bbf55ce89a4ab7cd27c7e938b1.woff2 +0 -0
  831. package/src/styles/fonts/noto-serif-sc/400/9fd53607094e329fa8e5c785b3ff0f1a.woff2 +0 -0
  832. package/src/styles/fonts/noto-serif-sc/400/a578742770fcd2226e3c45b5b6efdcb0.woff2 +0 -0
  833. package/src/styles/fonts/noto-serif-sc/400/ae401fb4db80d5ff5cd3f8d9bc811070.woff2 +0 -0
  834. package/src/styles/fonts/noto-serif-sc/400/b0ab3a7f319ce6dd3c9a4de2674e7c72.woff2 +0 -0
  835. package/src/styles/fonts/noto-serif-sc/400/b159deb135e9946eea0572d52778170b.woff2 +0 -0
  836. package/src/styles/fonts/noto-serif-sc/400/b91450304d9ac44f5c6e0da0792e055d.woff2 +0 -0
  837. package/src/styles/fonts/noto-serif-sc/400/baa325551b381c5e035ef143e56d4abe.woff2 +0 -0
  838. package/src/styles/fonts/noto-serif-sc/400/c0c7836749e585cee24ab2f8457c5b01.woff2 +0 -0
  839. package/src/styles/fonts/noto-serif-sc/400/c2ac4ef1860812036ca2b8c4e4089bdc.woff2 +0 -0
  840. package/src/styles/fonts/noto-serif-sc/400/c31019c08bd22464f7a88f090281404c.woff2 +0 -0
  841. package/src/styles/fonts/noto-serif-sc/400/c57ee3b49b7e45b995539a6b2c51f138.woff2 +0 -0
  842. package/src/styles/fonts/noto-serif-sc/400/cd75ca47da9ae4c0899e37d4c543319b.woff2 +0 -0
  843. package/src/styles/fonts/noto-serif-sc/400/d12ce1d8445213317f9163283e58a05d.woff2 +0 -0
  844. package/src/styles/fonts/noto-serif-sc/400/d28fb13acf9ced9f0657fd4012c81cd2.woff2 +0 -0
  845. package/src/styles/fonts/noto-serif-sc/400/d3714e6b90de8e2085dfb2514464dd6a.woff2 +0 -0
  846. package/src/styles/fonts/noto-serif-sc/400/d3beff96216c8af1aa79246476b6a323.woff2 +0 -0
  847. package/src/styles/fonts/noto-serif-sc/400/d3e311f30c811dc339c262a79a51877e.woff2 +0 -0
  848. package/src/styles/fonts/noto-serif-sc/400/d51f4cdc83711e510f5d25f03235597e.woff2 +0 -0
  849. package/src/styles/fonts/noto-serif-sc/400/d5df4a8dfd4328c67d933b3912c6ad0f.woff2 +0 -0
  850. package/src/styles/fonts/noto-serif-sc/400/dccda6a2e2db3b530788bdfa2acd1979.woff2 +0 -0
  851. package/src/styles/fonts/noto-serif-sc/400/f5237486197aeff59341a1ff38b8eff8.woff2 +0 -0
  852. package/src/styles/fonts/noto-serif-sc/400/f553d54ef931066712d8f3f0ce018e1b.woff2 +0 -0
  853. package/src/styles/fonts/noto-serif-sc/400/f649cba8e14c33d6bf2265483b14b895.woff2 +0 -0
  854. package/src/styles/fonts/noto-serif-sc/400/f6839df1bf7cb4dc8d27e5ea55bbe633.woff2 +0 -0
  855. package/src/styles/fonts/noto-serif-sc/400/f6b7304e028980f77a7f7007bb540abd.woff2 +0 -0
  856. package/src/styles/fonts/noto-serif-sc/400/f8db8bef0a6e1178835d350ae0d384a1.woff2 +0 -0
  857. package/src/styles/fonts/noto-serif-sc/400/fb4649a82c50620773d79820e2e5ff13.woff2 +0 -0
  858. package/src/styles/fonts/noto-serif-sc/400/fb9402d6c6357a825affc402f14d5a7e.woff2 +0 -0
  859. package/src/styles/fonts/noto-serif-sc/700/0041f681602cc834bb5c55ced0155b8e.woff2 +0 -0
  860. package/src/styles/fonts/noto-serif-sc/700/00c9ac960d866ffaf8a866e5939024e2.woff2 +0 -0
  861. package/src/styles/fonts/noto-serif-sc/700/02e48e353415a00e0f556608cab33a43.woff2 +0 -0
  862. package/src/styles/fonts/noto-serif-sc/700/02faf6bb0ab4d56ada037c0bbaf9b9f7.woff2 +0 -0
  863. package/src/styles/fonts/noto-serif-sc/700/057a3d44d7fc606f113d863376d0ecf0.woff2 +0 -0
  864. package/src/styles/fonts/noto-serif-sc/700/10f2b44b3711d3f5bdcc30d373b543d1.woff2 +0 -0
  865. package/src/styles/fonts/noto-serif-sc/700/14c1506106d92621bafb11016735194e.woff2 +0 -0
  866. package/src/styles/fonts/noto-serif-sc/700/154a2c266902003bd8b7449386b10776.woff2 +0 -0
  867. package/src/styles/fonts/noto-serif-sc/700/19e39850472250bfdbce654d30859879.woff2 +0 -0
  868. package/src/styles/fonts/noto-serif-sc/700/1b5d0d740450fb996749464c9b882025.woff2 +0 -0
  869. package/src/styles/fonts/noto-serif-sc/700/1bca0a2a8840ad0ee9414940593db144.woff2 +0 -0
  870. package/src/styles/fonts/noto-serif-sc/700/1cda27dcaab977ae4ef5d5ab2a10ae03.woff2 +0 -0
  871. package/src/styles/fonts/noto-serif-sc/700/1f4bc38a1c50f55f335f5411cae47696.woff2 +0 -0
  872. package/src/styles/fonts/noto-serif-sc/700/2022cf097cb952d9fe75b53b4587d2c3.woff2 +0 -0
  873. package/src/styles/fonts/noto-serif-sc/700/2240a3c43ca5ef59ae3c348c7884792f.woff2 +0 -0
  874. package/src/styles/fonts/noto-serif-sc/700/280e3d2b58e9ad3501816072e01b0c13.woff2 +0 -0
  875. package/src/styles/fonts/noto-serif-sc/700/2874d07e228da9583b0e73646dacd498.woff2 +0 -0
  876. package/src/styles/fonts/noto-serif-sc/700/29d49891713a2785a3a383001cf58c59.woff2 +0 -0
  877. package/src/styles/fonts/noto-serif-sc/700/2d81eb6ab0ebbc0cabfb3a3341ba8800.woff2 +0 -0
  878. package/src/styles/fonts/noto-serif-sc/700/3179006d1c7ebfa50d27482a2859d9a0.woff2 +0 -0
  879. package/src/styles/fonts/noto-serif-sc/700/34c2edb3c37f71258f5c4a31091f0c6c.woff2 +0 -0
  880. package/src/styles/fonts/noto-serif-sc/700/389a950f2a1211946d294716e679e381.woff2 +0 -0
  881. package/src/styles/fonts/noto-serif-sc/700/42a74b6a625bbf0a9616ed4db3152c88.woff2 +0 -0
  882. package/src/styles/fonts/noto-serif-sc/700/457485e72835364662dfead6281638c1.woff2 +0 -0
  883. package/src/styles/fonts/noto-serif-sc/700/488846410760fe128dae939836ca5423.woff2 +0 -0
  884. package/src/styles/fonts/noto-serif-sc/700/48eb0a91e50c7f026e248c64145e72af.woff2 +0 -0
  885. package/src/styles/fonts/noto-serif-sc/700/4b0e79ba18b2ce424fa93e84996d7cba.woff2 +0 -0
  886. package/src/styles/fonts/noto-serif-sc/700/54e301f412730f391225db59dae1c8d5.woff2 +0 -0
  887. package/src/styles/fonts/noto-serif-sc/700/597d69d0710e0178b162afb0a0c20401.woff2 +0 -0
  888. package/src/styles/fonts/noto-serif-sc/700/5cc23a76e122d0ad2f7cede41bc35b27.woff2 +0 -0
  889. package/src/styles/fonts/noto-serif-sc/700/5d48855bed5f3554eff91b573d7376ac.woff2 +0 -0
  890. package/src/styles/fonts/noto-serif-sc/700/5ddcbe564b29ef08632e1aeb33455435.woff2 +0 -0
  891. package/src/styles/fonts/noto-serif-sc/700/605667a998e91e2b6a4a3cd7c31fe5a9.woff2 +0 -0
  892. package/src/styles/fonts/noto-serif-sc/700/67f32ceea9e78e5109f87724ad886010.woff2 +0 -0
  893. package/src/styles/fonts/noto-serif-sc/700/68f2fab82ec8e9291f08c3145111549c.woff2 +0 -0
  894. package/src/styles/fonts/noto-serif-sc/700/69519ada3f3f74ca20aacb8af48ab6b4.woff2 +0 -0
  895. package/src/styles/fonts/noto-serif-sc/700/6a6e884fb2b65ec5b4a3d5ecd0d01a6a.woff2 +0 -0
  896. package/src/styles/fonts/noto-serif-sc/700/6e1a8b45b01939088c3a8cfcf8c10681.woff2 +0 -0
  897. package/src/styles/fonts/noto-serif-sc/700/70adaf50c56d5ff859c64d35e0f1e34e.woff2 +0 -0
  898. package/src/styles/fonts/noto-serif-sc/700/72ee453ac0e19bd2c631c8921c44e3de.woff2 +0 -0
  899. package/src/styles/fonts/noto-serif-sc/700/753b5f6fb254bacb6618ace25af3df60.woff2 +0 -0
  900. package/src/styles/fonts/noto-serif-sc/700/76d4244186d118eea245d1385a4de2ec.woff2 +0 -0
  901. package/src/styles/fonts/noto-serif-sc/700/7a86b155111ba20f3e87306ff6beac77.woff2 +0 -0
  902. package/src/styles/fonts/noto-serif-sc/700/7b1e76975b0984e6f83e3f9f8069e784.woff2 +0 -0
  903. package/src/styles/fonts/noto-serif-sc/700/7b6c60131822a0e4d36d980d52509d4e.woff2 +0 -0
  904. package/src/styles/fonts/noto-serif-sc/700/7cbda564cb2dd4799ab9e89d51286aa7.woff2 +0 -0
  905. package/src/styles/fonts/noto-serif-sc/700/7f60eefa15956d6f06dd92404887d58c.woff2 +0 -0
  906. package/src/styles/fonts/noto-serif-sc/700/7f8c15e0ecb102738981d9fa4cb6b921.woff2 +0 -0
  907. package/src/styles/fonts/noto-serif-sc/700/812b5a4b87f3a7b4afc1cfebc864f413.woff2 +0 -0
  908. package/src/styles/fonts/noto-serif-sc/700/812dfb7f8144d01b3cc9d5ce0b472f40.woff2 +0 -0
  909. package/src/styles/fonts/noto-serif-sc/700/84742b1ede4f0bb6d27131298eba21b4.woff2 +0 -0
  910. package/src/styles/fonts/noto-serif-sc/700/8555f0285e3d28e95e2fc0ccccd9caff.woff2 +0 -0
  911. package/src/styles/fonts/noto-serif-sc/700/8e04e64c8f68d292a18d4160fbde8671.woff2 +0 -0
  912. package/src/styles/fonts/noto-serif-sc/700/958efb9b2fa2ea0008ffef009885f9f8.woff2 +0 -0
  913. package/src/styles/fonts/noto-serif-sc/700/9ca9b71010a5faeee7047ef97aeee13b.woff2 +0 -0
  914. package/src/styles/fonts/noto-serif-sc/700/9cd0b77920b9d6c64eb686493123fc76.woff2 +0 -0
  915. package/src/styles/fonts/noto-serif-sc/700/9ebd27835ffcbd794e67151ab046ce68.woff2 +0 -0
  916. package/src/styles/fonts/noto-serif-sc/700/a077f51cfb5cffb4ff4d8e229c0e9e79.woff2 +0 -0
  917. package/src/styles/fonts/noto-serif-sc/700/a397997b579d3945c9c70a979c17a8ad.woff2 +0 -0
  918. package/src/styles/fonts/noto-serif-sc/700/a68d9d5027803832bb28e78cdcd04949.woff2 +0 -0
  919. package/src/styles/fonts/noto-serif-sc/700/a904b05966368bcf90b632c7c2e5f76b.woff2 +0 -0
  920. package/src/styles/fonts/noto-serif-sc/700/aa218a2c45f3749537ce876201e5152b.woff2 +0 -0
  921. package/src/styles/fonts/noto-serif-sc/700/aa28db16818f9eaa8c817f289e1c3270.woff2 +0 -0
  922. package/src/styles/fonts/noto-serif-sc/700/aa96d698491c2540e2dcf7009c65c456.woff2 +0 -0
  923. package/src/styles/fonts/noto-serif-sc/700/ae25c41034ddc1a9e0b41f5034c9aa4b.woff2 +0 -0
  924. package/src/styles/fonts/noto-serif-sc/700/ae289ae3f8cdb54a3a6c07174517afec.woff2 +0 -0
  925. package/src/styles/fonts/noto-serif-sc/700/b02dfa2aa52cbdb1b2f11a9f44335469.woff2 +0 -0
  926. package/src/styles/fonts/noto-serif-sc/700/b2e326f7f9b807451bf9c745df747efe.woff2 +0 -0
  927. package/src/styles/fonts/noto-serif-sc/700/c33c59feccf391f0c5f1f5d24e36d1fe.woff2 +0 -0
  928. package/src/styles/fonts/noto-serif-sc/700/c82fd9456d7465b5e5bd3659e9b14c55.woff2 +0 -0
  929. package/src/styles/fonts/noto-serif-sc/700/c90b7b65d2b9696fbf3a506738f94d68.woff2 +0 -0
  930. package/src/styles/fonts/noto-serif-sc/700/cba6ad3981cb7861428d4be169ee8124.woff2 +0 -0
  931. package/src/styles/fonts/noto-serif-sc/700/cc26525aa2af1f0b929af32ce50a7fba.woff2 +0 -0
  932. package/src/styles/fonts/noto-serif-sc/700/cd0e7b51eddb22a77a09b025c0281434.woff2 +0 -0
  933. package/src/styles/fonts/noto-serif-sc/700/cd6d074f3957d58bac58437fc97e5e33.woff2 +0 -0
  934. package/src/styles/fonts/noto-serif-sc/700/cef249b6d013fb0cc0d574176bc23811.woff2 +0 -0
  935. package/src/styles/fonts/noto-serif-sc/700/d0bd387fda28e58d3c9b3efa2468dd8a.woff2 +0 -0
  936. package/src/styles/fonts/noto-serif-sc/700/da93ae099ff3b7aae27b3f674d3fc721.woff2 +0 -0
  937. package/src/styles/fonts/noto-serif-sc/700/e264213b9e102dabc603adb6e4fda5e6.woff2 +0 -0
  938. package/src/styles/fonts/noto-serif-sc/700/e7c7ef3669ae48c0a736f06ca471e1d7.woff2 +0 -0
  939. package/src/styles/fonts/noto-serif-sc/700/e81a742cacef744130c40de1b90837d8.woff2 +0 -0
  940. package/src/styles/fonts/noto-serif-sc/700/e8b755172122d1d0a5dd453e96b0ff24.woff2 +0 -0
  941. package/src/styles/fonts/noto-serif-sc/700/e99280299c305402eaa5271a3e36c49b.woff2 +0 -0
  942. package/src/styles/fonts/noto-serif-sc/700/e9c66b085052ece66bfadf45f711d3e1.woff2 +0 -0
  943. package/src/styles/fonts/noto-serif-sc/700/ed7c6dafaa6d8bcf015ef0ca574837df.woff2 +0 -0
  944. package/src/styles/fonts/noto-serif-sc/700/f2900a1d30c3a33129f4e2225669bd0e.woff2 +0 -0
  945. package/src/styles/fonts/noto-serif-sc/700/fa7d3b99744d7f2dc9e00864a97a62d6.woff2 +0 -0
  946. package/src/styles/fonts/noto-serif-sc/700/fb0e90665980954719c2eb685b130bc0.woff2 +0 -0
  947. package/src/styles/fonts/noto-serif-sc/noto-serif-sc.css +3615 -0
  948. package/src/styles/fonts/noto-serif-tc/400/008ea9091e332c639ceb18874eacd60c.woff2 +0 -0
  949. package/src/styles/fonts/noto-serif-tc/400/053bd3d7aec0040d0cc50c261a1f4e3e.woff2 +0 -0
  950. package/src/styles/fonts/noto-serif-tc/400/0713613227cc4c686c45a279f8bdc166.woff2 +0 -0
  951. package/src/styles/fonts/noto-serif-tc/400/0e97f44ebc65384c346fe19bcc52fa20.woff2 +0 -0
  952. package/src/styles/fonts/noto-serif-tc/400/1139d32ae2bdeb26c0c8f31330aa9a9f.woff2 +0 -0
  953. package/src/styles/fonts/noto-serif-tc/400/15f8c0df47fd639d1b0d9bd5cf507c9b.woff2 +0 -0
  954. package/src/styles/fonts/noto-serif-tc/400/1668bd859ffe15bed7d5563117d8d5fb.woff2 +0 -0
  955. package/src/styles/fonts/noto-serif-tc/400/190e3f8632494e7c095117f26b1c811e.woff2 +0 -0
  956. package/src/styles/fonts/noto-serif-tc/400/19ad151c22ce1befe0a9ea643fbee570.woff2 +0 -0
  957. package/src/styles/fonts/noto-serif-tc/400/1c820b5295868008ca7c78afa5b7655d.woff2 +0 -0
  958. package/src/styles/fonts/noto-serif-tc/400/1fbe225742c69f4ba9ea5f74922f0ca1.woff2 +0 -0
  959. package/src/styles/fonts/noto-serif-tc/400/2a7cedfcd6e4c7cec36f4fd7b0f329c2.woff2 +0 -0
  960. package/src/styles/fonts/noto-serif-tc/400/2acea04a920f6af31e7db97052f563c6.woff2 +0 -0
  961. package/src/styles/fonts/noto-serif-tc/400/2e98b666924b8e0a09d1aeeefd24bdd2.woff2 +0 -0
  962. package/src/styles/fonts/noto-serif-tc/400/2fd3fceb6faed5e3db768e88d7614dca.woff2 +0 -0
  963. package/src/styles/fonts/noto-serif-tc/400/35cf5dd04315e0b906e1a413d7905a2f.woff2 +0 -0
  964. package/src/styles/fonts/noto-serif-tc/400/387c811226f303af62f1e21aae6f5c83.woff2 +0 -0
  965. package/src/styles/fonts/noto-serif-tc/400/3b41385fc27419c19822060daa0b5cb3.woff2 +0 -0
  966. package/src/styles/fonts/noto-serif-tc/400/3cbe4a697fd595ef42c899de7d3e5445.woff2 +0 -0
  967. package/src/styles/fonts/noto-serif-tc/400/3d83dacbbec3d8532ae9afede21f3aab.woff2 +0 -0
  968. package/src/styles/fonts/noto-serif-tc/400/47479c470fae70f10b7c964a7ecbf274.woff2 +0 -0
  969. package/src/styles/fonts/noto-serif-tc/400/4dc0728df0f2ba70796f45f05654c7ba.woff2 +0 -0
  970. package/src/styles/fonts/noto-serif-tc/400/4dc2bc2c55b47f57d13b63aa6b1c8bd4.woff2 +0 -0
  971. package/src/styles/fonts/noto-serif-tc/400/4e1cc6aafb411b572c8d3511e925ecf1.woff2 +0 -0
  972. package/src/styles/fonts/noto-serif-tc/400/5227dbe9933760a48baff21ebd13fc98.woff2 +0 -0
  973. package/src/styles/fonts/noto-serif-tc/400/526b263e72c189f4b065738aaa6d423a.woff2 +0 -0
  974. package/src/styles/fonts/noto-serif-tc/400/54da934819a917f561b439bfd10f88b6.woff2 +0 -0
  975. package/src/styles/fonts/noto-serif-tc/400/5bfc7a121c35ae42623ef804fb525e0e.woff2 +0 -0
  976. package/src/styles/fonts/noto-serif-tc/400/5f90024544c2907c6c0203c6210c50be.woff2 +0 -0
  977. package/src/styles/fonts/noto-serif-tc/400/649b12d7cee7bb981842946e4547e6ca.woff2 +0 -0
  978. package/src/styles/fonts/noto-serif-tc/400/653bef2ed891ae48d8ed712283080649.woff2 +0 -0
  979. package/src/styles/fonts/noto-serif-tc/400/67d2a81f06ba352f17fbdc3a5e6ea59e.woff2 +0 -0
  980. package/src/styles/fonts/noto-serif-tc/400/68304f3229cf763465f044fccb5892c0.woff2 +0 -0
  981. package/src/styles/fonts/noto-serif-tc/400/688a88911e4da17b609196a959b8b930.woff2 +0 -0
  982. package/src/styles/fonts/noto-serif-tc/400/6db6ddf72c38a78ce44c1327701152e1.woff2 +0 -0
  983. package/src/styles/fonts/noto-serif-tc/400/77a7533bd21ccd33192d142a93555aa8.woff2 +0 -0
  984. package/src/styles/fonts/noto-serif-tc/400/7d65a3d6a65050eb5e6eca43398aeba4.woff2 +0 -0
  985. package/src/styles/fonts/noto-serif-tc/400/7dfc711962c8771f97e7c8898a6bcb65.woff2 +0 -0
  986. package/src/styles/fonts/noto-serif-tc/400/7ef123b62d530fcba73974fa265e0aae.woff2 +0 -0
  987. package/src/styles/fonts/noto-serif-tc/400/80466082a896fd328f30a78593c7c568.woff2 +0 -0
  988. package/src/styles/fonts/noto-serif-tc/400/8a3c84b0df36f851f5fea75ee8757951.woff2 +0 -0
  989. package/src/styles/fonts/noto-serif-tc/400/8b0c8c9f8cfa9fa090d97c5a5efb1f4c.woff2 +0 -0
  990. package/src/styles/fonts/noto-serif-tc/400/8dc035a34c76e6515ca203e2df182588.woff2 +0 -0
  991. package/src/styles/fonts/noto-serif-tc/400/8eb06109812cb80be44f47b8179c2709.woff2 +0 -0
  992. package/src/styles/fonts/noto-serif-tc/400/904324af375d5fd370af1054355a050e.woff2 +0 -0
  993. package/src/styles/fonts/noto-serif-tc/400/911a2092d64d6d6494b254d819af2b91.woff2 +0 -0
  994. package/src/styles/fonts/noto-serif-tc/400/9de02d745b8e25c6411fb152fb067748.woff2 +0 -0
  995. package/src/styles/fonts/noto-serif-tc/400/9eb33a430058d839ebbe2af4b2e0daa9.woff2 +0 -0
  996. package/src/styles/fonts/noto-serif-tc/400/9f5a73aa8ba417688019d628f334db07.woff2 +0 -0
  997. package/src/styles/fonts/noto-serif-tc/400/a0f0c06d5c7a3ffa97706178cce212a8.woff2 +0 -0
  998. package/src/styles/fonts/noto-serif-tc/400/a38c1830367f784181b6f544b0b11bbd.woff2 +0 -0
  999. package/src/styles/fonts/noto-serif-tc/400/a9cf85e27428c14351d30eac8cbc8d91.woff2 +0 -0
  1000. package/src/styles/fonts/noto-serif-tc/400/aa0ce6740f301351761a0615cc8b2e99.woff2 +0 -0
  1001. package/src/styles/fonts/noto-serif-tc/400/bc3f0cb8b55ee11d32b94ca488976f8d.woff2 +0 -0
  1002. package/src/styles/fonts/noto-serif-tc/400/bcb3307527d6d0033bf0f17660b91e71.woff2 +0 -0
  1003. package/src/styles/fonts/noto-serif-tc/400/bf1acc86e17b4229c548828a9d6f455d.woff2 +0 -0
  1004. package/src/styles/fonts/noto-serif-tc/400/c3fbc1f2557c343863a10698f8c966a2.woff2 +0 -0
  1005. package/src/styles/fonts/noto-serif-tc/400/c5c1c0be944ea39a3f50a02d32f5b759.woff2 +0 -0
  1006. package/src/styles/fonts/noto-serif-tc/400/c5f1075caf6d1344ee720de85114a521.woff2 +0 -0
  1007. package/src/styles/fonts/noto-serif-tc/400/cae29b3f8951eaf20d2f61c2206e28d9.woff2 +0 -0
  1008. package/src/styles/fonts/noto-serif-tc/400/d043b8d7a48bb0ac59ee1f1477d88eee.woff2 +0 -0
  1009. package/src/styles/fonts/noto-serif-tc/400/d320b000b5978c7251148a6a154741b8.woff2 +0 -0
  1010. package/src/styles/fonts/noto-serif-tc/400/da13b136efb1d1e4c76575af8f79a698.woff2 +0 -0
  1011. package/src/styles/fonts/noto-serif-tc/400/da2cf0ec56bf69374ee37764c7e3ea3d.woff2 +0 -0
  1012. package/src/styles/fonts/noto-serif-tc/400/daf62255dd60679946f28c442ca62533.woff2 +0 -0
  1013. package/src/styles/fonts/noto-serif-tc/400/dd01a1035345f6921a48525b8ce08f06.woff2 +0 -0
  1014. package/src/styles/fonts/noto-serif-tc/400/e4fb59479cedc87ba79785590bf861ca.woff2 +0 -0
  1015. package/src/styles/fonts/noto-serif-tc/400/e5d00355f73293d40b61299459d17ca5.woff2 +0 -0
  1016. package/src/styles/fonts/noto-serif-tc/400/e647b8d2efc501c0cc0e407249cc7135.woff2 +0 -0
  1017. package/src/styles/fonts/noto-serif-tc/400/e6e60ffb2ebd1828628764b507060aea.woff2 +0 -0
  1018. package/src/styles/fonts/noto-serif-tc/400/f00eb499abb94fa7b799d6d8c9b050e9.woff2 +0 -0
  1019. package/src/styles/fonts/noto-serif-tc/400/f7d36ffff7a75c9c6216d576a57dd00d.woff2 +0 -0
  1020. package/src/styles/fonts/noto-serif-tc/400/f7f3f63e7a149cd89eccab3b52171d05.woff2 +0 -0
  1021. package/src/styles/fonts/noto-serif-tc/400/fcc41f6a067ddd658bba5c9dff234a32.woff2 +0 -0
  1022. package/src/styles/fonts/noto-serif-tc/400/fd6ad889fcf3583bd9b0b6db53aad434.woff2 +0 -0
  1023. package/src/styles/fonts/noto-serif-tc/700/0c055db157e7a13f3103cc2a6b67fec3.woff2 +0 -0
  1024. package/src/styles/fonts/noto-serif-tc/700/0d3f5cc265cb6c439c517f2c4cebbddf.woff2 +0 -0
  1025. package/src/styles/fonts/noto-serif-tc/700/1259e5825b314fe2b8bb96d6e8069ee5.woff2 +0 -0
  1026. package/src/styles/fonts/noto-serif-tc/700/12c518ebfe62818af550c08947e359e7.woff2 +0 -0
  1027. package/src/styles/fonts/noto-serif-tc/700/145831a59caa06d894022fe60212ed21.woff2 +0 -0
  1028. package/src/styles/fonts/noto-serif-tc/700/169a096e61d38a773216f51d1ec2cc06.woff2 +0 -0
  1029. package/src/styles/fonts/noto-serif-tc/700/1884a2b22d314c7d57707f03aec348e0.woff2 +0 -0
  1030. package/src/styles/fonts/noto-serif-tc/700/1e2640116bbba817f43c43cc69371cf1.woff2 +0 -0
  1031. package/src/styles/fonts/noto-serif-tc/700/2573703213da30d3ba18925b100b2c2b.woff2 +0 -0
  1032. package/src/styles/fonts/noto-serif-tc/700/26839c0e47c73514b8d8f660d24d6b19.woff2 +0 -0
  1033. package/src/styles/fonts/noto-serif-tc/700/2a22e14a9ad53f2abb3c7e85017b7d12.woff2 +0 -0
  1034. package/src/styles/fonts/noto-serif-tc/700/2b3e8c5703b91f39f6027f43f0da6f4b.woff2 +0 -0
  1035. package/src/styles/fonts/noto-serif-tc/700/2f27ee4fb2cf6a280e110e09c18ef73e.woff2 +0 -0
  1036. package/src/styles/fonts/noto-serif-tc/700/31342cebfa5ea7fac06b4ea372d96bc5.woff2 +0 -0
  1037. package/src/styles/fonts/noto-serif-tc/700/375329ba0b50b94b35006498e555867c.woff2 +0 -0
  1038. package/src/styles/fonts/noto-serif-tc/700/427577dcb707d1d35eebd155b4222aa7.woff2 +0 -0
  1039. package/src/styles/fonts/noto-serif-tc/700/450a5b53be0a8a778bb0b623e86b652f.woff2 +0 -0
  1040. package/src/styles/fonts/noto-serif-tc/700/477866c8396474a17317dcac3e7a014f.woff2 +0 -0
  1041. package/src/styles/fonts/noto-serif-tc/700/478ebdaadda7775c391c5dcab4e697df.woff2 +0 -0
  1042. package/src/styles/fonts/noto-serif-tc/700/48d6a97a185c799be4fe67aaf7edf213.woff2 +0 -0
  1043. package/src/styles/fonts/noto-serif-tc/700/490edb9fc8a4356aea556eed32287464.woff2 +0 -0
  1044. package/src/styles/fonts/noto-serif-tc/700/4c4bdd0b3f3a52e28f3b643c1c5d43be.woff2 +0 -0
  1045. package/src/styles/fonts/noto-serif-tc/700/4c96411f3693a9a8657a9c1190f82bce.woff2 +0 -0
  1046. package/src/styles/fonts/noto-serif-tc/700/4c9aa12aba2a6a57410eacaff7427916.woff2 +0 -0
  1047. package/src/styles/fonts/noto-serif-tc/700/4cca7233bf8ce5dec2e5d146b993d626.woff2 +0 -0
  1048. package/src/styles/fonts/noto-serif-tc/700/4d0a9128d06ea857f203bf5d007b1ab9.woff2 +0 -0
  1049. package/src/styles/fonts/noto-serif-tc/700/4e5384920bbb155d9d8d74887b09ea5b.woff2 +0 -0
  1050. package/src/styles/fonts/noto-serif-tc/700/50cfd672bfa62512ba090420acf35c87.woff2 +0 -0
  1051. package/src/styles/fonts/noto-serif-tc/700/551b1d7a0b80c8d42af09863cdca7f01.woff2 +0 -0
  1052. package/src/styles/fonts/noto-serif-tc/700/555d990ab3fd7d3d66c6d1fa9a82fec5.woff2 +0 -0
  1053. package/src/styles/fonts/noto-serif-tc/700/5979c33a7eb5963bf8e83e46931b5fb5.woff2 +0 -0
  1054. package/src/styles/fonts/noto-serif-tc/700/59966ee0b069b577510fe68c350da0ee.woff2 +0 -0
  1055. package/src/styles/fonts/noto-serif-tc/700/60a14064ed334f0155795d795e926abe.woff2 +0 -0
  1056. package/src/styles/fonts/noto-serif-tc/700/611b62d5fd9698d9b5ce495ba6f14c93.woff2 +0 -0
  1057. package/src/styles/fonts/noto-serif-tc/700/6e83fe0b6e708eaf1c3003d6dee11488.woff2 +0 -0
  1058. package/src/styles/fonts/noto-serif-tc/700/70861376e5d4f92f8aa7aa1b2749b617.woff2 +0 -0
  1059. package/src/styles/fonts/noto-serif-tc/700/7124d150570d39ced8d45507dc11ca1e.woff2 +0 -0
  1060. package/src/styles/fonts/noto-serif-tc/700/79a7fdf7d9c722b5723ae25e6ff8e203.woff2 +0 -0
  1061. package/src/styles/fonts/noto-serif-tc/700/8c8393bc875f1ee36697a2113f4421ea.woff2 +0 -0
  1062. package/src/styles/fonts/noto-serif-tc/700/8e6c9bb43afb8cbbff7cf1055e67c9bd.woff2 +0 -0
  1063. package/src/styles/fonts/noto-serif-tc/700/90ac4f9d2aa02afdace2843b49fc18bb.woff2 +0 -0
  1064. package/src/styles/fonts/noto-serif-tc/700/90b6f57d77847f512fd11db74fa912f1.woff2 +0 -0
  1065. package/src/styles/fonts/noto-serif-tc/700/913759e6690f9fc0746a20b96f4bdcb4.woff2 +0 -0
  1066. package/src/styles/fonts/noto-serif-tc/700/9154e26efe532a85a27d80902f5a2d6c.woff2 +0 -0
  1067. package/src/styles/fonts/noto-serif-tc/700/94e7ed67f1557b76fead6b6e456a0415.woff2 +0 -0
  1068. package/src/styles/fonts/noto-serif-tc/700/95127a92346c04fec1fa81d6295b0a28.woff2 +0 -0
  1069. package/src/styles/fonts/noto-serif-tc/700/9fbc06b2e3ff16b9d705c76db563ef17.woff2 +0 -0
  1070. package/src/styles/fonts/noto-serif-tc/700/a3b929542e6c5a0644b73a7c8a8b6c03.woff2 +0 -0
  1071. package/src/styles/fonts/noto-serif-tc/700/a8857f5d478f101c053ba02d2f223e90.woff2 +0 -0
  1072. package/src/styles/fonts/noto-serif-tc/700/aa64c9953af43ca65832f413895bb433.woff2 +0 -0
  1073. package/src/styles/fonts/noto-serif-tc/700/ada8f0241244c60ec8d3d59ad37f20a5.woff2 +0 -0
  1074. package/src/styles/fonts/noto-serif-tc/700/b341de0bc0bfe194a6c28dcfb566029e.woff2 +0 -0
  1075. package/src/styles/fonts/noto-serif-tc/700/b846c293981ca5429eabaa967f222f26.woff2 +0 -0
  1076. package/src/styles/fonts/noto-serif-tc/700/be64f9379412876e00fd3a0bfa6b6fe9.woff2 +0 -0
  1077. package/src/styles/fonts/noto-serif-tc/700/befed8a4fa817773fa7109db6fe07f56.woff2 +0 -0
  1078. package/src/styles/fonts/noto-serif-tc/700/c09ee2b219982f8d46ad9968b7e6e0ba.woff2 +0 -0
  1079. package/src/styles/fonts/noto-serif-tc/700/c39ec937c6a8d124e8b68cf829ea5ad4.woff2 +0 -0
  1080. package/src/styles/fonts/noto-serif-tc/700/c3fd21315345ae541f6e98067059fa19.woff2 +0 -0
  1081. package/src/styles/fonts/noto-serif-tc/700/c568a16e3168ceb1f191b70022c492ea.woff2 +0 -0
  1082. package/src/styles/fonts/noto-serif-tc/700/c5e66d60be3375835bbd8d6b797f6eac.woff2 +0 -0
  1083. package/src/styles/fonts/noto-serif-tc/700/cd10a3af2133805d8c92104d1ee6ff18.woff2 +0 -0
  1084. package/src/styles/fonts/noto-serif-tc/700/d15a3317942b7d31978a759fbf2222c8.woff2 +0 -0
  1085. package/src/styles/fonts/noto-serif-tc/700/d740dc2e854aaa7b3dcdd3ed25455eeb.woff2 +0 -0
  1086. package/src/styles/fonts/noto-serif-tc/700/d8325ba7ae651bc30440905bd67b95f1.woff2 +0 -0
  1087. package/src/styles/fonts/noto-serif-tc/700/e2204cf85edcb96c5de5c3dcf240da9d.woff2 +0 -0
  1088. package/src/styles/fonts/noto-serif-tc/700/e3e913e145ddcd9323b2a0972967feb6.woff2 +0 -0
  1089. package/src/styles/fonts/noto-serif-tc/700/f2fb1f1fbf7e44afb53c672ec286a22e.woff2 +0 -0
  1090. package/src/styles/fonts/noto-serif-tc/700/f372129c60aaece937cf7b91ee75c9b8.woff2 +0 -0
  1091. package/src/styles/fonts/noto-serif-tc/700/f5d7487963d43c89da63aaf10f2e6fb7.woff2 +0 -0
  1092. package/src/styles/fonts/noto-serif-tc/700/f75496953a40ff241178240209f56990.woff2 +0 -0
  1093. package/src/styles/fonts/noto-serif-tc/700/f92d74d1d217d21b39075ff23f79f7fd.woff2 +0 -0
  1094. package/src/styles/fonts/noto-serif-tc/700/f9d6d981d8b87b3e469027277f585741.woff2 +0 -0
  1095. package/src/styles/fonts/noto-serif-tc/700/fa8ed469ef290bfeb571418fe0abb628.woff2 +0 -0
  1096. package/src/styles/fonts/noto-serif-tc/700/fb61b690208eff56e6d8432951270901.woff2 +0 -0
  1097. package/src/styles/fonts/noto-serif-tc/700/ff0937ad63cda71ff420945ead55ab4d.woff2 +0 -0
  1098. package/src/styles/fonts/noto-serif-tc/noto-serif-tc.css +3104 -0
  1099. package/src/styles/tokens.css +42 -7
  1100. package/src/styles/ui.css +7597 -3395
  1101. package/src/types/app-context.ts +7 -0
  1102. package/src/types/bindings.ts +77 -27
  1103. package/src/types/config.ts +104 -4
  1104. package/src/types/constants.ts +43 -4
  1105. package/src/types/entities.ts +63 -4
  1106. package/src/types/operations.ts +45 -15
  1107. package/src/types/props.ts +48 -10
  1108. package/src/types/views.ts +51 -8
  1109. package/src/ui/__tests__/color-themes.test.ts +81 -0
  1110. package/src/ui/__tests__/font-themes.test.ts +76 -7
  1111. package/src/ui/color-themes.ts +424 -238
  1112. package/src/ui/compose/ComposeDialog.tsx +149 -91
  1113. package/src/ui/compose/ComposePrompt.tsx +20 -3
  1114. package/src/ui/dash/ActionButtons.tsx +16 -2
  1115. package/src/ui/dash/DangerZone.tsx +10 -1
  1116. package/src/ui/dash/StatusBadge.tsx +3 -3
  1117. package/src/ui/dash/appearance/AdvancedContent.tsx +9 -2
  1118. package/src/ui/dash/appearance/ColorThemeContent.tsx +326 -63
  1119. package/src/ui/dash/appearance/FontThemeContent.tsx +75 -17
  1120. package/src/ui/dash/appearance/NavigationContent.tsx +48 -21
  1121. package/src/ui/dash/settings/AccountContent.tsx +7 -2
  1122. package/src/ui/dash/settings/AccountMenuContent.tsx +242 -111
  1123. package/src/ui/dash/settings/ApiTokensContent.tsx +38 -8
  1124. package/src/ui/dash/settings/AvatarContent.tsx +1 -1
  1125. package/src/ui/dash/settings/DeleteAccountContent.tsx +300 -0
  1126. package/src/ui/dash/settings/GeneralContent.tsx +120 -6
  1127. package/src/ui/dash/settings/SessionsContent.tsx +23 -5
  1128. package/src/ui/dash/settings/SettingsDirectory.tsx +99 -0
  1129. package/src/ui/dash/settings/SettingsRootContent.tsx +176 -221
  1130. package/src/ui/feed/CuratedThreadPreview.tsx +73 -0
  1131. package/src/ui/feed/LinkCard.tsx +52 -36
  1132. package/src/ui/feed/NoteCard.tsx +10 -4
  1133. package/src/ui/feed/PostStatusBadges.tsx +4 -21
  1134. package/src/ui/feed/QuoteCard.tsx +11 -3
  1135. package/src/ui/feed/ThreadPreview.tsx +11 -5
  1136. package/src/ui/feed/TimelineFeed.tsx +53 -15
  1137. package/src/ui/feed/__tests__/timeline-cards.test.ts +172 -0
  1138. package/src/ui/feed/thread-preview-state.ts +2 -4
  1139. package/src/ui/font-themes.ts +211 -43
  1140. package/src/ui/layouts/BaseLayout.tsx +161 -30
  1141. package/src/ui/layouts/SiteLayout.tsx +51 -18
  1142. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +169 -0
  1143. package/src/ui/pages/ArchivePage.tsx +320 -120
  1144. package/src/ui/pages/BrandPage.tsx +927 -0
  1145. package/src/ui/pages/CollectionEditorPage.tsx +155 -0
  1146. package/src/ui/pages/CollectionPage.tsx +482 -23
  1147. package/src/ui/pages/CollectionsPage.tsx +57 -57
  1148. package/src/ui/pages/FeaturedPage.tsx +24 -3
  1149. package/src/ui/pages/HomePage.tsx +13 -0
  1150. package/src/ui/pages/PostPage.tsx +13 -4
  1151. package/src/ui/pages/SearchPage.tsx +12 -2
  1152. package/src/ui/pages/ThemeSamplePage.tsx +1160 -0
  1153. package/src/ui/shared/AdminBreadcrumb.tsx +22 -6
  1154. package/src/ui/shared/CollectionDirectory.tsx +131 -0
  1155. package/src/ui/shared/CollectionsManager.tsx +277 -0
  1156. package/src/ui/shared/DecorativeQuoteMark.tsx +32 -0
  1157. package/src/ui/shared/HomePageBranding.tsx +22 -0
  1158. package/src/ui/shared/JantBrandMark.tsx +35 -0
  1159. package/src/ui/shared/MediaGallery.tsx +106 -36
  1160. package/src/ui/shared/PaginatedPageHeader.tsx +49 -0
  1161. package/src/ui/shared/Pagination.tsx +2 -6
  1162. package/src/ui/shared/PostFooter.tsx +130 -67
  1163. package/src/ui/shared/__tests__/home-page-branding.test.tsx +14 -0
  1164. package/src/ui/shared/__tests__/media-gallery.test.ts +82 -0
  1165. package/src/ui/shared/__tests__/navigation-labels.test.ts +103 -0
  1166. package/src/ui/shared/__tests__/pagination.test.ts +18 -1
  1167. package/src/ui/shared/__tests__/post-footer.test.ts +122 -0
  1168. package/src/ui/shared/collection-management-labels.ts +161 -0
  1169. package/src/ui/shared/navigation-labels.ts +126 -0
  1170. package/src/vendor/datastar.js +1955 -7
  1171. package/dist/client/assets/heic-to-DIRPI3VF.js +0 -1
  1172. package/dist/client/assets/module-RjUF93sV.js +0 -716
  1173. package/dist/client/assets/native-48B9X9Wg.js +0 -1
  1174. package/dist/client/assets/url-FWFqPJPb.js +0 -1
  1175. package/dist/client/client.css +0 -1
  1176. package/dist/client/client.js +0 -33565
  1177. package/src/client/components/collection-sidebar-types.ts +0 -43
  1178. package/src/client/tiptap/paste-image.ts +0 -129
  1179. package/src/lib/emoji-catalog.ts +0 -963
  1180. package/src/lib/icon-catalog.ts +0 -5213
  1181. package/src/ui/shared/CollectionsSidebar.tsx +0 -294
@@ -1,6 +1,8 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
3
  import { describe, it, expect, beforeEach, vi } from "vitest";
4
+ import type { Editor } from "@tiptap/core";
5
+
4
6
  import type {
5
7
  ComposeLabels,
6
8
  ComposeCollection,
@@ -21,6 +23,46 @@ function requireElement<T extends globalThis.Element>(
21
23
  return element;
22
24
  }
23
25
 
26
+ function requireEditor(el: JantComposeEditor): Editor {
27
+ const editor = (el as unknown as { _editor?: Editor | null })._editor;
28
+ if (!editor) {
29
+ throw new Error("expected compose editor instance");
30
+ }
31
+ return editor;
32
+ }
33
+
34
+ async function flushUpdates(el?: JantComposeDialog) {
35
+ await Promise.resolve();
36
+ await Promise.resolve();
37
+ if (el) {
38
+ await el.updateComplete;
39
+ }
40
+ }
41
+
42
+ function mockSlugApi(
43
+ handler: (url: URL) => { status?: number; body: unknown },
44
+ ): ReturnType<typeof vi.spyOn> {
45
+ return vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
46
+ const raw =
47
+ typeof input === "string"
48
+ ? input
49
+ : input instanceof URL
50
+ ? input.toString()
51
+ : input.url;
52
+ const url = new URL(raw, "http://localhost");
53
+
54
+ if (url.pathname === "/api/posts/slug") {
55
+ const { status = 200, body } = handler(url);
56
+ return new Response(JSON.stringify(body), {
57
+ status,
58
+ headers: { "Content-Type": "application/json" },
59
+ });
60
+ }
61
+
62
+ throw new Error(`Unexpected fetch: ${url.pathname}${url.search}`);
63
+ });
64
+ }
65
+
24
66
  const labels: ComposeLabels = {
25
67
  cancel: "Cancel",
26
68
  note: "Note",
@@ -32,6 +74,9 @@ const labels: ComposeLabels = {
32
74
  titlePlaceholder: "Title",
33
75
  bodyPlaceholder: "What's on your mind...",
34
76
  urlPlaceholder: "Paste a URL...",
77
+ urlInvalid: "Enter a valid URL starting with http://, https://, or mailto:.",
78
+ linkUrlRequired: "Add a URL before posting this link.",
79
+ linkTitleRequired: "Add a title before posting this link.",
35
80
  linkTitlePlaceholder: "Give it a title...",
36
81
  thoughtsPlaceholder: "Your thoughts (optional)",
37
82
  quotePlaceholder: "Type the quote...",
@@ -45,9 +90,10 @@ const labels: ComposeLabels = {
45
90
  rate: "Rate",
46
91
  emoji: "Emoji",
47
92
  title: "Title",
93
+ fullscreen: "Fullscreen",
48
94
  collection: "Collection",
49
95
  searchCollections: "Search...",
50
- noCollections: "No collections found.",
96
+ noCollections: "No collections match that search. Try a different name.",
51
97
  emptyCollections: "Create a collection to get started.",
52
98
  post: "Post",
53
99
  addAlt: "+ ALT",
@@ -55,6 +101,7 @@ const labels: ComposeLabels = {
55
101
  altPlaceholder: "Describe this...",
56
102
  altHint: "Alt text improves accessibility",
57
103
  addMore: "Add",
104
+ removeAttachment: "Remove attachment",
58
105
  uploading: "Uploading...",
59
106
  published: "Published!",
60
107
  view: "View",
@@ -66,10 +113,16 @@ const labels: ComposeLabels = {
66
113
  confirmCloseSave: "Save",
67
114
  confirmCloseCancel: "Cancel",
68
115
  confirmCloseDiscard: "Don't save",
116
+ confirmAttachedTitle: "Save text attachment?",
117
+ confirmAttachedSubtitle:
118
+ "Save these changes to the text attachment, discard them, or keep editing.",
119
+ confirmAttachedSave: "Save",
120
+ confirmAttachedDiscard: "Don't save",
69
121
  confirmEditTitle: "You have unsaved changes",
70
122
  confirmEditSubtitle: "Do you want to publish your changes or discard them?",
71
123
  confirmEditPublish: "Publish",
72
124
  confirmEditDiscard: "Discard",
125
+ discardChangesConfirm: "Discard changes?",
73
126
  drafts: "Drafts",
74
127
  draftsEmpty: "No drafts yet. Save a draft to find it here.",
75
128
  deleteDraft: "Delete Draft",
@@ -80,36 +133,58 @@ const labels: ComposeLabels = {
80
133
  collectionCountLabel: "%name% + %count% more",
81
134
  draftRestored: "Draft restored.",
82
135
  reply: "Reply",
83
- publishFeatured: "Post as Featured",
84
- publishUnlisted: "Post Unlisted",
136
+ publishHideFromLatest: "Hide from Latest",
85
137
  publishPrivate: "Post as Private",
138
+ publishSettings: "Publish settings",
139
+ publishVisibilityLabel: "Visibility",
140
+ publishVisibilityPublic: "Public",
141
+ publishVisibilityPublicHint: "Appears in Latest.",
142
+ publishVisibilityHiddenFromLatest: "Hidden from Latest",
143
+ publishVisibilityHiddenFromLatestHint:
144
+ "Doesn't appear in Latest. Still appears in collections you add it to.",
145
+ publishVisibilityPrivate: "Private",
146
+ publishVisibilityPrivateHint: "Only visible when signed in.",
147
+ publishSlugLabel: "Custom link",
148
+ publishSlugPlaceholder: "your-post-link",
149
+ publishSlugHint: "Leave blank to generate one automatically.",
150
+ publishSlugAuto: "Generate automatically",
151
+ publishSlugReset: "Reset link",
152
+ publishSlugSuggested: "Suggested link",
153
+ publishSlugGenerating: "Generating a link...",
154
+ publishSlugChecking: "Checking link...",
155
+ publishSlugTaken: "This link is already in use. Choose something else.",
156
+ publishSlugInvalid: "Use lowercase letters, numbers, and hyphens only.",
157
+ publishSlugReserved: "This link is reserved. Choose something else.",
158
+ postHiddenFromLatest: "Post hidden",
159
+ postPrivately: "Post privately",
86
160
  showMore: "Show more",
87
161
  showLess: "Show less",
88
162
  collectionFormLabels: {
89
163
  titleLabel: "Title",
90
164
  titlePlaceholder: "My Collection",
91
- slugLabel: "Slug",
92
- slugHelp: "URL-safe identifier",
165
+ slugLabel: "Collection link",
166
+ slugHelp: "This is the last part of the collection link.",
167
+ slugInvalidHelp: "Use lowercase letters, numbers, and hyphens only.",
168
+ slugReservedHelp: "This link is reserved. Choose something else.",
169
+ editSlugLabel: "Edit link",
170
+ resetSlugLabel: "Reset link",
171
+ quickHint: "More options are available after you create it.",
172
+ quickSubmitLabel: "Done",
173
+ createdLabel: "Collection created.",
93
174
  descriptionLabel: "Description (optional)",
94
175
  descriptionPlaceholder: "What's this collection about?",
95
- removeIcon: "Remove",
96
- iconsTab: "Icons",
97
- emojisTab: "Emojis",
98
- searchIconsPlaceholder: "Search icons...",
99
- searchEmojisPlaceholder: "Search emojis...",
100
176
  sortOrderLabel: "Sort Order",
101
177
  sortNewest: "Newest first",
102
178
  sortOldest: "Oldest first",
103
179
  sortRatingDesc: "Highest rated",
104
- sortRatingAsc: "Lowest rated",
105
180
  submitLabel: "Save",
106
181
  cancelLabel: "Cancel",
107
182
  },
108
183
  };
109
184
 
110
185
  const collections: ComposeCollection[] = [
111
- { id: "col-1", title: "Books", iconHtml: "" },
112
- { id: "col-2", title: "Movies", iconHtml: "<span>🎬</span>" },
186
+ { id: "col-1", title: "Books" },
187
+ { id: "col-2", title: "Movies" },
113
188
  ];
114
189
 
115
190
  async function createElement(
@@ -128,7 +203,14 @@ async function createElement(
128
203
 
129
204
  describe("JantComposeDialog", () => {
130
205
  beforeEach(() => {
206
+ vi.restoreAllMocks();
131
207
  document.body.innerHTML = "";
208
+ globalThis.localStorage.clear();
209
+ (
210
+ customElements.get("jant-compose-dialog") as typeof HTMLElement & {
211
+ _lastNewPostVisibility: string;
212
+ }
213
+ )._lastNewPostVisibility = "public";
132
214
  });
133
215
 
134
216
  it("renders with collections and labels", async () => {
@@ -150,6 +232,97 @@ describe("JantComposeDialog", () => {
150
232
  "expected post button",
151
233
  );
152
234
  expect(postBtn.textContent?.trim()).toBe("Post");
235
+ expect(postBtn.disabled).toBe(true);
236
+ expect(
237
+ requireElement(
238
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
239
+ "expected publish settings toggle",
240
+ ).disabled,
241
+ ).toBe(false);
242
+ expect(
243
+ el.querySelector<HTMLButtonElement>(
244
+ '.compose-tool-btn-view[aria-label="Fullscreen"]',
245
+ ),
246
+ ).not.toBeNull();
247
+ });
248
+
249
+ it("focuses the main editor body after fullscreen closes", async () => {
250
+ const el = await createElement();
251
+ const editor = requireElement(
252
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
253
+ "expected compose editor",
254
+ );
255
+ const focusSpy = vi.spyOn(editor, "focusInput");
256
+
257
+ document.dispatchEvent(
258
+ new CustomEvent("jant:fullscreen-close", {
259
+ detail: {
260
+ json: null,
261
+ title: "",
262
+ showTitle: false,
263
+ replyExpanded: false,
264
+ },
265
+ }),
266
+ );
267
+
268
+ await flushUpdates(el);
269
+ await flushUpdates(el);
270
+
271
+ expect(focusSpy).toHaveBeenCalledWith("end");
272
+ });
273
+
274
+ it("opens publish settings even when publish is disabled", async () => {
275
+ const el = await createElement();
276
+
277
+ requireElement(
278
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
279
+ "expected publish settings toggle",
280
+ ).click();
281
+ await el.updateComplete;
282
+
283
+ expect(el.querySelector(".compose-publish-panel")).not.toBeNull();
284
+ });
285
+
286
+ it("updates the draft button label to match its current action", async () => {
287
+ const el = await createElement();
288
+
289
+ const draftButton = requireElement(
290
+ el.querySelector<HTMLButtonElement>(".compose-dialog-draft-btn"),
291
+ "expected draft button",
292
+ );
293
+ expect(draftButton.title).toBe("Drafts");
294
+ expect(draftButton.getAttribute("aria-label")).toBe("Drafts");
295
+
296
+ const editor = requireElement(
297
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
298
+ "expected compose editor",
299
+ );
300
+ editor._bodyJson = {
301
+ type: "doc",
302
+ content: [
303
+ { type: "paragraph", content: [{ type: "text", text: "Hello world" }] },
304
+ ],
305
+ };
306
+ await editor.updateComplete;
307
+ await el.updateComplete;
308
+
309
+ expect(draftButton.title).toBe("Save as draft");
310
+ expect(draftButton.getAttribute("aria-label")).toBe("Save as draft");
311
+ });
312
+
313
+ it("shows visibility hints in the publish settings panel", async () => {
314
+ const el = await createElement();
315
+
316
+ requireElement(
317
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
318
+ "expected publish settings toggle",
319
+ ).click();
320
+ await el.updateComplete;
321
+
322
+ expect(el.textContent).toContain(
323
+ "Doesn't appear in Latest. Still appears in collections you add it to.",
324
+ );
325
+ expect(el.textContent).toContain("Only visible when signed in.");
153
326
  });
154
327
 
155
328
  it("format switching updates active state", async () => {
@@ -177,6 +350,9 @@ describe("JantComposeDialog", () => {
177
350
  expect(noteBtn.classList.contains("compose-segmented-item-active")).toBe(
178
351
  false,
179
352
  );
353
+ expect(
354
+ el.querySelector('.compose-tool-btn-view[aria-label="Fullscreen"]'),
355
+ ).toBeNull();
180
356
  });
181
357
 
182
358
  it("submit dispatches jant:compose-submit-deferred with correct payload", async () => {
@@ -218,80 +394,46 @@ describe("JantComposeDialog", () => {
218
394
  expect(detail.status).toBe("published");
219
395
  expect(detail.visibility).toBe("public");
220
396
  expect(detail.collectionIds).toEqual([]);
221
- expect(detail.mediaIds).toEqual([]);
222
- expect(detail.mediaAlts).toEqual({});
397
+ expect(detail.attachments).toEqual([]);
223
398
  expect(detail.pendingAttachments).toEqual([]);
224
399
  });
225
400
 
226
- it("collection selector toggles IDs", async () => {
401
+ it("submit omits a hidden rating while keeping the compose body", async () => {
227
402
  const el = await createElement();
228
-
229
- // Open collection combobox
230
- const trigger = requireElement(
231
- el.querySelector<HTMLButtonElement>(".compose-collection-trigger"),
232
- "expected collection trigger",
233
- );
234
- trigger.click();
235
- await el.updateComplete;
236
-
237
- const options = el.querySelectorAll<HTMLElement>(
238
- "[data-popover] [role='option']",
403
+ const editor = requireElement(
404
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
405
+ "expected compose editor",
239
406
  );
240
- expect(options.length).toBe(2);
241
-
242
- // Select first collection
243
- options[0].click();
244
- await el.updateComplete;
245
- expect(el._collectionIds).toEqual(["col-1"]);
246
-
247
- // Select second collection
248
- options[1].click();
249
- await el.updateComplete;
250
- expect(el._collectionIds).toEqual(["col-1", "col-2"]);
251
-
252
- // Deselect first
253
- options[0].click();
254
- await el.updateComplete;
255
- expect(el._collectionIds).toEqual(["col-2"]);
256
- });
257
-
258
- it("reset restores initial state", async () => {
259
- const el = await createElement();
260
- el._format = "link";
261
- el._collectionIds = ["col-1", "col-2"];
262
- el._loading = true;
263
- el._draftSourceId = "abc123";
264
-
265
- el.reset();
266
-
267
- expect(el._format).toBe("note");
268
- expect(el._collectionIds).toEqual([]);
269
- expect(el._loading).toBe(false);
270
- expect(el._draftSourceId).toBeNull();
271
- });
407
+ editor._bodyJson = {
408
+ type: "doc",
409
+ content: [
410
+ {
411
+ type: "paragraph",
412
+ content: [{ type: "text", text: "Post without visible rating" }],
413
+ },
414
+ ],
415
+ };
416
+ editor._rating = 3;
417
+ editor._showRating = false;
418
+ await editor.updateComplete;
272
419
 
273
- it("loading state disables submit button", async () => {
274
- const el = await createElement();
275
- el._loading = true;
276
- await el.updateComplete;
420
+ let receivedDetail: ComposeSubmitDetail | null = null;
421
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
422
+ receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
423
+ });
277
424
 
278
- const postBtn = requireElement(
425
+ requireElement(
279
426
  el.querySelector<HTMLButtonElement>(".compose-publish-main"),
280
427
  "expected post button",
281
- );
282
- expect(postBtn.disabled).toBe(true);
283
- });
284
-
285
- it("renders collection selector even without collections", async () => {
286
- const el = await createElement([]);
428
+ ).click();
287
429
 
288
- // Collection trigger is still shown so users can create new collections
289
- expect(el.querySelector(".compose-collection-trigger")).not.toBeNull();
290
- const actionRow = el.querySelector(".compose-action-row");
291
- expect(actionRow).not.toBeNull();
430
+ expect(receivedDetail).not.toBeNull();
431
+ const detail = receivedDetail as unknown as ComposeSubmitDetail;
432
+ expect(detail.rating).toBe(0);
433
+ expect(detail.body).toContain("Post without visible rating");
292
434
  });
293
435
 
294
- it("draft button with content shows confirm panel", async () => {
436
+ it("includes publish settings in the submit payload", async () => {
295
437
  const el = await createElement();
296
438
  const editor = requireElement(
297
439
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
@@ -302,431 +444,2437 @@ describe("JantComposeDialog", () => {
302
444
  content: [
303
445
  {
304
446
  type: "paragraph",
305
- content: [{ type: "text", text: "Draft content" }],
447
+ content: [{ type: "text", text: "Configured post" }],
306
448
  },
307
449
  ],
308
450
  };
309
451
  await editor.updateComplete;
310
452
 
311
- // Click the draft header button — should show confirm panel
312
- const draftBtn = requireElement(
313
- el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
314
- "expected draft button",
453
+ requireElement(
454
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
455
+ "expected publish settings toggle",
456
+ ).click();
457
+ await el.updateComplete;
458
+
459
+ const options = el.querySelectorAll<HTMLButtonElement>(
460
+ ".compose-publish-option[role='radio']",
315
461
  );
316
- draftBtn.click();
462
+ expect(options).toHaveLength(3);
463
+ options[1]?.click();
317
464
  await el.updateComplete;
318
465
 
319
- expect(el._confirmPanelOpen).toBe(true);
320
- expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
466
+ expect(
467
+ requireElement(
468
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
469
+ "expected publish button",
470
+ ).textContent?.trim(),
471
+ ).toBe("Post hidden");
472
+
473
+ let receivedDetail: ComposeSubmitDetail | null = null;
474
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
475
+ receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
476
+ });
477
+
478
+ requireElement(
479
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
480
+ "expected publish button",
481
+ ).click();
482
+
483
+ expect(receivedDetail).not.toBeNull();
484
+ expect((receivedDetail as unknown as ComposeSubmitDetail).visibility).toBe(
485
+ "latest_hidden",
486
+ );
487
+ expect(
488
+ (receivedDetail as unknown as ComposeSubmitDetail).slug,
489
+ ).toBeUndefined();
321
490
  });
322
491
 
323
- it("draft button without content opens drafts panel", async () => {
492
+ it("updates the publish button label for private visibility", async () => {
324
493
  const el = await createElement();
325
494
 
326
- // Mock fetch for drafts list
327
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
328
- new Response(JSON.stringify({ posts: [] }), {
329
- status: 200,
330
- headers: { "Content-Type": "application/json" },
331
- }),
332
- );
495
+ requireElement(
496
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
497
+ "expected publish settings toggle",
498
+ ).click();
499
+ await el.updateComplete;
333
500
 
334
- // Click the draft header button — should open drafts panel
335
- const draftBtn = requireElement(
336
- el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
337
- "expected draft button",
501
+ const options = el.querySelectorAll<HTMLButtonElement>(
502
+ ".compose-publish-option[role='radio']",
338
503
  );
339
- draftBtn.click();
504
+ expect(options).toHaveLength(3);
505
+ options[2]?.click();
340
506
  await el.updateComplete;
341
507
 
342
- expect(el._draftsPanelOpen).toBe(true);
508
+ expect(
509
+ requireElement(
510
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
511
+ "expected publish button",
512
+ ).textContent?.trim(),
513
+ ).toBe("Post privately");
514
+ });
343
515
 
344
- // Wait for fetch to resolve
345
- await new Promise((r) => setTimeout(r, 0));
516
+ it("updates the publish button label for hidden visibility", async () => {
517
+ const el = await createElement();
518
+
519
+ requireElement(
520
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
521
+ "expected publish settings toggle",
522
+ ).click();
346
523
  await el.updateComplete;
347
524
 
348
- expect(el._draftsLoading).toBe(false);
349
- expect(el.querySelector(".compose-drafts-panel")).not.toBeNull();
525
+ const options = el.querySelectorAll<HTMLButtonElement>(
526
+ ".compose-publish-option[role='radio']",
527
+ );
528
+ expect(options).toHaveLength(3);
529
+ options[1]?.click();
530
+ await el.updateComplete;
350
531
 
351
- fetchSpy.mockRestore();
532
+ expect(
533
+ requireElement(
534
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
535
+ "expected publish button",
536
+ ).textContent?.trim(),
537
+ ).toBe("Post hidden");
352
538
  });
353
539
 
354
- it("does not dispatch submit when loading", async () => {
540
+ it("opens a new post with the requested collection and keeps the last visibility until refresh", async () => {
355
541
  const el = await createElement();
356
- el._loading = true;
542
+
543
+ await el.openNew({ collectionId: "col-2", restoreDraft: false });
357
544
  await el.updateComplete;
358
545
 
359
- let dispatched = false;
360
- el.addEventListener("jant:compose-submit-deferred", () => {
361
- dispatched = true;
362
- });
546
+ expect(el._collectionIds).toEqual(["col-2"]);
547
+ expect(el._visibility).toBe("public");
363
548
 
364
- const postBtn = requireElement(
365
- el.querySelector<HTMLButtonElement>(".compose-publish-main"),
366
- "expected post button",
549
+ requireElement(
550
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
551
+ "expected publish settings toggle",
552
+ ).click();
553
+ await el.updateComplete;
554
+
555
+ const options = el.querySelectorAll<HTMLButtonElement>(
556
+ ".compose-publish-option[role='radio']",
367
557
  );
368
- postBtn.click();
558
+ options[1]?.click();
559
+ await el.updateComplete;
369
560
 
370
- expect(dispatched).toBe(false);
371
- });
561
+ expect(el._visibility).toBe("latest_hidden");
372
562
 
373
- it("loading state shows spinner in submit button", async () => {
374
- const el = await createElement();
375
- el._loading = true;
563
+ el.reset();
376
564
  await el.updateComplete;
377
565
 
378
- const spinner = el.querySelector(".compose-publish-main .animate-spin");
379
- expect(spinner).not.toBeNull();
566
+ expect(el._visibility).toBe("latest_hidden");
567
+
568
+ await el.openNew({ collectionId: "col-1", restoreDraft: false });
569
+ await el.updateComplete;
570
+
571
+ expect(el._collectionIds).toEqual(["col-1"]);
572
+ expect(el._visibility).toBe("latest_hidden");
380
573
  });
381
574
 
382
- it("no old media picker dialog is rendered", async () => {
575
+ it("prepends the requested collection even when a local draft is restored", async () => {
383
576
  const el = await createElement();
384
577
 
385
- expect(el.querySelector("#compose-media-picker")).toBeNull();
386
- expect(el.querySelector(".compose-media-picker")).toBeNull();
578
+ globalThis.localStorage.setItem(
579
+ "jant:compose-draft",
580
+ JSON.stringify({
581
+ format: "note",
582
+ title: "",
583
+ bodyJson: {
584
+ type: "doc",
585
+ content: [
586
+ {
587
+ type: "paragraph",
588
+ content: [{ type: "text", text: "Draft body" }],
589
+ },
590
+ ],
591
+ },
592
+ url: "",
593
+ quoteText: "",
594
+ quoteAuthor: "",
595
+ slug: "",
596
+ visibility: "public",
597
+ rating: 0,
598
+ showTitle: false,
599
+ showRating: false,
600
+ collectionIds: ["col-1"],
601
+ attachedTexts: [],
602
+ attachmentOrder: [],
603
+ savedAt: Date.now(),
604
+ }),
605
+ );
606
+
607
+ await el.openNew({ collectionId: "col-2" });
608
+ await flushUpdates(el);
609
+
610
+ expect(el._collectionIds).toEqual(["col-2", "col-1"]);
387
611
  });
388
612
 
389
- it("editor renders attachments when present", async () => {
613
+ it("includes a custom slug from the publish settings panel in the submit payload", async () => {
614
+ mockSlugApi((url) => {
615
+ if (url.searchParams.get("mode") === "suggest") {
616
+ return { body: { slug: "configured-post" } };
617
+ }
618
+ if (url.searchParams.get("mode") === "check") {
619
+ return { body: { slug: "custom-link", available: true } };
620
+ }
621
+ throw new Error(`Unexpected slug mode: ${url.search}`);
622
+ });
623
+
624
+ const el = await createElement();
625
+ const editor = requireElement(
626
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
627
+ "expected compose editor",
628
+ );
629
+ editor._bodyJson = {
630
+ type: "doc",
631
+ content: [
632
+ {
633
+ type: "paragraph",
634
+ content: [{ type: "text", text: "Configured post" }],
635
+ },
636
+ ],
637
+ };
638
+ await editor.updateComplete;
639
+
640
+ const publishToggle = requireElement(
641
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
642
+ "expected publish settings toggle",
643
+ );
644
+ publishToggle.click();
645
+ await el.updateComplete;
646
+ await flushUpdates(el);
647
+
648
+ const slugInput = requireElement(
649
+ el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
650
+ "expected custom link input",
651
+ );
652
+ slugInput.value = "custom-link";
653
+ slugInput.dispatchEvent(new Event("input", { bubbles: true }));
654
+ await new Promise((resolve) => setTimeout(resolve, 300));
655
+ await el.updateComplete;
656
+
657
+ let receivedDetail: ComposeSubmitDetail | null = null;
658
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
659
+ receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
660
+ });
661
+
662
+ requireElement(
663
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
664
+ "expected publish button",
665
+ ).click();
666
+
667
+ expect(receivedDetail).not.toBeNull();
668
+ expect((receivedDetail as unknown as ComposeSubmitDetail).slug).toBe(
669
+ "custom-link",
670
+ );
671
+ });
672
+
673
+ it("reopens the publish settings panel with the custom link field intact", async () => {
674
+ mockSlugApi((url) => {
675
+ if (url.searchParams.get("mode") === "suggest") {
676
+ return { body: { slug: "reading-notes" } };
677
+ }
678
+ if (url.searchParams.get("mode") === "check") {
679
+ return { body: { slug: "reading-notes", available: true } };
680
+ }
681
+ throw new Error(`Unexpected slug mode: ${url.search}`);
682
+ });
683
+
684
+ const el = await createElement();
685
+
686
+ const publishToggle = requireElement(
687
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
688
+ "expected publish settings toggle",
689
+ );
690
+
691
+ publishToggle.click();
692
+ await el.updateComplete;
693
+ await flushUpdates(el);
694
+
695
+ const slugInput = requireElement(
696
+ el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
697
+ "expected custom link input",
698
+ );
699
+ slugInput.value = "reading-notes";
700
+ slugInput.dispatchEvent(new Event("input", { bubbles: true }));
701
+ await new Promise((resolve) => setTimeout(resolve, 300));
702
+ await el.updateComplete;
703
+
704
+ publishToggle.click();
705
+ await el.updateComplete;
706
+ publishToggle.click();
707
+ await el.updateComplete;
708
+
709
+ expect(el.querySelector(".compose-publish-slug-input")).not.toBeNull();
710
+ });
711
+
712
+ it("shows a slug error and blocks publish when the custom link is invalid", async () => {
713
+ mockSlugApi(() => ({ body: { slug: "hello-world" } }));
714
+
715
+ const el = await createElement();
716
+ const editor = requireElement(
717
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
718
+ "expected compose editor",
719
+ );
720
+ editor._bodyJson = {
721
+ type: "doc",
722
+ content: [
723
+ { type: "paragraph", content: [{ type: "text", text: "Hello world" }] },
724
+ ],
725
+ };
726
+ await editor.updateComplete;
727
+
728
+ const publishToggle = requireElement(
729
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
730
+ "expected publish settings toggle",
731
+ );
732
+ publishToggle.click();
733
+ await el.updateComplete;
734
+ await flushUpdates(el);
735
+
736
+ const slugInput = requireElement(
737
+ el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
738
+ "expected custom link input",
739
+ );
740
+ slugInput.value = "bad/slug";
741
+ slugInput.dispatchEvent(new Event("input", { bubbles: true }));
742
+ await el.updateComplete;
743
+
744
+ expect(
745
+ requireElement(
746
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
747
+ "expected publish button",
748
+ ).disabled,
749
+ ).toBe(true);
750
+ expect(
751
+ el.querySelector("[data-compose-slug-error]")?.textContent?.trim(),
752
+ ).toBe("Use lowercase letters, numbers, and hyphens only.");
753
+
754
+ let receivedDetail: ComposeSubmitDetail | null = null;
755
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
756
+ receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
757
+ });
758
+
759
+ requireElement(
760
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
761
+ "expected publish button",
762
+ ).click();
763
+
764
+ expect(receivedDetail).toBeNull();
765
+ });
766
+
767
+ it("shows a suggested slug without submitting it by default", async () => {
768
+ mockSlugApi((url) => {
769
+ if (url.searchParams.get("mode") === "suggest") {
770
+ return { body: { slug: "hello-world" } };
771
+ }
772
+ throw new Error(`Unexpected slug mode: ${url.search}`);
773
+ });
774
+
775
+ const el = await createElement();
776
+ const editor = requireElement(
777
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
778
+ "expected compose editor",
779
+ );
780
+ editor._showTitle = true;
781
+ editor._title = "Hello World";
782
+ editor._bodyJson = {
783
+ type: "doc",
784
+ content: [
785
+ { type: "paragraph", content: [{ type: "text", text: "Hello world" }] },
786
+ ],
787
+ };
788
+ await editor.updateComplete;
789
+
790
+ const publishToggle = requireElement(
791
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
792
+ "expected publish settings toggle",
793
+ );
794
+ publishToggle.click();
795
+ await el.updateComplete;
796
+ await flushUpdates(el);
797
+
798
+ expect(
799
+ el.querySelector(".compose-slug-suggestion-value")?.textContent?.trim(),
800
+ ).toBe("/hello-world");
801
+
802
+ let receivedDetail: ComposeSubmitDetail | null = null;
803
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
804
+ receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
805
+ });
806
+
807
+ requireElement(
808
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
809
+ "expected publish button",
810
+ ).click();
811
+
812
+ expect(receivedDetail).not.toBeNull();
813
+ expect(
814
+ (receivedDetail as unknown as ComposeSubmitDetail).slug,
815
+ ).toBeUndefined();
816
+ });
817
+
818
+ it("shows visibility and custom link settings in the publish panel", async () => {
819
+ const el = await createElement();
820
+
821
+ const publishToggle = requireElement(
822
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
823
+ "expected publish settings toggle",
824
+ );
825
+ publishToggle.click();
826
+ await el.updateComplete;
827
+
828
+ const panel = requireElement(
829
+ el.querySelector<HTMLElement>("[data-compose-publish-panel]"),
830
+ "expected publish settings panel",
831
+ );
832
+ expect(panel.textContent).not.toContain("Publish settings");
833
+ expect(panel.textContent).toContain("Visibility");
834
+ expect(panel.textContent).toContain("Custom link");
835
+ expect(panel.textContent).not.toContain("Save as draft");
836
+ expect(panel.textContent).not.toContain("Discard");
837
+ expect(panel.querySelector(".compose-publish-slug-input")).not.toBeNull();
838
+ });
839
+
840
+ it("clears the custom slug with the reset action", async () => {
841
+ mockSlugApi((url) => {
842
+ if (url.searchParams.get("mode") === "suggest") {
843
+ return { body: { slug: "hello-world" } };
844
+ }
845
+ if (url.searchParams.get("mode") === "check") {
846
+ return { body: { slug: "manual-link", available: true } };
847
+ }
848
+ throw new Error(`Unexpected slug mode: ${url.search}`);
849
+ });
850
+
851
+ const el = await createElement();
852
+ const publishToggle = requireElement(
853
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
854
+ "expected publish settings toggle",
855
+ );
856
+ publishToggle.click();
857
+ await el.updateComplete;
858
+ await flushUpdates(el);
859
+
860
+ const slugInput = requireElement(
861
+ el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
862
+ "expected custom link input",
863
+ );
864
+ slugInput.value = "manual-link";
865
+ slugInput.dispatchEvent(new Event("input", { bubbles: true }));
866
+ await new Promise((resolve) => setTimeout(resolve, 300));
867
+ await el.updateComplete;
868
+
869
+ requireElement(
870
+ Array.from(
871
+ el.querySelectorAll<HTMLButtonElement>(
872
+ ".compose-publish-section-action",
873
+ ),
874
+ ).find((button) => button.textContent?.includes("Reset link")) ?? null,
875
+ "expected reset action",
876
+ ).click();
877
+ await flushUpdates(el);
878
+
879
+ expect(
880
+ requireElement(
881
+ el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
882
+ "expected custom link input",
883
+ ).value,
884
+ ).toBe("");
885
+ });
886
+
887
+ it("does not request or show a suggested slug when no title is available", async () => {
888
+ const fetchSpy = vi
889
+ .spyOn(globalThis, "fetch")
890
+ .mockImplementation(async () => {
891
+ throw new Error(
892
+ "slug suggestion should not be requested without a title",
893
+ );
894
+ });
895
+
896
+ const el = await createElement();
897
+ el._format = "quote";
898
+ await el.updateComplete;
899
+
900
+ const publishToggle = requireElement(
901
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
902
+ "expected publish settings toggle",
903
+ );
904
+ publishToggle.click();
905
+ await el.updateComplete;
906
+ await flushUpdates(el);
907
+
908
+ expect(fetchSpy).not.toHaveBeenCalled();
909
+ expect(el.querySelector(".compose-slug-suggestion")).toBeNull();
910
+ expect(el.querySelector(".compose-publish-slug-status")).toBeNull();
911
+ });
912
+
913
+ it("refreshes the suggested slug when the title changes", async () => {
914
+ vi.useFakeTimers();
915
+ mockSlugApi((url) => {
916
+ if (url.searchParams.get("mode") === "suggest") {
917
+ const title = url.searchParams.get("title");
918
+ return {
919
+ body: {
920
+ slug: title === "Updated Title" ? "updated-title" : "hello-world",
921
+ },
922
+ };
923
+ }
924
+ throw new Error(`Unexpected slug mode: ${url.search}`);
925
+ });
926
+
927
+ try {
928
+ const el = await createElement();
929
+ const editor = requireElement(
930
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
931
+ "expected compose editor",
932
+ );
933
+ editor._showTitle = true;
934
+ editor._title = "Hello World";
935
+ await editor.updateComplete;
936
+
937
+ const publishToggle = requireElement(
938
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
939
+ "expected publish settings toggle",
940
+ );
941
+ publishToggle.click();
942
+ await el.updateComplete;
943
+ await flushUpdates(el);
944
+
945
+ expect(
946
+ el.querySelector(".compose-slug-suggestion-value")?.textContent?.trim(),
947
+ ).toBe("/hello-world");
948
+
949
+ editor._title = "Updated Title";
950
+ el.dispatchEvent(
951
+ new CustomEvent("jant:compose-content-changed", { bubbles: true }),
952
+ );
953
+ await vi.runAllTimersAsync();
954
+ await flushUpdates(el);
955
+
956
+ expect(
957
+ el.querySelector(".compose-slug-suggestion-value")?.textContent?.trim(),
958
+ ).toBe("/updated-title");
959
+ } finally {
960
+ vi.useRealTimers();
961
+ }
962
+ });
963
+
964
+ it("shows an async error when the manual custom link is already taken", async () => {
965
+ vi.useFakeTimers();
966
+ mockSlugApi((url) => {
967
+ if (url.searchParams.get("mode") === "suggest") {
968
+ return { body: { slug: "hello-world" } };
969
+ }
970
+ if (url.searchParams.get("mode") === "check") {
971
+ return { body: { slug: "taken-link", available: false } };
972
+ }
973
+ throw new Error(`Unexpected slug mode: ${url.search}`);
974
+ });
975
+
976
+ try {
977
+ const el = await createElement();
978
+ const editor = requireElement(
979
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
980
+ "expected compose editor",
981
+ );
982
+ editor._bodyJson = {
983
+ type: "doc",
984
+ content: [
985
+ {
986
+ type: "paragraph",
987
+ content: [{ type: "text", text: "Configured post" }],
988
+ },
989
+ ],
990
+ };
991
+ await editor.updateComplete;
992
+
993
+ const publishToggle = requireElement(
994
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
995
+ "expected publish settings toggle",
996
+ );
997
+ publishToggle.click();
998
+ await el.updateComplete;
999
+ await flushUpdates(el);
1000
+
1001
+ const slugInput = requireElement(
1002
+ el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
1003
+ "expected custom link input",
1004
+ );
1005
+ slugInput.value = "taken-link";
1006
+ slugInput.dispatchEvent(new Event("input", { bubbles: true }));
1007
+ await vi.runAllTimersAsync();
1008
+ await el.updateComplete;
1009
+
1010
+ expect(
1011
+ el.querySelector("[data-compose-slug-error]")?.textContent?.trim(),
1012
+ ).toBe("This link is already in use. Choose something else.");
1013
+ expect(
1014
+ requireElement(
1015
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
1016
+ "expected publish button",
1017
+ ).disabled,
1018
+ ).toBe(true);
1019
+ } finally {
1020
+ vi.useRealTimers();
1021
+ }
1022
+ });
1023
+
1024
+ it("keeps publish enabled and hides checking status while the custom link check is pending", async () => {
1025
+ vi.useFakeTimers();
1026
+ mockSlugApi((url) => {
1027
+ if (url.searchParams.get("mode") === "check") {
1028
+ return { body: { slug: "pending-link", available: true } };
1029
+ }
1030
+ throw new Error(`Unexpected slug mode: ${url.search}`);
1031
+ });
1032
+
1033
+ try {
1034
+ const el = await createElement();
1035
+ const editor = requireElement(
1036
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1037
+ "expected compose editor",
1038
+ );
1039
+ editor._bodyJson = {
1040
+ type: "doc",
1041
+ content: [
1042
+ {
1043
+ type: "paragraph",
1044
+ content: [{ type: "text", text: "Configured post" }],
1045
+ },
1046
+ ],
1047
+ };
1048
+ await editor.updateComplete;
1049
+
1050
+ requireElement(
1051
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
1052
+ "expected publish settings toggle",
1053
+ ).click();
1054
+ await el.updateComplete;
1055
+
1056
+ const slugInput = requireElement(
1057
+ el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
1058
+ "expected custom link input",
1059
+ );
1060
+ slugInput.value = "pending-link";
1061
+ slugInput.dispatchEvent(new Event("input", { bubbles: true }));
1062
+ await el.updateComplete;
1063
+
1064
+ expect(el._slugCheckLoading).toBe(true);
1065
+ expect(
1066
+ requireElement(
1067
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
1068
+ "expected publish button",
1069
+ ).disabled,
1070
+ ).toBe(false);
1071
+ expect(el.textContent).not.toContain("Checking link...");
1072
+ expect(el.querySelector("[data-compose-slug-error]")).toBeNull();
1073
+ } finally {
1074
+ vi.useRealTimers();
1075
+ }
1076
+ });
1077
+
1078
+ it("shows the final post link preview while editing a valid custom link", async () => {
1079
+ mockSlugApi((url) => {
1080
+ if (url.searchParams.get("mode") === "check") {
1081
+ return { body: { slug: "final-link", available: true } };
1082
+ }
1083
+ throw new Error(`Unexpected slug mode: ${url.search}`);
1084
+ });
1085
+
1086
+ const el = await createElement();
1087
+ const editor = requireElement(
1088
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1089
+ "expected compose editor",
1090
+ );
1091
+ editor._bodyJson = {
1092
+ type: "doc",
1093
+ content: [
1094
+ {
1095
+ type: "paragraph",
1096
+ content: [{ type: "text", text: "Configured post" }],
1097
+ },
1098
+ ],
1099
+ };
1100
+ await editor.updateComplete;
1101
+
1102
+ requireElement(
1103
+ el.querySelector<HTMLButtonElement>(".compose-publish-toggle"),
1104
+ "expected publish settings toggle",
1105
+ ).click();
1106
+ await el.updateComplete;
1107
+
1108
+ const slugInput = requireElement(
1109
+ el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
1110
+ "expected custom link input",
1111
+ );
1112
+ slugInput.value = "final-link";
1113
+ slugInput.dispatchEvent(new Event("input", { bubbles: true }));
1114
+ await el.updateComplete;
1115
+
1116
+ expect(
1117
+ el.querySelector("[data-compose-slug-preview]")?.textContent?.trim(),
1118
+ ).toBe(`${globalThis.location.origin}/final-link`);
1119
+ });
1120
+
1121
+ it("includes the thread root id when replying", async () => {
1122
+ const el = await createElement();
1123
+ await el.openReply(
1124
+ "019ce8ce-d6d8-7fda-a5df-c2da2bef5ade",
1125
+ {
1126
+ contentHtml: "<p>Parent</p>",
1127
+ dateText: "Mar 14",
1128
+ },
1129
+ "019ce8cf-19a1-7d16-9a75-017a9ac7299d",
1130
+ {
1131
+ kind: "timeline-item",
1132
+ id: "019ce8cf-19a1-7d16-9a75-017a9ac7299d",
1133
+ },
1134
+ );
1135
+
1136
+ const editor = requireElement(
1137
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1138
+ "expected compose editor",
1139
+ );
1140
+ editor._bodyJson = {
1141
+ type: "doc",
1142
+ content: [
1143
+ { type: "paragraph", content: [{ type: "text", text: "Reply body" }] },
1144
+ ],
1145
+ };
1146
+ await editor.updateComplete;
1147
+ await el.updateComplete;
1148
+
1149
+ let receivedDetail:
1150
+ | (ComposeSubmitDetail & { pendingAttachments: unknown[] })
1151
+ | null = null;
1152
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
1153
+ const customEvent = event as CustomEvent<
1154
+ ComposeSubmitDetail & { pendingAttachments: unknown[] }
1155
+ >;
1156
+ receivedDetail = customEvent.detail;
1157
+ });
1158
+
1159
+ requireElement(
1160
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
1161
+ "expected reply button",
1162
+ ).click();
1163
+
1164
+ expect(receivedDetail).not.toBeNull();
1165
+ const detail = receivedDetail as unknown as ComposeSubmitDetail & {
1166
+ pendingAttachments: unknown[];
1167
+ };
1168
+ expect(detail.replyToId).toBe("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade");
1169
+ expect(detail.replyThreadRootId).toBe(
1170
+ "019ce8cf-19a1-7d16-9a75-017a9ac7299d",
1171
+ );
1172
+ expect(detail.replyRefreshKind).toBe("timeline-item");
1173
+ expect(detail.replyRefreshId).toBe("019ce8cf-19a1-7d16-9a75-017a9ac7299d");
1174
+ });
1175
+
1176
+ it("omits visibility from locked edit submissions", async () => {
1177
+ const el = await createElement();
1178
+ const editor = requireElement(
1179
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1180
+ "expected compose editor",
1181
+ );
1182
+
1183
+ el._editPostId = "post-123";
1184
+ el._visibilityLocked = true;
1185
+ el._slug = "reply-note";
1186
+ editor._bodyJson = {
1187
+ type: "doc",
1188
+ content: [
1189
+ {
1190
+ type: "paragraph",
1191
+ content: [{ type: "text", text: "Edited reply" }],
1192
+ },
1193
+ ],
1194
+ };
1195
+ await editor.updateComplete;
1196
+ await el.updateComplete;
1197
+
1198
+ let receivedDetail: ComposeSubmitDetail | null = null;
1199
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
1200
+ receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
1201
+ });
1202
+
1203
+ requireElement(
1204
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
1205
+ "expected publish button",
1206
+ ).click();
1207
+
1208
+ expect(receivedDetail).not.toBeNull();
1209
+ expect(
1210
+ (receivedDetail as unknown as ComposeSubmitDetail).visibility,
1211
+ ).toBeUndefined();
1212
+ expect((receivedDetail as unknown as ComposeSubmitDetail).slug).toBe(
1213
+ "reply-note",
1214
+ );
1215
+ });
1216
+
1217
+ it("collection selector toggles IDs", async () => {
1218
+ const el = await createElement();
1219
+
1220
+ // Open collection combobox
1221
+ const trigger = requireElement(
1222
+ el.querySelector<HTMLButtonElement>(".compose-collection-trigger"),
1223
+ "expected collection trigger",
1224
+ );
1225
+ trigger.click();
1226
+ await el.updateComplete;
1227
+
1228
+ const options = el.querySelectorAll<HTMLElement>(
1229
+ "[data-popover] [role='option']",
1230
+ );
1231
+ expect(options.length).toBe(2);
1232
+
1233
+ // Select first collection
1234
+ options[0].click();
1235
+ await el.updateComplete;
1236
+ expect(el._collectionIds).toEqual(["col-1"]);
1237
+ expect(
1238
+ el.querySelector(".compose-collection-label")?.textContent?.trim(),
1239
+ ).toBe("Books");
1240
+ expect(
1241
+ options[0]?.querySelector(".compose-collection-option-label"),
1242
+ ).not.toBeNull();
1243
+
1244
+ // Select second collection
1245
+ options[1].click();
1246
+ await el.updateComplete;
1247
+ expect(el._collectionIds).toEqual(["col-1", "col-2"]);
1248
+
1249
+ // Deselect first
1250
+ options[0].click();
1251
+ await el.updateComplete;
1252
+ expect(el._collectionIds).toEqual(["col-2"]);
1253
+ });
1254
+
1255
+ it("reset restores initial state", async () => {
1256
+ const el = await createElement();
1257
+ el._format = "link";
1258
+ el._collectionIds = ["col-1", "col-2"];
1259
+ el._loading = true;
1260
+ el._draftSourceId = "abc123";
1261
+
1262
+ el.reset();
1263
+
1264
+ expect(el._format).toBe("note");
1265
+ expect(el._collectionIds).toEqual([]);
1266
+ expect(el._loading).toBe(false);
1267
+ expect(el._draftSourceId).toBeNull();
1268
+ });
1269
+
1270
+ it("loading state disables submit button", async () => {
1271
+ const el = await createElement();
1272
+ el._loading = true;
1273
+ await el.updateComplete;
1274
+
1275
+ const postBtn = requireElement(
1276
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
1277
+ "expected post button",
1278
+ );
1279
+ expect(postBtn.disabled).toBe(true);
1280
+ });
1281
+
1282
+ it("renders collection selector even without collections", async () => {
1283
+ const el = await createElement([]);
1284
+
1285
+ // Collection trigger is still shown so users can create new collections
1286
+ expect(el.querySelector(".compose-collection-trigger")).not.toBeNull();
1287
+ const actionRow = el.querySelector(".compose-action-row");
1288
+ expect(actionRow).not.toBeNull();
1289
+ });
1290
+
1291
+ it("refreshes collections from the compose-sorted endpoint", async () => {
1292
+ const el = await createElement([{ id: "col-1", title: "Books" }]);
1293
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
1294
+ new Response(
1295
+ JSON.stringify({
1296
+ collections: [
1297
+ { id: "col-2", title: "Movies" },
1298
+ { id: "col-1", title: "Books" },
1299
+ ],
1300
+ }),
1301
+ {
1302
+ status: 200,
1303
+ headers: { "Content-Type": "application/json" },
1304
+ },
1305
+ ),
1306
+ );
1307
+
1308
+ const refreshed = await el.refreshCollections();
1309
+
1310
+ expect(refreshed).toBe(true);
1311
+ expect(fetchSpy).toHaveBeenCalledWith("/api/collections?view=compose", {
1312
+ headers: { Accept: "application/json" },
1313
+ });
1314
+ expect(el.collections).toEqual([
1315
+ { id: "col-2", title: "Movies" },
1316
+ { id: "col-1", title: "Books" },
1317
+ ]);
1318
+ });
1319
+
1320
+ it("opens a quick collection dialog from the collection selector", async () => {
1321
+ const el = await createElement();
1322
+ const trigger = requireElement(
1323
+ el.querySelector<HTMLButtonElement>(".compose-collection-trigger"),
1324
+ "expected collection trigger",
1325
+ );
1326
+
1327
+ trigger.click();
1328
+ await el.updateComplete;
1329
+
1330
+ const addAction = requireElement(
1331
+ el.querySelector<HTMLElement>(".compose-collection-add-action"),
1332
+ "expected add collection action",
1333
+ );
1334
+ addAction.click();
1335
+ await el.updateComplete;
1336
+
1337
+ const composeInner = requireElement(
1338
+ el.querySelector<HTMLElement>(".compose-dialog-inner"),
1339
+ "expected compose dialog inner",
1340
+ );
1341
+ expect(el.querySelector("[data-collection-quick-dialog]")).not.toBeNull();
1342
+ expect(
1343
+ composeInner.classList.contains("compose-dialog-inner-suspended"),
1344
+ ).toBe(true);
1345
+ expect(composeInner.getAttribute("aria-hidden")).toBe("true");
1346
+ expect(
1347
+ el.querySelector("[data-collection-quick-dialog] textarea"),
1348
+ ).toBeNull();
1349
+ expect(
1350
+ el.querySelector("[data-collection-quick-dialog] select"),
1351
+ ).toBeNull();
1352
+ expect(
1353
+ el.querySelector("[data-collection-quick-dialog] [data-icon-trigger]"),
1354
+ ).toBeNull();
1355
+ expect(
1356
+ el.querySelector(
1357
+ "[data-collection-quick-dialog] [data-collection-slug-input]",
1358
+ ),
1359
+ ).toBeNull();
1360
+ expect(
1361
+ el.querySelector(
1362
+ "[data-collection-quick-dialog] .collection-quick-dialog-cancel",
1363
+ )?.textContent,
1364
+ ).toContain("Cancel");
1365
+ expect(
1366
+ el.querySelector(
1367
+ "[data-collection-quick-dialog] .collection-quick-dialog-submit",
1368
+ )?.textContent,
1369
+ ).toContain("Done");
1370
+ expect(el.textContent).toContain(
1371
+ "More options are available after you create it.",
1372
+ );
1373
+
1374
+ requireElement(
1375
+ el.querySelector<HTMLButtonElement>(
1376
+ "[data-collection-quick-dialog] .collection-quick-dialog-cancel",
1377
+ ),
1378
+ "expected quick dialog cancel button",
1379
+ ).click();
1380
+ await flushUpdates(el);
1381
+
1382
+ expect(el.querySelector("[data-collection-quick-dialog]")).toBeNull();
1383
+ expect(
1384
+ composeInner.classList.contains("compose-dialog-inner-suspended"),
1385
+ ).toBe(false);
1386
+ expect(composeInner.getAttribute("aria-hidden")).toBe("false");
1387
+ });
1388
+
1389
+ it("restores compose after creating a collection from the quick dialog", async () => {
1390
+ const el = await createElement();
1391
+ const trigger = requireElement(
1392
+ el.querySelector<HTMLButtonElement>(".compose-collection-trigger"),
1393
+ "expected collection trigger",
1394
+ );
1395
+
1396
+ trigger.click();
1397
+ await el.updateComplete;
1398
+
1399
+ requireElement(
1400
+ el.querySelector<HTMLElement>(".compose-collection-add-action"),
1401
+ "expected add collection action",
1402
+ ).click();
1403
+ await el.updateComplete;
1404
+
1405
+ const fetchSpy = vi
1406
+ .spyOn(globalThis, "fetch")
1407
+ .mockImplementation(async (input, init) => {
1408
+ const raw =
1409
+ typeof input === "string"
1410
+ ? input
1411
+ : input instanceof URL
1412
+ ? input.toString()
1413
+ : input.url;
1414
+ const url = new URL(raw, "http://localhost");
1415
+ const method =
1416
+ init?.method ??
1417
+ (typeof input === "string" || input instanceof URL
1418
+ ? "GET"
1419
+ : input.method);
1420
+
1421
+ if (url.pathname === "/api/collections" && method === "POST") {
1422
+ return new Response(
1423
+ JSON.stringify({ id: "col-3", title: "Travel" }),
1424
+ {
1425
+ status: 200,
1426
+ headers: { "Content-Type": "application/json" },
1427
+ },
1428
+ );
1429
+ }
1430
+
1431
+ if (
1432
+ url.pathname === "/api/collections" &&
1433
+ url.searchParams.get("view") === "compose"
1434
+ ) {
1435
+ return new Response(
1436
+ JSON.stringify({
1437
+ collections: [
1438
+ { id: "col-3", title: "Travel" },
1439
+ { id: "col-1", title: "Books" },
1440
+ { id: "col-2", title: "Movies" },
1441
+ ],
1442
+ }),
1443
+ {
1444
+ status: 200,
1445
+ headers: { "Content-Type": "application/json" },
1446
+ },
1447
+ );
1448
+ }
1449
+
1450
+ throw new Error(`Unexpected fetch: ${url.pathname}${url.search}`);
1451
+ });
1452
+
1453
+ const form = requireElement(
1454
+ el.querySelector<HTMLElement>(
1455
+ "[data-collection-quick-dialog] jant-collection-form",
1456
+ ),
1457
+ "expected quick collection form",
1458
+ );
1459
+ form.dispatchEvent(
1460
+ new CustomEvent("jant:collection-submit", {
1461
+ bubbles: true,
1462
+ detail: {
1463
+ data: {
1464
+ title: "Travel",
1465
+ slug: "travel",
1466
+ description: "",
1467
+ sortOrder: "newest",
1468
+ icon: "",
1469
+ },
1470
+ },
1471
+ }),
1472
+ );
1473
+
1474
+ await vi.waitFor(() => {
1475
+ expect(el.querySelector("[data-collection-quick-dialog]")).toBeNull();
1476
+ });
1477
+
1478
+ const composeInner = requireElement(
1479
+ el.querySelector<HTMLElement>(".compose-dialog-inner"),
1480
+ "expected compose dialog inner",
1481
+ );
1482
+ expect(fetchSpy).toHaveBeenCalled();
1483
+ expect(el.querySelector("[data-collection-quick-dialog]")).toBeNull();
1484
+ expect(
1485
+ composeInner.classList.contains("compose-dialog-inner-suspended"),
1486
+ ).toBe(false);
1487
+ expect(el._collectionIds).toContain("col-3");
1488
+ expect(el.collections).toEqual([
1489
+ { id: "col-3", title: "Travel" },
1490
+ { id: "col-1", title: "Books" },
1491
+ { id: "col-2", title: "Movies" },
1492
+ ]);
1493
+ });
1494
+
1495
+ it("draft button with content shows confirm panel", async () => {
1496
+ const el = await createElement();
1497
+ const editor = requireElement(
1498
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1499
+ "expected compose editor",
1500
+ );
1501
+ editor._bodyJson = {
1502
+ type: "doc",
1503
+ content: [
1504
+ {
1505
+ type: "paragraph",
1506
+ content: [{ type: "text", text: "Draft content" }],
1507
+ },
1508
+ ],
1509
+ };
1510
+ await editor.updateComplete;
1511
+
1512
+ // Click the draft header button — should show confirm panel
1513
+ const draftBtn = requireElement(
1514
+ el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
1515
+ "expected draft button",
1516
+ );
1517
+ draftBtn.click();
1518
+ await el.updateComplete;
1519
+
1520
+ expect(el._confirmPanelOpen).toBe(true);
1521
+ expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
1522
+ });
1523
+
1524
+ it("draft button without content opens drafts panel", async () => {
1525
+ const el = await createElement();
1526
+
1527
+ // Mock fetch for drafts list
1528
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
1529
+ new Response(JSON.stringify({ posts: [] }), {
1530
+ status: 200,
1531
+ headers: { "Content-Type": "application/json" },
1532
+ }),
1533
+ );
1534
+
1535
+ // Click the draft header button — should open drafts panel
1536
+ const draftBtn = requireElement(
1537
+ el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
1538
+ "expected draft button",
1539
+ );
1540
+ draftBtn.click();
1541
+ await el.updateComplete;
1542
+
1543
+ expect(el._draftsPanelOpen).toBe(true);
1544
+
1545
+ // Wait for fetch to resolve
1546
+ await new Promise((r) => setTimeout(r, 0));
1547
+ await el.updateComplete;
1548
+
1549
+ expect(el._draftsLoading).toBe(false);
1550
+ expect(el.querySelector(".compose-drafts-panel")).not.toBeNull();
1551
+
1552
+ fetchSpy.mockRestore();
1553
+ });
1554
+
1555
+ it("does not dispatch submit when loading", async () => {
1556
+ const el = await createElement();
1557
+ el._loading = true;
1558
+ await el.updateComplete;
1559
+
1560
+ let dispatched = false;
1561
+ el.addEventListener("jant:compose-submit-deferred", () => {
1562
+ dispatched = true;
1563
+ });
1564
+
1565
+ const postBtn = requireElement(
1566
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
1567
+ "expected post button",
1568
+ );
1569
+ postBtn.click();
1570
+
1571
+ expect(dispatched).toBe(false);
1572
+ });
1573
+
1574
+ it("loading state shows spinner in submit button", async () => {
1575
+ const el = await createElement();
1576
+ el._loading = true;
1577
+ await el.updateComplete;
1578
+
1579
+ const spinner = el.querySelector(".compose-publish-main .animate-spin");
1580
+ expect(spinner).not.toBeNull();
1581
+ });
1582
+
1583
+ it("no old media picker dialog is rendered", async () => {
1584
+ const el = await createElement();
1585
+
1586
+ expect(el.querySelector("#compose-media-picker")).toBeNull();
1587
+ expect(el.querySelector(".compose-media-picker")).toBeNull();
1588
+ });
1589
+
1590
+ it("editor renders attachments when present", async () => {
1591
+ const el = await createElement();
1592
+ const editor = requireElement(
1593
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1594
+ "expected compose editor",
1595
+ );
1596
+
1597
+ // Simulate adding an attachment
1598
+ const blob = new Blob(["fake-image"], { type: "image/png" });
1599
+ const file = new File([blob], "test.png", { type: "image/png" });
1600
+ const previewUrl = URL.createObjectURL(blob);
1601
+
1602
+ editor._attachments = [
1603
+ {
1604
+ clientId: "test-id-1",
1605
+ file,
1606
+ previewUrl,
1607
+ status: "done",
1608
+ progress: null,
1609
+ mediaId: "media-1",
1610
+ alt: "",
1611
+ error: null,
1612
+ summary: null,
1613
+ chars: null,
1614
+ },
1615
+ ];
1616
+ editor._attachmentOrder = ["test-id-1"];
1617
+ await editor.updateComplete;
1618
+
1619
+ // Thumbnail strip should be visible
1620
+ expect(editor.querySelector(".compose-attachments")).not.toBeNull();
1621
+ expect(editor.querySelector(".compose-attachment-thumb")).not.toBeNull();
1622
+ // ALT button should be visible
1623
+ expect(editor.querySelector(".compose-attachment-alt")).not.toBeNull();
1624
+ // Media tool button should show inline "Add" label
1625
+ const mediaBtn =
1626
+ editor.querySelector<HTMLButtonElement>(".compose-tool-btn");
1627
+ expect(mediaBtn?.querySelector(".compose-tool-label")?.textContent).toBe(
1628
+ "Add",
1629
+ );
1630
+
1631
+ URL.revokeObjectURL(previewUrl);
1632
+ });
1633
+
1634
+ it("remove button clears attachment", async () => {
1635
+ const el = await createElement();
1636
+ const editor = requireElement(
1637
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1638
+ "expected compose editor",
1639
+ );
1640
+
1641
+ const blob = new Blob(["fake-image"], { type: "image/png" });
1642
+ const file = new File([blob], "test.png", { type: "image/png" });
1643
+ const previewUrl = URL.createObjectURL(blob);
1644
+
1645
+ editor._attachments = [
1646
+ {
1647
+ clientId: "test-id-1",
1648
+ file,
1649
+ previewUrl,
1650
+ status: "done",
1651
+ progress: null,
1652
+ mediaId: "media-1",
1653
+ alt: "",
1654
+ error: null,
1655
+ summary: null,
1656
+ chars: null,
1657
+ },
1658
+ ];
1659
+ editor._attachmentOrder = ["test-id-1"];
1660
+ await editor.updateComplete;
1661
+
1662
+ // Click remove button
1663
+ const removeBtn = requireElement(
1664
+ editor.querySelector<HTMLButtonElement>(".compose-attachment-remove"),
1665
+ "expected remove button",
1666
+ );
1667
+ removeBtn.click();
1668
+ await editor.updateComplete;
1669
+
1670
+ // Attachment strip should be gone (no attachments)
1671
+ expect(editor.querySelector(".compose-attachments")).toBeNull();
1672
+ expect(editor._attachments.length).toBe(0);
1673
+ });
1674
+
1675
+ it("alt panel opens and closes", async () => {
1676
+ const el = await createElement();
1677
+ const editor = requireElement(
1678
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1679
+ "expected compose editor",
1680
+ );
1681
+
1682
+ const blob = new Blob(["fake-image"], { type: "image/png" });
1683
+ const file = new File([blob], "test.png", { type: "image/png" });
1684
+ const previewUrl = URL.createObjectURL(blob);
1685
+
1686
+ editor._attachments = [
1687
+ {
1688
+ clientId: "test-id-1",
1689
+ file,
1690
+ previewUrl,
1691
+ status: "done",
1692
+ progress: null,
1693
+ mediaId: "media-1",
1694
+ alt: "",
1695
+ error: null,
1696
+ summary: null,
1697
+ chars: null,
1698
+ },
1699
+ ];
1700
+ editor._attachmentOrder = ["test-id-1"];
1701
+ await editor.updateComplete;
1702
+
1703
+ // Click ALT button
1704
+ const altBtn = requireElement(
1705
+ editor.querySelector<HTMLButtonElement>(".compose-attachment-alt"),
1706
+ "expected alt button",
1707
+ );
1708
+ altBtn.click();
1709
+ await editor.updateComplete;
1710
+ await el.updateComplete;
1711
+
1712
+ // Alt panel should be visible in the dialog (covers entire dialog)
1713
+ expect(el.querySelector(".compose-alt-panel")).not.toBeNull();
1714
+ expect(editor._showAltPanel).toBe(true);
1715
+
1716
+ // Click done to close
1717
+ const doneBtn = el.querySelector<HTMLButtonElement>(
1718
+ ".compose-alt-panel .compose-post-btn",
1719
+ );
1720
+ doneBtn?.click();
1721
+ await el.updateComplete;
1722
+
1723
+ expect(editor._showAltPanel).toBe(true); // Editor still tracks its own state
1724
+ expect(el.querySelector(".compose-alt-panel")).toBeNull();
1725
+
1726
+ URL.revokeObjectURL(previewUrl);
1727
+ });
1728
+
1729
+ it("submit includes ordered attachment inputs from completed attachments", async () => {
1730
+ const el = await createElement();
1731
+ const editor = requireElement(
1732
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1733
+ "expected compose editor",
1734
+ );
1735
+
1736
+ const blob = new Blob(["fake-image"], { type: "image/png" });
1737
+ const file = new File([blob], "test.png", { type: "image/png" });
1738
+ const previewUrl = URL.createObjectURL(blob);
1739
+
1740
+ editor._attachments = [
1741
+ {
1742
+ clientId: "test-id-1",
1743
+ file,
1744
+ previewUrl,
1745
+ status: "done",
1746
+ progress: null,
1747
+ mediaId: "media-1",
1748
+ alt: "A test image",
1749
+ error: null,
1750
+ summary: null,
1751
+ chars: null,
1752
+ },
1753
+ ];
1754
+ editor._attachmentOrder = ["test-id-1"];
1755
+ editor._bodyJson = {
1756
+ type: "doc",
1757
+ content: [
1758
+ {
1759
+ type: "paragraph",
1760
+ content: [{ type: "text", text: "Post with image" }],
1761
+ },
1762
+ ],
1763
+ };
1764
+ await editor.updateComplete;
1765
+
1766
+ let receivedDetail:
1767
+ | (ComposeSubmitDetail & { pendingAttachments: unknown[] })
1768
+ | null = null;
1769
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
1770
+ const customEvent = event as CustomEvent<
1771
+ ComposeSubmitDetail & { pendingAttachments: unknown[] }
1772
+ >;
1773
+ receivedDetail = customEvent.detail;
1774
+ });
1775
+
1776
+ requireElement(
1777
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
1778
+ "expected post button",
1779
+ ).click();
1780
+
1781
+ expect(receivedDetail).not.toBeNull();
1782
+ const detail = receivedDetail as unknown as ComposeSubmitDetail & {
1783
+ pendingAttachments: unknown[];
1784
+ };
1785
+ expect(detail.attachments).toEqual([
1786
+ {
1787
+ type: "media",
1788
+ clientId: "test-id-1",
1789
+ mediaId: "media-1",
1790
+ alt: "A test image",
1791
+ },
1792
+ ]);
1793
+ expect(detail.pendingAttachments).toEqual([]);
1794
+
1795
+ URL.revokeObjectURL(previewUrl);
1796
+ });
1797
+
1798
+ it("dispatches deferred submit when uploads are pending", async () => {
390
1799
  const el = await createElement();
391
1800
  const editor = requireElement(
392
1801
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
393
1802
  "expected compose editor",
394
1803
  );
395
1804
 
396
- // Simulate adding an attachment
397
1805
  const blob = new Blob(["fake-image"], { type: "image/png" });
398
1806
  const file = new File([blob], "test.png", { type: "image/png" });
399
1807
  const previewUrl = URL.createObjectURL(blob);
400
1808
 
401
- editor._attachments = [
402
- {
403
- clientId: "test-id-1",
404
- file,
405
- previewUrl,
406
- status: "done",
407
- progress: null,
408
- mediaId: "media-1",
409
- alt: "",
410
- error: null,
411
- summary: null,
412
- chars: null,
413
- },
414
- ];
415
- editor._attachmentOrder = ["test-id-1"];
1809
+ editor._attachments = [
1810
+ {
1811
+ clientId: "test-id-1",
1812
+ file,
1813
+ previewUrl,
1814
+ status: "uploading",
1815
+ progress: null,
1816
+ mediaId: null,
1817
+ alt: "Alt for pending",
1818
+ error: null,
1819
+ summary: null,
1820
+ chars: null,
1821
+ },
1822
+ ];
1823
+ editor._bodyJson = {
1824
+ type: "doc",
1825
+ content: [
1826
+ {
1827
+ type: "paragraph",
1828
+ content: [{ type: "text", text: "Post with pending upload" }],
1829
+ },
1830
+ ],
1831
+ };
1832
+ await editor.updateComplete;
1833
+
1834
+ let deferredEvent: CustomEvent | null = null;
1835
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
1836
+ deferredEvent = event as CustomEvent;
1837
+ });
1838
+
1839
+ requireElement(
1840
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
1841
+ "expected post button",
1842
+ ).click();
1843
+
1844
+ expect(deferredEvent).not.toBeNull();
1845
+ expect(
1846
+ (deferredEvent as unknown as CustomEvent).detail.pendingAttachments,
1847
+ ).toHaveLength(1);
1848
+
1849
+ URL.revokeObjectURL(previewUrl);
1850
+ });
1851
+
1852
+ // ── Close confirmation ─────────────────────────────────────────────
1853
+
1854
+ it("requestClose on empty form closes immediately without confirmation", async () => {
1855
+ const el = await createElement();
1856
+
1857
+ // Ensure no confirmation panel appears
1858
+ el.requestClose();
1859
+ await el.updateComplete;
1860
+
1861
+ expect(el._confirmPanelOpen).toBe(false);
1862
+ expect(el.querySelector(".compose-confirm-panel")).toBeNull();
1863
+ });
1864
+
1865
+ it("requestClose on empty form clears opener focus after closing", async () => {
1866
+ const el = await createElement();
1867
+ const trigger = document.createElement("button");
1868
+ trigger.type = "button";
1869
+ document.body.appendChild(trigger);
1870
+
1871
+ const dialog = document.createElement("dialog");
1872
+ const closeSpy = vi.spyOn(dialog, "close");
1873
+ const closestSpy = vi
1874
+ .spyOn(el, "closest")
1875
+ .mockImplementation((selector: string) =>
1876
+ selector === "dialog"
1877
+ ? dialog
1878
+ : HTMLElement.prototype.closest.call(el, selector),
1879
+ );
1880
+
1881
+ trigger.focus();
1882
+ const blurSpy = vi.spyOn(trigger, "blur");
1883
+
1884
+ el.requestClose();
1885
+ await el.updateComplete;
1886
+
1887
+ expect(closeSpy).toHaveBeenCalledTimes(1);
1888
+ expect(blurSpy).toHaveBeenCalledTimes(1);
1889
+ expect(document.activeElement).not.toBe(trigger);
1890
+
1891
+ closestSpy.mockRestore();
1892
+ });
1893
+
1894
+ it("treats dialog backdrop clicks as close requests", async () => {
1895
+ const el = await createElement();
1896
+ const dialog = document.createElement("dialog");
1897
+ const requestCloseSpy = vi.spyOn(el, "requestClose");
1898
+ (
1899
+ el as unknown as {
1900
+ _dialogEl: HTMLDialogElement | null;
1901
+ _handleDialogClick: (event: Event) => void;
1902
+ }
1903
+ )._dialogEl = dialog;
1904
+ vi.spyOn(document, "elementFromPoint").mockReturnValue(dialog);
1905
+
1906
+ (
1907
+ el as unknown as {
1908
+ _handleDialogClick: (event: Event) => void;
1909
+ }
1910
+ )._handleDialogClick({
1911
+ target: dialog,
1912
+ clientX: 24,
1913
+ clientY: 24,
1914
+ } as unknown as Event);
1915
+
1916
+ expect(requestCloseSpy).toHaveBeenCalledTimes(1);
1917
+ });
1918
+
1919
+ it("ignores backdrop clicks that actually land on editor floating UI", async () => {
1920
+ const el = await createElement();
1921
+ const dialog = document.createElement("dialog");
1922
+ const requestCloseSpy = vi.spyOn(el, "requestClose");
1923
+ const floatingUi = document.createElement("div");
1924
+ floatingUi.setAttribute("data-editor-floating-ui", "true");
1925
+ (
1926
+ el as unknown as {
1927
+ _dialogEl: HTMLDialogElement | null;
1928
+ _handleDialogClick: (event: Event) => void;
1929
+ }
1930
+ )._dialogEl = dialog;
1931
+ vi.spyOn(document, "elementFromPoint").mockReturnValue(floatingUi);
1932
+
1933
+ (
1934
+ el as unknown as {
1935
+ _handleDialogClick: (event: Event) => void;
1936
+ }
1937
+ )._handleDialogClick({
1938
+ target: dialog,
1939
+ clientX: 24,
1940
+ clientY: 24,
1941
+ } as unknown as Event);
1942
+
1943
+ expect(requestCloseSpy).not.toHaveBeenCalled();
1944
+ });
1945
+
1946
+ it("beforeunload does not warn when dialog was only opened", async () => {
1947
+ const el = await createElement();
1948
+ vi.spyOn(el, "closest").mockReturnValue({
1949
+ open: true,
1950
+ addEventListener: vi.fn(),
1951
+ removeEventListener: vi.fn(),
1952
+ } as unknown as HTMLDialogElement);
1953
+
1954
+ const event = new Event("beforeunload", {
1955
+ cancelable: true,
1956
+ }) as globalThis.BeforeUnloadEvent;
1957
+
1958
+ window.dispatchEvent(event);
1959
+
1960
+ expect(event.defaultPrevented).toBe(false);
1961
+ expect(
1962
+ (
1963
+ el as unknown as { _hasUnsavedChanges: () => boolean }
1964
+ )._hasUnsavedChanges(),
1965
+ ).toBe(false);
1966
+ });
1967
+
1968
+ it("beforeunload does not warn after switching to link without entering content", async () => {
1969
+ const el = await createElement();
1970
+ vi.spyOn(el, "closest").mockReturnValue({
1971
+ open: true,
1972
+ addEventListener: vi.fn(),
1973
+ removeEventListener: vi.fn(),
1974
+ } as unknown as HTMLDialogElement);
1975
+
1976
+ el._format = "link";
1977
+ await flushUpdates(el);
1978
+
1979
+ const event = new Event("beforeunload", {
1980
+ cancelable: true,
1981
+ }) as globalThis.BeforeUnloadEvent;
1982
+
1983
+ window.dispatchEvent(event);
1984
+
1985
+ expect(event.defaultPrevented).toBe(false);
1986
+ expect(
1987
+ (
1988
+ el as unknown as { _hasUnsavedChanges: () => boolean }
1989
+ )._hasUnsavedChanges(),
1990
+ ).toBe(false);
1991
+ });
1992
+
1993
+ it("beforeunload does not warn after switching to quote without entering content", async () => {
1994
+ const el = await createElement();
1995
+ vi.spyOn(el, "closest").mockReturnValue({
1996
+ open: true,
1997
+ addEventListener: vi.fn(),
1998
+ removeEventListener: vi.fn(),
1999
+ } as unknown as HTMLDialogElement);
2000
+
2001
+ el._format = "quote";
2002
+ await flushUpdates(el);
2003
+
2004
+ const event = new Event("beforeunload", {
2005
+ cancelable: true,
2006
+ }) as globalThis.BeforeUnloadEvent;
2007
+
2008
+ window.dispatchEvent(event);
2009
+
2010
+ expect(event.defaultPrevented).toBe(false);
2011
+ expect(
2012
+ (
2013
+ el as unknown as { _hasUnsavedChanges: () => boolean }
2014
+ )._hasUnsavedChanges(),
2015
+ ).toBe(false);
2016
+ });
2017
+
2018
+ it("ignores empty attached text placeholders when checking unsaved changes", async () => {
2019
+ const el = await createElement();
2020
+ const editor = requireElement(
2021
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2022
+ "expected compose editor",
2023
+ );
2024
+
2025
+ editor._attachedTexts = [
2026
+ {
2027
+ clientId: "t1",
2028
+ bodyJson: null,
2029
+ bodyHtml: "",
2030
+ summary: "",
2031
+ },
2032
+ ];
2033
+ editor._attachmentOrder = ["t1"];
2034
+ await editor.updateComplete;
2035
+
2036
+ expect(
2037
+ (
2038
+ el as unknown as { _hasUnsavedChanges: () => boolean }
2039
+ )._hasUnsavedChanges(),
2040
+ ).toBe(false);
2041
+ });
2042
+
2043
+ it("beforeunload warns after compose content changes", async () => {
2044
+ const el = await createElement();
2045
+ vi.spyOn(el, "closest").mockReturnValue({
2046
+ open: true,
2047
+ addEventListener: vi.fn(),
2048
+ removeEventListener: vi.fn(),
2049
+ } as unknown as HTMLDialogElement);
2050
+ const editor = requireElement(
2051
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2052
+ "expected compose editor",
2053
+ );
2054
+
2055
+ editor._bodyJson = {
2056
+ type: "doc",
2057
+ content: [
2058
+ { type: "paragraph", content: [{ type: "text", text: "Unsaved" }] },
2059
+ ],
2060
+ };
416
2061
  await editor.updateComplete;
417
2062
 
418
- // Thumbnail strip should be visible
419
- expect(editor.querySelector(".compose-attachments")).not.toBeNull();
420
- expect(editor.querySelector(".compose-attachment-thumb")).not.toBeNull();
421
- // ALT button should be visible
422
- expect(editor.querySelector(".compose-attachment-alt")).not.toBeNull();
423
- // Media tool button should show inline "Add" label
424
- const mediaBtn =
425
- editor.querySelector<HTMLButtonElement>(".compose-tool-btn");
426
- expect(mediaBtn?.querySelector(".compose-tool-label")?.textContent).toBe(
427
- "Add",
2063
+ const event = new Event("beforeunload", {
2064
+ cancelable: true,
2065
+ }) as globalThis.BeforeUnloadEvent;
2066
+
2067
+ window.dispatchEvent(event);
2068
+
2069
+ expect(event.defaultPrevented).toBe(true);
2070
+ });
2071
+
2072
+ it("ignores native dialog cancel right after file picker cancel", async () => {
2073
+ const el = await createElement();
2074
+ const editor = requireElement(
2075
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2076
+ "expected compose editor",
428
2077
  );
429
2078
 
430
- URL.revokeObjectURL(previewUrl);
2079
+ editor.dispatchEvent(
2080
+ new CustomEvent("jant:file-picker-open", {
2081
+ bubbles: true,
2082
+ composed: true,
2083
+ }),
2084
+ );
2085
+ editor.dispatchEvent(
2086
+ new CustomEvent("jant:file-picker-close", {
2087
+ bubbles: true,
2088
+ composed: true,
2089
+ detail: { cancelled: true },
2090
+ }),
2091
+ );
2092
+
2093
+ const requestCloseSpy = vi.spyOn(el, "requestClose");
2094
+ const cancelEvent = new Event("cancel", {
2095
+ cancelable: true,
2096
+ });
2097
+
2098
+ (
2099
+ el as unknown as {
2100
+ _handleDialogCancel: (event: Event) => void;
2101
+ }
2102
+ )._handleDialogCancel(cancelEvent);
2103
+
2104
+ expect(cancelEvent.defaultPrevented).toBe(true);
2105
+ expect(requestCloseSpy).not.toHaveBeenCalled();
431
2106
  });
432
2107
 
433
- it("remove button clears attachment", async () => {
2108
+ it("ignores Escape right after file picker cancel", async () => {
434
2109
  const el = await createElement();
435
2110
  const editor = requireElement(
436
2111
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
437
2112
  "expected compose editor",
438
2113
  );
2114
+ editor._bodyJson = {
2115
+ type: "doc",
2116
+ content: [
2117
+ {
2118
+ type: "paragraph",
2119
+ content: [{ type: "text", text: "Keep open" }],
2120
+ },
2121
+ ],
2122
+ };
2123
+ await editor.updateComplete;
2124
+
2125
+ editor.dispatchEvent(
2126
+ new CustomEvent("jant:file-picker-open", {
2127
+ bubbles: true,
2128
+ composed: true,
2129
+ }),
2130
+ );
2131
+ editor.dispatchEvent(
2132
+ new CustomEvent("jant:file-picker-close", {
2133
+ bubbles: true,
2134
+ composed: true,
2135
+ detail: { cancelled: true },
2136
+ }),
2137
+ );
439
2138
 
440
- const blob = new Blob(["fake-image"], { type: "image/png" });
441
- const file = new File([blob], "test.png", { type: "image/png" });
442
- const previewUrl = URL.createObjectURL(blob);
2139
+ el.dispatchEvent(
2140
+ new globalThis.KeyboardEvent("keydown", {
2141
+ key: "Escape",
2142
+ bubbles: true,
2143
+ }),
2144
+ );
2145
+ await el.updateComplete;
443
2146
 
444
- editor._attachments = [
445
- {
446
- clientId: "test-id-1",
447
- file,
448
- previewUrl,
449
- status: "done",
450
- progress: null,
451
- mediaId: "media-1",
452
- alt: "",
453
- error: null,
454
- summary: null,
455
- chars: null,
456
- },
457
- ];
458
- editor._attachmentOrder = ["test-id-1"];
2147
+ expect(el._confirmPanelOpen).toBe(false);
2148
+ });
2149
+
2150
+ it("still closes normally after file picker selection", async () => {
2151
+ const el = await createElement();
2152
+ const editor = requireElement(
2153
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2154
+ "expected compose editor",
2155
+ );
2156
+ editor._bodyJson = {
2157
+ type: "doc",
2158
+ content: [
2159
+ {
2160
+ type: "paragraph",
2161
+ content: [{ type: "text", text: "Close after selecting" }],
2162
+ },
2163
+ ],
2164
+ };
459
2165
  await editor.updateComplete;
460
2166
 
461
- // Click remove button
462
- const removeBtn = requireElement(
463
- editor.querySelector<HTMLButtonElement>(".compose-attachment-remove"),
464
- "expected remove button",
2167
+ editor.dispatchEvent(
2168
+ new CustomEvent("jant:file-picker-open", {
2169
+ bubbles: true,
2170
+ composed: true,
2171
+ }),
465
2172
  );
466
- removeBtn.click();
2173
+ editor.dispatchEvent(
2174
+ new CustomEvent("jant:file-picker-close", {
2175
+ bubbles: true,
2176
+ composed: true,
2177
+ detail: { cancelled: false },
2178
+ }),
2179
+ );
2180
+
2181
+ el.dispatchEvent(
2182
+ new globalThis.KeyboardEvent("keydown", {
2183
+ key: "Escape",
2184
+ bubbles: true,
2185
+ }),
2186
+ );
2187
+ await el.updateComplete;
2188
+
2189
+ expect(el._confirmPanelOpen).toBe(true);
2190
+ });
2191
+
2192
+ it("clears file picker Escape suppression after pointer interaction", async () => {
2193
+ const el = await createElement();
2194
+ const editor = requireElement(
2195
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2196
+ "expected compose editor",
2197
+ );
2198
+ editor._bodyJson = {
2199
+ type: "doc",
2200
+ content: [
2201
+ {
2202
+ type: "paragraph",
2203
+ content: [{ type: "text", text: "Close after clicking back" }],
2204
+ },
2205
+ ],
2206
+ };
467
2207
  await editor.updateComplete;
468
2208
 
469
- // Attachment strip should be gone (no attachments)
470
- expect(editor.querySelector(".compose-attachments")).toBeNull();
471
- expect(editor._attachments.length).toBe(0);
2209
+ editor.dispatchEvent(
2210
+ new CustomEvent("jant:file-picker-open", {
2211
+ bubbles: true,
2212
+ composed: true,
2213
+ }),
2214
+ );
2215
+ editor.dispatchEvent(
2216
+ new CustomEvent("jant:file-picker-close", {
2217
+ bubbles: true,
2218
+ composed: true,
2219
+ detail: { cancelled: true },
2220
+ }),
2221
+ );
2222
+
2223
+ el.dispatchEvent(
2224
+ new globalThis.PointerEvent("pointerdown", {
2225
+ bubbles: true,
2226
+ }),
2227
+ );
2228
+ el.dispatchEvent(
2229
+ new globalThis.KeyboardEvent("keydown", {
2230
+ key: "Escape",
2231
+ bubbles: true,
2232
+ }),
2233
+ );
2234
+ await el.updateComplete;
2235
+
2236
+ expect(el._confirmPanelOpen).toBe(true);
472
2237
  });
473
2238
 
474
- it("alt panel opens and closes", async () => {
2239
+ it("clears file picker Escape suppression after non-Escape key input", async () => {
475
2240
  const el = await createElement();
476
2241
  const editor = requireElement(
477
2242
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
478
2243
  "expected compose editor",
479
2244
  );
2245
+ editor._bodyJson = {
2246
+ type: "doc",
2247
+ content: [
2248
+ {
2249
+ type: "paragraph",
2250
+ content: [{ type: "text", text: "Close after typing again" }],
2251
+ },
2252
+ ],
2253
+ };
2254
+ await editor.updateComplete;
480
2255
 
481
- const blob = new Blob(["fake-image"], { type: "image/png" });
482
- const file = new File([blob], "test.png", { type: "image/png" });
483
- const previewUrl = URL.createObjectURL(blob);
2256
+ editor.dispatchEvent(
2257
+ new CustomEvent("jant:file-picker-open", {
2258
+ bubbles: true,
2259
+ composed: true,
2260
+ }),
2261
+ );
2262
+ editor.dispatchEvent(
2263
+ new CustomEvent("jant:file-picker-close", {
2264
+ bubbles: true,
2265
+ composed: true,
2266
+ detail: { cancelled: true },
2267
+ }),
2268
+ );
484
2269
 
485
- editor._attachments = [
486
- {
487
- clientId: "test-id-1",
488
- file,
489
- previewUrl,
490
- status: "done",
491
- progress: null,
492
- mediaId: "media-1",
493
- alt: "",
494
- error: null,
495
- summary: null,
496
- chars: null,
497
- },
498
- ];
499
- editor._attachmentOrder = ["test-id-1"];
2270
+ el.dispatchEvent(
2271
+ new globalThis.KeyboardEvent("keydown", {
2272
+ key: "a",
2273
+ bubbles: true,
2274
+ }),
2275
+ );
2276
+ el.dispatchEvent(
2277
+ new globalThis.KeyboardEvent("keydown", {
2278
+ key: "Escape",
2279
+ bubbles: true,
2280
+ }),
2281
+ );
2282
+ await el.updateComplete;
2283
+
2284
+ expect(el._confirmPanelOpen).toBe(true);
2285
+ });
2286
+
2287
+ it("Escape cancels slash commands without opening the close confirmation", async () => {
2288
+ const el = await createElement();
2289
+ const editor = requireElement(
2290
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2291
+ "expected compose editor",
2292
+ );
2293
+ const tiptap = requireEditor(editor);
2294
+
2295
+ tiptap.commands.focus("end");
2296
+ tiptap.commands.insertContent("/");
2297
+ await flushUpdates(el);
2298
+
2299
+ expect(document.querySelector(".tiptap-slash-menu")).not.toBeNull();
2300
+
2301
+ const requestCloseSpy = vi.spyOn(el, "requestClose");
2302
+ const escapeEvent = new globalThis.KeyboardEvent("keydown", {
2303
+ key: "Escape",
2304
+ bubbles: true,
2305
+ cancelable: true,
2306
+ });
2307
+ tiptap.view.dom.dispatchEvent(escapeEvent);
2308
+ await flushUpdates(el);
2309
+
2310
+ expect(escapeEvent.defaultPrevented).toBe(true);
2311
+ expect(requestCloseSpy).not.toHaveBeenCalled();
2312
+ expect(el._confirmPanelOpen).toBe(false);
2313
+ expect(document.querySelector(".tiptap-slash-menu")).toBeNull();
2314
+ expect(tiptap.getText()).toBe("");
2315
+ });
2316
+
2317
+ it("shows a clear empty state when slash commands have no matches", async () => {
2318
+ const el = await createElement();
2319
+ const editor = requireElement(
2320
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2321
+ "expected compose editor",
2322
+ );
2323
+ const tiptap = requireEditor(editor);
2324
+
2325
+ tiptap.commands.focus("end");
2326
+ tiptap.commands.insertContent("/zzz");
2327
+ await flushUpdates(el);
2328
+
2329
+ const menu = requireElement(
2330
+ document.querySelector<HTMLElement>(".tiptap-slash-menu"),
2331
+ "expected slash menu",
2332
+ );
2333
+ expect(menu.querySelectorAll(".tiptap-slash-item")).toHaveLength(0);
2334
+ expect(
2335
+ requireElement(
2336
+ menu.querySelector<HTMLElement>(".tiptap-slash-empty"),
2337
+ "expected slash empty state",
2338
+ ).textContent,
2339
+ ).toContain("No matches. Try another command.");
2340
+
2341
+ const requestCloseSpy = vi.spyOn(el, "requestClose");
2342
+ const enterEvent = new globalThis.KeyboardEvent("keydown", {
2343
+ key: "Enter",
2344
+ bubbles: true,
2345
+ cancelable: true,
2346
+ });
2347
+ tiptap.view.dom.dispatchEvent(enterEvent);
2348
+ await flushUpdates(el);
2349
+
2350
+ expect(enterEvent.defaultPrevented).toBe(true);
2351
+ expect(requestCloseSpy).not.toHaveBeenCalled();
2352
+ expect(el._confirmPanelOpen).toBe(false);
2353
+ expect(tiptap.getText()).toBe("/zzz");
2354
+ });
2355
+
2356
+ it("dialog cancel closes the emoji picker before prompting to save", async () => {
2357
+ const el = await createElement();
2358
+ const editor = requireElement(
2359
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2360
+ "expected compose editor",
2361
+ );
2362
+ editor._bodyJson = {
2363
+ type: "doc",
2364
+ content: [
2365
+ { type: "paragraph", content: [{ type: "text", text: "Keep typing" }] },
2366
+ ],
2367
+ };
2368
+ editor._showEmojiPicker = true;
500
2369
  await editor.updateComplete;
501
2370
 
502
- // Click ALT button
503
- const altBtn = requireElement(
504
- editor.querySelector<HTMLButtonElement>(".compose-attachment-alt"),
505
- "expected alt button",
2371
+ const closeEmojiPickerSpy = vi.spyOn(editor, "closeEmojiPicker");
2372
+ const focusSpy = vi.spyOn(editor, "focusInput");
2373
+
2374
+ (
2375
+ el as unknown as {
2376
+ _handleDialogCancel: (event: Event) => void;
2377
+ }
2378
+ )._handleDialogCancel(new Event("cancel", { cancelable: true }));
2379
+ await flushUpdates(el);
2380
+
2381
+ expect(closeEmojiPickerSpy).toHaveBeenCalledWith({ restoreFocus: true });
2382
+ expect(el._confirmPanelOpen).toBe(false);
2383
+ expect(focusSpy).toHaveBeenCalledWith("end");
2384
+ });
2385
+
2386
+ it("dialog cancel closes the collection selector and keeps selected collections", async () => {
2387
+ const el = await createElement();
2388
+ const editor = requireElement(
2389
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2390
+ "expected compose editor",
506
2391
  );
507
- altBtn.click();
2392
+ editor._bodyJson = {
2393
+ type: "doc",
2394
+ content: [
2395
+ { type: "paragraph", content: [{ type: "text", text: "Tagged post" }] },
2396
+ ],
2397
+ };
2398
+ el._showCollection = true;
2399
+ el._collectionIds = ["col-1"];
508
2400
  await editor.updateComplete;
509
- await el.updateComplete;
510
-
511
- // Alt panel should be visible in the dialog (covers entire dialog)
512
- expect(el.querySelector(".compose-alt-panel")).not.toBeNull();
513
- expect(editor._showAltPanel).toBe(true);
514
2401
 
515
- // Click done to close
516
- const doneBtn = el.querySelector<HTMLButtonElement>(
517
- ".compose-alt-panel .compose-post-btn",
518
- );
519
- doneBtn?.click();
520
- await el.updateComplete;
2402
+ const focusSpy = vi.spyOn(editor, "focusInput");
521
2403
 
522
- expect(editor._showAltPanel).toBe(true); // Editor still tracks its own state
523
- expect(el.querySelector(".compose-alt-panel")).toBeNull();
2404
+ (
2405
+ el as unknown as {
2406
+ _handleDialogCancel: (event: Event) => void;
2407
+ }
2408
+ )._handleDialogCancel(new Event("cancel", { cancelable: true }));
2409
+ await flushUpdates(el);
524
2410
 
525
- URL.revokeObjectURL(previewUrl);
2411
+ expect(el._showCollection).toBe(false);
2412
+ expect(el._collectionIds).toEqual(["col-1"]);
2413
+ expect(el._confirmPanelOpen).toBe(false);
2414
+ expect(focusSpy).toHaveBeenCalledWith("end");
526
2415
  });
527
2416
 
528
- it("submit includes mediaIds and mediaAlts from completed attachments", async () => {
2417
+ it("dialog cancel closes publish settings and returns focus to the editor", async () => {
529
2418
  const el = await createElement();
530
2419
  const editor = requireElement(
531
2420
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
532
2421
  "expected compose editor",
533
2422
  );
534
-
535
- const blob = new Blob(["fake-image"], { type: "image/png" });
536
- const file = new File([blob], "test.png", { type: "image/png" });
537
- const previewUrl = URL.createObjectURL(blob);
538
-
539
- editor._attachments = [
540
- {
541
- clientId: "test-id-1",
542
- file,
543
- previewUrl,
544
- status: "done",
545
- progress: null,
546
- mediaId: "media-1",
547
- alt: "A test image",
548
- error: null,
549
- summary: null,
550
- chars: null,
551
- },
552
- ];
553
2423
  editor._bodyJson = {
554
2424
  type: "doc",
555
2425
  content: [
556
2426
  {
557
2427
  type: "paragraph",
558
- content: [{ type: "text", text: "Post with image" }],
2428
+ content: [{ type: "text", text: "Publish panel content" }],
559
2429
  },
560
2430
  ],
561
2431
  };
2432
+ el._showPublishPanel = true;
562
2433
  await editor.updateComplete;
563
2434
 
564
- let receivedDetail:
565
- | (ComposeSubmitDetail & { pendingAttachments: unknown[] })
566
- | null = null;
567
- el.addEventListener("jant:compose-submit-deferred", (event) => {
568
- const customEvent = event as CustomEvent<
569
- ComposeSubmitDetail & { pendingAttachments: unknown[] }
570
- >;
571
- receivedDetail = customEvent.detail;
572
- });
573
-
574
- requireElement(
575
- el.querySelector<HTMLButtonElement>(".compose-publish-main"),
576
- "expected post button",
577
- ).click();
2435
+ const focusSpy = vi.spyOn(editor, "focusInput");
578
2436
 
579
- expect(receivedDetail).not.toBeNull();
580
- const detail = receivedDetail as unknown as ComposeSubmitDetail & {
581
- pendingAttachments: unknown[];
582
- };
583
- expect(detail.mediaIds).toEqual(["media-1"]);
584
- expect(detail.mediaAlts).toEqual({ "media-1": "A test image" });
585
- expect(detail.pendingAttachments).toEqual([]);
2437
+ (
2438
+ el as unknown as {
2439
+ _handleDialogCancel: (event: Event) => void;
2440
+ }
2441
+ )._handleDialogCancel(new Event("cancel", { cancelable: true }));
2442
+ await flushUpdates(el);
586
2443
 
587
- URL.revokeObjectURL(previewUrl);
2444
+ expect(el._showPublishPanel).toBe(false);
2445
+ expect(el._confirmPanelOpen).toBe(false);
2446
+ expect(focusSpy).toHaveBeenCalledWith("end");
588
2447
  });
589
2448
 
590
- it("dispatches deferred submit when uploads are pending", async () => {
2449
+ it("requestClose with content shows confirmation panel", async () => {
591
2450
  const el = await createElement();
592
2451
  const editor = requireElement(
593
2452
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
594
2453
  "expected compose editor",
595
2454
  );
2455
+ editor._bodyJson = {
2456
+ type: "doc",
2457
+ content: [
2458
+ { type: "paragraph", content: [{ type: "text", text: "Some text" }] },
2459
+ ],
2460
+ };
2461
+ await editor.updateComplete;
596
2462
 
597
- const blob = new Blob(["fake-image"], { type: "image/png" });
598
- const file = new File([blob], "test.png", { type: "image/png" });
599
- const previewUrl = URL.createObjectURL(blob);
2463
+ el.requestClose();
2464
+ await el.updateComplete;
600
2465
 
601
- editor._attachments = [
602
- {
603
- clientId: "test-id-1",
604
- file,
605
- previewUrl,
606
- status: "uploading",
607
- progress: null,
608
- mediaId: null,
609
- alt: "Alt for pending",
610
- error: null,
611
- summary: null,
612
- chars: null,
613
- },
614
- ];
2466
+ expect(el._confirmPanelOpen).toBe(true);
2467
+ expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
2468
+ expect(
2469
+ el.querySelector(".compose-confirm-title")?.textContent?.trim(),
2470
+ ).toBe("Save to drafts?");
2471
+ });
2472
+
2473
+ it("Cmd/Ctrl+Enter publishes from the main compose editor", async () => {
2474
+ const el = await createElement();
2475
+ const editor = requireElement(
2476
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2477
+ "expected compose editor",
2478
+ );
615
2479
  editor._bodyJson = {
616
2480
  type: "doc",
617
2481
  content: [
618
2482
  {
619
2483
  type: "paragraph",
620
- content: [{ type: "text", text: "Post with pending upload" }],
2484
+ content: [{ type: "text", text: "Publish from shortcut" }],
621
2485
  },
622
2486
  ],
623
2487
  };
624
2488
  await editor.updateComplete;
625
2489
 
626
- let deferredEvent: CustomEvent | null = null;
627
- el.addEventListener("jant:compose-submit-deferred", (event) => {
628
- deferredEvent = event as CustomEvent;
629
- });
630
-
631
- requireElement(
632
- el.querySelector<HTMLButtonElement>(".compose-publish-main"),
633
- "expected post button",
634
- ).click();
2490
+ const submitSpy = vi.spyOn(
2491
+ el as unknown as { _submit: (status: "published" | "draft") => void },
2492
+ "_submit",
2493
+ );
635
2494
 
636
- expect(deferredEvent).not.toBeNull();
637
- expect(
638
- (deferredEvent as unknown as CustomEvent).detail.pendingAttachments,
639
- ).toHaveLength(1);
2495
+ el.dispatchEvent(
2496
+ new globalThis.KeyboardEvent("keydown", {
2497
+ key: "Enter",
2498
+ metaKey: true,
2499
+ bubbles: true,
2500
+ }),
2501
+ );
640
2502
 
641
- URL.revokeObjectURL(previewUrl);
2503
+ expect(submitSpy).toHaveBeenCalledWith("published");
642
2504
  });
643
2505
 
644
- // ── Close confirmation ─────────────────────────────────────────────
2506
+ it("Cmd/Ctrl+Enter focuses the link URL when the URL is invalid", async () => {
2507
+ const el = await createElement();
2508
+ el._format = "link";
2509
+ await el.updateComplete;
645
2510
 
646
- it("requestClose on empty form closes immediately without confirmation", async () => {
2511
+ const editor = requireElement(
2512
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2513
+ "expected compose editor",
2514
+ );
2515
+ editor._url = "example.com";
2516
+ await editor.updateComplete;
2517
+
2518
+ const focusUrlSpy = vi.spyOn(editor, "focusUrlInput");
2519
+ const submitSpy = vi.spyOn(
2520
+ el as unknown as { _submit: (status: "published" | "draft") => void },
2521
+ "_submit",
2522
+ );
2523
+
2524
+ el.dispatchEvent(
2525
+ new globalThis.KeyboardEvent("keydown", {
2526
+ key: "Enter",
2527
+ metaKey: true,
2528
+ bubbles: true,
2529
+ }),
2530
+ );
2531
+
2532
+ expect(focusUrlSpy).toHaveBeenCalledWith("end");
2533
+ expect(submitSpy).not.toHaveBeenCalled();
2534
+ });
2535
+
2536
+ it("blocks publish for link posts without showing a title error on blur", async () => {
647
2537
  const el = await createElement();
2538
+ el._format = "link";
2539
+ await el.updateComplete;
648
2540
 
649
- // Ensure no confirmation panel appears
650
- el.requestClose();
2541
+ const editor = requireElement(
2542
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
2543
+ "expected compose editor",
2544
+ );
2545
+ editor._url = "https://example.com";
2546
+ await editor.updateComplete;
2547
+
2548
+ expect(
2549
+ requireElement(
2550
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
2551
+ "expected publish button",
2552
+ ).disabled,
2553
+ ).toBe(true);
2554
+
2555
+ const titleInput = requireElement(
2556
+ el.querySelector<HTMLInputElement>(".compose-link-title"),
2557
+ "expected link title input",
2558
+ );
2559
+ titleInput.dispatchEvent(new Event("blur"));
651
2560
  await el.updateComplete;
652
2561
 
653
- expect(el._confirmPanelOpen).toBe(false);
654
- expect(el.querySelector(".compose-confirm-panel")).toBeNull();
2562
+ expect(el.querySelector("[data-compose-link-title-error]")).toBeNull();
655
2563
  });
656
2564
 
657
- it("beforeunload does not warn when dialog was only opened", async () => {
2565
+ it("Cmd/Ctrl+Enter finishes an attached text editor instead of publishing", async () => {
658
2566
  const el = await createElement();
659
- vi.spyOn(el, "closest").mockReturnValue({
660
- open: true,
661
- addEventListener: vi.fn(),
662
- removeEventListener: vi.fn(),
663
- } as unknown as HTMLDialogElement);
664
-
665
- const event = new Event("beforeunload", {
666
- cancelable: true,
667
- }) as globalThis.BeforeUnloadEvent;
2567
+ (
2568
+ el as unknown as {
2569
+ _attachedPanelOpen: boolean;
2570
+ }
2571
+ )._attachedPanelOpen = true;
2572
+
2573
+ const doneSpy = vi.spyOn(
2574
+ el as unknown as { _doneAttachedPanel: () => void },
2575
+ "_doneAttachedPanel",
2576
+ );
2577
+ const submitSpy = vi.spyOn(
2578
+ el as unknown as { _submit: (status: "published" | "draft") => void },
2579
+ "_submit",
2580
+ );
668
2581
 
669
- window.dispatchEvent(event);
2582
+ el.dispatchEvent(
2583
+ new globalThis.KeyboardEvent("keydown", {
2584
+ key: "Enter",
2585
+ metaKey: true,
2586
+ bubbles: true,
2587
+ }),
2588
+ );
670
2589
 
671
- expect(event.defaultPrevented).toBe(false);
672
- expect(
673
- (
674
- el as unknown as { _hasUnsavedChanges: () => boolean }
675
- )._hasUnsavedChanges(),
676
- ).toBe(false);
2590
+ expect(doneSpy).toHaveBeenCalled();
2591
+ expect(submitSpy).not.toHaveBeenCalled();
677
2592
  });
678
2593
 
679
- it("beforeunload warns after compose content changes", async () => {
2594
+ it("cancelling an empty attached text editor discards it without confirmation", async () => {
680
2595
  const el = await createElement();
681
- vi.spyOn(el, "closest").mockReturnValue({
682
- open: true,
683
- addEventListener: vi.fn(),
684
- removeEventListener: vi.fn(),
685
- } as unknown as HTMLDialogElement);
686
2596
  const editor = requireElement(
687
2597
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
688
2598
  "expected compose editor",
689
2599
  );
690
2600
 
691
- editor._bodyJson = {
692
- type: "doc",
693
- content: [
694
- { type: "paragraph", content: [{ type: "text", text: "Unsaved" }] },
695
- ],
696
- };
2601
+ editor._attachedTexts = [
2602
+ {
2603
+ clientId: "t1",
2604
+ bodyJson: null,
2605
+ bodyHtml: "",
2606
+ summary: "",
2607
+ },
2608
+ ];
2609
+ editor._attachmentOrder = ["t1"];
697
2610
  await editor.updateComplete;
698
2611
 
699
- const event = new Event("beforeunload", {
700
- cancelable: true,
701
- }) as globalThis.BeforeUnloadEvent;
702
-
703
- window.dispatchEvent(event);
704
-
705
- expect(event.defaultPrevented).toBe(true);
2612
+ (
2613
+ el as unknown as {
2614
+ _attachedTextIndex: number;
2615
+ _attachedPanelOpen: boolean;
2616
+ _attachedEditor: { getJSON(): unknown; destroy(): void } | null;
2617
+ _attachedTextSnapshot: unknown;
2618
+ _cancelAttachedPanel: () => Promise<void>;
2619
+ }
2620
+ )._attachedTextIndex = 0;
2621
+ (
2622
+ el as unknown as {
2623
+ _attachedPanelOpen: boolean;
2624
+ }
2625
+ )._attachedPanelOpen = true;
2626
+ (
2627
+ el as unknown as {
2628
+ _attachedEditor: { getJSON(): unknown; destroy(): void } | null;
2629
+ }
2630
+ )._attachedEditor = {
2631
+ getJSON: () => ({
2632
+ type: "doc",
2633
+ content: [{ type: "paragraph" }],
2634
+ }),
2635
+ destroy: vi.fn(),
2636
+ };
2637
+ (
2638
+ el as unknown as {
2639
+ _attachedTextSnapshot: unknown;
2640
+ }
2641
+ )._attachedTextSnapshot = null;
2642
+
2643
+ await (
2644
+ el as unknown as {
2645
+ _cancelAttachedPanel: () => Promise<void>;
2646
+ }
2647
+ )._cancelAttachedPanel();
2648
+
2649
+ expect(el._attachedPanelOpen).toBe(false);
2650
+ expect(editor._attachedTexts).toEqual([]);
2651
+ expect(editor._attachmentOrder).toEqual([]);
706
2652
  });
707
2653
 
708
- it("requestClose with content shows confirmation panel", async () => {
2654
+ it("reopening an attached text editor places the cursor at the end", async () => {
709
2655
  const el = await createElement();
710
2656
  const editor = requireElement(
711
2657
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
712
2658
  "expected compose editor",
713
2659
  );
714
- editor._bodyJson = {
715
- type: "doc",
716
- content: [
717
- { type: "paragraph", content: [{ type: "text", text: "Some text" }] },
718
- ],
719
- };
2660
+
2661
+ editor._attachedTexts = [
2662
+ {
2663
+ clientId: "t1",
2664
+ bodyJson: {
2665
+ type: "doc",
2666
+ content: [
2667
+ {
2668
+ type: "paragraph",
2669
+ content: [{ type: "text", text: "Existing attachment body" }],
2670
+ },
2671
+ ],
2672
+ },
2673
+ bodyHtml: "<p>Existing attachment body</p>",
2674
+ summary: "Existing attachment body",
2675
+ },
2676
+ ];
2677
+ editor._attachmentOrder = ["t1"];
720
2678
  await editor.updateComplete;
721
2679
 
722
- el.requestClose();
2680
+ (
2681
+ el as unknown as {
2682
+ _handleAttachedPanelOpen: (event: Event) => void;
2683
+ }
2684
+ )._handleAttachedPanelOpen(
2685
+ new CustomEvent("jant:attached-panel-open", {
2686
+ detail: { index: 0 },
2687
+ }),
2688
+ );
2689
+ await flushUpdates(el);
2690
+
2691
+ const attachedEditor = (
2692
+ el as unknown as {
2693
+ _attachedEditor: {
2694
+ state: {
2695
+ selection: { from: number; to: number };
2696
+ };
2697
+ } | null;
2698
+ }
2699
+ )._attachedEditor;
2700
+ if (!attachedEditor) {
2701
+ throw new Error("expected attached editor instance");
2702
+ }
2703
+
2704
+ expect(attachedEditor.state.selection.from).toBeGreaterThan(1);
2705
+ expect(attachedEditor.state.selection.from).toBe(
2706
+ attachedEditor.state.selection.to,
2707
+ );
2708
+ });
2709
+
2710
+ it("cancelling a dirty attached text editor uses the shared three-action confirm panel", async () => {
2711
+ const el = await createElement();
2712
+
2713
+ (
2714
+ el as unknown as {
2715
+ _attachedTextIndex: number;
2716
+ _attachedPanelOpen: boolean;
2717
+ _attachedEditor: {
2718
+ commands: { focus: () => void };
2719
+ destroy(): void;
2720
+ getJSON(): unknown;
2721
+ } | null;
2722
+ _attachedTextSnapshot: unknown;
2723
+ _cancelAttachedPanel: () => void;
2724
+ }
2725
+ )._attachedTextIndex = 0;
2726
+ (
2727
+ el as unknown as {
2728
+ _attachedPanelOpen: boolean;
2729
+ }
2730
+ )._attachedPanelOpen = true;
2731
+ (
2732
+ el as unknown as {
2733
+ _attachedEditor: {
2734
+ commands: { focus: () => void };
2735
+ destroy(): void;
2736
+ getJSON(): unknown;
2737
+ } | null;
2738
+ }
2739
+ )._attachedEditor = {
2740
+ commands: { focus: vi.fn() },
2741
+ destroy: vi.fn(),
2742
+ getJSON: () => ({
2743
+ type: "doc",
2744
+ content: [
2745
+ {
2746
+ type: "paragraph",
2747
+ content: [{ type: "text", text: "Changed attachment" }],
2748
+ },
2749
+ ],
2750
+ }),
2751
+ };
2752
+ (
2753
+ el as unknown as {
2754
+ _attachedTextSnapshot: unknown;
2755
+ }
2756
+ )._attachedTextSnapshot = null;
2757
+
2758
+ (
2759
+ el as unknown as {
2760
+ _cancelAttachedPanel: () => void;
2761
+ }
2762
+ )._cancelAttachedPanel();
723
2763
  await el.updateComplete;
724
2764
 
725
2765
  expect(el._confirmPanelOpen).toBe(true);
726
2766
  expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
727
2767
  expect(
728
2768
  el.querySelector(".compose-confirm-title")?.textContent?.trim(),
729
- ).toBe("Save to drafts?");
2769
+ ).toBe("Save text attachment?");
2770
+ expect(el.querySelector(".compose-confirm-save")?.textContent?.trim()).toBe(
2771
+ "Save",
2772
+ );
2773
+ expect(
2774
+ el.querySelector(".compose-confirm-discard")?.textContent?.trim(),
2775
+ ).toBe("Don't save");
2776
+ expect(
2777
+ el.querySelector(".compose-confirm-cancel")?.textContent?.trim(),
2778
+ ).toBe("Cancel");
2779
+ });
2780
+
2781
+ it("cancel on attached text confirm returns focus to the attached editor", async () => {
2782
+ const el = await createElement();
2783
+ const focusSpy = vi.fn();
2784
+
2785
+ (
2786
+ el as unknown as {
2787
+ _attachedPanelOpen: boolean;
2788
+ _confirmPanelOpen: boolean;
2789
+ _confirmForAttachedText: boolean;
2790
+ _attachedEditor: {
2791
+ commands: { focus: () => void };
2792
+ destroy(): void;
2793
+ } | null;
2794
+ }
2795
+ )._attachedPanelOpen = true;
2796
+ (
2797
+ el as unknown as {
2798
+ _confirmPanelOpen: boolean;
2799
+ _confirmForAttachedText: boolean;
2800
+ _attachedEditor: {
2801
+ commands: { focus: () => void };
2802
+ destroy(): void;
2803
+ } | null;
2804
+ }
2805
+ )._confirmPanelOpen = true;
2806
+ (
2807
+ el as unknown as {
2808
+ _confirmForAttachedText: boolean;
2809
+ _attachedEditor: {
2810
+ commands: { focus: () => void };
2811
+ destroy(): void;
2812
+ } | null;
2813
+ }
2814
+ )._confirmForAttachedText = true;
2815
+ (
2816
+ el as unknown as {
2817
+ _attachedEditor: {
2818
+ commands: { focus: () => void };
2819
+ destroy(): void;
2820
+ } | null;
2821
+ }
2822
+ )._attachedEditor = {
2823
+ commands: { focus: focusSpy },
2824
+ destroy: vi.fn(),
2825
+ };
2826
+
2827
+ el.requestClose();
2828
+ await el.updateComplete;
2829
+ await flushUpdates(el);
2830
+
2831
+ expect(el._confirmPanelOpen).toBe(false);
2832
+ expect(focusSpy).toHaveBeenCalled();
2833
+ });
2834
+
2835
+ it("clicking the empty attached editor area focuses the attached editor", async () => {
2836
+ const el = await createElement();
2837
+ const focusSpy = vi.fn();
2838
+
2839
+ (
2840
+ el as unknown as {
2841
+ _attachedPanelOpen: boolean;
2842
+ _attachedEditor: {
2843
+ commands: { focus: () => void };
2844
+ destroy(): void;
2845
+ getJSON(): unknown;
2846
+ } | null;
2847
+ }
2848
+ )._attachedPanelOpen = true;
2849
+ (
2850
+ el as unknown as {
2851
+ _attachedEditor: {
2852
+ commands: { focus: () => void };
2853
+ destroy(): void;
2854
+ getJSON(): unknown;
2855
+ } | null;
2856
+ }
2857
+ )._attachedEditor = {
2858
+ commands: { focus: focusSpy },
2859
+ destroy: vi.fn(),
2860
+ getJSON: () => ({
2861
+ type: "doc",
2862
+ content: [{ type: "paragraph" }],
2863
+ }),
2864
+ };
2865
+ await el.updateComplete;
2866
+
2867
+ const container = requireElement(
2868
+ el.querySelector<HTMLElement>(".compose-attached-tiptap"),
2869
+ "expected attached editor container",
2870
+ );
2871
+ container.dispatchEvent(
2872
+ new MouseEvent("mousedown", {
2873
+ bubbles: true,
2874
+ }),
2875
+ );
2876
+
2877
+ expect(focusSpy).toHaveBeenCalled();
730
2878
  });
731
2879
 
732
2880
  it("confirm save draft dispatches submit-deferred with draft status", async () => {
@@ -1130,6 +3278,7 @@ describe("JantComposeDialog", () => {
1130
3278
  "expected compose editor",
1131
3279
  );
1132
3280
  editor._rating = 3;
3281
+ editor._showRating = true;
1133
3282
  await editor.updateComplete;
1134
3283
 
1135
3284
  el.requestClose();
@@ -1137,4 +3286,20 @@ describe("JantComposeDialog", () => {
1137
3286
 
1138
3287
  expect(el._confirmPanelOpen).toBe(true);
1139
3288
  });
3289
+
3290
+ it("hidden rating does not trigger confirmation on close", async () => {
3291
+ const el = await createElement();
3292
+ const editor = requireElement(
3293
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
3294
+ "expected compose editor",
3295
+ );
3296
+ editor._rating = 3;
3297
+ editor._showRating = false;
3298
+ await editor.updateComplete;
3299
+
3300
+ el.requestClose();
3301
+ await el.updateComplete;
3302
+
3303
+ expect(el._confirmPanelOpen).toBe(false);
3304
+ });
1140
3305
  });