@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
@@ -0,0 +1,254 @@
1
+ /** Accessible table-dimension picker opened by the compose slash command. */
2
+
3
+ import type { Editor, Range } from "@tiptap/core";
4
+ import {
5
+ getFixedFloatingContainerRect,
6
+ getFloatingPosition,
7
+ } from "./floating-position.js";
8
+ import type { TableControlLabels } from "./table-control-labels.js";
9
+
10
+ const MAX_ROWS = 8;
11
+ const MAX_COLUMNS = 8;
12
+ const DEFAULT_ROWS = 3;
13
+ const DEFAULT_COLUMNS = 3;
14
+
15
+ interface ActivePicker {
16
+ close: (restoreFocus?: boolean) => void;
17
+ }
18
+
19
+ let activePicker: ActivePicker | null = null;
20
+
21
+ function formatSizeLabel(
22
+ template: string,
23
+ rows: number,
24
+ columns: number,
25
+ ): string {
26
+ return template
27
+ .replaceAll("%rows%", String(rows))
28
+ .replaceAll("%cols%", String(columns));
29
+ }
30
+
31
+ /**
32
+ * Opens a bounded 8×8 table-size picker at the current slash-command range.
33
+ *
34
+ * @param editor - Editor receiving the selected table
35
+ * @param range - Slash query range to remove before opening the picker
36
+ * @param labels - Localized table UI labels
37
+ * @returns Nothing
38
+ */
39
+ export function openTableSizePicker(
40
+ editor: Editor,
41
+ range: Range,
42
+ labels: TableControlLabels,
43
+ ): void {
44
+ activePicker?.close(false);
45
+
46
+ editor.chain().focus().deleteRange(range).run();
47
+ const insertPosition = editor.state.selection.from;
48
+ const dialog = editor.view.dom.closest("dialog");
49
+ const container = dialog ?? document.body;
50
+
51
+ const picker = document.createElement("div");
52
+ picker.className = "tiptap-table-size-picker";
53
+ picker.dataset.editorFloatingUi = "true";
54
+ picker.style.position = "fixed";
55
+ picker.setAttribute("role", "dialog");
56
+ picker.setAttribute("aria-label", labels.sizePickerLabel);
57
+
58
+ const header = document.createElement("div");
59
+ header.className = "tiptap-table-size-picker-header";
60
+ const title = document.createElement("span");
61
+ title.textContent = labels.sizePickerLabel;
62
+ const dimensions = document.createElement("output");
63
+ dimensions.className = "tiptap-table-size-picker-dimensions";
64
+ dimensions.setAttribute("aria-live", "polite");
65
+ header.appendChild(title);
66
+ header.appendChild(dimensions);
67
+ picker.appendChild(header);
68
+
69
+ const grid = document.createElement("div");
70
+ grid.className = "tiptap-table-size-grid";
71
+ grid.setAttribute("role", "grid");
72
+ grid.setAttribute("aria-label", labels.sizePickerLabel);
73
+ picker.appendChild(grid);
74
+
75
+ let selectedRows = DEFAULT_ROWS;
76
+ let selectedColumns = DEFAULT_COLUMNS;
77
+ let closed = false;
78
+ const cells: HTMLButtonElement[][] = [];
79
+
80
+ function labelFor(rows: number, columns: number): string {
81
+ return formatSizeLabel(labels.insertTableSize, rows, columns);
82
+ }
83
+
84
+ function updateSelection(rows: number, columns: number): void {
85
+ selectedRows = Math.min(Math.max(rows, 1), MAX_ROWS);
86
+ selectedColumns = Math.min(Math.max(columns, 1), MAX_COLUMNS);
87
+ dimensions.textContent = `${selectedRows} × ${selectedColumns}`;
88
+ picker.setAttribute(
89
+ "aria-label",
90
+ `${labels.sizePickerLabel}: ${labelFor(selectedRows, selectedColumns)}`,
91
+ );
92
+
93
+ for (let row = 1; row <= MAX_ROWS; row += 1) {
94
+ for (let column = 1; column <= MAX_COLUMNS; column += 1) {
95
+ const cell = cells[row - 1]?.[column - 1];
96
+ if (!cell) continue;
97
+ const isCurrent = row === selectedRows && column === selectedColumns;
98
+ cell.classList.toggle(
99
+ "is-selected",
100
+ row <= selectedRows && column <= selectedColumns,
101
+ );
102
+ cell.classList.toggle("is-current", isCurrent);
103
+ cell.tabIndex = isCurrent ? 0 : -1;
104
+ cell.setAttribute("aria-selected", String(isCurrent));
105
+ }
106
+ }
107
+ }
108
+
109
+ function focusSelection(): void {
110
+ cells[selectedRows - 1]?.[selectedColumns - 1]?.focus();
111
+ }
112
+
113
+ function insertTable(): void {
114
+ if (closed) return;
115
+ close(false);
116
+ editor
117
+ .chain()
118
+ .focus()
119
+ .setTextSelection(insertPosition)
120
+ .insertTable({
121
+ rows: selectedRows,
122
+ cols: selectedColumns,
123
+ withHeaderRow: true,
124
+ })
125
+ .run();
126
+ }
127
+
128
+ function onKeydown(event: KeyboardEvent): void {
129
+ let nextRows = selectedRows;
130
+ let nextColumns = selectedColumns;
131
+ if (event.key === "ArrowUp") nextRows -= 1;
132
+ else if (event.key === "ArrowDown") nextRows += 1;
133
+ else if (event.key === "ArrowLeft") nextColumns -= 1;
134
+ else if (event.key === "ArrowRight") nextColumns += 1;
135
+ else if (event.key === "Enter" || event.key === " ") {
136
+ event.preventDefault();
137
+ event.stopPropagation();
138
+ insertTable();
139
+ return;
140
+ } else if (event.key === "Escape") {
141
+ event.preventDefault();
142
+ event.stopPropagation();
143
+ close(true);
144
+ return;
145
+ } else {
146
+ return;
147
+ }
148
+
149
+ event.preventDefault();
150
+ event.stopPropagation();
151
+ updateSelection(nextRows, nextColumns);
152
+ focusSelection();
153
+ }
154
+
155
+ for (let row = 1; row <= MAX_ROWS; row += 1) {
156
+ const rowElement = document.createElement("div");
157
+ rowElement.className = "tiptap-table-size-grid-row";
158
+ rowElement.setAttribute("role", "row");
159
+ const rowCells: HTMLButtonElement[] = [];
160
+ for (let column = 1; column <= MAX_COLUMNS; column += 1) {
161
+ const cell = document.createElement("button");
162
+ cell.type = "button";
163
+ cell.className = "tiptap-table-size-cell";
164
+ cell.dataset.row = String(row);
165
+ cell.dataset.column = String(column);
166
+ cell.setAttribute("role", "gridcell");
167
+ cell.setAttribute("aria-label", labelFor(row, column));
168
+ cell.addEventListener("pointerenter", () => {
169
+ updateSelection(row, column);
170
+ });
171
+ cell.addEventListener("focus", () => {
172
+ updateSelection(row, column);
173
+ });
174
+ cell.addEventListener("keydown", onKeydown);
175
+ cell.addEventListener("click", insertTable);
176
+ rowElement.appendChild(cell);
177
+ rowCells.push(cell);
178
+ }
179
+ cells.push(rowCells);
180
+ grid.appendChild(rowElement);
181
+ }
182
+
183
+ function positionPicker(): void {
184
+ if (closed) return;
185
+ const coords = editor.view.coordsAtPos(insertPosition);
186
+ const rect = picker.getBoundingClientRect();
187
+ const layout = getFloatingPosition({
188
+ anchorRect: {
189
+ left: coords.left,
190
+ right: coords.right,
191
+ top: coords.top,
192
+ bottom: coords.bottom,
193
+ },
194
+ containerRect: getFixedFloatingContainerRect(dialog),
195
+ floatingWidth: rect.width,
196
+ floatingHeight: rect.height,
197
+ preferredPlacement: "bottom",
198
+ fallbackPlacement: "top",
199
+ align: "start",
200
+ });
201
+ picker.style.left = `${layout.left}px`;
202
+ picker.style.top = `${layout.top}px`;
203
+ picker.dataset.placement = layout.placement;
204
+ }
205
+
206
+ function onOutsidePointer(event: MouseEvent): void {
207
+ const target = event.target;
208
+ if (target instanceof Node && !picker.contains(target)) {
209
+ close(false);
210
+ }
211
+ }
212
+
213
+ function onOutsideFocus(event: globalThis.FocusEvent): void {
214
+ const target = event.target;
215
+ if (
216
+ target instanceof Node &&
217
+ !picker.contains(target) &&
218
+ !editor.view.dom.contains(target)
219
+ ) {
220
+ close(false);
221
+ }
222
+ }
223
+
224
+ function onEditorDestroy(): void {
225
+ close(false);
226
+ }
227
+
228
+ function close(restoreFocus = false): void {
229
+ if (closed) return;
230
+ closed = true;
231
+ document.removeEventListener("mousedown", onOutsidePointer, true);
232
+ document.removeEventListener("focusin", onOutsideFocus, true);
233
+ window.removeEventListener("resize", positionPicker);
234
+ window.removeEventListener("scroll", positionPicker, true);
235
+ editor.off("destroy", onEditorDestroy);
236
+ picker.remove();
237
+ if (activePicker?.close === close) activePicker = null;
238
+ if (restoreFocus && !editor.isDestroyed) editor.view.focus();
239
+ }
240
+
241
+ activePicker = { close };
242
+ container.appendChild(picker);
243
+ updateSelection(DEFAULT_ROWS, DEFAULT_COLUMNS);
244
+ positionPicker();
245
+ window.addEventListener("resize", positionPicker);
246
+ window.addEventListener("scroll", positionPicker, true);
247
+ editor.on("destroy", onEditorDestroy);
248
+ setTimeout(() => {
249
+ if (closed) return;
250
+ document.addEventListener("mousedown", onOutsidePointer, true);
251
+ document.addEventListener("focusin", onOutsideFocus, true);
252
+ focusSelection();
253
+ }, 0);
254
+ }
@@ -96,6 +96,8 @@ const COPY_ICON =
96
96
  '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
97
97
  const CHECK_ICON =
98
98
  '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg>';
99
+ const TOAST_DURATION_MS = 3000;
100
+ const ACTION_TOAST_DURATION_MS = 8000;
99
101
 
100
102
  /** Build toast inner content using safe DOM APIs (icon is trusted, text uses textContent). */
101
103
  function setToastContent(
@@ -158,14 +160,14 @@ export function showToast(message: string, type: ToastType = "success"): void {
158
160
  setTimeout(() => {
159
161
  toast.classList.add("toast-out");
160
162
  toast.addEventListener("animationend", () => toast.remove());
161
- }, 3000);
163
+ }, TOAST_DURATION_MS);
162
164
  }
163
165
 
164
166
  /**
165
167
  * Show a toast with an action link.
166
168
  *
167
169
  * @param message - Text to display
168
- * @param action - Action link with label and href
170
+ * @param action - Action link rendered beside the message
169
171
  * @param type - Visual style: "success" (default) or "error"
170
172
  *
171
173
  * @example
@@ -191,7 +193,7 @@ export function showToastWithAction(
191
193
  setTimeout(() => {
192
194
  toast.classList.add("toast-out");
193
195
  toast.addEventListener("animationend", () => toast.remove());
194
- }, 3000);
196
+ }, ACTION_TOAST_DURATION_MS);
195
197
  }
196
198
 
197
199
  /**
@@ -285,7 +287,7 @@ export function replaceWithAutoClose(
285
287
  setTimeout(() => {
286
288
  toast.classList.add("toast-out");
287
289
  toast.addEventListener("animationend", () => toast.remove());
288
- }, 3000);
290
+ }, TOAST_DURATION_MS);
289
291
  }
290
292
 
291
293
  /**
@@ -293,7 +295,7 @@ export function replaceWithAutoClose(
293
295
  *
294
296
  * @param id - The toast identifier
295
297
  * @param message - New message text
296
- * @param action - Action link with label and href
298
+ * @param action - Action link rendered beside the message
297
299
  * @param type - Visual style: "success" (default) or "error"
298
300
  *
299
301
  * @example
@@ -318,7 +320,7 @@ export function replaceWithAutoCloseAction(
318
320
  setTimeout(() => {
319
321
  toast.classList.add("toast-out");
320
322
  toast.addEventListener("animationend", () => toast.remove());
321
- }, 3000);
323
+ }, ACTION_TOAST_DURATION_MS);
322
324
  }
323
325
 
324
326
  /**
@@ -18,6 +18,7 @@ import "./client/slash-discovery-bridge.js";
18
18
  import "./client/compose-shortcuts.js";
19
19
  import "./client/components/jant-settings-general.js";
20
20
  import "./client/components/jant-settings-avatar.js";
21
+ import "./client/components/jant-config-editor.js";
21
22
  import "./client/settings-bridge.js";
22
23
  import "./client/components/jant-repo-picker.js";
23
24
  import "./client/components/jant-collection-form.js";
package/src/client.ts CHANGED
@@ -19,6 +19,7 @@ import "./client/note-expand.js";
19
19
  import "./client/archive-nav.js";
20
20
  import "./client/site-header-nav.js";
21
21
  import "./client/collection-sort-menu.js";
22
+ import "./client/footnote-rail.js";
22
23
 
23
24
  declare const __JANT_VERSION__: string;
24
25
 
@@ -8,7 +8,7 @@ vi.mock("../../../bin/lib/wrangler-cli.js", () => ({
8
8
  runLocalWrangler: runLocalWranglerMock,
9
9
  }));
10
10
 
11
- const { parseWranglerError, queryD1 } =
11
+ const { executeD1, parseWranglerError, queryD1 } =
12
12
  await import("../../../bin/lib/d1-query.js");
13
13
 
14
14
  function createWranglerError(stderr) {
@@ -81,4 +81,46 @@ describe("d1-query Wrangler error parsing", () => {
81
81
  ).toThrow("Wrangler error: Authentication error.");
82
82
  expect(runLocalWranglerMock).toHaveBeenCalledTimes(1);
83
83
  });
84
+
85
+ it("allows tracked migrations to disable transient retries", () => {
86
+ runLocalWranglerMock.mockImplementationOnce(() => {
87
+ throw createWranglerError("fetch failed");
88
+ });
89
+
90
+ expect(() =>
91
+ executeD1("CREATE TABLE replacement (id TEXT)", "d1-remote", {
92
+ quiet: true,
93
+ retryAttempts: 1,
94
+ retryDelayMs: 0,
95
+ }),
96
+ ).toThrow("fetch failed");
97
+ expect(runLocalWranglerMock).toHaveBeenCalledTimes(1);
98
+ });
99
+
100
+ it("keeps Wrangler target flags out of child-process options", () => {
101
+ runLocalWranglerMock.mockReturnValue(
102
+ JSON.stringify([{ results: [{ count: 1 }], success: true }]),
103
+ );
104
+
105
+ queryD1("SELECT 1", "d1-remote", {
106
+ configPath: "/tmp/wrangler.toml",
107
+ database: "CONTENT_DB",
108
+ env: "preview",
109
+ persistTo: "/tmp/d1",
110
+ retryAttempts: 1,
111
+ });
112
+
113
+ expect(runLocalWranglerMock).toHaveBeenCalledWith(
114
+ expect.arrayContaining([
115
+ "CONTENT_DB",
116
+ "--config",
117
+ "/tmp/wrangler.toml",
118
+ "--env",
119
+ "preview",
120
+ "--persist-to",
121
+ "/tmp/d1",
122
+ ]),
123
+ {},
124
+ );
125
+ });
84
126
  });
@@ -10,6 +10,7 @@ afterEach(() => {
10
10
  for (const dir of tempDirs.splice(0, tempDirs.length)) {
11
11
  rmSync(dir, { recursive: true, force: true });
12
12
  }
13
+ vi.restoreAllMocks();
13
14
  });
14
15
 
15
16
  describe("migration runner", () => {
@@ -53,6 +54,68 @@ describe("migration runner", () => {
53
54
  expect(execute).toHaveBeenCalledOnce();
54
55
  });
55
56
 
57
+ it("reconciles an interrupted response when the migration marker committed", () => {
58
+ const tempDir = mkdtempSync(join(tmpdir(), "jant-migration-runner-"));
59
+ tempDirs.push(tempDir);
60
+
61
+ const sqlPath = join(tempDir, "0001_test.sql");
62
+ writeFileSync(sqlPath, "CREATE TABLE post (id INTEGER PRIMARY KEY);");
63
+
64
+ const execute = vi.fn();
65
+ const executeTrackedFile = vi.fn(() => {
66
+ throw new Error("fetch failed");
67
+ });
68
+ const query = vi
69
+ .fn()
70
+ .mockReturnValueOnce([])
71
+ .mockReturnValueOnce([{ name: "0001_test.sql" }]);
72
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
73
+
74
+ expect(
75
+ applyTrackedSqlFiles(
76
+ { execute, executeTrackedFile, query },
77
+ {
78
+ files: [{ name: "0001_test.sql", path: sqlPath }],
79
+ headline: "Schema migrations",
80
+ tableName: "d1_migrations",
81
+ },
82
+ ),
83
+ ).toBe(1);
84
+ expect(executeTrackedFile).toHaveBeenCalledOnce();
85
+ expect(query).toHaveBeenCalledTimes(2);
86
+ expect(logSpy).toHaveBeenCalledWith(
87
+ "[1/1] 0001_test.sql ✅ (confirmed after interrupted response)",
88
+ );
89
+ });
90
+
91
+ it("surfaces an interrupted migration when no marker was committed", () => {
92
+ const tempDir = mkdtempSync(join(tmpdir(), "jant-migration-runner-"));
93
+ tempDirs.push(tempDir);
94
+
95
+ const sqlPath = join(tempDir, "0001_test.sql");
96
+ writeFileSync(sqlPath, "CREATE TABLE post (id INTEGER PRIMARY KEY);");
97
+
98
+ const execute = vi.fn();
99
+ const executeTrackedFile = vi.fn(() => {
100
+ throw new Error("fetch failed");
101
+ });
102
+ const query = vi.fn().mockReturnValue([]);
103
+ vi.spyOn(console, "log").mockImplementation(() => {});
104
+
105
+ expect(() =>
106
+ applyTrackedSqlFiles(
107
+ { execute, executeTrackedFile, query },
108
+ {
109
+ files: [{ name: "0001_test.sql", path: sqlPath }],
110
+ headline: "Schema migrations",
111
+ tableName: "d1_migrations",
112
+ },
113
+ ),
114
+ ).toThrow("Failed to apply 0001_test.sql: fetch failed");
115
+ expect(executeTrackedFile).toHaveBeenCalledOnce();
116
+ expect(query).toHaveBeenCalledTimes(2);
117
+ });
118
+
56
119
  it("splits SQL statements without breaking semicolons inside strings", async () => {
57
120
  const { splitSqlStatements } =
58
121
  await import("../../../bin/lib/migration-runner.js");