@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
@@ -1,43 +0,0 @@
1
- /**
2
- * Timeline Feed Component
3
- *
4
- * Main feed wrapper 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 } from "./ThreadPreview.js";
9
- export const TimelineFeed = ({ items, hasMore, nextCursor })=>{
10
- const { i18n: $__i18n, _: $__ } = $_useLingui();
11
- return /*#__PURE__*/ _jsxs("div", {
12
- children: [
13
- /*#__PURE__*/ _jsx("div", {
14
- id: "timeline-feed",
15
- class: "flex flex-col gap-4",
16
- children: items.map((item)=>{
17
- if (item.threadPreview) {
18
- return /*#__PURE__*/ _jsx(ThreadPreview, {
19
- rootPost: item.post,
20
- previewReplies: item.threadPreview.replies,
21
- totalReplyCount: item.threadPreview.totalReplyCount
22
- }, item.post.id);
23
- }
24
- return /*#__PURE__*/ _jsx(TimelineItem, {
25
- item: item
26
- }, item.post.id);
27
- })
28
- }),
29
- hasMore && nextCursor && /*#__PURE__*/ _jsx("div", {
30
- id: "load-more-container",
31
- class: "mt-6 text-center",
32
- children: /*#__PURE__*/ _jsx("button", {
33
- class: "btn btn-outline",
34
- "data-on:click": `@get('/api/timeline?cursor=${nextCursor}')`,
35
- children: $__i18n._({
36
- id: "yQ2kGp",
37
- message: "Load more"
38
- })
39
- })
40
- })
41
- ]
42
- });
43
- };
@@ -1,25 +0,0 @@
1
- /**
2
- * Timeline Item Component
3
- *
4
- * Dispatches to the correct card component based on post type.
5
- */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
6
- import { NoteCard } from "./NoteCard.js";
7
- import { ArticleCard } from "./ArticleCard.js";
8
- import { LinkCard } from "./LinkCard.js";
9
- import { QuoteCard } from "./QuoteCard.js";
10
- import { ImageCard } from "./ImageCard.js";
11
- const CARD_MAP = {
12
- note: NoteCard,
13
- article: ArticleCard,
14
- link: LinkCard,
15
- quote: QuoteCard,
16
- image: ImageCard,
17
- page: NoteCard
18
- };
19
- export const TimelineItem = ({ item, compact, cardOverride })=>{
20
- const Card = cardOverride ?? CARD_MAP[item.post.type];
21
- return /*#__PURE__*/ _jsx(Card, {
22
- post: item.post,
23
- compact: compact
24
- });
25
- };
@@ -1,8 +0,0 @@
1
- export { NoteCard } from "./NoteCard.js";
2
- export { ArticleCard } from "./ArticleCard.js";
3
- export { LinkCard } from "./LinkCard.js";
4
- export { QuoteCard } from "./QuoteCard.js";
5
- export { ImageCard } from "./ImageCard.js";
6
- export { ThreadPreview } from "./ThreadPreview.js";
7
- export { TimelineItem } from "./TimelineItem.js";
8
- export { TimelineFeed } from "./TimelineFeed.js";
@@ -1,160 +0,0 @@
1
- /**
2
- * Site Layout
3
- *
4
- * Two-column layout for public pages with sidebar navigation.
5
- * On mobile, uses a slide-out drawer menu.
6
- */ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
7
- /**
8
- * Determine if a navigation link is active based on the current path.
9
- *
10
- * @param linkUrl - The link's URL
11
- * @param currentPath - The current page path
12
- * @returns Whether the link should be shown as active
13
- */ function isLinkActive(linkUrl, currentPath) {
14
- // External links are never active
15
- if (linkUrl.startsWith("http://") || linkUrl.startsWith("https://")) {
16
- return false;
17
- }
18
- // Exact match for home
19
- if (linkUrl === "/") {
20
- return currentPath === "/";
21
- }
22
- // Prefix match for other internal links
23
- return currentPath === linkUrl || currentPath.startsWith(linkUrl + "/");
24
- }
25
- /**
26
- * Check if a URL is external
27
- */ function isExternalUrl(url) {
28
- return url.startsWith("http://") || url.startsWith("https://");
29
- }
30
- /**
31
- * Render navigation links with dot indicator for active state.
32
- */ function NavLinks({ navigationLinks, currentPath }) {
33
- return /*#__PURE__*/ _jsx(_Fragment, {
34
- children: navigationLinks.map((link)=>{
35
- const active = isLinkActive(link.url, currentPath);
36
- const external = isExternalUrl(link.url);
37
- return /*#__PURE__*/ _jsxs("a", {
38
- href: link.url,
39
- class: `text-sm flex items-center gap-2 py-0.5 ${active ? "text-primary font-medium" : "text-muted-foreground hover:text-foreground"}`,
40
- ...external ? {
41
- target: "_blank",
42
- rel: "noopener noreferrer"
43
- } : {},
44
- children: [
45
- /*#__PURE__*/ _jsx("span", {
46
- class: `size-1.5 rounded-full shrink-0 ${active ? "bg-primary" : "bg-transparent"}`
47
- }),
48
- link.label,
49
- external && /*#__PURE__*/ _jsx("span", {
50
- class: "ml-1 text-xs opacity-50",
51
- children: "↗"
52
- })
53
- ]
54
- }, link.id);
55
- })
56
- });
57
- }
58
- export const SiteLayout = ({ siteName, navigationLinks, currentPath, children })=>{
59
- return /*#__PURE__*/ _jsxs("div", {
60
- class: "container py-8 md:flex md:gap-12",
61
- "data-signals": JSON.stringify({
62
- _drawerOpen: false
63
- }),
64
- children: [
65
- /*#__PURE__*/ _jsxs("div", {
66
- class: "flex items-center justify-between mb-6 md:hidden",
67
- children: [
68
- /*#__PURE__*/ _jsx("a", {
69
- href: "/",
70
- class: "text-xl font-semibold",
71
- children: siteName
72
- }),
73
- /*#__PURE__*/ _jsx("button", {
74
- "data-on:click": "$_drawerOpen = true",
75
- class: "p-2 -mr-2 text-muted-foreground hover:text-foreground",
76
- "aria-label": "Open menu",
77
- children: /*#__PURE__*/ _jsx("svg", {
78
- class: "size-5",
79
- fill: "none",
80
- viewBox: "0 0 24 24",
81
- "stroke-width": "1.5",
82
- stroke: "currentColor",
83
- children: /*#__PURE__*/ _jsx("path", {
84
- "stroke-linecap": "round",
85
- "stroke-linejoin": "round",
86
- d: "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
87
- })
88
- })
89
- })
90
- ]
91
- }),
92
- /*#__PURE__*/ _jsx("div", {
93
- class: "fixed inset-0 bg-black/50 z-40 opacity-0 pointer-events-none transition-opacity duration-300 ease-in-out md:hidden",
94
- "data-class": "{'opacity-100 pointer-events-auto': $_drawerOpen, 'opacity-0 pointer-events-none': !$_drawerOpen}",
95
- "data-on:click": "$_drawerOpen = false"
96
- }),
97
- /*#__PURE__*/ _jsxs("aside", {
98
- class: "fixed inset-y-0 left-0 w-64 bg-background z-50 p-6 overflow-y-auto shadow-lg -translate-x-full transition-transform duration-300 ease-in-out md:hidden",
99
- "data-class": "{'translate-x-0': $_drawerOpen, '-translate-x-full': !$_drawerOpen}",
100
- children: [
101
- /*#__PURE__*/ _jsxs("div", {
102
- class: "flex items-center justify-between mb-8",
103
- children: [
104
- /*#__PURE__*/ _jsx("a", {
105
- href: "/",
106
- class: "text-xl font-semibold",
107
- children: siteName
108
- }),
109
- /*#__PURE__*/ _jsx("button", {
110
- "data-on:click": "$_drawerOpen = false",
111
- class: "p-2 -mr-2 text-muted-foreground hover:text-foreground",
112
- "aria-label": "Close menu",
113
- children: /*#__PURE__*/ _jsx("svg", {
114
- class: "size-5",
115
- fill: "none",
116
- viewBox: "0 0 24 24",
117
- "stroke-width": "1.5",
118
- stroke: "currentColor",
119
- children: /*#__PURE__*/ _jsx("path", {
120
- "stroke-linecap": "round",
121
- "stroke-linejoin": "round",
122
- d: "M6 18L18 6M6 6l12 12"
123
- })
124
- })
125
- })
126
- ]
127
- }),
128
- /*#__PURE__*/ _jsx("nav", {
129
- class: "flex flex-col gap-0.5",
130
- children: /*#__PURE__*/ _jsx(NavLinks, {
131
- navigationLinks: navigationLinks,
132
- currentPath: currentPath
133
- })
134
- })
135
- ]
136
- }),
137
- /*#__PURE__*/ _jsxs("aside", {
138
- class: "hidden md:block md:w-48 md:shrink-0 md:sticky md:top-8 md:self-start",
139
- children: [
140
- /*#__PURE__*/ _jsx("a", {
141
- href: "/",
142
- class: "text-xl font-semibold block mb-20",
143
- children: siteName
144
- }),
145
- /*#__PURE__*/ _jsx("nav", {
146
- class: "flex flex-col gap-0.5",
147
- children: /*#__PURE__*/ _jsx(NavLinks, {
148
- navigationLinks: navigationLinks,
149
- currentPath: currentPath
150
- })
151
- })
152
- ]
153
- }),
154
- /*#__PURE__*/ _jsx("main", {
155
- class: "flex-1 min-w-0",
156
- children: children
157
- })
158
- ]
159
- });
160
- };
@@ -1,57 +0,0 @@
1
- /**
2
- * Article Card Component
3
- *
4
- * Prominent title + excerpt for type="article" posts.
5
- */
6
-
7
- import type { FC } from "hono/jsx";
8
- import type { TimelineCardProps } from "../../../types.js";
9
- import * as sqid from "../../../lib/sqid.js";
10
- import * as time from "../../../lib/time.js";
11
-
12
- export const ArticleCard: FC<TimelineCardProps> = ({ post, compact }) => {
13
- const permalink = `/p/${sqid.encode(post.id)}`;
14
- const excerpt = post.content
15
- ? post.content.length > 160
16
- ? post.content.slice(0, 160) + "..."
17
- : post.content
18
- : null;
19
-
20
- return (
21
- <article
22
- class={`h-entry timeline-card${compact ? " timeline-card-compact" : ""}`}
23
- >
24
- {post.title && (
25
- <h2
26
- class={`p-name font-semibold ${compact ? "text-sm" : "text-lg"} mb-1`}
27
- >
28
- <a href={permalink} class="u-url hover:underline">
29
- {post.title}
30
- </a>
31
- </h2>
32
- )}
33
- {!compact && excerpt && (
34
- <p class="e-content text-sm text-muted-foreground line-clamp-3">
35
- {excerpt}
36
- </p>
37
- )}
38
- <footer class="mt-2 text-xs text-muted-foreground">
39
- <a href={permalink} class="u-url hover:underline">
40
- <time
41
- class="dt-published"
42
- datetime={time.toISOString(post.publishedAt)}
43
- >
44
- {time.formatDate(post.publishedAt)}
45
- </time>
46
- </a>
47
- {!compact && (
48
- <span class="ml-2">
49
- <a href={permalink} class="hover:underline">
50
- Read more &rarr;
51
- </a>
52
- </span>
53
- )}
54
- </footer>
55
- </article>
56
- );
57
- };
@@ -1,80 +0,0 @@
1
- /**
2
- * Image Card Component
3
- *
4
- * Image-first layout for type="image" posts.
5
- */
6
-
7
- import type { FC } from "hono/jsx";
8
- import type { TimelineCardProps } from "../../../types.js";
9
- import { MediaGallery } from "../MediaGallery.js";
10
- import * as sqid from "../../../lib/sqid.js";
11
- import * as time from "../../../lib/time.js";
12
-
13
- export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
14
- const permalink = `/p/${sqid.encode(post.id)}`;
15
-
16
- if (compact) {
17
- return (
18
- <article class="h-entry timeline-card timeline-card-compact">
19
- {post.title && (
20
- <h2 class="p-name text-sm font-medium mb-1">
21
- <a href={permalink} class="u-url hover:underline">
22
- {post.title}
23
- </a>
24
- </h2>
25
- )}
26
- {post.contentHtml && (
27
- <div
28
- class="e-content prose prose-sm text-muted-foreground"
29
- dangerouslySetInnerHTML={{ __html: post.contentHtml }}
30
- />
31
- )}
32
- <footer class="mt-1 text-xs text-muted-foreground">
33
- <a href={permalink} class="u-url hover:underline">
34
- <time
35
- class="dt-published"
36
- datetime={time.toISOString(post.publishedAt)}
37
- >
38
- {time.formatDate(post.publishedAt)}
39
- </time>
40
- </a>
41
- </footer>
42
- </article>
43
- );
44
- }
45
-
46
- return (
47
- <article class="h-entry timeline-card timeline-card-image">
48
- {post.mediaAttachments.length > 0 && (
49
- <div class="timeline-card-image-gallery">
50
- <MediaGallery attachments={post.mediaAttachments} />
51
- </div>
52
- )}
53
- <div class="p-4">
54
- {post.title && (
55
- <h2 class="p-name font-medium mb-1">
56
- <a href={permalink} class="u-url hover:underline">
57
- {post.title}
58
- </a>
59
- </h2>
60
- )}
61
- {post.contentHtml && (
62
- <div
63
- class="e-content prose prose-sm"
64
- dangerouslySetInnerHTML={{ __html: post.contentHtml }}
65
- />
66
- )}
67
- <footer class="mt-2 text-xs text-muted-foreground">
68
- <a href={permalink} class="u-url hover:underline">
69
- <time
70
- class="dt-published"
71
- datetime={time.toISOString(post.publishedAt)}
72
- >
73
- {time.formatDate(post.publishedAt)}
74
- </time>
75
- </a>
76
- </footer>
77
- </div>
78
- </article>
79
- );
80
- };
@@ -1,66 +0,0 @@
1
- /**
2
- * Link Card Component
3
- *
4
- * External link emphasis for type="link" posts.
5
- */
6
-
7
- import type { FC } from "hono/jsx";
8
- import type { TimelineCardProps } from "../../../types.js";
9
- import * as sqid from "../../../lib/sqid.js";
10
- import * as time from "../../../lib/time.js";
11
-
12
- export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
13
- const permalink = `/p/${sqid.encode(post.id)}`;
14
-
15
- return (
16
- <article
17
- class={`h-entry timeline-card timeline-card-link${compact ? " timeline-card-compact" : ""}`}
18
- >
19
- {post.sourceDomain && (
20
- <div class="text-xs text-muted-foreground mb-1 flex items-center gap-1">
21
- <svg
22
- class="size-3"
23
- xmlns="http://www.w3.org/2000/svg"
24
- fill="none"
25
- viewBox="0 0 24 24"
26
- stroke-width="2"
27
- stroke="currentColor"
28
- >
29
- <path 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" />
30
- </svg>
31
- <span>{post.sourceDomain}</span>
32
- </div>
33
- )}
34
- {post.title && (
35
- <h2
36
- class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
37
- >
38
- <a
39
- href={post.sourceUrl || permalink}
40
- class="u-url hover:underline"
41
- target={post.sourceUrl ? "_blank" : undefined}
42
- rel={post.sourceUrl ? "noopener noreferrer" : undefined}
43
- >
44
- {post.title}
45
- </a>
46
- </h2>
47
- )}
48
- {!compact && post.contentHtml && (
49
- <div
50
- class="e-content prose prose-sm text-muted-foreground"
51
- dangerouslySetInnerHTML={{ __html: post.contentHtml }}
52
- />
53
- )}
54
- <footer class="mt-2 text-xs text-muted-foreground">
55
- <a href={permalink} class="hover:underline">
56
- <time
57
- class="dt-published"
58
- datetime={time.toISOString(post.publishedAt)}
59
- >
60
- {time.formatDate(post.publishedAt)}
61
- </time>
62
- </a>
63
- </footer>
64
- </article>
65
- );
66
- };
@@ -1,41 +0,0 @@
1
- /**
2
- * Note Card Component
3
- *
4
- * Text-first, minimal 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 "../MediaGallery.js";
10
- import * as sqid from "../../../lib/sqid.js";
11
- import * as time from "../../../lib/time.js";
12
-
13
- export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
14
- const permalink = `/p/${sqid.encode(post.id)}`;
15
-
16
- return (
17
- <article
18
- class={`h-entry timeline-card${compact ? " timeline-card-compact" : ""}`}
19
- >
20
- {post.contentHtml && (
21
- <div
22
- class={`e-content prose ${compact ? "prose-sm" : "prose-sm"}`}
23
- dangerouslySetInnerHTML={{ __html: post.contentHtml }}
24
- />
25
- )}
26
- {!compact && post.mediaAttachments.length > 0 && (
27
- <MediaGallery attachments={post.mediaAttachments} />
28
- )}
29
- <footer class="mt-2 text-xs text-muted-foreground">
30
- <a href={permalink} class="u-url hover:underline">
31
- <time
32
- class="dt-published"
33
- datetime={time.toISOString(post.publishedAt)}
34
- >
35
- {time.formatDate(post.publishedAt)}
36
- </time>
37
- </a>
38
- </footer>
39
- </article>
40
- );
41
- };
@@ -1,49 +0,0 @@
1
- /**
2
- * Thread Preview Component
3
- *
4
- * Inline thread preview: root card + 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 * as sqid from "../../../lib/sqid.js";
12
-
13
- export const ThreadPreview: FC<ThreadPreviewProps> = ({
14
- rootPost,
15
- previewReplies,
16
- totalReplyCount,
17
- }) => {
18
- const { t } = useLingui();
19
- const permalink = `/p/${sqid.encode(rootPost.id)}`;
20
- const remainingCount = totalReplyCount - previewReplies.length;
21
-
22
- return (
23
- <div class="timeline-thread">
24
- <TimelineItem item={{ post: rootPost }} />
25
- {previewReplies.length > 0 && (
26
- <div class="timeline-thread-replies">
27
- {previewReplies.map((reply) => (
28
- <div key={reply.id} class="timeline-thread-reply">
29
- <TimelineItem item={{ post: reply }} compact />
30
- </div>
31
- ))}
32
- {remainingCount > 0 && (
33
- <div class="timeline-thread-reply">
34
- <a
35
- href={permalink}
36
- class="text-sm text-muted-foreground hover:text-foreground hover:underline"
37
- >
38
- {t({
39
- message: `Show ${remainingCount} more ${remainingCount === 1 ? "reply" : "replies"}`,
40
- comment: "@context: Link to show remaining thread replies",
41
- })}
42
- </a>
43
- </div>
44
- )}
45
- </div>
46
- )}
47
- </div>
48
- );
49
- };
@@ -1,39 +0,0 @@
1
- /**
2
- * Timeline Item Component
3
- *
4
- * Dispatches to the correct card component based on post type.
5
- */
6
-
7
- import type { FC } from "hono/jsx";
8
- import type { TimelineItemData, TimelineCardProps } from "../../../types.js";
9
- import { NoteCard } from "./NoteCard.js";
10
- import { ArticleCard } from "./ArticleCard.js";
11
- import { LinkCard } from "./LinkCard.js";
12
- import { QuoteCard } from "./QuoteCard.js";
13
- import { ImageCard } from "./ImageCard.js";
14
- import type { PostType } from "../../../types.js";
15
-
16
- const CARD_MAP: Record<PostType, FC<TimelineCardProps>> = {
17
- note: NoteCard,
18
- article: ArticleCard,
19
- link: LinkCard,
20
- quote: QuoteCard,
21
- image: ImageCard,
22
- page: NoteCard,
23
- };
24
-
25
- interface TimelineItemProps {
26
- item: TimelineItemData;
27
- compact?: boolean;
28
- /** Override card component (for theme overrides) */
29
- cardOverride?: FC<TimelineCardProps>;
30
- }
31
-
32
- export const TimelineItem: FC<TimelineItemProps> = ({
33
- item,
34
- compact,
35
- cardOverride,
36
- }) => {
37
- const Card = cardOverride ?? CARD_MAP[item.post.type];
38
- return <Card post={item.post} compact={compact} />;
39
- };
@@ -1,8 +0,0 @@
1
- export { NoteCard } from "./NoteCard.js";
2
- export { ArticleCard } from "./ArticleCard.js";
3
- export { LinkCard } from "./LinkCard.js";
4
- export { QuoteCard } from "./QuoteCard.js";
5
- export { ImageCard } from "./ImageCard.js";
6
- export { ThreadPreview } from "./ThreadPreview.js";
7
- export { TimelineItem } from "./TimelineItem.js";
8
- export { TimelineFeed } from "./TimelineFeed.js";