@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,336 @@
1
+ /**
2
+ * Threads theme styles
3
+ *
4
+ * Design tokens extracted from threads.com source CSS.
5
+ * Key variables from Threads' --barcelona-* system:
6
+ * --barcelona-side-navigation-width: 76px
7
+ * --barcelona-column-layout-max-width: 640px
8
+ * --barcelona-columns-item-horizontal-padding: 24px
9
+ * --barcelona-primary-column-outline: rgb(213,213,213) / rgb(45,45,45)
10
+ * --barcelona-threadline: rgb(229,229,229) / rgb(51,54,56)
11
+ * --barcelona-navigation-icon: rgb(77,77,77) / rgb(184,184,184)
12
+ * --barcelona-navigation-item-hover-background: rgba(0,0,0,0.035)
13
+ */
14
+
15
+ /* Tell Tailwind to scan threads theme source files for class usage */
16
+ @source "./";
17
+
18
+ /* =========================================================================
19
+ * Threads Design Tokens (as CSS custom properties)
20
+ * ========================================================================= */
21
+
22
+ :root {
23
+ --threads-sidebar-width: 76px;
24
+ --threads-column-max-width: 640px;
25
+ --threads-column-padding: 24px;
26
+ --threads-column-outline: rgb(213, 213, 213);
27
+ --threads-threadline: rgb(229, 229, 229);
28
+ --threads-page-bg: rgb(250, 250, 250);
29
+ --threads-elevated-bg: rgb(255, 255, 255);
30
+ --threads-nav-icon: rgb(77, 77, 77);
31
+ --threads-nav-icon-active: rgb(0, 0, 0);
32
+ --threads-nav-hover-bg: rgba(0, 0, 0, 0.035);
33
+ --threads-text-primary: rgb(0, 0, 0);
34
+ --threads-text-secondary: rgb(119, 119, 119);
35
+ --threads-media-outline: rgba(0, 0, 0, 0.15);
36
+ --threads-divider: rgba(0, 0, 0, 0.15);
37
+ --threads-container-radius: 24px;
38
+ --threads-mobile-header-height: 60px;
39
+ }
40
+
41
+ @media (prefers-color-scheme: dark) {
42
+ :root {
43
+ --threads-column-outline: rgb(45, 45, 45);
44
+ --threads-threadline: rgb(51, 54, 56);
45
+ --threads-page-bg: rgb(10, 10, 10);
46
+ --threads-elevated-bg: rgb(24, 24, 24);
47
+ --threads-nav-icon: rgb(184, 184, 184);
48
+ --threads-nav-icon-active: rgb(243, 245, 247);
49
+ --threads-nav-hover-bg: rgba(255, 255, 255, 0.08);
50
+ --threads-text-primary: rgb(243, 245, 247);
51
+ --threads-text-secondary: rgb(153, 153, 153);
52
+ --threads-media-outline: rgba(243, 245, 247, 0.2);
53
+ --threads-divider: rgba(255, 255, 255, 0.15);
54
+ }
55
+ }
56
+
57
+ .dark {
58
+ --threads-column-outline: rgb(45, 45, 45);
59
+ --threads-threadline: rgb(51, 54, 56);
60
+ --threads-page-bg: rgb(10, 10, 10);
61
+ --threads-elevated-bg: rgb(24, 24, 24);
62
+ --threads-nav-icon: rgb(184, 184, 184);
63
+ --threads-nav-icon-active: rgb(243, 245, 247);
64
+ --threads-nav-hover-bg: rgba(255, 255, 255, 0.08);
65
+ --threads-text-primary: rgb(243, 245, 247);
66
+ --threads-text-secondary: rgb(153, 153, 153);
67
+ --threads-media-outline: rgba(243, 245, 247, 0.2);
68
+ --threads-divider: rgba(255, 255, 255, 0.15);
69
+ }
70
+
71
+ @layer components {
72
+ /* =========================================================================
73
+ * Page Layout
74
+ * ========================================================================= */
75
+
76
+ .threads-page {
77
+ min-height: 100vh;
78
+ min-height: 100dvh;
79
+ background-color: var(--threads-page-bg);
80
+ }
81
+
82
+ /* --- Left sidebar (desktop only) ---------------------------------------- */
83
+
84
+ .threads-sidebar {
85
+ position: fixed;
86
+ inset: 0 auto 0 0;
87
+ z-index: 30;
88
+ display: none;
89
+ flex-direction: column;
90
+ align-items: center;
91
+ width: var(--threads-sidebar-width);
92
+ padding: 12px 0;
93
+ background-color: var(--threads-page-bg);
94
+ }
95
+
96
+ @media (min-width: 700px) {
97
+ .threads-sidebar {
98
+ display: flex;
99
+ }
100
+ }
101
+
102
+ .threads-logo {
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ width: 48px;
107
+ height: 48px;
108
+ margin-bottom: 4px;
109
+ color: var(--threads-nav-icon-active);
110
+ }
111
+
112
+ .threads-sidebar-link {
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: center;
116
+ width: 48px;
117
+ height: 48px;
118
+ border-radius: 10px;
119
+ color: var(--threads-nav-icon);
120
+ transition:
121
+ background-color 0.15s,
122
+ color 0.15s;
123
+ }
124
+
125
+ .threads-sidebar-link:hover {
126
+ color: var(--threads-nav-icon-active);
127
+ background-color: var(--threads-nav-hover-bg);
128
+ }
129
+
130
+ .threads-sidebar-link-active {
131
+ color: var(--threads-nav-icon-active);
132
+ }
133
+
134
+ /* --- Main content area -------------------------------------------------- */
135
+
136
+ .threads-main {
137
+ padding-bottom: var(--threads-mobile-header-height);
138
+ }
139
+
140
+ @media (min-width: 700px) {
141
+ .threads-main {
142
+ padding-bottom: 0;
143
+ padding-left: var(--threads-sidebar-width);
144
+ }
145
+ }
146
+
147
+ .threads-container {
148
+ max-width: var(--threads-column-max-width);
149
+ margin: 0 auto;
150
+ }
151
+
152
+ /* Mobile: full-bleed white, no border-radius */
153
+ .threads-content {
154
+ background-color: var(--threads-elevated-bg);
155
+ padding: 0 var(--threads-column-padding);
156
+ }
157
+
158
+ /* Desktop: rounded white card on gray background */
159
+ @media (min-width: 700px) {
160
+ .threads-container {
161
+ padding: 16px 24px 0;
162
+ }
163
+
164
+ .threads-content {
165
+ border-radius: var(--threads-container-radius);
166
+ border: 0.5px solid var(--threads-column-outline);
167
+ padding: 0 var(--threads-column-padding);
168
+ min-height: calc(100vh - 32px);
169
+ }
170
+ }
171
+
172
+ /* --- Home page: each date group as its own card ------------------------ */
173
+
174
+ .threads-content:has(#timeline-feed) {
175
+ background-color: transparent;
176
+ padding: 0;
177
+ }
178
+
179
+ .threads-content #timeline-feed {
180
+ display: flex;
181
+ flex-direction: column;
182
+ gap: 4px;
183
+ }
184
+
185
+ .threads-content #timeline-feed > div {
186
+ background-color: var(--threads-elevated-bg);
187
+ padding: 0 var(--threads-column-padding) 16px;
188
+ }
189
+
190
+ @media (min-width: 700px) {
191
+ .threads-content:has(#timeline-feed) {
192
+ border: none;
193
+ border-radius: 0;
194
+ min-height: auto;
195
+ }
196
+
197
+ .threads-content #timeline-feed {
198
+ gap: 16px;
199
+ }
200
+
201
+ .threads-content #timeline-feed > div {
202
+ border-radius: var(--threads-container-radius);
203
+ border: 0.5px solid var(--threads-column-outline);
204
+ }
205
+ }
206
+
207
+ /* --- Mobile bottom tab bar ---------------------------------------------- */
208
+
209
+ .threads-mobile-tabs {
210
+ position: fixed;
211
+ inset: auto 0 0 0;
212
+ z-index: 30;
213
+ display: flex;
214
+ height: var(--threads-mobile-header-height);
215
+ background-color: var(--threads-elevated-bg);
216
+ border-top: 0.5px solid var(--threads-column-outline);
217
+ }
218
+
219
+ @media (min-width: 700px) {
220
+ .threads-mobile-tabs {
221
+ display: none;
222
+ }
223
+ }
224
+
225
+ .threads-mobile-tab {
226
+ display: flex;
227
+ align-items: center;
228
+ justify-content: center;
229
+ flex: 1;
230
+ color: var(--threads-nav-icon);
231
+ transition: color 0.15s;
232
+ }
233
+
234
+ .threads-mobile-tab-active {
235
+ color: var(--threads-nav-icon-active);
236
+ }
237
+
238
+ /* =========================================================================
239
+ * Timeline & Post Components
240
+ * ========================================================================= */
241
+
242
+ /* Date group header — centered, muted label at top of each card */
243
+ .threads-date-header {
244
+ padding: 16px 0 8px;
245
+ text-align: center;
246
+ font-size: 13px;
247
+ color: var(--threads-text-secondary);
248
+ }
249
+
250
+ /* Post divider — full-width 0.5px line matching Threads.
251
+ * Targets the <hr class="border-border my-5"> used by both
252
+ * TimelineFeed (initial render) and the SSE load-more handler. */
253
+ .threads-content hr.border-border {
254
+ border: none;
255
+ border-top: 0.5px solid var(--threads-divider);
256
+ margin-left: calc(-1 * var(--threads-column-padding));
257
+ margin-right: calc(-1 * var(--threads-column-padding));
258
+ }
259
+
260
+ /* Thread reply container with vertical connector line */
261
+ .threads-replies {
262
+ position: relative;
263
+ margin-left: 16px;
264
+ padding-left: 16px;
265
+ margin-top: 8px;
266
+ }
267
+
268
+ .threads-replies::before {
269
+ content: "";
270
+ position: absolute;
271
+ left: 0;
272
+ top: 0;
273
+ bottom: 0;
274
+ width: 2px;
275
+ background-color: var(--threads-threadline);
276
+ }
277
+
278
+ /* Individual reply in thread */
279
+ .threads-reply {
280
+ padding: 12px 0;
281
+ }
282
+
283
+ .threads-reply + .threads-reply {
284
+ border-top: 0.5px solid var(--threads-divider);
285
+ }
286
+
287
+ /* Compact text for thread replies */
288
+ .threads-compact {
289
+ @apply text-sm;
290
+ }
291
+
292
+ /* Media — rounded corners + subtle outline like Threads */
293
+ .threads-media img {
294
+ border-radius: 8px;
295
+ outline: 1px solid var(--threads-media-outline);
296
+ outline-offset: -1px;
297
+ }
298
+
299
+ /* Image carousel — horizontal scroll like Threads.net */
300
+ .threads-carousel-track {
301
+ display: flex;
302
+ overflow-x: auto;
303
+ gap: 4px;
304
+ margin-inline: calc(-1 * var(--threads-column-padding));
305
+ padding-inline: var(--threads-column-padding);
306
+ scrollbar-width: none; /* Firefox */
307
+ -ms-overflow-style: none; /* IE/Edge */
308
+ }
309
+ .threads-carousel-track::-webkit-scrollbar {
310
+ display: none; /* Chrome/Safari */
311
+ }
312
+ .threads-carousel-item {
313
+ flex: 0 0 auto;
314
+ width: 224px;
315
+ height: 280px;
316
+ }
317
+ .threads-carousel-img {
318
+ width: 100%;
319
+ height: 100%;
320
+ object-fit: cover;
321
+ }
322
+
323
+ /* Link preview styling */
324
+ .threads-link-preview {
325
+ border-radius: 12px;
326
+ border: 0.5px solid var(--threads-column-outline);
327
+ padding: 12px 16px;
328
+ margin-top: 8px;
329
+ }
330
+
331
+ /* Quote block styling */
332
+ .threads-quote {
333
+ padding-left: 12px;
334
+ border-left: 2px solid var(--threads-text-secondary);
335
+ }
336
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Threads Theme - Link Card
3
+ *
4
+ * Compact link preview box — date is shown at the feed level as a group header.
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
+ // Extract domain from URL for display
12
+ let domain: string | undefined;
13
+ if (post.url) {
14
+ try {
15
+ domain = new URL(post.url).hostname.replace(/^www\./, "");
16
+ } catch {
17
+ // Invalid URL, skip domain display
18
+ }
19
+ }
20
+
21
+ return (
22
+ <article class={`h-entry${compact ? " threads-compact" : ""}`}>
23
+ {domain && (
24
+ <div class="text-xs text-muted-foreground mb-1 flex items-center gap-1">
25
+ <svg
26
+ class="size-3"
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ fill="none"
29
+ viewBox="0 0 24 24"
30
+ stroke-width="2"
31
+ stroke="currentColor"
32
+ >
33
+ <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" />
34
+ </svg>
35
+ <span>{domain}</span>
36
+ </div>
37
+ )}
38
+ {post.title && (
39
+ <h2
40
+ class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
41
+ >
42
+ <a
43
+ href={post.url || post.permalink}
44
+ class="u-url hover:underline"
45
+ target={post.url ? "_blank" : undefined}
46
+ rel={post.url ? "noopener noreferrer" : undefined}
47
+ >
48
+ {post.title}
49
+ </a>
50
+ </h2>
51
+ )}
52
+ {!compact && post.bodyHtml && (
53
+ <div
54
+ class="e-content prose text-muted-foreground"
55
+ dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
56
+ />
57
+ )}
58
+ <footer class="mt-2 text-xs text-muted-foreground">
59
+ <a href={post.permalink} class="hover:underline">
60
+ <time class="dt-published" datetime={post.publishedAt}>
61
+ {post.publishedAtFormatted}
62
+ </time>
63
+ </a>
64
+ </footer>
65
+ </article>
66
+ );
67
+ };
@@ -0,0 +1,58 @@
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
+ */
7
+
8
+ import type { FC } from "hono/jsx";
9
+ import type { TimelineCardProps } from "../../../types.js";
10
+ import { MediaGallery } from "../../../theme/index.js";
11
+
12
+ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
13
+ const isArticle = !!post.title;
14
+ const displayHtml = isArticle ? post.summaryHtml : post.bodyHtml;
15
+
16
+ return (
17
+ <article class={`h-entry${compact ? " threads-compact" : ""}`}>
18
+ {isArticle && (
19
+ <h2
20
+ class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
21
+ >
22
+ <a href={post.permalink} class="u-url hover:underline">
23
+ {post.title}
24
+ </a>
25
+ </h2>
26
+ )}
27
+ {displayHtml && (
28
+ <div
29
+ class={`e-content prose ${compact ? "prose-sm" : isArticle ? "text-muted-foreground" : ""}`}
30
+ dangerouslySetInnerHTML={{ __html: displayHtml }}
31
+ />
32
+ )}
33
+ {!compact && post.media.length > 0 && (
34
+ <div class="threads-media mt-3">
35
+ <MediaGallery attachments={post.media} />
36
+ </div>
37
+ )}
38
+ {!compact && isArticle && post.summaryHasMore && (
39
+ <a
40
+ href={post.permalink}
41
+ class="text-sm text-muted-foreground hover:underline mt-1 inline-block"
42
+ >
43
+ Read more →
44
+ </a>
45
+ )}
46
+ <footer class="mt-2">
47
+ <a
48
+ href={post.permalink}
49
+ class="u-url text-xs text-muted-foreground hover:underline"
50
+ >
51
+ <time class="dt-published" datetime={post.publishedAt}>
52
+ {post.publishedAtRelative}
53
+ </time>
54
+ </a>
55
+ </footer>
56
+ </article>
57
+ );
58
+ };
@@ -0,0 +1,63 @@
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
+ */
12
+
13
+ import type { FC } from "hono/jsx";
14
+ import type { TimelineCardProps } from "../../../types.js";
15
+
16
+ export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
17
+ return (
18
+ <article class={`h-entry${compact ? " threads-compact" : ""}`}>
19
+ {post.quoteText && (
20
+ <blockquote class="threads-quote">
21
+ <div
22
+ class={`e-content ${compact ? "text-sm" : "text-base"} leading-relaxed`}
23
+ >
24
+ {post.quoteText}
25
+ </div>
26
+ </blockquote>
27
+ )}
28
+ {!compact && (post.title || post.url) && (
29
+ <div class="mt-2 text-sm text-muted-foreground">
30
+ &mdash;{" "}
31
+ {post.url ? (
32
+ <a
33
+ href={post.url}
34
+ class="hover:underline"
35
+ target="_blank"
36
+ rel="noopener noreferrer"
37
+ >
38
+ {post.title || "Source"}
39
+ </a>
40
+ ) : (
41
+ <span>{post.title}</span>
42
+ )}
43
+ </div>
44
+ )}
45
+ {!compact && post.bodyHtml && (
46
+ <div
47
+ class="mt-3 prose text-muted-foreground"
48
+ dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
49
+ />
50
+ )}
51
+ <footer class="mt-2">
52
+ <a
53
+ href={post.permalink}
54
+ class="u-url text-xs text-muted-foreground hover:underline"
55
+ >
56
+ <time class="dt-published" datetime={post.publishedAt}>
57
+ {post.publishedAtRelative}
58
+ </time>
59
+ </a>
60
+ </footer>
61
+ </article>
62
+ );
63
+ };
@@ -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
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
@@ -23,22 +23,24 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
23
23
  <div>
24
24
  <TimelineItem item={{ post: rootPost }} theme={theme} />
25
25
  {previewReplies.length > 0 && (
26
- <div class="ml-4 mt-2 border-l border-border pl-4 flex flex-col gap-3">
26
+ <div class="threads-replies">
27
27
  {previewReplies.map((reply) => (
28
- <div key={reply.id}>
28
+ <div key={reply.id} class="threads-reply">
29
29
  <TimelineItemFromPost post={reply} compact theme={theme} />
30
30
  </div>
31
31
  ))}
32
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>
33
+ <div class="threads-reply">
34
+ <a
35
+ href={rootPost.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>
42
44
  )}
43
45
  </div>
44
46
  )}
@@ -0,0 +1,62 @@
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
+ */
7
+
8
+ import type { FC } from "hono/jsx";
9
+ import type { TimelineFeedProps } from "../../../types.js";
10
+ import { TimelineItem } from "./TimelineItem.js";
11
+ import { ThreadPreview as DefaultThreadPreview } from "./ThreadPreview.js";
12
+ import { TimelineLoadMore as DefaultTimelineLoadMore } from "./TimelineLoadMore.js";
13
+ import { groupByDate } from "./groupByDate.js";
14
+
15
+ export const TimelineFeed: FC<TimelineFeedProps> = ({
16
+ items,
17
+ hasMore,
18
+ nextCursor,
19
+ theme,
20
+ }) => {
21
+ const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
22
+ const ResolvedLoadMore = theme?.TimelineLoadMore ?? DefaultTimelineLoadMore;
23
+ const groups = groupByDate(items);
24
+
25
+ return (
26
+ <div>
27
+ <div id="timeline-feed">
28
+ {groups.map((group) => (
29
+ <div key={group.dateKey} class="threads-card">
30
+ <div class="threads-date-header">
31
+ <span>{group.label}</span>
32
+ </div>
33
+ <div id={`date-items-${group.dateKey}`} class="flex flex-col">
34
+ {group.items.map((item, i) => (
35
+ <div key={item.post.id}>
36
+ {i > 0 && <hr class="border-border my-5" />}
37
+ {item.threadPreview ? (
38
+ <ResolvedThreadPreview
39
+ rootPost={item.post}
40
+ previewReplies={item.threadPreview.replies}
41
+ totalReplyCount={item.threadPreview.totalReplyCount}
42
+ theme={theme}
43
+ />
44
+ ) : (
45
+ <TimelineItem item={item} theme={theme} />
46
+ )}
47
+ </div>
48
+ ))}
49
+ </div>
50
+ </div>
51
+ ))}
52
+ </div>
53
+ {hasMore && nextCursor && (
54
+ <ResolvedLoadMore
55
+ nextCursor={nextCursor}
56
+ lastDate={groups.at(-1)?.dateKey}
57
+ theme={theme}
58
+ />
59
+ )}
60
+ </div>
61
+ );
62
+ };
@@ -1,7 +1,7 @@
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
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
@@ -10,30 +10,22 @@ import type {
10
10
  TimelineCardProps,
11
11
  ThemeComponents,
12
12
  PostView,
13
+ Format,
13
14
  } from "../../../types.js";
14
15
  import { NoteCard } from "./NoteCard.js";
15
- import { ArticleCard } from "./ArticleCard.js";
16
16
  import { LinkCard } from "./LinkCard.js";
17
17
  import { QuoteCard } from "./QuoteCard.js";
18
- import { ImageCard } from "./ImageCard.js";
19
- import type { PostType } from "../../../types.js";
20
18
 
21
- const CARD_MAP: Record<PostType, FC<TimelineCardProps>> = {
19
+ const CARD_MAP: Record<Format, FC<TimelineCardProps>> = {
22
20
  note: NoteCard,
23
- article: ArticleCard,
24
21
  link: LinkCard,
25
22
  quote: QuoteCard,
26
- image: ImageCard,
27
- page: NoteCard,
28
23
  };
29
24
 
30
- const THEME_KEY_MAP: Record<PostType, keyof ThemeComponents> = {
25
+ const THEME_KEY_MAP: Record<Format, keyof ThemeComponents> = {
31
26
  note: "NoteCard",
32
- article: "ArticleCard",
33
27
  link: "LinkCard",
34
28
  quote: "QuoteCard",
35
- image: "ImageCard",
36
- page: "NoteCard",
37
29
  };
38
30
 
39
31
  interface TimelineItemProps {
@@ -56,9 +48,9 @@ export const TimelineItem: FC<TimelineItemProps> = ({
56
48
  cardOverride,
57
49
  theme,
58
50
  }) => {
59
- const themeKey = THEME_KEY_MAP[item.post.type];
51
+ const themeKey = THEME_KEY_MAP[item.post.format];
60
52
  const themeCard = theme?.[themeKey] as FC<TimelineCardProps> | undefined;
61
- const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.type];
53
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.format];
62
54
  return <Card post={item.post} compact={compact} />;
63
55
  };
64
56
 
@@ -68,8 +60,8 @@ export const TimelineItemFromPost: FC<TimelineItemFromPostProps> = ({
68
60
  cardOverride,
69
61
  theme,
70
62
  }) => {
71
- const themeKey = THEME_KEY_MAP[post.type];
63
+ const themeKey = THEME_KEY_MAP[post.format];
72
64
  const themeCard = theme?.[themeKey] as FC<TimelineCardProps> | undefined;
73
- const Card = cardOverride ?? themeCard ?? CARD_MAP[post.type];
65
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[post.format];
74
66
  return <Card post={post} compact={compact} />;
75
67
  };