@jant/core 0.3.25 → 0.3.26

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 (131) hide show
  1. package/dist/app.js +67 -562
  2. package/dist/client.js +1 -0
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/lib/avatar-upload.js +134 -0
  7. package/dist/lib/config.js +39 -0
  8. package/dist/lib/constants.js +10 -10
  9. package/dist/lib/favicon.js +102 -0
  10. package/dist/lib/image.js +13 -17
  11. package/dist/lib/media-helpers.js +2 -2
  12. package/dist/lib/navigation.js +23 -3
  13. package/dist/lib/render.js +10 -1
  14. package/dist/lib/schemas.js +31 -0
  15. package/dist/lib/timezones.js +388 -0
  16. package/dist/lib/view.js +1 -1
  17. package/dist/routes/api/posts.js +1 -1
  18. package/dist/routes/api/upload.js +3 -3
  19. package/dist/routes/auth/reset.js +221 -0
  20. package/dist/routes/auth/setup.js +194 -0
  21. package/dist/routes/auth/signin.js +176 -0
  22. package/dist/routes/dash/collections.js +23 -415
  23. package/dist/routes/dash/media.js +12 -392
  24. package/dist/routes/dash/pages.js +7 -330
  25. package/dist/routes/dash/redirects.js +18 -12
  26. package/dist/routes/dash/settings.js +198 -577
  27. package/dist/routes/feed/rss.js +2 -1
  28. package/dist/routes/feed/sitemap.js +4 -2
  29. package/dist/routes/pages/featured.js +5 -1
  30. package/dist/routes/pages/home.js +26 -1
  31. package/dist/routes/pages/latest.js +45 -0
  32. package/dist/services/post.js +30 -50
  33. package/dist/types/bindings.js +3 -0
  34. package/dist/types/config.js +147 -0
  35. package/dist/types/constants.js +27 -0
  36. package/dist/types/entities.js +3 -0
  37. package/dist/types/operations.js +3 -0
  38. package/dist/types/props.js +3 -0
  39. package/dist/types/views.js +5 -0
  40. package/dist/types.js +8 -111
  41. package/dist/ui/color-themes.js +33 -33
  42. package/dist/ui/compose/ComposeDialog.js +36 -21
  43. package/dist/ui/dash/PageForm.js +21 -15
  44. package/dist/ui/dash/PostForm.js +22 -16
  45. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  46. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  47. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  48. package/dist/ui/dash/media/MediaListContent.js +166 -0
  49. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  50. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  51. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  52. package/dist/ui/dash/settings/AccountContent.js +209 -0
  53. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  54. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  55. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  56. package/dist/ui/font-themes.js +36 -0
  57. package/dist/ui/layouts/BaseLayout.js +24 -2
  58. package/dist/ui/layouts/SiteLayout.js +47 -19
  59. package/package.json +1 -1
  60. package/src/app.tsx +93 -553
  61. package/src/client.ts +1 -0
  62. package/src/i18n/locales/en.po +240 -175
  63. package/src/i18n/locales/en.ts +1 -1
  64. package/src/i18n/locales/zh-Hans.po +240 -175
  65. package/src/i18n/locales/zh-Hans.ts +1 -1
  66. package/src/i18n/locales/zh-Hant.po +240 -175
  67. package/src/i18n/locales/zh-Hant.ts +1 -1
  68. package/src/lib/__tests__/config.test.ts +192 -0
  69. package/src/lib/__tests__/favicon.test.ts +151 -0
  70. package/src/lib/__tests__/image.test.ts +2 -6
  71. package/src/lib/__tests__/timezones.test.ts +61 -0
  72. package/src/lib/__tests__/view.test.ts +2 -2
  73. package/src/lib/avatar-upload.ts +165 -0
  74. package/src/lib/config.ts +47 -0
  75. package/src/lib/constants.ts +19 -11
  76. package/src/lib/favicon.ts +115 -0
  77. package/src/lib/image.ts +13 -21
  78. package/src/lib/media-helpers.ts +2 -2
  79. package/src/lib/navigation.ts +33 -2
  80. package/src/lib/render.tsx +15 -1
  81. package/src/lib/schemas.ts +39 -0
  82. package/src/lib/timezones.ts +325 -0
  83. package/src/lib/view.ts +1 -1
  84. package/src/routes/api/posts.ts +1 -1
  85. package/src/routes/api/upload.ts +2 -3
  86. package/src/routes/auth/reset.tsx +239 -0
  87. package/src/routes/auth/setup.tsx +189 -0
  88. package/src/routes/auth/signin.tsx +163 -0
  89. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  90. package/src/routes/dash/collections.tsx +17 -366
  91. package/src/routes/dash/media.tsx +12 -414
  92. package/src/routes/dash/pages.tsx +8 -348
  93. package/src/routes/dash/redirects.tsx +20 -14
  94. package/src/routes/dash/settings.tsx +243 -534
  95. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  96. package/src/routes/feed/rss.ts +3 -1
  97. package/src/routes/feed/sitemap.ts +4 -2
  98. package/src/routes/pages/featured.tsx +7 -1
  99. package/src/routes/pages/home.tsx +25 -2
  100. package/src/routes/pages/latest.tsx +59 -0
  101. package/src/services/post.ts +34 -66
  102. package/src/styles/components.css +0 -65
  103. package/src/styles/tokens.css +1 -1
  104. package/src/styles/ui.css +24 -40
  105. package/src/types/bindings.ts +30 -0
  106. package/src/types/config.ts +183 -0
  107. package/src/types/constants.ts +26 -0
  108. package/src/types/entities.ts +109 -0
  109. package/src/types/operations.ts +88 -0
  110. package/src/types/props.ts +115 -0
  111. package/src/types/views.ts +172 -0
  112. package/src/types.ts +8 -644
  113. package/src/ui/__tests__/font-themes.test.ts +34 -0
  114. package/src/ui/color-themes.ts +34 -34
  115. package/src/ui/compose/ComposeDialog.tsx +40 -21
  116. package/src/ui/dash/PageForm.tsx +25 -19
  117. package/src/ui/dash/PostForm.tsx +26 -20
  118. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  119. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  120. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  121. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  122. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  123. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  124. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  125. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  126. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  127. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  128. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  129. package/src/ui/font-themes.ts +54 -0
  130. package/src/ui/layouts/BaseLayout.tsx +17 -0
  131. package/src/ui/layouts/SiteLayout.tsx +45 -31
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Configuration System
3
+ *
4
+ * Single Source of Truth for all configuration fields.
5
+ */
6
+
7
+ import type { ColorTheme } from "../ui/color-themes.js";
8
+ import type { FeedData, SitemapData } from "./views.js";
9
+
10
+ /**
11
+ * Configuration Registry - Single Source of Truth
12
+ *
13
+ * All available configuration fields with their metadata.
14
+ * Add new fields here, and they'll automatically work everywhere.
15
+ *
16
+ * Priority logic:
17
+ * - envOnly: false -> User-configurable (DB > ENV > Default)
18
+ * - envOnly: true -> Environment-only (ENV > Default)
19
+ */
20
+ export const CONFIG_FIELDS = {
21
+ // User-configurable (can be modified in dashboard)
22
+ SITE_NAME: {
23
+ defaultValue: "Jant",
24
+ envOnly: false,
25
+ },
26
+ SITE_DESCRIPTION: {
27
+ defaultValue: "A microblog powered by Jant",
28
+ envOnly: false,
29
+ },
30
+ SITE_LANGUAGE: {
31
+ defaultValue: "en",
32
+ envOnly: false,
33
+ },
34
+ HOME_DEFAULT_VIEW: {
35
+ defaultValue: "latest",
36
+ envOnly: false,
37
+ },
38
+
39
+ // Environment-only (deployment/infrastructure config)
40
+ SITE_URL: {
41
+ defaultValue: "",
42
+ envOnly: true,
43
+ },
44
+ AUTH_SECRET: {
45
+ defaultValue: "",
46
+ envOnly: true,
47
+ },
48
+ R2_PUBLIC_URL: {
49
+ defaultValue: "",
50
+ envOnly: true,
51
+ },
52
+ IMAGE_TRANSFORM_URL: {
53
+ defaultValue: "",
54
+ envOnly: true,
55
+ },
56
+ DEMO_EMAIL: {
57
+ defaultValue: "",
58
+ envOnly: true,
59
+ },
60
+ DEMO_PASSWORD: {
61
+ defaultValue: "",
62
+ envOnly: true,
63
+ },
64
+ PAGE_SIZE: {
65
+ defaultValue: "20",
66
+ envOnly: true,
67
+ },
68
+ STORAGE_DRIVER: {
69
+ defaultValue: "r2",
70
+ envOnly: true,
71
+ },
72
+ S3_ENDPOINT: {
73
+ defaultValue: "",
74
+ envOnly: true,
75
+ },
76
+ S3_BUCKET: {
77
+ defaultValue: "",
78
+ envOnly: true,
79
+ },
80
+ S3_ACCESS_KEY_ID: {
81
+ defaultValue: "",
82
+ envOnly: true,
83
+ },
84
+ S3_SECRET_ACCESS_KEY: {
85
+ defaultValue: "",
86
+ envOnly: true,
87
+ },
88
+ S3_REGION: {
89
+ defaultValue: "auto",
90
+ envOnly: true,
91
+ },
92
+ S3_PUBLIC_URL: {
93
+ defaultValue: "",
94
+ envOnly: true,
95
+ },
96
+
97
+ // Internal settings (DB-only, not configurable via env or dashboard)
98
+ THEME: {
99
+ defaultValue: "",
100
+ envOnly: false,
101
+ internal: true,
102
+ },
103
+ CUSTOM_CSS: {
104
+ defaultValue: "",
105
+ envOnly: false,
106
+ internal: true,
107
+ },
108
+ SITE_AVATAR: {
109
+ defaultValue: "",
110
+ envOnly: false,
111
+ internal: true,
112
+ },
113
+ SHOW_HEADER_AVATAR: {
114
+ defaultValue: "",
115
+ envOnly: false,
116
+ internal: true,
117
+ },
118
+ SITE_FAVICON_ICO: {
119
+ defaultValue: "",
120
+ envOnly: false,
121
+ internal: true,
122
+ },
123
+ SITE_FAVICON_APPLE_TOUCH: {
124
+ defaultValue: "",
125
+ envOnly: false,
126
+ internal: true,
127
+ },
128
+ FONT_THEME: {
129
+ defaultValue: "",
130
+ envOnly: false,
131
+ internal: true,
132
+ },
133
+ TIME_ZONE: {
134
+ defaultValue: "UTC",
135
+ envOnly: false,
136
+ },
137
+ SITE_FOOTER: {
138
+ defaultValue: "",
139
+ envOnly: false,
140
+ },
141
+ NOINDEX: {
142
+ defaultValue: "",
143
+ envOnly: false,
144
+ },
145
+ ONBOARDING_STATUS: {
146
+ defaultValue: "pending",
147
+ envOnly: false,
148
+ internal: true,
149
+ },
150
+ PASSWORD_RESET_TOKEN: {
151
+ defaultValue: "",
152
+ envOnly: false,
153
+ internal: true,
154
+ },
155
+ } as const;
156
+
157
+ export type ConfigKey = keyof typeof CONFIG_FIELDS;
158
+
159
+ /**
160
+ * Main Jant configuration
161
+ *
162
+ * Configuration Philosophy:
163
+ * - Use environment variables for runtime config (API keys, feature flags, site settings)
164
+ * - Use code config (this object) for CSS customization and feed overrides
165
+ *
166
+ * Site-level settings (name, description, language) are configured via
167
+ * environment variables, not here. See lib/config.ts for details.
168
+ */
169
+ export interface JantConfig {
170
+ /** CSS variable overrides (highest priority after custom CSS) */
171
+ cssVariables?: Record<string, string>;
172
+ /** Replace built-in color themes with custom list */
173
+ colorThemes?: ColorTheme[];
174
+ /** Custom feed renderers */
175
+ feed?: {
176
+ /** Custom RSS 2.0 renderer -- returns XML string */
177
+ rss?: (data: FeedData) => string;
178
+ /** Custom Atom renderer -- returns XML string */
179
+ atom?: (data: FeedData) => string;
180
+ /** Custom Sitemap renderer -- returns XML string */
181
+ sitemap?: (data: SitemapData) => string;
182
+ };
183
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Content Type Constants
3
+ */
4
+
5
+ export const FORMATS = ["note", "link", "quote"] as const;
6
+ export type Format = (typeof FORMATS)[number];
7
+
8
+ export const STATUSES = ["draft", "published"] as const;
9
+ export type Status = (typeof STATUSES)[number];
10
+
11
+ export const SORT_ORDERS = [
12
+ "newest",
13
+ "oldest",
14
+ "rating_desc",
15
+ "rating_asc",
16
+ ] as const;
17
+ export type SortOrder = (typeof SORT_ORDERS)[number];
18
+
19
+ export const NAV_ITEM_TYPES = ["page", "link"] as const;
20
+ export type NavItemType = (typeof NAV_ITEM_TYPES)[number];
21
+
22
+ export const MAX_MEDIA_ATTACHMENTS = 20;
23
+ export const MAX_PINNED_POSTS = 3;
24
+
25
+ export const STORAGE_DRIVERS = ["r2", "s3"] as const;
26
+ export type StorageDriver = (typeof STORAGE_DRIVERS)[number];
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Entity Types (database-level models)
3
+ */
4
+
5
+ import type { Format, Status, SortOrder, NavItemType } from "./constants.js";
6
+
7
+ export interface Post {
8
+ id: number;
9
+ format: Format;
10
+ status: Status;
11
+ featured: number; // 0 | 1
12
+ pinned: number; // 0 | 1
13
+ path: string | null;
14
+ title: string | null;
15
+ url: string | null;
16
+ body: string | null;
17
+ bodyHtml: string | null;
18
+ quoteText: string | null;
19
+ rating: number | null;
20
+ collectionId: number | null;
21
+ replyToId: number | null;
22
+ threadId: number | null;
23
+ deletedAt: number | null;
24
+ publishedAt: number;
25
+ createdAt: number;
26
+ updatedAt: number;
27
+ }
28
+
29
+ export interface Page {
30
+ id: number;
31
+ slug: string;
32
+ title: string | null;
33
+ body: string | null;
34
+ bodyHtml: string | null;
35
+ status: Status;
36
+ createdAt: number;
37
+ updatedAt: number;
38
+ }
39
+
40
+ export interface Media {
41
+ id: string; // UUIDv7
42
+ postId: number | null;
43
+ filename: string;
44
+ originalName: string;
45
+ mimeType: string;
46
+ size: number;
47
+ storageKey: string;
48
+ provider: string;
49
+ width: number | null;
50
+ height: number | null;
51
+ alt: string | null;
52
+ position: number;
53
+ blurhash: string | null;
54
+ createdAt: number;
55
+ }
56
+
57
+ export interface MediaAttachment {
58
+ id: string;
59
+ url: string;
60
+ previewUrl: string;
61
+ alt: string | null;
62
+ blurhash: string | null;
63
+ width: number | null;
64
+ height: number | null;
65
+ position: number;
66
+ mimeType: string;
67
+ }
68
+
69
+ export interface PostWithMedia extends Post {
70
+ mediaAttachments: MediaAttachment[];
71
+ }
72
+
73
+ export interface Collection {
74
+ id: number;
75
+ slug: string;
76
+ title: string;
77
+ description: string | null;
78
+ icon: string | null;
79
+ sortOrder: SortOrder;
80
+ position: number;
81
+ showDivider: number; // 0 | 1
82
+ createdAt: number;
83
+ updatedAt: number;
84
+ }
85
+
86
+ export interface NavItem {
87
+ id: number;
88
+ type: NavItemType;
89
+ label: string;
90
+ url: string;
91
+ pageId: number | null;
92
+ position: number;
93
+ createdAt: number;
94
+ updatedAt: number;
95
+ }
96
+
97
+ export interface Redirect {
98
+ id: number;
99
+ fromPath: string;
100
+ toPath: string;
101
+ type: 301 | 302;
102
+ createdAt: number;
103
+ }
104
+
105
+ export interface Setting {
106
+ key: string;
107
+ value: string;
108
+ updatedAt: number;
109
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Operation Types (create/update DTOs)
3
+ */
4
+
5
+ import type { Format, Status, SortOrder, NavItemType } from "./constants.js";
6
+
7
+ export interface CreatePost {
8
+ format: Format;
9
+ status?: Status;
10
+ featured?: boolean;
11
+ pinned?: boolean;
12
+ path?: string;
13
+ title?: string;
14
+ url?: string;
15
+ body?: string;
16
+ quoteText?: string;
17
+ rating?: number;
18
+ collectionId?: number;
19
+ replyToId?: number;
20
+ publishedAt?: number;
21
+ mediaIds?: string[];
22
+ }
23
+
24
+ export interface UpdatePost {
25
+ format?: Format;
26
+ status?: Status;
27
+ featured?: boolean;
28
+ pinned?: boolean;
29
+ path?: string | null;
30
+ title?: string | null;
31
+ url?: string | null;
32
+ body?: string | null;
33
+ quoteText?: string | null;
34
+ rating?: number | null;
35
+ collectionId?: number | null;
36
+ publishedAt?: number;
37
+ mediaIds?: string[];
38
+ }
39
+
40
+ export interface CreatePage {
41
+ slug: string;
42
+ title?: string;
43
+ body?: string;
44
+ status?: Status;
45
+ }
46
+
47
+ export interface UpdatePage {
48
+ slug?: string;
49
+ title?: string | null;
50
+ body?: string | null;
51
+ status?: Status;
52
+ }
53
+
54
+ export interface CreateNavItem {
55
+ type: NavItemType;
56
+ label: string;
57
+ url: string;
58
+ pageId?: number;
59
+ position?: number;
60
+ }
61
+
62
+ export interface UpdateNavItem {
63
+ type?: NavItemType;
64
+ label?: string;
65
+ url?: string;
66
+ pageId?: number | null;
67
+ position?: number;
68
+ }
69
+
70
+ export interface CreateCollection {
71
+ slug: string;
72
+ title: string;
73
+ description?: string;
74
+ icon?: string;
75
+ sortOrder?: SortOrder;
76
+ position?: number;
77
+ showDivider?: boolean;
78
+ }
79
+
80
+ export interface UpdateCollection {
81
+ slug?: string;
82
+ title?: string;
83
+ description?: string | null;
84
+ icon?: string | null;
85
+ sortOrder?: SortOrder;
86
+ position?: number;
87
+ showDivider?: boolean;
88
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Page-Level Props & Feed Data Types
3
+ */
4
+
5
+ import type { Format } from "./constants.js";
6
+ import type { Collection } from "./entities.js";
7
+ import type {
8
+ PostView,
9
+ PageView,
10
+ TimelineItemView,
11
+ SearchResultView,
12
+ ArchiveGroup,
13
+ } from "./views.js";
14
+
15
+ // =============================================================================
16
+ // Page-Level Props
17
+ // =============================================================================
18
+
19
+ /** Props for the home page component */
20
+ export interface HomePageProps {
21
+ items: TimelineItemView[];
22
+ pinnedItems: PostView[];
23
+ currentPage: number;
24
+ totalPages: number;
25
+ }
26
+
27
+ /** Props for the single post page component */
28
+ export interface PostPageProps {
29
+ post: PostView;
30
+ }
31
+
32
+ /** Props for the custom page component */
33
+ export interface SinglePageProps {
34
+ page: PageView;
35
+ }
36
+
37
+ /** Props for the featured page component */
38
+ export interface FeaturedPageProps {
39
+ items: TimelineItemView[];
40
+ }
41
+
42
+ /** Props for the archive page component */
43
+ export interface ArchivePageProps {
44
+ groups: ArchiveGroup[];
45
+ hasMore: boolean;
46
+ nextCursor?: number;
47
+ format?: Format;
48
+ featured?: boolean;
49
+ }
50
+
51
+ /** Props for the search page component */
52
+ export interface SearchPageProps {
53
+ query: string;
54
+ results: SearchResultView[];
55
+ error?: string;
56
+ hasMore: boolean;
57
+ page: number;
58
+ }
59
+
60
+ /** Props for the single collection page component */
61
+ export interface CollectionPageProps {
62
+ collection: Collection;
63
+ posts: PostView[];
64
+ hasMore: boolean;
65
+ nextCursor?: number;
66
+ }
67
+
68
+ /** Props for the collections list page component */
69
+ export interface CollectionsPageProps {
70
+ collections: (Collection & { postCount: number })[];
71
+ }
72
+
73
+ // =============================================================================
74
+ // Feed Data Types
75
+ // =============================================================================
76
+
77
+ /** Data passed to RSS/Atom feed renderers */
78
+ export interface FeedData {
79
+ siteName: string;
80
+ siteDescription: string;
81
+ siteUrl: string;
82
+ siteLanguage: string;
83
+ posts: PostView[];
84
+ }
85
+
86
+ /** Data passed to sitemap renderers */
87
+ export interface SitemapData {
88
+ siteUrl: string;
89
+ posts: PostView[];
90
+ pages: PageView[];
91
+ }
92
+
93
+ // =============================================================================
94
+ // Timeline Types
95
+ // =============================================================================
96
+
97
+ /** Props for per-type timeline cards */
98
+ export interface TimelineCardProps {
99
+ post: PostView;
100
+ compact?: boolean;
101
+ }
102
+
103
+ /** Props for thread inline preview */
104
+ export interface ThreadPreviewProps {
105
+ rootPost: PostView;
106
+ previewReplies: PostView[];
107
+ totalReplyCount: number;
108
+ }
109
+
110
+ /** Props for the timeline feed wrapper */
111
+ export interface TimelineFeedProps {
112
+ items: TimelineItemView[];
113
+ currentPage?: number;
114
+ totalPages?: number;
115
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * View Model Types (render-ready, for theme components)
3
+ */
4
+
5
+ import type { Format, Status, NavItemType } from "./constants.js";
6
+ import type { Post, Collection } from "./entities.js";
7
+
8
+ /**
9
+ * Render-ready post data for theme components.
10
+ * All fields are pre-computed -- no lib/ imports needed.
11
+ */
12
+ export interface PostView {
13
+ // Identity
14
+ id: number;
15
+ /** Pre-computed permalink: "/{path}" if path set, otherwise "/p/{sqid}" */
16
+ permalink: string;
17
+ /** Custom URL path, if set. Supports multi-level paths (e.g. "2024/my-post") */
18
+ path?: string;
19
+
20
+ // Content
21
+ title?: string;
22
+ /** Pre-sanitized HTML */
23
+ bodyHtml?: string;
24
+ /** Pre-computed excerpt, max 160 chars */
25
+ excerpt?: string;
26
+ /** HTML excerpt for article previews (paragraph-aware, ~500 chars) */
27
+ summaryHtml?: string;
28
+ /** Whether summaryHtml was truncated (content continues beyond excerpt) */
29
+ summaryHasMore?: boolean;
30
+ /** URL for link/quote formats */
31
+ url?: string;
32
+ /** Quoted text for quote format */
33
+ quoteText?: string;
34
+
35
+ // Metadata
36
+ format: Format;
37
+ status: Status;
38
+ featured: boolean;
39
+ pinned: boolean;
40
+ rating?: number;
41
+
42
+ // Collection
43
+ collectionId?: number;
44
+
45
+ // Time -- pre-formatted
46
+ /** ISO 8601 string */
47
+ publishedAt: string;
48
+ /** Human-readable, e.g. "Feb 1, 2024" */
49
+ publishedAtFormatted: string;
50
+ /** 24-hour time, e.g. "23:05" */
51
+ publishedAtTime: string;
52
+ /** Short relative time, e.g. "5m", "3h", "2d", "Feb 1" */
53
+ publishedAtRelative: string;
54
+ /** ISO 8601 string */
55
+ updatedAt: string;
56
+
57
+ // Media -- URLs pre-computed
58
+ media: MediaView[];
59
+
60
+ // Thread context
61
+ replyToId?: number;
62
+ threadRootId?: number;
63
+
64
+ // Raw content (for forms/editing, not typical theme use)
65
+ body?: string;
66
+ }
67
+
68
+ /**
69
+ * Render-ready page data for theme components.
70
+ */
71
+ export interface PageView {
72
+ id: number;
73
+ slug: string;
74
+ title?: string;
75
+ bodyHtml?: string;
76
+ status: Status;
77
+ createdAt: string;
78
+ updatedAt: string;
79
+ }
80
+
81
+ /**
82
+ * Render-ready media data for theme components.
83
+ * URLs are pre-computed -- no lib/ imports needed.
84
+ */
85
+ export interface MediaView {
86
+ id: string;
87
+ /** Full-size URL, pre-computed */
88
+ url: string;
89
+ /** Thumbnail URL, pre-computed */
90
+ thumbnailUrl: string;
91
+ mimeType: string;
92
+ altText?: string;
93
+ width?: number;
94
+ height?: number;
95
+ size?: number;
96
+ }
97
+
98
+ /**
99
+ * Render-ready navigation item for theme components.
100
+ * Active/external state pre-computed.
101
+ */
102
+ export interface NavItemView {
103
+ id: number;
104
+ type: NavItemType;
105
+ label: string;
106
+ url: string;
107
+ pageId?: number;
108
+ /** Pre-computed based on currentPath */
109
+ isActive: boolean;
110
+ /** Pre-computed: starts with http(s):// */
111
+ isExternal: boolean;
112
+ }
113
+
114
+ /**
115
+ * Search result from FTS5
116
+ */
117
+ export interface SearchResult {
118
+ post: Post;
119
+ /** FTS5 rank score (lower is better) */
120
+ rank: number;
121
+ /** Highlighted snippet from content */
122
+ snippet?: string;
123
+ }
124
+
125
+ /**
126
+ * Render-ready search result for theme components.
127
+ */
128
+ export interface SearchResultView {
129
+ post: PostView;
130
+ rank: number;
131
+ snippet?: string;
132
+ }
133
+
134
+ /**
135
+ * Render-ready timeline item for theme components.
136
+ */
137
+ export interface TimelineItemView {
138
+ post: PostView;
139
+ threadPreview?: {
140
+ replies: PostView[];
141
+ totalReplyCount: number;
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Typed archive group with pre-formatted label.
147
+ */
148
+ export interface ArchiveGroup {
149
+ /** e.g. "2024" */
150
+ year: string;
151
+ /** e.g. "02" */
152
+ month: string;
153
+ /** Pre-formatted, e.g. "February 2024" */
154
+ label: string;
155
+ posts: PostView[];
156
+ }
157
+
158
+ /**
159
+ * Site Layout Props
160
+ */
161
+ export interface SiteLayoutProps {
162
+ siteName: string;
163
+ siteDescription?: string;
164
+ links: NavItemView[];
165
+ currentPath: string;
166
+ isAuthenticated?: boolean;
167
+ collections?: Collection[];
168
+ homeDefaultView?: string;
169
+ siteAvatarUrl?: string;
170
+ showHeaderAvatar?: boolean;
171
+ siteFooterHtml?: string;
172
+ }