@jant/core 0.3.38 → 0.3.40

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