@jant/core 0.3.22 → 0.3.24

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 (178) hide show
  1. package/dist/app.js +23 -5
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +5 -6
  7. package/dist/lib/constants.js +1 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/navigation.js +4 -5
  11. package/dist/lib/render.js +1 -1
  12. package/dist/lib/schemas.js +80 -38
  13. package/dist/lib/theme-components.js +8 -11
  14. package/dist/lib/time.js +56 -1
  15. package/dist/lib/timeline.js +119 -0
  16. package/dist/lib/view.js +62 -73
  17. package/dist/routes/api/posts.js +29 -35
  18. package/dist/routes/api/search.js +5 -6
  19. package/dist/routes/api/upload.js +13 -13
  20. package/dist/routes/dash/collections.js +22 -40
  21. package/dist/routes/dash/index.js +2 -2
  22. package/dist/routes/dash/navigation.js +25 -24
  23. package/dist/routes/dash/pages.js +42 -57
  24. package/dist/routes/dash/posts.js +27 -35
  25. package/dist/routes/feed/rss.js +2 -4
  26. package/dist/routes/feed/sitemap.js +10 -7
  27. package/dist/routes/pages/archive.js +12 -11
  28. package/dist/routes/pages/collection.js +11 -5
  29. package/dist/routes/pages/home.js +53 -61
  30. package/dist/routes/pages/page.js +60 -29
  31. package/dist/routes/pages/post.js +5 -12
  32. package/dist/routes/pages/search.js +3 -4
  33. package/dist/services/collection.js +52 -64
  34. package/dist/services/index.js +5 -3
  35. package/dist/services/navigation.js +29 -53
  36. package/dist/services/page.js +80 -0
  37. package/dist/services/post.js +68 -69
  38. package/dist/services/search.js +24 -18
  39. package/dist/theme/components/MediaGallery.js +19 -91
  40. package/dist/theme/components/PageForm.js +15 -15
  41. package/dist/theme/components/PostForm.js +136 -129
  42. package/dist/theme/components/PostList.js +13 -8
  43. package/dist/theme/components/ThreadView.js +3 -3
  44. package/dist/theme/components/TypeBadge.js +3 -14
  45. package/dist/theme/components/VisibilityBadge.js +33 -23
  46. package/dist/theme/components/index.js +0 -2
  47. package/dist/theme/index.js +10 -16
  48. package/dist/theme/layouts/index.js +0 -1
  49. package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
  50. package/dist/themes/threads/index.js +81 -0
  51. package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
  52. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  53. package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
  54. package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
  55. package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
  56. package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
  57. package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
  58. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  59. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  60. package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
  61. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  62. package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
  63. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  64. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  65. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  66. package/dist/types.js +24 -40
  67. package/package.json +2 -1
  68. package/src/__tests__/helpers/app.ts +4 -0
  69. package/src/__tests__/helpers/db.ts +51 -74
  70. package/src/app.tsx +27 -6
  71. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  72. package/src/db/migrations/meta/_journal.json +7 -0
  73. package/src/db/schema.ts +63 -46
  74. package/src/i18n/locales/en.po +216 -164
  75. package/src/i18n/locales/en.ts +1 -1
  76. package/src/i18n/locales/zh-Hans.po +216 -164
  77. package/src/i18n/locales/zh-Hans.ts +1 -1
  78. package/src/i18n/locales/zh-Hant.po +216 -164
  79. package/src/i18n/locales/zh-Hant.ts +1 -1
  80. package/src/index.ts +30 -15
  81. package/src/lib/__tests__/excerpt.test.ts +125 -0
  82. package/src/lib/__tests__/schemas.test.ts +166 -105
  83. package/src/lib/__tests__/theme-components.test.ts +4 -25
  84. package/src/lib/__tests__/time.test.ts +62 -0
  85. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  86. package/src/lib/__tests__/view.test.ts +217 -67
  87. package/src/lib/constants.ts +1 -4
  88. package/src/lib/excerpt.ts +87 -0
  89. package/src/lib/feed.ts +22 -7
  90. package/src/lib/navigation.ts +6 -7
  91. package/src/lib/render.tsx +1 -1
  92. package/src/lib/schemas.ts +118 -52
  93. package/src/lib/theme-components.ts +10 -13
  94. package/src/lib/time.ts +64 -0
  95. package/src/lib/timeline.ts +170 -0
  96. package/src/lib/view.ts +81 -83
  97. package/src/preset.css +45 -0
  98. package/src/routes/api/__tests__/posts.test.ts +50 -108
  99. package/src/routes/api/__tests__/search.test.ts +2 -3
  100. package/src/routes/api/posts.ts +30 -30
  101. package/src/routes/api/search.ts +4 -4
  102. package/src/routes/api/upload.ts +16 -6
  103. package/src/routes/dash/collections.tsx +18 -40
  104. package/src/routes/dash/index.tsx +2 -2
  105. package/src/routes/dash/navigation.tsx +27 -26
  106. package/src/routes/dash/pages.tsx +45 -60
  107. package/src/routes/dash/posts.tsx +44 -52
  108. package/src/routes/feed/rss.ts +2 -1
  109. package/src/routes/feed/sitemap.ts +14 -4
  110. package/src/routes/pages/archive.tsx +14 -10
  111. package/src/routes/pages/collection.tsx +17 -6
  112. package/src/routes/pages/home.tsx +56 -81
  113. package/src/routes/pages/page.tsx +64 -27
  114. package/src/routes/pages/post.tsx +5 -14
  115. package/src/routes/pages/search.tsx +2 -2
  116. package/src/services/__tests__/collection.test.ts +257 -158
  117. package/src/services/__tests__/media.test.ts +18 -18
  118. package/src/services/__tests__/navigation.test.ts +161 -87
  119. package/src/services/__tests__/post-timeline.test.ts +92 -88
  120. package/src/services/__tests__/post.test.ts +342 -206
  121. package/src/services/__tests__/search.test.ts +19 -25
  122. package/src/services/collection.ts +71 -113
  123. package/src/services/index.ts +9 -8
  124. package/src/services/navigation.ts +38 -71
  125. package/src/services/page.ts +124 -0
  126. package/src/services/post.ts +93 -103
  127. package/src/services/search.ts +38 -27
  128. package/src/styles/components.css +0 -54
  129. package/src/theme/components/MediaGallery.tsx +27 -96
  130. package/src/theme/components/PageForm.tsx +21 -21
  131. package/src/theme/components/PostForm.tsx +122 -118
  132. package/src/theme/components/PostList.tsx +58 -49
  133. package/src/theme/components/ThreadView.tsx +6 -3
  134. package/src/theme/components/TypeBadge.tsx +9 -17
  135. package/src/theme/components/VisibilityBadge.tsx +40 -23
  136. package/src/theme/components/index.ts +0 -13
  137. package/src/theme/index.ts +10 -16
  138. package/src/theme/layouts/index.ts +0 -1
  139. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  140. package/src/themes/threads/index.ts +100 -0
  141. package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
  142. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  143. package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
  144. package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
  145. package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
  146. package/src/themes/threads/pages/SinglePage.tsx +23 -0
  147. package/src/themes/threads/style.css +336 -0
  148. package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
  149. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  150. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  151. package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
  152. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  153. package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
  154. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  155. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  156. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  157. package/src/types.ts +242 -98
  158. package/dist/routes/api/timeline.js +0 -120
  159. package/dist/theme/components/timeline/ArticleCard.js +0 -46
  160. package/dist/theme/components/timeline/ImageCard.js +0 -83
  161. package/dist/theme/components/timeline/NoteCard.js +0 -34
  162. package/dist/theme/components/timeline/QuoteCard.js +0 -48
  163. package/dist/theme/components/timeline/TimelineFeed.js +0 -46
  164. package/dist/theme/components/timeline/index.js +0 -8
  165. package/dist/theme/layouts/SiteLayout.js +0 -131
  166. package/dist/theme/pages/CollectionPage.js +0 -63
  167. package/dist/theme/pages/index.js +0 -11
  168. package/src/routes/api/timeline.tsx +0 -159
  169. package/src/theme/components/timeline/ArticleCard.tsx +0 -45
  170. package/src/theme/components/timeline/ImageCard.tsx +0 -70
  171. package/src/theme/components/timeline/NoteCard.tsx +0 -34
  172. package/src/theme/components/timeline/QuoteCard.tsx +0 -48
  173. package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
  174. package/src/theme/components/timeline/index.ts +0 -8
  175. package/src/theme/layouts/SiteLayout.tsx +0 -132
  176. package/src/theme/pages/CollectionPage.tsx +0 -60
  177. package/src/theme/pages/SinglePage.tsx +0 -24
  178. package/src/theme/pages/index.ts +0 -13
@@ -37,56 +37,65 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
37
37
 
38
38
  return (
39
39
  <div class="flex flex-col divide-y">
40
- {posts.map((post) => (
41
- <ListItemRow
42
- key={post.id}
43
- actions={
44
- <ActionButtons
45
- editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
46
- editLabel={t({
47
- message: "Edit",
48
- comment: "@context: Button to edit post",
49
- })}
50
- viewHref={`/p/${sqid.encode(post.id)}`}
51
- viewLabel={t({
52
- message: "View",
53
- comment: "@context: Button to view post on public site",
54
- })}
55
- deleteAction={`/dash/posts/${sqid.encode(post.id)}/delete`}
56
- deleteConfirm={t({
57
- message:
58
- "Are you sure you want to delete this post? This cannot be undone.",
59
- comment:
60
- "@context: Confirmation dialog when deleting a post from the list",
61
- })}
62
- />
63
- }
64
- >
65
- <div class="flex items-center gap-2 mb-1">
66
- <TypeBadge type={post.type} />
67
- <VisibilityBadge visibility={post.visibility} />
68
- <span class="text-xs text-muted-foreground">
69
- {time.formatDate(post.publishedAt)}
70
- </span>
71
- </div>
72
- <a
73
- href={`/dash/posts/${sqid.encode(post.id)}`}
74
- class="font-medium hover:underline"
40
+ {posts.map((post) => {
41
+ const permalink = post.slug
42
+ ? `/${post.slug}`
43
+ : `/p/${sqid.encode(post.id)}`;
44
+ return (
45
+ <ListItemRow
46
+ key={post.id}
47
+ actions={
48
+ <ActionButtons
49
+ editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
50
+ editLabel={t({
51
+ message: "Edit",
52
+ comment: "@context: Button to edit post",
53
+ })}
54
+ viewHref={permalink}
55
+ viewLabel={t({
56
+ message: "View",
57
+ comment: "@context: Button to view post on public site",
58
+ })}
59
+ deleteAction={`/dash/posts/${sqid.encode(post.id)}/delete`}
60
+ deleteConfirm={t({
61
+ message:
62
+ "Are you sure you want to delete this post? This cannot be undone.",
63
+ comment:
64
+ "@context: Confirmation dialog when deleting a post from the list",
65
+ })}
66
+ />
67
+ }
75
68
  >
76
- {post.title ||
77
- post.content?.slice(0, 60) ||
78
- t({
79
- message: "Untitled",
80
- comment: "@context: Default title for untitled post",
81
- })}
82
- </a>
83
- {post.content && !post.title && (
84
- <p class="text-sm text-muted-foreground mt-1 line-clamp-2">
85
- {post.content.slice(0, 120)}
86
- </p>
87
- )}
88
- </ListItemRow>
89
- ))}
69
+ <div class="flex items-center gap-2 mb-1">
70
+ <TypeBadge type={post.format} />
71
+ <VisibilityBadge
72
+ status={post.status}
73
+ featured={post.featured === 1}
74
+ pinned={post.pinned === 1}
75
+ />
76
+ <span class="text-xs text-muted-foreground">
77
+ {time.formatDate(post.publishedAt)}
78
+ </span>
79
+ </div>
80
+ <a
81
+ href={`/dash/posts/${sqid.encode(post.id)}`}
82
+ class="font-medium hover:underline"
83
+ >
84
+ {post.title ||
85
+ post.body?.slice(0, 60) ||
86
+ t({
87
+ message: "Untitled",
88
+ comment: "@context: Default title for untitled post",
89
+ })}
90
+ </a>
91
+ {post.body && !post.title && (
92
+ <p class="text-sm text-muted-foreground mt-1 line-clamp-2">
93
+ {post.body.slice(0, 120)}
94
+ </p>
95
+ )}
96
+ </ListItemRow>
97
+ );
98
+ })}
90
99
  </div>
91
100
  );
92
101
  };
@@ -34,7 +34,10 @@ const ThreadPost: FC<{
34
34
  >
35
35
  {post.title && (
36
36
  <h2 class="p-name text-lg font-medium mb-2">
37
- <a href={`/p/${sqid.encode(post.id)}`} class="u-url hover:underline">
37
+ <a
38
+ href={`${post.slug ? `/${post.slug}` : `/p/${sqid.encode(post.id)}`}`}
39
+ class="u-url hover:underline"
40
+ >
38
41
  {post.title}
39
42
  </a>
40
43
  </h2>
@@ -42,7 +45,7 @@ const ThreadPost: FC<{
42
45
 
43
46
  <div
44
47
  class="e-content prose prose-sm"
45
- dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
48
+ dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
46
49
  />
47
50
 
48
51
  <footer class="mt-3 flex items-center gap-3 text-sm text-muted-foreground">
@@ -62,7 +65,7 @@ const ThreadPost: FC<{
62
65
  )}
63
66
  {!isCurrent && (
64
67
  <a
65
- href={`/p/${sqid.encode(post.id)}`}
68
+ href={`${post.slug ? `/${post.slug}` : `/p/${sqid.encode(post.id)}`}`}
66
69
  class="text-xs hover:underline"
67
70
  >
68
71
  {t({
@@ -1,36 +1,28 @@
1
1
  /**
2
- * Type Badge Component
2
+ * Format Badge Component
3
3
  *
4
- * Displays a badge indicating the type of a post (note, article, link, etc.)
4
+ * Displays a badge indicating the format of a post (note, link, quote).
5
+ * Named TypeBadge for backward compatibility with theme overrides.
5
6
  */
6
7
 
7
8
  import type { FC } from "hono/jsx";
8
9
  import { useLingui } from "@lingui/react/macro";
9
- import type { PostType } from "../../types.js";
10
+ import type { Format } from "../../types.js";
10
11
 
11
12
  export interface TypeBadgeProps {
12
- type: PostType;
13
+ type: Format;
13
14
  }
14
15
 
15
16
  export const TypeBadge: FC<TypeBadgeProps> = ({ type }) => {
16
17
  const { t } = useLingui();
17
18
 
18
- const labels: Record<PostType, string> = {
19
- note: t({ message: "Note", comment: "@context: Post type badge - note" }),
20
- article: t({
21
- message: "Article",
22
- comment: "@context: Post type badge - article",
23
- }),
24
- link: t({ message: "Link", comment: "@context: Post type badge - link" }),
19
+ const labels: Record<Format, string> = {
20
+ note: t({ message: "Note", comment: "@context: Post format badge - note" }),
21
+ link: t({ message: "Link", comment: "@context: Post format badge - link" }),
25
22
  quote: t({
26
23
  message: "Quote",
27
- comment: "@context: Post type badge - quote",
28
- }),
29
- image: t({
30
- message: "Image",
31
- comment: "@context: Post type badge - image",
24
+ comment: "@context: Post format badge - quote",
32
25
  }),
33
- page: t({ message: "Page", comment: "@context: Post type badge - page" }),
34
26
  };
35
27
 
36
28
  return <span class="badge-outline">{labels[type]}</span>;
@@ -1,45 +1,62 @@
1
1
  /**
2
- * Visibility Badge Component
2
+ * Status Badge Component
3
3
  *
4
- * Displays a badge indicating the visibility level of a post
4
+ * Displays badges for post status, featured, and pinned state.
5
+ * Named VisibilityBadge for backward compatibility with theme overrides.
5
6
  */
6
7
 
7
8
  import type { FC } from "hono/jsx";
8
9
  import { useLingui } from "@lingui/react/macro";
9
- import type { Visibility } from "../../types.js";
10
+ import type { Status } from "../../types.js";
10
11
 
11
12
  export interface VisibilityBadgeProps {
12
- visibility: Visibility;
13
+ status: Status;
14
+ featured?: boolean;
15
+ pinned?: boolean;
13
16
  }
14
17
 
15
- export const VisibilityBadge: FC<VisibilityBadgeProps> = ({ visibility }) => {
18
+ export const VisibilityBadge: FC<VisibilityBadgeProps> = ({
19
+ status,
20
+ featured,
21
+ pinned,
22
+ }) => {
16
23
  const { t } = useLingui();
17
24
 
18
- const variants: Record<Visibility, string> = {
19
- featured: "badge-primary",
20
- quiet: "badge-secondary",
21
- unlisted: "badge-outline",
25
+ const statusVariants: Record<Status, string> = {
26
+ published: "badge-secondary",
22
27
  draft: "badge-outline",
23
28
  };
24
29
 
25
- const labels: Record<Visibility, string> = {
26
- featured: t({
27
- message: "Featured",
28
- comment: "@context: Post visibility badge - featured",
29
- }),
30
- quiet: t({
31
- message: "Quiet",
32
- comment: "@context: Post visibility badge - normal",
33
- }),
34
- unlisted: t({
35
- message: "Unlisted",
36
- comment: "@context: Post visibility badge - unlisted",
30
+ const statusLabels: Record<Status, string> = {
31
+ published: t({
32
+ message: "Published",
33
+ comment: "@context: Post status badge - published",
37
34
  }),
38
35
  draft: t({
39
36
  message: "Draft",
40
- comment: "@context: Post visibility badge - draft",
37
+ comment: "@context: Post status badge - draft",
41
38
  }),
42
39
  };
43
40
 
44
- return <span class={variants[visibility]}>{labels[visibility]}</span>;
41
+ return (
42
+ <span class="flex items-center gap-1">
43
+ <span class={statusVariants[status]}>{statusLabels[status]}</span>
44
+ {featured && (
45
+ <span class="badge-primary">
46
+ {t({
47
+ message: "Featured",
48
+ comment: "@context: Post badge - featured",
49
+ })}
50
+ </span>
51
+ )}
52
+ {pinned && (
53
+ <span class="badge-outline">
54
+ {t({
55
+ message: "Pinned",
56
+ comment: "@context: Post badge - pinned",
57
+ })}
58
+ </span>
59
+ )}
60
+ </span>
61
+ );
45
62
  };
@@ -21,16 +21,3 @@ export {
21
21
  VisibilityBadge,
22
22
  type VisibilityBadgeProps,
23
23
  } from "./VisibilityBadge.js";
24
-
25
- // Timeline components
26
- export {
27
- NoteCard,
28
- ArticleCard,
29
- LinkCard,
30
- QuoteCard,
31
- ImageCard,
32
- ThreadPreview,
33
- TimelineItem,
34
- TimelineItemFromPost,
35
- TimelineFeed,
36
- } from "./timeline/index.js";
@@ -1,28 +1,22 @@
1
1
  /**
2
- * Jant Theme Components
2
+ * Jant Theme - Shared Infrastructure
3
3
  *
4
- * These components can be imported for wrapping/extending:
4
+ * Exports shared layouts, components, and color themes used by all themes.
5
+ * Individual theme packages (minimal, card, etc.) import from here.
5
6
  *
6
7
  * @example
7
8
  * ```typescript
8
- * import { PostPage } from "@jant/core/theme";
9
- * import type { PostPageProps } from "@jant/core";
10
- *
11
- * export function MyPostPage(props: PostPageProps) {
12
- * return (
13
- * <div class="my-wrapper">
14
- * <PostPage {...props} />
15
- * </div>
16
- * );
17
- * }
9
+ * // In a theme package:
10
+ * import { MediaGallery, Pagination } from "@jant/core/theme";
11
+ * import type { ColorTheme } from "@jant/core/theme";
18
12
  * ```
19
13
  */
20
14
 
21
- // Layout components
15
+ // Layout components (BaseLayout, DashLayout)
22
16
  export * from "./layouts/index.js";
23
17
 
24
- // UI components
18
+ // Shared UI components (MediaGallery, Pagination, EmptyState, etc.)
25
19
  export * from "./components/index.js";
26
20
 
27
- // Page components
28
- export * from "./pages/index.js";
21
+ // Color themes
22
+ export * from "./color-themes.js";
@@ -4,5 +4,4 @@ export {
4
4
  type ToastProps,
5
5
  } from "./BaseLayout.js";
6
6
  export { DashLayout, type DashLayoutProps } from "./DashLayout.js";
7
- export { SiteLayout } from "./SiteLayout.js";
8
7
  export type { SiteLayoutProps } from "../../types.js";
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Threads Theme - Site Layout
3
+ *
4
+ * Left icon sidebar (76px) on desktop, bottom tab bar (60px) on mobile.
5
+ * Gray page background (#fafafa) with white rounded content container.
6
+ * All dimensions match threads.com's --barcelona-* design tokens.
7
+ */
8
+
9
+ import type { FC, PropsWithChildren } from "hono/jsx";
10
+ import type { NavItemView, SiteLayoutProps } from "../../types.js";
11
+
12
+ /** Map known URL paths to SVG icons. Size 26x26 matching Threads' nav icons. */
13
+ function NavIcon({ url, isActive }: { url: string; isActive: boolean }) {
14
+ const stroke = "currentColor";
15
+ const sw = isActive ? "2.25" : "1.75";
16
+ const cls = "size-[26px]";
17
+
18
+ // Home
19
+ if (url === "/") {
20
+ return (
21
+ <svg
22
+ class={cls}
23
+ fill="none"
24
+ viewBox="0 0 24 24"
25
+ stroke-width={sw}
26
+ stroke={stroke}
27
+ >
28
+ <path
29
+ stroke-linecap="round"
30
+ stroke-linejoin="round"
31
+ d="m2.25 12 8.954-8.955a1.126 1.126 0 0 1 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
32
+ />
33
+ </svg>
34
+ );
35
+ }
36
+
37
+ // Search
38
+ if (url === "/search") {
39
+ return (
40
+ <svg
41
+ class={cls}
42
+ fill="none"
43
+ viewBox="0 0 24 24"
44
+ stroke-width={sw}
45
+ stroke={stroke}
46
+ >
47
+ <path
48
+ stroke-linecap="round"
49
+ stroke-linejoin="round"
50
+ d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
51
+ />
52
+ </svg>
53
+ );
54
+ }
55
+
56
+ // Archive
57
+ if (url === "/archive") {
58
+ return (
59
+ <svg
60
+ class={cls}
61
+ fill="none"
62
+ viewBox="0 0 24 24"
63
+ stroke-width={sw}
64
+ stroke={stroke}
65
+ >
66
+ <path
67
+ stroke-linecap="round"
68
+ stroke-linejoin="round"
69
+ d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"
70
+ />
71
+ </svg>
72
+ );
73
+ }
74
+
75
+ // RSS — common for /feed, /rss, /atom
76
+ if (url.match(/\/(feed|rss|atom)/)) {
77
+ return (
78
+ <svg
79
+ class={cls}
80
+ fill="none"
81
+ viewBox="0 0 24 24"
82
+ stroke-width={sw}
83
+ stroke={stroke}
84
+ >
85
+ <path
86
+ stroke-linecap="round"
87
+ stroke-linejoin="round"
88
+ d="M12.75 19.5v-.75a7.5 7.5 0 0 0-7.5-7.5H4.5m0-6.75h.75c7.87 0 14.25 6.38 14.25 14.25v.75M4.5 19.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
89
+ />
90
+ </svg>
91
+ );
92
+ }
93
+
94
+ // External link
95
+ if (url.startsWith("http")) {
96
+ return (
97
+ <svg
98
+ class={cls}
99
+ fill="none"
100
+ viewBox="0 0 24 24"
101
+ stroke-width={sw}
102
+ stroke={stroke}
103
+ >
104
+ <path
105
+ stroke-linecap="round"
106
+ stroke-linejoin="round"
107
+ d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
108
+ />
109
+ </svg>
110
+ );
111
+ }
112
+
113
+ // Default: generic page icon
114
+ return (
115
+ <svg
116
+ class={cls}
117
+ fill="none"
118
+ viewBox="0 0 24 24"
119
+ stroke-width={sw}
120
+ stroke={stroke}
121
+ >
122
+ <path
123
+ stroke-linecap="round"
124
+ stroke-linejoin="round"
125
+ d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
126
+ />
127
+ </svg>
128
+ );
129
+ }
130
+
131
+ function SidebarLink({ link }: { link: NavItemView }) {
132
+ return (
133
+ <a
134
+ href={link.url}
135
+ class={`threads-sidebar-link ${link.isActive ? "threads-sidebar-link-active" : ""}`}
136
+ title={link.label}
137
+ {...(link.isExternal
138
+ ? { target: "_blank", rel: "noopener noreferrer" }
139
+ : {})}
140
+ >
141
+ <NavIcon url={link.url} isActive={link.isActive} />
142
+ </a>
143
+ );
144
+ }
145
+
146
+ function MobileTabLink({ link }: { link: NavItemView }) {
147
+ return (
148
+ <a
149
+ href={link.url}
150
+ class={`threads-mobile-tab ${link.isActive ? "threads-mobile-tab-active" : ""}`}
151
+ {...(link.isExternal
152
+ ? { target: "_blank", rel: "noopener noreferrer" }
153
+ : {})}
154
+ >
155
+ <NavIcon url={link.url} isActive={link.isActive} />
156
+ </a>
157
+ );
158
+ }
159
+
160
+ export const ThreadsSiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
161
+ siteName,
162
+ links,
163
+ children,
164
+ }) => {
165
+ return (
166
+ <div class="threads-page">
167
+ {/* Desktop: left icon sidebar — no border, on gray background */}
168
+ <aside class="threads-sidebar">
169
+ <a href="/" class="threads-logo" title={siteName}>
170
+ <span class="text-2xl font-black leading-none">@</span>
171
+ </a>
172
+ <nav class="flex flex-1 flex-col items-center gap-1">
173
+ {links.map((link) => (
174
+ <SidebarLink key={link.id} link={link} />
175
+ ))}
176
+ </nav>
177
+ </aside>
178
+
179
+ {/* Main content — white rounded container on gray background */}
180
+ <main class="threads-main">
181
+ <div class="threads-container">
182
+ <div class="threads-content">{children}</div>
183
+ </div>
184
+ </main>
185
+
186
+ {/* Mobile: bottom tab bar */}
187
+ <nav class="threads-mobile-tabs">
188
+ {links.map((link) => (
189
+ <MobileTabLink key={link.id} link={link} />
190
+ ))}
191
+ </nav>
192
+ </div>
193
+ );
194
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Threads Theme
3
+ *
4
+ * A clean, centered timeline theme inspired by Threads.net.
5
+ * Posts separated by thin dividers, no cards, with thread connector lines.
6
+ *
7
+ * This is the default theme for Jant.
8
+ */
9
+
10
+ import type { JantTheme, ThemeComponents } from "../../types.js";
11
+ import type { ColorTheme } from "../../theme/color-themes.js";
12
+
13
+ // Layout
14
+ import { ThreadsSiteLayout } from "./ThreadsSiteLayout.js";
15
+
16
+ // Pages
17
+ import { HomePage } from "./pages/HomePage.js";
18
+ import { PostPage } from "./pages/PostPage.js";
19
+ import { SinglePage } from "./pages/SinglePage.js";
20
+ import { ArchivePage } from "./pages/ArchivePage.js";
21
+ import { SearchPage } from "./pages/SearchPage.js";
22
+ import { CollectionPage } from "./pages/CollectionPage.js";
23
+
24
+ // Timeline
25
+ import { NoteCard } from "./timeline/NoteCard.js";
26
+ import { LinkCard } from "./timeline/LinkCard.js";
27
+ import { QuoteCard } from "./timeline/QuoteCard.js";
28
+ import { ThreadPreview } from "./timeline/ThreadPreview.js";
29
+ import { TimelineFeed } from "./timeline/TimelineFeed.js";
30
+ import { TimelineLoadMore } from "./timeline/TimelineLoadMore.js";
31
+ import { timelineMore } from "./timeline/timelineMore.js";
32
+
33
+ export interface ThemeOptions {
34
+ /** Override individual components */
35
+ components?: Partial<ThemeComponents>;
36
+ /** CSS variable overrides */
37
+ cssVariables?: Record<string, string>;
38
+ /** Custom color themes */
39
+ colorThemes?: ColorTheme[];
40
+ }
41
+
42
+ /**
43
+ * Create the threads theme configuration.
44
+ *
45
+ * @param options - Optional overrides for components, CSS variables, or color themes
46
+ * @returns A JantTheme configuration object
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * import { createApp } from "@jant/core";
51
+ * import { threadsTheme } from "@jant/core";
52
+ *
53
+ * export default createApp({
54
+ * theme: threadsTheme(),
55
+ * });
56
+ * ```
57
+ */
58
+ export function theme(options?: ThemeOptions): JantTheme {
59
+ return {
60
+ name: "threads",
61
+ components: {
62
+ SiteLayout: ThreadsSiteLayout,
63
+ HomePage,
64
+ PostPage,
65
+ SinglePage,
66
+ ArchivePage,
67
+ SearchPage,
68
+ CollectionPage,
69
+ NoteCard,
70
+ LinkCard,
71
+ QuoteCard,
72
+ ThreadPreview,
73
+ TimelineFeed,
74
+ TimelineLoadMore,
75
+ ...options?.components,
76
+ },
77
+ timelineMore,
78
+ cssVariables: {
79
+ ...options?.cssVariables,
80
+ },
81
+ colorThemes: options?.colorThemes,
82
+ };
83
+ }
84
+
85
+ // Re-export individual components for wrapping/extending
86
+ export { ThreadsSiteLayout } from "./ThreadsSiteLayout.js";
87
+ export { HomePage } from "./pages/HomePage.js";
88
+ export { PostPage } from "./pages/PostPage.js";
89
+ export { SinglePage } from "./pages/SinglePage.js";
90
+ export { ArchivePage } from "./pages/ArchivePage.js";
91
+ export { SearchPage } from "./pages/SearchPage.js";
92
+ export { CollectionPage } from "./pages/CollectionPage.js";
93
+ export { NoteCard } from "./timeline/NoteCard.js";
94
+ export { LinkCard } from "./timeline/LinkCard.js";
95
+ export { QuoteCard } from "./timeline/QuoteCard.js";
96
+ export { ThreadPreview } from "./timeline/ThreadPreview.js";
97
+ export { TimelineFeed } from "./timeline/TimelineFeed.js";
98
+ export { TimelineLoadMore } from "./timeline/TimelineLoadMore.js";
99
+ export { TimelineItem, TimelineItemFromPost } from "./timeline/TimelineItem.js";
100
+ export { timelineMore } from "./timeline/timelineMore.js";