@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,55 @@
1
+ /**
2
+ * Compose Prompt
3
+ *
4
+ * "What's new?" prompt bar at the top of the content area.
5
+ * Clicking it opens the compose dialog.
6
+ */
7
+
8
+ import type { FC } from "hono/jsx";
9
+ import { useLingui } from "@lingui/react/macro";
10
+
11
+ export const ComposePrompt: FC = () => {
12
+ const { t } = useLingui();
13
+
14
+ return (
15
+ <div class="compose-prompt">
16
+ <button
17
+ type="button"
18
+ class="compose-prompt-trigger"
19
+ onclick="document.getElementById('compose-dialog').showModal()"
20
+ >
21
+ <span class="compose-prompt-avatar">
22
+ <svg
23
+ xmlns="http://www.w3.org/2000/svg"
24
+ width="16"
25
+ height="16"
26
+ viewBox="0 0 24 24"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ stroke-width="2"
30
+ stroke-linecap="round"
31
+ stroke-linejoin="round"
32
+ >
33
+ <path d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 1 1 3.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
34
+ </svg>
35
+ </span>
36
+ <span class="compose-prompt-text">
37
+ {t({
38
+ message: "What's new?",
39
+ comment: "@context: Compose prompt placeholder text",
40
+ })}
41
+ </span>
42
+ </button>
43
+ <button
44
+ type="button"
45
+ class="compose-prompt-post-btn"
46
+ onclick="document.getElementById('compose-dialog').showModal()"
47
+ >
48
+ {t({
49
+ message: "Post",
50
+ comment: "@context: Compose prompt post button",
51
+ })}
52
+ </button>
53
+ </div>
54
+ );
55
+ };
@@ -2,18 +2,17 @@
2
2
  * Format Badge Component
3
3
  *
4
4
  * Displays a badge indicating the format of a post (note, link, quote).
5
- * Named TypeBadge for backward compatibility with theme overrides.
6
5
  */
7
6
 
8
7
  import type { FC } from "hono/jsx";
9
8
  import { useLingui } from "@lingui/react/macro";
10
9
  import type { Format } from "../../types.js";
11
10
 
12
- export interface TypeBadgeProps {
11
+ export interface FormatBadgeProps {
13
12
  type: Format;
14
13
  }
15
14
 
16
- export const TypeBadge: FC<TypeBadgeProps> = ({ type }) => {
15
+ export const FormatBadge: FC<FormatBadgeProps> = ({ type }) => {
17
16
  const { t } = useLingui();
18
17
 
19
18
  const labels: Record<Format, string> = {
@@ -41,7 +41,6 @@ export const PostForm: FC<PostFormProps> = ({
41
41
  body: post?.body ?? "",
42
42
  url: post?.url ?? "",
43
43
  quoteText: post?.quoteText ?? "",
44
- slug: post?.slug ?? "",
45
44
  status: post?.status ?? "published",
46
45
  featured: post?.featured === 1,
47
46
  pinned: post?.pinned === 1,
@@ -287,30 +286,6 @@ export const PostForm: FC<PostFormProps> = ({
287
286
  </div>
288
287
  )}
289
288
 
290
- {/* Custom slug (optional) */}
291
- <div class="field">
292
- <label class="label">
293
- {t({
294
- message: "Custom Slug (optional)",
295
- comment: "@context: Post form field",
296
- })}
297
- </label>
298
- <input
299
- type="text"
300
- data-bind="slug"
301
- class="input"
302
- placeholder="my-custom-url"
303
- pattern="[a-z0-9-]*"
304
- />
305
- <p class="text-xs text-muted-foreground mt-1">
306
- {t({
307
- message:
308
- "Custom URL path. Leave empty to use default /p/ID format.",
309
- comment: "@context: Slug help text",
310
- })}
311
- </p>
312
- </div>
313
-
314
289
  {/* Submit */}
315
290
  <div class="flex gap-2">
316
291
  <button type="submit" class="btn" data-attr-disabled="$_loading">
@@ -7,9 +7,9 @@ import { useLingui } from "@lingui/react/macro";
7
7
  import type { Post } from "../../types.js";
8
8
  import * as sqid from "../../lib/sqid.js";
9
9
  import * as time from "../../lib/time.js";
10
- import { VisibilityBadge } from "./VisibilityBadge.js";
11
- import { TypeBadge } from "./TypeBadge.js";
12
- import { EmptyState } from "./EmptyState.js";
10
+ import { StatusBadge } from "./StatusBadge.js";
11
+ import { FormatBadge } from "./FormatBadge.js";
12
+ import { EmptyState } from "../shared/EmptyState.js";
13
13
  import { ListItemRow } from "./ListItemRow.js";
14
14
  import { ActionButtons } from "./ActionButtons.js";
15
15
 
@@ -38,8 +38,8 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
38
38
  return (
39
39
  <div class="flex flex-col divide-y">
40
40
  {posts.map((post) => {
41
- const permalink = post.slug
42
- ? `/${post.slug}`
41
+ const permalink = post.path
42
+ ? `/${post.path}`
43
43
  : `/p/${sqid.encode(post.id)}`;
44
44
  return (
45
45
  <ListItemRow
@@ -67,8 +67,8 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
67
67
  }
68
68
  >
69
69
  <div class="flex items-center gap-2 mb-1">
70
- <TypeBadge type={post.format} />
71
- <VisibilityBadge
70
+ <FormatBadge type={post.format} />
71
+ <StatusBadge
72
72
  status={post.status}
73
73
  featured={post.featured === 1}
74
74
  pinned={post.pinned === 1}
@@ -2,20 +2,19 @@
2
2
  * Status Badge Component
3
3
  *
4
4
  * Displays badges for post status, featured, and pinned state.
5
- * Named VisibilityBadge for backward compatibility with theme overrides.
6
5
  */
7
6
 
8
7
  import type { FC } from "hono/jsx";
9
8
  import { useLingui } from "@lingui/react/macro";
10
9
  import type { Status } from "../../types.js";
11
10
 
12
- export interface VisibilityBadgeProps {
11
+ export interface StatusBadgeProps {
13
12
  status: Status;
14
13
  featured?: boolean;
15
14
  pinned?: boolean;
16
15
  }
17
16
 
18
- export const VisibilityBadge: FC<VisibilityBadgeProps> = ({
17
+ export const StatusBadge: FC<StatusBadgeProps> = ({
19
18
  status,
20
19
  featured,
21
20
  pinned,
@@ -0,0 +1,10 @@
1
+ export { ActionButtons, type ActionButtonsProps } from "./ActionButtons.js";
2
+ export { CrudPageHeader, type CrudPageHeaderProps } from "./CrudPageHeader.js";
3
+ export { DangerZone, type DangerZoneProps } from "./DangerZone.js";
4
+ export { EmptyState, type EmptyStateProps } from "../shared/EmptyState.js";
5
+ export { FormatBadge, type FormatBadgeProps } from "./FormatBadge.js";
6
+ export { ListItemRow, type ListItemRowProps } from "./ListItemRow.js";
7
+ export { PageForm, type PageFormProps } from "./PageForm.js";
8
+ export { PostForm, type PostFormProps } from "./PostForm.js";
9
+ export { PostList, type PostListProps } from "./PostList.js";
10
+ export { StatusBadge, type StatusBadgeProps } from "./StatusBadge.js";
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Threads Theme - Link Card
2
+ * Link Card
3
3
  *
4
4
  * Compact link preview box — date is shown at the feed level as a group header.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
- import type { TimelineCardProps } from "../../../types.js";
8
+ import type { TimelineCardProps } from "../../types.js";
9
9
 
10
10
  export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
11
11
  // Extract domain from URL for display
@@ -19,7 +19,11 @@ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
19
19
  }
20
20
 
21
21
  return (
22
- <article class={`h-entry${compact ? " threads-compact" : ""}`}>
22
+ <article
23
+ class={`h-entry${compact ? " feed-compact" : ""}`}
24
+ data-post
25
+ data-format="link"
26
+ >
23
27
  {domain && (
24
28
  <div class="text-xs text-muted-foreground mb-1 flex items-center gap-1">
25
29
  <svg
@@ -52,10 +56,11 @@ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
52
56
  {!compact && post.bodyHtml && (
53
57
  <div
54
58
  class="e-content prose text-muted-foreground"
59
+ data-post-body
55
60
  dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
56
61
  />
57
62
  )}
58
- <footer class="mt-2 text-xs text-muted-foreground">
63
+ <footer class="mt-2 text-xs text-muted-foreground" data-post-meta>
59
64
  <a href={post.permalink} class="hover:underline">
60
65
  <time class="dt-published" datetime={post.publishedAt}>
61
66
  {post.publishedAtFormatted}
@@ -1,20 +1,24 @@
1
1
  /**
2
- * Threads Theme - Note Card
2
+ * Note Card
3
3
  *
4
- * Without title: plain text note date is shown at the feed level as a group header.
4
+ * Without title: plain text note with full date in footer.
5
5
  * With title: article-style rendering with summary excerpt and "Read more" link.
6
6
  */
7
7
 
8
8
  import type { FC } from "hono/jsx";
9
- import type { TimelineCardProps } from "../../../types.js";
10
- import { MediaGallery } from "../../../theme/index.js";
9
+ import type { TimelineCardProps } from "../../types.js";
10
+ import { MediaGallery } from "../shared/MediaGallery.js";
11
11
 
12
12
  export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
13
13
  const isArticle = !!post.title;
14
14
  const displayHtml = isArticle ? post.summaryHtml : post.bodyHtml;
15
15
 
16
16
  return (
17
- <article class={`h-entry${compact ? " threads-compact" : ""}`}>
17
+ <article
18
+ class={`h-entry${compact ? " feed-compact" : ""}`}
19
+ data-post
20
+ data-format="note"
21
+ >
18
22
  {isArticle && (
19
23
  <h2
20
24
  class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
@@ -27,11 +31,12 @@ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
27
31
  {displayHtml && (
28
32
  <div
29
33
  class={`e-content prose ${compact ? "prose-sm" : isArticle ? "text-muted-foreground" : ""}`}
34
+ data-post-body
30
35
  dangerouslySetInnerHTML={{ __html: displayHtml }}
31
36
  />
32
37
  )}
33
38
  {!compact && post.media.length > 0 && (
34
- <div class="threads-media mt-3">
39
+ <div class="mt-3" data-post-media>
35
40
  <MediaGallery attachments={post.media} />
36
41
  </div>
37
42
  )}
@@ -43,13 +48,13 @@ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
43
48
  Read more →
44
49
  </a>
45
50
  )}
46
- <footer class="mt-2">
51
+ <footer class="mt-2" data-post-meta>
47
52
  <a
48
53
  href={post.permalink}
49
54
  class="u-url text-xs text-muted-foreground hover:underline"
50
55
  >
51
56
  <time class="dt-published" datetime={post.publishedAt}>
52
- {post.publishedAtRelative}
57
+ {post.publishedAtFormatted}
53
58
  </time>
54
59
  </a>
55
60
  </footer>
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Threads Theme - Quote Card
2
+ * Quote Card
3
3
  *
4
- * Left-border accent blockquote date is shown at the feed level as a group header.
4
+ * Left-border accent blockquote with full date in footer.
5
5
  *
6
- * v2 fields:
6
+ * Fields:
7
7
  * - quoteText: the quoted text
8
8
  * - title: attribution (who said it)
9
9
  * - url: source link
@@ -11,13 +11,17 @@
11
11
  */
12
12
 
13
13
  import type { FC } from "hono/jsx";
14
- import type { TimelineCardProps } from "../../../types.js";
14
+ import type { TimelineCardProps } from "../../types.js";
15
15
 
16
16
  export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
17
17
  return (
18
- <article class={`h-entry${compact ? " threads-compact" : ""}`}>
18
+ <article
19
+ class={`h-entry${compact ? " feed-compact" : ""}`}
20
+ data-post
21
+ data-format="quote"
22
+ >
19
23
  {post.quoteText && (
20
- <blockquote class="threads-quote">
24
+ <blockquote class="feed-quote">
21
25
  <div
22
26
  class={`e-content ${compact ? "text-sm" : "text-base"} leading-relaxed`}
23
27
  >
@@ -45,16 +49,17 @@ export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
45
49
  {!compact && post.bodyHtml && (
46
50
  <div
47
51
  class="mt-3 prose text-muted-foreground"
52
+ data-post-body
48
53
  dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
49
54
  />
50
55
  )}
51
- <footer class="mt-2">
56
+ <footer class="mt-2" data-post-meta>
52
57
  <a
53
58
  href={post.permalink}
54
59
  class="u-url text-xs text-muted-foreground hover:underline"
55
60
  >
56
61
  <time class="dt-published" datetime={post.publishedAt}>
57
- {post.publishedAtRelative}
62
+ {post.publishedAtFormatted}
58
63
  </time>
59
64
  </a>
60
65
  </footer>
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Threads Theme - Thread Preview
2
+ * Thread Preview
3
3
  *
4
4
  * Root post + vertical line connector + compact replies underneath.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
- import type { ThreadPreviewProps } from "../../../types.js";
9
+ import type { ThreadPreviewProps } from "../../types.js";
10
10
  import { TimelineItem } from "./TimelineItem.js";
11
11
  import { TimelineItemFromPost } from "./TimelineItem.js";
12
12
 
@@ -14,23 +14,22 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
14
14
  rootPost,
15
15
  previewReplies,
16
16
  totalReplyCount,
17
- theme,
18
17
  }) => {
19
18
  const { t } = useLingui();
20
19
  const remainingCount = totalReplyCount - previewReplies.length;
21
20
 
22
21
  return (
23
22
  <div>
24
- <TimelineItem item={{ post: rootPost }} theme={theme} />
23
+ <TimelineItem item={{ post: rootPost }} />
25
24
  {previewReplies.length > 0 && (
26
- <div class="threads-replies">
25
+ <div class="feed-replies">
27
26
  {previewReplies.map((reply) => (
28
- <div key={reply.id} class="threads-reply">
29
- <TimelineItemFromPost post={reply} compact theme={theme} />
27
+ <div key={reply.id} class="feed-reply">
28
+ <TimelineItemFromPost post={reply} compact />
30
29
  </div>
31
30
  ))}
32
31
  {remainingCount > 0 && (
33
- <div class="threads-reply">
32
+ <div class="feed-reply">
34
33
  <a
35
34
  href={rootPost.permalink}
36
35
  class="text-sm text-muted-foreground hover:text-foreground hover:underline"
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Timeline Feed
3
+ *
4
+ * Flat list of posts separated by simple dividers.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineFeedProps } from "../../types.js";
9
+ import { TimelineItem } from "./TimelineItem.js";
10
+ import { ThreadPreview } from "./ThreadPreview.js";
11
+ import { PagePagination } from "../shared/Pagination.js";
12
+
13
+ export const TimelineFeed: FC<TimelineFeedProps> = ({
14
+ items,
15
+ currentPage,
16
+ totalPages,
17
+ }) => {
18
+ return (
19
+ <div data-feed>
20
+ <div id="timeline-feed">
21
+ <div id="timeline-items" class="flex flex-col">
22
+ {items.map((item, i) => (
23
+ <div key={item.post.id}>
24
+ {i > 0 && <hr class="feed-divider" />}
25
+ {item.threadPreview ? (
26
+ <ThreadPreview
27
+ rootPost={item.post}
28
+ previewReplies={item.threadPreview.replies}
29
+ totalReplyCount={item.threadPreview.totalReplyCount}
30
+ />
31
+ ) : (
32
+ <TimelineItem item={item} />
33
+ )}
34
+ </div>
35
+ ))}
36
+ </div>
37
+ </div>
38
+ {currentPage !== undefined &&
39
+ totalPages !== undefined &&
40
+ totalPages > 1 && (
41
+ <PagePagination
42
+ baseUrl="/"
43
+ currentPage={currentPage}
44
+ totalPages={totalPages}
45
+ />
46
+ )}
47
+ </div>
48
+ );
49
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Timeline Item
3
+ *
4
+ * Dispatches to the correct card component based on post format.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type {
9
+ TimelineItemView,
10
+ TimelineCardProps,
11
+ PostView,
12
+ Format,
13
+ } from "../../types.js";
14
+ import { NoteCard } from "./NoteCard.js";
15
+ import { LinkCard } from "./LinkCard.js";
16
+ import { QuoteCard } from "./QuoteCard.js";
17
+
18
+ const CARD_MAP: Record<Format, FC<TimelineCardProps>> = {
19
+ note: NoteCard,
20
+ link: LinkCard,
21
+ quote: QuoteCard,
22
+ };
23
+
24
+ interface TimelineItemProps {
25
+ item: TimelineItemView;
26
+ compact?: boolean;
27
+ }
28
+
29
+ interface TimelineItemFromPostProps {
30
+ post: PostView;
31
+ compact?: boolean;
32
+ }
33
+
34
+ export const TimelineItem: FC<TimelineItemProps> = ({ item, compact }) => {
35
+ const Card = CARD_MAP[item.post.format];
36
+ return <Card post={item.post} compact={compact} />;
37
+ };
38
+
39
+ export const TimelineItemFromPost: FC<TimelineItemFromPostProps> = ({
40
+ post,
41
+ compact,
42
+ }) => {
43
+ const Card = CARD_MAP[post.format];
44
+ return <Card post={post} compact={compact} />;
45
+ };
@@ -42,6 +42,12 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
42
42
  // Read theme style from Hono context if available
43
43
  const themeStyle = c ? c.get("themeStyle") : undefined;
44
44
 
45
+ // Read custom CSS from Hono context if available
46
+ const customCSS = c ? c.get("customCSS") : undefined;
47
+
48
+ // Check authentication status for data attribute
49
+ const isAuthenticated = c ? c.get("isAuthenticated") : false;
50
+
45
51
  return (
46
52
  <html lang={resolvedLang}>
47
53
  <head>
@@ -52,9 +58,13 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
52
58
  <ViteClient />
53
59
  <Link href="/src/style.css" rel="stylesheet" />
54
60
  {themeStyle && <style>{themeStyle}</style>}
61
+ {customCSS && <style>{customCSS}</style>}
55
62
  <Script src="/src/client.ts" />
56
63
  </head>
57
- <body class="bg-background text-foreground antialiased">
64
+ <body
65
+ class="bg-background text-foreground antialiased"
66
+ {...(isAuthenticated ? { "data-authenticated": true } : {})}
67
+ >
58
68
  {content}
59
69
  <div id="toast-container" class="toast-container">
60
70
  {toast && (
@@ -126,16 +126,6 @@ function DashLayoutContent({
126
126
  comment: "@context: Dashboard navigation - URL redirects",
127
127
  })}
128
128
  </a>
129
- <a
130
- href="/dash/navigation"
131
- class={navClass("/dash/navigation", /^\/dash\/navigation/)}
132
- >
133
- {t({
134
- message: "Navigation",
135
- comment:
136
- "@context: Dashboard navigation - navigation links management",
137
- })}
138
- </a>
139
129
  <a
140
130
  href="/dash/settings"
141
131
  class={navClass("/dash/settings", /^\/dash\/settings/)}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Site Layout
3
+ *
4
+ * Vertical header: site name on top, custom nav links below, description under nav.
5
+ * Content area with browse filter tabs and compose prompt/dialog for authenticated users.
6
+ */
7
+
8
+ import type { FC, PropsWithChildren } from "hono/jsx";
9
+ import { useLingui } from "@lingui/react/macro";
10
+ import type { NavItemView, SiteLayoutProps } from "../../types.js";
11
+ import { ComposeDialog } from "../compose/ComposeDialog.js";
12
+ import { ComposePrompt } from "../compose/ComposePrompt.js";
13
+
14
+ function HeaderLink({ link }: { link: NavItemView }) {
15
+ return (
16
+ <a
17
+ href={link.url}
18
+ class={`site-header-link ${link.isActive ? "site-header-link-active" : ""}`}
19
+ {...(link.isExternal
20
+ ? { target: "_blank", rel: "noopener noreferrer" }
21
+ : {})}
22
+ >
23
+ {link.label}
24
+ </a>
25
+ );
26
+ }
27
+
28
+ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
29
+ siteName,
30
+ siteDescription,
31
+ links,
32
+ currentPath,
33
+ isAuthenticated,
34
+ collections,
35
+ children,
36
+ }) => {
37
+ const { t } = useLingui();
38
+
39
+ const browseLinks = [
40
+ {
41
+ href: "/",
42
+ label: t({
43
+ message: "Latest",
44
+ comment: "@context: Browse filter for latest posts",
45
+ }),
46
+ },
47
+ {
48
+ href: "/featured",
49
+ label: t({
50
+ message: "Featured",
51
+ comment: "@context: Browse filter for featured posts",
52
+ }),
53
+ },
54
+ // {
55
+ // href: "/collections",
56
+ // label: t({
57
+ // message: "Collections",
58
+ // comment: "@context: Browse filter for collections",
59
+ // }),
60
+ // },
61
+ // {
62
+ // href: "/archive",
63
+ // label: t({
64
+ // message: "Archive",
65
+ // comment: "@context: Browse filter for archive",
66
+ // }),
67
+ // },
68
+ ];
69
+
70
+ const searchLabel = t({
71
+ message: "Search",
72
+ comment: "@context: Search icon link in browse nav",
73
+ });
74
+
75
+ const isHomePage = currentPath === "/" || currentPath === "/featured";
76
+
77
+ return (
78
+ <div class="site-page">
79
+ <header class="site-header">
80
+ <div class="site-header-inner">
81
+ <div class="site-header-top site-header-top-bordered">
82
+ <a href="/" class="site-logo">
83
+ {siteName}
84
+ </a>
85
+ <div class="site-header-right">
86
+ {links.length > 0 && (
87
+ <nav class="site-header-nav">
88
+ {links.map((link) => (
89
+ <HeaderLink key={link.id} link={link} />
90
+ ))}
91
+ </nav>
92
+ )}
93
+ <a
94
+ href="/search"
95
+ class={`site-header-search ${currentPath === "/search" ? "site-header-search-active" : ""}`}
96
+ aria-label={searchLabel}
97
+ title={searchLabel}
98
+ >
99
+ <svg
100
+ xmlns="http://www.w3.org/2000/svg"
101
+ width="16"
102
+ height="16"
103
+ viewBox="0 0 24 24"
104
+ fill="none"
105
+ stroke="currentColor"
106
+ stroke-width="2"
107
+ stroke-linecap="round"
108
+ stroke-linejoin="round"
109
+ >
110
+ <circle cx="11" cy="11" r="8" />
111
+ <path d="m21 21-4.35-4.35" />
112
+ </svg>
113
+ </a>
114
+ </div>
115
+ </div>
116
+ {isHomePage && siteDescription && (
117
+ <p class="site-description">{siteDescription}</p>
118
+ )}
119
+ </div>
120
+ </header>
121
+
122
+ <main class="site-main">
123
+ <div class="site-container">
124
+ <div class="site-content">
125
+ {isHomePage && (
126
+ <nav class="site-browse-nav">
127
+ {browseLinks.map((link, i) => (
128
+ <>
129
+ {i > 0 && <span class="site-browse-sep">/</span>}
130
+ <a
131
+ key={link.href}
132
+ href={link.href}
133
+ class={`site-browse-link ${currentPath === link.href ? "site-browse-link-active" : ""}`}
134
+ >
135
+ {link.label}
136
+ </a>
137
+ </>
138
+ ))}
139
+ </nav>
140
+ )}
141
+ {isHomePage && isAuthenticated && <ComposePrompt />}
142
+ {children}
143
+ </div>
144
+ </div>
145
+ </main>
146
+
147
+ {isAuthenticated && <ComposeDialog collections={collections} />}
148
+ </div>
149
+ );
150
+ };