@jant/core 0.3.22 → 0.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/app.js +23 -5
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +5 -6
  7. package/dist/lib/constants.js +1 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/navigation.js +4 -5
  11. package/dist/lib/render.js +1 -1
  12. package/dist/lib/schemas.js +80 -38
  13. package/dist/lib/theme-components.js +8 -11
  14. package/dist/lib/time.js +56 -1
  15. package/dist/lib/timeline.js +119 -0
  16. package/dist/lib/view.js +62 -73
  17. package/dist/routes/api/posts.js +29 -35
  18. package/dist/routes/api/search.js +5 -6
  19. package/dist/routes/api/upload.js +13 -13
  20. package/dist/routes/dash/collections.js +22 -40
  21. package/dist/routes/dash/index.js +2 -2
  22. package/dist/routes/dash/navigation.js +25 -24
  23. package/dist/routes/dash/pages.js +42 -57
  24. package/dist/routes/dash/posts.js +27 -35
  25. package/dist/routes/feed/rss.js +2 -4
  26. package/dist/routes/feed/sitemap.js +10 -7
  27. package/dist/routes/pages/archive.js +12 -11
  28. package/dist/routes/pages/collection.js +11 -5
  29. package/dist/routes/pages/home.js +53 -61
  30. package/dist/routes/pages/page.js +60 -29
  31. package/dist/routes/pages/post.js +5 -12
  32. package/dist/routes/pages/search.js +3 -4
  33. package/dist/services/collection.js +52 -64
  34. package/dist/services/index.js +5 -3
  35. package/dist/services/navigation.js +29 -53
  36. package/dist/services/page.js +80 -0
  37. package/dist/services/post.js +68 -69
  38. package/dist/services/search.js +24 -18
  39. package/dist/theme/components/MediaGallery.js +19 -91
  40. package/dist/theme/components/PageForm.js +15 -15
  41. package/dist/theme/components/PostForm.js +136 -129
  42. package/dist/theme/components/PostList.js +13 -8
  43. package/dist/theme/components/ThreadView.js +3 -3
  44. package/dist/theme/components/TypeBadge.js +3 -14
  45. package/dist/theme/components/VisibilityBadge.js +33 -23
  46. package/dist/theme/components/index.js +0 -2
  47. package/dist/theme/index.js +10 -16
  48. package/dist/theme/layouts/index.js +0 -1
  49. package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
  50. package/dist/themes/threads/index.js +81 -0
  51. package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
  52. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  53. package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
  54. package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
  55. package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
  56. package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
  57. package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
  58. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  59. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  60. package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
  61. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  62. package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
  63. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  64. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  65. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  66. package/dist/types.js +24 -40
  67. package/package.json +2 -1
  68. package/src/__tests__/helpers/app.ts +4 -0
  69. package/src/__tests__/helpers/db.ts +51 -74
  70. package/src/app.tsx +27 -6
  71. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  72. package/src/db/migrations/meta/_journal.json +7 -0
  73. package/src/db/schema.ts +63 -46
  74. package/src/i18n/locales/en.po +216 -164
  75. package/src/i18n/locales/en.ts +1 -1
  76. package/src/i18n/locales/zh-Hans.po +216 -164
  77. package/src/i18n/locales/zh-Hans.ts +1 -1
  78. package/src/i18n/locales/zh-Hant.po +216 -164
  79. package/src/i18n/locales/zh-Hant.ts +1 -1
  80. package/src/index.ts +30 -15
  81. package/src/lib/__tests__/excerpt.test.ts +125 -0
  82. package/src/lib/__tests__/schemas.test.ts +166 -105
  83. package/src/lib/__tests__/theme-components.test.ts +4 -25
  84. package/src/lib/__tests__/time.test.ts +62 -0
  85. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  86. package/src/lib/__tests__/view.test.ts +217 -67
  87. package/src/lib/constants.ts +1 -4
  88. package/src/lib/excerpt.ts +87 -0
  89. package/src/lib/feed.ts +22 -7
  90. package/src/lib/navigation.ts +6 -7
  91. package/src/lib/render.tsx +1 -1
  92. package/src/lib/schemas.ts +118 -52
  93. package/src/lib/theme-components.ts +10 -13
  94. package/src/lib/time.ts +64 -0
  95. package/src/lib/timeline.ts +170 -0
  96. package/src/lib/view.ts +81 -83
  97. package/src/preset.css +45 -0
  98. package/src/routes/api/__tests__/posts.test.ts +50 -108
  99. package/src/routes/api/__tests__/search.test.ts +2 -3
  100. package/src/routes/api/posts.ts +30 -30
  101. package/src/routes/api/search.ts +4 -4
  102. package/src/routes/api/upload.ts +16 -6
  103. package/src/routes/dash/collections.tsx +18 -40
  104. package/src/routes/dash/index.tsx +2 -2
  105. package/src/routes/dash/navigation.tsx +27 -26
  106. package/src/routes/dash/pages.tsx +45 -60
  107. package/src/routes/dash/posts.tsx +44 -52
  108. package/src/routes/feed/rss.ts +2 -1
  109. package/src/routes/feed/sitemap.ts +14 -4
  110. package/src/routes/pages/archive.tsx +14 -10
  111. package/src/routes/pages/collection.tsx +17 -6
  112. package/src/routes/pages/home.tsx +56 -81
  113. package/src/routes/pages/page.tsx +64 -27
  114. package/src/routes/pages/post.tsx +5 -14
  115. package/src/routes/pages/search.tsx +2 -2
  116. package/src/services/__tests__/collection.test.ts +257 -158
  117. package/src/services/__tests__/media.test.ts +18 -18
  118. package/src/services/__tests__/navigation.test.ts +161 -87
  119. package/src/services/__tests__/post-timeline.test.ts +92 -88
  120. package/src/services/__tests__/post.test.ts +342 -206
  121. package/src/services/__tests__/search.test.ts +19 -25
  122. package/src/services/collection.ts +71 -113
  123. package/src/services/index.ts +9 -8
  124. package/src/services/navigation.ts +38 -71
  125. package/src/services/page.ts +124 -0
  126. package/src/services/post.ts +93 -103
  127. package/src/services/search.ts +38 -27
  128. package/src/styles/components.css +0 -54
  129. package/src/theme/components/MediaGallery.tsx +27 -96
  130. package/src/theme/components/PageForm.tsx +21 -21
  131. package/src/theme/components/PostForm.tsx +122 -118
  132. package/src/theme/components/PostList.tsx +58 -49
  133. package/src/theme/components/ThreadView.tsx +6 -3
  134. package/src/theme/components/TypeBadge.tsx +9 -17
  135. package/src/theme/components/VisibilityBadge.tsx +40 -23
  136. package/src/theme/components/index.ts +0 -13
  137. package/src/theme/index.ts +10 -16
  138. package/src/theme/layouts/index.ts +0 -1
  139. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  140. package/src/themes/threads/index.ts +100 -0
  141. package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
  142. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  143. package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
  144. package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
  145. package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
  146. package/src/themes/threads/pages/SinglePage.tsx +23 -0
  147. package/src/themes/threads/style.css +336 -0
  148. package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
  149. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  150. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  151. package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
  152. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  153. package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
  154. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  155. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  156. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  157. package/src/types.ts +242 -98
  158. package/dist/routes/api/timeline.js +0 -120
  159. package/dist/theme/components/timeline/ArticleCard.js +0 -46
  160. package/dist/theme/components/timeline/ImageCard.js +0 -83
  161. package/dist/theme/components/timeline/NoteCard.js +0 -34
  162. package/dist/theme/components/timeline/QuoteCard.js +0 -48
  163. package/dist/theme/components/timeline/TimelineFeed.js +0 -46
  164. package/dist/theme/components/timeline/index.js +0 -8
  165. package/dist/theme/layouts/SiteLayout.js +0 -131
  166. package/dist/theme/pages/CollectionPage.js +0 -63
  167. package/dist/theme/pages/index.js +0 -11
  168. package/src/routes/api/timeline.tsx +0 -159
  169. package/src/theme/components/timeline/ArticleCard.tsx +0 -45
  170. package/src/theme/components/timeline/ImageCard.tsx +0 -70
  171. package/src/theme/components/timeline/NoteCard.tsx +0 -34
  172. package/src/theme/components/timeline/QuoteCard.tsx +0 -48
  173. package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
  174. package/src/theme/components/timeline/index.ts +0 -8
  175. package/src/theme/layouts/SiteLayout.tsx +0 -132
  176. package/src/theme/pages/CollectionPage.tsx +0 -60
  177. package/src/theme/pages/SinglePage.tsx +0 -24
  178. package/src/theme/pages/index.ts +0 -13
@@ -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
+ }
@@ -1,18 +1,26 @@
1
1
  /**
2
- * Link Card Component
2
+ * Threads Theme - Link Card
3
3
  *
4
- * External link emphasis for type="link" posts.
4
+ * Compact link preview box date is shown at the feed level as a group header.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import type { TimelineCardProps } from "../../../types.js";
9
9
 
10
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
+
11
21
  return (
12
- <article
13
- class={`h-entry timeline-card timeline-card-link${compact ? " timeline-card-compact" : ""}`}
14
- >
15
- {post.sourceDomain && (
22
+ <article class={`h-entry${compact ? " threads-compact" : ""}`}>
23
+ {domain && (
16
24
  <div class="text-xs text-muted-foreground mb-1 flex items-center gap-1">
17
25
  <svg
18
26
  class="size-3"
@@ -24,7 +32,7 @@ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
24
32
  >
25
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" />
26
34
  </svg>
27
- <span>{post.sourceDomain}</span>
35
+ <span>{domain}</span>
28
36
  </div>
29
37
  )}
30
38
  {post.title && (
@@ -32,19 +40,19 @@ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
32
40
  class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
33
41
  >
34
42
  <a
35
- href={post.sourceUrl || post.permalink}
43
+ href={post.url || post.permalink}
36
44
  class="u-url hover:underline"
37
- target={post.sourceUrl ? "_blank" : undefined}
38
- rel={post.sourceUrl ? "noopener noreferrer" : undefined}
45
+ target={post.url ? "_blank" : undefined}
46
+ rel={post.url ? "noopener noreferrer" : undefined}
39
47
  >
40
48
  {post.title}
41
49
  </a>
42
50
  </h2>
43
51
  )}
44
- {!compact && post.contentHtml && (
52
+ {!compact && post.bodyHtml && (
45
53
  <div
46
- class="e-content prose prose-sm text-muted-foreground"
47
- dangerouslySetInnerHTML={{ __html: post.contentHtml }}
54
+ class="e-content prose text-muted-foreground"
55
+ dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
48
56
  />
49
57
  )}
50
58
  <footer class="mt-2 text-xs text-muted-foreground">
@@ -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
- * Thread Preview Component
2
+ * Threads Theme - Thread Preview
3
3
  *
4
- * Inline thread preview: root card + compact replies + "show more" link.
4
+ * Root post + vertical line connector + compact replies underneath.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
@@ -20,17 +20,17 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
20
20
  const remainingCount = totalReplyCount - previewReplies.length;
21
21
 
22
22
  return (
23
- <div class="timeline-thread">
23
+ <div>
24
24
  <TimelineItem item={{ post: rootPost }} theme={theme} />
25
25
  {previewReplies.length > 0 && (
26
- <div class="timeline-thread-replies">
26
+ <div class="threads-replies">
27
27
  {previewReplies.map((reply) => (
28
- <div key={reply.id} class="timeline-thread-reply">
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
- <div class="timeline-thread-reply">
33
+ <div class="threads-reply">
34
34
  <a
35
35
  href={rootPost.permalink}
36
36
  class="text-sm text-muted-foreground hover:text-foreground hover:underline"
@@ -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,8 +1,7 @@
1
1
  /**
2
- * Timeline Item Component
2
+ * Threads Theme - Timeline Item
3
3
  *
4
- * Dispatches to the correct card component based on post type.
5
- * Resolves card overrides from theme components if provided.
4
+ * Dispatches to the correct card component based on post format.
6
5
  */
7
6
 
8
7
  import type { FC } from "hono/jsx";
@@ -11,38 +10,28 @@ import type {
11
10
  TimelineCardProps,
12
11
  ThemeComponents,
13
12
  PostView,
13
+ Format,
14
14
  } from "../../../types.js";
15
15
  import { NoteCard } from "./NoteCard.js";
16
- import { ArticleCard } from "./ArticleCard.js";
17
16
  import { LinkCard } from "./LinkCard.js";
18
17
  import { QuoteCard } from "./QuoteCard.js";
19
- import { ImageCard } from "./ImageCard.js";
20
- import type { PostType } from "../../../types.js";
21
18
 
22
- const CARD_MAP: Record<PostType, FC<TimelineCardProps>> = {
19
+ const CARD_MAP: Record<Format, FC<TimelineCardProps>> = {
23
20
  note: NoteCard,
24
- article: ArticleCard,
25
21
  link: LinkCard,
26
22
  quote: QuoteCard,
27
- image: ImageCard,
28
- page: NoteCard,
29
23
  };
30
24
 
31
- const THEME_KEY_MAP: Record<PostType, keyof ThemeComponents> = {
25
+ const THEME_KEY_MAP: Record<Format, keyof ThemeComponents> = {
32
26
  note: "NoteCard",
33
- article: "ArticleCard",
34
27
  link: "LinkCard",
35
28
  quote: "QuoteCard",
36
- image: "ImageCard",
37
- page: "NoteCard",
38
29
  };
39
30
 
40
31
  interface TimelineItemProps {
41
32
  item: TimelineItemView;
42
33
  compact?: boolean;
43
- /** Override card component (for direct overrides) */
44
34
  cardOverride?: FC<TimelineCardProps>;
45
- /** Theme components for cascade resolution */
46
35
  theme?: ThemeComponents;
47
36
  }
48
37
 
@@ -59,9 +48,9 @@ export const TimelineItem: FC<TimelineItemProps> = ({
59
48
  cardOverride,
60
49
  theme,
61
50
  }) => {
62
- const themeKey = THEME_KEY_MAP[item.post.type];
51
+ const themeKey = THEME_KEY_MAP[item.post.format];
63
52
  const themeCard = theme?.[themeKey] as FC<TimelineCardProps> | undefined;
64
- const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.type];
53
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.format];
65
54
  return <Card post={item.post} compact={compact} />;
66
55
  };
67
56
 
@@ -71,8 +60,8 @@ export const TimelineItemFromPost: FC<TimelineItemFromPostProps> = ({
71
60
  cardOverride,
72
61
  theme,
73
62
  }) => {
74
- const themeKey = THEME_KEY_MAP[post.type];
63
+ const themeKey = THEME_KEY_MAP[post.format];
75
64
  const themeCard = theme?.[themeKey] as FC<TimelineCardProps> | undefined;
76
- const Card = cardOverride ?? themeCard ?? CARD_MAP[post.type];
65
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[post.format];
77
66
  return <Card post={post} compact={compact} />;
78
67
  };