@jant/core 0.3.20 → 0.3.22
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 +60 -17
- package/dist/index.js +8 -0
- 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 +20 -16
- package/dist/routes/dash/collections.js +38 -10
- package/dist/routes/dash/navigation.js +22 -8
- package/dist/routes/dash/redirects.js +19 -5
- package/dist/routes/dash/settings.js +57 -15
- 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/PageForm.js +22 -8
- package/dist/theme/components/PostForm.js +22 -8
- package/dist/theme/components/index.js +1 -1
- package/dist/theme/components/timeline/ArticleCard.js +7 -11
- package/dist/theme/components/timeline/ImageCard.js +10 -13
- package/dist/theme/components/timeline/LinkCard.js +4 -7
- package/dist/theme/components/timeline/NoteCard.js +5 -8
- package/dist/theme/components/timeline/QuoteCard.js +3 -6
- package/dist/theme/components/timeline/ThreadPreview.js +9 -10
- package/dist/theme/components/timeline/TimelineFeed.js +8 -5
- package/dist/theme/components/timeline/TimelineItem.js +22 -2
- package/dist/theme/components/timeline/index.js +1 -1
- package/dist/theme/index.js +6 -3
- package/dist/theme/layouts/SiteLayout.js +10 -39
- package/dist/theme/pages/ArchivePage.js +157 -0
- package/dist/theme/pages/CollectionPage.js +63 -0
- package/dist/theme/pages/HomePage.js +26 -0
- package/dist/theme/pages/PostPage.js +48 -0
- package/dist/theme/pages/SearchPage.js +120 -0
- package/dist/theme/pages/SinglePage.js +23 -0
- package/dist/theme/pages/index.js +11 -0
- package/package.json +2 -1
- package/src/app.tsx +48 -17
- package/src/i18n/locales/en.po +171 -147
- package/src/i18n/locales/zh-Hans.po +171 -147
- package/src/i18n/locales/zh-Hant.po +171 -147
- package/src/index.ts +51 -2
- package/src/lib/__tests__/theme-components.test.ts +33 -14
- package/src/lib/__tests__/view.test.ts +375 -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 +32 -25
- package/src/routes/dash/collections.tsx +30 -10
- package/src/routes/dash/navigation.tsx +20 -10
- package/src/routes/dash/redirects.tsx +15 -5
- package/src/routes/dash/settings.tsx +53 -15
- 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/theme/components/MediaGallery.tsx +12 -12
- package/src/theme/components/PageForm.tsx +20 -10
- package/src/theme/components/PostForm.tsx +20 -10
- package/src/theme/components/index.ts +1 -0
- package/src/theme/components/timeline/ArticleCard.tsx +7 -19
- package/src/theme/components/timeline/ImageCard.tsx +10 -20
- package/src/theme/components/timeline/LinkCard.tsx +4 -11
- package/src/theme/components/timeline/NoteCard.tsx +5 -12
- package/src/theme/components/timeline/QuoteCard.tsx +3 -10
- package/src/theme/components/timeline/ThreadPreview.tsx +5 -5
- package/src/theme/components/timeline/TimelineFeed.tsx +7 -3
- package/src/theme/components/timeline/TimelineItem.tsx +43 -4
- package/src/theme/components/timeline/index.ts +1 -1
- package/src/theme/index.ts +7 -3
- package/src/theme/layouts/SiteLayout.tsx +25 -77
- package/src/theme/layouts/index.ts +2 -1
- package/src/theme/pages/ArchivePage.tsx +160 -0
- package/src/theme/pages/CollectionPage.tsx +60 -0
- package/src/theme/pages/HomePage.tsx +42 -0
- package/src/theme/pages/PostPage.tsx +44 -0
- package/src/theme/pages/SearchPage.tsx +128 -0
- package/src/theme/pages/SinglePage.tsx +24 -0
- package/src/theme/pages/index.ts +13 -0
- package/src/types.ts +262 -38
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Home Page Component
|
|
3
|
+
*
|
|
4
|
+
* Renders the timeline feed with thread previews.
|
|
5
|
+
* Theme authors can replace this entirely via ThemeComponents.HomePage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import type { HomePageProps } from "../../types.js";
|
|
11
|
+
import { TimelineFeed as DefaultTimelineFeed } from "../components/timeline/TimelineFeed.js";
|
|
12
|
+
|
|
13
|
+
export const HomePage: FC<HomePageProps> = ({
|
|
14
|
+
items,
|
|
15
|
+
hasMore,
|
|
16
|
+
nextCursor,
|
|
17
|
+
theme,
|
|
18
|
+
}) => {
|
|
19
|
+
const { t } = useLingui();
|
|
20
|
+
|
|
21
|
+
const Feed = theme?.TimelineFeed ?? DefaultTimelineFeed;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{items.length === 0 ? (
|
|
26
|
+
<p class="text-muted-foreground">
|
|
27
|
+
{t({
|
|
28
|
+
message: "No posts yet.",
|
|
29
|
+
comment: "@context: Empty state message on home page",
|
|
30
|
+
})}
|
|
31
|
+
</p>
|
|
32
|
+
) : (
|
|
33
|
+
<Feed
|
|
34
|
+
items={items}
|
|
35
|
+
hasMore={hasMore}
|
|
36
|
+
nextCursor={nextCursor}
|
|
37
|
+
theme={theme}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Post Page Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a single post with media gallery.
|
|
5
|
+
* Theme authors can replace this entirely via ThemeComponents.PostPage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import type { PostPageProps } from "../../types.js";
|
|
11
|
+
import { MediaGallery as DefaultMediaGallery } from "../components/MediaGallery.js";
|
|
12
|
+
|
|
13
|
+
export const PostPage: FC<PostPageProps> = ({ post, theme }) => {
|
|
14
|
+
const { t } = useLingui();
|
|
15
|
+
|
|
16
|
+
const Gallery = theme?.MediaGallery ?? DefaultMediaGallery;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<article class="h-entry">
|
|
20
|
+
{post.title && (
|
|
21
|
+
<h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
|
|
22
|
+
)}
|
|
23
|
+
|
|
24
|
+
<div
|
|
25
|
+
class="e-content prose"
|
|
26
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
27
|
+
/>
|
|
28
|
+
|
|
29
|
+
{post.media.length > 0 && <Gallery attachments={post.media} />}
|
|
30
|
+
|
|
31
|
+
<footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
|
|
32
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
33
|
+
{post.publishedAtFormatted}
|
|
34
|
+
</time>
|
|
35
|
+
<a href={post.permalink} class="u-url ml-4">
|
|
36
|
+
{t({
|
|
37
|
+
message: "Permalink",
|
|
38
|
+
comment: "@context: Link to permanent URL of post",
|
|
39
|
+
})}
|
|
40
|
+
</a>
|
|
41
|
+
</footer>
|
|
42
|
+
</article>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Search Page Component
|
|
3
|
+
*
|
|
4
|
+
* Renders search form and results with page-based pagination.
|
|
5
|
+
* Theme authors can replace this entirely via ThemeComponents.SearchPage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import type { SearchPageProps } from "../../types.js";
|
|
11
|
+
import { PagePagination as DefaultPagePagination } from "../components/Pagination.js";
|
|
12
|
+
|
|
13
|
+
export const SearchPage: FC<SearchPageProps> = ({
|
|
14
|
+
query,
|
|
15
|
+
results,
|
|
16
|
+
error,
|
|
17
|
+
hasMore,
|
|
18
|
+
page,
|
|
19
|
+
theme,
|
|
20
|
+
}) => {
|
|
21
|
+
const { t } = useLingui();
|
|
22
|
+
const searchTitle = t({
|
|
23
|
+
message: "Search",
|
|
24
|
+
comment: "@context: Search page title",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const PaginationComponent = theme?.PagePagination ?? DefaultPagePagination;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
|
|
32
|
+
|
|
33
|
+
{/* Search form */}
|
|
34
|
+
<form method="get" action="/search" class="mb-8">
|
|
35
|
+
<div class="flex gap-2">
|
|
36
|
+
<input
|
|
37
|
+
type="search"
|
|
38
|
+
name="q"
|
|
39
|
+
class="input flex-1"
|
|
40
|
+
placeholder={t({
|
|
41
|
+
message: "Search posts...",
|
|
42
|
+
comment: "@context: Search input placeholder",
|
|
43
|
+
})}
|
|
44
|
+
value={query}
|
|
45
|
+
autofocus
|
|
46
|
+
/>
|
|
47
|
+
<button type="submit" class="btn">
|
|
48
|
+
{t({
|
|
49
|
+
message: "Search",
|
|
50
|
+
comment: "@context: Search submit button",
|
|
51
|
+
})}
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
</form>
|
|
55
|
+
|
|
56
|
+
{/* Error */}
|
|
57
|
+
{error && (
|
|
58
|
+
<div class="alert-destructive mb-6">
|
|
59
|
+
<h2>{error}</h2>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{/* Results */}
|
|
64
|
+
{query && !error && (
|
|
65
|
+
<div>
|
|
66
|
+
<p class="text-sm text-muted-foreground mb-4">
|
|
67
|
+
{results.length === 0
|
|
68
|
+
? t({
|
|
69
|
+
message: "No results found.",
|
|
70
|
+
comment: "@context: Search empty results",
|
|
71
|
+
})
|
|
72
|
+
: results.length === 1
|
|
73
|
+
? t({
|
|
74
|
+
message: "Found 1 result",
|
|
75
|
+
comment: "@context: Search results count - single",
|
|
76
|
+
})
|
|
77
|
+
: t({
|
|
78
|
+
message: "Found {count} results",
|
|
79
|
+
comment: "@context: Search results count - multiple",
|
|
80
|
+
values: { count: String(results.length) },
|
|
81
|
+
})}
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
{results.length > 0 && (
|
|
85
|
+
<>
|
|
86
|
+
<div class="flex flex-col gap-4">
|
|
87
|
+
{results.map((result) => (
|
|
88
|
+
<article
|
|
89
|
+
key={result.post.id}
|
|
90
|
+
class="p-4 rounded-lg border hover:border-primary"
|
|
91
|
+
>
|
|
92
|
+
<a href={result.post.permalink} class="block">
|
|
93
|
+
<h2 class="font-medium hover:underline">
|
|
94
|
+
{result.post.title ||
|
|
95
|
+
result.post.content?.slice(0, 60) ||
|
|
96
|
+
`Post #${result.post.id}`}
|
|
97
|
+
</h2>
|
|
98
|
+
|
|
99
|
+
{result.snippet && (
|
|
100
|
+
<p
|
|
101
|
+
class="text-sm text-muted-foreground mt-2 line-clamp-2"
|
|
102
|
+
dangerouslySetInnerHTML={{ __html: result.snippet }}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
<footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
|
|
107
|
+
<span class="badge-outline">{result.post.type}</span>
|
|
108
|
+
<time datetime={result.post.publishedAt}>
|
|
109
|
+
{result.post.publishedAtFormatted}
|
|
110
|
+
</time>
|
|
111
|
+
</footer>
|
|
112
|
+
</a>
|
|
113
|
+
</article>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<PaginationComponent
|
|
118
|
+
baseUrl={`/search?q=${encodeURIComponent(query)}`}
|
|
119
|
+
currentPage={page}
|
|
120
|
+
hasMore={hasMore}
|
|
121
|
+
/>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Single Page Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a custom page (type "page").
|
|
5
|
+
* Theme authors can replace this entirely via ThemeComponents.SinglePage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import type { SinglePageProps } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
export const SinglePage: FC<SinglePageProps> = ({ page }) => {
|
|
12
|
+
return (
|
|
13
|
+
<article class="h-entry">
|
|
14
|
+
{page.title && (
|
|
15
|
+
<h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>
|
|
16
|
+
)}
|
|
17
|
+
|
|
18
|
+
<div
|
|
19
|
+
class="e-content prose"
|
|
20
|
+
dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
|
|
21
|
+
/>
|
|
22
|
+
</article>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Page Components
|
|
3
|
+
*
|
|
4
|
+
* These are the built-in page components that render each public page.
|
|
5
|
+
* Theme authors can import these to wrap/extend them.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { HomePage } from "./HomePage.js";
|
|
9
|
+
export { PostPage } from "./PostPage.js";
|
|
10
|
+
export { SinglePage } from "./SinglePage.js";
|
|
11
|
+
export { ArchivePage } from "./ArchivePage.js";
|
|
12
|
+
export { SearchPage } from "./SearchPage.js";
|
|
13
|
+
export { CollectionPage } from "./CollectionPage.js";
|
package/src/types.ts
CHANGED
|
@@ -291,6 +291,121 @@ export interface UpdateNavigationLink {
|
|
|
291
291
|
position?: number;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// =============================================================================
|
|
295
|
+
// View Model Types (render-ready, for theme components)
|
|
296
|
+
// =============================================================================
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Render-ready post data for theme components.
|
|
300
|
+
* All fields are pre-computed — no lib/ imports needed.
|
|
301
|
+
*/
|
|
302
|
+
export interface PostView {
|
|
303
|
+
// Identity
|
|
304
|
+
id: number;
|
|
305
|
+
/** Pre-computed permalink, e.g. "/p/jR3k" */
|
|
306
|
+
permalink: string;
|
|
307
|
+
|
|
308
|
+
// Content
|
|
309
|
+
title?: string;
|
|
310
|
+
/** Pre-sanitized HTML */
|
|
311
|
+
contentHtml?: string;
|
|
312
|
+
/** Pre-computed excerpt, max 160 chars */
|
|
313
|
+
excerpt?: string;
|
|
314
|
+
|
|
315
|
+
// Metadata
|
|
316
|
+
type: PostType;
|
|
317
|
+
visibility: Visibility;
|
|
318
|
+
/** Custom path for pages, e.g. "/about" */
|
|
319
|
+
path?: string;
|
|
320
|
+
|
|
321
|
+
// Time — pre-formatted
|
|
322
|
+
/** ISO 8601 string */
|
|
323
|
+
publishedAt: string;
|
|
324
|
+
/** Human-readable, e.g. "Feb 1, 2024" */
|
|
325
|
+
publishedAtFormatted: string;
|
|
326
|
+
/** ISO 8601 string */
|
|
327
|
+
updatedAt: string;
|
|
328
|
+
|
|
329
|
+
// Source (for link/quote types)
|
|
330
|
+
sourceUrl?: string;
|
|
331
|
+
sourceName?: string;
|
|
332
|
+
sourceDomain?: string;
|
|
333
|
+
|
|
334
|
+
// Media — URLs pre-computed
|
|
335
|
+
media: MediaView[];
|
|
336
|
+
|
|
337
|
+
// Thread context
|
|
338
|
+
replyToId?: number;
|
|
339
|
+
threadRootId?: number;
|
|
340
|
+
|
|
341
|
+
// Raw content (for forms/editing, not typical theme use)
|
|
342
|
+
content?: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Render-ready media data for theme components.
|
|
347
|
+
* URLs are pre-computed — no lib/ imports needed.
|
|
348
|
+
*/
|
|
349
|
+
export interface MediaView {
|
|
350
|
+
id: string;
|
|
351
|
+
/** Full-size URL, pre-computed */
|
|
352
|
+
url: string;
|
|
353
|
+
/** Thumbnail URL, pre-computed */
|
|
354
|
+
thumbnailUrl: string;
|
|
355
|
+
mimeType: string;
|
|
356
|
+
altText?: string;
|
|
357
|
+
width?: number;
|
|
358
|
+
height?: number;
|
|
359
|
+
size?: number;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Render-ready navigation link for theme components.
|
|
364
|
+
* Active/external state pre-computed.
|
|
365
|
+
*/
|
|
366
|
+
export interface NavLinkView {
|
|
367
|
+
id: number;
|
|
368
|
+
label: string;
|
|
369
|
+
url: string;
|
|
370
|
+
/** Pre-computed based on currentPath */
|
|
371
|
+
isActive: boolean;
|
|
372
|
+
/** Pre-computed: starts with http(s):// */
|
|
373
|
+
isExternal: boolean;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Render-ready search result for theme components.
|
|
378
|
+
*/
|
|
379
|
+
export interface SearchResultView {
|
|
380
|
+
post: PostView;
|
|
381
|
+
rank: number;
|
|
382
|
+
snippet?: string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Render-ready timeline item for theme components.
|
|
387
|
+
*/
|
|
388
|
+
export interface TimelineItemView {
|
|
389
|
+
post: PostView;
|
|
390
|
+
threadPreview?: {
|
|
391
|
+
replies: PostView[];
|
|
392
|
+
totalReplyCount: number;
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Typed archive group with pre-formatted label.
|
|
398
|
+
*/
|
|
399
|
+
export interface ArchiveGroup {
|
|
400
|
+
/** e.g. "2024" */
|
|
401
|
+
year: string;
|
|
402
|
+
/** e.g. "02" */
|
|
403
|
+
month: string;
|
|
404
|
+
/** Pre-formatted, e.g. "February 2024" */
|
|
405
|
+
label: string;
|
|
406
|
+
posts: PostView[];
|
|
407
|
+
}
|
|
408
|
+
|
|
294
409
|
// =============================================================================
|
|
295
410
|
// Configuration Types
|
|
296
411
|
// =============================================================================
|
|
@@ -299,35 +414,93 @@ import type { FC, PropsWithChildren } from "hono/jsx";
|
|
|
299
414
|
import type { ColorTheme } from "./theme/color-themes.js";
|
|
300
415
|
|
|
301
416
|
/**
|
|
302
|
-
*
|
|
417
|
+
* Search result from FTS5
|
|
303
418
|
*/
|
|
304
|
-
export interface
|
|
305
|
-
|
|
306
|
-
|
|
419
|
+
export interface SearchResult {
|
|
420
|
+
post: Post;
|
|
421
|
+
/** FTS5 rank score (lower is better) */
|
|
422
|
+
rank: number;
|
|
423
|
+
/** Highlighted snippet from content */
|
|
424
|
+
snippet?: string;
|
|
307
425
|
}
|
|
308
426
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
427
|
+
// =============================================================================
|
|
428
|
+
// Site Layout Props
|
|
429
|
+
// =============================================================================
|
|
430
|
+
|
|
431
|
+
export interface SiteLayoutProps {
|
|
432
|
+
siteName: string;
|
|
433
|
+
links: NavLinkView[];
|
|
434
|
+
currentPath: string;
|
|
313
435
|
}
|
|
314
436
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
437
|
+
// =============================================================================
|
|
438
|
+
// Page-Level Props
|
|
439
|
+
// =============================================================================
|
|
440
|
+
|
|
441
|
+
/** Props for the home page component */
|
|
442
|
+
export interface HomePageProps {
|
|
443
|
+
items: TimelineItemView[];
|
|
444
|
+
hasMore: boolean;
|
|
445
|
+
nextCursor?: number;
|
|
446
|
+
theme?: ThemeComponents;
|
|
318
447
|
}
|
|
319
448
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
449
|
+
/** Props for the single post page component */
|
|
450
|
+
export interface PostPageProps {
|
|
451
|
+
post: PostView;
|
|
452
|
+
theme?: ThemeComponents;
|
|
324
453
|
}
|
|
325
454
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
455
|
+
/** Props for the custom page component */
|
|
456
|
+
export interface SinglePageProps {
|
|
457
|
+
page: PostView;
|
|
458
|
+
theme?: ThemeComponents;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Props for the archive page component */
|
|
462
|
+
export interface ArchivePageProps {
|
|
463
|
+
groups: ArchiveGroup[];
|
|
464
|
+
hasMore: boolean;
|
|
465
|
+
nextCursor?: number;
|
|
466
|
+
type?: PostType;
|
|
467
|
+
theme?: ThemeComponents;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Props for the search page component */
|
|
471
|
+
export interface SearchPageProps {
|
|
472
|
+
query: string;
|
|
473
|
+
results: SearchResultView[];
|
|
474
|
+
error?: string;
|
|
475
|
+
hasMore: boolean;
|
|
476
|
+
page: number;
|
|
477
|
+
theme?: ThemeComponents;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Props for the collection page component */
|
|
481
|
+
export interface CollectionPageProps {
|
|
482
|
+
collection: Collection;
|
|
483
|
+
posts: PostView[];
|
|
484
|
+
theme?: ThemeComponents;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// =============================================================================
|
|
488
|
+
// Feed Data Types
|
|
489
|
+
// =============================================================================
|
|
490
|
+
|
|
491
|
+
/** Data passed to RSS/Atom feed renderers */
|
|
492
|
+
export interface FeedData {
|
|
493
|
+
siteName: string;
|
|
494
|
+
siteDescription: string;
|
|
495
|
+
siteUrl: string;
|
|
496
|
+
siteLanguage: string;
|
|
497
|
+
posts: PostView[];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Data passed to sitemap renderers */
|
|
501
|
+
export interface SitemapData {
|
|
502
|
+
siteUrl: string;
|
|
503
|
+
posts: PostView[];
|
|
331
504
|
}
|
|
332
505
|
|
|
333
506
|
// =============================================================================
|
|
@@ -336,42 +509,42 @@ export interface EmptyStateProps {
|
|
|
336
509
|
|
|
337
510
|
/** Props for per-type timeline cards */
|
|
338
511
|
export interface TimelineCardProps {
|
|
339
|
-
post:
|
|
512
|
+
post: PostView;
|
|
340
513
|
compact?: boolean;
|
|
341
514
|
}
|
|
342
515
|
|
|
343
516
|
/** Props for thread inline preview */
|
|
344
517
|
export interface ThreadPreviewProps {
|
|
345
|
-
rootPost:
|
|
346
|
-
previewReplies:
|
|
518
|
+
rootPost: PostView;
|
|
519
|
+
previewReplies: PostView[];
|
|
347
520
|
totalReplyCount: number;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
/** Data structure for a single timeline item */
|
|
351
|
-
export interface TimelineItemData {
|
|
352
|
-
post: PostWithMedia;
|
|
353
|
-
threadPreview?: {
|
|
354
|
-
replies: PostWithMedia[];
|
|
355
|
-
totalReplyCount: number;
|
|
356
|
-
};
|
|
521
|
+
theme?: ThemeComponents;
|
|
357
522
|
}
|
|
358
523
|
|
|
359
524
|
/** Props for the timeline feed wrapper */
|
|
360
525
|
export interface TimelineFeedProps {
|
|
361
|
-
items:
|
|
526
|
+
items: TimelineItemView[];
|
|
362
527
|
hasMore: boolean;
|
|
363
528
|
nextCursor?: number;
|
|
529
|
+
theme?: ThemeComponents;
|
|
364
530
|
}
|
|
365
531
|
|
|
366
532
|
/**
|
|
367
533
|
* Theme component overrides
|
|
368
534
|
*/
|
|
369
535
|
export interface ThemeComponents {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
536
|
+
// Layout
|
|
537
|
+
SiteLayout?: FC<PropsWithChildren<SiteLayoutProps>>;
|
|
538
|
+
|
|
539
|
+
// Pages
|
|
540
|
+
HomePage?: FC<HomePageProps>;
|
|
541
|
+
PostPage?: FC<PostPageProps>;
|
|
542
|
+
SinglePage?: FC<SinglePageProps>;
|
|
543
|
+
ArchivePage?: FC<ArchivePageProps>;
|
|
544
|
+
SearchPage?: FC<SearchPageProps>;
|
|
545
|
+
CollectionPage?: FC<CollectionPageProps>;
|
|
546
|
+
|
|
547
|
+
// Timeline sub-components
|
|
375
548
|
NoteCard?: FC<TimelineCardProps>;
|
|
376
549
|
ArticleCard?: FC<TimelineCardProps>;
|
|
377
550
|
LinkCard?: FC<TimelineCardProps>;
|
|
@@ -379,6 +552,48 @@ export interface ThemeComponents {
|
|
|
379
552
|
ImageCard?: FC<TimelineCardProps>;
|
|
380
553
|
ThreadPreview?: FC<ThreadPreviewProps>;
|
|
381
554
|
TimelineFeed?: FC<TimelineFeedProps>;
|
|
555
|
+
|
|
556
|
+
// Shared sub-components (re-exported real prop types from component files)
|
|
557
|
+
Pagination?: FC<PaginationComponentProps>;
|
|
558
|
+
PagePagination?: FC<PagePaginationComponentProps>;
|
|
559
|
+
EmptyState?: FC<EmptyStateComponentProps>;
|
|
560
|
+
MediaGallery?: FC<MediaGalleryComponentProps>;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Real component prop types (re-exported from component files via index.ts).
|
|
565
|
+
* These are provided here as aliases to avoid circular imports in types.ts.
|
|
566
|
+
* The canonical definitions live in the component files.
|
|
567
|
+
*/
|
|
568
|
+
|
|
569
|
+
/** @see Pagination component in theme/components/Pagination.tsx */
|
|
570
|
+
export interface PaginationComponentProps {
|
|
571
|
+
baseUrl: string;
|
|
572
|
+
hasMore: boolean;
|
|
573
|
+
nextCursor?: number | string;
|
|
574
|
+
prevCursor?: number | string;
|
|
575
|
+
cursorParam?: string;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** @see PagePagination component in theme/components/Pagination.tsx */
|
|
579
|
+
export interface PagePaginationComponentProps {
|
|
580
|
+
baseUrl: string;
|
|
581
|
+
currentPage: number;
|
|
582
|
+
hasMore: boolean;
|
|
583
|
+
pageParam?: string;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** @see EmptyState component in theme/components/EmptyState.tsx */
|
|
587
|
+
export interface EmptyStateComponentProps {
|
|
588
|
+
message: string;
|
|
589
|
+
ctaText?: string;
|
|
590
|
+
ctaHref?: string;
|
|
591
|
+
centered?: boolean;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** @see MediaGallery component in theme/components/MediaGallery.tsx */
|
|
595
|
+
export interface MediaGalleryComponentProps {
|
|
596
|
+
attachments: MediaView[];
|
|
382
597
|
}
|
|
383
598
|
|
|
384
599
|
/**
|
|
@@ -389,6 +604,15 @@ export interface JantTheme {
|
|
|
389
604
|
name?: string;
|
|
390
605
|
/** Component overrides */
|
|
391
606
|
components?: ThemeComponents;
|
|
607
|
+
/** Feed renderer overrides (RSS, Atom, Sitemap) */
|
|
608
|
+
feed?: {
|
|
609
|
+
/** Custom RSS 2.0 renderer — returns XML string */
|
|
610
|
+
rss?: (data: FeedData) => string;
|
|
611
|
+
/** Custom Atom renderer — returns XML string */
|
|
612
|
+
atom?: (data: FeedData) => string;
|
|
613
|
+
/** Custom Sitemap renderer — returns XML string */
|
|
614
|
+
sitemap?: (data: SitemapData) => string;
|
|
615
|
+
};
|
|
392
616
|
/** CSS variable overrides (highest priority, always applied) */
|
|
393
617
|
cssVariables?: Record<string, string>;
|
|
394
618
|
/** Replace built-in color themes with a custom list */
|