@jant/core 0.0.1

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 (93) hide show
  1. package/bin/jant.js +188 -0
  2. package/drizzle.config.ts +10 -0
  3. package/lingui.config.ts +16 -0
  4. package/package.json +116 -0
  5. package/src/app.tsx +377 -0
  6. package/src/assets/datastar.min.js +8 -0
  7. package/src/auth.ts +38 -0
  8. package/src/client.ts +6 -0
  9. package/src/db/index.ts +14 -0
  10. package/src/db/migrations/0000_solid_moon_knight.sql +118 -0
  11. package/src/db/migrations/0001_add_search_fts.sql +40 -0
  12. package/src/db/migrations/0002_collection_path.sql +2 -0
  13. package/src/db/migrations/0003_collection_path_nullable.sql +21 -0
  14. package/src/db/migrations/0004_media_uuid.sql +35 -0
  15. package/src/db/migrations/meta/0000_snapshot.json +784 -0
  16. package/src/db/migrations/meta/_journal.json +41 -0
  17. package/src/db/schema.ts +159 -0
  18. package/src/i18n/EXAMPLES.md +235 -0
  19. package/src/i18n/README.md +296 -0
  20. package/src/i18n/Trans.tsx +31 -0
  21. package/src/i18n/context.tsx +101 -0
  22. package/src/i18n/detect.ts +100 -0
  23. package/src/i18n/i18n.ts +62 -0
  24. package/src/i18n/index.ts +65 -0
  25. package/src/i18n/locales/en.po +875 -0
  26. package/src/i18n/locales/en.ts +1 -0
  27. package/src/i18n/locales/zh-Hans.po +875 -0
  28. package/src/i18n/locales/zh-Hans.ts +1 -0
  29. package/src/i18n/locales/zh-Hant.po +875 -0
  30. package/src/i18n/locales/zh-Hant.ts +1 -0
  31. package/src/i18n/locales.ts +14 -0
  32. package/src/i18n/middleware.ts +59 -0
  33. package/src/index.ts +42 -0
  34. package/src/lib/assets.ts +47 -0
  35. package/src/lib/constants.ts +67 -0
  36. package/src/lib/image.ts +107 -0
  37. package/src/lib/index.ts +9 -0
  38. package/src/lib/markdown.ts +93 -0
  39. package/src/lib/schemas.ts +92 -0
  40. package/src/lib/sqid.ts +79 -0
  41. package/src/lib/sse.ts +152 -0
  42. package/src/lib/time.ts +117 -0
  43. package/src/lib/url.ts +107 -0
  44. package/src/middleware/auth.ts +59 -0
  45. package/src/routes/api/posts.ts +127 -0
  46. package/src/routes/api/search.ts +53 -0
  47. package/src/routes/api/upload.ts +240 -0
  48. package/src/routes/dash/collections.tsx +341 -0
  49. package/src/routes/dash/index.tsx +89 -0
  50. package/src/routes/dash/media.tsx +551 -0
  51. package/src/routes/dash/pages.tsx +245 -0
  52. package/src/routes/dash/posts.tsx +202 -0
  53. package/src/routes/dash/redirects.tsx +155 -0
  54. package/src/routes/dash/settings.tsx +93 -0
  55. package/src/routes/feed/rss.ts +119 -0
  56. package/src/routes/feed/sitemap.ts +75 -0
  57. package/src/routes/pages/archive.tsx +223 -0
  58. package/src/routes/pages/collection.tsx +79 -0
  59. package/src/routes/pages/home.tsx +93 -0
  60. package/src/routes/pages/page.tsx +64 -0
  61. package/src/routes/pages/post.tsx +81 -0
  62. package/src/routes/pages/search.tsx +162 -0
  63. package/src/services/collection.ts +180 -0
  64. package/src/services/index.ts +40 -0
  65. package/src/services/media.ts +97 -0
  66. package/src/services/post.ts +279 -0
  67. package/src/services/redirect.ts +74 -0
  68. package/src/services/search.ts +117 -0
  69. package/src/services/settings.ts +76 -0
  70. package/src/theme/components/ActionButtons.tsx +98 -0
  71. package/src/theme/components/CrudPageHeader.tsx +48 -0
  72. package/src/theme/components/DangerZone.tsx +77 -0
  73. package/src/theme/components/EmptyState.tsx +56 -0
  74. package/src/theme/components/ListItemRow.tsx +24 -0
  75. package/src/theme/components/PageForm.tsx +114 -0
  76. package/src/theme/components/Pagination.tsx +196 -0
  77. package/src/theme/components/PostForm.tsx +122 -0
  78. package/src/theme/components/PostList.tsx +68 -0
  79. package/src/theme/components/ThreadView.tsx +118 -0
  80. package/src/theme/components/TypeBadge.tsx +28 -0
  81. package/src/theme/components/VisibilityBadge.tsx +33 -0
  82. package/src/theme/components/index.ts +12 -0
  83. package/src/theme/index.ts +24 -0
  84. package/src/theme/layouts/BaseLayout.tsx +49 -0
  85. package/src/theme/layouts/DashLayout.tsx +108 -0
  86. package/src/theme/layouts/index.ts +2 -0
  87. package/src/theme/styles/main.css +52 -0
  88. package/src/types.ts +222 -0
  89. package/static/assets/datastar.min.js +7 -0
  90. package/static/assets/image-processor.js +234 -0
  91. package/tsconfig.json +16 -0
  92. package/vite.config.ts +82 -0
  93. package/wrangler.toml +21 -0
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Thread View Component
3
+ *
4
+ * Displays a thread of posts with reply chain visualization
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "../../i18n/index.js";
9
+ import type { Post } from "../../types.js";
10
+ import * as sqid from "../../lib/sqid.js";
11
+ import * as time from "../../lib/time.js";
12
+
13
+ export interface ThreadViewProps {
14
+ /** All posts in the thread, ordered by createdAt */
15
+ posts: Post[];
16
+ /** ID of the currently viewed post (to highlight) */
17
+ currentPostId: number;
18
+ }
19
+
20
+ const ThreadPost: FC<{
21
+ post: Post;
22
+ isCurrent: boolean;
23
+ isRoot: boolean;
24
+ }> = ({ post, isCurrent, isRoot }) => {
25
+ const { t } = useLingui();
26
+ return (
27
+ <article
28
+ id={`post-${post.id}`}
29
+ class={`h-entry p-4 rounded-lg border ${
30
+ isCurrent
31
+ ? "border-primary bg-primary/5 ring-2 ring-primary/20"
32
+ : "border-border hover:border-muted-foreground/30"
33
+ }`}
34
+ >
35
+ {post.title && (
36
+ <h2 class="p-name text-lg font-medium mb-2">
37
+ <a href={`/p/${sqid.encode(post.id)}`} class="u-url hover:underline">
38
+ {post.title}
39
+ </a>
40
+ </h2>
41
+ )}
42
+
43
+ <div
44
+ class="e-content prose prose-sm"
45
+ dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
46
+ />
47
+
48
+ <footer class="mt-3 flex items-center gap-3 text-sm text-muted-foreground">
49
+ <time class="dt-published" datetime={time.toISOString(post.publishedAt)}>
50
+ {time.formatDate(post.publishedAt)}
51
+ </time>
52
+ {isRoot && (
53
+ <span class="text-xs">
54
+ {t({ message: "Thread start", comment: "@context: Thread view indicator - first post in thread" })}
55
+ </span>
56
+ )}
57
+ {!isCurrent && (
58
+ <a
59
+ href={`/p/${sqid.encode(post.id)}`}
60
+ class="text-xs hover:underline"
61
+ >
62
+ {t({ message: "Permalink", comment: "@context: Link to individual post in thread" })}
63
+ </a>
64
+ )}
65
+ </footer>
66
+ </article>
67
+ );
68
+ };
69
+
70
+ export const ThreadView: FC<ThreadViewProps> = ({ posts, currentPostId }) => {
71
+ const { t } = useLingui();
72
+ if (posts.length === 0) {
73
+ return null;
74
+ }
75
+
76
+ const rootPost = posts[0];
77
+ const isThread = posts.length > 1;
78
+
79
+ // Single post, no thread
80
+ if (!isThread) {
81
+ return (
82
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Early return for empty array at line 73 guarantees posts[0] exists
83
+ <ThreadPost post={rootPost!} isCurrent={true} isRoot={false} />
84
+ );
85
+ }
86
+
87
+ const threadLabel = posts.length === 1
88
+ ? t({ message: "Thread with 1 post", comment: "@context: Thread view header - single post" })
89
+ : t({ message: "Thread with {count} posts", comment: "@context: Thread view header - multiple posts", values: { count: String(posts.length) } });
90
+
91
+ return (
92
+ <div class="thread-view">
93
+ <div class="mb-4 text-sm text-muted-foreground">
94
+ {threadLabel}
95
+ </div>
96
+
97
+ <div class="flex flex-col gap-3">
98
+ {posts.map((post, index) => (
99
+ <div key={post.id} class="relative">
100
+ {/* Connection line */}
101
+ {index > 0 && (
102
+ <div class="absolute left-6 -top-3 w-0.5 h-3 bg-border" />
103
+ )}
104
+ {index < posts.length - 1 && (
105
+ <div class="absolute left-6 -bottom-3 w-0.5 h-3 bg-border" />
106
+ )}
107
+
108
+ <ThreadPost
109
+ post={post}
110
+ isCurrent={post.id === currentPostId}
111
+ isRoot={index === 0}
112
+ />
113
+ </div>
114
+ ))}
115
+ </div>
116
+ </div>
117
+ );
118
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Type Badge Component
3
+ *
4
+ * Displays a badge indicating the type of a post (note, article, link, etc.)
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "../../i18n/index.js";
9
+ import type { PostType } from "../../types.js";
10
+
11
+ export interface TypeBadgeProps {
12
+ type: PostType;
13
+ }
14
+
15
+ export const TypeBadge: FC<TypeBadgeProps> = ({ type }) => {
16
+ const { t } = useLingui();
17
+
18
+ const labels: Record<PostType, string> = {
19
+ note: t({ message: "Note", comment: "@context: Post type badge - note" }),
20
+ article: t({ message: "Article", comment: "@context: Post type badge - article" }),
21
+ link: t({ message: "Link", comment: "@context: Post type badge - link" }),
22
+ quote: t({ message: "Quote", comment: "@context: Post type badge - quote" }),
23
+ image: t({ message: "Image", comment: "@context: Post type badge - image" }),
24
+ page: t({ message: "Page", comment: "@context: Post type badge - page" }),
25
+ };
26
+
27
+ return <span class="badge-outline">{labels[type]}</span>;
28
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Visibility Badge Component
3
+ *
4
+ * Displays a badge indicating the visibility level of a post
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "../../i18n/index.js";
9
+ import type { Visibility } from "../../types.js";
10
+
11
+ export interface VisibilityBadgeProps {
12
+ visibility: Visibility;
13
+ }
14
+
15
+ export const VisibilityBadge: FC<VisibilityBadgeProps> = ({ visibility }) => {
16
+ const { t } = useLingui();
17
+
18
+ const variants: Record<Visibility, string> = {
19
+ featured: "badge-primary",
20
+ quiet: "badge-secondary",
21
+ unlisted: "badge-outline",
22
+ draft: "badge-outline",
23
+ };
24
+
25
+ const labels: Record<Visibility, string> = {
26
+ featured: t({ message: "Featured", comment: "@context: Post visibility badge - featured" }),
27
+ quiet: t({ message: "Quiet", comment: "@context: Post visibility badge - normal" }),
28
+ unlisted: t({ message: "Unlisted", comment: "@context: Post visibility badge - unlisted" }),
29
+ draft: t({ message: "Draft", comment: "@context: Post visibility badge - draft" }),
30
+ };
31
+
32
+ return <span class={variants[visibility]}>{labels[visibility]}</span>;
33
+ };
@@ -0,0 +1,12 @@
1
+ export { ActionButtons, type ActionButtonsProps } from "./ActionButtons.js";
2
+ export { CrudPageHeader, type CrudPageHeaderProps } from "./CrudPageHeader.js";
3
+ export { DangerZone, type DangerZoneProps } from "./DangerZone.js";
4
+ export { EmptyState, type EmptyStateProps } from "./EmptyState.js";
5
+ export { ListItemRow, type ListItemRowProps } from "./ListItemRow.js";
6
+ export { PageForm, type PageFormProps } from "./PageForm.js";
7
+ export { Pagination, LoadMore, PagePagination, type PaginationProps, type LoadMoreProps, type PagePaginationProps } from "./Pagination.js";
8
+ export { PostForm, type PostFormProps } from "./PostForm.js";
9
+ export { PostList, type PostListProps } from "./PostList.js";
10
+ export { ThreadView, type ThreadViewProps } from "./ThreadView.js";
11
+ export { TypeBadge, type TypeBadgeProps } from "./TypeBadge.js";
12
+ export { VisibilityBadge, type VisibilityBadgeProps } from "./VisibilityBadge.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Jant Theme Components
3
+ *
4
+ * These components can be imported for wrapping/extending:
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { PostCard } from "@jant/core/theme";
9
+ *
10
+ * export function MyPostCard(props) {
11
+ * return (
12
+ * <div class="my-wrapper">
13
+ * <PostCard {...props} />
14
+ * </div>
15
+ * );
16
+ * }
17
+ * ```
18
+ */
19
+
20
+ // Layout components
21
+ export * from "./layouts/index.js";
22
+
23
+ // UI components
24
+ export * from "./components/index.js";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Base HTML Layout
3
+ *
4
+ * Provides the HTML shell with meta tags, styles, and scripts.
5
+ * If Context is provided, automatically wraps children with I18nProvider.
6
+ */
7
+
8
+ import type { FC, PropsWithChildren } from "hono/jsx";
9
+ import type { Context } from "hono";
10
+ import { getAssets } from "../../lib/assets.js";
11
+ import { I18nProvider } from "../../i18n/index.js";
12
+
13
+ export interface BaseLayoutProps {
14
+ title: string;
15
+ description?: string;
16
+ lang?: string;
17
+ c?: Context;
18
+ }
19
+
20
+ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
21
+ title,
22
+ description,
23
+ lang = "en",
24
+ c,
25
+ children,
26
+ }) => {
27
+ // Get assets at render time (supports runtime manifest loading)
28
+ const assets = getAssets();
29
+
30
+ // Automatically wrap with I18nProvider if Context is provided
31
+ const content = c ? <I18nProvider c={c}>{children}</I18nProvider> : children;
32
+
33
+ return (
34
+ <html lang={lang}>
35
+ <head>
36
+ <meta charset="UTF-8" />
37
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
38
+ <title>{title}</title>
39
+ {description && <meta name="description" content={description} />}
40
+ <link rel="stylesheet" href={assets.styles} />
41
+ <script type="module" src={assets.client} defer />
42
+ <script type="module" src={assets.datastar} defer />
43
+ </head>
44
+ <body class="bg-background text-foreground antialiased">
45
+ {content}
46
+ </body>
47
+ </html>
48
+ );
49
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Dashboard Layout
3
+ *
4
+ * Layout for admin dashboard pages
5
+ */
6
+
7
+ import type { FC, PropsWithChildren } from "hono/jsx";
8
+ import type { Context } from "hono";
9
+ import { useLingui } from "../../i18n/index.js";
10
+ import { BaseLayout } from "./BaseLayout.js";
11
+
12
+ export interface DashLayoutProps {
13
+ c: Context;
14
+ title: string;
15
+ siteName: string;
16
+ currentPath?: string;
17
+ }
18
+
19
+ function DashLayoutContent({
20
+ siteName,
21
+ currentPath,
22
+ children,
23
+ }: PropsWithChildren<Omit<DashLayoutProps, "c" | "title">>) {
24
+ const { t } = useLingui();
25
+
26
+ const isActive = (path: string, match?: RegExp) => {
27
+ if (!currentPath) return false;
28
+ if (match) return match.test(currentPath);
29
+ return currentPath === path;
30
+ };
31
+
32
+ const navClass = (path: string, match?: RegExp) =>
33
+ `justify-start px-3 py-2 text-sm rounded-md ${
34
+ isActive(path, match)
35
+ ? "bg-accent text-accent-foreground font-medium"
36
+ : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
37
+ }`;
38
+
39
+ return (
40
+ <div class="min-h-screen">
41
+ {/* Header */}
42
+ <header class="border-b bg-card">
43
+ <div class="container flex h-14 items-center justify-between">
44
+ <a href="/dash" class="font-semibold">
45
+ {siteName}
46
+ </a>
47
+ <nav class="flex items-center gap-4">
48
+ <a href="/" class="text-sm text-muted-foreground hover:text-foreground">
49
+ {t({ message: "View Site", comment: "@context: Dashboard header link to view the public site" })}
50
+ </a>
51
+ <a href="/signout" class="text-sm text-muted-foreground hover:text-foreground">
52
+ {t({ message: "Sign Out", comment: "@context: Dashboard header link to sign out" })}
53
+ </a>
54
+ </nav>
55
+ </div>
56
+ </header>
57
+
58
+ {/* Sidebar + Main */}
59
+ <div class="container flex gap-8 py-8">
60
+ {/* Sidebar */}
61
+ <aside class="w-48 shrink-0">
62
+ <nav class="flex flex-col gap-1">
63
+ <a href="/dash" class={navClass("/dash", /^\/dash$/)}>
64
+ {t({ message: "Dashboard", comment: "@context: Dashboard navigation - main dashboard page" })}
65
+ </a>
66
+ <a href="/dash/posts" class={navClass("/dash/posts", /^\/dash\/posts/)}>
67
+ {t({ message: "Posts", comment: "@context: Dashboard navigation - posts management" })}
68
+ </a>
69
+ <a href="/dash/pages" class={navClass("/dash/pages", /^\/dash\/pages/)}>
70
+ {t({ message: "Pages", comment: "@context: Dashboard navigation - pages management" })}
71
+ </a>
72
+ <a href="/dash/media" class={navClass("/dash/media", /^\/dash\/media/)}>
73
+ {t({ message: "Media", comment: "@context: Dashboard navigation - media library" })}
74
+ </a>
75
+ <a href="/dash/collections" class={navClass("/dash/collections", /^\/dash\/collections/)}>
76
+ {t({ message: "Collections", comment: "@context: Dashboard navigation - collections management" })}
77
+ </a>
78
+ <a href="/dash/redirects" class={navClass("/dash/redirects", /^\/dash\/redirects/)}>
79
+ {t({ message: "Redirects", comment: "@context: Dashboard navigation - URL redirects" })}
80
+ </a>
81
+ <a href="/dash/settings" class={navClass("/dash/settings", /^\/dash\/settings/)}>
82
+ {t({ message: "Settings", comment: "@context: Dashboard navigation - site settings" })}
83
+ </a>
84
+ </nav>
85
+ </aside>
86
+
87
+ {/* Main content */}
88
+ <main class="flex-1 min-w-0">{children}</main>
89
+ </div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ export const DashLayout: FC<PropsWithChildren<DashLayoutProps>> = ({
95
+ c,
96
+ title,
97
+ siteName,
98
+ currentPath,
99
+ children,
100
+ }) => {
101
+ return (
102
+ <BaseLayout title={`${title} - ${siteName}`} c={c}>
103
+ <DashLayoutContent siteName={siteName} currentPath={currentPath}>
104
+ {children}
105
+ </DashLayoutContent>
106
+ </BaseLayout>
107
+ );
108
+ };
@@ -0,0 +1,2 @@
1
+ export { BaseLayout, type BaseLayoutProps } from "./BaseLayout.js";
2
+ export { DashLayout, type DashLayoutProps } from "./DashLayout.js";
@@ -0,0 +1,52 @@
1
+ @import "tailwindcss";
2
+ @import "basecoat-css";
3
+
4
+ /* Scan source files for Tailwind classes */
5
+ @source "../../**/*.tsx";
6
+ @source "../../**/*.ts";
7
+
8
+ /* Custom theme variables */
9
+ :root {
10
+ --radius: 0.5rem;
11
+ }
12
+
13
+ /* Custom utilities */
14
+ @layer utilities {
15
+ .container {
16
+ @apply mx-auto max-w-2xl px-4;
17
+ }
18
+ }
19
+
20
+ /* Badge component */
21
+ @layer components {
22
+ .badge {
23
+ @apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium;
24
+ background-color: var(--color-secondary);
25
+ color: var(--color-secondary-foreground);
26
+ }
27
+
28
+ .badge-primary {
29
+ @apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium;
30
+ background-color: var(--color-primary);
31
+ color: var(--color-primary-foreground);
32
+ }
33
+
34
+ .badge-secondary {
35
+ @apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium;
36
+ background-color: var(--color-secondary);
37
+ color: var(--color-secondary-foreground);
38
+ }
39
+
40
+ .badge-outline {
41
+ @apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium;
42
+ @apply border bg-transparent;
43
+ border-color: var(--color-border);
44
+ color: var(--color-foreground);
45
+ }
46
+
47
+ .badge-destructive {
48
+ @apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium;
49
+ background-color: var(--color-destructive);
50
+ color: white;
51
+ }
52
+ }
package/src/types.ts ADDED
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Jant Type Definitions
3
+ */
4
+
5
+ // =============================================================================
6
+ // Content Types
7
+ // =============================================================================
8
+
9
+ export const POST_TYPES = ["note", "article", "link", "quote", "image", "page"] as const;
10
+ export type PostType = (typeof POST_TYPES)[number];
11
+
12
+ export const VISIBILITY_LEVELS = ["featured", "quiet", "unlisted", "draft"] as const;
13
+ export type Visibility = (typeof VISIBILITY_LEVELS)[number];
14
+
15
+
16
+ // =============================================================================
17
+ // Cloudflare Bindings
18
+ // =============================================================================
19
+
20
+ export interface Bindings {
21
+ DB: D1Database;
22
+ R2?: R2Bucket;
23
+ SITE_URL: string;
24
+ AUTH_SECRET?: string;
25
+ R2_PUBLIC_URL?: string;
26
+ IMAGE_TRANSFORM_URL?: string;
27
+ }
28
+
29
+
30
+ // =============================================================================
31
+ // Entity Types
32
+ // =============================================================================
33
+
34
+ export interface Post {
35
+ id: number;
36
+ type: PostType;
37
+ visibility: Visibility;
38
+ title: string | null;
39
+ path: string | null;
40
+ content: string | null;
41
+ contentHtml: string | null;
42
+ sourceUrl: string | null;
43
+ sourceName: string | null;
44
+ sourceDomain: string | null;
45
+ replyToId: number | null;
46
+ threadId: number | null;
47
+ deletedAt: number | null;
48
+ publishedAt: number;
49
+ createdAt: number;
50
+ updatedAt: number;
51
+ }
52
+
53
+ export interface Media {
54
+ id: string; // UUIDv7
55
+ postId: number | null;
56
+ filename: string;
57
+ originalName: string;
58
+ mimeType: string;
59
+ size: number;
60
+ r2Key: string;
61
+ width: number | null;
62
+ height: number | null;
63
+ alt: string | null;
64
+ createdAt: number;
65
+ }
66
+
67
+ export interface Collection {
68
+ id: number;
69
+ title: string;
70
+ path: string | null;
71
+ description: string | null;
72
+ createdAt: number;
73
+ updatedAt: number;
74
+ }
75
+
76
+ export interface PostCollection {
77
+ postId: number;
78
+ collectionId: number;
79
+ addedAt: number;
80
+ }
81
+
82
+ export interface Redirect {
83
+ id: number;
84
+ fromPath: string;
85
+ toPath: string;
86
+ type: 301 | 302;
87
+ createdAt: number;
88
+ }
89
+
90
+ export interface Setting {
91
+ key: string;
92
+ value: string;
93
+ updatedAt: number;
94
+ }
95
+
96
+ // =============================================================================
97
+ // Operation Types
98
+ // =============================================================================
99
+
100
+ export interface CreatePost {
101
+ type: PostType;
102
+ visibility?: Visibility;
103
+ title?: string;
104
+ path?: string;
105
+ content?: string;
106
+ sourceUrl?: string;
107
+ sourceName?: string;
108
+ replyToId?: number;
109
+ publishedAt?: number;
110
+ }
111
+
112
+ export interface UpdatePost {
113
+ type?: PostType;
114
+ visibility?: Visibility;
115
+ title?: string | null;
116
+ path?: string | null;
117
+ content?: string | null;
118
+ sourceUrl?: string | null;
119
+ sourceName?: string | null;
120
+ publishedAt?: number;
121
+ }
122
+
123
+ // =============================================================================
124
+ // Configuration Types
125
+ // =============================================================================
126
+
127
+ import type { FC, PropsWithChildren } from "hono/jsx";
128
+
129
+ /**
130
+ * Props for overridable theme components
131
+ */
132
+ export interface BaseLayoutProps extends PropsWithChildren {
133
+ title?: string;
134
+ description?: string;
135
+ }
136
+
137
+ export interface PostCardProps {
138
+ post: Post;
139
+ showExcerpt?: boolean;
140
+ showDate?: boolean;
141
+ }
142
+
143
+ export interface PostListProps {
144
+ posts: Post[];
145
+ emptyMessage?: string;
146
+ }
147
+
148
+ export interface PaginationProps {
149
+ currentPage: number;
150
+ totalPages: number;
151
+ basePath: string;
152
+ }
153
+
154
+ export interface EmptyStateProps {
155
+ title: string;
156
+ description?: string;
157
+ actionLabel?: string;
158
+ actionHref?: string;
159
+ }
160
+
161
+ /**
162
+ * Theme component overrides
163
+ */
164
+ export interface ThemeComponents {
165
+ BaseLayout?: FC<BaseLayoutProps>;
166
+ PostCard?: FC<PostCardProps>;
167
+ PostList?: FC<PostListProps>;
168
+ Pagination?: FC<PaginationProps>;
169
+ EmptyState?: FC<EmptyStateProps>;
170
+ }
171
+
172
+ /**
173
+ * Theme configuration
174
+ */
175
+ export interface JantTheme {
176
+ /** Theme name */
177
+ name?: string;
178
+ /** Component overrides */
179
+ components?: ThemeComponents;
180
+ /** CSS variable overrides */
181
+ cssVariables?: Record<string, string>;
182
+ }
183
+
184
+ /**
185
+ * Site configuration
186
+ */
187
+ export interface SiteConfig {
188
+ /** Site name */
189
+ name?: string;
190
+ /** Site description */
191
+ description?: string;
192
+ /** Default language */
193
+ language?: string;
194
+ /** Site URL (usually set via env) */
195
+ url?: string;
196
+ }
197
+
198
+ /**
199
+ * Feature toggles
200
+ */
201
+ export interface FeatureConfig {
202
+ /** Enable search (default: true) */
203
+ search?: boolean;
204
+ /** Enable RSS feed (default: true) */
205
+ rss?: boolean;
206
+ /** Enable sitemap (default: true) */
207
+ sitemap?: boolean;
208
+ /** Enable i18n (default: true) */
209
+ i18n?: boolean;
210
+ }
211
+
212
+ /**
213
+ * Main Jant configuration
214
+ */
215
+ export interface JantConfig {
216
+ /** Site configuration */
217
+ site?: SiteConfig;
218
+ /** Theme configuration */
219
+ theme?: JantTheme;
220
+ /** Feature toggles */
221
+ features?: FeatureConfig;
222
+ }