@jant/core 0.3.23 → 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 (169) hide show
  1. package/dist/app.js +4 -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 +3 -3
  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 +61 -72
  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/themes/threads/ThreadsSiteLayout.js +172 -0
  47. package/dist/themes/threads/index.js +81 -0
  48. package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
  49. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  50. package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
  51. package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
  52. package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
  53. package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
  54. package/dist/themes/threads/timeline/LinkCard.js +68 -0
  55. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  56. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  57. package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
  58. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  59. package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
  60. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  61. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  62. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  63. package/dist/types.js +24 -40
  64. package/package.json +2 -1
  65. package/src/__tests__/helpers/app.ts +4 -0
  66. package/src/__tests__/helpers/db.ts +51 -74
  67. package/src/app.tsx +4 -6
  68. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  69. package/src/db/migrations/meta/_journal.json +7 -0
  70. package/src/db/schema.ts +63 -46
  71. package/src/i18n/locales/en.po +216 -164
  72. package/src/i18n/locales/en.ts +1 -1
  73. package/src/i18n/locales/zh-Hans.po +216 -164
  74. package/src/i18n/locales/zh-Hans.ts +1 -1
  75. package/src/i18n/locales/zh-Hant.po +216 -164
  76. package/src/i18n/locales/zh-Hant.ts +1 -1
  77. package/src/index.ts +28 -12
  78. package/src/lib/__tests__/excerpt.test.ts +125 -0
  79. package/src/lib/__tests__/schemas.test.ts +166 -105
  80. package/src/lib/__tests__/theme-components.test.ts +4 -25
  81. package/src/lib/__tests__/time.test.ts +62 -0
  82. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  83. package/src/lib/__tests__/view.test.ts +199 -51
  84. package/src/lib/constants.ts +1 -4
  85. package/src/lib/excerpt.ts +87 -0
  86. package/src/lib/feed.ts +22 -7
  87. package/src/lib/navigation.ts +6 -7
  88. package/src/lib/render.tsx +1 -1
  89. package/src/lib/schemas.ts +118 -52
  90. package/src/lib/theme-components.ts +10 -13
  91. package/src/lib/time.ts +64 -0
  92. package/src/lib/timeline.ts +170 -0
  93. package/src/lib/view.ts +80 -82
  94. package/src/preset.css +45 -0
  95. package/src/routes/api/__tests__/posts.test.ts +50 -108
  96. package/src/routes/api/__tests__/search.test.ts +2 -3
  97. package/src/routes/api/posts.ts +30 -30
  98. package/src/routes/api/search.ts +4 -4
  99. package/src/routes/api/upload.ts +16 -6
  100. package/src/routes/dash/collections.tsx +18 -40
  101. package/src/routes/dash/index.tsx +2 -2
  102. package/src/routes/dash/navigation.tsx +27 -26
  103. package/src/routes/dash/pages.tsx +45 -60
  104. package/src/routes/dash/posts.tsx +44 -52
  105. package/src/routes/feed/rss.ts +2 -1
  106. package/src/routes/feed/sitemap.ts +14 -4
  107. package/src/routes/pages/archive.tsx +14 -10
  108. package/src/routes/pages/collection.tsx +17 -6
  109. package/src/routes/pages/home.tsx +56 -81
  110. package/src/routes/pages/page.tsx +64 -27
  111. package/src/routes/pages/post.tsx +5 -14
  112. package/src/routes/pages/search.tsx +2 -2
  113. package/src/services/__tests__/collection.test.ts +257 -158
  114. package/src/services/__tests__/media.test.ts +18 -18
  115. package/src/services/__tests__/navigation.test.ts +161 -87
  116. package/src/services/__tests__/post-timeline.test.ts +92 -88
  117. package/src/services/__tests__/post.test.ts +342 -206
  118. package/src/services/__tests__/search.test.ts +19 -25
  119. package/src/services/collection.ts +71 -113
  120. package/src/services/index.ts +9 -8
  121. package/src/services/navigation.ts +38 -71
  122. package/src/services/page.ts +124 -0
  123. package/src/services/post.ts +93 -103
  124. package/src/services/search.ts +38 -27
  125. package/src/theme/components/MediaGallery.tsx +27 -96
  126. package/src/theme/components/PageForm.tsx +21 -21
  127. package/src/theme/components/PostForm.tsx +122 -118
  128. package/src/theme/components/PostList.tsx +58 -49
  129. package/src/theme/components/ThreadView.tsx +6 -3
  130. package/src/theme/components/TypeBadge.tsx +9 -17
  131. package/src/theme/components/VisibilityBadge.tsx +40 -23
  132. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  133. package/src/themes/{minimal → threads}/index.ts +30 -13
  134. package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
  135. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  136. package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
  137. package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
  138. package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
  139. package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
  140. package/src/themes/threads/style.css +336 -0
  141. package/src/themes/threads/timeline/LinkCard.tsx +67 -0
  142. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  143. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  144. package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
  145. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  146. package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
  147. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  148. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  149. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  150. package/src/types.ts +242 -98
  151. package/dist/routes/api/timeline.js +0 -120
  152. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  153. package/dist/themes/minimal/index.js +0 -65
  154. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  155. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  156. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  157. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  158. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  159. package/dist/themes/minimal/timeline/QuoteCard.js +0 -48
  160. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  161. package/src/routes/api/timeline.tsx +0 -159
  162. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  163. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  164. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  165. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  166. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  167. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  168. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  169. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
@@ -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
  };
@@ -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
+ };
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Minimal Theme
2
+ * Threads Theme
3
3
  *
4
- * A content-first, borderless theme inspired by Tufte CSS and Manton.org.
5
- * Single-column layout with serif-friendly typography and generous whitespace.
4
+ * A clean, centered timeline theme inspired by Threads.net.
5
+ * Posts separated by thin dividers, no cards, with thread connector lines.
6
6
  *
7
7
  * This is the default theme for Jant.
8
8
  */
@@ -11,7 +11,7 @@ import type { JantTheme, ThemeComponents } from "../../types.js";
11
11
  import type { ColorTheme } from "../../theme/color-themes.js";
12
12
 
13
13
  // Layout
14
- import { SiteLayout } from "./MinimalSiteLayout.js";
14
+ import { ThreadsSiteLayout } from "./ThreadsSiteLayout.js";
15
15
 
16
16
  // Pages
17
17
  import { HomePage } from "./pages/HomePage.js";
@@ -23,12 +23,12 @@ import { CollectionPage } from "./pages/CollectionPage.js";
23
23
 
24
24
  // Timeline
25
25
  import { NoteCard } from "./timeline/NoteCard.js";
26
- import { ArticleCard } from "./timeline/ArticleCard.js";
27
26
  import { LinkCard } from "./timeline/LinkCard.js";
28
27
  import { QuoteCard } from "./timeline/QuoteCard.js";
29
- import { ImageCard } from "./timeline/ImageCard.js";
30
28
  import { ThreadPreview } from "./timeline/ThreadPreview.js";
31
29
  import { TimelineFeed } from "./timeline/TimelineFeed.js";
30
+ import { TimelineLoadMore } from "./timeline/TimelineLoadMore.js";
31
+ import { timelineMore } from "./timeline/timelineMore.js";
32
32
 
33
33
  export interface ThemeOptions {
34
34
  /** Override individual components */
@@ -40,7 +40,7 @@ export interface ThemeOptions {
40
40
  }
41
41
 
42
42
  /**
43
- * Create the minimal theme configuration.
43
+ * Create the threads theme configuration.
44
44
  *
45
45
  * @param options - Optional overrides for components, CSS variables, or color themes
46
46
  * @returns A JantTheme configuration object
@@ -48,18 +48,18 @@ export interface ThemeOptions {
48
48
  * @example
49
49
  * ```typescript
50
50
  * import { createApp } from "@jant/core";
51
- * import { minimalTheme } from "@jant/core";
51
+ * import { threadsTheme } from "@jant/core";
52
52
  *
53
53
  * export default createApp({
54
- * theme: minimalTheme(), // re-exported as minimalTheme from @jant/core
54
+ * theme: threadsTheme(),
55
55
  * });
56
56
  * ```
57
57
  */
58
58
  export function theme(options?: ThemeOptions): JantTheme {
59
59
  return {
60
- name: "minimal",
60
+ name: "threads",
61
61
  components: {
62
- SiteLayout,
62
+ SiteLayout: ThreadsSiteLayout,
63
63
  HomePage,
64
64
  PostPage,
65
65
  SinglePage,
@@ -67,17 +67,34 @@ export function theme(options?: ThemeOptions): JantTheme {
67
67
  SearchPage,
68
68
  CollectionPage,
69
69
  NoteCard,
70
- ArticleCard,
71
70
  LinkCard,
72
71
  QuoteCard,
73
- ImageCard,
74
72
  ThreadPreview,
75
73
  TimelineFeed,
74
+ TimelineLoadMore,
76
75
  ...options?.components,
77
76
  },
77
+ timelineMore,
78
78
  cssVariables: {
79
79
  ...options?.cssVariables,
80
80
  },
81
81
  colorThemes: options?.colorThemes,
82
82
  };
83
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";