@jant/core 0.3.21 → 0.3.23
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 -4
- package/dist/index.js +11 -4
- package/dist/lib/feed.js +112 -0
- package/dist/lib/navigation.js +9 -9
- package/dist/lib/render.js +48 -0
- package/dist/lib/theme-components.js +18 -18
- package/dist/lib/view.js +228 -0
- package/dist/routes/api/timeline.js +22 -18
- package/dist/routes/feed/rss.js +34 -78
- package/dist/routes/feed/sitemap.js +11 -26
- package/dist/routes/pages/archive.js +18 -195
- package/dist/routes/pages/collection.js +16 -70
- package/dist/routes/pages/home.js +25 -47
- package/dist/routes/pages/page.js +15 -27
- package/dist/routes/pages/post.js +25 -79
- package/dist/routes/pages/search.js +20 -130
- package/dist/theme/components/MediaGallery.js +10 -10
- package/dist/theme/components/index.js +0 -2
- package/dist/theme/index.js +10 -13
- package/dist/theme/layouts/index.js +0 -1
- package/dist/themes/minimal/MinimalSiteLayout.js +83 -0
- package/dist/themes/minimal/index.js +65 -0
- package/dist/themes/minimal/pages/ArchivePage.js +156 -0
- package/dist/themes/minimal/pages/CollectionPage.js +65 -0
- package/dist/themes/minimal/pages/HomePage.js +25 -0
- package/dist/themes/minimal/pages/PostPage.js +47 -0
- package/dist/themes/minimal/pages/SearchPage.js +121 -0
- package/dist/themes/minimal/pages/SinglePage.js +22 -0
- package/dist/themes/minimal/timeline/ArticleCard.js +36 -0
- package/dist/themes/minimal/timeline/ImageCard.js +67 -0
- package/dist/themes/minimal/timeline/LinkCard.js +47 -0
- package/dist/themes/minimal/timeline/NoteCard.js +34 -0
- package/dist/{theme/components → themes/minimal}/timeline/QuoteCard.js +9 -12
- package/dist/themes/minimal/timeline/ThreadPreview.js +46 -0
- package/dist/themes/minimal/timeline/TimelineFeed.js +48 -0
- package/dist/themes/minimal/timeline/TimelineItem.js +44 -0
- package/package.json +2 -1
- package/src/app.tsx +27 -4
- package/src/i18n/locales/en.po +53 -53
- package/src/i18n/locales/zh-Hans.po +53 -53
- package/src/i18n/locales/zh-Hant.po +53 -53
- package/src/index.ts +54 -6
- package/src/lib/__tests__/theme-components.test.ts +33 -14
- package/src/lib/__tests__/view.test.ts +377 -0
- package/src/lib/feed.ts +148 -0
- package/src/lib/navigation.ts +11 -11
- package/src/lib/render.tsx +67 -0
- package/src/lib/theme-components.ts +27 -35
- package/src/lib/view.ts +318 -0
- package/src/routes/api/__tests__/timeline.test.ts +3 -3
- package/src/routes/api/timeline.tsx +34 -27
- package/src/routes/feed/rss.ts +47 -94
- package/src/routes/feed/sitemap.ts +8 -30
- package/src/routes/pages/archive.tsx +24 -209
- package/src/routes/pages/collection.tsx +19 -75
- package/src/routes/pages/home.tsx +42 -76
- package/src/routes/pages/page.tsx +17 -28
- package/src/routes/pages/post.tsx +28 -86
- package/src/routes/pages/search.tsx +29 -151
- package/src/services/search.ts +2 -8
- package/src/styles/components.css +0 -54
- package/src/theme/components/MediaGallery.tsx +12 -12
- package/src/theme/components/index.ts +0 -12
- package/src/theme/index.ts +11 -13
- package/src/theme/layouts/index.ts +1 -1
- package/src/themes/minimal/MinimalSiteLayout.tsx +100 -0
- package/src/themes/minimal/index.ts +83 -0
- package/src/themes/minimal/pages/ArchivePage.tsx +157 -0
- package/src/themes/minimal/pages/CollectionPage.tsx +60 -0
- package/src/themes/minimal/pages/HomePage.tsx +41 -0
- package/src/themes/minimal/pages/PostPage.tsx +43 -0
- package/src/themes/minimal/pages/SearchPage.tsx +122 -0
- package/src/themes/minimal/pages/SinglePage.tsx +23 -0
- package/src/themes/minimal/timeline/ArticleCard.tsx +37 -0
- package/src/themes/minimal/timeline/ImageCard.tsx +63 -0
- package/src/themes/minimal/timeline/LinkCard.tsx +48 -0
- package/src/themes/minimal/timeline/NoteCard.tsx +35 -0
- package/src/{theme/components → themes/minimal}/timeline/QuoteCard.tsx +11 -17
- package/src/themes/minimal/timeline/ThreadPreview.tsx +47 -0
- package/src/{theme/components → themes/minimal}/timeline/TimelineFeed.tsx +20 -15
- package/src/themes/minimal/timeline/TimelineItem.tsx +75 -0
- package/src/types.ts +262 -38
- package/dist/theme/components/timeline/ArticleCard.js +0 -50
- package/dist/theme/components/timeline/ImageCard.js +0 -86
- package/dist/theme/components/timeline/LinkCard.js +0 -62
- package/dist/theme/components/timeline/NoteCard.js +0 -37
- package/dist/theme/components/timeline/ThreadPreview.js +0 -52
- package/dist/theme/components/timeline/TimelineFeed.js +0 -43
- package/dist/theme/components/timeline/TimelineItem.js +0 -25
- package/dist/theme/components/timeline/index.js +0 -8
- package/dist/theme/layouts/SiteLayout.js +0 -160
- package/src/theme/components/timeline/ArticleCard.tsx +0 -57
- package/src/theme/components/timeline/ImageCard.tsx +0 -80
- package/src/theme/components/timeline/LinkCard.tsx +0 -66
- package/src/theme/components/timeline/NoteCard.tsx +0 -41
- package/src/theme/components/timeline/ThreadPreview.tsx +0 -49
- package/src/theme/components/timeline/TimelineItem.tsx +0 -39
- package/src/theme/components/timeline/index.ts +0 -8
- package/src/theme/layouts/SiteLayout.tsx +0 -184
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { FC } from "hono/jsx";
|
|
9
|
-
import type {
|
|
9
|
+
import type { MediaView } from "../../types.js";
|
|
10
10
|
|
|
11
11
|
export interface MediaGalleryProps {
|
|
12
|
-
attachments:
|
|
12
|
+
attachments: MediaView[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
@@ -23,8 +23,8 @@ export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
|
23
23
|
<div class="mt-3">
|
|
24
24
|
<a href={img.url} target="_blank" rel="noopener noreferrer">
|
|
25
25
|
<img
|
|
26
|
-
src={img.
|
|
27
|
-
alt={img.
|
|
26
|
+
src={img.thumbnailUrl}
|
|
27
|
+
alt={img.altText || ""}
|
|
28
28
|
width={img.width ?? undefined}
|
|
29
29
|
height={img.height ?? undefined}
|
|
30
30
|
class="rounded-lg max-w-full h-auto"
|
|
@@ -47,8 +47,8 @@ export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
|
47
47
|
class="aspect-square"
|
|
48
48
|
>
|
|
49
49
|
<img
|
|
50
|
-
src={img.
|
|
51
|
-
alt={img.
|
|
50
|
+
src={img.thumbnailUrl}
|
|
51
|
+
alt={img.altText || ""}
|
|
52
52
|
class="w-full h-full object-cover"
|
|
53
53
|
loading="lazy"
|
|
54
54
|
/>
|
|
@@ -70,8 +70,8 @@ export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
|
70
70
|
class="row-span-2"
|
|
71
71
|
>
|
|
72
72
|
<img
|
|
73
|
-
src={first.
|
|
74
|
-
alt={first.
|
|
73
|
+
src={first.thumbnailUrl}
|
|
74
|
+
alt={first.altText || ""}
|
|
75
75
|
class="w-full h-full object-cover"
|
|
76
76
|
loading="lazy"
|
|
77
77
|
/>
|
|
@@ -85,8 +85,8 @@ export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
|
85
85
|
class="aspect-square"
|
|
86
86
|
>
|
|
87
87
|
<img
|
|
88
|
-
src={img.
|
|
89
|
-
alt={img.
|
|
88
|
+
src={img.thumbnailUrl}
|
|
89
|
+
alt={img.altText || ""}
|
|
90
90
|
class="w-full h-full object-cover"
|
|
91
91
|
loading="lazy"
|
|
92
92
|
/>
|
|
@@ -111,8 +111,8 @@ export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
|
111
111
|
class="relative aspect-square"
|
|
112
112
|
>
|
|
113
113
|
<img
|
|
114
|
-
src={img.
|
|
115
|
-
alt={img.
|
|
114
|
+
src={img.thumbnailUrl}
|
|
115
|
+
alt={img.altText || ""}
|
|
116
116
|
class="w-full h-full object-cover"
|
|
117
117
|
loading="lazy"
|
|
118
118
|
/>
|
|
@@ -21,15 +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
|
-
TimelineFeed,
|
|
35
|
-
} from "./timeline/index.js";
|
package/src/theme/index.ts
CHANGED
|
@@ -1,24 +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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* return (
|
|
12
|
-
* <div class="my-wrapper">
|
|
13
|
-
* <PostCard {...props} />
|
|
14
|
-
* </div>
|
|
15
|
-
* );
|
|
16
|
-
* }
|
|
9
|
+
* // In a theme package:
|
|
10
|
+
* import { MediaGallery, Pagination } from "@jant/core/theme";
|
|
11
|
+
* import type { ColorTheme } from "@jant/core/theme";
|
|
17
12
|
* ```
|
|
18
13
|
*/
|
|
19
14
|
|
|
20
|
-
// Layout components
|
|
15
|
+
// Layout components (BaseLayout, DashLayout)
|
|
21
16
|
export * from "./layouts/index.js";
|
|
22
17
|
|
|
23
|
-
// UI components
|
|
18
|
+
// Shared UI components (MediaGallery, Pagination, EmptyState, etc.)
|
|
24
19
|
export * from "./components/index.js";
|
|
20
|
+
|
|
21
|
+
// Color themes
|
|
22
|
+
export * from "./color-themes.js";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Site Layout
|
|
3
|
+
*
|
|
4
|
+
* Single-column, centered layout with horizontal nav.
|
|
5
|
+
* Inspired by Tufte CSS and Manton.org.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC, PropsWithChildren } from "hono/jsx";
|
|
9
|
+
import type { NavLinkView, SiteLayoutProps } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
function NavLinks({ links }: { links: NavLinkView[] }) {
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
{links.map((link) => (
|
|
15
|
+
<a
|
|
16
|
+
key={link.id}
|
|
17
|
+
href={link.url}
|
|
18
|
+
class={`text-sm ${
|
|
19
|
+
link.isActive
|
|
20
|
+
? "text-foreground font-medium"
|
|
21
|
+
: "text-muted-foreground hover:text-foreground"
|
|
22
|
+
}`}
|
|
23
|
+
{...(link.isExternal
|
|
24
|
+
? { target: "_blank", rel: "noopener noreferrer" }
|
|
25
|
+
: {})}
|
|
26
|
+
>
|
|
27
|
+
{link.label}
|
|
28
|
+
{link.isExternal && (
|
|
29
|
+
<span class="ml-0.5 text-xs opacity-50">↗</span>
|
|
30
|
+
)}
|
|
31
|
+
</a>
|
|
32
|
+
))}
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
|
|
38
|
+
siteName,
|
|
39
|
+
links,
|
|
40
|
+
children,
|
|
41
|
+
}) => {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
class="max-w-2xl mx-auto px-4 py-8"
|
|
45
|
+
data-signals={JSON.stringify({ _menuOpen: false })}
|
|
46
|
+
>
|
|
47
|
+
{/* Header */}
|
|
48
|
+
<header class="mb-12">
|
|
49
|
+
<div class="flex items-center justify-between">
|
|
50
|
+
<a href="/" class="text-xl font-semibold">
|
|
51
|
+
{siteName}
|
|
52
|
+
</a>
|
|
53
|
+
|
|
54
|
+
{/* Mobile menu toggle */}
|
|
55
|
+
{links.length > 0 && (
|
|
56
|
+
<button
|
|
57
|
+
data-on:click="$_menuOpen = !$_menuOpen"
|
|
58
|
+
class="p-2 -mr-2 text-muted-foreground hover:text-foreground sm:hidden"
|
|
59
|
+
aria-label="Toggle menu"
|
|
60
|
+
>
|
|
61
|
+
<svg
|
|
62
|
+
class="size-5"
|
|
63
|
+
fill="none"
|
|
64
|
+
viewBox="0 0 24 24"
|
|
65
|
+
stroke-width="1.5"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
>
|
|
68
|
+
<path
|
|
69
|
+
stroke-linecap="round"
|
|
70
|
+
stroke-linejoin="round"
|
|
71
|
+
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
74
|
+
</button>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Desktop nav (inline) */}
|
|
79
|
+
{links.length > 0 && (
|
|
80
|
+
<nav class="hidden sm:flex flex-wrap gap-x-4 gap-y-1 mt-3">
|
|
81
|
+
<NavLinks links={links} />
|
|
82
|
+
</nav>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{/* Mobile nav (collapsible) */}
|
|
86
|
+
{links.length > 0 && (
|
|
87
|
+
<nav
|
|
88
|
+
class="sm:hidden flex flex-col gap-1 mt-3 overflow-hidden"
|
|
89
|
+
data-show="$_menuOpen"
|
|
90
|
+
>
|
|
91
|
+
<NavLinks links={links} />
|
|
92
|
+
</nav>
|
|
93
|
+
)}
|
|
94
|
+
</header>
|
|
95
|
+
|
|
96
|
+
{/* Main content */}
|
|
97
|
+
<main>{children}</main>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme
|
|
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.
|
|
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 { SiteLayout } from "./MinimalSiteLayout.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 { ArticleCard } from "./timeline/ArticleCard.js";
|
|
27
|
+
import { LinkCard } from "./timeline/LinkCard.js";
|
|
28
|
+
import { QuoteCard } from "./timeline/QuoteCard.js";
|
|
29
|
+
import { ImageCard } from "./timeline/ImageCard.js";
|
|
30
|
+
import { ThreadPreview } from "./timeline/ThreadPreview.js";
|
|
31
|
+
import { TimelineFeed } from "./timeline/TimelineFeed.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 minimal 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 { minimalTheme } from "@jant/core";
|
|
52
|
+
*
|
|
53
|
+
* export default createApp({
|
|
54
|
+
* theme: minimalTheme(), // re-exported as minimalTheme from @jant/core
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function theme(options?: ThemeOptions): JantTheme {
|
|
59
|
+
return {
|
|
60
|
+
name: "minimal",
|
|
61
|
+
components: {
|
|
62
|
+
SiteLayout,
|
|
63
|
+
HomePage,
|
|
64
|
+
PostPage,
|
|
65
|
+
SinglePage,
|
|
66
|
+
ArchivePage,
|
|
67
|
+
SearchPage,
|
|
68
|
+
CollectionPage,
|
|
69
|
+
NoteCard,
|
|
70
|
+
ArticleCard,
|
|
71
|
+
LinkCard,
|
|
72
|
+
QuoteCard,
|
|
73
|
+
ImageCard,
|
|
74
|
+
ThreadPreview,
|
|
75
|
+
TimelineFeed,
|
|
76
|
+
...options?.components,
|
|
77
|
+
},
|
|
78
|
+
cssVariables: {
|
|
79
|
+
...options?.cssVariables,
|
|
80
|
+
},
|
|
81
|
+
colorThemes: options?.colorThemes,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Archive Page
|
|
3
|
+
*
|
|
4
|
+
* Date-first list with type filter and cursor pagination.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { ArchivePageProps } from "../../../types.js";
|
|
10
|
+
import { POST_TYPES } from "../../../types.js";
|
|
11
|
+
import { Pagination as DefaultPagination } from "../../../theme/components/Pagination.js";
|
|
12
|
+
|
|
13
|
+
function getTypeLabel(type: string): string {
|
|
14
|
+
const { t } = useLingui();
|
|
15
|
+
const labels: Record<string, string> = {
|
|
16
|
+
note: t({ message: "Note", comment: "@context: Post type label - note" }),
|
|
17
|
+
article: t({
|
|
18
|
+
message: "Article",
|
|
19
|
+
comment: "@context: Post type label - article",
|
|
20
|
+
}),
|
|
21
|
+
link: t({ message: "Link", comment: "@context: Post type label - link" }),
|
|
22
|
+
quote: t({
|
|
23
|
+
message: "Quote",
|
|
24
|
+
comment: "@context: Post type label - quote",
|
|
25
|
+
}),
|
|
26
|
+
image: t({
|
|
27
|
+
message: "Image",
|
|
28
|
+
comment: "@context: Post type label - image",
|
|
29
|
+
}),
|
|
30
|
+
page: t({ message: "Page", comment: "@context: Post type label - page" }),
|
|
31
|
+
};
|
|
32
|
+
return labels[type] ?? type;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getTypeLabelPlural(type: string): string {
|
|
36
|
+
const { t } = useLingui();
|
|
37
|
+
const labels: Record<string, string> = {
|
|
38
|
+
note: t({
|
|
39
|
+
message: "Notes",
|
|
40
|
+
comment: "@context: Post type label plural - notes",
|
|
41
|
+
}),
|
|
42
|
+
article: t({
|
|
43
|
+
message: "Articles",
|
|
44
|
+
comment: "@context: Post type label plural - articles",
|
|
45
|
+
}),
|
|
46
|
+
link: t({
|
|
47
|
+
message: "Links",
|
|
48
|
+
comment: "@context: Post type label plural - links",
|
|
49
|
+
}),
|
|
50
|
+
quote: t({
|
|
51
|
+
message: "Quotes",
|
|
52
|
+
comment: "@context: Post type label plural - quotes",
|
|
53
|
+
}),
|
|
54
|
+
image: t({
|
|
55
|
+
message: "Images",
|
|
56
|
+
comment: "@context: Post type label plural - images",
|
|
57
|
+
}),
|
|
58
|
+
page: t({
|
|
59
|
+
message: "Pages",
|
|
60
|
+
comment: "@context: Post type label plural - pages",
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
return labels[type] ?? `${type}s`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const ArchivePage: FC<ArchivePageProps> = ({
|
|
67
|
+
groups,
|
|
68
|
+
hasMore,
|
|
69
|
+
nextCursor,
|
|
70
|
+
type,
|
|
71
|
+
theme,
|
|
72
|
+
}) => {
|
|
73
|
+
const { t } = useLingui();
|
|
74
|
+
const title = type
|
|
75
|
+
? getTypeLabelPlural(type)
|
|
76
|
+
: t({ message: "Archive", comment: "@context: Archive page title" });
|
|
77
|
+
|
|
78
|
+
const PaginationComponent = theme?.Pagination ?? DefaultPagination;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div>
|
|
82
|
+
<header class="mb-8">
|
|
83
|
+
<h1 class="text-2xl font-semibold">{title}</h1>
|
|
84
|
+
|
|
85
|
+
<nav class="flex flex-wrap gap-2 mt-4">
|
|
86
|
+
<a
|
|
87
|
+
href="/archive"
|
|
88
|
+
class={`text-sm ${!type ? "font-medium text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
|
89
|
+
>
|
|
90
|
+
{t({
|
|
91
|
+
message: "All",
|
|
92
|
+
comment: "@context: Archive filter - all types",
|
|
93
|
+
})}
|
|
94
|
+
</a>
|
|
95
|
+
{POST_TYPES.filter((t) => t !== "page").map((typeKey) => (
|
|
96
|
+
<a
|
|
97
|
+
key={typeKey}
|
|
98
|
+
href={`/archive?type=${typeKey}`}
|
|
99
|
+
class={`text-sm ${type === typeKey ? "font-medium text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
|
100
|
+
>
|
|
101
|
+
{getTypeLabelPlural(typeKey)}
|
|
102
|
+
</a>
|
|
103
|
+
))}
|
|
104
|
+
</nav>
|
|
105
|
+
</header>
|
|
106
|
+
|
|
107
|
+
<main>
|
|
108
|
+
{groups.length === 0 ? (
|
|
109
|
+
<p class="text-muted-foreground">
|
|
110
|
+
{t({
|
|
111
|
+
message: "No posts found.",
|
|
112
|
+
comment: "@context: Archive empty state",
|
|
113
|
+
})}
|
|
114
|
+
</p>
|
|
115
|
+
) : (
|
|
116
|
+
groups.map((group) => (
|
|
117
|
+
<section key={`${group.year}-${group.month}`} class="mb-8">
|
|
118
|
+
<h2 class="text-lg font-medium mb-4 text-muted-foreground">
|
|
119
|
+
{group.label}
|
|
120
|
+
</h2>
|
|
121
|
+
<div class="flex flex-col gap-3">
|
|
122
|
+
{group.posts.map((post) => (
|
|
123
|
+
<article key={post.id} class="flex items-baseline gap-4">
|
|
124
|
+
<time
|
|
125
|
+
class="text-sm text-muted-foreground w-12 shrink-0"
|
|
126
|
+
datetime={post.publishedAt}
|
|
127
|
+
>
|
|
128
|
+
{new Date(post.publishedAt).getUTCDate()}
|
|
129
|
+
</time>
|
|
130
|
+
<div class="flex-1 min-w-0">
|
|
131
|
+
<a href={post.permalink} class="hover:underline">
|
|
132
|
+
{post.title ||
|
|
133
|
+
post.content?.slice(0, 80) ||
|
|
134
|
+
`Post #${post.id}`}
|
|
135
|
+
</a>
|
|
136
|
+
{!type && (
|
|
137
|
+
<span class="ml-2 text-xs text-muted-foreground">
|
|
138
|
+
{getTypeLabel(post.type)}
|
|
139
|
+
</span>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
</article>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
</section>
|
|
146
|
+
))
|
|
147
|
+
)}
|
|
148
|
+
</main>
|
|
149
|
+
|
|
150
|
+
<PaginationComponent
|
|
151
|
+
baseUrl={type ? `/archive?type=${type}` : "/archive"}
|
|
152
|
+
hasMore={hasMore}
|
|
153
|
+
nextCursor={nextCursor}
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Collection Page
|
|
3
|
+
*
|
|
4
|
+
* Simple list of posts in a collection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { CollectionPageProps } from "../../../types.js";
|
|
10
|
+
|
|
11
|
+
export const CollectionPage: FC<CollectionPageProps> = ({
|
|
12
|
+
collection,
|
|
13
|
+
posts,
|
|
14
|
+
}) => {
|
|
15
|
+
const { t } = useLingui();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div>
|
|
19
|
+
<header class="mb-8">
|
|
20
|
+
<h1 class="text-2xl font-semibold">{collection.title}</h1>
|
|
21
|
+
{collection.description && (
|
|
22
|
+
<p class="text-muted-foreground mt-2">{collection.description}</p>
|
|
23
|
+
)}
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<main class="flex flex-col">
|
|
27
|
+
{posts.length === 0 ? (
|
|
28
|
+
<p class="text-muted-foreground">
|
|
29
|
+
{t({
|
|
30
|
+
message: "No posts in this collection.",
|
|
31
|
+
comment: "@context: Empty state message",
|
|
32
|
+
})}
|
|
33
|
+
</p>
|
|
34
|
+
) : (
|
|
35
|
+
posts.map((post, i) => (
|
|
36
|
+
<article key={post.id} class="h-entry">
|
|
37
|
+
{i > 0 && <hr class="my-6 border-border" />}
|
|
38
|
+
{post.title && (
|
|
39
|
+
<h2 class="p-name text-lg font-medium mb-2">
|
|
40
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
41
|
+
{post.title}
|
|
42
|
+
</a>
|
|
43
|
+
</h2>
|
|
44
|
+
)}
|
|
45
|
+
<div
|
|
46
|
+
class="e-content prose prose-sm"
|
|
47
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
48
|
+
/>
|
|
49
|
+
<footer class="mt-2 text-sm text-muted-foreground">
|
|
50
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
51
|
+
{post.publishedAtFormatted}
|
|
52
|
+
</time>
|
|
53
|
+
</footer>
|
|
54
|
+
</article>
|
|
55
|
+
))
|
|
56
|
+
)}
|
|
57
|
+
</main>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Home Page
|
|
3
|
+
*
|
|
4
|
+
* Renders the timeline feed with thread previews.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { HomePageProps } from "../../../types.js";
|
|
10
|
+
import { TimelineFeed as DefaultTimelineFeed } from "../timeline/TimelineFeed.js";
|
|
11
|
+
|
|
12
|
+
export const HomePage: FC<HomePageProps> = ({
|
|
13
|
+
items,
|
|
14
|
+
hasMore,
|
|
15
|
+
nextCursor,
|
|
16
|
+
theme,
|
|
17
|
+
}) => {
|
|
18
|
+
const { t } = useLingui();
|
|
19
|
+
|
|
20
|
+
const Feed = theme?.TimelineFeed ?? DefaultTimelineFeed;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
{items.length === 0 ? (
|
|
25
|
+
<p class="text-muted-foreground">
|
|
26
|
+
{t({
|
|
27
|
+
message: "No posts yet.",
|
|
28
|
+
comment: "@context: Empty state message on home page",
|
|
29
|
+
})}
|
|
30
|
+
</p>
|
|
31
|
+
) : (
|
|
32
|
+
<Feed
|
|
33
|
+
items={items}
|
|
34
|
+
hasMore={hasMore}
|
|
35
|
+
nextCursor={nextCursor}
|
|
36
|
+
theme={theme}
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Post Page
|
|
3
|
+
*
|
|
4
|
+
* Clean article layout for a single post.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { PostPageProps } from "../../../types.js";
|
|
10
|
+
import { MediaGallery as DefaultMediaGallery } from "../../../theme/components/MediaGallery.js";
|
|
11
|
+
|
|
12
|
+
export const PostPage: FC<PostPageProps> = ({ post, theme }) => {
|
|
13
|
+
const { t } = useLingui();
|
|
14
|
+
|
|
15
|
+
const Gallery = theme?.MediaGallery ?? DefaultMediaGallery;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<article class="h-entry">
|
|
19
|
+
{post.title && (
|
|
20
|
+
<h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
|
|
21
|
+
)}
|
|
22
|
+
|
|
23
|
+
<div
|
|
24
|
+
class="e-content prose"
|
|
25
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
{post.media.length > 0 && <Gallery attachments={post.media} />}
|
|
29
|
+
|
|
30
|
+
<footer class="mt-8 pt-4 border-t border-border text-sm text-muted-foreground">
|
|
31
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
32
|
+
{post.publishedAtFormatted}
|
|
33
|
+
</time>
|
|
34
|
+
<a href={post.permalink} class="u-url ml-4 hover:underline">
|
|
35
|
+
{t({
|
|
36
|
+
message: "Permalink",
|
|
37
|
+
comment: "@context: Link to permanent URL of post",
|
|
38
|
+
})}
|
|
39
|
+
</a>
|
|
40
|
+
</footer>
|
|
41
|
+
</article>
|
|
42
|
+
);
|
|
43
|
+
};
|