@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,76 @@
1
+ /**
2
+ * Collections Listing Page
3
+ *
4
+ * Lists all collections with titles, descriptions, and post counts.
5
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
+ import { useLingui as $_useLingui } from "@jant/core/i18n";
7
+ export const CollectionsPage = ({ collections })=>{
8
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
9
+ return /*#__PURE__*/ _jsxs("div", {
10
+ class: "py-6",
11
+ "data-page": "collections",
12
+ children: [
13
+ /*#__PURE__*/ _jsx("header", {
14
+ class: "mb-8",
15
+ children: /*#__PURE__*/ _jsx("h1", {
16
+ class: "text-2xl font-semibold",
17
+ children: $__i18n._({
18
+ id: "DoJzLz",
19
+ message: "Collections"
20
+ })
21
+ })
22
+ }),
23
+ /*#__PURE__*/ _jsx("main", {
24
+ children: collections.length === 0 ? /*#__PURE__*/ _jsx("p", {
25
+ class: "text-muted-foreground",
26
+ children: $__i18n._({
27
+ id: "+MACwa",
28
+ message: "No collections yet."
29
+ })
30
+ }) : /*#__PURE__*/ _jsx("div", {
31
+ class: "divide-y divide-border",
32
+ children: collections.map((collection)=>/*#__PURE__*/ _jsx("a", {
33
+ href: "/c/" + collection.slug,
34
+ class: "block py-4 hover:bg-accent/50 -mx-4 px-4 rounded-md transition-colors",
35
+ children: /*#__PURE__*/ _jsxs("div", {
36
+ class: "flex items-center gap-3",
37
+ children: [
38
+ collection.icon && /*#__PURE__*/ _jsx("span", {
39
+ class: "text-2xl",
40
+ children: collection.icon
41
+ }),
42
+ /*#__PURE__*/ _jsxs("div", {
43
+ class: "flex-1 min-w-0",
44
+ children: [
45
+ /*#__PURE__*/ _jsx("h2", {
46
+ class: "font-medium",
47
+ children: collection.title
48
+ }),
49
+ collection.description && /*#__PURE__*/ _jsx("p", {
50
+ class: "text-sm text-muted-foreground mt-1",
51
+ children: collection.description
52
+ })
53
+ ]
54
+ }),
55
+ /*#__PURE__*/ _jsxs("span", {
56
+ class: "text-sm text-muted-foreground shrink-0",
57
+ children: [
58
+ collection.postCount,
59
+ " ",
60
+ collection.postCount === 1 ? $__i18n._({
61
+ id: "GHg6h/",
62
+ message: "post"
63
+ }) : $__i18n._({
64
+ id: "jt/Ow/",
65
+ message: "posts"
66
+ })
67
+ ]
68
+ })
69
+ ]
70
+ })
71
+ }, collection.id))
72
+ })
73
+ })
74
+ ]
75
+ });
76
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Featured Page
3
+ *
4
+ * Shows featured posts as a timeline feed.
5
+ */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
6
+ import { useLingui as $_useLingui } from "@jant/core/i18n";
7
+ import { TimelineFeed } from "../feed/TimelineFeed.js";
8
+ export const FeaturedPage = ({ items })=>{
9
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
10
+ return /*#__PURE__*/ _jsx("div", {
11
+ "data-page": "featured",
12
+ children: /*#__PURE__*/ _jsx("main", {
13
+ children: items.length === 0 ? /*#__PURE__*/ _jsx("p", {
14
+ class: "text-muted-foreground",
15
+ children: $__i18n._({
16
+ id: "0yIy82",
17
+ message: "No featured posts yet."
18
+ })
19
+ }) : /*#__PURE__*/ _jsx(TimelineFeed, {
20
+ items: items
21
+ })
22
+ })
23
+ });
24
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Home Page
3
+ *
4
+ * Timeline feed with per-type card components and thread previews.
5
+ */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
6
+ import { useLingui as $_useLingui } from "@jant/core/i18n";
7
+ import { TimelineFeed } from "../feed/TimelineFeed.js";
8
+ export const HomePage = ({ items, currentPage, totalPages })=>{
9
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
10
+ return /*#__PURE__*/ _jsx("div", {
11
+ "data-page": "home",
12
+ children: items.length === 0 ? /*#__PURE__*/ _jsx("p", {
13
+ class: "py-12 text-center text-muted-foreground",
14
+ children: $__i18n._({
15
+ id: "ODiSoW",
16
+ message: "No posts yet."
17
+ })
18
+ }) : /*#__PURE__*/ _jsx(TimelineFeed, {
19
+ items: items,
20
+ currentPage: currentPage,
21
+ totalPages: totalPages
22
+ })
23
+ });
24
+ };
@@ -1,31 +1,39 @@
1
1
  /**
2
- * Minimal Theme - Post Page
2
+ * Single 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
  */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
6
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
- import { MediaGallery as DefaultMediaGallery } from "../../../theme/components/MediaGallery.js";
8
- export const PostPage = ({ post, theme })=>{
7
+ import { MediaGallery } from "../shared/MediaGallery.js";
8
+ export const PostPage = ({ post })=>{
9
9
  const { i18n: $__i18n, _: $__ } = $_useLingui();
10
- const Gallery = theme?.MediaGallery ?? DefaultMediaGallery;
11
10
  return /*#__PURE__*/ _jsxs("article", {
12
- class: "h-entry",
11
+ class: "h-entry py-6",
12
+ "data-page": "post",
13
+ "data-post": true,
14
+ "data-format": post.format,
13
15
  children: [
14
16
  post.title && /*#__PURE__*/ _jsx("h1", {
15
17
  class: "p-name text-2xl font-semibold mb-4",
16
18
  children: post.title
17
19
  }),
18
- /*#__PURE__*/ _jsx("div", {
20
+ post.bodyHtml && /*#__PURE__*/ _jsx("div", {
19
21
  class: "e-content prose",
22
+ "data-post-body": true,
20
23
  dangerouslySetInnerHTML: {
21
- __html: post.contentHtml || ""
24
+ __html: post.bodyHtml
22
25
  }
23
26
  }),
24
- post.media.length > 0 && /*#__PURE__*/ _jsx(Gallery, {
25
- attachments: post.media
27
+ post.media.length > 0 && /*#__PURE__*/ _jsx("div", {
28
+ class: "mt-4",
29
+ "data-post-media": true,
30
+ children: /*#__PURE__*/ _jsx(MediaGallery, {
31
+ attachments: post.media
32
+ })
26
33
  }),
27
34
  /*#__PURE__*/ _jsxs("footer", {
28
- class: "mt-8 pt-4 border-t border-border text-sm text-muted-foreground",
35
+ class: "mt-6 pt-4 border-t text-sm text-muted-foreground",
36
+ "data-post-meta": true,
29
37
  children: [
30
38
  /*#__PURE__*/ _jsx("time", {
31
39
  class: "dt-published",
@@ -34,7 +42,7 @@ export const PostPage = ({ post, theme })=>{
34
42
  }),
35
43
  /*#__PURE__*/ _jsx("a", {
36
44
  href: post.permalink,
37
- class: "u-url ml-4 hover:underline",
45
+ class: "u-url ml-4",
38
46
  children: $__i18n._({
39
47
  id: "D9Oea+",
40
48
  message: "Permalink"
@@ -1,18 +1,19 @@
1
1
  /**
2
- * Minimal Theme - Search Page
2
+ * 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
  */ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
6
6
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
- import { PagePagination as DefaultPagePagination } from "../../../theme/components/Pagination.js";
8
- export const SearchPage = ({ query, results, error, hasMore, page, theme })=>{
7
+ import { PagePagination } from "../shared/Pagination.js";
8
+ export const SearchPage = ({ query, results, error, hasMore, page })=>{
9
9
  const { i18n: $__i18n, _: $__ } = $_useLingui();
10
10
  const searchTitle = $__i18n._({
11
11
  id: "A1taO8",
12
12
  message: "Search"
13
13
  });
14
- const PaginationComponent = theme?.PagePagination ?? DefaultPagePagination;
15
14
  return /*#__PURE__*/ _jsxs("div", {
15
+ class: "py-6",
16
+ "data-page": "search",
16
17
  children: [
17
18
  /*#__PURE__*/ _jsx("h1", {
18
19
  class: "text-2xl font-semibold mb-6",
@@ -71,31 +72,31 @@ export const SearchPage = ({ query, results, error, hasMore, page, theme })=>{
71
72
  results.length > 0 && /*#__PURE__*/ _jsxs(_Fragment, {
72
73
  children: [
73
74
  /*#__PURE__*/ _jsx("div", {
74
- class: "flex flex-col gap-4",
75
+ class: "divide-y divide-border",
75
76
  children: results.map((result)=>/*#__PURE__*/ _jsx("article", {
76
- class: "py-3",
77
+ class: "py-4",
78
+ "data-post": true,
79
+ "data-format": result.post.format,
77
80
  children: /*#__PURE__*/ _jsxs("a", {
78
81
  href: result.post.permalink,
79
- class: "block group",
82
+ class: "block",
80
83
  children: [
81
84
  /*#__PURE__*/ _jsx("h2", {
82
- class: "font-medium group-hover:underline",
83
- children: result.post.title || result.post.content?.slice(0, 60) || `Post #${result.post.id}`
85
+ class: "font-medium hover:underline",
86
+ children: result.post.title || result.post.excerpt?.slice(0, 60) || "Post #" + result.post.id
84
87
  }),
85
88
  result.snippet && /*#__PURE__*/ _jsx("p", {
86
- class: "text-sm text-muted-foreground mt-1 line-clamp-2",
89
+ class: "text-sm text-muted-foreground mt-2 line-clamp-2",
87
90
  dangerouslySetInnerHTML: {
88
91
  __html: result.snippet
89
92
  }
90
93
  }),
91
94
  /*#__PURE__*/ _jsxs("footer", {
92
- class: "flex items-center gap-2 mt-1 text-xs text-muted-foreground",
95
+ class: "flex items-center gap-2 mt-2 text-xs text-muted-foreground",
93
96
  children: [
94
97
  /*#__PURE__*/ _jsx("span", {
95
- children: result.post.type
96
- }),
97
- /*#__PURE__*/ _jsx("span", {
98
- children: "·"
98
+ class: "badge-outline",
99
+ children: result.post.format
99
100
  }),
100
101
  /*#__PURE__*/ _jsx("time", {
101
102
  datetime: result.post.publishedAt,
@@ -107,8 +108,8 @@ export const SearchPage = ({ query, results, error, hasMore, page, theme })=>{
107
108
  })
108
109
  }, result.post.id))
109
110
  }),
110
- /*#__PURE__*/ _jsx(PaginationComponent, {
111
- baseUrl: `/search?q=${encodeURIComponent(query)}`,
111
+ /*#__PURE__*/ _jsx(PagePagination, {
112
+ baseUrl: "/search?q=" + encodeURIComponent(query),
112
113
  currentPage: page,
113
114
  hasMore: hasMore
114
115
  })
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Minimal Theme - Single Page
2
+ * Single Page (Custom Page)
3
3
  *
4
- * Simple page content layout for type="page" posts.
4
+ * Custom page view clean centered content.
5
5
  */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
6
  export const SinglePage = ({ page })=>{
7
7
  return /*#__PURE__*/ _jsxs("article", {
8
- class: "h-entry",
8
+ class: "h-entry py-6",
9
+ "data-page": "single-page",
9
10
  children: [
10
11
  page.title && /*#__PURE__*/ _jsx("h1", {
11
12
  class: "p-name text-2xl font-semibold mb-6",
@@ -14,7 +15,7 @@ export const SinglePage = ({ page })=>{
14
15
  /*#__PURE__*/ _jsx("div", {
15
16
  class: "e-content prose",
16
17
  dangerouslySetInnerHTML: {
17
- __html: page.contentHtml || ""
18
+ __html: page.bodyHtml || ""
18
19
  }
19
20
  })
20
21
  ]
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Media Gallery Component
3
+ *
4
+ * Renders media attachments in a horizontal scrollable row,
5
+ * similar to an image carousel.
6
+ */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
7
+ export const MediaGallery = ({ attachments })=>{
8
+ const images = attachments.filter((a)=>a.mimeType.startsWith("image/"));
9
+ if (images.length === 0) return null;
10
+ const single = images.length === 1;
11
+ return /*#__PURE__*/ _jsx("div", {
12
+ class: `mt-3 flex gap-2 ${single ? "" : "overflow-x-auto scroll-smooth snap-x snap-mandatory"}`,
13
+ style: single ? undefined : "scrollbar-width: none; -ms-overflow-style: none;",
14
+ children: images.map((img)=>{
15
+ const aspectRatio = img.width && img.height ? img.width / img.height : 4 / 3;
16
+ const itemWidth = single ? undefined : `${Math.round(320 * Math.min(Math.max(aspectRatio, 0.6), 1.6))}px`;
17
+ return /*#__PURE__*/ _jsx("a", {
18
+ href: img.url,
19
+ target: "_blank",
20
+ rel: "noopener noreferrer",
21
+ class: `${single ? "" : "shrink-0 snap-start"} block rounded-lg overflow-hidden`,
22
+ style: single ? undefined : {
23
+ width: itemWidth,
24
+ maxWidth: "85%"
25
+ },
26
+ children: /*#__PURE__*/ _jsx("img", {
27
+ src: img.thumbnailUrl,
28
+ alt: img.altText || "",
29
+ class: single ? "rounded-lg max-w-full max-h-96 h-auto object-contain" : "h-80 w-full object-cover",
30
+ loading: "lazy"
31
+ })
32
+ }, img.id);
33
+ })
34
+ });
35
+ };
@@ -4,6 +4,7 @@
4
4
  * Cursor-based pagination for post lists
5
5
  */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
6
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
+ import { getPageNumbers } from "../../lib/pagination.js";
7
8
  export const Pagination = ({ baseUrl, hasMore, nextCursor, prevCursor, cursorParam = "cursor" })=>{
8
9
  const { i18n: $__i18n, _: $__ } = $_useLingui();
9
10
  const hasPrev = prevCursor !== undefined;
@@ -82,10 +83,10 @@ export const LoadMore = ({ href, hasMore, text })=>{
82
83
  })
83
84
  });
84
85
  };
85
- export const PagePagination = ({ baseUrl, currentPage, hasMore, pageParam = "page" })=>{
86
+ export const PagePagination = ({ baseUrl, currentPage, hasMore, totalPages, pageParam = "page" })=>{
86
87
  const { i18n: $__i18n, _: $__ } = $_useLingui();
87
88
  const hasPrev = currentPage > 1;
88
- const hasNext = hasMore;
89
+ const hasNext = totalPages ? currentPage < totalPages : hasMore ?? false;
89
90
  if (!hasPrev && !hasNext) {
90
91
  return null;
91
92
  }
@@ -107,6 +108,44 @@ export const PagePagination = ({ baseUrl, currentPage, hasMore, pageParam = "pag
107
108
  id: "hXzOVo",
108
109
  message: "Next"
109
110
  });
111
+ // Numbered pagination when totalPages is known
112
+ if (totalPages && totalPages > 1) {
113
+ const pageNumbers = getPageNumbers(currentPage, totalPages);
114
+ return /*#__PURE__*/ _jsxs("nav", {
115
+ class: "flex items-center justify-center gap-4 py-6 text-sm",
116
+ "aria-label": "Pagination",
117
+ children: [
118
+ hasPrev ? /*#__PURE__*/ _jsx("a", {
119
+ href: buildUrl(currentPage - 1),
120
+ class: "underline text-muted-foreground hover:text-foreground",
121
+ children: prevText
122
+ }) : /*#__PURE__*/ _jsx("span", {
123
+ class: "text-muted-foreground/50",
124
+ children: prevText
125
+ }),
126
+ pageNumbers.map((page, i)=>page === 0 ? /*#__PURE__*/ _jsx("span", {
127
+ class: "text-muted-foreground",
128
+ children: "..."
129
+ }, `ellipsis-${i}`) : page === currentPage ? /*#__PURE__*/ _jsx("span", {
130
+ "aria-current": "page",
131
+ children: page
132
+ }, page) : /*#__PURE__*/ _jsx("a", {
133
+ href: buildUrl(page),
134
+ class: "underline text-muted-foreground hover:text-foreground",
135
+ children: page
136
+ }, page)),
137
+ hasNext ? /*#__PURE__*/ _jsx("a", {
138
+ href: buildUrl(currentPage + 1),
139
+ class: "underline text-muted-foreground hover:text-foreground",
140
+ children: nextText
141
+ }) : /*#__PURE__*/ _jsx("span", {
142
+ class: "text-muted-foreground/50",
143
+ children: nextText
144
+ })
145
+ ]
146
+ });
147
+ }
148
+ // Simple prev/next fallback when totalPages is unknown
110
149
  const pageText = $__i18n._({
111
150
  id: "tiq7kl",
112
151
  message: "Page {page}"
@@ -15,7 +15,7 @@ const ThreadPost = ({ post, isCurrent, isRoot })=>{
15
15
  post.title && /*#__PURE__*/ _jsx("h2", {
16
16
  class: "p-name text-lg font-medium mb-2",
17
17
  children: /*#__PURE__*/ _jsx("a", {
18
- href: `/p/${sqid.encode(post.id)}`,
18
+ href: `${post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`}`,
19
19
  class: "u-url hover:underline",
20
20
  children: post.title
21
21
  })
@@ -23,7 +23,7 @@ const ThreadPost = ({ post, isCurrent, isRoot })=>{
23
23
  /*#__PURE__*/ _jsx("div", {
24
24
  class: "e-content prose prose-sm",
25
25
  dangerouslySetInnerHTML: {
26
- __html: post.contentHtml || ""
26
+ __html: post.bodyHtml || ""
27
27
  }
28
28
  }),
29
29
  /*#__PURE__*/ _jsxs("footer", {
@@ -42,7 +42,7 @@ const ThreadPost = ({ post, isCurrent, isRoot })=>{
42
42
  })
43
43
  }),
44
44
  !isCurrent && /*#__PURE__*/ _jsx("a", {
45
- href: `/p/${sqid.encode(post.id)}`,
45
+ href: `${post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`}`,
46
46
  class: "text-xs hover:underline",
47
47
  children: $__i18n._({
48
48
  id: "D9Oea+",
@@ -0,0 +1,5 @@
1
+ export { EmptyState } from "./EmptyState.js";
2
+ export { MediaGallery } from "./MediaGallery.js";
3
+ export { Pagination, LoadMore, PagePagination } from "./Pagination.js";
4
+ export { getPageNumbers } from "../../lib/pagination.js";
5
+ export { ThreadView } from "./ThreadView.js";
package/package.json CHANGED
@@ -1,20 +1,13 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
- "bin": {
7
- "jant": "./bin/jant.js"
8
- },
9
6
  "exports": {
10
7
  ".": {
11
8
  "types": "./src/index.ts",
12
9
  "default": "./dist/index.js"
13
10
  },
14
- "./theme": {
15
- "types": "./src/theme/index.ts",
16
- "default": "./dist/theme/index.js"
17
- },
18
11
  "./i18n": {
19
12
  "types": "./src/i18n/index.ts",
20
13
  "default": "./dist/i18n/index.js"
@@ -23,7 +16,6 @@
23
16
  "./client": "./dist/client.js"
24
17
  },
25
18
  "files": [
26
- "bin",
27
19
  "dist",
28
20
  "src"
29
21
  ],
@@ -33,6 +25,7 @@
33
25
  "dependencies": {
34
26
  "@aws-sdk/client-s3": "^3.987.0",
35
27
  "@lingui/core": "^5.9.0",
28
+ "@tailwindcss/typography": "^0.5.19",
36
29
  "basecoat-css": "^0.3.10",
37
30
  "better-auth": "^1.4.18",
38
31
  "drizzle-orm": "^0.45.1",
@@ -9,11 +9,13 @@ import type { Bindings } from "../../types.js";
9
9
  import type { AppVariables } from "../../app.js";
10
10
  import { createTestDatabase } from "./db.js";
11
11
  import { createPostService } from "../../services/post.js";
12
+ import { createPageService } from "../../services/page.js";
12
13
  import { createSettingsService } from "../../services/settings.js";
13
14
  import { createRedirectService } from "../../services/redirect.js";
14
15
  import { createMediaService } from "../../services/media.js";
15
16
  import { createCollectionService } from "../../services/collection.js";
16
17
  import { createSearchService } from "../../services/search.js";
18
+ import { createNavItemService } from "../../services/navigation.js";
17
19
  import type { Database } from "../../db/index.js";
18
20
  import type BetterSqlite3 from "better-sqlite3";
19
21
 
@@ -40,11 +42,13 @@ export function createTestApp(options: TestAppOptions = {}) {
40
42
 
41
43
  const services = {
42
44
  posts: createPostService(db),
45
+ pages: createPageService(db),
43
46
  settings: createSettingsService(db),
44
47
  redirects: createRedirectService(db),
45
48
  media: createMediaService(db),
46
49
  collections: createCollectionService(db),
47
50
  search: createSearchService(mockD1),
51
+ navItems: createNavItemService(db),
48
52
  };
49
53
 
50
54
  const app = new Hono<Env>();
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Test Database Helper
3
3
  *
4
- * Creates an in-memory SQLite database with all migrations applied.
4
+ * Creates an in-memory SQLite database with all migrations applied (up to v2).
5
5
  * Used for service integration tests.
6
6
  */
7
7
 
@@ -13,11 +13,22 @@ import { resolve } from "path";
13
13
 
14
14
  const MIGRATIONS_DIR = resolve(import.meta.dirname, "../../db/migrations");
15
15
 
16
+ /**
17
+ * Applies a migration file, splitting on Drizzle statement breakpoints.
18
+ */
19
+ function applyMigration(sqlite: Database.Database, filename: string) {
20
+ const migration = readFileSync(resolve(MIGRATIONS_DIR, filename), "utf-8");
21
+ for (const sql of migration.split("--> statement-breakpoint")) {
22
+ const trimmed = sql.trim();
23
+ if (trimmed) sqlite.exec(trimmed);
24
+ }
25
+ }
26
+
16
27
  /**
17
28
  * Creates a fresh in-memory SQLite database with all migrations applied.
18
29
  * Each call returns an isolated database instance for test isolation.
19
30
  *
20
- * @param options.fts - Whether to apply FTS5 migration (default: false).
31
+ * @param options.fts - Whether to enable FTS5 for search tests (default: false).
21
32
  * The trigram tokenizer used in production may not be available in all
22
33
  * better-sqlite3 builds, so FTS is opt-in for tests that need it.
23
34
  */
@@ -28,86 +39,55 @@ export function createTestDatabase(options?: { fts?: boolean }) {
28
39
  sqlite.pragma("journal_mode = WAL");
29
40
  sqlite.pragma("foreign_keys = ON");
30
41
 
31
- // Apply base schema migration
32
- const migration0 = readFileSync(
33
- resolve(MIGRATIONS_DIR, "0000_square_wallflower.sql"),
42
+ // Apply v1 base migrations (0000-0004)
43
+ applyMigration(sqlite, "0000_square_wallflower.sql");
44
+ // Skip 0001 (FTS) — v2 migration will create updated FTS if needed
45
+ applyMigration(sqlite, "0002_add_media_attachments.sql");
46
+ applyMigration(sqlite, "0003_add_navigation_links.sql");
47
+ applyMigration(sqlite, "0004_add_storage_provider.sql");
48
+
49
+ // Apply v2 schema migration (0005)
50
+ // Split FTS-related statements so we can handle them separately
51
+ const v2Migration = readFileSync(
52
+ resolve(MIGRATIONS_DIR, "0005_v2_schema_migration.sql"),
34
53
  "utf-8",
35
54
  );
36
55
 
37
- // Drizzle migrations use --> statement-breakpoint as separator
38
- for (const sql of migration0.split("--> statement-breakpoint")) {
39
- const trimmed = sql.trim();
40
- if (trimmed) sqlite.exec(trimmed);
41
- }
56
+ for (const stmt of v2Migration.split("--> statement-breakpoint")) {
57
+ const trimmed = stmt.trim();
58
+ if (!trimmed) continue;
59
+
60
+ // Skip FTS-related statements if FTS not requested
61
+ const isFts = trimmed.includes("posts_fts");
62
+ if (!options?.fts && isFts) continue;
42
63
 
43
- // Optionally apply FTS5 migration (with fallback tokenizer)
44
- if (options?.fts) {
45
64
  try {
46
- const migration1 = readFileSync(
47
- resolve(MIGRATIONS_DIR, "0001_add_search_fts.sql"),
48
- "utf-8",
49
- );
50
- sqlite.exec(migration1);
65
+ sqlite.exec(trimmed);
51
66
  } catch {
52
- // Fallback: create FTS table with default tokenizer if trigram not available
53
- sqlite.exec(`
54
- CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
55
- title,
56
- content,
57
- content='posts',
58
- content_rowid='id'
59
- );
60
-
61
- CREATE TRIGGER IF NOT EXISTS posts_fts_insert AFTER INSERT ON posts
62
- WHEN NEW.deleted_at IS NULL
63
- BEGIN
64
- INSERT INTO posts_fts(rowid, title, content)
65
- VALUES (NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.content, ''));
66
- END;
67
-
68
- CREATE TRIGGER IF NOT EXISTS posts_fts_update AFTER UPDATE ON posts BEGIN
69
- DELETE FROM posts_fts WHERE rowid = OLD.id;
70
- INSERT INTO posts_fts(rowid, title, content)
71
- SELECT NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.content, '')
72
- WHERE NEW.deleted_at IS NULL;
73
- END;
74
-
75
- CREATE TRIGGER IF NOT EXISTS posts_fts_delete AFTER DELETE ON posts BEGIN
76
- DELETE FROM posts_fts WHERE rowid = OLD.id;
77
- END;
78
- `);
67
+ // Handle trigram tokenizer failure for FTS virtual table
68
+ if (options?.fts && trimmed.includes("CREATE VIRTUAL TABLE")) {
69
+ sqlite.exec(`
70
+ CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
71
+ title,
72
+ body,
73
+ quote_text,
74
+ content='posts',
75
+ content_rowid='id'
76
+ );
77
+ `);
78
+ }
79
+ // Ignore DROP TRIGGER/TABLE IF EXISTS failures silently
80
+ else if (
81
+ !trimmed.startsWith("DROP TRIGGER") &&
82
+ !trimmed.startsWith("DROP TABLE")
83
+ ) {
84
+ throw new Error(`Migration statement failed: ${trimmed.slice(0, 100)}`);
85
+ }
79
86
  }
80
87
  }
81
88
 
82
- // Apply media attachments migration (position + blurhash)
83
- const migration2 = readFileSync(
84
- resolve(MIGRATIONS_DIR, "0002_add_media_attachments.sql"),
85
- "utf-8",
86
- );
87
- for (const sql of migration2.split("--> statement-breakpoint")) {
88
- const trimmed = sql.trim();
89
- if (trimmed) sqlite.exec(trimmed);
90
- }
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
-
102
- // Apply storage provider migration
103
- const migration4 = readFileSync(
104
- resolve(MIGRATIONS_DIR, "0004_add_storage_provider.sql"),
105
- "utf-8",
106
- );
107
- for (const sql of migration4.split("--> statement-breakpoint")) {
108
- const trimmed = sql.trim();
109
- if (trimmed) sqlite.exec(trimmed);
110
- }
89
+ // Apply 0006: rename slug to path on posts
90
+ applyMigration(sqlite, "0006_rename_slug_to_path.sql");
111
91
 
112
92
  const db = drizzle(sqlite, { schema });
113
93