@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.
- package/dist/app.js +23 -5
- package/dist/db/schema.js +72 -47
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +5 -6
- package/dist/lib/constants.js +1 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/navigation.js +4 -5
- package/dist/lib/render.js +1 -1
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme-components.js +8 -11
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +119 -0
- package/dist/lib/view.js +62 -73
- package/dist/routes/api/posts.js +29 -35
- package/dist/routes/api/search.js +5 -6
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/dash/collections.js +22 -40
- package/dist/routes/dash/index.js +2 -2
- package/dist/routes/dash/navigation.js +25 -24
- package/dist/routes/dash/pages.js +42 -57
- package/dist/routes/dash/posts.js +27 -35
- package/dist/routes/feed/rss.js +2 -4
- package/dist/routes/feed/sitemap.js +10 -7
- package/dist/routes/pages/archive.js +12 -11
- package/dist/routes/pages/collection.js +11 -5
- package/dist/routes/pages/home.js +53 -61
- package/dist/routes/pages/page.js +60 -29
- package/dist/routes/pages/post.js +5 -12
- package/dist/routes/pages/search.js +3 -4
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +80 -0
- package/dist/services/post.js +68 -69
- package/dist/services/search.js +24 -18
- package/dist/theme/components/MediaGallery.js +19 -91
- package/dist/theme/components/PageForm.js +15 -15
- package/dist/theme/components/PostForm.js +136 -129
- package/dist/theme/components/PostList.js +13 -8
- package/dist/theme/components/ThreadView.js +3 -3
- package/dist/theme/components/TypeBadge.js +3 -14
- package/dist/theme/components/VisibilityBadge.js +33 -23
- package/dist/theme/components/index.js +0 -2
- package/dist/theme/index.js +10 -16
- package/dist/theme/layouts/index.js +0 -1
- package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
- package/dist/themes/threads/index.js +81 -0
- package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
- package/dist/themes/threads/pages/CollectionPage.js +65 -0
- package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
- package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
- package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
- package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
- package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
- package/dist/themes/threads/timeline/NoteCard.js +53 -0
- package/dist/themes/threads/timeline/QuoteCard.js +59 -0
- package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
- package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
- package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
- package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
- package/dist/themes/threads/timeline/groupByDate.js +22 -0
- package/dist/themes/threads/timeline/timelineMore.js +107 -0
- package/dist/types.js +24 -40
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +51 -74
- package/src/app.tsx +27 -6
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +216 -164
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +216 -164
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +216 -164
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +30 -15
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +166 -105
- package/src/lib/__tests__/theme-components.test.ts +4 -25
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
- package/src/lib/__tests__/view.test.ts +217 -67
- package/src/lib/constants.ts +1 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/navigation.ts +6 -7
- package/src/lib/render.tsx +1 -1
- package/src/lib/schemas.ts +118 -52
- package/src/lib/theme-components.ts +10 -13
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +170 -0
- package/src/lib/view.ts +81 -83
- package/src/preset.css +45 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/posts.ts +30 -30
- package/src/routes/api/search.ts +4 -4
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/dash/collections.tsx +18 -40
- package/src/routes/dash/index.tsx +2 -2
- package/src/routes/dash/navigation.tsx +27 -26
- package/src/routes/dash/pages.tsx +45 -60
- package/src/routes/dash/posts.tsx +44 -52
- package/src/routes/feed/rss.ts +2 -1
- package/src/routes/feed/sitemap.ts +14 -4
- package/src/routes/pages/archive.tsx +14 -10
- package/src/routes/pages/collection.tsx +17 -6
- package/src/routes/pages/home.tsx +56 -81
- package/src/routes/pages/page.tsx +64 -27
- package/src/routes/pages/post.tsx +5 -14
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +342 -206
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +124 -0
- package/src/services/post.ts +93 -103
- package/src/services/search.ts +38 -27
- package/src/styles/components.css +0 -54
- package/src/theme/components/MediaGallery.tsx +27 -96
- package/src/theme/components/PageForm.tsx +21 -21
- package/src/theme/components/PostForm.tsx +122 -118
- package/src/theme/components/PostList.tsx +58 -49
- package/src/theme/components/ThreadView.tsx +6 -3
- package/src/theme/components/TypeBadge.tsx +9 -17
- package/src/theme/components/VisibilityBadge.tsx +40 -23
- package/src/theme/components/index.ts +0 -13
- package/src/theme/index.ts +10 -16
- package/src/theme/layouts/index.ts +0 -1
- package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
- package/src/themes/threads/index.ts +100 -0
- package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
- package/src/themes/threads/pages/CollectionPage.tsx +61 -0
- package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
- package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
- package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
- package/src/themes/threads/pages/SinglePage.tsx +23 -0
- package/src/themes/threads/style.css +336 -0
- package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
- package/src/themes/threads/timeline/NoteCard.tsx +58 -0
- package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
- package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
- package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
- package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
- package/src/themes/threads/timeline/groupByDate.ts +30 -0
- package/src/themes/threads/timeline/timelineMore.tsx +130 -0
- package/src/types.ts +242 -98
- package/dist/routes/api/timeline.js +0 -120
- package/dist/theme/components/timeline/ArticleCard.js +0 -46
- package/dist/theme/components/timeline/ImageCard.js +0 -83
- package/dist/theme/components/timeline/NoteCard.js +0 -34
- package/dist/theme/components/timeline/QuoteCard.js +0 -48
- package/dist/theme/components/timeline/TimelineFeed.js +0 -46
- package/dist/theme/components/timeline/index.js +0 -8
- package/dist/theme/layouts/SiteLayout.js +0 -131
- package/dist/theme/pages/CollectionPage.js +0 -63
- package/dist/theme/pages/index.js +0 -11
- package/src/routes/api/timeline.tsx +0 -159
- package/src/theme/components/timeline/ArticleCard.tsx +0 -45
- package/src/theme/components/timeline/ImageCard.tsx +0 -70
- package/src/theme/components/timeline/NoteCard.tsx +0 -34
- package/src/theme/components/timeline/QuoteCard.tsx +0 -48
- package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
- package/src/theme/components/timeline/index.ts +0 -8
- package/src/theme/layouts/SiteLayout.tsx +0 -132
- package/src/theme/pages/CollectionPage.tsx +0 -60
- package/src/theme/pages/SinglePage.tsx +0 -24
- 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
|
|
2
|
+
* Threads Theme - Link Card
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
|
|
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>{
|
|
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.
|
|
43
|
+
href={post.url || post.permalink}
|
|
36
44
|
class="u-url hover:underline"
|
|
37
|
-
target={post.
|
|
38
|
-
rel={post.
|
|
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.
|
|
52
|
+
{!compact && post.bodyHtml && (
|
|
45
53
|
<div
|
|
46
|
-
class="e-content prose
|
|
47
|
-
dangerouslySetInnerHTML={{ __html: post.
|
|
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
|
+
—{" "}
|
|
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
|
|
2
|
+
* Threads Theme - Thread Preview
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
23
|
+
<div>
|
|
24
24
|
<TimelineItem item={{ post: rootPost }} theme={theme} />
|
|
25
25
|
{previewReplies.length > 0 && (
|
|
26
|
-
<div class="
|
|
26
|
+
<div class="threads-replies">
|
|
27
27
|
{previewReplies.map((reply) => (
|
|
28
|
-
<div key={reply.id} class="
|
|
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="
|
|
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
|
|
2
|
+
* Threads Theme - Timeline Item
|
|
3
3
|
*
|
|
4
|
-
* Dispatches to the correct card component based on post
|
|
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<
|
|
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<
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
65
|
+
const Card = cardOverride ?? themeCard ?? CARD_MAP[post.format];
|
|
77
66
|
return <Card post={post} compact={compact} />;
|
|
78
67
|
};
|