@jant/core 0.3.22 → 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 (178) hide show
  1. package/dist/app.js +23 -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 +5 -6
  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 +62 -73
  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/theme/components/index.js +0 -2
  47. package/dist/theme/index.js +10 -16
  48. package/dist/theme/layouts/index.js +0 -1
  49. package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
  50. package/dist/themes/threads/index.js +81 -0
  51. package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
  52. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  53. package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
  54. package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
  55. package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
  56. package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
  57. package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
  58. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  59. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  60. package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
  61. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  62. package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
  63. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  64. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  65. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  66. package/dist/types.js +24 -40
  67. package/package.json +2 -1
  68. package/src/__tests__/helpers/app.ts +4 -0
  69. package/src/__tests__/helpers/db.ts +51 -74
  70. package/src/app.tsx +27 -6
  71. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  72. package/src/db/migrations/meta/_journal.json +7 -0
  73. package/src/db/schema.ts +63 -46
  74. package/src/i18n/locales/en.po +216 -164
  75. package/src/i18n/locales/en.ts +1 -1
  76. package/src/i18n/locales/zh-Hans.po +216 -164
  77. package/src/i18n/locales/zh-Hans.ts +1 -1
  78. package/src/i18n/locales/zh-Hant.po +216 -164
  79. package/src/i18n/locales/zh-Hant.ts +1 -1
  80. package/src/index.ts +30 -15
  81. package/src/lib/__tests__/excerpt.test.ts +125 -0
  82. package/src/lib/__tests__/schemas.test.ts +166 -105
  83. package/src/lib/__tests__/theme-components.test.ts +4 -25
  84. package/src/lib/__tests__/time.test.ts +62 -0
  85. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  86. package/src/lib/__tests__/view.test.ts +217 -67
  87. package/src/lib/constants.ts +1 -4
  88. package/src/lib/excerpt.ts +87 -0
  89. package/src/lib/feed.ts +22 -7
  90. package/src/lib/navigation.ts +6 -7
  91. package/src/lib/render.tsx +1 -1
  92. package/src/lib/schemas.ts +118 -52
  93. package/src/lib/theme-components.ts +10 -13
  94. package/src/lib/time.ts +64 -0
  95. package/src/lib/timeline.ts +170 -0
  96. package/src/lib/view.ts +81 -83
  97. package/src/preset.css +45 -0
  98. package/src/routes/api/__tests__/posts.test.ts +50 -108
  99. package/src/routes/api/__tests__/search.test.ts +2 -3
  100. package/src/routes/api/posts.ts +30 -30
  101. package/src/routes/api/search.ts +4 -4
  102. package/src/routes/api/upload.ts +16 -6
  103. package/src/routes/dash/collections.tsx +18 -40
  104. package/src/routes/dash/index.tsx +2 -2
  105. package/src/routes/dash/navigation.tsx +27 -26
  106. package/src/routes/dash/pages.tsx +45 -60
  107. package/src/routes/dash/posts.tsx +44 -52
  108. package/src/routes/feed/rss.ts +2 -1
  109. package/src/routes/feed/sitemap.ts +14 -4
  110. package/src/routes/pages/archive.tsx +14 -10
  111. package/src/routes/pages/collection.tsx +17 -6
  112. package/src/routes/pages/home.tsx +56 -81
  113. package/src/routes/pages/page.tsx +64 -27
  114. package/src/routes/pages/post.tsx +5 -14
  115. package/src/routes/pages/search.tsx +2 -2
  116. package/src/services/__tests__/collection.test.ts +257 -158
  117. package/src/services/__tests__/media.test.ts +18 -18
  118. package/src/services/__tests__/navigation.test.ts +161 -87
  119. package/src/services/__tests__/post-timeline.test.ts +92 -88
  120. package/src/services/__tests__/post.test.ts +342 -206
  121. package/src/services/__tests__/search.test.ts +19 -25
  122. package/src/services/collection.ts +71 -113
  123. package/src/services/index.ts +9 -8
  124. package/src/services/navigation.ts +38 -71
  125. package/src/services/page.ts +124 -0
  126. package/src/services/post.ts +93 -103
  127. package/src/services/search.ts +38 -27
  128. package/src/styles/components.css +0 -54
  129. package/src/theme/components/MediaGallery.tsx +27 -96
  130. package/src/theme/components/PageForm.tsx +21 -21
  131. package/src/theme/components/PostForm.tsx +122 -118
  132. package/src/theme/components/PostList.tsx +58 -49
  133. package/src/theme/components/ThreadView.tsx +6 -3
  134. package/src/theme/components/TypeBadge.tsx +9 -17
  135. package/src/theme/components/VisibilityBadge.tsx +40 -23
  136. package/src/theme/components/index.ts +0 -13
  137. package/src/theme/index.ts +10 -16
  138. package/src/theme/layouts/index.ts +0 -1
  139. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  140. package/src/themes/threads/index.ts +100 -0
  141. package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
  142. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  143. package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
  144. package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
  145. package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
  146. package/src/themes/threads/pages/SinglePage.tsx +23 -0
  147. package/src/themes/threads/style.css +336 -0
  148. package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
  149. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  150. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  151. package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
  152. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  153. package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
  154. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  155. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  156. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  157. package/src/types.ts +242 -98
  158. package/dist/routes/api/timeline.js +0 -120
  159. package/dist/theme/components/timeline/ArticleCard.js +0 -46
  160. package/dist/theme/components/timeline/ImageCard.js +0 -83
  161. package/dist/theme/components/timeline/NoteCard.js +0 -34
  162. package/dist/theme/components/timeline/QuoteCard.js +0 -48
  163. package/dist/theme/components/timeline/TimelineFeed.js +0 -46
  164. package/dist/theme/components/timeline/index.js +0 -8
  165. package/dist/theme/layouts/SiteLayout.js +0 -131
  166. package/dist/theme/pages/CollectionPage.js +0 -63
  167. package/dist/theme/pages/index.js +0 -11
  168. package/src/routes/api/timeline.tsx +0 -159
  169. package/src/theme/components/timeline/ArticleCard.tsx +0 -45
  170. package/src/theme/components/timeline/ImageCard.tsx +0 -70
  171. package/src/theme/components/timeline/NoteCard.tsx +0 -34
  172. package/src/theme/components/timeline/QuoteCard.tsx +0 -48
  173. package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
  174. package/src/theme/components/timeline/index.ts +0 -8
  175. package/src/theme/layouts/SiteLayout.tsx +0 -132
  176. package/src/theme/pages/CollectionPage.tsx +0 -60
  177. package/src/theme/pages/SinglePage.tsx +0 -24
  178. package/src/theme/pages/index.ts +0 -13
@@ -1,13 +1,22 @@
1
1
  /**
2
- * Link Card Component
2
+ * Threads Theme - Link Card
3
3
  *
4
- * External link emphasis for type="link" posts.
4
+ * Compact link preview box date is shown at the feed level as a group header.
5
5
  */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
6
  export const LinkCard = ({ post, compact })=>{
7
+ // Extract domain from URL for display
8
+ let domain;
9
+ if (post.url) {
10
+ try {
11
+ domain = new URL(post.url).hostname.replace(/^www\./, "");
12
+ } catch {
13
+ // Invalid URL, skip domain display
14
+ }
15
+ }
7
16
  return /*#__PURE__*/ _jsxs("article", {
8
- class: `h-entry timeline-card timeline-card-link${compact ? " timeline-card-compact" : ""}`,
17
+ class: `h-entry${compact ? " threads-compact" : ""}`,
9
18
  children: [
10
- post.sourceDomain && /*#__PURE__*/ _jsxs("div", {
19
+ domain && /*#__PURE__*/ _jsxs("div", {
11
20
  class: "text-xs text-muted-foreground mb-1 flex items-center gap-1",
12
21
  children: [
13
22
  /*#__PURE__*/ _jsx("svg", {
@@ -22,24 +31,24 @@ export const LinkCard = ({ post, compact })=>{
22
31
  })
23
32
  }),
24
33
  /*#__PURE__*/ _jsx("span", {
25
- children: post.sourceDomain
34
+ children: domain
26
35
  })
27
36
  ]
28
37
  }),
29
38
  post.title && /*#__PURE__*/ _jsx("h2", {
30
39
  class: `p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`,
31
40
  children: /*#__PURE__*/ _jsx("a", {
32
- href: post.sourceUrl || post.permalink,
41
+ href: post.url || post.permalink,
33
42
  class: "u-url hover:underline",
34
- target: post.sourceUrl ? "_blank" : undefined,
35
- rel: post.sourceUrl ? "noopener noreferrer" : undefined,
43
+ target: post.url ? "_blank" : undefined,
44
+ rel: post.url ? "noopener noreferrer" : undefined,
36
45
  children: post.title
37
46
  })
38
47
  }),
39
- !compact && post.contentHtml && /*#__PURE__*/ _jsx("div", {
40
- class: "e-content prose prose-sm text-muted-foreground",
48
+ !compact && post.bodyHtml && /*#__PURE__*/ _jsx("div", {
49
+ class: "e-content prose text-muted-foreground",
41
50
  dangerouslySetInnerHTML: {
42
- __html: post.contentHtml
51
+ __html: post.bodyHtml
43
52
  }
44
53
  }),
45
54
  /*#__PURE__*/ _jsx("footer", {
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Threads Theme - Note Card
3
+ *
4
+ * Without title: plain text note — date is shown at the feed level as a group header.
5
+ * With title: article-style rendering with summary excerpt and "Read more" link.
6
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
7
+ import { MediaGallery } from "../../../theme/index.js";
8
+ export const NoteCard = ({ post, compact })=>{
9
+ const isArticle = !!post.title;
10
+ const displayHtml = isArticle ? post.summaryHtml : post.bodyHtml;
11
+ return /*#__PURE__*/ _jsxs("article", {
12
+ class: `h-entry${compact ? " threads-compact" : ""}`,
13
+ children: [
14
+ isArticle && /*#__PURE__*/ _jsx("h2", {
15
+ class: `p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`,
16
+ children: /*#__PURE__*/ _jsx("a", {
17
+ href: post.permalink,
18
+ class: "u-url hover:underline",
19
+ children: post.title
20
+ })
21
+ }),
22
+ displayHtml && /*#__PURE__*/ _jsx("div", {
23
+ class: `e-content prose ${compact ? "prose-sm" : isArticle ? "text-muted-foreground" : ""}`,
24
+ dangerouslySetInnerHTML: {
25
+ __html: displayHtml
26
+ }
27
+ }),
28
+ !compact && post.media.length > 0 && /*#__PURE__*/ _jsx("div", {
29
+ class: "threads-media mt-3",
30
+ children: /*#__PURE__*/ _jsx(MediaGallery, {
31
+ attachments: post.media
32
+ })
33
+ }),
34
+ !compact && isArticle && post.summaryHasMore && /*#__PURE__*/ _jsx("a", {
35
+ href: post.permalink,
36
+ class: "text-sm text-muted-foreground hover:underline mt-1 inline-block",
37
+ children: "Read more →"
38
+ }),
39
+ /*#__PURE__*/ _jsx("footer", {
40
+ class: "mt-2",
41
+ children: /*#__PURE__*/ _jsx("a", {
42
+ href: post.permalink,
43
+ class: "u-url text-xs text-muted-foreground hover:underline",
44
+ children: /*#__PURE__*/ _jsx("time", {
45
+ class: "dt-published",
46
+ datetime: post.publishedAt,
47
+ children: post.publishedAtRelative
48
+ })
49
+ })
50
+ })
51
+ ]
52
+ });
53
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Threads Theme - Quote Card
3
+ *
4
+ * Left-border accent blockquote — date is shown at the feed level as a group header.
5
+ *
6
+ * v2 fields:
7
+ * - quoteText: the quoted text
8
+ * - title: attribution (who said it)
9
+ * - url: source link
10
+ * - bodyHtml: commentary
11
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
12
+ export const QuoteCard = ({ post, compact })=>{
13
+ return /*#__PURE__*/ _jsxs("article", {
14
+ class: `h-entry${compact ? " threads-compact" : ""}`,
15
+ children: [
16
+ post.quoteText && /*#__PURE__*/ _jsx("blockquote", {
17
+ class: "threads-quote",
18
+ children: /*#__PURE__*/ _jsx("div", {
19
+ class: `e-content ${compact ? "text-sm" : "text-base"} leading-relaxed`,
20
+ children: post.quoteText
21
+ })
22
+ }),
23
+ !compact && (post.title || post.url) && /*#__PURE__*/ _jsxs("div", {
24
+ class: "mt-2 text-sm text-muted-foreground",
25
+ children: [
26
+ "—",
27
+ " ",
28
+ post.url ? /*#__PURE__*/ _jsx("a", {
29
+ href: post.url,
30
+ class: "hover:underline",
31
+ target: "_blank",
32
+ rel: "noopener noreferrer",
33
+ children: post.title || "Source"
34
+ }) : /*#__PURE__*/ _jsx("span", {
35
+ children: post.title
36
+ })
37
+ ]
38
+ }),
39
+ !compact && post.bodyHtml && /*#__PURE__*/ _jsx("div", {
40
+ class: "mt-3 prose text-muted-foreground",
41
+ dangerouslySetInnerHTML: {
42
+ __html: post.bodyHtml
43
+ }
44
+ }),
45
+ /*#__PURE__*/ _jsx("footer", {
46
+ class: "mt-2",
47
+ children: /*#__PURE__*/ _jsx("a", {
48
+ href: post.permalink,
49
+ class: "u-url text-xs text-muted-foreground hover:underline",
50
+ children: /*#__PURE__*/ _jsx("time", {
51
+ class: "dt-published",
52
+ datetime: post.publishedAt,
53
+ children: post.publishedAtRelative
54
+ })
55
+ })
56
+ })
57
+ ]
58
+ });
59
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Thread Preview Component
2
+ * Threads Theme - Thread Preview
3
3
  *
4
- * Inline thread preview: root card + compact replies + "show more" link.
4
+ * Root post + vertical line connector + compact replies underneath.
5
5
  */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
6
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
7
  import { TimelineItem } from "./TimelineItem.js";
@@ -10,7 +10,6 @@ export const ThreadPreview = ({ rootPost, previewReplies, totalReplyCount, theme
10
10
  const { i18n: $__i18n, _: $__ } = $_useLingui();
11
11
  const remainingCount = totalReplyCount - previewReplies.length;
12
12
  return /*#__PURE__*/ _jsxs("div", {
13
- class: "timeline-thread",
14
13
  children: [
15
14
  /*#__PURE__*/ _jsx(TimelineItem, {
16
15
  item: {
@@ -19,10 +18,10 @@ export const ThreadPreview = ({ rootPost, previewReplies, totalReplyCount, theme
19
18
  theme: theme
20
19
  }),
21
20
  previewReplies.length > 0 && /*#__PURE__*/ _jsxs("div", {
22
- class: "timeline-thread-replies",
21
+ class: "threads-replies",
23
22
  children: [
24
23
  previewReplies.map((reply)=>/*#__PURE__*/ _jsx("div", {
25
- class: "timeline-thread-reply",
24
+ class: "threads-reply",
26
25
  children: /*#__PURE__*/ _jsx(TimelineItemFromPost, {
27
26
  post: reply,
28
27
  compact: true,
@@ -30,7 +29,7 @@ export const ThreadPreview = ({ rootPost, previewReplies, totalReplyCount, theme
30
29
  })
31
30
  }, reply.id)),
32
31
  remainingCount > 0 && /*#__PURE__*/ _jsx("div", {
33
- class: "timeline-thread-reply",
32
+ class: "threads-reply",
34
33
  children: /*#__PURE__*/ _jsx("a", {
35
34
  href: rootPost.permalink,
36
35
  class: "text-sm text-muted-foreground hover:text-foreground hover:underline",
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Threads Theme - Timeline Feed
3
+ *
4
+ * Date-grouped posts separated by thin dividers.
5
+ * A centered date header appears above each group.
6
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
7
+ import { TimelineItem } from "./TimelineItem.js";
8
+ import { ThreadPreview as DefaultThreadPreview } from "./ThreadPreview.js";
9
+ import { TimelineLoadMore as DefaultTimelineLoadMore } from "./TimelineLoadMore.js";
10
+ import { groupByDate } from "./groupByDate.js";
11
+ export const TimelineFeed = ({ items, hasMore, nextCursor, theme })=>{
12
+ const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
13
+ const ResolvedLoadMore = theme?.TimelineLoadMore ?? DefaultTimelineLoadMore;
14
+ const groups = groupByDate(items);
15
+ return /*#__PURE__*/ _jsxs("div", {
16
+ children: [
17
+ /*#__PURE__*/ _jsx("div", {
18
+ id: "timeline-feed",
19
+ children: groups.map((group)=>/*#__PURE__*/ _jsxs("div", {
20
+ class: "threads-card",
21
+ children: [
22
+ /*#__PURE__*/ _jsx("div", {
23
+ class: "threads-date-header",
24
+ children: /*#__PURE__*/ _jsx("span", {
25
+ children: group.label
26
+ })
27
+ }),
28
+ /*#__PURE__*/ _jsx("div", {
29
+ id: `date-items-${group.dateKey}`,
30
+ class: "flex flex-col",
31
+ children: group.items.map((item, i)=>/*#__PURE__*/ _jsxs("div", {
32
+ children: [
33
+ i > 0 && /*#__PURE__*/ _jsx("hr", {
34
+ class: "border-border my-5"
35
+ }),
36
+ item.threadPreview ? /*#__PURE__*/ _jsx(ResolvedThreadPreview, {
37
+ rootPost: item.post,
38
+ previewReplies: item.threadPreview.replies,
39
+ totalReplyCount: item.threadPreview.totalReplyCount,
40
+ theme: theme
41
+ }) : /*#__PURE__*/ _jsx(TimelineItem, {
42
+ item: item,
43
+ theme: theme
44
+ })
45
+ ]
46
+ }, item.post.id))
47
+ })
48
+ ]
49
+ }, group.dateKey))
50
+ }),
51
+ hasMore && nextCursor && /*#__PURE__*/ _jsx(ResolvedLoadMore, {
52
+ nextCursor: nextCursor,
53
+ lastDate: groups.at(-1)?.dateKey,
54
+ theme: theme
55
+ })
56
+ ]
57
+ });
58
+ };
@@ -1,43 +1,34 @@
1
1
  /**
2
- * Timeline Item Component
2
+ * Threads Theme - Timeline Item
3
3
  *
4
- * Dispatches to the correct card component based on post type.
5
- * Resolves card overrides from theme components if provided.
4
+ * Dispatches to the correct card component based on post format.
6
5
  */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
7
6
  import { NoteCard } from "./NoteCard.js";
8
- import { ArticleCard } from "./ArticleCard.js";
9
7
  import { LinkCard } from "./LinkCard.js";
10
8
  import { QuoteCard } from "./QuoteCard.js";
11
- import { ImageCard } from "./ImageCard.js";
12
9
  const CARD_MAP = {
13
10
  note: NoteCard,
14
- article: ArticleCard,
15
11
  link: LinkCard,
16
- quote: QuoteCard,
17
- image: ImageCard,
18
- page: NoteCard
12
+ quote: QuoteCard
19
13
  };
20
14
  const THEME_KEY_MAP = {
21
15
  note: "NoteCard",
22
- article: "ArticleCard",
23
16
  link: "LinkCard",
24
- quote: "QuoteCard",
25
- image: "ImageCard",
26
- page: "NoteCard"
17
+ quote: "QuoteCard"
27
18
  };
28
19
  export const TimelineItem = ({ item, compact, cardOverride, theme })=>{
29
- const themeKey = THEME_KEY_MAP[item.post.type];
20
+ const themeKey = THEME_KEY_MAP[item.post.format];
30
21
  const themeCard = theme?.[themeKey];
31
- const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.type];
22
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.format];
32
23
  return /*#__PURE__*/ _jsx(Card, {
33
24
  post: item.post,
34
25
  compact: compact
35
26
  });
36
27
  };
37
28
  export const TimelineItemFromPost = ({ post, compact, cardOverride, theme })=>{
38
- const themeKey = THEME_KEY_MAP[post.type];
29
+ const themeKey = THEME_KEY_MAP[post.format];
39
30
  const themeCard = theme?.[themeKey];
40
- const Card = cardOverride ?? themeCard ?? CARD_MAP[post.type];
31
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[post.format];
41
32
  return /*#__PURE__*/ _jsx(Card, {
42
33
  post: post,
43
34
  compact: compact
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Threads Theme - Timeline Load More
3
+ *
4
+ * Auto-loads more posts when scrolled into view.
5
+ * Passes `lastDate` to the server so it can merge date groups across pages.
6
+ */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
7
+ import { useLingui as $_useLingui } from "@jant/core/i18n";
8
+ export const TimelineLoadMore = ({ nextCursor, lastDate })=>{
9
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
10
+ const url = lastDate ? `/?cursor=${nextCursor}&lastDate=${lastDate}` : `/?cursor=${nextCursor}`;
11
+ return /*#__PURE__*/ _jsx("div", {
12
+ id: "load-more-container",
13
+ class: "py-6 text-center",
14
+ "data-on-intersect__once": `@get('${url}')`,
15
+ children: /*#__PURE__*/ _jsx("span", {
16
+ class: "text-sm text-muted-foreground",
17
+ children: $__i18n._({
18
+ id: "Z3FXyt",
19
+ message: "Loading..."
20
+ })
21
+ })
22
+ });
23
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Groups timeline items by their publication date (YYYY-MM-DD).
3
+ *
4
+ * Shared between TimelineFeed (initial render) and timelineMore (SSE patches)
5
+ * so that both produce identical date group structure.
6
+ */ export function groupByDate(items) {
7
+ const groups = [];
8
+ let current = null;
9
+ for (const item of items){
10
+ const dateKey = item.post.publishedAt.slice(0, 10);
11
+ if (!current || current.dateKey !== dateKey) {
12
+ current = {
13
+ dateKey,
14
+ label: item.post.publishedAtFormatted,
15
+ items: []
16
+ };
17
+ groups.push(current);
18
+ }
19
+ current.items.push(item);
20
+ }
21
+ return groups;
22
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Threads Theme - Timeline Load-More SSE Renderer
3
+ *
4
+ * Produces SSE DOM patches for incremental timeline loading.
5
+ * Uses date-grouped layout with threads-specific HTML (threads-card, threads-date-header)
6
+ * matching TimelineFeed's initial render exactly.
7
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
8
+ import { groupByDate } from "./groupByDate.js";
9
+ import { TimelineItem } from "./TimelineItem.js";
10
+ import { ThreadPreview as DefaultThreadPreview } from "./ThreadPreview.js";
11
+ import { TimelineLoadMore as DefaultTimelineLoadMore } from "./TimelineLoadMore.js";
12
+ function renderItem(item, props) {
13
+ const theme = props.theme;
14
+ const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
15
+ if (item.threadPreview) {
16
+ return /*#__PURE__*/ _jsx(ResolvedThreadPreview, {
17
+ rootPost: item.post,
18
+ previewReplies: item.threadPreview.replies,
19
+ totalReplyCount: item.threadPreview.totalReplyCount,
20
+ theme: theme
21
+ }).toString();
22
+ }
23
+ return /*#__PURE__*/ _jsx(TimelineItem, {
24
+ item: item,
25
+ theme: theme
26
+ }).toString();
27
+ }
28
+ /**
29
+ * Renders SSE patches for the threads theme's load-more response.
30
+ *
31
+ * @param props - Timeline more props with items, pagination, and theme
32
+ * @returns Array of DOM patch instructions for the SSE stream
33
+ */ export function timelineMore(props) {
34
+ const { items, lastDate, hasMore, nextCursor, theme } = props;
35
+ const patches = [];
36
+ const groups = groupByDate(items);
37
+ if (groups.length === 0) return patches;
38
+ const firstGroup = groups[0];
39
+ const isContinuation = lastDate === firstGroup.dateKey;
40
+ // Continuation items: append into the existing date group's container
41
+ if (isContinuation) {
42
+ const continuationHtml = firstGroup.items.map((item)=>{
43
+ const content = renderItem(item, props);
44
+ return `<div><hr class="border-border my-5"/>${content}</div>`;
45
+ }).join("");
46
+ if (continuationHtml) {
47
+ patches.push({
48
+ selector: `#date-items-${lastDate}`,
49
+ content: continuationHtml,
50
+ mode: "append"
51
+ });
52
+ }
53
+ }
54
+ // New date groups: append to #timeline-feed
55
+ const newGroups = isContinuation ? groups.slice(1) : groups;
56
+ if (newGroups.length > 0) {
57
+ const newGroupsHtml = newGroups.map((group)=>{
58
+ const itemsHtml = group.items.map((item, i)=>{
59
+ const content = renderItem(item, props);
60
+ return i > 0 ? `<div><hr class="border-border my-5"/>${content}</div>` : `<div>${content}</div>`;
61
+ }).join("");
62
+ return /*#__PURE__*/ _jsxs("div", {
63
+ class: "threads-card",
64
+ children: [
65
+ /*#__PURE__*/ _jsx("div", {
66
+ class: "threads-date-header",
67
+ children: /*#__PURE__*/ _jsx("span", {
68
+ children: group.label
69
+ })
70
+ }),
71
+ /*#__PURE__*/ _jsx("div", {
72
+ id: `date-items-${group.dateKey}`,
73
+ class: "flex flex-col",
74
+ dangerouslySetInnerHTML: {
75
+ __html: itemsHtml
76
+ }
77
+ })
78
+ ]
79
+ }).toString();
80
+ }).join("");
81
+ patches.push({
82
+ selector: "#timeline-feed",
83
+ content: newGroupsHtml,
84
+ mode: "append"
85
+ });
86
+ }
87
+ // Load-more button
88
+ const ResolvedLoadMore = theme?.TimelineLoadMore ?? DefaultTimelineLoadMore;
89
+ const lastGroupDate = groups.at(-1)?.dateKey;
90
+ if (hasMore && nextCursor) {
91
+ patches.push({
92
+ selector: "#load-more-container",
93
+ content: /*#__PURE__*/ _jsx(ResolvedLoadMore, {
94
+ nextCursor: nextCursor,
95
+ lastDate: lastGroupDate,
96
+ theme: theme
97
+ }).toString()
98
+ });
99
+ } else {
100
+ patches.push({
101
+ selector: "#load-more-container",
102
+ content: "",
103
+ mode: "remove"
104
+ });
105
+ }
106
+ return patches;
107
+ }
package/dist/types.js CHANGED
@@ -1,53 +1,33 @@
1
1
  /**
2
- * Jant Type Definitions
2
+ * Jant Type Definitions (v2)
3
3
  */ // =============================================================================
4
4
  // Content Types
5
5
  // =============================================================================
6
- export const POST_TYPES = [
6
+ export const FORMATS = [
7
7
  "note",
8
- "article",
9
8
  "link",
10
- "quote",
11
- "image",
12
- "page"
9
+ "quote"
10
+ ];
11
+ export const STATUSES = [
12
+ "draft",
13
+ "published"
14
+ ];
15
+ export const SORT_ORDERS = [
16
+ "newest",
17
+ "oldest",
18
+ "rating_desc",
19
+ "rating_asc"
20
+ ];
21
+ export const NAV_ITEM_TYPES = [
22
+ "page",
23
+ "link"
13
24
  ];
14
25
  export const MAX_MEDIA_ATTACHMENTS = 20;
15
- /**
16
- * Media attachment rules per post type.
17
- * Each entry is [min, max] or null (no media allowed).
18
- */ export const POST_TYPE_MEDIA_RULES = {
19
- note: [
20
- 0,
21
- 20
22
- ],
23
- article: [
24
- 0,
25
- 20
26
- ],
27
- image: [
28
- 1,
29
- 20
30
- ],
31
- link: [
32
- 0,
33
- 1
34
- ],
35
- quote: [
36
- 0,
37
- 20
38
- ],
39
- page: null
40
- };
26
+ export const MAX_PINNED_POSTS = 3;
41
27
  export const STORAGE_DRIVERS = [
42
28
  "r2",
43
29
  "s3"
44
30
  ];
45
- export const VISIBILITY_LEVELS = [
46
- "featured",
47
- "quiet",
48
- "unlisted",
49
- "draft"
50
- ];
51
31
  // =============================================================================
52
32
  // Configuration System
53
33
  // =============================================================================
@@ -58,8 +38,8 @@ export const VISIBILITY_LEVELS = [
58
38
  * Add new fields here, and they'll automatically work everywhere.
59
39
  *
60
40
  * Priority logic:
61
- * - envOnly: false User-configurable (DB > ENV > Default)
62
- * - envOnly: true Environment-only (ENV > Default)
41
+ * - envOnly: false -> User-configurable (DB > ENV > Default)
42
+ * - envOnly: true -> Environment-only (ENV > Default)
63
43
  */ export const CONFIG_FIELDS = {
64
44
  // User-configurable (can be modified in dashboard)
65
45
  SITE_NAME: {
@@ -99,6 +79,10 @@ export const VISIBILITY_LEVELS = [
99
79
  defaultValue: "",
100
80
  envOnly: true
101
81
  },
82
+ PAGE_SIZE: {
83
+ defaultValue: "20",
84
+ envOnly: true
85
+ },
102
86
  STORAGE_DRIVER: {
103
87
  defaultValue: "r2",
104
88
  envOnly: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,7 @@
33
33
  "dependencies": {
34
34
  "@aws-sdk/client-s3": "^3.987.0",
35
35
  "@lingui/core": "^5.9.0",
36
+ "@tailwindcss/typography": "^0.5.19",
36
37
  "basecoat-css": "^0.3.10",
37
38
  "better-auth": "^1.4.18",
38
39
  "drizzle-orm": "^0.45.1",
@@ -9,11 +9,13 @@ import type { Bindings } from "../../types.js";
9
9
  import type { AppVariables } from "../../app.js";
10
10
  import { createTestDatabase } from "./db.js";
11
11
  import { createPostService } from "../../services/post.js";
12
+ import { createPageService } from "../../services/page.js";
12
13
  import { createSettingsService } from "../../services/settings.js";
13
14
  import { createRedirectService } from "../../services/redirect.js";
14
15
  import { createMediaService } from "../../services/media.js";
15
16
  import { createCollectionService } from "../../services/collection.js";
16
17
  import { createSearchService } from "../../services/search.js";
18
+ import { createNavItemService } from "../../services/navigation.js";
17
19
  import type { Database } from "../../db/index.js";
18
20
  import type BetterSqlite3 from "better-sqlite3";
19
21
 
@@ -40,11 +42,13 @@ export function createTestApp(options: TestAppOptions = {}) {
40
42
 
41
43
  const services = {
42
44
  posts: createPostService(db),
45
+ pages: createPageService(db),
43
46
  settings: createSettingsService(db),
44
47
  redirects: createRedirectService(db),
45
48
  media: createMediaService(db),
46
49
  collections: createCollectionService(db),
47
50
  search: createSearchService(mockD1),
51
+ navItems: createNavItemService(db),
48
52
  };
49
53
 
50
54
  const app = new Hono<Env>();