@jant/core 0.3.24 → 0.3.25

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 (206) hide show
  1. package/dist/app.js +50 -25
  2. package/dist/db/schema.js +1 -1
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +3 -9
  7. package/dist/lib/constants.js +1 -0
  8. package/dist/lib/nav-reorder.js +1 -1
  9. package/dist/lib/navigation.js +26 -1
  10. package/dist/lib/pagination.js +44 -0
  11. package/dist/lib/render.js +7 -11
  12. package/dist/lib/schemas.js +3 -3
  13. package/dist/lib/theme.js +4 -4
  14. package/dist/lib/timeline.js +24 -48
  15. package/dist/lib/view.js +2 -2
  16. package/dist/routes/api/collections.js +124 -0
  17. package/dist/routes/api/nav-items.js +104 -0
  18. package/dist/routes/api/pages.js +91 -0
  19. package/dist/routes/api/posts.js +2 -2
  20. package/dist/routes/api/search.js +2 -2
  21. package/dist/routes/api/settings.js +68 -0
  22. package/dist/routes/compose.js +48 -0
  23. package/dist/routes/dash/collections.js +2 -2
  24. package/dist/routes/dash/index.js +1 -1
  25. package/dist/routes/dash/media.js +2 -2
  26. package/dist/routes/dash/pages.js +411 -62
  27. package/dist/routes/dash/posts.js +3 -5
  28. package/dist/routes/dash/redirects.js +2 -2
  29. package/dist/routes/dash/settings.js +79 -5
  30. package/dist/routes/feed/rss.js +2 -2
  31. package/dist/routes/feed/sitemap.js +1 -1
  32. package/dist/routes/pages/archive.js +3 -6
  33. package/dist/routes/pages/collection.js +3 -6
  34. package/dist/routes/pages/collections.js +28 -0
  35. package/dist/routes/pages/featured.js +32 -0
  36. package/dist/routes/pages/home.js +9 -50
  37. package/dist/routes/pages/page.js +29 -32
  38. package/dist/routes/pages/post.js +3 -6
  39. package/dist/routes/pages/search.js +3 -6
  40. package/dist/services/page.js +5 -1
  41. package/dist/services/post.js +40 -6
  42. package/dist/services/search.js +1 -1
  43. package/dist/ui/compose/ComposeDialog.js +452 -0
  44. package/dist/ui/compose/ComposePrompt.js +55 -0
  45. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  46. package/dist/{theme/components → ui/dash}/PostForm.js +0 -27
  47. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  48. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  49. package/dist/{theme/components → ui/dash}/index.js +3 -6
  50. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  51. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  52. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  53. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  54. package/dist/ui/feed/TimelineFeed.js +41 -0
  55. package/dist/ui/feed/TimelineItem.js +27 -0
  56. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  57. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  58. package/dist/ui/layouts/SiteLayout.js +141 -0
  59. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  60. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  61. package/dist/ui/pages/CollectionsPage.js +76 -0
  62. package/dist/ui/pages/FeaturedPage.js +24 -0
  63. package/dist/ui/pages/HomePage.js +24 -0
  64. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  65. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  66. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  67. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  68. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  69. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  70. package/dist/ui/shared/index.js +5 -0
  71. package/package.json +1 -9
  72. package/src/__tests__/helpers/db.ts +3 -0
  73. package/src/app.tsx +57 -27
  74. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  75. package/src/db/migrations/meta/_journal.json +7 -0
  76. package/src/db/schema.ts +1 -1
  77. package/src/i18n/locales/en.po +332 -181
  78. package/src/i18n/locales/en.ts +1 -1
  79. package/src/i18n/locales/zh-Hans.po +332 -181
  80. package/src/i18n/locales/zh-Hans.ts +1 -1
  81. package/src/i18n/locales/zh-Hant.po +332 -181
  82. package/src/i18n/locales/zh-Hant.ts +1 -1
  83. package/src/index.ts +7 -36
  84. package/src/lib/__tests__/schemas.test.ts +60 -19
  85. package/src/lib/__tests__/timeline.test.ts +45 -81
  86. package/src/lib/__tests__/view.test.ts +13 -7
  87. package/src/lib/constants.ts +1 -0
  88. package/src/lib/nav-reorder.ts +1 -1
  89. package/src/lib/navigation.ts +40 -2
  90. package/src/lib/pagination.ts +50 -0
  91. package/src/lib/render.tsx +7 -14
  92. package/src/lib/schemas.ts +8 -6
  93. package/src/lib/theme.ts +5 -5
  94. package/src/lib/timeline.ts +28 -57
  95. package/src/lib/view.ts +2 -2
  96. package/src/preset.css +2 -1
  97. package/src/routes/__tests__/compose.test.ts +199 -0
  98. package/src/routes/api/__tests__/collections.test.ts +249 -0
  99. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  100. package/src/routes/api/__tests__/pages.test.ts +218 -0
  101. package/src/routes/api/__tests__/settings.test.ts +132 -0
  102. package/src/routes/api/collections.ts +143 -0
  103. package/src/routes/api/nav-items.ts +115 -0
  104. package/src/routes/api/pages.ts +101 -0
  105. package/src/routes/api/posts.ts +2 -2
  106. package/src/routes/api/search.ts +2 -2
  107. package/src/routes/api/settings.ts +91 -0
  108. package/src/routes/compose.ts +63 -0
  109. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  110. package/src/routes/dash/collections.tsx +2 -2
  111. package/src/routes/dash/index.tsx +1 -1
  112. package/src/routes/dash/media.tsx +2 -2
  113. package/src/routes/dash/pages.tsx +443 -70
  114. package/src/routes/dash/posts.tsx +3 -7
  115. package/src/routes/dash/redirects.tsx +2 -2
  116. package/src/routes/dash/settings.tsx +83 -5
  117. package/src/routes/feed/rss.ts +2 -2
  118. package/src/routes/feed/sitemap.ts +1 -1
  119. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  120. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  121. package/src/routes/pages/archive.tsx +2 -6
  122. package/src/routes/pages/collection.tsx +2 -6
  123. package/src/routes/pages/collections.tsx +36 -0
  124. package/src/routes/pages/featured.tsx +38 -0
  125. package/src/routes/pages/home.tsx +9 -55
  126. package/src/routes/pages/page.tsx +28 -30
  127. package/src/routes/pages/post.tsx +2 -5
  128. package/src/routes/pages/search.tsx +2 -6
  129. package/src/services/__tests__/page.test.ts +106 -0
  130. package/src/services/__tests__/post.test.ts +114 -15
  131. package/src/services/page.ts +13 -1
  132. package/src/services/post.ts +57 -7
  133. package/src/services/search.ts +2 -2
  134. package/src/styles/tokens.css +47 -0
  135. package/src/styles/ui.css +491 -0
  136. package/src/types.ts +29 -159
  137. package/src/ui/compose/ComposeDialog.tsx +395 -0
  138. package/src/ui/compose/ComposePrompt.tsx +55 -0
  139. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  140. package/src/{theme/components → ui/dash}/PostForm.tsx +0 -25
  141. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  142. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  143. package/src/ui/dash/index.ts +10 -0
  144. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  145. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  146. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  147. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  148. package/src/ui/feed/TimelineFeed.tsx +49 -0
  149. package/src/ui/feed/TimelineItem.tsx +45 -0
  150. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  151. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  152. package/src/ui/layouts/SiteLayout.tsx +150 -0
  153. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  154. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  155. package/src/ui/pages/CollectionsPage.tsx +73 -0
  156. package/src/ui/pages/FeaturedPage.tsx +31 -0
  157. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  158. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  159. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  160. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  161. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  162. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  163. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  164. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  165. package/src/ui/shared/index.ts +12 -0
  166. package/bin/jant.js +0 -185
  167. package/dist/lib/theme-components.js +0 -46
  168. package/dist/routes/dash/navigation.js +0 -289
  169. package/dist/theme/index.js +0 -18
  170. package/dist/theme/layouts/index.js +0 -2
  171. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  172. package/dist/themes/threads/index.js +0 -81
  173. package/dist/themes/threads/pages/HomePage.js +0 -25
  174. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  175. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  176. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  177. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  178. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  179. package/src/lib/__tests__/theme-components.test.ts +0 -105
  180. package/src/lib/theme-components.ts +0 -65
  181. package/src/routes/dash/navigation.tsx +0 -317
  182. package/src/theme/components/index.ts +0 -23
  183. package/src/theme/index.ts +0 -22
  184. package/src/theme/layouts/index.ts +0 -7
  185. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  186. package/src/themes/threads/index.ts +0 -100
  187. package/src/themes/threads/style.css +0 -336
  188. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  189. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  190. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  191. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  192. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  193. /package/dist/{theme → ui}/color-themes.js +0 -0
  194. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  195. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  196. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  197. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  198. /package/dist/{theme/components → ui/dash}/PageForm.js +0 -0
  199. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  200. /package/src/{theme → ui}/color-themes.ts +0 -0
  201. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  202. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  203. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  204. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  205. /package/src/{theme/components → ui/dash}/PageForm.tsx +0 -0
  206. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -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>
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Collections Listing Page
3
+ *
4
+ * Lists all collections with titles, descriptions, and post counts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "@lingui/react/macro";
9
+ import type { CollectionsPageProps } from "../../types.js";
10
+
11
+ export const CollectionsPage: FC<CollectionsPageProps> = ({ collections }) => {
12
+ const { t } = useLingui();
13
+
14
+ return (
15
+ <div class="py-6" data-page="collections">
16
+ <header class="mb-8">
17
+ <h1 class="text-2xl font-semibold">
18
+ {t({
19
+ message: "Collections",
20
+ comment: "@context: Collections page heading",
21
+ })}
22
+ </h1>
23
+ </header>
24
+
25
+ <main>
26
+ {collections.length === 0 ? (
27
+ <p class="text-muted-foreground">
28
+ {t({
29
+ message: "No collections yet.",
30
+ comment: "@context: Empty state message on collections page",
31
+ })}
32
+ </p>
33
+ ) : (
34
+ <div class="divide-y divide-border">
35
+ {collections.map((collection) => (
36
+ <a
37
+ key={collection.id}
38
+ href={"/c/" + collection.slug}
39
+ class="block py-4 hover:bg-accent/50 -mx-4 px-4 rounded-md transition-colors"
40
+ >
41
+ <div class="flex items-center gap-3">
42
+ {collection.icon && (
43
+ <span class="text-2xl">{collection.icon}</span>
44
+ )}
45
+ <div class="flex-1 min-w-0">
46
+ <h2 class="font-medium">{collection.title}</h2>
47
+ {collection.description && (
48
+ <p class="text-sm text-muted-foreground mt-1">
49
+ {collection.description}
50
+ </p>
51
+ )}
52
+ </div>
53
+ <span class="text-sm text-muted-foreground shrink-0">
54
+ {collection.postCount}{" "}
55
+ {collection.postCount === 1
56
+ ? t({
57
+ message: "post",
58
+ comment: "@context: Singular post count label",
59
+ })
60
+ : t({
61
+ message: "posts",
62
+ comment: "@context: Plural post count label",
63
+ })}
64
+ </span>
65
+ </div>
66
+ </a>
67
+ ))}
68
+ </div>
69
+ )}
70
+ </main>
71
+ </div>
72
+ );
73
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Featured Page
3
+ *
4
+ * Shows featured posts as a timeline feed.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "@lingui/react/macro";
9
+ import type { FeaturedPageProps } from "../../types.js";
10
+ import { TimelineFeed } from "../feed/TimelineFeed.js";
11
+
12
+ export const FeaturedPage: FC<FeaturedPageProps> = ({ items }) => {
13
+ const { t } = useLingui();
14
+
15
+ return (
16
+ <div data-page="featured">
17
+ <main>
18
+ {items.length === 0 ? (
19
+ <p class="text-muted-foreground">
20
+ {t({
21
+ message: "No featured posts yet.",
22
+ comment: "@context: Empty state message on featured page",
23
+ })}
24
+ </p>
25
+ ) : (
26
+ <TimelineFeed items={items} />
27
+ )}
28
+ </main>
29
+ </div>
30
+ );
31
+ };
@@ -1,26 +1,23 @@
1
1
  /**
2
- * Threads Theme - Home Page
2
+ * Home Page
3
3
  *
4
- * Clean feed of posts separated by dividers.
4
+ * Timeline feed with per-type card components and thread previews.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
- import type { HomePageProps } from "../../../types.js";
10
- import { TimelineFeed as DefaultTimelineFeed } from "../timeline/TimelineFeed.js";
9
+ import type { HomePageProps } from "../../types.js";
10
+ import { TimelineFeed } from "../feed/TimelineFeed.js";
11
11
 
12
12
  export const HomePage: FC<HomePageProps> = ({
13
13
  items,
14
- hasMore,
15
- nextCursor,
16
- theme,
14
+ currentPage,
15
+ totalPages,
17
16
  }) => {
18
17
  const { t } = useLingui();
19
18
 
20
- const Feed = theme?.TimelineFeed ?? DefaultTimelineFeed;
21
-
22
19
  return (
23
- <>
20
+ <div data-page="home">
24
21
  {items.length === 0 ? (
25
22
  <p class="py-12 text-center text-muted-foreground">
26
23
  {t({
@@ -29,13 +26,12 @@ export const HomePage: FC<HomePageProps> = ({
29
26
  })}
30
27
  </p>
31
28
  ) : (
32
- <Feed
29
+ <TimelineFeed
33
30
  items={items}
34
- hasMore={hasMore}
35
- nextCursor={nextCursor}
36
- theme={theme}
31
+ currentPage={currentPage}
32
+ totalPages={totalPages}
37
33
  />
38
34
  )}
39
- </>
35
+ </div>
40
36
  );
41
37
  };
@@ -1,37 +1,46 @@
1
1
  /**
2
- * Threads Theme - Post Page
2
+ * Single Post Page
3
3
  *
4
4
  * Single post view — clean, no card border, with divider footer.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
- import type { PostPageProps } from "../../../types.js";
10
- import { MediaGallery as DefaultMediaGallery } from "../../../theme/index.js";
9
+ import type { PostPageProps } from "../../types.js";
10
+ import { MediaGallery } from "../shared/MediaGallery.js";
11
11
 
12
- export const PostPage: FC<PostPageProps> = ({ post, theme }) => {
12
+ export const PostPage: FC<PostPageProps> = ({ post }) => {
13
13
  const { t } = useLingui();
14
14
 
15
- const Gallery = theme?.MediaGallery ?? DefaultMediaGallery;
16
-
17
15
  return (
18
- <article class="h-entry py-6">
16
+ <article
17
+ class="h-entry py-6"
18
+ data-page="post"
19
+ data-post
20
+ data-format={post.format}
21
+ >
19
22
  {post.title && (
20
23
  <h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
21
24
  )}
22
25
 
23
- <div
24
- class="e-content prose"
25
- dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
26
- />
26
+ {post.bodyHtml && (
27
+ <div
28
+ class="e-content prose"
29
+ data-post-body
30
+ dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
31
+ />
32
+ )}
27
33
 
28
34
  {post.media.length > 0 && (
29
- <div class="threads-media mt-4">
30
- <Gallery attachments={post.media} />
35
+ <div class="mt-4" data-post-media>
36
+ <MediaGallery attachments={post.media} />
31
37
  </div>
32
38
  )}
33
39
 
34
- <footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
40
+ <footer
41
+ class="mt-6 pt-4 border-t text-sm text-muted-foreground"
42
+ data-post-meta
43
+ >
35
44
  <time class="dt-published" datetime={post.publishedAt}>
36
45
  {post.publishedAtFormatted}
37
46
  </time>
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Threads Theme - Search Page
2
+ * Search Page
3
3
  *
4
4
  * Search form and results — divider-separated instead of bordered cards.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
- import type { SearchPageProps } from "../../../types.js";
10
- import { PagePagination as DefaultPagePagination } from "../../../theme/index.js";
9
+ import type { SearchPageProps } from "../../types.js";
10
+ import { PagePagination } from "../shared/Pagination.js";
11
11
 
12
12
  export const SearchPage: FC<SearchPageProps> = ({
13
13
  query,
@@ -15,7 +15,6 @@ export const SearchPage: FC<SearchPageProps> = ({
15
15
  error,
16
16
  hasMore,
17
17
  page,
18
- theme,
19
18
  }) => {
20
19
  const { t } = useLingui();
21
20
  const searchTitle = t({
@@ -23,10 +22,8 @@ export const SearchPage: FC<SearchPageProps> = ({
23
22
  comment: "@context: Search page title",
24
23
  });
25
24
 
26
- const PaginationComponent = theme?.PagePagination ?? DefaultPagePagination;
27
-
28
25
  return (
29
- <div class="py-6">
26
+ <div class="py-6" data-page="search">
30
27
  <h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
31
28
 
32
29
  {/* Search form */}
@@ -84,12 +81,17 @@ export const SearchPage: FC<SearchPageProps> = ({
84
81
  <>
85
82
  <div class="divide-y divide-border">
86
83
  {results.map((result) => (
87
- <article key={result.post.id} class="py-4">
84
+ <article
85
+ key={result.post.id}
86
+ class="py-4"
87
+ data-post
88
+ data-format={result.post.format}
89
+ >
88
90
  <a href={result.post.permalink} class="block">
89
91
  <h2 class="font-medium hover:underline">
90
92
  {result.post.title ||
91
93
  result.post.excerpt?.slice(0, 60) ||
92
- `Post #${result.post.id}`}
94
+ "Post #" + result.post.id}
93
95
  </h2>
94
96
 
95
97
  {result.snippet && (
@@ -110,8 +112,8 @@ export const SearchPage: FC<SearchPageProps> = ({
110
112
  ))}
111
113
  </div>
112
114
 
113
- <PaginationComponent
114
- baseUrl={`/search?q=${encodeURIComponent(query)}`}
115
+ <PagePagination
116
+ baseUrl={"/search?q=" + encodeURIComponent(query)}
115
117
  currentPage={page}
116
118
  hasMore={hasMore}
117
119
  />
@@ -1,15 +1,15 @@
1
1
  /**
2
- * Threads Theme - Single Page
2
+ * Single Page (Custom Page)
3
3
  *
4
- * Custom page (type "page") view — clean centered content.
4
+ * Custom page view — clean centered content.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
- import type { SinglePageProps } from "../../../types.js";
8
+ import type { SinglePageProps } from "../../types.js";
9
9
 
10
10
  export const SinglePage: FC<SinglePageProps> = ({ page }) => {
11
11
  return (
12
- <article class="h-entry py-6">
12
+ <article class="h-entry py-6" data-page="single-page">
13
13
  {page.title && (
14
14
  <h1 class="p-name text-2xl font-semibold mb-6">{page.title}</h1>
15
15
  )}
@@ -2,7 +2,7 @@
2
2
  * Media Gallery Component
3
3
  *
4
4
  * Renders media attachments in a horizontal scrollable row,
5
- * similar to Threads.net's image carousel.
5
+ * similar to an image carousel.
6
6
  */
7
7
 
8
8
  import type { FC } from "hono/jsx";
@@ -6,6 +6,7 @@
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
+ import { getPageNumbers } from "../../lib/pagination.js";
9
10
 
10
11
  export interface PaginationProps {
11
12
  /** Base URL for pagination links (e.g., "/archive", "/search?q=test") */
@@ -115,15 +116,20 @@ export const LoadMore: FC<LoadMoreProps> = ({ href, hasMore, text }) => {
115
116
  };
116
117
 
117
118
  /**
118
- * Page-based pagination (for search results etc.)
119
+ * Page-based pagination with optional numbered pages.
120
+ *
121
+ * When `totalPages` is provided, renders numbered page links with ellipsis.
122
+ * Otherwise falls back to simple Previous/Next navigation.
119
123
  */
120
124
  export interface PagePaginationProps {
121
125
  /** Base URL (query params will be added) */
122
126
  baseUrl: string;
123
127
  /** Current page (1-indexed) */
124
128
  currentPage: number;
125
- /** Whether there are more pages */
126
- hasMore: boolean;
129
+ /** Whether there are more pages (used when totalPages is unknown) */
130
+ hasMore?: boolean;
131
+ /** Total number of pages (enables numbered pagination) */
132
+ totalPages?: number;
127
133
  /** Page parameter name (default: "page") */
128
134
  pageParam?: string;
129
135
  }
@@ -132,11 +138,12 @@ export const PagePagination: FC<PagePaginationProps> = ({
132
138
  baseUrl,
133
139
  currentPage,
134
140
  hasMore,
141
+ totalPages,
135
142
  pageParam = "page",
136
143
  }) => {
137
144
  const { t } = useLingui();
138
145
  const hasPrev = currentPage > 1;
139
- const hasNext = hasMore;
146
+ const hasNext = totalPages ? currentPage < totalPages : (hasMore ?? false);
140
147
 
141
148
  if (!hasPrev && !hasNext) {
142
149
  return null;
@@ -161,6 +168,62 @@ export const PagePagination: FC<PagePaginationProps> = ({
161
168
  message: "Next",
162
169
  comment: "@context: Pagination button - next page",
163
170
  });
171
+
172
+ // Numbered pagination when totalPages is known
173
+ if (totalPages && totalPages > 1) {
174
+ const pageNumbers = getPageNumbers(currentPage, totalPages);
175
+
176
+ return (
177
+ <nav
178
+ class="flex items-center justify-center gap-4 py-6 text-sm"
179
+ aria-label="Pagination"
180
+ >
181
+ {hasPrev ? (
182
+ <a
183
+ href={buildUrl(currentPage - 1)}
184
+ class="underline text-muted-foreground hover:text-foreground"
185
+ >
186
+ {prevText}
187
+ </a>
188
+ ) : (
189
+ <span class="text-muted-foreground/50">{prevText}</span>
190
+ )}
191
+
192
+ {pageNumbers.map((page, i) =>
193
+ page === 0 ? (
194
+ <span key={`ellipsis-${i}`} class="text-muted-foreground">
195
+ ...
196
+ </span>
197
+ ) : page === currentPage ? (
198
+ <span key={page} aria-current="page">
199
+ {page}
200
+ </span>
201
+ ) : (
202
+ <a
203
+ key={page}
204
+ href={buildUrl(page)}
205
+ class="underline text-muted-foreground hover:text-foreground"
206
+ >
207
+ {page}
208
+ </a>
209
+ ),
210
+ )}
211
+
212
+ {hasNext ? (
213
+ <a
214
+ href={buildUrl(currentPage + 1)}
215
+ class="underline text-muted-foreground hover:text-foreground"
216
+ >
217
+ {nextText}
218
+ </a>
219
+ ) : (
220
+ <span class="text-muted-foreground/50">{nextText}</span>
221
+ )}
222
+ </nav>
223
+ );
224
+ }
225
+
226
+ // Simple prev/next fallback when totalPages is unknown
164
227
  const pageText = t({
165
228
  message: "Page {page}",
166
229
  comment: "@context: Pagination - current page indicator",
@@ -35,7 +35,7 @@ const ThreadPost: FC<{
35
35
  {post.title && (
36
36
  <h2 class="p-name text-lg font-medium mb-2">
37
37
  <a
38
- href={`${post.slug ? `/${post.slug}` : `/p/${sqid.encode(post.id)}`}`}
38
+ href={`${post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`}`}
39
39
  class="u-url hover:underline"
40
40
  >
41
41
  {post.title}
@@ -65,7 +65,7 @@ const ThreadPost: FC<{
65
65
  )}
66
66
  {!isCurrent && (
67
67
  <a
68
- href={`${post.slug ? `/${post.slug}` : `/p/${sqid.encode(post.id)}`}`}
68
+ href={`${post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`}`}
69
69
  class="text-xs hover:underline"
70
70
  >
71
71
  {t({
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { getPageNumbers } from "../../../lib/pagination.js";
3
+
4
+ describe("getPageNumbers", () => {
5
+ it("returns all pages when totalPages <= 7", () => {
6
+ expect(getPageNumbers(1, 1)).toEqual([1]);
7
+ expect(getPageNumbers(1, 5)).toEqual([1, 2, 3, 4, 5]);
8
+ expect(getPageNumbers(3, 7)).toEqual([1, 2, 3, 4, 5, 6, 7]);
9
+ });
10
+
11
+ it("shows ellipsis for gaps in large page ranges", () => {
12
+ // Page 1 of 20: 1, 2, ..., 20
13
+ const result = getPageNumbers(1, 20);
14
+ expect(result).toEqual([1, 2, 0, 20]);
15
+ });
16
+
17
+ it("shows ellipsis on both sides for middle pages", () => {
18
+ // Page 10 of 20: 1, ..., 9, 10, 11, ..., 20
19
+ const result = getPageNumbers(10, 20);
20
+ expect(result).toEqual([1, 0, 9, 10, 11, 0, 20]);
21
+ });
22
+
23
+ it("shows ellipsis only on right for early pages", () => {
24
+ // Page 3 of 20: 1, 2, 3, 4, ..., 20
25
+ const result = getPageNumbers(3, 20);
26
+ expect(result).toEqual([1, 2, 3, 4, 0, 20]);
27
+ });
28
+
29
+ it("shows ellipsis only on left for late pages", () => {
30
+ // Page 18 of 20: 1, ..., 17, 18, 19, 20
31
+ const result = getPageNumbers(18, 20);
32
+ expect(result).toEqual([1, 0, 17, 18, 19, 20]);
33
+ });
34
+
35
+ it("handles last page", () => {
36
+ // Page 20 of 20: 1, ..., 19, 20
37
+ const result = getPageNumbers(20, 20);
38
+ expect(result).toEqual([1, 0, 19, 20]);
39
+ });
40
+
41
+ it("handles page 2 of large range", () => {
42
+ // Page 2 of 20: 1, 2, 3, ..., 20
43
+ const result = getPageNumbers(2, 20);
44
+ expect(result).toEqual([1, 2, 3, 0, 20]);
45
+ });
46
+ });
@@ -0,0 +1,12 @@
1
+ export { EmptyState, type EmptyStateProps } from "./EmptyState.js";
2
+ export { MediaGallery, type MediaGalleryProps } from "./MediaGallery.js";
3
+ export {
4
+ Pagination,
5
+ LoadMore,
6
+ PagePagination,
7
+ type PaginationProps,
8
+ type LoadMoreProps,
9
+ type PagePaginationProps,
10
+ } from "./Pagination.js";
11
+ export { getPageNumbers } from "../../lib/pagination.js";
12
+ export { ThreadView, type ThreadViewProps } from "./ThreadView.js";