@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.
- package/dist/app.js +23 -5
- package/dist/db/schema.js +72 -47
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +5 -6
- package/dist/lib/constants.js +1 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/navigation.js +4 -5
- package/dist/lib/render.js +1 -1
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme-components.js +8 -11
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +119 -0
- package/dist/lib/view.js +62 -73
- package/dist/routes/api/posts.js +29 -35
- package/dist/routes/api/search.js +5 -6
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/dash/collections.js +22 -40
- package/dist/routes/dash/index.js +2 -2
- package/dist/routes/dash/navigation.js +25 -24
- package/dist/routes/dash/pages.js +42 -57
- package/dist/routes/dash/posts.js +27 -35
- package/dist/routes/feed/rss.js +2 -4
- package/dist/routes/feed/sitemap.js +10 -7
- package/dist/routes/pages/archive.js +12 -11
- package/dist/routes/pages/collection.js +11 -5
- package/dist/routes/pages/home.js +53 -61
- package/dist/routes/pages/page.js +60 -29
- package/dist/routes/pages/post.js +5 -12
- package/dist/routes/pages/search.js +3 -4
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +80 -0
- package/dist/services/post.js +68 -69
- package/dist/services/search.js +24 -18
- package/dist/theme/components/MediaGallery.js +19 -91
- package/dist/theme/components/PageForm.js +15 -15
- package/dist/theme/components/PostForm.js +136 -129
- package/dist/theme/components/PostList.js +13 -8
- package/dist/theme/components/ThreadView.js +3 -3
- package/dist/theme/components/TypeBadge.js +3 -14
- package/dist/theme/components/VisibilityBadge.js +33 -23
- package/dist/theme/components/index.js +0 -2
- package/dist/theme/index.js +10 -16
- package/dist/theme/layouts/index.js +0 -1
- package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
- package/dist/themes/threads/index.js +81 -0
- package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
- package/dist/themes/threads/pages/CollectionPage.js +65 -0
- package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
- package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
- package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
- package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
- package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
- package/dist/themes/threads/timeline/NoteCard.js +53 -0
- package/dist/themes/threads/timeline/QuoteCard.js +59 -0
- package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
- package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
- package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
- package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
- package/dist/themes/threads/timeline/groupByDate.js +22 -0
- package/dist/themes/threads/timeline/timelineMore.js +107 -0
- package/dist/types.js +24 -40
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +51 -74
- package/src/app.tsx +27 -6
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +216 -164
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +216 -164
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +216 -164
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +30 -15
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +166 -105
- package/src/lib/__tests__/theme-components.test.ts +4 -25
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
- package/src/lib/__tests__/view.test.ts +217 -67
- package/src/lib/constants.ts +1 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/navigation.ts +6 -7
- package/src/lib/render.tsx +1 -1
- package/src/lib/schemas.ts +118 -52
- package/src/lib/theme-components.ts +10 -13
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +170 -0
- package/src/lib/view.ts +81 -83
- package/src/preset.css +45 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/posts.ts +30 -30
- package/src/routes/api/search.ts +4 -4
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/dash/collections.tsx +18 -40
- package/src/routes/dash/index.tsx +2 -2
- package/src/routes/dash/navigation.tsx +27 -26
- package/src/routes/dash/pages.tsx +45 -60
- package/src/routes/dash/posts.tsx +44 -52
- package/src/routes/feed/rss.ts +2 -1
- package/src/routes/feed/sitemap.ts +14 -4
- package/src/routes/pages/archive.tsx +14 -10
- package/src/routes/pages/collection.tsx +17 -6
- package/src/routes/pages/home.tsx +56 -81
- package/src/routes/pages/page.tsx +64 -27
- package/src/routes/pages/post.tsx +5 -14
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +342 -206
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +124 -0
- package/src/services/post.ts +93 -103
- package/src/services/search.ts +38 -27
- package/src/styles/components.css +0 -54
- package/src/theme/components/MediaGallery.tsx +27 -96
- package/src/theme/components/PageForm.tsx +21 -21
- package/src/theme/components/PostForm.tsx +122 -118
- package/src/theme/components/PostList.tsx +58 -49
- package/src/theme/components/ThreadView.tsx +6 -3
- package/src/theme/components/TypeBadge.tsx +9 -17
- package/src/theme/components/VisibilityBadge.tsx +40 -23
- package/src/theme/components/index.ts +0 -13
- package/src/theme/index.ts +10 -16
- package/src/theme/layouts/index.ts +0 -1
- package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
- package/src/themes/threads/index.ts +100 -0
- package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
- package/src/themes/threads/pages/CollectionPage.tsx +61 -0
- package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
- package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
- package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
- package/src/themes/threads/pages/SinglePage.tsx +23 -0
- package/src/themes/threads/style.css +336 -0
- package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
- package/src/themes/threads/timeline/NoteCard.tsx +58 -0
- package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
- package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
- package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
- package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
- package/src/themes/threads/timeline/groupByDate.ts +30 -0
- package/src/themes/threads/timeline/timelineMore.tsx +130 -0
- package/src/types.ts +242 -98
- package/dist/routes/api/timeline.js +0 -120
- package/dist/theme/components/timeline/ArticleCard.js +0 -46
- package/dist/theme/components/timeline/ImageCard.js +0 -83
- package/dist/theme/components/timeline/NoteCard.js +0 -34
- package/dist/theme/components/timeline/QuoteCard.js +0 -48
- package/dist/theme/components/timeline/TimelineFeed.js +0 -46
- package/dist/theme/components/timeline/index.js +0 -8
- package/dist/theme/layouts/SiteLayout.js +0 -131
- package/dist/theme/pages/CollectionPage.js +0 -63
- package/dist/theme/pages/index.js +0 -11
- package/src/routes/api/timeline.tsx +0 -159
- package/src/theme/components/timeline/ArticleCard.tsx +0 -45
- package/src/theme/components/timeline/ImageCard.tsx +0 -70
- package/src/theme/components/timeline/NoteCard.tsx +0 -34
- package/src/theme/components/timeline/QuoteCard.tsx +0 -48
- package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
- package/src/theme/components/timeline/index.ts +0 -8
- package/src/theme/layouts/SiteLayout.tsx +0 -132
- package/src/theme/pages/CollectionPage.tsx +0 -60
- package/src/theme/pages/SinglePage.tsx +0 -24
- 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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
77
|
-
post.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
</
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
2
|
+
* Format Badge Component
|
|
3
3
|
*
|
|
4
|
-
* Displays a badge indicating the
|
|
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 {
|
|
10
|
+
import type { Format } from "../../types.js";
|
|
10
11
|
|
|
11
12
|
export interface TypeBadgeProps {
|
|
12
|
-
type:
|
|
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<
|
|
19
|
-
note: t({ message: "Note", comment: "@context: Post
|
|
20
|
-
|
|
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
|
|
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
|
-
*
|
|
2
|
+
* Status Badge Component
|
|
3
3
|
*
|
|
4
|
-
* Displays
|
|
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 {
|
|
10
|
+
import type { Status } from "../../types.js";
|
|
10
11
|
|
|
11
12
|
export interface VisibilityBadgeProps {
|
|
12
|
-
|
|
13
|
+
status: Status;
|
|
14
|
+
featured?: boolean;
|
|
15
|
+
pinned?: boolean;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
export const VisibilityBadge: FC<VisibilityBadgeProps> = ({
|
|
18
|
+
export const VisibilityBadge: FC<VisibilityBadgeProps> = ({
|
|
19
|
+
status,
|
|
20
|
+
featured,
|
|
21
|
+
pinned,
|
|
22
|
+
}) => {
|
|
16
23
|
const { t } = useLingui();
|
|
17
24
|
|
|
18
|
-
const
|
|
19
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
message: "
|
|
28
|
-
comment: "@context: Post
|
|
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
|
|
37
|
+
comment: "@context: Post status badge - draft",
|
|
41
38
|
}),
|
|
42
39
|
};
|
|
43
40
|
|
|
44
|
-
return
|
|
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";
|
package/src/theme/index.ts
CHANGED
|
@@ -1,28 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Jant Theme
|
|
2
|
+
* Jant Theme - Shared Infrastructure
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
9
|
-
* import
|
|
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
|
-
//
|
|
28
|
-
export * from "./
|
|
21
|
+
// Color themes
|
|
22
|
+
export * from "./color-themes.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";
|