@jant/core 0.3.36 → 0.3.38

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 (271) 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/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
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,
@@ -54,16 +54,25 @@ export function buildMediaMap(
54
54
  fit: "scale-down",
55
55
  })
56
56
  : mediaUrl;
57
+ const posterUrl = m.posterKey
58
+ ? getMediaUrl(m.posterKey, publicUrl)
59
+ : null;
57
60
  return {
58
61
  id: m.id,
59
62
  url: mediaUrl,
60
63
  previewUrl,
61
64
  alt: m.alt,
62
65
  blurhash: m.blurhash,
66
+ waveform: m.waveform,
67
+ posterUrl,
63
68
  width: m.width,
64
69
  height: m.height,
65
70
  position: m.position,
66
71
  mimeType: m.mimeType,
72
+ originalName: m.originalName,
73
+ size: m.size,
74
+ summary: m.summary,
75
+ chars: m.chars,
67
76
  };
68
77
  }),
69
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,7 +49,16 @@ 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;
@@ -64,6 +79,8 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
64
79
  siteFooterHtml: navData.siteFooterHtml,
65
80
  sidebar,
66
81
  uploadMaxFileSize: appConfig.uploadMaxFileSize,
82
+ showComposeDialog,
83
+ showHeader,
67
84
  };
68
85
  const faviconUrl = appConfig.siteAvatarUrl || undefined;
69
86
  const faviconVersion = appConfig.faviconVersion || undefined;
@@ -78,6 +95,7 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
78
95
  faviconVersion={faviconVersion}
79
96
  noindex={noindex}
80
97
  isAuthenticated={navData.isAuthenticated}
98
+ toast={toast}
81
99
  >
82
100
  <SiteLayout {...layoutProps}>{content}</SiteLayout>
83
101
  </BaseLayout>,
@@ -134,12 +134,16 @@ export function resolveConfig(
134
134
  imageTransformUrl,
135
135
 
136
136
  // Upload (ENV only)
137
- uploadMaxFileSize: parseInt(env.UPLOAD_MAX_FILE_SIZE ?? "500", 10) || 500,
137
+ uploadMaxFileSize:
138
+ parseInt(env.UPLOAD_MAX_FILE_SIZE_MB ?? "500", 10) || 500,
138
139
 
139
140
  // Summary extraction (ENV only)
140
141
  summaryMaxParagraphs: parseInt(env.SUMMARY_MAX_PARAGRAPHS ?? "5", 10) || 5,
141
142
  summaryMaxChars: parseInt(env.SUMMARY_MAX_CHARS ?? "500", 10) || 500,
142
143
 
144
+ // Slug (ENV only)
145
+ slugIdLength: parseInt(env.SLUG_ID_LENGTH ?? "5", 10) || 5,
146
+
143
147
  // Pagination/Feed (ENV only)
144
148
  pageSize: parseInt(env.PAGE_SIZE ?? "20", 10) || 20,
145
149
  rssFeedLimit: parseInt(env.RSS_FEED_LIMIT ?? "50", 10) || 50,
@@ -161,7 +165,7 @@ export function resolveConfig(
161
165
  siteAvatarUrl,
162
166
  faviconVersion: allSettings["SITE_FAVICON_VERSION"] ?? "",
163
167
 
164
- // Dashboard form placeholders (ENV > Default, without DB)
168
+ // Settings form placeholders (ENV > Default, without DB)
165
169
  fallbacks: {
166
170
  siteName: resolveFallback("SITE_NAME", env),
167
171
  siteDescription: resolveFallback("SITE_DESCRIPTION", env),