@jant/core 0.3.23 → 0.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/dist/app.js +4 -5
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +3 -3
  7. package/dist/lib/constants.js +1 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/navigation.js +4 -5
  11. package/dist/lib/render.js +1 -1
  12. package/dist/lib/schemas.js +80 -38
  13. package/dist/lib/theme-components.js +8 -11
  14. package/dist/lib/time.js +56 -1
  15. package/dist/lib/timeline.js +119 -0
  16. package/dist/lib/view.js +61 -72
  17. package/dist/routes/api/posts.js +29 -35
  18. package/dist/routes/api/search.js +5 -6
  19. package/dist/routes/api/upload.js +13 -13
  20. package/dist/routes/dash/collections.js +22 -40
  21. package/dist/routes/dash/index.js +2 -2
  22. package/dist/routes/dash/navigation.js +25 -24
  23. package/dist/routes/dash/pages.js +42 -57
  24. package/dist/routes/dash/posts.js +27 -35
  25. package/dist/routes/feed/rss.js +2 -4
  26. package/dist/routes/feed/sitemap.js +10 -7
  27. package/dist/routes/pages/archive.js +12 -11
  28. package/dist/routes/pages/collection.js +11 -5
  29. package/dist/routes/pages/home.js +53 -61
  30. package/dist/routes/pages/page.js +60 -29
  31. package/dist/routes/pages/post.js +5 -12
  32. package/dist/routes/pages/search.js +3 -4
  33. package/dist/services/collection.js +52 -64
  34. package/dist/services/index.js +5 -3
  35. package/dist/services/navigation.js +29 -53
  36. package/dist/services/page.js +80 -0
  37. package/dist/services/post.js +68 -69
  38. package/dist/services/search.js +24 -18
  39. package/dist/theme/components/MediaGallery.js +19 -91
  40. package/dist/theme/components/PageForm.js +15 -15
  41. package/dist/theme/components/PostForm.js +136 -129
  42. package/dist/theme/components/PostList.js +13 -8
  43. package/dist/theme/components/ThreadView.js +3 -3
  44. package/dist/theme/components/TypeBadge.js +3 -14
  45. package/dist/theme/components/VisibilityBadge.js +33 -23
  46. package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
  47. package/dist/themes/threads/index.js +81 -0
  48. package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
  49. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  50. package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
  51. package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
  52. package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
  53. package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
  54. package/dist/themes/threads/timeline/LinkCard.js +68 -0
  55. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  56. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  57. package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
  58. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  59. package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
  60. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  61. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  62. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  63. package/dist/types.js +24 -40
  64. package/package.json +2 -1
  65. package/src/__tests__/helpers/app.ts +4 -0
  66. package/src/__tests__/helpers/db.ts +51 -74
  67. package/src/app.tsx +4 -6
  68. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  69. package/src/db/migrations/meta/_journal.json +7 -0
  70. package/src/db/schema.ts +63 -46
  71. package/src/i18n/locales/en.po +216 -164
  72. package/src/i18n/locales/en.ts +1 -1
  73. package/src/i18n/locales/zh-Hans.po +216 -164
  74. package/src/i18n/locales/zh-Hans.ts +1 -1
  75. package/src/i18n/locales/zh-Hant.po +216 -164
  76. package/src/i18n/locales/zh-Hant.ts +1 -1
  77. package/src/index.ts +28 -12
  78. package/src/lib/__tests__/excerpt.test.ts +125 -0
  79. package/src/lib/__tests__/schemas.test.ts +166 -105
  80. package/src/lib/__tests__/theme-components.test.ts +4 -25
  81. package/src/lib/__tests__/time.test.ts +62 -0
  82. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  83. package/src/lib/__tests__/view.test.ts +199 -51
  84. package/src/lib/constants.ts +1 -4
  85. package/src/lib/excerpt.ts +87 -0
  86. package/src/lib/feed.ts +22 -7
  87. package/src/lib/navigation.ts +6 -7
  88. package/src/lib/render.tsx +1 -1
  89. package/src/lib/schemas.ts +118 -52
  90. package/src/lib/theme-components.ts +10 -13
  91. package/src/lib/time.ts +64 -0
  92. package/src/lib/timeline.ts +170 -0
  93. package/src/lib/view.ts +80 -82
  94. package/src/preset.css +45 -0
  95. package/src/routes/api/__tests__/posts.test.ts +50 -108
  96. package/src/routes/api/__tests__/search.test.ts +2 -3
  97. package/src/routes/api/posts.ts +30 -30
  98. package/src/routes/api/search.ts +4 -4
  99. package/src/routes/api/upload.ts +16 -6
  100. package/src/routes/dash/collections.tsx +18 -40
  101. package/src/routes/dash/index.tsx +2 -2
  102. package/src/routes/dash/navigation.tsx +27 -26
  103. package/src/routes/dash/pages.tsx +45 -60
  104. package/src/routes/dash/posts.tsx +44 -52
  105. package/src/routes/feed/rss.ts +2 -1
  106. package/src/routes/feed/sitemap.ts +14 -4
  107. package/src/routes/pages/archive.tsx +14 -10
  108. package/src/routes/pages/collection.tsx +17 -6
  109. package/src/routes/pages/home.tsx +56 -81
  110. package/src/routes/pages/page.tsx +64 -27
  111. package/src/routes/pages/post.tsx +5 -14
  112. package/src/routes/pages/search.tsx +2 -2
  113. package/src/services/__tests__/collection.test.ts +257 -158
  114. package/src/services/__tests__/media.test.ts +18 -18
  115. package/src/services/__tests__/navigation.test.ts +161 -87
  116. package/src/services/__tests__/post-timeline.test.ts +92 -88
  117. package/src/services/__tests__/post.test.ts +342 -206
  118. package/src/services/__tests__/search.test.ts +19 -25
  119. package/src/services/collection.ts +71 -113
  120. package/src/services/index.ts +9 -8
  121. package/src/services/navigation.ts +38 -71
  122. package/src/services/page.ts +124 -0
  123. package/src/services/post.ts +93 -103
  124. package/src/services/search.ts +38 -27
  125. package/src/theme/components/MediaGallery.tsx +27 -96
  126. package/src/theme/components/PageForm.tsx +21 -21
  127. package/src/theme/components/PostForm.tsx +122 -118
  128. package/src/theme/components/PostList.tsx +58 -49
  129. package/src/theme/components/ThreadView.tsx +6 -3
  130. package/src/theme/components/TypeBadge.tsx +9 -17
  131. package/src/theme/components/VisibilityBadge.tsx +40 -23
  132. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  133. package/src/themes/{minimal → threads}/index.ts +30 -13
  134. package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
  135. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  136. package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
  137. package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
  138. package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
  139. package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
  140. package/src/themes/threads/style.css +336 -0
  141. package/src/themes/threads/timeline/LinkCard.tsx +67 -0
  142. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  143. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  144. package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
  145. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  146. package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
  147. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  148. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  149. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  150. package/src/types.ts +242 -98
  151. package/dist/routes/api/timeline.js +0 -120
  152. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  153. package/dist/themes/minimal/index.js +0 -65
  154. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  155. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  156. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  157. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  158. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  159. package/dist/themes/minimal/timeline/QuoteCard.js +0 -48
  160. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  161. package/src/routes/api/timeline.tsx +0 -159
  162. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  163. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  164. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  165. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  166. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  167. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  168. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  169. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
@@ -1,106 +1,96 @@
1
1
  /**
2
- * Minimal Theme - Archive Page
2
+ * Threads Theme - Archive Page
3
3
  *
4
- * Date-first list with type filter and cursor pagination.
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
9
  import type { ArchivePageProps } from "../../../types.js";
10
- import { POST_TYPES } from "../../../types.js";
11
- import { Pagination as DefaultPagination } from "../../../theme/components/Pagination.js";
10
+ import { FORMATS } from "../../../types.js";
11
+ import { Pagination as DefaultPagination } from "../../../theme/index.js";
12
12
 
13
- function getTypeLabel(type: string): string {
13
+ function getFormatLabel(format: string): string {
14
14
  const { t } = useLingui();
15
15
  const labels: Record<string, string> = {
16
- note: t({ message: "Note", comment: "@context: Post type label - note" }),
17
- article: t({
18
- message: "Article",
19
- comment: "@context: Post type label - article",
20
- }),
21
- link: t({ message: "Link", comment: "@context: Post type label - link" }),
16
+ note: t({ message: "Note", comment: "@context: Post format label - note" }),
17
+ link: t({ message: "Link", comment: "@context: Post format label - link" }),
22
18
  quote: t({
23
19
  message: "Quote",
24
- comment: "@context: Post type label - quote",
25
- }),
26
- image: t({
27
- message: "Image",
28
- comment: "@context: Post type label - image",
20
+ comment: "@context: Post format label - quote",
29
21
  }),
30
- page: t({ message: "Page", comment: "@context: Post type label - page" }),
31
22
  };
32
- return labels[type] ?? type;
23
+ return labels[format] ?? format;
33
24
  }
34
25
 
35
- function getTypeLabelPlural(type: string): string {
26
+ function getFormatLabelPlural(format: string): string {
36
27
  const { t } = useLingui();
37
28
  const labels: Record<string, string> = {
38
29
  note: t({
39
30
  message: "Notes",
40
- comment: "@context: Post type label plural - notes",
41
- }),
42
- article: t({
43
- message: "Articles",
44
- comment: "@context: Post type label plural - articles",
31
+ comment: "@context: Post format label plural - notes",
45
32
  }),
46
33
  link: t({
47
34
  message: "Links",
48
- comment: "@context: Post type label plural - links",
35
+ comment: "@context: Post format label plural - links",
49
36
  }),
50
37
  quote: t({
51
38
  message: "Quotes",
52
- comment: "@context: Post type label plural - quotes",
53
- }),
54
- image: t({
55
- message: "Images",
56
- comment: "@context: Post type label plural - images",
57
- }),
58
- page: t({
59
- message: "Pages",
60
- comment: "@context: Post type label plural - pages",
39
+ comment: "@context: Post format label plural - quotes",
61
40
  }),
62
41
  };
63
- return labels[type] ?? `${type}s`;
42
+ return labels[format] ?? `${format}s`;
64
43
  }
65
44
 
66
45
  export const ArchivePage: FC<ArchivePageProps> = ({
67
46
  groups,
68
47
  hasMore,
69
48
  nextCursor,
70
- type,
49
+ format,
50
+ featured,
71
51
  theme,
72
52
  }) => {
73
53
  const { t } = useLingui();
74
- const title = type
75
- ? getTypeLabelPlural(type)
54
+ const title = format
55
+ ? getFormatLabelPlural(format)
76
56
  : t({ message: "Archive", comment: "@context: Archive page title" });
77
57
 
78
58
  const PaginationComponent = theme?.Pagination ?? DefaultPagination;
79
59
 
80
60
  return (
81
- <div>
61
+ <div class="py-6">
82
62
  <header class="mb-8">
83
63
  <h1 class="text-2xl font-semibold">{title}</h1>
84
64
 
65
+ {/* Format filter */}
85
66
  <nav class="flex flex-wrap gap-2 mt-4">
86
67
  <a
87
68
  href="/archive"
88
- class={`text-sm ${!type ? "font-medium text-foreground" : "text-muted-foreground hover:text-foreground"}`}
69
+ class={`badge ${!format && !featured ? "badge-primary" : "badge-outline"}`}
89
70
  >
90
71
  {t({
91
72
  message: "All",
92
- comment: "@context: Archive filter - all types",
73
+ comment: "@context: Archive filter - all formats",
93
74
  })}
94
75
  </a>
95
- {POST_TYPES.filter((t) => t !== "page").map((typeKey) => (
76
+ {FORMATS.map((formatKey) => (
96
77
  <a
97
- key={typeKey}
98
- href={`/archive?type=${typeKey}`}
99
- class={`text-sm ${type === typeKey ? "font-medium text-foreground" : "text-muted-foreground hover:text-foreground"}`}
78
+ key={formatKey}
79
+ href={`/archive?format=${formatKey}`}
80
+ class={`badge ${format === formatKey ? "badge-primary" : "badge-outline"}`}
100
81
  >
101
- {getTypeLabelPlural(typeKey)}
82
+ {getFormatLabelPlural(formatKey)}
102
83
  </a>
103
84
  ))}
85
+ <a
86
+ href="/archive?featured=true"
87
+ class={`badge ${featured ? "badge-primary" : "badge-outline"}`}
88
+ >
89
+ {t({
90
+ message: "Featured",
91
+ comment: "@context: Archive filter - featured posts",
92
+ })}
93
+ </a>
104
94
  </nav>
105
95
  </header>
106
96
 
@@ -118,9 +108,12 @@ export const ArchivePage: FC<ArchivePageProps> = ({
118
108
  <h2 class="text-lg font-medium mb-4 text-muted-foreground">
119
109
  {group.label}
120
110
  </h2>
121
- <div class="flex flex-col gap-3">
111
+ <div class="divide-y divide-border">
122
112
  {group.posts.map((post) => (
123
- <article key={post.id} class="flex items-baseline gap-4">
113
+ <article
114
+ key={post.id}
115
+ class="flex items-baseline gap-4 py-2.5"
116
+ >
124
117
  <time
125
118
  class="text-sm text-muted-foreground w-12 shrink-0"
126
119
  datetime={post.publishedAt}
@@ -130,12 +123,12 @@ export const ArchivePage: FC<ArchivePageProps> = ({
130
123
  <div class="flex-1 min-w-0">
131
124
  <a href={post.permalink} class="hover:underline">
132
125
  {post.title ||
133
- post.content?.slice(0, 80) ||
126
+ post.excerpt?.slice(0, 80) ||
134
127
  `Post #${post.id}`}
135
128
  </a>
136
- {!type && (
137
- <span class="ml-2 text-xs text-muted-foreground">
138
- {getTypeLabel(post.type)}
129
+ {!format && (
130
+ <span class="ml-2 badge-outline text-xs">
131
+ {getFormatLabel(post.format)}
139
132
  </span>
140
133
  )}
141
134
  </div>
@@ -147,8 +140,15 @@ export const ArchivePage: FC<ArchivePageProps> = ({
147
140
  )}
148
141
  </main>
149
142
 
143
+ {/* Pagination */}
150
144
  <PaginationComponent
151
- baseUrl={type ? `/archive?type=${type}` : "/archive"}
145
+ baseUrl={
146
+ format
147
+ ? `/archive?format=${format}`
148
+ : featured
149
+ ? "/archive?featured=true"
150
+ : "/archive"
151
+ }
152
152
  hasMore={hasMore}
153
153
  nextCursor={nextCursor}
154
154
  />
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Threads Theme - Collection Page
3
+ *
4
+ * Collection header with divider-separated post list.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "@lingui/react/macro";
9
+ import type { CollectionPageProps } from "../../../types.js";
10
+
11
+ export const CollectionPage: FC<CollectionPageProps> = ({
12
+ collection,
13
+ posts,
14
+ }) => {
15
+ const { t } = useLingui();
16
+
17
+ return (
18
+ <div class="py-6">
19
+ <header class="mb-8">
20
+ <h1 class="text-2xl font-semibold">{collection.title}</h1>
21
+ {collection.description && (
22
+ <p class="text-muted-foreground mt-2">{collection.description}</p>
23
+ )}
24
+ </header>
25
+
26
+ <main>
27
+ {posts.length === 0 ? (
28
+ <p class="text-muted-foreground">
29
+ {t({
30
+ message: "No posts in this collection.",
31
+ comment: "@context: Empty state message",
32
+ })}
33
+ </p>
34
+ ) : (
35
+ <div class="divide-y divide-border">
36
+ {posts.map((post) => (
37
+ <article key={post.id} class="h-entry py-4">
38
+ {post.title && (
39
+ <h2 class="p-name text-lg font-medium mb-2">
40
+ <a href={post.permalink} class="u-url hover:underline">
41
+ {post.title}
42
+ </a>
43
+ </h2>
44
+ )}
45
+ <div
46
+ class="e-content prose prose-sm"
47
+ dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
48
+ />
49
+ <footer class="mt-2 text-sm text-muted-foreground">
50
+ <time class="dt-published" datetime={post.publishedAt}>
51
+ {post.publishedAtFormatted}
52
+ </time>
53
+ </footer>
54
+ </article>
55
+ ))}
56
+ </div>
57
+ )}
58
+ </main>
59
+ </div>
60
+ );
61
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Minimal Theme - Home Page
2
+ * Threads Theme - Home Page
3
3
  *
4
- * Renders the timeline feed with thread previews.
4
+ * Clean feed of posts separated by dividers.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
@@ -22,7 +22,7 @@ export const HomePage: FC<HomePageProps> = ({
22
22
  return (
23
23
  <>
24
24
  {items.length === 0 ? (
25
- <p class="text-muted-foreground">
25
+ <p class="py-12 text-center text-muted-foreground">
26
26
  {t({
27
27
  message: "No posts yet.",
28
28
  comment: "@context: Empty state message on home page",
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Minimal Theme - Post Page
2
+ * Threads Theme - Post Page
3
3
  *
4
- * Clean article layout for a single post.
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
9
  import type { PostPageProps } from "../../../types.js";
10
- import { MediaGallery as DefaultMediaGallery } from "../../../theme/components/MediaGallery.js";
10
+ import { MediaGallery as DefaultMediaGallery } from "../../../theme/index.js";
11
11
 
12
12
  export const PostPage: FC<PostPageProps> = ({ post, theme }) => {
13
13
  const { t } = useLingui();
@@ -15,23 +15,27 @@ export const PostPage: FC<PostPageProps> = ({ post, theme }) => {
15
15
  const Gallery = theme?.MediaGallery ?? DefaultMediaGallery;
16
16
 
17
17
  return (
18
- <article class="h-entry">
18
+ <article class="h-entry py-6">
19
19
  {post.title && (
20
20
  <h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
21
21
  )}
22
22
 
23
23
  <div
24
24
  class="e-content prose"
25
- dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
25
+ dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
26
26
  />
27
27
 
28
- {post.media.length > 0 && <Gallery attachments={post.media} />}
28
+ {post.media.length > 0 && (
29
+ <div class="threads-media mt-4">
30
+ <Gallery attachments={post.media} />
31
+ </div>
32
+ )}
29
33
 
30
- <footer class="mt-8 pt-4 border-t border-border text-sm text-muted-foreground">
34
+ <footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
31
35
  <time class="dt-published" datetime={post.publishedAt}>
32
36
  {post.publishedAtFormatted}
33
37
  </time>
34
- <a href={post.permalink} class="u-url ml-4 hover:underline">
38
+ <a href={post.permalink} class="u-url ml-4">
35
39
  {t({
36
40
  message: "Permalink",
37
41
  comment: "@context: Link to permanent URL of post",
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Minimal Theme - Search Page
2
+ * Threads Theme - Search Page
3
3
  *
4
- * Minimal search form + results with page-based pagination.
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
9
  import type { SearchPageProps } from "../../../types.js";
10
- import { PagePagination as DefaultPagePagination } from "../../../theme/components/Pagination.js";
10
+ import { PagePagination as DefaultPagePagination } from "../../../theme/index.js";
11
11
 
12
12
  export const SearchPage: FC<SearchPageProps> = ({
13
13
  query,
@@ -26,9 +26,10 @@ export const SearchPage: FC<SearchPageProps> = ({
26
26
  const PaginationComponent = theme?.PagePagination ?? DefaultPagePagination;
27
27
 
28
28
  return (
29
- <div>
29
+ <div class="py-6">
30
30
  <h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
31
31
 
32
+ {/* Search form */}
32
33
  <form method="get" action="/search" class="mb-8">
33
34
  <div class="flex gap-2">
34
35
  <input
@@ -51,12 +52,14 @@ export const SearchPage: FC<SearchPageProps> = ({
51
52
  </div>
52
53
  </form>
53
54
 
55
+ {/* Error */}
54
56
  {error && (
55
57
  <div class="alert-destructive mb-6">
56
58
  <h2>{error}</h2>
57
59
  </div>
58
60
  )}
59
61
 
62
+ {/* Results */}
60
63
  {query && !error && (
61
64
  <div>
62
65
  <p class="text-sm text-muted-foreground mb-4">
@@ -79,26 +82,25 @@ export const SearchPage: FC<SearchPageProps> = ({
79
82
 
80
83
  {results.length > 0 && (
81
84
  <>
82
- <div class="flex flex-col gap-4">
85
+ <div class="divide-y divide-border">
83
86
  {results.map((result) => (
84
- <article key={result.post.id} class="py-3">
85
- <a href={result.post.permalink} class="block group">
86
- <h2 class="font-medium group-hover:underline">
87
+ <article key={result.post.id} class="py-4">
88
+ <a href={result.post.permalink} class="block">
89
+ <h2 class="font-medium hover:underline">
87
90
  {result.post.title ||
88
- result.post.content?.slice(0, 60) ||
91
+ result.post.excerpt?.slice(0, 60) ||
89
92
  `Post #${result.post.id}`}
90
93
  </h2>
91
94
 
92
95
  {result.snippet && (
93
96
  <p
94
- class="text-sm text-muted-foreground mt-1 line-clamp-2"
97
+ class="text-sm text-muted-foreground mt-2 line-clamp-2"
95
98
  dangerouslySetInnerHTML={{ __html: result.snippet }}
96
99
  />
97
100
  )}
98
101
 
99
- <footer class="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
100
- <span>{result.post.type}</span>
101
- <span>&middot;</span>
102
+ <footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
103
+ <span class="badge-outline">{result.post.format}</span>
102
104
  <time datetime={result.post.publishedAt}>
103
105
  {result.post.publishedAtFormatted}
104
106
  </time>
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Minimal Theme - Single Page
2
+ * Threads Theme - Single Page
3
3
  *
4
- * Simple page content layout for type="page" posts.
4
+ * Custom page (type "page") view — clean centered content.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
@@ -9,14 +9,14 @@ import type { SinglePageProps } from "../../../types.js";
9
9
 
10
10
  export const SinglePage: FC<SinglePageProps> = ({ page }) => {
11
11
  return (
12
- <article class="h-entry">
12
+ <article class="h-entry py-6">
13
13
  {page.title && (
14
14
  <h1 class="p-name text-2xl font-semibold mb-6">{page.title}</h1>
15
15
  )}
16
16
 
17
17
  <div
18
18
  class="e-content prose"
19
- dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
19
+ dangerouslySetInnerHTML={{ __html: page.bodyHtml || "" }}
20
20
  />
21
21
  </article>
22
22
  );