@jant/core 0.3.21 → 0.3.23

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 (99) hide show
  1. package/dist/app.js +23 -4
  2. package/dist/index.js +11 -4
  3. package/dist/lib/feed.js +112 -0
  4. package/dist/lib/navigation.js +9 -9
  5. package/dist/lib/render.js +48 -0
  6. package/dist/lib/theme-components.js +18 -18
  7. package/dist/lib/view.js +228 -0
  8. package/dist/routes/api/timeline.js +22 -18
  9. package/dist/routes/feed/rss.js +34 -78
  10. package/dist/routes/feed/sitemap.js +11 -26
  11. package/dist/routes/pages/archive.js +18 -195
  12. package/dist/routes/pages/collection.js +16 -70
  13. package/dist/routes/pages/home.js +25 -47
  14. package/dist/routes/pages/page.js +15 -27
  15. package/dist/routes/pages/post.js +25 -79
  16. package/dist/routes/pages/search.js +20 -130
  17. package/dist/theme/components/MediaGallery.js +10 -10
  18. package/dist/theme/components/index.js +0 -2
  19. package/dist/theme/index.js +10 -13
  20. package/dist/theme/layouts/index.js +0 -1
  21. package/dist/themes/minimal/MinimalSiteLayout.js +83 -0
  22. package/dist/themes/minimal/index.js +65 -0
  23. package/dist/themes/minimal/pages/ArchivePage.js +156 -0
  24. package/dist/themes/minimal/pages/CollectionPage.js +65 -0
  25. package/dist/themes/minimal/pages/HomePage.js +25 -0
  26. package/dist/themes/minimal/pages/PostPage.js +47 -0
  27. package/dist/themes/minimal/pages/SearchPage.js +121 -0
  28. package/dist/themes/minimal/pages/SinglePage.js +22 -0
  29. package/dist/themes/minimal/timeline/ArticleCard.js +36 -0
  30. package/dist/themes/minimal/timeline/ImageCard.js +67 -0
  31. package/dist/themes/minimal/timeline/LinkCard.js +47 -0
  32. package/dist/themes/minimal/timeline/NoteCard.js +34 -0
  33. package/dist/{theme/components → themes/minimal}/timeline/QuoteCard.js +9 -12
  34. package/dist/themes/minimal/timeline/ThreadPreview.js +46 -0
  35. package/dist/themes/minimal/timeline/TimelineFeed.js +48 -0
  36. package/dist/themes/minimal/timeline/TimelineItem.js +44 -0
  37. package/package.json +2 -1
  38. package/src/app.tsx +27 -4
  39. package/src/i18n/locales/en.po +53 -53
  40. package/src/i18n/locales/zh-Hans.po +53 -53
  41. package/src/i18n/locales/zh-Hant.po +53 -53
  42. package/src/index.ts +54 -6
  43. package/src/lib/__tests__/theme-components.test.ts +33 -14
  44. package/src/lib/__tests__/view.test.ts +377 -0
  45. package/src/lib/feed.ts +148 -0
  46. package/src/lib/navigation.ts +11 -11
  47. package/src/lib/render.tsx +67 -0
  48. package/src/lib/theme-components.ts +27 -35
  49. package/src/lib/view.ts +318 -0
  50. package/src/routes/api/__tests__/timeline.test.ts +3 -3
  51. package/src/routes/api/timeline.tsx +34 -27
  52. package/src/routes/feed/rss.ts +47 -94
  53. package/src/routes/feed/sitemap.ts +8 -30
  54. package/src/routes/pages/archive.tsx +24 -209
  55. package/src/routes/pages/collection.tsx +19 -75
  56. package/src/routes/pages/home.tsx +42 -76
  57. package/src/routes/pages/page.tsx +17 -28
  58. package/src/routes/pages/post.tsx +28 -86
  59. package/src/routes/pages/search.tsx +29 -151
  60. package/src/services/search.ts +2 -8
  61. package/src/styles/components.css +0 -54
  62. package/src/theme/components/MediaGallery.tsx +12 -12
  63. package/src/theme/components/index.ts +0 -12
  64. package/src/theme/index.ts +11 -13
  65. package/src/theme/layouts/index.ts +1 -1
  66. package/src/themes/minimal/MinimalSiteLayout.tsx +100 -0
  67. package/src/themes/minimal/index.ts +83 -0
  68. package/src/themes/minimal/pages/ArchivePage.tsx +157 -0
  69. package/src/themes/minimal/pages/CollectionPage.tsx +60 -0
  70. package/src/themes/minimal/pages/HomePage.tsx +41 -0
  71. package/src/themes/minimal/pages/PostPage.tsx +43 -0
  72. package/src/themes/minimal/pages/SearchPage.tsx +122 -0
  73. package/src/themes/minimal/pages/SinglePage.tsx +23 -0
  74. package/src/themes/minimal/timeline/ArticleCard.tsx +37 -0
  75. package/src/themes/minimal/timeline/ImageCard.tsx +63 -0
  76. package/src/themes/minimal/timeline/LinkCard.tsx +48 -0
  77. package/src/themes/minimal/timeline/NoteCard.tsx +35 -0
  78. package/src/{theme/components → themes/minimal}/timeline/QuoteCard.tsx +11 -17
  79. package/src/themes/minimal/timeline/ThreadPreview.tsx +47 -0
  80. package/src/{theme/components → themes/minimal}/timeline/TimelineFeed.tsx +20 -15
  81. package/src/themes/minimal/timeline/TimelineItem.tsx +75 -0
  82. package/src/types.ts +262 -38
  83. package/dist/theme/components/timeline/ArticleCard.js +0 -50
  84. package/dist/theme/components/timeline/ImageCard.js +0 -86
  85. package/dist/theme/components/timeline/LinkCard.js +0 -62
  86. package/dist/theme/components/timeline/NoteCard.js +0 -37
  87. package/dist/theme/components/timeline/ThreadPreview.js +0 -52
  88. package/dist/theme/components/timeline/TimelineFeed.js +0 -43
  89. package/dist/theme/components/timeline/TimelineItem.js +0 -25
  90. package/dist/theme/components/timeline/index.js +0 -8
  91. package/dist/theme/layouts/SiteLayout.js +0 -160
  92. package/src/theme/components/timeline/ArticleCard.tsx +0 -57
  93. package/src/theme/components/timeline/ImageCard.tsx +0 -80
  94. package/src/theme/components/timeline/LinkCard.tsx +0 -66
  95. package/src/theme/components/timeline/NoteCard.tsx +0 -41
  96. package/src/theme/components/timeline/ThreadPreview.tsx +0 -49
  97. package/src/theme/components/timeline/TimelineItem.tsx +0 -39
  98. package/src/theme/components/timeline/index.ts +0 -8
  99. package/src/theme/layouts/SiteLayout.tsx +0 -184
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Minimal Theme - Search Page
3
+ *
4
+ * Minimal search form + results with page-based pagination.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "@lingui/react/macro";
9
+ import type { SearchPageProps } from "../../../types.js";
10
+ import { PagePagination as DefaultPagePagination } from "../../../theme/components/Pagination.js";
11
+
12
+ export const SearchPage: FC<SearchPageProps> = ({
13
+ query,
14
+ results,
15
+ error,
16
+ hasMore,
17
+ page,
18
+ theme,
19
+ }) => {
20
+ const { t } = useLingui();
21
+ const searchTitle = t({
22
+ message: "Search",
23
+ comment: "@context: Search page title",
24
+ });
25
+
26
+ const PaginationComponent = theme?.PagePagination ?? DefaultPagePagination;
27
+
28
+ return (
29
+ <div>
30
+ <h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
31
+
32
+ <form method="get" action="/search" class="mb-8">
33
+ <div class="flex gap-2">
34
+ <input
35
+ type="search"
36
+ name="q"
37
+ class="input flex-1"
38
+ placeholder={t({
39
+ message: "Search posts...",
40
+ comment: "@context: Search input placeholder",
41
+ })}
42
+ value={query}
43
+ autofocus
44
+ />
45
+ <button type="submit" class="btn">
46
+ {t({
47
+ message: "Search",
48
+ comment: "@context: Search submit button",
49
+ })}
50
+ </button>
51
+ </div>
52
+ </form>
53
+
54
+ {error && (
55
+ <div class="alert-destructive mb-6">
56
+ <h2>{error}</h2>
57
+ </div>
58
+ )}
59
+
60
+ {query && !error && (
61
+ <div>
62
+ <p class="text-sm text-muted-foreground mb-4">
63
+ {results.length === 0
64
+ ? t({
65
+ message: "No results found.",
66
+ comment: "@context: Search empty results",
67
+ })
68
+ : results.length === 1
69
+ ? t({
70
+ message: "Found 1 result",
71
+ comment: "@context: Search results count - single",
72
+ })
73
+ : t({
74
+ message: "Found {count} results",
75
+ comment: "@context: Search results count - multiple",
76
+ values: { count: String(results.length) },
77
+ })}
78
+ </p>
79
+
80
+ {results.length > 0 && (
81
+ <>
82
+ <div class="flex flex-col gap-4">
83
+ {results.map((result) => (
84
+ <article key={result.post.id} class="py-3">
85
+ <a href={result.post.permalink} class="block group">
86
+ <h2 class="font-medium group-hover:underline">
87
+ {result.post.title ||
88
+ result.post.content?.slice(0, 60) ||
89
+ `Post #${result.post.id}`}
90
+ </h2>
91
+
92
+ {result.snippet && (
93
+ <p
94
+ class="text-sm text-muted-foreground mt-1 line-clamp-2"
95
+ dangerouslySetInnerHTML={{ __html: result.snippet }}
96
+ />
97
+ )}
98
+
99
+ <footer class="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
100
+ <span>{result.post.type}</span>
101
+ <span>&middot;</span>
102
+ <time datetime={result.post.publishedAt}>
103
+ {result.post.publishedAtFormatted}
104
+ </time>
105
+ </footer>
106
+ </a>
107
+ </article>
108
+ ))}
109
+ </div>
110
+
111
+ <PaginationComponent
112
+ baseUrl={`/search?q=${encodeURIComponent(query)}`}
113
+ currentPage={page}
114
+ hasMore={hasMore}
115
+ />
116
+ </>
117
+ )}
118
+ </div>
119
+ )}
120
+ </div>
121
+ );
122
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Minimal Theme - Single Page
3
+ *
4
+ * Simple page content layout for type="page" posts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { SinglePageProps } from "../../../types.js";
9
+
10
+ export const SinglePage: FC<SinglePageProps> = ({ page }) => {
11
+ return (
12
+ <article class="h-entry">
13
+ {page.title && (
14
+ <h1 class="p-name text-2xl font-semibold mb-6">{page.title}</h1>
15
+ )}
16
+
17
+ <div
18
+ class="e-content prose"
19
+ dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
20
+ />
21
+ </article>
22
+ );
23
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Minimal Theme - Article Card
3
+ *
4
+ * Title + excerpt, borderless, for type="article" posts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineCardProps } from "../../../types.js";
9
+
10
+ export const ArticleCard: FC<TimelineCardProps> = ({ post, compact }) => {
11
+ return (
12
+ <article class={`h-entry${compact ? " text-sm" : ""}`}>
13
+ {post.title && (
14
+ <h2 class={`p-name font-semibold ${compact ? "text-sm" : "text-lg"}`}>
15
+ <a href={post.permalink} class="u-url hover:underline">
16
+ {post.title}
17
+ </a>
18
+ </h2>
19
+ )}
20
+ {!compact && post.excerpt && (
21
+ <p class="e-content text-muted-foreground mt-1 line-clamp-3">
22
+ {post.excerpt}
23
+ </p>
24
+ )}
25
+ <footer class="mt-2">
26
+ <a
27
+ href={post.permalink}
28
+ class="u-url text-xs text-muted-foreground hover:text-foreground"
29
+ >
30
+ <time class="dt-published" datetime={post.publishedAt}>
31
+ {post.publishedAtFormatted}
32
+ </time>
33
+ </a>
34
+ </footer>
35
+ </article>
36
+ );
37
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Minimal Theme - Image Card
3
+ *
4
+ * Inline images with no card frame for type="image" posts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineCardProps } from "../../../types.js";
9
+ import { MediaGallery } from "../../../theme/components/MediaGallery.js";
10
+
11
+ export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
12
+ if (compact) {
13
+ return (
14
+ <article class="h-entry text-sm">
15
+ {post.title && (
16
+ <h2 class="p-name font-medium text-sm">
17
+ <a href={post.permalink} class="u-url hover:underline">
18
+ {post.title}
19
+ </a>
20
+ </h2>
21
+ )}
22
+ {post.contentHtml && (
23
+ <div
24
+ class="e-content prose prose-sm text-muted-foreground"
25
+ dangerouslySetInnerHTML={{ __html: post.contentHtml }}
26
+ />
27
+ )}
28
+ <footer class="mt-1">
29
+ <a
30
+ href={post.permalink}
31
+ class="u-url text-xs text-muted-foreground hover:text-foreground"
32
+ >
33
+ <time class="dt-published" datetime={post.publishedAt}>
34
+ {post.publishedAtFormatted}
35
+ </time>
36
+ </a>
37
+ </footer>
38
+ </article>
39
+ );
40
+ }
41
+
42
+ return (
43
+ <article class="h-entry">
44
+ {post.contentHtml && (
45
+ <div
46
+ class="e-content prose prose-sm"
47
+ dangerouslySetInnerHTML={{ __html: post.contentHtml }}
48
+ />
49
+ )}
50
+ {post.media.length > 0 && <MediaGallery attachments={post.media} />}
51
+ <footer class="mt-2">
52
+ <a
53
+ href={post.permalink}
54
+ class="u-url text-xs text-muted-foreground hover:text-foreground"
55
+ >
56
+ <time class="dt-published" datetime={post.publishedAt}>
57
+ {post.publishedAtFormatted}
58
+ </time>
59
+ </a>
60
+ </footer>
61
+ </article>
62
+ );
63
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Minimal Theme - Link Card
3
+ *
4
+ * Subtle external link indicator for type="link" posts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineCardProps } from "../../../types.js";
9
+
10
+ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
11
+ return (
12
+ <article class={`h-entry${compact ? " text-sm" : ""}`}>
13
+ {post.title && (
14
+ <h2 class={`p-name font-semibold ${compact ? "text-sm" : "text-base"}`}>
15
+ <a
16
+ href={post.sourceUrl || post.permalink}
17
+ class="u-url hover:underline"
18
+ target={post.sourceUrl ? "_blank" : undefined}
19
+ rel={post.sourceUrl ? "noopener noreferrer" : undefined}
20
+ >
21
+ {post.title}
22
+ </a>
23
+ </h2>
24
+ )}
25
+ {post.sourceDomain && (
26
+ <div class="text-xs text-muted-foreground mt-0.5">
27
+ &#8599; {post.sourceDomain}
28
+ </div>
29
+ )}
30
+ {!compact && post.contentHtml && (
31
+ <div
32
+ class="e-content prose prose-sm text-muted-foreground mt-1"
33
+ dangerouslySetInnerHTML={{ __html: post.contentHtml }}
34
+ />
35
+ )}
36
+ <footer class="mt-2">
37
+ <a
38
+ href={post.permalink}
39
+ class="text-xs text-muted-foreground hover:text-foreground"
40
+ >
41
+ <time class="dt-published" datetime={post.publishedAt}>
42
+ {post.publishedAtFormatted}
43
+ </time>
44
+ </a>
45
+ </footer>
46
+ </article>
47
+ );
48
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Minimal Theme - Note Card
3
+ *
4
+ * Borderless, content-first card for type="note" posts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineCardProps } from "../../../types.js";
9
+ import { MediaGallery } from "../../../theme/components/MediaGallery.js";
10
+
11
+ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
12
+ return (
13
+ <article class={`h-entry${compact ? " text-sm" : ""}`}>
14
+ {post.contentHtml && (
15
+ <div
16
+ class={`e-content prose ${compact ? "prose-sm" : ""}`}
17
+ dangerouslySetInnerHTML={{ __html: post.contentHtml }}
18
+ />
19
+ )}
20
+ {!compact && post.media.length > 0 && (
21
+ <MediaGallery attachments={post.media} />
22
+ )}
23
+ <footer class="mt-2">
24
+ <a
25
+ href={post.permalink}
26
+ class="u-url text-xs text-muted-foreground hover:text-foreground"
27
+ >
28
+ <time class="dt-published" datetime={post.publishedAt}>
29
+ {post.publishedAtFormatted}
30
+ </time>
31
+ </a>
32
+ </footer>
33
+ </article>
34
+ );
35
+ };
@@ -1,24 +1,18 @@
1
1
  /**
2
- * Quote Card Component
2
+ * Minimal Theme - Quote Card
3
3
  *
4
- * Blockquote + attribution for type="quote" posts.
4
+ * Subtle blockquote with left border for type="quote" posts.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import type { TimelineCardProps } from "../../../types.js";
9
- import * as sqid from "../../../lib/sqid.js";
10
- import * as time from "../../../lib/time.js";
11
9
 
12
10
  export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
13
- const permalink = `/p/${sqid.encode(post.id)}`;
14
-
15
11
  return (
16
- <article
17
- class={`h-entry timeline-card timeline-card-quote${compact ? " timeline-card-compact" : ""}`}
18
- >
12
+ <article class={`h-entry${compact ? " text-sm" : ""}`}>
19
13
  {post.contentHtml && (
20
14
  <blockquote
21
- class={`e-content italic ${compact ? "text-sm" : "text-base"} leading-relaxed`}
15
+ class={`e-content border-l-2 border-muted-foreground/30 pl-4 italic ${compact ? "text-sm" : ""} leading-relaxed`}
22
16
  >
23
17
  <div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
24
18
  </blockquote>
@@ -40,13 +34,13 @@ export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
40
34
  )}
41
35
  </div>
42
36
  )}
43
- <footer class="mt-2 text-xs text-muted-foreground">
44
- <a href={permalink} class="u-url hover:underline">
45
- <time
46
- class="dt-published"
47
- datetime={time.toISOString(post.publishedAt)}
48
- >
49
- {time.formatDate(post.publishedAt)}
37
+ <footer class="mt-2">
38
+ <a
39
+ href={post.permalink}
40
+ class="u-url text-xs text-muted-foreground hover:text-foreground"
41
+ >
42
+ <time class="dt-published" datetime={post.publishedAt}>
43
+ {post.publishedAtFormatted}
50
44
  </time>
51
45
  </a>
52
46
  </footer>
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Minimal Theme - Thread Preview
3
+ *
4
+ * Minimal thread indicator: root post + compact replies + "show more" link.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "@lingui/react/macro";
9
+ import type { ThreadPreviewProps } from "../../../types.js";
10
+ import { TimelineItem } from "./TimelineItem.js";
11
+ import { TimelineItemFromPost } from "./TimelineItem.js";
12
+
13
+ export const ThreadPreview: FC<ThreadPreviewProps> = ({
14
+ rootPost,
15
+ previewReplies,
16
+ totalReplyCount,
17
+ theme,
18
+ }) => {
19
+ const { t } = useLingui();
20
+ const remainingCount = totalReplyCount - previewReplies.length;
21
+
22
+ return (
23
+ <div>
24
+ <TimelineItem item={{ post: rootPost }} theme={theme} />
25
+ {previewReplies.length > 0 && (
26
+ <div class="ml-4 mt-2 border-l border-border pl-4 flex flex-col gap-3">
27
+ {previewReplies.map((reply) => (
28
+ <div key={reply.id}>
29
+ <TimelineItemFromPost post={reply} compact theme={theme} />
30
+ </div>
31
+ ))}
32
+ {remainingCount > 0 && (
33
+ <a
34
+ href={rootPost.permalink}
35
+ class="text-sm text-muted-foreground hover:text-foreground hover:underline"
36
+ >
37
+ {t({
38
+ message: `Show ${remainingCount} more ${remainingCount === 1 ? "reply" : "replies"}`,
39
+ comment: "@context: Link to show remaining thread replies",
40
+ })}
41
+ </a>
42
+ )}
43
+ </div>
44
+ )}
45
+ </div>
46
+ );
47
+ };
@@ -1,43 +1,48 @@
1
1
  /**
2
- * Timeline Feed Component
2
+ * Minimal Theme - Timeline Feed
3
3
  *
4
- * Main feed wrapper with load-more button.
4
+ * Divider-separated stream of posts with load-more button.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
9
  import type { TimelineFeedProps } from "../../../types.js";
10
10
  import { TimelineItem } from "./TimelineItem.js";
11
- import { ThreadPreview } from "./ThreadPreview.js";
11
+ import { ThreadPreview as DefaultThreadPreview } from "./ThreadPreview.js";
12
12
 
13
13
  export const TimelineFeed: FC<TimelineFeedProps> = ({
14
14
  items,
15
15
  hasMore,
16
16
  nextCursor,
17
+ theme,
17
18
  }) => {
18
19
  const { t } = useLingui();
19
20
 
21
+ const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
22
+
20
23
  return (
21
24
  <div>
22
- <div id="timeline-feed" class="flex flex-col gap-4">
23
- {items.map((item) => {
24
- if (item.threadPreview) {
25
- return (
26
- <ThreadPreview
27
- key={item.post.id}
25
+ <div id="timeline-feed" class="flex flex-col">
26
+ {items.map((item, i) => (
27
+ <div key={item.post.id}>
28
+ {i > 0 && <hr class="my-6 border-border" />}
29
+ {item.threadPreview ? (
30
+ <ResolvedThreadPreview
28
31
  rootPost={item.post}
29
32
  previewReplies={item.threadPreview.replies}
30
33
  totalReplyCount={item.threadPreview.totalReplyCount}
34
+ theme={theme}
31
35
  />
32
- );
33
- }
34
- return <TimelineItem key={item.post.id} item={item} />;
35
- })}
36
+ ) : (
37
+ <TimelineItem item={item} theme={theme} />
38
+ )}
39
+ </div>
40
+ ))}
36
41
  </div>
37
42
  {hasMore && nextCursor && (
38
- <div id="load-more-container" class="mt-6 text-center">
43
+ <div id="load-more-container" class="mt-8 text-center">
39
44
  <button
40
- class="btn btn-outline"
45
+ class="text-sm text-muted-foreground hover:text-foreground hover:underline"
41
46
  data-on:click={`@get('/api/timeline?cursor=${nextCursor}')`}
42
47
  >
43
48
  {t({
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Minimal Theme - Timeline Item
3
+ *
4
+ * Dispatches to the correct card component based on post type.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type {
9
+ TimelineItemView,
10
+ TimelineCardProps,
11
+ ThemeComponents,
12
+ PostView,
13
+ } from "../../../types.js";
14
+ import { NoteCard } from "./NoteCard.js";
15
+ import { ArticleCard } from "./ArticleCard.js";
16
+ import { LinkCard } from "./LinkCard.js";
17
+ import { QuoteCard } from "./QuoteCard.js";
18
+ import { ImageCard } from "./ImageCard.js";
19
+ import type { PostType } from "../../../types.js";
20
+
21
+ const CARD_MAP: Record<PostType, FC<TimelineCardProps>> = {
22
+ note: NoteCard,
23
+ article: ArticleCard,
24
+ link: LinkCard,
25
+ quote: QuoteCard,
26
+ image: ImageCard,
27
+ page: NoteCard,
28
+ };
29
+
30
+ const THEME_KEY_MAP: Record<PostType, keyof ThemeComponents> = {
31
+ note: "NoteCard",
32
+ article: "ArticleCard",
33
+ link: "LinkCard",
34
+ quote: "QuoteCard",
35
+ image: "ImageCard",
36
+ page: "NoteCard",
37
+ };
38
+
39
+ interface TimelineItemProps {
40
+ item: TimelineItemView;
41
+ compact?: boolean;
42
+ cardOverride?: FC<TimelineCardProps>;
43
+ theme?: ThemeComponents;
44
+ }
45
+
46
+ interface TimelineItemFromPostProps {
47
+ post: PostView;
48
+ compact?: boolean;
49
+ cardOverride?: FC<TimelineCardProps>;
50
+ theme?: ThemeComponents;
51
+ }
52
+
53
+ export const TimelineItem: FC<TimelineItemProps> = ({
54
+ item,
55
+ compact,
56
+ cardOverride,
57
+ theme,
58
+ }) => {
59
+ const themeKey = THEME_KEY_MAP[item.post.type];
60
+ const themeCard = theme?.[themeKey] as FC<TimelineCardProps> | undefined;
61
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.type];
62
+ return <Card post={item.post} compact={compact} />;
63
+ };
64
+
65
+ export const TimelineItemFromPost: FC<TimelineItemFromPostProps> = ({
66
+ post,
67
+ compact,
68
+ cardOverride,
69
+ theme,
70
+ }) => {
71
+ const themeKey = THEME_KEY_MAP[post.type];
72
+ const themeCard = theme?.[themeKey] as FC<TimelineCardProps> | undefined;
73
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[post.type];
74
+ return <Card post={post} compact={compact} />;
75
+ };