@jant/core 0.3.24 → 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 (206) hide show
  1. package/dist/app.js +50 -25
  2. package/dist/db/schema.js +1 -1
  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 +3 -9
  7. package/dist/lib/constants.js +1 -0
  8. package/dist/lib/nav-reorder.js +1 -1
  9. package/dist/lib/navigation.js +26 -1
  10. package/dist/lib/pagination.js +44 -0
  11. package/dist/lib/render.js +7 -11
  12. package/dist/lib/schemas.js +3 -3
  13. package/dist/lib/theme.js +4 -4
  14. package/dist/lib/timeline.js +24 -48
  15. package/dist/lib/view.js +2 -2
  16. package/dist/routes/api/collections.js +124 -0
  17. package/dist/routes/api/nav-items.js +104 -0
  18. package/dist/routes/api/pages.js +91 -0
  19. package/dist/routes/api/posts.js +2 -2
  20. package/dist/routes/api/search.js +2 -2
  21. package/dist/routes/api/settings.js +68 -0
  22. package/dist/routes/compose.js +48 -0
  23. package/dist/routes/dash/collections.js +2 -2
  24. package/dist/routes/dash/index.js +1 -1
  25. package/dist/routes/dash/media.js +2 -2
  26. package/dist/routes/dash/pages.js +411 -62
  27. package/dist/routes/dash/posts.js +3 -5
  28. package/dist/routes/dash/redirects.js +2 -2
  29. package/dist/routes/dash/settings.js +79 -5
  30. package/dist/routes/feed/rss.js +2 -2
  31. package/dist/routes/feed/sitemap.js +1 -1
  32. package/dist/routes/pages/archive.js +3 -6
  33. package/dist/routes/pages/collection.js +3 -6
  34. package/dist/routes/pages/collections.js +28 -0
  35. package/dist/routes/pages/featured.js +32 -0
  36. package/dist/routes/pages/home.js +9 -50
  37. package/dist/routes/pages/page.js +29 -32
  38. package/dist/routes/pages/post.js +3 -6
  39. package/dist/routes/pages/search.js +3 -6
  40. package/dist/services/page.js +5 -1
  41. package/dist/services/post.js +40 -6
  42. package/dist/services/search.js +1 -1
  43. package/dist/ui/compose/ComposeDialog.js +452 -0
  44. package/dist/ui/compose/ComposePrompt.js +55 -0
  45. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  46. package/dist/{theme/components → ui/dash}/PostForm.js +0 -27
  47. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  48. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  49. package/dist/{theme/components → ui/dash}/index.js +3 -6
  50. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  51. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  52. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  53. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  54. package/dist/ui/feed/TimelineFeed.js +41 -0
  55. package/dist/ui/feed/TimelineItem.js +27 -0
  56. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  57. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  58. package/dist/ui/layouts/SiteLayout.js +141 -0
  59. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  60. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  61. package/dist/ui/pages/CollectionsPage.js +76 -0
  62. package/dist/ui/pages/FeaturedPage.js +24 -0
  63. package/dist/ui/pages/HomePage.js +24 -0
  64. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  65. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  66. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  67. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  68. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  69. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  70. package/dist/ui/shared/index.js +5 -0
  71. package/package.json +1 -9
  72. package/src/__tests__/helpers/db.ts +3 -0
  73. package/src/app.tsx +57 -27
  74. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  75. package/src/db/migrations/meta/_journal.json +7 -0
  76. package/src/db/schema.ts +1 -1
  77. package/src/i18n/locales/en.po +332 -181
  78. package/src/i18n/locales/en.ts +1 -1
  79. package/src/i18n/locales/zh-Hans.po +332 -181
  80. package/src/i18n/locales/zh-Hans.ts +1 -1
  81. package/src/i18n/locales/zh-Hant.po +332 -181
  82. package/src/i18n/locales/zh-Hant.ts +1 -1
  83. package/src/index.ts +7 -36
  84. package/src/lib/__tests__/schemas.test.ts +60 -19
  85. package/src/lib/__tests__/timeline.test.ts +45 -81
  86. package/src/lib/__tests__/view.test.ts +13 -7
  87. package/src/lib/constants.ts +1 -0
  88. package/src/lib/nav-reorder.ts +1 -1
  89. package/src/lib/navigation.ts +40 -2
  90. package/src/lib/pagination.ts +50 -0
  91. package/src/lib/render.tsx +7 -14
  92. package/src/lib/schemas.ts +8 -6
  93. package/src/lib/theme.ts +5 -5
  94. package/src/lib/timeline.ts +28 -57
  95. package/src/lib/view.ts +2 -2
  96. package/src/preset.css +2 -1
  97. package/src/routes/__tests__/compose.test.ts +199 -0
  98. package/src/routes/api/__tests__/collections.test.ts +249 -0
  99. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  100. package/src/routes/api/__tests__/pages.test.ts +218 -0
  101. package/src/routes/api/__tests__/settings.test.ts +132 -0
  102. package/src/routes/api/collections.ts +143 -0
  103. package/src/routes/api/nav-items.ts +115 -0
  104. package/src/routes/api/pages.ts +101 -0
  105. package/src/routes/api/posts.ts +2 -2
  106. package/src/routes/api/search.ts +2 -2
  107. package/src/routes/api/settings.ts +91 -0
  108. package/src/routes/compose.ts +63 -0
  109. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  110. package/src/routes/dash/collections.tsx +2 -2
  111. package/src/routes/dash/index.tsx +1 -1
  112. package/src/routes/dash/media.tsx +2 -2
  113. package/src/routes/dash/pages.tsx +443 -70
  114. package/src/routes/dash/posts.tsx +3 -7
  115. package/src/routes/dash/redirects.tsx +2 -2
  116. package/src/routes/dash/settings.tsx +83 -5
  117. package/src/routes/feed/rss.ts +2 -2
  118. package/src/routes/feed/sitemap.ts +1 -1
  119. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  120. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  121. package/src/routes/pages/archive.tsx +2 -6
  122. package/src/routes/pages/collection.tsx +2 -6
  123. package/src/routes/pages/collections.tsx +36 -0
  124. package/src/routes/pages/featured.tsx +38 -0
  125. package/src/routes/pages/home.tsx +9 -55
  126. package/src/routes/pages/page.tsx +28 -30
  127. package/src/routes/pages/post.tsx +2 -5
  128. package/src/routes/pages/search.tsx +2 -6
  129. package/src/services/__tests__/page.test.ts +106 -0
  130. package/src/services/__tests__/post.test.ts +114 -15
  131. package/src/services/page.ts +13 -1
  132. package/src/services/post.ts +57 -7
  133. package/src/services/search.ts +2 -2
  134. package/src/styles/tokens.css +47 -0
  135. package/src/styles/ui.css +491 -0
  136. package/src/types.ts +29 -159
  137. package/src/ui/compose/ComposeDialog.tsx +395 -0
  138. package/src/ui/compose/ComposePrompt.tsx +55 -0
  139. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  140. package/src/{theme/components → ui/dash}/PostForm.tsx +0 -25
  141. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  142. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  143. package/src/ui/dash/index.ts +10 -0
  144. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  145. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  146. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  147. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  148. package/src/ui/feed/TimelineFeed.tsx +49 -0
  149. package/src/ui/feed/TimelineItem.tsx +45 -0
  150. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  151. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  152. package/src/ui/layouts/SiteLayout.tsx +150 -0
  153. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  154. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  155. package/src/ui/pages/CollectionsPage.tsx +73 -0
  156. package/src/ui/pages/FeaturedPage.tsx +31 -0
  157. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  158. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  159. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  160. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  161. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  162. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  163. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  164. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  165. package/src/ui/shared/index.ts +12 -0
  166. package/bin/jant.js +0 -185
  167. package/dist/lib/theme-components.js +0 -46
  168. package/dist/routes/dash/navigation.js +0 -289
  169. package/dist/theme/index.js +0 -18
  170. package/dist/theme/layouts/index.js +0 -2
  171. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  172. package/dist/themes/threads/index.js +0 -81
  173. package/dist/themes/threads/pages/HomePage.js +0 -25
  174. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  175. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  176. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  177. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  178. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  179. package/src/lib/__tests__/theme-components.test.ts +0 -105
  180. package/src/lib/theme-components.ts +0 -65
  181. package/src/routes/dash/navigation.tsx +0 -317
  182. package/src/theme/components/index.ts +0 -23
  183. package/src/theme/index.ts +0 -22
  184. package/src/theme/layouts/index.ts +0 -7
  185. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  186. package/src/themes/threads/index.ts +0 -100
  187. package/src/themes/threads/style.css +0 -336
  188. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  189. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  190. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  191. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  192. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  193. /package/dist/{theme → ui}/color-themes.js +0 -0
  194. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  195. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  196. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  197. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  198. /package/dist/{theme/components → ui/dash}/PageForm.js +0 -0
  199. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  200. /package/src/{theme → ui}/color-themes.ts +0 -0
  201. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  202. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  203. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  204. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  205. /package/src/{theme/components → ui/dash}/PageForm.tsx +0 -0
  206. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -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);
@@ -40,7 +40,7 @@ export const rssRoutes = new Hono();
40
40
  // RSS 2.0 Feed - main feed at /feed
41
41
  rssRoutes.get("/", async (c)=>{
42
42
  const feedData = await buildFeedData(c);
43
- const renderer = c.var.config.theme?.feed?.rss ?? defaultRssRenderer;
43
+ const renderer = c.var.config.feed?.rss ?? defaultRssRenderer;
44
44
  const xml = renderer(feedData);
45
45
  return new Response(xml, {
46
46
  headers: {
@@ -51,7 +51,7 @@ rssRoutes.get("/", async (c)=>{
51
51
  // Atom Feed
52
52
  rssRoutes.get("/atom.xml", async (c)=>{
53
53
  const feedData = await buildFeedData(c);
54
- const renderer = c.var.config.theme?.feed?.atom ?? defaultAtomRenderer;
54
+ const renderer = c.var.config.feed?.atom ?? defaultAtomRenderer;
55
55
  const xml = renderer(feedData);
56
56
  return new Response(xml, {
57
57
  headers: {
@@ -19,7 +19,7 @@ sitemapRoutes.get("/sitemap.xml", async (c)=>{
19
19
  const mediaCtx = createMediaContext(c);
20
20
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
21
21
  const pageViews = publishedPages.map(toPageView);
22
- const renderer = c.var.config.theme?.feed?.sitemap ?? defaultSitemapRenderer;
22
+ const renderer = c.var.config.feed?.sitemap ?? defaultSitemapRenderer;
23
23
  const xml = renderer({
24
24
  siteUrl,
25
25
  posts: postViews,
@@ -5,7 +5,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
5
5
  * Shows all posts, optionally filtered by format or featured status
6
6
  */ import { Hono } from "hono";
7
7
  import { FORMATS } from "../../types.js";
8
- import { ArchivePage as DefaultArchivePage } from "../../themes/threads/pages/ArchivePage.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";
@@ -48,18 +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
58
  format: format,
61
- featured: featured,
62
- theme: components
59
+ featured: featured
63
60
  })
64
61
  });
65
62
  });
@@ -2,7 +2,7 @@ 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/threads/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";
@@ -21,17 +21,14 @@ collectionRoutes.get("/:slug", async (c)=>{
21
21
  // Transform to View Models
22
22
  const mediaCtx = createMediaContext(c);
23
23
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
24
- const components = c.var.config.theme?.components;
25
- const Page = components?.CollectionPage ?? DefaultCollectionPage;
26
24
  return renderPublicPage(c, {
27
25
  title: `${collection.title} - ${navData.siteName}`,
28
26
  description: collection.description ?? undefined,
29
27
  navData,
30
- content: /*#__PURE__*/ _jsx(Page, {
28
+ content: /*#__PURE__*/ _jsx(CollectionPage, {
31
29
  collection: collection,
32
30
  posts: postViews,
33
- hasMore: false,
34
- theme: components
31
+ hasMore: false
35
32
  })
36
33
  });
37
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,58 +3,20 @@ 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
- * Handles both full-page rendering and load-more SSE responses.
6
+ * Uses page-based pagination.
7
7
  */ import { Hono } from "hono";
8
8
  import { getNavigationData } from "../../lib/navigation.js";
9
9
  import { renderPublicPage } from "../../lib/render.js";
10
10
  import { assembleTimeline } from "../../lib/timeline.js";
11
- import { sse } from "../../lib/sse.js";
12
11
  import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
13
- import { HomePage as DefaultHomePage } from "../../themes/threads/pages/HomePage.js";
12
+ import { HomePage } from "../../ui/pages/HomePage.js";
14
13
  export const homeRoutes = new Hono();
15
14
  homeRoutes.get("/", async (c)=>{
16
- const cursorParam = c.req.query("cursor");
17
- const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
18
- const lastDate = c.req.query("lastDate");
19
- const { items, hasMore, nextCursor } = await assembleTimeline(c, {
20
- cursor: cursor && !isNaN(cursor) ? cursor : undefined
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
21
19
  });
22
- // SSE load-more response
23
- if (cursor && !isNaN(cursor)) {
24
- if (items.length === 0) {
25
- return sse(c, async (stream)=>{
26
- stream.remove("#load-more-container");
27
- });
28
- }
29
- const themeConfig = c.var.config.theme;
30
- const renderMore = themeConfig?.timelineMore;
31
- if (!renderMore) {
32
- // Should never happen — default theme always provides timelineMore
33
- return sse(c, async (stream)=>{
34
- stream.remove("#load-more-container");
35
- });
36
- }
37
- const patches = renderMore({
38
- items,
39
- lastDate: lastDate ?? undefined,
40
- hasMore,
41
- nextCursor,
42
- theme: themeConfig?.components
43
- });
44
- return sse(c, async (stream)=>{
45
- for (const patch of patches){
46
- if (patch.mode === "remove") {
47
- stream.remove(patch.selector);
48
- } else {
49
- stream.patchElements(patch.content, {
50
- mode: patch.mode,
51
- selector: patch.selector
52
- });
53
- }
54
- }
55
- });
56
- }
57
- // Full page render
58
20
  const navData = await getNavigationData(c);
59
21
  // Fetch pinned posts
60
22
  const pinnedPosts = await c.var.services.posts.list({
@@ -64,17 +26,14 @@ homeRoutes.get("/", async (c)=>{
64
26
  });
65
27
  const mediaCtx = createMediaContext(c);
66
28
  const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
67
- const components = c.var.config.theme?.components;
68
- const Page = components?.HomePage ?? DefaultHomePage;
69
29
  return renderPublicPage(c, {
70
30
  title: navData.siteName,
71
31
  navData,
72
- content: /*#__PURE__*/ _jsx(Page, {
32
+ content: /*#__PURE__*/ _jsx(HomePage, {
73
33
  items: items,
74
34
  pinnedItems: pinnedItems,
75
- hasMore: hasMore,
76
- nextCursor: nextCursor,
77
- theme: components
35
+ currentPage: currentPage,
36
+ totalPages: totalPages
78
37
  })
79
38
  });
80
39
  });
@@ -2,44 +2,44 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Custom Page Route
4
4
  *
5
- * Serves pages from the pages table and posts with custom slugs.
5
+ * Serves pages from the pages table and posts with custom paths.
6
6
  * This is a catch-all route mounted at "/" - must be registered last.
7
+ * Supports multi-level paths (e.g. /2024/my-post) for posts.
7
8
  */ import { Hono } from "hono";
8
- import { SinglePage as DefaultSinglePage } from "../../themes/threads/pages/SinglePage.js";
9
- import { PostPage as DefaultPostPage } from "../../themes/threads/pages/PostPage.js";
9
+ import { SinglePage } from "../../ui/pages/SinglePage.js";
10
+ import { PostPage } from "../../ui/pages/PostPage.js";
10
11
  import { getNavigationData } from "../../lib/navigation.js";
11
12
  import { renderPublicPage } from "../../lib/render.js";
12
13
  import { buildMediaMap } from "../../lib/media-helpers.js";
13
14
  import { createMediaContext, toPageView, toPostView } from "../../lib/view.js";
14
15
  export const pageRoutes = new Hono();
15
- // Catch-all for custom page paths and post slugs
16
- pageRoutes.get("/:slug", async (c)=>{
17
- const slug = c.req.param("slug");
18
- // First, try to find a page by slug
19
- const page = await c.var.services.pages.getBySlug(slug);
20
- if (page) {
21
- // Don't show draft pages
22
- if (page.status === "draft") {
23
- return c.notFound();
16
+ // Catch-all for custom page slugs and post paths (including multi-level)
17
+ pageRoutes.get("/*", async (c)=>{
18
+ const fullPath = c.req.path.slice(1); // Remove leading /
19
+ if (!fullPath) return c.notFound();
20
+ const isMultiSegment = fullPath.includes("/");
21
+ // Pages only have single-level slugs; skip page lookup for multi-segment paths
22
+ if (!isMultiSegment) {
23
+ const page = await c.var.services.pages.getBySlug(fullPath);
24
+ if (page) {
25
+ if (page.status === "draft") {
26
+ return c.notFound();
27
+ }
28
+ const navData = await getNavigationData(c);
29
+ const pageView = toPageView(page);
30
+ return renderPublicPage(c, {
31
+ title: `${page.title || fullPath} - ${navData.siteName}`,
32
+ description: page.body?.slice(0, 160),
33
+ navData,
34
+ content: /*#__PURE__*/ _jsx(SinglePage, {
35
+ page: pageView
36
+ })
37
+ });
24
38
  }
25
- const navData = await getNavigationData(c);
26
- const pageView = toPageView(page);
27
- const components = c.var.config.theme?.components;
28
- const Page = components?.SinglePage ?? DefaultSinglePage;
29
- return renderPublicPage(c, {
30
- title: `${page.title || slug} - ${navData.siteName}`,
31
- description: page.body?.slice(0, 160),
32
- navData,
33
- content: /*#__PURE__*/ _jsx(Page, {
34
- page: pageView,
35
- theme: components
36
- })
37
- });
38
39
  }
39
- // Then, try to find a post by slug
40
- const post = await c.var.services.posts.getBySlug(slug);
40
+ // Posts support multi-level paths
41
+ const post = await c.var.services.posts.getByPath(fullPath);
41
42
  if (post) {
42
- // Don't show draft posts
43
43
  if (post.status === "draft") {
44
44
  return c.notFound();
45
45
  }
@@ -55,15 +55,12 @@ pageRoutes.get("/:slug", async (c)=>{
55
55
  }, mediaCtx);
56
56
  const navData = await getNavigationData(c);
57
57
  const title = post.title || navData.siteName;
58
- const components = c.var.config.theme?.components;
59
- const PostPage = components?.PostPage ?? DefaultPostPage;
60
58
  return renderPublicPage(c, {
61
59
  title,
62
60
  description: post.body?.slice(0, 160),
63
61
  navData,
64
62
  content: /*#__PURE__*/ _jsx(PostPage, {
65
- post: postView,
66
- theme: components
63
+ post: postView
67
64
  })
68
65
  });
69
66
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Single Post Page Route
4
4
  */ import { Hono } from "hono";
5
- import { PostPage as DefaultPostPage } from "../../themes/threads/pages/PostPage.js";
5
+ import { PostPage } from "../../ui/pages/PostPage.js";
6
6
  import * as sqid from "../../lib/sqid.js";
7
7
  import { getNavigationData } from "../../lib/navigation.js";
8
8
  import { renderPublicPage } from "../../lib/render.js";
@@ -33,15 +33,12 @@ postRoutes.get("/:id", async (c)=>{
33
33
  }, mediaCtx);
34
34
  const navData = await getNavigationData(c);
35
35
  const title = post.title || navData.siteName;
36
- const components = c.var.config.theme?.components;
37
- const Page = components?.PostPage ?? DefaultPostPage;
38
36
  return renderPublicPage(c, {
39
37
  title,
40
38
  description: post.body?.slice(0, 160),
41
39
  navData,
42
- content: /*#__PURE__*/ _jsx(Page, {
43
- post: postView,
44
- theme: components
40
+ content: /*#__PURE__*/ _jsx(PostPage, {
41
+ post: postView
45
42
  })
46
43
  });
47
44
  });
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Search Page Route
4
4
  */ import { Hono } from "hono";
5
- import { SearchPage as DefaultSearchPage } from "../../themes/threads/pages/SearchPage.js";
5
+ import { SearchPage } from "../../ui/pages/SearchPage.js";
6
6
  import { getNavigationData } from "../../lib/navigation.js";
7
7
  import { renderPublicPage } from "../../lib/render.js";
8
8
  import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
@@ -40,18 +40,15 @@ searchRoutes.get("/", async (c)=>{
40
40
  // Transform to View Models
41
41
  const mediaCtx = createMediaContext(c);
42
42
  const resultViews = toSearchResultViews(results, mediaCtx);
43
- const components = c.var.config.theme?.components;
44
- const Page = components?.SearchPage ?? DefaultSearchPage;
45
43
  return renderPublicPage(c, {
46
44
  title: query ? `Search: ${query} - ${navData.siteName}` : `Search - ${navData.siteName}`,
47
45
  navData,
48
- content: /*#__PURE__*/ _jsx(Page, {
46
+ content: /*#__PURE__*/ _jsx(SearchPage, {
49
47
  query: query,
50
48
  results: resultViews,
51
49
  error: error,
52
50
  hasMore: hasMore,
53
- page: page,
54
- theme: components
51
+ page: page
55
52
  })
56
53
  });
57
54
  });
@@ -2,7 +2,7 @@
2
2
  * Page Service
3
3
  *
4
4
  * CRUD operations for standalone pages (about, now, etc.)
5
- */ import { eq, desc } from "drizzle-orm";
5
+ */ import { eq, desc, sql } from "drizzle-orm";
6
6
  import { pages, navItems } from "../db/schema.js";
7
7
  import { now } from "../lib/time.js";
8
8
  import { render as renderMarkdown } from "../lib/markdown.js";
@@ -32,6 +32,10 @@ export function createPageService(db) {
32
32
  const rows = await db.select().from(pages).orderBy(desc(pages.createdAt));
33
33
  return rows.map(toPage);
34
34
  },
35
+ async listNotInNav () {
36
+ const rows = await db.select().from(pages).where(sql`${pages.id} NOT IN (SELECT ${navItems.pageId} FROM ${navItems} WHERE ${navItems.pageId} IS NOT NULL)`).orderBy(desc(pages.createdAt));
37
+ return rows.map(toPage);
38
+ },
35
39
  async create (data) {
36
40
  const timestamp = now();
37
41
  const bodyHtml = data.body ? renderMarkdown(data.body) : null;
@@ -16,7 +16,7 @@ export function createPostService(db) {
16
16
  status: row.status,
17
17
  featured: row.featured,
18
18
  pinned: row.pinned,
19
- slug: row.slug,
19
+ path: row.path,
20
20
  title: row.title,
21
21
  url: row.url,
22
22
  body: row.body,
@@ -37,8 +37,8 @@ export function createPostService(db) {
37
37
  const result = await db.select().from(posts).where(and(eq(posts.id, id), isNull(posts.deletedAt))).limit(1);
38
38
  return result[0] ? toPost(result[0]) : null;
39
39
  },
40
- async getBySlug (slug) {
41
- const result = await db.select().from(posts).where(and(eq(posts.slug, slug), isNull(posts.deletedAt))).limit(1);
40
+ async getByPath (path) {
41
+ const result = await db.select().from(posts).where(and(eq(posts.path, path), isNull(posts.deletedAt))).limit(1);
42
42
  return result[0] ? toPost(result[0]) : null;
43
43
  },
44
44
  async list (filters = {}) {
@@ -70,10 +70,44 @@ export function createPostService(db) {
70
70
  if (filters.cursor) {
71
71
  conditions.push(sql`${posts.id} < ${filters.cursor}`);
72
72
  }
73
- const query = db.select().from(posts).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(desc(posts.publishedAt), desc(posts.id)).limit(filters.limit ?? 100);
73
+ let query = db.select().from(posts).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(desc(posts.publishedAt), desc(posts.id)).limit(filters.limit ?? 100);
74
+ if (filters.offset !== undefined) {
75
+ query = query.offset(filters.offset);
76
+ }
74
77
  const rows = await query;
75
78
  return rows.map(toPost);
76
79
  },
80
+ async count (filters = {}) {
81
+ const conditions = [];
82
+ if (filters.status) {
83
+ conditions.push(eq(posts.status, filters.status));
84
+ }
85
+ if (filters.featured !== undefined) {
86
+ conditions.push(eq(posts.featured, filters.featured ? 1 : 0));
87
+ }
88
+ if (filters.pinned !== undefined) {
89
+ conditions.push(eq(posts.pinned, filters.pinned ? 1 : 0));
90
+ }
91
+ if (filters.format) {
92
+ conditions.push(eq(posts.format, filters.format));
93
+ }
94
+ if (filters.collectionId !== undefined) {
95
+ conditions.push(eq(posts.collectionId, filters.collectionId));
96
+ }
97
+ if (filters.threadId) {
98
+ conditions.push(eq(posts.threadId, filters.threadId));
99
+ }
100
+ if (filters.excludeReplies) {
101
+ conditions.push(isNull(posts.threadId));
102
+ }
103
+ if (!filters.includeDeleted) {
104
+ conditions.push(isNull(posts.deletedAt));
105
+ }
106
+ const result = await db.select({
107
+ count: sql`count(*)`.as("count")
108
+ }).from(posts).where(conditions.length > 0 ? and(...conditions) : undefined);
109
+ return result[0]?.count ?? 0;
110
+ },
77
111
  async create (data) {
78
112
  const timestamp = now();
79
113
  const bodyHtml = data.body ? renderMarkdown(data.body) : null;
@@ -98,7 +132,7 @@ export function createPostService(db) {
98
132
  status,
99
133
  featured: featured ? 1 : 0,
100
134
  pinned: data.pinned ? 1 : 0,
101
- slug: data.slug ?? null,
135
+ path: data.path ?? null,
102
136
  title: data.title ?? null,
103
137
  url: data.url ?? null,
104
138
  body: data.body ?? null,
@@ -123,7 +157,7 @@ export function createPostService(db) {
123
157
  updatedAt: timestamp
124
158
  };
125
159
  if (data.format !== undefined) updates.format = data.format;
126
- if (data.slug !== undefined) updates.slug = data.slug;
160
+ if (data.path !== undefined) updates.path = data.path;
127
161
  if (data.title !== undefined) updates.title = data.title;
128
162
  if (data.url !== undefined) updates.url = data.url;
129
163
  if (data.quoteText !== undefined) updates.quoteText = data.quoteText;
@@ -44,7 +44,7 @@
44
44
  status: row.status,
45
45
  featured: row.featured,
46
46
  pinned: row.pinned,
47
- slug: row.slug,
47
+ path: row.path,
48
48
  title: row.title,
49
49
  url: row.url,
50
50
  body: row.body,