@jant/core 0.3.24 → 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 (206) hide show
  1. package/dist/app.js +50 -25
  2. package/dist/db/schema.js +1 -1
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +3 -9
  7. package/dist/lib/constants.js +1 -0
  8. package/dist/lib/nav-reorder.js +1 -1
  9. package/dist/lib/navigation.js +26 -1
  10. package/dist/lib/pagination.js +44 -0
  11. package/dist/lib/render.js +7 -11
  12. package/dist/lib/schemas.js +3 -3
  13. package/dist/lib/theme.js +4 -4
  14. package/dist/lib/timeline.js +24 -48
  15. package/dist/lib/view.js +2 -2
  16. package/dist/routes/api/collections.js +124 -0
  17. package/dist/routes/api/nav-items.js +104 -0
  18. package/dist/routes/api/pages.js +91 -0
  19. package/dist/routes/api/posts.js +2 -2
  20. package/dist/routes/api/search.js +2 -2
  21. package/dist/routes/api/settings.js +68 -0
  22. package/dist/routes/compose.js +48 -0
  23. package/dist/routes/dash/collections.js +2 -2
  24. package/dist/routes/dash/index.js +1 -1
  25. package/dist/routes/dash/media.js +2 -2
  26. package/dist/routes/dash/pages.js +411 -62
  27. package/dist/routes/dash/posts.js +3 -5
  28. package/dist/routes/dash/redirects.js +2 -2
  29. package/dist/routes/dash/settings.js +79 -5
  30. package/dist/routes/feed/rss.js +2 -2
  31. package/dist/routes/feed/sitemap.js +1 -1
  32. package/dist/routes/pages/archive.js +3 -6
  33. package/dist/routes/pages/collection.js +3 -6
  34. package/dist/routes/pages/collections.js +28 -0
  35. package/dist/routes/pages/featured.js +32 -0
  36. package/dist/routes/pages/home.js +9 -50
  37. package/dist/routes/pages/page.js +29 -32
  38. package/dist/routes/pages/post.js +3 -6
  39. package/dist/routes/pages/search.js +3 -6
  40. package/dist/services/page.js +5 -1
  41. package/dist/services/post.js +40 -6
  42. package/dist/services/search.js +1 -1
  43. package/dist/ui/compose/ComposeDialog.js +452 -0
  44. package/dist/ui/compose/ComposePrompt.js +55 -0
  45. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  46. package/dist/{theme/components → ui/dash}/PostForm.js +0 -27
  47. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  48. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  49. package/dist/{theme/components → ui/dash}/index.js +3 -6
  50. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  51. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  52. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  53. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  54. package/dist/ui/feed/TimelineFeed.js +41 -0
  55. package/dist/ui/feed/TimelineItem.js +27 -0
  56. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  57. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  58. package/dist/ui/layouts/SiteLayout.js +141 -0
  59. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  60. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  61. package/dist/ui/pages/CollectionsPage.js +76 -0
  62. package/dist/ui/pages/FeaturedPage.js +24 -0
  63. package/dist/ui/pages/HomePage.js +24 -0
  64. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  65. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  66. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  67. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  68. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  69. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  70. package/dist/ui/shared/index.js +5 -0
  71. package/package.json +1 -9
  72. package/src/__tests__/helpers/db.ts +3 -0
  73. package/src/app.tsx +57 -27
  74. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  75. package/src/db/migrations/meta/_journal.json +7 -0
  76. package/src/db/schema.ts +1 -1
  77. package/src/i18n/locales/en.po +332 -181
  78. package/src/i18n/locales/en.ts +1 -1
  79. package/src/i18n/locales/zh-Hans.po +332 -181
  80. package/src/i18n/locales/zh-Hans.ts +1 -1
  81. package/src/i18n/locales/zh-Hant.po +332 -181
  82. package/src/i18n/locales/zh-Hant.ts +1 -1
  83. package/src/index.ts +7 -36
  84. package/src/lib/__tests__/schemas.test.ts +60 -19
  85. package/src/lib/__tests__/timeline.test.ts +45 -81
  86. package/src/lib/__tests__/view.test.ts +13 -7
  87. package/src/lib/constants.ts +1 -0
  88. package/src/lib/nav-reorder.ts +1 -1
  89. package/src/lib/navigation.ts +40 -2
  90. package/src/lib/pagination.ts +50 -0
  91. package/src/lib/render.tsx +7 -14
  92. package/src/lib/schemas.ts +8 -6
  93. package/src/lib/theme.ts +5 -5
  94. package/src/lib/timeline.ts +28 -57
  95. package/src/lib/view.ts +2 -2
  96. package/src/preset.css +2 -1
  97. package/src/routes/__tests__/compose.test.ts +199 -0
  98. package/src/routes/api/__tests__/collections.test.ts +249 -0
  99. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  100. package/src/routes/api/__tests__/pages.test.ts +218 -0
  101. package/src/routes/api/__tests__/settings.test.ts +132 -0
  102. package/src/routes/api/collections.ts +143 -0
  103. package/src/routes/api/nav-items.ts +115 -0
  104. package/src/routes/api/pages.ts +101 -0
  105. package/src/routes/api/posts.ts +2 -2
  106. package/src/routes/api/search.ts +2 -2
  107. package/src/routes/api/settings.ts +91 -0
  108. package/src/routes/compose.ts +63 -0
  109. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  110. package/src/routes/dash/collections.tsx +2 -2
  111. package/src/routes/dash/index.tsx +1 -1
  112. package/src/routes/dash/media.tsx +2 -2
  113. package/src/routes/dash/pages.tsx +443 -70
  114. package/src/routes/dash/posts.tsx +3 -7
  115. package/src/routes/dash/redirects.tsx +2 -2
  116. package/src/routes/dash/settings.tsx +83 -5
  117. package/src/routes/feed/rss.ts +2 -2
  118. package/src/routes/feed/sitemap.ts +1 -1
  119. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  120. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  121. package/src/routes/pages/archive.tsx +2 -6
  122. package/src/routes/pages/collection.tsx +2 -6
  123. package/src/routes/pages/collections.tsx +36 -0
  124. package/src/routes/pages/featured.tsx +38 -0
  125. package/src/routes/pages/home.tsx +9 -55
  126. package/src/routes/pages/page.tsx +28 -30
  127. package/src/routes/pages/post.tsx +2 -5
  128. package/src/routes/pages/search.tsx +2 -6
  129. package/src/services/__tests__/page.test.ts +106 -0
  130. package/src/services/__tests__/post.test.ts +114 -15
  131. package/src/services/page.ts +13 -1
  132. package/src/services/post.ts +57 -7
  133. package/src/services/search.ts +2 -2
  134. package/src/styles/tokens.css +47 -0
  135. package/src/styles/ui.css +491 -0
  136. package/src/types.ts +29 -159
  137. package/src/ui/compose/ComposeDialog.tsx +395 -0
  138. package/src/ui/compose/ComposePrompt.tsx +55 -0
  139. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  140. package/src/{theme/components → ui/dash}/PostForm.tsx +0 -25
  141. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  142. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  143. package/src/ui/dash/index.ts +10 -0
  144. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  145. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  146. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  147. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  148. package/src/ui/feed/TimelineFeed.tsx +49 -0
  149. package/src/ui/feed/TimelineItem.tsx +45 -0
  150. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  151. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  152. package/src/ui/layouts/SiteLayout.tsx +150 -0
  153. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  154. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  155. package/src/ui/pages/CollectionsPage.tsx +73 -0
  156. package/src/ui/pages/FeaturedPage.tsx +31 -0
  157. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  158. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  159. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  160. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  161. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  162. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  163. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  164. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  165. package/src/ui/shared/index.ts +12 -0
  166. package/bin/jant.js +0 -185
  167. package/dist/lib/theme-components.js +0 -46
  168. package/dist/routes/dash/navigation.js +0 -289
  169. package/dist/theme/index.js +0 -18
  170. package/dist/theme/layouts/index.js +0 -2
  171. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  172. package/dist/themes/threads/index.js +0 -81
  173. package/dist/themes/threads/pages/HomePage.js +0 -25
  174. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  175. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  176. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  177. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  178. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  179. package/src/lib/__tests__/theme-components.test.ts +0 -105
  180. package/src/lib/theme-components.ts +0 -65
  181. package/src/routes/dash/navigation.tsx +0 -317
  182. package/src/theme/components/index.ts +0 -23
  183. package/src/theme/index.ts +0 -22
  184. package/src/theme/layouts/index.ts +0 -7
  185. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  186. package/src/themes/threads/index.ts +0 -100
  187. package/src/themes/threads/style.css +0 -336
  188. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  189. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  190. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  191. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  192. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  193. /package/dist/{theme → ui}/color-themes.js +0 -0
  194. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  195. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  196. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  197. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  198. /package/dist/{theme/components → ui/dash}/PageForm.js +0 -0
  199. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  200. /package/src/{theme → ui}/color-themes.ts +0 -0
  201. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  202. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  203. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  204. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  205. /package/src/{theme/components → ui/dash}/PageForm.tsx +0 -0
  206. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -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,34 +1,39 @@
1
1
  /**
2
- * Threads Theme - Post Page
2
+ * Single Post Page
3
3
  *
4
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/index.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
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.bodyHtml || ""
24
+ __html: post.bodyHtml
22
25
  }
23
26
  }),
24
27
  post.media.length > 0 && /*#__PURE__*/ _jsx("div", {
25
- class: "threads-media mt-4",
26
- children: /*#__PURE__*/ _jsx(Gallery, {
28
+ class: "mt-4",
29
+ "data-post-media": true,
30
+ children: /*#__PURE__*/ _jsx(MediaGallery, {
27
31
  attachments: post.media
28
32
  })
29
33
  }),
30
34
  /*#__PURE__*/ _jsxs("footer", {
31
35
  class: "mt-6 pt-4 border-t text-sm text-muted-foreground",
36
+ "data-post-meta": true,
32
37
  children: [
33
38
  /*#__PURE__*/ _jsx("time", {
34
39
  class: "dt-published",
@@ -1,19 +1,19 @@
1
1
  /**
2
- * Threads Theme - Search Page
2
+ * Search Page
3
3
  *
4
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/index.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", {
16
15
  class: "py-6",
16
+ "data-page": "search",
17
17
  children: [
18
18
  /*#__PURE__*/ _jsx("h1", {
19
19
  class: "text-2xl font-semibold mb-6",
@@ -75,13 +75,15 @@ export const SearchPage = ({ query, results, error, hasMore, page, theme })=>{
75
75
  class: "divide-y divide-border",
76
76
  children: results.map((result)=>/*#__PURE__*/ _jsx("article", {
77
77
  class: "py-4",
78
+ "data-post": true,
79
+ "data-format": result.post.format,
78
80
  children: /*#__PURE__*/ _jsxs("a", {
79
81
  href: result.post.permalink,
80
82
  class: "block",
81
83
  children: [
82
84
  /*#__PURE__*/ _jsx("h2", {
83
85
  class: "font-medium hover:underline",
84
- children: result.post.title || result.post.excerpt?.slice(0, 60) || `Post #${result.post.id}`
86
+ children: result.post.title || result.post.excerpt?.slice(0, 60) || "Post #" + result.post.id
85
87
  }),
86
88
  result.snippet && /*#__PURE__*/ _jsx("p", {
87
89
  class: "text-sm text-muted-foreground mt-2 line-clamp-2",
@@ -106,8 +108,8 @@ export const SearchPage = ({ query, results, error, hasMore, page, theme })=>{
106
108
  })
107
109
  }, result.post.id))
108
110
  }),
109
- /*#__PURE__*/ _jsx(PaginationComponent, {
110
- baseUrl: `/search?q=${encodeURIComponent(query)}`,
111
+ /*#__PURE__*/ _jsx(PagePagination, {
112
+ baseUrl: "/search?q=" + encodeURIComponent(query),
111
113
  currentPage: page,
112
114
  hasMore: hasMore
113
115
  })
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Threads Theme - Single Page
2
+ * Single Page (Custom Page)
3
3
  *
4
- * Custom page (type "page") view — clean centered content.
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
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",
@@ -2,7 +2,7 @@
2
2
  * Media Gallery Component
3
3
  *
4
4
  * Renders media attachments in a horizontal scrollable row,
5
- * similar to Threads.net's image carousel.
5
+ * similar to an image carousel.
6
6
  */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
7
7
  export const MediaGallery = ({ attachments })=>{
8
8
  const images = attachments.filter((a)=>a.mimeType.startsWith("image/"));
@@ -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: `${post.slug ? `/${post.slug}` : `/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
  })
@@ -42,7 +42,7 @@ const ThreadPost = ({ post, isCurrent, isRoot })=>{
42
42
  })
43
43
  }),
44
44
  !isCurrent && /*#__PURE__*/ _jsx("a", {
45
- href: `${post.slug ? `/${post.slug}` : `/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.24",
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
  ],
@@ -86,6 +86,9 @@ export function createTestDatabase(options?: { fts?: boolean }) {
86
86
  }
87
87
  }
88
88
 
89
+ // Apply 0006: rename slug to path on posts
90
+ applyMigration(sqlite, "0006_rename_slug_to_path.sql");
91
+
89
92
  const db = drizzle(sqlite, { schema });
90
93
 
91
94
  return { db, sqlite };
package/src/app.tsx CHANGED
@@ -11,7 +11,6 @@ import { i18nMiddleware } from "./i18n/index.js";
11
11
  import { useLingui } from "@lingui/react/macro";
12
12
  import type { Bindings, JantConfig } from "./types.js";
13
13
  import { SETTINGS_KEYS } from "./lib/constants.js";
14
- import { theme as threadsTheme } from "./themes/threads/index.js";
15
14
  import { hashPassword } from "better-auth/crypto";
16
15
 
17
16
  // Routes - Pages
@@ -21,6 +20,8 @@ import { pageRoutes } from "./routes/pages/page.js";
21
20
  import { collectionRoutes } from "./routes/pages/collection.js";
22
21
  import { archiveRoutes } from "./routes/pages/archive.js";
23
22
  import { searchRoutes } from "./routes/pages/search.js";
23
+ import { featuredRoutes } from "./routes/pages/featured.js";
24
+ import { collectionsPageRoutes } from "./routes/pages/collections.js";
24
25
 
25
26
  // Routes - Dashboard
26
27
  import { dashIndexRoutes } from "./routes/dash/index.js";
@@ -30,12 +31,18 @@ import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
30
31
  import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
31
32
  import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
32
33
  import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
33
- import { navigationRoutes as dashNavigationRoutes } from "./routes/dash/navigation.js";
34
34
 
35
35
  // Routes - API
36
36
  import { postsApiRoutes } from "./routes/api/posts.js";
37
+ import { pagesApiRoutes } from "./routes/api/pages.js";
38
+ import { navItemsApiRoutes } from "./routes/api/nav-items.js";
39
+ import { collectionsApiRoutes } from "./routes/api/collections.js";
40
+ import { settingsApiRoutes } from "./routes/api/settings.js";
37
41
  import { uploadApiRoutes } from "./routes/api/upload.js";
38
42
  import { searchApiRoutes } from "./routes/api/search.js";
43
+ // Routes - Compose
44
+ import { composeRoutes } from "./routes/compose.js";
45
+
39
46
  // Routes - Feed
40
47
  import { rssRoutes } from "./routes/feed/rss.js";
41
48
  import { sitemapRoutes } from "./routes/feed/sitemap.js";
@@ -45,7 +52,7 @@ import { requireAuth } from "./middleware/auth.js";
45
52
  import { requireOnboarding } from "./middleware/onboarding.js";
46
53
 
47
54
  // Layouts for auth pages
48
- import { BaseLayout } from "./theme/layouts/index.js";
55
+ import { BaseLayout } from "./ui/layouts/BaseLayout.js";
49
56
  import { dsRedirect, dsToast } from "./lib/sse.js";
50
57
  import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
51
58
  import { createStorageDriver, type StorageDriver } from "./lib/storage.js";
@@ -56,6 +63,8 @@ export interface AppVariables {
56
63
  auth: Auth;
57
64
  config: JantConfig;
58
65
  themeStyle: string;
66
+ customCSS: string;
67
+ isAuthenticated: boolean;
59
68
  storage: StorageDriver | null;
60
69
  }
61
70
 
@@ -76,30 +85,12 @@ export type App = Hono<{ Bindings: Bindings; Variables: AppVariables }>;
76
85
  * import { createApp } from "@jant/core";
77
86
  *
78
87
  * export default createApp({
79
- * theme: { components: { PostPage: MyPostPage } },
88
+ * cssVariables: { "--card-radius": "0" },
80
89
  * });
81
90
  * ```
82
91
  */
83
92
  export function createApp(config: JantConfig = {}): App {
84
- // Merge with default threads theme
85
- const defaultTheme = threadsTheme();
86
- const resolvedConfig: JantConfig = {
87
- ...config,
88
- theme: {
89
- name: config.theme?.name ?? defaultTheme.name,
90
- components: {
91
- ...defaultTheme.components,
92
- ...config.theme?.components,
93
- },
94
- timelineMore: config.theme?.timelineMore ?? defaultTheme.timelineMore,
95
- cssVariables: {
96
- ...defaultTheme.cssVariables,
97
- ...config.theme?.cssVariables,
98
- },
99
- colorThemes: config.theme?.colorThemes ?? defaultTheme.colorThemes,
100
- feed: config.theme?.feed,
101
- },
102
- };
93
+ const resolvedConfig: JantConfig = { ...config };
103
94
 
104
95
  const app = new Hono<{ Bindings: Bindings; Variables: AppVariables }>();
105
96
 
@@ -133,18 +124,37 @@ export function createApp(config: JantConfig = {}): App {
133
124
  // Onboarding gate — redirect to /setup if not yet initialized
134
125
  app.use("*", requireOnboarding());
135
126
 
136
- // Theme middleware - resolve active color theme and build CSS
127
+ // Theme middleware - resolve active color theme, custom CSS, and auth state
137
128
  app.use("*", async (c, next) => {
138
- const themeId = await c.var.services.settings.get(SETTINGS_KEYS.THEME);
129
+ const [themeId, customCSS] = await Promise.all([
130
+ c.var.services.settings.get(SETTINGS_KEYS.THEME),
131
+ c.var.services.settings.get(SETTINGS_KEYS.CUSTOM_CSS),
132
+ ]);
139
133
  const themes = getAvailableThemes(resolvedConfig);
140
134
  const activeTheme = themeId
141
135
  ? themes.find((t) => t.id === themeId)
142
136
  : undefined;
143
137
  const themeStyle = buildThemeStyle(
144
138
  activeTheme,
145
- resolvedConfig.theme?.cssVariables,
139
+ resolvedConfig.cssVariables,
146
140
  );
147
141
  c.set("themeStyle", themeStyle);
142
+ c.set("customCSS", customCSS ?? "");
143
+
144
+ // Check auth state for data-authenticated attribute on <body>
145
+ let isAuthenticated = false;
146
+ if (c.var.auth) {
147
+ try {
148
+ const session = await c.var.auth.api.getSession({
149
+ headers: c.req.raw.headers,
150
+ });
151
+ isAuthenticated = !!session;
152
+ } catch {
153
+ // Not authenticated
154
+ }
155
+ }
156
+ c.set("isAuthenticated", isAuthenticated);
157
+
148
158
  await next();
149
159
  });
150
160
 
@@ -196,6 +206,10 @@ export function createApp(config: JantConfig = {}): App {
196
206
 
197
207
  // API Routes
198
208
  app.route("/api/posts", postsApiRoutes);
209
+ app.route("/api/pages", pagesApiRoutes);
210
+ app.route("/api/nav-items", navItemsApiRoutes);
211
+ app.route("/api/collections", collectionsApiRoutes);
212
+ app.route("/api/settings", settingsApiRoutes);
199
213
 
200
214
  // Setup page component
201
215
  const SetupContent: FC = () => {
@@ -338,6 +352,18 @@ export function createApp(config: JantConfig = {}): App {
338
352
 
339
353
  await c.var.services.settings.completeOnboarding();
340
354
 
355
+ // Seed default navigation items
356
+ await c.var.services.navItems.create({
357
+ type: "link",
358
+ label: "Featured",
359
+ url: "/featured",
360
+ });
361
+ await c.var.services.navItems.create({
362
+ type: "link",
363
+ label: "Collections",
364
+ url: "/collections",
365
+ });
366
+
341
367
  return dsRedirect("/signin?setup");
342
368
  } catch (err) {
343
369
  // eslint-disable-next-line no-console -- Error logging is intentional
@@ -718,7 +744,6 @@ export function createApp(config: JantConfig = {}): App {
718
744
  app.route("/dash/settings", dashSettingsRoutes);
719
745
  app.route("/dash/redirects", dashRedirectsRoutes);
720
746
  app.route("/dash/collections", dashCollectionsRoutes);
721
- app.route("/dash/navigation", dashNavigationRoutes);
722
747
  // API routes
723
748
  app.route("/api/upload", uploadApiRoutes);
724
749
  app.route("/api/search", searchApiRoutes);
@@ -751,6 +776,9 @@ export function createApp(config: JantConfig = {}): App {
751
776
  return new Response(object.body, { headers });
752
777
  });
753
778
 
779
+ // Compose route (auth enforced in route middleware)
780
+ app.route("/compose", composeRoutes);
781
+
754
782
  // Feed routes
755
783
  app.route("/feed", rssRoutes);
756
784
  app.route("/", sitemapRoutes);
@@ -758,6 +786,8 @@ export function createApp(config: JantConfig = {}): App {
758
786
  // Frontend routes
759
787
  app.route("/search", searchRoutes);
760
788
  app.route("/archive", archiveRoutes);
789
+ app.route("/featured", featuredRoutes);
790
+ app.route("/collections", collectionsPageRoutes);
761
791
  app.route("/c", collectionRoutes);
762
792
  app.route("/p", postRoutes);
763
793
  app.route("/", homeRoutes);
@@ -0,0 +1,5 @@
1
+ ALTER TABLE posts RENAME COLUMN slug TO path;
2
+ --> statement-breakpoint
3
+ DROP INDEX IF EXISTS posts_slug_unique;
4
+ --> statement-breakpoint
5
+ CREATE UNIQUE INDEX posts_path_unique ON posts (path);
@@ -43,6 +43,13 @@
43
43
  "when": 1771346168875,
44
44
  "tag": "0005_v2_schema_migration",
45
45
  "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "6",
50
+ "when": 1771746168876,
51
+ "tag": "0006_rename_slug_to_path",
52
+ "breakpoints": true
46
53
  }
47
54
  ]
48
55
  }
package/src/db/schema.ts CHANGED
@@ -22,7 +22,7 @@ export const posts = sqliteTable("posts", {
22
22
  .default("published"),
23
23
  featured: integer("featured").notNull().default(0),
24
24
  pinned: integer("pinned").notNull().default(0),
25
- slug: text("slug").unique(),
25
+ path: text("path").unique(),
26
26
  title: text("title"),
27
27
  url: text("url"),
28
28
  body: text("body"),