@jant/core 0.3.23 → 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 (248) hide show
  1. package/dist/app.js +50 -26
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +5 -11
  7. package/dist/lib/constants.js +2 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/nav-reorder.js +1 -1
  11. package/dist/lib/navigation.js +30 -6
  12. package/dist/lib/pagination.js +44 -0
  13. package/dist/lib/render.js +7 -11
  14. package/dist/lib/schemas.js +80 -38
  15. package/dist/lib/theme.js +4 -4
  16. package/dist/lib/time.js +56 -1
  17. package/dist/lib/timeline.js +95 -0
  18. package/dist/lib/view.js +61 -72
  19. package/dist/routes/api/collections.js +124 -0
  20. package/dist/routes/api/nav-items.js +104 -0
  21. package/dist/routes/api/pages.js +91 -0
  22. package/dist/routes/api/posts.js +27 -33
  23. package/dist/routes/api/search.js +4 -5
  24. package/dist/routes/api/settings.js +68 -0
  25. package/dist/routes/api/upload.js +13 -13
  26. package/dist/routes/compose.js +48 -0
  27. package/dist/routes/dash/collections.js +24 -42
  28. package/dist/routes/dash/index.js +3 -3
  29. package/dist/routes/dash/media.js +2 -2
  30. package/dist/routes/dash/pages.js +440 -106
  31. package/dist/routes/dash/posts.js +27 -37
  32. package/dist/routes/dash/redirects.js +2 -2
  33. package/dist/routes/dash/settings.js +79 -5
  34. package/dist/routes/feed/rss.js +4 -6
  35. package/dist/routes/feed/sitemap.js +11 -8
  36. package/dist/routes/pages/archive.js +13 -15
  37. package/dist/routes/pages/collection.js +12 -9
  38. package/dist/routes/pages/collections.js +28 -0
  39. package/dist/routes/pages/featured.js +32 -0
  40. package/dist/routes/pages/home.js +19 -68
  41. package/dist/routes/pages/page.js +57 -29
  42. package/dist/routes/pages/post.js +7 -17
  43. package/dist/routes/pages/search.js +5 -9
  44. package/dist/services/collection.js +52 -64
  45. package/dist/services/index.js +5 -3
  46. package/dist/services/navigation.js +29 -53
  47. package/dist/services/page.js +84 -0
  48. package/dist/services/post.js +102 -69
  49. package/dist/services/search.js +24 -18
  50. package/dist/types.js +24 -40
  51. package/dist/ui/compose/ComposeDialog.js +452 -0
  52. package/dist/ui/compose/ComposePrompt.js +55 -0
  53. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
  54. package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
  55. package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
  56. package/dist/{theme/components → ui/dash}/PostList.js +18 -13
  57. package/dist/ui/dash/StatusBadge.js +46 -0
  58. package/dist/{theme/components → ui/dash}/index.js +3 -6
  59. package/dist/ui/feed/LinkCard.js +72 -0
  60. package/dist/ui/feed/NoteCard.js +58 -0
  61. package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
  62. package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
  63. package/dist/ui/feed/TimelineFeed.js +41 -0
  64. package/dist/ui/feed/TimelineItem.js +27 -0
  65. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  66. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  67. package/dist/ui/layouts/SiteLayout.js +141 -0
  68. package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
  69. package/dist/ui/pages/CollectionPage.js +70 -0
  70. package/dist/ui/pages/CollectionsPage.js +76 -0
  71. package/dist/ui/pages/FeaturedPage.js +24 -0
  72. package/dist/ui/pages/HomePage.js +24 -0
  73. package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
  74. package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
  75. package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
  76. package/dist/ui/shared/MediaGallery.js +35 -0
  77. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  78. package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
  79. package/dist/ui/shared/index.js +5 -0
  80. package/package.json +2 -9
  81. package/src/__tests__/helpers/app.ts +4 -0
  82. package/src/__tests__/helpers/db.ts +53 -73
  83. package/src/app.tsx +56 -28
  84. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  85. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  86. package/src/db/migrations/meta/_journal.json +14 -0
  87. package/src/db/schema.ts +63 -46
  88. package/src/i18n/locales/en.po +443 -240
  89. package/src/i18n/locales/en.ts +1 -1
  90. package/src/i18n/locales/zh-Hans.po +443 -240
  91. package/src/i18n/locales/zh-Hans.ts +1 -1
  92. package/src/i18n/locales/zh-Hant.po +443 -240
  93. package/src/i18n/locales/zh-Hant.ts +1 -1
  94. package/src/index.ts +29 -42
  95. package/src/lib/__tests__/excerpt.test.ts +125 -0
  96. package/src/lib/__tests__/schemas.test.ts +201 -99
  97. package/src/lib/__tests__/time.test.ts +62 -0
  98. package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
  99. package/src/lib/__tests__/view.test.ts +204 -50
  100. package/src/lib/constants.ts +2 -4
  101. package/src/lib/excerpt.ts +87 -0
  102. package/src/lib/feed.ts +22 -7
  103. package/src/lib/nav-reorder.ts +1 -1
  104. package/src/lib/navigation.ts +45 -8
  105. package/src/lib/pagination.ts +50 -0
  106. package/src/lib/render.tsx +7 -14
  107. package/src/lib/schemas.ts +119 -51
  108. package/src/lib/theme.ts +5 -5
  109. package/src/lib/time.ts +64 -0
  110. package/src/lib/timeline.ts +141 -0
  111. package/src/lib/view.ts +80 -82
  112. package/src/preset.css +46 -0
  113. package/src/routes/__tests__/compose.test.ts +199 -0
  114. package/src/routes/api/__tests__/collections.test.ts +249 -0
  115. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  116. package/src/routes/api/__tests__/pages.test.ts +218 -0
  117. package/src/routes/api/__tests__/posts.test.ts +50 -108
  118. package/src/routes/api/__tests__/search.test.ts +2 -3
  119. package/src/routes/api/__tests__/settings.test.ts +132 -0
  120. package/src/routes/api/collections.ts +143 -0
  121. package/src/routes/api/nav-items.ts +115 -0
  122. package/src/routes/api/pages.ts +101 -0
  123. package/src/routes/api/posts.ts +28 -28
  124. package/src/routes/api/search.ts +3 -3
  125. package/src/routes/api/settings.ts +91 -0
  126. package/src/routes/api/upload.ts +16 -6
  127. package/src/routes/compose.ts +63 -0
  128. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  129. package/src/routes/dash/collections.tsx +20 -42
  130. package/src/routes/dash/index.tsx +3 -3
  131. package/src/routes/dash/media.tsx +2 -2
  132. package/src/routes/dash/pages.tsx +480 -122
  133. package/src/routes/dash/posts.tsx +42 -54
  134. package/src/routes/dash/redirects.tsx +2 -2
  135. package/src/routes/dash/settings.tsx +83 -5
  136. package/src/routes/feed/rss.ts +4 -3
  137. package/src/routes/feed/sitemap.ts +15 -5
  138. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  139. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  140. package/src/routes/pages/archive.tsx +15 -15
  141. package/src/routes/pages/collection.tsx +16 -9
  142. package/src/routes/pages/collections.tsx +36 -0
  143. package/src/routes/pages/featured.tsx +38 -0
  144. package/src/routes/pages/home.tsx +21 -92
  145. package/src/routes/pages/page.tsx +62 -27
  146. package/src/routes/pages/post.tsx +6 -18
  147. package/src/routes/pages/search.tsx +3 -7
  148. package/src/services/__tests__/collection.test.ts +257 -158
  149. package/src/services/__tests__/media.test.ts +18 -18
  150. package/src/services/__tests__/navigation.test.ts +161 -87
  151. package/src/services/__tests__/page.test.ts +106 -0
  152. package/src/services/__tests__/post-timeline.test.ts +92 -88
  153. package/src/services/__tests__/post.test.ts +432 -197
  154. package/src/services/__tests__/search.test.ts +19 -25
  155. package/src/services/collection.ts +71 -113
  156. package/src/services/index.ts +9 -8
  157. package/src/services/navigation.ts +38 -71
  158. package/src/services/page.ts +136 -0
  159. package/src/services/post.ts +141 -101
  160. package/src/services/search.ts +38 -27
  161. package/src/styles/tokens.css +47 -0
  162. package/src/styles/ui.css +491 -0
  163. package/src/types.ts +212 -198
  164. package/src/ui/compose/ComposeDialog.tsx +395 -0
  165. package/src/ui/compose/ComposePrompt.tsx +55 -0
  166. package/src/ui/dash/FormatBadge.tsx +28 -0
  167. package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
  168. package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
  169. package/src/ui/dash/PostList.tsx +101 -0
  170. package/src/ui/dash/StatusBadge.tsx +61 -0
  171. package/src/ui/dash/index.ts +10 -0
  172. package/src/ui/feed/LinkCard.tsx +72 -0
  173. package/src/ui/feed/NoteCard.tsx +63 -0
  174. package/src/ui/feed/QuoteCard.tsx +68 -0
  175. package/src/ui/feed/ThreadPreview.tsx +48 -0
  176. package/src/ui/feed/TimelineFeed.tsx +49 -0
  177. package/src/ui/feed/TimelineItem.tsx +45 -0
  178. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  179. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  180. package/src/ui/layouts/SiteLayout.tsx +150 -0
  181. package/src/ui/pages/ArchivePage.tsx +162 -0
  182. package/src/ui/pages/CollectionPage.tsx +70 -0
  183. package/src/ui/pages/CollectionsPage.tsx +73 -0
  184. package/src/ui/pages/FeaturedPage.tsx +31 -0
  185. package/src/ui/pages/HomePage.tsx +37 -0
  186. package/src/ui/pages/PostPage.tsx +56 -0
  187. package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
  188. package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
  189. package/src/ui/shared/MediaGallery.tsx +59 -0
  190. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  191. package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
  192. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  193. package/src/ui/shared/index.ts +12 -0
  194. package/bin/jant.js +0 -185
  195. package/dist/lib/theme-components.js +0 -49
  196. package/dist/routes/api/timeline.js +0 -120
  197. package/dist/routes/dash/navigation.js +0 -288
  198. package/dist/theme/components/MediaGallery.js +0 -107
  199. package/dist/theme/components/VisibilityBadge.js +0 -37
  200. package/dist/theme/index.js +0 -18
  201. package/dist/theme/layouts/index.js +0 -2
  202. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  203. package/dist/themes/minimal/index.js +0 -65
  204. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  205. package/dist/themes/minimal/pages/HomePage.js +0 -25
  206. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  207. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  208. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  209. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  210. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  211. package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
  212. package/src/lib/__tests__/theme-components.test.ts +0 -126
  213. package/src/lib/theme-components.ts +0 -68
  214. package/src/routes/api/timeline.tsx +0 -159
  215. package/src/routes/dash/navigation.tsx +0 -316
  216. package/src/theme/components/MediaGallery.tsx +0 -128
  217. package/src/theme/components/PostList.tsx +0 -92
  218. package/src/theme/components/TypeBadge.tsx +0 -37
  219. package/src/theme/components/VisibilityBadge.tsx +0 -45
  220. package/src/theme/components/index.ts +0 -23
  221. package/src/theme/index.ts +0 -22
  222. package/src/theme/layouts/index.ts +0 -7
  223. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  224. package/src/themes/minimal/index.ts +0 -83
  225. package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
  226. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  227. package/src/themes/minimal/pages/HomePage.tsx +0 -41
  228. package/src/themes/minimal/pages/PostPage.tsx +0 -43
  229. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  230. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  231. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  232. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  233. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  234. package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
  235. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
  236. package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
  237. /package/dist/{theme → ui}/color-themes.js +0 -0
  238. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  239. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  240. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  241. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  242. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  243. /package/src/{theme → ui}/color-themes.ts +0 -0
  244. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  245. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  246. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  247. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  248. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Quote Card
3
+ *
4
+ * Left-border accent blockquote with full date in footer.
5
+ *
6
+ * Fields:
7
+ * - quoteText: the quoted text
8
+ * - title: attribution (who said it)
9
+ * - url: source link
10
+ * - bodyHtml: commentary
11
+ */
12
+
13
+ import type { FC } from "hono/jsx";
14
+ import type { TimelineCardProps } from "../../types.js";
15
+
16
+ export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
17
+ return (
18
+ <article
19
+ class={`h-entry${compact ? " feed-compact" : ""}`}
20
+ data-post
21
+ data-format="quote"
22
+ >
23
+ {post.quoteText && (
24
+ <blockquote class="feed-quote">
25
+ <div
26
+ class={`e-content ${compact ? "text-sm" : "text-base"} leading-relaxed`}
27
+ >
28
+ {post.quoteText}
29
+ </div>
30
+ </blockquote>
31
+ )}
32
+ {!compact && (post.title || post.url) && (
33
+ <div class="mt-2 text-sm text-muted-foreground">
34
+ &mdash;{" "}
35
+ {post.url ? (
36
+ <a
37
+ href={post.url}
38
+ class="hover:underline"
39
+ target="_blank"
40
+ rel="noopener noreferrer"
41
+ >
42
+ {post.title || "Source"}
43
+ </a>
44
+ ) : (
45
+ <span>{post.title}</span>
46
+ )}
47
+ </div>
48
+ )}
49
+ {!compact && post.bodyHtml && (
50
+ <div
51
+ class="mt-3 prose text-muted-foreground"
52
+ data-post-body
53
+ dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
54
+ />
55
+ )}
56
+ <footer class="mt-2" data-post-meta>
57
+ <a
58
+ href={post.permalink}
59
+ class="u-url text-xs text-muted-foreground hover:underline"
60
+ >
61
+ <time class="dt-published" datetime={post.publishedAt}>
62
+ {post.publishedAtFormatted}
63
+ </time>
64
+ </a>
65
+ </footer>
66
+ </article>
67
+ );
68
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Thread Preview
3
+ *
4
+ * Root post + vertical line connector + compact replies underneath.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "@lingui/react/macro";
9
+ import type { ThreadPreviewProps } from "../../types.js";
10
+ import { TimelineItem } from "./TimelineItem.js";
11
+ import { TimelineItemFromPost } from "./TimelineItem.js";
12
+
13
+ export const ThreadPreview: FC<ThreadPreviewProps> = ({
14
+ rootPost,
15
+ previewReplies,
16
+ totalReplyCount,
17
+ }) => {
18
+ const { t } = useLingui();
19
+ const remainingCount = totalReplyCount - previewReplies.length;
20
+
21
+ return (
22
+ <div>
23
+ <TimelineItem item={{ post: rootPost }} />
24
+ {previewReplies.length > 0 && (
25
+ <div class="feed-replies">
26
+ {previewReplies.map((reply) => (
27
+ <div key={reply.id} class="feed-reply">
28
+ <TimelineItemFromPost post={reply} compact />
29
+ </div>
30
+ ))}
31
+ {remainingCount > 0 && (
32
+ <div class="feed-reply">
33
+ <a
34
+ href={rootPost.permalink}
35
+ class="text-sm text-muted-foreground hover:text-foreground hover:underline"
36
+ >
37
+ {t({
38
+ message: `Show ${remainingCount} more ${remainingCount === 1 ? "reply" : "replies"}`,
39
+ comment: "@context: Link to show remaining thread replies",
40
+ })}
41
+ </a>
42
+ </div>
43
+ )}
44
+ </div>
45
+ )}
46
+ </div>
47
+ );
48
+ };
@@ -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
+ };
@@ -42,6 +42,12 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
42
42
  // Read theme style from Hono context if available
43
43
  const themeStyle = c ? c.get("themeStyle") : undefined;
44
44
 
45
+ // Read custom CSS from Hono context if available
46
+ const customCSS = c ? c.get("customCSS") : undefined;
47
+
48
+ // Check authentication status for data attribute
49
+ const isAuthenticated = c ? c.get("isAuthenticated") : false;
50
+
45
51
  return (
46
52
  <html lang={resolvedLang}>
47
53
  <head>
@@ -52,9 +58,13 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
52
58
  <ViteClient />
53
59
  <Link href="/src/style.css" rel="stylesheet" />
54
60
  {themeStyle && <style>{themeStyle}</style>}
61
+ {customCSS && <style>{customCSS}</style>}
55
62
  <Script src="/src/client.ts" />
56
63
  </head>
57
- <body class="bg-background text-foreground antialiased">
64
+ <body
65
+ class="bg-background text-foreground antialiased"
66
+ {...(isAuthenticated ? { "data-authenticated": true } : {})}
67
+ >
58
68
  {content}
59
69
  <div id="toast-container" class="toast-container">
60
70
  {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,150 @@
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
+ children,
36
+ }) => {
37
+ const { t } = useLingui();
38
+
39
+ const browseLinks = [
40
+ {
41
+ href: "/",
42
+ label: t({
43
+ message: "Latest",
44
+ comment: "@context: Browse filter for latest posts",
45
+ }),
46
+ },
47
+ {
48
+ href: "/featured",
49
+ label: t({
50
+ message: "Featured",
51
+ comment: "@context: Browse filter for featured posts",
52
+ }),
53
+ },
54
+ // {
55
+ // href: "/collections",
56
+ // label: t({
57
+ // message: "Collections",
58
+ // comment: "@context: Browse filter for collections",
59
+ // }),
60
+ // },
61
+ // {
62
+ // href: "/archive",
63
+ // label: t({
64
+ // message: "Archive",
65
+ // comment: "@context: Browse filter for archive",
66
+ // }),
67
+ // },
68
+ ];
69
+
70
+ const searchLabel = t({
71
+ message: "Search",
72
+ comment: "@context: Search icon link in browse nav",
73
+ });
74
+
75
+ const isHomePage = currentPath === "/" || currentPath === "/featured";
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
+ {siteName}
84
+ </a>
85
+ <div class="site-header-right">
86
+ {links.length > 0 && (
87
+ <nav class="site-header-nav">
88
+ {links.map((link) => (
89
+ <HeaderLink key={link.id} link={link} />
90
+ ))}
91
+ </nav>
92
+ )}
93
+ <a
94
+ href="/search"
95
+ class={`site-header-search ${currentPath === "/search" ? "site-header-search-active" : ""}`}
96
+ aria-label={searchLabel}
97
+ title={searchLabel}
98
+ >
99
+ <svg
100
+ xmlns="http://www.w3.org/2000/svg"
101
+ width="16"
102
+ height="16"
103
+ viewBox="0 0 24 24"
104
+ fill="none"
105
+ stroke="currentColor"
106
+ stroke-width="2"
107
+ stroke-linecap="round"
108
+ stroke-linejoin="round"
109
+ >
110
+ <circle cx="11" cy="11" r="8" />
111
+ <path d="m21 21-4.35-4.35" />
112
+ </svg>
113
+ </a>
114
+ </div>
115
+ </div>
116
+ {isHomePage && siteDescription && (
117
+ <p class="site-description">{siteDescription}</p>
118
+ )}
119
+ </div>
120
+ </header>
121
+
122
+ <main class="site-main">
123
+ <div class="site-container">
124
+ <div class="site-content">
125
+ {isHomePage && (
126
+ <nav class="site-browse-nav">
127
+ {browseLinks.map((link, i) => (
128
+ <>
129
+ {i > 0 && <span class="site-browse-sep">/</span>}
130
+ <a
131
+ key={link.href}
132
+ href={link.href}
133
+ class={`site-browse-link ${currentPath === link.href ? "site-browse-link-active" : ""}`}
134
+ >
135
+ {link.label}
136
+ </a>
137
+ </>
138
+ ))}
139
+ </nav>
140
+ )}
141
+ {isHomePage && isAuthenticated && <ComposePrompt />}
142
+ {children}
143
+ </div>
144
+ </div>
145
+ </main>
146
+
147
+ {isAuthenticated && <ComposeDialog collections={collections} />}
148
+ </div>
149
+ );
150
+ };
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Archive Page
3
+ *
4
+ * Posts grouped by year-month with format filter and cursor pagination.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "@lingui/react/macro";
9
+ import type { ArchivePageProps } from "../../types.js";
10
+ import { FORMATS } from "../../types.js";
11
+ import { Pagination } from "../shared/Pagination.js";
12
+
13
+ function getFormatLabel(format: string): string {
14
+ const { t } = useLingui();
15
+ const labels: Record<string, string> = {
16
+ note: t({ message: "Note", comment: "@context: Post format label - note" }),
17
+ link: t({ message: "Link", comment: "@context: Post format label - link" }),
18
+ quote: t({
19
+ message: "Quote",
20
+ comment: "@context: Post format label - quote",
21
+ }),
22
+ };
23
+ return labels[format] ?? format;
24
+ }
25
+
26
+ function getFormatLabelPlural(format: string): string {
27
+ const { t } = useLingui();
28
+ const labels: Record<string, string> = {
29
+ note: t({
30
+ message: "Notes",
31
+ comment: "@context: Post format label plural - notes",
32
+ }),
33
+ link: t({
34
+ message: "Links",
35
+ comment: "@context: Post format label plural - links",
36
+ }),
37
+ quote: t({
38
+ message: "Quotes",
39
+ comment: "@context: Post format label plural - quotes",
40
+ }),
41
+ };
42
+ return labels[format] ?? format + "s";
43
+ }
44
+
45
+ export const ArchivePage: FC<ArchivePageProps> = ({
46
+ groups,
47
+ hasMore,
48
+ nextCursor,
49
+ format,
50
+ featured,
51
+ }) => {
52
+ const { t } = useLingui();
53
+ const title = format
54
+ ? getFormatLabelPlural(format)
55
+ : t({ message: "Archive", comment: "@context: Archive page title" });
56
+
57
+ return (
58
+ <div class="py-6" data-page="archive">
59
+ <header class="mb-8">
60
+ <h1 class="text-2xl font-semibold">{title}</h1>
61
+
62
+ {/* Format filter */}
63
+ <nav class="flex flex-wrap gap-2 mt-4">
64
+ <a
65
+ href="/archive"
66
+ class={
67
+ "badge " +
68
+ (!format && !featured ? "badge-primary" : "badge-outline")
69
+ }
70
+ >
71
+ {t({
72
+ message: "All",
73
+ comment: "@context: Archive filter - all formats",
74
+ })}
75
+ </a>
76
+ {FORMATS.map((formatKey) => (
77
+ <a
78
+ key={formatKey}
79
+ href={"/archive?format=" + formatKey}
80
+ class={
81
+ "badge " +
82
+ (format === formatKey ? "badge-primary" : "badge-outline")
83
+ }
84
+ >
85
+ {getFormatLabelPlural(formatKey)}
86
+ </a>
87
+ ))}
88
+ <a
89
+ href="/archive?featured=true"
90
+ class={"badge " + (featured ? "badge-primary" : "badge-outline")}
91
+ >
92
+ {t({
93
+ message: "Featured",
94
+ comment: "@context: Archive filter - featured posts",
95
+ })}
96
+ </a>
97
+ </nav>
98
+ </header>
99
+
100
+ <main>
101
+ {groups.length === 0 ? (
102
+ <p class="text-muted-foreground">
103
+ {t({
104
+ message: "No posts found.",
105
+ comment: "@context: Archive empty state",
106
+ })}
107
+ </p>
108
+ ) : (
109
+ groups.map((group) => (
110
+ <section key={group.year + "-" + group.month} class="mb-8">
111
+ <h2 class="text-lg font-medium mb-4 text-muted-foreground">
112
+ {group.label}
113
+ </h2>
114
+ <div class="divide-y divide-border">
115
+ {group.posts.map((post) => (
116
+ <article
117
+ key={post.id}
118
+ class="flex items-baseline gap-4 py-2.5"
119
+ data-post
120
+ data-format={post.format}
121
+ >
122
+ <time
123
+ class="text-sm text-muted-foreground w-12 shrink-0"
124
+ datetime={post.publishedAt}
125
+ >
126
+ {new Date(post.publishedAt).getUTCDate()}
127
+ </time>
128
+ <div class="flex-1 min-w-0">
129
+ <a href={post.permalink} class="hover:underline">
130
+ {post.title ||
131
+ post.excerpt?.slice(0, 80) ||
132
+ "Post #" + post.id}
133
+ </a>
134
+ {!format && (
135
+ <span class="ml-2 badge-outline text-xs">
136
+ {getFormatLabel(post.format)}
137
+ </span>
138
+ )}
139
+ </div>
140
+ </article>
141
+ ))}
142
+ </div>
143
+ </section>
144
+ ))
145
+ )}
146
+ </main>
147
+
148
+ {/* Pagination */}
149
+ <Pagination
150
+ baseUrl={
151
+ format
152
+ ? "/archive?format=" + format
153
+ : featured
154
+ ? "/archive?featured=true"
155
+ : "/archive"
156
+ }
157
+ hasMore={hasMore}
158
+ nextCursor={nextCursor}
159
+ />
160
+ </div>
161
+ );
162
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 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" data-page="collection">
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
38
+ key={post.id}
39
+ class="h-entry py-4"
40
+ data-post
41
+ data-format={post.format}
42
+ >
43
+ {post.title && (
44
+ <h2 class="p-name text-lg font-medium mb-2">
45
+ <a href={post.permalink} class="u-url hover:underline">
46
+ {post.title}
47
+ </a>
48
+ </h2>
49
+ )}
50
+ <div
51
+ class="e-content prose prose-sm"
52
+ data-post-body
53
+ dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
54
+ />
55
+ <footer
56
+ class="mt-2 text-sm text-muted-foreground"
57
+ data-post-meta
58
+ >
59
+ <time class="dt-published" datetime={post.publishedAt}>
60
+ {post.publishedAtFormatted}
61
+ </time>
62
+ </footer>
63
+ </article>
64
+ ))}
65
+ </div>
66
+ )}
67
+ </main>
68
+ </div>
69
+ );
70
+ };