@jant/core 0.3.24 → 0.3.26

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 (277) hide show
  1. package/dist/app.js +101 -571
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +1 -1
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/index.js +3 -9
  8. package/dist/lib/avatar-upload.js +134 -0
  9. package/dist/lib/config.js +39 -0
  10. package/dist/lib/constants.js +10 -9
  11. package/dist/lib/favicon.js +102 -0
  12. package/dist/lib/image.js +13 -17
  13. package/dist/lib/media-helpers.js +2 -2
  14. package/dist/lib/nav-reorder.js +1 -1
  15. package/dist/lib/navigation.js +48 -3
  16. package/dist/lib/pagination.js +44 -0
  17. package/dist/lib/render.js +16 -11
  18. package/dist/lib/schemas.js +34 -3
  19. package/dist/lib/theme.js +4 -4
  20. package/dist/lib/timeline.js +24 -48
  21. package/dist/lib/timezones.js +388 -0
  22. package/dist/lib/view.js +3 -3
  23. package/dist/routes/api/collections.js +124 -0
  24. package/dist/routes/api/nav-items.js +104 -0
  25. package/dist/routes/api/pages.js +91 -0
  26. package/dist/routes/api/posts.js +3 -3
  27. package/dist/routes/api/search.js +2 -2
  28. package/dist/routes/api/settings.js +68 -0
  29. package/dist/routes/api/upload.js +3 -3
  30. package/dist/routes/auth/reset.js +221 -0
  31. package/dist/routes/auth/setup.js +194 -0
  32. package/dist/routes/auth/signin.js +176 -0
  33. package/dist/routes/compose.js +48 -0
  34. package/dist/routes/dash/collections.js +24 -416
  35. package/dist/routes/dash/index.js +1 -1
  36. package/dist/routes/dash/media.js +13 -393
  37. package/dist/routes/dash/pages.js +112 -86
  38. package/dist/routes/dash/posts.js +3 -5
  39. package/dist/routes/dash/redirects.js +20 -14
  40. package/dist/routes/dash/settings.js +213 -518
  41. package/dist/routes/feed/rss.js +4 -3
  42. package/dist/routes/feed/sitemap.js +5 -3
  43. package/dist/routes/pages/archive.js +3 -6
  44. package/dist/routes/pages/collection.js +3 -6
  45. package/dist/routes/pages/collections.js +28 -0
  46. package/dist/routes/pages/featured.js +36 -0
  47. package/dist/routes/pages/home.js +33 -49
  48. package/dist/routes/pages/latest.js +45 -0
  49. package/dist/routes/pages/page.js +29 -32
  50. package/dist/routes/pages/post.js +3 -6
  51. package/dist/routes/pages/search.js +3 -6
  52. package/dist/services/page.js +5 -1
  53. package/dist/services/post.js +45 -31
  54. package/dist/services/search.js +1 -1
  55. package/dist/types/bindings.js +3 -0
  56. package/dist/types/config.js +147 -0
  57. package/dist/types/constants.js +27 -0
  58. package/dist/types/entities.js +3 -0
  59. package/dist/types/operations.js +3 -0
  60. package/dist/types/props.js +3 -0
  61. package/dist/types/views.js +5 -0
  62. package/dist/types.js +8 -111
  63. package/dist/{theme → ui}/color-themes.js +33 -33
  64. package/dist/ui/compose/ComposeDialog.js +467 -0
  65. package/dist/ui/compose/ComposePrompt.js +55 -0
  66. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  67. package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
  68. package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
  69. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  70. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  71. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  72. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  73. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  74. package/dist/{theme/components → ui/dash}/index.js +3 -6
  75. package/dist/ui/dash/media/MediaListContent.js +166 -0
  76. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  77. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  78. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  79. package/dist/ui/dash/settings/AccountContent.js +209 -0
  80. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  81. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  82. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  83. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  84. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  85. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  86. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  87. package/dist/ui/feed/TimelineFeed.js +41 -0
  88. package/dist/ui/feed/TimelineItem.js +27 -0
  89. package/dist/ui/font-themes.js +36 -0
  90. package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
  91. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  92. package/dist/ui/layouts/SiteLayout.js +169 -0
  93. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  94. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  95. package/dist/ui/pages/CollectionsPage.js +76 -0
  96. package/dist/ui/pages/FeaturedPage.js +24 -0
  97. package/dist/ui/pages/HomePage.js +24 -0
  98. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  99. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  100. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  101. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  102. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  103. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  104. package/dist/ui/shared/index.js +5 -0
  105. package/package.json +1 -9
  106. package/src/__tests__/helpers/db.ts +3 -0
  107. package/src/app.tsx +131 -561
  108. package/src/client.ts +1 -0
  109. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  110. package/src/db/migrations/meta/_journal.json +7 -0
  111. package/src/db/schema.ts +1 -1
  112. package/src/i18n/locales/en.po +477 -261
  113. package/src/i18n/locales/en.ts +1 -1
  114. package/src/i18n/locales/zh-Hans.po +477 -261
  115. package/src/i18n/locales/zh-Hans.ts +1 -1
  116. package/src/i18n/locales/zh-Hant.po +477 -261
  117. package/src/i18n/locales/zh-Hant.ts +1 -1
  118. package/src/index.ts +7 -36
  119. package/src/lib/__tests__/config.test.ts +192 -0
  120. package/src/lib/__tests__/favicon.test.ts +151 -0
  121. package/src/lib/__tests__/image.test.ts +2 -6
  122. package/src/lib/__tests__/schemas.test.ts +60 -19
  123. package/src/lib/__tests__/timeline.test.ts +45 -81
  124. package/src/lib/__tests__/timezones.test.ts +61 -0
  125. package/src/lib/__tests__/view.test.ts +15 -9
  126. package/src/lib/avatar-upload.ts +165 -0
  127. package/src/lib/config.ts +47 -0
  128. package/src/lib/constants.ts +19 -10
  129. package/src/lib/favicon.ts +115 -0
  130. package/src/lib/image.ts +13 -21
  131. package/src/lib/media-helpers.ts +2 -2
  132. package/src/lib/nav-reorder.ts +1 -1
  133. package/src/lib/navigation.ts +73 -4
  134. package/src/lib/pagination.ts +50 -0
  135. package/src/lib/render.tsx +22 -15
  136. package/src/lib/schemas.ts +47 -6
  137. package/src/lib/theme.ts +5 -5
  138. package/src/lib/timeline.ts +28 -57
  139. package/src/lib/timezones.ts +325 -0
  140. package/src/lib/view.ts +3 -3
  141. package/src/preset.css +2 -1
  142. package/src/routes/__tests__/compose.test.ts +199 -0
  143. package/src/routes/api/__tests__/collections.test.ts +249 -0
  144. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  145. package/src/routes/api/__tests__/pages.test.ts +218 -0
  146. package/src/routes/api/__tests__/settings.test.ts +132 -0
  147. package/src/routes/api/collections.ts +143 -0
  148. package/src/routes/api/nav-items.ts +115 -0
  149. package/src/routes/api/pages.ts +101 -0
  150. package/src/routes/api/posts.ts +3 -3
  151. package/src/routes/api/search.ts +2 -2
  152. package/src/routes/api/settings.ts +91 -0
  153. package/src/routes/api/upload.ts +2 -3
  154. package/src/routes/auth/reset.tsx +239 -0
  155. package/src/routes/auth/setup.tsx +189 -0
  156. package/src/routes/auth/signin.tsx +163 -0
  157. package/src/routes/compose.ts +63 -0
  158. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  159. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  160. package/src/routes/dash/collections.tsx +18 -367
  161. package/src/routes/dash/index.tsx +1 -1
  162. package/src/routes/dash/media.tsx +13 -415
  163. package/src/routes/dash/pages.tsx +131 -98
  164. package/src/routes/dash/posts.tsx +3 -7
  165. package/src/routes/dash/redirects.tsx +22 -16
  166. package/src/routes/dash/settings.tsx +265 -478
  167. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  168. package/src/routes/feed/rss.ts +5 -3
  169. package/src/routes/feed/sitemap.ts +5 -3
  170. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  171. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  172. package/src/routes/pages/archive.tsx +2 -6
  173. package/src/routes/pages/collection.tsx +2 -6
  174. package/src/routes/pages/collections.tsx +36 -0
  175. package/src/routes/pages/featured.tsx +44 -0
  176. package/src/routes/pages/home.tsx +30 -53
  177. package/src/routes/pages/latest.tsx +59 -0
  178. package/src/routes/pages/page.tsx +28 -30
  179. package/src/routes/pages/post.tsx +2 -5
  180. package/src/routes/pages/search.tsx +2 -6
  181. package/src/services/__tests__/page.test.ts +106 -0
  182. package/src/services/__tests__/post.test.ts +114 -15
  183. package/src/services/page.ts +13 -1
  184. package/src/services/post.ts +58 -40
  185. package/src/services/search.ts +2 -2
  186. package/src/styles/components.css +0 -65
  187. package/src/styles/tokens.css +47 -0
  188. package/src/styles/ui.css +475 -0
  189. package/src/types/bindings.ts +30 -0
  190. package/src/types/config.ts +183 -0
  191. package/src/types/constants.ts +26 -0
  192. package/src/types/entities.ts +109 -0
  193. package/src/types/operations.ts +88 -0
  194. package/src/types/props.ts +115 -0
  195. package/src/types/views.ts +172 -0
  196. package/src/types.ts +8 -774
  197. package/src/ui/__tests__/font-themes.test.ts +34 -0
  198. package/src/{theme → ui}/color-themes.ts +34 -34
  199. package/src/ui/compose/ComposeDialog.tsx +414 -0
  200. package/src/ui/compose/ComposePrompt.tsx +55 -0
  201. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  202. package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
  203. package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
  204. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  205. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  206. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  207. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  208. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  209. package/src/ui/dash/index.ts +10 -0
  210. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  211. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  212. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  213. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  214. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  215. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  216. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  217. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  218. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  219. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  220. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  221. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  222. package/src/ui/feed/TimelineFeed.tsx +49 -0
  223. package/src/ui/feed/TimelineItem.tsx +45 -0
  224. package/src/ui/font-themes.ts +54 -0
  225. package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
  226. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  227. package/src/ui/layouts/SiteLayout.tsx +164 -0
  228. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  229. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  230. package/src/ui/pages/CollectionsPage.tsx +73 -0
  231. package/src/ui/pages/FeaturedPage.tsx +31 -0
  232. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  233. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  234. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  235. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  236. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  237. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  238. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  239. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  240. package/src/ui/shared/index.ts +12 -0
  241. package/bin/jant.js +0 -185
  242. package/dist/lib/theme-components.js +0 -46
  243. package/dist/routes/dash/navigation.js +0 -289
  244. package/dist/theme/index.js +0 -18
  245. package/dist/theme/layouts/index.js +0 -2
  246. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  247. package/dist/themes/threads/index.js +0 -81
  248. package/dist/themes/threads/pages/HomePage.js +0 -25
  249. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  250. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  251. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  252. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  253. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  254. package/src/lib/__tests__/theme-components.test.ts +0 -105
  255. package/src/lib/theme-components.ts +0 -65
  256. package/src/routes/dash/navigation.tsx +0 -317
  257. package/src/theme/components/index.ts +0 -23
  258. package/src/theme/index.ts +0 -22
  259. package/src/theme/layouts/index.ts +0 -7
  260. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  261. package/src/themes/threads/index.ts +0 -100
  262. package/src/themes/threads/style.css +0 -336
  263. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  264. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  265. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  266. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  267. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  268. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  269. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  270. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  271. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  272. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  273. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  274. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  275. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  276. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  277. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Threads Theme - Quote Card
2
+ * Quote Card
3
3
  *
4
- * Left-border accent blockquote date is shown at the feed level as a group header.
4
+ * Left-border accent blockquote with full date in footer.
5
5
  *
6
- * v2 fields:
6
+ * Fields:
7
7
  * - quoteText: the quoted text
8
8
  * - title: attribution (who said it)
9
9
  * - url: source link
@@ -11,13 +11,17 @@
11
11
  */
12
12
 
13
13
  import type { FC } from "hono/jsx";
14
- import type { TimelineCardProps } from "../../../types.js";
14
+ import type { TimelineCardProps } from "../../types.js";
15
15
 
16
16
  export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
17
17
  return (
18
- <article class={`h-entry${compact ? " threads-compact" : ""}`}>
18
+ <article
19
+ class={`h-entry${compact ? " feed-compact" : ""}`}
20
+ data-post
21
+ data-format="quote"
22
+ >
19
23
  {post.quoteText && (
20
- <blockquote class="threads-quote">
24
+ <blockquote class="feed-quote">
21
25
  <div
22
26
  class={`e-content ${compact ? "text-sm" : "text-base"} leading-relaxed`}
23
27
  >
@@ -45,16 +49,17 @@ export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
45
49
  {!compact && post.bodyHtml && (
46
50
  <div
47
51
  class="mt-3 prose text-muted-foreground"
52
+ data-post-body
48
53
  dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
49
54
  />
50
55
  )}
51
- <footer class="mt-2">
56
+ <footer class="mt-2" data-post-meta>
52
57
  <a
53
58
  href={post.permalink}
54
59
  class="u-url text-xs text-muted-foreground hover:underline"
55
60
  >
56
61
  <time class="dt-published" datetime={post.publishedAt}>
57
- {post.publishedAtRelative}
62
+ {post.publishedAtFormatted}
58
63
  </time>
59
64
  </a>
60
65
  </footer>
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Threads Theme - Thread Preview
2
+ * 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";
8
8
  import { useLingui } from "@lingui/react/macro";
9
- import type { ThreadPreviewProps } from "../../../types.js";
9
+ import type { ThreadPreviewProps } from "../../types.js";
10
10
  import { TimelineItem } from "./TimelineItem.js";
11
11
  import { TimelineItemFromPost } from "./TimelineItem.js";
12
12
 
@@ -14,23 +14,22 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
14
14
  rootPost,
15
15
  previewReplies,
16
16
  totalReplyCount,
17
- theme,
18
17
  }) => {
19
18
  const { t } = useLingui();
20
19
  const remainingCount = totalReplyCount - previewReplies.length;
21
20
 
22
21
  return (
23
22
  <div>
24
- <TimelineItem item={{ post: rootPost }} theme={theme} />
23
+ <TimelineItem item={{ post: rootPost }} />
25
24
  {previewReplies.length > 0 && (
26
- <div class="threads-replies">
25
+ <div class="feed-replies">
27
26
  {previewReplies.map((reply) => (
28
- <div key={reply.id} class="threads-reply">
29
- <TimelineItemFromPost post={reply} compact theme={theme} />
27
+ <div key={reply.id} class="feed-reply">
28
+ <TimelineItemFromPost post={reply} compact />
30
29
  </div>
31
30
  ))}
32
31
  {remainingCount > 0 && (
33
- <div class="threads-reply">
32
+ <div class="feed-reply">
34
33
  <a
35
34
  href={rootPost.permalink}
36
35
  class="text-sm text-muted-foreground hover:text-foreground hover:underline"
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Timeline Feed
3
+ *
4
+ * Flat list of posts separated by simple dividers.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineFeedProps } from "../../types.js";
9
+ import { TimelineItem } from "./TimelineItem.js";
10
+ import { ThreadPreview } from "./ThreadPreview.js";
11
+ import { PagePagination } from "../shared/Pagination.js";
12
+
13
+ export const TimelineFeed: FC<TimelineFeedProps> = ({
14
+ items,
15
+ currentPage,
16
+ totalPages,
17
+ }) => {
18
+ return (
19
+ <div data-feed>
20
+ <div id="timeline-feed">
21
+ <div id="timeline-items" class="flex flex-col">
22
+ {items.map((item, i) => (
23
+ <div key={item.post.id}>
24
+ {i > 0 && <hr class="feed-divider" />}
25
+ {item.threadPreview ? (
26
+ <ThreadPreview
27
+ rootPost={item.post}
28
+ previewReplies={item.threadPreview.replies}
29
+ totalReplyCount={item.threadPreview.totalReplyCount}
30
+ />
31
+ ) : (
32
+ <TimelineItem item={item} />
33
+ )}
34
+ </div>
35
+ ))}
36
+ </div>
37
+ </div>
38
+ {currentPage !== undefined &&
39
+ totalPages !== undefined &&
40
+ totalPages > 1 && (
41
+ <PagePagination
42
+ baseUrl="/"
43
+ currentPage={currentPage}
44
+ totalPages={totalPages}
45
+ />
46
+ )}
47
+ </div>
48
+ );
49
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Timeline Item
3
+ *
4
+ * Dispatches to the correct card component based on post format.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type {
9
+ TimelineItemView,
10
+ TimelineCardProps,
11
+ PostView,
12
+ Format,
13
+ } from "../../types.js";
14
+ import { NoteCard } from "./NoteCard.js";
15
+ import { LinkCard } from "./LinkCard.js";
16
+ import { QuoteCard } from "./QuoteCard.js";
17
+
18
+ const CARD_MAP: Record<Format, FC<TimelineCardProps>> = {
19
+ note: NoteCard,
20
+ link: LinkCard,
21
+ quote: QuoteCard,
22
+ };
23
+
24
+ interface TimelineItemProps {
25
+ item: TimelineItemView;
26
+ compact?: boolean;
27
+ }
28
+
29
+ interface TimelineItemFromPostProps {
30
+ post: PostView;
31
+ compact?: boolean;
32
+ }
33
+
34
+ export const TimelineItem: FC<TimelineItemProps> = ({ item, compact }) => {
35
+ const Card = CARD_MAP[item.post.format];
36
+ return <Card post={item.post} compact={compact} />;
37
+ };
38
+
39
+ export const TimelineItemFromPost: FC<TimelineItemFromPostProps> = ({
40
+ post,
41
+ compact,
42
+ }) => {
43
+ const Card = CARD_MAP[post.format];
44
+ return <Card post={post} compact={compact} />;
45
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Built-in Font Themes
3
+ *
4
+ * System-font-only presets — no external font loading required.
5
+ */
6
+
7
+ /**
8
+ * A font theme definition with display metadata.
9
+ */
10
+ export interface FontTheme {
11
+ /** Stored in DB settings, e.g. "serif" */
12
+ id: string;
13
+ /** Display name, e.g. "Serif" */
14
+ name: string;
15
+ /** CSS font-family stack */
16
+ fontFamily: string;
17
+ /** Short description for the picker UI */
18
+ description: string;
19
+ }
20
+
21
+ export const BUILTIN_FONT_THEMES: FontTheme[] = [
22
+ {
23
+ id: "default",
24
+ name: "System Default",
25
+ // 现代系统字体栈:先英文,后 Mac/iOS 中文,再 Win 中文
26
+ fontFamily:
27
+ 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Source Han Sans CN", sans-serif',
28
+ description: "与你的操作系统保持一致,最稳定的阅读体验",
29
+ },
30
+ {
31
+ id: "serif",
32
+ name: "Classic Serif",
33
+ // Charter 是 Apple 系统自带的极品衬线体
34
+ fontFamily:
35
+ 'Charter, "Bitstream Charter", "Sitka Text", Georgia, "Songti SC", "Source Han Serif CN", "STSong", "SimSun", serif',
36
+ description: "传统的衬线体,适合深度长文阅读",
37
+ },
38
+ {
39
+ id: "humanist",
40
+ name: "Humanist",
41
+ // Optima 具有书法韵味,Candara 是 Windows 上的优质人文体
42
+ fontFamily:
43
+ 'Optima, Candara, "Noto Sans", "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif',
44
+ description: "温润如玉的字体风格,兼具现代感与书法美感",
45
+ },
46
+ {
47
+ id: "mono",
48
+ name: "Monospace",
49
+ // 优先使用 JetBrains Mono 或 SF Mono
50
+ fontFamily:
51
+ '"JetBrains Mono", "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", "PingFang SC", "Microsoft YaHei", monospace',
52
+ description: "等宽字体,适合技术内容或代码展示",
53
+ },
54
+ ];
@@ -23,6 +23,8 @@ export interface BaseLayoutProps {
23
23
  lang?: string;
24
24
  c?: Context;
25
25
  toast?: ToastProps;
26
+ faviconUrl?: string;
27
+ noindex?: boolean;
26
28
  }
27
29
 
28
30
  export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
@@ -31,17 +33,31 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
31
33
  lang,
32
34
  c,
33
35
  toast,
36
+ faviconUrl,
37
+ noindex,
34
38
  children,
35
39
  }) => {
36
40
  // Read lang from Hono context if available, otherwise use prop or default
37
41
  const resolvedLang = lang ?? (c ? c.get("lang") : "en");
38
42
 
43
+ // Read faviconUrl from context when not provided as prop (fixes dashboard favicon)
44
+ const resolvedFaviconUrl = faviconUrl ?? (c ? c.get("faviconUrl") : undefined);
45
+
46
+ // Read noindex from context when not provided as prop
47
+ const resolvedNoindex = noindex ?? (c ? c.get("noindex") : undefined);
48
+
39
49
  // Automatically wrap with I18nProvider if Context is provided
40
50
  const content = c ? <I18nProvider c={c}>{children}</I18nProvider> : children;
41
51
 
42
52
  // Read theme style from Hono context if available
43
53
  const themeStyle = c ? c.get("themeStyle") : undefined;
44
54
 
55
+ // Read custom CSS from Hono context if available
56
+ const customCSS = c ? c.get("customCSS") : undefined;
57
+
58
+ // Check authentication status for data attribute
59
+ const isAuthenticated = c ? c.get("isAuthenticated") : false;
60
+
45
61
  return (
46
62
  <html lang={resolvedLang}>
47
63
  <head>
@@ -49,12 +65,23 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
49
65
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
50
66
  <title>{title}</title>
51
67
  {description && <meta name="description" content={description} />}
68
+ {resolvedNoindex && <meta name="robots" content="noindex, nofollow" />}
69
+ {resolvedFaviconUrl && (
70
+ <>
71
+ <link rel="icon" href="/favicon.ico" sizes="16x16 32x32" />
72
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
73
+ </>
74
+ )}
52
75
  <ViteClient />
53
76
  <Link href="/src/style.css" rel="stylesheet" />
54
77
  {themeStyle && <style>{themeStyle}</style>}
78
+ {customCSS && <style>{customCSS}</style>}
55
79
  <Script src="/src/client.ts" />
56
80
  </head>
57
- <body class="bg-background text-foreground antialiased">
81
+ <body
82
+ class="bg-background text-foreground antialiased"
83
+ {...(isAuthenticated ? { "data-authenticated": true } : {})}
84
+ >
58
85
  {content}
59
86
  <div id="toast-container" class="toast-container">
60
87
  {toast && (
@@ -126,16 +126,6 @@ function DashLayoutContent({
126
126
  comment: "@context: Dashboard navigation - URL redirects",
127
127
  })}
128
128
  </a>
129
- <a
130
- href="/dash/navigation"
131
- class={navClass("/dash/navigation", /^\/dash\/navigation/)}
132
- >
133
- {t({
134
- message: "Navigation",
135
- comment:
136
- "@context: Dashboard navigation - navigation links management",
137
- })}
138
- </a>
139
129
  <a
140
130
  href="/dash/settings"
141
131
  class={navClass("/dash/settings", /^\/dash\/settings/)}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Site Layout
3
+ *
4
+ * Vertical header: site name on top, custom nav links below, description under nav.
5
+ * Content area with browse filter tabs and compose prompt/dialog for authenticated users.
6
+ */
7
+
8
+ import type { FC, PropsWithChildren } from "hono/jsx";
9
+ import { useLingui } from "@lingui/react/macro";
10
+ import type { NavItemView, SiteLayoutProps } from "../../types.js";
11
+ import { ComposeDialog } from "../compose/ComposeDialog.js";
12
+ import { ComposePrompt } from "../compose/ComposePrompt.js";
13
+
14
+ function HeaderLink({ link }: { link: NavItemView }) {
15
+ return (
16
+ <a
17
+ href={link.url}
18
+ class={`site-header-link ${link.isActive ? "site-header-link-active" : ""}`}
19
+ {...(link.isExternal
20
+ ? { target: "_blank", rel: "noopener noreferrer" }
21
+ : {})}
22
+ >
23
+ {link.label}
24
+ </a>
25
+ );
26
+ }
27
+
28
+ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
29
+ siteName,
30
+ siteDescription,
31
+ links,
32
+ currentPath,
33
+ isAuthenticated,
34
+ collections,
35
+ homeDefaultView,
36
+ siteAvatarUrl,
37
+ showHeaderAvatar,
38
+ siteFooterHtml,
39
+ children,
40
+ }) => {
41
+ const { t } = useLingui();
42
+
43
+ const latestHref = homeDefaultView === "featured" ? "/latest" : "/";
44
+ const featuredHref = homeDefaultView === "featured" ? "/" : "/featured";
45
+
46
+ const latestLink = {
47
+ href: latestHref,
48
+ label: t({
49
+ message: "Latest",
50
+ comment: "@context: Browse filter for latest posts",
51
+ }),
52
+ };
53
+ const featuredLink = {
54
+ href: featuredHref,
55
+ label: t({
56
+ message: "Featured",
57
+ comment: "@context: Browse filter for featured posts",
58
+ }),
59
+ };
60
+
61
+ // Default view tab comes first
62
+ const browseLinks =
63
+ homeDefaultView === "featured"
64
+ ? [featuredLink, latestLink]
65
+ : [latestLink, featuredLink];
66
+
67
+ const searchLabel = t({
68
+ message: "Search",
69
+ comment: "@context: Search icon link in browse nav",
70
+ });
71
+
72
+ const isHomePage =
73
+ currentPath === "/" ||
74
+ currentPath === "/featured" ||
75
+ currentPath === "/latest";
76
+
77
+ return (
78
+ <div class="site-page">
79
+ <header class="site-header">
80
+ <div class="site-header-inner">
81
+ <div class="site-header-top site-header-top-bordered">
82
+ <a href="/" class="site-logo">
83
+ {showHeaderAvatar && siteAvatarUrl && (
84
+ <img src={siteAvatarUrl} class="site-logo-avatar" alt="" />
85
+ )}
86
+ {siteName}
87
+ </a>
88
+ <div class="site-header-right">
89
+ {links.length > 0 && (
90
+ <nav class="site-header-nav">
91
+ {links.map((link) => (
92
+ <HeaderLink key={link.id} link={link} />
93
+ ))}
94
+ </nav>
95
+ )}
96
+ <a
97
+ href="/search"
98
+ class={`site-header-search ${currentPath === "/search" ? "site-header-search-active" : ""}`}
99
+ aria-label={searchLabel}
100
+ title={searchLabel}
101
+ >
102
+ <svg
103
+ xmlns="http://www.w3.org/2000/svg"
104
+ width="16"
105
+ height="16"
106
+ viewBox="0 0 24 24"
107
+ fill="none"
108
+ stroke="currentColor"
109
+ stroke-width="2"
110
+ stroke-linecap="round"
111
+ stroke-linejoin="round"
112
+ >
113
+ <circle cx="11" cy="11" r="8" />
114
+ <path d="m21 21-4.35-4.35" />
115
+ </svg>
116
+ </a>
117
+ </div>
118
+ </div>
119
+ {isHomePage && siteDescription && (
120
+ <p class="site-description">{siteDescription}</p>
121
+ )}
122
+ </div>
123
+ </header>
124
+
125
+ <main class="site-main">
126
+ <div class="site-container">
127
+ <div class="site-content">
128
+ {isHomePage && (
129
+ <nav class="site-browse-nav">
130
+ {browseLinks.map((link, i) => (
131
+ <>
132
+ {i > 0 && <span class="site-browse-sep">/</span>}
133
+ <a
134
+ key={link.href}
135
+ href={link.href}
136
+ class={`site-browse-link ${currentPath === link.href ? "site-browse-link-active" : ""}`}
137
+ >
138
+ {link.label}
139
+ </a>
140
+ </>
141
+ ))}
142
+ </nav>
143
+ )}
144
+ {isHomePage && isAuthenticated && <ComposePrompt />}
145
+ {children}
146
+ </div>
147
+ </div>
148
+ </main>
149
+
150
+ {siteFooterHtml && (
151
+ <footer class="site-footer" data-footer>
152
+ <div class="site-container">
153
+ <div
154
+ class="prose"
155
+ dangerouslySetInnerHTML={{ __html: siteFooterHtml }}
156
+ />
157
+ </div>
158
+ </footer>
159
+ )}
160
+
161
+ {isAuthenticated && <ComposeDialog collections={collections} />}
162
+ </div>
163
+ );
164
+ };
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Threads Theme - Archive Page
2
+ * Archive Page
3
3
  *
4
4
  * Posts grouped by year-month with format filter and cursor pagination.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
- import type { ArchivePageProps } from "../../../types.js";
10
- import { FORMATS } from "../../../types.js";
11
- import { Pagination as DefaultPagination } from "../../../theme/index.js";
9
+ import type { ArchivePageProps } from "../../types.js";
10
+ import { FORMATS } from "../../types.js";
11
+ import { Pagination } from "../shared/Pagination.js";
12
12
 
13
13
  function getFormatLabel(format: string): string {
14
14
  const { t } = useLingui();
@@ -39,7 +39,7 @@ function getFormatLabelPlural(format: string): string {
39
39
  comment: "@context: Post format label plural - quotes",
40
40
  }),
41
41
  };
42
- return labels[format] ?? `${format}s`;
42
+ return labels[format] ?? format + "s";
43
43
  }
44
44
 
45
45
  export const ArchivePage: FC<ArchivePageProps> = ({
@@ -48,17 +48,14 @@ export const ArchivePage: FC<ArchivePageProps> = ({
48
48
  nextCursor,
49
49
  format,
50
50
  featured,
51
- theme,
52
51
  }) => {
53
52
  const { t } = useLingui();
54
53
  const title = format
55
54
  ? getFormatLabelPlural(format)
56
55
  : t({ message: "Archive", comment: "@context: Archive page title" });
57
56
 
58
- const PaginationComponent = theme?.Pagination ?? DefaultPagination;
59
-
60
57
  return (
61
- <div class="py-6">
58
+ <div class="py-6" data-page="archive">
62
59
  <header class="mb-8">
63
60
  <h1 class="text-2xl font-semibold">{title}</h1>
64
61
 
@@ -66,7 +63,10 @@ export const ArchivePage: FC<ArchivePageProps> = ({
66
63
  <nav class="flex flex-wrap gap-2 mt-4">
67
64
  <a
68
65
  href="/archive"
69
- class={`badge ${!format && !featured ? "badge-primary" : "badge-outline"}`}
66
+ class={
67
+ "badge " +
68
+ (!format && !featured ? "badge-primary" : "badge-outline")
69
+ }
70
70
  >
71
71
  {t({
72
72
  message: "All",
@@ -76,15 +76,18 @@ export const ArchivePage: FC<ArchivePageProps> = ({
76
76
  {FORMATS.map((formatKey) => (
77
77
  <a
78
78
  key={formatKey}
79
- href={`/archive?format=${formatKey}`}
80
- class={`badge ${format === formatKey ? "badge-primary" : "badge-outline"}`}
79
+ href={"/archive?format=" + formatKey}
80
+ class={
81
+ "badge " +
82
+ (format === formatKey ? "badge-primary" : "badge-outline")
83
+ }
81
84
  >
82
85
  {getFormatLabelPlural(formatKey)}
83
86
  </a>
84
87
  ))}
85
88
  <a
86
89
  href="/archive?featured=true"
87
- class={`badge ${featured ? "badge-primary" : "badge-outline"}`}
90
+ class={"badge " + (featured ? "badge-primary" : "badge-outline")}
88
91
  >
89
92
  {t({
90
93
  message: "Featured",
@@ -104,7 +107,7 @@ export const ArchivePage: FC<ArchivePageProps> = ({
104
107
  </p>
105
108
  ) : (
106
109
  groups.map((group) => (
107
- <section key={`${group.year}-${group.month}`} class="mb-8">
110
+ <section key={group.year + "-" + group.month} class="mb-8">
108
111
  <h2 class="text-lg font-medium mb-4 text-muted-foreground">
109
112
  {group.label}
110
113
  </h2>
@@ -113,6 +116,8 @@ export const ArchivePage: FC<ArchivePageProps> = ({
113
116
  <article
114
117
  key={post.id}
115
118
  class="flex items-baseline gap-4 py-2.5"
119
+ data-post
120
+ data-format={post.format}
116
121
  >
117
122
  <time
118
123
  class="text-sm text-muted-foreground w-12 shrink-0"
@@ -124,7 +129,7 @@ export const ArchivePage: FC<ArchivePageProps> = ({
124
129
  <a href={post.permalink} class="hover:underline">
125
130
  {post.title ||
126
131
  post.excerpt?.slice(0, 80) ||
127
- `Post #${post.id}`}
132
+ "Post #" + post.id}
128
133
  </a>
129
134
  {!format && (
130
135
  <span class="ml-2 badge-outline text-xs">
@@ -141,10 +146,10 @@ export const ArchivePage: FC<ArchivePageProps> = ({
141
146
  </main>
142
147
 
143
148
  {/* Pagination */}
144
- <PaginationComponent
149
+ <Pagination
145
150
  baseUrl={
146
151
  format
147
- ? `/archive?format=${format}`
152
+ ? "/archive?format=" + format
148
153
  : featured
149
154
  ? "/archive?featured=true"
150
155
  : "/archive"
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Threads Theme - Collection Page
2
+ * Collection Page
3
3
  *
4
4
  * Collection header with divider-separated post list.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
- import type { CollectionPageProps } from "../../../types.js";
9
+ import type { CollectionPageProps } from "../../types.js";
10
10
 
11
11
  export const CollectionPage: FC<CollectionPageProps> = ({
12
12
  collection,
@@ -15,7 +15,7 @@ export const CollectionPage: FC<CollectionPageProps> = ({
15
15
  const { t } = useLingui();
16
16
 
17
17
  return (
18
- <div class="py-6">
18
+ <div class="py-6" data-page="collection">
19
19
  <header class="mb-8">
20
20
  <h1 class="text-2xl font-semibold">{collection.title}</h1>
21
21
  {collection.description && (
@@ -34,7 +34,12 @@ export const CollectionPage: FC<CollectionPageProps> = ({
34
34
  ) : (
35
35
  <div class="divide-y divide-border">
36
36
  {posts.map((post) => (
37
- <article key={post.id} class="h-entry py-4">
37
+ <article
38
+ key={post.id}
39
+ class="h-entry py-4"
40
+ data-post
41
+ data-format={post.format}
42
+ >
38
43
  {post.title && (
39
44
  <h2 class="p-name text-lg font-medium mb-2">
40
45
  <a href={post.permalink} class="u-url hover:underline">
@@ -44,9 +49,13 @@ export const CollectionPage: FC<CollectionPageProps> = ({
44
49
  )}
45
50
  <div
46
51
  class="e-content prose prose-sm"
52
+ data-post-body
47
53
  dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
48
54
  />
49
- <footer class="mt-2 text-sm text-muted-foreground">
55
+ <footer
56
+ class="mt-2 text-sm text-muted-foreground"
57
+ data-post-meta
58
+ >
50
59
  <time class="dt-published" datetime={post.publishedAt}>
51
60
  {post.publishedAtFormatted}
52
61
  </time>