@jant/core 0.3.35 → 0.3.37

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 (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
package/src/lib/icons.ts CHANGED
@@ -93,7 +93,8 @@ export function parseCollectionIcon(
93
93
  if (
94
94
  typeof parsed.name === "string" &&
95
95
  typeof parsed.svg === "string" &&
96
- typeof parsed.color === "string"
96
+ typeof parsed.color === "string" &&
97
+ /^#[0-9a-f]{3,6}$/i.test(parsed.color)
97
98
  ) {
98
99
  return parsed as unknown as CollectionIcon;
99
100
  }
@@ -186,7 +187,7 @@ function applyIconSize(svg: string, size: number, color?: string): string {
186
187
  let result = svg
187
188
  .replace(/width="24"/, `width="${size}"`)
188
189
  .replace(/height="24"/, `height="${size}"`);
189
- if (color) {
190
+ if (color && /^#[0-9a-f]{3,6}$/i.test(color)) {
190
191
  result = result.replace("<svg", `<svg style="color: ${color}"`);
191
192
  }
192
193
  return result;
package/src/lib/index.ts CHANGED
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  export * from "./constants.js";
6
- export * as sqid from "./sqid.js";
7
6
  export * as time from "./time.js";
8
7
  export * as url from "./url.js";
9
8
  export * as markdown from "./markdown.js";
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Markdown → TipTap JSON Conversion
3
+ *
4
+ * Converts Markdown strings to TipTap JSON documents using `marked.lexer()`
5
+ * for tokenization. Enables the API to accept Markdown while the internal
6
+ * pipeline (renderTiptapJson / extractBodyText / extractSummary) continues
7
+ * to operate on TipTap JSON.
8
+ */
9
+
10
+ import { marked, type Token, type Tokens } from "marked";
11
+
12
+ interface TiptapMark {
13
+ type: string;
14
+ attrs?: Record<string, unknown>;
15
+ }
16
+
17
+ interface TiptapNode {
18
+ type: string;
19
+ content?: TiptapNode[];
20
+ text?: string;
21
+ marks?: TiptapMark[];
22
+ attrs?: Record<string, unknown>;
23
+ }
24
+
25
+ /**
26
+ * Converts a Markdown string to a TipTap JSON document string.
27
+ *
28
+ * Uses `marked.lexer()` to tokenize, then maps each token to the
29
+ * corresponding TipTap node structure that `renderTiptapJson()` expects.
30
+ *
31
+ * @param markdown - Markdown source text
32
+ * @returns Stringified TipTap JSON document
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const json = markdownToTiptapJson("Hello **world**");
37
+ * // '{"type":"doc","content":[{"type":"paragraph","content":[...]}]}'
38
+ * ```
39
+ */
40
+ export function markdownToTiptapJson(markdown: string): string {
41
+ const tokens = marked.lexer(markdown, { gfm: true, breaks: true });
42
+ const content = tokens.flatMap(blockTokenToNodes);
43
+ // Ensure at least one node so the doc is valid
44
+ if (content.length === 0) {
45
+ content.push({ type: "paragraph" });
46
+ }
47
+ const doc: TiptapNode = { type: "doc", content };
48
+ return JSON.stringify(doc);
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Block-level token → TipTap node mapping
53
+ // ---------------------------------------------------------------------------
54
+
55
+ function blockTokenToNodes(token: Token): TiptapNode[] {
56
+ switch (token.type) {
57
+ case "paragraph":
58
+ return [
59
+ {
60
+ type: "paragraph",
61
+ content: inlineTokensToNodes(
62
+ (token as Tokens.Paragraph).tokens ?? [],
63
+ [],
64
+ ),
65
+ },
66
+ ];
67
+
68
+ // Tight list items use "text" instead of "paragraph" as the block wrapper
69
+ case "text": {
70
+ const t = token as Tokens.Text;
71
+ return [
72
+ {
73
+ type: "paragraph",
74
+ content: inlineTokensToNodes(t.tokens ?? [], []),
75
+ },
76
+ ];
77
+ }
78
+
79
+ case "heading": {
80
+ const t = token as Tokens.Heading;
81
+ return [
82
+ {
83
+ type: "heading",
84
+ attrs: { level: t.depth },
85
+ content: inlineTokensToNodes(t.tokens ?? [], []),
86
+ },
87
+ ];
88
+ }
89
+
90
+ case "code": {
91
+ const t = token as Tokens.Code;
92
+ const node: TiptapNode = {
93
+ type: "codeBlock",
94
+ content: [{ type: "text", text: t.text }],
95
+ };
96
+ if (t.lang) {
97
+ node.attrs = { language: t.lang };
98
+ }
99
+ return [node];
100
+ }
101
+
102
+ case "blockquote": {
103
+ const t = token as Tokens.Blockquote;
104
+ const inner = (t.tokens ?? []).flatMap(blockTokenToNodes);
105
+ return [{ type: "blockquote", content: inner }];
106
+ }
107
+
108
+ case "list": {
109
+ const t = token as Tokens.List;
110
+ const listType = t.ordered ? "orderedList" : "bulletList";
111
+ const items = t.items.map(listItemToNode);
112
+ const node: TiptapNode = { type: listType, content: items };
113
+ if (t.ordered && t.start !== undefined && t.start !== 1) {
114
+ node.attrs = { start: t.start };
115
+ }
116
+ return [node];
117
+ }
118
+
119
+ case "hr":
120
+ return [{ type: "horizontalRule" }];
121
+
122
+ case "html": {
123
+ const t = token as Tokens.HTML;
124
+ if (t.text.trim() === "<!--more-->") {
125
+ return [{ type: "moreBreak" }];
126
+ }
127
+ // Other raw HTML: wrap in a paragraph as plain text
128
+ return [
129
+ {
130
+ type: "paragraph",
131
+ content: [{ type: "text", text: t.text.trim() }],
132
+ },
133
+ ];
134
+ }
135
+
136
+ case "table": {
137
+ const t = token as Tokens.Table;
138
+ const rows: TiptapNode[] = [];
139
+
140
+ // Header row
141
+ const headerCells = t.header.map(
142
+ (cell): TiptapNode => ({
143
+ type: "tableHeader",
144
+ content: [
145
+ {
146
+ type: "paragraph",
147
+ content: inlineTokensToNodes(cell.tokens, []),
148
+ },
149
+ ],
150
+ }),
151
+ );
152
+ rows.push({ type: "tableRow", content: headerCells });
153
+
154
+ // Body rows
155
+ for (const row of t.rows) {
156
+ const cells = row.map(
157
+ (cell): TiptapNode => ({
158
+ type: "tableCell",
159
+ content: [
160
+ {
161
+ type: "paragraph",
162
+ content: inlineTokensToNodes(cell.tokens, []),
163
+ },
164
+ ],
165
+ }),
166
+ );
167
+ rows.push({ type: "tableRow", content: cells });
168
+ }
169
+
170
+ return [{ type: "table", content: rows }];
171
+ }
172
+
173
+ case "space":
174
+ return [];
175
+
176
+ default:
177
+ return [];
178
+ }
179
+ }
180
+
181
+ function listItemToNode(item: Tokens.ListItem): TiptapNode {
182
+ // A list item's tokens can be block-level (loose list) or inline
183
+ const children = (item.tokens ?? []).flatMap(blockTokenToNodes);
184
+ return { type: "listItem", content: children };
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // Inline token → TipTap node mapping (flattened marks model)
189
+ // ---------------------------------------------------------------------------
190
+
191
+ function inlineTokensToNodes(
192
+ tokens: Token[],
193
+ marks: TiptapMark[],
194
+ ): TiptapNode[] {
195
+ const nodes: TiptapNode[] = [];
196
+
197
+ for (const token of tokens) {
198
+ switch (token.type) {
199
+ case "text": {
200
+ const t = token as Tokens.Text;
201
+ // marked may nest inline tokens inside text tokens
202
+ if (t.tokens && t.tokens.length > 0) {
203
+ nodes.push(...inlineTokensToNodes(t.tokens, marks));
204
+ } else {
205
+ const textNode: TiptapNode = { type: "text", text: t.text };
206
+ if (marks.length > 0) textNode.marks = [...marks];
207
+ nodes.push(textNode);
208
+ }
209
+ break;
210
+ }
211
+
212
+ case "strong": {
213
+ const t = token as Tokens.Strong;
214
+ const newMarks = [...marks, { type: "bold" }];
215
+ nodes.push(...inlineTokensToNodes(t.tokens ?? [], newMarks));
216
+ break;
217
+ }
218
+
219
+ case "em": {
220
+ const t = token as Tokens.Em;
221
+ const newMarks = [...marks, { type: "italic" }];
222
+ nodes.push(...inlineTokensToNodes(t.tokens ?? [], newMarks));
223
+ break;
224
+ }
225
+
226
+ case "codespan": {
227
+ const t = token as Tokens.Codespan;
228
+ const textNode: TiptapNode = { type: "text", text: t.text };
229
+ textNode.marks = [...marks, { type: "code" }];
230
+ nodes.push(textNode);
231
+ break;
232
+ }
233
+
234
+ case "del": {
235
+ const t = token as Tokens.Del;
236
+ const newMarks = [...marks, { type: "strike" }];
237
+ nodes.push(...inlineTokensToNodes(t.tokens ?? [], newMarks));
238
+ break;
239
+ }
240
+
241
+ case "link": {
242
+ const t = token as Tokens.Link;
243
+ const linkMark: TiptapMark = {
244
+ type: "link",
245
+ attrs: { href: t.href, target: "_blank" },
246
+ };
247
+ const newMarks = [...marks, linkMark];
248
+ nodes.push(...inlineTokensToNodes(t.tokens ?? [], newMarks));
249
+ break;
250
+ }
251
+
252
+ case "image": {
253
+ const t = token as Tokens.Image;
254
+ const imgAttrs: Record<string, unknown> = { src: t.href };
255
+ if (t.text) imgAttrs.alt = t.text;
256
+ if (t.title) imgAttrs.title = t.title;
257
+ const imgNode: TiptapNode = { type: "image", attrs: imgAttrs };
258
+ nodes.push(imgNode);
259
+ break;
260
+ }
261
+
262
+ case "br":
263
+ nodes.push({ type: "hardBreak" });
264
+ break;
265
+
266
+ case "escape": {
267
+ const t = token as Tokens.Escape;
268
+ const textNode: TiptapNode = { type: "text", text: t.text };
269
+ if (marks.length > 0) textNode.marks = [...marks];
270
+ nodes.push(textNode);
271
+ break;
272
+ }
273
+
274
+ default:
275
+ // For any unhandled inline token with raw text, emit as text
276
+ if ("text" in token && typeof token.text === "string") {
277
+ const textNode: TiptapNode = { type: "text", text: token.text };
278
+ if (marks.length > 0) textNode.marks = [...marks];
279
+ nodes.push(textNode);
280
+ }
281
+ break;
282
+ }
283
+ }
284
+
285
+ return nodes;
286
+ }
@@ -28,12 +28,12 @@ import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
28
28
  * ```
29
29
  */
30
30
  export function buildMediaMap(
31
- rawMediaMap: Map<number, Media[]>,
31
+ rawMediaMap: Map<string, Media[]>,
32
32
  r2PublicUrl?: string,
33
33
  imageTransformUrl?: string,
34
34
  s3PublicUrl?: string,
35
- ): Map<number, MediaAttachment[]> {
36
- const mediaMap = new Map<number, MediaAttachment[]>();
35
+ ): Map<string, MediaAttachment[]> {
36
+ const mediaMap = new Map<string, MediaAttachment[]>();
37
37
  for (const [postId, mediaList] of rawMediaMap) {
38
38
  mediaMap.set(
39
39
  postId,
@@ -43,26 +43,36 @@ export function buildMediaMap(
43
43
  r2PublicUrl,
44
44
  s3PublicUrl,
45
45
  );
46
- return {
47
- id: m.id,
48
- url: getMediaUrl(m.storageKey, publicUrl),
49
- previewUrl: getImageUrl(
50
- getMediaUrl(m.storageKey, publicUrl),
51
- imageTransformUrl,
52
- {
46
+ const mediaUrl = getMediaUrl(m.storageKey, publicUrl);
47
+ // Only apply image transforms for image MIME types
48
+ const previewUrl = m.mimeType.startsWith("image/")
49
+ ? getImageUrl(mediaUrl, imageTransformUrl, {
53
50
  width: 1200,
54
51
  height: 768,
55
52
  quality: 80,
56
53
  format: "auto",
57
54
  fit: "scale-down",
58
- },
59
- ),
55
+ })
56
+ : mediaUrl;
57
+ const posterUrl = m.posterKey
58
+ ? getMediaUrl(m.posterKey, publicUrl)
59
+ : null;
60
+ return {
61
+ id: m.id,
62
+ url: mediaUrl,
63
+ previewUrl,
60
64
  alt: m.alt,
61
65
  blurhash: m.blurhash,
66
+ waveform: m.waveform,
67
+ posterUrl,
62
68
  width: m.width,
63
69
  height: m.height,
64
70
  position: m.position,
65
71
  mimeType: m.mimeType,
72
+ originalName: m.originalName,
73
+ size: m.size,
74
+ summary: m.summary,
75
+ chars: m.chars,
66
76
  };
67
77
  }),
68
78
  );
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Random ID Generation
3
+ *
4
+ * Wraps nanoid's `customAlphabet` to produce short, URL-safe random IDs
5
+ * using a lowercase alphanumeric alphabet (0-9, a-z).
6
+ */
7
+
8
+ import { customAlphabet } from "nanoid";
9
+
10
+ const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
11
+
12
+ /**
13
+ * Generates a random ID of the given length using lowercase alphanumeric characters.
14
+ *
15
+ * Uses nanoid's `customAlphabet` for uniform distribution.
16
+ *
17
+ * @param length - Number of characters in the generated ID
18
+ * @returns Random alphanumeric string
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * generateRandomId(5); // e.g. "a3k9m"
23
+ * generateRandomId(8); // e.g. "b7x2q4fn"
24
+ * ```
25
+ */
26
+ export function generateRandomId(length: number): string {
27
+ const generate = customAlphabet(ALPHABET, length);
28
+ return generate();
29
+ }
@@ -83,7 +83,7 @@ export async function getNavigationData(c: Context): Promise<NavigationData> {
83
83
 
84
84
  // Only load collections when authenticated (for compose dialog)
85
85
  if (isAuthenticated) {
86
- collections = await c.var.services.collections.list();
86
+ collections = await c.var.services.collections.listByRecentActivity();
87
87
  }
88
88
 
89
89
  return {
@@ -8,7 +8,7 @@
8
8
  import type { Context } from "hono";
9
9
  import type { Child } from "hono/jsx";
10
10
  import type { SiteLayoutProps } from "../types.js";
11
- import { BaseLayout } from "../ui/layouts/BaseLayout.js";
11
+ import { BaseLayout, type ToastProps } from "../ui/layouts/BaseLayout.js";
12
12
  import { SiteLayout } from "../ui/layouts/SiteLayout.js";
13
13
  import type { NavigationData } from "./navigation.js";
14
14
 
@@ -23,6 +23,12 @@ export interface RenderPublicPageOptions {
23
23
  content: Child;
24
24
  /** Optional sidebar content for sidebar layout */
25
25
  sidebar?: Child;
26
+ /** Optional toast notification */
27
+ toast?: ToastProps;
28
+ /** Whether to render the shared compose dialog shell */
29
+ showComposeDialog?: boolean;
30
+ /** Whether to render the site header */
31
+ showHeader?: boolean;
26
32
  }
27
33
 
28
34
  /**
@@ -43,11 +49,23 @@ export interface RenderPublicPageOptions {
43
49
  * ```
44
50
  */
45
51
  export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
46
- const { title, description, navData, content, sidebar } = options;
52
+ const {
53
+ title,
54
+ description,
55
+ navData,
56
+ content,
57
+ sidebar,
58
+ toast,
59
+ showComposeDialog,
60
+ showHeader,
61
+ } = options;
47
62
 
48
63
  // Use siteDescription as meta description fallback when not explicitly provided
49
64
  const metaDescription = description || navData.siteDescription || undefined;
50
65
 
66
+ // Read favicon, version, and noindex from appConfig
67
+ const appConfig = c.get("appConfig");
68
+
51
69
  const layoutProps: SiteLayoutProps = {
52
70
  siteName: navData.siteName,
53
71
  links: navData.links,
@@ -60,10 +78,10 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
60
78
  showHeaderAvatar: navData.showHeaderAvatar,
61
79
  siteFooterHtml: navData.siteFooterHtml,
62
80
  sidebar,
81
+ uploadMaxFileSize: appConfig.uploadMaxFileSize,
82
+ showComposeDialog,
83
+ showHeader,
63
84
  };
64
-
65
- // Read favicon, version, and noindex from appConfig
66
- const appConfig = c.get("appConfig");
67
85
  const faviconUrl = appConfig.siteAvatarUrl || undefined;
68
86
  const faviconVersion = appConfig.faviconVersion || undefined;
69
87
  const noindex = appConfig.noindex;
@@ -77,6 +95,7 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
77
95
  faviconVersion={faviconVersion}
78
96
  noindex={noindex}
79
97
  isAuthenticated={navData.isAuthenticated}
98
+ toast={toast}
80
99
  >
81
100
  <SiteLayout {...layoutProps}>{content}</SiteLayout>
82
101
  </BaseLayout>,
@@ -117,7 +117,7 @@ export function resolveConfig(
117
117
  resolve("HEADER_NAV_MAX_VISIBLE", allSettings, env),
118
118
  10,
119
119
  );
120
- return Math.max(0, Math.min(5, isNaN(parsed) ? 3 : parsed));
120
+ return Math.max(0, Math.min(5, isNaN(parsed) ? 2 : parsed));
121
121
  })(),
122
122
  timeZone: resolve("TIME_ZONE", allSettings, env),
123
123
  siteFooter: resolve("SITE_FOOTER", allSettings, env),
@@ -133,6 +133,17 @@ export function resolveConfig(
133
133
  s3PublicUrl,
134
134
  imageTransformUrl,
135
135
 
136
+ // Upload (ENV only)
137
+ uploadMaxFileSize:
138
+ parseInt(env.UPLOAD_MAX_FILE_SIZE_MB ?? "500", 10) || 500,
139
+
140
+ // Summary extraction (ENV only)
141
+ summaryMaxParagraphs: parseInt(env.SUMMARY_MAX_PARAGRAPHS ?? "5", 10) || 5,
142
+ summaryMaxChars: parseInt(env.SUMMARY_MAX_CHARS ?? "500", 10) || 500,
143
+
144
+ // Slug (ENV only)
145
+ slugIdLength: parseInt(env.SLUG_ID_LENGTH ?? "5", 10) || 5,
146
+
136
147
  // Pagination/Feed (ENV only)
137
148
  pageSize: parseInt(env.PAGE_SIZE ?? "20", 10) || 20,
138
149
  rssFeedLimit: parseInt(env.RSS_FEED_LIMIT ?? "50", 10) || 50,
@@ -154,7 +165,7 @@ export function resolveConfig(
154
165
  siteAvatarUrl,
155
166
  faviconVersion: allSettings["SITE_FAVICON_VERSION"] ?? "",
156
167
 
157
- // Dashboard form placeholders (ENV > Default, without DB)
168
+ // Settings form placeholders (ENV > Default, without DB)
158
169
  fallbacks: {
159
170
  siteName: resolveFallback("SITE_NAME", env),
160
171
  siteDescription: resolveFallback("SITE_DESCRIPTION", env),