@jant/core 0.3.40 → 0.3.43

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 (1191) hide show
  1. package/README.md +21 -34
  2. package/bin/commands/assets/upload.js +247 -0
  3. package/bin/commands/collections.js +268 -0
  4. package/bin/commands/import-site.js +1169 -343
  5. package/bin/commands/media.js +302 -0
  6. package/bin/commands/migrate-text-attachments.js +183 -0
  7. package/bin/commands/migrate.js +263 -2
  8. package/bin/commands/posts.js +262 -0
  9. package/bin/commands/search-reindex.js +175 -0
  10. package/bin/commands/search.js +53 -0
  11. package/bin/commands/settings.js +93 -0
  12. package/bin/commands/site/export.js +34 -33
  13. package/bin/commands/site/{localize-media.js → pull-media.js} +17 -17
  14. package/bin/lib/d1-query.js +15 -3
  15. package/bin/lib/http-api.js +223 -0
  16. package/bin/lib/hugo-markdown.js +102 -0
  17. package/bin/lib/media-upload.js +206 -0
  18. package/bin/lib/migration-runner.js +64 -0
  19. package/bin/lib/runtime-target.js +7 -2
  20. package/bin/lib/site-media-parser.js +71 -16
  21. package/bin/lib/site-pull-media.js +695 -0
  22. package/bin/lib/site-snapshot.js +0 -1
  23. package/dist/app-Ctl0T0zO.js +5 -0
  24. package/dist/{app-CAtsuLLh.js → app-GbfwoeDJ.js} +24357 -23139
  25. package/dist/client/.vite/manifest.json +3485 -0
  26. package/dist/client/_assets/chunks/02611a045a7fe83a12014e3debc9f731-BqEDtwXC.woff2 +0 -0
  27. package/dist/client/_assets/chunks/033466ef683afe931f7f520cfb42d928-BluLHEh9.woff2 +0 -0
  28. package/dist/client/_assets/chunks/04b8bde59cff68eeee74fb1914eb09c7-D5j2CLTt.woff2 +0 -0
  29. package/dist/client/_assets/chunks/057a6a98bda7fe57105ddaa99ec82015-DY_AoijT.woff2 +0 -0
  30. package/dist/client/_assets/chunks/05ac821472e235943ed1435e4bb8ecad-CUm8GiH6.woff2 +0 -0
  31. package/dist/client/_assets/chunks/05da12edb9d52210581dc6ec4541031f-BXAOx2XP.woff2 +0 -0
  32. package/dist/client/_assets/chunks/074c1f8483d5a4d8c45c8c5f3e4cbb32-CDqdMQm8.woff2 +0 -0
  33. package/dist/client/_assets/chunks/0968e4861204b51f62a2f8e9f15dd5e0-D5tLgF_8.woff2 +0 -0
  34. package/dist/client/_assets/chunks/0acd1fe2b2ea1ad1bfee7ae1fa139d27-BKK1m412.woff2 +0 -0
  35. package/dist/client/_assets/chunks/0c2ab6b295e55f356f8020d4e7747522-CkCQjQw3.woff2 +0 -0
  36. package/dist/client/_assets/chunks/0c5f9492af03a4fa42c784de94649de1-CQlJqksa.woff2 +0 -0
  37. package/dist/client/_assets/chunks/0d5dec931dc885f07fe5cd5af8bed675-C74x-U_U.woff2 +0 -0
  38. package/dist/client/_assets/chunks/0d9a936885a4c39077438effd3779cbd-Bw0fYF4S.woff2 +0 -0
  39. package/dist/client/_assets/chunks/0e645da524f7cfc0e8c3c03fb2b08428-BVeI_5Th.woff2 +0 -0
  40. package/dist/client/_assets/chunks/0f5e1a18987dbc84ca05188c129e1936-B6_lZ6fE.woff2 +0 -0
  41. package/dist/client/_assets/chunks/1076f0f6f66d28d7a2f16427faad4413-ubiXnD1P.woff2 +0 -0
  42. package/dist/client/_assets/chunks/112743a4ab5fdd1498dfdf2b11336380-B5FzQxTK.woff2 +0 -0
  43. package/dist/client/_assets/chunks/1211f03d3ff5759a702631445793f60e-DDcSiyCt.woff2 +0 -0
  44. package/dist/client/_assets/chunks/12ef1ba76bd20b004b865266a1aa70b3-BwmaD87S.woff2 +0 -0
  45. package/dist/client/_assets/chunks/135e83b403475c5dc9e49b43501a5b84-C5mE_KZS.woff2 +0 -0
  46. package/dist/client/_assets/chunks/1380210a722ac9aeef54005ad7b015c9-CjA-MP82.woff2 +0 -0
  47. package/dist/client/_assets/chunks/13b2c53b30e6a3e4de7132dbc18dbdfc-B9WOB7kv.woff2 +0 -0
  48. package/dist/client/_assets/chunks/147b24c2f08d03bbed30887d4cb3311c-IeeFCJ13.woff2 +0 -0
  49. package/dist/client/_assets/chunks/152395634a207579552f8cb54db88599-C2y2JyNZ.woff2 +0 -0
  50. package/dist/client/_assets/chunks/15d95680dc31cc6ce20e0a5464106dc4-CpD8dqRm.woff2 +0 -0
  51. package/dist/client/_assets/chunks/16663e567f81f4725a1522f37e18f71f-BNoxS9pQ.woff2 +0 -0
  52. package/dist/client/_assets/chunks/1733f27476507ca68b68a803bc533cc2-DXL-14Ug.woff2 +0 -0
  53. package/dist/client/_assets/chunks/17c7f5d0a45e92ede0e5dec3a2c77efa-CNj3jQMl.woff2 +0 -0
  54. package/dist/client/_assets/chunks/1824321da801e6257b902f3d1d09054c-D4dgdyWB.woff2 +0 -0
  55. package/dist/client/_assets/chunks/188c2db794f3dd7a45889ddbc81da9bb-C9NbUBBO.woff2 +0 -0
  56. package/dist/client/_assets/chunks/1a08931435f885e707edb85978ea3bb6-BOiP2u7G.woff2 +0 -0
  57. package/dist/client/_assets/chunks/1a3a92c7c060a71a6c35819b9ebcdbb9-DFfonQfz.woff2 +0 -0
  58. package/dist/client/_assets/chunks/1be602ad456b0d75902d352116cb35fd-Bc-XQtQh.woff2 +0 -0
  59. package/dist/client/_assets/chunks/1cf737900dd49c2e88f1b3221a82a602-CfTmnns3.woff2 +0 -0
  60. package/dist/client/_assets/chunks/1fba7ec0e412e911bf31841de5a8a4e4-18ELMbFF.woff2 +0 -0
  61. package/dist/client/_assets/chunks/233d3a685ee18276b319363474599d47-BaFdJ0-G.woff2 +0 -0
  62. package/dist/client/_assets/chunks/2612c60f9dc5459ac42763591240a1d6-CTD69cq1.woff2 +0 -0
  63. package/dist/client/_assets/chunks/284b53bbefb06924cf236d24c8ed5641-CPDiuOv6.woff2 +0 -0
  64. package/dist/client/_assets/chunks/28a97af9ab9d38983d20f39ff09840e1-mFqcT3Jw.woff2 +0 -0
  65. package/dist/client/_assets/chunks/2a56eaf19d1d38a6b57e2a388f733676-CU52qBlE.woff2 +0 -0
  66. package/dist/client/_assets/chunks/2ab8cf1f23a5ac7939a7876054e711a7-B4T3dK7A.woff2 +0 -0
  67. package/dist/client/_assets/chunks/2abfbab82b6a7c04426afc054d2464da-D5PIIHde.woff2 +0 -0
  68. package/dist/client/_assets/chunks/2bfaadaf3479c72286248e6de0be0ec9-DEpU6O74.woff2 +0 -0
  69. package/dist/client/_assets/chunks/2c8c55e4cec85ce77e95cac9d330f5cf-BCTA96EZ.woff2 +0 -0
  70. package/dist/client/_assets/chunks/2e6f4bb71ef6b38765d51acc5a79f638-Mbs6PECD.woff2 +0 -0
  71. package/dist/client/_assets/chunks/2ed9981d2e8983365bd051159b976b6d-D8gg6NWM.woff2 +0 -0
  72. package/dist/client/_assets/chunks/2f4c633e923ba30c6ba376367379fc91-CIJCFztv.woff2 +0 -0
  73. package/dist/client/_assets/chunks/31194d303a67561926a544ed0e072aee-w3-eGYAi.woff2 +0 -0
  74. package/dist/client/_assets/chunks/31a197213ae61d7043c81013f52d10d6-DXZrrkJP.woff2 +0 -0
  75. package/dist/client/_assets/chunks/349965eee0a9b6c984a319ab96a4ece9-DByAVmXw.woff2 +0 -0
  76. package/dist/client/_assets/chunks/34e6750e00c3a911ef87dc66b336594d-DNbWesOu.woff2 +0 -0
  77. package/dist/client/_assets/chunks/3623d96698132c61e861f984ce11cc23-jqV9ErzX.woff2 +0 -0
  78. package/dist/client/_assets/chunks/36cda3eae13370b934bba4429fab9078-BPDcUlw9.woff2 +0 -0
  79. package/dist/client/_assets/chunks/36f3df4730cfca4fc77fe52b52e6a126-BwK10NC0.woff2 +0 -0
  80. package/dist/client/_assets/chunks/37887deb7a9c466cf2af6ee7ff372ea6-BrAerJB1.woff2 +0 -0
  81. package/dist/client/_assets/chunks/37dbd564820cce2f7b3331e442da0df3-Cql2kzIi.woff2 +0 -0
  82. package/dist/client/_assets/chunks/37dc8505b20b37a2477a9121b16d44a7-CoXw6XWV.woff2 +0 -0
  83. package/dist/client/_assets/chunks/39211b02f24c69cbd1b54f6d41b01933-4Xu7vvnq.woff2 +0 -0
  84. package/dist/client/_assets/chunks/3aa13f1843d7c8fa16109b6395a9a29d-BQFZUc3B.woff2 +0 -0
  85. package/dist/client/_assets/chunks/3aba89c4d4281553078de4622b498cbb-8QS1zDYk.woff2 +0 -0
  86. package/dist/client/_assets/chunks/3ac0a07878cacdc98bb889f40f5970d3-C-BaM6-y.woff2 +0 -0
  87. package/dist/client/_assets/chunks/3d88558097559d775231194d43745ba7-CR2aqGC7.woff2 +0 -0
  88. package/dist/client/_assets/chunks/3dc3e8c35524cad2d1236f3393104ccf-9sqBsF7B.woff2 +0 -0
  89. package/dist/client/_assets/chunks/3e24063b19abd2b051af986a6870046d-TlweM3SW.woff2 +0 -0
  90. package/dist/client/_assets/chunks/3e2c06bdd9dcab29aeaeb0cfb7fa608a-BaqU-dIt.woff2 +0 -0
  91. package/dist/client/_assets/chunks/424123f9371ba357087363263eeafcda-C8SVfEJi.woff2 +0 -0
  92. package/dist/client/_assets/chunks/43b3bbf43055c3e65db9cdf2c5b18c1c-CbHx6-Ki.woff2 +0 -0
  93. package/dist/client/_assets/chunks/4410f876d27c9821839474b6b1059fd7-ClYz91aY.woff2 +0 -0
  94. package/dist/client/_assets/chunks/44704471b60c18b3ecec5300a88f1102-DDmmy6-m.woff2 +0 -0
  95. package/dist/client/_assets/chunks/45140742dfa8686b58db2899ce89f89a-D5IMMzDM.woff2 +0 -0
  96. package/dist/client/_assets/chunks/46604090efbb99c2db1800928af0a239-JX2fpYX_.woff2 +0 -0
  97. package/dist/client/_assets/chunks/46998df85d31e629c0142f0556f5e7c5-BIkNG9Wh.woff2 +0 -0
  98. package/dist/client/_assets/chunks/469d2754e315e77270c12fa0ab28026c-D-y-vsfA.woff2 +0 -0
  99. package/dist/client/_assets/chunks/479329a39a3eb6c0047e6b0981919855-vZKCRx42.woff2 +0 -0
  100. package/dist/client/_assets/chunks/491bbba374093fff045f55803a22bfef-DSQXiDCd.woff2 +0 -0
  101. package/dist/client/_assets/chunks/49902f1c41b281f8eb3771abdd4dccb8-DxVxNg2W.woff2 +0 -0
  102. package/dist/client/_assets/chunks/4c0760284569b87d8f2290c324953973-Bqd-TKHH.woff2 +0 -0
  103. package/dist/client/_assets/chunks/4cba92c942694bdc479bc54101bb1aa4-BrfsrgrC.woff2 +0 -0
  104. package/dist/client/_assets/chunks/4d38e56738156625329d93bd5c8cc74a-DONkMP0y.woff2 +0 -0
  105. package/dist/client/_assets/chunks/4db4f31a16965baa42cb7dad667a8f04-Dt1Cy4gr.woff2 +0 -0
  106. package/dist/client/_assets/chunks/4e7d263c30bb48604069a3b4404d4eeb-DNPCkvKD.woff2 +0 -0
  107. package/dist/client/_assets/chunks/5279c7e465d9871edf51fd02e691eee7-CKhyhvVf.woff2 +0 -0
  108. package/dist/client/_assets/chunks/527ff336279465617aafbafe1a830632-CC3lv4pm.woff2 +0 -0
  109. package/dist/client/_assets/chunks/54bfdb6f21f9d6191e5100580239e14f-PGlqYvgl.woff2 +0 -0
  110. package/dist/client/_assets/chunks/56b40518ea0608e62826bd44ee27f5f9-BD9F9Oss.woff2 +0 -0
  111. package/dist/client/_assets/chunks/56be5e32fd01668fe5ba814d94905839-CSyFA-9l.woff2 +0 -0
  112. package/dist/client/_assets/chunks/572b9e28f1e6d89b765a16d809db84a1-BIYMjzEa.woff2 +0 -0
  113. package/dist/client/_assets/chunks/57e9a86651366c1ba299e47aa7e358c2-BHuZXB4W.woff2 +0 -0
  114. package/dist/client/_assets/chunks/57f89adeae55aa7fe2add3fc1801ce03-Cy3PNrfx.woff2 +0 -0
  115. package/dist/client/_assets/chunks/5921cef36750ff6092d94b83a802823b-hF-uymTh.woff2 +0 -0
  116. package/dist/client/_assets/chunks/59e95d8b5332dcdae894a19946ab767d-ClRfsjWZ.woff2 +0 -0
  117. package/dist/client/_assets/chunks/5acc7f736217259db79a42bf44241c48-FYQUuFOM.woff2 +0 -0
  118. package/dist/client/_assets/chunks/5c05f8b75c9d7f25b3c04ca711cf43b3-D-YWPkSq.woff2 +0 -0
  119. package/dist/client/_assets/chunks/5df9ce98c75f8c50fb4fff7f01a23925-DgoCENyW.woff2 +0 -0
  120. package/dist/client/_assets/chunks/5e0124c7265f1b4cf0fc797b94efbebc-BTBNHsSB.woff2 +0 -0
  121. package/dist/client/_assets/chunks/5ec349ff62b9ee5fe0c6dae867704dc4-B4dezhrz.woff2 +0 -0
  122. package/dist/client/_assets/chunks/60020a830d809c26970cf8e48ba1eeda-B6FDTSik.woff2 +0 -0
  123. package/dist/client/_assets/chunks/601f812e8b1eafecd5ba585a6a6d7962-gIVBlUU9.woff2 +0 -0
  124. package/dist/client/_assets/chunks/6025ddaf99d86cc8bfd781006b19fbd6-CS-ze337.woff2 +0 -0
  125. package/dist/client/_assets/chunks/60bcfa3ea7446eb42394aab02d28be40-C-PQfCu3.woff2 +0 -0
  126. package/dist/client/_assets/chunks/60f6e28b3f0400b0e8fe32c3c7116102-Dag9L2qU.woff2 +0 -0
  127. package/dist/client/_assets/chunks/615a7e0d63f257109d599b2a1977de68-D6Y93yoG.woff2 +0 -0
  128. package/dist/client/_assets/chunks/615b38e0b5bb3ea68426f44f47706e6f-B5yzIIer.woff2 +0 -0
  129. package/dist/client/_assets/chunks/61a2c80d0c924d5a6187c02e8d1d1642-BY3MmHbZ.woff2 +0 -0
  130. package/dist/client/_assets/chunks/62fc94f8d4c5a750b7f25e7387539910-3ql2_WDK.woff2 +0 -0
  131. package/dist/client/_assets/chunks/639c7c6b0b0c27c738702741cfa4b8c0-BCD3OGri.woff2 +0 -0
  132. package/dist/client/_assets/chunks/63fafcb069520613d0ea35ad3e6b1e42-DP0Rrj4o.woff2 +0 -0
  133. package/dist/client/_assets/chunks/64a404d675f1d726f0891b7a80794595-DU3L0I9s.woff2 +0 -0
  134. package/dist/client/_assets/chunks/65a60d87c64228d258a123cbe85f5f31-B4kbf-79.woff2 +0 -0
  135. package/dist/client/_assets/chunks/65e9c4585d71bf48a5c62f367010da16-CvzgzReN.woff2 +0 -0
  136. package/dist/client/_assets/chunks/68a3a4bf337f8a0722be76676e20b850-Dnaw8EPD.woff2 +0 -0
  137. package/dist/client/_assets/chunks/6968e889e18891d912809fe484c2e745-Bj1m5e0s.woff2 +0 -0
  138. package/dist/client/_assets/chunks/6abdd163d2c4b85698db8aa1ce149361-B5FVGJuo.woff2 +0 -0
  139. package/dist/client/_assets/chunks/6b75213eb0be40ce84241eb2bb438a2e-CqHmF-NR.woff2 +0 -0
  140. package/dist/client/_assets/chunks/70714891ad3fbfc3d5f10a8669dacc5a-BWk5nLFN.woff2 +0 -0
  141. package/dist/client/_assets/chunks/7158022889c6c177062ac85036e7af10-B3D4K22n.woff2 +0 -0
  142. package/dist/client/_assets/chunks/73b794d885b88f7befb7ea8ab1780a15-D0ZehnEI.woff2 +0 -0
  143. package/dist/client/_assets/chunks/743f290baf027d4626a86e22f3d44600-DDTDQdRC.woff2 +0 -0
  144. package/dist/client/_assets/chunks/7520ca6ca8c60eb1e62d50e71c8d30f1-CVRaGu2G.woff2 +0 -0
  145. package/dist/client/_assets/chunks/757019c01c5e2c2b0f54293cea3b5636-BTovUnoH.woff2 +0 -0
  146. package/dist/client/_assets/chunks/759a647b77791b1e98f99bc0ab5317a7-CrK6FIFz.woff2 +0 -0
  147. package/dist/client/_assets/chunks/761d84afca8d7f34eebefe538adba827-DRbiVuwX.woff2 +0 -0
  148. package/dist/client/_assets/chunks/76e07530323418ec723c5d7634c9bcca-DGtVJTTH.woff2 +0 -0
  149. package/dist/client/_assets/chunks/76e51630143b95b6322ef93ad330da7a-D-kV82C_.woff2 +0 -0
  150. package/dist/client/_assets/chunks/76f117a3baacda304a71965a17266a13-DFfvsFUm.woff2 +0 -0
  151. package/dist/client/_assets/chunks/773819b7b9b8fd404a929867c0fd677e-Bu1xCMZb.woff2 +0 -0
  152. package/dist/client/_assets/chunks/77747f17625a259fb405b7bbdd84ac4b-o_vNfh-6.woff2 +0 -0
  153. package/dist/client/_assets/chunks/7817dd16805145d8538ad57590f69f5a-DguSUP3O.woff2 +0 -0
  154. package/dist/client/_assets/chunks/788498548ddfb710d6ef3c7bff79fa97-3ffoQfRd.woff2 +0 -0
  155. package/dist/client/_assets/chunks/78ea6c40923ce95367e3517dd1e5a849-DvLDRhX9.woff2 +0 -0
  156. package/dist/client/_assets/chunks/79aa7c8c842c4a27cf57c0a3a1ffca5a-sDoCeoKI.woff2 +0 -0
  157. package/dist/client/_assets/chunks/7b59f0ec7792b18458dc5a361e37884c-DWvxkKmT.woff2 +0 -0
  158. package/dist/client/_assets/chunks/7c3945788a689a69356c1a622d69d48b-1JjIpss2.woff2 +0 -0
  159. package/dist/client/_assets/chunks/7cc72fd2c9105560422b6a67c6162945-Co3AF5_G.woff2 +0 -0
  160. package/dist/client/_assets/chunks/7d0ea0690e432462f4d05a23d720ec19-Dx7DK4C3.woff2 +0 -0
  161. package/dist/client/_assets/chunks/7dffa5c1bec57e0903fd62357401ff1a-DuQHmiiN.woff2 +0 -0
  162. package/dist/client/_assets/chunks/7e38ca789a9e76ac91d9256374e154f0-BtETgGRe.woff2 +0 -0
  163. package/dist/client/_assets/chunks/7f9a6c9286b68de9c72b0024f7beeb40-JpXdo4FC.woff2 +0 -0
  164. package/dist/client/_assets/chunks/815a0b797465190f60d8b1a04656c42e-Spzo94nU.woff2 +0 -0
  165. package/dist/client/_assets/chunks/823d1bb11097331238d103c7f72138dc-CGN6m2VF.woff2 +0 -0
  166. package/dist/client/_assets/chunks/82f6ccc063960eed1cdfd1d61ada8862-CsmAqx2a.woff2 +0 -0
  167. package/dist/client/_assets/chunks/835b6505bb9eea9678925a1fa885353d-C6mSBwUc.woff2 +0 -0
  168. package/dist/client/_assets/chunks/845b4b67564d62cf5cad242f7ac08ea5-DnvzrloJ.woff2 +0 -0
  169. package/dist/client/_assets/chunks/84c8e7bc0931008ed91f44ed12dfea94-C2Hvhh-b.woff2 +0 -0
  170. package/dist/client/_assets/chunks/8726558cf297cbda831cc19514897205-gSFV1vW0.woff2 +0 -0
  171. package/dist/client/_assets/chunks/8855472f3c02f6c7ebb3216617ebe4af-C1JsWVqq.woff2 +0 -0
  172. package/dist/client/_assets/chunks/887a11290ba78b1e66c6d2f67043005e-L_A1f5Fz.woff2 +0 -0
  173. package/dist/client/_assets/chunks/888ba53510213d5d1f6a7deb60e569b6-OP8E-ELN.woff2 +0 -0
  174. package/dist/client/_assets/chunks/88db1d042074fb6e66821ffc10941930-kZ6K3e62.woff2 +0 -0
  175. package/dist/client/_assets/chunks/892aa49b2529c89bb4076d4aa51fe30f-Bw0_Km2A.woff2 +0 -0
  176. package/dist/client/_assets/chunks/8b5c9ef81159f31d2ab35f45ca4373e0-Djw95sKC.woff2 +0 -0
  177. package/dist/client/_assets/chunks/8b91c7c2ed390f1278b9befa3aa87233-B2C3iFbQ.woff2 +0 -0
  178. package/dist/client/_assets/chunks/8dc18c0cebe6aa4bf4c45dbb831048ab-bUlANLit.woff2 +0 -0
  179. package/dist/client/_assets/chunks/8e231d707f0c4f8e3cde90a6b52a79aa-CvwbWlul.woff2 +0 -0
  180. package/dist/client/_assets/chunks/8fb45117a62d92dce44a80f0b729ead5-Djr4-o5-.woff2 +0 -0
  181. package/dist/client/_assets/chunks/9064c12fd72c7aba235d8c3881755b91-BnguxT-p.woff2 +0 -0
  182. package/dist/client/_assets/chunks/90b7848e9b1623b77bdcf155e90b839c-CFvPRnoM.woff2 +0 -0
  183. package/dist/client/_assets/chunks/911b9e53e9814de2998c60bf3115f560-DmPmj635.woff2 +0 -0
  184. package/dist/client/_assets/chunks/919a879bd2d580d8491a31a449390689-BY2w-8_Y.woff2 +0 -0
  185. package/dist/client/_assets/chunks/91da6cb174bebfb96976e2f1316be2fe-DztAJb6W.woff2 +0 -0
  186. package/dist/client/_assets/chunks/92fdc376bce277874e75db666f175b44-DJUnHEIN.woff2 +0 -0
  187. package/dist/client/_assets/chunks/930a23430f0eb64480d7fe5f82834e21-2VrDPT7E.woff2 +0 -0
  188. package/dist/client/_assets/chunks/934dfdbed2bb2c4b6129199d699a34fa-CtpHsjRD.woff2 +0 -0
  189. package/dist/client/_assets/chunks/93bed0e5f2dd5a21cf73304fcfbc149d-DJiaETX7.woff2 +0 -0
  190. package/dist/client/_assets/chunks/93f3ea6918533d96d9c252378b9a4af0-D-FJaGi-.woff2 +0 -0
  191. package/dist/client/_assets/chunks/95a0951d2a2722ff773a1a45e8c474d6-DuZg-BpP.woff2 +0 -0
  192. package/dist/client/_assets/chunks/95ff41191c76ef893ac70a1043e95e44-C08QDbJO.woff2 +0 -0
  193. package/dist/client/_assets/chunks/96c75862c8cec51a7c05ff025fea86cd-DBnRwCuW.woff2 +0 -0
  194. package/dist/client/_assets/chunks/97d7a17234f2b5030ae9697ae00aded2-CkUSaB-p.woff2 +0 -0
  195. package/dist/client/_assets/chunks/982cd1765ca6bbb3e6547e47834d5148-DkIybx1r.woff2 +0 -0
  196. package/dist/client/_assets/chunks/985d4d41afa0934a4eb2de35473fb9a2-CF7rgJMw.woff2 +0 -0
  197. package/dist/client/_assets/chunks/995c4fda62d25c3b79dd5987737df6ae-CY5ri8HW.woff2 +0 -0
  198. package/dist/client/_assets/chunks/9b0c3caac458c7bc9c9133e40405ddea-DhPrYOcz.woff2 +0 -0
  199. package/dist/client/_assets/chunks/9b21e8244f5930c48ad5073f83777b6c-CuE9-r-r.woff2 +0 -0
  200. package/dist/client/_assets/chunks/9bb38fd05201de666b88b82d56a386f7-nMl6lB8X.woff2 +0 -0
  201. package/dist/client/_assets/chunks/9d0afa53dc2f92ba42024f55f11f6779-Bozpx8bL.woff2 +0 -0
  202. package/dist/client/_assets/chunks/9e13a47242926f1cd7d68c08d9ef8889-DwKQjc8M.woff2 +0 -0
  203. package/dist/client/_assets/chunks/9eff2ab2a9d2ced47ab761bf6fdb11ec-D86-Zm1H.woff2 +0 -0
  204. package/dist/client/_assets/chunks/9f1963fe8d4d6878d717d872a8f3fdb5-DxjITUFq.woff2 +0 -0
  205. package/dist/client/_assets/chunks/9f3bdab4025dc7c5ba1a5871e62e8918-BqxDEFLJ.woff2 +0 -0
  206. package/dist/client/_assets/chunks/9fce02d1a09f464c36c9fcfb14a354c5-F2tMAB_j.woff2 +0 -0
  207. package/dist/client/_assets/chunks/a00e94d4f04df6aa659db9c4954c7efe-CRBKCj_3.woff2 +0 -0
  208. package/dist/client/_assets/chunks/a0a6755c7e3f9e4cf03387027dd9f16c-BAyRqF2w.woff2 +0 -0
  209. package/dist/client/_assets/chunks/a0d00fe4816c95a8c7dffd445ef00b03-CxcTiRLJ.woff2 +0 -0
  210. package/dist/client/_assets/chunks/a0f199077fa1a33bc2a1e01c64de7fe6-IdgVJmZD.woff2 +0 -0
  211. package/dist/client/_assets/chunks/a2b0597837e382aca19919c4b74e58c8-7iVOBjzI.woff2 +0 -0
  212. package/dist/client/_assets/chunks/a373e85c96aa0dc303092429fb837e51-Dvmw8mbX.woff2 +0 -0
  213. package/dist/client/_assets/chunks/a379ada89abd750c587ded29f77e731c-CgPBsEsv.woff2 +0 -0
  214. package/dist/client/_assets/chunks/a4475c442c6b259354c9eda62e64e7df-TZ0c8vK-.woff2 +0 -0
  215. package/dist/client/_assets/chunks/a48be56ce5d3a99dc8f8331398004412-BR_ixH1W.woff2 +0 -0
  216. package/dist/client/_assets/chunks/a4a39b5f048671aa37c2b2a9581ab08a-EDfDYsUd.woff2 +0 -0
  217. package/dist/client/_assets/chunks/a534449d67561ac9b06489f64e57ad98-C355TCrD.woff2 +0 -0
  218. package/dist/client/_assets/chunks/a6e044424780a57610833cc856c15bf5-x1b8FE4H.woff2 +0 -0
  219. package/dist/client/_assets/chunks/a7c35e42a659347e490c3cb7983b2e7f-BWpA0-oz.woff2 +0 -0
  220. package/dist/client/_assets/chunks/a7c58070d68dc1724e9d4e781afb78db-ql27pT-6.woff2 +0 -0
  221. package/dist/client/_assets/chunks/a806759e72987951d6d08b7cbdf36a1b-TvLDwB_u.woff2 +0 -0
  222. package/dist/client/_assets/chunks/a88481ec8bc7be11cb66e46781a79bb9-Ck-r_sGx.woff2 +0 -0
  223. package/dist/client/_assets/chunks/a919a8c6fec6d7e8bb21177965f2e9d7-CC5uWzwd.woff2 +0 -0
  224. package/dist/client/_assets/chunks/a9a016d4a93409f65278327e8a8bb38d-dylrNwF8.woff2 +0 -0
  225. package/dist/client/_assets/chunks/aa662092cf249707123ca4d575e2764b-CSxNsTUL.woff2 +0 -0
  226. package/dist/client/_assets/chunks/aae1eda193285ab817a2d1b408440326-CjyjBoRm.woff2 +0 -0
  227. package/dist/client/_assets/chunks/ab92d41062a7644faa45f50bf384454a-Cq0X8a9H.woff2 +0 -0
  228. package/dist/client/_assets/chunks/accc43762ad05edfbff66ba1380d192a-B5jIykaZ.woff2 +0 -0
  229. package/dist/client/_assets/chunks/ad47bcd6d4663026d648c132d969318d-BYdIbDge.woff2 +0 -0
  230. package/dist/client/_assets/chunks/ad67113f88b59582991cb0c5d33ea19f-DOH1vcIZ.woff2 +0 -0
  231. package/dist/client/_assets/chunks/adb6e613fb99c6dd660b727101f554a8-DJDw_ZZK.woff2 +0 -0
  232. package/dist/client/_assets/chunks/adc4a98a89870ef984ee8f4b57c8a3a5-D30vw26R.woff2 +0 -0
  233. package/dist/client/_assets/chunks/af57a2e8c72f9ba0efb1af771d32c124-D5Zwq9pS.woff2 +0 -0
  234. package/dist/client/_assets/chunks/b0ae5f374e43dac992a4a5d334cebc0b-Ccoi4dPt.woff2 +0 -0
  235. package/dist/client/_assets/chunks/b0cb664cb2e1371efda8943c0b7dcd1c-YzGFnelF.woff2 +0 -0
  236. package/dist/client/_assets/chunks/b0d1cdced482352cf0d3ae58638aacb9-IunWfc6R.woff2 +0 -0
  237. package/dist/client/_assets/chunks/b12379ab782468d725519cd07a7d15bc-IjbydKs2.woff2 +0 -0
  238. package/dist/client/_assets/chunks/b320f5d185b2cff933ac549c184031c5-IYnPrIz4.woff2 +0 -0
  239. package/dist/client/_assets/chunks/b326a8a9c33b554db570da94f60bc380-B6dzaCQg.woff2 +0 -0
  240. package/dist/client/_assets/chunks/b55486f8a459c838fe329d4e79a8c211-DW8q7oGV.woff2 +0 -0
  241. package/dist/client/_assets/chunks/b6117ff2993b11bb1fdc7ea3588a010c-JoMYL5W_.woff2 +0 -0
  242. package/dist/client/_assets/chunks/b61982951bd51b724143c30dfaaa9fe9-D3KhBKl6.woff2 +0 -0
  243. package/dist/client/_assets/chunks/b91234c10fd8b8c8abc88e03afe66a1f-BABNh2la.woff2 +0 -0
  244. package/dist/client/_assets/chunks/b9317198f118a1dfd8ddf2b82ec028f3-CiMecxeH.woff2 +0 -0
  245. package/dist/client/_assets/chunks/ba825ae79b1a6f7e0cce5215fcb5c96f-B6EWU_D5.woff2 +0 -0
  246. package/dist/client/_assets/chunks/ba9c59d7dfa4494db1bb764ada81467d-D0yPcC1m.woff2 +0 -0
  247. package/dist/client/_assets/chunks/bab547459d514f46206e340c4bb2dc88-YnBqlGhw.woff2 +0 -0
  248. package/dist/client/_assets/chunks/bb0b15492b8cdbbec57c0bcfa0aa9241-BjIFbEsN.woff2 +0 -0
  249. package/dist/client/_assets/chunks/bc6183ac08f0fac78c46f80c10cf7c92-B81nbRb3.woff2 +0 -0
  250. package/dist/client/_assets/chunks/bc8ccea5abf6598cf3cfa97eb59804bb-B_7IbluG.woff2 +0 -0
  251. package/dist/client/_assets/chunks/bc95a792cbca6639214c9b0da13392ff-CHQe0E-D.woff2 +0 -0
  252. package/dist/client/_assets/chunks/bcc39eda837bb7a7a3d37c8c60fffb81-CGd1Zk6S.woff2 +0 -0
  253. package/dist/client/_assets/chunks/bcfa62f35731856246c146d3a6932bf3-DMDq6w4v.woff2 +0 -0
  254. package/dist/client/_assets/chunks/bd73264d7f98776708d5d6f3c9b78fcc-d9QWWMym.woff2 +0 -0
  255. package/dist/client/_assets/chunks/bed610b217d500f5975cfc9fe6157570-B5t7s1bw.woff2 +0 -0
  256. package/dist/client/_assets/chunks/bedc74b423b7293b6ad0bdecc61c42cc-4Nnh7peP.woff2 +0 -0
  257. package/dist/client/_assets/chunks/bf8901f8f11d4f433ea17dabc9370ea6-B8dkfvGn.woff2 +0 -0
  258. package/dist/client/_assets/chunks/bfb00d4a4c48661bd0be99f300a0faae-OxsWZ7l-.woff2 +0 -0
  259. package/dist/client/_assets/chunks/c0098958e20db68cab90097b5e62516f-VpjQo3vV.woff2 +0 -0
  260. package/dist/client/_assets/chunks/c063897793f593eb26d6ff7b7baaba18-BWeJJ1sH.woff2 +0 -0
  261. package/dist/client/_assets/chunks/c1b0df29ae41d764904df84e9ac83d1e-D1N934vs.woff2 +0 -0
  262. package/dist/client/_assets/chunks/c4143bb9f2fe77b6ccf20088a8904650-nHfYM24e.woff2 +0 -0
  263. package/dist/client/_assets/chunks/c42c67070cfe99cf823df92d81c7fa6e-B_a405Ez.woff2 +0 -0
  264. package/dist/client/_assets/chunks/c4bfaa5e50798246e3770718b7a7c84a-B-r6Y_o0.woff2 +0 -0
  265. package/dist/client/_assets/chunks/c4d749e45ecd5a5aed5a0bb3ebfd355d-CkqE1fRi.woff2 +0 -0
  266. package/dist/client/_assets/chunks/c6c2971ad1f3221f6cf84028aa0f477e-D7BZGIcY.woff2 +0 -0
  267. package/dist/client/_assets/chunks/c7ff3f6bbdcd5f604b7343602ab904df-Bq7UjVrZ.woff2 +0 -0
  268. package/dist/client/_assets/chunks/c84598999133455503042e06f4ab79cb-DIXndJX3.woff2 +0 -0
  269. package/dist/client/_assets/chunks/c945c62368357d05a53206620460fb30-82gFEf6n.woff2 +0 -0
  270. package/dist/client/_assets/chunks/c96d83978add28b356c22c4c84916733-BL4VaPIE.woff2 +0 -0
  271. package/dist/client/_assets/chunks/c97f41eef722121d86f55d553c056a39-CjejgQO2.woff2 +0 -0
  272. package/dist/client/_assets/chunks/ca62704509932d3232d62918de97af3f-CKWF-3mX.woff2 +0 -0
  273. package/dist/client/_assets/chunks/ca9a533988d7019597a60d4e17127e0c-ClZ6ygZM.woff2 +0 -0
  274. package/dist/client/_assets/chunks/cb7ccd6494256f7a2977b7c2b0225592-Cxp8XObm.woff2 +0 -0
  275. package/dist/client/_assets/chunks/ce022e18a1377ac509443c3c3790b431-DpH0t6Tn.woff2 +0 -0
  276. package/dist/client/_assets/chunks/ce41d70ce6a069a498525c9e15c45cf2-DGtgv-_b.woff2 +0 -0
  277. package/dist/client/_assets/chunks/cf2b28f90f47276f7e2688a65e88a101-LVbNkXHj.woff2 +0 -0
  278. package/dist/client/_assets/chunks/cf9cffe56636322f62b40d61130fbc5e-DNCBMqAo.woff2 +0 -0
  279. package/dist/client/_assets/chunks/d1f064825fa5784b5c930652bd831cce-BU6qwjp8.woff2 +0 -0
  280. package/dist/client/_assets/chunks/d20d8944bc0b85f5b2aae4b24f343516-oY9l-NmC.woff2 +0 -0
  281. package/dist/client/_assets/chunks/d2faabcedd19f016e7b21bce073c0ec8-Dv6eFCf5.woff2 +0 -0
  282. package/dist/client/_assets/chunks/d320171e57480510f87dbbc7d5264b0a-CcCfjaFX.woff2 +0 -0
  283. package/dist/client/_assets/chunks/d36644d6502527e1fff205d0c7eca434-kVBj8zy_.woff2 +0 -0
  284. package/dist/client/_assets/chunks/d3a72a99d365dddfbca8d017a8011368-Bgdmhl3M.woff2 +0 -0
  285. package/dist/client/_assets/chunks/d40ab99c7a38026f411c8f112f742b48-D7Nm7fg6.woff2 +0 -0
  286. package/dist/client/_assets/chunks/d42aafa0f246ad3b4c16fe96d3a3a432-CRxTqZXU.woff2 +0 -0
  287. package/dist/client/_assets/chunks/d4aaf23c13ae808a4bed617afd13aa07-d_T1SKe9.woff2 +0 -0
  288. package/dist/client/_assets/chunks/d509ee5c8241bc7c3de2039d75564fa5-BpNFD9JE.woff2 +0 -0
  289. package/dist/client/_assets/chunks/d675d717cd329bfd4c0524f76ae1579c-Bd5_ctoQ.woff2 +0 -0
  290. package/dist/client/_assets/chunks/d689b1861d7e4377dd72ad3013482612-jPsVOU3F.woff2 +0 -0
  291. package/dist/client/_assets/chunks/d6bb686bddfbe8f38a36a68e609a8667-C_QduS-X.woff2 +0 -0
  292. package/dist/client/_assets/chunks/d9020ff69a83b2a6ee1f42ae480f7db0-DLBUPMCe.woff2 +0 -0
  293. package/dist/client/_assets/chunks/d9047070d72a816b3dba9d40c2d85e69-LTQ42vEp.woff2 +0 -0
  294. package/dist/client/_assets/chunks/da04549f3f4ed28076b01b8cd710d313-CDXOeduw.woff2 +0 -0
  295. package/dist/client/_assets/chunks/da7af303f8c645f9a9dbae0e6e32dd35-DCkS2jqi.woff2 +0 -0
  296. package/dist/client/_assets/chunks/da90a9012ab2d98f759e3fa0820ef502-D0D9d1zg.woff2 +0 -0
  297. package/dist/client/_assets/chunks/dc6e234ded795e91f76d6647f628fbf0-IkooXcRh.woff2 +0 -0
  298. package/dist/client/_assets/chunks/dc8f6256445e68199540be9ade33529f-umb5tWbZ.woff2 +0 -0
  299. package/dist/client/_assets/chunks/dd0cfdfdac0866e66d587e2b5a9e9961-bl4ozE0c.woff2 +0 -0
  300. package/dist/client/_assets/chunks/de304c8a02e45ded4f8dcc479d167198-ClRu89-4.woff2 +0 -0
  301. package/dist/client/_assets/chunks/df10d94bea357a43313e20da5d84bd30-CmAgh2oA.woff2 +0 -0
  302. package/dist/client/_assets/chunks/df9568257eb29b156449fdd4bec5ec76-BMz6aLRr.woff2 +0 -0
  303. package/dist/client/_assets/chunks/e067cd0ed76c90cd0a93c9339253f20b-6RxCQ4jy.woff2 +0 -0
  304. package/dist/client/_assets/chunks/e08b07772e7bed3cec2832d43f7fd339-CQCkeKsr.woff2 +0 -0
  305. package/dist/client/_assets/chunks/e12150d5a39b30be8f567968c7a527b0-DjdoWzeQ.woff2 +0 -0
  306. package/dist/client/_assets/chunks/e34b2b141e472dc776c86fdf8eea23b0-Cjk7H-Ev.woff2 +0 -0
  307. package/dist/client/_assets/chunks/e444fd88f1390636e603d0d681538ac8-Bx7CPOZh.woff2 +0 -0
  308. package/dist/client/_assets/chunks/e53fcb2381eee345db4f6f973dd95a3e-DmkkCUWz.woff2 +0 -0
  309. package/dist/client/_assets/chunks/e5bd313ef81f687d398aacb11cec3069-CwBEbcji.woff2 +0 -0
  310. package/dist/client/_assets/chunks/e79898628283edc27180fc39d0d769c1-By2pplQz.woff2 +0 -0
  311. package/dist/client/_assets/chunks/e8c15be62faf978d208925e79ea6a10d-Ct5M9lTP.woff2 +0 -0
  312. package/dist/client/_assets/chunks/e8c364c16daa04835bf32d293d2598db-3v4Tut_d.woff2 +0 -0
  313. package/dist/client/_assets/chunks/e91c4d941ed9f5c2f3e27f205a3a225e-BwOJ2Lst.woff2 +0 -0
  314. package/dist/client/_assets/chunks/e9c566be7a5d38a9085225f7372bc82b-BCtnCVML.woff2 +0 -0
  315. package/dist/client/_assets/chunks/e9ebc567a711eeb29019ddae3e0ce7fe-CnNhV49O.woff2 +0 -0
  316. package/dist/client/_assets/chunks/eaf332445a40942928e5f5750c1c3116-qZ5LSRrj.woff2 +0 -0
  317. package/dist/client/_assets/chunks/eb5afb3d952b8593782caec6026514b6-BXugZF2T.woff2 +0 -0
  318. package/dist/client/_assets/chunks/ec86e23683052da5cfdc3b77641bd15a-BeLcbcRc.woff2 +0 -0
  319. package/dist/client/_assets/chunks/ecb8875a56c7b038b35432fda41ae128-BVqgNf1o.woff2 +0 -0
  320. package/dist/client/_assets/chunks/eda1b0cb6d1719dd9bedcf3216a9e8de-BJTfLL5z.woff2 +0 -0
  321. package/dist/client/_assets/chunks/edd6a4f608d04fc0351d7688cfc321e4-d3V4U40c.woff2 +0 -0
  322. package/dist/client/_assets/chunks/ee6fce20b420a480714607c66d7f97e5-DEtC-G5G.woff2 +0 -0
  323. package/dist/client/_assets/chunks/eee3836d6ac17ebb2c450bbcbc9db121-C5a2TZjQ.woff2 +0 -0
  324. package/dist/client/_assets/chunks/et-book-bold-line-figures-BFJr2_zv.woff +0 -0
  325. package/dist/client/_assets/chunks/et-book-display-italic-old-style-figures-CoeRJAe8.woff +0 -0
  326. package/dist/client/_assets/chunks/et-book-roman-line-figures-CaA40oOf.woff +0 -0
  327. package/dist/client/_assets/chunks/f1661731474b78bdf81114daf10b254f-KnMKX_U3.woff2 +0 -0
  328. package/dist/client/_assets/chunks/f2c58dee206ba9355046fc23d05491f7-Cm4tCWav.woff2 +0 -0
  329. package/dist/client/_assets/chunks/f50ac27ea4358d67fdda403c2bb52467-WCfYHde1.woff2 +0 -0
  330. package/dist/client/_assets/chunks/f55334112f8e8be82d65db29887a663f-vDq1zH8k.woff2 +0 -0
  331. package/dist/client/_assets/chunks/f5738255e92d8dd34a46d1bcdf4c4074-tpghTzL1.woff2 +0 -0
  332. package/dist/client/_assets/chunks/f740e93f3c0277ecc616594103bca683-C4BgW_ko.woff2 +0 -0
  333. package/dist/client/_assets/chunks/f7633b5af033d76ff2fb3c3c266d77c5-BCkrZxwO.woff2 +0 -0
  334. package/dist/client/_assets/chunks/f7d8468cba2335a83ee414ea68291bab-CVnotkZU.woff2 +0 -0
  335. package/dist/client/_assets/chunks/f91cd722855f4269256eae1187df64ec-DJlAx1A_.woff2 +0 -0
  336. package/dist/client/_assets/chunks/f9695c6c4df2bf6bc03045ff79d4f01f-DDaYGYHT.woff2 +0 -0
  337. package/dist/client/_assets/chunks/fa6e58ce4b52695e7ae19bbea6336ec8-B30jEpze.woff2 +0 -0
  338. package/dist/client/_assets/chunks/fada6eaa68ff8816afe43d2a36c5423e-DlGAWNPb.woff2 +0 -0
  339. package/dist/client/_assets/chunks/fbcc4bf5367218951172bdee6f77d7a6-Dax2VSCd.woff2 +0 -0
  340. package/dist/client/_assets/chunks/fc895f5ce66b656f4a933097bf2a8775-DjjysNdf.woff2 +0 -0
  341. package/dist/client/_assets/chunks/fc9ae5b600fb711f2d67e93ce768cba4-D1lx7LBz.woff2 +0 -0
  342. package/dist/client/_assets/chunks/fca6720fd14c467d29a90f18ef3859b9-B3btrwq3.woff2 +0 -0
  343. package/dist/client/_assets/chunks/fd1bb507bcbf04856eeb7f7fd47ea579-NR1nGJTt.woff2 +0 -0
  344. package/dist/client/_assets/chunks/fd1bceb55d3e0183ac2454b8532fec7d-D3t-v6nn.woff2 +0 -0
  345. package/dist/client/_assets/chunks/fd2069e6e8588c70b4f11364093b81f2-BoXhYVgw.woff2 +0 -0
  346. package/dist/client/_assets/chunks/{url-CG0eolsk.js → url-pLre2DM_.js} +1 -1
  347. package/dist/client/_assets/client-C_kImWZj.css +2 -0
  348. package/dist/client/_assets/client-D95FNDg5.js +272 -0
  349. package/dist/client/_assets/client-auth-CXILhW1b.js +4547 -0
  350. package/dist/client/_assets/client-cjk-jp-DZwrTzQC.css +1 -0
  351. package/dist/client/_assets/client-cjk-kr-_3ZNI2ZP.css +1 -0
  352. package/dist/env-CgaH9Mut.js +252 -0
  353. package/dist/github-api-BkRWnqMx.js +176 -0
  354. package/dist/github-app-WeadXMb8.js +275 -0
  355. package/dist/github-sync-7y_nTXx1.js +4723 -0
  356. package/dist/index.js +88 -2
  357. package/dist/node.js +71 -30
  358. package/dist/url-umUptr5z.js +334 -0
  359. package/package.json +16 -12
  360. package/src/__tests__/bin/content-cli.test.ts +179 -0
  361. package/src/__tests__/bin/media-cli.test.ts +192 -0
  362. package/src/__tests__/export-hugo-build.test.ts +178 -0
  363. package/src/__tests__/export-import-roundtrip.test.ts +265 -0
  364. package/src/__tests__/export-service.test.ts +763 -510
  365. package/src/__tests__/helpers/app.ts +15 -4
  366. package/src/__tests__/helpers/export-fixtures.ts +108 -0
  367. package/src/__tests__/import-site-command.test.ts +358 -350
  368. package/src/__tests__/site-pull-media.test.ts +256 -0
  369. package/src/app.tsx +68 -13
  370. package/src/client/__tests__/collection-form-bridge.test.ts +87 -7
  371. package/src/client/__tests__/collection-page-actions.test.ts +4 -4
  372. package/src/client/__tests__/collection-picker-order.test.ts +53 -0
  373. package/src/client/__tests__/collection-sort-menu.test.ts +2 -2
  374. package/src/client/__tests__/compose-bridge.test.ts +305 -13
  375. package/src/client/__tests__/compose-launch.test.ts +120 -0
  376. package/src/client/__tests__/compose-shortcuts.test.ts +141 -0
  377. package/src/client/__tests__/feed-video-player.test.ts +44 -0
  378. package/src/client/__tests__/site-header-nav.test.ts +250 -0
  379. package/src/client/__tests__/sortable-list.test.ts +6 -2
  380. package/src/client/__tests__/thread-context.test.ts +117 -0
  381. package/src/client/collection-form-bridge.ts +109 -4
  382. package/src/client/collection-page-actions.ts +3 -1
  383. package/src/client/collection-picker-order.ts +104 -0
  384. package/src/client/components/__tests__/{jant-collection-sidebar.test.ts → jant-collection-directory.test.ts} +91 -10
  385. package/src/client/components/__tests__/jant-collection-form.test.ts +40 -18
  386. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1285 -66
  387. package/src/client/components/__tests__/jant-compose-editor.test.ts +254 -19
  388. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +127 -1
  389. package/src/client/components/__tests__/jant-media-lightbox.test.ts +237 -0
  390. package/src/client/components/__tests__/jant-nav-manager.test.ts +326 -0
  391. package/src/client/components/__tests__/jant-post-menu.test.ts +372 -2
  392. package/src/client/components/__tests__/jant-settings-avatar.test.ts +61 -5
  393. package/src/client/components/__tests__/jant-settings-general.test.ts +29 -28
  394. package/src/client/components/__tests__/jant-text-preview.test.ts +206 -0
  395. package/src/client/components/collection-manager-types.ts +18 -1
  396. package/src/client/components/compose-types.ts +48 -0
  397. package/src/client/components/jant-collection-directory.ts +1448 -0
  398. package/src/client/components/jant-collection-form.ts +65 -17
  399. package/src/client/components/jant-command-palette.ts +544 -0
  400. package/src/client/components/jant-compose-dialog.ts +2490 -375
  401. package/src/client/components/jant-compose-editor.ts +420 -75
  402. package/src/client/components/jant-compose-fullscreen.ts +59 -8
  403. package/src/client/components/jant-media-lightbox.ts +373 -18
  404. package/src/client/components/jant-nav-manager.ts +624 -286
  405. package/src/client/components/jant-post-menu.ts +451 -126
  406. package/src/client/components/jant-repo-picker-types.ts +39 -0
  407. package/src/client/components/jant-repo-picker.ts +799 -0
  408. package/src/client/components/jant-settings-avatar.ts +16 -2
  409. package/src/client/components/jant-settings-general.ts +137 -35
  410. package/src/client/components/jant-text-preview.ts +239 -55
  411. package/src/client/components/nav-manager-types.ts +25 -9
  412. package/src/client/components/settings-types.ts +13 -2
  413. package/src/client/compose-bridge.ts +428 -68
  414. package/src/client/compose-launch.ts +41 -4
  415. package/src/client/compose-shortcuts.ts +77 -5
  416. package/src/client/feed-video-player.ts +374 -0
  417. package/src/client/image-processor.ts +53 -127
  418. package/src/client/media-lightbox-events.ts +1 -0
  419. package/src/client/media-scroll-hint.ts +66 -0
  420. package/src/client/multipart-upload.ts +4 -1
  421. package/src/client/palette-shortcuts.ts +35 -0
  422. package/src/client/search-rank.ts +74 -0
  423. package/src/client/settings-bridge.ts +7 -0
  424. package/src/client/site-header-nav.d.ts +1 -0
  425. package/src/client/site-header-nav.js +139 -51
  426. package/src/client/sortable-list.ts +2 -0
  427. package/src/client/thread-context.ts +54 -11
  428. package/src/client/tiptap/__tests__/block-insertion.test.ts +65 -0
  429. package/src/client/tiptap/__tests__/footnotes.test.ts +692 -0
  430. package/src/client/tiptap/__tests__/insert-paragraph-around.test.ts +228 -0
  431. package/src/client/tiptap/__tests__/link-toolbar.test.ts +120 -4
  432. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +48 -0
  433. package/src/client/tiptap/block-insertion.ts +36 -0
  434. package/src/client/tiptap/bubble-menu.ts +34 -4
  435. package/src/client/tiptap/create-editor.ts +49 -30
  436. package/src/client/tiptap/embed-dialog.ts +301 -0
  437. package/src/client/tiptap/embed-node.ts +373 -0
  438. package/src/client/tiptap/embed-paste.ts +91 -0
  439. package/src/client/tiptap/extensions.ts +80 -28
  440. package/src/client/tiptap/footnotes.ts +718 -0
  441. package/src/client/tiptap/html-block-node.ts +222 -0
  442. package/src/client/tiptap/image-node.ts +49 -6
  443. package/src/client/tiptap/inline-image-upload.ts +200 -40
  444. package/src/client/tiptap/insert-paragraph-around.ts +79 -0
  445. package/src/client/tiptap/link-input-rules.ts +50 -16
  446. package/src/client/tiptap/link-toolbar.ts +156 -133
  447. package/src/client/tiptap/markdown-clipboard.ts +120 -0
  448. package/src/client/tiptap/more-break.ts +36 -3
  449. package/src/client/tiptap/paste-media.ts +6 -0
  450. package/src/client/tiptap/slash-commands.ts +167 -14
  451. package/src/client/tiptap/tab-indent.ts +184 -0
  452. package/src/client/tiptap/toolbar-mode.ts +15 -2
  453. package/src/client/tiptap/wrapping-input-rules.ts +102 -0
  454. package/src/client/types/sortablejs.d.ts +9 -1
  455. package/src/client/upload-session.ts +2 -1
  456. package/src/client/video-processor.ts +177 -20
  457. package/src/client-auth.ts +9 -1
  458. package/src/client-site.ts +15 -0
  459. package/src/client.ts +2 -0
  460. package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
  461. package/src/db/__tests__/migration-rehearsal.test.ts +5 -1
  462. package/src/db/__tests__/migrations.test.ts +25 -2
  463. package/src/db/backfills/0001_strip_collection_path_c_prefix.sql +11 -0
  464. package/src/db/backfills/0002_strip_collection_path_c_prefix_fix.sql +12 -0
  465. package/src/db/backfills/0003_clear_system_nav_default_labels.sql +15 -0
  466. package/src/db/dialect.ts +40 -0
  467. package/src/db/migrations/0005_busy_the_phantom.sql +39 -0
  468. package/src/db/migrations/0006_adorable_magdalene.sql +3 -0
  469. package/src/db/migrations/0007_unusual_warstar.sql +1 -0
  470. package/src/db/migrations/0008_nasty_lockheed.sql +1 -0
  471. package/src/db/migrations/0009_clear_fixer.sql +31 -0
  472. package/src/db/migrations/0010_futuristic_preak.sql +31 -0
  473. package/src/db/migrations/0011_bizarre_smasher.sql +30 -0
  474. package/src/db/migrations/0012_furry_thena.sql +52 -0
  475. package/src/db/migrations/0013_mixed_lightspeed.sql +39 -0
  476. package/src/db/migrations/0014_high_the_santerians.sql +41 -0
  477. package/src/db/migrations/0015_skinny_shinobi_shaw.sql +1 -0
  478. package/src/db/migrations/0016_remarkable_nicolaos.sql +17 -0
  479. package/src/db/migrations/0017_powerful_moonstone.sql +14 -0
  480. package/src/db/migrations/0018_yummy_franklin_richards.sql +6 -0
  481. package/src/db/migrations/meta/0005_snapshot.json +1904 -0
  482. package/src/db/migrations/meta/0006_snapshot.json +1925 -0
  483. package/src/db/migrations/meta/0007_snapshot.json +1933 -0
  484. package/src/db/migrations/meta/0008_snapshot.json +1940 -0
  485. package/src/db/migrations/meta/0009_snapshot.json +1952 -0
  486. package/src/db/migrations/meta/0010_snapshot.json +1952 -0
  487. package/src/db/migrations/meta/0011_snapshot.json +1948 -0
  488. package/src/db/migrations/meta/0012_snapshot.json +1955 -0
  489. package/src/db/migrations/meta/0013_snapshot.json +1977 -0
  490. package/src/db/migrations/meta/0014_snapshot.json +1988 -0
  491. package/src/db/migrations/meta/0015_snapshot.json +1995 -0
  492. package/src/db/migrations/meta/0016_snapshot.json +2104 -0
  493. package/src/db/migrations/meta/0017_snapshot.json +2188 -0
  494. package/src/db/migrations/meta/0018_snapshot.json +2225 -0
  495. package/src/db/migrations/meta/_journal.json +98 -0
  496. package/src/db/migrations/pg/0003_motionless_norrin_radd.sql +21 -0
  497. package/src/db/migrations/pg/0004_nervous_captain_midlands.sql +3 -0
  498. package/src/db/migrations/pg/0005_romantic_mesmero.sql +1 -0
  499. package/src/db/migrations/pg/0006_perpetual_bruce_banner.sql +1 -0
  500. package/src/db/migrations/pg/0007_nav_item_placement.sql +1 -0
  501. package/src/db/migrations/pg/0008_yielding_frightful_four.sql +3 -0
  502. package/src/db/migrations/pg/0009_outstanding_ogun.sql +1 -0
  503. package/src/db/migrations/pg/0010_overjoyed_gertrude_yorkes.sql +28 -0
  504. package/src/db/migrations/pg/0011_fixed_hulk.sql +19 -0
  505. package/src/db/migrations/pg/0012_cute_shockwave.sql +2 -0
  506. package/src/db/migrations/pg/0013_bizarre_obadiah_stane.sql +1 -0
  507. package/src/db/migrations/pg/0014_tearful_grim_reaper.sql +16 -0
  508. package/src/db/migrations/pg/0015_daffy_mikhail_rasputin.sql +14 -0
  509. package/src/db/migrations/pg/0016_familiar_lionheart.sql +6 -0
  510. package/src/db/migrations/pg/meta/0003_snapshot.json +2482 -0
  511. package/src/db/migrations/pg/meta/0004_snapshot.json +2500 -0
  512. package/src/db/migrations/pg/meta/0005_snapshot.json +2507 -0
  513. package/src/db/migrations/pg/meta/0006_snapshot.json +2513 -0
  514. package/src/db/migrations/pg/meta/0008_snapshot.json +2524 -0
  515. package/src/db/migrations/pg/meta/0009_snapshot.json +2520 -0
  516. package/src/db/migrations/pg/meta/0010_snapshot.json +2526 -0
  517. package/src/db/migrations/pg/meta/0011_snapshot.json +2563 -0
  518. package/src/db/migrations/pg/meta/0012_snapshot.json +2573 -0
  519. package/src/db/migrations/pg/meta/0013_snapshot.json +2579 -0
  520. package/src/db/migrations/pg/meta/0014_snapshot.json +2702 -0
  521. package/src/db/migrations/pg/meta/0015_snapshot.json +2803 -0
  522. package/src/db/migrations/pg/meta/0016_snapshot.json +2840 -0
  523. package/src/db/migrations/pg/meta/_journal.json +98 -0
  524. package/src/db/pg/__tests__/node.test.ts +38 -0
  525. package/src/db/pg/node.ts +149 -0
  526. package/src/db/pg/schema.ts +148 -12
  527. package/src/db/rehearsal-fixtures/demo-current.sql +47 -3
  528. package/src/db/schema.ts +162 -12
  529. package/src/i18n/Trans.tsx +3 -3
  530. package/src/i18n/__tests__/context.test.tsx +9 -6
  531. package/src/i18n/__tests__/detect.test.ts +65 -48
  532. package/src/i18n/__tests__/fallback.test.ts +40 -0
  533. package/src/i18n/__tests__/message-placeholders.test.ts +43 -0
  534. package/src/i18n/__tests__/middleware.test.ts +83 -0
  535. package/src/i18n/context.tsx +9 -44
  536. package/src/i18n/detect.ts +95 -28
  537. package/src/i18n/i18n.ts +41 -17
  538. package/src/i18n/index.ts +7 -5
  539. package/src/i18n/locales/{en.po → public/en.po} +284 -1151
  540. package/src/i18n/locales/public/en.ts +1 -0
  541. package/src/i18n/locales/public/zh-Hans.po +2327 -0
  542. package/src/i18n/locales/public/zh-Hans.ts +1 -0
  543. package/src/i18n/locales/public/zh-Hant.po +2327 -0
  544. package/src/i18n/locales/public/zh-Hant.ts +1 -0
  545. package/src/i18n/locales/settings/en.po +1622 -0
  546. package/src/i18n/locales/settings/en.ts +1 -0
  547. package/src/i18n/locales/settings/zh-Hans.po +1622 -0
  548. package/src/i18n/locales/settings/zh-Hans.ts +1 -0
  549. package/src/i18n/locales/settings/zh-Hant.po +1622 -0
  550. package/src/i18n/locales/settings/zh-Hant.ts +1 -0
  551. package/src/i18n/middleware.ts +37 -24
  552. package/src/index.ts +4 -6
  553. package/src/lib/__tests__/collection-groups.test.ts +17 -0
  554. package/src/lib/__tests__/constants.test.ts +2 -24
  555. package/src/lib/__tests__/csp-builder.test.ts +68 -0
  556. package/src/lib/__tests__/display-text.test.ts +27 -0
  557. package/src/lib/__tests__/embed-providers.test.ts +104 -0
  558. package/src/lib/__tests__/embed-render.test.ts +60 -0
  559. package/src/lib/__tests__/feed.test.ts +125 -63
  560. package/src/lib/__tests__/hosted-signin.test.ts +41 -2
  561. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +88 -0
  562. package/src/lib/__tests__/markdown-to-tiptap.test.ts +59 -0
  563. package/src/lib/__tests__/markdown.test.ts +47 -5
  564. package/src/lib/__tests__/navigation.test.ts +132 -0
  565. package/src/lib/__tests__/post-display.test.ts +3 -2
  566. package/src/lib/__tests__/post-meta.test.ts +3 -0
  567. package/src/lib/__tests__/rate-limit-d1.test.ts +82 -0
  568. package/src/lib/__tests__/rate-limit-memory.test.ts +69 -0
  569. package/src/lib/__tests__/resolve-config.test.ts +30 -25
  570. package/src/lib/__tests__/schemas.test.ts +20 -3
  571. package/src/lib/__tests__/summary.test.ts +540 -1
  572. package/src/lib/__tests__/theme.test.ts +41 -2
  573. package/src/lib/__tests__/timeline.test.ts +33 -27
  574. package/src/lib/__tests__/tiptap-render.test.ts +224 -0
  575. package/src/lib/__tests__/tiptap-to-markdown.test.ts +51 -0
  576. package/src/lib/__tests__/video-playback.test.ts +48 -0
  577. package/src/lib/__tests__/view.test.ts +181 -8
  578. package/src/lib/__tests__/worker-response-cache.test.ts +311 -0
  579. package/src/lib/__tests__/youtube.test.ts +99 -0
  580. package/src/lib/api-media.ts +55 -0
  581. package/src/lib/api-posts.ts +99 -0
  582. package/src/lib/api-search.ts +47 -0
  583. package/src/lib/api-settings.ts +122 -0
  584. package/src/lib/asset-path.ts +20 -2
  585. package/src/lib/collection-groups.ts +2 -1
  586. package/src/lib/collection-paths.ts +40 -0
  587. package/src/lib/constants.ts +3 -24
  588. package/src/lib/csp-builder.ts +99 -0
  589. package/src/lib/decorative-quote-mark.ts +11 -0
  590. package/src/lib/display-text.ts +56 -0
  591. package/src/lib/embed-providers.ts +289 -0
  592. package/src/lib/embed-render.ts +151 -0
  593. package/src/lib/env.ts +70 -1
  594. package/src/lib/excerpt.ts +11 -2
  595. package/src/lib/feed.ts +154 -95
  596. package/src/lib/footnotes.ts +146 -0
  597. package/src/lib/github-api.ts +423 -0
  598. package/src/lib/github-app-state.ts +135 -0
  599. package/src/lib/github-app.ts +487 -0
  600. package/src/lib/github-sync-queue-handler.ts +69 -0
  601. package/src/lib/github-sync-site-config.ts +57 -0
  602. package/src/lib/github-sync-status.tsx +87 -0
  603. package/src/lib/github-sync-trigger.ts +199 -0
  604. package/src/lib/github-sync-worker.ts +72 -0
  605. package/src/lib/hosted-signin.ts +9 -3
  606. package/src/lib/hugo-markdown.ts +255 -0
  607. package/src/lib/job-queue-cf.ts +18 -0
  608. package/src/lib/job-queue-db.ts +149 -0
  609. package/src/lib/job-queue.ts +35 -0
  610. package/src/lib/markdown-manager.ts +864 -0
  611. package/src/lib/markdown-to-tiptap.ts +4 -323
  612. package/src/lib/markdown.ts +16 -24
  613. package/src/lib/media-helpers.ts +1 -0
  614. package/src/lib/navigation.ts +50 -19
  615. package/src/lib/post-display.ts +10 -22
  616. package/src/lib/post-meta.ts +20 -2
  617. package/src/lib/public-storage.ts +6 -1
  618. package/src/lib/rate-limit-d1.ts +99 -0
  619. package/src/lib/rate-limit-memory.ts +105 -0
  620. package/src/lib/rate-limit.ts +63 -0
  621. package/src/lib/render.tsx +14 -2
  622. package/src/lib/resolve-config.ts +32 -11
  623. package/src/lib/rich-image.ts +89 -0
  624. package/src/lib/schemas.ts +180 -13
  625. package/src/lib/summary.ts +162 -20
  626. package/src/lib/theme.ts +119 -0
  627. package/src/lib/timeline.ts +63 -42
  628. package/src/lib/tiptap-render.ts +265 -126
  629. package/src/lib/tiptap-to-markdown.ts +6 -329
  630. package/src/lib/upload.ts +97 -6
  631. package/src/lib/url.ts +34 -0
  632. package/src/lib/version.ts +18 -0
  633. package/src/lib/video-playback.ts +73 -0
  634. package/src/lib/view.ts +168 -94
  635. package/src/lib/webhook-signature.ts +65 -0
  636. package/src/lib/worker-response-cache.ts +220 -0
  637. package/src/lib/youtube.ts +119 -0
  638. package/src/middleware/__tests__/auth.test.ts +44 -4
  639. package/src/middleware/__tests__/onboarding.test.ts +3 -3
  640. package/src/middleware/__tests__/rate-limit.test.ts +113 -0
  641. package/src/middleware/__tests__/secure-headers.test.ts +28 -1
  642. package/src/middleware/__tests__/session.test.ts +85 -0
  643. package/src/middleware/auth.ts +62 -25
  644. package/src/middleware/config.ts +7 -10
  645. package/src/middleware/cors.ts +49 -0
  646. package/src/middleware/rate-limit.ts +54 -0
  647. package/src/middleware/secure-headers.ts +45 -55
  648. package/src/middleware/session.ts +36 -0
  649. package/src/node/__tests__/cli-migrate.test.ts +111 -0
  650. package/src/node/__tests__/cli-runtime-target.test.ts +1 -1
  651. package/src/node/__tests__/cli-site-token-env.test.ts +2 -2
  652. package/src/preset.css +316 -12
  653. package/src/routes/__tests__/compose.test.ts +4 -2
  654. package/src/routes/api/__tests__/collections.test.ts +81 -27
  655. package/src/routes/api/__tests__/mcp.test.ts +389 -0
  656. package/src/routes/api/__tests__/nav-items.test.ts +163 -2
  657. package/src/routes/api/__tests__/posts.test.ts +190 -0
  658. package/src/routes/api/__tests__/search.test.ts +48 -0
  659. package/src/routes/api/__tests__/settings.test.ts +99 -3
  660. package/src/routes/api/__tests__/upload-multipart.test.ts +21 -6
  661. package/src/routes/api/__tests__/upload.test.ts +132 -0
  662. package/src/routes/api/__tests__/uploads.test.ts +5 -3
  663. package/src/routes/api/collections.ts +67 -26
  664. package/src/routes/api/custom-urls.ts +1 -0
  665. package/src/routes/api/export.ts +8 -4
  666. package/src/routes/api/github-sync.tsx +380 -0
  667. package/src/routes/api/internal/__tests__/sites.test.ts +45 -2
  668. package/src/routes/api/internal/search-reindex.ts +40 -0
  669. package/src/routes/api/internal/sites.ts +2 -0
  670. package/src/routes/api/internal/text-attachments.ts +60 -0
  671. package/src/routes/api/internal/uploads.ts +5 -1
  672. package/src/routes/api/mcp.ts +30 -0
  673. package/src/routes/api/nav-items.ts +30 -11
  674. package/src/routes/api/palette.ts +17 -0
  675. package/src/routes/api/posts.ts +37 -106
  676. package/src/routes/api/public/__tests__/posts.test.ts +372 -0
  677. package/src/routes/api/public/posts.ts +302 -0
  678. package/src/routes/api/search.ts +13 -46
  679. package/src/routes/api/settings.ts +44 -58
  680. package/src/routes/api/upload-multipart.ts +15 -5
  681. package/src/routes/api/upload.ts +62 -49
  682. package/src/routes/api/uploads.ts +1 -0
  683. package/src/routes/auth/__tests__/hosted-sso.test.ts +33 -0
  684. package/src/routes/auth/__tests__/setup.test.ts +29 -4
  685. package/src/routes/auth/dev.ts +1 -1
  686. package/src/routes/auth/hosted-sso-expired-page.tsx +89 -0
  687. package/src/routes/auth/hosted-sso.ts +42 -1
  688. package/src/routes/auth/reset.tsx +46 -32
  689. package/src/routes/auth/setup.tsx +53 -43
  690. package/src/routes/auth/signin.tsx +57 -29
  691. package/src/routes/compose.tsx +114 -1
  692. package/src/routes/dash/__tests__/font-theme.test.ts +6 -27
  693. package/src/routes/dash/__tests__/settings-avatar.test.ts +2 -3
  694. package/src/routes/dash/custom-urls.tsx +250 -112
  695. package/src/routes/dash/settings.tsx +1182 -75
  696. package/src/routes/feed/__tests__/{rss.test.ts → feed.test.ts} +62 -123
  697. package/src/routes/feed/__tests__/sitemap.test.ts +320 -4
  698. package/src/routes/feed/feed.ts +386 -0
  699. package/src/routes/feed/manifest.ts +67 -0
  700. package/src/routes/feed/sitemap.ts +211 -27
  701. package/src/routes/pages/__tests__/collection-routing.test.ts +176 -0
  702. package/src/routes/pages/__tests__/collections.test.ts +2 -3
  703. package/src/routes/pages/__tests__/featured.test.ts +2 -3
  704. package/src/routes/pages/__tests__/page-canonical.test.ts +101 -0
  705. package/src/routes/pages/archive.tsx +523 -127
  706. package/src/routes/pages/collection.tsx +197 -43
  707. package/src/routes/pages/collections.tsx +5 -1
  708. package/src/routes/pages/home.tsx +26 -17
  709. package/src/routes/pages/latest.tsx +3 -3
  710. package/src/routes/pages/page.tsx +255 -3
  711. package/src/routes/pages/partials.tsx +8 -14
  712. package/src/routes/pages/search.tsx +17 -1
  713. package/src/routes/pages/theme-sample.tsx +1 -2
  714. package/src/runtime/__tests__/node.test.ts +1 -1
  715. package/src/runtime/cloudflare.ts +4 -0
  716. package/src/runtime/node.ts +16 -0
  717. package/src/runtime/readiness.ts +1 -1
  718. package/src/runtime/site.ts +9 -1
  719. package/src/services/__tests__/collection.test.ts +137 -54
  720. package/src/services/__tests__/github-app-installations.test.ts +181 -0
  721. package/src/services/__tests__/github-sync-classify.test.ts +189 -0
  722. package/src/services/__tests__/github-sync-push.test.ts +159 -0
  723. package/src/services/__tests__/media.test.ts +444 -7
  724. package/src/services/__tests__/navigation.test.ts +271 -14
  725. package/src/services/__tests__/post-timeline.test.ts +31 -21
  726. package/src/services/__tests__/post.test.ts +288 -9
  727. package/src/services/__tests__/search.test.ts +44 -0
  728. package/src/services/__tests__/settings.test.ts +43 -21
  729. package/src/services/auth.ts +2 -2
  730. package/src/services/bootstrap.ts +10 -7
  731. package/src/services/collection.ts +591 -193
  732. package/src/services/custom-url.ts +34 -7
  733. package/src/services/export-theme/assets/client-site.css +2 -0
  734. package/src/services/export-theme/assets/client-site.js +145 -0
  735. package/src/services/export-theme/layouts/_default/alias.html +49 -0
  736. package/src/services/export-theme/layouts/_default/baseof.html +21 -0
  737. package/src/services/export-theme/layouts/_default/list.html +104 -0
  738. package/src/services/export-theme/layouts/_default/rss.xml +160 -0
  739. package/src/services/export-theme/layouts/_default/single.html +15 -0
  740. package/src/services/export-theme/layouts/archive/list.html +31 -0
  741. package/src/services/export-theme/layouts/collection/single.html +19 -0
  742. package/src/services/export-theme/layouts/collections/list.html +111 -0
  743. package/src/services/export-theme/layouts/featured/list.html +28 -0
  744. package/src/services/export-theme/layouts/index.html +41 -0
  745. package/src/services/export-theme/layouts/partials/feed-post-content.xml +107 -0
  746. package/src/services/export-theme/layouts/partials/footer.html +32 -0
  747. package/src/services/export-theme/layouts/partials/head.html +52 -0
  748. package/src/services/export-theme/layouts/partials/header.html +115 -0
  749. package/src/services/export-theme/layouts/partials/media-gallery.html +246 -0
  750. package/src/services/export-theme/layouts/partials/pagination.html +55 -0
  751. package/src/services/export-theme/layouts/partials/post-card.html +127 -0
  752. package/src/services/export-theme/layouts/partials/reply.html +91 -0
  753. package/src/services/export-theme/layouts/partials/thread-preview.html +82 -0
  754. package/src/services/export-theme/layouts/post/list.html +129 -0
  755. package/src/services/export-theme/styles/main.css +1981 -0
  756. package/src/services/export-theme/theme.toml +12 -0
  757. package/src/services/export.ts +1372 -2412
  758. package/src/services/github-app-installations.ts +302 -0
  759. package/src/services/github-sync.ts +769 -0
  760. package/src/services/index.ts +15 -0
  761. package/src/services/mcp.ts +1324 -0
  762. package/src/services/media.ts +336 -25
  763. package/src/services/navigation.ts +115 -23
  764. package/src/services/path.ts +129 -28
  765. package/src/services/post.ts +858 -197
  766. package/src/services/search.ts +3 -0
  767. package/src/services/settings.ts +29 -15
  768. package/src/services/site-admin.ts +26 -6
  769. package/src/services/upload-session.ts +69 -34
  770. package/src/style-cjk-jp.css +3 -0
  771. package/src/style-cjk-kr.css +3 -0
  772. package/src/styles/components.css +160 -59
  773. package/src/styles/fonts/et-book/et-book-bold-line-figures.woff +0 -0
  774. package/src/styles/fonts/et-book/et-book-display-italic-old-style-figures.woff +0 -0
  775. package/src/styles/fonts/et-book/et-book-roman-line-figures.woff +0 -0
  776. package/src/styles/fonts/et-book.css +31 -0
  777. package/src/styles/fonts/latin.css +1 -0
  778. package/src/styles/fonts/noto-serif-jp/400/057a6a98bda7fe57105ddaa99ec82015.woff2 +0 -0
  779. package/src/styles/fonts/noto-serif-jp/400/0acd1fe2b2ea1ad1bfee7ae1fa139d27.woff2 +0 -0
  780. package/src/styles/fonts/noto-serif-jp/400/0c2ab6b295e55f356f8020d4e7747522.woff2 +0 -0
  781. package/src/styles/fonts/noto-serif-jp/400/0c5f9492af03a4fa42c784de94649de1.woff2 +0 -0
  782. package/src/styles/fonts/noto-serif-jp/400/0d9a936885a4c39077438effd3779cbd.woff2 +0 -0
  783. package/src/styles/fonts/noto-serif-jp/400/0e645da524f7cfc0e8c3c03fb2b08428.woff2 +0 -0
  784. package/src/styles/fonts/noto-serif-jp/400/135e83b403475c5dc9e49b43501a5b84.woff2 +0 -0
  785. package/src/styles/fonts/noto-serif-jp/400/13b2c53b30e6a3e4de7132dbc18dbdfc.woff2 +0 -0
  786. package/src/styles/fonts/noto-serif-jp/400/15d95680dc31cc6ce20e0a5464106dc4.woff2 +0 -0
  787. package/src/styles/fonts/noto-serif-jp/400/1733f27476507ca68b68a803bc533cc2.woff2 +0 -0
  788. package/src/styles/fonts/noto-serif-jp/400/188c2db794f3dd7a45889ddbc81da9bb.woff2 +0 -0
  789. package/src/styles/fonts/noto-serif-jp/400/1cf737900dd49c2e88f1b3221a82a602.woff2 +0 -0
  790. package/src/styles/fonts/noto-serif-jp/400/233d3a685ee18276b319363474599d47.woff2 +0 -0
  791. package/src/styles/fonts/noto-serif-jp/400/284b53bbefb06924cf236d24c8ed5641.woff2 +0 -0
  792. package/src/styles/fonts/noto-serif-jp/400/2a56eaf19d1d38a6b57e2a388f733676.woff2 +0 -0
  793. package/src/styles/fonts/noto-serif-jp/400/2abfbab82b6a7c04426afc054d2464da.woff2 +0 -0
  794. package/src/styles/fonts/noto-serif-jp/400/2e6f4bb71ef6b38765d51acc5a79f638.woff2 +0 -0
  795. package/src/styles/fonts/noto-serif-jp/400/31194d303a67561926a544ed0e072aee.woff2 +0 -0
  796. package/src/styles/fonts/noto-serif-jp/400/3623d96698132c61e861f984ce11cc23.woff2 +0 -0
  797. package/src/styles/fonts/noto-serif-jp/400/39211b02f24c69cbd1b54f6d41b01933.woff2 +0 -0
  798. package/src/styles/fonts/noto-serif-jp/400/3e24063b19abd2b051af986a6870046d.woff2 +0 -0
  799. package/src/styles/fonts/noto-serif-jp/400/4410f876d27c9821839474b6b1059fd7.woff2 +0 -0
  800. package/src/styles/fonts/noto-serif-jp/400/479329a39a3eb6c0047e6b0981919855.woff2 +0 -0
  801. package/src/styles/fonts/noto-serif-jp/400/527ff336279465617aafbafe1a830632.woff2 +0 -0
  802. package/src/styles/fonts/noto-serif-jp/400/56b40518ea0608e62826bd44ee27f5f9.woff2 +0 -0
  803. package/src/styles/fonts/noto-serif-jp/400/5acc7f736217259db79a42bf44241c48.woff2 +0 -0
  804. package/src/styles/fonts/noto-serif-jp/400/5c05f8b75c9d7f25b3c04ca711cf43b3.woff2 +0 -0
  805. package/src/styles/fonts/noto-serif-jp/400/5e0124c7265f1b4cf0fc797b94efbebc.woff2 +0 -0
  806. package/src/styles/fonts/noto-serif-jp/400/615a7e0d63f257109d599b2a1977de68.woff2 +0 -0
  807. package/src/styles/fonts/noto-serif-jp/400/61a2c80d0c924d5a6187c02e8d1d1642.woff2 +0 -0
  808. package/src/styles/fonts/noto-serif-jp/400/62fc94f8d4c5a750b7f25e7387539910.woff2 +0 -0
  809. package/src/styles/fonts/noto-serif-jp/400/639c7c6b0b0c27c738702741cfa4b8c0.woff2 +0 -0
  810. package/src/styles/fonts/noto-serif-jp/400/68a3a4bf337f8a0722be76676e20b850.woff2 +0 -0
  811. package/src/styles/fonts/noto-serif-jp/400/6abdd163d2c4b85698db8aa1ce149361.woff2 +0 -0
  812. package/src/styles/fonts/noto-serif-jp/400/73b794d885b88f7befb7ea8ab1780a15.woff2 +0 -0
  813. package/src/styles/fonts/noto-serif-jp/400/743f290baf027d4626a86e22f3d44600.woff2 +0 -0
  814. package/src/styles/fonts/noto-serif-jp/400/757019c01c5e2c2b0f54293cea3b5636.woff2 +0 -0
  815. package/src/styles/fonts/noto-serif-jp/400/76f117a3baacda304a71965a17266a13.woff2 +0 -0
  816. package/src/styles/fonts/noto-serif-jp/400/77747f17625a259fb405b7bbdd84ac4b.woff2 +0 -0
  817. package/src/styles/fonts/noto-serif-jp/400/788498548ddfb710d6ef3c7bff79fa97.woff2 +0 -0
  818. package/src/styles/fonts/noto-serif-jp/400/78ea6c40923ce95367e3517dd1e5a849.woff2 +0 -0
  819. package/src/styles/fonts/noto-serif-jp/400/7f9a6c9286b68de9c72b0024f7beeb40.woff2 +0 -0
  820. package/src/styles/fonts/noto-serif-jp/400/823d1bb11097331238d103c7f72138dc.woff2 +0 -0
  821. package/src/styles/fonts/noto-serif-jp/400/888ba53510213d5d1f6a7deb60e569b6.woff2 +0 -0
  822. package/src/styles/fonts/noto-serif-jp/400/892aa49b2529c89bb4076d4aa51fe30f.woff2 +0 -0
  823. package/src/styles/fonts/noto-serif-jp/400/95ff41191c76ef893ac70a1043e95e44.woff2 +0 -0
  824. package/src/styles/fonts/noto-serif-jp/400/96c75862c8cec51a7c05ff025fea86cd.woff2 +0 -0
  825. package/src/styles/fonts/noto-serif-jp/400/9b21e8244f5930c48ad5073f83777b6c.woff2 +0 -0
  826. package/src/styles/fonts/noto-serif-jp/400/9fce02d1a09f464c36c9fcfb14a354c5.woff2 +0 -0
  827. package/src/styles/fonts/noto-serif-jp/400/a0a6755c7e3f9e4cf03387027dd9f16c.woff2 +0 -0
  828. package/src/styles/fonts/noto-serif-jp/400/a4475c442c6b259354c9eda62e64e7df.woff2 +0 -0
  829. package/src/styles/fonts/noto-serif-jp/400/a4a39b5f048671aa37c2b2a9581ab08a.woff2 +0 -0
  830. package/src/styles/fonts/noto-serif-jp/400/a534449d67561ac9b06489f64e57ad98.woff2 +0 -0
  831. package/src/styles/fonts/noto-serif-jp/400/a919a8c6fec6d7e8bb21177965f2e9d7.woff2 +0 -0
  832. package/src/styles/fonts/noto-serif-jp/400/ad67113f88b59582991cb0c5d33ea19f.woff2 +0 -0
  833. package/src/styles/fonts/noto-serif-jp/400/adb6e613fb99c6dd660b727101f554a8.woff2 +0 -0
  834. package/src/styles/fonts/noto-serif-jp/400/adc4a98a89870ef984ee8f4b57c8a3a5.woff2 +0 -0
  835. package/src/styles/fonts/noto-serif-jp/400/b12379ab782468d725519cd07a7d15bc.woff2 +0 -0
  836. package/src/styles/fonts/noto-serif-jp/400/b55486f8a459c838fe329d4e79a8c211.woff2 +0 -0
  837. package/src/styles/fonts/noto-serif-jp/400/bb0b15492b8cdbbec57c0bcfa0aa9241.woff2 +0 -0
  838. package/src/styles/fonts/noto-serif-jp/400/bedc74b423b7293b6ad0bdecc61c42cc.woff2 +0 -0
  839. package/src/styles/fonts/noto-serif-jp/400/bfb00d4a4c48661bd0be99f300a0faae.woff2 +0 -0
  840. package/src/styles/fonts/noto-serif-jp/400/c1b0df29ae41d764904df84e9ac83d1e.woff2 +0 -0
  841. package/src/styles/fonts/noto-serif-jp/400/c42c67070cfe99cf823df92d81c7fa6e.woff2 +0 -0
  842. package/src/styles/fonts/noto-serif-jp/400/c4d749e45ecd5a5aed5a0bb3ebfd355d.woff2 +0 -0
  843. package/src/styles/fonts/noto-serif-jp/400/c96d83978add28b356c22c4c84916733.woff2 +0 -0
  844. package/src/styles/fonts/noto-serif-jp/400/cb7ccd6494256f7a2977b7c2b0225592.woff2 +0 -0
  845. package/src/styles/fonts/noto-serif-jp/400/ce41d70ce6a069a498525c9e15c45cf2.woff2 +0 -0
  846. package/src/styles/fonts/noto-serif-jp/400/d509ee5c8241bc7c3de2039d75564fa5.woff2 +0 -0
  847. package/src/styles/fonts/noto-serif-jp/400/d675d717cd329bfd4c0524f76ae1579c.woff2 +0 -0
  848. package/src/styles/fonts/noto-serif-jp/400/dc8f6256445e68199540be9ade33529f.woff2 +0 -0
  849. package/src/styles/fonts/noto-serif-jp/400/e8c15be62faf978d208925e79ea6a10d.woff2 +0 -0
  850. package/src/styles/fonts/noto-serif-jp/400/e91c4d941ed9f5c2f3e27f205a3a225e.woff2 +0 -0
  851. package/src/styles/fonts/noto-serif-jp/400/e9c566be7a5d38a9085225f7372bc82b.woff2 +0 -0
  852. package/src/styles/fonts/noto-serif-jp/400/e9ebc567a711eeb29019ddae3e0ce7fe.woff2 +0 -0
  853. package/src/styles/fonts/noto-serif-jp/400/ecb8875a56c7b038b35432fda41ae128.woff2 +0 -0
  854. package/src/styles/fonts/noto-serif-jp/400/f1661731474b78bdf81114daf10b254f.woff2 +0 -0
  855. package/src/styles/fonts/noto-serif-jp/400/f2c58dee206ba9355046fc23d05491f7.woff2 +0 -0
  856. package/src/styles/fonts/noto-serif-jp/400/f50ac27ea4358d67fdda403c2bb52467.woff2 +0 -0
  857. package/src/styles/fonts/noto-serif-jp/400/f55334112f8e8be82d65db29887a663f.woff2 +0 -0
  858. package/src/styles/fonts/noto-serif-jp/400/f740e93f3c0277ecc616594103bca683.woff2 +0 -0
  859. package/src/styles/fonts/noto-serif-jp/400/f7633b5af033d76ff2fb3c3c266d77c5.woff2 +0 -0
  860. package/src/styles/fonts/noto-serif-jp/400/f7d8468cba2335a83ee414ea68291bab.woff2 +0 -0
  861. package/src/styles/fonts/noto-serif-jp/400/fc9ae5b600fb711f2d67e93ce768cba4.woff2 +0 -0
  862. package/src/styles/fonts/noto-serif-jp/400/fd1bceb55d3e0183ac2454b8532fec7d.woff2 +0 -0
  863. package/src/styles/fonts/noto-serif-jp/700/02611a045a7fe83a12014e3debc9f731.woff2 +0 -0
  864. package/src/styles/fonts/noto-serif-jp/700/0f5e1a18987dbc84ca05188c129e1936.woff2 +0 -0
  865. package/src/styles/fonts/noto-serif-jp/700/112743a4ab5fdd1498dfdf2b11336380.woff2 +0 -0
  866. package/src/styles/fonts/noto-serif-jp/700/1211f03d3ff5759a702631445793f60e.woff2 +0 -0
  867. package/src/styles/fonts/noto-serif-jp/700/147b24c2f08d03bbed30887d4cb3311c.woff2 +0 -0
  868. package/src/styles/fonts/noto-serif-jp/700/16663e567f81f4725a1522f37e18f71f.woff2 +0 -0
  869. package/src/styles/fonts/noto-serif-jp/700/1824321da801e6257b902f3d1d09054c.woff2 +0 -0
  870. package/src/styles/fonts/noto-serif-jp/700/1a3a92c7c060a71a6c35819b9ebcdbb9.woff2 +0 -0
  871. package/src/styles/fonts/noto-serif-jp/700/1fba7ec0e412e911bf31841de5a8a4e4.woff2 +0 -0
  872. package/src/styles/fonts/noto-serif-jp/700/28a97af9ab9d38983d20f39ff09840e1.woff2 +0 -0
  873. package/src/styles/fonts/noto-serif-jp/700/2bfaadaf3479c72286248e6de0be0ec9.woff2 +0 -0
  874. package/src/styles/fonts/noto-serif-jp/700/2c8c55e4cec85ce77e95cac9d330f5cf.woff2 +0 -0
  875. package/src/styles/fonts/noto-serif-jp/700/2ed9981d2e8983365bd051159b976b6d.woff2 +0 -0
  876. package/src/styles/fonts/noto-serif-jp/700/2f4c633e923ba30c6ba376367379fc91.woff2 +0 -0
  877. package/src/styles/fonts/noto-serif-jp/700/349965eee0a9b6c984a319ab96a4ece9.woff2 +0 -0
  878. package/src/styles/fonts/noto-serif-jp/700/36cda3eae13370b934bba4429fab9078.woff2 +0 -0
  879. package/src/styles/fonts/noto-serif-jp/700/3aa13f1843d7c8fa16109b6395a9a29d.woff2 +0 -0
  880. package/src/styles/fonts/noto-serif-jp/700/3e2c06bdd9dcab29aeaeb0cfb7fa608a.woff2 +0 -0
  881. package/src/styles/fonts/noto-serif-jp/700/424123f9371ba357087363263eeafcda.woff2 +0 -0
  882. package/src/styles/fonts/noto-serif-jp/700/43b3bbf43055c3e65db9cdf2c5b18c1c.woff2 +0 -0
  883. package/src/styles/fonts/noto-serif-jp/700/44704471b60c18b3ecec5300a88f1102.woff2 +0 -0
  884. package/src/styles/fonts/noto-serif-jp/700/46604090efbb99c2db1800928af0a239.woff2 +0 -0
  885. package/src/styles/fonts/noto-serif-jp/700/46998df85d31e629c0142f0556f5e7c5.woff2 +0 -0
  886. package/src/styles/fonts/noto-serif-jp/700/49902f1c41b281f8eb3771abdd4dccb8.woff2 +0 -0
  887. package/src/styles/fonts/noto-serif-jp/700/4db4f31a16965baa42cb7dad667a8f04.woff2 +0 -0
  888. package/src/styles/fonts/noto-serif-jp/700/572b9e28f1e6d89b765a16d809db84a1.woff2 +0 -0
  889. package/src/styles/fonts/noto-serif-jp/700/5921cef36750ff6092d94b83a802823b.woff2 +0 -0
  890. package/src/styles/fonts/noto-serif-jp/700/59e95d8b5332dcdae894a19946ab767d.woff2 +0 -0
  891. package/src/styles/fonts/noto-serif-jp/700/5ec349ff62b9ee5fe0c6dae867704dc4.woff2 +0 -0
  892. package/src/styles/fonts/noto-serif-jp/700/601f812e8b1eafecd5ba585a6a6d7962.woff2 +0 -0
  893. package/src/styles/fonts/noto-serif-jp/700/6025ddaf99d86cc8bfd781006b19fbd6.woff2 +0 -0
  894. package/src/styles/fonts/noto-serif-jp/700/60f6e28b3f0400b0e8fe32c3c7116102.woff2 +0 -0
  895. package/src/styles/fonts/noto-serif-jp/700/64a404d675f1d726f0891b7a80794595.woff2 +0 -0
  896. package/src/styles/fonts/noto-serif-jp/700/7158022889c6c177062ac85036e7af10.woff2 +0 -0
  897. package/src/styles/fonts/noto-serif-jp/700/7e38ca789a9e76ac91d9256374e154f0.woff2 +0 -0
  898. package/src/styles/fonts/noto-serif-jp/700/815a0b797465190f60d8b1a04656c42e.woff2 +0 -0
  899. package/src/styles/fonts/noto-serif-jp/700/8726558cf297cbda831cc19514897205.woff2 +0 -0
  900. package/src/styles/fonts/noto-serif-jp/700/8855472f3c02f6c7ebb3216617ebe4af.woff2 +0 -0
  901. package/src/styles/fonts/noto-serif-jp/700/8e231d707f0c4f8e3cde90a6b52a79aa.woff2 +0 -0
  902. package/src/styles/fonts/noto-serif-jp/700/9064c12fd72c7aba235d8c3881755b91.woff2 +0 -0
  903. package/src/styles/fonts/noto-serif-jp/700/911b9e53e9814de2998c60bf3115f560.woff2 +0 -0
  904. package/src/styles/fonts/noto-serif-jp/700/92fdc376bce277874e75db666f175b44.woff2 +0 -0
  905. package/src/styles/fonts/noto-serif-jp/700/930a23430f0eb64480d7fe5f82834e21.woff2 +0 -0
  906. package/src/styles/fonts/noto-serif-jp/700/93bed0e5f2dd5a21cf73304fcfbc149d.woff2 +0 -0
  907. package/src/styles/fonts/noto-serif-jp/700/93f3ea6918533d96d9c252378b9a4af0.woff2 +0 -0
  908. package/src/styles/fonts/noto-serif-jp/700/95a0951d2a2722ff773a1a45e8c474d6.woff2 +0 -0
  909. package/src/styles/fonts/noto-serif-jp/700/982cd1765ca6bbb3e6547e47834d5148.woff2 +0 -0
  910. package/src/styles/fonts/noto-serif-jp/700/9b0c3caac458c7bc9c9133e40405ddea.woff2 +0 -0
  911. package/src/styles/fonts/noto-serif-jp/700/9f3bdab4025dc7c5ba1a5871e62e8918.woff2 +0 -0
  912. package/src/styles/fonts/noto-serif-jp/700/a00e94d4f04df6aa659db9c4954c7efe.woff2 +0 -0
  913. package/src/styles/fonts/noto-serif-jp/700/a2b0597837e382aca19919c4b74e58c8.woff2 +0 -0
  914. package/src/styles/fonts/noto-serif-jp/700/a373e85c96aa0dc303092429fb837e51.woff2 +0 -0
  915. package/src/styles/fonts/noto-serif-jp/700/a6e044424780a57610833cc856c15bf5.woff2 +0 -0
  916. package/src/styles/fonts/noto-serif-jp/700/a7c58070d68dc1724e9d4e781afb78db.woff2 +0 -0
  917. package/src/styles/fonts/noto-serif-jp/700/accc43762ad05edfbff66ba1380d192a.woff2 +0 -0
  918. package/src/styles/fonts/noto-serif-jp/700/ad47bcd6d4663026d648c132d969318d.woff2 +0 -0
  919. package/src/styles/fonts/noto-serif-jp/700/af57a2e8c72f9ba0efb1af771d32c124.woff2 +0 -0
  920. package/src/styles/fonts/noto-serif-jp/700/b0ae5f374e43dac992a4a5d334cebc0b.woff2 +0 -0
  921. package/src/styles/fonts/noto-serif-jp/700/b91234c10fd8b8c8abc88e03afe66a1f.woff2 +0 -0
  922. package/src/styles/fonts/noto-serif-jp/700/b9317198f118a1dfd8ddf2b82ec028f3.woff2 +0 -0
  923. package/src/styles/fonts/noto-serif-jp/700/ba825ae79b1a6f7e0cce5215fcb5c96f.woff2 +0 -0
  924. package/src/styles/fonts/noto-serif-jp/700/bc6183ac08f0fac78c46f80c10cf7c92.woff2 +0 -0
  925. package/src/styles/fonts/noto-serif-jp/700/bc8ccea5abf6598cf3cfa97eb59804bb.woff2 +0 -0
  926. package/src/styles/fonts/noto-serif-jp/700/bc95a792cbca6639214c9b0da13392ff.woff2 +0 -0
  927. package/src/styles/fonts/noto-serif-jp/700/bcfa62f35731856246c146d3a6932bf3.woff2 +0 -0
  928. package/src/styles/fonts/noto-serif-jp/700/c063897793f593eb26d6ff7b7baaba18.woff2 +0 -0
  929. package/src/styles/fonts/noto-serif-jp/700/c6c2971ad1f3221f6cf84028aa0f477e.woff2 +0 -0
  930. package/src/styles/fonts/noto-serif-jp/700/c97f41eef722121d86f55d553c056a39.woff2 +0 -0
  931. package/src/styles/fonts/noto-serif-jp/700/ca62704509932d3232d62918de97af3f.woff2 +0 -0
  932. package/src/styles/fonts/noto-serif-jp/700/d1f064825fa5784b5c930652bd831cce.woff2 +0 -0
  933. package/src/styles/fonts/noto-serif-jp/700/d2faabcedd19f016e7b21bce073c0ec8.woff2 +0 -0
  934. package/src/styles/fonts/noto-serif-jp/700/d320171e57480510f87dbbc7d5264b0a.woff2 +0 -0
  935. package/src/styles/fonts/noto-serif-jp/700/d3a72a99d365dddfbca8d017a8011368.woff2 +0 -0
  936. package/src/styles/fonts/noto-serif-jp/700/d42aafa0f246ad3b4c16fe96d3a3a432.woff2 +0 -0
  937. package/src/styles/fonts/noto-serif-jp/700/d4aaf23c13ae808a4bed617afd13aa07.woff2 +0 -0
  938. package/src/styles/fonts/noto-serif-jp/700/d6bb686bddfbe8f38a36a68e609a8667.woff2 +0 -0
  939. package/src/styles/fonts/noto-serif-jp/700/d9020ff69a83b2a6ee1f42ae480f7db0.woff2 +0 -0
  940. package/src/styles/fonts/noto-serif-jp/700/da90a9012ab2d98f759e3fa0820ef502.woff2 +0 -0
  941. package/src/styles/fonts/noto-serif-jp/700/df10d94bea357a43313e20da5d84bd30.woff2 +0 -0
  942. package/src/styles/fonts/noto-serif-jp/700/e34b2b141e472dc776c86fdf8eea23b0.woff2 +0 -0
  943. package/src/styles/fonts/noto-serif-jp/700/e444fd88f1390636e603d0d681538ac8.woff2 +0 -0
  944. package/src/styles/fonts/noto-serif-jp/700/e79898628283edc27180fc39d0d769c1.woff2 +0 -0
  945. package/src/styles/fonts/noto-serif-jp/700/eaf332445a40942928e5f5750c1c3116.woff2 +0 -0
  946. package/src/styles/fonts/noto-serif-jp/700/f91cd722855f4269256eae1187df64ec.woff2 +0 -0
  947. package/src/styles/fonts/noto-serif-jp/700/fc895f5ce66b656f4a933097bf2a8775.woff2 +0 -0
  948. package/src/styles/fonts/noto-serif-jp/700/fca6720fd14c467d29a90f18ef3859b9.woff2 +0 -0
  949. package/src/styles/fonts/noto-serif-jp/700/fd1bb507bcbf04856eeb7f7fd47ea579.woff2 +0 -0
  950. package/src/styles/fonts/noto-serif-jp/noto-serif-jp.css +3349 -0
  951. package/src/styles/fonts/noto-serif-kr/400/033466ef683afe931f7f520cfb42d928.woff2 +0 -0
  952. package/src/styles/fonts/noto-serif-kr/400/05da12edb9d52210581dc6ec4541031f.woff2 +0 -0
  953. package/src/styles/fonts/noto-serif-kr/400/0968e4861204b51f62a2f8e9f15dd5e0.woff2 +0 -0
  954. package/src/styles/fonts/noto-serif-kr/400/0d5dec931dc885f07fe5cd5af8bed675.woff2 +0 -0
  955. package/src/styles/fonts/noto-serif-kr/400/1076f0f6f66d28d7a2f16427faad4413.woff2 +0 -0
  956. package/src/styles/fonts/noto-serif-kr/400/12ef1ba76bd20b004b865266a1aa70b3.woff2 +0 -0
  957. package/src/styles/fonts/noto-serif-kr/400/152395634a207579552f8cb54db88599.woff2 +0 -0
  958. package/src/styles/fonts/noto-serif-kr/400/1a08931435f885e707edb85978ea3bb6.woff2 +0 -0
  959. package/src/styles/fonts/noto-serif-kr/400/31a197213ae61d7043c81013f52d10d6.woff2 +0 -0
  960. package/src/styles/fonts/noto-serif-kr/400/37dbd564820cce2f7b3331e442da0df3.woff2 +0 -0
  961. package/src/styles/fonts/noto-serif-kr/400/37dc8505b20b37a2477a9121b16d44a7.woff2 +0 -0
  962. package/src/styles/fonts/noto-serif-kr/400/3ac0a07878cacdc98bb889f40f5970d3.woff2 +0 -0
  963. package/src/styles/fonts/noto-serif-kr/400/491bbba374093fff045f55803a22bfef.woff2 +0 -0
  964. package/src/styles/fonts/noto-serif-kr/400/4d38e56738156625329d93bd5c8cc74a.woff2 +0 -0
  965. package/src/styles/fonts/noto-serif-kr/400/57e9a86651366c1ba299e47aa7e358c2.woff2 +0 -0
  966. package/src/styles/fonts/noto-serif-kr/400/57f89adeae55aa7fe2add3fc1801ce03.woff2 +0 -0
  967. package/src/styles/fonts/noto-serif-kr/400/5df9ce98c75f8c50fb4fff7f01a23925.woff2 +0 -0
  968. package/src/styles/fonts/noto-serif-kr/400/60020a830d809c26970cf8e48ba1eeda.woff2 +0 -0
  969. package/src/styles/fonts/noto-serif-kr/400/60bcfa3ea7446eb42394aab02d28be40.woff2 +0 -0
  970. package/src/styles/fonts/noto-serif-kr/400/63fafcb069520613d0ea35ad3e6b1e42.woff2 +0 -0
  971. package/src/styles/fonts/noto-serif-kr/400/65a60d87c64228d258a123cbe85f5f31.woff2 +0 -0
  972. package/src/styles/fonts/noto-serif-kr/400/6968e889e18891d912809fe484c2e745.woff2 +0 -0
  973. package/src/styles/fonts/noto-serif-kr/400/70714891ad3fbfc3d5f10a8669dacc5a.woff2 +0 -0
  974. package/src/styles/fonts/noto-serif-kr/400/761d84afca8d7f34eebefe538adba827.woff2 +0 -0
  975. package/src/styles/fonts/noto-serif-kr/400/76e07530323418ec723c5d7634c9bcca.woff2 +0 -0
  976. package/src/styles/fonts/noto-serif-kr/400/773819b7b9b8fd404a929867c0fd677e.woff2 +0 -0
  977. package/src/styles/fonts/noto-serif-kr/400/7817dd16805145d8538ad57590f69f5a.woff2 +0 -0
  978. package/src/styles/fonts/noto-serif-kr/400/7b59f0ec7792b18458dc5a361e37884c.woff2 +0 -0
  979. package/src/styles/fonts/noto-serif-kr/400/7c3945788a689a69356c1a622d69d48b.woff2 +0 -0
  980. package/src/styles/fonts/noto-serif-kr/400/835b6505bb9eea9678925a1fa885353d.woff2 +0 -0
  981. package/src/styles/fonts/noto-serif-kr/400/8b5c9ef81159f31d2ab35f45ca4373e0.woff2 +0 -0
  982. package/src/styles/fonts/noto-serif-kr/400/8b91c7c2ed390f1278b9befa3aa87233.woff2 +0 -0
  983. package/src/styles/fonts/noto-serif-kr/400/8fb45117a62d92dce44a80f0b729ead5.woff2 +0 -0
  984. package/src/styles/fonts/noto-serif-kr/400/90b7848e9b1623b77bdcf155e90b839c.woff2 +0 -0
  985. package/src/styles/fonts/noto-serif-kr/400/985d4d41afa0934a4eb2de35473fb9a2.woff2 +0 -0
  986. package/src/styles/fonts/noto-serif-kr/400/9bb38fd05201de666b88b82d56a386f7.woff2 +0 -0
  987. package/src/styles/fonts/noto-serif-kr/400/9eff2ab2a9d2ced47ab761bf6fdb11ec.woff2 +0 -0
  988. package/src/styles/fonts/noto-serif-kr/400/9f1963fe8d4d6878d717d872a8f3fdb5.woff2 +0 -0
  989. package/src/styles/fonts/noto-serif-kr/400/a0d00fe4816c95a8c7dffd445ef00b03.woff2 +0 -0
  990. package/src/styles/fonts/noto-serif-kr/400/a0f199077fa1a33bc2a1e01c64de7fe6.woff2 +0 -0
  991. package/src/styles/fonts/noto-serif-kr/400/a7c35e42a659347e490c3cb7983b2e7f.woff2 +0 -0
  992. package/src/styles/fonts/noto-serif-kr/400/aa662092cf249707123ca4d575e2764b.woff2 +0 -0
  993. package/src/styles/fonts/noto-serif-kr/400/aae1eda193285ab817a2d1b408440326.woff2 +0 -0
  994. package/src/styles/fonts/noto-serif-kr/400/b0cb664cb2e1371efda8943c0b7dcd1c.woff2 +0 -0
  995. package/src/styles/fonts/noto-serif-kr/400/b0d1cdced482352cf0d3ae58638aacb9.woff2 +0 -0
  996. package/src/styles/fonts/noto-serif-kr/400/b320f5d185b2cff933ac549c184031c5.woff2 +0 -0
  997. package/src/styles/fonts/noto-serif-kr/400/ba9c59d7dfa4494db1bb764ada81467d.woff2 +0 -0
  998. package/src/styles/fonts/noto-serif-kr/400/bab547459d514f46206e340c4bb2dc88.woff2 +0 -0
  999. package/src/styles/fonts/noto-serif-kr/400/bcc39eda837bb7a7a3d37c8c60fffb81.woff2 +0 -0
  1000. package/src/styles/fonts/noto-serif-kr/400/bd73264d7f98776708d5d6f3c9b78fcc.woff2 +0 -0
  1001. package/src/styles/fonts/noto-serif-kr/400/bed610b217d500f5975cfc9fe6157570.woff2 +0 -0
  1002. package/src/styles/fonts/noto-serif-kr/400/bf8901f8f11d4f433ea17dabc9370ea6.woff2 +0 -0
  1003. package/src/styles/fonts/noto-serif-kr/400/c4143bb9f2fe77b6ccf20088a8904650.woff2 +0 -0
  1004. package/src/styles/fonts/noto-serif-kr/400/c7ff3f6bbdcd5f604b7343602ab904df.woff2 +0 -0
  1005. package/src/styles/fonts/noto-serif-kr/400/c945c62368357d05a53206620460fb30.woff2 +0 -0
  1006. package/src/styles/fonts/noto-serif-kr/400/ce022e18a1377ac509443c3c3790b431.woff2 +0 -0
  1007. package/src/styles/fonts/noto-serif-kr/400/cf9cffe56636322f62b40d61130fbc5e.woff2 +0 -0
  1008. package/src/styles/fonts/noto-serif-kr/400/d20d8944bc0b85f5b2aae4b24f343516.woff2 +0 -0
  1009. package/src/styles/fonts/noto-serif-kr/400/d36644d6502527e1fff205d0c7eca434.woff2 +0 -0
  1010. package/src/styles/fonts/noto-serif-kr/400/d40ab99c7a38026f411c8f112f742b48.woff2 +0 -0
  1011. package/src/styles/fonts/noto-serif-kr/400/dc6e234ded795e91f76d6647f628fbf0.woff2 +0 -0
  1012. package/src/styles/fonts/noto-serif-kr/400/dd0cfdfdac0866e66d587e2b5a9e9961.woff2 +0 -0
  1013. package/src/styles/fonts/noto-serif-kr/400/de304c8a02e45ded4f8dcc479d167198.woff2 +0 -0
  1014. package/src/styles/fonts/noto-serif-kr/400/e8c364c16daa04835bf32d293d2598db.woff2 +0 -0
  1015. package/src/styles/fonts/noto-serif-kr/400/ec86e23683052da5cfdc3b77641bd15a.woff2 +0 -0
  1016. package/src/styles/fonts/noto-serif-kr/400/eda1b0cb6d1719dd9bedcf3216a9e8de.woff2 +0 -0
  1017. package/src/styles/fonts/noto-serif-kr/400/ee6fce20b420a480714607c66d7f97e5.woff2 +0 -0
  1018. package/src/styles/fonts/noto-serif-kr/400/eee3836d6ac17ebb2c450bbcbc9db121.woff2 +0 -0
  1019. package/src/styles/fonts/noto-serif-kr/400/f5738255e92d8dd34a46d1bcdf4c4074.woff2 +0 -0
  1020. package/src/styles/fonts/noto-serif-kr/400/fa6e58ce4b52695e7ae19bbea6336ec8.woff2 +0 -0
  1021. package/src/styles/fonts/noto-serif-kr/400/fd2069e6e8588c70b4f11364093b81f2.woff2 +0 -0
  1022. package/src/styles/fonts/noto-serif-kr/700/04b8bde59cff68eeee74fb1914eb09c7.woff2 +0 -0
  1023. package/src/styles/fonts/noto-serif-kr/700/05ac821472e235943ed1435e4bb8ecad.woff2 +0 -0
  1024. package/src/styles/fonts/noto-serif-kr/700/074c1f8483d5a4d8c45c8c5f3e4cbb32.woff2 +0 -0
  1025. package/src/styles/fonts/noto-serif-kr/700/1380210a722ac9aeef54005ad7b015c9.woff2 +0 -0
  1026. package/src/styles/fonts/noto-serif-kr/700/17c7f5d0a45e92ede0e5dec3a2c77efa.woff2 +0 -0
  1027. package/src/styles/fonts/noto-serif-kr/700/1be602ad456b0d75902d352116cb35fd.woff2 +0 -0
  1028. package/src/styles/fonts/noto-serif-kr/700/2612c60f9dc5459ac42763591240a1d6.woff2 +0 -0
  1029. package/src/styles/fonts/noto-serif-kr/700/2ab8cf1f23a5ac7939a7876054e711a7.woff2 +0 -0
  1030. package/src/styles/fonts/noto-serif-kr/700/34e6750e00c3a911ef87dc66b336594d.woff2 +0 -0
  1031. package/src/styles/fonts/noto-serif-kr/700/36f3df4730cfca4fc77fe52b52e6a126.woff2 +0 -0
  1032. package/src/styles/fonts/noto-serif-kr/700/37887deb7a9c466cf2af6ee7ff372ea6.woff2 +0 -0
  1033. package/src/styles/fonts/noto-serif-kr/700/3aba89c4d4281553078de4622b498cbb.woff2 +0 -0
  1034. package/src/styles/fonts/noto-serif-kr/700/3d88558097559d775231194d43745ba7.woff2 +0 -0
  1035. package/src/styles/fonts/noto-serif-kr/700/3dc3e8c35524cad2d1236f3393104ccf.woff2 +0 -0
  1036. package/src/styles/fonts/noto-serif-kr/700/45140742dfa8686b58db2899ce89f89a.woff2 +0 -0
  1037. package/src/styles/fonts/noto-serif-kr/700/469d2754e315e77270c12fa0ab28026c.woff2 +0 -0
  1038. package/src/styles/fonts/noto-serif-kr/700/4c0760284569b87d8f2290c324953973.woff2 +0 -0
  1039. package/src/styles/fonts/noto-serif-kr/700/4cba92c942694bdc479bc54101bb1aa4.woff2 +0 -0
  1040. package/src/styles/fonts/noto-serif-kr/700/4e7d263c30bb48604069a3b4404d4eeb.woff2 +0 -0
  1041. package/src/styles/fonts/noto-serif-kr/700/5279c7e465d9871edf51fd02e691eee7.woff2 +0 -0
  1042. package/src/styles/fonts/noto-serif-kr/700/54bfdb6f21f9d6191e5100580239e14f.woff2 +0 -0
  1043. package/src/styles/fonts/noto-serif-kr/700/56be5e32fd01668fe5ba814d94905839.woff2 +0 -0
  1044. package/src/styles/fonts/noto-serif-kr/700/615b38e0b5bb3ea68426f44f47706e6f.woff2 +0 -0
  1045. package/src/styles/fonts/noto-serif-kr/700/65e9c4585d71bf48a5c62f367010da16.woff2 +0 -0
  1046. package/src/styles/fonts/noto-serif-kr/700/6b75213eb0be40ce84241eb2bb438a2e.woff2 +0 -0
  1047. package/src/styles/fonts/noto-serif-kr/700/7520ca6ca8c60eb1e62d50e71c8d30f1.woff2 +0 -0
  1048. package/src/styles/fonts/noto-serif-kr/700/759a647b77791b1e98f99bc0ab5317a7.woff2 +0 -0
  1049. package/src/styles/fonts/noto-serif-kr/700/76e51630143b95b6322ef93ad330da7a.woff2 +0 -0
  1050. package/src/styles/fonts/noto-serif-kr/700/79aa7c8c842c4a27cf57c0a3a1ffca5a.woff2 +0 -0
  1051. package/src/styles/fonts/noto-serif-kr/700/7cc72fd2c9105560422b6a67c6162945.woff2 +0 -0
  1052. package/src/styles/fonts/noto-serif-kr/700/7d0ea0690e432462f4d05a23d720ec19.woff2 +0 -0
  1053. package/src/styles/fonts/noto-serif-kr/700/7dffa5c1bec57e0903fd62357401ff1a.woff2 +0 -0
  1054. package/src/styles/fonts/noto-serif-kr/700/82f6ccc063960eed1cdfd1d61ada8862.woff2 +0 -0
  1055. package/src/styles/fonts/noto-serif-kr/700/845b4b67564d62cf5cad242f7ac08ea5.woff2 +0 -0
  1056. package/src/styles/fonts/noto-serif-kr/700/84c8e7bc0931008ed91f44ed12dfea94.woff2 +0 -0
  1057. package/src/styles/fonts/noto-serif-kr/700/887a11290ba78b1e66c6d2f67043005e.woff2 +0 -0
  1058. package/src/styles/fonts/noto-serif-kr/700/88db1d042074fb6e66821ffc10941930.woff2 +0 -0
  1059. package/src/styles/fonts/noto-serif-kr/700/8dc18c0cebe6aa4bf4c45dbb831048ab.woff2 +0 -0
  1060. package/src/styles/fonts/noto-serif-kr/700/919a879bd2d580d8491a31a449390689.woff2 +0 -0
  1061. package/src/styles/fonts/noto-serif-kr/700/91da6cb174bebfb96976e2f1316be2fe.woff2 +0 -0
  1062. package/src/styles/fonts/noto-serif-kr/700/934dfdbed2bb2c4b6129199d699a34fa.woff2 +0 -0
  1063. package/src/styles/fonts/noto-serif-kr/700/97d7a17234f2b5030ae9697ae00aded2.woff2 +0 -0
  1064. package/src/styles/fonts/noto-serif-kr/700/995c4fda62d25c3b79dd5987737df6ae.woff2 +0 -0
  1065. package/src/styles/fonts/noto-serif-kr/700/9d0afa53dc2f92ba42024f55f11f6779.woff2 +0 -0
  1066. package/src/styles/fonts/noto-serif-kr/700/9e13a47242926f1cd7d68c08d9ef8889.woff2 +0 -0
  1067. package/src/styles/fonts/noto-serif-kr/700/a379ada89abd750c587ded29f77e731c.woff2 +0 -0
  1068. package/src/styles/fonts/noto-serif-kr/700/a48be56ce5d3a99dc8f8331398004412.woff2 +0 -0
  1069. package/src/styles/fonts/noto-serif-kr/700/a806759e72987951d6d08b7cbdf36a1b.woff2 +0 -0
  1070. package/src/styles/fonts/noto-serif-kr/700/a88481ec8bc7be11cb66e46781a79bb9.woff2 +0 -0
  1071. package/src/styles/fonts/noto-serif-kr/700/a9a016d4a93409f65278327e8a8bb38d.woff2 +0 -0
  1072. package/src/styles/fonts/noto-serif-kr/700/ab92d41062a7644faa45f50bf384454a.woff2 +0 -0
  1073. package/src/styles/fonts/noto-serif-kr/700/b326a8a9c33b554db570da94f60bc380.woff2 +0 -0
  1074. package/src/styles/fonts/noto-serif-kr/700/b6117ff2993b11bb1fdc7ea3588a010c.woff2 +0 -0
  1075. package/src/styles/fonts/noto-serif-kr/700/b61982951bd51b724143c30dfaaa9fe9.woff2 +0 -0
  1076. package/src/styles/fonts/noto-serif-kr/700/c0098958e20db68cab90097b5e62516f.woff2 +0 -0
  1077. package/src/styles/fonts/noto-serif-kr/700/c4bfaa5e50798246e3770718b7a7c84a.woff2 +0 -0
  1078. package/src/styles/fonts/noto-serif-kr/700/c84598999133455503042e06f4ab79cb.woff2 +0 -0
  1079. package/src/styles/fonts/noto-serif-kr/700/ca9a533988d7019597a60d4e17127e0c.woff2 +0 -0
  1080. package/src/styles/fonts/noto-serif-kr/700/cf2b28f90f47276f7e2688a65e88a101.woff2 +0 -0
  1081. package/src/styles/fonts/noto-serif-kr/700/d689b1861d7e4377dd72ad3013482612.woff2 +0 -0
  1082. package/src/styles/fonts/noto-serif-kr/700/d9047070d72a816b3dba9d40c2d85e69.woff2 +0 -0
  1083. package/src/styles/fonts/noto-serif-kr/700/da04549f3f4ed28076b01b8cd710d313.woff2 +0 -0
  1084. package/src/styles/fonts/noto-serif-kr/700/da7af303f8c645f9a9dbae0e6e32dd35.woff2 +0 -0
  1085. package/src/styles/fonts/noto-serif-kr/700/df9568257eb29b156449fdd4bec5ec76.woff2 +0 -0
  1086. package/src/styles/fonts/noto-serif-kr/700/e067cd0ed76c90cd0a93c9339253f20b.woff2 +0 -0
  1087. package/src/styles/fonts/noto-serif-kr/700/e08b07772e7bed3cec2832d43f7fd339.woff2 +0 -0
  1088. package/src/styles/fonts/noto-serif-kr/700/e12150d5a39b30be8f567968c7a527b0.woff2 +0 -0
  1089. package/src/styles/fonts/noto-serif-kr/700/e53fcb2381eee345db4f6f973dd95a3e.woff2 +0 -0
  1090. package/src/styles/fonts/noto-serif-kr/700/e5bd313ef81f687d398aacb11cec3069.woff2 +0 -0
  1091. package/src/styles/fonts/noto-serif-kr/700/eb5afb3d952b8593782caec6026514b6.woff2 +0 -0
  1092. package/src/styles/fonts/noto-serif-kr/700/edd6a4f608d04fc0351d7688cfc321e4.woff2 +0 -0
  1093. package/src/styles/fonts/noto-serif-kr/700/f9695c6c4df2bf6bc03045ff79d4f01f.woff2 +0 -0
  1094. package/src/styles/fonts/noto-serif-kr/700/fada6eaa68ff8816afe43d2a36c5423e.woff2 +0 -0
  1095. package/src/styles/fonts/noto-serif-kr/700/fbcc4bf5367218951172bdee6f77d7a6.woff2 +0 -0
  1096. package/src/styles/fonts/noto-serif-kr/noto-serif-kr.css +2601 -0
  1097. package/src/styles/site-media.css +823 -0
  1098. package/src/styles/tokens.css +292 -21
  1099. package/src/styles/ui.css +4188 -1857
  1100. package/src/types/app-context.ts +20 -0
  1101. package/src/types/bindings.ts +3 -1
  1102. package/src/types/config.ts +147 -9
  1103. package/src/types/constants.ts +46 -11
  1104. package/src/types/entities.ts +24 -8
  1105. package/src/types/operations.ts +80 -1
  1106. package/src/types/props.ts +10 -9
  1107. package/src/types/raw-assets.d.ts +39 -0
  1108. package/src/types/views.ts +28 -7
  1109. package/src/ui/__tests__/color-themes.test.ts +7 -40
  1110. package/src/ui/__tests__/font-themes.test.ts +30 -11
  1111. package/src/ui/color-themes.ts +311 -270
  1112. package/src/ui/compose/ComposeDialog.tsx +712 -394
  1113. package/src/ui/compose/ComposePrompt.tsx +15 -10
  1114. package/src/ui/dash/ActionButtons.tsx +27 -18
  1115. package/src/ui/dash/DangerZone.tsx +15 -10
  1116. package/src/ui/dash/FormatBadge.tsx +21 -8
  1117. package/src/ui/dash/StatusBadge.tsx +33 -22
  1118. package/src/ui/dash/appearance/AdvancedContent.tsx +44 -30
  1119. package/src/ui/dash/appearance/CodeInjectionContent.tsx +164 -0
  1120. package/src/ui/dash/appearance/ColorThemeContent.tsx +127 -160
  1121. package/src/ui/dash/appearance/FontThemeContent.tsx +20 -15
  1122. package/src/ui/dash/appearance/NavigationContent.tsx +368 -225
  1123. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +54 -0
  1124. package/src/ui/dash/settings/AccountContent.tsx +34 -23
  1125. package/src/ui/dash/settings/AccountMenuContent.tsx +226 -137
  1126. package/src/ui/dash/settings/ApiTokensContent.tsx +122 -79
  1127. package/src/ui/dash/settings/AvatarContent.tsx +73 -45
  1128. package/src/ui/dash/settings/DeleteAccountContent.tsx +106 -66
  1129. package/src/ui/dash/settings/GeneralContent.tsx +274 -157
  1130. package/src/ui/dash/settings/GitHubSyncContent.tsx +558 -0
  1131. package/src/ui/dash/settings/SessionsContent.tsx +79 -38
  1132. package/src/ui/dash/settings/SettingsRootContent.tsx +209 -109
  1133. package/src/ui/dash/settings/__tests__/AccountMenuContent.test.tsx +67 -0
  1134. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +75 -0
  1135. package/src/ui/feed/CuratedThreadPreview.tsx +24 -14
  1136. package/src/ui/feed/LinkCard.tsx +116 -84
  1137. package/src/ui/feed/LinkPreview.tsx +75 -0
  1138. package/src/ui/feed/NoteCard.tsx +60 -16
  1139. package/src/ui/feed/PostStatusBadges.tsx +15 -0
  1140. package/src/ui/feed/QuoteCard.tsx +10 -3
  1141. package/src/ui/feed/ThreadPreview.tsx +62 -36
  1142. package/src/ui/feed/TimelineFeed.tsx +2 -1
  1143. package/src/ui/feed/__tests__/thread-preview.test.ts +220 -11
  1144. package/src/ui/feed/__tests__/timeline-cards.test.ts +186 -5
  1145. package/src/ui/feed/thread-preview-state.ts +23 -10
  1146. package/src/ui/font-themes.ts +57 -97
  1147. package/src/ui/layouts/BaseLayout.tsx +230 -136
  1148. package/src/ui/layouts/SiteLayout.tsx +431 -143
  1149. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +76 -6
  1150. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +76 -0
  1151. package/src/ui/pages/ArchivePage.tsx +320 -207
  1152. package/src/ui/pages/BrandPage.tsx +499 -341
  1153. package/src/ui/pages/CollectionEditorPage.tsx +50 -30
  1154. package/src/ui/pages/CollectionPage.tsx +151 -78
  1155. package/src/ui/pages/CollectionsPage.tsx +27 -18
  1156. package/src/ui/pages/ComposePage.tsx +15 -10
  1157. package/src/ui/pages/FeaturedPage.tsx +16 -11
  1158. package/src/ui/pages/HomePage.tsx +15 -10
  1159. package/src/ui/pages/PostPage.tsx +5 -1
  1160. package/src/ui/pages/SearchPage.tsx +93 -43
  1161. package/src/ui/pages/ThemeSamplePage.tsx +758 -520
  1162. package/src/ui/pages/__tests__/ArchivePage.test.tsx +72 -0
  1163. package/src/ui/shared/CollectionDirectory.tsx +215 -34
  1164. package/src/ui/shared/CollectionsManager.tsx +155 -75
  1165. package/src/ui/shared/DecorativeQuoteMark.tsx +12 -9
  1166. package/src/ui/shared/MediaGallery.tsx +350 -256
  1167. package/src/ui/shared/Pagination.tsx +36 -25
  1168. package/src/ui/shared/PostFooter.tsx +139 -96
  1169. package/src/ui/shared/__tests__/media-gallery.test.ts +27 -0
  1170. package/src/ui/shared/__tests__/navigation-labels.test.ts +79 -28
  1171. package/src/ui/shared/__tests__/post-footer.test.ts +42 -17
  1172. package/src/ui/shared/collection-management-labels.ts +109 -33
  1173. package/src/ui/shared/navigation-labels.ts +78 -8
  1174. package/bin/lib/site-localize-media.js +0 -427
  1175. package/dist/client/_assets/client-auth.js +0 -3260
  1176. package/dist/client/_assets/client.css +0 -2
  1177. package/dist/client/_assets/client.js +0 -380
  1178. package/src/__tests__/site-localize-media.test.ts +0 -150
  1179. package/src/client/components/jant-collection-sidebar.ts +0 -816
  1180. package/src/i18n/locales/en.ts +0 -1
  1181. package/src/i18n/locales/zh-Hans.po +0 -3197
  1182. package/src/i18n/locales/zh-Hans.ts +0 -1
  1183. package/src/i18n/locales/zh-Hant.po +0 -3197
  1184. package/src/i18n/locales/zh-Hant.ts +0 -1
  1185. package/src/routes/feed/rss.ts +0 -216
  1186. package/src/types/lingui-react-macro.d.ts +0 -34
  1187. /package/dist/client/_assets/chunks/{heic-to-XcUDQvtx.js → heic-to-DUUaO23q.js} +0 -0
  1188. /package/dist/client/_assets/chunks/{module-ChVQstFd.js → module-DcsAZQZ_.js} +0 -0
  1189. /package/dist/client/_assets/chunks/{native-CR5HLOyf.js → native-DpcrFAPh.js} +0 -0
  1190. /package/dist/client/_assets/{client-cjk.css → client-cjk-B7Z0snDu.css} +0 -0
  1191. /package/dist/client/_assets/{client-cjk-tc.css → client-cjk-tc-BesJYrb2.css} +0 -0
@@ -29,25 +29,62 @@ import type {
29
29
  import type { CollectionSubmitDetail } from "./collection-types.js";
30
30
  import { showToast } from "../toast.js";
31
31
  import { publicPath } from "../runtime-paths.js";
32
+ import { parseMarkdownDocument } from "../../lib/markdown-manager.js";
33
+ import {
34
+ applyItemOrder,
35
+ filterCollectionsBySearch,
36
+ getSelectedFirstOrder,
37
+ } from "../collection-picker-order.js";
32
38
  import type { JantComposeEditor } from "./jant-compose-editor.js";
33
39
  import { getMediaCategory } from "../../lib/upload.js";
34
40
  import { getSlugValidationIssue } from "../../lib/slug-format.js";
35
41
  import { createTiptapEditor } from "../tiptap/create-editor.js";
42
+ import { MAX_THREAD_POSTS } from "../../types.js";
43
+
44
+ interface ReplyToMedia {
45
+ url: string;
46
+ previewUrl: string;
47
+ alt?: string;
48
+ mimeType: string;
49
+ width?: number;
50
+ height?: number;
51
+ }
36
52
 
37
53
  interface ReplyToData {
38
54
  contentHtml: string;
39
55
  dateText: string;
56
+ media?: ReplyToMedia[];
57
+ }
58
+
59
+ interface ThreadItem {
60
+ id: string;
61
+ format: ComposeFormat;
40
62
  }
41
63
 
42
- interface ComposeMediaAttachmentResponse {
64
+ interface ApiMediaAttachment {
65
+ type: "media";
43
66
  id: string;
44
67
  previewUrl: string;
45
68
  alt?: string;
46
69
  mimeType: string;
47
70
  url?: string;
71
+ width?: number;
72
+ height?: number;
73
+ summary?: string;
74
+ chars?: number;
75
+ originalName?: string;
76
+ }
77
+
78
+ interface ApiTextAttachment {
79
+ type: "text";
80
+ id: string;
81
+ contentUrl: string;
48
82
  summary?: string;
83
+ chars?: number;
49
84
  }
50
85
 
86
+ type ApiAttachment = ApiMediaAttachment | ApiTextAttachment;
87
+
51
88
  interface ComposePostResponse {
52
89
  id: string;
53
90
  threadId?: string;
@@ -56,7 +93,7 @@ interface ComposePostResponse {
56
93
  visibility?: ComposeVisibility | null;
57
94
  replyToId?: string | null;
58
95
  collectionIds?: string[];
59
- mediaAttachments?: ComposeMediaAttachmentResponse[];
96
+ attachments?: ApiAttachment[];
60
97
  title?: string | null;
61
98
  body?: string | null;
62
99
  url?: string | null;
@@ -75,12 +112,20 @@ interface DraftsResponse {
75
112
  interface ComposeOpenOptions {
76
113
  collectionId?: string;
77
114
  restoreDraft?: boolean;
115
+ initialFormat?: ComposeFormat;
116
+ }
117
+
118
+ interface ComposeReplyOpenOptions {
119
+ restoreDraft?: boolean;
120
+ initialFormat?: ComposeFormat;
78
121
  }
79
122
 
80
123
  interface ComposeStateSnapshot {
81
124
  format: ComposeFormat;
82
125
  collectionIds: string[];
83
126
  slug: string;
127
+ publishedAtInput: string;
128
+ publishedAtTimeMinutes: number | null;
84
129
  visibility: ComposeVisibility;
85
130
  title: string;
86
131
  bodyJson: JSONContent | null;
@@ -116,6 +161,16 @@ interface ComposeFilePickerCloseDetail {
116
161
  cancelled?: boolean;
117
162
  }
118
163
 
164
+ interface ComposePublishSummaryChip {
165
+ kind: "publishedAt" | "slug";
166
+ text: string;
167
+ actionLabel: string;
168
+ value: string;
169
+ }
170
+
171
+ const COMPOSE_PUBLISH_PANEL_FULLSCREEN_QUERY =
172
+ "(max-width: 700px), (max-height: 760px), (hover: none) and (pointer: coarse)";
173
+
119
174
  const COMPOSE_DIALOG_HEADER_ICONS = {
120
175
  drafts: `
121
176
  <rect x="3.85" y="3.45" width="7.85" height="8.35" rx="2.35" />
@@ -274,9 +329,10 @@ function toComposeCollections(value: unknown): ComposeCollection[] {
274
329
 
275
330
  const id = Reflect.get(item, "id");
276
331
  const title = Reflect.get(item, "title");
332
+ const slug = Reflect.get(item, "slug");
277
333
  if (typeof id !== "string" || typeof title !== "string") return [];
278
334
 
279
- return [{ id, title }];
335
+ return [{ id, title, slug: typeof slug === "string" ? slug : "" }];
280
336
  });
281
337
  }
282
338
 
@@ -293,9 +349,181 @@ function normalizeComposeDoc(json: JSONContent | null): JSONContent | null {
293
349
  return isEmptyComposeDoc(json) ? null : json;
294
350
  }
295
351
 
352
+ function padDateTimePart(value: number): string {
353
+ return String(value).padStart(2, "0");
354
+ }
355
+
356
+ function toLocalDateInputValue(timestamp: number): string {
357
+ const date = new Date(timestamp * 1000);
358
+ return `${date.getFullYear()}-${padDateTimePart(
359
+ date.getMonth() + 1,
360
+ )}-${padDateTimePart(date.getDate())}`;
361
+ }
362
+
363
+ function parseLocalDateInputValue(
364
+ value: string,
365
+ ): { year: number; monthIndex: number; day: number } | null {
366
+ const trimmed = value.trim();
367
+ if (!trimmed) return null;
368
+
369
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
370
+ if (!match) return null;
371
+
372
+ const year = Number(match[1]);
373
+ const month = Number(match[2]);
374
+ const day = Number(match[3]);
375
+ const parsed = new Date(year, month - 1, day);
376
+ if (
377
+ Number.isNaN(parsed.getTime()) ||
378
+ parsed.getFullYear() !== year ||
379
+ parsed.getMonth() !== month - 1 ||
380
+ parsed.getDate() !== day
381
+ ) {
382
+ return null;
383
+ }
384
+
385
+ return { year, monthIndex: month - 1, day };
386
+ }
387
+
388
+ function getTimestampTimeMinutes(timestamp: number): number {
389
+ const date = new Date(timestamp * 1000);
390
+ return date.getHours() * 60 + date.getMinutes();
391
+ }
392
+
393
+ function buildTimestampFromLocalDate(
394
+ value: string,
395
+ timeMinutes: number,
396
+ ): number | null {
397
+ const parsed = parseLocalDateInputValue(value);
398
+ if (!parsed) return null;
399
+
400
+ const clampedMinutes = Math.min(Math.max(timeMinutes, 0), 23 * 60 + 59);
401
+ const hours = Math.floor(clampedMinutes / 60);
402
+ const minutes = clampedMinutes % 60;
403
+ return Math.floor(
404
+ new Date(
405
+ parsed.year,
406
+ parsed.monthIndex,
407
+ parsed.day,
408
+ hours,
409
+ minutes,
410
+ 0,
411
+ 0,
412
+ ).getTime() / 1000,
413
+ );
414
+ }
415
+
416
+ /**
417
+ * Split API attachments into media items and resolved text attachments
418
+ * for use with `editor.populate()`.
419
+ */
420
+ async function resolveApiAttachments(allAttachments: ApiAttachment[]) {
421
+ const mediaItems = allAttachments.filter(
422
+ (a): a is ApiMediaAttachment => a.type === "media",
423
+ );
424
+ const textItems = allAttachments.filter(
425
+ (a): a is ApiTextAttachment => a.type === "text",
426
+ );
427
+
428
+ const media = mediaItems.map((m) => ({
429
+ id: m.id,
430
+ previewUrl: m.previewUrl,
431
+ alt: m.alt,
432
+ mimeType: m.mimeType,
433
+ originalName: m.originalName,
434
+ summary: m.summary,
435
+ chars: m.chars,
436
+ }));
437
+
438
+ const textAttachments = await Promise.all(
439
+ textItems.map(async (m) => {
440
+ // Editing a post hydrates existing text attachments back into the
441
+ // editor. Fetch the markdown source from the auth'd attachments API
442
+ // (compose is always admin-authenticated) and parse it into a
443
+ // Tiptap document on the client. The `.html` sibling is not used
444
+ // here — the editor owns HTML rendering from the JSON source.
445
+ try {
446
+ const res = await fetch(m.contentUrl);
447
+ if (res.ok) {
448
+ const payload = (await res.json()) as {
449
+ content?: string;
450
+ contentFormat?: string;
451
+ };
452
+ const markdown =
453
+ payload.contentFormat === "markdown" && payload.content
454
+ ? payload.content
455
+ : "";
456
+ const doc = markdown ? parseMarkdownDocument(markdown) : null;
457
+ if (doc) {
458
+ return {
459
+ bodyJson: JSON.stringify(doc),
460
+ bodyHtml: "",
461
+ summary: m.summary ?? "",
462
+ mediaId: m.id,
463
+ };
464
+ }
465
+ }
466
+ } catch {
467
+ // Fetch or parse failed — fall through to the empty-shell return
468
+ // below. The attachment stays visible in the composer so the user
469
+ // can decide whether to keep, edit, or remove it explicitly; we
470
+ // do not silently drop it.
471
+ }
472
+ return {
473
+ bodyJson: JSON.stringify({
474
+ type: "doc",
475
+ content: [{ type: "paragraph" }],
476
+ }),
477
+ bodyHtml: "",
478
+ summary: m.summary ?? "",
479
+ mediaId: m.id,
480
+ };
481
+ }),
482
+ );
483
+
484
+ const attachmentOrder = allAttachments.map((a) => a.id);
485
+
486
+ return { media, textAttachments, attachmentOrder };
487
+ }
488
+
296
489
  export class JantComposeDialog extends LitElement {
297
490
  private static _lastNewPostVisibility: ComposeVisibility = "public";
298
491
 
492
+ /** The collection ID that triggered this compose session (for per-collection visibility memory). */
493
+ private _sourceCollectionId: string | null = null;
494
+
495
+ private static _collectionVisibilityKey(collectionId: string): string {
496
+ return `jant:collection-visibility:${collectionId}`;
497
+ }
498
+
499
+ private static _getCollectionVisibility(
500
+ collectionId: string,
501
+ ): ComposeVisibility | null {
502
+ try {
503
+ const v = globalThis.localStorage.getItem(
504
+ JantComposeDialog._collectionVisibilityKey(collectionId),
505
+ );
506
+ if (v === "public" || v === "latest_hidden" || v === "private") return v;
507
+ } catch {
508
+ // localStorage unavailable
509
+ }
510
+ return null;
511
+ }
512
+
513
+ private static _setCollectionVisibility(
514
+ collectionId: string,
515
+ visibility: ComposeVisibility,
516
+ ) {
517
+ try {
518
+ globalThis.localStorage.setItem(
519
+ JantComposeDialog._collectionVisibilityKey(collectionId),
520
+ visibility,
521
+ );
522
+ } catch {
523
+ // localStorage unavailable
524
+ }
525
+ }
526
+
299
527
  static properties = {
300
528
  collections: { type: Array },
301
529
  labels: { type: Object },
@@ -306,6 +534,7 @@ export class JantComposeDialog extends LitElement {
306
534
  _format: { state: true },
307
535
  _status: { state: true },
308
536
  _loading: { state: true },
537
+ _openingEdit: { state: true },
309
538
  _collectionIds: { state: true },
310
539
  _showCollection: { state: true },
311
540
  _collectionSearch: { state: true },
@@ -325,14 +554,19 @@ export class JantComposeDialog extends LitElement {
325
554
  _replyToId: { state: true },
326
555
  _replyToData: { state: true },
327
556
  _replyExpanded: { state: true },
557
+ _threadItems: { state: true },
558
+ _focusedThreadIndex: { state: true },
328
559
  _slug: { state: true },
560
+ _publishedAtInput: { state: true },
329
561
  _visibility: { state: true },
330
562
  _showPublishPanel: { state: true },
563
+ _publishPanelFullscreen: { state: true },
331
564
  _suggestedSlug: { state: true },
332
565
  _suggestedSlugLoading: { state: true },
333
566
  _slugCheckLoading: { state: true },
334
567
  _slugTaken: { state: true },
335
568
  _visibilityLocked: { state: true },
569
+ _quietReply: { state: true },
336
570
  };
337
571
 
338
572
  declare collections: ComposeCollection[];
@@ -344,6 +578,7 @@ export class JantComposeDialog extends LitElement {
344
578
  declare _format: ComposeFormat;
345
579
  declare _status: "published" | "draft";
346
580
  declare _loading: boolean;
581
+ declare _openingEdit: boolean;
347
582
  declare _collectionIds: string[];
348
583
  declare _showCollection: boolean;
349
584
  declare _collectionSearch: string;
@@ -363,14 +598,19 @@ export class JantComposeDialog extends LitElement {
363
598
  declare _replyToId: string | null;
364
599
  declare _replyToData: ReplyToData | null;
365
600
  declare _replyExpanded: boolean;
601
+ declare _threadItems: ThreadItem[];
602
+ declare _focusedThreadIndex: number;
366
603
  declare _slug: string;
604
+ declare _publishedAtInput: string;
367
605
  declare _visibility: ComposeVisibility;
368
606
  declare _showPublishPanel: boolean;
607
+ declare _publishPanelFullscreen: boolean;
369
608
  declare _suggestedSlug: string;
370
609
  declare _suggestedSlugLoading: boolean;
371
610
  declare _slugCheckLoading: boolean;
372
611
  declare _slugTaken: boolean;
373
612
  declare _visibilityLocked: boolean;
613
+ declare _quietReply: boolean;
374
614
 
375
615
  private _attachedEditor: Editor | null = null;
376
616
  private _attachedTextSnapshot: JSONContent | null = null;
@@ -388,6 +628,11 @@ export class JantComposeDialog extends LitElement {
388
628
  | "post-view"
389
629
  | null = null;
390
630
  private _replyRefreshId: string | null = null;
631
+ private _publishedAtTimeMinutes: number | null = null;
632
+ private _originalPublishedAt: number | null = null;
633
+ private _initialPublishedAtTimeMinutes: number | null = null;
634
+ private _initialPublishedAtInput = "";
635
+ private _initialSlug = "";
391
636
  private _slugCheckTimer: ReturnType<typeof setTimeout> | null = null;
392
637
  private _slugSuggestTimer: ReturnType<typeof setTimeout> | null = null;
393
638
  private _slugSuggestRequestId = 0;
@@ -395,8 +640,13 @@ export class JantComposeDialog extends LitElement {
395
640
  private _slugSuggestionKey = "";
396
641
  private _suppressBeforeUnload = false;
397
642
  private _dialogEl: HTMLDialogElement | null = null;
643
+ private _mousedownOnBackdrop = false;
398
644
  private _filePickerActive = false;
399
645
  private _ignoreNextEscapeClose = false;
646
+ private _openEditRequestId = 0;
647
+ private _collectionPickerOrder: string[] = [];
648
+ private _suppressCollectionOptionClickUntil = 0;
649
+ private _suppressedCollectionOptionId: string | null = null;
400
650
 
401
651
  createRenderRoot() {
402
652
  this.innerHTML = "";
@@ -414,6 +664,7 @@ export class JantComposeDialog extends LitElement {
414
664
  this._format = "note";
415
665
  this._status = "published";
416
666
  this._loading = false;
667
+ this._openingEdit = false;
417
668
  this._collectionIds = [];
418
669
  this._showCollection = false;
419
670
  this._collectionSearch = "";
@@ -433,17 +684,28 @@ export class JantComposeDialog extends LitElement {
433
684
  this._replyToId = null;
434
685
  this._replyToData = null;
435
686
  this._replyExpanded = false;
687
+ this._threadItems = [];
688
+ this._focusedThreadIndex = 0;
436
689
  this._replyThreadRootId = null;
437
690
  this._replyRefreshKind = null;
438
691
  this._replyRefreshId = null;
439
692
  this._slug = "";
693
+ this._publishedAtInput = "";
694
+ this._publishedAtTimeMinutes = null;
695
+ this._originalPublishedAt = null;
696
+ this._initialPublishedAtTimeMinutes = null;
697
+ this._initialPublishedAtInput = "";
698
+ this._initialSlug = "";
440
699
  this._visibility = JantComposeDialog._lastNewPostVisibility;
700
+ this._sourceCollectionId = null;
441
701
  this._showPublishPanel = false;
702
+ this._publishPanelFullscreen = false;
442
703
  this._suggestedSlug = "";
443
704
  this._suggestedSlugLoading = false;
444
705
  this._slugCheckLoading = false;
445
706
  this._slugTaken = false;
446
707
  this._visibilityLocked = false;
708
+ this._quietReply = false;
447
709
  }
448
710
 
449
711
  private get _editor(): JantComposeEditor | null {
@@ -467,26 +729,37 @@ export class JantComposeDialog extends LitElement {
467
729
  titleInput?.select();
468
730
  });
469
731
  }
732
+ if (changed.has("_showCollection") && this._showCollection) {
733
+ this._scheduleCollectionPickerAutofocus();
734
+ }
470
735
  if (
471
736
  changed.has("_format") ||
472
737
  changed.has("_collectionIds") ||
473
738
  changed.has("_slug") ||
739
+ changed.has("_publishedAtInput") ||
474
740
  changed.has("_visibility")
475
741
  ) {
476
- // Schedule draft auto-save for new-post mode only
477
- if (!this._editPostId && !this._draftSourceId) {
742
+ // Schedule draft auto-save (new-post and edit modes, not draft-load)
743
+ if (!this._draftSourceId) {
478
744
  this._scheduleDraftSave();
479
745
  }
480
746
  }
747
+ if (this._showPublishPanel) {
748
+ this._updatePublishPanelLayout();
749
+ }
750
+ if (this._showCollection) {
751
+ this._updateCollectionPopoverSide();
752
+ }
481
753
  }
482
754
 
483
755
  reset() {
756
+ this._openEditRequestId += 1;
484
757
  this._format = "note";
485
758
  this._status = "published";
486
759
  this._loading = false;
760
+ this._openingEdit = false;
487
761
  this._collectionIds = [];
488
- this._showCollection = false;
489
- this._collectionSearch = "";
762
+ this._closeCollectionPicker();
490
763
  this._altPanelOpen = false;
491
764
  this._altPanelIndex = 0;
492
765
  this._attachedPanelOpen = false;
@@ -503,11 +776,20 @@ export class JantComposeDialog extends LitElement {
503
776
  this._replyToId = null;
504
777
  this._replyToData = null;
505
778
  this._replyExpanded = false;
779
+ this._threadItems = [];
780
+ this._focusedThreadIndex = 0;
506
781
  this._replyThreadRootId = null;
507
782
  this._replyRefreshKind = null;
508
783
  this._replyRefreshId = null;
509
784
  this._slug = "";
785
+ this._publishedAtInput = "";
786
+ this._publishedAtTimeMinutes = null;
787
+ this._originalPublishedAt = null;
788
+ this._initialPublishedAtTimeMinutes = null;
789
+ this._initialPublishedAtInput = "";
790
+ this._initialSlug = "";
510
791
  this._visibility = JantComposeDialog._lastNewPostVisibility;
792
+ this._sourceCollectionId = null;
511
793
  this._showPublishPanel = false;
512
794
  this._suggestedSlug = "";
513
795
  this._suggestedSlugLoading = false;
@@ -515,6 +797,7 @@ export class JantComposeDialog extends LitElement {
515
797
  this._slugTaken = false;
516
798
  this._slugSuggestionKey = "";
517
799
  this._visibilityLocked = false;
800
+ this._quietReply = false;
518
801
  this._confirmForDrafts = false;
519
802
  this._confirmForAttachedText = false;
520
803
  this._initialSnapshot = null;
@@ -534,6 +817,7 @@ export class JantComposeDialog extends LitElement {
534
817
  async refreshCollections(): Promise<boolean> {
535
818
  try {
536
819
  const res = await fetch("/api/collections?view=compose", {
820
+ cache: "no-store",
537
821
  headers: { Accept: "application/json" },
538
822
  });
539
823
  if (!res.ok) return false;
@@ -552,105 +836,99 @@ export class JantComposeDialog extends LitElement {
552
836
 
553
837
  async openEdit(id: string) {
554
838
  this.reset();
555
-
556
- const res = await fetch(`/api/posts/${id}`);
557
- if (!res.ok) return;
558
- const post = (await res.json()) as ComposePostResponse;
559
-
839
+ const requestId = ++this._openEditRequestId;
840
+ this._openingEdit = true;
560
841
  this._editPostId = id;
561
- this._format = post.format;
562
- this._slug = post.slug ?? "";
563
- this._slugTaken = false;
564
- this._slugCheckLoading = false;
565
- this._suggestedSlug = "";
566
- this._suggestedSlugLoading = false;
567
- this._slugSuggestionKey = "";
568
- this._visibility = post.visibility ?? "public";
569
- this._visibilityLocked = Boolean(post.replyToId);
570
842
 
571
- // Pre-fill collection memberships if present
572
- if (post.collectionIds?.length) {
573
- this._collectionIds = post.collectionIds;
843
+ const dialog = this.closest("dialog");
844
+ if (dialog && !dialog.open) {
845
+ dialog.showModal();
574
846
  }
575
-
576
- // Wait for Lit to render with the new format before populating editor
577
847
  await this.updateComplete;
848
+ this._focusDialogShell();
578
849
 
579
- // Separate text media items from other media attachments
580
- const allMedia = post.mediaAttachments ?? [];
581
- const nonTextMedia = allMedia.filter(
582
- (m: { mimeType: string }) => !m.mimeType.startsWith("text/"),
583
- );
584
- const textMedia = allMedia.filter(
585
- (m: { mimeType: string }) => m.mimeType === "text/x-tiptap+json",
586
- );
850
+ try {
851
+ const res = await fetch(`/api/posts/${id}`);
852
+ if (!res.ok) throw new Error("Failed to load post");
853
+ const post = (await res.json()) as ComposePostResponse;
854
+ if (requestId !== this._openEditRequestId) return;
587
855
 
588
- // Fetch text content for TipTap text media items (stored as { json, html } envelope)
589
- const textAttachments = await Promise.all(
590
- textMedia.map(
591
- async (m: { id: string; url?: string; summary?: string }) => {
592
- try {
593
- const textRes = await fetch(`/api/media/${m.id}/content`);
594
- if (textRes.ok) {
595
- const raw = await textRes.text();
596
- const envelope = JSON.parse(raw) as {
597
- json?: unknown;
598
- html?: string;
599
- };
600
- return {
601
- bodyJson: JSON.stringify(envelope.json ?? {}),
602
- bodyHtml: envelope.html ?? "",
603
- summary: m.summary ?? "",
604
- mediaId: m.id,
605
- };
606
- }
607
- } catch {
608
- // Fetch failed — skip
609
- }
610
- return {
611
- bodyJson: "{}",
612
- bodyHtml: "",
613
- summary: m.summary ?? "",
614
- mediaId: m.id,
615
- };
616
- },
617
- ),
618
- );
856
+ this._format = post.format;
857
+ this._slug = post.slug ?? "";
858
+ this._slugTaken = false;
859
+ this._slugCheckLoading = false;
860
+ this._suggestedSlug = "";
861
+ this._suggestedSlugLoading = false;
862
+ this._slugSuggestionKey = "";
863
+ this._publishedAtInput = post.publishedAt
864
+ ? toLocalDateInputValue(post.publishedAt)
865
+ : "";
866
+ this._publishedAtTimeMinutes = post.publishedAt
867
+ ? getTimestampTimeMinutes(post.publishedAt)
868
+ : null;
869
+ this._originalPublishedAt = post.publishedAt ?? null;
870
+ this._initialPublishedAtTimeMinutes = this._publishedAtTimeMinutes;
871
+ this._initialPublishedAtInput = this._publishedAtInput;
872
+ this._initialSlug = this._slug.trim();
873
+ this._visibility = post.visibility ?? "public";
874
+ this._visibilityLocked = Boolean(post.replyToId);
875
+
876
+ if (post.replyToId) {
877
+ this._replyToId = post.replyToId;
878
+ await this._fetchReplyContext(post.replyToId);
879
+ if (requestId !== this._openEditRequestId) return;
880
+ }
619
881
 
620
- this._editor?.populate({
621
- format: post.format,
622
- title: post.format === "quote" ? undefined : (post.title ?? undefined),
623
- bodyJson: post.body ?? undefined,
624
- url:
625
- post.format === "quote"
626
- ? (post.sourceUrl ?? undefined)
627
- : (post.url ?? undefined),
628
- quoteText: post.quoteText ?? undefined,
629
- quoteAuthor:
630
- post.format === "quote" ? (post.sourceName ?? undefined) : undefined,
631
- rating: post.rating ?? undefined,
632
- media: nonTextMedia.map(
633
- (m: {
634
- id: string;
635
- previewUrl: string;
636
- alt?: string;
637
- mimeType: string;
638
- }) => ({
639
- id: m.id,
640
- previewUrl: m.previewUrl,
641
- alt: m.alt,
642
- mimeType: m.mimeType,
643
- }),
644
- ),
645
- textAttachments,
646
- attachmentOrder: allMedia.map((m: { id: string }) => m.id),
647
- });
882
+ if (post.collectionIds?.length) {
883
+ this._collectionIds = post.collectionIds;
884
+ }
648
885
 
649
- this.closest("dialog")?.showModal();
650
- globalThis.requestAnimationFrame(() => {
651
- this._editor?.focusInput();
652
- this._captureInitialSnapshot();
653
- });
886
+ const allAttachments = post.attachments ?? [];
887
+ const { media, textAttachments, attachmentOrder } =
888
+ await resolveApiAttachments(allAttachments);
889
+ if (requestId !== this._openEditRequestId) return;
890
+
891
+ this._openingEdit = false;
892
+ await this.updateComplete;
893
+ if (requestId !== this._openEditRequestId) return;
894
+
895
+ // Check for a local edit draft (unsaved changes from a previous session)
896
+ const restored = this._restoreEditDraftIfAvailable(id);
897
+
898
+ if (!restored) {
899
+ this._editor?.populate({
900
+ format: post.format,
901
+ title:
902
+ post.format === "quote" ? undefined : (post.title ?? undefined),
903
+ bodyJson: post.body ?? undefined,
904
+ url:
905
+ post.format === "quote"
906
+ ? (post.sourceUrl ?? undefined)
907
+ : (post.url ?? undefined),
908
+ quoteText: post.quoteText ?? undefined,
909
+ quoteAuthor:
910
+ post.format === "quote"
911
+ ? (post.sourceName ?? undefined)
912
+ : undefined,
913
+ rating: post.rating ?? undefined,
914
+ media,
915
+ textAttachments,
916
+ attachmentOrder,
917
+ });
918
+ }
919
+
920
+ globalThis.requestAnimationFrame(() => {
921
+ if (requestId !== this._openEditRequestId) return;
922
+ this._focusDialogShell();
923
+ this._captureInitialSnapshot();
924
+ });
925
+ } catch {
926
+ if (requestId !== this._openEditRequestId) return;
927
+ this._openingEdit = false;
928
+ this._closeDialog();
929
+ this.reset();
930
+ showToast(this.labels.loadPostFailed, "error");
931
+ }
654
932
  }
655
933
 
656
934
  async openNew(options?: ComposeOpenOptions) {
@@ -660,6 +938,10 @@ export class JantComposeDialog extends LitElement {
660
938
  await this.restoreLocalDraft();
661
939
  }
662
940
 
941
+ if (options?.initialFormat) {
942
+ this._format = options.initialFormat;
943
+ }
944
+
663
945
  if (
664
946
  options?.collectionId &&
665
947
  !this._collectionIds.includes(options.collectionId)
@@ -667,6 +949,17 @@ export class JantComposeDialog extends LitElement {
667
949
  this._collectionIds = [options.collectionId, ...this._collectionIds];
668
950
  }
669
951
 
952
+ // Restore per-collection visibility preference (only for new posts, not restored drafts)
953
+ if (options?.collectionId) {
954
+ this._sourceCollectionId = options.collectionId;
955
+ const saved = JantComposeDialog._getCollectionVisibility(
956
+ options.collectionId,
957
+ );
958
+ if (saved) {
959
+ this._visibility = saved;
960
+ }
961
+ }
962
+
670
963
  this.closest("dialog")?.showModal();
671
964
  await this.updateComplete;
672
965
  this._editor?.focusInput();
@@ -689,6 +982,7 @@ export class JantComposeDialog extends LitElement {
689
982
  kind: "timeline-item" | "post-card" | "post-view";
690
983
  id: string;
691
984
  },
985
+ options?: ComposeReplyOpenOptions,
692
986
  ) {
693
987
  this.reset();
694
988
  this._replyToId = id;
@@ -696,7 +990,12 @@ export class JantComposeDialog extends LitElement {
696
990
  this._replyRefreshKind = refreshTarget?.kind ?? null;
697
991
  this._replyRefreshId = refreshTarget?.id ?? null;
698
992
  this._replyToData = replyData ?? null;
699
- this._format = "note";
993
+ this._visibilityLocked = true;
994
+ this._format = options?.initialFormat ?? "note";
995
+
996
+ if (options?.restoreDraft !== false) {
997
+ await this.restoreLocalDraft({ expectedReplyToId: id });
998
+ }
700
999
 
701
1000
  this.closest("dialog")?.showModal();
702
1001
  await this.updateComplete;
@@ -722,9 +1021,20 @@ export class JantComposeDialog extends LitElement {
722
1021
  day: "numeric",
723
1022
  })
724
1023
  : "";
1024
+ const media: ReplyToMedia[] = (post.attachments ?? [])
1025
+ .filter((a): a is ApiMediaAttachment => a.type === "media")
1026
+ .map((m) => ({
1027
+ url: m.url ?? m.previewUrl,
1028
+ previewUrl: m.previewUrl,
1029
+ alt: m.alt,
1030
+ mimeType: m.mimeType,
1031
+ width: m.width,
1032
+ height: m.height,
1033
+ }));
725
1034
  this._replyToData = {
726
1035
  contentHtml: (post.bodyHtml as string) ?? "",
727
1036
  dateText,
1037
+ media: media.length > 0 ? media : undefined,
728
1038
  };
729
1039
  } catch {
730
1040
  // Parent unavailable — reply mode still works, just no preview
@@ -779,6 +1089,22 @@ export class JantComposeDialog extends LitElement {
779
1089
  }
780
1090
 
781
1091
  private _hasContent(): boolean {
1092
+ if (this._threadItems.length > 0) {
1093
+ // Thread mode: check the first editor for content
1094
+ const firstEditor = this.querySelector<JantComposeEditor>(
1095
+ "jant-compose-editor",
1096
+ );
1097
+ if (!firstEditor) return false;
1098
+ const data = firstEditor.getData();
1099
+ return (
1100
+ !!data.body ||
1101
+ !!data.title.trim() ||
1102
+ !!data.url.trim() ||
1103
+ !!data.quoteText.trim() ||
1104
+ data.attachments.length > 0
1105
+ );
1106
+ }
1107
+
782
1108
  const editor = this._editor;
783
1109
  if (!editor) return false;
784
1110
 
@@ -813,6 +1139,8 @@ export class JantComposeDialog extends LitElement {
813
1139
  format: this._format,
814
1140
  collectionIds: [...this._collectionIds],
815
1141
  slug: this._slug,
1142
+ publishedAtInput: this._publishedAtInput,
1143
+ publishedAtTimeMinutes: this._publishedAtTimeMinutes,
816
1144
  visibility: this._visibility,
817
1145
  title: editorData.title,
818
1146
  bodyJson: editor.getNormalizedBodyJson(),
@@ -869,8 +1197,7 @@ export class JantComposeDialog extends LitElement {
869
1197
 
870
1198
  // Dismiss any open dropdowns first
871
1199
  if (this._showCollection) {
872
- this._showCollection = false;
873
- this._collectionSearch = "";
1200
+ this._closeCollectionPicker();
874
1201
  }
875
1202
  if (this._showPublishPanel) {
876
1203
  this._showPublishPanel = false;
@@ -941,17 +1268,19 @@ export class JantComposeDialog extends LitElement {
941
1268
  this._confirmForAttachedText = false;
942
1269
  this._doneAttachedPanel();
943
1270
  } else if (this._confirmForDrafts) {
944
- this._dispatchSubmit("draft");
945
1271
  this._confirmPanelOpen = false;
946
- this.reset();
947
- this._openDraftsPanel();
1272
+ if (!this._editor?.hasPendingInlineImageUploads()) {
1273
+ this._finishDraftSaveAndOpenDrafts();
1274
+ } else {
1275
+ void this._saveDraftAndOpenDrafts();
1276
+ }
948
1277
  } else if (this._editPostId) {
949
1278
  // Editing a published post — publish the update directly
950
1279
  this._confirmPanelOpen = false;
951
- this._submit("published");
1280
+ void this._submit("published");
952
1281
  } else {
953
1282
  this._confirmPanelOpen = false;
954
- this._submit("draft");
1283
+ void this._submit("draft");
955
1284
  }
956
1285
  }
957
1286
 
@@ -973,6 +1302,75 @@ export class JantComposeDialog extends LitElement {
973
1302
  }
974
1303
  }
975
1304
 
1305
+ /** Build the submit payload for a single thread editor (index-aware). */
1306
+ private _buildEditorPostDetail(
1307
+ editor: JantComposeEditor,
1308
+ format: ComposeFormat,
1309
+ index: number,
1310
+ status: "published" | "draft",
1311
+ ): ComposeSubmitDetail {
1312
+ const editorData = editor.getData();
1313
+ const mediaAttachments = new Map(
1314
+ (editorData.attachments ?? []).map((a) => [a.clientId, a]),
1315
+ );
1316
+ const textAttachments = new Map(
1317
+ editorData.attachedTexts.map((t) => [t.clientId, t]),
1318
+ );
1319
+ const orderedAttachments: ComposeSubmitAttachment[] = [];
1320
+ for (const clientId of editorData.attachmentOrder) {
1321
+ const media = mediaAttachments.get(clientId);
1322
+ if (media) {
1323
+ orderedAttachments.push({
1324
+ type: "media",
1325
+ clientId,
1326
+ mediaId: media.mediaId,
1327
+ alt: media.alt || undefined,
1328
+ });
1329
+ continue;
1330
+ }
1331
+ const text = textAttachments.get(clientId);
1332
+ if (text?.bodyJson) {
1333
+ orderedAttachments.push({
1334
+ type: "text",
1335
+ clientId,
1336
+ bodyJson: text.bodyJson,
1337
+ summary: text.summary,
1338
+ mediaId: text.mediaId,
1339
+ originalBodyJson: normalizeComposeDoc(text.originalBodyJson ?? null),
1340
+ });
1341
+ }
1342
+ }
1343
+ // Only root post (index 0) carries shared publish settings
1344
+ const isRoot = index === 0;
1345
+ return {
1346
+ format,
1347
+ title: editorData.title,
1348
+ body: editorData.body,
1349
+ url: editorData.url,
1350
+ quoteText: editorData.quoteText,
1351
+ quoteAuthor: editorData.quoteAuthor,
1352
+ status,
1353
+ slug: isRoot ? this._slug.trim() || undefined : undefined,
1354
+ publishedAt: isRoot ? this._getPublishedAtSubmitValue(status) : undefined,
1355
+ visibility: isRoot
1356
+ ? this._visibilityLocked
1357
+ ? undefined
1358
+ : this._visibility
1359
+ : undefined,
1360
+ rating: editorData.rating,
1361
+ collectionIds: isRoot ? [...this._collectionIds] : [],
1362
+ attachments: orderedAttachments,
1363
+ replyToId: isRoot ? (this._replyToId ?? undefined) : undefined,
1364
+ replyThreadRootId: isRoot
1365
+ ? (this._replyThreadRootId ?? undefined)
1366
+ : undefined,
1367
+ replyRefreshKind: isRoot
1368
+ ? (this._replyRefreshKind ?? undefined)
1369
+ : undefined,
1370
+ replyRefreshId: isRoot ? (this._replyRefreshId ?? undefined) : undefined,
1371
+ };
1372
+ }
1373
+
976
1374
  private _buildSubmitDetail(
977
1375
  status: "published" | "draft",
978
1376
  ): ComposeSubmitDetail | null {
@@ -1025,6 +1423,7 @@ export class JantComposeDialog extends LitElement {
1025
1423
  quoteText: editorData.quoteText,
1026
1424
  quoteAuthor: editorData.quoteAuthor,
1027
1425
  slug: this._slug.trim() || undefined,
1426
+ publishedAt: this._getPublishedAtSubmitValue(status),
1028
1427
  status,
1029
1428
  visibility: this._visibilityLocked ? undefined : this._visibility,
1030
1429
  rating: editorData.rating,
@@ -1032,18 +1431,55 @@ export class JantComposeDialog extends LitElement {
1032
1431
  attachments: orderedAttachments,
1033
1432
  editPostId: this._editPostId ?? this._draftSourceId ?? undefined,
1034
1433
  replyToId: this._replyToId ?? undefined,
1434
+ quietReply: this._quietReply || undefined,
1035
1435
  replyThreadRootId: this._replyThreadRootId ?? undefined,
1036
1436
  replyRefreshKind: this._replyRefreshKind ?? undefined,
1037
1437
  replyRefreshId: this._replyRefreshId ?? undefined,
1038
1438
  };
1039
1439
  }
1040
1440
 
1041
- private _focusBlockedSubmitField(): boolean {
1441
+ private _focusBlockedSubmitField(status: "published" | "draft"): boolean {
1442
+ if (
1443
+ status === "published" &&
1444
+ this._getPublishedAtValidationMessage() !== null
1445
+ ) {
1446
+ this._revealPublishedAtField();
1447
+ return true;
1448
+ }
1449
+
1042
1450
  if (this._getSlugValidationMessage()) {
1043
1451
  this._revealSlugField();
1044
1452
  return true;
1045
1453
  }
1046
1454
 
1455
+ // ── Thread mode: validate each editor against its own format ──────
1456
+ if (this._threadItems.length > 0) {
1457
+ const editors = Array.from(
1458
+ this.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
1459
+ );
1460
+ for (let i = 0; i < this._threadItems.length; i++) {
1461
+ const item = this._threadItems[i];
1462
+ const editor = editors[i];
1463
+ if (!editor) continue;
1464
+ if (editor.getUrlValidationMessage()) {
1465
+ editor.revealUrlValidation();
1466
+ editor.focusUrlInput("end");
1467
+ return true;
1468
+ }
1469
+ if (editor.getLinkTitleValidationMessage()) {
1470
+ editor.revealLinkTitleValidation();
1471
+ editor.focusLinkTitleInput("end");
1472
+ return true;
1473
+ }
1474
+ if (item.format === "quote" && !editor._quoteText.trim()) {
1475
+ editor.focusInput("end");
1476
+ return true;
1477
+ }
1478
+ }
1479
+ return false;
1480
+ }
1481
+
1482
+ // ── Single-post mode ─────────────────────────────────────────────
1047
1483
  const editor = this._editor;
1048
1484
  if (!editor) return false;
1049
1485
 
@@ -1069,17 +1505,65 @@ export class JantComposeDialog extends LitElement {
1069
1505
 
1070
1506
  private _dispatchSubmit(status: "published" | "draft"): boolean {
1071
1507
  if (this._loading) return false;
1508
+ if (this._focusBlockedSubmitField(status)) return false;
1509
+ if (!this._draftSourceId) {
1510
+ this._cancelDraftSaveTimer();
1511
+ this._saveDraftToStorage();
1512
+ }
1513
+
1514
+ // ── Thread mode ────────────────────────────────────────────────────
1515
+ if (this._threadItems.length > 0) {
1516
+ if (this._threadItems.length > MAX_THREAD_POSTS) {
1517
+ showToast(this._getThreadLimitMessage(), "error");
1518
+ return false;
1519
+ }
1520
+
1521
+ const editors = Array.from(
1522
+ this.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
1523
+ );
1524
+ if (editors.length !== this._threadItems.length) return false;
1525
+
1526
+ const threadPosts: ComposeSubmitDetail[] = [];
1527
+ const allPending: ComposeAttachment[] = [];
1528
+
1529
+ for (let i = 0; i < this._threadItems.length; i++) {
1530
+ const item = this._threadItems[i];
1531
+ const editor = editors[i];
1532
+ if (!editor) return false;
1533
+ threadPosts.push(
1534
+ this._buildEditorPostDetail(editor, item.format, i, status),
1535
+ );
1536
+ allPending.push(
1537
+ ...(editor._attachments ?? []).filter(
1538
+ (a) =>
1539
+ a.status === "pending" ||
1540
+ a.status === "processing" ||
1541
+ a.status === "uploading",
1542
+ ),
1543
+ );
1544
+ }
1545
+ this.dispatchEvent(
1546
+ new CustomEvent("jant:compose-submit-deferred", {
1547
+ bubbles: true,
1548
+ detail: {
1549
+ ...threadPosts[0],
1550
+ editPostId: this._editPostId ?? this._draftSourceId ?? undefined,
1551
+ threadPosts,
1552
+ pendingAttachments: allPending,
1553
+ },
1554
+ }),
1555
+ );
1556
+ return true;
1557
+ }
1558
+
1559
+ // ── Single-post mode ───────────────────────────────────────────────
1072
1560
  const editor = this._editor;
1073
1561
  if (!editor) return false;
1074
- if (this._focusBlockedSubmitField()) {
1075
- return false;
1076
- }
1077
1562
 
1078
1563
  const detail = this._buildSubmitDetail(status);
1079
1564
  if (!detail) return false;
1080
1565
 
1081
- const attachments = editor._attachments ?? [];
1082
- const pendingAttachments = attachments.filter(
1566
+ const pendingAttachments = (editor._attachments ?? []).filter(
1083
1567
  (a) =>
1084
1568
  a.status === "pending" ||
1085
1569
  a.status === "processing" ||
@@ -1095,9 +1579,30 @@ export class JantComposeDialog extends LitElement {
1095
1579
  return true;
1096
1580
  }
1097
1581
 
1098
- private _submit(status: "published" | "draft") {
1099
- this._showPublishPanel = false;
1100
- this._clearDraftFromStorage();
1582
+ private _saveDraftAndOpenDrafts() {
1583
+ this._finishDraftSaveAndOpenDrafts();
1584
+ }
1585
+
1586
+ private _finishDraftSaveAndOpenDrafts() {
1587
+ if (!this._dispatchSubmit("draft")) return;
1588
+ this.reset();
1589
+ // The bridge writes the draft asynchronously via fetch after we dispatch
1590
+ // the submit event, so fetching the drafts list immediately would miss
1591
+ // the new draft. Wait for the bridge's completion event before loading.
1592
+ const onComplete = () => {
1593
+ clearTimeout(fallbackTimer);
1594
+ if (!this.isConnected) return;
1595
+ void this._openDraftsPanel();
1596
+ };
1597
+ const fallbackTimer = globalThis.setTimeout(() => {
1598
+ document.removeEventListener("jant:compose-submit-complete", onComplete);
1599
+ }, 10_000);
1600
+ document.addEventListener("jant:compose-submit-complete", onComplete, {
1601
+ once: true,
1602
+ });
1603
+ }
1604
+
1605
+ private _finishSubmit(status: "published" | "draft") {
1101
1606
  if (!this._dispatchSubmit(status)) return;
1102
1607
  if (this.pageMode) {
1103
1608
  this._loading = true;
@@ -1107,6 +1612,11 @@ export class JantComposeDialog extends LitElement {
1107
1612
  this.reset();
1108
1613
  }
1109
1614
 
1615
+ private _submit(status: "published" | "draft") {
1616
+ this._showPublishPanel = false;
1617
+ this._finishSubmit(status);
1618
+ }
1619
+
1110
1620
  private _toggleCollection(id: string) {
1111
1621
  if (this._collectionIds.includes(id)) {
1112
1622
  this._collectionIds = this._collectionIds.filter((cid) => cid !== id);
@@ -1120,13 +1630,303 @@ export class JantComposeDialog extends LitElement {
1120
1630
  const first = collections.find((c) => c.id === ids[0]);
1121
1631
  if (!first) return "";
1122
1632
  if (ids.length === 1) return first.title;
1633
+ if (ids.length === 2) {
1634
+ const second = collections.find((c) => c.id === ids[1]);
1635
+ return second ? `${first.title}, ${second.title}` : first.title;
1636
+ }
1123
1637
  return this.labels.collectionCountLabel
1124
1638
  .replace("%name%", first.title)
1125
1639
  .replace("%count%", String(ids.length - 1));
1126
1640
  }
1127
1641
 
1128
- private _cancelSlugTimers() {
1129
- if (this._slugCheckTimer !== null) {
1642
+ private _prepareCollectionPickerOrder() {
1643
+ this._collectionPickerOrder = getSelectedFirstOrder(
1644
+ this.collections ?? [],
1645
+ this._collectionIds,
1646
+ );
1647
+ }
1648
+
1649
+ private _focusCollectionPickerInitialTarget() {
1650
+ this.querySelector<HTMLElement>(
1651
+ ".compose-collection-search-input, .compose-collection-option, .compose-collection-add-action",
1652
+ )?.focus();
1653
+ }
1654
+
1655
+ private _focusCollectionSearchInput() {
1656
+ const searchInput = this.querySelector<HTMLInputElement>(
1657
+ ".compose-collection-search-input",
1658
+ );
1659
+ if (!searchInput) return false;
1660
+
1661
+ searchInput.focus();
1662
+ const cursor = searchInput.value.length;
1663
+ searchInput.setSelectionRange(cursor, cursor);
1664
+ return true;
1665
+ }
1666
+
1667
+ private _closeCollectionPicker(options?: {
1668
+ restoreFocus?: "trigger" | "editor";
1669
+ }) {
1670
+ this._showCollection = false;
1671
+ this._collectionSearch = "";
1672
+ this._suppressedCollectionOptionId = null;
1673
+ this._suppressCollectionOptionClickUntil = 0;
1674
+
1675
+ if (options?.restoreFocus === "trigger") {
1676
+ this.updateComplete.then(() => {
1677
+ this.querySelector<HTMLElement>(".compose-collection-trigger")?.focus();
1678
+ });
1679
+ return;
1680
+ }
1681
+
1682
+ if (options?.restoreFocus === "editor") {
1683
+ this._restorePageEditorFocus();
1684
+ }
1685
+ }
1686
+
1687
+ private _suppressNextCollectionOptionClick(collectionId: string) {
1688
+ this._suppressedCollectionOptionId = collectionId;
1689
+ this._suppressCollectionOptionClickUntil = Date.now() + 250;
1690
+ }
1691
+
1692
+ private _isTouchViewport() {
1693
+ return (
1694
+ globalThis.matchMedia?.("(hover: none) and (pointer: coarse)")?.matches ??
1695
+ false
1696
+ );
1697
+ }
1698
+
1699
+ private _shouldAutofocusCollectionPicker() {
1700
+ return !this._isTouchViewport();
1701
+ }
1702
+
1703
+ private _shouldAutofocusFormatInput() {
1704
+ return !this._isTouchViewport();
1705
+ }
1706
+
1707
+ private _scheduleCollectionPickerAutofocus() {
1708
+ if (!this._shouldAutofocusCollectionPicker()) return;
1709
+
1710
+ globalThis.requestAnimationFrame(() => {
1711
+ if (!this._showCollection) return;
1712
+ this._focusCollectionPickerInitialTarget();
1713
+ });
1714
+ }
1715
+
1716
+ private _scheduleCollectionSearchFocus() {
1717
+ globalThis.requestAnimationFrame(() => {
1718
+ if (!this._showCollection) return;
1719
+ if (!this._focusCollectionSearchInput()) {
1720
+ this._focusCollectionPickerInitialTarget();
1721
+ }
1722
+ });
1723
+ }
1724
+
1725
+ private _isPrintableCollectionSearchKey(key: string) {
1726
+ return key.length === 1 && key.trim().length > 0;
1727
+ }
1728
+
1729
+ private _handleCollectionTriggerKeydown = (
1730
+ event: globalThis.KeyboardEvent,
1731
+ ) => {
1732
+ if (
1733
+ event.defaultPrevented ||
1734
+ event.isComposing ||
1735
+ event.altKey ||
1736
+ event.ctrlKey ||
1737
+ event.metaKey
1738
+ ) {
1739
+ return;
1740
+ }
1741
+
1742
+ if (event.key === "ArrowDown") {
1743
+ event.preventDefault();
1744
+ this._scheduleCollectionSearchFocus();
1745
+ return;
1746
+ }
1747
+
1748
+ if (
1749
+ !this._showCollection ||
1750
+ !this._isPrintableCollectionSearchKey(event.key)
1751
+ ) {
1752
+ return;
1753
+ }
1754
+
1755
+ event.preventDefault();
1756
+ this._collectionSearch += event.key;
1757
+ this._scheduleCollectionSearchFocus();
1758
+ };
1759
+
1760
+ private _getCollectionOptionElements() {
1761
+ return Array.from(
1762
+ this.querySelectorAll<HTMLButtonElement>(".compose-collection-option"),
1763
+ );
1764
+ }
1765
+
1766
+ private _handleCollectionSearchKeydown = (
1767
+ event: globalThis.KeyboardEvent,
1768
+ ) => {
1769
+ if (
1770
+ event.defaultPrevented ||
1771
+ event.isComposing ||
1772
+ event.altKey ||
1773
+ event.ctrlKey ||
1774
+ event.metaKey
1775
+ ) {
1776
+ return;
1777
+ }
1778
+
1779
+ if (event.key === "Enter") {
1780
+ event.preventDefault();
1781
+ this._closeCollectionPicker({ restoreFocus: "trigger" });
1782
+ return;
1783
+ }
1784
+
1785
+ if (event.key !== "ArrowDown") {
1786
+ return;
1787
+ }
1788
+
1789
+ const [firstOption] = this._getCollectionOptionElements();
1790
+ const addAction = this.querySelector<HTMLButtonElement>(
1791
+ ".compose-collection-add-action",
1792
+ );
1793
+ const nextTarget = firstOption ?? addAction;
1794
+ if (!nextTarget) return;
1795
+
1796
+ event.preventDefault();
1797
+ nextTarget.focus();
1798
+ };
1799
+
1800
+ private _handleCollectionOptionKeydown = (
1801
+ event: globalThis.KeyboardEvent,
1802
+ collectionId: string,
1803
+ ) => {
1804
+ if (
1805
+ event.defaultPrevented ||
1806
+ event.isComposing ||
1807
+ event.altKey ||
1808
+ event.ctrlKey ||
1809
+ event.metaKey
1810
+ ) {
1811
+ return;
1812
+ }
1813
+
1814
+ const options = this._getCollectionOptionElements();
1815
+ const currentTarget = event.currentTarget as HTMLButtonElement | null;
1816
+ const currentIndex = currentTarget ? options.indexOf(currentTarget) : -1;
1817
+
1818
+ if (event.key === "ArrowDown") {
1819
+ const addAction = this.querySelector<HTMLButtonElement>(
1820
+ ".compose-collection-add-action",
1821
+ );
1822
+ const nextTarget =
1823
+ currentIndex >= 0
1824
+ ? (options[currentIndex + 1] ?? addAction)
1825
+ : options[0];
1826
+ if (!nextTarget) return;
1827
+
1828
+ event.preventDefault();
1829
+ nextTarget.focus();
1830
+ return;
1831
+ }
1832
+
1833
+ if (event.key === "ArrowUp") {
1834
+ const searchInput = this.querySelector<HTMLInputElement>(
1835
+ ".compose-collection-search-input",
1836
+ );
1837
+ const previousTarget =
1838
+ currentIndex > 0 ? options[currentIndex - 1] : searchInput;
1839
+ if (!previousTarget) return;
1840
+
1841
+ event.preventDefault();
1842
+ previousTarget.focus();
1843
+ return;
1844
+ }
1845
+
1846
+ if (event.key === " " || event.key === "Spacebar") {
1847
+ event.preventDefault();
1848
+ this._suppressNextCollectionOptionClick(collectionId);
1849
+ this._toggleCollection(collectionId);
1850
+ return;
1851
+ }
1852
+
1853
+ if (event.key === "Enter") {
1854
+ event.preventDefault();
1855
+ this._suppressNextCollectionOptionClick(collectionId);
1856
+ this._closeCollectionPicker({ restoreFocus: "trigger" });
1857
+ }
1858
+ };
1859
+
1860
+ private _handleCollectionOptionClick = (collectionId: string) => {
1861
+ if (
1862
+ this._suppressedCollectionOptionId === collectionId &&
1863
+ Date.now() <= this._suppressCollectionOptionClickUntil
1864
+ ) {
1865
+ this._suppressedCollectionOptionId = null;
1866
+ this._suppressCollectionOptionClickUntil = 0;
1867
+ return;
1868
+ }
1869
+
1870
+ this._suppressedCollectionOptionId = null;
1871
+ this._suppressCollectionOptionClickUntil = 0;
1872
+ this._toggleCollection(collectionId);
1873
+ };
1874
+
1875
+ private _handleCollectionAddActionKeydown = (
1876
+ event: globalThis.KeyboardEvent,
1877
+ ) => {
1878
+ if (
1879
+ event.defaultPrevented ||
1880
+ event.isComposing ||
1881
+ event.altKey ||
1882
+ event.ctrlKey ||
1883
+ event.metaKey ||
1884
+ event.key !== "ArrowUp"
1885
+ ) {
1886
+ return;
1887
+ }
1888
+
1889
+ const options = this._getCollectionOptionElements();
1890
+ const searchInput = this.querySelector<HTMLInputElement>(
1891
+ ".compose-collection-search-input",
1892
+ );
1893
+ const previousTarget = options.at(-1) ?? searchInput;
1894
+ if (!previousTarget) return;
1895
+
1896
+ event.preventDefault();
1897
+ previousTarget.focus();
1898
+ };
1899
+
1900
+ private _updateCollectionPopoverSide() {
1901
+ const trigger = this.querySelector<HTMLElement>(
1902
+ ".compose-collection-trigger",
1903
+ );
1904
+ const popover = this.querySelector<HTMLElement>(
1905
+ ".compose-collection-popover[data-popover]",
1906
+ );
1907
+ if (!trigger || !popover) return;
1908
+
1909
+ const visualViewport = globalThis.visualViewport;
1910
+ const viewportTop = visualViewport?.offsetTop ?? 0;
1911
+ const viewportBottom =
1912
+ viewportTop + (visualViewport?.height ?? globalThis.innerHeight);
1913
+ const triggerRect = trigger.getBoundingClientRect();
1914
+ const edgePadding = 12;
1915
+ const gap = 4;
1916
+ const availableBelow = Math.max(
1917
+ 0,
1918
+ viewportBottom - edgePadding - triggerRect.bottom - gap,
1919
+ );
1920
+ const availableAbove = Math.max(
1921
+ 0,
1922
+ triggerRect.top - viewportTop - edgePadding - gap,
1923
+ );
1924
+
1925
+ popover.dataset.side = availableBelow >= availableAbove ? "bottom" : "top";
1926
+ }
1927
+
1928
+ private _cancelSlugTimers() {
1929
+ if (this._slugCheckTimer !== null) {
1130
1930
  clearTimeout(this._slugCheckTimer);
1131
1931
  this._slugCheckTimer = null;
1132
1932
  }
@@ -1310,6 +2110,7 @@ export class JantComposeDialog extends LitElement {
1310
2110
 
1311
2111
  connectedCallback() {
1312
2112
  super.connectedCallback();
2113
+ this._syncPublishPanelPresentation();
1313
2114
  this.addEventListener("keydown", this._handleKeydown);
1314
2115
  this.addEventListener("jant:alt-panel-open", this._handleAltPanelOpen);
1315
2116
  this.addEventListener("jant:alt-panel-close", this._handleAltPanelClose);
@@ -1339,11 +2140,24 @@ export class JantComposeDialog extends LitElement {
1339
2140
 
1340
2141
  // Flush pending draft save before page unload (covers refresh/close mid-debounce)
1341
2142
  window.addEventListener("beforeunload", this._onBeforeUnload);
2143
+ window.addEventListener("resize", this._handleViewportChange);
2144
+ window.addEventListener("scroll", this._handleViewportChange, {
2145
+ passive: true,
2146
+ });
2147
+ globalThis.visualViewport?.addEventListener(
2148
+ "resize",
2149
+ this._handleViewportChange,
2150
+ );
2151
+ globalThis.visualViewport?.addEventListener(
2152
+ "scroll",
2153
+ this._handleViewportChange,
2154
+ );
1342
2155
 
1343
2156
  // Intercept native dialog cancel (ESC) to route through requestClose
1344
2157
  this._dialogEl = this.closest("dialog");
1345
2158
  if (this._dialogEl) {
1346
2159
  this._dialogEl.addEventListener("cancel", this._handleDialogCancel);
2160
+ this._dialogEl.addEventListener("mousedown", this._handleDialogMousedown);
1347
2161
  this._dialogEl.addEventListener("click", this._handleDialogClick);
1348
2162
  }
1349
2163
 
@@ -1383,12 +2197,26 @@ export class JantComposeDialog extends LitElement {
1383
2197
  this._handleFullscreenClose as EventListener,
1384
2198
  );
1385
2199
  window.removeEventListener("beforeunload", this._onBeforeUnload);
2200
+ window.removeEventListener("resize", this._handleViewportChange);
2201
+ window.removeEventListener("scroll", this._handleViewportChange);
2202
+ globalThis.visualViewport?.removeEventListener(
2203
+ "resize",
2204
+ this._handleViewportChange,
2205
+ );
2206
+ globalThis.visualViewport?.removeEventListener(
2207
+ "scroll",
2208
+ this._handleViewportChange,
2209
+ );
1386
2210
  this._cancelSlugTimers();
1387
2211
  this._destroyAttachedEditor();
1388
2212
  this._cancelDraftSaveTimer();
1389
2213
 
1390
2214
  if (this._dialogEl) {
1391
2215
  this._dialogEl.removeEventListener("cancel", this._handleDialogCancel);
2216
+ this._dialogEl.removeEventListener(
2217
+ "mousedown",
2218
+ this._handleDialogMousedown,
2219
+ );
1392
2220
  this._dialogEl.removeEventListener("click", this._handleDialogClick);
1393
2221
  this._dialogEl = null;
1394
2222
  }
@@ -1410,6 +2238,25 @@ export class JantComposeDialog extends LitElement {
1410
2238
  this._clearFilePickerEscapeState();
1411
2239
  };
1412
2240
 
2241
+ private _syncPublishPanelPresentation() {
2242
+ const nextValue =
2243
+ globalThis.matchMedia?.(COMPOSE_PUBLISH_PANEL_FULLSCREEN_QUERY)
2244
+ ?.matches ?? false;
2245
+ if (nextValue !== this._publishPanelFullscreen) {
2246
+ this._publishPanelFullscreen = nextValue;
2247
+ }
2248
+ }
2249
+
2250
+ private _handleViewportChange = () => {
2251
+ this._syncPublishPanelPresentation();
2252
+ if (this._showPublishPanel) {
2253
+ this.updateComplete.then(() => this._updatePublishPanelLayout());
2254
+ }
2255
+ if (this._showCollection) {
2256
+ this.updateComplete.then(() => this._updateCollectionPopoverSide());
2257
+ }
2258
+ };
2259
+
1413
2260
  private _handleDialogCancel = (e: Event) => {
1414
2261
  e.preventDefault();
1415
2262
  if (this._shouldIgnoreEscapeClose()) return;
@@ -1417,8 +2264,19 @@ export class JantComposeDialog extends LitElement {
1417
2264
  this.requestClose();
1418
2265
  };
1419
2266
 
2267
+ private _handleDialogMousedown = (e: Event) => {
2268
+ // Track whether the mousedown originated on the backdrop (the <dialog>
2269
+ // itself). When the user drag-selects text inside the editor and the
2270
+ // pointer overshoots to the backdrop, the subsequent click event fires
2271
+ // with target === dialog. Without this guard, that click triggers
2272
+ // requestClose() and the unsaved-changes confirmation pops up.
2273
+ this._mousedownOnBackdrop = e.target === this._dialogEl;
2274
+ };
2275
+
1420
2276
  private _handleDialogClick = (e: Event) => {
1421
2277
  if (!this._dialogEl || e.target !== this._dialogEl) return;
2278
+ // Only treat as backdrop click when mousedown also started on the backdrop
2279
+ if (!this._mousedownOnBackdrop) return;
1422
2280
 
1423
2281
  const mouseEvent = e as MouseEvent;
1424
2282
  const hitTarget = document.elementFromPoint(
@@ -1435,8 +2293,17 @@ export class JantComposeDialog extends LitElement {
1435
2293
  this.requestClose();
1436
2294
  };
1437
2295
 
1438
- private _focusPageEditorEnd() {
1439
- this.updateComplete.then(() => this._editor?.focusInput("end"));
2296
+ private _focusDialogShell() {
2297
+ const shell = this.querySelector<HTMLElement>(".compose-dialog-inner");
2298
+ if (shell) {
2299
+ shell.focus();
2300
+ return;
2301
+ }
2302
+ this._dialogEl?.focus();
2303
+ }
2304
+
2305
+ private _restorePageEditorFocus() {
2306
+ this.updateComplete.then(() => this._editor?.focusInput());
1440
2307
  }
1441
2308
 
1442
2309
  private _dismissEscapeOverlay(): boolean {
@@ -1456,21 +2323,18 @@ export class JantComposeDialog extends LitElement {
1456
2323
  }
1457
2324
 
1458
2325
  if (this._showCollection) {
1459
- this._showCollection = false;
1460
- this._collectionSearch = "";
1461
- this._focusPageEditorEnd();
2326
+ this._closeCollectionPicker({ restoreFocus: "editor" });
1462
2327
  return true;
1463
2328
  }
1464
2329
 
1465
2330
  if (this._showPublishPanel) {
1466
- this._showPublishPanel = false;
1467
- this._focusPageEditorEnd();
2331
+ this._closePublishPanel(true);
1468
2332
  return true;
1469
2333
  }
1470
2334
 
1471
2335
  if (this._altPanelOpen) {
1472
2336
  this._closeAltPanel();
1473
- this._focusPageEditorEnd();
2337
+ this._restorePageEditorFocus();
1474
2338
  return true;
1475
2339
  }
1476
2340
 
@@ -1513,10 +2377,32 @@ export class JantComposeDialog extends LitElement {
1513
2377
  return;
1514
2378
  }
1515
2379
  if (!this._canPublish()) {
1516
- this._focusBlockedSubmitField();
2380
+ this._focusBlockedSubmitField("published");
1517
2381
  return;
1518
2382
  }
1519
- this._submit("published");
2383
+ void this._submit("published");
2384
+ } else if (
2385
+ (ke.metaKey || ke.ctrlKey) &&
2386
+ !ke.altKey &&
2387
+ !ke.shiftKey &&
2388
+ ke.key >= "1" &&
2389
+ ke.key <= String(JantComposeDialog._FORMATS.length)
2390
+ ) {
2391
+ ke.preventDefault();
2392
+ const target = JantComposeDialog._FORMATS[Number(ke.key) - 1];
2393
+ if (this._threadItems.length > 0) {
2394
+ const editor = this.querySelectorAll<JantComposeEditor>(
2395
+ "jant-compose-editor",
2396
+ )[this._focusedThreadIndex];
2397
+ editor?.dispatchEvent(
2398
+ new CustomEvent("jant:thread-format-change", {
2399
+ detail: { format: target },
2400
+ bubbles: true,
2401
+ }),
2402
+ );
2403
+ } else {
2404
+ this._switchFormat(target);
2405
+ }
1520
2406
  }
1521
2407
  };
1522
2408
 
@@ -1555,8 +2441,12 @@ export class JantComposeDialog extends LitElement {
1555
2441
  e.detail.json as import("@tiptap/core").JSONContent,
1556
2442
  e.detail.title,
1557
2443
  e.detail.showTitle,
2444
+ e.detail.selection,
1558
2445
  );
1559
- this.updateComplete.then(() => editor.focusInput("end"));
2446
+ // Adopt any in-flight inline image uploads from the fullscreen editor
2447
+ // so blob: placeholder URLs get replaced when uploads complete.
2448
+ editor.adoptPendingUploads();
2449
+ this.updateComplete.then(() => editor.focusSelection(e.detail.selection));
1560
2450
  }
1561
2451
  this._replyExpanded = e.detail.replyExpanded;
1562
2452
  };
@@ -1680,7 +2570,7 @@ export class JantComposeDialog extends LitElement {
1680
2570
  | DraftsResponse
1681
2571
  | Record<string, unknown>[];
1682
2572
  const posts = Array.isArray(json) ? json : (json.posts ?? []);
1683
- this._drafts = (posts as Record<string, unknown>[]).map(
2573
+ const allDraftItems = (posts as Record<string, unknown>[]).map(
1684
2574
  (p): DraftItem => ({
1685
2575
  id: p.id as string,
1686
2576
  format: p.format as ComposeFormat,
@@ -1698,15 +2588,23 @@ export class JantComposeDialog extends LitElement {
1698
2588
  replyToId: (p.replyToId as string) ?? null,
1699
2589
  updatedAt: p.updatedAt as number,
1700
2590
  mediaAttachments: (
1701
- (p.mediaAttachments as DraftItem["mediaAttachments"]) ?? []
1702
- ).map((m) => ({
1703
- id: m.id,
1704
- previewUrl: m.previewUrl,
1705
- alt: m.alt,
1706
- mimeType: m.mimeType,
1707
- })),
2591
+ (p.attachments as ApiAttachment[] | undefined) ?? []
2592
+ )
2593
+ .filter((a): a is ApiMediaAttachment => a.type === "media")
2594
+ .map((m) => ({
2595
+ id: m.id,
2596
+ previewUrl: m.previewUrl,
2597
+ alt: m.alt ?? null,
2598
+ mimeType: m.mimeType,
2599
+ })),
1708
2600
  }),
1709
2601
  );
2602
+ // Filter out thread reply drafts: posts whose replyToId points to another
2603
+ // draft in the same list are inner thread posts — only the root should appear.
2604
+ const draftIds = new Set(allDraftItems.map((d) => d.id));
2605
+ this._drafts = allDraftItems.filter(
2606
+ (d) => !d.replyToId || !draftIds.has(d.replyToId),
2607
+ );
1710
2608
  } catch {
1711
2609
  this._draftsError = "Could not load drafts. Try again.";
1712
2610
  this._drafts = [];
@@ -1721,6 +2619,36 @@ export class JantComposeDialog extends LitElement {
1721
2619
  this.updateComplete.then(() => this._editor?.focusInput());
1722
2620
  }
1723
2621
 
2622
+ /**
2623
+ * Resolve text attachments for a post's media list and call editor.populate().
2624
+ * Shared between single-post and thread draft loading.
2625
+ */
2626
+ private async _populateEditorFromPost(
2627
+ editor: JantComposeEditor,
2628
+ post: ComposePostResponse,
2629
+ ) {
2630
+ const allAttachments = post.attachments ?? [];
2631
+ const { media, textAttachments, attachmentOrder } =
2632
+ await resolveApiAttachments(allAttachments);
2633
+
2634
+ editor.populate({
2635
+ format: post.format,
2636
+ title: post.format === "quote" ? undefined : (post.title ?? undefined),
2637
+ bodyJson: post.body ?? undefined,
2638
+ url:
2639
+ post.format === "quote"
2640
+ ? (post.sourceUrl ?? undefined)
2641
+ : (post.url ?? undefined),
2642
+ quoteText: post.quoteText ?? undefined,
2643
+ quoteAuthor:
2644
+ post.format === "quote" ? (post.sourceName ?? undefined) : undefined,
2645
+ rating: post.rating ?? undefined,
2646
+ media,
2647
+ textAttachments,
2648
+ attachmentOrder,
2649
+ });
2650
+ }
2651
+
1724
2652
  private async _loadDraft(id: string) {
1725
2653
  this._draftsPanelOpen = false;
1726
2654
  this._draftMenuOpenId = null;
@@ -1738,6 +2666,16 @@ export class JantComposeDialog extends LitElement {
1738
2666
  this._suggestedSlug = "";
1739
2667
  this._suggestedSlugLoading = false;
1740
2668
  this._slugSuggestionKey = "";
2669
+ this._publishedAtInput = post.publishedAt
2670
+ ? toLocalDateInputValue(post.publishedAt)
2671
+ : "";
2672
+ this._publishedAtTimeMinutes = post.publishedAt
2673
+ ? getTimestampTimeMinutes(post.publishedAt)
2674
+ : null;
2675
+ this._originalPublishedAt = post.publishedAt ?? null;
2676
+ this._initialPublishedAtTimeMinutes = this._publishedAtTimeMinutes;
2677
+ this._initialPublishedAtInput = this._publishedAtInput;
2678
+ this._initialSlug = this._slug.trim();
1741
2679
  this._visibility = post.visibility ?? "public";
1742
2680
  this._visibilityLocked = Boolean(post.replyToId);
1743
2681
 
@@ -1745,83 +2683,128 @@ export class JantComposeDialog extends LitElement {
1745
2683
  this._collectionIds = post.collectionIds;
1746
2684
  }
1747
2685
 
1748
- // Restore reply context if this draft was a reply
2686
+ // Restore reply context if this draft was a reply to a published post
1749
2687
  if (post.replyToId) {
1750
2688
  this._replyToId = post.replyToId;
1751
2689
  await this._fetchReplyContext(post.replyToId);
1752
2690
  }
1753
2691
 
1754
- await this.updateComplete;
2692
+ // ── Thread draft: check if this root has other draft posts in its thread ──
2693
+ const isThreadRoot =
2694
+ post.threadId === post.id || post.threadId === undefined;
2695
+ if (isThreadRoot) {
2696
+ // Fetch all drafts to find other posts in this thread
2697
+ try {
2698
+ const draftsRes = await fetch("/api/posts?status=draft&limit=50");
2699
+ if (draftsRes.ok) {
2700
+ const draftsJson = (await draftsRes.json()) as
2701
+ | { posts?: Record<string, unknown>[] }
2702
+ | Record<string, unknown>[];
2703
+ const allDrafts = Array.isArray(draftsJson)
2704
+ ? draftsJson
2705
+ : (draftsJson.posts ?? []);
2706
+ // Collect other posts in the same thread, sorted by their implied order
2707
+ // (they have replyToId chains starting from the root)
2708
+ const threadDrafts = (allDrafts as Record<string, unknown>[])
2709
+ .filter(
2710
+ (p) =>
2711
+ p.id !== post.id &&
2712
+ p.threadId === post.id &&
2713
+ p.status === "draft",
2714
+ )
2715
+ .map((p) => ({
2716
+ id: p.id as string,
2717
+ format: p.format as ComposeFormat,
2718
+ replyToId: (p.replyToId as string) ?? null,
2719
+ title: (p.title as string) ?? null,
2720
+ body: (p.body as string) ?? null,
2721
+ url: (p.url as string) ?? null,
2722
+ sourceUrl: (p.sourceUrl as string) ?? null,
2723
+ sourceName: (p.sourceName as string) ?? null,
2724
+ quoteText: (p.quoteText as string) ?? null,
2725
+ rating: (p.rating as number) ?? null,
2726
+ attachments: (p.attachments as ApiAttachment[] | undefined) ?? [],
2727
+ visibility: (p.visibility as ComposeVisibility) ?? null,
2728
+ }));
2729
+
2730
+ if (threadDrafts.length > 0) {
2731
+ // Sort by reply chain: walk replyToId to get ordered list
2732
+ const ordered: typeof threadDrafts = [];
2733
+ let prevId: string = post.id;
2734
+ for (let i = 0; i < threadDrafts.length; i++) {
2735
+ const next = threadDrafts.find((p) => p.replyToId === prevId);
2736
+ if (!next) break;
2737
+ ordered.push(next);
2738
+ prevId = next.id;
2739
+ }
2740
+ // Any remaining posts not in chain (shouldn't happen, but be safe)
2741
+ for (const p of threadDrafts) {
2742
+ if (!ordered.includes(p)) ordered.push(p);
2743
+ }
1755
2744
 
1756
- // Separate text media items from other media attachments
1757
- const allMedia = post.mediaAttachments ?? [];
1758
- const nonTextMedia = allMedia.filter(
1759
- (m: { mimeType: string }) => !m.mimeType.startsWith("text/"),
1760
- );
1761
- const textMedia = allMedia.filter(
1762
- (m: { mimeType: string }) => m.mimeType === "text/x-tiptap+json",
1763
- );
2745
+ // Enter thread mode
2746
+ this._threadItems = [
2747
+ { id: crypto.randomUUID(), format: post.format },
2748
+ ...ordered.map((p) => ({
2749
+ id: crypto.randomUUID(),
2750
+ format: p.format,
2751
+ })),
2752
+ ];
2753
+ this._focusedThreadIndex = 0;
2754
+
2755
+ await this.updateComplete;
2756
+
2757
+ const editors = Array.from(
2758
+ this.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
2759
+ );
2760
+
2761
+ // Populate root editor
2762
+ const rootEditor = editors[0];
2763
+ if (rootEditor) {
2764
+ await this._populateEditorFromPost(rootEditor, post);
2765
+ }
1764
2766
 
1765
- // Fetch text content for TipTap text media items (stored as { json, html } envelope)
1766
- const textAttachments = await Promise.all(
1767
- textMedia.map(
1768
- async (m: { id: string; url?: string; summary?: string }) => {
1769
- try {
1770
- const textRes = await fetch(`/api/media/${m.id}/content`);
1771
- if (textRes.ok) {
1772
- const raw = await textRes.text();
1773
- const envelope = JSON.parse(raw) as {
1774
- json?: unknown;
1775
- html?: string;
1776
- };
1777
- return {
1778
- bodyJson: JSON.stringify(envelope.json ?? {}),
1779
- bodyHtml: envelope.html ?? "",
1780
- summary: m.summary ?? "",
1781
- mediaId: m.id,
1782
- };
2767
+ // Populate reply editors
2768
+ for (let i = 0; i < ordered.length; i++) {
2769
+ const replyEditor = editors[i + 1];
2770
+ if (!replyEditor) continue;
2771
+ const p = ordered[i];
2772
+ await this._populateEditorFromPost(replyEditor, {
2773
+ id: p.id,
2774
+ threadId: post.id,
2775
+ format: p.format,
2776
+ replyToId: p.replyToId,
2777
+ title: p.title,
2778
+ body: p.body,
2779
+ url: p.url,
2780
+ sourceUrl: p.sourceUrl,
2781
+ sourceName: p.sourceName,
2782
+ quoteText: p.quoteText,
2783
+ rating: p.rating,
2784
+ attachments: p.attachments,
2785
+ visibility: p.visibility,
2786
+ });
1783
2787
  }
1784
- } catch {
1785
- // Fetch failed — skip
2788
+
2789
+ globalThis.requestAnimationFrame(() => {
2790
+ editors[0]?.focusInput();
2791
+ this._captureInitialSnapshot();
2792
+ });
2793
+ return;
1786
2794
  }
1787
- return {
1788
- bodyJson: "{}",
1789
- bodyHtml: "",
1790
- summary: m.summary ?? "",
1791
- mediaId: m.id,
1792
- };
1793
- },
1794
- ),
1795
- );
2795
+ }
2796
+ } catch {
2797
+ // Fall through to single-post load if thread fetch fails
2798
+ }
2799
+ }
1796
2800
 
1797
- this._editor?.populate({
1798
- format: post.format,
1799
- title: post.format === "quote" ? undefined : (post.title ?? undefined),
1800
- bodyJson: post.body ?? undefined,
1801
- url:
1802
- post.format === "quote"
1803
- ? (post.sourceUrl ?? undefined)
1804
- : (post.url ?? undefined),
1805
- quoteText: post.quoteText ?? undefined,
1806
- quoteAuthor:
1807
- post.format === "quote" ? (post.sourceName ?? undefined) : undefined,
1808
- rating: post.rating ?? undefined,
1809
- media: nonTextMedia.map(
1810
- (m: {
1811
- id: string;
1812
- previewUrl: string;
1813
- alt?: string;
1814
- mimeType: string;
1815
- }) => ({
1816
- id: m.id,
1817
- previewUrl: m.previewUrl,
1818
- alt: m.alt,
1819
- mimeType: m.mimeType,
1820
- }),
1821
- ),
1822
- textAttachments,
1823
- attachmentOrder: allMedia.map((m: { id: string }) => m.id),
1824
- });
2801
+ // ── Single-post draft ─────────────────────────────────────────────
2802
+ await this.updateComplete;
2803
+
2804
+ const editor = this._editor;
2805
+ if (editor) {
2806
+ await this._populateEditorFromPost(editor, post);
2807
+ }
1825
2808
 
1826
2809
  globalThis.requestAnimationFrame(() => {
1827
2810
  this._editor?.focusInput();
@@ -1862,18 +2845,33 @@ export class JantComposeDialog extends LitElement {
1862
2845
  return null;
1863
2846
  }
1864
2847
 
2848
+ private _getThreadLimitMessage(): string {
2849
+ return (
2850
+ this.labels.threadLimitReached ||
2851
+ `Threads can include up to ${MAX_THREAD_POSTS} posts.`
2852
+ );
2853
+ }
2854
+
1865
2855
  // ── Local draft auto-save (globalThis.localStorage) ──────────────────────────
1866
2856
 
1867
2857
  private static _DRAFT_KEY = "jant:compose-draft";
2858
+ private static _EDIT_DRAFT_KEY_PREFIX = "jant:compose-edit:";
1868
2859
  private static _DRAFT_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
1869
2860
 
2861
+ private _currentDraftStorageKey(): string {
2862
+ if (this._editPostId) {
2863
+ return JantComposeDialog._EDIT_DRAFT_KEY_PREFIX + this._editPostId;
2864
+ }
2865
+ return JantComposeDialog._DRAFT_KEY;
2866
+ }
2867
+
1870
2868
  private _onContentChanged = () => {
1871
2869
  this.requestUpdate();
1872
2870
  if (!this._hasManualSlug()) {
1873
2871
  this._scheduleSuggestedSlugRefresh();
1874
2872
  }
1875
- // Schedule localStorage auto-save for new-post mode only
1876
- if (!this._editPostId && !this._draftSourceId) {
2873
+ // Schedule localStorage auto-save (new-post and edit modes, not draft-load)
2874
+ if (!this._draftSourceId) {
1877
2875
  this._scheduleDraftSave();
1878
2876
  }
1879
2877
  };
@@ -1910,6 +2908,86 @@ export class JantComposeDialog extends LitElement {
1910
2908
  };
1911
2909
 
1912
2910
  private _saveDraftToStorage() {
2911
+ // ── Thread mode ────────────────────────────────────────────────────
2912
+ if (this._threadItems.length > 0) {
2913
+ const editors = Array.from(
2914
+ this.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
2915
+ );
2916
+ const hasContent = editors.some((editor) => {
2917
+ const data = editor.getData();
2918
+ return (
2919
+ !!data.body ||
2920
+ !!data.title.trim() ||
2921
+ !!data.url.trim() ||
2922
+ !!data.quoteText.trim() ||
2923
+ data.rating > 0 ||
2924
+ data.attachedTexts.length > 0 ||
2925
+ data.attachments.length > 0
2926
+ );
2927
+ });
2928
+
2929
+ if (!hasContent) {
2930
+ globalThis.localStorage.removeItem(this._currentDraftStorageKey());
2931
+ return;
2932
+ }
2933
+
2934
+ const threadItems = this._threadItems
2935
+ .map((item, i) => {
2936
+ const editor = editors[i];
2937
+ if (!editor) return null;
2938
+ const data = editor.getData();
2939
+ return {
2940
+ format: item.format,
2941
+ title: data.title,
2942
+ bodyJson: editor.getNormalizedBodyJson(),
2943
+ url: data.url,
2944
+ quoteText: data.quoteText,
2945
+ quoteAuthor: data.quoteAuthor,
2946
+ attachedTexts: data.attachedTexts.map((t) => ({
2947
+ clientId: t.clientId,
2948
+ bodyJson: t.bodyJson,
2949
+ bodyHtml: t.bodyHtml,
2950
+ summary: t.summary,
2951
+ })),
2952
+ attachmentOrder: [...data.attachmentOrder],
2953
+ };
2954
+ })
2955
+ .filter((item): item is NonNullable<typeof item> => item !== null);
2956
+
2957
+ const draft: LocalDraft = {
2958
+ format: this._threadItems[0]?.format ?? this._format,
2959
+ title: "",
2960
+ bodyJson: null,
2961
+ url: "",
2962
+ quoteText: "",
2963
+ quoteAuthor: "",
2964
+ slug: this._slug,
2965
+ publishedAtInput: this._publishedAtInput,
2966
+ publishedAtTimeMinutes: this._publishedAtTimeMinutes,
2967
+ visibility: this._visibility,
2968
+ rating: 0,
2969
+ showTitle: false,
2970
+ showRating: false,
2971
+ collectionIds: [...this._collectionIds],
2972
+ replyToId: this._replyToId,
2973
+ attachedTexts: [],
2974
+ attachmentOrder: [],
2975
+ threadItems,
2976
+ savedAt: Date.now(),
2977
+ };
2978
+
2979
+ try {
2980
+ globalThis.localStorage.setItem(
2981
+ this._currentDraftStorageKey(),
2982
+ JSON.stringify(draft),
2983
+ );
2984
+ } catch {
2985
+ // Storage full or unavailable — silently ignore
2986
+ }
2987
+ return;
2988
+ }
2989
+
2990
+ // ── Single-post mode ───────────────────────────────────────────────
1913
2991
  const editor = this._editor;
1914
2992
  if (!editor) return;
1915
2993
 
@@ -1936,6 +3014,8 @@ export class JantComposeDialog extends LitElement {
1936
3014
  quoteText: data.quoteText,
1937
3015
  quoteAuthor: data.quoteAuthor,
1938
3016
  slug: this._slug,
3017
+ publishedAtInput: this._publishedAtInput,
3018
+ publishedAtTimeMinutes: this._publishedAtTimeMinutes,
1939
3019
  visibility: this._visibility,
1940
3020
  rating: data.rating,
1941
3021
  showTitle:
@@ -1967,10 +3047,20 @@ export class JantComposeDialog extends LitElement {
1967
3047
 
1968
3048
  private _clearDraftFromStorage() {
1969
3049
  this._cancelDraftSaveTimer();
1970
- globalThis.localStorage.removeItem(JantComposeDialog._DRAFT_KEY);
3050
+ globalThis.localStorage.removeItem(this._currentDraftStorageKey());
3051
+ }
3052
+
3053
+ clearLocalDraftFromStorage() {
3054
+ this._clearDraftFromStorage();
3055
+ }
3056
+
3057
+ clearEditDraftFromStorage(postId: string) {
3058
+ globalThis.localStorage.removeItem(
3059
+ JantComposeDialog._EDIT_DRAFT_KEY_PREFIX + postId,
3060
+ );
1971
3061
  }
1972
3062
 
1973
- async restoreLocalDraft() {
3063
+ async restoreLocalDraft(options?: { expectedReplyToId?: string }) {
1974
3064
  // Don't restore if already in edit or draft-load mode
1975
3065
  if (this._editPostId || this._draftSourceId) return;
1976
3066
  // Don't restore if the editor already has content (e.g. reopened dialog)
@@ -1998,7 +3088,13 @@ export class JantComposeDialog extends LitElement {
1998
3088
  return;
1999
3089
  }
2000
3090
 
2001
- this._format = draft.format;
3091
+ if (
3092
+ options?.expectedReplyToId !== undefined &&
3093
+ draft.replyToId !== options.expectedReplyToId
3094
+ ) {
3095
+ return;
3096
+ }
3097
+
2002
3098
  this._collectionIds = [...(draft.collectionIds ?? [])];
2003
3099
  this._slug = draft.slug ?? "";
2004
3100
  this._slugTaken = false;
@@ -2006,17 +3102,77 @@ export class JantComposeDialog extends LitElement {
2006
3102
  this._suggestedSlug = "";
2007
3103
  this._suggestedSlugLoading = false;
2008
3104
  this._slugSuggestionKey = "";
3105
+ this._publishedAtInput = draft.publishedAtInput ?? "";
3106
+ this._publishedAtTimeMinutes = draft.publishedAtTimeMinutes ?? null;
2009
3107
  this._visibility = draft.visibility ?? "public";
2010
3108
 
2011
3109
  // Restore reply context if this draft was a reply
2012
3110
  if (draft.replyToId) {
2013
3111
  this._replyToId = draft.replyToId;
3112
+ this._visibilityLocked = true;
2014
3113
  await this._fetchReplyContext(draft.replyToId);
2015
3114
  }
2016
3115
 
2017
- await this.updateComplete;
3116
+ // ── Thread draft restore ─────────────────────────────────────────
3117
+ if (draft.threadItems && draft.threadItems.length >= 2) {
3118
+ this._format = draft.threadItems[0].format;
3119
+ this._threadItems = draft.threadItems.map((item) => ({
3120
+ id: crypto.randomUUID(),
3121
+ format: item.format,
3122
+ }));
3123
+ this._focusedThreadIndex = 0;
2018
3124
 
2019
- const textAttachments = draft.attachedTexts?.flatMap((t) => {
3125
+ await this.updateComplete;
3126
+
3127
+ const editors = Array.from(
3128
+ this.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
3129
+ );
3130
+ for (let i = 0; i < draft.threadItems.length; i++) {
3131
+ const item = draft.threadItems[i];
3132
+ const editor = editors[i];
3133
+ if (!editor) continue;
3134
+
3135
+ const textAttachments = item.attachedTexts?.flatMap((t) => {
3136
+ const bodyJson = normalizeComposeDoc(t.bodyJson);
3137
+ if (!bodyJson) return [];
3138
+ return [
3139
+ {
3140
+ clientId: t.clientId,
3141
+ bodyJson: JSON.stringify(bodyJson),
3142
+ bodyHtml: t.bodyHtml,
3143
+ summary: t.summary,
3144
+ },
3145
+ ];
3146
+ });
3147
+
3148
+ editor.populate({
3149
+ format: item.format,
3150
+ title: item.title || undefined,
3151
+ bodyJson: item.bodyJson ? JSON.stringify(item.bodyJson) : undefined,
3152
+ url: item.url || undefined,
3153
+ quoteText: item.quoteText || undefined,
3154
+ quoteAuthor: item.quoteAuthor || undefined,
3155
+ textAttachments: textAttachments?.length
3156
+ ? textAttachments
3157
+ : undefined,
3158
+ attachmentOrder: item.attachmentOrder,
3159
+ });
3160
+ }
3161
+
3162
+ this._draftRestored = true;
3163
+ showToast(this.labels.draftRestored);
3164
+ globalThis.requestAnimationFrame(() => {
3165
+ this._captureInitialSnapshot();
3166
+ });
3167
+ return;
3168
+ }
3169
+
3170
+ // ── Single-post draft restore ────────────────────────────────────
3171
+ this._format = draft.format;
3172
+
3173
+ await this.updateComplete;
3174
+
3175
+ const textAttachments = draft.attachedTexts?.flatMap((t) => {
2020
3176
  const bodyJson = normalizeComposeDoc(t.bodyJson);
2021
3177
  if (!bodyJson) return [];
2022
3178
  return [
@@ -2050,6 +3206,75 @@ export class JantComposeDialog extends LitElement {
2050
3206
  });
2051
3207
  }
2052
3208
 
3209
+ /**
3210
+ * Check for a local edit draft for the given post ID and restore it if valid.
3211
+ * Returns true if a draft was restored, false otherwise.
3212
+ */
3213
+ private _restoreEditDraftIfAvailable(postId: string): boolean {
3214
+ const key = JantComposeDialog._EDIT_DRAFT_KEY_PREFIX + postId;
3215
+
3216
+ let raw: string | null;
3217
+ try {
3218
+ raw = globalThis.localStorage.getItem(key);
3219
+ } catch {
3220
+ return false;
3221
+ }
3222
+ if (!raw) return false;
3223
+
3224
+ let draft: LocalDraft;
3225
+ try {
3226
+ draft = JSON.parse(raw) as LocalDraft;
3227
+ } catch {
3228
+ globalThis.localStorage.removeItem(key);
3229
+ return false;
3230
+ }
3231
+
3232
+ if (Date.now() - draft.savedAt > JantComposeDialog._DRAFT_MAX_AGE) {
3233
+ globalThis.localStorage.removeItem(key);
3234
+ return false;
3235
+ }
3236
+
3237
+ // Restore metadata
3238
+ this._format = draft.format;
3239
+ this._collectionIds = [...(draft.collectionIds ?? [])];
3240
+ this._slug = draft.slug ?? "";
3241
+ this._publishedAtInput = draft.publishedAtInput ?? "";
3242
+ this._publishedAtTimeMinutes = draft.publishedAtTimeMinutes ?? null;
3243
+ this._visibility = draft.visibility ?? "public";
3244
+
3245
+ // Restore editor content
3246
+ const textAttachments = draft.attachedTexts?.flatMap((t) => {
3247
+ const bodyJson = normalizeComposeDoc(t.bodyJson);
3248
+ if (!bodyJson) return [];
3249
+ return [
3250
+ {
3251
+ clientId: t.clientId,
3252
+ bodyJson: JSON.stringify(bodyJson),
3253
+ bodyHtml: t.bodyHtml,
3254
+ summary: t.summary,
3255
+ },
3256
+ ];
3257
+ });
3258
+
3259
+ this._editor?.populate({
3260
+ format: draft.format,
3261
+ title: draft.title || undefined,
3262
+ bodyJson: draft.bodyJson ? JSON.stringify(draft.bodyJson) : undefined,
3263
+ url: draft.url || undefined,
3264
+ quoteText: draft.quoteText || undefined,
3265
+ quoteAuthor: draft.quoteAuthor || undefined,
3266
+ rating: draft.rating || undefined,
3267
+ showTitle: draft.showTitle,
3268
+ showRating: draft.showRating,
3269
+ textAttachments: textAttachments?.length ? textAttachments : undefined,
3270
+ attachmentOrder: draft.attachmentOrder,
3271
+ });
3272
+
3273
+ this._draftRestored = true;
3274
+ showToast(this.labels.draftRestored);
3275
+ return true;
3276
+ }
3277
+
2053
3278
  private async _focusPageEditorOnMount() {
2054
3279
  if (this._pageFocusApplied) return;
2055
3280
 
@@ -2194,7 +3419,7 @@ export class JantComposeDialog extends LitElement {
2194
3419
  private _renderReplyContext() {
2195
3420
  if (!this._replyToId || !this._replyToData) return nothing;
2196
3421
 
2197
- const { contentHtml, dateText } = this._replyToData;
3422
+ const { contentHtml, dateText, media } = this._replyToData;
2198
3423
  const isExpanded = this._replyExpanded;
2199
3424
 
2200
3425
  return html`
@@ -2208,6 +3433,38 @@ export class JantComposeDialog extends LitElement {
2208
3433
  >
2209
3434
  <div class="compose-reply-context-body">
2210
3435
  ${unsafeHTML(contentHtml)}
3436
+ ${media?.length
3437
+ ? html`<div
3438
+ class="compose-reply-context-media"
3439
+ data-post-media
3440
+ data-lightbox-group=${JSON.stringify(
3441
+ media.map((m) => ({
3442
+ url: m.url,
3443
+ alt: m.alt ?? "",
3444
+ width: m.width,
3445
+ height: m.height,
3446
+ mimeType: m.mimeType,
3447
+ })),
3448
+ )}
3449
+ >
3450
+ ${media.map(
3451
+ (m, i) => html`
3452
+ <a
3453
+ href=${m.url}
3454
+ data-lightbox-index=${i}
3455
+ class="compose-reply-context-media-link"
3456
+ >
3457
+ <img
3458
+ src=${m.previewUrl}
3459
+ alt=${m.alt ?? ""}
3460
+ class="compose-reply-context-media-img"
3461
+ loading="lazy"
3462
+ />
3463
+ </a>
3464
+ `,
3465
+ )}
3466
+ </div>`
3467
+ : nothing}
2211
3468
  </div>
2212
3469
  ${!isExpanded
2213
3470
  ? html`<div class="compose-reply-fade"></div>`
@@ -2229,10 +3486,22 @@ export class JantComposeDialog extends LitElement {
2229
3486
  `;
2230
3487
  }
2231
3488
 
3489
+ private static readonly _FORMATS: ComposeFormat[] = ["note", "link", "quote"];
3490
+
3491
+ private _switchFormat(target: ComposeFormat) {
3492
+ if (this._format === target) return;
3493
+ if (this._editPostId) return;
3494
+ this._format = target;
3495
+ this._showPublishPanel = false;
3496
+ if (this._shouldAutofocusFormatInput()) {
3497
+ globalThis.requestAnimationFrame(() => this._editor?.focusInput());
3498
+ }
3499
+ }
3500
+
2232
3501
  // ── Render helpers ────────────────────────────────────────────────
2233
3502
 
2234
3503
  private _renderHeader() {
2235
- const formats: ComposeFormat[] = ["note", "link", "quote"];
3504
+ const formats = JantComposeDialog._FORMATS;
2236
3505
  const formatLabels: Record<ComposeFormat, string> = {
2237
3506
  note: this.labels.note,
2238
3507
  link: this.labels.link,
@@ -2257,37 +3526,35 @@ export class JantComposeDialog extends LitElement {
2257
3526
  ? html`<span class="compose-dialog-title"
2258
3527
  >${this.labels.editPost}</span
2259
3528
  >`
2260
- : html`
2261
- <div class="compose-segmented">
2262
- <div
2263
- class=${classMap({
2264
- "compose-format-pill": true,
2265
- "compose-format-pill-link": this._format === "link",
2266
- "compose-format-pill-quote": this._format === "quote",
2267
- })}
2268
- ></div>
2269
- ${formats.map(
2270
- (f) => html`
2271
- <button
2272
- type="button"
2273
- class=${classMap({
2274
- "compose-segmented-item": true,
2275
- "compose-segmented-item-active": this._format === f,
2276
- })}
2277
- @click=${() => {
2278
- this._format = f;
2279
- this._showPublishPanel = false;
2280
- globalThis.requestAnimationFrame(() =>
2281
- this._editor?.focusInput(),
2282
- );
2283
- }}
2284
- >
2285
- ${formatLabels[f]}
2286
- </button>
2287
- `,
2288
- )}
2289
- </div>
2290
- `}
3529
+ : this._threadItems.length > 0
3530
+ ? html`<span class="compose-dialog-title"
3531
+ >${this.labels.newThread}</span
3532
+ >`
3533
+ : html`
3534
+ <div class="compose-segmented">
3535
+ <div
3536
+ class=${classMap({
3537
+ "compose-format-pill": true,
3538
+ "compose-format-pill-link": this._format === "link",
3539
+ "compose-format-pill-quote": this._format === "quote",
3540
+ })}
3541
+ ></div>
3542
+ ${formats.map(
3543
+ (f) => html`
3544
+ <button
3545
+ type="button"
3546
+ class=${classMap({
3547
+ "compose-segmented-item": true,
3548
+ "compose-segmented-item-active": this._format === f,
3549
+ })}
3550
+ @click=${() => this._switchFormat(f)}
3551
+ >
3552
+ ${formatLabels[f]}
3553
+ </button>
3554
+ `,
3555
+ )}
3556
+ </div>
3557
+ `}
2291
3558
  </div>
2292
3559
 
2293
3560
  <div class="compose-dialog-header-actions">
@@ -2310,16 +3577,21 @@ export class JantComposeDialog extends LitElement {
2310
3577
 
2311
3578
  private _renderCollectionSelector() {
2312
3579
  const collections = this.collections ?? [];
2313
- const search = this._collectionSearch.toLowerCase();
2314
- const filtered = search
2315
- ? collections.filter((c) => c.title.toLowerCase().includes(search))
2316
- : collections;
3580
+ const orderedCollections = applyItemOrder(
3581
+ collections,
3582
+ this._collectionPickerOrder,
3583
+ );
3584
+ const hasSearch = this._collectionSearch.trim().length > 0;
3585
+ const filtered = filterCollectionsBySearch(
3586
+ orderedCollections,
3587
+ this._collectionSearch,
3588
+ );
2317
3589
  const selectedCount = this._collectionIds.length;
2318
3590
  const selectedLabel =
2319
3591
  selectedCount > 0
2320
3592
  ? this._selectedCollectionLabel(collections)
2321
3593
  : this.labels.collection;
2322
- const emptyLabel = search
3594
+ const emptyLabel = hasSearch
2323
3595
  ? this.labels.noCollections
2324
3596
  : this.labels.emptyCollections;
2325
3597
 
@@ -2328,13 +3600,14 @@ export class JantComposeDialog extends LitElement {
2328
3600
  ${this._showCollection
2329
3601
  ? html`<div
2330
3602
  class="compose-dropdown-backdrop"
2331
- @click=${() => {
2332
- this._showCollection = false;
2333
- this._collectionSearch = "";
2334
- }}
3603
+ @click=${() => this._closeCollectionPicker()}
2335
3604
  ></div>`
2336
3605
  : nothing}
2337
- <div class="select compose-collection-select" data-select-initialized>
3606
+ <div
3607
+ class="select compose-collection-select"
3608
+ data-select-initialized
3609
+ data-open=${this._showCollection ? "true" : nothing}
3610
+ >
2338
3611
  <button
2339
3612
  type="button"
2340
3613
  class="compose-collection-trigger"
@@ -2342,11 +3615,16 @@ export class JantComposeDialog extends LitElement {
2342
3615
  aria-expanded=${this._showCollection ? "true" : "false"}
2343
3616
  data-open=${this._showCollection ? "true" : nothing}
2344
3617
  data-selected=${selectedCount > 0 ? "true" : nothing}
3618
+ @keydown=${this._handleCollectionTriggerKeydown}
2345
3619
  @click=${() => {
3620
+ const nextOpen = !this._showCollection;
2346
3621
  this._showPublishPanel = false;
2347
- this._showCollection = !this._showCollection;
2348
- if (!this._showCollection) {
2349
- this._collectionSearch = "";
3622
+ if (nextOpen) {
3623
+ this._prepareCollectionPickerOrder();
3624
+ }
3625
+ this._showCollection = nextOpen;
3626
+ if (!nextOpen) {
3627
+ this._closeCollectionPicker();
2350
3628
  }
2351
3629
  }}
2352
3630
  >
@@ -2365,7 +3643,6 @@ export class JantComposeDialog extends LitElement {
2365
3643
  <div
2366
3644
  class="compose-collection-popover"
2367
3645
  data-popover
2368
- data-side="bottom"
2369
3646
  aria-hidden=${this._showCollection ? "false" : "true"}
2370
3647
  >
2371
3648
  ${collections.length > 0
@@ -2384,6 +3661,7 @@ export class JantComposeDialog extends LitElement {
2384
3661
  autocorrect="off"
2385
3662
  spellcheck="false"
2386
3663
  .value=${this._collectionSearch}
3664
+ @keydown=${this._handleCollectionSearchKeydown}
2387
3665
  @input=${(e: Event) => {
2388
3666
  this._collectionSearch = (
2389
3667
  e.target as HTMLInputElement
@@ -2412,7 +3690,10 @@ export class JantComposeDialog extends LitElement {
2412
3690
  role="option"
2413
3691
  data-value=${col.id}
2414
3692
  aria-selected=${selected ? "true" : "false"}
2415
- @click=${() => this._toggleCollection(col.id)}
3693
+ @keydown=${(event: globalThis.KeyboardEvent) =>
3694
+ this._handleCollectionOptionKeydown(event, col.id)}
3695
+ @click=${() =>
3696
+ this._handleCollectionOptionClick(col.id)}
2416
3697
  >
2417
3698
  <span class="compose-collection-option-label"
2418
3699
  >${col.title}</span
@@ -2463,9 +3744,9 @@ export class JantComposeDialog extends LitElement {
2463
3744
  <button
2464
3745
  type="button"
2465
3746
  class="compose-collection-add-action"
3747
+ @keydown=${this._handleCollectionAddActionKeydown}
2466
3748
  @click=${() => {
2467
- this._showCollection = false;
2468
- this._collectionSearch = "";
3749
+ this._closeCollectionPicker();
2469
3750
  this._addCollectionPanelOpen = true;
2470
3751
  }}
2471
3752
  >
@@ -2514,6 +3795,7 @@ export class JantComposeDialog extends LitElement {
2514
3795
  const created = (await res.json().catch(() => null)) as {
2515
3796
  id: string;
2516
3797
  title: string;
3798
+ slug?: string;
2517
3799
  error?: string;
2518
3800
  } | null;
2519
3801
 
@@ -2528,6 +3810,7 @@ export class JantComposeDialog extends LitElement {
2528
3810
  const newCollection: ComposeCollection = {
2529
3811
  id: created.id,
2530
3812
  title: created.title,
3813
+ slug: created.slug ?? "",
2531
3814
  };
2532
3815
 
2533
3816
  const refreshed = await this.refreshCollections();
@@ -2859,24 +4142,204 @@ export class JantComposeDialog extends LitElement {
2859
4142
  return null;
2860
4143
  }
2861
4144
 
2862
- private _revealSlugField() {
4145
+ private _getCurrentTimestamp(): number {
4146
+ return Math.floor(Date.now() / 1000);
4147
+ }
4148
+
4149
+ private _getPublishedAtMaxInput(): string {
4150
+ return toLocalDateInputValue(this._getCurrentTimestamp());
4151
+ }
4152
+
4153
+ private _getPublishedAtTimeMinutes(): number {
4154
+ return (
4155
+ this._publishedAtTimeMinutes ??
4156
+ getTimestampTimeMinutes(this._getCurrentTimestamp())
4157
+ );
4158
+ }
4159
+
4160
+ private _hasPublishedAtValue(): boolean {
4161
+ return this._publishedAtInput.trim().length > 0;
4162
+ }
4163
+
4164
+ private _getPublishedAtValidationMessage(): string | null {
4165
+ if (!this._hasPublishedAtValue()) return null;
4166
+
4167
+ const parsedDate = parseLocalDateInputValue(this._publishedAtInput);
4168
+ if (parsedDate === null) {
4169
+ return this.labels.publishDateInvalid;
4170
+ }
4171
+
4172
+ if (this._publishedAtInput > this._getPublishedAtMaxInput()) {
4173
+ return this.labels.publishDateFutureError;
4174
+ }
4175
+
4176
+ return null;
4177
+ }
4178
+
4179
+ private _getPublishedAtSummary(): { input: string; text: string } | null {
4180
+ if (this._editPostId || this._draftSourceId) {
4181
+ if (this._publishedAtInput === this._initialPublishedAtInput) {
4182
+ return null;
4183
+ }
4184
+ if (!this._hasPublishedAtValue()) {
4185
+ return {
4186
+ input: "",
4187
+ text: this.labels.publishDateSummaryNow,
4188
+ };
4189
+ }
4190
+ }
4191
+
4192
+ if (this._getPublishedAtValidationMessage() !== null) return null;
4193
+
4194
+ const parsedDate = parseLocalDateInputValue(this._publishedAtInput);
4195
+ if (parsedDate === null) return null;
4196
+
4197
+ return {
4198
+ input: this._publishedAtInput,
4199
+ text: new Intl.DateTimeFormat(undefined, {
4200
+ month: "short",
4201
+ day: "numeric",
4202
+ year: "numeric",
4203
+ }).format(
4204
+ new Date(parsedDate.year, parsedDate.monthIndex, parsedDate.day),
4205
+ ),
4206
+ };
4207
+ }
4208
+
4209
+ private _getSlugSummary(): ComposePublishSummaryChip | null {
4210
+ const currentSlug = this._slug.trim();
4211
+
4212
+ if (currentSlug && this._getSlugValidationMessage() !== null) {
4213
+ return null;
4214
+ }
4215
+
4216
+ if (this._editPostId || this._draftSourceId) {
4217
+ if (currentSlug === this._initialSlug) {
4218
+ return null;
4219
+ }
4220
+
4221
+ if (!currentSlug) {
4222
+ return {
4223
+ kind: "slug",
4224
+ text: this.labels.publishSlugSummaryAuto,
4225
+ actionLabel: this.labels.publishSlugSummaryAction,
4226
+ value: currentSlug,
4227
+ };
4228
+ }
4229
+ }
4230
+
4231
+ if (!currentSlug) return null;
4232
+
4233
+ return {
4234
+ kind: "slug",
4235
+ text: `/${currentSlug}`,
4236
+ actionLabel: this.labels.publishSlugSummaryAction,
4237
+ value: currentSlug,
4238
+ };
4239
+ }
4240
+
4241
+ private _getPublishSummaryChips(): ComposePublishSummaryChip[] {
4242
+ const chips: ComposePublishSummaryChip[] = [];
4243
+ const publishedAtSummary = this._getPublishedAtSummary();
4244
+ if (publishedAtSummary !== null) {
4245
+ chips.push({
4246
+ kind: "publishedAt",
4247
+ text: publishedAtSummary.text,
4248
+ actionLabel: this.labels.publishDateSummaryAction,
4249
+ value: publishedAtSummary.input,
4250
+ });
4251
+ }
4252
+
4253
+ const slugSummary = this._getSlugSummary();
4254
+ if (slugSummary !== null) {
4255
+ chips.push(slugSummary);
4256
+ }
4257
+
4258
+ return chips;
4259
+ }
4260
+
4261
+ private _getPublishedAtSubmitValue(
4262
+ status: "published" | "draft",
4263
+ ): number | undefined {
4264
+ if (status === "draft") return undefined;
4265
+
4266
+ const publishedAt = buildTimestampFromLocalDate(
4267
+ this._publishedAtInput,
4268
+ this._getPublishedAtTimeMinutes(),
4269
+ );
4270
+ if (publishedAt !== null) {
4271
+ // If editing and the user didn't change date or time, preserve the
4272
+ // original timestamp (including seconds) to avoid silent truncation.
4273
+ if (
4274
+ this._originalPublishedAt !== null &&
4275
+ this._publishedAtInput === this._initialPublishedAtInput &&
4276
+ this._publishedAtTimeMinutes === this._initialPublishedAtTimeMinutes
4277
+ ) {
4278
+ return Math.min(this._originalPublishedAt, this._getCurrentTimestamp());
4279
+ }
4280
+ return Math.min(publishedAt, this._getCurrentTimestamp());
4281
+ }
4282
+
4283
+ if (this._publishedAtInput.trim().length > 0) {
4284
+ return undefined;
4285
+ }
4286
+
4287
+ if (this._editPostId) {
4288
+ return this._getCurrentTimestamp();
4289
+ }
4290
+
4291
+ return undefined;
4292
+ }
4293
+
4294
+ private _openPublishPanelAndFocus(selector: string) {
2863
4295
  this._showCollection = false;
2864
4296
  this._collectionSearch = "";
2865
4297
  this._showPublishPanel = true;
2866
4298
  this._confirmPanelOpen = false;
2867
4299
  this._scheduleSuggestedSlugRefresh(true);
2868
4300
  this.updateComplete.then(() => {
2869
- this.querySelector<HTMLInputElement>(
2870
- ".compose-publish-slug-input",
2871
- )?.focus();
4301
+ this.querySelector<HTMLElement>(selector)?.focus();
2872
4302
  });
2873
4303
  }
2874
4304
 
4305
+ private _revealSlugField() {
4306
+ this._openPublishPanelAndFocus(".compose-publish-slug-input");
4307
+ }
4308
+
4309
+ private _revealPublishedAtField() {
4310
+ this._openPublishPanelAndFocus(".compose-publish-date-input");
4311
+ }
4312
+
2875
4313
  private _canPublish(): boolean {
2876
4314
  if (this._loading) return false;
4315
+ if (this._getPublishedAtValidationMessage()) return false;
4316
+ if (this._getSlugValidationMessage()) return false;
4317
+
4318
+ if (this._threadItems.length > 0) {
4319
+ // Thread mode: validate all editors
4320
+ const editors = Array.from(
4321
+ this.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
4322
+ );
4323
+ if (editors.length === 0) return false;
4324
+ for (const editor of editors) {
4325
+ if (editor.getUrlValidationMessage()) return false;
4326
+ if (editor.getLinkTitleValidationMessage()) return false;
4327
+ }
4328
+ // Every editor in the thread must have content
4329
+ return editors.every((editor) => {
4330
+ const data = editor.getData();
4331
+ return (
4332
+ !!data.body ||
4333
+ !!data.title.trim() ||
4334
+ !!data.url.trim() ||
4335
+ !!data.quoteText.trim() ||
4336
+ data.attachments.length > 0
4337
+ );
4338
+ });
4339
+ }
4340
+
2877
4341
  const editor = this._editor;
2878
4342
  if (!editor) return false;
2879
- if (this._getSlugValidationMessage()) return false;
2880
4343
  if (editor.getUrlValidationMessage()) return false;
2881
4344
  if (editor.getLinkTitleValidationMessage()) return false;
2882
4345
 
@@ -2890,6 +4353,21 @@ export class JantComposeDialog extends LitElement {
2890
4353
  return this._hasContent();
2891
4354
  }
2892
4355
 
4356
+ private _focusPublishPanelInitialField() {
4357
+ const selector = this._visibilityLocked
4358
+ ? ".compose-publish-date-input"
4359
+ : ".compose-publish-option[role='radio']";
4360
+ this.querySelector<HTMLElement>(selector)?.focus();
4361
+ }
4362
+
4363
+ private _closePublishPanel(restoreFocus = false) {
4364
+ if (!this._showPublishPanel) return;
4365
+ this._showPublishPanel = false;
4366
+ if (restoreFocus) {
4367
+ this.updateComplete.then(() => this._restorePageEditorFocus());
4368
+ }
4369
+ }
4370
+
2893
4371
  private _togglePublishPanel() {
2894
4372
  this._showCollection = false;
2895
4373
  this._collectionSearch = "";
@@ -2897,6 +4375,7 @@ export class JantComposeDialog extends LitElement {
2897
4375
  this._showPublishPanel = nextOpen;
2898
4376
  if (nextOpen) {
2899
4377
  this._scheduleSuggestedSlugRefresh(true);
4378
+ this.updateComplete.then(() => this._focusPublishPanelInitialField());
2900
4379
  }
2901
4380
  }
2902
4381
 
@@ -2905,8 +4384,13 @@ export class JantComposeDialog extends LitElement {
2905
4384
  this._visibility = visibility;
2906
4385
  if (!this._editPostId && !this._draftSourceId && !this._replyToId) {
2907
4386
  JantComposeDialog._lastNewPostVisibility = visibility;
4387
+ if (this._sourceCollectionId) {
4388
+ JantComposeDialog._setCollectionVisibility(
4389
+ this._sourceCollectionId,
4390
+ visibility,
4391
+ );
4392
+ }
2908
4393
  }
2909
- this._showPublishPanel = false;
2910
4394
  }
2911
4395
 
2912
4396
  private _onSlugInput(e: Event) {
@@ -2921,6 +4405,21 @@ export class JantComposeDialog extends LitElement {
2921
4405
  this._scheduleSuggestedSlugRefresh();
2922
4406
  }
2923
4407
 
4408
+ private _onPublishedAtInput(e: Event) {
4409
+ this._publishedAtInput = (e.target as HTMLInputElement).value;
4410
+ }
4411
+
4412
+ private _resetPublishedAt() {
4413
+ const currentTimestamp = this._getCurrentTimestamp();
4414
+ this._publishedAtInput = toLocalDateInputValue(currentTimestamp);
4415
+ this._publishedAtTimeMinutes = getTimestampTimeMinutes(currentTimestamp);
4416
+ this.updateComplete.then(() => {
4417
+ this.querySelector<HTMLInputElement>(
4418
+ ".compose-publish-date-input",
4419
+ )?.focus();
4420
+ });
4421
+ }
4422
+
2924
4423
  private _renderVisibilityIcon(
2925
4424
  visibility: ComposeVisibility,
2926
4425
  variant: "menu" | "toggle" = "menu",
@@ -2939,10 +4438,9 @@ export class JantComposeDialog extends LitElement {
2939
4438
  );
2940
4439
  }
2941
4440
 
2942
- private _renderPublishVisibilityOption(
4441
+ private _renderPublishVisibilityChip(
2943
4442
  visibility: ComposeVisibility,
2944
4443
  label: string,
2945
- hint: string,
2946
4444
  ) {
2947
4445
  const selected = this._visibility === visibility;
2948
4446
 
@@ -2950,8 +4448,8 @@ export class JantComposeDialog extends LitElement {
2950
4448
  <button
2951
4449
  type="button"
2952
4450
  class=${classMap({
2953
- "compose-publish-option": true,
2954
- "compose-publish-option-selected": selected,
4451
+ "compose-publish-chip": true,
4452
+ "compose-publish-chip-selected": selected,
2955
4453
  })}
2956
4454
  role="radio"
2957
4455
  aria-checked=${selected ? "true" : "false"}
@@ -2959,20 +4457,74 @@ export class JantComposeDialog extends LitElement {
2959
4457
  @click=${() => this._setVisibility(visibility)}
2960
4458
  >
2961
4459
  ${this._renderVisibilityIcon(visibility)}
2962
- <span class="compose-publish-copy">
2963
- <span class="compose-publish-row-label">${label}</span>
2964
- <span class="compose-publish-row-hint">${hint}</span>
2965
- </span>
2966
- ${selected
2967
- ? renderComposePublishActionIcon(
2968
- COMPOSE_PUBLISH_ACTION_ICONS.check,
2969
- "compose-publish-row-check",
2970
- )
2971
- : nothing}
4460
+ <span class="compose-publish-chip-label">${label}</span>
2972
4461
  </button>
2973
4462
  `;
2974
4463
  }
2975
4464
 
4465
+ private _getVisibilityHint(): string {
4466
+ switch (this._visibility) {
4467
+ case "public":
4468
+ return this.labels.publishVisibilityPublicHint;
4469
+ case "latest_hidden":
4470
+ return this.labels.publishVisibilityHiddenFromLatestHint;
4471
+ case "private":
4472
+ return this.labels.publishVisibilityPrivateHint;
4473
+ }
4474
+ }
4475
+
4476
+ private _renderPublishDateSection() {
4477
+ const publishedAtError = this._getPublishedAtValidationMessage();
4478
+
4479
+ return html`
4480
+ <section class="compose-publish-section">
4481
+ <div class="compose-publish-section-header">
4482
+ <div class="compose-publish-section-copy">
4483
+ <p class="compose-publish-section-label">
4484
+ ${this.labels.publishDateLabel}
4485
+ </p>
4486
+ <p class="compose-publish-section-hint">
4487
+ ${this.labels.publishDateHint}
4488
+ </p>
4489
+ </div>
4490
+ ${this._hasPublishedAtValue()
4491
+ ? html`
4492
+ <button
4493
+ type="button"
4494
+ class="compose-publish-section-action"
4495
+ @click=${() => this._resetPublishedAt()}
4496
+ >
4497
+ ${this.labels.publishDateReset}
4498
+ </button>
4499
+ `
4500
+ : nothing}
4501
+ </div>
4502
+ <div class="compose-publish-date-field">
4503
+ <div class="compose-publish-date-input-wrap">
4504
+ <input
4505
+ type="date"
4506
+ class="compose-input compose-publish-date-input"
4507
+ .value=${this._publishedAtInput}
4508
+ max=${this._getPublishedAtMaxInput()}
4509
+ aria-invalid=${publishedAtError ? "true" : "false"}
4510
+ @input=${(e: Event) => this._onPublishedAtInput(e)}
4511
+ />
4512
+ </div>
4513
+ ${publishedAtError
4514
+ ? html`<p
4515
+ class=${classMap({
4516
+ "compose-publish-date-status": true,
4517
+ "compose-publish-date-status-error": true,
4518
+ })}
4519
+ >
4520
+ ${publishedAtError}
4521
+ </p>`
4522
+ : nothing}
4523
+ </div>
4524
+ </section>
4525
+ `;
4526
+ }
4527
+
2976
4528
  private _renderPublishSlugSection() {
2977
4529
  const slugError = this._getSlugValidationMessage();
2978
4530
  const slugStatus = this._getSlugStatusMessage();
@@ -3084,57 +4636,258 @@ export class JantComposeDialog extends LitElement {
3084
4636
  `;
3085
4637
  }
3086
4638
 
3087
- private _renderPublishPanel() {
3088
- if (!this._showPublishPanel) return nothing;
4639
+ private _renderQuietReplySection() {
4640
+ if (!this._replyToId) return nothing;
4641
+
4642
+ return html`
4643
+ <section class="compose-publish-section">
4644
+ <label class="compose-publish-switch-row">
4645
+ <div class="compose-publish-section-copy">
4646
+ <p class="compose-publish-section-label">
4647
+ ${this.labels.quietReplyLabel}
4648
+ </p>
4649
+ <p class="compose-publish-section-hint">
4650
+ ${this.labels.quietReplyHint}
4651
+ </p>
4652
+ </div>
4653
+ <input
4654
+ type="checkbox"
4655
+ role="switch"
4656
+ class="input"
4657
+ .checked=${this._quietReply}
4658
+ @change=${(e: Event) => {
4659
+ this._quietReply = (e.target as HTMLInputElement).checked;
4660
+ }}
4661
+ />
4662
+ </label>
4663
+ </section>
4664
+ `;
4665
+ }
4666
+
4667
+ private _renderPublishPanelSections() {
4668
+ return html`
4669
+ ${this._replyToId ? this._renderQuietReplySection() : nothing}
4670
+ ${this._visibilityLocked
4671
+ ? nothing
4672
+ : html`
4673
+ <section class="compose-publish-section">
4674
+ <div class="compose-publish-section-header">
4675
+ <div class="compose-publish-section-copy">
4676
+ <p class="compose-publish-section-label">
4677
+ ${this.labels.publishVisibilityLabel}
4678
+ </p>
4679
+ </div>
4680
+ </div>
4681
+ <div class="compose-publish-chips" role="radiogroup">
4682
+ ${this._renderPublishVisibilityChip(
4683
+ "public",
4684
+ this.labels.publishVisibilityPublic,
4685
+ )}
4686
+ ${this._renderPublishVisibilityChip(
4687
+ "latest_hidden",
4688
+ this.labels.publishVisibilityHiddenFromLatest,
4689
+ )}
4690
+ ${this._renderPublishVisibilityChip(
4691
+ "private",
4692
+ this.labels.publishVisibilityPrivate,
4693
+ )}
4694
+ </div>
4695
+ <p class="compose-publish-chip-hint">
4696
+ ${this._getVisibilityHint()}
4697
+ </p>
4698
+ </section>
4699
+ `}
4700
+ ${this._visibilityLocked && !this._replyToId
4701
+ ? nothing
4702
+ : html`<div class="compose-publish-divider" aria-hidden="true"></div>`}
4703
+ ${this._renderPublishDateSection()}
4704
+ <div class="compose-publish-divider" aria-hidden="true"></div>
4705
+ ${this._renderPublishSlugSection()}
4706
+ `;
4707
+ }
4708
+
4709
+ private _renderDesktopPublishPanel() {
4710
+ if (!this._showPublishPanel || this._publishPanelFullscreen) {
4711
+ return nothing;
4712
+ }
3089
4713
 
3090
4714
  return html`
3091
4715
  <div
3092
- class="compose-publish-panel"
4716
+ class="compose-publish-panel compose-publish-panel-desktop"
3093
4717
  role="dialog"
3094
4718
  aria-label=${this.labels.publishSettings}
4719
+ data-position="up"
3095
4720
  data-compose-publish-panel
4721
+ data-compose-publish-panel-desktop
3096
4722
  >
3097
- ${this._visibilityLocked
3098
- ? nothing
3099
- : html`
3100
- <section class="compose-publish-section">
3101
- <div class="compose-publish-section-header">
3102
- <div class="compose-publish-section-copy">
3103
- <p class="compose-publish-section-label">
3104
- ${this.labels.publishVisibilityLabel}
3105
- </p>
3106
- </div>
3107
- </div>
3108
- <div class="compose-publish-list" role="radiogroup">
3109
- ${this._renderPublishVisibilityOption(
3110
- "public",
3111
- this.labels.publishVisibilityPublic,
3112
- this.labels.publishVisibilityPublicHint,
3113
- )}
3114
- ${this._renderPublishVisibilityOption(
3115
- "latest_hidden",
3116
- this.labels.publishVisibilityHiddenFromLatest,
3117
- this.labels.publishVisibilityHiddenFromLatestHint,
3118
- )}
3119
- ${this._renderPublishVisibilityOption(
3120
- "private",
3121
- this.labels.publishVisibilityPrivate,
3122
- this.labels.publishVisibilityPrivateHint,
3123
- )}
3124
- </div>
3125
- </section>
3126
- `}
3127
- ${this._visibilityLocked
3128
- ? nothing
3129
- : html`<div
3130
- class="compose-publish-divider"
3131
- aria-hidden="true"
3132
- ></div>`}
3133
- ${this._renderPublishSlugSection()}
4723
+ ${this._renderPublishPanelSections()}
4724
+ <div class="compose-publish-panel-footer">
4725
+ <button
4726
+ type="button"
4727
+ class="compose-publish-done"
4728
+ @click=${() => this._closePublishPanel(true)}
4729
+ >
4730
+ ${this.labels.done}
4731
+ </button>
4732
+ </div>
3134
4733
  </div>
3135
4734
  `;
3136
4735
  }
3137
4736
 
4737
+ private _renderMobilePublishPanel() {
4738
+ if (!this._showPublishPanel || !this._publishPanelFullscreen) {
4739
+ return nothing;
4740
+ }
4741
+
4742
+ return html`
4743
+ <div
4744
+ class="compose-publish-panel compose-publish-panel-mobile"
4745
+ role="dialog"
4746
+ aria-label=${this.labels.publishSettings}
4747
+ aria-modal="true"
4748
+ data-compose-publish-panel
4749
+ data-compose-publish-panel-mobile
4750
+ >
4751
+ <div class="compose-alt-header compose-publish-mobile-header">
4752
+ <button
4753
+ type="button"
4754
+ class="compose-attached-panel-back"
4755
+ @click=${() => this._closePublishPanel(true)}
4756
+ >
4757
+ <svg
4758
+ class="icon-fine"
4759
+ width="16"
4760
+ height="16"
4761
+ viewBox="0 0 16 16"
4762
+ fill="none"
4763
+ stroke="currentColor"
4764
+ stroke-width="1.5"
4765
+ stroke-linecap="round"
4766
+ stroke-linejoin="round"
4767
+ >
4768
+ <path d="M11 3L6 8l5 5" />
4769
+ </svg>
4770
+ </button>
4771
+ <span class="compose-alt-title">${this.labels.publishSettings}</span>
4772
+ <button
4773
+ type="button"
4774
+ class="compose-attached-cancel compose-publish-mobile-done"
4775
+ @click=${() => this._closePublishPanel(true)}
4776
+ >
4777
+ ${this.labels.done}
4778
+ </button>
4779
+ </div>
4780
+ <div class="compose-publish-mobile-body">
4781
+ ${this._renderPublishPanelSections()}
4782
+ </div>
4783
+ </div>
4784
+ `;
4785
+ }
4786
+
4787
+ private _renderPublishSummary(summary: ComposePublishSummaryChip) {
4788
+ const isPublishedAt = summary.kind === "publishedAt";
4789
+
4790
+ return html`
4791
+ <button
4792
+ type="button"
4793
+ class="compose-publish-summary"
4794
+ data-compose-publish-summary=${summary.kind}
4795
+ data-compose-publish-date-summary=${isPublishedAt ? "" : nothing}
4796
+ data-compose-publish-slug-summary=${isPublishedAt ? nothing : ""}
4797
+ data-publish-summary-value=${summary.value}
4798
+ data-publish-date=${isPublishedAt ? summary.value : nothing}
4799
+ data-publish-slug=${isPublishedAt ? nothing : summary.value}
4800
+ aria-label=${summary.actionLabel}
4801
+ title=${summary.actionLabel}
4802
+ ?disabled=${this._loading}
4803
+ @click=${() =>
4804
+ isPublishedAt
4805
+ ? this._revealPublishedAtField()
4806
+ : this._revealSlugField()}
4807
+ >
4808
+ <span class="compose-publish-summary-icon" aria-hidden="true">
4809
+ ${isPublishedAt
4810
+ ? html`<svg
4811
+ width="14"
4812
+ height="14"
4813
+ viewBox="0 0 16 16"
4814
+ fill="none"
4815
+ stroke="currentColor"
4816
+ stroke-width="1.35"
4817
+ stroke-linecap="round"
4818
+ stroke-linejoin="round"
4819
+ >
4820
+ <rect x="2.75" y="3.45" width="10.5" height="9.8" rx="2.2" />
4821
+ <path d="M5.35 2.55v2.1" />
4822
+ <path d="M10.65 2.55v2.1" />
4823
+ <path d="M2.75 6.2h10.5" />
4824
+ </svg>`
4825
+ : html`<svg
4826
+ width="14"
4827
+ height="14"
4828
+ viewBox="0 0 16 16"
4829
+ fill="none"
4830
+ stroke="currentColor"
4831
+ stroke-width="1.35"
4832
+ stroke-linecap="round"
4833
+ stroke-linejoin="round"
4834
+ >
4835
+ <path d="M9.75 4.15h1.35a2.75 2.75 0 0 1 0 5.5H9.75" />
4836
+ <path d="M6.25 9.65H4.9a2.75 2.75 0 1 1 0-5.5h1.35" />
4837
+ <path d="M5.65 8h4.7" />
4838
+ </svg>`}
4839
+ </span>
4840
+ <span class="compose-publish-summary-text">${summary.text}</span>
4841
+ </button>
4842
+ `;
4843
+ }
4844
+
4845
+ private _renderPublishSummaries() {
4846
+ const summaries = this._getPublishSummaryChips();
4847
+ if (summaries.length === 0) return nothing;
4848
+
4849
+ return html`
4850
+ <div class="compose-publish-summaries">
4851
+ ${summaries.map((summary) => this._renderPublishSummary(summary))}
4852
+ </div>
4853
+ `;
4854
+ }
4855
+
4856
+ private _updatePublishPanelLayout() {
4857
+ if (this._publishPanelFullscreen) return;
4858
+
4859
+ const publishGroup = this.querySelector<HTMLElement>(
4860
+ ".compose-publish-group",
4861
+ );
4862
+ const panel = this.querySelector<HTMLElement>(
4863
+ "[data-compose-publish-panel-desktop]",
4864
+ );
4865
+ if (!publishGroup || !panel) return;
4866
+
4867
+ const visualViewport = globalThis.visualViewport;
4868
+ const viewportTop = visualViewport?.offsetTop ?? 0;
4869
+ const viewportBottom =
4870
+ viewportTop + (visualViewport?.height ?? globalThis.innerHeight);
4871
+ const groupRect = publishGroup.getBoundingClientRect();
4872
+ const edgePadding = 12;
4873
+ const gap = 10;
4874
+ const topBoundary = viewportTop + edgePadding;
4875
+ const bottomBoundary = viewportBottom - edgePadding;
4876
+ const availableAbove = Math.max(0, groupRect.top - topBoundary - gap);
4877
+ const availableBelow = Math.max(0, bottomBoundary - groupRect.bottom - gap);
4878
+ const direction = availableBelow >= availableAbove ? "down" : "up";
4879
+ const maxHeight = Math.max(
4880
+ 1,
4881
+ Math.floor(direction === "up" ? availableAbove : availableBelow),
4882
+ );
4883
+
4884
+ panel.dataset.position = direction;
4885
+ panel.style.setProperty(
4886
+ "--compose-publish-panel-max-height",
4887
+ `${maxHeight}px`,
4888
+ );
4889
+ }
4890
+
3138
4891
  private _renderPublishButton() {
3139
4892
  const spinner = html`<svg
3140
4893
  class="animate-spin size-4"
@@ -3152,59 +4905,405 @@ export class JantComposeDialog extends LitElement {
3152
4905
  const canPublish = this._canPublish();
3153
4906
 
3154
4907
  return html`
3155
- <div class="compose-publish-group">
3156
- ${this._showPublishPanel
3157
- ? html`<div
3158
- class="compose-dropdown-backdrop"
3159
- @click=${() => {
3160
- this._showPublishPanel = false;
3161
- }}
3162
- ></div>`
3163
- : nothing}
4908
+ <div class="compose-publish-shell">
4909
+ ${this._renderPublishSummaries()}
3164
4910
  <div
3165
- role="group"
3166
4911
  class=${classMap({
3167
- "compose-publish-buttons": true,
3168
- "compose-publish-buttons-inactive": !canPublish && !this._loading,
4912
+ "compose-publish-group": true,
4913
+ "compose-publish-group-open": this._showPublishPanel,
3169
4914
  })}
3170
4915
  >
3171
- <button
3172
- type="button"
4916
+ ${this._showPublishPanel && !this._publishPanelFullscreen
4917
+ ? html`<div
4918
+ class="compose-dropdown-backdrop"
4919
+ @click=${() => this._closePublishPanel(true)}
4920
+ ></div>`
4921
+ : nothing}
4922
+ <div
4923
+ role="group"
3173
4924
  class=${classMap({
3174
- "compose-publish-main": true,
3175
- "compose-publish-main-loading": this._loading,
4925
+ "compose-publish-buttons": true,
4926
+ "compose-publish-buttons-inactive": !canPublish && !this._loading,
3176
4927
  })}
3177
- ?disabled=${!canPublish}
3178
- @click=${() => this._submit("published")}
3179
4928
  >
3180
- ${this._loading ? spinner : nothing} ${this._getSubmitLabel()}
3181
- </button>
3182
- <button
3183
- type="button"
3184
- class=${classMap({
3185
- "compose-publish-toggle": true,
3186
- "compose-publish-toggle-loading": this._loading,
3187
- })}
3188
- ?disabled=${this._loading}
3189
- aria-haspopup="dialog"
3190
- aria-expanded=${this._showPublishPanel ? "true" : "false"}
3191
- aria-label=${this.labels.publishSettings}
3192
- title=${this.labels.publishSettings}
3193
- @click=${() => this._togglePublishPanel()}
4929
+ <button
4930
+ type="button"
4931
+ class=${classMap({
4932
+ "compose-publish-main": true,
4933
+ "compose-publish-main-loading": this._loading,
4934
+ })}
4935
+ ?disabled=${!canPublish}
4936
+ @click=${() => void this._submit("published")}
4937
+ >
4938
+ ${this._loading ? spinner : nothing} ${this._getSubmitLabel()}
4939
+ </button>
4940
+ <button
4941
+ type="button"
4942
+ class=${classMap({
4943
+ "compose-publish-toggle": true,
4944
+ "compose-publish-toggle-loading": this._loading,
4945
+ })}
4946
+ ?disabled=${this._loading}
4947
+ aria-haspopup="dialog"
4948
+ aria-expanded=${this._showPublishPanel ? "true" : "false"}
4949
+ aria-label=${this.labels.publishSettings}
4950
+ title=${this.labels.publishSettings}
4951
+ @click=${() => this._togglePublishPanel()}
4952
+ >
4953
+ ${renderComposePublishActionIcon(
4954
+ COMPOSE_PUBLISH_ACTION_ICONS.chevron,
4955
+ "compose-publish-toggle-chevron",
4956
+ )}
4957
+ </button>
4958
+ </div>
4959
+ ${this._renderDesktopPublishPanel()}
4960
+ </div>
4961
+ </div>
4962
+ `;
4963
+ }
4964
+
4965
+ private _renderEditLoadingState() {
4966
+ return html`
4967
+ <div
4968
+ class="compose-edit-loading"
4969
+ role="status"
4970
+ aria-live="polite"
4971
+ aria-busy="true"
4972
+ >
4973
+ <div class="compose-edit-loading-status">
4974
+ <svg
4975
+ class="animate-spin size-5"
4976
+ xmlns="http://www.w3.org/2000/svg"
4977
+ viewBox="0 0 24 24"
4978
+ fill="none"
4979
+ stroke="currentColor"
4980
+ stroke-width="2"
4981
+ stroke-linecap="round"
4982
+ stroke-linejoin="round"
4983
+ aria-hidden="true"
3194
4984
  >
3195
- ${renderComposePublishActionIcon(
3196
- COMPOSE_PUBLISH_ACTION_ICONS.chevron,
3197
- "compose-publish-toggle-chevron",
3198
- )}
3199
- </button>
4985
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
4986
+ </svg>
4987
+ <span>${this.labels.loadingPost}</span>
3200
4988
  </div>
3201
- ${this._renderPublishPanel()}
4989
+ <div class="compose-edit-loading-skeleton">
4990
+ <div class="skel-input compose-edit-loading-title"></div>
4991
+ <div class="skel-section-lg compose-edit-loading-body"></div>
4992
+ <div class="compose-edit-loading-footer">
4993
+ <div class="skel-input compose-edit-loading-chip"></div>
4994
+ <div class="skel-input compose-edit-loading-submit"></div>
4995
+ </div>
4996
+ </div>
4997
+ </div>
4998
+ `;
4999
+ }
5000
+
5001
+ // ── Thread compose ───────────────────────────────────────────────
5002
+
5003
+ private _addThreadItem() {
5004
+ const nextCount =
5005
+ this._threadItems.length === 0 ? 2 : this._threadItems.length + 1;
5006
+ if (nextCount > MAX_THREAD_POSTS) {
5007
+ showToast(this._getThreadLimitMessage(), "error");
5008
+ return;
5009
+ }
5010
+
5011
+ const lastFormat =
5012
+ this._threadItems.length > 0
5013
+ ? this._threadItems[this._threadItems.length - 1].format
5014
+ : this._format;
5015
+
5016
+ if (this._threadItems.length === 0) {
5017
+ // Entering thread mode: snapshot current single editor's state
5018
+ const currentEditor = this._editor;
5019
+ const editorState = currentEditor?.getEditorState() ?? null;
5020
+ const editorData = currentEditor?.getData();
5021
+ const bodyJson = currentEditor?.getNormalizedBodyJson() ?? null;
5022
+
5023
+ this._threadItems = [
5024
+ { id: crypto.randomUUID(), format: this._format },
5025
+ { id: crypto.randomUUID(), format: lastFormat },
5026
+ ];
5027
+
5028
+ // Capture rating state before re-render (these can't change asynchronously)
5029
+ const capturedRating = currentEditor?._rating ?? 0;
5030
+ const capturedShowRating = currentEditor?._showRating ?? false;
5031
+
5032
+ // Restore first thread item's content from the snapshot
5033
+ if (editorState || editorData) {
5034
+ this.updateComplete.then(() => {
5035
+ const editors = this.querySelectorAll<JantComposeEditor>(
5036
+ "jant-compose-editor",
5037
+ );
5038
+ const firstEditor = editors[0];
5039
+ if (!firstEditor) return;
5040
+ if (editorState) {
5041
+ firstEditor.setEditorState(
5042
+ editorState.json,
5043
+ editorState.title,
5044
+ editorState.showTitle,
5045
+ editorState.selection,
5046
+ );
5047
+ }
5048
+ if (editorData) {
5049
+ if (this._format === "link" && editorData.url) {
5050
+ firstEditor._url = editorData.url;
5051
+ } else if (this._format === "quote") {
5052
+ if (editorData.quoteText)
5053
+ firstEditor._quoteText = editorData.quoteText;
5054
+ if (editorData.quoteAuthor)
5055
+ firstEditor._quoteAuthor = editorData.quoteAuthor;
5056
+ }
5057
+ if (bodyJson) {
5058
+ firstEditor._bodyJson = bodyJson;
5059
+ }
5060
+ }
5061
+ // Read attachment state from the old editor NOW (after re-render) so
5062
+ // we get the latest mediaId for any uploads that completed during the
5063
+ // render cycle. The old editor element is still in memory even though
5064
+ // it has been removed from the DOM.
5065
+ const latestAttachments = currentEditor?._attachments ?? [];
5066
+ const latestAttachmentOrder = currentEditor?._attachmentOrder ?? [];
5067
+ const latestAttachedTexts = currentEditor?._attachedTexts ?? [];
5068
+ if (latestAttachments.length > 0) {
5069
+ firstEditor._attachments = [...latestAttachments];
5070
+ firstEditor._attachmentOrder = [...latestAttachmentOrder];
5071
+ }
5072
+ if (latestAttachedTexts.length > 0) {
5073
+ firstEditor._attachedTexts = [...latestAttachedTexts];
5074
+ }
5075
+ if (capturedRating > 0) {
5076
+ firstEditor._rating = capturedRating;
5077
+ firstEditor._showRating = capturedShowRating;
5078
+ }
5079
+ });
5080
+ }
5081
+ } else {
5082
+ this._threadItems = [
5083
+ ...this._threadItems,
5084
+ { id: crypto.randomUUID(), format: lastFormat },
5085
+ ];
5086
+ }
5087
+
5088
+ this._focusedThreadIndex = this._threadItems.length - 1;
5089
+ this.updateComplete.then(() => {
5090
+ const editors = this.querySelectorAll<JantComposeEditor>(
5091
+ "jant-compose-editor",
5092
+ );
5093
+ editors[this._focusedThreadIndex]?.focusInput();
5094
+ });
5095
+ }
5096
+
5097
+ private _removeThreadItem(index: number) {
5098
+ if (this._threadItems.length <= 1) return;
5099
+ const newItems = this._threadItems.filter((_, i) => i !== index);
5100
+
5101
+ if (newItems.length === 1) {
5102
+ // Exiting thread mode: capture remaining thread editor's state and restore
5103
+ // it to the single-post editor after thread mode is cleared.
5104
+ const editors = this.querySelectorAll<JantComposeEditor>(
5105
+ "jant-compose-editor",
5106
+ );
5107
+ const remainingIndex = index === 0 ? 1 : 0;
5108
+ const remainingEditor = editors[remainingIndex] ?? null;
5109
+ const editorState = remainingEditor?.getEditorState() ?? null;
5110
+ const editorData = remainingEditor?.getData();
5111
+ const bodyJson = remainingEditor?.getNormalizedBodyJson() ?? null;
5112
+ const remainingFormat = newItems[0].format;
5113
+ // Capture rating state before re-render (can't change asynchronously)
5114
+ const capturedRating = remainingEditor?._rating ?? 0;
5115
+ const capturedShowRating = remainingEditor?._showRating ?? false;
5116
+
5117
+ this._threadItems = [];
5118
+ this._focusedThreadIndex = 0;
5119
+ this._format = remainingFormat;
5120
+
5121
+ this.updateComplete.then(() => {
5122
+ const singleEditor = this._editor;
5123
+ if (!singleEditor) return;
5124
+ singleEditor.format = remainingFormat;
5125
+ if (editorState) {
5126
+ singleEditor.setEditorState(
5127
+ editorState.json,
5128
+ editorState.title,
5129
+ editorState.showTitle,
5130
+ editorState.selection,
5131
+ );
5132
+ }
5133
+ if (editorData) {
5134
+ if (remainingFormat === "link" && editorData.url) {
5135
+ singleEditor._url = editorData.url;
5136
+ } else if (remainingFormat === "quote") {
5137
+ if (editorData.quoteText)
5138
+ singleEditor._quoteText = editorData.quoteText;
5139
+ if (editorData.quoteAuthor)
5140
+ singleEditor._quoteAuthor = editorData.quoteAuthor;
5141
+ }
5142
+ if (bodyJson) {
5143
+ singleEditor._bodyJson = bodyJson;
5144
+ }
5145
+ }
5146
+ // Read attachment state from the old editor NOW so we capture any
5147
+ // mediaIds set by uploads that completed during the render cycle.
5148
+ const latestAttachments = remainingEditor?._attachments ?? [];
5149
+ const latestAttachmentOrder = remainingEditor?._attachmentOrder ?? [];
5150
+ const latestAttachedTexts = remainingEditor?._attachedTexts ?? [];
5151
+ if (latestAttachments.length > 0) {
5152
+ singleEditor._attachments = [...latestAttachments];
5153
+ singleEditor._attachmentOrder = [...latestAttachmentOrder];
5154
+ }
5155
+ if (latestAttachedTexts.length > 0) {
5156
+ singleEditor._attachedTexts = [...latestAttachedTexts];
5157
+ }
5158
+ if (capturedRating > 0) {
5159
+ singleEditor._rating = capturedRating;
5160
+ singleEditor._showRating = capturedShowRating;
5161
+ }
5162
+ singleEditor.focusInput();
5163
+ this.requestUpdate();
5164
+ });
5165
+ } else {
5166
+ this._threadItems = newItems;
5167
+ this._focusedThreadIndex = Math.min(
5168
+ this._focusedThreadIndex,
5169
+ newItems.length - 1,
5170
+ );
5171
+ }
5172
+ }
5173
+
5174
+ private _renderThreadPost(
5175
+ item: ThreadItem,
5176
+ index: number,
5177
+ showRemove: boolean,
5178
+ ) {
5179
+ return html`
5180
+ <div
5181
+ class="compose-editor-row compose-thread-post"
5182
+ data-thread-index=${index}
5183
+ @focusin=${() => {
5184
+ this._focusedThreadIndex = index;
5185
+ }}
5186
+ @jant:thread-format-change=${(
5187
+ e: CustomEvent<{ format: ComposeFormat }>,
5188
+ ) => {
5189
+ e.stopPropagation();
5190
+ this._threadItems = this._threadItems.map((it, i) =>
5191
+ i === index ? { ...it, format: e.detail.format } : it,
5192
+ );
5193
+ this._format = e.detail.format;
5194
+ }}
5195
+ @jant:thread-remove=${(e: Event) => {
5196
+ e.stopPropagation();
5197
+ this._removeThreadItem(index);
5198
+ }}
5199
+ >
5200
+ <div class="compose-thread-dot"></div>
5201
+ <jant-compose-editor
5202
+ .format=${item.format}
5203
+ .labels=${this.labels}
5204
+ .uploadMaxFileSize=${this.uploadMaxFileSize}
5205
+ .threadItem=${true}
5206
+ .removable=${showRemove}
5207
+ data-thread-id=${item.id}
5208
+ ></jant-compose-editor>
5209
+ </div>
5210
+ `;
5211
+ }
5212
+
5213
+ private _renderAddToThreadRow() {
5214
+ const PLUS_ICON = `<circle cx="8" cy="8" r="5.7"/><path d="M8 5.55v4.9M5.55 8h4.9"/>`;
5215
+ const editors = this.querySelectorAll<JantComposeEditor>(
5216
+ "jant-compose-editor",
5217
+ );
5218
+ const lastEditor = editors[editors.length - 1];
5219
+ const lastData = lastEditor?.getData();
5220
+ const lastEmpty =
5221
+ !lastData ||
5222
+ (!lastData.body &&
5223
+ !lastData.title.trim() &&
5224
+ !lastData.url.trim() &&
5225
+ !lastData.quoteText.trim() &&
5226
+ lastData.attachments.length === 0);
5227
+ const atLimit = this._threadItems.length >= MAX_THREAD_POSTS;
5228
+ return html`
5229
+ <div class="compose-thread-add-row">
5230
+ <div class="compose-thread-add-dot"></div>
5231
+ <button
5232
+ type="button"
5233
+ class="compose-thread-add-btn"
5234
+ ?disabled=${lastEmpty || atLimit}
5235
+ @click=${() => this._addThreadItem()}
5236
+ >
5237
+ <svg
5238
+ width="14"
5239
+ height="14"
5240
+ viewBox="0 0 16 16"
5241
+ fill="none"
5242
+ stroke="currentColor"
5243
+ stroke-width="1.65"
5244
+ stroke-linecap="round"
5245
+ stroke-linejoin="round"
5246
+ aria-hidden="true"
5247
+ >
5248
+ ${unsafeSVG(PLUS_ICON)}
5249
+ </svg>
5250
+ Add to thread
5251
+ </button>
5252
+ </div>
5253
+ `;
5254
+ }
5255
+
5256
+ private _renderThreadComposeLayout() {
5257
+ const isReply = !!(this._replyToId && this._replyToData);
5258
+ const items = this._threadItems;
5259
+ const showRemove = items.length > 1;
5260
+
5261
+ return html`
5262
+ <div
5263
+ class="compose-thread-layout compose-thread-compose-layout"
5264
+ @jant:compose-content-changed=${() => this._scheduleDraftSave()}
5265
+ >
5266
+ ${isReply ? this._renderReplyContext() : nothing}
5267
+ ${items.map((item, i) => this._renderThreadPost(item, i, showRemove))}
5268
+ ${this._renderAddToThreadRow()}
5269
+ </div>
5270
+ `;
5271
+ }
5272
+
5273
+ private _renderAddThreadTrigger() {
5274
+ const PLUS_ICON = `<circle cx="8" cy="8" r="5.7"/><path d="M8 5.55v4.9M5.55 8h4.9"/>`;
5275
+ const disabled = !this._hasContent();
5276
+ return html`
5277
+ <div class="compose-add-thread-trigger">
5278
+ <button
5279
+ type="button"
5280
+ class="compose-add-thread-btn"
5281
+ ?disabled=${disabled}
5282
+ @click=${() => this._addThreadItem()}
5283
+ >
5284
+ <svg
5285
+ width="14"
5286
+ height="14"
5287
+ viewBox="0 0 16 16"
5288
+ fill="none"
5289
+ stroke="currentColor"
5290
+ stroke-width="1.65"
5291
+ stroke-linecap="round"
5292
+ stroke-linejoin="round"
5293
+ aria-hidden="true"
5294
+ >
5295
+ ${unsafeSVG(PLUS_ICON)}
5296
+ </svg>
5297
+ Add to thread
5298
+ </button>
3202
5299
  </div>
3203
5300
  `;
3204
5301
  }
3205
5302
 
3206
5303
  render() {
3207
5304
  const isReply = !!(this._replyToId && this._replyToData);
5305
+ const isThreadMode = this._threadItems.length > 0;
5306
+ const isOpeningEdit = this._openingEdit;
3208
5307
  const editor = html`<jant-compose-editor
3209
5308
  .format=${this._format}
3210
5309
  .labels=${this.labels}
@@ -3218,27 +5317,43 @@ export class JantComposeDialog extends LitElement {
3218
5317
  "compose-dialog-inner-page": this.pageMode,
3219
5318
  "compose-dialog-inner-suspended": this._addCollectionPanelOpen,
3220
5319
  })}
5320
+ tabindex="-1"
3221
5321
  aria-hidden=${this._addCollectionPanelOpen ? "true" : "false"}
3222
5322
  ?inert=${this._addCollectionPanelOpen}
3223
5323
  >
3224
5324
  ${this._renderHeader()}
3225
- ${isReply
3226
- ? html`
3227
- <div class="compose-thread-layout">
3228
- ${this._renderReplyContext()}
3229
- <div class="compose-editor-row">
3230
- <div class="compose-thread-dot"></div>
3231
- ${editor}
3232
- </div>
3233
- </div>
3234
- `
3235
- : editor}
3236
-
3237
- <div class="compose-action-row">
3238
- ${this._renderCollectionSelector()} ${this._renderPublishButton()}
3239
- </div>
3240
- ${this._renderAttachedPanel()} ${this._renderAltPanel()}
3241
- ${this._renderDraftsPanel()} ${this._renderConfirmPanel()}
5325
+ ${isOpeningEdit
5326
+ ? this._renderEditLoadingState()
5327
+ : isThreadMode
5328
+ ? this._renderThreadComposeLayout()
5329
+ : isReply
5330
+ ? html`
5331
+ <div class="compose-thread-layout">
5332
+ ${this._renderReplyContext()}
5333
+ <div class="compose-editor-row">
5334
+ <div class="compose-thread-dot"></div>
5335
+ ${editor}
5336
+ </div>
5337
+ </div>
5338
+ `
5339
+ : editor}
5340
+ ${isOpeningEdit || isThreadMode || this._editPostId
5341
+ ? nothing
5342
+ : this._renderAddThreadTrigger()}
5343
+ ${isOpeningEdit
5344
+ ? nothing
5345
+ : html`<div
5346
+ class=${classMap({
5347
+ "compose-action-row": true,
5348
+ "compose-action-row-overlay-open":
5349
+ this._showPublishPanel || this._showCollection,
5350
+ })}
5351
+ >
5352
+ ${this._renderCollectionSelector()} ${this._renderPublishButton()}
5353
+ </div>`}
5354
+ ${this._renderMobilePublishPanel()} ${this._renderAttachedPanel()}
5355
+ ${this._renderAltPanel()} ${this._renderDraftsPanel()}
5356
+ ${this._renderConfirmPanel()}
3242
5357
  </div>
3243
5358
  ${this._renderAddCollectionPanel()}
3244
5359
  `;