@jant/core 0.3.38 → 0.3.39

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 (1177) hide show
  1. package/LICENSE +189 -189
  2. package/README.md +5 -4
  3. package/bin/commands/assets/prepare.js +55 -0
  4. package/bin/commands/db/execute-file.js +113 -0
  5. package/bin/commands/db/export.js +5 -0
  6. package/bin/commands/db/rehearse.js +65 -0
  7. package/bin/commands/deploy.js +162 -0
  8. package/bin/commands/export.js +73 -75
  9. package/bin/commands/import-site.js +1531 -267
  10. package/bin/commands/migrate.js +79 -0
  11. package/bin/commands/reset-password.js +80 -7
  12. package/bin/commands/site/export.js +415 -0
  13. package/bin/commands/site/import.js +5 -0
  14. package/bin/commands/site/localize-media.js +118 -0
  15. package/bin/commands/site/snapshot/export.js +357 -0
  16. package/bin/commands/site/snapshot/import.js +363 -0
  17. package/bin/commands/start.js +46 -0
  18. package/bin/commands/uploads/cleanup.js +119 -0
  19. package/bin/jant.js +55 -15
  20. package/bin/lib/cli-api-token.js +6 -0
  21. package/bin/lib/d1-query.js +194 -0
  22. package/bin/lib/load-node-runtime.js +10 -0
  23. package/bin/lib/migration-artifacts.js +99 -0
  24. package/bin/lib/migration-rehearsal.js +438 -0
  25. package/bin/lib/migration-runner.js +311 -0
  26. package/bin/lib/node-database.js +48 -0
  27. package/bin/lib/node-sqlite.js +158 -0
  28. package/bin/lib/public-assets.js +66 -0
  29. package/bin/lib/r2-query.js +170 -0
  30. package/bin/lib/runtime-target.js +33 -0
  31. package/bin/lib/site-localize-media.js +427 -0
  32. package/bin/lib/site-media-parser.js +1150 -0
  33. package/bin/lib/site-selection.js +222 -0
  34. package/bin/lib/site-snapshot.js +484 -0
  35. package/bin/lib/site-url.js +84 -0
  36. package/bin/lib/sql-export.js +194 -0
  37. package/bin/lib/wrangler-cli.js +41 -0
  38. package/bin/lib/wrangler-config.js +86 -0
  39. package/dist/app-BfoG98VD.js +30410 -0
  40. package/dist/client/_assets/chunks/0041f681602cc834bb5c55ced0155b8e-BNpHLJgt.woff2 +0 -0
  41. package/dist/client/_assets/chunks/008ea9091e332c639ceb18874eacd60c-CygB704Q.woff2 +0 -0
  42. package/dist/client/_assets/chunks/00c9ac960d866ffaf8a866e5939024e2-CqRrlxSi.woff2 +0 -0
  43. package/dist/client/_assets/chunks/02629e5d0a9860b7fe32ec1f0563213a-YvgSIv_V.woff2 +0 -0
  44. package/dist/client/_assets/chunks/02e48e353415a00e0f556608cab33a43-YFgu4zi3.woff2 +0 -0
  45. package/dist/client/_assets/chunks/02faf6bb0ab4d56ada037c0bbaf9b9f7-DUuUM2eo.woff2 +0 -0
  46. package/dist/client/_assets/chunks/031089da45fbfb7dc18ac827bef4c56e-D4ahCOTO.woff2 +0 -0
  47. package/dist/client/_assets/chunks/03ac785139320b7b13bac9c150bf72bf-BG7zubxs.woff2 +0 -0
  48. package/dist/client/_assets/chunks/053bd3d7aec0040d0cc50c261a1f4e3e-BdG4yI0n.woff2 +0 -0
  49. package/dist/client/_assets/chunks/057a3d44d7fc606f113d863376d0ecf0-BsEMbuKV.woff2 +0 -0
  50. package/dist/client/_assets/chunks/0713613227cc4c686c45a279f8bdc166-BzLoJcLo.woff2 +0 -0
  51. package/dist/client/_assets/chunks/0b1f83a3c7e715560a55ad9eb0fb1c94-asGrAhxW.woff2 +0 -0
  52. package/dist/client/_assets/chunks/0c055db157e7a13f3103cc2a6b67fec3-1T6huY_I.woff2 +0 -0
  53. package/dist/client/_assets/chunks/0d3f5cc265cb6c439c517f2c4cebbddf-CmkfpVHX.woff2 +0 -0
  54. package/dist/client/_assets/chunks/0e97f44ebc65384c346fe19bcc52fa20-BJTzmZzt.woff2 +0 -0
  55. package/dist/client/_assets/chunks/10f2b44b3711d3f5bdcc30d373b543d1-BRTxD_6-.woff2 +0 -0
  56. package/dist/client/_assets/chunks/1139d32ae2bdeb26c0c8f31330aa9a9f-B7k4Da0E.woff2 +0 -0
  57. package/dist/client/_assets/chunks/1259e5825b314fe2b8bb96d6e8069ee5-Ba8VoZCg.woff2 +0 -0
  58. package/dist/client/_assets/chunks/12c518ebfe62818af550c08947e359e7-B7eDp07N.woff2 +0 -0
  59. package/dist/client/_assets/chunks/145831a59caa06d894022fe60212ed21-B3xSIk4b.woff2 +0 -0
  60. package/dist/client/_assets/chunks/14b040a2dda256936bc7b3470e548394-CF7zgNWs.woff2 +0 -0
  61. package/dist/client/_assets/chunks/14c1506106d92621bafb11016735194e-cbk_paoP.woff2 +0 -0
  62. package/dist/client/_assets/chunks/154a2c266902003bd8b7449386b10776-CYgd0apt.woff2 +0 -0
  63. package/dist/client/_assets/chunks/15f8c0df47fd639d1b0d9bd5cf507c9b-B11bYwtn.woff2 +0 -0
  64. package/dist/client/_assets/chunks/1668bd859ffe15bed7d5563117d8d5fb-DJotYU-j.woff2 +0 -0
  65. package/dist/client/_assets/chunks/169a096e61d38a773216f51d1ec2cc06-CzS4CZjy.woff2 +0 -0
  66. package/dist/client/_assets/chunks/1884a2b22d314c7d57707f03aec348e0-BdBJ4_JO.woff2 +0 -0
  67. package/dist/client/_assets/chunks/189f272ea2600c74d576b7b15c014922-CylwUmhO.woff2 +0 -0
  68. package/dist/client/_assets/chunks/190e3f8632494e7c095117f26b1c811e-DT09KhZe.woff2 +0 -0
  69. package/dist/client/_assets/chunks/19ad151c22ce1befe0a9ea643fbee570-BOjbPzxL.woff2 +0 -0
  70. package/dist/client/_assets/chunks/19e39850472250bfdbce654d30859879-Db8MVQ-5.woff2 +0 -0
  71. package/dist/client/_assets/chunks/1b5d0d740450fb996749464c9b882025-BG0yepI2.woff2 +0 -0
  72. package/dist/client/_assets/chunks/1bca0a2a8840ad0ee9414940593db144-CfiRZ0Fs.woff2 +0 -0
  73. package/dist/client/_assets/chunks/1c820b5295868008ca7c78afa5b7655d-rGkaVY0Z.woff2 +0 -0
  74. package/dist/client/_assets/chunks/1cda27dcaab977ae4ef5d5ab2a10ae03-3n1CbajX.woff2 +0 -0
  75. package/dist/client/_assets/chunks/1e2640116bbba817f43c43cc69371cf1-BLnekPNy.woff2 +0 -0
  76. package/dist/client/_assets/chunks/1f4bc38a1c50f55f335f5411cae47696-3J1JFXeu.woff2 +0 -0
  77. package/dist/client/_assets/chunks/1fbccc182322b513f57cd156a9a491b0-bRVH28a5.woff2 +0 -0
  78. package/dist/client/_assets/chunks/1fbe225742c69f4ba9ea5f74922f0ca1-BHRIiKDB.woff2 +0 -0
  79. package/dist/client/_assets/chunks/2022cf097cb952d9fe75b53b4587d2c3-Btdq5ZEB.woff2 +0 -0
  80. package/dist/client/_assets/chunks/21e0a71d86be7b8a9e812e7af09dd061-SSZaQU5Z.woff2 +0 -0
  81. package/dist/client/_assets/chunks/2240a3c43ca5ef59ae3c348c7884792f-DQgT8QLe.woff2 +0 -0
  82. package/dist/client/_assets/chunks/2573703213da30d3ba18925b100b2c2b-D61uHyrJ.woff2 +0 -0
  83. package/dist/client/_assets/chunks/265e048cc9f2f8a711ba585a534d5351-geHDzSNf.woff2 +0 -0
  84. package/dist/client/_assets/chunks/26839c0e47c73514b8d8f660d24d6b19-D2gmzZl-.woff2 +0 -0
  85. package/dist/client/_assets/chunks/280e3d2b58e9ad3501816072e01b0c13-Dx5yDwjz.woff2 +0 -0
  86. package/dist/client/_assets/chunks/2874d07e228da9583b0e73646dacd498-26mUURU-.woff2 +0 -0
  87. package/dist/client/_assets/chunks/29d49891713a2785a3a383001cf58c59-DVb6JpZh.woff2 +0 -0
  88. package/dist/client/_assets/chunks/2a22e14a9ad53f2abb3c7e85017b7d12-CU_4APNf.woff2 +0 -0
  89. package/dist/client/_assets/chunks/2a7cedfcd6e4c7cec36f4fd7b0f329c2-BUp7BYdq.woff2 +0 -0
  90. package/dist/client/_assets/chunks/2acea04a920f6af31e7db97052f563c6-KdL2n7-Y.woff2 +0 -0
  91. package/dist/client/_assets/chunks/2ae2ca951489c9d50cde5b36a2a5515b-CWV12jwD.woff2 +0 -0
  92. package/dist/client/_assets/chunks/2b3e8c5703b91f39f6027f43f0da6f4b-FHYDvFqX.woff2 +0 -0
  93. package/dist/client/_assets/chunks/2ba802b14f21a58fc61606c88fa93373-BmrW6szo.woff2 +0 -0
  94. package/dist/client/_assets/chunks/2d81eb6ab0ebbc0cabfb3a3341ba8800-Ds2YcMj0.woff2 +0 -0
  95. package/dist/client/_assets/chunks/2deb444546774c3a3ab38c75eb69cdfb-Dm9USQXk.woff2 +0 -0
  96. package/dist/client/_assets/chunks/2e98b666924b8e0a09d1aeeefd24bdd2-Cmh3DrKG.woff2 +0 -0
  97. package/dist/client/_assets/chunks/2f27ee4fb2cf6a280e110e09c18ef73e-D_yYWYMZ.woff2 +0 -0
  98. package/dist/client/_assets/chunks/2fbccf9a3853eb59db1a825e044515fd-CiShE3Wo.woff2 +0 -0
  99. package/dist/client/_assets/chunks/2fd3fceb6faed5e3db768e88d7614dca-D74stxc-.woff2 +0 -0
  100. package/dist/client/_assets/chunks/2ff009fa8701505d7f3dc6c83763f019-nkOuUfA-.woff2 +0 -0
  101. package/dist/client/_assets/chunks/31342cebfa5ea7fac06b4ea372d96bc5-DdIH_TkU.woff2 +0 -0
  102. package/dist/client/_assets/chunks/31424fe5d54692e7c8b38021ccb8597c-CfNu45zJ.woff2 +0 -0
  103. package/dist/client/_assets/chunks/3179006d1c7ebfa50d27482a2859d9a0-e7PNUH9y.woff2 +0 -0
  104. package/dist/client/_assets/chunks/34c2edb3c37f71258f5c4a31091f0c6c-Cyr8VjPf.woff2 +0 -0
  105. package/dist/client/_assets/chunks/35cf5dd04315e0b906e1a413d7905a2f-A-QPUrVX.woff2 +0 -0
  106. package/dist/client/_assets/chunks/360c190344c26278bbc50e2f4d6a2b3f-B7OhzvgR.woff2 +0 -0
  107. package/dist/client/_assets/chunks/375329ba0b50b94b35006498e555867c-CDc5XB7_.woff2 +0 -0
  108. package/dist/client/_assets/chunks/387c811226f303af62f1e21aae6f5c83-BmPYUAdb.woff2 +0 -0
  109. package/dist/client/_assets/chunks/389a950f2a1211946d294716e679e381-Diuxk8JN.woff2 +0 -0
  110. package/dist/client/_assets/chunks/39b0bbc910af9d2d6dcd8bd4abd6387d-NhvZf71d.woff2 +0 -0
  111. package/dist/client/_assets/chunks/3adc8f6350cf5067bcb6dc5e44c45d41-DIO3oy0b.woff2 +0 -0
  112. package/dist/client/_assets/chunks/3b41385fc27419c19822060daa0b5cb3-Dh5JVnsu.woff2 +0 -0
  113. package/dist/client/_assets/chunks/3bed5bd57de8f738e53cddaea88983d9-BAGVbLtM.woff2 +0 -0
  114. package/dist/client/_assets/chunks/3c201fd8d1bb20abe7d06b940e83a4d9-Bz0dCLvM.woff2 +0 -0
  115. package/dist/client/_assets/chunks/3cbe4a697fd595ef42c899de7d3e5445-zWwFRRI_.woff2 +0 -0
  116. package/dist/client/_assets/chunks/3d385ea0880df7204258e290648ec012-BQqoFH8R.woff2 +0 -0
  117. package/dist/client/_assets/chunks/3d83dacbbec3d8532ae9afede21f3aab-BH06SEJj.woff2 +0 -0
  118. package/dist/client/_assets/chunks/3dc1a4f0d7af59e16b5162a2b077a442-CkXZW3CY.woff2 +0 -0
  119. package/dist/client/_assets/chunks/3dc68e473fe23bd076dd46785cd23583-Cv6HZ0Dr.woff2 +0 -0
  120. package/dist/client/_assets/chunks/427577dcb707d1d35eebd155b4222aa7-D7-mHA6o.woff2 +0 -0
  121. package/dist/client/_assets/chunks/42a74b6a625bbf0a9616ed4db3152c88-CQ58KTMS.woff2 +0 -0
  122. package/dist/client/_assets/chunks/435b7dca567809813fcb395a27ed83a0-C6E3YXBr.woff2 +0 -0
  123. package/dist/client/_assets/chunks/43693195e775d515689fa035394067fd-DLMrklI6.woff2 +0 -0
  124. package/dist/client/_assets/chunks/43fb49e5b79ee7e553869d84e6e08b1e-BgOVrd59.woff2 +0 -0
  125. package/dist/client/_assets/chunks/44dcbbc3cc8f22e613b342d691511ab6-dRyYwGGi.woff2 +0 -0
  126. package/dist/client/_assets/chunks/450a5b53be0a8a778bb0b623e86b652f-qwII2gXW.woff2 +0 -0
  127. package/dist/client/_assets/chunks/457485e72835364662dfead6281638c1-Mv279UyL.woff2 +0 -0
  128. package/dist/client/_assets/chunks/47479c470fae70f10b7c964a7ecbf274-D9VNBclD.woff2 +0 -0
  129. package/dist/client/_assets/chunks/474fac21b12b7efd71f7c321578878b0-BJRz_Yd3.woff2 +0 -0
  130. package/dist/client/_assets/chunks/477866c8396474a17317dcac3e7a014f-e90LWIFy.woff2 +0 -0
  131. package/dist/client/_assets/chunks/478ebdaadda7775c391c5dcab4e697df-RfXJJ1gl.woff2 +0 -0
  132. package/dist/client/_assets/chunks/488846410760fe128dae939836ca5423-D0YbXQOX.woff2 +0 -0
  133. package/dist/client/_assets/chunks/48d6a97a185c799be4fe67aaf7edf213-CqLkiJik.woff2 +0 -0
  134. package/dist/client/_assets/chunks/48eb0a91e50c7f026e248c64145e72af-D8LB2Dye.woff2 +0 -0
  135. package/dist/client/_assets/chunks/490edb9fc8a4356aea556eed32287464-Bxk-XtyT.woff2 +0 -0
  136. package/dist/client/_assets/chunks/4a23fe6e82fd496b5eb20401b6164efe-CfhsfLd6.woff2 +0 -0
  137. package/dist/client/_assets/chunks/4b0e79ba18b2ce424fa93e84996d7cba-B0dS4UvF.woff2 +0 -0
  138. package/dist/client/_assets/chunks/4bc743968cf1c3ce5711de67ef1ccc4d-Dmn20LlT.woff2 +0 -0
  139. package/dist/client/_assets/chunks/4c4bdd0b3f3a52e28f3b643c1c5d43be-BLqyuzyK.woff2 +0 -0
  140. package/dist/client/_assets/chunks/4c96411f3693a9a8657a9c1190f82bce-BhP7AS2j.woff2 +0 -0
  141. package/dist/client/_assets/chunks/4c9aa12aba2a6a57410eacaff7427916-CLDZgzM7.woff2 +0 -0
  142. package/dist/client/_assets/chunks/4cca7233bf8ce5dec2e5d146b993d626-Bf7OgJe6.woff2 +0 -0
  143. package/dist/client/_assets/chunks/4cf0f292f3358bd2f73b1cf4ec1476f3-CIfjw85D.woff2 +0 -0
  144. package/dist/client/_assets/chunks/4d0a9128d06ea857f203bf5d007b1ab9-BZ-BuqS9.woff2 +0 -0
  145. package/dist/client/_assets/chunks/4dc0728df0f2ba70796f45f05654c7ba-DGd3NDaT.woff2 +0 -0
  146. package/dist/client/_assets/chunks/4dc2bc2c55b47f57d13b63aa6b1c8bd4-g3WbclK2.woff2 +0 -0
  147. package/dist/client/_assets/chunks/4e1cc6aafb411b572c8d3511e925ecf1-BUf15jYj.woff2 +0 -0
  148. package/dist/client/_assets/chunks/4e5384920bbb155d9d8d74887b09ea5b-2KLteQXT.woff2 +0 -0
  149. package/dist/client/_assets/chunks/501f66f24bce8234441954de1b568403-CIe4N6pO.woff2 +0 -0
  150. package/dist/client/_assets/chunks/50cfd672bfa62512ba090420acf35c87-CibXrwCT.woff2 +0 -0
  151. package/dist/client/_assets/chunks/5227dbe9933760a48baff21ebd13fc98-v8CBITMX.woff2 +0 -0
  152. package/dist/client/_assets/chunks/526b263e72c189f4b065738aaa6d423a-Dj5Ns6Ev.woff2 +0 -0
  153. package/dist/client/_assets/chunks/53a88404451448cd2e620a0ca0e45a20-CI0P5NCv.woff2 +0 -0
  154. package/dist/client/_assets/chunks/54da934819a917f561b439bfd10f88b6-CpoNRhz7.woff2 +0 -0
  155. package/dist/client/_assets/chunks/54e301f412730f391225db59dae1c8d5-CxTAz9F1.woff2 +0 -0
  156. package/dist/client/_assets/chunks/551b1d7a0b80c8d42af09863cdca7f01-BIGKH8GS.woff2 +0 -0
  157. package/dist/client/_assets/chunks/555d990ab3fd7d3d66c6d1fa9a82fec5-BndAe6f2.woff2 +0 -0
  158. package/dist/client/_assets/chunks/557cd00c5d6827e13d72a0c71b23587b-C6gpr-g-.woff2 +0 -0
  159. package/dist/client/_assets/chunks/563fa31542d553f25abab65cf7f81e1d-Cl_1me5X.woff2 +0 -0
  160. package/dist/client/_assets/chunks/56e1c4734bbbb38af2fbc262bf6e98f2-DZ32amUx.woff2 +0 -0
  161. package/dist/client/_assets/chunks/5947f5da5da9a352a2b534ee64bfc29a-DT0Fj-SL.woff2 +0 -0
  162. package/dist/client/_assets/chunks/5979c33a7eb5963bf8e83e46931b5fb5--4f9yv1z.woff2 +0 -0
  163. package/dist/client/_assets/chunks/597d69d0710e0178b162afb0a0c20401-CpThXFdo.woff2 +0 -0
  164. package/dist/client/_assets/chunks/59966ee0b069b577510fe68c350da0ee-Bs_HDvyP.woff2 +0 -0
  165. package/dist/client/_assets/chunks/5a10741e41259e235841440394c0763d-uCZGOg0A.woff2 +0 -0
  166. package/dist/client/_assets/chunks/5bfc7a121c35ae42623ef804fb525e0e-mS7vyFFz.woff2 +0 -0
  167. package/dist/client/_assets/chunks/5cc23a76e122d0ad2f7cede41bc35b27-4T1ZVQJu.woff2 +0 -0
  168. package/dist/client/_assets/chunks/5d48855bed5f3554eff91b573d7376ac-CPzsdNDF.woff2 +0 -0
  169. package/dist/client/_assets/chunks/5ddcbe564b29ef08632e1aeb33455435-CQzDKa-z.woff2 +0 -0
  170. package/dist/client/_assets/chunks/5f90024544c2907c6c0203c6210c50be-C_1uxIky.woff2 +0 -0
  171. package/dist/client/_assets/chunks/605667a998e91e2b6a4a3cd7c31fe5a9-Co2lOjER.woff2 +0 -0
  172. package/dist/client/_assets/chunks/60a14064ed334f0155795d795e926abe-C2HuVyw5.woff2 +0 -0
  173. package/dist/client/_assets/chunks/60d8b0805a0a8c54a6cca216004beff5-CV1GX5br.woff2 +0 -0
  174. package/dist/client/_assets/chunks/611b62d5fd9698d9b5ce495ba6f14c93-CPi2xRKF.woff2 +0 -0
  175. package/dist/client/_assets/chunks/61bf4287453da4025d03fa6b2dba66ca-CMh4XzzB.woff2 +0 -0
  176. package/dist/client/_assets/chunks/638369541268ed5a10af97ad77498c73-DrpvGu-L.woff2 +0 -0
  177. package/dist/client/_assets/chunks/649b12d7cee7bb981842946e4547e6ca-EACt8KkC.woff2 +0 -0
  178. package/dist/client/_assets/chunks/653bef2ed891ae48d8ed712283080649-C3d1ZTAc.woff2 +0 -0
  179. package/dist/client/_assets/chunks/67d2a81f06ba352f17fbdc3a5e6ea59e-DC8Iostn.woff2 +0 -0
  180. package/dist/client/_assets/chunks/67f32ceea9e78e5109f87724ad886010-C_3-9sSn.woff2 +0 -0
  181. package/dist/client/_assets/chunks/68304f3229cf763465f044fccb5892c0-CTvPlN8c.woff2 +0 -0
  182. package/dist/client/_assets/chunks/687d0f0f90a9b23e40102e16ad8e9836-Bzlx1W-7.woff2 +0 -0
  183. package/dist/client/_assets/chunks/688a88911e4da17b609196a959b8b930-BgwP4zIQ.woff2 +0 -0
  184. package/dist/client/_assets/chunks/68f2fab82ec8e9291f08c3145111549c-CXqagiiH.woff2 +0 -0
  185. package/dist/client/_assets/chunks/69519ada3f3f74ca20aacb8af48ab6b4-DncDOElk.woff2 +0 -0
  186. package/dist/client/_assets/chunks/6a6e884fb2b65ec5b4a3d5ecd0d01a6a-DBNJo4jq.woff2 +0 -0
  187. package/dist/client/_assets/chunks/6db6ddf72c38a78ce44c1327701152e1-CFUWMaQQ.woff2 +0 -0
  188. package/dist/client/_assets/chunks/6e1a8b45b01939088c3a8cfcf8c10681-BemqZSaI.woff2 +0 -0
  189. package/dist/client/_assets/chunks/6e2164fad867d166de2e5b274f04a563-_YD_QMym.woff2 +0 -0
  190. package/dist/client/_assets/chunks/6e83fe0b6e708eaf1c3003d6dee11488-CRqY8z2g.woff2 +0 -0
  191. package/dist/client/_assets/chunks/6eefc9d430171c1e1e4034ecadee31c8-d3VCnQ01.woff2 +0 -0
  192. package/dist/client/_assets/chunks/70861376e5d4f92f8aa7aa1b2749b617-BqwxmSo2.woff2 +0 -0
  193. package/dist/client/_assets/chunks/70adaf50c56d5ff859c64d35e0f1e34e-BCmTRExx.woff2 +0 -0
  194. package/dist/client/_assets/chunks/7124d150570d39ced8d45507dc11ca1e-Co_mUkqP.woff2 +0 -0
  195. package/dist/client/_assets/chunks/71eafb8fbe3a734283517e230ad8b6db-CFmZ-qAS.woff2 +0 -0
  196. package/dist/client/_assets/chunks/72ee453ac0e19bd2c631c8921c44e3de-CEidyyfI.woff2 +0 -0
  197. package/dist/client/_assets/chunks/751f54dbb115140d5b645a6ba4aff5d3-DY-S96zI.woff2 +0 -0
  198. package/dist/client/_assets/chunks/753b5f6fb254bacb6618ace25af3df60-uBG6c32o.woff2 +0 -0
  199. package/dist/client/_assets/chunks/7609e7e74dd4d916a7abc7ecc7d95f7e-CzrNhght.woff2 +0 -0
  200. package/dist/client/_assets/chunks/76b9d6fe838ae4151d95ce7200aa2bf6-BfQWDykU.woff2 +0 -0
  201. package/dist/client/_assets/chunks/76d4244186d118eea245d1385a4de2ec-CEoql1pw.woff2 +0 -0
  202. package/dist/client/_assets/chunks/77a7533bd21ccd33192d142a93555aa8-BzSb8ker.woff2 +0 -0
  203. package/dist/client/_assets/chunks/78ce29fed872e44fc9014d94875d2aac-D1BDuJXp.woff2 +0 -0
  204. package/dist/client/_assets/chunks/79a7fdf7d9c722b5723ae25e6ff8e203-5rPAnIr7.woff2 +0 -0
  205. package/dist/client/_assets/chunks/79a85a253e9b3f12d2e2cb15e16b3003-DTjWGv2X.woff2 +0 -0
  206. package/dist/client/_assets/chunks/7a86b155111ba20f3e87306ff6beac77-DchSNxJ1.woff2 +0 -0
  207. package/dist/client/_assets/chunks/7b1e76975b0984e6f83e3f9f8069e784-OB_rGzsR.woff2 +0 -0
  208. package/dist/client/_assets/chunks/7b6c60131822a0e4d36d980d52509d4e-8dQVuUiW.woff2 +0 -0
  209. package/dist/client/_assets/chunks/7caa14a095a6bc313aab780fe4ff7999-BJ-80Ois.woff2 +0 -0
  210. package/dist/client/_assets/chunks/7cbda564cb2dd4799ab9e89d51286aa7-MlT7YRSz.woff2 +0 -0
  211. package/dist/client/_assets/chunks/7d138084cf03c14116b11297fce0e3e3-CdWCcyam.woff2 +0 -0
  212. package/dist/client/_assets/chunks/7d65a3d6a65050eb5e6eca43398aeba4-CYB359u-.woff2 +0 -0
  213. package/dist/client/_assets/chunks/7dfc711962c8771f97e7c8898a6bcb65-Yv69yrg4.woff2 +0 -0
  214. package/dist/client/_assets/chunks/7ef123b62d530fcba73974fa265e0aae-CD7IMlBc.woff2 +0 -0
  215. package/dist/client/_assets/chunks/7f60eefa15956d6f06dd92404887d58c-LqioDjlc.woff2 +0 -0
  216. package/dist/client/_assets/chunks/7f8c15e0ecb102738981d9fa4cb6b921-8qiw7Fmn.woff2 +0 -0
  217. package/dist/client/_assets/chunks/80466082a896fd328f30a78593c7c568-BMAVDyAy.woff2 +0 -0
  218. package/dist/client/_assets/chunks/812b5a4b87f3a7b4afc1cfebc864f413-B4_mWnLK.woff2 +0 -0
  219. package/dist/client/_assets/chunks/812dfb7f8144d01b3cc9d5ce0b472f40-DxhfKDhh.woff2 +0 -0
  220. package/dist/client/_assets/chunks/84742b1ede4f0bb6d27131298eba21b4-4_QYQ-wj.woff2 +0 -0
  221. package/dist/client/_assets/chunks/8555f0285e3d28e95e2fc0ccccd9caff-CEnfoaRy.woff2 +0 -0
  222. package/dist/client/_assets/chunks/880162ae92cd9e120eb4e4e11fae459d-Cj61LKOn.woff2 +0 -0
  223. package/dist/client/_assets/chunks/8a3c84b0df36f851f5fea75ee8757951-CfHrA-x8.woff2 +0 -0
  224. package/dist/client/_assets/chunks/8b0c8c9f8cfa9fa090d97c5a5efb1f4c-DZc3EGF4.woff2 +0 -0
  225. package/dist/client/_assets/chunks/8c8393bc875f1ee36697a2113f4421ea-CnBugeCf.woff2 +0 -0
  226. package/dist/client/_assets/chunks/8dc035a34c76e6515ca203e2df182588-B1reQPCB.woff2 +0 -0
  227. package/dist/client/_assets/chunks/8e04e64c8f68d292a18d4160fbde8671-DkT7v-sd.woff2 +0 -0
  228. package/dist/client/_assets/chunks/8e6c9bb43afb8cbbff7cf1055e67c9bd-c7wzPI7v.woff2 +0 -0
  229. package/dist/client/_assets/chunks/8eb06109812cb80be44f47b8179c2709-DOaU0xD8.woff2 +0 -0
  230. package/dist/client/_assets/chunks/8f2b960c2823670e94f5b08aa65657e6-CHT-Gunr.woff2 +0 -0
  231. package/dist/client/_assets/chunks/8f89f57230d184f92a36e241874229d7-D6P0FMCn.woff2 +0 -0
  232. package/dist/client/_assets/chunks/904324af375d5fd370af1054355a050e-CHiXmgO7.woff2 +0 -0
  233. package/dist/client/_assets/chunks/90ac4f9d2aa02afdace2843b49fc18bb-CWArmWzF.woff2 +0 -0
  234. package/dist/client/_assets/chunks/90b6f57d77847f512fd11db74fa912f1-DzYbq8hW.woff2 +0 -0
  235. package/dist/client/_assets/chunks/911a2092d64d6d6494b254d819af2b91-C0xmZ-Cu.woff2 +0 -0
  236. package/dist/client/_assets/chunks/913759e6690f9fc0746a20b96f4bdcb4-B0k9hqx0.woff2 +0 -0
  237. package/dist/client/_assets/chunks/9154e26efe532a85a27d80902f5a2d6c-DJQmzPmB.woff2 +0 -0
  238. package/dist/client/_assets/chunks/9180de34b48b325200a97e267befff32-ClJx20hD.woff2 +0 -0
  239. package/dist/client/_assets/chunks/94e7ed67f1557b76fead6b6e456a0415-CjAhhvRn.woff2 +0 -0
  240. package/dist/client/_assets/chunks/95127a92346c04fec1fa81d6295b0a28-DNbeXPGm.woff2 +0 -0
  241. package/dist/client/_assets/chunks/958efb9b2fa2ea0008ffef009885f9f8-CNOwBDS1.woff2 +0 -0
  242. package/dist/client/_assets/chunks/95df3b9f681d9df411c30aea5b24f2e0-Dv7GHFVj.woff2 +0 -0
  243. package/dist/client/_assets/chunks/975af5a496e8d87d821910aa9fe4d598-CG5KDMOI.woff2 +0 -0
  244. package/dist/client/_assets/chunks/97a874bbf55ce89a4ab7cd27c7e938b1-fhost-bL.woff2 +0 -0
  245. package/dist/client/_assets/chunks/9ca9b71010a5faeee7047ef97aeee13b-K6NP7P3o.woff2 +0 -0
  246. package/dist/client/_assets/chunks/9cd0b77920b9d6c64eb686493123fc76-Db6wX5BE.woff2 +0 -0
  247. package/dist/client/_assets/chunks/9de02d745b8e25c6411fb152fb067748-CGxZtq8G.woff2 +0 -0
  248. package/dist/client/_assets/chunks/9eb33a430058d839ebbe2af4b2e0daa9-Cjd-WSds.woff2 +0 -0
  249. package/dist/client/_assets/chunks/9ebd27835ffcbd794e67151ab046ce68-DmxtZVr5.woff2 +0 -0
  250. package/dist/client/_assets/chunks/9f5a73aa8ba417688019d628f334db07-P03uxox_.woff2 +0 -0
  251. package/dist/client/_assets/chunks/9fbc06b2e3ff16b9d705c76db563ef17-XDHSv5Te.woff2 +0 -0
  252. package/dist/client/_assets/chunks/9fd53607094e329fa8e5c785b3ff0f1a-B-9qgOdM.woff2 +0 -0
  253. package/dist/client/_assets/chunks/a077f51cfb5cffb4ff4d8e229c0e9e79-BaPgyZ5H.woff2 +0 -0
  254. package/dist/client/_assets/chunks/a0f0c06d5c7a3ffa97706178cce212a8-DN_NE4Ic.woff2 +0 -0
  255. package/dist/client/_assets/chunks/a38c1830367f784181b6f544b0b11bbd-BmPGDXn1.woff2 +0 -0
  256. package/dist/client/_assets/chunks/a397997b579d3945c9c70a979c17a8ad-DXa54wGz.woff2 +0 -0
  257. package/dist/client/_assets/chunks/a3b929542e6c5a0644b73a7c8a8b6c03-B5i2p-Hj.woff2 +0 -0
  258. package/dist/client/_assets/chunks/a578742770fcd2226e3c45b5b6efdcb0-C1QhQdHg.woff2 +0 -0
  259. package/dist/client/_assets/chunks/a68d9d5027803832bb28e78cdcd04949-D_bwWhwg.woff2 +0 -0
  260. package/dist/client/_assets/chunks/a8857f5d478f101c053ba02d2f223e90-CHOaRPpq.woff2 +0 -0
  261. package/dist/client/_assets/chunks/a904b05966368bcf90b632c7c2e5f76b-BbTXYQVh.woff2 +0 -0
  262. package/dist/client/_assets/chunks/a9cf85e27428c14351d30eac8cbc8d91-DMaVc3-8.woff2 +0 -0
  263. package/dist/client/_assets/chunks/aa0ce6740f301351761a0615cc8b2e99-4RL3pitV.woff2 +0 -0
  264. package/dist/client/_assets/chunks/aa218a2c45f3749537ce876201e5152b-DndHqA-p.woff2 +0 -0
  265. package/dist/client/_assets/chunks/aa28db16818f9eaa8c817f289e1c3270-Drm1Jwhe.woff2 +0 -0
  266. package/dist/client/_assets/chunks/aa64c9953af43ca65832f413895bb433-CR32RteD.woff2 +0 -0
  267. package/dist/client/_assets/chunks/aa96d698491c2540e2dcf7009c65c456-BXlPVR0v.woff2 +0 -0
  268. package/dist/client/_assets/chunks/ada8f0241244c60ec8d3d59ad37f20a5-BFQsznEm.woff2 +0 -0
  269. package/dist/client/_assets/chunks/ae25c41034ddc1a9e0b41f5034c9aa4b-COQJkHOc.woff2 +0 -0
  270. package/dist/client/_assets/chunks/ae289ae3f8cdb54a3a6c07174517afec-DFGIoTY6.woff2 +0 -0
  271. package/dist/client/_assets/chunks/ae401fb4db80d5ff5cd3f8d9bc811070-Byt9AwoT.woff2 +0 -0
  272. package/dist/client/_assets/chunks/b02dfa2aa52cbdb1b2f11a9f44335469-DVWsuPSL.woff2 +0 -0
  273. package/dist/client/_assets/chunks/b0ab3a7f319ce6dd3c9a4de2674e7c72-CksmA6Hr.woff2 +0 -0
  274. package/dist/client/_assets/chunks/b159deb135e9946eea0572d52778170b-BZrtOTn0.woff2 +0 -0
  275. package/dist/client/_assets/chunks/b2e326f7f9b807451bf9c745df747efe-BvlONCPB.woff2 +0 -0
  276. package/dist/client/_assets/chunks/b341de0bc0bfe194a6c28dcfb566029e-DJAj3r6U.woff2 +0 -0
  277. package/dist/client/_assets/chunks/b846c293981ca5429eabaa967f222f26-GS-TAaO6.woff2 +0 -0
  278. package/dist/client/_assets/chunks/b91450304d9ac44f5c6e0da0792e055d-vS0S7kl0.woff2 +0 -0
  279. package/dist/client/_assets/chunks/baa325551b381c5e035ef143e56d4abe-DJg3GhKJ.woff2 +0 -0
  280. package/dist/client/_assets/chunks/bc3f0cb8b55ee11d32b94ca488976f8d-Dp5KZMSD.woff2 +0 -0
  281. package/dist/client/_assets/chunks/bcb3307527d6d0033bf0f17660b91e71-BMgRW_eW.woff2 +0 -0
  282. package/dist/client/_assets/chunks/be64f9379412876e00fd3a0bfa6b6fe9-BJ3FMWFM.woff2 +0 -0
  283. package/dist/client/_assets/chunks/befed8a4fa817773fa7109db6fe07f56-3sAmDsOO.woff2 +0 -0
  284. package/dist/client/_assets/chunks/bf1acc86e17b4229c548828a9d6f455d-JNuPCGDM.woff2 +0 -0
  285. package/dist/client/_assets/chunks/c09ee2b219982f8d46ad9968b7e6e0ba-qVb8yJHb.woff2 +0 -0
  286. package/dist/client/_assets/chunks/c0c7836749e585cee24ab2f8457c5b01-Ds0003oT.woff2 +0 -0
  287. package/dist/client/_assets/chunks/c2ac4ef1860812036ca2b8c4e4089bdc-DsziO8Du.woff2 +0 -0
  288. package/dist/client/_assets/chunks/c31019c08bd22464f7a88f090281404c-CUC0jMW1.woff2 +0 -0
  289. package/dist/client/_assets/chunks/c33c59feccf391f0c5f1f5d24e36d1fe-BXTVggad.woff2 +0 -0
  290. package/dist/client/_assets/chunks/c39ec937c6a8d124e8b68cf829ea5ad4-CarrkdmQ.woff2 +0 -0
  291. package/dist/client/_assets/chunks/c3fbc1f2557c343863a10698f8c966a2-BiOC1yo2.woff2 +0 -0
  292. package/dist/client/_assets/chunks/c3fd21315345ae541f6e98067059fa19-rHtO7vK5.woff2 +0 -0
  293. package/dist/client/_assets/chunks/c568a16e3168ceb1f191b70022c492ea-BpAiyU80.woff2 +0 -0
  294. package/dist/client/_assets/chunks/c57ee3b49b7e45b995539a6b2c51f138-IHDXeVRU.woff2 +0 -0
  295. package/dist/client/_assets/chunks/c5c1c0be944ea39a3f50a02d32f5b759-DAwbrvTL.woff2 +0 -0
  296. package/dist/client/_assets/chunks/c5e66d60be3375835bbd8d6b797f6eac-DWp4tpvN.woff2 +0 -0
  297. package/dist/client/_assets/chunks/c5f1075caf6d1344ee720de85114a521-DTTbeLhd.woff2 +0 -0
  298. package/dist/client/_assets/chunks/c82fd9456d7465b5e5bd3659e9b14c55-Cs6h6hE6.woff2 +0 -0
  299. package/dist/client/_assets/chunks/c90b7b65d2b9696fbf3a506738f94d68-DsvgiuQQ.woff2 +0 -0
  300. package/dist/client/_assets/chunks/cae29b3f8951eaf20d2f61c2206e28d9-CoHXmhmG.woff2 +0 -0
  301. package/dist/client/_assets/chunks/cba6ad3981cb7861428d4be169ee8124-CBdFsPAF.woff2 +0 -0
  302. package/dist/client/_assets/chunks/cc26525aa2af1f0b929af32ce50a7fba-D55LVtW9.woff2 +0 -0
  303. package/dist/client/_assets/chunks/cd0e7b51eddb22a77a09b025c0281434-ByI_TSTq.woff2 +0 -0
  304. package/dist/client/_assets/chunks/cd10a3af2133805d8c92104d1ee6ff18-Dtr5mBXe.woff2 +0 -0
  305. package/dist/client/_assets/chunks/cd6d074f3957d58bac58437fc97e5e33-CrVZuSbw.woff2 +0 -0
  306. package/dist/client/_assets/chunks/cd75ca47da9ae4c0899e37d4c543319b-DXU1s4Ja.woff2 +0 -0
  307. package/dist/client/_assets/chunks/cef249b6d013fb0cc0d574176bc23811-BL7WfNHA.woff2 +0 -0
  308. package/dist/client/_assets/chunks/d043b8d7a48bb0ac59ee1f1477d88eee-d5CwY2I8.woff2 +0 -0
  309. package/dist/client/_assets/chunks/d0bd387fda28e58d3c9b3efa2468dd8a-CjDFowFS.woff2 +0 -0
  310. package/dist/client/_assets/chunks/d12ce1d8445213317f9163283e58a05d-4RtGQnak.woff2 +0 -0
  311. package/dist/client/_assets/chunks/d15a3317942b7d31978a759fbf2222c8-DQ9QFQdS.woff2 +0 -0
  312. package/dist/client/_assets/chunks/d28fb13acf9ced9f0657fd4012c81cd2-D8Bgk6sd.woff2 +0 -0
  313. package/dist/client/_assets/chunks/d320b000b5978c7251148a6a154741b8-CcGZjeFx.woff2 +0 -0
  314. package/dist/client/_assets/chunks/d3714e6b90de8e2085dfb2514464dd6a-8LsVAcmb.woff2 +0 -0
  315. package/dist/client/_assets/chunks/d3beff96216c8af1aa79246476b6a323-hQysoMK3.woff2 +0 -0
  316. package/dist/client/_assets/chunks/d3e311f30c811dc339c262a79a51877e-CkUTbS74.woff2 +0 -0
  317. package/dist/client/_assets/chunks/d51f4cdc83711e510f5d25f03235597e-BY3VAGvr.woff2 +0 -0
  318. package/dist/client/_assets/chunks/d5df4a8dfd4328c67d933b3912c6ad0f-A2fccswC.woff2 +0 -0
  319. package/dist/client/_assets/chunks/d740dc2e854aaa7b3dcdd3ed25455eeb-Cw8LrB06.woff2 +0 -0
  320. package/dist/client/_assets/chunks/d8325ba7ae651bc30440905bd67b95f1-Dg8M_ijr.woff2 +0 -0
  321. package/dist/client/_assets/chunks/da13b136efb1d1e4c76575af8f79a698-BbfzLGVB.woff2 +0 -0
  322. package/dist/client/_assets/chunks/da2cf0ec56bf69374ee37764c7e3ea3d-fyVBKE2g.woff2 +0 -0
  323. package/dist/client/_assets/chunks/da93ae099ff3b7aae27b3f674d3fc721-BDG5ywtd.woff2 +0 -0
  324. package/dist/client/_assets/chunks/daf62255dd60679946f28c442ca62533-BnIjYSn3.woff2 +0 -0
  325. package/dist/client/_assets/chunks/dccda6a2e2db3b530788bdfa2acd1979-ByL7eZcn.woff2 +0 -0
  326. package/dist/client/_assets/chunks/dd01a1035345f6921a48525b8ce08f06-vkP6t4fC.woff2 +0 -0
  327. package/dist/client/_assets/chunks/e2204cf85edcb96c5de5c3dcf240da9d-Ch3zKjD-.woff2 +0 -0
  328. package/dist/client/_assets/chunks/e264213b9e102dabc603adb6e4fda5e6-CyxmQBHB.woff2 +0 -0
  329. package/dist/client/_assets/chunks/e3e913e145ddcd9323b2a0972967feb6-3sjEdzck.woff2 +0 -0
  330. package/dist/client/_assets/chunks/e4fb59479cedc87ba79785590bf861ca-sx7RZ8Vu.woff2 +0 -0
  331. package/dist/client/_assets/chunks/e5d00355f73293d40b61299459d17ca5-D8Vt3D-z.woff2 +0 -0
  332. package/dist/client/_assets/chunks/e647b8d2efc501c0cc0e407249cc7135-BrSW9DU4.woff2 +0 -0
  333. package/dist/client/_assets/chunks/e6e60ffb2ebd1828628764b507060aea-Dqyf48QC.woff2 +0 -0
  334. package/dist/client/_assets/chunks/e7c7ef3669ae48c0a736f06ca471e1d7-DNCebEB-.woff2 +0 -0
  335. package/dist/client/_assets/chunks/e81a742cacef744130c40de1b90837d8-DtAIFBk_.woff2 +0 -0
  336. package/dist/client/_assets/chunks/e8b755172122d1d0a5dd453e96b0ff24-BNTSyPYU.woff2 +0 -0
  337. package/dist/client/_assets/chunks/e99280299c305402eaa5271a3e36c49b-BpqXwl7U.woff2 +0 -0
  338. package/dist/client/_assets/chunks/e9c66b085052ece66bfadf45f711d3e1-DVza3Uob.woff2 +0 -0
  339. package/dist/client/_assets/chunks/ed7c6dafaa6d8bcf015ef0ca574837df-C7bcndJl.woff2 +0 -0
  340. package/dist/client/_assets/chunks/f00eb499abb94fa7b799d6d8c9b050e9-D_XvVTI9.woff2 +0 -0
  341. package/dist/client/_assets/chunks/f2900a1d30c3a33129f4e2225669bd0e-in84LilX.woff2 +0 -0
  342. package/dist/client/_assets/chunks/f2fb1f1fbf7e44afb53c672ec286a22e-tFk0sTQc.woff2 +0 -0
  343. package/dist/client/_assets/chunks/f372129c60aaece937cf7b91ee75c9b8-DiP_JYOt.woff2 +0 -0
  344. package/dist/client/_assets/chunks/f5237486197aeff59341a1ff38b8eff8-D_uuqrNZ.woff2 +0 -0
  345. package/dist/client/_assets/chunks/f553d54ef931066712d8f3f0ce018e1b-D8K983U5.woff2 +0 -0
  346. package/dist/client/_assets/chunks/f5d7487963d43c89da63aaf10f2e6fb7-B7GIrfPU.woff2 +0 -0
  347. package/dist/client/_assets/chunks/f649cba8e14c33d6bf2265483b14b895-Nm5wSov8.woff2 +0 -0
  348. package/dist/client/_assets/chunks/f6839df1bf7cb4dc8d27e5ea55bbe633-BIjWKN84.woff2 +0 -0
  349. package/dist/client/_assets/chunks/f6b7304e028980f77a7f7007bb540abd-CIlBt6sf.woff2 +0 -0
  350. package/dist/client/_assets/chunks/f75496953a40ff241178240209f56990-MOSKd67d.woff2 +0 -0
  351. package/dist/client/_assets/chunks/f7d36ffff7a75c9c6216d576a57dd00d-CaXQA5A8.woff2 +0 -0
  352. package/dist/client/_assets/chunks/f7f3f63e7a149cd89eccab3b52171d05-oNOOSdDV.woff2 +0 -0
  353. package/dist/client/_assets/chunks/f8db8bef0a6e1178835d350ae0d384a1-ZAiKM369.woff2 +0 -0
  354. package/dist/client/_assets/chunks/f92d74d1d217d21b39075ff23f79f7fd-BlTe0_IX.woff2 +0 -0
  355. package/dist/client/_assets/chunks/f9d6d981d8b87b3e469027277f585741-DQqIv77a.woff2 +0 -0
  356. package/dist/client/_assets/chunks/fa7d3b99744d7f2dc9e00864a97a62d6-Pfww07DK.woff2 +0 -0
  357. package/dist/client/_assets/chunks/fa8ed469ef290bfeb571418fe0abb628-D5v-Z7kY.woff2 +0 -0
  358. package/dist/client/_assets/chunks/fb0e90665980954719c2eb685b130bc0-CiWRvo58.woff2 +0 -0
  359. package/dist/client/_assets/chunks/fb4649a82c50620773d79820e2e5ff13-dw14czgx.woff2 +0 -0
  360. package/dist/client/_assets/chunks/fb61b690208eff56e6d8432951270901-CUAnVvWa.woff2 +0 -0
  361. package/dist/client/_assets/chunks/fb9402d6c6357a825affc402f14d5a7e-CIp4G53L.woff2 +0 -0
  362. package/dist/client/_assets/chunks/fcc41f6a067ddd658bba5c9dff234a32-BtZTNo2r.woff2 +0 -0
  363. package/dist/client/_assets/chunks/fd6ad889fcf3583bd9b0b6db53aad434-DqiKyYmz.woff2 +0 -0
  364. package/dist/client/_assets/chunks/ff0937ad63cda71ff420945ead55ab4d-CXGPdnOz.woff2 +0 -0
  365. package/dist/client/_assets/chunks/heic-to-XcUDQvtx.js +1 -0
  366. package/dist/client/_assets/chunks/literata-cyrillic-ext-wght-normal-CGKlZYBf.woff2 +0 -0
  367. package/dist/client/_assets/chunks/literata-cyrillic-wght-normal-DLqwHbi6.woff2 +0 -0
  368. package/dist/client/_assets/chunks/literata-greek-ext-wght-normal-e3e57Shi.woff2 +0 -0
  369. package/dist/client/_assets/chunks/literata-greek-wght-normal-CO1l-giJ.woff2 +0 -0
  370. package/dist/client/_assets/chunks/literata-latin-ext-wght-normal-BnEbWgdZ.woff2 +0 -0
  371. package/dist/client/_assets/chunks/literata-latin-wght-normal-DLxlUchJ.woff2 +0 -0
  372. package/dist/client/_assets/chunks/literata-vietnamese-wght-normal-LcSrhZ7T.woff2 +0 -0
  373. package/dist/client/_assets/chunks/module-ChVQstFd.js +716 -0
  374. package/dist/client/_assets/chunks/native-CR5HLOyf.js +1 -0
  375. package/dist/client/_assets/chunks/news-cycle-cyrillic-400-normal-9BSXki1I.woff +0 -0
  376. package/dist/client/_assets/chunks/news-cycle-cyrillic-400-normal-CUYmhJri.woff2 +0 -0
  377. package/dist/client/_assets/chunks/news-cycle-cyrillic-ext-400-normal-CKyb9yaQ.woff2 +0 -0
  378. package/dist/client/_assets/chunks/news-cycle-cyrillic-ext-400-normal-Llxpm7uO.woff +0 -0
  379. package/dist/client/_assets/chunks/news-cycle-greek-400-normal-BiAZ91Zl.woff2 +0 -0
  380. package/dist/client/_assets/chunks/news-cycle-greek-400-normal-D88oNngQ.woff +0 -0
  381. package/dist/client/_assets/chunks/news-cycle-greek-ext-400-normal-Ckclqjrc.woff2 +0 -0
  382. package/dist/client/_assets/chunks/news-cycle-greek-ext-400-normal-oz-bBDWj.woff +0 -0
  383. package/dist/client/_assets/chunks/news-cycle-latin-400-normal-BLgpQ3uo.woff +0 -0
  384. package/dist/client/_assets/chunks/news-cycle-latin-400-normal-BgW97ttO.woff2 +0 -0
  385. package/dist/client/_assets/chunks/news-cycle-latin-ext-400-normal-DmDltzLi.woff2 +0 -0
  386. package/dist/client/_assets/chunks/news-cycle-latin-ext-400-normal-lewDoXxP.woff +0 -0
  387. package/dist/client/_assets/chunks/news-cycle-vietnamese-400-normal-QPSuG-pc.woff +0 -0
  388. package/dist/client/_assets/chunks/news-cycle-vietnamese-400-normal-t2mxI9x9.woff2 +0 -0
  389. package/dist/client/_assets/chunks/newsreader-latin-ext-wght-normal-C-3rgBeH.woff2 +0 -0
  390. package/dist/client/_assets/chunks/newsreader-latin-wght-normal-CCVVNp6i.woff2 +0 -0
  391. package/dist/client/_assets/chunks/newsreader-vietnamese-wght-normal-Czsa-EzN.woff2 +0 -0
  392. package/dist/client/_assets/chunks/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  393. package/dist/client/_assets/chunks/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  394. package/dist/client/_assets/chunks/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  395. package/dist/client/_assets/chunks/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  396. package/dist/client/_assets/chunks/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  397. package/dist/client/_assets/chunks/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  398. package/dist/client/_assets/chunks/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  399. package/dist/client/_assets/chunks/url-CG0eolsk.js +1 -0
  400. package/dist/client/_assets/client-auth.js +3251 -0
  401. package/dist/client/_assets/client-cjk-tc.css +1 -0
  402. package/dist/client/_assets/client-cjk.css +1 -0
  403. package/dist/client/_assets/client.css +2 -0
  404. package/dist/client/_assets/client.js +380 -0
  405. package/dist/index.js +2 -20359
  406. package/dist/node.js +479 -0
  407. package/package.json +67 -40
  408. package/src/__tests__/bin/uploads-cleanup.test.ts +77 -0
  409. package/src/__tests__/export-service.test.ts +550 -0
  410. package/src/__tests__/helpers/app.ts +60 -27
  411. package/src/__tests__/helpers/db.ts +14 -24
  412. package/src/__tests__/helpers/lingui-core-macro-mock.ts +15 -2
  413. package/src/__tests__/import-site-command.test.ts +406 -0
  414. package/src/__tests__/site-localize-media.test.ts +150 -0
  415. package/src/__tests__/site-media-parser.test.ts +216 -0
  416. package/src/app.tsx +272 -175
  417. package/src/assets/branding/generated/README.txt +15 -0
  418. package/src/assets/branding/generated/jant-apple-touch-icon.png +0 -0
  419. package/src/assets/branding/generated/jant-brand-assets.zip +0 -0
  420. package/src/assets/branding/generated/jant-brand-tile-512.png +0 -0
  421. package/src/assets/branding/generated/jant-brand-tile.svg +1 -0
  422. package/src/assets/branding/generated/jant-circle-tile-512.png +0 -0
  423. package/src/assets/branding/generated/jant-circle-tile.svg +1 -0
  424. package/src/assets/branding/generated/jant-favicon.ico +0 -0
  425. package/src/assets/branding/generated/jant-logo-negative.svg +1 -0
  426. package/src/assets/branding/generated/jant-logo-positive-512.png +0 -0
  427. package/src/assets/branding/generated/jant-logo-positive.svg +1 -0
  428. package/src/assets/branding/generated/jant-social-preview.png +0 -0
  429. package/src/assets/branding/generated/jant-square-tile-512.png +0 -0
  430. package/src/assets/branding/generated/jant-square-tile.svg +1 -0
  431. package/src/assets/branding/jant-logo-positive.svg +3 -0
  432. package/src/auth.ts +47 -6
  433. package/src/client/__tests__/avatar-upload.test.ts +186 -0
  434. package/src/client/__tests__/collection-form-bridge.test.ts +127 -0
  435. package/src/client/__tests__/collection-page-actions.test.ts +112 -0
  436. package/src/client/__tests__/collection-sort-menu.test.ts +78 -0
  437. package/src/client/__tests__/compose-bridge.test.ts +260 -0
  438. package/src/client/__tests__/compose-discovery.test.ts +155 -0
  439. package/src/client/__tests__/compose-shortcuts.test.ts +133 -0
  440. package/src/client/__tests__/confirm.test.ts +98 -0
  441. package/src/client/__tests__/custom-url-menu.test.ts +104 -0
  442. package/src/client/__tests__/form-enter-submit.test.ts +110 -0
  443. package/src/client/__tests__/post-form-bridge.test.ts +82 -0
  444. package/src/client/__tests__/sortable-list.test.ts +91 -0
  445. package/src/client/__tests__/toast.test.ts +46 -0
  446. package/src/client/archive-nav.js +1 -3
  447. package/src/client/avatar-upload.ts +66 -25
  448. package/src/client/collection-form-bridge.ts +32 -15
  449. package/src/client/collection-page-actions.ts +160 -0
  450. package/src/client/collection-sort-menu.ts +105 -0
  451. package/src/client/components/__tests__/jant-collection-form.test.ts +199 -51
  452. package/src/client/components/__tests__/jant-collection-sidebar.test.ts +250 -0
  453. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2507 -342
  454. package/src/client/components/__tests__/jant-compose-editor.test.ts +484 -20
  455. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +298 -0
  456. package/src/client/components/__tests__/jant-post-form.test.ts +9 -8
  457. package/src/client/components/__tests__/jant-post-menu.test.ts +325 -0
  458. package/src/client/components/__tests__/jant-settings-avatar.test.ts +27 -3
  459. package/src/client/components/__tests__/jant-settings-general.test.ts +312 -84
  460. package/src/client/components/collection-manager-types.ts +51 -0
  461. package/src/client/components/collection-types.ts +12 -10
  462. package/src/client/components/compose-types.ts +80 -13
  463. package/src/client/components/jant-collection-form.ts +338 -509
  464. package/src/client/components/jant-collection-sidebar.ts +461 -463
  465. package/src/client/components/jant-compose-dialog.ts +1566 -479
  466. package/src/client/components/jant-compose-editor.ts +531 -276
  467. package/src/client/components/jant-compose-fullscreen.ts +184 -112
  468. package/src/client/components/jant-confirm-dialog.ts +249 -0
  469. package/src/client/components/jant-nav-manager.ts +79 -103
  470. package/src/client/components/jant-post-form.ts +4 -1
  471. package/src/client/components/jant-post-menu.ts +583 -306
  472. package/src/client/components/jant-settings-avatar.ts +15 -9
  473. package/src/client/components/jant-settings-general.ts +444 -137
  474. package/src/client/components/nav-manager-types.ts +6 -3
  475. package/src/client/components/post-form-template.ts +2 -9
  476. package/src/client/components/post-form-types.ts +2 -4
  477. package/src/client/components/settings-types.ts +34 -2
  478. package/src/client/compose-bridge.ts +297 -171
  479. package/src/client/compose-discovery-bridge.ts +9 -0
  480. package/src/client/compose-discovery.ts +254 -0
  481. package/src/client/compose-launch.ts +98 -0
  482. package/src/client/compose-shortcuts.ts +61 -0
  483. package/src/client/confirm.ts +40 -0
  484. package/src/client/custom-url-menu.ts +128 -0
  485. package/src/client/form-enter-submit.ts +87 -0
  486. package/src/client/json.ts +37 -0
  487. package/src/client/multipart-upload.ts +51 -18
  488. package/src/client/post-form-bridge.ts +20 -14
  489. package/src/client/runtime-paths.ts +72 -0
  490. package/src/client/settings-bridge.ts +54 -10
  491. package/src/client/site-header-nav.js +84 -0
  492. package/src/client/sortable-list.ts +152 -0
  493. package/src/client/thread-context.ts +11 -5
  494. package/src/client/tiptap/__tests__/floating-position.test.ts +74 -0
  495. package/src/client/tiptap/__tests__/link-toolbar.test.ts +94 -0
  496. package/src/client/tiptap/__tests__/paste-media.test.ts +39 -0
  497. package/src/client/tiptap/bubble-menu.ts +98 -13
  498. package/src/client/tiptap/create-editor.ts +6 -0
  499. package/src/client/tiptap/extensions.ts +15 -4
  500. package/src/client/tiptap/floating-position.ts +167 -0
  501. package/src/client/tiptap/inline-image-upload.ts +73 -0
  502. package/src/client/tiptap/link-input-rules.ts +56 -0
  503. package/src/client/tiptap/link-toolbar.ts +60 -14
  504. package/src/client/tiptap/paste-media.ts +120 -0
  505. package/src/client/tiptap/slash-commands.ts +205 -52
  506. package/src/client/tiptap/toolbar-mode.ts +31 -0
  507. package/src/client/toast.ts +136 -16
  508. package/src/client/types/sortablejs.d.ts +4 -4
  509. package/src/client/upload-session.ts +270 -0
  510. package/src/client/upload-with-metadata.ts +10 -17
  511. package/src/client-auth.ts +38 -0
  512. package/src/client.ts +10 -29
  513. package/src/db/__tests__/d1-query.test.ts +84 -0
  514. package/src/db/__tests__/demo-canonical-snapshot.test.ts +78 -0
  515. package/src/db/__tests__/demo-env-loader.test.ts +114 -0
  516. package/src/db/__tests__/dialect.test.ts +42 -0
  517. package/src/db/__tests__/migration-artifacts.test.ts +82 -0
  518. package/src/db/__tests__/migration-rehearsal.test.ts +73 -0
  519. package/src/db/__tests__/migration-runner.test.ts +72 -0
  520. package/src/db/__tests__/migrations.test.ts +293 -8
  521. package/src/db/__tests__/r2-query.test.ts +102 -0
  522. package/src/db/__tests__/wrangler-config.test.ts +86 -0
  523. package/src/db/backfills/0000_normalize_legacy_time_zone_values.sql +38 -0
  524. package/src/db/backfills/README.md +11 -0
  525. package/src/db/dialect.ts +71 -0
  526. package/src/db/index.ts +84 -2
  527. package/src/db/migrations/0000_baseline.sql +35 -57
  528. package/src/db/migrations/0002_site_aware_core.sql +354 -0
  529. package/src/db/migrations/0003_fts_site_aware.sql +38 -0
  530. package/src/db/migrations/0004_perpetual_eternity.sql +29 -0
  531. package/src/db/migrations/meta/0000_snapshot.json +134 -138
  532. package/src/db/migrations/meta/0001_snapshot.json +135 -139
  533. package/src/db/migrations/meta/0002_snapshot.json +1717 -0
  534. package/src/db/migrations/meta/0003_snapshot.json +1717 -0
  535. package/src/db/migrations/meta/0004_snapshot.json +1897 -0
  536. package/src/db/migrations/meta/_journal.json +23 -2
  537. package/src/db/migrations/pg/0000_pg_site_aware_baseline.sql +306 -0
  538. package/src/db/migrations/pg/0001_pg_search_support.sql +8 -0
  539. package/src/db/migrations/pg/0002_breezy_lockjaw.sql +29 -0
  540. package/src/db/migrations/pg/README.md +8 -0
  541. package/src/db/migrations/pg/meta/0000_snapshot.json +2195 -0
  542. package/src/db/migrations/pg/meta/0001_snapshot.json +2248 -0
  543. package/src/db/migrations/pg/meta/0002_snapshot.json +2476 -0
  544. package/src/db/migrations/pg/meta/_journal.json +27 -0
  545. package/src/db/pg/__tests__/node.test.ts +58 -0
  546. package/src/db/pg/node.ts +151 -0
  547. package/src/db/pg/schema.ts +739 -0
  548. package/src/db/raw-query.ts +15 -0
  549. package/src/db/rehearsal-fixtures/demo-current.json +25 -0
  550. package/src/db/rehearsal-fixtures/demo-current.sql +142 -0
  551. package/src/db/schema-bundle.ts +12 -0
  552. package/src/db/schema.ts +309 -128
  553. package/src/db/sqlite/schema.ts +1 -0
  554. package/src/i18n/__tests__/context.test.tsx +35 -0
  555. package/src/i18n/context.tsx +10 -2
  556. package/src/i18n/locales/en.po +1778 -231
  557. package/src/i18n/locales/en.ts +1 -1
  558. package/src/i18n/locales/glossary.zh-Hans.yml +116 -0
  559. package/src/i18n/locales/glossary.zh-Hant.yml +116 -0
  560. package/src/i18n/locales/zh-Hans.po +1819 -272
  561. package/src/i18n/locales/zh-Hans.ts +1 -1
  562. package/src/i18n/locales/zh-Hant.po +1823 -276
  563. package/src/i18n/locales/zh-Hant.ts +1 -1
  564. package/src/index.ts +4 -0
  565. package/src/lib/__tests__/asset-path.test.ts +54 -0
  566. package/src/lib/__tests__/blurhash-placeholder.test.ts +25 -1
  567. package/src/lib/__tests__/collection-sort.test.ts +45 -0
  568. package/src/lib/__tests__/constants.test.ts +1 -0
  569. package/src/lib/__tests__/favicon.test.ts +1 -1
  570. package/src/lib/__tests__/feed.test.ts +147 -0
  571. package/src/lib/__tests__/hosted-control-plane.test.ts +106 -0
  572. package/src/lib/__tests__/hosted-domain.test.ts +78 -0
  573. package/src/lib/__tests__/hosted-signin.test.ts +103 -0
  574. package/src/lib/__tests__/icons.test.ts +14 -172
  575. package/src/lib/__tests__/image.test.ts +23 -4
  576. package/src/lib/__tests__/jant-branding.test.ts +160 -0
  577. package/src/lib/__tests__/markdown-to-tiptap.test.ts +27 -0
  578. package/src/lib/__tests__/page-title.test.ts +20 -0
  579. package/src/lib/__tests__/password.test.ts +60 -0
  580. package/src/lib/__tests__/post-display.test.ts +99 -0
  581. package/src/lib/__tests__/post-meta.test.ts +126 -0
  582. package/src/lib/__tests__/public-storage.test.ts +75 -0
  583. package/src/lib/__tests__/resolve-config.test.ts +156 -28
  584. package/src/lib/__tests__/schemas.test.ts +268 -22
  585. package/src/lib/__tests__/search-snippet.test.ts +43 -0
  586. package/src/lib/__tests__/site-resolution.test.ts +39 -0
  587. package/src/lib/__tests__/slug-format.test.ts +46 -0
  588. package/src/lib/__tests__/startup-config.test.ts +87 -0
  589. package/src/lib/__tests__/storage.test.ts +218 -1
  590. package/src/lib/__tests__/theme.test.ts +50 -13
  591. package/src/lib/__tests__/time.test.ts +44 -0
  592. package/src/lib/__tests__/timeline.test.ts +410 -4
  593. package/src/lib/__tests__/timezones.test.ts +51 -18
  594. package/src/lib/__tests__/tiptap-to-markdown.test.ts +23 -0
  595. package/src/lib/__tests__/url.test.ts +79 -1
  596. package/src/lib/__tests__/view.test.ts +74 -13
  597. package/src/lib/asset-path.ts +87 -0
  598. package/src/lib/blurhash-placeholder.ts +56 -1
  599. package/src/lib/collection-sort.ts +61 -0
  600. package/src/lib/confirm.ts +37 -0
  601. package/src/lib/constants.ts +3 -6
  602. package/src/lib/crypto.ts +47 -0
  603. package/src/lib/env.ts +245 -0
  604. package/src/lib/errors.ts +25 -9
  605. package/src/lib/favicon.ts +2 -1
  606. package/src/lib/featured-icons.ts +33 -0
  607. package/src/lib/feed.ts +73 -24
  608. package/src/lib/hosted-control-plane-sync.ts +27 -0
  609. package/src/lib/hosted-control-plane.ts +99 -0
  610. package/src/lib/hosted-domain-check.ts +98 -0
  611. package/src/lib/hosted-domain.ts +61 -0
  612. package/src/lib/hosted-signin.ts +115 -0
  613. package/src/lib/hosted-sso.ts +110 -0
  614. package/src/lib/icons.ts +10 -175
  615. package/src/lib/ids.ts +54 -0
  616. package/src/lib/image.ts +18 -7
  617. package/src/lib/jant-branding-generated.ts +947 -0
  618. package/src/lib/jant-branding.ts +520 -0
  619. package/src/lib/markdown-to-tiptap.ts +57 -0
  620. package/src/lib/media-helpers.ts +6 -2
  621. package/src/lib/navigation.ts +9 -2
  622. package/src/lib/page-title.ts +23 -0
  623. package/src/lib/pagination.ts +34 -0
  624. package/src/lib/password.ts +164 -0
  625. package/src/lib/post-display.ts +162 -0
  626. package/src/lib/post-meta.ts +95 -0
  627. package/src/lib/public-storage.ts +57 -0
  628. package/src/lib/render.tsx +22 -0
  629. package/src/lib/resolve-config.ts +78 -27
  630. package/src/lib/schemas.ts +264 -73
  631. package/src/lib/search-snippet.ts +110 -8
  632. package/src/lib/site-resolution.ts +28 -0
  633. package/src/lib/slug-format.ts +84 -0
  634. package/src/lib/slug.ts +1 -1
  635. package/src/lib/startup-config.ts +244 -0
  636. package/src/lib/storage.ts +722 -28
  637. package/src/lib/theme.ts +13 -6
  638. package/src/lib/time.ts +59 -15
  639. package/src/lib/timeline.ts +386 -62
  640. package/src/lib/timezones.ts +120 -50
  641. package/src/lib/tiptap-render.ts +1 -3
  642. package/src/lib/tiptap-to-markdown.ts +49 -0
  643. package/src/lib/upload.ts +216 -11
  644. package/src/lib/url.ts +210 -0
  645. package/src/lib/view.ts +87 -31
  646. package/src/middleware/__tests__/auth.test.ts +216 -14
  647. package/src/middleware/__tests__/error-handler.test.ts +37 -0
  648. package/src/middleware/__tests__/onboarding.test.ts +9 -0
  649. package/src/middleware/__tests__/secure-headers.test.ts +111 -0
  650. package/src/middleware/auth.ts +156 -28
  651. package/src/middleware/config.ts +29 -9
  652. package/src/middleware/error-handler.ts +16 -1
  653. package/src/middleware/onboarding.ts +15 -6
  654. package/src/middleware/secure-headers.ts +136 -25
  655. package/src/node/__tests__/cli-db-execute-file.test.ts +109 -0
  656. package/src/node/__tests__/cli-deploy.test.ts +124 -0
  657. package/src/node/__tests__/cli-export.test.ts +66 -0
  658. package/src/node/__tests__/cli-reset-password.test.ts +91 -0
  659. package/src/node/__tests__/cli-runtime-target.test.ts +37 -0
  660. package/src/node/__tests__/cli-site-selection.test.ts +133 -0
  661. package/src/node/__tests__/cli-site-snapshot.test.ts +636 -0
  662. package/src/node/__tests__/cli-site-token-env.test.ts +137 -0
  663. package/src/node/__tests__/runtime.test.ts +346 -0
  664. package/src/node/index.ts +18 -0
  665. package/src/node/request-handler.ts +668 -0
  666. package/src/node/runtime.ts +88 -0
  667. package/src/preset.css +73 -16
  668. package/src/routes/__tests__/compose.test.ts +77 -4
  669. package/src/routes/api/__tests__/attachments.test.ts +92 -0
  670. package/src/routes/api/__tests__/collections.test.ts +138 -24
  671. package/src/routes/api/__tests__/nav-items.test.ts +81 -20
  672. package/src/routes/api/__tests__/posts.test.ts +315 -57
  673. package/src/routes/api/__tests__/search.test.ts +25 -1
  674. package/src/routes/api/__tests__/settings.test.ts +380 -2
  675. package/src/routes/api/__tests__/upload-multipart.test.ts +216 -26
  676. package/src/routes/api/__tests__/uploads.test.ts +420 -0
  677. package/src/routes/api/attachments.ts +24 -0
  678. package/src/routes/api/collections.ts +59 -28
  679. package/src/routes/api/custom-urls.ts +6 -5
  680. package/src/routes/api/export.ts +37 -7
  681. package/src/routes/api/internal/__tests__/api-tokens.test.ts +55 -0
  682. package/src/routes/api/internal/__tests__/sites.test.ts +472 -0
  683. package/src/routes/api/internal/__tests__/uploads.test.ts +131 -0
  684. package/src/routes/api/internal/api-tokens.ts +17 -0
  685. package/src/routes/api/internal/sites.ts +241 -0
  686. package/src/routes/api/internal/uploads.ts +37 -0
  687. package/src/routes/api/nav-items.ts +24 -15
  688. package/src/routes/api/posts.ts +222 -74
  689. package/src/routes/api/search.ts +50 -9
  690. package/src/routes/api/settings.ts +141 -7
  691. package/src/routes/api/upload-multipart.ts +105 -16
  692. package/src/routes/api/upload.ts +90 -21
  693. package/src/routes/api/uploads.ts +247 -0
  694. package/src/routes/auth/__tests__/dev.test.ts +135 -0
  695. package/src/routes/auth/__tests__/hosted-sso.test.ts +136 -0
  696. package/src/routes/auth/__tests__/setup.test.ts +73 -49
  697. package/src/routes/auth/dev.ts +72 -0
  698. package/src/routes/auth/hosted-sso.ts +64 -0
  699. package/src/routes/auth/reset.tsx +41 -7
  700. package/src/routes/auth/setup.tsx +37 -38
  701. package/src/routes/auth/signin.tsx +36 -6
  702. package/src/routes/compose.tsx +25 -38
  703. package/src/routes/dash/__tests__/font-theme.test.ts +45 -34
  704. package/src/routes/dash/__tests__/settings-avatar.test.ts +24 -11
  705. package/src/routes/dash/custom-urls.tsx +245 -67
  706. package/src/routes/dash/settings.tsx +530 -139
  707. package/src/routes/feed/__tests__/rss.test.ts +200 -51
  708. package/src/routes/feed/__tests__/sitemap.test.ts +64 -0
  709. package/src/routes/feed/rss.ts +112 -48
  710. package/src/routes/feed/sitemap.ts +16 -6
  711. package/src/routes/hosted/__tests__/domain-check.test.ts +94 -0
  712. package/src/routes/hosted/domain-check.ts +39 -0
  713. package/src/routes/pages/__tests__/collections.test.ts +27 -21
  714. package/src/routes/pages/__tests__/featured.test.ts +9 -2
  715. package/src/routes/pages/archive.tsx +79 -33
  716. package/src/routes/pages/brand.tsx +119 -0
  717. package/src/routes/pages/collection.tsx +143 -59
  718. package/src/routes/pages/collections.tsx +46 -20
  719. package/src/routes/pages/featured.tsx +31 -39
  720. package/src/routes/pages/home.tsx +50 -25
  721. package/src/routes/pages/latest.tsx +20 -4
  722. package/src/routes/pages/new.tsx +10 -6
  723. package/src/routes/pages/page.tsx +27 -53
  724. package/src/routes/pages/partials.tsx +83 -0
  725. package/src/routes/pages/search.tsx +11 -9
  726. package/src/routes/pages/theme-sample.tsx +98 -0
  727. package/src/runtime/__tests__/node.test.ts +258 -0
  728. package/src/runtime/cloudflare.ts +102 -0
  729. package/src/runtime/index.ts +14 -0
  730. package/src/runtime/node.ts +187 -0
  731. package/src/runtime/site.ts +169 -0
  732. package/src/services/__tests__/api-token.test.ts +5 -2
  733. package/src/services/__tests__/auth.test.ts +348 -0
  734. package/src/services/__tests__/collection.test.ts +154 -16
  735. package/src/services/__tests__/custom-url.test.ts +10 -3
  736. package/src/services/__tests__/hosted-handoff.test.ts +123 -0
  737. package/src/services/__tests__/media.test.ts +256 -33
  738. package/src/services/__tests__/navigation.test.ts +89 -4
  739. package/src/services/__tests__/post-timeline.test.ts +111 -2
  740. package/src/services/__tests__/post.test.ts +463 -38
  741. package/src/services/__tests__/search.test.ts +145 -14
  742. package/src/services/__tests__/settings.test.ts +162 -2
  743. package/src/services/__tests__/site-admin.test.ts +48 -0
  744. package/src/services/__tests__/site-profile.test.ts +175 -0
  745. package/src/services/api-token.ts +44 -11
  746. package/src/services/auth.ts +211 -9
  747. package/src/services/bootstrap.ts +68 -0
  748. package/src/services/collection.ts +501 -100
  749. package/src/services/custom-url.ts +42 -12
  750. package/src/services/export.ts +2269 -320
  751. package/src/services/hosted-handoff.ts +184 -0
  752. package/src/services/index.ts +110 -12
  753. package/src/services/media.ts +309 -39
  754. package/src/services/navigation.ts +141 -20
  755. package/src/services/path.ts +45 -9
  756. package/src/services/post.ts +1529 -246
  757. package/src/services/search.ts +222 -29
  758. package/src/services/settings.ts +202 -48
  759. package/src/services/site-admin.ts +825 -0
  760. package/src/services/site-member.ts +74 -0
  761. package/src/services/site-profile.ts +52 -0
  762. package/src/services/site.ts +250 -0
  763. package/src/services/upload-session.ts +707 -0
  764. package/src/style-cjk-tc.css +3 -0
  765. package/src/style-cjk.css +3 -0
  766. package/src/styles/components.css +647 -16
  767. package/src/styles/fonts/latin.css +4 -0
  768. package/src/styles/fonts/noto-serif-sc/400/02629e5d0a9860b7fe32ec1f0563213a.woff2 +0 -0
  769. package/src/styles/fonts/noto-serif-sc/400/031089da45fbfb7dc18ac827bef4c56e.woff2 +0 -0
  770. package/src/styles/fonts/noto-serif-sc/400/03ac785139320b7b13bac9c150bf72bf.woff2 +0 -0
  771. package/src/styles/fonts/noto-serif-sc/400/0b1f83a3c7e715560a55ad9eb0fb1c94.woff2 +0 -0
  772. package/src/styles/fonts/noto-serif-sc/400/14b040a2dda256936bc7b3470e548394.woff2 +0 -0
  773. package/src/styles/fonts/noto-serif-sc/400/189f272ea2600c74d576b7b15c014922.woff2 +0 -0
  774. package/src/styles/fonts/noto-serif-sc/400/1fbccc182322b513f57cd156a9a491b0.woff2 +0 -0
  775. package/src/styles/fonts/noto-serif-sc/400/21e0a71d86be7b8a9e812e7af09dd061.woff2 +0 -0
  776. package/src/styles/fonts/noto-serif-sc/400/265e048cc9f2f8a711ba585a534d5351.woff2 +0 -0
  777. package/src/styles/fonts/noto-serif-sc/400/2ae2ca951489c9d50cde5b36a2a5515b.woff2 +0 -0
  778. package/src/styles/fonts/noto-serif-sc/400/2ba802b14f21a58fc61606c88fa93373.woff2 +0 -0
  779. package/src/styles/fonts/noto-serif-sc/400/2deb444546774c3a3ab38c75eb69cdfb.woff2 +0 -0
  780. package/src/styles/fonts/noto-serif-sc/400/2fbccf9a3853eb59db1a825e044515fd.woff2 +0 -0
  781. package/src/styles/fonts/noto-serif-sc/400/2ff009fa8701505d7f3dc6c83763f019.woff2 +0 -0
  782. package/src/styles/fonts/noto-serif-sc/400/31424fe5d54692e7c8b38021ccb8597c.woff2 +0 -0
  783. package/src/styles/fonts/noto-serif-sc/400/360c190344c26278bbc50e2f4d6a2b3f.woff2 +0 -0
  784. package/src/styles/fonts/noto-serif-sc/400/39b0bbc910af9d2d6dcd8bd4abd6387d.woff2 +0 -0
  785. package/src/styles/fonts/noto-serif-sc/400/3adc8f6350cf5067bcb6dc5e44c45d41.woff2 +0 -0
  786. package/src/styles/fonts/noto-serif-sc/400/3bed5bd57de8f738e53cddaea88983d9.woff2 +0 -0
  787. package/src/styles/fonts/noto-serif-sc/400/3c201fd8d1bb20abe7d06b940e83a4d9.woff2 +0 -0
  788. package/src/styles/fonts/noto-serif-sc/400/3d385ea0880df7204258e290648ec012.woff2 +0 -0
  789. package/src/styles/fonts/noto-serif-sc/400/3dc1a4f0d7af59e16b5162a2b077a442.woff2 +0 -0
  790. package/src/styles/fonts/noto-serif-sc/400/3dc68e473fe23bd076dd46785cd23583.woff2 +0 -0
  791. package/src/styles/fonts/noto-serif-sc/400/435b7dca567809813fcb395a27ed83a0.woff2 +0 -0
  792. package/src/styles/fonts/noto-serif-sc/400/43693195e775d515689fa035394067fd.woff2 +0 -0
  793. package/src/styles/fonts/noto-serif-sc/400/43fb49e5b79ee7e553869d84e6e08b1e.woff2 +0 -0
  794. package/src/styles/fonts/noto-serif-sc/400/44dcbbc3cc8f22e613b342d691511ab6.woff2 +0 -0
  795. package/src/styles/fonts/noto-serif-sc/400/474fac21b12b7efd71f7c321578878b0.woff2 +0 -0
  796. package/src/styles/fonts/noto-serif-sc/400/4a23fe6e82fd496b5eb20401b6164efe.woff2 +0 -0
  797. package/src/styles/fonts/noto-serif-sc/400/4bc743968cf1c3ce5711de67ef1ccc4d.woff2 +0 -0
  798. package/src/styles/fonts/noto-serif-sc/400/4cf0f292f3358bd2f73b1cf4ec1476f3.woff2 +0 -0
  799. package/src/styles/fonts/noto-serif-sc/400/501f66f24bce8234441954de1b568403.woff2 +0 -0
  800. package/src/styles/fonts/noto-serif-sc/400/53a88404451448cd2e620a0ca0e45a20.woff2 +0 -0
  801. package/src/styles/fonts/noto-serif-sc/400/557cd00c5d6827e13d72a0c71b23587b.woff2 +0 -0
  802. package/src/styles/fonts/noto-serif-sc/400/563fa31542d553f25abab65cf7f81e1d.woff2 +0 -0
  803. package/src/styles/fonts/noto-serif-sc/400/56e1c4734bbbb38af2fbc262bf6e98f2.woff2 +0 -0
  804. package/src/styles/fonts/noto-serif-sc/400/5947f5da5da9a352a2b534ee64bfc29a.woff2 +0 -0
  805. package/src/styles/fonts/noto-serif-sc/400/5a10741e41259e235841440394c0763d.woff2 +0 -0
  806. package/src/styles/fonts/noto-serif-sc/400/60d8b0805a0a8c54a6cca216004beff5.woff2 +0 -0
  807. package/src/styles/fonts/noto-serif-sc/400/61bf4287453da4025d03fa6b2dba66ca.woff2 +0 -0
  808. package/src/styles/fonts/noto-serif-sc/400/638369541268ed5a10af97ad77498c73.woff2 +0 -0
  809. package/src/styles/fonts/noto-serif-sc/400/687d0f0f90a9b23e40102e16ad8e9836.woff2 +0 -0
  810. package/src/styles/fonts/noto-serif-sc/400/6e2164fad867d166de2e5b274f04a563.woff2 +0 -0
  811. package/src/styles/fonts/noto-serif-sc/400/6eefc9d430171c1e1e4034ecadee31c8.woff2 +0 -0
  812. package/src/styles/fonts/noto-serif-sc/400/71eafb8fbe3a734283517e230ad8b6db.woff2 +0 -0
  813. package/src/styles/fonts/noto-serif-sc/400/751f54dbb115140d5b645a6ba4aff5d3.woff2 +0 -0
  814. package/src/styles/fonts/noto-serif-sc/400/7609e7e74dd4d916a7abc7ecc7d95f7e.woff2 +0 -0
  815. package/src/styles/fonts/noto-serif-sc/400/76b9d6fe838ae4151d95ce7200aa2bf6.woff2 +0 -0
  816. package/src/styles/fonts/noto-serif-sc/400/78ce29fed872e44fc9014d94875d2aac.woff2 +0 -0
  817. package/src/styles/fonts/noto-serif-sc/400/79a85a253e9b3f12d2e2cb15e16b3003.woff2 +0 -0
  818. package/src/styles/fonts/noto-serif-sc/400/7caa14a095a6bc313aab780fe4ff7999.woff2 +0 -0
  819. package/src/styles/fonts/noto-serif-sc/400/7d138084cf03c14116b11297fce0e3e3.woff2 +0 -0
  820. package/src/styles/fonts/noto-serif-sc/400/880162ae92cd9e120eb4e4e11fae459d.woff2 +0 -0
  821. package/src/styles/fonts/noto-serif-sc/400/8f2b960c2823670e94f5b08aa65657e6.woff2 +0 -0
  822. package/src/styles/fonts/noto-serif-sc/400/8f89f57230d184f92a36e241874229d7.woff2 +0 -0
  823. package/src/styles/fonts/noto-serif-sc/400/9180de34b48b325200a97e267befff32.woff2 +0 -0
  824. package/src/styles/fonts/noto-serif-sc/400/95df3b9f681d9df411c30aea5b24f2e0.woff2 +0 -0
  825. package/src/styles/fonts/noto-serif-sc/400/975af5a496e8d87d821910aa9fe4d598.woff2 +0 -0
  826. package/src/styles/fonts/noto-serif-sc/400/97a874bbf55ce89a4ab7cd27c7e938b1.woff2 +0 -0
  827. package/src/styles/fonts/noto-serif-sc/400/9fd53607094e329fa8e5c785b3ff0f1a.woff2 +0 -0
  828. package/src/styles/fonts/noto-serif-sc/400/a578742770fcd2226e3c45b5b6efdcb0.woff2 +0 -0
  829. package/src/styles/fonts/noto-serif-sc/400/ae401fb4db80d5ff5cd3f8d9bc811070.woff2 +0 -0
  830. package/src/styles/fonts/noto-serif-sc/400/b0ab3a7f319ce6dd3c9a4de2674e7c72.woff2 +0 -0
  831. package/src/styles/fonts/noto-serif-sc/400/b159deb135e9946eea0572d52778170b.woff2 +0 -0
  832. package/src/styles/fonts/noto-serif-sc/400/b91450304d9ac44f5c6e0da0792e055d.woff2 +0 -0
  833. package/src/styles/fonts/noto-serif-sc/400/baa325551b381c5e035ef143e56d4abe.woff2 +0 -0
  834. package/src/styles/fonts/noto-serif-sc/400/c0c7836749e585cee24ab2f8457c5b01.woff2 +0 -0
  835. package/src/styles/fonts/noto-serif-sc/400/c2ac4ef1860812036ca2b8c4e4089bdc.woff2 +0 -0
  836. package/src/styles/fonts/noto-serif-sc/400/c31019c08bd22464f7a88f090281404c.woff2 +0 -0
  837. package/src/styles/fonts/noto-serif-sc/400/c57ee3b49b7e45b995539a6b2c51f138.woff2 +0 -0
  838. package/src/styles/fonts/noto-serif-sc/400/cd75ca47da9ae4c0899e37d4c543319b.woff2 +0 -0
  839. package/src/styles/fonts/noto-serif-sc/400/d12ce1d8445213317f9163283e58a05d.woff2 +0 -0
  840. package/src/styles/fonts/noto-serif-sc/400/d28fb13acf9ced9f0657fd4012c81cd2.woff2 +0 -0
  841. package/src/styles/fonts/noto-serif-sc/400/d3714e6b90de8e2085dfb2514464dd6a.woff2 +0 -0
  842. package/src/styles/fonts/noto-serif-sc/400/d3beff96216c8af1aa79246476b6a323.woff2 +0 -0
  843. package/src/styles/fonts/noto-serif-sc/400/d3e311f30c811dc339c262a79a51877e.woff2 +0 -0
  844. package/src/styles/fonts/noto-serif-sc/400/d51f4cdc83711e510f5d25f03235597e.woff2 +0 -0
  845. package/src/styles/fonts/noto-serif-sc/400/d5df4a8dfd4328c67d933b3912c6ad0f.woff2 +0 -0
  846. package/src/styles/fonts/noto-serif-sc/400/dccda6a2e2db3b530788bdfa2acd1979.woff2 +0 -0
  847. package/src/styles/fonts/noto-serif-sc/400/f5237486197aeff59341a1ff38b8eff8.woff2 +0 -0
  848. package/src/styles/fonts/noto-serif-sc/400/f553d54ef931066712d8f3f0ce018e1b.woff2 +0 -0
  849. package/src/styles/fonts/noto-serif-sc/400/f649cba8e14c33d6bf2265483b14b895.woff2 +0 -0
  850. package/src/styles/fonts/noto-serif-sc/400/f6839df1bf7cb4dc8d27e5ea55bbe633.woff2 +0 -0
  851. package/src/styles/fonts/noto-serif-sc/400/f6b7304e028980f77a7f7007bb540abd.woff2 +0 -0
  852. package/src/styles/fonts/noto-serif-sc/400/f8db8bef0a6e1178835d350ae0d384a1.woff2 +0 -0
  853. package/src/styles/fonts/noto-serif-sc/400/fb4649a82c50620773d79820e2e5ff13.woff2 +0 -0
  854. package/src/styles/fonts/noto-serif-sc/400/fb9402d6c6357a825affc402f14d5a7e.woff2 +0 -0
  855. package/src/styles/fonts/noto-serif-sc/700/0041f681602cc834bb5c55ced0155b8e.woff2 +0 -0
  856. package/src/styles/fonts/noto-serif-sc/700/00c9ac960d866ffaf8a866e5939024e2.woff2 +0 -0
  857. package/src/styles/fonts/noto-serif-sc/700/02e48e353415a00e0f556608cab33a43.woff2 +0 -0
  858. package/src/styles/fonts/noto-serif-sc/700/02faf6bb0ab4d56ada037c0bbaf9b9f7.woff2 +0 -0
  859. package/src/styles/fonts/noto-serif-sc/700/057a3d44d7fc606f113d863376d0ecf0.woff2 +0 -0
  860. package/src/styles/fonts/noto-serif-sc/700/10f2b44b3711d3f5bdcc30d373b543d1.woff2 +0 -0
  861. package/src/styles/fonts/noto-serif-sc/700/14c1506106d92621bafb11016735194e.woff2 +0 -0
  862. package/src/styles/fonts/noto-serif-sc/700/154a2c266902003bd8b7449386b10776.woff2 +0 -0
  863. package/src/styles/fonts/noto-serif-sc/700/19e39850472250bfdbce654d30859879.woff2 +0 -0
  864. package/src/styles/fonts/noto-serif-sc/700/1b5d0d740450fb996749464c9b882025.woff2 +0 -0
  865. package/src/styles/fonts/noto-serif-sc/700/1bca0a2a8840ad0ee9414940593db144.woff2 +0 -0
  866. package/src/styles/fonts/noto-serif-sc/700/1cda27dcaab977ae4ef5d5ab2a10ae03.woff2 +0 -0
  867. package/src/styles/fonts/noto-serif-sc/700/1f4bc38a1c50f55f335f5411cae47696.woff2 +0 -0
  868. package/src/styles/fonts/noto-serif-sc/700/2022cf097cb952d9fe75b53b4587d2c3.woff2 +0 -0
  869. package/src/styles/fonts/noto-serif-sc/700/2240a3c43ca5ef59ae3c348c7884792f.woff2 +0 -0
  870. package/src/styles/fonts/noto-serif-sc/700/280e3d2b58e9ad3501816072e01b0c13.woff2 +0 -0
  871. package/src/styles/fonts/noto-serif-sc/700/2874d07e228da9583b0e73646dacd498.woff2 +0 -0
  872. package/src/styles/fonts/noto-serif-sc/700/29d49891713a2785a3a383001cf58c59.woff2 +0 -0
  873. package/src/styles/fonts/noto-serif-sc/700/2d81eb6ab0ebbc0cabfb3a3341ba8800.woff2 +0 -0
  874. package/src/styles/fonts/noto-serif-sc/700/3179006d1c7ebfa50d27482a2859d9a0.woff2 +0 -0
  875. package/src/styles/fonts/noto-serif-sc/700/34c2edb3c37f71258f5c4a31091f0c6c.woff2 +0 -0
  876. package/src/styles/fonts/noto-serif-sc/700/389a950f2a1211946d294716e679e381.woff2 +0 -0
  877. package/src/styles/fonts/noto-serif-sc/700/42a74b6a625bbf0a9616ed4db3152c88.woff2 +0 -0
  878. package/src/styles/fonts/noto-serif-sc/700/457485e72835364662dfead6281638c1.woff2 +0 -0
  879. package/src/styles/fonts/noto-serif-sc/700/488846410760fe128dae939836ca5423.woff2 +0 -0
  880. package/src/styles/fonts/noto-serif-sc/700/48eb0a91e50c7f026e248c64145e72af.woff2 +0 -0
  881. package/src/styles/fonts/noto-serif-sc/700/4b0e79ba18b2ce424fa93e84996d7cba.woff2 +0 -0
  882. package/src/styles/fonts/noto-serif-sc/700/54e301f412730f391225db59dae1c8d5.woff2 +0 -0
  883. package/src/styles/fonts/noto-serif-sc/700/597d69d0710e0178b162afb0a0c20401.woff2 +0 -0
  884. package/src/styles/fonts/noto-serif-sc/700/5cc23a76e122d0ad2f7cede41bc35b27.woff2 +0 -0
  885. package/src/styles/fonts/noto-serif-sc/700/5d48855bed5f3554eff91b573d7376ac.woff2 +0 -0
  886. package/src/styles/fonts/noto-serif-sc/700/5ddcbe564b29ef08632e1aeb33455435.woff2 +0 -0
  887. package/src/styles/fonts/noto-serif-sc/700/605667a998e91e2b6a4a3cd7c31fe5a9.woff2 +0 -0
  888. package/src/styles/fonts/noto-serif-sc/700/67f32ceea9e78e5109f87724ad886010.woff2 +0 -0
  889. package/src/styles/fonts/noto-serif-sc/700/68f2fab82ec8e9291f08c3145111549c.woff2 +0 -0
  890. package/src/styles/fonts/noto-serif-sc/700/69519ada3f3f74ca20aacb8af48ab6b4.woff2 +0 -0
  891. package/src/styles/fonts/noto-serif-sc/700/6a6e884fb2b65ec5b4a3d5ecd0d01a6a.woff2 +0 -0
  892. package/src/styles/fonts/noto-serif-sc/700/6e1a8b45b01939088c3a8cfcf8c10681.woff2 +0 -0
  893. package/src/styles/fonts/noto-serif-sc/700/70adaf50c56d5ff859c64d35e0f1e34e.woff2 +0 -0
  894. package/src/styles/fonts/noto-serif-sc/700/72ee453ac0e19bd2c631c8921c44e3de.woff2 +0 -0
  895. package/src/styles/fonts/noto-serif-sc/700/753b5f6fb254bacb6618ace25af3df60.woff2 +0 -0
  896. package/src/styles/fonts/noto-serif-sc/700/76d4244186d118eea245d1385a4de2ec.woff2 +0 -0
  897. package/src/styles/fonts/noto-serif-sc/700/7a86b155111ba20f3e87306ff6beac77.woff2 +0 -0
  898. package/src/styles/fonts/noto-serif-sc/700/7b1e76975b0984e6f83e3f9f8069e784.woff2 +0 -0
  899. package/src/styles/fonts/noto-serif-sc/700/7b6c60131822a0e4d36d980d52509d4e.woff2 +0 -0
  900. package/src/styles/fonts/noto-serif-sc/700/7cbda564cb2dd4799ab9e89d51286aa7.woff2 +0 -0
  901. package/src/styles/fonts/noto-serif-sc/700/7f60eefa15956d6f06dd92404887d58c.woff2 +0 -0
  902. package/src/styles/fonts/noto-serif-sc/700/7f8c15e0ecb102738981d9fa4cb6b921.woff2 +0 -0
  903. package/src/styles/fonts/noto-serif-sc/700/812b5a4b87f3a7b4afc1cfebc864f413.woff2 +0 -0
  904. package/src/styles/fonts/noto-serif-sc/700/812dfb7f8144d01b3cc9d5ce0b472f40.woff2 +0 -0
  905. package/src/styles/fonts/noto-serif-sc/700/84742b1ede4f0bb6d27131298eba21b4.woff2 +0 -0
  906. package/src/styles/fonts/noto-serif-sc/700/8555f0285e3d28e95e2fc0ccccd9caff.woff2 +0 -0
  907. package/src/styles/fonts/noto-serif-sc/700/8e04e64c8f68d292a18d4160fbde8671.woff2 +0 -0
  908. package/src/styles/fonts/noto-serif-sc/700/958efb9b2fa2ea0008ffef009885f9f8.woff2 +0 -0
  909. package/src/styles/fonts/noto-serif-sc/700/9ca9b71010a5faeee7047ef97aeee13b.woff2 +0 -0
  910. package/src/styles/fonts/noto-serif-sc/700/9cd0b77920b9d6c64eb686493123fc76.woff2 +0 -0
  911. package/src/styles/fonts/noto-serif-sc/700/9ebd27835ffcbd794e67151ab046ce68.woff2 +0 -0
  912. package/src/styles/fonts/noto-serif-sc/700/a077f51cfb5cffb4ff4d8e229c0e9e79.woff2 +0 -0
  913. package/src/styles/fonts/noto-serif-sc/700/a397997b579d3945c9c70a979c17a8ad.woff2 +0 -0
  914. package/src/styles/fonts/noto-serif-sc/700/a68d9d5027803832bb28e78cdcd04949.woff2 +0 -0
  915. package/src/styles/fonts/noto-serif-sc/700/a904b05966368bcf90b632c7c2e5f76b.woff2 +0 -0
  916. package/src/styles/fonts/noto-serif-sc/700/aa218a2c45f3749537ce876201e5152b.woff2 +0 -0
  917. package/src/styles/fonts/noto-serif-sc/700/aa28db16818f9eaa8c817f289e1c3270.woff2 +0 -0
  918. package/src/styles/fonts/noto-serif-sc/700/aa96d698491c2540e2dcf7009c65c456.woff2 +0 -0
  919. package/src/styles/fonts/noto-serif-sc/700/ae25c41034ddc1a9e0b41f5034c9aa4b.woff2 +0 -0
  920. package/src/styles/fonts/noto-serif-sc/700/ae289ae3f8cdb54a3a6c07174517afec.woff2 +0 -0
  921. package/src/styles/fonts/noto-serif-sc/700/b02dfa2aa52cbdb1b2f11a9f44335469.woff2 +0 -0
  922. package/src/styles/fonts/noto-serif-sc/700/b2e326f7f9b807451bf9c745df747efe.woff2 +0 -0
  923. package/src/styles/fonts/noto-serif-sc/700/c33c59feccf391f0c5f1f5d24e36d1fe.woff2 +0 -0
  924. package/src/styles/fonts/noto-serif-sc/700/c82fd9456d7465b5e5bd3659e9b14c55.woff2 +0 -0
  925. package/src/styles/fonts/noto-serif-sc/700/c90b7b65d2b9696fbf3a506738f94d68.woff2 +0 -0
  926. package/src/styles/fonts/noto-serif-sc/700/cba6ad3981cb7861428d4be169ee8124.woff2 +0 -0
  927. package/src/styles/fonts/noto-serif-sc/700/cc26525aa2af1f0b929af32ce50a7fba.woff2 +0 -0
  928. package/src/styles/fonts/noto-serif-sc/700/cd0e7b51eddb22a77a09b025c0281434.woff2 +0 -0
  929. package/src/styles/fonts/noto-serif-sc/700/cd6d074f3957d58bac58437fc97e5e33.woff2 +0 -0
  930. package/src/styles/fonts/noto-serif-sc/700/cef249b6d013fb0cc0d574176bc23811.woff2 +0 -0
  931. package/src/styles/fonts/noto-serif-sc/700/d0bd387fda28e58d3c9b3efa2468dd8a.woff2 +0 -0
  932. package/src/styles/fonts/noto-serif-sc/700/da93ae099ff3b7aae27b3f674d3fc721.woff2 +0 -0
  933. package/src/styles/fonts/noto-serif-sc/700/e264213b9e102dabc603adb6e4fda5e6.woff2 +0 -0
  934. package/src/styles/fonts/noto-serif-sc/700/e7c7ef3669ae48c0a736f06ca471e1d7.woff2 +0 -0
  935. package/src/styles/fonts/noto-serif-sc/700/e81a742cacef744130c40de1b90837d8.woff2 +0 -0
  936. package/src/styles/fonts/noto-serif-sc/700/e8b755172122d1d0a5dd453e96b0ff24.woff2 +0 -0
  937. package/src/styles/fonts/noto-serif-sc/700/e99280299c305402eaa5271a3e36c49b.woff2 +0 -0
  938. package/src/styles/fonts/noto-serif-sc/700/e9c66b085052ece66bfadf45f711d3e1.woff2 +0 -0
  939. package/src/styles/fonts/noto-serif-sc/700/ed7c6dafaa6d8bcf015ef0ca574837df.woff2 +0 -0
  940. package/src/styles/fonts/noto-serif-sc/700/f2900a1d30c3a33129f4e2225669bd0e.woff2 +0 -0
  941. package/src/styles/fonts/noto-serif-sc/700/fa7d3b99744d7f2dc9e00864a97a62d6.woff2 +0 -0
  942. package/src/styles/fonts/noto-serif-sc/700/fb0e90665980954719c2eb685b130bc0.woff2 +0 -0
  943. package/src/styles/fonts/noto-serif-sc/noto-serif-sc.css +3615 -0
  944. package/src/styles/fonts/noto-serif-tc/400/008ea9091e332c639ceb18874eacd60c.woff2 +0 -0
  945. package/src/styles/fonts/noto-serif-tc/400/053bd3d7aec0040d0cc50c261a1f4e3e.woff2 +0 -0
  946. package/src/styles/fonts/noto-serif-tc/400/0713613227cc4c686c45a279f8bdc166.woff2 +0 -0
  947. package/src/styles/fonts/noto-serif-tc/400/0e97f44ebc65384c346fe19bcc52fa20.woff2 +0 -0
  948. package/src/styles/fonts/noto-serif-tc/400/1139d32ae2bdeb26c0c8f31330aa9a9f.woff2 +0 -0
  949. package/src/styles/fonts/noto-serif-tc/400/15f8c0df47fd639d1b0d9bd5cf507c9b.woff2 +0 -0
  950. package/src/styles/fonts/noto-serif-tc/400/1668bd859ffe15bed7d5563117d8d5fb.woff2 +0 -0
  951. package/src/styles/fonts/noto-serif-tc/400/190e3f8632494e7c095117f26b1c811e.woff2 +0 -0
  952. package/src/styles/fonts/noto-serif-tc/400/19ad151c22ce1befe0a9ea643fbee570.woff2 +0 -0
  953. package/src/styles/fonts/noto-serif-tc/400/1c820b5295868008ca7c78afa5b7655d.woff2 +0 -0
  954. package/src/styles/fonts/noto-serif-tc/400/1fbe225742c69f4ba9ea5f74922f0ca1.woff2 +0 -0
  955. package/src/styles/fonts/noto-serif-tc/400/2a7cedfcd6e4c7cec36f4fd7b0f329c2.woff2 +0 -0
  956. package/src/styles/fonts/noto-serif-tc/400/2acea04a920f6af31e7db97052f563c6.woff2 +0 -0
  957. package/src/styles/fonts/noto-serif-tc/400/2e98b666924b8e0a09d1aeeefd24bdd2.woff2 +0 -0
  958. package/src/styles/fonts/noto-serif-tc/400/2fd3fceb6faed5e3db768e88d7614dca.woff2 +0 -0
  959. package/src/styles/fonts/noto-serif-tc/400/35cf5dd04315e0b906e1a413d7905a2f.woff2 +0 -0
  960. package/src/styles/fonts/noto-serif-tc/400/387c811226f303af62f1e21aae6f5c83.woff2 +0 -0
  961. package/src/styles/fonts/noto-serif-tc/400/3b41385fc27419c19822060daa0b5cb3.woff2 +0 -0
  962. package/src/styles/fonts/noto-serif-tc/400/3cbe4a697fd595ef42c899de7d3e5445.woff2 +0 -0
  963. package/src/styles/fonts/noto-serif-tc/400/3d83dacbbec3d8532ae9afede21f3aab.woff2 +0 -0
  964. package/src/styles/fonts/noto-serif-tc/400/47479c470fae70f10b7c964a7ecbf274.woff2 +0 -0
  965. package/src/styles/fonts/noto-serif-tc/400/4dc0728df0f2ba70796f45f05654c7ba.woff2 +0 -0
  966. package/src/styles/fonts/noto-serif-tc/400/4dc2bc2c55b47f57d13b63aa6b1c8bd4.woff2 +0 -0
  967. package/src/styles/fonts/noto-serif-tc/400/4e1cc6aafb411b572c8d3511e925ecf1.woff2 +0 -0
  968. package/src/styles/fonts/noto-serif-tc/400/5227dbe9933760a48baff21ebd13fc98.woff2 +0 -0
  969. package/src/styles/fonts/noto-serif-tc/400/526b263e72c189f4b065738aaa6d423a.woff2 +0 -0
  970. package/src/styles/fonts/noto-serif-tc/400/54da934819a917f561b439bfd10f88b6.woff2 +0 -0
  971. package/src/styles/fonts/noto-serif-tc/400/5bfc7a121c35ae42623ef804fb525e0e.woff2 +0 -0
  972. package/src/styles/fonts/noto-serif-tc/400/5f90024544c2907c6c0203c6210c50be.woff2 +0 -0
  973. package/src/styles/fonts/noto-serif-tc/400/649b12d7cee7bb981842946e4547e6ca.woff2 +0 -0
  974. package/src/styles/fonts/noto-serif-tc/400/653bef2ed891ae48d8ed712283080649.woff2 +0 -0
  975. package/src/styles/fonts/noto-serif-tc/400/67d2a81f06ba352f17fbdc3a5e6ea59e.woff2 +0 -0
  976. package/src/styles/fonts/noto-serif-tc/400/68304f3229cf763465f044fccb5892c0.woff2 +0 -0
  977. package/src/styles/fonts/noto-serif-tc/400/688a88911e4da17b609196a959b8b930.woff2 +0 -0
  978. package/src/styles/fonts/noto-serif-tc/400/6db6ddf72c38a78ce44c1327701152e1.woff2 +0 -0
  979. package/src/styles/fonts/noto-serif-tc/400/77a7533bd21ccd33192d142a93555aa8.woff2 +0 -0
  980. package/src/styles/fonts/noto-serif-tc/400/7d65a3d6a65050eb5e6eca43398aeba4.woff2 +0 -0
  981. package/src/styles/fonts/noto-serif-tc/400/7dfc711962c8771f97e7c8898a6bcb65.woff2 +0 -0
  982. package/src/styles/fonts/noto-serif-tc/400/7ef123b62d530fcba73974fa265e0aae.woff2 +0 -0
  983. package/src/styles/fonts/noto-serif-tc/400/80466082a896fd328f30a78593c7c568.woff2 +0 -0
  984. package/src/styles/fonts/noto-serif-tc/400/8a3c84b0df36f851f5fea75ee8757951.woff2 +0 -0
  985. package/src/styles/fonts/noto-serif-tc/400/8b0c8c9f8cfa9fa090d97c5a5efb1f4c.woff2 +0 -0
  986. package/src/styles/fonts/noto-serif-tc/400/8dc035a34c76e6515ca203e2df182588.woff2 +0 -0
  987. package/src/styles/fonts/noto-serif-tc/400/8eb06109812cb80be44f47b8179c2709.woff2 +0 -0
  988. package/src/styles/fonts/noto-serif-tc/400/904324af375d5fd370af1054355a050e.woff2 +0 -0
  989. package/src/styles/fonts/noto-serif-tc/400/911a2092d64d6d6494b254d819af2b91.woff2 +0 -0
  990. package/src/styles/fonts/noto-serif-tc/400/9de02d745b8e25c6411fb152fb067748.woff2 +0 -0
  991. package/src/styles/fonts/noto-serif-tc/400/9eb33a430058d839ebbe2af4b2e0daa9.woff2 +0 -0
  992. package/src/styles/fonts/noto-serif-tc/400/9f5a73aa8ba417688019d628f334db07.woff2 +0 -0
  993. package/src/styles/fonts/noto-serif-tc/400/a0f0c06d5c7a3ffa97706178cce212a8.woff2 +0 -0
  994. package/src/styles/fonts/noto-serif-tc/400/a38c1830367f784181b6f544b0b11bbd.woff2 +0 -0
  995. package/src/styles/fonts/noto-serif-tc/400/a9cf85e27428c14351d30eac8cbc8d91.woff2 +0 -0
  996. package/src/styles/fonts/noto-serif-tc/400/aa0ce6740f301351761a0615cc8b2e99.woff2 +0 -0
  997. package/src/styles/fonts/noto-serif-tc/400/bc3f0cb8b55ee11d32b94ca488976f8d.woff2 +0 -0
  998. package/src/styles/fonts/noto-serif-tc/400/bcb3307527d6d0033bf0f17660b91e71.woff2 +0 -0
  999. package/src/styles/fonts/noto-serif-tc/400/bf1acc86e17b4229c548828a9d6f455d.woff2 +0 -0
  1000. package/src/styles/fonts/noto-serif-tc/400/c3fbc1f2557c343863a10698f8c966a2.woff2 +0 -0
  1001. package/src/styles/fonts/noto-serif-tc/400/c5c1c0be944ea39a3f50a02d32f5b759.woff2 +0 -0
  1002. package/src/styles/fonts/noto-serif-tc/400/c5f1075caf6d1344ee720de85114a521.woff2 +0 -0
  1003. package/src/styles/fonts/noto-serif-tc/400/cae29b3f8951eaf20d2f61c2206e28d9.woff2 +0 -0
  1004. package/src/styles/fonts/noto-serif-tc/400/d043b8d7a48bb0ac59ee1f1477d88eee.woff2 +0 -0
  1005. package/src/styles/fonts/noto-serif-tc/400/d320b000b5978c7251148a6a154741b8.woff2 +0 -0
  1006. package/src/styles/fonts/noto-serif-tc/400/da13b136efb1d1e4c76575af8f79a698.woff2 +0 -0
  1007. package/src/styles/fonts/noto-serif-tc/400/da2cf0ec56bf69374ee37764c7e3ea3d.woff2 +0 -0
  1008. package/src/styles/fonts/noto-serif-tc/400/daf62255dd60679946f28c442ca62533.woff2 +0 -0
  1009. package/src/styles/fonts/noto-serif-tc/400/dd01a1035345f6921a48525b8ce08f06.woff2 +0 -0
  1010. package/src/styles/fonts/noto-serif-tc/400/e4fb59479cedc87ba79785590bf861ca.woff2 +0 -0
  1011. package/src/styles/fonts/noto-serif-tc/400/e5d00355f73293d40b61299459d17ca5.woff2 +0 -0
  1012. package/src/styles/fonts/noto-serif-tc/400/e647b8d2efc501c0cc0e407249cc7135.woff2 +0 -0
  1013. package/src/styles/fonts/noto-serif-tc/400/e6e60ffb2ebd1828628764b507060aea.woff2 +0 -0
  1014. package/src/styles/fonts/noto-serif-tc/400/f00eb499abb94fa7b799d6d8c9b050e9.woff2 +0 -0
  1015. package/src/styles/fonts/noto-serif-tc/400/f7d36ffff7a75c9c6216d576a57dd00d.woff2 +0 -0
  1016. package/src/styles/fonts/noto-serif-tc/400/f7f3f63e7a149cd89eccab3b52171d05.woff2 +0 -0
  1017. package/src/styles/fonts/noto-serif-tc/400/fcc41f6a067ddd658bba5c9dff234a32.woff2 +0 -0
  1018. package/src/styles/fonts/noto-serif-tc/400/fd6ad889fcf3583bd9b0b6db53aad434.woff2 +0 -0
  1019. package/src/styles/fonts/noto-serif-tc/700/0c055db157e7a13f3103cc2a6b67fec3.woff2 +0 -0
  1020. package/src/styles/fonts/noto-serif-tc/700/0d3f5cc265cb6c439c517f2c4cebbddf.woff2 +0 -0
  1021. package/src/styles/fonts/noto-serif-tc/700/1259e5825b314fe2b8bb96d6e8069ee5.woff2 +0 -0
  1022. package/src/styles/fonts/noto-serif-tc/700/12c518ebfe62818af550c08947e359e7.woff2 +0 -0
  1023. package/src/styles/fonts/noto-serif-tc/700/145831a59caa06d894022fe60212ed21.woff2 +0 -0
  1024. package/src/styles/fonts/noto-serif-tc/700/169a096e61d38a773216f51d1ec2cc06.woff2 +0 -0
  1025. package/src/styles/fonts/noto-serif-tc/700/1884a2b22d314c7d57707f03aec348e0.woff2 +0 -0
  1026. package/src/styles/fonts/noto-serif-tc/700/1e2640116bbba817f43c43cc69371cf1.woff2 +0 -0
  1027. package/src/styles/fonts/noto-serif-tc/700/2573703213da30d3ba18925b100b2c2b.woff2 +0 -0
  1028. package/src/styles/fonts/noto-serif-tc/700/26839c0e47c73514b8d8f660d24d6b19.woff2 +0 -0
  1029. package/src/styles/fonts/noto-serif-tc/700/2a22e14a9ad53f2abb3c7e85017b7d12.woff2 +0 -0
  1030. package/src/styles/fonts/noto-serif-tc/700/2b3e8c5703b91f39f6027f43f0da6f4b.woff2 +0 -0
  1031. package/src/styles/fonts/noto-serif-tc/700/2f27ee4fb2cf6a280e110e09c18ef73e.woff2 +0 -0
  1032. package/src/styles/fonts/noto-serif-tc/700/31342cebfa5ea7fac06b4ea372d96bc5.woff2 +0 -0
  1033. package/src/styles/fonts/noto-serif-tc/700/375329ba0b50b94b35006498e555867c.woff2 +0 -0
  1034. package/src/styles/fonts/noto-serif-tc/700/427577dcb707d1d35eebd155b4222aa7.woff2 +0 -0
  1035. package/src/styles/fonts/noto-serif-tc/700/450a5b53be0a8a778bb0b623e86b652f.woff2 +0 -0
  1036. package/src/styles/fonts/noto-serif-tc/700/477866c8396474a17317dcac3e7a014f.woff2 +0 -0
  1037. package/src/styles/fonts/noto-serif-tc/700/478ebdaadda7775c391c5dcab4e697df.woff2 +0 -0
  1038. package/src/styles/fonts/noto-serif-tc/700/48d6a97a185c799be4fe67aaf7edf213.woff2 +0 -0
  1039. package/src/styles/fonts/noto-serif-tc/700/490edb9fc8a4356aea556eed32287464.woff2 +0 -0
  1040. package/src/styles/fonts/noto-serif-tc/700/4c4bdd0b3f3a52e28f3b643c1c5d43be.woff2 +0 -0
  1041. package/src/styles/fonts/noto-serif-tc/700/4c96411f3693a9a8657a9c1190f82bce.woff2 +0 -0
  1042. package/src/styles/fonts/noto-serif-tc/700/4c9aa12aba2a6a57410eacaff7427916.woff2 +0 -0
  1043. package/src/styles/fonts/noto-serif-tc/700/4cca7233bf8ce5dec2e5d146b993d626.woff2 +0 -0
  1044. package/src/styles/fonts/noto-serif-tc/700/4d0a9128d06ea857f203bf5d007b1ab9.woff2 +0 -0
  1045. package/src/styles/fonts/noto-serif-tc/700/4e5384920bbb155d9d8d74887b09ea5b.woff2 +0 -0
  1046. package/src/styles/fonts/noto-serif-tc/700/50cfd672bfa62512ba090420acf35c87.woff2 +0 -0
  1047. package/src/styles/fonts/noto-serif-tc/700/551b1d7a0b80c8d42af09863cdca7f01.woff2 +0 -0
  1048. package/src/styles/fonts/noto-serif-tc/700/555d990ab3fd7d3d66c6d1fa9a82fec5.woff2 +0 -0
  1049. package/src/styles/fonts/noto-serif-tc/700/5979c33a7eb5963bf8e83e46931b5fb5.woff2 +0 -0
  1050. package/src/styles/fonts/noto-serif-tc/700/59966ee0b069b577510fe68c350da0ee.woff2 +0 -0
  1051. package/src/styles/fonts/noto-serif-tc/700/60a14064ed334f0155795d795e926abe.woff2 +0 -0
  1052. package/src/styles/fonts/noto-serif-tc/700/611b62d5fd9698d9b5ce495ba6f14c93.woff2 +0 -0
  1053. package/src/styles/fonts/noto-serif-tc/700/6e83fe0b6e708eaf1c3003d6dee11488.woff2 +0 -0
  1054. package/src/styles/fonts/noto-serif-tc/700/70861376e5d4f92f8aa7aa1b2749b617.woff2 +0 -0
  1055. package/src/styles/fonts/noto-serif-tc/700/7124d150570d39ced8d45507dc11ca1e.woff2 +0 -0
  1056. package/src/styles/fonts/noto-serif-tc/700/79a7fdf7d9c722b5723ae25e6ff8e203.woff2 +0 -0
  1057. package/src/styles/fonts/noto-serif-tc/700/8c8393bc875f1ee36697a2113f4421ea.woff2 +0 -0
  1058. package/src/styles/fonts/noto-serif-tc/700/8e6c9bb43afb8cbbff7cf1055e67c9bd.woff2 +0 -0
  1059. package/src/styles/fonts/noto-serif-tc/700/90ac4f9d2aa02afdace2843b49fc18bb.woff2 +0 -0
  1060. package/src/styles/fonts/noto-serif-tc/700/90b6f57d77847f512fd11db74fa912f1.woff2 +0 -0
  1061. package/src/styles/fonts/noto-serif-tc/700/913759e6690f9fc0746a20b96f4bdcb4.woff2 +0 -0
  1062. package/src/styles/fonts/noto-serif-tc/700/9154e26efe532a85a27d80902f5a2d6c.woff2 +0 -0
  1063. package/src/styles/fonts/noto-serif-tc/700/94e7ed67f1557b76fead6b6e456a0415.woff2 +0 -0
  1064. package/src/styles/fonts/noto-serif-tc/700/95127a92346c04fec1fa81d6295b0a28.woff2 +0 -0
  1065. package/src/styles/fonts/noto-serif-tc/700/9fbc06b2e3ff16b9d705c76db563ef17.woff2 +0 -0
  1066. package/src/styles/fonts/noto-serif-tc/700/a3b929542e6c5a0644b73a7c8a8b6c03.woff2 +0 -0
  1067. package/src/styles/fonts/noto-serif-tc/700/a8857f5d478f101c053ba02d2f223e90.woff2 +0 -0
  1068. package/src/styles/fonts/noto-serif-tc/700/aa64c9953af43ca65832f413895bb433.woff2 +0 -0
  1069. package/src/styles/fonts/noto-serif-tc/700/ada8f0241244c60ec8d3d59ad37f20a5.woff2 +0 -0
  1070. package/src/styles/fonts/noto-serif-tc/700/b341de0bc0bfe194a6c28dcfb566029e.woff2 +0 -0
  1071. package/src/styles/fonts/noto-serif-tc/700/b846c293981ca5429eabaa967f222f26.woff2 +0 -0
  1072. package/src/styles/fonts/noto-serif-tc/700/be64f9379412876e00fd3a0bfa6b6fe9.woff2 +0 -0
  1073. package/src/styles/fonts/noto-serif-tc/700/befed8a4fa817773fa7109db6fe07f56.woff2 +0 -0
  1074. package/src/styles/fonts/noto-serif-tc/700/c09ee2b219982f8d46ad9968b7e6e0ba.woff2 +0 -0
  1075. package/src/styles/fonts/noto-serif-tc/700/c39ec937c6a8d124e8b68cf829ea5ad4.woff2 +0 -0
  1076. package/src/styles/fonts/noto-serif-tc/700/c3fd21315345ae541f6e98067059fa19.woff2 +0 -0
  1077. package/src/styles/fonts/noto-serif-tc/700/c568a16e3168ceb1f191b70022c492ea.woff2 +0 -0
  1078. package/src/styles/fonts/noto-serif-tc/700/c5e66d60be3375835bbd8d6b797f6eac.woff2 +0 -0
  1079. package/src/styles/fonts/noto-serif-tc/700/cd10a3af2133805d8c92104d1ee6ff18.woff2 +0 -0
  1080. package/src/styles/fonts/noto-serif-tc/700/d15a3317942b7d31978a759fbf2222c8.woff2 +0 -0
  1081. package/src/styles/fonts/noto-serif-tc/700/d740dc2e854aaa7b3dcdd3ed25455eeb.woff2 +0 -0
  1082. package/src/styles/fonts/noto-serif-tc/700/d8325ba7ae651bc30440905bd67b95f1.woff2 +0 -0
  1083. package/src/styles/fonts/noto-serif-tc/700/e2204cf85edcb96c5de5c3dcf240da9d.woff2 +0 -0
  1084. package/src/styles/fonts/noto-serif-tc/700/e3e913e145ddcd9323b2a0972967feb6.woff2 +0 -0
  1085. package/src/styles/fonts/noto-serif-tc/700/f2fb1f1fbf7e44afb53c672ec286a22e.woff2 +0 -0
  1086. package/src/styles/fonts/noto-serif-tc/700/f372129c60aaece937cf7b91ee75c9b8.woff2 +0 -0
  1087. package/src/styles/fonts/noto-serif-tc/700/f5d7487963d43c89da63aaf10f2e6fb7.woff2 +0 -0
  1088. package/src/styles/fonts/noto-serif-tc/700/f75496953a40ff241178240209f56990.woff2 +0 -0
  1089. package/src/styles/fonts/noto-serif-tc/700/f92d74d1d217d21b39075ff23f79f7fd.woff2 +0 -0
  1090. package/src/styles/fonts/noto-serif-tc/700/f9d6d981d8b87b3e469027277f585741.woff2 +0 -0
  1091. package/src/styles/fonts/noto-serif-tc/700/fa8ed469ef290bfeb571418fe0abb628.woff2 +0 -0
  1092. package/src/styles/fonts/noto-serif-tc/700/fb61b690208eff56e6d8432951270901.woff2 +0 -0
  1093. package/src/styles/fonts/noto-serif-tc/700/ff0937ad63cda71ff420945ead55ab4d.woff2 +0 -0
  1094. package/src/styles/fonts/noto-serif-tc/noto-serif-tc.css +3104 -0
  1095. package/src/styles/tokens.css +42 -7
  1096. package/src/styles/ui.css +7586 -3401
  1097. package/src/types/app-context.ts +7 -0
  1098. package/src/types/bindings.ts +77 -27
  1099. package/src/types/config.ts +104 -4
  1100. package/src/types/constants.ts +43 -4
  1101. package/src/types/entities.ts +63 -4
  1102. package/src/types/operations.ts +45 -15
  1103. package/src/types/props.ts +46 -9
  1104. package/src/types/views.ts +51 -8
  1105. package/src/ui/__tests__/color-themes.test.ts +81 -0
  1106. package/src/ui/__tests__/font-themes.test.ts +76 -7
  1107. package/src/ui/color-themes.ts +424 -238
  1108. package/src/ui/compose/ComposeDialog.tsx +149 -91
  1109. package/src/ui/compose/ComposePrompt.tsx +20 -3
  1110. package/src/ui/dash/ActionButtons.tsx +16 -2
  1111. package/src/ui/dash/DangerZone.tsx +10 -1
  1112. package/src/ui/dash/StatusBadge.tsx +3 -3
  1113. package/src/ui/dash/appearance/AdvancedContent.tsx +9 -2
  1114. package/src/ui/dash/appearance/ColorThemeContent.tsx +326 -63
  1115. package/src/ui/dash/appearance/FontThemeContent.tsx +75 -17
  1116. package/src/ui/dash/appearance/NavigationContent.tsx +48 -21
  1117. package/src/ui/dash/settings/AccountContent.tsx +7 -2
  1118. package/src/ui/dash/settings/AccountMenuContent.tsx +242 -111
  1119. package/src/ui/dash/settings/ApiTokensContent.tsx +38 -8
  1120. package/src/ui/dash/settings/AvatarContent.tsx +1 -1
  1121. package/src/ui/dash/settings/DeleteAccountContent.tsx +300 -0
  1122. package/src/ui/dash/settings/GeneralContent.tsx +120 -6
  1123. package/src/ui/dash/settings/SessionsContent.tsx +23 -5
  1124. package/src/ui/dash/settings/SettingsDirectory.tsx +99 -0
  1125. package/src/ui/dash/settings/SettingsRootContent.tsx +176 -221
  1126. package/src/ui/feed/CuratedThreadPreview.tsx +73 -0
  1127. package/src/ui/feed/LinkCard.tsx +52 -36
  1128. package/src/ui/feed/NoteCard.tsx +10 -4
  1129. package/src/ui/feed/PostStatusBadges.tsx +4 -21
  1130. package/src/ui/feed/QuoteCard.tsx +11 -3
  1131. package/src/ui/feed/ThreadPreview.tsx +11 -5
  1132. package/src/ui/feed/TimelineFeed.tsx +53 -15
  1133. package/src/ui/feed/__tests__/timeline-cards.test.ts +172 -0
  1134. package/src/ui/feed/thread-preview-state.ts +2 -4
  1135. package/src/ui/font-themes.ts +211 -43
  1136. package/src/ui/layouts/BaseLayout.tsx +161 -30
  1137. package/src/ui/layouts/SiteLayout.tsx +51 -18
  1138. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +169 -0
  1139. package/src/ui/pages/ArchivePage.tsx +320 -120
  1140. package/src/ui/pages/BrandPage.tsx +927 -0
  1141. package/src/ui/pages/CollectionEditorPage.tsx +155 -0
  1142. package/src/ui/pages/CollectionPage.tsx +410 -18
  1143. package/src/ui/pages/CollectionsPage.tsx +57 -57
  1144. package/src/ui/pages/FeaturedPage.tsx +24 -3
  1145. package/src/ui/pages/HomePage.tsx +13 -0
  1146. package/src/ui/pages/PostPage.tsx +13 -4
  1147. package/src/ui/pages/SearchPage.tsx +12 -2
  1148. package/src/ui/pages/ThemeSamplePage.tsx +1160 -0
  1149. package/src/ui/shared/AdminBreadcrumb.tsx +22 -6
  1150. package/src/ui/shared/CollectionDirectory.tsx +117 -0
  1151. package/src/ui/shared/CollectionsManager.tsx +277 -0
  1152. package/src/ui/shared/DecorativeQuoteMark.tsx +32 -0
  1153. package/src/ui/shared/HomePageBranding.tsx +22 -0
  1154. package/src/ui/shared/JantBrandMark.tsx +35 -0
  1155. package/src/ui/shared/MediaGallery.tsx +106 -36
  1156. package/src/ui/shared/PaginatedPageHeader.tsx +49 -0
  1157. package/src/ui/shared/Pagination.tsx +2 -6
  1158. package/src/ui/shared/PostFooter.tsx +130 -67
  1159. package/src/ui/shared/__tests__/home-page-branding.test.tsx +14 -0
  1160. package/src/ui/shared/__tests__/media-gallery.test.ts +82 -0
  1161. package/src/ui/shared/__tests__/navigation-labels.test.ts +103 -0
  1162. package/src/ui/shared/__tests__/pagination.test.ts +18 -1
  1163. package/src/ui/shared/__tests__/post-footer.test.ts +122 -0
  1164. package/src/ui/shared/collection-management-labels.ts +161 -0
  1165. package/src/ui/shared/navigation-labels.ts +126 -0
  1166. package/src/vendor/datastar.js +1955 -7
  1167. package/dist/client/assets/heic-to-DIRPI3VF.js +0 -1
  1168. package/dist/client/assets/module-RjUF93sV.js +0 -716
  1169. package/dist/client/assets/native-48B9X9Wg.js +0 -1
  1170. package/dist/client/assets/url-FWFqPJPb.js +0 -1
  1171. package/dist/client/client.css +0 -1
  1172. package/dist/client/client.js +0 -33565
  1173. package/src/client/components/collection-sidebar-types.ts +0 -43
  1174. package/src/client/tiptap/paste-image.ts +0 -129
  1175. package/src/lib/emoji-catalog.ts +0 -963
  1176. package/src/lib/icon-catalog.ts +0 -5213
  1177. package/src/ui/shared/CollectionsSidebar.tsx +0 -294
@@ -3,29 +3,58 @@
3
3
  *
4
4
  * CRUD operations for posts with Thread support.
5
5
  * Posts have format (note/link/quote), status (draft/published),
6
- * visibility (public/unlisted/private), featuredAt, and pinnedAt timestamp.
6
+ * visibility (public/latest_hidden/private), featuredAt, and pinnedAt timestamp.
7
7
  */
8
8
 
9
- import { eq, and, isNull, desc, inArray, sql, isNotNull } from "drizzle-orm";
10
- import type { BatchItem } from "drizzle-orm/batch";
11
- import { uuidv7 } from "uuidv7";
12
- import { type Database, batchQueryRows } from "../db/index.js";
13
- import { pathRegistry, posts, postCollections } from "../db/schema.js";
9
+ import {
10
+ eq,
11
+ and,
12
+ type SQL,
13
+ type SQLWrapper,
14
+ isNull,
15
+ desc,
16
+ inArray,
17
+ sql,
18
+ isNotNull,
19
+ asc,
20
+ lte,
21
+ } from "drizzle-orm";
22
+ import {
23
+ type Database,
24
+ batchQueryRows,
25
+ supportsDrizzleTransaction,
26
+ } from "../db/index.js";
27
+ import type { DatabaseDialect } from "../db/dialect.js";
28
+ import {
29
+ sqliteSchemaBundle,
30
+ type DatabaseSchema,
31
+ } from "../db/schema-bundle.js";
32
+ import { createEntityId } from "../lib/ids.js";
14
33
  import { now } from "../lib/time.js";
15
34
  import { renderTiptapJson } from "../lib/tiptap-render.js";
16
35
  import { extractSummary, extractBodyText } from "../lib/summary.js";
17
36
  import { markdownToTiptapJson } from "../lib/markdown-to-tiptap.js";
18
37
  import { generatePostSlug } from "../lib/slug.js";
38
+ import { getSlugValidationIssue } from "../lib/slug-format.js";
19
39
  import { normalizePath, slugify } from "../lib/url.js";
20
40
  import type { StorageDriver } from "../lib/storage.js";
21
41
  import type { MediaService } from "./media.js";
42
+ import {
43
+ FORMATS,
44
+ MAX_MEDIA_ATTACHMENTS,
45
+ STATUSES,
46
+ VISIBILITIES,
47
+ } from "../types.js";
22
48
  import type {
49
+ CollectionSortOrder,
23
50
  Format,
24
51
  Status,
25
52
  Visibility,
53
+ SortOrder,
26
54
  MediaKind,
27
55
  Post,
28
56
  CreatePost,
57
+ PostAttachmentInput,
29
58
  UpdatePost,
30
59
  ThreadTimelineContext,
31
60
  } from "../types.js";
@@ -42,6 +71,11 @@ export interface PostDeleteDeps {
42
71
  storage?: StorageDriver | null;
43
72
  }
44
73
 
74
+ export interface PostAttachmentDeps extends PostDeleteDeps {
75
+ storageDriver: string;
76
+ maxFileSizeMB: number;
77
+ }
78
+
45
79
  export interface PostFilters {
46
80
  format?: Format;
47
81
  status?: Status;
@@ -51,8 +85,8 @@ export interface PostFilters {
51
85
  collectionId?: string;
52
86
  /** Exclude posts that are replies (have replyToId set) */
53
87
  excludeReplies?: boolean;
54
- /** Exclude unlisted posts from results */
55
- excludeUnlisted?: boolean;
88
+ /** Exclude posts hidden from Latest from results */
89
+ excludeLatestHidden?: boolean;
56
90
  /** Exclude private posts from results */
57
91
  excludePrivate?: boolean;
58
92
  includeDeleted?: boolean;
@@ -67,8 +101,12 @@ export interface PostFilters {
67
101
  hasMedia?: boolean;
68
102
  /** Filter by title presence */
69
103
  hasTitle?: boolean;
104
+ /** Filter by rating presence */
105
+ hasRating?: boolean;
106
+ /** Explicit result sort order */
107
+ sortOrder?: SortOrder;
70
108
  limit?: number;
71
- cursor?: string; // post id for cursor pagination (UUIDv7 sorts chronologically)
109
+ cursor?: string;
72
110
  offset?: number; // offset for page-based pagination
73
111
  }
74
112
 
@@ -78,18 +116,63 @@ export interface SummaryConfig {
78
116
  maxChars: number;
79
117
  }
80
118
 
119
+ interface ThreadRootPageOptions {
120
+ status?: Status;
121
+ excludePrivate?: boolean;
122
+ limit?: number;
123
+ offset?: number;
124
+ }
125
+
126
+ interface CollectionThreadRootPageOptions extends ThreadRootPageOptions {
127
+ sortOrder?: CollectionSortOrder;
128
+ }
129
+
130
+ interface CursorSortKey {
131
+ direction: "asc" | "desc";
132
+ expr: SQLWrapper;
133
+ value: number | string;
134
+ }
135
+
136
+ export interface CollectionFeedEntry {
137
+ post: Post;
138
+ collectedAt: number;
139
+ }
140
+
81
141
  export interface PostService {
82
142
  getById(id: string): Promise<Post | null>;
83
143
  getBySlug(slug: string): Promise<Post | null>;
144
+ suggestSlug(input: {
145
+ title?: string;
146
+ slug?: string;
147
+ excludePostId?: string;
148
+ }): Promise<string>;
149
+ checkSlugAvailability(slug: string, excludePostId?: string): Promise<boolean>;
84
150
  list(filters?: PostFilters): Promise<Post[]>;
85
151
  /** Count posts matching filters (ignores cursor, offset, limit) */
86
152
  count(filters?: PostFilters): Promise<number>;
153
+ /** Count posts grouped by published year-month (YYYY-MM) */
154
+ countByYearMonth(
155
+ filters?: PostFilters,
156
+ ): Promise<{ yearMonth: string; count: number }[]>;
87
157
  create(data: CreatePost, summaryConfig?: SummaryConfig): Promise<Post>;
158
+ createWithAttachments(
159
+ data: CreatePost,
160
+ attachments: PostAttachmentInput[] | undefined,
161
+ deps: PostAttachmentDeps,
162
+ summaryConfig?: SummaryConfig,
163
+ ): Promise<Post>;
88
164
  update(
89
165
  id: string,
90
166
  data: UpdatePost,
91
167
  summaryConfig?: SummaryConfig,
92
168
  ): Promise<Post | null>;
169
+ updateWithAttachments(
170
+ id: string,
171
+ data: UpdatePost,
172
+ attachments: PostAttachmentInput[] | undefined,
173
+ deps: PostAttachmentDeps,
174
+ summaryConfig?: SummaryConfig,
175
+ ): Promise<Post | null>;
93
176
  /**
94
177
  * Soft-delete a post and clean up its media (storage files + DB records).
95
178
  * Thread roots cascade to all replies.
@@ -115,6 +198,32 @@ export interface PostService {
115
198
  getThreadTimelineContext(
116
199
  rootIds: string[],
117
200
  ): Promise<Map<string, ThreadTimelineContext>>;
201
+ /** Count distinct thread roots that contain featured published posts */
202
+ countFeaturedThreadRoots(options?: ThreadRootPageOptions): Promise<number>;
203
+ /** List featured thread root IDs ordered by the latest featured post in each thread */
204
+ listFeaturedThreadRootIds(options?: ThreadRootPageOptions): Promise<string[]>;
205
+ /** Count distinct thread roots that contain published posts in the given collection */
206
+ countCollectionThreadRoots(
207
+ collectionId: string,
208
+ options?: ThreadRootPageOptions,
209
+ ): Promise<number>;
210
+ /** List collection thread root IDs ordered by collected-at or rating semantics */
211
+ listCollectionThreadRootIds(
212
+ collectionId: string,
213
+ options?: CollectionThreadRootPageOptions,
214
+ ): Promise<string[]>;
215
+ /** List collection feed entries ordered by latest added-at timestamp */
216
+ listCollectionFeedEntries(
217
+ collectionId: string,
218
+ options?: ThreadRootPageOptions,
219
+ ): Promise<CollectionFeedEntry[]>;
220
+ /** Fetch all published, non-deleted posts for each requested thread root */
221
+ getPublishedThreads(rootIds: string[]): Promise<Map<string, Post[]>>;
222
+ /** For each thread, return post IDs that belong to the given collection */
223
+ getCollectionPostIdsByThread(
224
+ collectionId: string,
225
+ threadIds: string[],
226
+ ): Promise<Map<string, string[]>>;
118
227
  /** Get distinct years that have published posts */
119
228
  getDistinctYears(filters?: PostFilters): Promise<number[]>;
120
229
  /** For each thread ID, return the ID of the last published, non-deleted post */
@@ -150,11 +259,77 @@ function hasNonEmptyText(value: string | null | undefined): boolean {
150
259
  return typeof value === "string" && value.trim().length > 0;
151
260
  }
152
261
 
262
+ function ensureAllowedPostValue<T extends string>(
263
+ value: string,
264
+ allowed: readonly T[],
265
+ message: string,
266
+ ErrorCtor: new (message: string) => Error = ValidationError,
267
+ ): T {
268
+ if ((allowed as readonly string[]).includes(value)) {
269
+ return value as T;
270
+ }
271
+
272
+ throw new ErrorCtor(message);
273
+ }
274
+
275
+ function ensurePostFormat(
276
+ value: string,
277
+ ErrorCtor: new (message: string) => Error = ValidationError,
278
+ ): Format {
279
+ return ensureAllowedPostValue(
280
+ value,
281
+ FORMATS,
282
+ "Format must be note, link, or quote.",
283
+ ErrorCtor,
284
+ );
285
+ }
286
+
287
+ function ensurePostStatus(
288
+ value: string,
289
+ ErrorCtor: new (message: string) => Error = ValidationError,
290
+ ): Status {
291
+ return ensureAllowedPostValue(
292
+ value,
293
+ STATUSES,
294
+ "Status must be draft or published.",
295
+ ErrorCtor,
296
+ );
297
+ }
298
+
299
+ function ensurePostVisibility(
300
+ value: string,
301
+ ErrorCtor: new (message: string) => Error = ValidationError,
302
+ ): Visibility {
303
+ return ensureAllowedPostValue(
304
+ value,
305
+ VISIBILITIES,
306
+ "Visibility must be public, hidden from Latest, or private.",
307
+ ErrorCtor,
308
+ );
309
+ }
310
+
311
+ function ensurePostRating(
312
+ value: number | null | undefined,
313
+ ErrorCtor: new (message: string) => Error = ValidationError,
314
+ ): number | null {
315
+ if (value === null || value === undefined) {
316
+ return null;
317
+ }
318
+
319
+ if (Number.isInteger(value) && value >= 1 && value <= 5) {
320
+ return value;
321
+ }
322
+
323
+ throw new ErrorCtor("Rating must be an integer between 1 and 5.");
324
+ }
325
+
153
326
  function assertPostFormatShape(data: {
154
327
  format: Format;
328
+ title?: string | null;
155
329
  url?: string | null;
156
330
  quoteText?: string | null;
157
331
  }): void {
332
+ const hasTitle = hasNonEmptyText(data.title);
158
333
  const hasUrl = hasNonEmptyText(data.url);
159
334
  const hasQuoteText = hasNonEmptyText(data.quoteText);
160
335
 
@@ -169,6 +344,9 @@ function assertPostFormatShape(data: {
169
344
  }
170
345
 
171
346
  if (data.format === "link") {
347
+ if (!hasTitle) {
348
+ throw new ValidationError("Link posts need a title.");
349
+ }
172
350
  if (!hasUrl) {
173
351
  throw new ValidationError("Link posts need a URL.");
174
352
  }
@@ -198,24 +376,65 @@ function assertDraftPublishedAt(
198
376
 
199
377
  export function createPostService(
200
378
  db: Database,
201
- config: { slugIdLength: number },
202
- paths: PathService = createPathService(db),
379
+ config: {
380
+ slugIdLength: number;
381
+ databaseDialect?: DatabaseDialect;
382
+ },
383
+ siteId: string,
384
+ paths: PathService | undefined,
385
+ databaseSchema: DatabaseSchema = sqliteSchemaBundle,
203
386
  ): PostService {
387
+ const resolvedPaths = paths ?? createPathService(db, siteId, databaseSchema);
388
+ const { pathRegistry, posts, postCollections } = databaseSchema;
389
+ const databaseDialect = config.databaseDialect ?? "sqlite";
390
+ const usesBatchWrites = !supportsDrizzleTransaction(db, databaseDialect);
391
+
392
+ function buildPublishedYearMonthExpr(): SQL<string> {
393
+ return databaseDialect === "pg"
394
+ ? sql<string>`to_char(timezone('UTC', to_timestamp(${posts.publishedAt})), 'YYYY-MM')`
395
+ : sql<string>`strftime('%Y-%m', ${posts.publishedAt}, 'unixepoch')`;
396
+ }
397
+
398
+ function buildPublishedYearExpr(): SQL<string> {
399
+ return databaseDialect === "pg"
400
+ ? sql<string>`to_char(timezone('UTC', to_timestamp(${posts.publishedAt})), 'YYYY')`
401
+ : sql<string>`strftime('%Y', ${posts.publishedAt}, 'unixepoch')`;
402
+ }
403
+
204
404
  const effectiveVisibilityExpr = sql<string>`coalesce(
205
405
  ${posts.visibility},
206
406
  (SELECT root.visibility FROM post AS root WHERE root.id = ${posts.threadId})
207
407
  )`;
208
408
 
209
- /** Check if a slug is available (not used by posts or custom_urls) */
409
+ /** Check if a slug is available (not used by posts or path_registry) */
210
410
  async function isSlugAvailable(slug: string): Promise<boolean> {
211
- return paths.isPathAvailable(slug);
411
+ return resolvedPaths.isPathAvailable(slug);
412
+ }
413
+
414
+ async function isSlugAvailableForPost(
415
+ slug: string,
416
+ excludePostId?: string,
417
+ ): Promise<boolean> {
418
+ const resolved = await resolvedPaths.resolve(slug);
419
+ if (!resolved) return true;
420
+
421
+ return Boolean(
422
+ excludePostId &&
423
+ resolved.kind === "slug" &&
424
+ resolved.postId === excludePostId,
425
+ );
212
426
  }
213
427
 
214
428
  async function pathExists(path: string): Promise<boolean> {
215
429
  const rows = await db
216
430
  .select({ id: pathRegistry.id })
217
431
  .from(pathRegistry)
218
- .where(eq(pathRegistry.path, normalizePath(path)))
432
+ .where(
433
+ and(
434
+ eq(pathRegistry.siteId, siteId),
435
+ eq(pathRegistry.path, normalizePath(path)),
436
+ ),
437
+ )
219
438
  .limit(1);
220
439
  return rows.length > 0;
221
440
  }
@@ -228,23 +447,32 @@ export function createPostService(
228
447
  ),
229
448
  })
230
449
  .from(posts)
231
- .where(and(eq(posts.threadId, rootId), isNull(posts.deletedAt)));
450
+ .where(
451
+ and(
452
+ eq(posts.siteId, siteId),
453
+ eq(posts.threadId, rootId),
454
+ isNull(posts.deletedAt),
455
+ ),
456
+ );
232
457
 
233
458
  const latestPublishedAt = rootRows[0]?.latestPublishedAt ?? null;
234
459
  const root = await db
235
460
  .select({ updatedAt: posts.updatedAt })
236
461
  .from(posts)
237
- .where(eq(posts.id, rootId))
462
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, rootId)))
238
463
  .limit(1);
239
464
 
240
465
  const lastActivityAt = latestPublishedAt ?? root[0]?.updatedAt ?? now();
241
466
 
242
- await db.update(posts).set({ lastActivityAt }).where(eq(posts.id, rootId));
467
+ await db
468
+ .update(posts)
469
+ .set({ lastActivityAt })
470
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, rootId)));
243
471
  }
244
472
 
245
473
  /** Build WHERE conditions from filters (shared by list and count) */
246
474
  function buildFilterConditions(filters: PostFilters) {
247
- const conditions = [];
475
+ const conditions = [eq(posts.siteId, siteId)];
248
476
 
249
477
  if (filters.status) {
250
478
  conditions.push(eq(posts.status, filters.status));
@@ -252,8 +480,8 @@ export function createPostService(
252
480
  if (filters.visibility !== undefined) {
253
481
  conditions.push(sql`${effectiveVisibilityExpr} = ${filters.visibility}`);
254
482
  }
255
- if (filters.excludeUnlisted) {
256
- conditions.push(sql`${effectiveVisibilityExpr} != 'unlisted'`);
483
+ if (filters.excludeLatestHidden) {
484
+ conditions.push(sql`${effectiveVisibilityExpr} != 'latest_hidden'`);
257
485
  }
258
486
  if (filters.excludePrivate) {
259
487
  conditions.push(sql`${effectiveVisibilityExpr} != 'private'`);
@@ -278,7 +506,12 @@ export function createPostService(
278
506
  if (filters.collectionId !== undefined) {
279
507
  // Filter by collection via junction table
280
508
  conditions.push(
281
- sql`${posts.id} IN (SELECT post_id FROM post_collection WHERE collection_id = ${filters.collectionId})`,
509
+ sql`${posts.id} IN (
510
+ SELECT post_id
511
+ FROM post_collection
512
+ WHERE site_id = ${siteId}
513
+ AND collection_id = ${filters.collectionId}
514
+ )`,
282
515
  );
283
516
  }
284
517
  if (filters.threadId) {
@@ -299,14 +532,27 @@ export function createPostService(
299
532
  if (filters.mediaKinds && filters.mediaKinds.length > 0) {
300
533
  const placeholders = filters.mediaKinds.map((k) => sql`${k}`);
301
534
  conditions.push(
302
- sql`${posts.id} IN (SELECT post_id FROM media WHERE media_kind IN (${sql.join(placeholders, sql`, `)}))`,
535
+ sql`${posts.id} IN (
536
+ SELECT post_id
537
+ FROM media
538
+ WHERE site_id = ${siteId}
539
+ AND media_kind IN (${sql.join(placeholders, sql`, `)})
540
+ )`,
303
541
  );
304
542
  }
305
543
  if (filters.hasMedia !== undefined) {
306
544
  if (filters.hasMedia) {
307
- conditions.push(sql`${posts.id} IN (SELECT post_id FROM media)`);
545
+ conditions.push(
546
+ sql`${posts.id} IN (
547
+ SELECT post_id FROM media WHERE site_id = ${siteId}
548
+ )`,
549
+ );
308
550
  } else {
309
- conditions.push(sql`${posts.id} NOT IN (SELECT post_id FROM media)`);
551
+ conditions.push(
552
+ sql`${posts.id} NOT IN (
553
+ SELECT post_id FROM media WHERE site_id = ${siteId}
554
+ )`,
555
+ );
310
556
  }
311
557
  }
312
558
  if (filters.hasTitle !== undefined) {
@@ -318,20 +564,163 @@ export function createPostService(
318
564
  conditions.push(sql`(${posts.title} IS NULL OR ${posts.title} = '')`);
319
565
  }
320
566
  }
567
+ if (filters.hasRating !== undefined) {
568
+ conditions.push(
569
+ filters.hasRating ? isNotNull(posts.rating) : isNull(posts.rating),
570
+ );
571
+ }
321
572
 
322
573
  return conditions;
323
574
  }
324
575
 
576
+ function getCursorSortTimestamp(row: typeof posts.$inferSelect): number {
577
+ return row.status === "draft" ? row.updatedAt : (row.lastActivityAt ?? -1);
578
+ }
579
+
580
+ function buildLexicographicCursorCondition(
581
+ keys: [CursorSortKey, ...CursorSortKey[]],
582
+ ): SQL<unknown> {
583
+ const [first, ...rest] = keys;
584
+ const comparison =
585
+ first.direction === "desc"
586
+ ? sql`${first.expr} < ${first.value}`
587
+ : sql`${first.expr} > ${first.value}`;
588
+
589
+ if (rest.length === 0) {
590
+ return comparison;
591
+ }
592
+
593
+ return sql`(
594
+ ${comparison}
595
+ OR (${first.expr} = ${first.value} AND ${buildLexicographicCursorCondition(
596
+ rest as [CursorSortKey, ...CursorSortKey[]],
597
+ )})
598
+ )`;
599
+ }
600
+
601
+ async function buildListCursorCondition(
602
+ filters: PostFilters,
603
+ ): Promise<SQL<unknown> | null> {
604
+ if (!filters.cursor) {
605
+ return null;
606
+ }
607
+
608
+ const cursorRow = await db
609
+ .select()
610
+ .from(posts)
611
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, filters.cursor)))
612
+ .limit(1);
613
+ const cursorPost = cursorRow[0];
614
+
615
+ if (!cursorPost) {
616
+ return null;
617
+ }
618
+
619
+ const sortTimestampExpr =
620
+ filters.status === "draft"
621
+ ? posts.updatedAt
622
+ : filters.status === "published"
623
+ ? posts.lastActivityAt
624
+ : sql<number>`CASE
625
+ WHEN ${posts.status} = 'draft' THEN ${posts.updatedAt}
626
+ ELSE ${posts.lastActivityAt}
627
+ END`;
628
+ const pinnedSortExpr = sql<number>`coalesce(${posts.pinnedAt}, -1)`;
629
+ const featuredSortExpr = sql<number>`coalesce(${posts.featuredAt}, -1)`;
630
+ const sortTimestampSortExpr = sql<number>`coalesce(${sortTimestampExpr}, -1)`;
631
+ const ratingPresenceExpr = sql<number>`CASE
632
+ WHEN ${posts.rating} IS NULL THEN 0
633
+ ELSE 1
634
+ END`;
635
+ const ratingSortExpr = sql<number>`coalesce(${posts.rating}, -1)`;
636
+ const cursorPinnedAt = cursorPost.pinnedAt ?? -1;
637
+ const cursorFeaturedAt = cursorPost.featuredAt ?? -1;
638
+ const cursorSortTimestamp = getCursorSortTimestamp(cursorPost);
639
+ const cursorRatingPresence = cursorPost.rating === null ? 0 : 1;
640
+ const cursorRating = cursorPost.rating ?? -1;
641
+
642
+ if (filters.featured || filters.sortOrder === undefined) {
643
+ if (filters.featured) {
644
+ return buildLexicographicCursorCondition([
645
+ { direction: "desc", expr: pinnedSortExpr, value: cursorPinnedAt },
646
+ {
647
+ direction: "desc",
648
+ expr: featuredSortExpr,
649
+ value: cursorFeaturedAt,
650
+ },
651
+ { direction: "desc", expr: posts.id, value: cursorPost.id },
652
+ ]);
653
+ }
654
+
655
+ return buildLexicographicCursorCondition([
656
+ { direction: "desc", expr: pinnedSortExpr, value: cursorPinnedAt },
657
+ {
658
+ direction: "desc",
659
+ expr: sortTimestampSortExpr,
660
+ value: cursorSortTimestamp,
661
+ },
662
+ { direction: "desc", expr: posts.id, value: cursorPost.id },
663
+ ]);
664
+ }
665
+
666
+ if (filters.sortOrder === "oldest") {
667
+ return buildLexicographicCursorCondition([
668
+ { direction: "desc", expr: pinnedSortExpr, value: cursorPinnedAt },
669
+ {
670
+ direction: "asc",
671
+ expr: sortTimestampSortExpr,
672
+ value: cursorSortTimestamp,
673
+ },
674
+ { direction: "asc", expr: posts.id, value: cursorPost.id },
675
+ ]);
676
+ }
677
+
678
+ if (filters.sortOrder === "rating_desc") {
679
+ return buildLexicographicCursorCondition([
680
+ { direction: "desc", expr: pinnedSortExpr, value: cursorPinnedAt },
681
+ {
682
+ direction: "desc",
683
+ expr: ratingPresenceExpr,
684
+ value: cursorRatingPresence,
685
+ },
686
+ { direction: "desc", expr: ratingSortExpr, value: cursorRating },
687
+ {
688
+ direction: "desc",
689
+ expr: sortTimestampSortExpr,
690
+ value: cursorSortTimestamp,
691
+ },
692
+ { direction: "desc", expr: posts.id, value: cursorPost.id },
693
+ ]);
694
+ }
695
+
696
+ return buildLexicographicCursorCondition([
697
+ { direction: "desc", expr: pinnedSortExpr, value: cursorPinnedAt },
698
+ {
699
+ direction: "desc",
700
+ expr: ratingPresenceExpr,
701
+ value: cursorRatingPresence,
702
+ },
703
+ { direction: "asc", expr: ratingSortExpr, value: cursorRating },
704
+ {
705
+ direction: "desc",
706
+ expr: sortTimestampSortExpr,
707
+ value: cursorSortTimestamp,
708
+ },
709
+ { direction: "desc", expr: posts.id, value: cursorPost.id },
710
+ ]);
711
+ }
712
+
325
713
  function toPost(
326
714
  row: typeof posts.$inferSelect,
327
715
  slug: string,
328
- visibility: Visibility,
716
+ visibility: string,
329
717
  ): Post {
330
718
  return {
331
719
  id: row.id,
332
- format: row.format as Format,
333
- status: row.status as Status,
334
- visibility,
720
+ siteId: row.siteId,
721
+ format: ensurePostFormat(row.format, Error),
722
+ status: ensurePostStatus(row.status, Error),
723
+ visibility: ensurePostVisibility(visibility, Error),
335
724
  pinnedAt: row.pinnedAt,
336
725
  featuredAt: row.featuredAt,
337
726
  slug,
@@ -342,7 +731,7 @@ export function createPostService(
342
731
  bodyText: row.bodyText,
343
732
  quoteText: row.quoteText,
344
733
  summary: row.summary,
345
- rating: row.rating,
734
+ rating: ensurePostRating(row.rating, Error),
346
735
  replyToId: row.replyToId,
347
736
  threadId: row.threadId,
348
737
  deletedAt: row.deletedAt,
@@ -357,19 +746,21 @@ export function createPostService(
357
746
  row: typeof posts.$inferSelect | undefined,
358
747
  ): Promise<Post | null> {
359
748
  if (!row) return null;
360
- const slug = await paths.getPostSlug(row.id);
749
+ const slug = await resolvedPaths.getPostSlug(row.id);
361
750
  if (!slug) return null;
362
751
  const rootVisibilityMap = await getThreadVisibilityMap([row.threadId]);
363
752
  const visibility = rootVisibilityMap.get(row.threadId) ?? row.visibility;
364
753
  if (!visibility) return null;
365
- return toPost(row, slug, visibility as Visibility);
754
+ return toPost(row, slug, visibility);
366
755
  }
367
756
 
368
757
  async function hydratePosts(
369
758
  rows: (typeof posts.$inferSelect)[],
370
759
  ): Promise<Post[]> {
371
760
  if (rows.length === 0) return [];
372
- const slugMap = await paths.getPostSlugMap(rows.map((row) => row.id));
761
+ const slugMap = await resolvedPaths.getPostSlugMap(
762
+ rows.map((row) => row.id),
763
+ );
373
764
  const rootVisibilityMap = await getThreadVisibilityMap(
374
765
  rows.map((row) => row.threadId),
375
766
  );
@@ -378,13 +769,40 @@ export function createPostService(
378
769
  const slug = slugMap.get(row.id);
379
770
  const visibility =
380
771
  rootVisibilityMap.get(row.threadId) ?? row.visibility;
381
- return slug && visibility
382
- ? toPost(row, slug, visibility as Visibility)
383
- : null;
772
+ return slug && visibility ? toPost(row, slug, visibility) : null;
384
773
  })
385
774
  .filter((row): row is Post => row !== null);
386
775
  }
387
776
 
777
+ async function hydratePostsById(ids: string[]): Promise<Map<string, Post>> {
778
+ const result = new Map<string, Post>();
779
+ const uniqueIds = [...new Set(ids)];
780
+
781
+ if (uniqueIds.length === 0) {
782
+ return result;
783
+ }
784
+
785
+ const rows = await batchQueryRows(uniqueIds, (chunk) =>
786
+ db
787
+ .select()
788
+ .from(posts)
789
+ .where(
790
+ and(
791
+ eq(posts.siteId, siteId),
792
+ inArray(posts.id, chunk),
793
+ eq(posts.status, "published"),
794
+ isNull(posts.deletedAt),
795
+ ),
796
+ ),
797
+ );
798
+
799
+ for (const post of await hydratePosts(rows)) {
800
+ result.set(post.id, post);
801
+ }
802
+
803
+ return result;
804
+ }
805
+
388
806
  async function getThreadVisibilityMap(
389
807
  threadIds: string[],
390
808
  ): Promise<Map<string, Visibility>> {
@@ -396,38 +814,188 @@ export function createPostService(
396
814
  db
397
815
  .select({ id: posts.id, visibility: posts.visibility })
398
816
  .from(posts)
399
- .where(inArray(posts.id, chunk)),
817
+ .where(and(eq(posts.siteId, siteId), inArray(posts.id, chunk))),
400
818
  );
401
819
 
402
820
  for (const row of rows) {
403
821
  if (row.visibility) {
404
- result.set(row.id, row.visibility as Visibility);
822
+ result.set(row.id, ensurePostVisibility(row.visibility, Error));
405
823
  }
406
824
  }
407
825
 
408
826
  return result;
409
827
  }
410
828
 
829
+ function buildThreadRootPageConditions(options?: ThreadRootPageOptions) {
830
+ const conditions = [isNull(posts.deletedAt)];
831
+ const status = options?.status;
832
+
833
+ if (status) {
834
+ conditions.push(eq(posts.status, status));
835
+ }
836
+ if (options?.excludePrivate) {
837
+ conditions.push(sql`${effectiveVisibilityExpr} != 'private'`);
838
+ }
839
+
840
+ return conditions;
841
+ }
842
+
843
+ function isMediaAttachmentInput(
844
+ attachment: PostAttachmentInput,
845
+ ): attachment is Extract<PostAttachmentInput, { type: "media" }> {
846
+ return attachment.type === "media";
847
+ }
848
+
849
+ async function createAttachmentMediaIds(
850
+ attachments: PostAttachmentInput[],
851
+ deps: PostAttachmentDeps,
852
+ ) {
853
+ if (attachments.length > MAX_MEDIA_ATTACHMENTS) {
854
+ throw new ValidationError(
855
+ `Posts allow at most ${MAX_MEDIA_ATTACHMENTS} attachments`,
856
+ );
857
+ }
858
+
859
+ const orderedMediaIds: string[] = [];
860
+ const createdTextMediaIds: string[] = [];
861
+ const referencedMediaIds = attachments
862
+ .filter(isMediaAttachmentInput)
863
+ .map((attachment) => attachment.mediaId);
864
+
865
+ await deps.media.validateIds(referencedMediaIds);
866
+
867
+ try {
868
+ for (const attachment of attachments) {
869
+ if (isMediaAttachmentInput(attachment)) {
870
+ orderedMediaIds.push(attachment.mediaId);
871
+ continue;
872
+ }
873
+
874
+ const created = await deps.media.createTextAttachment(attachment, {
875
+ storage: deps.storage,
876
+ storageDriver: deps.storageDriver,
877
+ maxFileSizeMB: deps.maxFileSizeMB,
878
+ });
879
+ orderedMediaIds.push(created.id);
880
+ createdTextMediaIds.push(created.id);
881
+ }
882
+ } catch (error) {
883
+ await cleanupCreatedTextAttachments(createdTextMediaIds, deps);
884
+ throw error;
885
+ }
886
+
887
+ return { orderedMediaIds, createdTextMediaIds };
888
+ }
889
+
890
+ async function applyAttachmentAltUpdates(
891
+ attachments: PostAttachmentInput[],
892
+ deps: PostAttachmentDeps,
893
+ ) {
894
+ const altUpdates = attachments
895
+ .filter(isMediaAttachmentInput)
896
+ .filter((attachment) => attachment.alt !== undefined)
897
+ .map((attachment) =>
898
+ deps.media.updateAlt(attachment.mediaId, attachment.alt ?? ""),
899
+ );
900
+
901
+ await Promise.all(altUpdates);
902
+ }
903
+
904
+ async function cleanupCreatedTextAttachments(
905
+ mediaIds: string[],
906
+ deps: PostAttachmentDeps,
907
+ ) {
908
+ if (mediaIds.length === 0) return;
909
+ await deps.media.deleteByIds(mediaIds, deps.storage).catch(() => undefined);
910
+ }
911
+
912
+ async function getCollectionIdsForPost(postId: string): Promise<string[]> {
913
+ const rows = await db
914
+ .select({ collectionId: postCollections.collectionId })
915
+ .from(postCollections)
916
+ .where(
917
+ and(
918
+ eq(postCollections.siteId, siteId),
919
+ eq(postCollections.postId, postId),
920
+ ),
921
+ );
922
+
923
+ return rows.map((row) => row.collectionId);
924
+ }
925
+
926
+ function buildRollbackUpdate(
927
+ post: Post,
928
+ collectionIds: string[],
929
+ ): UpdatePost {
930
+ return {
931
+ format: post.format,
932
+ title: post.title,
933
+ body: post.body ?? null,
934
+ slug: post.slug,
935
+ status: post.status,
936
+ visibility: post.visibility,
937
+ pinned: post.pinnedAt !== null,
938
+ featured: post.featuredAt !== null,
939
+ url: post.url,
940
+ quoteText: post.quoteText,
941
+ rating: post.rating,
942
+ collectionIds,
943
+ publishedAt: post.publishedAt ?? undefined,
944
+ };
945
+ }
946
+
411
947
  return {
412
948
  async getById(id) {
413
949
  const result = await db
414
950
  .select()
415
951
  .from(posts)
416
- .where(and(eq(posts.id, id), isNull(posts.deletedAt)))
952
+ .where(
953
+ and(
954
+ eq(posts.siteId, siteId),
955
+ eq(posts.id, id),
956
+ isNull(posts.deletedAt),
957
+ ),
958
+ )
417
959
  .limit(1);
418
960
  return hydratePost(result[0]);
419
961
  },
420
962
 
421
963
  async getBySlug(slug) {
422
- const resolved = await paths.resolve(slug);
964
+ const resolved = await resolvedPaths.resolve(slug);
423
965
  if (!resolved || resolved.kind !== "slug" || !resolved.postId) {
424
966
  return null;
425
967
  }
426
968
  return this.getById(resolved.postId);
427
969
  },
428
970
 
971
+ async suggestSlug(input) {
972
+ return generatePostSlug({
973
+ slug: input.slug,
974
+ title: input.title,
975
+ idLength: config.slugIdLength,
976
+ isAvailable: (candidate) =>
977
+ isSlugAvailableForPost(candidate, input.excludePostId),
978
+ });
979
+ },
980
+
981
+ async checkSlugAvailability(slug, excludePostId) {
982
+ const issue = getSlugValidationIssue(slug);
983
+ if (issue === "invalid") {
984
+ throw new ValidationError("Slug contains invalid characters");
985
+ }
986
+ if (issue === "reserved") {
987
+ throw new ValidationError("Slug is reserved");
988
+ }
989
+
990
+ return isSlugAvailableForPost(slug, excludePostId);
991
+ },
992
+
429
993
  async list(filters = {}) {
430
994
  const conditions = buildFilterConditions(filters);
995
+ const cursorCondition = await buildListCursorCondition(filters);
996
+ if (filters.cursor && !cursorCondition) {
997
+ return [];
998
+ }
431
999
  const sortTimestamp =
432
1000
  filters.status === "draft"
433
1001
  ? posts.updatedAt
@@ -438,21 +1006,50 @@ export function createPostService(
438
1006
  ELSE ${posts.lastActivityAt}
439
1007
  END`;
440
1008
 
441
- if (filters.cursor) {
442
- conditions.push(sql`${posts.id} < ${filters.cursor}`);
1009
+ if (cursorCondition) {
1010
+ conditions.push(cursorCondition);
443
1011
  }
444
1012
 
445
- let query = db
1013
+ const ratingPresence = sql<number>`CASE
1014
+ WHEN ${posts.rating} IS NULL THEN 0
1015
+ ELSE 1
1016
+ END`;
1017
+
1018
+ const baseQuery = db
446
1019
  .select()
447
1020
  .from(posts)
448
1021
  .where(conditions.length > 0 ? and(...conditions) : undefined)
449
- .orderBy(
450
- desc(posts.pinnedAt),
451
- filters.featured ? desc(posts.featuredAt) : desc(sortTimestamp),
452
- desc(posts.id),
453
- )
454
1022
  .limit(filters.limit ?? 100);
455
1023
 
1024
+ let query =
1025
+ filters.featured || filters.sortOrder === undefined
1026
+ ? baseQuery.orderBy(
1027
+ desc(posts.pinnedAt),
1028
+ filters.featured ? desc(posts.featuredAt) : desc(sortTimestamp),
1029
+ desc(posts.id),
1030
+ )
1031
+ : filters.sortOrder === "oldest"
1032
+ ? baseQuery.orderBy(
1033
+ desc(posts.pinnedAt),
1034
+ asc(sortTimestamp),
1035
+ asc(posts.id),
1036
+ )
1037
+ : filters.sortOrder === "rating_desc"
1038
+ ? baseQuery.orderBy(
1039
+ desc(posts.pinnedAt),
1040
+ desc(ratingPresence),
1041
+ desc(posts.rating),
1042
+ desc(sortTimestamp),
1043
+ desc(posts.id),
1044
+ )
1045
+ : baseQuery.orderBy(
1046
+ desc(posts.pinnedAt),
1047
+ desc(ratingPresence),
1048
+ asc(posts.rating),
1049
+ desc(sortTimestamp),
1050
+ desc(posts.id),
1051
+ );
1052
+
456
1053
  if (filters.offset !== undefined) {
457
1054
  query = query.offset(filters.offset) as typeof query;
458
1055
  }
@@ -472,12 +1069,39 @@ export function createPostService(
472
1069
  return result[0]?.count ?? 0;
473
1070
  },
474
1071
 
1072
+ async countByYearMonth(filters = {}) {
1073
+ const conditions = [
1074
+ ...buildFilterConditions(filters),
1075
+ isNotNull(posts.publishedAt),
1076
+ ];
1077
+ const publishedYearMonthExpr = buildPublishedYearMonthExpr();
1078
+
1079
+ return db
1080
+ .select({
1081
+ yearMonth: publishedYearMonthExpr.as("year_month"),
1082
+ count: sql<number>`count(*)`.as("count"),
1083
+ })
1084
+ .from(posts)
1085
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
1086
+ .groupBy(publishedYearMonthExpr)
1087
+ .orderBy(desc(publishedYearMonthExpr));
1088
+ },
1089
+
475
1090
  async create(data, summaryConfig) {
476
- const id = uuidv7();
1091
+ const id = createEntityId("post");
477
1092
  const timestamp = now();
1093
+ const format = ensurePostFormat(data.format);
1094
+ const requestedStatus =
1095
+ data.status !== undefined ? ensurePostStatus(data.status) : undefined;
1096
+ const requestedVisibility =
1097
+ data.visibility !== undefined
1098
+ ? ensurePostVisibility(data.visibility)
1099
+ : undefined;
1100
+ const rating = ensurePostRating(data.rating);
478
1101
 
479
1102
  assertPostFormatShape({
480
- format: data.format,
1103
+ format,
1104
+ title: data.title,
481
1105
  url: data.url,
482
1106
  quoteText: data.quoteText,
483
1107
  });
@@ -490,7 +1114,7 @@ export function createPostService(
490
1114
 
491
1115
  // Generate summary for titled notes with body content
492
1116
  let summary: string | null = null;
493
- if (data.format === "note" && data.title && body && summaryConfig) {
1117
+ if (format === "note" && data.title && body && summaryConfig) {
494
1118
  summary = extractSummary(
495
1119
  body,
496
1120
  summaryConfig.maxParagraphs,
@@ -500,8 +1124,8 @@ export function createPostService(
500
1124
 
501
1125
  // Handle thread relationship
502
1126
  let threadId = id;
503
- let status: Status = data.status ?? "published";
504
- let visibility: Visibility | null = data.visibility ?? "public";
1127
+ let status: Status = requestedStatus ?? "published";
1128
+ let visibility: Visibility | null = requestedVisibility ?? "public";
505
1129
 
506
1130
  if (data.replyToId) {
507
1131
  const parent = await this.getById(data.replyToId);
@@ -524,7 +1148,7 @@ export function createPostService(
524
1148
  : await this.getById(parent.threadId);
525
1149
  if (root) {
526
1150
  if (data.status !== "draft") {
527
- status = root.status as Status;
1151
+ status = root.status;
528
1152
  }
529
1153
  }
530
1154
  visibility = null;
@@ -557,7 +1181,7 @@ export function createPostService(
557
1181
  isAvailable: isSlugAvailable,
558
1182
  });
559
1183
  // Verify the alias path is available before proceeding
560
- if (!(await paths.isPathAvailable(normalized))) {
1184
+ if (!(await resolvedPaths.isPathAvailable(normalized))) {
561
1185
  throw new ConflictError(`Path "${normalized}" is already in use`);
562
1186
  }
563
1187
  aliasPath = normalized;
@@ -574,48 +1198,41 @@ export function createPostService(
574
1198
  const collectionIds = [...new Set(data.collectionIds ?? [])];
575
1199
 
576
1200
  try {
577
- const writeQueries: BatchItem<"sqlite">[] = [
578
- db.insert(posts).values({
579
- id,
580
- format: data.format,
581
- status,
582
- visibility,
583
- pinnedAt: data.pinned ? timestamp : null,
584
- featuredAt: data.featured ? timestamp : null,
585
- title: data.title ?? null,
586
- url: data.url ?? null,
587
- body: body ?? null,
588
- bodyHtml,
589
- bodyText,
590
- quoteText: data.quoteText ?? null,
591
- summary,
592
- rating: data.rating ?? null,
593
- replyToId: data.replyToId ?? null,
594
- threadId,
595
- publishedAt,
596
- lastActivityAt: publishedAt ?? timestamp,
597
- createdAt: timestamp,
598
- updatedAt: timestamp,
599
- }),
600
- db.insert(pathRegistry).values({
601
- id: uuidv7(),
602
- path: normalizePath(slug),
603
- kind: "slug",
604
- postId: id,
605
- collectionId: null,
606
- redirectToPath: null,
607
- redirectType: null,
608
- createdAt: timestamp,
609
- updatedAt: timestamp,
610
- }),
611
- ];
612
-
613
- if (aliasPath) {
1201
+ if (usesBatchWrites) {
1202
+ const writeQueries = [];
1203
+
1204
+ writeQueries.push(
1205
+ db.insert(posts).values({
1206
+ id,
1207
+ siteId,
1208
+ format,
1209
+ status,
1210
+ visibility,
1211
+ pinnedAt: data.pinned ? timestamp : null,
1212
+ featuredAt: data.featured ? timestamp : null,
1213
+ title: data.title ?? null,
1214
+ url: data.url ?? null,
1215
+ body: body ?? null,
1216
+ bodyHtml,
1217
+ bodyText,
1218
+ quoteText: data.quoteText ?? null,
1219
+ summary,
1220
+ rating,
1221
+ replyToId: data.replyToId ?? null,
1222
+ threadId,
1223
+ publishedAt,
1224
+ lastActivityAt: publishedAt ?? timestamp,
1225
+ createdAt: timestamp,
1226
+ updatedAt: timestamp,
1227
+ }),
1228
+ );
1229
+
614
1230
  writeQueries.push(
615
1231
  db.insert(pathRegistry).values({
616
- id: uuidv7(),
617
- path: normalizePath(aliasPath),
618
- kind: "alias",
1232
+ id: createEntityId("path"),
1233
+ siteId,
1234
+ path: normalizePath(slug),
1235
+ kind: "slug",
619
1236
  postId: id,
620
1237
  collectionId: null,
621
1238
  redirectToPath: null,
@@ -624,26 +1241,109 @@ export function createPostService(
624
1241
  updatedAt: timestamp,
625
1242
  }),
626
1243
  );
627
- }
628
1244
 
629
- if (collectionIds.length > 0) {
630
- writeQueries.push(
631
- db.insert(postCollections).values(
632
- collectionIds.map((collectionId) => ({
1245
+ if (aliasPath) {
1246
+ writeQueries.push(
1247
+ db.insert(pathRegistry).values({
1248
+ id: createEntityId("path"),
1249
+ siteId,
1250
+ path: normalizePath(aliasPath),
1251
+ kind: "alias",
633
1252
  postId: id,
634
- collectionId,
1253
+ collectionId: null,
1254
+ redirectToPath: null,
1255
+ redirectType: null,
635
1256
  createdAt: timestamp,
636
- })),
637
- ),
1257
+ updatedAt: timestamp,
1258
+ }),
1259
+ );
1260
+ }
1261
+
1262
+ if (collectionIds.length > 0) {
1263
+ writeQueries.push(
1264
+ db.insert(postCollections).values(
1265
+ collectionIds.map((collectionId) => ({
1266
+ siteId,
1267
+ postId: id,
1268
+ collectionId,
1269
+ createdAt: timestamp,
1270
+ })),
1271
+ ),
1272
+ );
1273
+ }
1274
+
1275
+ await db.batch(
1276
+ writeQueries as [
1277
+ (typeof writeQueries)[number],
1278
+ ...(typeof writeQueries)[number][],
1279
+ ],
638
1280
  );
639
- }
1281
+ } else {
1282
+ await db.transaction(async (tx) => {
1283
+ await tx.insert(posts).values({
1284
+ id,
1285
+ siteId,
1286
+ format,
1287
+ status,
1288
+ visibility,
1289
+ pinnedAt: data.pinned ? timestamp : null,
1290
+ featuredAt: data.featured ? timestamp : null,
1291
+ title: data.title ?? null,
1292
+ url: data.url ?? null,
1293
+ body: body ?? null,
1294
+ bodyHtml,
1295
+ bodyText,
1296
+ quoteText: data.quoteText ?? null,
1297
+ summary,
1298
+ rating,
1299
+ replyToId: data.replyToId ?? null,
1300
+ threadId,
1301
+ publishedAt,
1302
+ lastActivityAt: publishedAt ?? timestamp,
1303
+ createdAt: timestamp,
1304
+ updatedAt: timestamp,
1305
+ });
640
1306
 
641
- await db.batch(
642
- writeQueries as [
643
- (typeof writeQueries)[number],
644
- ...(typeof writeQueries)[number][],
645
- ],
646
- );
1307
+ await tx.insert(pathRegistry).values({
1308
+ id: createEntityId("path"),
1309
+ siteId,
1310
+ path: normalizePath(slug),
1311
+ kind: "slug",
1312
+ postId: id,
1313
+ collectionId: null,
1314
+ redirectToPath: null,
1315
+ redirectType: null,
1316
+ createdAt: timestamp,
1317
+ updatedAt: timestamp,
1318
+ });
1319
+
1320
+ if (aliasPath) {
1321
+ await tx.insert(pathRegistry).values({
1322
+ id: createEntityId("path"),
1323
+ siteId,
1324
+ path: normalizePath(aliasPath),
1325
+ kind: "alias",
1326
+ postId: id,
1327
+ collectionId: null,
1328
+ redirectToPath: null,
1329
+ redirectType: null,
1330
+ createdAt: timestamp,
1331
+ updatedAt: timestamp,
1332
+ });
1333
+ }
1334
+
1335
+ if (collectionIds.length > 0) {
1336
+ await tx.insert(postCollections).values(
1337
+ collectionIds.map((collectionId) => ({
1338
+ siteId,
1339
+ postId: id,
1340
+ collectionId,
1341
+ createdAt: timestamp,
1342
+ })),
1343
+ );
1344
+ }
1345
+ });
1346
+ }
647
1347
  } catch (err) {
648
1348
  if (err instanceof ConflictError) {
649
1349
  throw new ConflictError(`Slug "${slug}" is already in use`);
@@ -667,19 +1367,61 @@ export function createPostService(
667
1367
  return post;
668
1368
  },
669
1369
 
1370
+ async createWithAttachments(data, attachments, deps, summaryConfig) {
1371
+ const attachmentInputs = attachments ?? [];
1372
+ const { orderedMediaIds, createdTextMediaIds } =
1373
+ await createAttachmentMediaIds(attachmentInputs, deps);
1374
+
1375
+ try {
1376
+ const post = await this.create(data, summaryConfig);
1377
+
1378
+ try {
1379
+ if (orderedMediaIds.length > 0) {
1380
+ await deps.media.attachToPost(post.id, orderedMediaIds);
1381
+ }
1382
+ await applyAttachmentAltUpdates(attachmentInputs, deps);
1383
+ return post;
1384
+ } catch (error) {
1385
+ await deps.media.attachToPost(post.id, []).catch(() => undefined);
1386
+ await this.delete(post.id, {
1387
+ media: deps.media,
1388
+ storage: deps.storage,
1389
+ }).catch(() => undefined);
1390
+ await cleanupCreatedTextAttachments(createdTextMediaIds, deps);
1391
+ throw error;
1392
+ }
1393
+ } catch (error) {
1394
+ await cleanupCreatedTextAttachments(createdTextMediaIds, deps);
1395
+ throw error;
1396
+ }
1397
+ },
1398
+
670
1399
  async update(id, data, summaryConfig) {
671
1400
  const existing = await this.getById(id);
672
1401
  if (!existing) return null;
673
1402
 
674
1403
  const timestamp = now();
675
- const nextFormat = data.format ?? existing.format;
1404
+ const nextFormat =
1405
+ data.format !== undefined
1406
+ ? ensurePostFormat(data.format)
1407
+ : existing.format;
676
1408
  const nextUrl = data.url !== undefined ? data.url : existing.url;
677
1409
  const nextQuoteText =
678
1410
  data.quoteText !== undefined ? data.quoteText : existing.quoteText;
679
- const nextStatus = data.status ?? existing.status;
1411
+ const nextStatus =
1412
+ data.status !== undefined
1413
+ ? ensurePostStatus(data.status)
1414
+ : existing.status;
1415
+ const nextVisibility =
1416
+ data.visibility !== undefined
1417
+ ? ensurePostVisibility(data.visibility)
1418
+ : undefined;
1419
+ const nextRating =
1420
+ data.rating !== undefined ? ensurePostRating(data.rating) : undefined;
680
1421
 
681
1422
  assertPostFormatShape({
682
1423
  format: nextFormat,
1424
+ title: data.title !== undefined ? data.title : existing.title,
683
1425
  url: nextUrl,
684
1426
  quoteText: nextQuoteText,
685
1427
  });
@@ -694,7 +1436,7 @@ export function createPostService(
694
1436
  data.slug !== undefined && data.slug !== existing.slug;
695
1437
  if (slugChanged && data.slug) {
696
1438
  try {
697
- await paths.updatePostSlug(id, data.slug);
1439
+ await resolvedPaths.updatePostSlug(id, data.slug);
698
1440
  } catch (err) {
699
1441
  if (err instanceof ConflictError) {
700
1442
  throw new ConflictError(`Slug "${data.slug}" is already in use`);
@@ -703,11 +1445,11 @@ export function createPostService(
703
1445
  }
704
1446
  }
705
1447
 
706
- if (data.format !== undefined) updates.format = data.format;
1448
+ if (data.format !== undefined) updates.format = nextFormat;
707
1449
  if (data.title !== undefined) updates.title = data.title;
708
1450
  if (data.url !== undefined) updates.url = data.url;
709
1451
  if (data.quoteText !== undefined) updates.quoteText = data.quoteText;
710
- if (data.rating !== undefined) updates.rating = data.rating;
1452
+ if (data.rating !== undefined) updates.rating = nextRating;
711
1453
  if (data.pinned !== undefined)
712
1454
  updates.pinnedAt = data.pinned ? now() : null;
713
1455
  if (data.featured !== undefined)
@@ -728,7 +1470,7 @@ export function createPostService(
728
1470
 
729
1471
  // Recompute summary when body, title, or format change
730
1472
  if (summaryConfig) {
731
- const format = data.format ?? (existing.format as Format);
1473
+ const format = nextFormat;
732
1474
  const title = data.title !== undefined ? data.title : existing.title;
733
1475
  const body =
734
1476
  data.bodyMarkdown !== undefined
@@ -767,8 +1509,7 @@ export function createPostService(
767
1509
  const statusChanged =
768
1510
  data.status !== undefined && data.status !== existing.status;
769
1511
  const visibilityChanged =
770
- data.visibility !== undefined &&
771
- data.visibility !== existing.visibility;
1512
+ nextVisibility !== undefined && nextVisibility !== existing.visibility;
772
1513
  const publishedAtChanged = data.publishedAt !== undefined;
773
1514
  const nextPublishedAt =
774
1515
  nextStatus === "draft"
@@ -779,9 +1520,9 @@ export function createPostService(
779
1520
  ? timestamp
780
1521
  : (existing.publishedAt ?? timestamp);
781
1522
 
782
- if (statusChanged) updates.status = data.status;
1523
+ if (statusChanged) updates.status = nextStatus;
783
1524
  if (visibilityChanged && !isThreadReply(existing)) {
784
- updates.visibility = data.visibility;
1525
+ updates.visibility = nextVisibility;
785
1526
  }
786
1527
  if (statusChanged || publishedAtChanged || existing.status === "draft") {
787
1528
  updates.publishedAt = nextPublishedAt;
@@ -793,6 +1534,9 @@ export function createPostService(
793
1534
  const needsReplyVisibilityCleanup =
794
1535
  !isThreadReply(existing) && (statusChanged || visibilityChanged);
795
1536
  const needsCollectionSync = data.collectionIds !== undefined;
1537
+ const nextCollectionIds = needsCollectionSync
1538
+ ? [...new Set(data.collectionIds ?? [])]
1539
+ : [];
796
1540
  const needsThreadActivityRecalc =
797
1541
  statusChanged || publishedAtChanged || existing.status === "draft";
798
1542
  const hasExtraWrites =
@@ -803,7 +1547,7 @@ export function createPostService(
803
1547
  const result = await db
804
1548
  .update(posts)
805
1549
  .set(updates)
806
- .where(eq(posts.id, id))
1550
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, id)))
807
1551
  .returning();
808
1552
  if (needsThreadActivityRecalc) {
809
1553
  await recalculateThreadLastActivity(existing.threadId);
@@ -812,69 +1556,191 @@ export function createPostService(
812
1556
  return hydratePost(result[0]);
813
1557
  }
814
1558
 
815
- // Complex case: batch cascade + update + collection sync atomically
816
- const writeQueries: BatchItem<"sqlite">[] = [];
1559
+ // Complex case: cascade + update + collection sync atomically
1560
+ const existingCollectionIds = needsCollectionSync
1561
+ ? await getCollectionIdsForPost(id)
1562
+ : [];
1563
+ let updateResult: (typeof posts.$inferSelect)[] | undefined;
817
1564
 
818
- if (needsCascade) {
819
- writeQueries.push(
820
- db
821
- .update(posts)
822
- .set({
823
- status: data.status ?? (existing.status as Status),
824
- publishedAt: nextStatus === "published" ? nextPublishedAt : null,
825
- lastActivityAt:
826
- nextStatus === "published"
827
- ? (nextPublishedAt ?? timestamp)
828
- : timestamp,
829
- updatedAt: timestamp,
830
- })
831
- .where(and(eq(posts.threadId, id), isNotNull(posts.replyToId))),
832
- );
833
- }
1565
+ if (usesBatchWrites) {
1566
+ const writeQueries = [];
1567
+
1568
+ if (needsCascade) {
1569
+ writeQueries.push(
1570
+ db
1571
+ .update(posts)
1572
+ .set({
1573
+ status: nextStatus,
1574
+ publishedAt:
1575
+ nextStatus === "published" ? nextPublishedAt : null,
1576
+ lastActivityAt:
1577
+ nextStatus === "published"
1578
+ ? (nextPublishedAt ?? timestamp)
1579
+ : timestamp,
1580
+ updatedAt: timestamp,
1581
+ })
1582
+ .where(
1583
+ and(
1584
+ eq(posts.siteId, siteId),
1585
+ eq(posts.threadId, id),
1586
+ isNotNull(posts.replyToId),
1587
+ ),
1588
+ ),
1589
+ );
1590
+ }
834
1591
 
835
- if (needsReplyVisibilityCleanup) {
1592
+ if (needsReplyVisibilityCleanup) {
1593
+ writeQueries.push(
1594
+ db
1595
+ .update(posts)
1596
+ .set({ visibility: null, updatedAt: timestamp })
1597
+ .where(
1598
+ and(
1599
+ eq(posts.siteId, siteId),
1600
+ eq(posts.threadId, id),
1601
+ isNotNull(posts.replyToId),
1602
+ ),
1603
+ ),
1604
+ );
1605
+ }
1606
+
1607
+ const updateIdx = writeQueries.length;
836
1608
  writeQueries.push(
837
1609
  db
838
1610
  .update(posts)
839
- .set({ visibility: null, updatedAt: timestamp })
840
- .where(and(eq(posts.threadId, id), isNotNull(posts.replyToId))),
1611
+ .set(updates)
1612
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, id)))
1613
+ .returning(),
841
1614
  );
842
- }
843
-
844
- // Post update is always present; track its index for result extraction
845
- const updateIdx = writeQueries.length;
846
- writeQueries.push(
847
- db.update(posts).set(updates).where(eq(posts.id, id)).returning(),
848
- );
849
1615
 
850
- if (needsCollectionSync) {
851
- writeQueries.push(
852
- db.delete(postCollections).where(eq(postCollections.postId, id)),
853
- );
854
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by needsCollectionSync
855
- if (data.collectionIds!.length > 0) {
856
- writeQueries.push(
857
- db.insert(postCollections).values(
858
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by needsCollectionSync
859
- data.collectionIds!.map((collectionId) => ({
860
- postId: id,
861
- collectionId,
862
- createdAt: now(),
863
- })),
864
- ),
1616
+ if (needsCollectionSync) {
1617
+ const existingIds = new Set(existingCollectionIds);
1618
+ const nextIds = new Set(nextCollectionIds);
1619
+ const removedIds = existingCollectionIds.filter(
1620
+ (cid) => !nextIds.has(cid),
1621
+ );
1622
+ const addedIds = nextCollectionIds.filter(
1623
+ (cid) => !existingIds.has(cid),
865
1624
  );
1625
+
1626
+ if (removedIds.length > 0) {
1627
+ writeQueries.push(
1628
+ db
1629
+ .delete(postCollections)
1630
+ .where(
1631
+ and(
1632
+ eq(postCollections.siteId, siteId),
1633
+ eq(postCollections.postId, id),
1634
+ inArray(postCollections.collectionId, removedIds),
1635
+ ),
1636
+ ),
1637
+ );
1638
+ }
1639
+
1640
+ if (addedIds.length > 0) {
1641
+ const collectionTimestamp = now();
1642
+ writeQueries.push(
1643
+ db.insert(postCollections).values(
1644
+ addedIds.map((collectionId) => ({
1645
+ siteId,
1646
+ postId: id,
1647
+ collectionId,
1648
+ createdAt: collectionTimestamp,
1649
+ })),
1650
+ ),
1651
+ );
1652
+ }
866
1653
  }
1654
+
1655
+ const results = await db.batch(
1656
+ writeQueries as [
1657
+ (typeof writeQueries)[number],
1658
+ ...(typeof writeQueries)[number][],
1659
+ ],
1660
+ );
1661
+ updateResult = results[updateIdx] as
1662
+ | (typeof posts.$inferSelect)[]
1663
+ | undefined;
1664
+ } else {
1665
+ await db.transaction(async (tx) => {
1666
+ if (needsCascade) {
1667
+ await tx
1668
+ .update(posts)
1669
+ .set({
1670
+ status: nextStatus,
1671
+ publishedAt:
1672
+ nextStatus === "published" ? nextPublishedAt : null,
1673
+ lastActivityAt:
1674
+ nextStatus === "published"
1675
+ ? (nextPublishedAt ?? timestamp)
1676
+ : timestamp,
1677
+ updatedAt: timestamp,
1678
+ })
1679
+ .where(
1680
+ and(
1681
+ eq(posts.siteId, siteId),
1682
+ eq(posts.threadId, id),
1683
+ isNotNull(posts.replyToId),
1684
+ ),
1685
+ );
1686
+ }
1687
+
1688
+ if (needsReplyVisibilityCleanup) {
1689
+ await tx
1690
+ .update(posts)
1691
+ .set({ visibility: null, updatedAt: timestamp })
1692
+ .where(
1693
+ and(
1694
+ eq(posts.siteId, siteId),
1695
+ eq(posts.threadId, id),
1696
+ isNotNull(posts.replyToId),
1697
+ ),
1698
+ );
1699
+ }
1700
+
1701
+ updateResult = await tx
1702
+ .update(posts)
1703
+ .set(updates)
1704
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, id)))
1705
+ .returning();
1706
+
1707
+ if (needsCollectionSync) {
1708
+ const existingIds = new Set(existingCollectionIds);
1709
+ const nextIds = new Set(nextCollectionIds);
1710
+ const removedIds = existingCollectionIds.filter(
1711
+ (cid) => !nextIds.has(cid),
1712
+ );
1713
+ const addedIds = nextCollectionIds.filter(
1714
+ (cid) => !existingIds.has(cid),
1715
+ );
1716
+
1717
+ if (removedIds.length > 0) {
1718
+ await tx
1719
+ .delete(postCollections)
1720
+ .where(
1721
+ and(
1722
+ eq(postCollections.siteId, siteId),
1723
+ eq(postCollections.postId, id),
1724
+ inArray(postCollections.collectionId, removedIds),
1725
+ ),
1726
+ );
1727
+ }
1728
+
1729
+ if (addedIds.length > 0) {
1730
+ const collectionTimestamp = now();
1731
+ await tx.insert(postCollections).values(
1732
+ addedIds.map((collectionId) => ({
1733
+ siteId,
1734
+ postId: id,
1735
+ collectionId,
1736
+ createdAt: collectionTimestamp,
1737
+ })),
1738
+ );
1739
+ }
1740
+ }
1741
+ });
867
1742
  }
868
1743
 
869
- const results = await db.batch(
870
- writeQueries as [
871
- (typeof writeQueries)[number],
872
- ...(typeof writeQueries)[number][],
873
- ],
874
- );
875
- const updateResult = results[updateIdx] as
876
- | (typeof posts.$inferSelect)[]
877
- | undefined;
878
1744
  if (needsThreadActivityRecalc) {
879
1745
  await recalculateThreadLastActivity(existing.threadId);
880
1746
  return this.getById(id);
@@ -882,6 +1748,80 @@ export function createPostService(
882
1748
  return hydratePost(updateResult?.[0]);
883
1749
  },
884
1750
 
1751
+ async updateWithAttachments(id, data, attachments, deps, summaryConfig) {
1752
+ if (attachments === undefined) {
1753
+ return this.update(id, data, summaryConfig);
1754
+ }
1755
+
1756
+ const existingPost = await this.getById(id);
1757
+ if (!existingPost) return null;
1758
+
1759
+ const existingCollectionIds = await getCollectionIdsForPost(id);
1760
+ const rollbackData = buildRollbackUpdate(
1761
+ existingPost,
1762
+ existingCollectionIds,
1763
+ );
1764
+ const existingAttachments = await deps.media.getByPostId(id);
1765
+ const previousMediaIds = existingAttachments.map(
1766
+ (attachment) => attachment.id,
1767
+ );
1768
+ const previousAltMap = new Map(
1769
+ existingAttachments.map((attachment) => [
1770
+ attachment.id,
1771
+ attachment.alt ?? "",
1772
+ ]),
1773
+ );
1774
+ const { orderedMediaIds, createdTextMediaIds } =
1775
+ await createAttachmentMediaIds(attachments, deps);
1776
+ const post = await this.update(id, data, summaryConfig);
1777
+
1778
+ if (!post) {
1779
+ await cleanupCreatedTextAttachments(createdTextMediaIds, deps);
1780
+ return null;
1781
+ }
1782
+
1783
+ let replacedAttachments = false;
1784
+
1785
+ try {
1786
+ await deps.media.attachToPost(post.id, orderedMediaIds);
1787
+ replacedAttachments = true;
1788
+ await applyAttachmentAltUpdates(attachments, deps);
1789
+
1790
+ const nextAttachmentIds = new Set(orderedMediaIds);
1791
+ const removedTextAttachmentIds = existingAttachments
1792
+ .filter(
1793
+ (attachment) =>
1794
+ attachment.mimeType === "text/x-tiptap+json" &&
1795
+ !nextAttachmentIds.has(attachment.id),
1796
+ )
1797
+ .map((attachment) => attachment.id);
1798
+ await deps.media
1799
+ .deleteByIds(removedTextAttachmentIds, deps.storage)
1800
+ .catch(() => undefined);
1801
+
1802
+ return post;
1803
+ } catch (error) {
1804
+ if (replacedAttachments) {
1805
+ await deps.media
1806
+ .attachToPost(post.id, previousMediaIds)
1807
+ .catch(() => undefined);
1808
+ await Promise.all(
1809
+ existingAttachments.map((attachment) =>
1810
+ deps.media.updateAlt(
1811
+ attachment.id,
1812
+ previousAltMap.get(attachment.id) ?? "",
1813
+ ),
1814
+ ),
1815
+ ).catch(() => undefined);
1816
+ }
1817
+ await this.update(id, rollbackData, summaryConfig).catch(
1818
+ () => undefined,
1819
+ );
1820
+ await cleanupCreatedTextAttachments(createdTextMediaIds, deps);
1821
+ throw error;
1822
+ }
1823
+ },
1824
+
885
1825
  async delete(id, deps) {
886
1826
  const existing = await this.getById(id);
887
1827
  if (!existing) return false;
@@ -913,13 +1853,13 @@ export function createPostService(
913
1853
  await db
914
1854
  .update(posts)
915
1855
  .set({ deletedAt: timestamp, updatedAt: timestamp })
916
- .where(eq(posts.threadId, id));
1856
+ .where(and(eq(posts.siteId, siteId), eq(posts.threadId, id)));
917
1857
  } else {
918
1858
  // Soft-delete the single reply
919
1859
  await db
920
1860
  .update(posts)
921
1861
  .set({ deletedAt: timestamp, updatedAt: timestamp })
922
- .where(eq(posts.id, id));
1862
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, id)));
923
1863
  await recalculateThreadLastActivity(existing.threadId);
924
1864
  }
925
1865
 
@@ -930,36 +1870,82 @@ export function createPostService(
930
1870
  const rows = await db
931
1871
  .select()
932
1872
  .from(posts)
933
- .where(and(eq(posts.threadId, rootId), isNull(posts.deletedAt)))
1873
+ .where(
1874
+ and(
1875
+ eq(posts.siteId, siteId),
1876
+ eq(posts.threadId, rootId),
1877
+ isNull(posts.deletedAt),
1878
+ ),
1879
+ )
934
1880
  .orderBy(posts.createdAt);
935
1881
 
936
1882
  return hydratePosts(rows);
937
1883
  },
938
1884
 
939
1885
  async updateThreadStatusAndVisibility(rootId, status, visibility) {
1886
+ const nextStatus = ensurePostStatus(status);
1887
+ const nextVisibility = ensurePostVisibility(visibility);
940
1888
  const timestamp = now();
941
- await db.batch([
942
- db
943
- .update(posts)
944
- .set({
945
- status,
946
- visibility,
947
- publishedAt: status === "published" ? timestamp : null,
948
- lastActivityAt: timestamp,
949
- updatedAt: timestamp,
950
- })
951
- .where(eq(posts.id, rootId)),
952
- db
953
- .update(posts)
954
- .set({
955
- status,
956
- visibility: null,
957
- publishedAt: status === "published" ? timestamp : null,
958
- lastActivityAt: timestamp,
959
- updatedAt: timestamp,
960
- })
961
- .where(and(eq(posts.threadId, rootId), isNotNull(posts.replyToId))),
962
- ]);
1889
+ if (usesBatchWrites) {
1890
+ await db.batch([
1891
+ db
1892
+ .update(posts)
1893
+ .set({
1894
+ status: nextStatus,
1895
+ visibility: nextVisibility,
1896
+ publishedAt: nextStatus === "published" ? timestamp : null,
1897
+ lastActivityAt: timestamp,
1898
+ updatedAt: timestamp,
1899
+ })
1900
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, rootId))),
1901
+ db
1902
+ .update(posts)
1903
+ .set({
1904
+ status: nextStatus,
1905
+ visibility: null,
1906
+ publishedAt: nextStatus === "published" ? timestamp : null,
1907
+ lastActivityAt: timestamp,
1908
+ updatedAt: timestamp,
1909
+ })
1910
+ .where(
1911
+ and(
1912
+ eq(posts.siteId, siteId),
1913
+ eq(posts.threadId, rootId),
1914
+ isNotNull(posts.replyToId),
1915
+ ),
1916
+ ),
1917
+ ]);
1918
+ } else {
1919
+ await db.transaction(async (tx) => {
1920
+ await tx
1921
+ .update(posts)
1922
+ .set({
1923
+ status: nextStatus,
1924
+ visibility: nextVisibility,
1925
+ publishedAt: nextStatus === "published" ? timestamp : null,
1926
+ lastActivityAt: timestamp,
1927
+ updatedAt: timestamp,
1928
+ })
1929
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, rootId)));
1930
+
1931
+ await tx
1932
+ .update(posts)
1933
+ .set({
1934
+ status: nextStatus,
1935
+ visibility: null,
1936
+ publishedAt: nextStatus === "published" ? timestamp : null,
1937
+ lastActivityAt: timestamp,
1938
+ updatedAt: timestamp,
1939
+ })
1940
+ .where(
1941
+ and(
1942
+ eq(posts.siteId, siteId),
1943
+ eq(posts.threadId, rootId),
1944
+ isNotNull(posts.replyToId),
1945
+ ),
1946
+ );
1947
+ });
1948
+ }
963
1949
  await recalculateThreadLastActivity(rootId);
964
1950
  },
965
1951
 
@@ -974,6 +1960,7 @@ export function createPostService(
974
1960
  .from(posts)
975
1961
  .where(
976
1962
  and(
1963
+ eq(posts.siteId, siteId),
977
1964
  inArray(posts.threadId, postIds),
978
1965
  eq(posts.status, "published"),
979
1966
  isNotNull(posts.replyToId),
@@ -992,29 +1979,57 @@ export function createPostService(
992
1979
  async getThreadPreviews(rootIds, previewCount = 3) {
993
1980
  if (rootIds.length === 0) return new Map();
994
1981
 
995
- const rows = await db
996
- .select()
1982
+ const rankedReplies = db
1983
+ .select({
1984
+ id: posts.id,
1985
+ threadId: posts.threadId,
1986
+ createdAt: posts.createdAt,
1987
+ previewRank: sql<number>`ROW_NUMBER() OVER (
1988
+ PARTITION BY ${posts.threadId}
1989
+ ORDER BY ${posts.createdAt}, ${posts.id}
1990
+ )`.as("preview_rank"),
1991
+ })
997
1992
  .from(posts)
998
1993
  .where(
999
1994
  and(
1995
+ eq(posts.siteId, siteId),
1000
1996
  inArray(posts.threadId, rootIds),
1001
1997
  eq(posts.status, "published"),
1002
1998
  isNotNull(posts.replyToId),
1003
1999
  isNull(posts.deletedAt),
1004
2000
  ),
1005
2001
  )
1006
- .orderBy(posts.threadId, posts.createdAt);
2002
+ .as("ranked_replies");
1007
2003
 
2004
+ const rankedRows = await db
2005
+ .select({
2006
+ id: rankedReplies.id,
2007
+ threadId: rankedReplies.threadId,
2008
+ createdAt: rankedReplies.createdAt,
2009
+ })
2010
+ .from(rankedReplies)
2011
+ .where(lte(rankedReplies.previewRank, previewCount))
2012
+ .orderBy(
2013
+ rankedReplies.threadId,
2014
+ rankedReplies.createdAt,
2015
+ rankedReplies.id,
2016
+ );
2017
+
2018
+ const hydratedPosts = await hydratePostsById(
2019
+ rankedRows.map((row) => row.id),
2020
+ );
1008
2021
  const result = new Map<string, Post[]>();
1009
- for (const post of await hydratePosts(rows)) {
1010
- const list = result.get(post.threadId);
2022
+ for (const row of rankedRows) {
2023
+ const post = hydratedPosts.get(row.id);
2024
+ if (!post) continue;
2025
+
2026
+ const list = result.get(row.threadId);
1011
2027
  if (list) {
1012
- if (list.length < previewCount) {
1013
- list.push(post);
1014
- }
1015
- } else {
1016
- result.set(post.threadId, [post]);
2028
+ list.push(post);
2029
+ continue;
1017
2030
  }
2031
+
2032
+ result.set(row.threadId, [post]);
1018
2033
  }
1019
2034
  return result;
1020
2035
  },
@@ -1022,46 +2037,312 @@ export function createPostService(
1022
2037
  async getThreadTimelineContext(rootIds) {
1023
2038
  if (rootIds.length === 0) return new Map();
1024
2039
 
1025
- // Fetch all non-deleted replies ordered by thread, newest first
1026
- const rows = await db
1027
- .select()
2040
+ const rankedReplies = db
2041
+ .select({
2042
+ id: posts.id,
2043
+ threadId: posts.threadId,
2044
+ replyToId: posts.replyToId,
2045
+ replyRank: sql<number>`ROW_NUMBER() OVER (
2046
+ PARTITION BY ${posts.threadId}
2047
+ ORDER BY ${posts.createdAt} DESC, ${posts.id} DESC
2048
+ )`.as("reply_rank"),
2049
+ totalReplyCount: sql<number>`COUNT(*) OVER (
2050
+ PARTITION BY ${posts.threadId}
2051
+ )`.as("total_reply_count"),
2052
+ })
1028
2053
  .from(posts)
1029
2054
  .where(
1030
2055
  and(
2056
+ eq(posts.siteId, siteId),
1031
2057
  inArray(posts.threadId, rootIds),
1032
2058
  eq(posts.status, "published"),
1033
2059
  isNotNull(posts.replyToId),
1034
2060
  isNull(posts.deletedAt),
1035
2061
  ),
1036
2062
  )
1037
- .orderBy(posts.threadId, desc(posts.createdAt), desc(posts.id));
2063
+ .as("ranked_replies");
2064
+
2065
+ const latestReplyRows = await db
2066
+ .select({
2067
+ threadId: rankedReplies.threadId,
2068
+ latestReplyId: rankedReplies.id,
2069
+ latestReplyToId: rankedReplies.replyToId,
2070
+ totalReplyCount: rankedReplies.totalReplyCount,
2071
+ })
2072
+ .from(rankedReplies)
2073
+ .where(eq(rankedReplies.replyRank, 1));
2074
+
2075
+ const relatedPostIds = latestReplyRows.flatMap((row) => {
2076
+ const ids = [row.latestReplyId];
2077
+
2078
+ if (row.latestReplyToId && row.latestReplyToId !== row.threadId) {
2079
+ ids.push(row.latestReplyToId);
2080
+ }
2081
+
2082
+ return ids;
2083
+ });
2084
+ const hydratedPosts = await hydratePostsById(relatedPostIds);
2085
+
2086
+ const result = new Map<string, ThreadTimelineContext>();
2087
+ for (const row of latestReplyRows) {
2088
+ const latestReply = hydratedPosts.get(row.latestReplyId);
2089
+ if (!latestReply) continue;
2090
+
2091
+ const parentReply =
2092
+ row.latestReplyToId && row.latestReplyToId !== row.threadId
2093
+ ? (hydratedPosts.get(row.latestReplyToId) ?? null)
2094
+ : null;
2095
+
2096
+ result.set(row.threadId, {
2097
+ latestReply,
2098
+ parentReply,
2099
+ totalReplyCount: row.totalReplyCount,
2100
+ });
2101
+ }
2102
+
2103
+ return result;
2104
+ },
2105
+
2106
+ async countFeaturedThreadRoots(options = {}) {
2107
+ const conditions = [
2108
+ ...buildThreadRootPageConditions(options),
2109
+ isNotNull(posts.featuredAt),
2110
+ ];
2111
+
2112
+ const rows = await db
2113
+ .select({
2114
+ count: sql<number>`count(distinct ${posts.threadId})`.as("count"),
2115
+ })
2116
+ .from(posts)
2117
+ .where(and(...conditions));
2118
+
2119
+ return rows[0]?.count ?? 0;
2120
+ },
2121
+
2122
+ async listFeaturedThreadRootIds(options = {}) {
2123
+ const conditions = [
2124
+ ...buildThreadRootPageConditions(options),
2125
+ isNotNull(posts.featuredAt),
2126
+ ];
2127
+ const latestFeaturedAt = sql<number>`MAX(${posts.featuredAt})`.as(
2128
+ "latest_featured_at",
2129
+ );
2130
+
2131
+ let query = db
2132
+ .select({
2133
+ threadId: posts.threadId,
2134
+ latestFeaturedAt,
2135
+ })
2136
+ .from(posts)
2137
+ .where(and(...conditions))
2138
+ .groupBy(posts.threadId)
2139
+ .orderBy(desc(latestFeaturedAt), desc(posts.threadId));
2140
+
2141
+ if (options.limit !== undefined) {
2142
+ query = query.limit(options.limit) as typeof query;
2143
+ }
2144
+ if (options.offset !== undefined) {
2145
+ query = query.offset(options.offset) as typeof query;
2146
+ }
2147
+
2148
+ const rows = await query;
2149
+ return rows.map((row) => row.threadId);
2150
+ },
2151
+
2152
+ async countCollectionThreadRoots(collectionId, options = {}) {
2153
+ const conditions = [
2154
+ ...buildThreadRootPageConditions(options),
2155
+ eq(postCollections.collectionId, collectionId),
2156
+ ];
2157
+
2158
+ const rows = await db
2159
+ .select({
2160
+ count: sql<number>`count(distinct ${posts.threadId})`.as("count"),
2161
+ })
2162
+ .from(posts)
2163
+ .innerJoin(
2164
+ postCollections,
2165
+ and(
2166
+ eq(postCollections.siteId, siteId),
2167
+ eq(postCollections.postId, posts.id),
2168
+ ),
2169
+ )
2170
+ .where(and(...conditions));
2171
+
2172
+ return rows[0]?.count ?? 0;
2173
+ },
2174
+
2175
+ async listCollectionThreadRootIds(collectionId, options = {}) {
2176
+ const conditions = [
2177
+ ...buildThreadRootPageConditions(options),
2178
+ eq(postCollections.collectionId, collectionId),
2179
+ ];
2180
+ const sortOrder = options.sortOrder ?? "newest";
2181
+ const collectedAt =
2182
+ sortOrder === "oldest"
2183
+ ? sql<number>`MIN(${postCollections.createdAt})`.as("collected_at")
2184
+ : sql<number>`MAX(${postCollections.createdAt})`.as("collected_at");
2185
+ const ratingPresence = sql<number>`MAX(
2186
+ CASE
2187
+ WHEN ${posts.rating} IS NULL THEN 0
2188
+ ELSE 1
2189
+ END
2190
+ )`.as("rating_presence");
2191
+ const ratingValue = sql<number | null>`MAX(${posts.rating})`.as(
2192
+ "rating_value",
2193
+ );
2194
+
2195
+ const baseQuery = db
2196
+ .select({
2197
+ threadId: posts.threadId,
2198
+ collectedAt,
2199
+ ratingPresence,
2200
+ ratingValue,
2201
+ })
2202
+ .from(posts)
2203
+ .innerJoin(
2204
+ postCollections,
2205
+ and(
2206
+ eq(postCollections.siteId, siteId),
2207
+ eq(postCollections.postId, posts.id),
2208
+ ),
2209
+ )
2210
+ .where(and(...conditions))
2211
+ .groupBy(posts.threadId);
2212
+
2213
+ let query =
2214
+ sortOrder === "oldest"
2215
+ ? baseQuery.orderBy(asc(collectedAt), asc(posts.threadId))
2216
+ : sortOrder === "rating_desc"
2217
+ ? baseQuery.orderBy(
2218
+ desc(ratingPresence),
2219
+ desc(ratingValue),
2220
+ desc(collectedAt),
2221
+ desc(posts.threadId),
2222
+ )
2223
+ : baseQuery.orderBy(desc(collectedAt), desc(posts.threadId));
2224
+
2225
+ if (options.limit !== undefined) {
2226
+ query = query.limit(options.limit) as typeof query;
2227
+ }
2228
+ if (options.offset !== undefined) {
2229
+ query = query.offset(options.offset) as typeof query;
2230
+ }
2231
+
2232
+ const rows = await query;
2233
+ return rows.map((row) => row.threadId);
2234
+ },
2235
+
2236
+ async listCollectionFeedEntries(collectionId, options = {}) {
2237
+ const conditions = [
2238
+ ...buildThreadRootPageConditions(options),
2239
+ eq(postCollections.collectionId, collectionId),
2240
+ ];
2241
+ const collectedAt = sql<number>`MAX(${postCollections.createdAt})`.as(
2242
+ "collected_at",
2243
+ );
2244
+
2245
+ let query = db
2246
+ .select({
2247
+ threadId: posts.threadId,
2248
+ collectedAt,
2249
+ })
2250
+ .from(posts)
2251
+ .innerJoin(
2252
+ postCollections,
2253
+ and(
2254
+ eq(postCollections.siteId, siteId),
2255
+ eq(postCollections.postId, posts.id),
2256
+ ),
2257
+ )
2258
+ .where(and(...conditions))
2259
+ .groupBy(posts.threadId)
2260
+ .orderBy(desc(collectedAt), desc(posts.threadId));
2261
+
2262
+ if (options.limit !== undefined) {
2263
+ query = query.limit(options.limit) as typeof query;
2264
+ }
2265
+ if (options.offset !== undefined) {
2266
+ query = query.offset(options.offset) as typeof query;
2267
+ }
2268
+
2269
+ const rows = await query;
2270
+ const postsById = await hydratePostsById(rows.map((row) => row.threadId));
2271
+
2272
+ return rows.flatMap((row) => {
2273
+ const post = postsById.get(row.threadId);
2274
+ return post ? [{ post, collectedAt: row.collectedAt }] : [];
2275
+ });
2276
+ },
2277
+
2278
+ async getPublishedThreads(rootIds) {
2279
+ const result = new Map<string, Post[]>();
2280
+ if (rootIds.length === 0) return result;
2281
+
2282
+ const unique = [...new Set(rootIds)];
2283
+ const rows = await db
2284
+ .select()
2285
+ .from(posts)
2286
+ .where(
2287
+ and(
2288
+ eq(posts.siteId, siteId),
2289
+ inArray(posts.threadId, unique),
2290
+ eq(posts.status, "published"),
2291
+ isNull(posts.deletedAt),
2292
+ ),
2293
+ )
2294
+ .orderBy(posts.threadId, posts.createdAt, posts.id);
1038
2295
 
1039
- // Group by threadId, extract latest reply + its parent + count
1040
- const grouped = new Map<string, Post[]>();
1041
2296
  for (const post of await hydratePosts(rows)) {
1042
- const list = grouped.get(post.threadId);
1043
- if (list) {
1044
- list.push(post);
2297
+ const thread = result.get(post.threadId);
2298
+ if (thread) {
2299
+ thread.push(post);
1045
2300
  } else {
1046
- grouped.set(post.threadId, [post]);
2301
+ result.set(post.threadId, [post]);
1047
2302
  }
1048
2303
  }
1049
2304
 
1050
- const result = new Map<string, ThreadTimelineContext>();
1051
- for (const [threadId, replies] of grouped) {
1052
- // replies are ordered newest-first; first element is the latest
1053
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- grouped only contains non-empty arrays
1054
- const latestReply = replies[0]!;
1055
- const totalReplyCount = replies.length;
1056
-
1057
- // Find parent of latestReply if it's not the root
1058
- let parentReply: Post | null = null;
1059
- if (latestReply.replyToId && latestReply.replyToId !== threadId) {
1060
- parentReply =
1061
- replies.find((r) => r.id === latestReply.replyToId) ?? null;
1062
- }
2305
+ return result;
2306
+ },
1063
2307
 
1064
- result.set(threadId, { latestReply, parentReply, totalReplyCount });
2308
+ async getCollectionPostIdsByThread(collectionId, threadIds) {
2309
+ const result = new Map<string, string[]>();
2310
+ if (threadIds.length === 0) return result;
2311
+
2312
+ const unique = [...new Set(threadIds)];
2313
+ const rows = await batchQueryRows(unique, (chunk) =>
2314
+ db
2315
+ .select({
2316
+ threadId: posts.threadId,
2317
+ postId: posts.id,
2318
+ })
2319
+ .from(posts)
2320
+ .innerJoin(
2321
+ postCollections,
2322
+ and(
2323
+ eq(postCollections.siteId, siteId),
2324
+ eq(postCollections.postId, posts.id),
2325
+ ),
2326
+ )
2327
+ .where(
2328
+ and(
2329
+ eq(posts.siteId, siteId),
2330
+ eq(postCollections.collectionId, collectionId),
2331
+ inArray(posts.threadId, chunk),
2332
+ eq(posts.status, "published"),
2333
+ isNull(posts.deletedAt),
2334
+ ),
2335
+ )
2336
+ .orderBy(posts.threadId, posts.createdAt, posts.id),
2337
+ );
2338
+
2339
+ for (const row of rows) {
2340
+ const list = result.get(row.threadId);
2341
+ if (list) {
2342
+ list.push(row.postId);
2343
+ } else {
2344
+ result.set(row.threadId, [row.postId]);
2345
+ }
1065
2346
  }
1066
2347
 
1067
2348
  return result;
@@ -1075,20 +2356,23 @@ export function createPostService(
1075
2356
  const rows = await db
1076
2357
  .select({
1077
2358
  threadId: posts.threadId,
1078
- id: sql<string>`(
1079
- SELECT p2.id FROM post AS p2
1080
- WHERE p2.thread_id = ${posts.threadId}
1081
- AND p2.deleted_at IS NULL
1082
- AND p2.status = 'published'
1083
- ORDER BY p2.created_at DESC, p2.id DESC
1084
- LIMIT 1
1085
- )`.as("last_id"),
2359
+ id: posts.id,
1086
2360
  })
1087
2361
  .from(posts)
1088
- .where(inArray(posts.id, unique));
2362
+ .where(
2363
+ and(
2364
+ eq(posts.siteId, siteId),
2365
+ inArray(posts.threadId, unique),
2366
+ eq(posts.status, "published"),
2367
+ isNull(posts.deletedAt),
2368
+ ),
2369
+ )
2370
+ .orderBy(posts.threadId, desc(posts.createdAt), desc(posts.id));
1089
2371
 
1090
2372
  for (const row of rows) {
1091
- if (row.id) result.set(row.threadId, row.id);
2373
+ if (!result.has(row.threadId)) {
2374
+ result.set(row.threadId, row.id);
2375
+ }
1092
2376
  }
1093
2377
  return result;
1094
2378
  },
@@ -1098,17 +2382,16 @@ export function createPostService(
1098
2382
  ...buildFilterConditions(filters),
1099
2383
  isNotNull(posts.publishedAt),
1100
2384
  ];
2385
+ const publishedYearExpr = buildPublishedYearExpr();
1101
2386
 
1102
2387
  const rows = await db
1103
2388
  .select({
1104
- year: sql<string>`strftime('%Y', ${posts.publishedAt}, 'unixepoch')`.as(
1105
- "year",
1106
- ),
2389
+ year: publishedYearExpr.as("year"),
1107
2390
  })
1108
2391
  .from(posts)
1109
2392
  .where(conditions.length > 0 ? and(...conditions) : undefined)
1110
- .groupBy(sql`strftime('%Y', ${posts.publishedAt}, 'unixepoch')`)
1111
- .orderBy(desc(sql`strftime('%Y', ${posts.publishedAt}, 'unixepoch')`));
2393
+ .groupBy(publishedYearExpr)
2394
+ .orderBy(desc(publishedYearExpr));
1112
2395
 
1113
2396
  return rows.map((r) => parseInt(r.year, 10));
1114
2397
  },