@jant/core 0.3.7 → 0.3.8

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 (241) hide show
  1. package/dist/app.js +4 -0
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +13 -0
  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 +3 -3
  8. package/dist/lib/media-helpers.js +43 -0
  9. package/dist/lib/nav-reorder.js +27 -0
  10. package/dist/lib/navigation.js +35 -0
  11. package/dist/lib/theme-components.js +49 -0
  12. package/dist/routes/api/timeline.js +115 -0
  13. package/dist/routes/api/upload.js +9 -5
  14. package/dist/routes/dash/navigation.js +274 -0
  15. package/dist/routes/pages/archive.js +14 -27
  16. package/dist/routes/pages/collection.js +10 -19
  17. package/dist/routes/pages/home.js +83 -126
  18. package/dist/routes/pages/page.js +19 -38
  19. package/dist/routes/pages/post.js +38 -51
  20. package/dist/routes/pages/search.js +13 -26
  21. package/dist/services/index.js +3 -1
  22. package/dist/services/media.js +1 -1
  23. package/dist/services/navigation.js +115 -0
  24. package/dist/services/post.js +26 -1
  25. package/dist/theme/components/PostList.js +5 -0
  26. package/dist/theme/components/index.js +2 -0
  27. package/dist/theme/components/timeline/ArticleCard.js +50 -0
  28. package/dist/theme/components/timeline/ImageCard.js +86 -0
  29. package/dist/theme/components/timeline/LinkCard.js +62 -0
  30. package/dist/theme/components/timeline/NoteCard.js +37 -0
  31. package/dist/theme/components/timeline/QuoteCard.js +51 -0
  32. package/dist/theme/components/timeline/ThreadPreview.js +52 -0
  33. package/dist/theme/components/timeline/TimelineFeed.js +43 -0
  34. package/dist/theme/components/timeline/TimelineItem.js +25 -0
  35. package/dist/theme/components/timeline/index.js +8 -0
  36. package/dist/theme/layouts/DashLayout.js +8 -0
  37. package/dist/theme/layouts/SiteLayout.js +160 -0
  38. package/dist/theme/layouts/index.js +1 -0
  39. package/dist/types/sortablejs.d.js +5 -0
  40. package/package.json +3 -2
  41. package/src/__tests__/helpers/db.ts +10 -0
  42. package/src/app.tsx +4 -0
  43. package/src/client.ts +1 -0
  44. package/src/db/migrations/0003_add_navigation_links.sql +8 -0
  45. package/src/db/migrations/meta/0003_snapshot.json +821 -0
  46. package/src/db/migrations/meta/_journal.json +14 -0
  47. package/src/db/schema.ts +13 -0
  48. package/src/i18n/locales/en.po +100 -32
  49. package/src/i18n/locales/en.ts +1 -1
  50. package/src/i18n/locales/zh-Hans.po +102 -55
  51. package/src/i18n/locales/zh-Hans.ts +1 -1
  52. package/src/i18n/locales/zh-Hant.po +102 -55
  53. package/src/i18n/locales/zh-Hant.ts +1 -1
  54. package/src/index.ts +5 -0
  55. package/src/lib/__tests__/theme-components.test.ts +107 -0
  56. package/src/lib/image.ts +3 -3
  57. package/src/lib/media-helpers.ts +54 -0
  58. package/src/lib/nav-reorder.ts +26 -0
  59. package/src/lib/navigation.ts +46 -0
  60. package/src/lib/theme-components.ts +76 -0
  61. package/src/routes/api/__tests__/posts.test.ts +8 -8
  62. package/src/routes/api/__tests__/timeline.test.ts +242 -0
  63. package/src/routes/api/timeline.tsx +145 -0
  64. package/src/routes/api/upload.ts +9 -5
  65. package/src/routes/dash/navigation.tsx +306 -0
  66. package/src/routes/pages/archive.tsx +15 -23
  67. package/src/routes/pages/collection.tsx +8 -15
  68. package/src/routes/pages/home.tsx +111 -122
  69. package/src/routes/pages/page.tsx +17 -30
  70. package/src/routes/pages/post.tsx +33 -42
  71. package/src/routes/pages/search.tsx +18 -22
  72. package/src/services/__tests__/media.test.ts +34 -7
  73. package/src/services/__tests__/navigation.test.ts +213 -0
  74. package/src/services/__tests__/post-timeline.test.ts +220 -0
  75. package/src/services/index.ts +7 -0
  76. package/src/services/media.ts +2 -1
  77. package/src/services/navigation.ts +165 -0
  78. package/src/services/post.ts +48 -1
  79. package/src/styles/components.css +59 -0
  80. package/src/theme/components/PostList.tsx +7 -0
  81. package/src/theme/components/index.ts +12 -0
  82. package/src/theme/components/timeline/ArticleCard.tsx +57 -0
  83. package/src/theme/components/timeline/ImageCard.tsx +80 -0
  84. package/src/theme/components/timeline/LinkCard.tsx +66 -0
  85. package/src/theme/components/timeline/NoteCard.tsx +41 -0
  86. package/src/theme/components/timeline/QuoteCard.tsx +55 -0
  87. package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
  88. package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
  89. package/src/theme/components/timeline/TimelineItem.tsx +39 -0
  90. package/src/theme/components/timeline/index.ts +8 -0
  91. package/src/theme/layouts/DashLayout.tsx +10 -0
  92. package/src/theme/layouts/SiteLayout.tsx +184 -0
  93. package/src/theme/layouts/index.ts +1 -0
  94. package/src/types/sortablejs.d.ts +23 -0
  95. package/src/types.ts +61 -0
  96. package/dist/app.d.ts +0 -38
  97. package/dist/app.d.ts.map +0 -1
  98. package/dist/auth.d.ts +0 -25
  99. package/dist/auth.d.ts.map +0 -1
  100. package/dist/db/index.d.ts +0 -10
  101. package/dist/db/index.d.ts.map +0 -1
  102. package/dist/db/schema.d.ts +0 -1543
  103. package/dist/db/schema.d.ts.map +0 -1
  104. package/dist/i18n/Trans.d.ts +0 -25
  105. package/dist/i18n/Trans.d.ts.map +0 -1
  106. package/dist/i18n/context.d.ts +0 -69
  107. package/dist/i18n/context.d.ts.map +0 -1
  108. package/dist/i18n/detect.d.ts +0 -20
  109. package/dist/i18n/detect.d.ts.map +0 -1
  110. package/dist/i18n/i18n.d.ts +0 -32
  111. package/dist/i18n/i18n.d.ts.map +0 -1
  112. package/dist/i18n/index.d.ts +0 -41
  113. package/dist/i18n/index.d.ts.map +0 -1
  114. package/dist/i18n/locales/en.d.ts +0 -3
  115. package/dist/i18n/locales/en.d.ts.map +0 -1
  116. package/dist/i18n/locales/zh-Hans.d.ts +0 -3
  117. package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
  118. package/dist/i18n/locales/zh-Hant.d.ts +0 -3
  119. package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
  120. package/dist/i18n/locales.d.ts +0 -11
  121. package/dist/i18n/locales.d.ts.map +0 -1
  122. package/dist/i18n/middleware.d.ts +0 -21
  123. package/dist/i18n/middleware.d.ts.map +0 -1
  124. package/dist/index.d.ts +0 -16
  125. package/dist/index.d.ts.map +0 -1
  126. package/dist/lib/config.d.ts +0 -83
  127. package/dist/lib/config.d.ts.map +0 -1
  128. package/dist/lib/constants.d.ts +0 -37
  129. package/dist/lib/constants.d.ts.map +0 -1
  130. package/dist/lib/image.d.ts +0 -73
  131. package/dist/lib/image.d.ts.map +0 -1
  132. package/dist/lib/index.d.ts +0 -9
  133. package/dist/lib/index.d.ts.map +0 -1
  134. package/dist/lib/markdown.d.ts +0 -60
  135. package/dist/lib/markdown.d.ts.map +0 -1
  136. package/dist/lib/schemas.d.ts +0 -130
  137. package/dist/lib/schemas.d.ts.map +0 -1
  138. package/dist/lib/sqid.d.ts +0 -60
  139. package/dist/lib/sqid.d.ts.map +0 -1
  140. package/dist/lib/sse.d.ts +0 -192
  141. package/dist/lib/sse.d.ts.map +0 -1
  142. package/dist/lib/theme.d.ts +0 -44
  143. package/dist/lib/theme.d.ts.map +0 -1
  144. package/dist/lib/time.d.ts +0 -90
  145. package/dist/lib/time.d.ts.map +0 -1
  146. package/dist/lib/url.d.ts +0 -82
  147. package/dist/lib/url.d.ts.map +0 -1
  148. package/dist/middleware/auth.d.ts +0 -24
  149. package/dist/middleware/auth.d.ts.map +0 -1
  150. package/dist/middleware/onboarding.d.ts +0 -26
  151. package/dist/middleware/onboarding.d.ts.map +0 -1
  152. package/dist/routes/api/posts.d.ts +0 -13
  153. package/dist/routes/api/posts.d.ts.map +0 -1
  154. package/dist/routes/api/search.d.ts +0 -13
  155. package/dist/routes/api/search.d.ts.map +0 -1
  156. package/dist/routes/api/upload.d.ts +0 -16
  157. package/dist/routes/api/upload.d.ts.map +0 -1
  158. package/dist/routes/dash/collections.d.ts +0 -13
  159. package/dist/routes/dash/collections.d.ts.map +0 -1
  160. package/dist/routes/dash/index.d.ts +0 -15
  161. package/dist/routes/dash/index.d.ts.map +0 -1
  162. package/dist/routes/dash/media.d.ts +0 -16
  163. package/dist/routes/dash/media.d.ts.map +0 -1
  164. package/dist/routes/dash/pages.d.ts +0 -15
  165. package/dist/routes/dash/pages.d.ts.map +0 -1
  166. package/dist/routes/dash/posts.d.ts +0 -13
  167. package/dist/routes/dash/posts.d.ts.map +0 -1
  168. package/dist/routes/dash/redirects.d.ts +0 -13
  169. package/dist/routes/dash/redirects.d.ts.map +0 -1
  170. package/dist/routes/dash/settings.d.ts +0 -15
  171. package/dist/routes/dash/settings.d.ts.map +0 -1
  172. package/dist/routes/feed/rss.d.ts +0 -13
  173. package/dist/routes/feed/rss.d.ts.map +0 -1
  174. package/dist/routes/feed/sitemap.d.ts +0 -13
  175. package/dist/routes/feed/sitemap.d.ts.map +0 -1
  176. package/dist/routes/pages/archive.d.ts +0 -15
  177. package/dist/routes/pages/archive.d.ts.map +0 -1
  178. package/dist/routes/pages/collection.d.ts +0 -13
  179. package/dist/routes/pages/collection.d.ts.map +0 -1
  180. package/dist/routes/pages/home.d.ts +0 -13
  181. package/dist/routes/pages/home.d.ts.map +0 -1
  182. package/dist/routes/pages/page.d.ts +0 -15
  183. package/dist/routes/pages/page.d.ts.map +0 -1
  184. package/dist/routes/pages/post.d.ts +0 -13
  185. package/dist/routes/pages/post.d.ts.map +0 -1
  186. package/dist/routes/pages/search.d.ts +0 -13
  187. package/dist/routes/pages/search.d.ts.map +0 -1
  188. package/dist/services/collection.d.ts +0 -32
  189. package/dist/services/collection.d.ts.map +0 -1
  190. package/dist/services/index.d.ts +0 -28
  191. package/dist/services/index.d.ts.map +0 -1
  192. package/dist/services/media.d.ts +0 -34
  193. package/dist/services/media.d.ts.map +0 -1
  194. package/dist/services/post.d.ts +0 -31
  195. package/dist/services/post.d.ts.map +0 -1
  196. package/dist/services/redirect.d.ts +0 -15
  197. package/dist/services/redirect.d.ts.map +0 -1
  198. package/dist/services/search.d.ts +0 -26
  199. package/dist/services/search.d.ts.map +0 -1
  200. package/dist/services/settings.d.ts +0 -18
  201. package/dist/services/settings.d.ts.map +0 -1
  202. package/dist/theme/color-themes.d.ts +0 -30
  203. package/dist/theme/color-themes.d.ts.map +0 -1
  204. package/dist/theme/components/ActionButtons.d.ts +0 -43
  205. package/dist/theme/components/ActionButtons.d.ts.map +0 -1
  206. package/dist/theme/components/CrudPageHeader.d.ts +0 -23
  207. package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
  208. package/dist/theme/components/DangerZone.d.ts +0 -36
  209. package/dist/theme/components/DangerZone.d.ts.map +0 -1
  210. package/dist/theme/components/EmptyState.d.ts +0 -27
  211. package/dist/theme/components/EmptyState.d.ts.map +0 -1
  212. package/dist/theme/components/ListItemRow.d.ts +0 -15
  213. package/dist/theme/components/ListItemRow.d.ts.map +0 -1
  214. package/dist/theme/components/MediaGallery.d.ts +0 -13
  215. package/dist/theme/components/MediaGallery.d.ts.map +0 -1
  216. package/dist/theme/components/PageForm.d.ts +0 -14
  217. package/dist/theme/components/PageForm.d.ts.map +0 -1
  218. package/dist/theme/components/Pagination.d.ts +0 -46
  219. package/dist/theme/components/Pagination.d.ts.map +0 -1
  220. package/dist/theme/components/PostForm.d.ts +0 -16
  221. package/dist/theme/components/PostForm.d.ts.map +0 -1
  222. package/dist/theme/components/PostList.d.ts +0 -10
  223. package/dist/theme/components/PostList.d.ts.map +0 -1
  224. package/dist/theme/components/ThreadView.d.ts +0 -15
  225. package/dist/theme/components/ThreadView.d.ts.map +0 -1
  226. package/dist/theme/components/TypeBadge.d.ts +0 -12
  227. package/dist/theme/components/TypeBadge.d.ts.map +0 -1
  228. package/dist/theme/components/VisibilityBadge.d.ts +0 -12
  229. package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
  230. package/dist/theme/components/index.d.ts +0 -14
  231. package/dist/theme/components/index.d.ts.map +0 -1
  232. package/dist/theme/index.d.ts +0 -21
  233. package/dist/theme/index.d.ts.map +0 -1
  234. package/dist/theme/layouts/BaseLayout.d.ts +0 -23
  235. package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
  236. package/dist/theme/layouts/DashLayout.d.ts +0 -17
  237. package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
  238. package/dist/theme/layouts/index.d.ts +0 -3
  239. package/dist/theme/layouts/index.d.ts.map +0 -1
  240. package/dist/types.d.ts +0 -237
  241. package/dist/types.d.ts.map +0 -1
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Link Card Component
3
+ *
4
+ * External link emphasis for type="link" posts.
5
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
+ import * as sqid from "../../../lib/sqid.js";
7
+ import * as time from "../../../lib/time.js";
8
+ export const LinkCard = ({ post, compact })=>{
9
+ const permalink = `/p/${sqid.encode(post.id)}`;
10
+ return /*#__PURE__*/ _jsxs("article", {
11
+ class: `h-entry timeline-card timeline-card-link${compact ? " timeline-card-compact" : ""}`,
12
+ children: [
13
+ post.sourceDomain && /*#__PURE__*/ _jsxs("div", {
14
+ class: "text-xs text-muted-foreground mb-1 flex items-center gap-1",
15
+ children: [
16
+ /*#__PURE__*/ _jsx("svg", {
17
+ class: "size-3",
18
+ xmlns: "http://www.w3.org/2000/svg",
19
+ fill: "none",
20
+ viewBox: "0 0 24 24",
21
+ "stroke-width": "2",
22
+ stroke: "currentColor",
23
+ children: /*#__PURE__*/ _jsx("path", {
24
+ 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"
25
+ })
26
+ }),
27
+ /*#__PURE__*/ _jsx("span", {
28
+ children: post.sourceDomain
29
+ })
30
+ ]
31
+ }),
32
+ post.title && /*#__PURE__*/ _jsx("h2", {
33
+ class: `p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`,
34
+ children: /*#__PURE__*/ _jsx("a", {
35
+ href: post.sourceUrl || permalink,
36
+ class: "u-url hover:underline",
37
+ target: post.sourceUrl ? "_blank" : undefined,
38
+ rel: post.sourceUrl ? "noopener noreferrer" : undefined,
39
+ children: post.title
40
+ })
41
+ }),
42
+ !compact && post.contentHtml && /*#__PURE__*/ _jsx("div", {
43
+ class: "e-content prose prose-sm text-muted-foreground",
44
+ dangerouslySetInnerHTML: {
45
+ __html: post.contentHtml
46
+ }
47
+ }),
48
+ /*#__PURE__*/ _jsx("footer", {
49
+ class: "mt-2 text-xs text-muted-foreground",
50
+ children: /*#__PURE__*/ _jsx("a", {
51
+ href: permalink,
52
+ class: "hover:underline",
53
+ children: /*#__PURE__*/ _jsx("time", {
54
+ class: "dt-published",
55
+ datetime: time.toISOString(post.publishedAt),
56
+ children: time.formatDate(post.publishedAt)
57
+ })
58
+ })
59
+ })
60
+ ]
61
+ });
62
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Note Card Component
3
+ *
4
+ * Text-first, minimal card for type="note" posts.
5
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
+ import { MediaGallery } from "../MediaGallery.js";
7
+ import * as sqid from "../../../lib/sqid.js";
8
+ import * as time from "../../../lib/time.js";
9
+ export const NoteCard = ({ post, compact })=>{
10
+ const permalink = `/p/${sqid.encode(post.id)}`;
11
+ return /*#__PURE__*/ _jsxs("article", {
12
+ class: `h-entry timeline-card${compact ? " timeline-card-compact" : ""}`,
13
+ children: [
14
+ post.contentHtml && /*#__PURE__*/ _jsx("div", {
15
+ class: `e-content prose ${compact ? "prose-sm" : "prose-sm"}`,
16
+ dangerouslySetInnerHTML: {
17
+ __html: post.contentHtml
18
+ }
19
+ }),
20
+ !compact && post.mediaAttachments.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
21
+ attachments: post.mediaAttachments
22
+ }),
23
+ /*#__PURE__*/ _jsx("footer", {
24
+ class: "mt-2 text-xs text-muted-foreground",
25
+ children: /*#__PURE__*/ _jsx("a", {
26
+ href: permalink,
27
+ class: "u-url hover:underline",
28
+ children: /*#__PURE__*/ _jsx("time", {
29
+ class: "dt-published",
30
+ datetime: time.toISOString(post.publishedAt),
31
+ children: time.formatDate(post.publishedAt)
32
+ })
33
+ })
34
+ })
35
+ ]
36
+ });
37
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Quote Card Component
3
+ *
4
+ * Blockquote + attribution for type="quote" posts.
5
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
+ import * as sqid from "../../../lib/sqid.js";
7
+ import * as time from "../../../lib/time.js";
8
+ export const QuoteCard = ({ post, compact })=>{
9
+ const permalink = `/p/${sqid.encode(post.id)}`;
10
+ return /*#__PURE__*/ _jsxs("article", {
11
+ class: `h-entry timeline-card timeline-card-quote${compact ? " timeline-card-compact" : ""}`,
12
+ children: [
13
+ post.contentHtml && /*#__PURE__*/ _jsx("blockquote", {
14
+ class: `e-content italic ${compact ? "text-sm" : "text-base"} leading-relaxed`,
15
+ children: /*#__PURE__*/ _jsx("div", {
16
+ dangerouslySetInnerHTML: {
17
+ __html: post.contentHtml
18
+ }
19
+ })
20
+ }),
21
+ !compact && (post.sourceName || post.sourceUrl) && /*#__PURE__*/ _jsxs("div", {
22
+ class: "mt-2 text-sm text-muted-foreground",
23
+ children: [
24
+ "—",
25
+ " ",
26
+ post.sourceUrl ? /*#__PURE__*/ _jsx("a", {
27
+ href: post.sourceUrl,
28
+ class: "hover:underline",
29
+ target: "_blank",
30
+ rel: "noopener noreferrer",
31
+ children: post.sourceName || post.sourceDomain || "Source"
32
+ }) : /*#__PURE__*/ _jsx("span", {
33
+ children: post.sourceName
34
+ })
35
+ ]
36
+ }),
37
+ /*#__PURE__*/ _jsx("footer", {
38
+ class: "mt-2 text-xs text-muted-foreground",
39
+ children: /*#__PURE__*/ _jsx("a", {
40
+ href: permalink,
41
+ class: "u-url hover:underline",
42
+ children: /*#__PURE__*/ _jsx("time", {
43
+ class: "dt-published",
44
+ datetime: time.toISOString(post.publishedAt),
45
+ children: time.formatDate(post.publishedAt)
46
+ })
47
+ })
48
+ })
49
+ ]
50
+ });
51
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Thread Preview Component
3
+ *
4
+ * Inline thread preview: root card + compact replies + "show more" link.
5
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
+ import { useLingui as $_useLingui } from "@jant/core/i18n";
7
+ import { TimelineItem } from "./TimelineItem.js";
8
+ import * as sqid from "../../../lib/sqid.js";
9
+ export const ThreadPreview = ({ rootPost, previewReplies, totalReplyCount })=>{
10
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
11
+ const permalink = `/p/${sqid.encode(rootPost.id)}`;
12
+ const remainingCount = totalReplyCount - previewReplies.length;
13
+ return /*#__PURE__*/ _jsxs("div", {
14
+ class: "timeline-thread",
15
+ children: [
16
+ /*#__PURE__*/ _jsx(TimelineItem, {
17
+ item: {
18
+ post: rootPost
19
+ }
20
+ }),
21
+ previewReplies.length > 0 && /*#__PURE__*/ _jsxs("div", {
22
+ class: "timeline-thread-replies",
23
+ children: [
24
+ previewReplies.map((reply)=>/*#__PURE__*/ _jsx("div", {
25
+ class: "timeline-thread-reply",
26
+ children: /*#__PURE__*/ _jsx(TimelineItem, {
27
+ item: {
28
+ post: reply
29
+ },
30
+ compact: true
31
+ })
32
+ }, reply.id)),
33
+ remainingCount > 0 && /*#__PURE__*/ _jsx("div", {
34
+ class: "timeline-thread-reply",
35
+ children: /*#__PURE__*/ _jsx("a", {
36
+ href: permalink,
37
+ class: "text-sm text-muted-foreground hover:text-foreground hover:underline",
38
+ children: $__i18n._({
39
+ id: "smzF8S",
40
+ message: "Show {remainingCount} more {0}",
41
+ values: {
42
+ remainingCount: remainingCount,
43
+ 0: remainingCount === 1 ? "reply" : "replies"
44
+ }
45
+ })
46
+ })
47
+ })
48
+ ]
49
+ })
50
+ ]
51
+ });
52
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Timeline Feed Component
3
+ *
4
+ * Main feed wrapper with load-more button.
5
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
+ import { useLingui as $_useLingui } from "@jant/core/i18n";
7
+ import { TimelineItem } from "./TimelineItem.js";
8
+ import { ThreadPreview } from "./ThreadPreview.js";
9
+ export const TimelineFeed = ({ items, hasMore, nextCursor })=>{
10
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
11
+ return /*#__PURE__*/ _jsxs("div", {
12
+ children: [
13
+ /*#__PURE__*/ _jsx("div", {
14
+ id: "timeline-feed",
15
+ class: "flex flex-col gap-4",
16
+ children: items.map((item)=>{
17
+ if (item.threadPreview) {
18
+ return /*#__PURE__*/ _jsx(ThreadPreview, {
19
+ rootPost: item.post,
20
+ previewReplies: item.threadPreview.replies,
21
+ totalReplyCount: item.threadPreview.totalReplyCount
22
+ }, item.post.id);
23
+ }
24
+ return /*#__PURE__*/ _jsx(TimelineItem, {
25
+ item: item
26
+ }, item.post.id);
27
+ })
28
+ }),
29
+ hasMore && nextCursor && /*#__PURE__*/ _jsx("div", {
30
+ id: "load-more-container",
31
+ class: "mt-6 text-center",
32
+ children: /*#__PURE__*/ _jsx("button", {
33
+ class: "btn btn-outline",
34
+ "data-on:click": `@get('/api/timeline?cursor=${nextCursor}')`,
35
+ children: $__i18n._({
36
+ id: "yQ2kGp",
37
+ message: "Load more"
38
+ })
39
+ })
40
+ })
41
+ ]
42
+ });
43
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Timeline Item Component
3
+ *
4
+ * Dispatches to the correct card component based on post type.
5
+ */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
6
+ import { NoteCard } from "./NoteCard.js";
7
+ import { ArticleCard } from "./ArticleCard.js";
8
+ import { LinkCard } from "./LinkCard.js";
9
+ import { QuoteCard } from "./QuoteCard.js";
10
+ import { ImageCard } from "./ImageCard.js";
11
+ const CARD_MAP = {
12
+ note: NoteCard,
13
+ article: ArticleCard,
14
+ link: LinkCard,
15
+ quote: QuoteCard,
16
+ image: ImageCard,
17
+ page: NoteCard
18
+ };
19
+ export const TimelineItem = ({ item, compact, cardOverride })=>{
20
+ const Card = cardOverride ?? CARD_MAP[item.post.type];
21
+ return /*#__PURE__*/ _jsx(Card, {
22
+ post: item.post,
23
+ compact: compact
24
+ });
25
+ };
@@ -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";
@@ -107,6 +107,14 @@ function DashLayoutContent({ siteName, currentPath, children }) {
107
107
  message: "Redirects"
108
108
  })
109
109
  }),
110
+ /*#__PURE__*/ _jsx("a", {
111
+ href: "/dash/navigation",
112
+ class: navClass("/dash/navigation", /^\/dash\/navigation/),
113
+ children: $__i18n._({
114
+ id: "UxKoFf",
115
+ message: "Navigation"
116
+ })
117
+ }),
110
118
  /*#__PURE__*/ _jsx("a", {
111
119
  href: "/dash/settings",
112
120
  class: navClass("/dash/settings", /^\/dash\/settings/),
@@ -0,0 +1,160 @@
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
+ */ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
7
+ /**
8
+ * Determine if a navigation link is active based on the current path.
9
+ *
10
+ * @param linkUrl - The link's URL
11
+ * @param currentPath - The current page path
12
+ * @returns Whether the link should be shown as active
13
+ */ function isLinkActive(linkUrl, currentPath) {
14
+ // External links are never active
15
+ if (linkUrl.startsWith("http://") || linkUrl.startsWith("https://")) {
16
+ return false;
17
+ }
18
+ // Exact match for home
19
+ if (linkUrl === "/") {
20
+ return currentPath === "/";
21
+ }
22
+ // Prefix match for other internal links
23
+ return currentPath === linkUrl || currentPath.startsWith(linkUrl + "/");
24
+ }
25
+ /**
26
+ * Check if a URL is external
27
+ */ function isExternalUrl(url) {
28
+ return url.startsWith("http://") || url.startsWith("https://");
29
+ }
30
+ /**
31
+ * Render navigation links with dot indicator for active state.
32
+ */ function NavLinks({ navigationLinks, currentPath }) {
33
+ return /*#__PURE__*/ _jsx(_Fragment, {
34
+ children: navigationLinks.map((link)=>{
35
+ const active = isLinkActive(link.url, currentPath);
36
+ const external = isExternalUrl(link.url);
37
+ return /*#__PURE__*/ _jsxs("a", {
38
+ href: link.url,
39
+ class: `text-sm flex items-center gap-2 py-0.5 ${active ? "text-primary font-medium" : "text-muted-foreground hover:text-foreground"}`,
40
+ ...external ? {
41
+ target: "_blank",
42
+ rel: "noopener noreferrer"
43
+ } : {},
44
+ children: [
45
+ /*#__PURE__*/ _jsx("span", {
46
+ class: `size-1.5 rounded-full shrink-0 ${active ? "bg-primary" : "bg-transparent"}`
47
+ }),
48
+ link.label,
49
+ external && /*#__PURE__*/ _jsx("span", {
50
+ class: "ml-1 text-xs opacity-50",
51
+ children: "↗"
52
+ })
53
+ ]
54
+ }, link.id);
55
+ })
56
+ });
57
+ }
58
+ export const SiteLayout = ({ siteName, navigationLinks, currentPath, children })=>{
59
+ return /*#__PURE__*/ _jsxs("div", {
60
+ class: "container py-8 md:flex md:gap-12",
61
+ "data-signals": JSON.stringify({
62
+ _drawerOpen: false
63
+ }),
64
+ children: [
65
+ /*#__PURE__*/ _jsxs("div", {
66
+ class: "flex items-center justify-between mb-6 md:hidden",
67
+ children: [
68
+ /*#__PURE__*/ _jsx("a", {
69
+ href: "/",
70
+ class: "text-xl font-semibold",
71
+ children: siteName
72
+ }),
73
+ /*#__PURE__*/ _jsx("button", {
74
+ "data-on:click": "$_drawerOpen = true",
75
+ class: "p-2 -mr-2 text-muted-foreground hover:text-foreground",
76
+ "aria-label": "Open menu",
77
+ children: /*#__PURE__*/ _jsx("svg", {
78
+ class: "size-5",
79
+ fill: "none",
80
+ viewBox: "0 0 24 24",
81
+ "stroke-width": "1.5",
82
+ stroke: "currentColor",
83
+ children: /*#__PURE__*/ _jsx("path", {
84
+ "stroke-linecap": "round",
85
+ "stroke-linejoin": "round",
86
+ d: "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
87
+ })
88
+ })
89
+ })
90
+ ]
91
+ }),
92
+ /*#__PURE__*/ _jsx("div", {
93
+ class: "fixed inset-0 bg-black/50 z-40 opacity-0 pointer-events-none transition-opacity duration-300 ease-in-out md:hidden",
94
+ "data-class": "{'opacity-100 pointer-events-auto': $_drawerOpen, 'opacity-0 pointer-events-none': !$_drawerOpen}",
95
+ "data-on:click": "$_drawerOpen = false"
96
+ }),
97
+ /*#__PURE__*/ _jsxs("aside", {
98
+ 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",
99
+ "data-class": "{'translate-x-0': $_drawerOpen, '-translate-x-full': !$_drawerOpen}",
100
+ children: [
101
+ /*#__PURE__*/ _jsxs("div", {
102
+ class: "flex items-center justify-between mb-8",
103
+ children: [
104
+ /*#__PURE__*/ _jsx("a", {
105
+ href: "/",
106
+ class: "text-xl font-semibold",
107
+ children: siteName
108
+ }),
109
+ /*#__PURE__*/ _jsx("button", {
110
+ "data-on:click": "$_drawerOpen = false",
111
+ class: "p-2 -mr-2 text-muted-foreground hover:text-foreground",
112
+ "aria-label": "Close menu",
113
+ children: /*#__PURE__*/ _jsx("svg", {
114
+ class: "size-5",
115
+ fill: "none",
116
+ viewBox: "0 0 24 24",
117
+ "stroke-width": "1.5",
118
+ stroke: "currentColor",
119
+ children: /*#__PURE__*/ _jsx("path", {
120
+ "stroke-linecap": "round",
121
+ "stroke-linejoin": "round",
122
+ d: "M6 18L18 6M6 6l12 12"
123
+ })
124
+ })
125
+ })
126
+ ]
127
+ }),
128
+ /*#__PURE__*/ _jsx("nav", {
129
+ class: "flex flex-col gap-0.5",
130
+ children: /*#__PURE__*/ _jsx(NavLinks, {
131
+ navigationLinks: navigationLinks,
132
+ currentPath: currentPath
133
+ })
134
+ })
135
+ ]
136
+ }),
137
+ /*#__PURE__*/ _jsxs("aside", {
138
+ class: "hidden md:block md:w-48 md:shrink-0 md:sticky md:top-8 md:self-start",
139
+ children: [
140
+ /*#__PURE__*/ _jsx("a", {
141
+ href: "/",
142
+ class: "text-xl font-semibold block mb-20",
143
+ children: siteName
144
+ }),
145
+ /*#__PURE__*/ _jsx("nav", {
146
+ class: "flex flex-col gap-0.5",
147
+ children: /*#__PURE__*/ _jsx(NavLinks, {
148
+ navigationLinks: navigationLinks,
149
+ currentPath: currentPath
150
+ })
151
+ })
152
+ ]
153
+ }),
154
+ /*#__PURE__*/ _jsx("main", {
155
+ class: "flex-1 min-w-0",
156
+ children: children
157
+ })
158
+ ]
159
+ });
160
+ };
@@ -1,2 +1,3 @@
1
1
  export { BaseLayout } from "./BaseLayout.js";
2
2
  export { DashLayout } from "./DashLayout.js";
3
+ export { SiteLayout } from "./SiteLayout.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Minimal type declarations for sortablejs
3
+ *
4
+ * Only covers the API surface used by nav-reorder.ts.
5
+ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,7 @@
37
37
  "drizzle-orm": "^0.45.1",
38
38
  "hono": "^4.11.7",
39
39
  "marked": "^17.0.1",
40
+ "sortablejs": "^1.15.6",
40
41
  "sqids": "^0.3.0",
41
42
  "uuidv7": "^1.1.0",
42
43
  "vite-ssr-components": "^0.5.2",
@@ -90,7 +91,7 @@
90
91
  "build": "pnpm build:lib",
91
92
  "build:lib": "swc src -d dist --strip-leading-paths --ignore '**/__tests__/**' && pnpm build:types",
92
93
  "build:types": "tsc -p tsconfig.build.json",
93
- "typecheck": "tsc --noEmit && tsc -p tsconfig.client.json",
94
+ "typecheck": "tsc --noEmit",
94
95
  "db:generate": "drizzle-kit generate",
95
96
  "i18n:extract": "lingui extract",
96
97
  "i18n:compile": "lingui compile --typescript",
@@ -89,6 +89,16 @@ export function createTestDatabase(options?: { fts?: boolean }) {
89
89
  if (trimmed) sqlite.exec(trimmed);
90
90
  }
91
91
 
92
+ // Apply navigation links migration
93
+ const migration3 = readFileSync(
94
+ resolve(MIGRATIONS_DIR, "0003_add_navigation_links.sql"),
95
+ "utf-8",
96
+ );
97
+ for (const sql of migration3.split("--> statement-breakpoint")) {
98
+ const trimmed = sql.trim();
99
+ if (trimmed) sqlite.exec(trimmed);
100
+ }
101
+
92
102
  const db = drizzle(sqlite, { schema });
93
103
 
94
104
  return { db, sqlite };
package/src/app.tsx CHANGED
@@ -29,11 +29,13 @@ import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
29
29
  import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
30
30
  import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
31
31
  import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
32
+ import { navigationRoutes as dashNavigationRoutes } from "./routes/dash/navigation.js";
32
33
 
33
34
  // Routes - API
34
35
  import { postsApiRoutes } from "./routes/api/posts.js";
35
36
  import { uploadApiRoutes } from "./routes/api/upload.js";
36
37
  import { searchApiRoutes } from "./routes/api/search.js";
38
+ import { timelineApiRoutes } from "./routes/api/timeline.js";
37
39
 
38
40
  // Routes - Feed
39
41
  import { rssRoutes } from "./routes/feed/rss.js";
@@ -168,6 +170,7 @@ export function createApp(config: JantConfig = {}): App {
168
170
 
169
171
  // API Routes
170
172
  app.route("/api/posts", postsApiRoutes);
173
+ app.route("/api/timeline", timelineApiRoutes);
171
174
 
172
175
  // Setup page component
173
176
  const SetupContent: FC = () => {
@@ -660,6 +663,7 @@ export function createApp(config: JantConfig = {}): App {
660
663
  app.route("/dash/settings", dashSettingsRoutes);
661
664
  app.route("/dash/redirects", dashRedirectsRoutes);
662
665
  app.route("/dash/collections", dashCollectionsRoutes);
666
+ app.route("/dash/navigation", dashNavigationRoutes);
663
667
  // API routes
664
668
  app.route("/api/upload", uploadApiRoutes);
665
669
  app.route("/api/search", searchApiRoutes);
package/src/client.ts CHANGED
@@ -11,3 +11,4 @@ import "./vendor/datastar.js";
11
11
  import "basecoat-css/all";
12
12
  import "./lib/image-processor.js";
13
13
  import "./lib/media-upload.js";
14
+ import "./lib/nav-reorder.js";
@@ -0,0 +1,8 @@
1
+ CREATE TABLE `navigation_links` (
2
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `label` text NOT NULL,
4
+ `url` text NOT NULL,
5
+ `position` integer DEFAULT 0 NOT NULL,
6
+ `created_at` integer NOT NULL,
7
+ `updated_at` integer NOT NULL
8
+ );