@jant/core 0.3.7 → 0.3.9

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 (258) hide show
  1. package/dist/app.js +11 -4
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +15 -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/lib/image.js +39 -15
  8. package/dist/lib/media-helpers.js +49 -0
  9. package/dist/lib/nav-reorder.js +27 -0
  10. package/dist/lib/navigation.js +35 -0
  11. package/dist/lib/storage.js +164 -0
  12. package/dist/lib/theme-components.js +49 -0
  13. package/dist/routes/api/posts.js +12 -7
  14. package/dist/routes/api/timeline.js +116 -0
  15. package/dist/routes/api/upload.js +35 -24
  16. package/dist/routes/dash/media.js +24 -14
  17. package/dist/routes/dash/navigation.js +274 -0
  18. package/dist/routes/dash/posts.js +4 -1
  19. package/dist/routes/feed/rss.js +3 -2
  20. package/dist/routes/pages/archive.js +14 -27
  21. package/dist/routes/pages/collection.js +10 -19
  22. package/dist/routes/pages/home.js +84 -126
  23. package/dist/routes/pages/page.js +19 -38
  24. package/dist/routes/pages/post.js +47 -56
  25. package/dist/routes/pages/search.js +13 -26
  26. package/dist/services/index.js +3 -1
  27. package/dist/services/media.js +8 -6
  28. package/dist/services/navigation.js +115 -0
  29. package/dist/services/post.js +26 -1
  30. package/dist/theme/components/PostForm.js +4 -3
  31. package/dist/theme/components/PostList.js +5 -0
  32. package/dist/theme/components/index.js +2 -0
  33. package/dist/theme/components/timeline/ArticleCard.js +50 -0
  34. package/dist/theme/components/timeline/ImageCard.js +86 -0
  35. package/dist/theme/components/timeline/LinkCard.js +62 -0
  36. package/dist/theme/components/timeline/NoteCard.js +37 -0
  37. package/dist/theme/components/timeline/QuoteCard.js +51 -0
  38. package/dist/theme/components/timeline/ThreadPreview.js +52 -0
  39. package/dist/theme/components/timeline/TimelineFeed.js +43 -0
  40. package/dist/theme/components/timeline/TimelineItem.js +25 -0
  41. package/dist/theme/components/timeline/index.js +8 -0
  42. package/dist/theme/layouts/DashLayout.js +8 -0
  43. package/dist/theme/layouts/SiteLayout.js +160 -0
  44. package/dist/theme/layouts/index.js +1 -0
  45. package/dist/types/sortablejs.d.js +5 -0
  46. package/dist/types.js +32 -0
  47. package/package.json +4 -2
  48. package/src/__tests__/helpers/app.ts +1 -0
  49. package/src/__tests__/helpers/db.ts +20 -0
  50. package/src/app.tsx +12 -7
  51. package/src/client.ts +1 -0
  52. package/src/db/migrations/0003_add_navigation_links.sql +8 -0
  53. package/src/db/migrations/0004_add_storage_provider.sql +3 -0
  54. package/src/db/migrations/meta/0003_snapshot.json +821 -0
  55. package/src/db/migrations/meta/_journal.json +21 -0
  56. package/src/db/schema.ts +15 -1
  57. package/src/i18n/locales/en.po +148 -80
  58. package/src/i18n/locales/en.ts +1 -1
  59. package/src/i18n/locales/zh-Hans.po +150 -103
  60. package/src/i18n/locales/zh-Hans.ts +1 -1
  61. package/src/i18n/locales/zh-Hant.po +150 -103
  62. package/src/i18n/locales/zh-Hant.ts +1 -1
  63. package/src/index.ts +5 -0
  64. package/src/lib/__tests__/image.test.ts +96 -0
  65. package/src/lib/__tests__/storage.test.ts +162 -0
  66. package/src/lib/__tests__/theme-components.test.ts +107 -0
  67. package/src/lib/image.ts +46 -16
  68. package/src/lib/media-helpers.ts +65 -0
  69. package/src/lib/nav-reorder.ts +26 -0
  70. package/src/lib/navigation.ts +46 -0
  71. package/src/lib/storage.ts +236 -0
  72. package/src/lib/theme-components.ts +76 -0
  73. package/src/routes/api/__tests__/posts.test.ts +8 -8
  74. package/src/routes/api/__tests__/timeline.test.ts +242 -0
  75. package/src/routes/api/posts.ts +20 -6
  76. package/src/routes/api/timeline.tsx +152 -0
  77. package/src/routes/api/upload.ts +52 -25
  78. package/src/routes/dash/media.tsx +40 -8
  79. package/src/routes/dash/navigation.tsx +306 -0
  80. package/src/routes/dash/posts.tsx +5 -0
  81. package/src/routes/feed/rss.ts +3 -2
  82. package/src/routes/pages/archive.tsx +15 -23
  83. package/src/routes/pages/collection.tsx +8 -15
  84. package/src/routes/pages/home.tsx +118 -122
  85. package/src/routes/pages/page.tsx +17 -30
  86. package/src/routes/pages/post.tsx +63 -60
  87. package/src/routes/pages/search.tsx +18 -22
  88. package/src/services/__tests__/media.test.ts +73 -28
  89. package/src/services/__tests__/navigation.test.ts +213 -0
  90. package/src/services/__tests__/post-timeline.test.ts +220 -0
  91. package/src/services/index.ts +7 -0
  92. package/src/services/media.ts +12 -8
  93. package/src/services/navigation.ts +165 -0
  94. package/src/services/post.ts +48 -1
  95. package/src/styles/components.css +59 -0
  96. package/src/theme/components/PostForm.tsx +13 -2
  97. package/src/theme/components/PostList.tsx +7 -0
  98. package/src/theme/components/index.ts +12 -0
  99. package/src/theme/components/timeline/ArticleCard.tsx +57 -0
  100. package/src/theme/components/timeline/ImageCard.tsx +80 -0
  101. package/src/theme/components/timeline/LinkCard.tsx +66 -0
  102. package/src/theme/components/timeline/NoteCard.tsx +41 -0
  103. package/src/theme/components/timeline/QuoteCard.tsx +55 -0
  104. package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
  105. package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
  106. package/src/theme/components/timeline/TimelineItem.tsx +39 -0
  107. package/src/theme/components/timeline/index.ts +8 -0
  108. package/src/theme/layouts/DashLayout.tsx +10 -0
  109. package/src/theme/layouts/SiteLayout.tsx +184 -0
  110. package/src/theme/layouts/index.ts +1 -0
  111. package/src/types/sortablejs.d.ts +23 -0
  112. package/src/types.ts +102 -1
  113. package/dist/app.d.ts +0 -38
  114. package/dist/app.d.ts.map +0 -1
  115. package/dist/auth.d.ts +0 -25
  116. package/dist/auth.d.ts.map +0 -1
  117. package/dist/db/index.d.ts +0 -10
  118. package/dist/db/index.d.ts.map +0 -1
  119. package/dist/db/schema.d.ts +0 -1543
  120. package/dist/db/schema.d.ts.map +0 -1
  121. package/dist/i18n/Trans.d.ts +0 -25
  122. package/dist/i18n/Trans.d.ts.map +0 -1
  123. package/dist/i18n/context.d.ts +0 -69
  124. package/dist/i18n/context.d.ts.map +0 -1
  125. package/dist/i18n/detect.d.ts +0 -20
  126. package/dist/i18n/detect.d.ts.map +0 -1
  127. package/dist/i18n/i18n.d.ts +0 -32
  128. package/dist/i18n/i18n.d.ts.map +0 -1
  129. package/dist/i18n/index.d.ts +0 -41
  130. package/dist/i18n/index.d.ts.map +0 -1
  131. package/dist/i18n/locales/en.d.ts +0 -3
  132. package/dist/i18n/locales/en.d.ts.map +0 -1
  133. package/dist/i18n/locales/zh-Hans.d.ts +0 -3
  134. package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
  135. package/dist/i18n/locales/zh-Hant.d.ts +0 -3
  136. package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
  137. package/dist/i18n/locales.d.ts +0 -11
  138. package/dist/i18n/locales.d.ts.map +0 -1
  139. package/dist/i18n/middleware.d.ts +0 -21
  140. package/dist/i18n/middleware.d.ts.map +0 -1
  141. package/dist/index.d.ts +0 -16
  142. package/dist/index.d.ts.map +0 -1
  143. package/dist/lib/config.d.ts +0 -83
  144. package/dist/lib/config.d.ts.map +0 -1
  145. package/dist/lib/constants.d.ts +0 -37
  146. package/dist/lib/constants.d.ts.map +0 -1
  147. package/dist/lib/image.d.ts +0 -73
  148. package/dist/lib/image.d.ts.map +0 -1
  149. package/dist/lib/index.d.ts +0 -9
  150. package/dist/lib/index.d.ts.map +0 -1
  151. package/dist/lib/markdown.d.ts +0 -60
  152. package/dist/lib/markdown.d.ts.map +0 -1
  153. package/dist/lib/schemas.d.ts +0 -130
  154. package/dist/lib/schemas.d.ts.map +0 -1
  155. package/dist/lib/sqid.d.ts +0 -60
  156. package/dist/lib/sqid.d.ts.map +0 -1
  157. package/dist/lib/sse.d.ts +0 -192
  158. package/dist/lib/sse.d.ts.map +0 -1
  159. package/dist/lib/theme.d.ts +0 -44
  160. package/dist/lib/theme.d.ts.map +0 -1
  161. package/dist/lib/time.d.ts +0 -90
  162. package/dist/lib/time.d.ts.map +0 -1
  163. package/dist/lib/url.d.ts +0 -82
  164. package/dist/lib/url.d.ts.map +0 -1
  165. package/dist/middleware/auth.d.ts +0 -24
  166. package/dist/middleware/auth.d.ts.map +0 -1
  167. package/dist/middleware/onboarding.d.ts +0 -26
  168. package/dist/middleware/onboarding.d.ts.map +0 -1
  169. package/dist/routes/api/posts.d.ts +0 -13
  170. package/dist/routes/api/posts.d.ts.map +0 -1
  171. package/dist/routes/api/search.d.ts +0 -13
  172. package/dist/routes/api/search.d.ts.map +0 -1
  173. package/dist/routes/api/upload.d.ts +0 -16
  174. package/dist/routes/api/upload.d.ts.map +0 -1
  175. package/dist/routes/dash/collections.d.ts +0 -13
  176. package/dist/routes/dash/collections.d.ts.map +0 -1
  177. package/dist/routes/dash/index.d.ts +0 -15
  178. package/dist/routes/dash/index.d.ts.map +0 -1
  179. package/dist/routes/dash/media.d.ts +0 -16
  180. package/dist/routes/dash/media.d.ts.map +0 -1
  181. package/dist/routes/dash/pages.d.ts +0 -15
  182. package/dist/routes/dash/pages.d.ts.map +0 -1
  183. package/dist/routes/dash/posts.d.ts +0 -13
  184. package/dist/routes/dash/posts.d.ts.map +0 -1
  185. package/dist/routes/dash/redirects.d.ts +0 -13
  186. package/dist/routes/dash/redirects.d.ts.map +0 -1
  187. package/dist/routes/dash/settings.d.ts +0 -15
  188. package/dist/routes/dash/settings.d.ts.map +0 -1
  189. package/dist/routes/feed/rss.d.ts +0 -13
  190. package/dist/routes/feed/rss.d.ts.map +0 -1
  191. package/dist/routes/feed/sitemap.d.ts +0 -13
  192. package/dist/routes/feed/sitemap.d.ts.map +0 -1
  193. package/dist/routes/pages/archive.d.ts +0 -15
  194. package/dist/routes/pages/archive.d.ts.map +0 -1
  195. package/dist/routes/pages/collection.d.ts +0 -13
  196. package/dist/routes/pages/collection.d.ts.map +0 -1
  197. package/dist/routes/pages/home.d.ts +0 -13
  198. package/dist/routes/pages/home.d.ts.map +0 -1
  199. package/dist/routes/pages/page.d.ts +0 -15
  200. package/dist/routes/pages/page.d.ts.map +0 -1
  201. package/dist/routes/pages/post.d.ts +0 -13
  202. package/dist/routes/pages/post.d.ts.map +0 -1
  203. package/dist/routes/pages/search.d.ts +0 -13
  204. package/dist/routes/pages/search.d.ts.map +0 -1
  205. package/dist/services/collection.d.ts +0 -32
  206. package/dist/services/collection.d.ts.map +0 -1
  207. package/dist/services/index.d.ts +0 -28
  208. package/dist/services/index.d.ts.map +0 -1
  209. package/dist/services/media.d.ts +0 -34
  210. package/dist/services/media.d.ts.map +0 -1
  211. package/dist/services/post.d.ts +0 -31
  212. package/dist/services/post.d.ts.map +0 -1
  213. package/dist/services/redirect.d.ts +0 -15
  214. package/dist/services/redirect.d.ts.map +0 -1
  215. package/dist/services/search.d.ts +0 -26
  216. package/dist/services/search.d.ts.map +0 -1
  217. package/dist/services/settings.d.ts +0 -18
  218. package/dist/services/settings.d.ts.map +0 -1
  219. package/dist/theme/color-themes.d.ts +0 -30
  220. package/dist/theme/color-themes.d.ts.map +0 -1
  221. package/dist/theme/components/ActionButtons.d.ts +0 -43
  222. package/dist/theme/components/ActionButtons.d.ts.map +0 -1
  223. package/dist/theme/components/CrudPageHeader.d.ts +0 -23
  224. package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
  225. package/dist/theme/components/DangerZone.d.ts +0 -36
  226. package/dist/theme/components/DangerZone.d.ts.map +0 -1
  227. package/dist/theme/components/EmptyState.d.ts +0 -27
  228. package/dist/theme/components/EmptyState.d.ts.map +0 -1
  229. package/dist/theme/components/ListItemRow.d.ts +0 -15
  230. package/dist/theme/components/ListItemRow.d.ts.map +0 -1
  231. package/dist/theme/components/MediaGallery.d.ts +0 -13
  232. package/dist/theme/components/MediaGallery.d.ts.map +0 -1
  233. package/dist/theme/components/PageForm.d.ts +0 -14
  234. package/dist/theme/components/PageForm.d.ts.map +0 -1
  235. package/dist/theme/components/Pagination.d.ts +0 -46
  236. package/dist/theme/components/Pagination.d.ts.map +0 -1
  237. package/dist/theme/components/PostForm.d.ts +0 -16
  238. package/dist/theme/components/PostForm.d.ts.map +0 -1
  239. package/dist/theme/components/PostList.d.ts +0 -10
  240. package/dist/theme/components/PostList.d.ts.map +0 -1
  241. package/dist/theme/components/ThreadView.d.ts +0 -15
  242. package/dist/theme/components/ThreadView.d.ts.map +0 -1
  243. package/dist/theme/components/TypeBadge.d.ts +0 -12
  244. package/dist/theme/components/TypeBadge.d.ts.map +0 -1
  245. package/dist/theme/components/VisibilityBadge.d.ts +0 -12
  246. package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
  247. package/dist/theme/components/index.d.ts +0 -14
  248. package/dist/theme/components/index.d.ts.map +0 -1
  249. package/dist/theme/index.d.ts +0 -21
  250. package/dist/theme/index.d.ts.map +0 -1
  251. package/dist/theme/layouts/BaseLayout.d.ts +0 -23
  252. package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
  253. package/dist/theme/layouts/DashLayout.d.ts +0 -17
  254. package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
  255. package/dist/theme/layouts/index.d.ts +0 -3
  256. package/dist/theme/layouts/index.d.ts.map +0 -1
  257. package/dist/types.d.ts +0 -237
  258. package/dist/types.d.ts.map +0 -1
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Link Card Component
3
+ *
4
+ * External link emphasis for type="link" posts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineCardProps } from "../../../types.js";
9
+ import * as sqid from "../../../lib/sqid.js";
10
+ import * as time from "../../../lib/time.js";
11
+
12
+ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
13
+ const permalink = `/p/${sqid.encode(post.id)}`;
14
+
15
+ return (
16
+ <article
17
+ class={`h-entry timeline-card timeline-card-link${compact ? " timeline-card-compact" : ""}`}
18
+ >
19
+ {post.sourceDomain && (
20
+ <div class="text-xs text-muted-foreground mb-1 flex items-center gap-1">
21
+ <svg
22
+ class="size-3"
23
+ xmlns="http://www.w3.org/2000/svg"
24
+ fill="none"
25
+ viewBox="0 0 24 24"
26
+ stroke-width="2"
27
+ stroke="currentColor"
28
+ >
29
+ <path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
30
+ </svg>
31
+ <span>{post.sourceDomain}</span>
32
+ </div>
33
+ )}
34
+ {post.title && (
35
+ <h2
36
+ class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
37
+ >
38
+ <a
39
+ href={post.sourceUrl || permalink}
40
+ class="u-url hover:underline"
41
+ target={post.sourceUrl ? "_blank" : undefined}
42
+ rel={post.sourceUrl ? "noopener noreferrer" : undefined}
43
+ >
44
+ {post.title}
45
+ </a>
46
+ </h2>
47
+ )}
48
+ {!compact && post.contentHtml && (
49
+ <div
50
+ class="e-content prose prose-sm text-muted-foreground"
51
+ dangerouslySetInnerHTML={{ __html: post.contentHtml }}
52
+ />
53
+ )}
54
+ <footer class="mt-2 text-xs text-muted-foreground">
55
+ <a href={permalink} class="hover:underline">
56
+ <time
57
+ class="dt-published"
58
+ datetime={time.toISOString(post.publishedAt)}
59
+ >
60
+ {time.formatDate(post.publishedAt)}
61
+ </time>
62
+ </a>
63
+ </footer>
64
+ </article>
65
+ );
66
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Note Card Component
3
+ *
4
+ * Text-first, minimal card for type="note" posts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineCardProps } from "../../../types.js";
9
+ import { MediaGallery } from "../MediaGallery.js";
10
+ import * as sqid from "../../../lib/sqid.js";
11
+ import * as time from "../../../lib/time.js";
12
+
13
+ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
14
+ const permalink = `/p/${sqid.encode(post.id)}`;
15
+
16
+ return (
17
+ <article
18
+ class={`h-entry timeline-card${compact ? " timeline-card-compact" : ""}`}
19
+ >
20
+ {post.contentHtml && (
21
+ <div
22
+ class={`e-content prose ${compact ? "prose-sm" : "prose-sm"}`}
23
+ dangerouslySetInnerHTML={{ __html: post.contentHtml }}
24
+ />
25
+ )}
26
+ {!compact && post.mediaAttachments.length > 0 && (
27
+ <MediaGallery attachments={post.mediaAttachments} />
28
+ )}
29
+ <footer class="mt-2 text-xs text-muted-foreground">
30
+ <a href={permalink} class="u-url hover:underline">
31
+ <time
32
+ class="dt-published"
33
+ datetime={time.toISOString(post.publishedAt)}
34
+ >
35
+ {time.formatDate(post.publishedAt)}
36
+ </time>
37
+ </a>
38
+ </footer>
39
+ </article>
40
+ );
41
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Quote Card Component
3
+ *
4
+ * Blockquote + attribution for type="quote" posts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineCardProps } from "../../../types.js";
9
+ import * as sqid from "../../../lib/sqid.js";
10
+ import * as time from "../../../lib/time.js";
11
+
12
+ export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
13
+ const permalink = `/p/${sqid.encode(post.id)}`;
14
+
15
+ return (
16
+ <article
17
+ class={`h-entry timeline-card timeline-card-quote${compact ? " timeline-card-compact" : ""}`}
18
+ >
19
+ {post.contentHtml && (
20
+ <blockquote
21
+ class={`e-content italic ${compact ? "text-sm" : "text-base"} leading-relaxed`}
22
+ >
23
+ <div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
24
+ </blockquote>
25
+ )}
26
+ {!compact && (post.sourceName || post.sourceUrl) && (
27
+ <div class="mt-2 text-sm text-muted-foreground">
28
+ &mdash;{" "}
29
+ {post.sourceUrl ? (
30
+ <a
31
+ href={post.sourceUrl}
32
+ class="hover:underline"
33
+ target="_blank"
34
+ rel="noopener noreferrer"
35
+ >
36
+ {post.sourceName || post.sourceDomain || "Source"}
37
+ </a>
38
+ ) : (
39
+ <span>{post.sourceName}</span>
40
+ )}
41
+ </div>
42
+ )}
43
+ <footer class="mt-2 text-xs text-muted-foreground">
44
+ <a href={permalink} class="u-url hover:underline">
45
+ <time
46
+ class="dt-published"
47
+ datetime={time.toISOString(post.publishedAt)}
48
+ >
49
+ {time.formatDate(post.publishedAt)}
50
+ </time>
51
+ </a>
52
+ </footer>
53
+ </article>
54
+ );
55
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Thread Preview Component
3
+ *
4
+ * Inline thread preview: root card + compact replies + "show more" link.
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 * as sqid from "../../../lib/sqid.js";
12
+
13
+ export const ThreadPreview: FC<ThreadPreviewProps> = ({
14
+ rootPost,
15
+ previewReplies,
16
+ totalReplyCount,
17
+ }) => {
18
+ const { t } = useLingui();
19
+ const permalink = `/p/${sqid.encode(rootPost.id)}`;
20
+ const remainingCount = totalReplyCount - previewReplies.length;
21
+
22
+ return (
23
+ <div class="timeline-thread">
24
+ <TimelineItem item={{ post: rootPost }} />
25
+ {previewReplies.length > 0 && (
26
+ <div class="timeline-thread-replies">
27
+ {previewReplies.map((reply) => (
28
+ <div key={reply.id} class="timeline-thread-reply">
29
+ <TimelineItem item={{ post: reply }} compact />
30
+ </div>
31
+ ))}
32
+ {remainingCount > 0 && (
33
+ <div class="timeline-thread-reply">
34
+ <a
35
+ href={permalink}
36
+ class="text-sm text-muted-foreground hover:text-foreground hover:underline"
37
+ >
38
+ {t({
39
+ message: `Show ${remainingCount} more ${remainingCount === 1 ? "reply" : "replies"}`,
40
+ comment: "@context: Link to show remaining thread replies",
41
+ })}
42
+ </a>
43
+ </div>
44
+ )}
45
+ </div>
46
+ )}
47
+ </div>
48
+ );
49
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Timeline Feed Component
3
+ *
4
+ * Main feed wrapper with load-more button.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "@lingui/react/macro";
9
+ import type { TimelineFeedProps } from "../../../types.js";
10
+ import { TimelineItem } from "./TimelineItem.js";
11
+ import { ThreadPreview } from "./ThreadPreview.js";
12
+
13
+ export const TimelineFeed: FC<TimelineFeedProps> = ({
14
+ items,
15
+ hasMore,
16
+ nextCursor,
17
+ }) => {
18
+ const { t } = useLingui();
19
+
20
+ return (
21
+ <div>
22
+ <div id="timeline-feed" class="flex flex-col gap-4">
23
+ {items.map((item) => {
24
+ if (item.threadPreview) {
25
+ return (
26
+ <ThreadPreview
27
+ key={item.post.id}
28
+ rootPost={item.post}
29
+ previewReplies={item.threadPreview.replies}
30
+ totalReplyCount={item.threadPreview.totalReplyCount}
31
+ />
32
+ );
33
+ }
34
+ return <TimelineItem key={item.post.id} item={item} />;
35
+ })}
36
+ </div>
37
+ {hasMore && nextCursor && (
38
+ <div id="load-more-container" class="mt-6 text-center">
39
+ <button
40
+ class="btn btn-outline"
41
+ data-on:click={`@get('/api/timeline?cursor=${nextCursor}')`}
42
+ >
43
+ {t({
44
+ message: "Load more",
45
+ comment: "@context: Button to load more posts in timeline",
46
+ })}
47
+ </button>
48
+ </div>
49
+ )}
50
+ </div>
51
+ );
52
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Timeline Item Component
3
+ *
4
+ * Dispatches to the correct card component based on post type.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineItemData, TimelineCardProps } from "../../../types.js";
9
+ import { NoteCard } from "./NoteCard.js";
10
+ import { ArticleCard } from "./ArticleCard.js";
11
+ import { LinkCard } from "./LinkCard.js";
12
+ import { QuoteCard } from "./QuoteCard.js";
13
+ import { ImageCard } from "./ImageCard.js";
14
+ import type { PostType } from "../../../types.js";
15
+
16
+ const CARD_MAP: Record<PostType, FC<TimelineCardProps>> = {
17
+ note: NoteCard,
18
+ article: ArticleCard,
19
+ link: LinkCard,
20
+ quote: QuoteCard,
21
+ image: ImageCard,
22
+ page: NoteCard,
23
+ };
24
+
25
+ interface TimelineItemProps {
26
+ item: TimelineItemData;
27
+ compact?: boolean;
28
+ /** Override card component (for theme overrides) */
29
+ cardOverride?: FC<TimelineCardProps>;
30
+ }
31
+
32
+ export const TimelineItem: FC<TimelineItemProps> = ({
33
+ item,
34
+ compact,
35
+ cardOverride,
36
+ }) => {
37
+ const Card = cardOverride ?? CARD_MAP[item.post.type];
38
+ return <Card post={item.post} compact={compact} />;
39
+ };
@@ -0,0 +1,8 @@
1
+ export { NoteCard } from "./NoteCard.js";
2
+ export { ArticleCard } from "./ArticleCard.js";
3
+ export { LinkCard } from "./LinkCard.js";
4
+ export { QuoteCard } from "./QuoteCard.js";
5
+ export { ImageCard } from "./ImageCard.js";
6
+ export { ThreadPreview } from "./ThreadPreview.js";
7
+ export { TimelineItem } from "./TimelineItem.js";
8
+ export { TimelineFeed } from "./TimelineFeed.js";
@@ -126,6 +126,16 @@ 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>
129
139
  <a
130
140
  href="/dash/settings"
131
141
  class={navClass("/dash/settings", /^\/dash\/settings/)}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Site Layout
3
+ *
4
+ * Two-column layout for public pages with sidebar navigation.
5
+ * On mobile, uses a slide-out drawer menu.
6
+ */
7
+
8
+ import type { FC, PropsWithChildren } from "hono/jsx";
9
+ import type { NavigationLink } from "../../types.js";
10
+
11
+ export interface SiteLayoutProps {
12
+ siteName: string;
13
+ navigationLinks: NavigationLink[];
14
+ currentPath: string;
15
+ }
16
+
17
+ /**
18
+ * Determine if a navigation link is active based on the current path.
19
+ *
20
+ * @param linkUrl - The link's URL
21
+ * @param currentPath - The current page path
22
+ * @returns Whether the link should be shown as active
23
+ */
24
+ function isLinkActive(linkUrl: string, currentPath: string): boolean {
25
+ // External links are never active
26
+ if (linkUrl.startsWith("http://") || linkUrl.startsWith("https://")) {
27
+ return false;
28
+ }
29
+
30
+ // Exact match for home
31
+ if (linkUrl === "/") {
32
+ return currentPath === "/";
33
+ }
34
+
35
+ // Prefix match for other internal links
36
+ return currentPath === linkUrl || currentPath.startsWith(linkUrl + "/");
37
+ }
38
+
39
+ /**
40
+ * Check if a URL is external
41
+ */
42
+ function isExternalUrl(url: string): boolean {
43
+ return url.startsWith("http://") || url.startsWith("https://");
44
+ }
45
+
46
+ /**
47
+ * Render navigation links with dot indicator for active state.
48
+ */
49
+ function NavLinks({
50
+ navigationLinks,
51
+ currentPath,
52
+ }: {
53
+ navigationLinks: NavigationLink[];
54
+ currentPath: string;
55
+ }) {
56
+ return (
57
+ <>
58
+ {navigationLinks.map((link) => {
59
+ const active = isLinkActive(link.url, currentPath);
60
+ const external = isExternalUrl(link.url);
61
+ return (
62
+ <a
63
+ key={link.id}
64
+ href={link.url}
65
+ class={`text-sm flex items-center gap-2 py-0.5 ${
66
+ active
67
+ ? "text-primary font-medium"
68
+ : "text-muted-foreground hover:text-foreground"
69
+ }`}
70
+ {...(external
71
+ ? { target: "_blank", rel: "noopener noreferrer" }
72
+ : {})}
73
+ >
74
+ <span
75
+ class={`size-1.5 rounded-full shrink-0 ${active ? "bg-primary" : "bg-transparent"}`}
76
+ />
77
+ {link.label}
78
+ {external && <span class="ml-1 text-xs opacity-50">↗</span>}
79
+ </a>
80
+ );
81
+ })}
82
+ </>
83
+ );
84
+ }
85
+
86
+ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
87
+ siteName,
88
+ navigationLinks,
89
+ currentPath,
90
+ children,
91
+ }) => {
92
+ return (
93
+ <div
94
+ class="container py-8 md:flex md:gap-12"
95
+ data-signals={JSON.stringify({ _drawerOpen: false })}
96
+ >
97
+ {/* Mobile header with hamburger */}
98
+ <div class="flex items-center justify-between mb-6 md:hidden">
99
+ <a href="/" class="text-xl font-semibold">
100
+ {siteName}
101
+ </a>
102
+ <button
103
+ data-on:click="$_drawerOpen = true"
104
+ class="p-2 -mr-2 text-muted-foreground hover:text-foreground"
105
+ aria-label="Open menu"
106
+ >
107
+ <svg
108
+ class="size-5"
109
+ fill="none"
110
+ viewBox="0 0 24 24"
111
+ stroke-width="1.5"
112
+ stroke="currentColor"
113
+ >
114
+ <path
115
+ stroke-linecap="round"
116
+ stroke-linejoin="round"
117
+ d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
118
+ />
119
+ </svg>
120
+ </button>
121
+ </div>
122
+
123
+ {/* Mobile drawer backdrop */}
124
+ <div
125
+ class="fixed inset-0 bg-black/50 z-40 opacity-0 pointer-events-none transition-opacity duration-300 ease-in-out md:hidden"
126
+ data-class="{'opacity-100 pointer-events-auto': $_drawerOpen, 'opacity-0 pointer-events-none': !$_drawerOpen}"
127
+ data-on:click="$_drawerOpen = false"
128
+ />
129
+
130
+ {/* Mobile drawer panel */}
131
+ <aside
132
+ class="fixed inset-y-0 left-0 w-64 bg-background z-50 p-6 overflow-y-auto shadow-lg -translate-x-full transition-transform duration-300 ease-in-out md:hidden"
133
+ data-class="{'translate-x-0': $_drawerOpen, '-translate-x-full': !$_drawerOpen}"
134
+ >
135
+ <div class="flex items-center justify-between mb-8">
136
+ <a href="/" class="text-xl font-semibold">
137
+ {siteName}
138
+ </a>
139
+ <button
140
+ data-on:click="$_drawerOpen = false"
141
+ class="p-2 -mr-2 text-muted-foreground hover:text-foreground"
142
+ aria-label="Close menu"
143
+ >
144
+ <svg
145
+ class="size-5"
146
+ fill="none"
147
+ viewBox="0 0 24 24"
148
+ stroke-width="1.5"
149
+ stroke="currentColor"
150
+ >
151
+ <path
152
+ stroke-linecap="round"
153
+ stroke-linejoin="round"
154
+ d="M6 18L18 6M6 6l12 12"
155
+ />
156
+ </svg>
157
+ </button>
158
+ </div>
159
+ <nav class="flex flex-col gap-0.5">
160
+ <NavLinks
161
+ navigationLinks={navigationLinks}
162
+ currentPath={currentPath}
163
+ />
164
+ </nav>
165
+ </aside>
166
+
167
+ {/* Desktop sidebar */}
168
+ <aside class="hidden md:block md:w-48 md:shrink-0 md:sticky md:top-8 md:self-start">
169
+ <a href="/" class="text-xl font-semibold block mb-20">
170
+ {siteName}
171
+ </a>
172
+ <nav class="flex flex-col gap-0.5">
173
+ <NavLinks
174
+ navigationLinks={navigationLinks}
175
+ currentPath={currentPath}
176
+ />
177
+ </nav>
178
+ </aside>
179
+
180
+ {/* Main content */}
181
+ <main class="flex-1 min-w-0">{children}</main>
182
+ </div>
183
+ );
184
+ };
@@ -4,3 +4,4 @@ export {
4
4
  type ToastProps,
5
5
  } from "./BaseLayout.js";
6
6
  export { DashLayout, type DashLayoutProps } from "./DashLayout.js";
7
+ export { SiteLayout, type SiteLayoutProps } from "./SiteLayout.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Minimal type declarations for sortablejs
3
+ *
4
+ * Only covers the API surface used by nav-reorder.ts.
5
+ */
6
+
7
+ declare module "sortablejs" {
8
+ interface SortableOptions {
9
+ animation?: number;
10
+ handle?: string;
11
+ onEnd?: (event: { oldIndex?: number; newIndex?: number }) => void;
12
+ }
13
+
14
+ interface SortableInstance {
15
+ destroy(): void;
16
+ }
17
+
18
+ const Sortable: {
19
+ create(el: HTMLElement, options?: SortableOptions): SortableInstance;
20
+ };
21
+
22
+ export default Sortable;
23
+ }