@jant/core 0.3.23 → 0.3.25

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 (248) hide show
  1. package/dist/app.js +50 -26
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +5 -11
  7. package/dist/lib/constants.js +2 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/nav-reorder.js +1 -1
  11. package/dist/lib/navigation.js +30 -6
  12. package/dist/lib/pagination.js +44 -0
  13. package/dist/lib/render.js +7 -11
  14. package/dist/lib/schemas.js +80 -38
  15. package/dist/lib/theme.js +4 -4
  16. package/dist/lib/time.js +56 -1
  17. package/dist/lib/timeline.js +95 -0
  18. package/dist/lib/view.js +61 -72
  19. package/dist/routes/api/collections.js +124 -0
  20. package/dist/routes/api/nav-items.js +104 -0
  21. package/dist/routes/api/pages.js +91 -0
  22. package/dist/routes/api/posts.js +27 -33
  23. package/dist/routes/api/search.js +4 -5
  24. package/dist/routes/api/settings.js +68 -0
  25. package/dist/routes/api/upload.js +13 -13
  26. package/dist/routes/compose.js +48 -0
  27. package/dist/routes/dash/collections.js +24 -42
  28. package/dist/routes/dash/index.js +3 -3
  29. package/dist/routes/dash/media.js +2 -2
  30. package/dist/routes/dash/pages.js +440 -106
  31. package/dist/routes/dash/posts.js +27 -37
  32. package/dist/routes/dash/redirects.js +2 -2
  33. package/dist/routes/dash/settings.js +79 -5
  34. package/dist/routes/feed/rss.js +4 -6
  35. package/dist/routes/feed/sitemap.js +11 -8
  36. package/dist/routes/pages/archive.js +13 -15
  37. package/dist/routes/pages/collection.js +12 -9
  38. package/dist/routes/pages/collections.js +28 -0
  39. package/dist/routes/pages/featured.js +32 -0
  40. package/dist/routes/pages/home.js +19 -68
  41. package/dist/routes/pages/page.js +57 -29
  42. package/dist/routes/pages/post.js +7 -17
  43. package/dist/routes/pages/search.js +5 -9
  44. package/dist/services/collection.js +52 -64
  45. package/dist/services/index.js +5 -3
  46. package/dist/services/navigation.js +29 -53
  47. package/dist/services/page.js +84 -0
  48. package/dist/services/post.js +102 -69
  49. package/dist/services/search.js +24 -18
  50. package/dist/types.js +24 -40
  51. package/dist/ui/compose/ComposeDialog.js +452 -0
  52. package/dist/ui/compose/ComposePrompt.js +55 -0
  53. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
  54. package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
  55. package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
  56. package/dist/{theme/components → ui/dash}/PostList.js +18 -13
  57. package/dist/ui/dash/StatusBadge.js +46 -0
  58. package/dist/{theme/components → ui/dash}/index.js +3 -6
  59. package/dist/ui/feed/LinkCard.js +72 -0
  60. package/dist/ui/feed/NoteCard.js +58 -0
  61. package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
  62. package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
  63. package/dist/ui/feed/TimelineFeed.js +41 -0
  64. package/dist/ui/feed/TimelineItem.js +27 -0
  65. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  66. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  67. package/dist/ui/layouts/SiteLayout.js +141 -0
  68. package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
  69. package/dist/ui/pages/CollectionPage.js +70 -0
  70. package/dist/ui/pages/CollectionsPage.js +76 -0
  71. package/dist/ui/pages/FeaturedPage.js +24 -0
  72. package/dist/ui/pages/HomePage.js +24 -0
  73. package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
  74. package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
  75. package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
  76. package/dist/ui/shared/MediaGallery.js +35 -0
  77. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  78. package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
  79. package/dist/ui/shared/index.js +5 -0
  80. package/package.json +2 -9
  81. package/src/__tests__/helpers/app.ts +4 -0
  82. package/src/__tests__/helpers/db.ts +53 -73
  83. package/src/app.tsx +56 -28
  84. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  85. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  86. package/src/db/migrations/meta/_journal.json +14 -0
  87. package/src/db/schema.ts +63 -46
  88. package/src/i18n/locales/en.po +443 -240
  89. package/src/i18n/locales/en.ts +1 -1
  90. package/src/i18n/locales/zh-Hans.po +443 -240
  91. package/src/i18n/locales/zh-Hans.ts +1 -1
  92. package/src/i18n/locales/zh-Hant.po +443 -240
  93. package/src/i18n/locales/zh-Hant.ts +1 -1
  94. package/src/index.ts +29 -42
  95. package/src/lib/__tests__/excerpt.test.ts +125 -0
  96. package/src/lib/__tests__/schemas.test.ts +201 -99
  97. package/src/lib/__tests__/time.test.ts +62 -0
  98. package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
  99. package/src/lib/__tests__/view.test.ts +204 -50
  100. package/src/lib/constants.ts +2 -4
  101. package/src/lib/excerpt.ts +87 -0
  102. package/src/lib/feed.ts +22 -7
  103. package/src/lib/nav-reorder.ts +1 -1
  104. package/src/lib/navigation.ts +45 -8
  105. package/src/lib/pagination.ts +50 -0
  106. package/src/lib/render.tsx +7 -14
  107. package/src/lib/schemas.ts +119 -51
  108. package/src/lib/theme.ts +5 -5
  109. package/src/lib/time.ts +64 -0
  110. package/src/lib/timeline.ts +141 -0
  111. package/src/lib/view.ts +80 -82
  112. package/src/preset.css +46 -0
  113. package/src/routes/__tests__/compose.test.ts +199 -0
  114. package/src/routes/api/__tests__/collections.test.ts +249 -0
  115. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  116. package/src/routes/api/__tests__/pages.test.ts +218 -0
  117. package/src/routes/api/__tests__/posts.test.ts +50 -108
  118. package/src/routes/api/__tests__/search.test.ts +2 -3
  119. package/src/routes/api/__tests__/settings.test.ts +132 -0
  120. package/src/routes/api/collections.ts +143 -0
  121. package/src/routes/api/nav-items.ts +115 -0
  122. package/src/routes/api/pages.ts +101 -0
  123. package/src/routes/api/posts.ts +28 -28
  124. package/src/routes/api/search.ts +3 -3
  125. package/src/routes/api/settings.ts +91 -0
  126. package/src/routes/api/upload.ts +16 -6
  127. package/src/routes/compose.ts +63 -0
  128. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  129. package/src/routes/dash/collections.tsx +20 -42
  130. package/src/routes/dash/index.tsx +3 -3
  131. package/src/routes/dash/media.tsx +2 -2
  132. package/src/routes/dash/pages.tsx +480 -122
  133. package/src/routes/dash/posts.tsx +42 -54
  134. package/src/routes/dash/redirects.tsx +2 -2
  135. package/src/routes/dash/settings.tsx +83 -5
  136. package/src/routes/feed/rss.ts +4 -3
  137. package/src/routes/feed/sitemap.ts +15 -5
  138. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  139. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  140. package/src/routes/pages/archive.tsx +15 -15
  141. package/src/routes/pages/collection.tsx +16 -9
  142. package/src/routes/pages/collections.tsx +36 -0
  143. package/src/routes/pages/featured.tsx +38 -0
  144. package/src/routes/pages/home.tsx +21 -92
  145. package/src/routes/pages/page.tsx +62 -27
  146. package/src/routes/pages/post.tsx +6 -18
  147. package/src/routes/pages/search.tsx +3 -7
  148. package/src/services/__tests__/collection.test.ts +257 -158
  149. package/src/services/__tests__/media.test.ts +18 -18
  150. package/src/services/__tests__/navigation.test.ts +161 -87
  151. package/src/services/__tests__/page.test.ts +106 -0
  152. package/src/services/__tests__/post-timeline.test.ts +92 -88
  153. package/src/services/__tests__/post.test.ts +432 -197
  154. package/src/services/__tests__/search.test.ts +19 -25
  155. package/src/services/collection.ts +71 -113
  156. package/src/services/index.ts +9 -8
  157. package/src/services/navigation.ts +38 -71
  158. package/src/services/page.ts +136 -0
  159. package/src/services/post.ts +141 -101
  160. package/src/services/search.ts +38 -27
  161. package/src/styles/tokens.css +47 -0
  162. package/src/styles/ui.css +491 -0
  163. package/src/types.ts +212 -198
  164. package/src/ui/compose/ComposeDialog.tsx +395 -0
  165. package/src/ui/compose/ComposePrompt.tsx +55 -0
  166. package/src/ui/dash/FormatBadge.tsx +28 -0
  167. package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
  168. package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
  169. package/src/ui/dash/PostList.tsx +101 -0
  170. package/src/ui/dash/StatusBadge.tsx +61 -0
  171. package/src/ui/dash/index.ts +10 -0
  172. package/src/ui/feed/LinkCard.tsx +72 -0
  173. package/src/ui/feed/NoteCard.tsx +63 -0
  174. package/src/ui/feed/QuoteCard.tsx +68 -0
  175. package/src/ui/feed/ThreadPreview.tsx +48 -0
  176. package/src/ui/feed/TimelineFeed.tsx +49 -0
  177. package/src/ui/feed/TimelineItem.tsx +45 -0
  178. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  179. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  180. package/src/ui/layouts/SiteLayout.tsx +150 -0
  181. package/src/ui/pages/ArchivePage.tsx +162 -0
  182. package/src/ui/pages/CollectionPage.tsx +70 -0
  183. package/src/ui/pages/CollectionsPage.tsx +73 -0
  184. package/src/ui/pages/FeaturedPage.tsx +31 -0
  185. package/src/ui/pages/HomePage.tsx +37 -0
  186. package/src/ui/pages/PostPage.tsx +56 -0
  187. package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
  188. package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
  189. package/src/ui/shared/MediaGallery.tsx +59 -0
  190. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  191. package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
  192. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  193. package/src/ui/shared/index.ts +12 -0
  194. package/bin/jant.js +0 -185
  195. package/dist/lib/theme-components.js +0 -49
  196. package/dist/routes/api/timeline.js +0 -120
  197. package/dist/routes/dash/navigation.js +0 -288
  198. package/dist/theme/components/MediaGallery.js +0 -107
  199. package/dist/theme/components/VisibilityBadge.js +0 -37
  200. package/dist/theme/index.js +0 -18
  201. package/dist/theme/layouts/index.js +0 -2
  202. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  203. package/dist/themes/minimal/index.js +0 -65
  204. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  205. package/dist/themes/minimal/pages/HomePage.js +0 -25
  206. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  207. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  208. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  209. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  210. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  211. package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
  212. package/src/lib/__tests__/theme-components.test.ts +0 -126
  213. package/src/lib/theme-components.ts +0 -68
  214. package/src/routes/api/timeline.tsx +0 -159
  215. package/src/routes/dash/navigation.tsx +0 -316
  216. package/src/theme/components/MediaGallery.tsx +0 -128
  217. package/src/theme/components/PostList.tsx +0 -92
  218. package/src/theme/components/TypeBadge.tsx +0 -37
  219. package/src/theme/components/VisibilityBadge.tsx +0 -45
  220. package/src/theme/components/index.ts +0 -23
  221. package/src/theme/index.ts +0 -22
  222. package/src/theme/layouts/index.ts +0 -7
  223. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  224. package/src/themes/minimal/index.ts +0 -83
  225. package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
  226. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  227. package/src/themes/minimal/pages/HomePage.tsx +0 -41
  228. package/src/themes/minimal/pages/PostPage.tsx +0 -43
  229. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  230. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  231. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  232. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  233. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  234. package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
  235. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
  236. package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
  237. /package/dist/{theme → ui}/color-themes.js +0 -0
  238. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  239. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  240. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  241. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  242. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  243. /package/src/{theme → ui}/color-themes.ts +0 -0
  244. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  245. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  246. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  247. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  248. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -4,8 +4,8 @@ import { getSiteName } from "../../lib/config.js";
4
4
  * Dashboard Posts Routes
5
5
  */ import { Hono } from "hono";
6
6
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
- import { DashLayout } from "../../theme/layouts/index.js";
8
- import { PostForm, PostList, CrudPageHeader, ActionButtons } from "../../theme/components/index.js";
7
+ import { DashLayout } from "../../ui/layouts/DashLayout.js";
8
+ import { PostForm, PostList, CrudPageHeader, ActionButtons } from "../../ui/dash/index.js";
9
9
  import * as sqid from "../../lib/sqid.js";
10
10
  import { dsRedirect } from "../../lib/sse.js";
11
11
  export const postsRoutes = new Hono();
@@ -51,12 +51,7 @@ function NewPostContent({ collections }) {
51
51
  // List posts
52
52
  postsRoutes.get("/", async (c)=>{
53
53
  const posts = await c.var.services.posts.list({
54
- visibility: [
55
- "featured",
56
- "quiet",
57
- "unlisted",
58
- "draft"
59
- ]
54
+ excludeReplies: true
60
55
  });
61
56
  const siteName = await getSiteName(c);
62
57
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
@@ -87,22 +82,21 @@ postsRoutes.get("/new", async (c)=>{
87
82
  postsRoutes.post("/", async (c)=>{
88
83
  const body = await c.req.json();
89
84
  const post = await c.var.services.posts.create({
90
- type: body.type,
85
+ format: body.format,
91
86
  title: body.title || undefined,
92
- content: body.content,
93
- visibility: body.visibility,
94
- sourceUrl: body.sourceUrl || undefined,
95
- sourceName: body.sourceName || undefined,
96
- path: body.path || undefined
87
+ body: body.body,
88
+ status: body.status,
89
+ featured: body.featured,
90
+ pinned: body.pinned,
91
+ url: body.url || undefined,
92
+ quoteText: body.quoteText || undefined,
93
+ rating: body.rating || undefined,
94
+ collectionId: body.collectionId || undefined
97
95
  });
98
96
  // Attach media if provided
99
97
  if (body.mediaIds && body.mediaIds.length > 0) {
100
98
  await c.var.services.media.attachToPost(post.id, body.mediaIds);
101
99
  }
102
- // Sync collection associations
103
- if (body.collectionIds) {
104
- await c.var.services.collections.syncPostCollections(post.id, body.collectionIds);
105
- }
106
100
  return dsRedirect(`/dash/posts/${sqid.encode(post.id)}`);
107
101
  });
108
102
  function ViewPostContent({ post }) {
@@ -111,6 +105,7 @@ function ViewPostContent({ post }) {
111
105
  id: "y28hnO",
112
106
  message: "Post"
113
107
  });
108
+ const permalink = post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`;
114
109
  return /*#__PURE__*/ _jsxs(_Fragment, {
115
110
  children: [
116
111
  /*#__PURE__*/ _jsxs("div", {
@@ -126,7 +121,7 @@ function ViewPostContent({ post }) {
126
121
  id: "ePK91l",
127
122
  message: "Edit"
128
123
  }),
129
- viewHref: `/p/${sqid.encode(post.id)}`,
124
+ viewHref: permalink,
130
125
  viewLabel: $__i18n._({
131
126
  id: "jpctdh",
132
127
  message: "View"
@@ -140,7 +135,7 @@ function ViewPostContent({ post }) {
140
135
  children: /*#__PURE__*/ _jsx("div", {
141
136
  class: "prose",
142
137
  dangerouslySetInnerHTML: {
143
- __html: post.contentHtml || ""
138
+ __html: post.bodyHtml || ""
144
139
  }
145
140
  })
146
141
  })
@@ -148,7 +143,7 @@ function ViewPostContent({ post }) {
148
143
  ]
149
144
  });
150
145
  }
151
- function EditPostContent({ post, mediaAttachments, r2PublicUrl, imageTransformUrl, s3PublicUrl, collections, postCollectionIds }) {
146
+ function EditPostContent({ post, mediaAttachments, r2PublicUrl, imageTransformUrl, s3PublicUrl, collections }) {
152
147
  const { i18n: $__i18n, _: $__ } = $_useLingui();
153
148
  return /*#__PURE__*/ _jsxs(_Fragment, {
154
149
  children: [
@@ -166,8 +161,7 @@ function EditPostContent({ post, mediaAttachments, r2PublicUrl, imageTransformUr
166
161
  r2PublicUrl: r2PublicUrl,
167
162
  imageTransformUrl: imageTransformUrl,
168
163
  s3PublicUrl: s3PublicUrl,
169
- collections: collections,
170
- postCollectionIds: postCollectionIds
164
+ collections: collections
171
165
  })
172
166
  ]
173
167
  });
@@ -202,8 +196,6 @@ postsRoutes.get("/:id/edit", async (c)=>{
202
196
  const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
203
197
  const s3PublicUrl = c.env.S3_PUBLIC_URL;
204
198
  const collections = await c.var.services.collections.list();
205
- const postCollections = await c.var.services.collections.getCollectionsForPost(post.id);
206
- const postCollectionIds = postCollections.map((col)=>col.id);
207
199
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
208
200
  c: c,
209
201
  title: `Edit: ${post.title || "Post"}`,
@@ -215,8 +207,7 @@ postsRoutes.get("/:id/edit", async (c)=>{
215
207
  r2PublicUrl: r2PublicUrl,
216
208
  imageTransformUrl: imageTransformUrl,
217
209
  s3PublicUrl: s3PublicUrl,
218
- collections: collections,
219
- postCollectionIds: postCollectionIds
210
+ collections: collections
220
211
  })
221
212
  }));
222
213
  });
@@ -226,22 +217,21 @@ postsRoutes.post("/:id", async (c)=>{
226
217
  if (!id) return c.notFound();
227
218
  const body = await c.req.json();
228
219
  await c.var.services.posts.update(id, {
229
- type: body.type,
220
+ format: body.format,
230
221
  title: body.title || null,
231
- content: body.content || null,
232
- visibility: body.visibility,
233
- sourceUrl: body.sourceUrl || null,
234
- sourceName: body.sourceName || null,
235
- path: body.path || null
222
+ body: body.body || null,
223
+ status: body.status,
224
+ featured: body.featured,
225
+ pinned: body.pinned,
226
+ url: body.url || null,
227
+ quoteText: body.quoteText || null,
228
+ rating: body.rating || null,
229
+ collectionId: body.collectionId || null
236
230
  });
237
231
  // Update media attachments if provided
238
232
  if (body.mediaIds !== undefined) {
239
233
  await c.var.services.media.attachToPost(id, body.mediaIds);
240
234
  }
241
- // Sync collection associations
242
- if (body.collectionIds !== undefined) {
243
- await c.var.services.collections.syncPostCollections(id, body.collectionIds);
244
- }
245
235
  return dsRedirect(`/dash/posts/${sqid.encode(id)}`);
246
236
  });
247
237
  // Delete post
@@ -4,8 +4,8 @@ import { getSiteName } from "../../lib/config.js";
4
4
  * Dashboard Redirects Routes
5
5
  */ import { Hono } from "hono";
6
6
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
- import { DashLayout } from "../../theme/layouts/index.js";
8
- import { EmptyState, ListItemRow, ActionButtons, CrudPageHeader } from "../../theme/components/index.js";
7
+ import { DashLayout } from "../../ui/layouts/DashLayout.js";
8
+ import { EmptyState, ListItemRow, ActionButtons, CrudPageHeader } from "../../ui/dash/index.js";
9
9
  import { dsRedirect } from "../../lib/sse.js";
10
10
  export const redirectsRoutes = new Hono();
11
11
  function RedirectsListContent({ redirects }) {
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-
5
5
  * Sub-pages: General, Appearance, Account
6
6
  */ import { Hono } from "hono";
7
7
  import { useLingui as $_useLingui } from "@jant/core/i18n";
8
- import { DashLayout } from "../../theme/layouts/index.js";
8
+ import { DashLayout } from "../../ui/layouts/DashLayout.js";
9
9
  import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
10
10
  import { getSiteLanguage, getSiteName, getConfigFallback } from "../../lib/config.js";
11
11
  import { SETTINGS_KEYS } from "../../lib/constants.js";
@@ -272,11 +272,14 @@ function ThemeCard({ theme, selected }) {
272
272
  })
273
273
  });
274
274
  }
275
- function AppearanceContent({ themes, currentThemeId }) {
275
+ function AppearanceContent({ themes, currentThemeId, customCSS }) {
276
276
  const { i18n: $__i18n, _: $__ } = $_useLingui();
277
- const signals = JSON.stringify({
277
+ const themeSignals = JSON.stringify({
278
278
  theme: currentThemeId
279
279
  }).replace(/</g, "\\u003c");
280
+ const cssSignals = JSON.stringify({
281
+ customCSS
282
+ }).replace(/</g, "\\u003c");
280
283
  return /*#__PURE__*/ _jsxs(_Fragment, {
281
284
  children: [
282
285
  /*#__PURE__*/ _jsx("h1", {
@@ -290,7 +293,7 @@ function AppearanceContent({ themes, currentThemeId }) {
290
293
  currentTab: "appearance"
291
294
  }),
292
295
  /*#__PURE__*/ _jsx("div", {
293
- "data-signals": signals,
296
+ "data-signals": themeSignals,
294
297
  "data-on:change": "@post('/dash/settings/appearance')",
295
298
  class: "max-w-3xl",
296
299
  children: /*#__PURE__*/ _jsxs("fieldset", {
@@ -318,6 +321,63 @@ function AppearanceContent({ themes, currentThemeId }) {
318
321
  })
319
322
  ]
320
323
  })
324
+ }),
325
+ /*#__PURE__*/ _jsxs("form", {
326
+ "data-signals": cssSignals,
327
+ "data-on:submit__prevent": "@post('/dash/settings/custom-css')",
328
+ "data-indicator": "_cssLoading",
329
+ class: "max-w-3xl mt-8",
330
+ children: [
331
+ /*#__PURE__*/ _jsxs("fieldset", {
332
+ children: [
333
+ /*#__PURE__*/ _jsx("legend", {
334
+ class: "text-lg font-semibold",
335
+ children: $__i18n._({
336
+ id: "9+vGLh",
337
+ message: "Custom CSS"
338
+ })
339
+ }),
340
+ /*#__PURE__*/ _jsx("p", {
341
+ class: "text-sm text-muted-foreground mb-4",
342
+ children: $__i18n._({
343
+ id: "vmQmHx",
344
+ message: "Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements."
345
+ })
346
+ }),
347
+ /*#__PURE__*/ _jsx("textarea", {
348
+ "data-bind": "customCSS",
349
+ class: "textarea font-mono text-sm min-h-32",
350
+ rows: 8,
351
+ placeholder: $__i18n._({
352
+ id: "wc+17X",
353
+ message: "/* Your custom CSS here */"
354
+ }),
355
+ children: customCSS
356
+ })
357
+ ]
358
+ }),
359
+ /*#__PURE__*/ _jsxs("button", {
360
+ type: "submit",
361
+ class: "btn mt-4",
362
+ "data-attr-disabled": "$_cssLoading",
363
+ children: [
364
+ /*#__PURE__*/ _jsx("span", {
365
+ "data-show": "!$_cssLoading",
366
+ children: $__i18n._({
367
+ id: "NU2Fqi",
368
+ message: "Save CSS"
369
+ })
370
+ }),
371
+ /*#__PURE__*/ _jsx("span", {
372
+ "data-show": "$_cssLoading",
373
+ children: $__i18n._({
374
+ id: "k1ifdL",
375
+ message: "Processing..."
376
+ })
377
+ })
378
+ ]
379
+ })
380
+ ]
321
381
  })
322
382
  ]
323
383
  });
@@ -583,6 +643,7 @@ settingsRoutes.get("/appearance", async (c)=>{
583
643
  const { settings } = c.var.services;
584
644
  const siteName = await getSiteName(c);
585
645
  const currentThemeId = await settings.get(SETTINGS_KEYS.THEME) ?? "default";
646
+ const customCSS = await settings.get(SETTINGS_KEYS.CUSTOM_CSS) ?? "";
586
647
  const themes = getAvailableThemes(c.var.config);
587
648
  const saved = c.req.query("saved") !== undefined;
588
649
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
@@ -595,7 +656,8 @@ settingsRoutes.get("/appearance", async (c)=>{
595
656
  } : undefined,
596
657
  children: /*#__PURE__*/ _jsx(AppearanceContent, {
597
658
  themes: themes,
598
- currentThemeId: currentThemeId
659
+ currentThemeId: currentThemeId,
660
+ customCSS: customCSS
599
661
  })
600
662
  }));
601
663
  });
@@ -615,6 +677,18 @@ settingsRoutes.post("/appearance", async (c)=>{
615
677
  }
616
678
  return dsRedirect("/dash/settings/appearance?saved");
617
679
  });
680
+ // Save custom CSS
681
+ settingsRoutes.post("/custom-css", async (c)=>{
682
+ const body = await c.req.json();
683
+ const { settings } = c.var.services;
684
+ const css = body.customCSS?.trim() ?? "";
685
+ if (css) {
686
+ await settings.set(SETTINGS_KEYS.CUSTOM_CSS, css);
687
+ } else {
688
+ await settings.remove(SETTINGS_KEYS.CUSTOM_CSS);
689
+ }
690
+ return dsToast("Custom CSS saved successfully.");
691
+ });
618
692
  // Account page
619
693
  settingsRoutes.get("/account", async (c)=>{
620
694
  const siteName = await getSiteName(c);
@@ -15,10 +15,8 @@ export const rssRoutes = new Hono();
15
15
  const siteUrl = c.env.SITE_URL;
16
16
  const siteLanguage = await getSiteLanguage(c);
17
17
  const posts = await c.var.services.posts.list({
18
- visibility: [
19
- "featured",
20
- "quiet"
21
- ],
18
+ status: "published",
19
+ excludeReplies: true,
22
20
  limit: 50
23
21
  });
24
22
  // Batch load media for enclosures
@@ -42,7 +40,7 @@ export const rssRoutes = new Hono();
42
40
  // RSS 2.0 Feed - main feed at /feed
43
41
  rssRoutes.get("/", async (c)=>{
44
42
  const feedData = await buildFeedData(c);
45
- const renderer = c.var.config.theme?.feed?.rss ?? defaultRssRenderer;
43
+ const renderer = c.var.config.feed?.rss ?? defaultRssRenderer;
46
44
  const xml = renderer(feedData);
47
45
  return new Response(xml, {
48
46
  headers: {
@@ -53,7 +51,7 @@ rssRoutes.get("/", async (c)=>{
53
51
  // Atom Feed
54
52
  rssRoutes.get("/atom.xml", async (c)=>{
55
53
  const feedData = await buildFeedData(c);
56
- const renderer = c.var.config.theme?.feed?.atom ?? defaultAtomRenderer;
54
+ const renderer = c.var.config.feed?.atom ?? defaultAtomRenderer;
57
55
  const xml = renderer(feedData);
58
56
  return new Response(xml, {
59
57
  headers: {
@@ -2,25 +2,28 @@
2
2
  * Sitemap Routes
3
3
  */ import { Hono } from "hono";
4
4
  import { defaultSitemapRenderer } from "../../lib/feed.js";
5
- import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
5
+ import { createMediaContext, toPostViewsFromPosts, toPageView } from "../../lib/view.js";
6
6
  export const sitemapRoutes = new Hono();
7
7
  // XML Sitemap
8
8
  sitemapRoutes.get("/sitemap.xml", async (c)=>{
9
9
  const siteUrl = c.env.SITE_URL;
10
10
  const posts = await c.var.services.posts.list({
11
- visibility: [
12
- "featured",
13
- "quiet"
14
- ],
11
+ status: "published",
12
+ excludeReplies: true,
15
13
  limit: 1000
16
14
  });
17
- // Transform to PostView[]
15
+ // Fetch published pages
16
+ const allPages = await c.var.services.pages.list();
17
+ const publishedPages = allPages.filter((p)=>p.status === "published");
18
+ // Transform to View Models
18
19
  const mediaCtx = createMediaContext(c);
19
20
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
20
- const renderer = c.var.config.theme?.feed?.sitemap ?? defaultSitemapRenderer;
21
+ const pageViews = publishedPages.map(toPageView);
22
+ const renderer = c.var.config.feed?.sitemap ?? defaultSitemapRenderer;
21
23
  const xml = renderer({
22
24
  siteUrl,
23
- posts: postViews
25
+ posts: postViews,
26
+ pages: pageViews
24
27
  });
25
28
  return new Response(xml, {
26
29
  headers: {
@@ -2,10 +2,10 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Archive Page Route
4
4
  *
5
- * Shows all posts, optionally filtered by type
5
+ * Shows all posts, optionally filtered by format or featured status
6
6
  */ import { Hono } from "hono";
7
- import { POST_TYPES } from "../../types.js";
8
- import { ArchivePage as DefaultArchivePage } from "../../themes/minimal/pages/ArchivePage.js";
7
+ import { FORMATS } from "../../types.js";
8
+ import { ArchivePage } from "../../ui/pages/ArchivePage.js";
9
9
  import { getNavigationData } from "../../lib/navigation.js";
10
10
  import { renderPublicPage } from "../../lib/render.js";
11
11
  import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
@@ -13,19 +13,19 @@ const PAGE_SIZE = 50;
13
13
  export const archiveRoutes = new Hono();
14
14
  // Archive page - all posts
15
15
  archiveRoutes.get("/", async (c)=>{
16
- const typeParam = c.req.query("type");
17
- const type = typeParam && POST_TYPES.includes(typeParam) ? typeParam : undefined;
16
+ const formatParam = c.req.query("format");
17
+ const format = formatParam && FORMATS.includes(formatParam) ? formatParam : undefined;
18
+ const featuredParam = c.req.query("featured");
19
+ const featured = featuredParam === "true" ? true : undefined;
18
20
  // Parse cursor
19
21
  const cursorParam = c.req.query("cursor");
20
22
  const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
21
23
  const navData = await getNavigationData(c);
22
24
  // Fetch one extra to check for more
23
25
  const posts = await c.var.services.posts.list({
24
- type,
25
- visibility: [
26
- "featured",
27
- "quiet"
28
- ],
26
+ format,
27
+ status: "published",
28
+ featured,
29
29
  excludeReplies: true,
30
30
  cursor,
31
31
  limit: PAGE_SIZE + 1
@@ -48,17 +48,15 @@ archiveRoutes.get("/", async (c)=>{
48
48
  // Transform to View Models
49
49
  const mediaCtx = createMediaContext(c);
50
50
  const groups = toArchiveGroups(grouped, mediaCtx);
51
- const components = c.var.config.theme?.components;
52
- const Page = components?.ArchivePage ?? DefaultArchivePage;
53
51
  return renderPublicPage(c, {
54
52
  title: `Archive - ${navData.siteName}`,
55
53
  navData,
56
- content: /*#__PURE__*/ _jsx(Page, {
54
+ content: /*#__PURE__*/ _jsx(ArchivePage, {
57
55
  groups: groups,
58
56
  hasMore: hasMore,
59
57
  nextCursor: nextCursor,
60
- type: type,
61
- theme: components
58
+ format: format,
59
+ featured: featured
62
60
  })
63
61
  });
64
62
  });
@@ -2,30 +2,33 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Collection Page Route
4
4
  */ import { Hono } from "hono";
5
- import { CollectionPage as DefaultCollectionPage } from "../../themes/minimal/pages/CollectionPage.js";
5
+ import { CollectionPage } from "../../ui/pages/CollectionPage.js";
6
6
  import { getNavigationData } from "../../lib/navigation.js";
7
7
  import { renderPublicPage } from "../../lib/render.js";
8
8
  import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
9
9
  export const collectionRoutes = new Hono();
10
- collectionRoutes.get("/:path", async (c)=>{
11
- const path = c.req.param("path");
12
- const collection = await c.var.services.collections.getByPath(path);
10
+ collectionRoutes.get("/:slug", async (c)=>{
11
+ const slug = c.req.param("slug");
12
+ const collection = await c.var.services.collections.getBySlug(slug);
13
13
  if (!collection) return c.notFound();
14
- const posts = await c.var.services.collections.getPosts(collection.id);
14
+ // Fetch posts in this collection
15
+ const posts = await c.var.services.posts.list({
16
+ collectionId: collection.id,
17
+ status: "published",
18
+ excludeReplies: true
19
+ });
15
20
  const navData = await getNavigationData(c);
16
21
  // Transform to View Models
17
22
  const mediaCtx = createMediaContext(c);
18
23
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
19
- const components = c.var.config.theme?.components;
20
- const Page = components?.CollectionPage ?? DefaultCollectionPage;
21
24
  return renderPublicPage(c, {
22
25
  title: `${collection.title} - ${navData.siteName}`,
23
26
  description: collection.description ?? undefined,
24
27
  navData,
25
- content: /*#__PURE__*/ _jsx(Page, {
28
+ content: /*#__PURE__*/ _jsx(CollectionPage, {
26
29
  collection: collection,
27
30
  posts: postViews,
28
- theme: components
31
+ hasMore: false
29
32
  })
30
33
  });
31
34
  });
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
+ /**
3
+ * Collections Listing Page Route
4
+ *
5
+ * Lists all collections with their post counts.
6
+ */ import { Hono } from "hono";
7
+ import { getNavigationData } from "../../lib/navigation.js";
8
+ import { renderPublicPage } from "../../lib/render.js";
9
+ import { CollectionsPage } from "../../ui/pages/CollectionsPage.js";
10
+ export const collectionsPageRoutes = new Hono();
11
+ collectionsPageRoutes.get("/", async (c)=>{
12
+ const [allCollections, postCounts] = await Promise.all([
13
+ c.var.services.collections.list(),
14
+ c.var.services.collections.getPostCounts()
15
+ ]);
16
+ const collections = allCollections.map((col)=>({
17
+ ...col,
18
+ postCount: postCounts.get(col.id) ?? 0
19
+ }));
20
+ const navData = await getNavigationData(c);
21
+ return renderPublicPage(c, {
22
+ title: `Collections - ${navData.siteName}`,
23
+ navData,
24
+ content: /*#__PURE__*/ _jsx(CollectionsPage, {
25
+ collections: collections
26
+ })
27
+ });
28
+ });
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
+ /**
3
+ * Featured Page Route
4
+ *
5
+ * Shows featured posts as a timeline feed.
6
+ */ import { Hono } from "hono";
7
+ import { getNavigationData } from "../../lib/navigation.js";
8
+ import { renderPublicPage } from "../../lib/render.js";
9
+ import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
10
+ import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
11
+ export const featuredRoutes = new Hono();
12
+ featuredRoutes.get("/", async (c)=>{
13
+ const posts = await c.var.services.posts.list({
14
+ featured: true,
15
+ status: "published",
16
+ excludeReplies: true
17
+ });
18
+ const navData = await getNavigationData(c);
19
+ const mediaCtx = createMediaContext(c);
20
+ const postViews = toPostViewsFromPosts(posts, mediaCtx);
21
+ // Convert to timeline items (simple — no thread previews)
22
+ const items = postViews.map((post)=>({
23
+ post
24
+ }));
25
+ return renderPublicPage(c, {
26
+ title: `Featured - ${navData.siteName}`,
27
+ navData,
28
+ content: /*#__PURE__*/ _jsx(FeaturedPage, {
29
+ items: items
30
+ })
31
+ });
32
+ });
@@ -3,86 +3,37 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
3
3
  * Home Page Route
4
4
  *
5
5
  * Timeline feed with per-type card components and thread previews.
6
+ * Uses page-based pagination.
6
7
  */ import { Hono } from "hono";
7
- import { buildMediaMap } from "../../lib/media-helpers.js";
8
8
  import { getNavigationData } from "../../lib/navigation.js";
9
9
  import { renderPublicPage } from "../../lib/render.js";
10
- import { HomePage as DefaultHomePage } from "../../themes/minimal/pages/HomePage.js";
11
- import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
12
- const PAGE_SIZE = 20;
10
+ import { assembleTimeline } from "../../lib/timeline.js";
11
+ import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
12
+ import { HomePage } from "../../ui/pages/HomePage.js";
13
13
  export const homeRoutes = new Hono();
14
14
  homeRoutes.get("/", async (c)=>{
15
+ const pageParam = c.req.query("page");
16
+ const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
17
+ const { items, currentPage, totalPages } = await assembleTimeline(c, {
18
+ page
19
+ });
15
20
  const navData = await getNavigationData(c);
16
- // Fetch one extra to determine if there are more
17
- const posts = await c.var.services.posts.list({
18
- visibility: [
19
- "featured",
20
- "quiet"
21
- ],
22
- excludeReplies: true,
23
- excludeTypes: [
24
- "page"
25
- ],
26
- limit: PAGE_SIZE + 1
21
+ // Fetch pinned posts
22
+ const pinnedPosts = await c.var.services.posts.list({
23
+ pinned: true,
24
+ status: "published",
25
+ excludeReplies: true
27
26
  });
28
- const hasMore = posts.length > PAGE_SIZE;
29
- const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
30
- // Batch load media attachments
31
- const postIds = displayPosts.map((p)=>p.id);
32
- const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
33
27
  const mediaCtx = createMediaContext(c);
34
- const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
35
- // Get reply counts to identify thread roots
36
- const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
37
- const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
38
- // Batch load thread previews
39
- const threadPreviews = await c.var.services.posts.getThreadPreviews(threadRootIds, 3);
40
- // Batch load media for preview replies
41
- const previewReplyIds = [];
42
- for (const replies of threadPreviews.values()){
43
- for (const reply of replies){
44
- previewReplyIds.push(reply.id);
45
- }
46
- }
47
- const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl) : new Map();
48
- // Assemble timeline items with View Models
49
- const items = displayPosts.map((post)=>{
50
- const postView = toPostView({
51
- ...post,
52
- mediaAttachments: mediaMap.get(post.id) ?? []
53
- }, mediaCtx);
54
- const replyCount = replyCounts.get(post.id) ?? 0;
55
- const previewReplies = threadPreviews.get(post.id);
56
- if (replyCount > 0 && previewReplies) {
57
- return {
58
- post: postView,
59
- threadPreview: {
60
- replies: toPostViews(previewReplies.map((r)=>({
61
- ...r,
62
- mediaAttachments: previewMediaMap.get(r.id) ?? []
63
- })), mediaCtx),
64
- totalReplyCount: replyCount
65
- }
66
- };
67
- }
68
- return {
69
- post: postView
70
- };
71
- });
72
- // Determine next cursor
73
- const lastPost = displayPosts[displayPosts.length - 1];
74
- const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
75
- // Resolve page component
76
- const components = c.var.config.theme?.components;
77
- const Page = components?.HomePage ?? DefaultHomePage;
28
+ const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
78
29
  return renderPublicPage(c, {
79
30
  title: navData.siteName,
80
31
  navData,
81
- content: /*#__PURE__*/ _jsx(Page, {
32
+ content: /*#__PURE__*/ _jsx(HomePage, {
82
33
  items: items,
83
- hasMore: hasMore,
84
- nextCursor: nextCursor,
85
- theme: components
34
+ pinnedItems: pinnedItems,
35
+ currentPage: currentPage,
36
+ totalPages: totalPages
86
37
  })
87
38
  });
88
39
  });