@jant/core 0.3.23 → 0.3.24

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 (169) hide show
  1. package/dist/app.js +4 -5
  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 +3 -3
  7. package/dist/lib/constants.js +1 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/navigation.js +4 -5
  11. package/dist/lib/render.js +1 -1
  12. package/dist/lib/schemas.js +80 -38
  13. package/dist/lib/theme-components.js +8 -11
  14. package/dist/lib/time.js +56 -1
  15. package/dist/lib/timeline.js +119 -0
  16. package/dist/lib/view.js +61 -72
  17. package/dist/routes/api/posts.js +29 -35
  18. package/dist/routes/api/search.js +5 -6
  19. package/dist/routes/api/upload.js +13 -13
  20. package/dist/routes/dash/collections.js +22 -40
  21. package/dist/routes/dash/index.js +2 -2
  22. package/dist/routes/dash/navigation.js +25 -24
  23. package/dist/routes/dash/pages.js +42 -57
  24. package/dist/routes/dash/posts.js +27 -35
  25. package/dist/routes/feed/rss.js +2 -4
  26. package/dist/routes/feed/sitemap.js +10 -7
  27. package/dist/routes/pages/archive.js +12 -11
  28. package/dist/routes/pages/collection.js +11 -5
  29. package/dist/routes/pages/home.js +53 -61
  30. package/dist/routes/pages/page.js +60 -29
  31. package/dist/routes/pages/post.js +5 -12
  32. package/dist/routes/pages/search.js +3 -4
  33. package/dist/services/collection.js +52 -64
  34. package/dist/services/index.js +5 -3
  35. package/dist/services/navigation.js +29 -53
  36. package/dist/services/page.js +80 -0
  37. package/dist/services/post.js +68 -69
  38. package/dist/services/search.js +24 -18
  39. package/dist/theme/components/MediaGallery.js +19 -91
  40. package/dist/theme/components/PageForm.js +15 -15
  41. package/dist/theme/components/PostForm.js +136 -129
  42. package/dist/theme/components/PostList.js +13 -8
  43. package/dist/theme/components/ThreadView.js +3 -3
  44. package/dist/theme/components/TypeBadge.js +3 -14
  45. package/dist/theme/components/VisibilityBadge.js +33 -23
  46. package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
  47. package/dist/themes/threads/index.js +81 -0
  48. package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
  49. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  50. package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
  51. package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
  52. package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
  53. package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
  54. package/dist/themes/threads/timeline/LinkCard.js +68 -0
  55. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  56. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  57. package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
  58. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  59. package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
  60. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  61. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  62. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  63. package/dist/types.js +24 -40
  64. package/package.json +2 -1
  65. package/src/__tests__/helpers/app.ts +4 -0
  66. package/src/__tests__/helpers/db.ts +51 -74
  67. package/src/app.tsx +4 -6
  68. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  69. package/src/db/migrations/meta/_journal.json +7 -0
  70. package/src/db/schema.ts +63 -46
  71. package/src/i18n/locales/en.po +216 -164
  72. package/src/i18n/locales/en.ts +1 -1
  73. package/src/i18n/locales/zh-Hans.po +216 -164
  74. package/src/i18n/locales/zh-Hans.ts +1 -1
  75. package/src/i18n/locales/zh-Hant.po +216 -164
  76. package/src/i18n/locales/zh-Hant.ts +1 -1
  77. package/src/index.ts +28 -12
  78. package/src/lib/__tests__/excerpt.test.ts +125 -0
  79. package/src/lib/__tests__/schemas.test.ts +166 -105
  80. package/src/lib/__tests__/theme-components.test.ts +4 -25
  81. package/src/lib/__tests__/time.test.ts +62 -0
  82. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  83. package/src/lib/__tests__/view.test.ts +199 -51
  84. package/src/lib/constants.ts +1 -4
  85. package/src/lib/excerpt.ts +87 -0
  86. package/src/lib/feed.ts +22 -7
  87. package/src/lib/navigation.ts +6 -7
  88. package/src/lib/render.tsx +1 -1
  89. package/src/lib/schemas.ts +118 -52
  90. package/src/lib/theme-components.ts +10 -13
  91. package/src/lib/time.ts +64 -0
  92. package/src/lib/timeline.ts +170 -0
  93. package/src/lib/view.ts +80 -82
  94. package/src/preset.css +45 -0
  95. package/src/routes/api/__tests__/posts.test.ts +50 -108
  96. package/src/routes/api/__tests__/search.test.ts +2 -3
  97. package/src/routes/api/posts.ts +30 -30
  98. package/src/routes/api/search.ts +4 -4
  99. package/src/routes/api/upload.ts +16 -6
  100. package/src/routes/dash/collections.tsx +18 -40
  101. package/src/routes/dash/index.tsx +2 -2
  102. package/src/routes/dash/navigation.tsx +27 -26
  103. package/src/routes/dash/pages.tsx +45 -60
  104. package/src/routes/dash/posts.tsx +44 -52
  105. package/src/routes/feed/rss.ts +2 -1
  106. package/src/routes/feed/sitemap.ts +14 -4
  107. package/src/routes/pages/archive.tsx +14 -10
  108. package/src/routes/pages/collection.tsx +17 -6
  109. package/src/routes/pages/home.tsx +56 -81
  110. package/src/routes/pages/page.tsx +64 -27
  111. package/src/routes/pages/post.tsx +5 -14
  112. package/src/routes/pages/search.tsx +2 -2
  113. package/src/services/__tests__/collection.test.ts +257 -158
  114. package/src/services/__tests__/media.test.ts +18 -18
  115. package/src/services/__tests__/navigation.test.ts +161 -87
  116. package/src/services/__tests__/post-timeline.test.ts +92 -88
  117. package/src/services/__tests__/post.test.ts +342 -206
  118. package/src/services/__tests__/search.test.ts +19 -25
  119. package/src/services/collection.ts +71 -113
  120. package/src/services/index.ts +9 -8
  121. package/src/services/navigation.ts +38 -71
  122. package/src/services/page.ts +124 -0
  123. package/src/services/post.ts +93 -103
  124. package/src/services/search.ts +38 -27
  125. package/src/theme/components/MediaGallery.tsx +27 -96
  126. package/src/theme/components/PageForm.tsx +21 -21
  127. package/src/theme/components/PostForm.tsx +122 -118
  128. package/src/theme/components/PostList.tsx +58 -49
  129. package/src/theme/components/ThreadView.tsx +6 -3
  130. package/src/theme/components/TypeBadge.tsx +9 -17
  131. package/src/theme/components/VisibilityBadge.tsx +40 -23
  132. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  133. package/src/themes/{minimal → threads}/index.ts +30 -13
  134. package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
  135. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  136. package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
  137. package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
  138. package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
  139. package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
  140. package/src/themes/threads/style.css +336 -0
  141. package/src/themes/threads/timeline/LinkCard.tsx +67 -0
  142. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  143. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  144. package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
  145. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  146. package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
  147. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  148. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  149. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  150. package/src/types.ts +242 -98
  151. package/dist/routes/api/timeline.js +0 -120
  152. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  153. package/dist/themes/minimal/index.js +0 -65
  154. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  155. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  156. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  157. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  158. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  159. package/dist/themes/minimal/timeline/QuoteCard.js +0 -48
  160. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  161. package/src/routes/api/timeline.tsx +0 -159
  162. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  163. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  164. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  165. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  166. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  167. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  168. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  169. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
@@ -1,83 +0,0 @@
1
- /**
2
- * Minimal Theme - Site Layout
3
- *
4
- * Single-column, centered layout with horizontal nav.
5
- * Inspired by Tufte CSS and Manton.org.
6
- */ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
7
- function NavLinks({ links }) {
8
- return /*#__PURE__*/ _jsx(_Fragment, {
9
- children: links.map((link)=>/*#__PURE__*/ _jsxs("a", {
10
- href: link.url,
11
- class: `text-sm ${link.isActive ? "text-foreground font-medium" : "text-muted-foreground hover:text-foreground"}`,
12
- ...link.isExternal ? {
13
- target: "_blank",
14
- rel: "noopener noreferrer"
15
- } : {},
16
- children: [
17
- link.label,
18
- link.isExternal && /*#__PURE__*/ _jsx("span", {
19
- class: "ml-0.5 text-xs opacity-50",
20
- children: "↗"
21
- })
22
- ]
23
- }, link.id))
24
- });
25
- }
26
- export const SiteLayout = ({ siteName, links, children })=>{
27
- return /*#__PURE__*/ _jsxs("div", {
28
- class: "max-w-2xl mx-auto px-4 py-8",
29
- "data-signals": JSON.stringify({
30
- _menuOpen: false
31
- }),
32
- children: [
33
- /*#__PURE__*/ _jsxs("header", {
34
- class: "mb-12",
35
- children: [
36
- /*#__PURE__*/ _jsxs("div", {
37
- class: "flex items-center justify-between",
38
- children: [
39
- /*#__PURE__*/ _jsx("a", {
40
- href: "/",
41
- class: "text-xl font-semibold",
42
- children: siteName
43
- }),
44
- links.length > 0 && /*#__PURE__*/ _jsx("button", {
45
- "data-on:click": "$_menuOpen = !$_menuOpen",
46
- class: "p-2 -mr-2 text-muted-foreground hover:text-foreground sm:hidden",
47
- "aria-label": "Toggle menu",
48
- children: /*#__PURE__*/ _jsx("svg", {
49
- class: "size-5",
50
- fill: "none",
51
- viewBox: "0 0 24 24",
52
- "stroke-width": "1.5",
53
- stroke: "currentColor",
54
- children: /*#__PURE__*/ _jsx("path", {
55
- "stroke-linecap": "round",
56
- "stroke-linejoin": "round",
57
- d: "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
58
- })
59
- })
60
- })
61
- ]
62
- }),
63
- links.length > 0 && /*#__PURE__*/ _jsx("nav", {
64
- class: "hidden sm:flex flex-wrap gap-x-4 gap-y-1 mt-3",
65
- children: /*#__PURE__*/ _jsx(NavLinks, {
66
- links: links
67
- })
68
- }),
69
- links.length > 0 && /*#__PURE__*/ _jsx("nav", {
70
- class: "sm:hidden flex flex-col gap-1 mt-3 overflow-hidden",
71
- "data-show": "$_menuOpen",
72
- children: /*#__PURE__*/ _jsx(NavLinks, {
73
- links: links
74
- })
75
- })
76
- ]
77
- }),
78
- /*#__PURE__*/ _jsx("main", {
79
- children: children
80
- })
81
- ]
82
- });
83
- };
@@ -1,65 +0,0 @@
1
- /**
2
- * Minimal Theme
3
- *
4
- * A content-first, borderless theme inspired by Tufte CSS and Manton.org.
5
- * Single-column layout with serif-friendly typography and generous whitespace.
6
- *
7
- * This is the default theme for Jant.
8
- */ // Layout
9
- import { SiteLayout } from "./MinimalSiteLayout.js";
10
- // Pages
11
- import { HomePage } from "./pages/HomePage.js";
12
- import { PostPage } from "./pages/PostPage.js";
13
- import { SinglePage } from "./pages/SinglePage.js";
14
- import { ArchivePage } from "./pages/ArchivePage.js";
15
- import { SearchPage } from "./pages/SearchPage.js";
16
- import { CollectionPage } from "./pages/CollectionPage.js";
17
- // Timeline
18
- import { NoteCard } from "./timeline/NoteCard.js";
19
- import { ArticleCard } from "./timeline/ArticleCard.js";
20
- import { LinkCard } from "./timeline/LinkCard.js";
21
- import { QuoteCard } from "./timeline/QuoteCard.js";
22
- import { ImageCard } from "./timeline/ImageCard.js";
23
- import { ThreadPreview } from "./timeline/ThreadPreview.js";
24
- import { TimelineFeed } from "./timeline/TimelineFeed.js";
25
- /**
26
- * Create the minimal theme configuration.
27
- *
28
- * @param options - Optional overrides for components, CSS variables, or color themes
29
- * @returns A JantTheme configuration object
30
- *
31
- * @example
32
- * ```typescript
33
- * import { createApp } from "@jant/core";
34
- * import { minimalTheme } from "@jant/core";
35
- *
36
- * export default createApp({
37
- * theme: minimalTheme(), // re-exported as minimalTheme from @jant/core
38
- * });
39
- * ```
40
- */ export function theme(options) {
41
- return {
42
- name: "minimal",
43
- components: {
44
- SiteLayout,
45
- HomePage,
46
- PostPage,
47
- SinglePage,
48
- ArchivePage,
49
- SearchPage,
50
- CollectionPage,
51
- NoteCard,
52
- ArticleCard,
53
- LinkCard,
54
- QuoteCard,
55
- ImageCard,
56
- ThreadPreview,
57
- TimelineFeed,
58
- ...options?.components
59
- },
60
- cssVariables: {
61
- ...options?.cssVariables
62
- },
63
- colorThemes: options?.colorThemes
64
- };
65
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * Minimal Theme - Collection Page
3
- *
4
- * Simple list of posts in a collection.
5
- */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
- import { useLingui as $_useLingui } from "@jant/core/i18n";
7
- export const CollectionPage = ({ collection, posts })=>{
8
- const { i18n: $__i18n, _: $__ } = $_useLingui();
9
- return /*#__PURE__*/ _jsxs("div", {
10
- children: [
11
- /*#__PURE__*/ _jsxs("header", {
12
- class: "mb-8",
13
- children: [
14
- /*#__PURE__*/ _jsx("h1", {
15
- class: "text-2xl font-semibold",
16
- children: collection.title
17
- }),
18
- collection.description && /*#__PURE__*/ _jsx("p", {
19
- class: "text-muted-foreground mt-2",
20
- children: collection.description
21
- })
22
- ]
23
- }),
24
- /*#__PURE__*/ _jsx("main", {
25
- class: "flex flex-col",
26
- children: posts.length === 0 ? /*#__PURE__*/ _jsx("p", {
27
- class: "text-muted-foreground",
28
- children: $__i18n._({
29
- id: "J4FNfC",
30
- message: "No posts in this collection."
31
- })
32
- }) : posts.map((post, i)=>/*#__PURE__*/ _jsxs("article", {
33
- class: "h-entry",
34
- children: [
35
- i > 0 && /*#__PURE__*/ _jsx("hr", {
36
- class: "my-6 border-border"
37
- }),
38
- post.title && /*#__PURE__*/ _jsx("h2", {
39
- class: "p-name text-lg font-medium mb-2",
40
- children: /*#__PURE__*/ _jsx("a", {
41
- href: post.permalink,
42
- class: "u-url hover:underline",
43
- children: post.title
44
- })
45
- }),
46
- /*#__PURE__*/ _jsx("div", {
47
- class: "e-content prose prose-sm",
48
- dangerouslySetInnerHTML: {
49
- __html: post.contentHtml || ""
50
- }
51
- }),
52
- /*#__PURE__*/ _jsx("footer", {
53
- class: "mt-2 text-sm text-muted-foreground",
54
- children: /*#__PURE__*/ _jsx("time", {
55
- class: "dt-published",
56
- datetime: post.publishedAt,
57
- children: post.publishedAtFormatted
58
- })
59
- })
60
- ]
61
- }, post.id))
62
- })
63
- ]
64
- });
65
- };
@@ -1,36 +0,0 @@
1
- /**
2
- * Minimal Theme - Article Card
3
- *
4
- * Title + excerpt, borderless, for type="article" posts.
5
- */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
- export const ArticleCard = ({ post, compact })=>{
7
- return /*#__PURE__*/ _jsxs("article", {
8
- class: `h-entry${compact ? " text-sm" : ""}`,
9
- children: [
10
- post.title && /*#__PURE__*/ _jsx("h2", {
11
- class: `p-name font-semibold ${compact ? "text-sm" : "text-lg"}`,
12
- children: /*#__PURE__*/ _jsx("a", {
13
- href: post.permalink,
14
- class: "u-url hover:underline",
15
- children: post.title
16
- })
17
- }),
18
- !compact && post.excerpt && /*#__PURE__*/ _jsx("p", {
19
- class: "e-content text-muted-foreground mt-1 line-clamp-3",
20
- children: post.excerpt
21
- }),
22
- /*#__PURE__*/ _jsx("footer", {
23
- class: "mt-2",
24
- children: /*#__PURE__*/ _jsx("a", {
25
- href: post.permalink,
26
- class: "u-url text-xs text-muted-foreground hover:text-foreground",
27
- children: /*#__PURE__*/ _jsx("time", {
28
- class: "dt-published",
29
- datetime: post.publishedAt,
30
- children: post.publishedAtFormatted
31
- })
32
- })
33
- })
34
- ]
35
- });
36
- };
@@ -1,67 +0,0 @@
1
- /**
2
- * Minimal Theme - Image Card
3
- *
4
- * Inline images with no card frame for type="image" posts.
5
- */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
- import { MediaGallery } from "../../../theme/components/MediaGallery.js";
7
- export const ImageCard = ({ post, compact })=>{
8
- if (compact) {
9
- return /*#__PURE__*/ _jsxs("article", {
10
- class: "h-entry text-sm",
11
- children: [
12
- post.title && /*#__PURE__*/ _jsx("h2", {
13
- class: "p-name font-medium text-sm",
14
- children: /*#__PURE__*/ _jsx("a", {
15
- href: post.permalink,
16
- class: "u-url hover:underline",
17
- children: post.title
18
- })
19
- }),
20
- post.contentHtml && /*#__PURE__*/ _jsx("div", {
21
- class: "e-content prose prose-sm text-muted-foreground",
22
- dangerouslySetInnerHTML: {
23
- __html: post.contentHtml
24
- }
25
- }),
26
- /*#__PURE__*/ _jsx("footer", {
27
- class: "mt-1",
28
- children: /*#__PURE__*/ _jsx("a", {
29
- href: post.permalink,
30
- class: "u-url text-xs text-muted-foreground hover:text-foreground",
31
- children: /*#__PURE__*/ _jsx("time", {
32
- class: "dt-published",
33
- datetime: post.publishedAt,
34
- children: post.publishedAtFormatted
35
- })
36
- })
37
- })
38
- ]
39
- });
40
- }
41
- return /*#__PURE__*/ _jsxs("article", {
42
- class: "h-entry",
43
- children: [
44
- post.contentHtml && /*#__PURE__*/ _jsx("div", {
45
- class: "e-content prose prose-sm",
46
- dangerouslySetInnerHTML: {
47
- __html: post.contentHtml
48
- }
49
- }),
50
- post.media.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
51
- attachments: post.media
52
- }),
53
- /*#__PURE__*/ _jsx("footer", {
54
- class: "mt-2",
55
- children: /*#__PURE__*/ _jsx("a", {
56
- href: post.permalink,
57
- class: "u-url text-xs text-muted-foreground hover:text-foreground",
58
- children: /*#__PURE__*/ _jsx("time", {
59
- class: "dt-published",
60
- datetime: post.publishedAt,
61
- children: post.publishedAtFormatted
62
- })
63
- })
64
- })
65
- ]
66
- });
67
- };
@@ -1,47 +0,0 @@
1
- /**
2
- * Minimal Theme - Link Card
3
- *
4
- * Subtle external link indicator for type="link" posts.
5
- */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
- export const LinkCard = ({ post, compact })=>{
7
- return /*#__PURE__*/ _jsxs("article", {
8
- class: `h-entry${compact ? " text-sm" : ""}`,
9
- children: [
10
- post.title && /*#__PURE__*/ _jsx("h2", {
11
- class: `p-name font-semibold ${compact ? "text-sm" : "text-base"}`,
12
- children: /*#__PURE__*/ _jsx("a", {
13
- href: post.sourceUrl || post.permalink,
14
- class: "u-url hover:underline",
15
- target: post.sourceUrl ? "_blank" : undefined,
16
- rel: post.sourceUrl ? "noopener noreferrer" : undefined,
17
- children: post.title
18
- })
19
- }),
20
- post.sourceDomain && /*#__PURE__*/ _jsxs("div", {
21
- class: "text-xs text-muted-foreground mt-0.5",
22
- children: [
23
- "↗ ",
24
- post.sourceDomain
25
- ]
26
- }),
27
- !compact && post.contentHtml && /*#__PURE__*/ _jsx("div", {
28
- class: "e-content prose prose-sm text-muted-foreground mt-1",
29
- dangerouslySetInnerHTML: {
30
- __html: post.contentHtml
31
- }
32
- }),
33
- /*#__PURE__*/ _jsx("footer", {
34
- class: "mt-2",
35
- children: /*#__PURE__*/ _jsx("a", {
36
- href: post.permalink,
37
- class: "text-xs text-muted-foreground hover:text-foreground",
38
- children: /*#__PURE__*/ _jsx("time", {
39
- class: "dt-published",
40
- datetime: post.publishedAt,
41
- children: post.publishedAtFormatted
42
- })
43
- })
44
- })
45
- ]
46
- });
47
- };
@@ -1,34 +0,0 @@
1
- /**
2
- * Minimal Theme - Note Card
3
- *
4
- * Borderless, content-first card for type="note" posts.
5
- */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
- import { MediaGallery } from "../../../theme/components/MediaGallery.js";
7
- export const NoteCard = ({ post, compact })=>{
8
- return /*#__PURE__*/ _jsxs("article", {
9
- class: `h-entry${compact ? " text-sm" : ""}`,
10
- children: [
11
- post.contentHtml && /*#__PURE__*/ _jsx("div", {
12
- class: `e-content prose ${compact ? "prose-sm" : ""}`,
13
- dangerouslySetInnerHTML: {
14
- __html: post.contentHtml
15
- }
16
- }),
17
- !compact && post.media.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
18
- attachments: post.media
19
- }),
20
- /*#__PURE__*/ _jsx("footer", {
21
- class: "mt-2",
22
- children: /*#__PURE__*/ _jsx("a", {
23
- href: post.permalink,
24
- class: "u-url text-xs text-muted-foreground hover:text-foreground",
25
- children: /*#__PURE__*/ _jsx("time", {
26
- class: "dt-published",
27
- datetime: post.publishedAt,
28
- children: post.publishedAtFormatted
29
- })
30
- })
31
- })
32
- ]
33
- });
34
- };
@@ -1,48 +0,0 @@
1
- /**
2
- * Minimal Theme - Quote Card
3
- *
4
- * Subtle blockquote with left border for type="quote" posts.
5
- */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
- export const QuoteCard = ({ post, compact })=>{
7
- return /*#__PURE__*/ _jsxs("article", {
8
- class: `h-entry${compact ? " text-sm" : ""}`,
9
- children: [
10
- post.contentHtml && /*#__PURE__*/ _jsx("blockquote", {
11
- class: `e-content border-l-2 border-muted-foreground/30 pl-4 italic ${compact ? "text-sm" : ""} leading-relaxed`,
12
- children: /*#__PURE__*/ _jsx("div", {
13
- dangerouslySetInnerHTML: {
14
- __html: post.contentHtml
15
- }
16
- })
17
- }),
18
- !compact && (post.sourceName || post.sourceUrl) && /*#__PURE__*/ _jsxs("div", {
19
- class: "mt-2 text-sm text-muted-foreground",
20
- children: [
21
- "—",
22
- " ",
23
- post.sourceUrl ? /*#__PURE__*/ _jsx("a", {
24
- href: post.sourceUrl,
25
- class: "hover:underline",
26
- target: "_blank",
27
- rel: "noopener noreferrer",
28
- children: post.sourceName || post.sourceDomain || "Source"
29
- }) : /*#__PURE__*/ _jsx("span", {
30
- children: post.sourceName
31
- })
32
- ]
33
- }),
34
- /*#__PURE__*/ _jsx("footer", {
35
- class: "mt-2",
36
- children: /*#__PURE__*/ _jsx("a", {
37
- href: post.permalink,
38
- class: "u-url text-xs text-muted-foreground hover:text-foreground",
39
- children: /*#__PURE__*/ _jsx("time", {
40
- class: "dt-published",
41
- datetime: post.publishedAt,
42
- children: post.publishedAtFormatted
43
- })
44
- })
45
- })
46
- ]
47
- });
48
- };
@@ -1,48 +0,0 @@
1
- /**
2
- * Minimal Theme - Timeline Feed
3
- *
4
- * Divider-separated stream of posts with load-more button.
5
- */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
- import { useLingui as $_useLingui } from "@jant/core/i18n";
7
- import { TimelineItem } from "./TimelineItem.js";
8
- import { ThreadPreview as DefaultThreadPreview } from "./ThreadPreview.js";
9
- export const TimelineFeed = ({ items, hasMore, nextCursor, theme })=>{
10
- const { i18n: $__i18n, _: $__ } = $_useLingui();
11
- const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
12
- return /*#__PURE__*/ _jsxs("div", {
13
- children: [
14
- /*#__PURE__*/ _jsx("div", {
15
- id: "timeline-feed",
16
- class: "flex flex-col",
17
- children: items.map((item, i)=>/*#__PURE__*/ _jsxs("div", {
18
- children: [
19
- i > 0 && /*#__PURE__*/ _jsx("hr", {
20
- class: "my-6 border-border"
21
- }),
22
- item.threadPreview ? /*#__PURE__*/ _jsx(ResolvedThreadPreview, {
23
- rootPost: item.post,
24
- previewReplies: item.threadPreview.replies,
25
- totalReplyCount: item.threadPreview.totalReplyCount,
26
- theme: theme
27
- }) : /*#__PURE__*/ _jsx(TimelineItem, {
28
- item: item,
29
- theme: theme
30
- })
31
- ]
32
- }, item.post.id))
33
- }),
34
- hasMore && nextCursor && /*#__PURE__*/ _jsx("div", {
35
- id: "load-more-container",
36
- class: "mt-8 text-center",
37
- children: /*#__PURE__*/ _jsx("button", {
38
- class: "text-sm text-muted-foreground hover:text-foreground hover:underline",
39
- "data-on:click": `@get('/api/timeline?cursor=${nextCursor}')`,
40
- children: $__i18n._({
41
- id: "yQ2kGp",
42
- message: "Load more"
43
- })
44
- })
45
- })
46
- ]
47
- });
48
- };
@@ -1,159 +0,0 @@
1
- /**
2
- * Timeline API Routes
3
- *
4
- * Provides load-more functionality for the timeline feed via SSE.
5
- */
6
-
7
- import { Hono } from "hono";
8
- import type { Bindings, TimelineItemView } from "../../types.js";
9
- import type { AppVariables } from "../../app.js";
10
- import { sse } from "../../lib/sse.js";
11
- import { buildMediaMap } from "../../lib/media-helpers.js";
12
- import { TimelineItem } from "../../themes/minimal/timeline/TimelineItem.js";
13
- import { ThreadPreview as DefaultThreadPreview } from "../../themes/minimal/timeline/ThreadPreview.js";
14
- import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
15
-
16
- type Env = { Bindings: Bindings; Variables: AppVariables };
17
-
18
- const PAGE_SIZE = 20;
19
-
20
- export const timelineApiRoutes = new Hono<Env>();
21
-
22
- timelineApiRoutes.get("/", async (c) => {
23
- const cursorParam = c.req.query("cursor");
24
- const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
25
-
26
- if (!cursor || isNaN(cursor)) {
27
- return c.json({ error: "cursor parameter required" }, 400);
28
- }
29
-
30
- // Fetch one extra to determine if there are more
31
- const posts = await c.var.services.posts.list({
32
- visibility: ["featured", "quiet"],
33
- excludeReplies: true,
34
- excludeTypes: ["page"],
35
- limit: PAGE_SIZE + 1,
36
- cursor,
37
- });
38
-
39
- const hasMore = posts.length > PAGE_SIZE;
40
- const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
41
-
42
- if (displayPosts.length === 0) {
43
- return sse(c, async (stream) => {
44
- stream.remove("#load-more-container");
45
- });
46
- }
47
-
48
- // Build media map
49
- const postIds = displayPosts.map((p) => p.id);
50
- const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
51
- const mediaCtx = createMediaContext(c);
52
- const mediaMap = buildMediaMap(
53
- rawMediaMap,
54
- mediaCtx.r2PublicUrl,
55
- mediaCtx.imageTransformUrl,
56
- mediaCtx.s3PublicUrl,
57
- );
58
-
59
- // Get reply counts to identify thread roots
60
- const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
61
- const threadRootIds = postIds.filter((id) => (replyCounts.get(id) ?? 0) > 0);
62
-
63
- // Get thread previews
64
- const threadPreviews = await c.var.services.posts.getThreadPreviews(
65
- threadRootIds,
66
- 3,
67
- );
68
-
69
- // Load media for preview replies
70
- const previewReplyIds: number[] = [];
71
- for (const replies of threadPreviews.values()) {
72
- for (const reply of replies) {
73
- previewReplyIds.push(reply.id);
74
- }
75
- }
76
- const previewMediaMap =
77
- previewReplyIds.length > 0
78
- ? buildMediaMap(
79
- await c.var.services.media.getByPostIds(previewReplyIds),
80
- mediaCtx.r2PublicUrl,
81
- mediaCtx.imageTransformUrl,
82
- mediaCtx.s3PublicUrl,
83
- )
84
- : new Map();
85
-
86
- // Assemble timeline items with View Models
87
- const items: TimelineItemView[] = displayPosts.map((post) => {
88
- const postView = toPostView(
89
- { ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
90
- mediaCtx,
91
- );
92
-
93
- const replyCount = replyCounts.get(post.id) ?? 0;
94
- const previewReplies = threadPreviews.get(post.id);
95
-
96
- if (replyCount > 0 && previewReplies) {
97
- return {
98
- post: postView,
99
- threadPreview: {
100
- replies: toPostViews(
101
- previewReplies.map((r) => ({
102
- ...r,
103
- mediaAttachments: previewMediaMap.get(r.id) ?? [],
104
- })),
105
- mediaCtx,
106
- ),
107
- totalReplyCount: replyCount,
108
- },
109
- };
110
- }
111
-
112
- return { post: postView };
113
- });
114
-
115
- // Resolve theme components for card rendering
116
- const theme = c.var.config.theme?.components;
117
- const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
118
-
119
- // Render items to HTML
120
- const itemsHtml = items
121
- .map((item) => {
122
- if (item.threadPreview) {
123
- return (
124
- <ResolvedThreadPreview
125
- rootPost={item.post}
126
- previewReplies={item.threadPreview.replies}
127
- totalReplyCount={item.threadPreview.totalReplyCount}
128
- theme={theme}
129
- />
130
- );
131
- }
132
- return <TimelineItem item={item} theme={theme} />;
133
- })
134
- .map((jsx) => jsx.toString())
135
- .join("");
136
-
137
- // Determine next cursor
138
- const lastPost = displayPosts[displayPosts.length - 1];
139
- const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
140
-
141
- // Build load-more button HTML
142
- const loadMoreHtml = nextCursor
143
- ? `<div id="load-more-container" class="mt-8 text-center"><button class="text-sm text-muted-foreground hover:text-foreground hover:underline" data-on:click="@get('/api/timeline?cursor=${nextCursor}')">Load more</button></div>`
144
- : "";
145
-
146
- return sse(c, async (stream) => {
147
- // Append new items to the feed
148
- stream.patchElements(itemsHtml, {
149
- mode: "append",
150
- selector: "#timeline-feed",
151
- });
152
- // Replace or remove the load-more container
153
- if (loadMoreHtml) {
154
- stream.patchElements(loadMoreHtml);
155
- } else {
156
- stream.remove("#load-more-container");
157
- }
158
- });
159
- });