@jant/core 0.3.24 → 0.3.26

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 (277) hide show
  1. package/dist/app.js +101 -571
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +1 -1
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/index.js +3 -9
  8. package/dist/lib/avatar-upload.js +134 -0
  9. package/dist/lib/config.js +39 -0
  10. package/dist/lib/constants.js +10 -9
  11. package/dist/lib/favicon.js +102 -0
  12. package/dist/lib/image.js +13 -17
  13. package/dist/lib/media-helpers.js +2 -2
  14. package/dist/lib/nav-reorder.js +1 -1
  15. package/dist/lib/navigation.js +48 -3
  16. package/dist/lib/pagination.js +44 -0
  17. package/dist/lib/render.js +16 -11
  18. package/dist/lib/schemas.js +34 -3
  19. package/dist/lib/theme.js +4 -4
  20. package/dist/lib/timeline.js +24 -48
  21. package/dist/lib/timezones.js +388 -0
  22. package/dist/lib/view.js +3 -3
  23. package/dist/routes/api/collections.js +124 -0
  24. package/dist/routes/api/nav-items.js +104 -0
  25. package/dist/routes/api/pages.js +91 -0
  26. package/dist/routes/api/posts.js +3 -3
  27. package/dist/routes/api/search.js +2 -2
  28. package/dist/routes/api/settings.js +68 -0
  29. package/dist/routes/api/upload.js +3 -3
  30. package/dist/routes/auth/reset.js +221 -0
  31. package/dist/routes/auth/setup.js +194 -0
  32. package/dist/routes/auth/signin.js +176 -0
  33. package/dist/routes/compose.js +48 -0
  34. package/dist/routes/dash/collections.js +24 -416
  35. package/dist/routes/dash/index.js +1 -1
  36. package/dist/routes/dash/media.js +13 -393
  37. package/dist/routes/dash/pages.js +112 -86
  38. package/dist/routes/dash/posts.js +3 -5
  39. package/dist/routes/dash/redirects.js +20 -14
  40. package/dist/routes/dash/settings.js +213 -518
  41. package/dist/routes/feed/rss.js +4 -3
  42. package/dist/routes/feed/sitemap.js +5 -3
  43. package/dist/routes/pages/archive.js +3 -6
  44. package/dist/routes/pages/collection.js +3 -6
  45. package/dist/routes/pages/collections.js +28 -0
  46. package/dist/routes/pages/featured.js +36 -0
  47. package/dist/routes/pages/home.js +33 -49
  48. package/dist/routes/pages/latest.js +45 -0
  49. package/dist/routes/pages/page.js +29 -32
  50. package/dist/routes/pages/post.js +3 -6
  51. package/dist/routes/pages/search.js +3 -6
  52. package/dist/services/page.js +5 -1
  53. package/dist/services/post.js +45 -31
  54. package/dist/services/search.js +1 -1
  55. package/dist/types/bindings.js +3 -0
  56. package/dist/types/config.js +147 -0
  57. package/dist/types/constants.js +27 -0
  58. package/dist/types/entities.js +3 -0
  59. package/dist/types/operations.js +3 -0
  60. package/dist/types/props.js +3 -0
  61. package/dist/types/views.js +5 -0
  62. package/dist/types.js +8 -111
  63. package/dist/{theme → ui}/color-themes.js +33 -33
  64. package/dist/ui/compose/ComposeDialog.js +467 -0
  65. package/dist/ui/compose/ComposePrompt.js +55 -0
  66. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  67. package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
  68. package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
  69. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  70. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  71. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  72. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  73. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  74. package/dist/{theme/components → ui/dash}/index.js +3 -6
  75. package/dist/ui/dash/media/MediaListContent.js +166 -0
  76. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  77. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  78. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  79. package/dist/ui/dash/settings/AccountContent.js +209 -0
  80. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  81. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  82. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  83. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  84. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  85. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  86. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  87. package/dist/ui/feed/TimelineFeed.js +41 -0
  88. package/dist/ui/feed/TimelineItem.js +27 -0
  89. package/dist/ui/font-themes.js +36 -0
  90. package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
  91. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  92. package/dist/ui/layouts/SiteLayout.js +169 -0
  93. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  94. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  95. package/dist/ui/pages/CollectionsPage.js +76 -0
  96. package/dist/ui/pages/FeaturedPage.js +24 -0
  97. package/dist/ui/pages/HomePage.js +24 -0
  98. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  99. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  100. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  101. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  102. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  103. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  104. package/dist/ui/shared/index.js +5 -0
  105. package/package.json +1 -9
  106. package/src/__tests__/helpers/db.ts +3 -0
  107. package/src/app.tsx +131 -561
  108. package/src/client.ts +1 -0
  109. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  110. package/src/db/migrations/meta/_journal.json +7 -0
  111. package/src/db/schema.ts +1 -1
  112. package/src/i18n/locales/en.po +477 -261
  113. package/src/i18n/locales/en.ts +1 -1
  114. package/src/i18n/locales/zh-Hans.po +477 -261
  115. package/src/i18n/locales/zh-Hans.ts +1 -1
  116. package/src/i18n/locales/zh-Hant.po +477 -261
  117. package/src/i18n/locales/zh-Hant.ts +1 -1
  118. package/src/index.ts +7 -36
  119. package/src/lib/__tests__/config.test.ts +192 -0
  120. package/src/lib/__tests__/favicon.test.ts +151 -0
  121. package/src/lib/__tests__/image.test.ts +2 -6
  122. package/src/lib/__tests__/schemas.test.ts +60 -19
  123. package/src/lib/__tests__/timeline.test.ts +45 -81
  124. package/src/lib/__tests__/timezones.test.ts +61 -0
  125. package/src/lib/__tests__/view.test.ts +15 -9
  126. package/src/lib/avatar-upload.ts +165 -0
  127. package/src/lib/config.ts +47 -0
  128. package/src/lib/constants.ts +19 -10
  129. package/src/lib/favicon.ts +115 -0
  130. package/src/lib/image.ts +13 -21
  131. package/src/lib/media-helpers.ts +2 -2
  132. package/src/lib/nav-reorder.ts +1 -1
  133. package/src/lib/navigation.ts +73 -4
  134. package/src/lib/pagination.ts +50 -0
  135. package/src/lib/render.tsx +22 -15
  136. package/src/lib/schemas.ts +47 -6
  137. package/src/lib/theme.ts +5 -5
  138. package/src/lib/timeline.ts +28 -57
  139. package/src/lib/timezones.ts +325 -0
  140. package/src/lib/view.ts +3 -3
  141. package/src/preset.css +2 -1
  142. package/src/routes/__tests__/compose.test.ts +199 -0
  143. package/src/routes/api/__tests__/collections.test.ts +249 -0
  144. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  145. package/src/routes/api/__tests__/pages.test.ts +218 -0
  146. package/src/routes/api/__tests__/settings.test.ts +132 -0
  147. package/src/routes/api/collections.ts +143 -0
  148. package/src/routes/api/nav-items.ts +115 -0
  149. package/src/routes/api/pages.ts +101 -0
  150. package/src/routes/api/posts.ts +3 -3
  151. package/src/routes/api/search.ts +2 -2
  152. package/src/routes/api/settings.ts +91 -0
  153. package/src/routes/api/upload.ts +2 -3
  154. package/src/routes/auth/reset.tsx +239 -0
  155. package/src/routes/auth/setup.tsx +189 -0
  156. package/src/routes/auth/signin.tsx +163 -0
  157. package/src/routes/compose.ts +63 -0
  158. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  159. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  160. package/src/routes/dash/collections.tsx +18 -367
  161. package/src/routes/dash/index.tsx +1 -1
  162. package/src/routes/dash/media.tsx +13 -415
  163. package/src/routes/dash/pages.tsx +131 -98
  164. package/src/routes/dash/posts.tsx +3 -7
  165. package/src/routes/dash/redirects.tsx +22 -16
  166. package/src/routes/dash/settings.tsx +265 -478
  167. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  168. package/src/routes/feed/rss.ts +5 -3
  169. package/src/routes/feed/sitemap.ts +5 -3
  170. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  171. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  172. package/src/routes/pages/archive.tsx +2 -6
  173. package/src/routes/pages/collection.tsx +2 -6
  174. package/src/routes/pages/collections.tsx +36 -0
  175. package/src/routes/pages/featured.tsx +44 -0
  176. package/src/routes/pages/home.tsx +30 -53
  177. package/src/routes/pages/latest.tsx +59 -0
  178. package/src/routes/pages/page.tsx +28 -30
  179. package/src/routes/pages/post.tsx +2 -5
  180. package/src/routes/pages/search.tsx +2 -6
  181. package/src/services/__tests__/page.test.ts +106 -0
  182. package/src/services/__tests__/post.test.ts +114 -15
  183. package/src/services/page.ts +13 -1
  184. package/src/services/post.ts +58 -40
  185. package/src/services/search.ts +2 -2
  186. package/src/styles/components.css +0 -65
  187. package/src/styles/tokens.css +47 -0
  188. package/src/styles/ui.css +475 -0
  189. package/src/types/bindings.ts +30 -0
  190. package/src/types/config.ts +183 -0
  191. package/src/types/constants.ts +26 -0
  192. package/src/types/entities.ts +109 -0
  193. package/src/types/operations.ts +88 -0
  194. package/src/types/props.ts +115 -0
  195. package/src/types/views.ts +172 -0
  196. package/src/types.ts +8 -774
  197. package/src/ui/__tests__/font-themes.test.ts +34 -0
  198. package/src/{theme → ui}/color-themes.ts +34 -34
  199. package/src/ui/compose/ComposeDialog.tsx +414 -0
  200. package/src/ui/compose/ComposePrompt.tsx +55 -0
  201. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  202. package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
  203. package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
  204. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  205. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  206. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  207. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  208. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  209. package/src/ui/dash/index.ts +10 -0
  210. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  211. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  212. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  213. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  214. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  215. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  216. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  217. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  218. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  219. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  220. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  221. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  222. package/src/ui/feed/TimelineFeed.tsx +49 -0
  223. package/src/ui/feed/TimelineItem.tsx +45 -0
  224. package/src/ui/font-themes.ts +54 -0
  225. package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
  226. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  227. package/src/ui/layouts/SiteLayout.tsx +164 -0
  228. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  229. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  230. package/src/ui/pages/CollectionsPage.tsx +73 -0
  231. package/src/ui/pages/FeaturedPage.tsx +31 -0
  232. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  233. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  234. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  235. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  236. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  237. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  238. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  239. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  240. package/src/ui/shared/index.ts +12 -0
  241. package/bin/jant.js +0 -185
  242. package/dist/lib/theme-components.js +0 -46
  243. package/dist/routes/dash/navigation.js +0 -289
  244. package/dist/theme/index.js +0 -18
  245. package/dist/theme/layouts/index.js +0 -2
  246. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  247. package/dist/themes/threads/index.js +0 -81
  248. package/dist/themes/threads/pages/HomePage.js +0 -25
  249. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  250. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  251. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  252. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  253. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  254. package/src/lib/__tests__/theme-components.test.ts +0 -105
  255. package/src/lib/theme-components.ts +0 -65
  256. package/src/routes/dash/navigation.tsx +0 -317
  257. package/src/theme/components/index.ts +0 -23
  258. package/src/theme/index.ts +0 -22
  259. package/src/theme/layouts/index.ts +0 -7
  260. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  261. package/src/themes/threads/index.ts +0 -100
  262. package/src/themes/threads/style.css +0 -336
  263. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  264. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  265. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  266. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  267. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  268. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  269. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  270. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  271. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  272. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  273. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  274. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  275. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  276. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  277. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -2,12 +2,15 @@
2
2
  * Navigation Helper
3
3
  *
4
4
  * Provides shared data fetching for public page navigation.
5
- */ import { getSiteName } from "./config.js";
5
+ */ import { getSiteName, getHomeDefaultView, getSiteFooter } from "./config.js";
6
6
  import { toNavItemViews } from "./view.js";
7
+ import { getMediaUrl, getPublicUrlForProvider } from "./image.js";
8
+ import { render as renderMarkdown } from "./markdown.js";
7
9
  /**
8
10
  * Fetch navigation data for public pages.
9
11
  *
10
12
  * Returns NavItemView[] with pre-computed isActive/isExternal state.
13
+ * Also checks authentication status and loads collections for authenticated users.
11
14
  *
12
15
  * @param c - Hono context
13
16
  * @returns Navigation data for SiteLayout
@@ -24,11 +27,53 @@ import { toNavItemViews } from "./view.js";
24
27
  */ export async function getNavigationData(c) {
25
28
  const items = await c.var.services.navItems.list();
26
29
  const currentPath = new URL(c.req.url).pathname;
27
- const siteName = await getSiteName(c);
30
+ const [siteName, homeDefaultView, siteFooter] = await Promise.all([
31
+ getSiteName(c),
32
+ getHomeDefaultView(c),
33
+ getSiteFooter(c)
34
+ ]);
35
+ // Only include description if explicitly set (DB or env), not the default
36
+ const dbDescription = await c.var.services.settings.get("SITE_DESCRIPTION");
37
+ const envDescription = c.env.SITE_DESCRIPTION;
38
+ const siteDescription = dbDescription || (typeof envDescription === "string" ? envDescription : "");
39
+ // Resolve avatar URL from storage key
40
+ const avatarKey = await c.var.services.settings.get("SITE_AVATAR");
41
+ const showHeaderAvatar = await c.var.services.settings.get("SHOW_HEADER_AVATAR") === "true";
42
+ let siteAvatarUrl;
43
+ if (avatarKey) {
44
+ const publicUrl = getPublicUrlForProvider(c.env.STORAGE_DRIVER || "r2", c.env.R2_PUBLIC_URL, c.env.S3_PUBLIC_URL);
45
+ siteAvatarUrl = getMediaUrl(avatarKey, publicUrl);
46
+ }
47
+ // Render footer markdown
48
+ const siteFooterHtml = siteFooter ? renderMarkdown(siteFooter) : undefined;
28
49
  const links = toNavItemViews(items, currentPath);
50
+ // Check auth status for compose button
51
+ let isAuthenticated = false;
52
+ let collections = [];
53
+ if (c.var.auth) {
54
+ try {
55
+ const session = await c.var.auth.api.getSession({
56
+ headers: c.req.raw.headers
57
+ });
58
+ isAuthenticated = !!session?.user;
59
+ } catch {
60
+ // Not authenticated
61
+ }
62
+ }
63
+ // Only load collections when authenticated (for compose dialog)
64
+ if (isAuthenticated) {
65
+ collections = await c.var.services.collections.list();
66
+ }
29
67
  return {
30
68
  links,
31
69
  currentPath,
32
- siteName
70
+ siteName,
71
+ siteDescription,
72
+ isAuthenticated,
73
+ collections,
74
+ homeDefaultView,
75
+ siteAvatarUrl,
76
+ showHeaderAvatar: showHeaderAvatar && !!siteAvatarUrl,
77
+ siteFooterHtml
33
78
  };
34
79
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pagination Utilities
3
+ *
4
+ * Pure utility functions for page-based pagination.
5
+ */ /**
6
+ * Computes which page numbers to display in a numbered pagination control.
7
+ * Always includes: first page, last page, current page, and 1 page on each side of current.
8
+ * Gaps between non-consecutive pages are represented by 0 (ellipsis marker).
9
+ *
10
+ * @param currentPage - The current active page (1-indexed)
11
+ * @param totalPages - Total number of pages
12
+ * @returns Array of page numbers, with 0 representing ellipsis gaps
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * getPageNumbers(1, 5) // [1, 2, 3, 4, 5]
17
+ * getPageNumbers(1, 20) // [1, 2, 0, 20]
18
+ * getPageNumbers(10, 20) // [1, 0, 9, 10, 11, 0, 20]
19
+ * ```
20
+ */ export function getPageNumbers(currentPage, totalPages) {
21
+ if (totalPages <= 7) {
22
+ return Array.from({
23
+ length: totalPages
24
+ }, (_, i)=>i + 1);
25
+ }
26
+ const pages = new Set();
27
+ pages.add(1);
28
+ pages.add(totalPages);
29
+ pages.add(currentPage);
30
+ if (currentPage > 1) pages.add(currentPage - 1);
31
+ if (currentPage < totalPages) pages.add(currentPage + 1);
32
+ const sorted = [
33
+ ...pages
34
+ ].sort((a, b)=>a - b);
35
+ // Insert 0 for gaps
36
+ const result = [];
37
+ for(let i = 0; i < sorted.length; i++){
38
+ if (i > 0 && sorted[i] - sorted[i - 1] > 1) {
39
+ result.push(0); // ellipsis marker
40
+ }
41
+ result.push(sorted[i]);
42
+ }
43
+ return result;
44
+ }
@@ -3,17 +3,12 @@
3
3
  *
4
4
  * Provides a single entry point for rendering public pages with the
5
5
  * correct layout stack: BaseLayout > SiteLayout > content.
6
- *
7
- * BaseLayout is always the built-in implementation (handles Vite assets,
8
- * I18nProvider, toast). SiteLayout is resolved from theme components.
9
6
  */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
10
- import { BaseLayout } from "../theme/layouts/BaseLayout.js";
11
- import { ThreadsSiteLayout as DefaultSiteLayout } from "../themes/threads/ThreadsSiteLayout.js";
7
+ import { BaseLayout } from "../ui/layouts/BaseLayout.js";
8
+ import { SiteLayout } from "../ui/layouts/SiteLayout.js";
12
9
  /**
13
10
  * Render a public page with the standard layout stack.
14
11
  *
15
- * Always uses the built-in BaseLayout, resolves SiteLayout from theme config.
16
- *
17
12
  * @param c - Hono context
18
13
  * @param options - Page rendering options
19
14
  * @returns Hono HTML response
@@ -29,18 +24,28 @@ import { ThreadsSiteLayout as DefaultSiteLayout } from "../themes/threads/Thread
29
24
  * ```
30
25
  */ export function renderPublicPage(c, options) {
31
26
  const { title, description, navData, content } = options;
32
- const components = c.var.config?.theme?.components;
33
- const Layout = components?.SiteLayout ?? DefaultSiteLayout;
34
27
  const layoutProps = {
35
28
  siteName: navData.siteName,
29
+ siteDescription: navData.siteDescription,
36
30
  links: navData.links,
37
- currentPath: navData.currentPath
31
+ currentPath: navData.currentPath,
32
+ isAuthenticated: navData.isAuthenticated,
33
+ collections: navData.collections,
34
+ homeDefaultView: navData.homeDefaultView,
35
+ siteAvatarUrl: navData.siteAvatarUrl,
36
+ showHeaderAvatar: navData.showHeaderAvatar,
37
+ siteFooterHtml: navData.siteFooterHtml
38
38
  };
39
+ // Read favicon and noindex from context (set by theme middleware)
40
+ const faviconUrl = c.get("faviconUrl");
41
+ const noindex = c.get("noindex");
39
42
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
40
43
  title: title,
41
44
  description: description,
42
45
  c: c,
43
- children: /*#__PURE__*/ _jsx(Layout, {
46
+ faviconUrl: faviconUrl,
47
+ noindex: noindex,
48
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
44
49
  ...layoutProps,
45
50
  children: content
46
51
  })
@@ -31,12 +31,12 @@ import { FORMATS, STATUSES, SORT_ORDERS, NAV_ITEM_TYPES, MAX_MEDIA_ATTACHMENTS }
31
31
  ]);
32
32
  /**
33
33
  * Rating schema (1-5 integer)
34
- */ export const RatingSchema = z.coerce.number().int().min(1).max(5).optional().or(z.literal("").transform(()=>undefined));
34
+ */ export const RatingSchema = z.coerce.number().int().min(0).max(5).optional().or(z.literal("").transform(()=>undefined)).transform((v)=>v === 0 ? undefined : v);
35
35
  /**
36
36
  * API request body schema for creating a post
37
37
  */ export const CreatePostSchema = z.object({
38
38
  format: FormatSchema,
39
- slug: z.string().regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/).optional().or(z.literal("").transform(()=>undefined)),
39
+ path: z.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/).optional().or(z.literal("").transform(()=>undefined)),
40
40
  title: z.string().optional(),
41
41
  body: z.string().optional(),
42
42
  status: StatusSchema.optional(),
@@ -51,7 +51,7 @@ import { FORMATS, STATUSES, SORT_ORDERS, NAV_ITEM_TYPES, MAX_MEDIA_ATTACHMENTS }
51
51
  url: z.url().optional().or(z.literal("")),
52
52
  quoteText: z.string().optional(),
53
53
  rating: RatingSchema,
54
- collectionId: z.coerce.number().int().positive().optional().or(z.literal("").transform(()=>undefined)),
54
+ collectionId: z.coerce.number().int().min(0).optional().or(z.literal("").transform(()=>undefined)).transform((v)=>v === 0 ? undefined : v),
55
55
  replyToId: z.string().optional(),
56
56
  publishedAt: z.number().int().positive().optional(),
57
57
  mediaIds: z.array(z.string()).max(MAX_MEDIA_ATTACHMENTS).optional()
@@ -99,6 +99,37 @@ import { FORMATS, STATUSES, SORT_ORDERS, NAV_ITEM_TYPES, MAX_MEDIA_ATTACHMENTS }
99
99
  /**
100
100
  * API request body schema for updating a collection
101
101
  */ export const UpdateCollectionSchema = CreateCollectionSchema.partial();
102
+ // =============================================================================
103
+ // Auth Schemas
104
+ // =============================================================================
105
+ /**
106
+ * Setup form validation schema
107
+ */ export const SetupSchema = z.object({
108
+ name: z.string().min(1, "Name is required"),
109
+ email: z.string().email("Invalid email address"),
110
+ password: z.string().min(8, "Password must be at least 8 characters")
111
+ });
112
+ /**
113
+ * Sign-in form validation schema
114
+ */ export const SigninSchema = z.object({
115
+ email: z.string().email("Invalid email address"),
116
+ password: z.string().min(1, "Password is required")
117
+ });
118
+ /**
119
+ * Password reset form validation schema
120
+ */ export const ResetPasswordSchema = z.object({
121
+ password: z.string().min(8, "Password must be at least 8 characters"),
122
+ confirmPassword: z.string().min(1),
123
+ token: z.string().min(1)
124
+ }).refine((d)=>d.password === d.confirmPassword, {
125
+ message: "Passwords do not match",
126
+ path: [
127
+ "confirmPassword"
128
+ ]
129
+ });
130
+ // =============================================================================
131
+ // Form Data Helpers
132
+ // =============================================================================
102
133
  /**
103
134
  * Form data helper: safely parse a FormData value with a schema
104
135
  *
package/dist/lib/theme.js CHANGED
@@ -2,11 +2,11 @@
2
2
  * Theme Resolution Helpers
3
3
  *
4
4
  * Resolves the active color theme and builds CSS for injection into `<head>`.
5
- */ import { BUILTIN_COLOR_THEMES } from "../theme/color-themes.js";
5
+ */ import { BUILTIN_COLOR_THEMES } from "../ui/color-themes.js";
6
6
  /**
7
7
  * Get the list of available color themes.
8
8
  *
9
- * Returns `config.theme.colorThemes` if provided, otherwise the built-in list.
9
+ * Returns `config.colorThemes` if provided, otherwise the built-in list.
10
10
  *
11
11
  * @param config - The Jant configuration
12
12
  * @returns Array of available color themes
@@ -16,7 +16,7 @@
16
16
  * const themes = getAvailableThemes(c.var.config);
17
17
  * ```
18
18
  */ export function getAvailableThemes(config) {
19
- return config.theme?.colorThemes ?? BUILTIN_COLOR_THEMES;
19
+ return config.colorThemes ?? BUILTIN_COLOR_THEMES;
20
20
  }
21
21
  /**
22
22
  * Build a `<style>` CSS string from a color theme and optional cssVariables overlay.
@@ -25,7 +25,7 @@
25
25
  * BaseCoat defaults → selected theme → cssVariables
26
26
  *
27
27
  * @param theme - The active color theme (undefined = no theme overrides)
28
- * @param cssVariables - Extra CSS variable overrides from `createApp({ theme: { cssVariables } })`
28
+ * @param cssVariables - Extra CSS variable overrides from `createApp({ cssVariables })`
29
29
  * @returns CSS string to inject in `<head>`, or empty string if nothing to inject
30
30
  *
31
31
  * Uses `:root:root` and `:root.dark` selectors for higher specificity than
@@ -2,44 +2,51 @@
2
2
  * Timeline Data Assembly
3
3
  *
4
4
  * Shared helper for assembling timeline items with media and thread previews.
5
- * Used by both full-page rendering and load-more SSE responses.
5
+ * Used by page rendering with page-based pagination.
6
6
  */ import { buildMediaMap } from "./media-helpers.js";
7
7
  import { createMediaContext, toPostView, toPostViews } from "./view.js";
8
8
  const DEFAULT_PAGE_SIZE = 20;
9
9
  /**
10
10
  * Assembles a page of timeline items with media attachments and thread previews.
11
11
  *
12
- * Fetches posts, batch-loads media, identifies threads, and returns
13
- * render-ready `TimelineItemView[]` with pagination info.
12
+ * Fetches posts using offset-based pagination, batch-loads media, identifies
13
+ * threads, and returns render-ready `TimelineItemView[]` with page info.
14
14
  *
15
15
  * @param c - Hono context (provides services + env)
16
- * @param options - Optional cursor for pagination
16
+ * @param options - Optional page number (1-indexed, defaults to 1)
17
17
  * @returns Assembled timeline items with pagination info
18
18
  *
19
19
  * @example
20
20
  * ```ts
21
- * const { items, hasMore, nextCursor } = await assembleTimeline(c);
22
- * const { items, hasMore, nextCursor } = await assembleTimeline(c, { cursor: 42 });
21
+ * const { items, currentPage, totalPages } = await assembleTimeline(c);
22
+ * const { items, currentPage, totalPages } = await assembleTimeline(c, { page: 2 });
23
23
  * ```
24
24
  */ export async function assembleTimeline(c, options) {
25
25
  const pageSize = parseInt(c.env.PAGE_SIZE ?? String(DEFAULT_PAGE_SIZE), 10) || DEFAULT_PAGE_SIZE;
26
- // Fetch one extra to determine if there are more
26
+ const page = Math.max(1, options?.page ?? 1);
27
+ const offset = (page - 1) * pageSize;
28
+ // Get total count for pagination
29
+ const totalCount = await c.var.services.posts.count({
30
+ status: "published",
31
+ excludeReplies: true
32
+ });
33
+ const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
34
+ // Fetch posts for the current page
27
35
  const posts = await c.var.services.posts.list({
28
36
  status: "published",
29
37
  excludeReplies: true,
30
- limit: pageSize + 1,
31
- cursor: options?.cursor
38
+ limit: pageSize,
39
+ offset
32
40
  });
33
- const hasMore = posts.length > pageSize;
34
- const displayPosts = hasMore ? posts.slice(0, pageSize) : posts;
35
- if (displayPosts.length === 0) {
41
+ if (posts.length === 0) {
36
42
  return {
37
43
  items: [],
38
- hasMore: false
44
+ currentPage: page,
45
+ totalPages
39
46
  };
40
47
  }
41
48
  // Batch load media attachments
42
- const postIds = displayPosts.map((p)=>p.id);
49
+ const postIds = posts.map((p)=>p.id);
43
50
  const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
44
51
  const mediaCtx = createMediaContext(c);
45
52
  const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
@@ -57,7 +64,7 @@ const DEFAULT_PAGE_SIZE = 20;
57
64
  }
58
65
  const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl) : new Map();
59
66
  // Assemble timeline items with View Models
60
- const items = displayPosts.map((post)=>{
67
+ const items = posts.map((post)=>{
61
68
  const postView = toPostView({
62
69
  ...post,
63
70
  mediaAttachments: mediaMap.get(post.id) ?? []
@@ -80,40 +87,9 @@ const DEFAULT_PAGE_SIZE = 20;
80
87
  post: postView
81
88
  };
82
89
  });
83
- // Determine next cursor
84
- const lastPost = displayPosts[displayPosts.length - 1];
85
- const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
86
90
  return {
87
91
  items,
88
- hasMore,
89
- nextCursor
92
+ currentPage: page,
93
+ totalPages
90
94
  };
91
95
  }
92
- /**
93
- * Groups timeline items by their publication date (YYYY-MM-DD).
94
- *
95
- * @param items - Timeline items to group
96
- * @returns Array of date groups, each containing items published on the same day
97
- *
98
- * @example
99
- * ```ts
100
- * const groups = groupByDate(items);
101
- * // [{ dateKey: "2024-02-01", label: "Feb 1, 2024", items: [...] }, ...]
102
- * ```
103
- */ export function groupByDate(items) {
104
- const groups = [];
105
- let current = null;
106
- for (const item of items){
107
- const dateKey = item.post.publishedAt.slice(0, 10);
108
- if (!current || current.dateKey !== dateKey) {
109
- current = {
110
- dateKey,
111
- label: item.post.publishedAtFormatted,
112
- items: []
113
- };
114
- groups.push(current);
115
- }
116
- current.items.push(item);
117
- }
118
- return groups;
119
- }