@jant/core 0.6.16 → 0.7.0

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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -24,19 +24,25 @@ import {
24
24
  responsiveSortableOptions,
25
25
  revertSortableDomMove,
26
26
  } from "../sortable-list.js";
27
+ import { isFeedNavKey } from "../../types/constants.js";
27
28
  import { showConfirmDialog } from "../confirm.js";
28
29
  import { showToast } from "../toast.js";
29
- import { publicPath } from "../runtime-paths.js";
30
+ import { publicPath, sitePathPrefix } from "../runtime-paths.js";
30
31
  import { applySiteHeaderHtml } from "../site-header-fragment.js";
31
- import { NAVIGATION_SETTINGS_PATH } from "../../lib/settings-paths.js";
32
- import {
33
- getCollectionEditPath,
34
- getCollectionPagePath,
35
- } from "../../lib/collection-paths.js";
32
+ import { getCollectionPagePath } from "../../lib/collection-paths.js";
33
+ import { FUNNEL_ICON_SVG } from "../icons.js";
34
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
36
35
  import { getSlugValidationIssue } from "../../lib/slug-format.js";
36
+ import {
37
+ looksLikeAddress,
38
+ normalizePath,
39
+ toInternalPath,
40
+ } from "../../lib/url.js";
37
41
  import type { CollectionSubmitDetail } from "./collection-types.js";
38
42
  import "./jant-collection-form.js";
43
+ import { openCollectionDialog } from "../collection-dialog-host.js";
39
44
  import type {
45
+ NavAddressResolution,
40
46
  NavManagerCollection,
41
47
  NavManagerItem,
42
48
  NavManagerLabels,
@@ -101,6 +107,7 @@ export class JantNavManager extends LitElement {
101
107
  _pageDialogView: { state: true },
102
108
  _pageQuery: { state: true },
103
109
  _pages: { state: true },
110
+ _pageAddress: { state: true },
104
111
  _pageSearchLoading: { state: true },
105
112
  _pageSearchError: { state: true },
106
113
  _selectedPageIndex: { state: true },
@@ -148,6 +155,8 @@ export class JantNavManager extends LitElement {
148
155
  declare _pageDialogView: PageDialogView;
149
156
  declare _pageQuery: string;
150
157
  declare _pages: NavManagerPage[];
158
+ /** What the last pasted address turned out to be, when one was pasted. */
159
+ declare _pageAddress: NavAddressResolution | null;
151
160
  declare _pageSearchLoading: boolean;
152
161
  declare _pageSearchError: boolean;
153
162
  declare _selectedPageIndex: number;
@@ -253,6 +262,7 @@ export class JantNavManager extends LitElement {
253
262
  this._pageDialogView = "picker";
254
263
  this._pageQuery = "";
255
264
  this._pages = [];
265
+ this._pageAddress = null;
256
266
  this._pageSearchLoading = false;
257
267
  this._pageSearchError = false;
258
268
  this._selectedPageIndex = 0;
@@ -323,8 +333,16 @@ export class JantNavManager extends LitElement {
323
333
  ? this.systemNavItems.find((config) => config.key === item.systemKey)
324
334
  ?.label
325
335
  : undefined;
336
+ // `targetTitle` comes back on mutation responses, which carry the raw nav
337
+ // item rather than the server-rendered `displayLabel` — without it a page
338
+ // or collection added with no label of its own would render blank until
339
+ // the next full load.
326
340
  const displayLabel =
327
- item.label.trim() || systemLabel || item.displayLabel || item.label;
341
+ item.label.trim() ||
342
+ systemLabel ||
343
+ item.displayLabel ||
344
+ item.targetTitle?.trim() ||
345
+ item.label;
328
346
 
329
347
  return { ...item, displayLabel };
330
348
  }
@@ -359,6 +377,7 @@ export class JantNavManager extends LitElement {
359
377
  this._pageDialogView = "picker";
360
378
  this._pageQuery = "";
361
379
  this._pages = [];
380
+ this._pageAddress = null;
362
381
  this._pageSearchError = false;
363
382
  this._selectedPageIndex = 0;
364
383
  this._addingPageId = null;
@@ -387,6 +406,45 @@ export class JantNavManager extends LitElement {
387
406
  void this.updateComplete.then(() => trigger?.focus());
388
407
  }
389
408
 
409
+ /**
410
+ * Look up the page at an address the author pasted.
411
+ *
412
+ * Searching titles cannot answer someone holding a URL, and the answer is
413
+ * often a reason rather than a result — a draft, a private page, a page that
414
+ * is already in the menu.
415
+ */
416
+ async #lookUpPageAddress(address: string) {
417
+ const requestId = ++this.#pageSearchRequestId;
418
+ const params = new URLSearchParams({ url: address });
419
+
420
+ this._pageSearchLoading = true;
421
+ this._pageSearchError = false;
422
+ try {
423
+ const res = await fetch(`/api/nav-items/resolve?${params.toString()}`, {
424
+ headers: { Accept: "application/json" },
425
+ });
426
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
427
+ const json = (await res.json()) as {
428
+ resolution?: NavAddressResolution;
429
+ };
430
+ if (requestId !== this.#pageSearchRequestId) return;
431
+
432
+ this._pages = [];
433
+ this._pageAddress = json.resolution ?? null;
434
+ this._selectedPageIndex = 0;
435
+ } catch {
436
+ if (requestId === this.#pageSearchRequestId) {
437
+ this._pages = [];
438
+ this._pageAddress = null;
439
+ this._pageSearchError = true;
440
+ }
441
+ } finally {
442
+ if (requestId === this.#pageSearchRequestId) {
443
+ this._pageSearchLoading = false;
444
+ }
445
+ }
446
+ }
447
+
390
448
  async #loadPageCandidates() {
391
449
  const requestId = ++this.#pageSearchRequestId;
392
450
  const params = new URLSearchParams({ limit: "20" });
@@ -395,6 +453,7 @@ export class JantNavManager extends LitElement {
395
453
 
396
454
  this._pageSearchLoading = true;
397
455
  this._pageSearchError = false;
456
+ this._pageAddress = null;
398
457
  try {
399
458
  const res = await fetch(`/api/nav-items/pages?${params.toString()}`, {
400
459
  headers: { Accept: "application/json" },
@@ -430,7 +489,10 @@ export class JantNavManager extends LitElement {
430
489
  }
431
490
  this.#pageSearchTimer = setTimeout(() => {
432
491
  this.#pageSearchTimer = null;
433
- void this.#loadPageCandidates();
492
+ const query = this._pageQuery.trim();
493
+ void (looksLikeAddress(query)
494
+ ? this.#lookUpPageAddress(query)
495
+ : this.#loadPageCandidates());
434
496
  }, 200);
435
497
  }
436
498
 
@@ -753,8 +815,7 @@ export class JantNavManager extends LitElement {
753
815
  body: JSON.stringify(submitEvent.detail.data),
754
816
  });
755
817
  const created = (await res.json().catch(() => null)) as
756
- | (CreatedCollection & { error?: string })
757
- | null;
818
+ (CreatedCollection & { error?: string }) | null;
758
819
  if (!res.ok || !created?.id || !created.title || !created.slug) {
759
820
  throw new Error("Invalid collection response");
760
821
  }
@@ -944,7 +1005,9 @@ export class JantNavManager extends LitElement {
944
1005
 
945
1006
  async #handleUpdate(item: NavManagerItem) {
946
1007
  const label = this._editLabel.trim();
947
- if (!label && item.type !== "system") {
1008
+ // Clearing the field hands the item back to its target's title, so only a
1009
+ // free-form link — which points at nothing that has one — still needs one.
1010
+ if (!label && item.type === "link") {
948
1011
  showToast(this.labels.labelRequired, "error");
949
1012
  return;
950
1013
  }
@@ -1054,7 +1117,7 @@ export class JantNavManager extends LitElement {
1054
1117
  // Add collection handler
1055
1118
  // ===========================================================================
1056
1119
 
1057
- async #handleAddCollection(collectionId: string) {
1120
+ async #handleAddCollection(collectionId: string, isSmart = false) {
1058
1121
  if (!collectionId || this._addingCollectionId) return;
1059
1122
 
1060
1123
  this._addingCollectionId = collectionId;
@@ -1062,11 +1125,22 @@ export class JantNavManager extends LitElement {
1062
1125
  const res = await fetch("/api/nav-items", {
1063
1126
  method: "POST",
1064
1127
  headers: this.#jsonMutationHeaders(),
1065
- body: JSON.stringify({
1066
- type: "collection",
1067
- collectionId,
1068
- placement: "header",
1069
- }),
1128
+ // Both kinds land in the same menu and behave identically once there:
1129
+ // the label follows the target's title, and the URL follows its
1130
+ // address. Only which column holds the key differs.
1131
+ body: JSON.stringify(
1132
+ isSmart
1133
+ ? {
1134
+ type: "smart_collection",
1135
+ smartCollectionId: collectionId,
1136
+ placement: "header",
1137
+ }
1138
+ : {
1139
+ type: "collection",
1140
+ collectionId,
1141
+ placement: "header",
1142
+ },
1143
+ ),
1070
1144
  });
1071
1145
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1072
1146
 
@@ -1092,29 +1166,22 @@ export class JantNavManager extends LitElement {
1092
1166
  // Suggested link handlers
1093
1167
  // ===========================================================================
1094
1168
 
1169
+ /**
1170
+ * The internal path two nav URLs have to share to be the same destination.
1171
+ *
1172
+ * The same comparison the server makes when it decides which suggested links
1173
+ * are already in the menu, so both sides agree that `/about`, `/blog/about`
1174
+ * on a prefixed deployment, and this site's own absolute URL are one page.
1175
+ */
1095
1176
  #getComparableInternalPath(url: string): string | null {
1096
- if (
1097
- url.startsWith("http://") ||
1098
- url.startsWith("https://") ||
1099
- url.startsWith("//") ||
1100
- url.startsWith("mailto:") ||
1101
- url.startsWith("tel:") ||
1102
- url.startsWith("#")
1103
- ) {
1104
- return null;
1105
- }
1177
+ const path = toInternalPath(url, {
1178
+ siteOrigins: [window.location.origin],
1179
+ sitePathPrefix: sitePathPrefix(),
1180
+ });
1181
+ if (path === null) return null;
1106
1182
 
1107
- try {
1108
- const pathname = new URL(url, "https://jant.invalid").pathname;
1109
- const normalized = pathname
1110
- .trim()
1111
- .toLowerCase()
1112
- .replace(/^\/+|\/+$/g, "")
1113
- .replace(/\/+/g, "/");
1114
- return normalized ? `/${normalized}` : "/";
1115
- } catch {
1116
- return null;
1117
- }
1183
+ const normalized = normalizePath(path);
1184
+ return normalized ? `/${normalized}` : "/";
1118
1185
  }
1119
1186
 
1120
1187
  #isSuggestedLinkAdded(link: NavManagerSuggestedLink): boolean {
@@ -1264,9 +1331,13 @@ export class JantNavManager extends LitElement {
1264
1331
  return this._items.filter((i) => i.placement === "more");
1265
1332
  }
1266
1333
 
1334
+ // Mirrors the server's projection in `lib/navigation.ts`: with feeds off,
1335
+ // neither feed entry renders, so neither may show in the preview either.
1267
1336
  #isVisibleInPreview(item: NavManagerItem): boolean {
1268
1337
  return (
1269
- this.rssFeedsEnabled || item.type !== "system" || item.systemKey !== "rss"
1338
+ this.rssFeedsEnabled ||
1339
+ item.type !== "system" ||
1340
+ !isFeedNavKey(item.systemKey)
1270
1341
  );
1271
1342
  }
1272
1343
 
@@ -1321,60 +1392,64 @@ export class JantNavManager extends LitElement {
1321
1392
  (item) => item.id,
1322
1393
  (item, index) =>
1323
1394
  html`<a
1324
- class=${index === 0
1325
- ? "site-header-link site-header-link-active"
1326
- : "site-header-link"}
1395
+ class=${
1396
+ index === 0
1397
+ ? "site-header-link site-header-link-active"
1398
+ : "site-header-link"
1399
+ }
1327
1400
  >
1328
1401
  ${item.displayLabel ?? item.label}
1329
1402
  </a>`,
1330
1403
  )}
1331
- ${moreItems.length > 0
1332
- ? html`
1333
- <div class="site-header-more" data-preview-more>
1334
- <button
1335
- type="button"
1336
- class="site-header-more-btn"
1337
- data-preview-more-trigger
1338
- aria-haspopup="menu"
1339
- aria-expanded=${this._showPreviewMore
1340
- ? "true"
1341
- : "false"}
1342
- @click=${this.#togglePreviewMore}
1343
- >
1344
- ${this.labels.moreSection}
1345
- <svg
1346
- xmlns="http://www.w3.org/2000/svg"
1347
- width="12"
1348
- height="12"
1349
- viewBox="0 0 24 24"
1350
- fill="none"
1351
- stroke="currentColor"
1352
- stroke-width="2.5"
1353
- stroke-linecap="round"
1354
- stroke-linejoin="round"
1355
- aria-hidden="true"
1404
+ ${
1405
+ moreItems.length > 0
1406
+ ? html`
1407
+ <div class="site-header-more" data-preview-more>
1408
+ <button
1409
+ type="button"
1410
+ class="site-header-more-btn"
1411
+ data-preview-more-trigger
1412
+ aria-haspopup="menu"
1413
+ aria-expanded=${
1414
+ this._showPreviewMore ? "true" : "false"
1415
+ }
1416
+ @click=${this.#togglePreviewMore}
1356
1417
  >
1357
- <path d="m6 9 6 6 6-6" />
1358
- </svg>
1359
- </button>
1360
- <div
1361
- class="site-header-more-popover"
1362
- aria-hidden=${this._showPreviewMore ? "false" : "true"}
1363
- @click=${(event: Event) => event.stopPropagation()}
1364
- >
1365
- ${repeat(
1366
- moreItems,
1367
- (item) => item.id,
1368
- (item) => html`
1369
- <span class="site-header-more-link">
1370
- ${item.displayLabel ?? item.label}
1371
- </span>
1372
- `,
1373
- )}
1418
+ ${this.labels.previewMore}
1419
+ <svg
1420
+ xmlns="http://www.w3.org/2000/svg"
1421
+ width="12"
1422
+ height="12"
1423
+ viewBox="0 0 24 24"
1424
+ fill="none"
1425
+ stroke="currentColor"
1426
+ stroke-width="2.5"
1427
+ stroke-linecap="round"
1428
+ stroke-linejoin="round"
1429
+ aria-hidden="true"
1430
+ >
1431
+ <path d="m6 9 6 6 6-6" />
1432
+ </svg>
1433
+ </button>
1434
+ <div
1435
+ class="site-header-more-popover"
1436
+ aria-hidden=${this._showPreviewMore ? "false" : "true"}
1437
+ @click=${(event: Event) => event.stopPropagation()}
1438
+ >
1439
+ ${repeat(
1440
+ moreItems,
1441
+ (item) => item.id,
1442
+ (item) => html`
1443
+ <span class="site-header-more-link">
1444
+ ${item.displayLabel ?? item.label}
1445
+ </span>
1446
+ `,
1447
+ )}
1448
+ </div>
1374
1449
  </div>
1375
- </div>
1376
- `
1377
- : nothing}
1450
+ `
1451
+ : nothing
1452
+ }
1378
1453
  </nav>
1379
1454
  </div>
1380
1455
  </div>
@@ -1448,7 +1523,8 @@ export class JantNavManager extends LitElement {
1448
1523
  <input
1449
1524
  type="text"
1450
1525
  class="input"
1451
- required
1526
+ maxlength="100"
1527
+ placeholder=${item.displayLabel ?? ""}
1452
1528
  .value=${this._editLabel}
1453
1529
  @input=${(e: Event) => {
1454
1530
  this._editLabel = (e.target as HTMLInputElement).value;
@@ -1484,8 +1560,8 @@ export class JantNavManager extends LitElement {
1484
1560
  <input
1485
1561
  type="text"
1486
1562
  class="input"
1487
- required
1488
1563
  maxlength="100"
1564
+ placeholder=${item.displayLabel ?? ""}
1489
1565
  .value=${this._editLabel}
1490
1566
  @input=${(e: Event) => {
1491
1567
  this._editLabel = (e.target as HTMLInputElement).value;
@@ -1630,9 +1706,9 @@ export class JantNavManager extends LitElement {
1630
1706
  stroke-width="2"
1631
1707
  stroke-linecap="round"
1632
1708
  stroke-linejoin="round"
1633
- style="transition: transform 0.15s; ${isEditing
1634
- ? "transform: rotate(180deg);"
1635
- : ""}"
1709
+ style="transition: transform 0.15s; ${
1710
+ isEditing ? "transform: rotate(180deg);" : ""
1711
+ }"
1636
1712
  >
1637
1713
  <path d="m6 9 6 6 6-6" />
1638
1714
  </svg>
@@ -1703,6 +1779,180 @@ export class JantNavManager extends LitElement {
1703
1779
  `;
1704
1780
  }
1705
1781
 
1782
+ /** Whether navigation already points at what an address resolved to. */
1783
+ #isAddressAlreadyAdded(resolution: NavAddressResolution): boolean {
1784
+ if (resolution.kind === "page") {
1785
+ return this._items.some((item) => item.postId === resolution.page.id);
1786
+ }
1787
+ if (resolution.kind === "collection") {
1788
+ return this._items.some(
1789
+ (item) => item.collectionId === resolution.collection.id,
1790
+ );
1791
+ }
1792
+ if (resolution.kind === "smart_collection") {
1793
+ return this._items.some(
1794
+ (item) => item.smartCollectionId === resolution.smartCollection.id,
1795
+ );
1796
+ }
1797
+ return false;
1798
+ }
1799
+
1800
+ /** The line to show when a pasted address cannot become a page item. */
1801
+ #addressStatus(resolution: NavAddressResolution): string {
1802
+ if (this.#isAddressAlreadyAdded(resolution)) {
1803
+ return this.labels.addressAlreadyAdded;
1804
+ }
1805
+
1806
+ switch (resolution.kind) {
1807
+ case "not_found":
1808
+ return this.labels.addressNotFound.replace(
1809
+ "{address}",
1810
+ resolution.address,
1811
+ );
1812
+ case "unpublished":
1813
+ return this.labels.addressUnpublished;
1814
+ case "private":
1815
+ return this.labels.addressPrivate;
1816
+ case "untitled":
1817
+ return this.labels.addressUntitled;
1818
+ case "external":
1819
+ return this.labels.addressExternal;
1820
+ case "link_only":
1821
+ return this.labels.addressLinkOnly;
1822
+ default:
1823
+ return "";
1824
+ }
1825
+ }
1826
+
1827
+ /**
1828
+ * Hand an address the picker cannot use over to the link form, filled in.
1829
+ *
1830
+ * Off-site addresses are a normal thing to want in navigation; they just
1831
+ * belong in the other kind of item, and retyping the URL is a poor way to
1832
+ * find that out.
1833
+ */
1834
+ #startLinkFromAddress(address: string) {
1835
+ this._newLinkUrl = address;
1836
+ this.#closePageDialog();
1837
+ this._showLinkForm = true;
1838
+ setTimeout(() => {
1839
+ document.addEventListener("click", this.#closeLinkForm);
1840
+ });
1841
+ void this.updateComplete.then(() => {
1842
+ this.querySelector<HTMLInputElement>("#nav-link-label")?.focus();
1843
+ });
1844
+ }
1845
+
1846
+ #renderPageAddress(resolution: NavAddressResolution) {
1847
+ const alreadyAdded = this.#isAddressAlreadyAdded(resolution);
1848
+
1849
+ if (resolution.kind === "page" && !alreadyAdded) {
1850
+ const page = resolution.page;
1851
+ return this.#renderPageAddressResult(
1852
+ page.title,
1853
+ publicPath(`/${page.slug}`),
1854
+ this._addingPageId === page.id,
1855
+ () => void this.#addPageToNavigation(page),
1856
+ );
1857
+ }
1858
+
1859
+ if (resolution.kind === "collection" && !alreadyAdded) {
1860
+ const collection = resolution.collection;
1861
+ return this.#renderPageAddressResult(
1862
+ collection.title,
1863
+ publicPath(getCollectionPagePath(collection.slug)),
1864
+ this._addingCollectionId === collection.id,
1865
+ () => {
1866
+ void this.#handleAddCollection(collection.id).then(() => {
1867
+ this.#closePageDialog();
1868
+ });
1869
+ },
1870
+ );
1871
+ }
1872
+
1873
+ if (resolution.kind === "smart_collection" && !alreadyAdded) {
1874
+ const smartCollection = resolution.smartCollection;
1875
+ return this.#renderPageAddressResult(
1876
+ smartCollection.title,
1877
+ publicPath(getCollectionPagePath(smartCollection.slug)),
1878
+ this._addingCollectionId === smartCollection.id,
1879
+ () => {
1880
+ void this.#handleAddCollection(smartCollection.id, true).then(() => {
1881
+ this.#closePageDialog();
1882
+ });
1883
+ },
1884
+ );
1885
+ }
1886
+
1887
+ const status = this.#addressStatus(resolution);
1888
+ const offerLink =
1889
+ resolution.kind === "external" || resolution.kind === "link_only";
1890
+
1891
+ return html`
1892
+ <p class="nav-page-status" role="status">${status}</p>
1893
+ ${
1894
+ offerLink
1895
+ ? html`
1896
+ <button
1897
+ type="button"
1898
+ class="btn-outline nav-page-address-link"
1899
+ @click=${() => this.#startLinkFromAddress(resolution.address)}
1900
+ >
1901
+ ${this.labels.addressAddAsLink}
1902
+ </button>
1903
+ `
1904
+ : nothing
1905
+ }
1906
+ `;
1907
+ }
1908
+
1909
+ #renderPageAddressResult(
1910
+ title: string,
1911
+ path: string,
1912
+ busy: boolean,
1913
+ add: () => void,
1914
+ ) {
1915
+ return html`
1916
+ <p class="nav-page-results-label">${this.labels.addressMatch}</p>
1917
+ <div class="nav-page-results" role="listbox">
1918
+ <button
1919
+ type="button"
1920
+ role="option"
1921
+ aria-selected="true"
1922
+ class="nav-page-result nav-page-result-selected"
1923
+ ?disabled=${busy}
1924
+ @click=${add}
1925
+ >
1926
+ <span class="nav-page-result-copy">
1927
+ <span class="nav-page-result-title">${title}</span>
1928
+ <span class="nav-page-result-path">${path}</span>
1929
+ </span>
1930
+ ${
1931
+ busy
1932
+ ? html`<span class="nav-page-spinner" aria-hidden="true"></span>`
1933
+ : html`
1934
+ <svg
1935
+ xmlns="http://www.w3.org/2000/svg"
1936
+ width="16"
1937
+ height="16"
1938
+ viewBox="0 0 24 24"
1939
+ fill="none"
1940
+ stroke="currentColor"
1941
+ stroke-width="2"
1942
+ stroke-linecap="round"
1943
+ stroke-linejoin="round"
1944
+ aria-hidden="true"
1945
+ >
1946
+ <path d="M12 5v14" />
1947
+ <path d="M5 12h14" />
1948
+ </svg>
1949
+ `
1950
+ }
1951
+ </button>
1952
+ </div>
1953
+ `;
1954
+ }
1955
+
1706
1956
  #renderPagePicker() {
1707
1957
  const status = this._pageSearchLoading
1708
1958
  ? this.labels.searchingPages
@@ -1740,14 +1990,16 @@ export class JantNavManager extends LitElement {
1740
1990
  id="nav-page-search"
1741
1991
  type="search"
1742
1992
  class="input nav-page-search-input"
1743
- placeholder=${this.labels.searchPages}
1993
+ placeholder=${this.labels.searchPagesHint}
1744
1994
  autocomplete="off"
1745
1995
  role="combobox"
1746
1996
  aria-expanded="true"
1747
1997
  aria-controls="nav-page-results"
1748
- aria-activedescendant=${this._pages.length > 0
1749
- ? `nav-page-result-${this._selectedPageIndex}`
1750
- : ""}
1998
+ aria-activedescendant=${
1999
+ this._pages.length > 0
2000
+ ? `nav-page-result-${this._selectedPageIndex}`
2001
+ : ""
2002
+ }
1751
2003
  .value=${this._pageQuery}
1752
2004
  @input=${(event: Event) => {
1753
2005
  this._pageQuery = (event.target as HTMLInputElement).value;
@@ -1757,70 +2009,82 @@ export class JantNavManager extends LitElement {
1757
2009
  />
1758
2010
  </div>
1759
2011
 
1760
- ${this._pages.length > 0
1761
- ? html`
1762
- <p class="nav-page-results-label">
1763
- ${this._pageQuery.trim()
1764
- ? this.labels.searchPages
1765
- : this.labels.recentPages}
1766
- </p>
1767
- <div
1768
- id="nav-page-results"
1769
- class="nav-page-results"
1770
- role="listbox"
1771
- >
1772
- ${this._pages.map((page, index) => {
1773
- const selected = index === this._selectedPageIndex;
1774
- const adding = this._addingPageId === page.id;
1775
- return html`
1776
- <button
1777
- id=${`nav-page-result-${index}`}
1778
- type="button"
1779
- role="option"
1780
- aria-selected=${selected ? "true" : "false"}
1781
- class=${`nav-page-result${
1782
- selected ? " nav-page-result-selected" : ""
1783
- }`}
1784
- ?disabled=${this._addingPageId !== null}
1785
- @mouseenter=${() => {
1786
- this._selectedPageIndex = index;
1787
- }}
1788
- @click=${() => void this.#addPageToNavigation(page)}
2012
+ ${
2013
+ this._pageSearchLoading || this._pageSearchError
2014
+ ? html`<p class="nav-page-status" role="status">${status}</p>`
2015
+ : this._pageAddress
2016
+ ? this.#renderPageAddress(this._pageAddress)
2017
+ : this._pages.length > 0
2018
+ ? html`
2019
+ <p class="nav-page-results-label">
2020
+ ${
2021
+ this._pageQuery.trim()
2022
+ ? this.labels.searchPages
2023
+ : this.labels.recentPages
2024
+ }
2025
+ </p>
2026
+ <div
2027
+ id="nav-page-results"
2028
+ class="nav-page-results"
2029
+ role="listbox"
1789
2030
  >
1790
- <span class="nav-page-result-copy">
1791
- <span class="nav-page-result-title">${page.title}</span>
1792
- <span class="nav-page-result-path"
1793
- >${publicPath(`/${page.slug}`)}</span
1794
- >
1795
- </span>
1796
- ${adding
1797
- ? html`<span
1798
- class="nav-page-spinner"
1799
- aria-hidden="true"
1800
- ></span>`
1801
- : html`
1802
- <svg
1803
- xmlns="http://www.w3.org/2000/svg"
1804
- width="16"
1805
- height="16"
1806
- viewBox="0 0 24 24"
1807
- fill="none"
1808
- stroke="currentColor"
1809
- stroke-width="2"
1810
- stroke-linecap="round"
1811
- stroke-linejoin="round"
1812
- aria-hidden="true"
1813
- >
1814
- <path d="M12 5v14" />
1815
- <path d="M5 12h14" />
1816
- </svg>
1817
- `}
1818
- </button>
1819
- `;
1820
- })}
1821
- </div>
1822
- `
1823
- : html`<p class="nav-page-status" role="status">${status}</p>`}
2031
+ ${this._pages.map((page, index) => {
2032
+ const selected = index === this._selectedPageIndex;
2033
+ const adding = this._addingPageId === page.id;
2034
+ return html`
2035
+ <button
2036
+ id=${`nav-page-result-${index}`}
2037
+ type="button"
2038
+ role="option"
2039
+ aria-selected=${selected ? "true" : "false"}
2040
+ class=${`nav-page-result${
2041
+ selected ? " nav-page-result-selected" : ""
2042
+ }`}
2043
+ ?disabled=${this._addingPageId !== null}
2044
+ @mouseenter=${() => {
2045
+ this._selectedPageIndex = index;
2046
+ }}
2047
+ @click=${() => void this.#addPageToNavigation(page)}
2048
+ >
2049
+ <span class="nav-page-result-copy">
2050
+ <span class="nav-page-result-title"
2051
+ >${page.title}</span
2052
+ >
2053
+ <span class="nav-page-result-path"
2054
+ >${publicPath(`/${page.slug}`)}</span
2055
+ >
2056
+ </span>
2057
+ ${
2058
+ adding
2059
+ ? html`<span
2060
+ class="nav-page-spinner"
2061
+ aria-hidden="true"
2062
+ ></span>`
2063
+ : html`
2064
+ <svg
2065
+ xmlns="http://www.w3.org/2000/svg"
2066
+ width="16"
2067
+ height="16"
2068
+ viewBox="0 0 24 24"
2069
+ fill="none"
2070
+ stroke="currentColor"
2071
+ stroke-width="2"
2072
+ stroke-linecap="round"
2073
+ stroke-linejoin="round"
2074
+ aria-hidden="true"
2075
+ >
2076
+ <path d="M12 5v14" />
2077
+ <path d="M5 12h14" />
2078
+ </svg>
2079
+ `
2080
+ }
2081
+ </button>
2082
+ `;
2083
+ })}
2084
+ </div>
2085
+ `
2086
+ : html`<p class="nav-page-status" role="status">${status}</p>`
2087
+ }
1824
2088
  </section>
1825
2089
  <footer class="nav-page-dialog-footer">
1826
2090
  <button
@@ -1921,15 +2185,19 @@ export class JantNavManager extends LitElement {
1921
2185
  </div>
1922
2186
  <p
1923
2187
  id="nav-new-page-slug-help"
1924
- class=${slugError
1925
- ? "text-xs text-destructive"
1926
- : "text-xs text-muted-foreground"}
2188
+ class=${
2189
+ slugError
2190
+ ? "text-xs text-destructive"
2191
+ : "text-xs text-muted-foreground"
2192
+ }
1927
2193
  >
1928
- ${slugError
1929
- ? slugError
1930
- : this._slugCheckLoading
1931
- ? this.labels.checkingAddress
1932
- : pagePath}
2194
+ ${
2195
+ slugError
2196
+ ? slugError
2197
+ : this._slugCheckLoading
2198
+ ? this.labels.checkingAddress
2199
+ : pagePath
2200
+ }
1933
2201
  </p>
1934
2202
  </div>
1935
2203
  <p class="nav-page-visibility-note">
@@ -1951,11 +2219,13 @@ export class JantNavManager extends LitElement {
1951
2219
  </svg>
1952
2220
  ${this.labels.pageVisibilityHint}
1953
2221
  </p>
1954
- ${this._createPageError
1955
- ? html`<p class="text-sm text-destructive" role="alert">
1956
- ${this._createPageError}
1957
- </p>`
1958
- : nothing}
2222
+ ${
2223
+ this._createPageError
2224
+ ? html`<p class="text-sm text-destructive" role="alert">
2225
+ ${this._createPageError}
2226
+ </p>`
2227
+ : nothing
2228
+ }
1959
2229
  </form>
1960
2230
  </section>
1961
2231
  <footer>
@@ -1971,13 +2241,15 @@ export class JantNavManager extends LitElement {
1971
2241
  type="submit"
1972
2242
  class="btn"
1973
2243
  form="nav-create-page-form"
1974
- ?disabled=${this._creatingPage ||
1975
- this._slugCheckLoading ||
1976
- Boolean(slugError)}
2244
+ ?disabled=${
2245
+ this._creatingPage || this._slugCheckLoading || Boolean(slugError)
2246
+ }
1977
2247
  >
1978
- ${this._creatingPage
1979
- ? this.labels.creatingPage
1980
- : this.labels.createPage}
2248
+ ${
2249
+ this._creatingPage
2250
+ ? this.labels.creatingPage
2251
+ : this.labels.createPage
2252
+ }
1981
2253
  </button>
1982
2254
  </footer>
1983
2255
  `;
@@ -2080,11 +2352,13 @@ export class JantNavManager extends LitElement {
2080
2352
  <path d="m6 6 12 12" />
2081
2353
  </svg>
2082
2354
  </button>
2083
- ${this._pageDialogView === "picker"
2084
- ? this.#renderPagePicker()
2085
- : this._pageDialogView === "create"
2086
- ? this.#renderCreatePage()
2087
- : this.#renderCreatedPage()}
2355
+ ${
2356
+ this._pageDialogView === "picker"
2357
+ ? this.#renderPagePicker()
2358
+ : this._pageDialogView === "create"
2359
+ ? this.#renderCreatePage()
2360
+ : this.#renderCreatedPage()
2361
+ }
2088
2362
  </div>
2089
2363
  </dialog>
2090
2364
  `;
@@ -2117,78 +2391,82 @@ export class JantNavManager extends LitElement {
2117
2391
  >
2118
2392
  ${this.labels.addLink}
2119
2393
  </button>
2120
- ${this._showLinkForm
2121
- ? html`
2122
- <div
2123
- id="nav-link-popover-content"
2124
- data-popover
2125
- data-side="top"
2126
- aria-hidden="false"
2127
- class="w-80"
2128
- style="bottom: 100%; margin-bottom: 0.5rem;"
2129
- @click=${(e: Event) => e.stopPropagation()}
2130
- >
2131
- <div class="grid gap-4">
2132
- <header class="grid gap-1.5">
2133
- <h4 class="leading-none font-medium">
2134
- ${this.labels.addLink}
2135
- </h4>
2136
- <p class="text-muted-foreground text-sm">
2137
- ${this.labels.addLinkDescription}
2138
- </p>
2139
- </header>
2140
- <form
2141
- class="form grid gap-2"
2142
- @submit=${(e: Event) => {
2143
- e.preventDefault();
2144
- this.#handleAddLink();
2145
- }}
2146
- >
2147
- <div class="grid grid-cols-3 items-center gap-4">
2148
- <label for="nav-link-label">${this.labels.label}</label>
2149
- <input
2150
- type="text"
2151
- id="nav-link-label"
2152
- class="col-span-2 h-8"
2153
- placeholder="Home"
2154
- required
2155
- .value=${this._newLinkLabel}
2156
- @input=${(e: Event) => {
2157
- this._newLinkLabel = (
2158
- e.target as HTMLInputElement
2159
- ).value;
2160
- }}
2161
- autofocus
2162
- />
2163
- </div>
2164
- <div class="grid grid-cols-3 items-center gap-4">
2165
- <label for="nav-link-url">${this.labels.url}</label>
2166
- <input
2167
- type="text"
2168
- id="nav-link-url"
2169
- class="col-span-2 h-8"
2170
- placeholder=${this.labels.urlPlaceholder}
2171
- required
2172
- .value=${this._newLinkUrl}
2173
- @input=${(e: Event) => {
2174
- this._newLinkUrl = (
2175
- e.target as HTMLInputElement
2176
- ).value;
2177
- }}
2178
- />
2179
- </div>
2180
- <button
2181
- type="submit"
2182
- class="btn-sm mt-2"
2183
- ?disabled=${this._addingLink}
2394
+ ${
2395
+ this._showLinkForm
2396
+ ? html`
2397
+ <div
2398
+ id="nav-link-popover-content"
2399
+ data-popover
2400
+ data-side="top"
2401
+ aria-hidden="false"
2402
+ class="w-80"
2403
+ style="bottom: 100%; margin-bottom: 0.5rem;"
2404
+ @click=${(e: Event) => e.stopPropagation()}
2405
+ >
2406
+ <div class="grid gap-4">
2407
+ <header class="grid gap-1.5">
2408
+ <h4 class="leading-none font-medium">
2409
+ ${this.labels.addLink}
2410
+ </h4>
2411
+ <p class="text-muted-foreground text-sm">
2412
+ ${this.labels.addLinkDescription}
2413
+ </p>
2414
+ </header>
2415
+ <form
2416
+ class="form grid gap-2"
2417
+ @submit=${(e: Event) => {
2418
+ e.preventDefault();
2419
+ this.#handleAddLink();
2420
+ }}
2184
2421
  >
2185
- ${this.labels.addLink}
2186
- </button>
2187
- </form>
2422
+ <div class="grid grid-cols-3 items-center gap-4">
2423
+ <label for="nav-link-label"
2424
+ >${this.labels.label}</label
2425
+ >
2426
+ <input
2427
+ type="text"
2428
+ id="nav-link-label"
2429
+ class="col-span-2 h-8"
2430
+ placeholder="Home"
2431
+ required
2432
+ .value=${this._newLinkLabel}
2433
+ @input=${(e: Event) => {
2434
+ this._newLinkLabel = (
2435
+ e.target as HTMLInputElement
2436
+ ).value;
2437
+ }}
2438
+ autofocus
2439
+ />
2440
+ </div>
2441
+ <div class="grid grid-cols-3 items-center gap-4">
2442
+ <label for="nav-link-url">${this.labels.url}</label>
2443
+ <input
2444
+ type="text"
2445
+ id="nav-link-url"
2446
+ class="col-span-2 h-8"
2447
+ placeholder=${this.labels.urlPlaceholder}
2448
+ required
2449
+ .value=${this._newLinkUrl}
2450
+ @input=${(e: Event) => {
2451
+ this._newLinkUrl = (
2452
+ e.target as HTMLInputElement
2453
+ ).value;
2454
+ }}
2455
+ />
2456
+ </div>
2457
+ <button
2458
+ type="submit"
2459
+ class="btn-sm mt-2"
2460
+ ?disabled=${this._addingLink}
2461
+ >
2462
+ ${this.labels.addLink}
2463
+ </button>
2464
+ </form>
2465
+ </div>
2188
2466
  </div>
2189
- </div>
2190
- `
2191
- : nothing}
2467
+ `
2468
+ : nothing
2469
+ }
2192
2470
  </div>
2193
2471
  </section>
2194
2472
  `;
@@ -2217,9 +2495,13 @@ export class JantNavManager extends LitElement {
2217
2495
  }
2218
2496
 
2219
2497
  const addedCollectionIds = new Set(
2220
- this._items
2221
- .filter((i) => i.type === "collection" && i.collectionId)
2222
- .map((i) => i.collectionId),
2498
+ this._items.flatMap((i) =>
2499
+ i.type === "collection" && i.collectionId
2500
+ ? [i.collectionId]
2501
+ : i.type === "smart_collection" && i.smartCollectionId
2502
+ ? [i.smartCollectionId]
2503
+ : [],
2504
+ ),
2223
2505
  );
2224
2506
  const available = this.collections.filter(
2225
2507
  (c) => !addedCollectionIds.has(c.id),
@@ -2311,89 +2593,107 @@ export class JantNavManager extends LitElement {
2311
2593
  stroke-linejoin="round"
2312
2594
  class="ml-1.5 -mr-0.5"
2313
2595
  aria-hidden="true"
2314
- style="transition: transform 0.15s; ${this._showCollectionPicker
2315
- ? "transform: rotate(180deg);"
2316
- : ""}"
2596
+ style="transition: transform 0.15s; ${
2597
+ this._showCollectionPicker ? "transform: rotate(180deg);" : ""
2598
+ }"
2317
2599
  >
2318
2600
  <path d="m6 9 6 6 6-6" />
2319
2601
  </svg>
2320
2602
  </button>
2321
- ${this._showCollectionPicker
2322
- ? html`
2323
- <div
2324
- class="collection-picker"
2325
- role="menu"
2326
- @click=${(e: Event) => e.stopPropagation()}
2327
- >
2328
- ${groups.map(
2329
- (group) => html`
2330
- ${group.label
2331
- ? html`<div class="collection-picker-group">
2332
- ${group.label}
2333
- </div>`
2334
- : nothing}
2335
- ${group.items.map((c) => {
2336
- const adding = this._addingCollectionId === c.id;
2337
- return html`
2338
- <button
2339
- type="button"
2340
- role="menuitem"
2341
- class="collection-picker-item"
2342
- ?disabled=${adding ||
2343
- this._addingCollectionId !== null}
2344
- @click=${() => this.#handleAddCollection(c.id)}
2345
- >
2346
- <span class="collection-picker-title">
2347
- ${c.title}
2348
- </span>
2349
- ${adding
2350
- ? html`<svg
2351
- xmlns="http://www.w3.org/2000/svg"
2352
- width="14"
2353
- height="14"
2354
- viewBox="0 0 24 24"
2355
- fill="none"
2356
- stroke="currentColor"
2357
- stroke-width="2"
2358
- stroke-linecap="round"
2359
- stroke-linejoin="round"
2360
- class="animate-spin shrink-0 text-muted-foreground"
2361
- >
2362
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
2363
- </svg>`
2364
- : nothing}
2365
- </button>
2366
- `;
2367
- })}
2368
- `,
2369
- )}
2370
- <div class="collection-picker-footer">
2371
- <button
2372
- type="button"
2373
- role="menuitem"
2374
- class="collection-picker-create"
2375
- data-create-collection-trigger
2376
- @click=${() => void this.#openCollectionDialog()}
2377
- >
2378
- ${this.labels.createNewCollection}
2379
- <span aria-hidden="true">+</span>
2380
- </button>
2603
+ ${
2604
+ this._showCollectionPicker
2605
+ ? html`
2606
+ <div
2607
+ class="collection-picker"
2608
+ role="menu"
2609
+ @click=${(e: Event) => e.stopPropagation()}
2610
+ >
2611
+ ${groups.map(
2612
+ (group) => html`
2613
+ ${
2614
+ group.label
2615
+ ? html`<div class="collection-picker-group">
2616
+ ${group.label}
2617
+ </div>`
2618
+ : nothing
2619
+ }
2620
+ ${group.items.map((c) => {
2621
+ const adding = this._addingCollectionId === c.id;
2622
+ return html`
2623
+ <button
2624
+ type="button"
2625
+ role="menuitem"
2626
+ class="collection-picker-item"
2627
+ ?disabled=${
2628
+ adding || this._addingCollectionId !== null
2629
+ }
2630
+ @click=${() =>
2631
+ this.#handleAddCollection(c.id, c.isSmart)}
2632
+ >
2633
+ <span class="collection-picker-title">
2634
+ ${c.title}
2635
+ </span>
2636
+ ${
2637
+ c.isSmart
2638
+ ? html`<span
2639
+ class="collection-directory-smart-icon"
2640
+ role="img"
2641
+ aria-label=${
2642
+ this.labels.smartCollectionLabel ?? ""
2643
+ }
2644
+ title=${
2645
+ this.labels.smartCollectionLabel ?? ""
2646
+ }
2647
+ >${unsafeHTML(FUNNEL_ICON_SVG)}</span
2648
+ >`
2649
+ : nothing
2650
+ }
2651
+ ${
2652
+ adding
2653
+ ? html`<svg
2654
+ xmlns="http://www.w3.org/2000/svg"
2655
+ width="14"
2656
+ height="14"
2657
+ viewBox="0 0 24 24"
2658
+ fill="none"
2659
+ stroke="currentColor"
2660
+ stroke-width="2"
2661
+ stroke-linecap="round"
2662
+ stroke-linejoin="round"
2663
+ class="animate-spin shrink-0 text-muted-foreground"
2664
+ >
2665
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
2666
+ </svg>`
2667
+ : nothing
2668
+ }
2669
+ </button>
2670
+ `;
2671
+ })}
2672
+ `,
2673
+ )}
2674
+ <div class="collection-picker-footer">
2675
+ <button
2676
+ type="button"
2677
+ role="menuitem"
2678
+ class="collection-picker-create"
2679
+ data-create-collection-trigger
2680
+ @click=${() => void this.#openCollectionDialog()}
2681
+ >
2682
+ ${this.labels.createNewCollection}
2683
+ <span aria-hidden="true">+</span>
2684
+ </button>
2685
+ </div>
2381
2686
  </div>
2382
- </div>
2383
- `
2384
- : nothing}
2687
+ `
2688
+ : nothing
2689
+ }
2385
2690
  </div>
2386
2691
  </section>
2387
2692
  `;
2388
2693
  }
2389
2694
 
2390
2695
  #renderCreateCollection() {
2391
- const initial = {
2392
- title: "",
2393
- slug: "",
2394
- description: "",
2395
- sortOrder: "newest" as const,
2396
- };
2696
+ const initial = { title: "", slug: "" };
2397
2697
 
2398
2698
  return html`
2399
2699
  <header>
@@ -2404,19 +2704,19 @@ export class JantNavManager extends LitElement {
2404
2704
  </header>
2405
2705
  <section>
2406
2706
  <jant-collection-form
2407
- variant="quick"
2408
2707
  .labels=${this.labels.collectionFormLabels}
2409
2708
  .initial=${initial}
2410
2709
  action=${publicPath("/api/collections")}
2411
- cancel-href=${publicPath(NAVIGATION_SETTINGS_PATH)}
2412
2710
  @jant:collection-submit=${(event: Event) =>
2413
2711
  void this.#handleCreateCollectionSubmit(event)}
2414
2712
  ></jant-collection-form>
2415
- ${this._createCollectionError
2416
- ? html`<p class="mt-3 text-sm text-destructive" role="alert">
2417
- ${this._createCollectionError}
2418
- </p>`
2419
- : nothing}
2713
+ ${
2714
+ this._createCollectionError
2715
+ ? html`<p class="mt-3 text-sm text-destructive" role="alert">
2716
+ ${this._createCollectionError}
2717
+ </p>`
2718
+ : nothing
2719
+ }
2420
2720
  </section>
2421
2721
  <footer>
2422
2722
  <button
@@ -2433,23 +2733,40 @@ export class JantNavManager extends LitElement {
2433
2733
  ?disabled=${this._creatingCollection}
2434
2734
  @click=${this.#submitCollectionForm}
2435
2735
  >
2436
- ${this._creatingCollection
2437
- ? this.labels.creatingCollection
2438
- : this.labels.createCollection}
2736
+ ${
2737
+ this._creatingCollection
2738
+ ? this.labels.creatingCollection
2739
+ : this.labels.createCollection
2740
+ }
2439
2741
  </button>
2440
2742
  </footer>
2441
2743
  `;
2442
2744
  }
2443
2745
 
2746
+ /**
2747
+ * Edit the collection just created, without leaving the navigation step.
2748
+ *
2749
+ * Its title and address can both move, so the picker's copy of it and the
2750
+ * card showing it are re-read from what the dialog saved.
2751
+ */
2752
+ async #editCreatedCollection(collectionId: string) {
2753
+ const { changed, collection } = await openCollectionDialog({
2754
+ collectionId,
2755
+ });
2756
+ if (!changed || !collection) return;
2757
+
2758
+ this._createdCollection = collection;
2759
+ this.collections = this.collections.map((entry) =>
2760
+ entry.id === collection.id
2761
+ ? { ...entry, title: collection.title, slug: collection.slug }
2762
+ : entry,
2763
+ );
2764
+ }
2765
+
2444
2766
  #renderCreatedCollection() {
2445
2767
  const collection = this._createdCollection;
2446
2768
  if (!collection) return nothing;
2447
2769
 
2448
- const returnTo = publicPath(NAVIGATION_SETTINGS_PATH);
2449
- const editHref = `${publicPath(
2450
- getCollectionEditPath(collection.slug),
2451
- )}?returnTo=${encodeURIComponent(returnTo)}`;
2452
-
2453
2770
  return html`
2454
2771
  <header class="nav-create-success-header">
2455
2772
  <span class="nav-create-success-icon" aria-hidden="true">
@@ -2481,15 +2798,16 @@ export class JantNavManager extends LitElement {
2481
2798
  </div>
2482
2799
  </section>
2483
2800
  <footer>
2484
- <a
2801
+ <!-- The collection dialog opens on top of this one rather than in a
2802
+ new tab: there is no editor page to send the author to, and the
2803
+ navigation step they came for is still waiting underneath. -->
2804
+ <button
2805
+ type="button"
2485
2806
  class="btn-outline"
2486
- href=${editHref}
2487
- target="_blank"
2488
- rel="noopener noreferrer"
2807
+ @click=${() => void this.#editCreatedCollection(collection.id)}
2489
2808
  >
2490
2809
  ${this.labels.editCollection}
2491
- <span aria-hidden="true">↗</span>
2492
- </a>
2810
+ </button>
2493
2811
  <button
2494
2812
  type="button"
2495
2813
  class="btn"
@@ -2546,9 +2864,11 @@ export class JantNavManager extends LitElement {
2546
2864
  <path d="m6 6 12 12" />
2547
2865
  </svg>
2548
2866
  </button>
2549
- ${this._collectionDialogView === "create"
2550
- ? this.#renderCreateCollection()
2551
- : this.#renderCreatedCollection()}
2867
+ ${
2868
+ this._collectionDialogView === "create"
2869
+ ? this.#renderCreateCollection()
2870
+ : this.#renderCreatedCollection()
2871
+ }
2552
2872
  </div>
2553
2873
  </dialog>
2554
2874
  `;
@@ -2603,11 +2923,13 @@ export class JantNavManager extends LitElement {
2603
2923
 
2604
2924
  <section class="mt-8">
2605
2925
  <h2 class="text-lg font-semibold mb-3">${this.labels.headerSection}</h2>
2606
- ${this.#headerItems.length === 0
2607
- ? html`<p class="text-sm text-muted-foreground py-4">
2608
- ${this.labels.emptyState}
2609
- </p>`
2610
- : nothing}
2926
+ ${
2927
+ this.#headerItems.length === 0
2928
+ ? html`<p class="text-sm text-muted-foreground py-4">
2929
+ ${this.labels.emptyState}
2930
+ </p>`
2931
+ : nothing
2932
+ }
2611
2933
  <div id="nav-items-header" class="nav-items-list">
2612
2934
  ${repeat(
2613
2935
  this.#headerItems,
@@ -2620,15 +2942,17 @@ export class JantNavManager extends LitElement {
2620
2942
  <section class="mt-8">
2621
2943
  <h2 class="text-lg font-semibold mb-3">${this.labels.moreSection}</h2>
2622
2944
  <div id="nav-items-more" class="nav-items-list nav-items-list-drop">
2623
- ${this.#moreItems.length > 0
2624
- ? repeat(
2625
- this.#moreItems,
2626
- (item) => item.id,
2627
- (item) => this.#renderItem(item),
2628
- )
2629
- : html`<p class="nav-items-empty-hint">
2630
- ${this.labels.moreEmptyHint}
2631
- </p>`}
2945
+ ${
2946
+ this.#moreItems.length > 0
2947
+ ? repeat(
2948
+ this.#moreItems,
2949
+ (item) => item.id,
2950
+ (item) => this.#renderItem(item),
2951
+ )
2952
+ : html`<p class="nav-items-empty-hint">
2953
+ ${this.labels.moreEmptyHint}
2954
+ </p>`
2955
+ }
2632
2956
  </div>
2633
2957
  </section>
2634
2958