@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
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Threads Theme - Link Card
3
+ *
4
+ * Compact link preview box — date is shown at the feed level as a group header.
5
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
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
+ }
16
+ return /*#__PURE__*/ _jsxs("article", {
17
+ class: `h-entry${compact ? " threads-compact" : ""}`,
18
+ children: [
19
+ domain && /*#__PURE__*/ _jsxs("div", {
20
+ class: "text-xs text-muted-foreground mb-1 flex items-center gap-1",
21
+ children: [
22
+ /*#__PURE__*/ _jsx("svg", {
23
+ class: "size-3",
24
+ xmlns: "http://www.w3.org/2000/svg",
25
+ fill: "none",
26
+ viewBox: "0 0 24 24",
27
+ "stroke-width": "2",
28
+ stroke: "currentColor",
29
+ children: /*#__PURE__*/ _jsx("path", {
30
+ d: "M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
31
+ })
32
+ }),
33
+ /*#__PURE__*/ _jsx("span", {
34
+ children: domain
35
+ })
36
+ ]
37
+ }),
38
+ post.title && /*#__PURE__*/ _jsx("h2", {
39
+ class: `p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`,
40
+ children: /*#__PURE__*/ _jsx("a", {
41
+ href: post.url || post.permalink,
42
+ class: "u-url hover:underline",
43
+ target: post.url ? "_blank" : undefined,
44
+ rel: post.url ? "noopener noreferrer" : undefined,
45
+ children: post.title
46
+ })
47
+ }),
48
+ !compact && post.bodyHtml && /*#__PURE__*/ _jsx("div", {
49
+ class: "e-content prose text-muted-foreground",
50
+ dangerouslySetInnerHTML: {
51
+ __html: post.bodyHtml
52
+ }
53
+ }),
54
+ /*#__PURE__*/ _jsx("footer", {
55
+ class: "mt-2 text-xs text-muted-foreground",
56
+ children: /*#__PURE__*/ _jsx("a", {
57
+ href: post.permalink,
58
+ class: "hover:underline",
59
+ children: /*#__PURE__*/ _jsx("time", {
60
+ class: "dt-published",
61
+ datetime: post.publishedAt,
62
+ children: post.publishedAtFormatted
63
+ })
64
+ })
65
+ })
66
+ ]
67
+ });
68
+ };
@@ -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
- * Minimal Theme - Thread Preview
2
+ * Threads Theme - Thread Preview
3
3
  *
4
- * Minimal thread indicator: root post + 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";
@@ -18,25 +18,29 @@ export const ThreadPreview = ({ rootPost, previewReplies, totalReplyCount, theme
18
18
  theme: theme
19
19
  }),
20
20
  previewReplies.length > 0 && /*#__PURE__*/ _jsxs("div", {
21
- class: "ml-4 mt-2 border-l border-border pl-4 flex flex-col gap-3",
21
+ class: "threads-replies",
22
22
  children: [
23
23
  previewReplies.map((reply)=>/*#__PURE__*/ _jsx("div", {
24
+ class: "threads-reply",
24
25
  children: /*#__PURE__*/ _jsx(TimelineItemFromPost, {
25
26
  post: reply,
26
27
  compact: true,
27
28
  theme: theme
28
29
  })
29
30
  }, reply.id)),
30
- remainingCount > 0 && /*#__PURE__*/ _jsx("a", {
31
- href: rootPost.permalink,
32
- class: "text-sm text-muted-foreground hover:text-foreground hover:underline",
33
- children: $__i18n._({
34
- id: "smzF8S",
35
- message: "Show {remainingCount} more {0}",
36
- values: {
37
- remainingCount: remainingCount,
38
- 0: remainingCount === 1 ? "reply" : "replies"
39
- }
31
+ remainingCount > 0 && /*#__PURE__*/ _jsx("div", {
32
+ class: "threads-reply",
33
+ children: /*#__PURE__*/ _jsx("a", {
34
+ href: rootPost.permalink,
35
+ class: "text-sm text-muted-foreground hover:text-foreground hover:underline",
36
+ children: $__i18n._({
37
+ id: "smzF8S",
38
+ message: "Show {remainingCount} more {0}",
39
+ values: {
40
+ remainingCount: remainingCount,
41
+ 0: remainingCount === 1 ? "reply" : "replies"
42
+ }
43
+ })
40
44
  })
41
45
  })
42
46
  ]
@@ -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,42 +1,34 @@
1
1
  /**
2
- * Minimal Theme - Timeline Item
2
+ * Threads Theme - Timeline Item
3
3
  *
4
- * Dispatches to the correct card component based on post type.
4
+ * Dispatches to the correct card component based on post format.
5
5
  */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
6
6
  import { NoteCard } from "./NoteCard.js";
7
- import { ArticleCard } from "./ArticleCard.js";
8
7
  import { LinkCard } from "./LinkCard.js";
9
8
  import { QuoteCard } from "./QuoteCard.js";
10
- import { ImageCard } from "./ImageCard.js";
11
9
  const CARD_MAP = {
12
10
  note: NoteCard,
13
- article: ArticleCard,
14
11
  link: LinkCard,
15
- quote: QuoteCard,
16
- image: ImageCard,
17
- page: NoteCard
12
+ quote: QuoteCard
18
13
  };
19
14
  const THEME_KEY_MAP = {
20
15
  note: "NoteCard",
21
- article: "ArticleCard",
22
16
  link: "LinkCard",
23
- quote: "QuoteCard",
24
- image: "ImageCard",
25
- page: "NoteCard"
17
+ quote: "QuoteCard"
26
18
  };
27
19
  export const TimelineItem = ({ item, compact, cardOverride, theme })=>{
28
- const themeKey = THEME_KEY_MAP[item.post.type];
20
+ const themeKey = THEME_KEY_MAP[item.post.format];
29
21
  const themeCard = theme?.[themeKey];
30
- const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.type];
22
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.format];
31
23
  return /*#__PURE__*/ _jsx(Card, {
32
24
  post: item.post,
33
25
  compact: compact
34
26
  });
35
27
  };
36
28
  export const TimelineItemFromPost = ({ post, compact, cardOverride, theme })=>{
37
- const themeKey = THEME_KEY_MAP[post.type];
29
+ const themeKey = THEME_KEY_MAP[post.format];
38
30
  const themeCard = theme?.[themeKey];
39
- const Card = cardOverride ?? themeCard ?? CARD_MAP[post.type];
31
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[post.format];
40
32
  return /*#__PURE__*/ _jsx(Card, {
41
33
  post: post,
42
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.23",
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>();