@jant/core 0.6.14 → 0.6.15

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 (311) hide show
  1. package/bin/commands/import-site.js +81 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -10,7 +10,7 @@ import type { Editor, JSONContent } from "@tiptap/core";
10
10
  import type {
11
11
  PostFormInitial,
12
12
  PostFormLabels,
13
- PostCollectionOption,
13
+ ThreadCollectionOption,
14
14
  PostMediaItem,
15
15
  PostSubmitDetail,
16
16
  PostFormat,
@@ -116,7 +116,7 @@ export class JantPostForm extends LitElement {
116
116
 
117
117
  declare labels: PostFormLabels;
118
118
  declare initial: PostFormInitial;
119
- declare collections: PostCollectionOption[];
119
+ declare collections: ThreadCollectionOption[];
120
120
  declare media: PostMediaItem[];
121
121
  declare action: string;
122
122
  declare cancelHref: string;
@@ -191,7 +191,7 @@ export class JantPostForm extends LitElement {
191
191
  });
192
192
  }
193
193
  if (typeof this.collections === "string") {
194
- this.collections = parseJson<PostCollectionOption[]>(
194
+ this.collections = parseJson<ThreadCollectionOption[]>(
195
195
  this.collections,
196
196
  [],
197
197
  );
@@ -27,6 +27,7 @@ import type { CollectionSubmitDetail } from "./collection-types.js";
27
27
 
28
28
  interface PostMenuData {
29
29
  id: string;
30
+ threadId: string;
30
31
  pinned: boolean;
31
32
  pinnedInCollection: boolean;
32
33
  featured: boolean;
@@ -44,7 +45,7 @@ interface CollectionsResponse {
44
45
  collections?: CollectionItem[];
45
46
  }
46
47
 
47
- interface PostCollectionsResponse {
48
+ interface ThreadCollectionsResponse {
48
49
  collectionIds?: string[];
49
50
  }
50
51
 
@@ -78,6 +79,34 @@ export function removeLeadingFeedDivider(
78
79
  firstDivider?.remove();
79
80
  }
80
81
 
82
+ function findThreadRootArticle(threadId: string): HTMLElement | null {
83
+ return (
84
+ Array.from(
85
+ document.querySelectorAll<HTMLElement>("article[data-post-id]"),
86
+ ).find((article) => article.dataset.postId === threadId) ?? null
87
+ );
88
+ }
89
+
90
+ function readPostMenuData(article: HTMLElement): PostMenuData | null {
91
+ const id = article.dataset.postId;
92
+ if (!id) return null;
93
+
94
+ const threadId = article.dataset.threadRootId ?? id;
95
+ const threadRootArticle = findThreadRootArticle(threadId) ?? article;
96
+
97
+ return {
98
+ id,
99
+ threadId,
100
+ pinned: article.hasAttribute("data-post-pinned"),
101
+ pinnedInCollection: threadRootArticle.hasAttribute(
102
+ "data-post-pinned-in-collection",
103
+ ),
104
+ featured: article.hasAttribute("data-post-featured"),
105
+ visibility: article.dataset.postVisibility ?? "public",
106
+ isReply: threadId !== id || article.hasAttribute("data-post-reply"),
107
+ };
108
+ }
109
+
81
110
  export class JantPostMenu extends LitElement {
82
111
  static properties = {
83
112
  _open: { state: true },
@@ -89,7 +118,7 @@ export class JantPostMenu extends LitElement {
89
118
  _collections: { state: true },
90
119
  _collectionsLoading: { state: true },
91
120
  _collectionSearch: { state: true },
92
- _postCollectionIds: { state: true },
121
+ _threadCollectionIds: { state: true },
93
122
  _addCollectionPanelOpen: { state: true },
94
123
  };
95
124
 
@@ -102,7 +131,7 @@ export class JantPostMenu extends LitElement {
102
131
  declare _collections: CollectionItem[] | null;
103
132
  declare _collectionsLoading: boolean;
104
133
  declare _collectionSearch: string;
105
- declare _postCollectionIds: string[];
134
+ declare _threadCollectionIds: string[];
106
135
  declare _addCollectionPanelOpen: boolean;
107
136
  declare _triggerEl: HTMLElement | null;
108
137
 
@@ -127,7 +156,7 @@ export class JantPostMenu extends LitElement {
127
156
  this._collections = null;
128
157
  this._collectionsLoading = false;
129
158
  this._collectionSearch = "";
130
- this._postCollectionIds = [];
159
+ this._threadCollectionIds = [];
131
160
  this._addCollectionPanelOpen = false;
132
161
  this._triggerEl = null;
133
162
  }
@@ -246,25 +275,16 @@ export class JantPostMenu extends LitElement {
246
275
  const article = trigger.closest<HTMLElement>("article[data-post]");
247
276
  if (!article) return;
248
277
 
249
- const postId = article.dataset.postId;
250
- if (!postId) return;
278
+ const menuData = readPostMenuData(article);
279
+ if (!menuData) return;
251
280
 
252
281
  // Toggle: close if same post, open if different
253
- if (this._open && this._data?.id === postId) {
282
+ if (this._open && this._data?.id === menuData.id) {
254
283
  this.#close();
255
284
  return;
256
285
  }
257
286
 
258
- this._data = {
259
- id: postId,
260
- pinned: article.hasAttribute("data-post-pinned"),
261
- pinnedInCollection: article.hasAttribute(
262
- "data-post-pinned-in-collection",
263
- ),
264
- featured: article.hasAttribute("data-post-featured"),
265
- visibility: article.dataset.postVisibility ?? "public",
266
- isReply: article.hasAttribute("data-post-reply"),
267
- };
287
+ this._data = menuData;
268
288
 
269
289
  // Position relative to trigger
270
290
  this._triggerEl = trigger;
@@ -472,19 +492,10 @@ export class JantPostMenu extends LitElement {
472
492
  * relative to the post's menu trigger button.
473
493
  */
474
494
  openCollectionsForPost(article: HTMLElement) {
475
- const postId = article.dataset.postId;
476
- if (!postId) return;
477
-
478
- this._data = {
479
- id: postId,
480
- pinned: article.hasAttribute("data-post-pinned"),
481
- pinnedInCollection: article.hasAttribute(
482
- "data-post-pinned-in-collection",
483
- ),
484
- featured: article.hasAttribute("data-post-featured"),
485
- visibility: article.dataset.postVisibility ?? "public",
486
- isReply: article.hasAttribute("data-post-reply"),
487
- };
495
+ const menuData = readPostMenuData(article);
496
+ if (!menuData || menuData.isReply) return;
497
+
498
+ this._data = menuData;
488
499
 
489
500
  const trigger = article.querySelector<HTMLElement>(
490
501
  "[data-post-menu-trigger]",
@@ -650,7 +661,7 @@ export class JantPostMenu extends LitElement {
650
661
 
651
662
  #toggleCollectionPin() {
652
663
  if (!this._data) return;
653
- const postId = this._data.id;
664
+ const threadId = this._data.threadId;
654
665
  const collectionId = document.querySelector<HTMLElement>(
655
666
  "[data-collection-id]",
656
667
  )?.dataset.collectionId;
@@ -659,9 +670,7 @@ export class JantPostMenu extends LitElement {
659
670
  const newPinned = !this._data.pinnedInCollection;
660
671
 
661
672
  // Optimistic update
662
- const article = document.querySelector<HTMLElement>(
663
- `article[data-post-id="${postId}"]`,
664
- );
673
+ const article = findThreadRootArticle(threadId);
665
674
  if (article) {
666
675
  if (newPinned) {
667
676
  article.setAttribute("data-post-pinned-in-collection", "");
@@ -677,14 +686,14 @@ export class JantPostMenu extends LitElement {
677
686
  this.#close();
678
687
 
679
688
  const method = newPinned ? "PUT" : "DELETE";
680
- fetch(`/api/collections/${collectionId}/posts/${postId}/pin`, { method })
689
+ fetch(`/api/collections/${collectionId}/threads/${threadId}/pin`, {
690
+ method,
691
+ })
681
692
  .then((res) => {
682
693
  if (!res.ok) throw new Error();
683
694
  })
684
695
  .catch(() => {
685
- const el = document.querySelector<HTMLElement>(
686
- `article[data-post-id="${postId}"]`,
687
- );
696
+ const el = findThreadRootArticle(threadId);
688
697
  if (el) {
689
698
  if (newPinned) {
690
699
  el.removeAttribute("data-post-pinned-in-collection");
@@ -754,7 +763,7 @@ export class JantPostMenu extends LitElement {
754
763
  }
755
764
 
756
765
  async #openCollectionPicker() {
757
- if (!this._data) return;
766
+ if (!this._data || this._data.isReply) return;
758
767
  const postId = this._data.id;
759
768
  this._view = "collections";
760
769
  this._collectionSearch = "";
@@ -776,18 +785,18 @@ export class JantPostMenu extends LitElement {
776
785
  const collectionsData =
777
786
  (await collectionsRes.json()) as CollectionsResponse;
778
787
  const collections = collectionsData.collections ?? [];
779
- let postCollectionIds = this._postCollectionIds;
788
+ let threadCollectionIds = this._threadCollectionIds;
780
789
 
781
790
  if (postRes.ok) {
782
- const postData = (await postRes.json()) as PostCollectionsResponse;
783
- postCollectionIds = postData.collectionIds ?? [];
791
+ const postData = (await postRes.json()) as ThreadCollectionsResponse;
792
+ threadCollectionIds = postData.collectionIds ?? [];
784
793
  }
785
794
  this.#collectionPickerOrder = getSelectedFirstOrder(
786
795
  collections,
787
- postCollectionIds,
796
+ threadCollectionIds,
788
797
  );
789
798
  this._collections = collections;
790
- this._postCollectionIds = postCollectionIds;
799
+ this._threadCollectionIds = threadCollectionIds;
791
800
  } catch {
792
801
  this._collections = this._collections ?? [];
793
802
  showToast("Could not load collections.", "error");
@@ -800,23 +809,23 @@ export class JantPostMenu extends LitElement {
800
809
 
801
810
  #toggleCollection(collectionId: string) {
802
811
  if (!this._data) return;
803
- const postId = this._data.id;
804
- const isSelected = this._postCollectionIds.includes(collectionId);
812
+ const threadId = this._data.threadId;
813
+ const isSelected = this._threadCollectionIds.includes(collectionId);
805
814
 
806
815
  // Optimistic update
807
816
  if (isSelected) {
808
- this._postCollectionIds = this._postCollectionIds.filter(
817
+ this._threadCollectionIds = this._threadCollectionIds.filter(
809
818
  (id) => id !== collectionId,
810
819
  );
811
820
  } else {
812
- this._postCollectionIds = [...this._postCollectionIds, collectionId];
821
+ this._threadCollectionIds = [...this._threadCollectionIds, collectionId];
813
822
  }
814
823
  this.#collectionsDirty = true;
815
824
  showToast(isSelected ? "Removed from collection." : "Added to collection.");
816
825
 
817
826
  // Fire request in background, revert on failure
818
827
  if (isSelected) {
819
- fetch(`/api/collections/${collectionId}/posts/${postId}`, {
828
+ fetch(`/api/collections/${collectionId}/threads/${threadId}`, {
820
829
  method: "DELETE",
821
830
  })
822
831
  .then((res) => {
@@ -825,19 +834,19 @@ export class JantPostMenu extends LitElement {
825
834
  })
826
835
  .catch(() => {
827
836
  // Revert: re-add
828
- if (!this._postCollectionIds.includes(collectionId)) {
829
- this._postCollectionIds = [
830
- ...this._postCollectionIds,
837
+ if (!this._threadCollectionIds.includes(collectionId)) {
838
+ this._threadCollectionIds = [
839
+ ...this._threadCollectionIds,
831
840
  collectionId,
832
841
  ];
833
842
  }
834
843
  showToast("Could not remove from collection. Try again.", "error");
835
844
  });
836
845
  } else {
837
- fetch(`/api/collections/${collectionId}/posts`, {
846
+ fetch(`/api/collections/${collectionId}/threads`, {
838
847
  method: "POST",
839
848
  headers: { "Content-Type": "application/json" },
840
- body: JSON.stringify({ postId }),
849
+ body: JSON.stringify({ threadId }),
841
850
  })
842
851
  .then((res) => {
843
852
  if (!res.ok) {
@@ -849,7 +858,7 @@ export class JantPostMenu extends LitElement {
849
858
  })
850
859
  .catch(() => {
851
860
  // Revert: remove
852
- this._postCollectionIds = this._postCollectionIds.filter(
861
+ this._threadCollectionIds = this._threadCollectionIds.filter(
853
862
  (id) => id !== collectionId,
854
863
  );
855
864
  showToast("Could not add to collection. Try again.", "error");
@@ -920,14 +929,14 @@ export class JantPostMenu extends LitElement {
920
929
 
921
930
  this._collections = [...(this._collections ?? []), newItem];
922
931
 
923
- // Auto-add the post to the newly created collection
932
+ // Auto-add the thread to the newly created collection
924
933
  if (this._data) {
925
- await fetch(`/api/collections/${created.id}/posts`, {
934
+ await fetch(`/api/collections/${created.id}/threads`, {
926
935
  method: "POST",
927
936
  headers: { "Content-Type": "application/json" },
928
- body: JSON.stringify({ postId: this._data.id }),
937
+ body: JSON.stringify({ threadId: this._data.threadId }),
929
938
  });
930
- this._postCollectionIds = [...this._postCollectionIds, created.id];
939
+ this._threadCollectionIds = [...this._threadCollectionIds, created.id];
931
940
  }
932
941
 
933
942
  await this.#refreshComposeCollections();
@@ -1193,7 +1202,7 @@ export class JantPostMenu extends LitElement {
1193
1202
  ? html`<div class="post-menu-picker-empty">Loading...</div>`
1194
1203
  : filtered.length > 0
1195
1204
  ? filtered.map((c) => {
1196
- const selected = this._postCollectionIds.includes(c.id);
1205
+ const selected = this._threadCollectionIds.includes(c.id);
1197
1206
  return html`
1198
1207
  <button
1199
1208
  type="button"
@@ -1492,19 +1501,22 @@ export class JantPostMenu extends LitElement {
1492
1501
  <span class="post-menu-item-trailing">${this.#iconEdit()}</span>
1493
1502
  </button>
1494
1503
 
1495
- <button
1496
- type="button"
1497
- role="menuitem"
1498
- class="post-menu-item"
1499
- data-post-menu-open-collections
1500
- @click=${() => this.#openCollectionPicker()}
1501
- >
1502
- <span class="post-menu-item-label">Add to collection</span>
1503
- <span class="post-menu-item-trailing post-menu-item-chevron"
1504
- >${this.#iconChevronRight()}</span
1505
- >
1506
- </button>
1507
-
1504
+ ${this._data.isReply
1505
+ ? nothing
1506
+ : html`
1507
+ <button
1508
+ type="button"
1509
+ role="menuitem"
1510
+ class="post-menu-item"
1511
+ data-post-menu-open-collections
1512
+ @click=${() => this.#openCollectionPicker()}
1513
+ >
1514
+ <span class="post-menu-item-label">Add to collection</span>
1515
+ <span class="post-menu-item-trailing post-menu-item-chevron"
1516
+ >${this.#iconChevronRight()}</span
1517
+ >
1518
+ </button>
1519
+ `}
1508
1520
  ${this._data.isReply
1509
1521
  ? nothing
1510
1522
  : html`
@@ -1562,7 +1574,7 @@ export class JantPostMenu extends LitElement {
1562
1574
  >
1563
1575
  </button>
1564
1576
  `}
1565
- ${collectionId
1577
+ ${collectionId && !this._data.isReply
1566
1578
  ? html`
1567
1579
  <button
1568
1580
  type="button"
@@ -3,18 +3,27 @@
3
3
  */
4
4
 
5
5
  import type { SystemNavKey } from "../../types/constants.js";
6
+ import type { CollectionFormLabels } from "./collection-types.js";
6
7
 
7
8
  export interface NavManagerItem {
8
9
  id: string;
9
- type: "link" | "system" | "collection";
10
+ type: "link" | "system" | "collection" | "page";
10
11
  systemKey?: SystemNavKey;
11
12
  collectionId?: string;
13
+ postId?: string;
12
14
  label: string;
13
15
  displayLabel?: string;
14
16
  url: string;
15
17
  placement?: "header" | "more";
16
18
  }
17
19
 
20
+ export interface NavManagerPage {
21
+ id: string;
22
+ title: string;
23
+ slug: string;
24
+ updatedAt: number;
25
+ }
26
+
18
27
  export interface SystemNavConfig {
19
28
  key: SystemNavKey;
20
29
  label: string;
@@ -36,8 +45,9 @@ export interface NavManagerSuggestedLink {
36
45
  url: string;
37
46
  targetType: "page" | "collection" | "archive";
38
47
  targetLabel: string;
39
- navItemType: "link" | "collection";
48
+ navItemType: "link" | "collection" | "page";
40
49
  collectionId?: string;
50
+ postId?: string;
41
51
  }
42
52
 
43
53
  export interface NavManagerLabels {
@@ -45,6 +55,7 @@ export interface NavManagerLabels {
45
55
  navigationItems: string;
46
56
  emptyState: string;
47
57
  link: string;
58
+ page: string;
48
59
  system: string;
49
60
  toggleEdit: string;
50
61
  label: string;
@@ -53,6 +64,7 @@ export interface NavManagerLabels {
53
64
  delete: string;
54
65
  remove: string;
55
66
  confirmDeleteLink: string;
67
+ confirmDeletePage: string;
56
68
  orderSaved: string;
57
69
  labelRequired: string;
58
70
  saveFailed: string;
@@ -68,12 +80,49 @@ export interface NavManagerLabels {
68
80
  suggestedLinksDescription: string;
69
81
  addSuggestedLink: string;
70
82
  suggestedLinkAdded: string;
83
+ addPageToNavigation: string;
84
+ addPageDescription: string;
85
+ addPage: string;
86
+ searchPages: string;
87
+ recentPages: string;
88
+ searchingPages: string;
89
+ noMatchingPages: string;
90
+ noPages: string;
91
+ pageSearchFailed: string;
92
+ createNewPage: string;
93
+ createPage: string;
94
+ createPageDescription: string;
95
+ pageTitle: string;
96
+ pageAddress: string;
97
+ pageVisibilityHint: string;
98
+ titleRequired: string;
99
+ slugInvalid: string;
100
+ slugReserved: string;
101
+ slugTooLong: string;
102
+ slugUnavailable: string;
103
+ checkingAddress: string;
104
+ creatingPage: string;
105
+ createPageFailed: string;
106
+ pageCreated: string;
107
+ pageCreatedDescription: string;
108
+ addToNavigation: string;
109
+ editPage: string;
110
+ pageAdded: string;
111
+ back: string;
71
112
  collection: string;
72
113
  addCollection: string;
73
114
  addCollectionToNavigation: string;
74
115
  addCollectionDescription: string;
75
116
  allCollectionsAdded: string;
76
117
  noCollections: string;
118
+ createNewCollection: string;
119
+ createCollection: string;
120
+ creatingCollection: string;
121
+ createCollectionFailed: string;
122
+ collectionCreatedDescription: string;
123
+ editCollection: string;
124
+ collectionAdded: string;
125
+ collectionFormLabels: CollectionFormLabels;
77
126
  confirmDeleteCollection: string;
78
127
  headerSection: string;
79
128
  moreSection: string;
@@ -59,7 +59,7 @@ export interface PostFormInitial {
59
59
  mediaIds: string[];
60
60
  }
61
61
 
62
- export interface PostCollectionOption {
62
+ export interface ThreadCollectionOption {
63
63
  id: number;
64
64
  title: string;
65
65
  }
@@ -621,7 +621,10 @@ function buildPostBody(
621
621
  ? detail.rating
622
622
  : null
623
623
  : detail.rating || undefined,
624
- collectionIds: detail.collectionIds,
624
+ // Reply creation and Child edits must not reorganize their existing
625
+ // Thread as a side effect. Collection management stays on Thread-level
626
+ // organization surfaces; JSON.stringify omits this undefined field.
627
+ collectionIds: detail.replyToId ? undefined : detail.collectionIds,
625
628
  attachments: attachments.length > 0 ? attachments : undefined,
626
629
  replyToId: detail.replyToId || undefined,
627
630
  quietReply: detail.quietReply || undefined,
@@ -1,4 +1,10 @@
1
- import { MEDIA_LIGHTBOX_TOGGLE_EVENT } from "./media-lightbox-events.js";
1
+ import {
2
+ isMediaVideoPlaybackPaused,
3
+ MEDIA_LIGHTBOX_TOGGLE_EVENT,
4
+ MEDIA_VIDEO_PLAYBACK_INTENT_EVENT,
5
+ setMediaVideoPlaybackPaused,
6
+ type MediaVideoPlaybackIntentDetail,
7
+ } from "./media-lightbox-events.js";
2
8
 
3
9
  const FEED_VIDEO_PRELOAD_ROOT_MARGIN = "75% 0px";
4
10
  const FEED_VIDEO_PLAY_THRESHOLD = 0.6;
@@ -72,6 +78,28 @@ function readMetrics(video: HTMLVideoElement): FeedVideoMetrics | undefined {
72
78
  return videoMetrics.get(video);
73
79
  }
74
80
 
81
+ /**
82
+ * Decide whether a feed video is excluded from autoplay by explicit user
83
+ * intent.
84
+ *
85
+ * @param mediaId - Stable media ID from the rendered video
86
+ * @returns `true` when autoplay should leave this media paused
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * shouldSuppressFeedVideoAutoplay("med_123");
91
+ * ```
92
+ */
93
+ export function shouldSuppressFeedVideoAutoplay(
94
+ mediaId: string | undefined,
95
+ ): boolean {
96
+ return isMediaVideoPlaybackPaused(mediaId);
97
+ }
98
+
99
+ function isAutoplaySuppressed(video: HTMLVideoElement): boolean {
100
+ return shouldSuppressFeedVideoAutoplay(video.dataset.feedVideoId);
101
+ }
102
+
75
103
  function getMuteButton(video: HTMLVideoElement): HTMLButtonElement | null {
76
104
  return (
77
105
  video
@@ -120,6 +148,11 @@ function pauseVideo(video: HTMLVideoElement | null): void {
120
148
  }
121
149
 
122
150
  function playVideo(video: HTMLVideoElement): void {
151
+ if (isAutoplaySuppressed(video)) {
152
+ pauseVideo(video);
153
+ return;
154
+ }
155
+
123
156
  ensureVideoLoaded(video);
124
157
  video.muted = soundEnabledVideo !== video;
125
158
  video.playsInline = true;
@@ -159,12 +192,13 @@ function reevaluateAutoplay(): void {
159
192
  for (const video of registeredVideos) {
160
193
  const metrics = readMetrics(video);
161
194
  if (!metrics) continue;
195
+ if (isAutoplaySuppressed(video)) continue;
162
196
  if (metrics.intersectionRatio < FEED_VIDEO_PLAY_THRESHOLD) continue;
163
197
  candidates.push({ video, ...metrics });
164
198
  }
165
199
 
166
200
  let winner: HTMLVideoElement | null = null;
167
- if (preferredVideo?.isConnected) {
201
+ if (preferredVideo?.isConnected && !isAutoplaySuppressed(preferredVideo)) {
168
202
  const preferredMetrics = readMetrics(preferredVideo);
169
203
  if (
170
204
  preferredMetrics &&
@@ -177,6 +211,8 @@ function reevaluateAutoplay(): void {
177
211
  ) {
178
212
  preferredVideo = null;
179
213
  }
214
+ } else if (preferredVideo && isAutoplaySuppressed(preferredVideo)) {
215
+ preferredVideo = null;
180
216
  }
181
217
 
182
218
  if (!winner) {
@@ -187,6 +223,7 @@ function reevaluateAutoplay(): void {
187
223
  const currentMetrics = activeVideo ? readMetrics(activeVideo) : undefined;
188
224
  if (
189
225
  activeVideo &&
226
+ !isAutoplaySuppressed(activeVideo) &&
190
227
  currentMetrics &&
191
228
  currentMetrics.intersectionRatio > FEED_VIDEO_PAUSE_THRESHOLD
192
229
  ) {
@@ -313,6 +350,10 @@ function handleMuteToggle(event: Event): void {
313
350
  return;
314
351
  }
315
352
 
353
+ const mediaId = video.dataset.feedVideoId?.trim();
354
+ if (mediaId) {
355
+ setMediaVideoPlaybackPaused(mediaId, false);
356
+ }
316
357
  preferredVideo = video;
317
358
 
318
359
  const willEnableSound = soundEnabledVideo !== video || video.muted;
@@ -358,6 +399,25 @@ document.addEventListener(MEDIA_LIGHTBOX_TOGGLE_EVENT, (event: Event) => {
358
399
  scheduleReevaluate();
359
400
  });
360
401
 
402
+ document.addEventListener(MEDIA_VIDEO_PLAYBACK_INTENT_EVENT, (event: Event) => {
403
+ const detail = (event as CustomEvent<MediaVideoPlaybackIntentDetail>).detail;
404
+ const mediaId = detail?.mediaId?.trim();
405
+ if (!mediaId || typeof detail.paused !== "boolean") {
406
+ return;
407
+ }
408
+
409
+ for (const video of registeredVideos) {
410
+ if (video.dataset.feedVideoId?.trim() !== mediaId) continue;
411
+ if (detail.paused) {
412
+ pauseVideo(video);
413
+ if (activeVideo === video) activeVideo = null;
414
+ if (preferredVideo === video) preferredVideo = null;
415
+ }
416
+ }
417
+
418
+ scheduleReevaluate();
419
+ });
420
+
361
421
  document.addEventListener("visibilitychange", () => {
362
422
  if (document.hidden) {
363
423
  pauseVideo(activeVideo);