@jant/core 0.3.23 → 0.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/dist/app.js +4 -5
  2. package/dist/db/schema.js +72 -47
  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/index.js +3 -3
  7. package/dist/lib/constants.js +1 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/navigation.js +4 -5
  11. package/dist/lib/render.js +1 -1
  12. package/dist/lib/schemas.js +80 -38
  13. package/dist/lib/theme-components.js +8 -11
  14. package/dist/lib/time.js +56 -1
  15. package/dist/lib/timeline.js +119 -0
  16. package/dist/lib/view.js +61 -72
  17. package/dist/routes/api/posts.js +29 -35
  18. package/dist/routes/api/search.js +5 -6
  19. package/dist/routes/api/upload.js +13 -13
  20. package/dist/routes/dash/collections.js +22 -40
  21. package/dist/routes/dash/index.js +2 -2
  22. package/dist/routes/dash/navigation.js +25 -24
  23. package/dist/routes/dash/pages.js +42 -57
  24. package/dist/routes/dash/posts.js +27 -35
  25. package/dist/routes/feed/rss.js +2 -4
  26. package/dist/routes/feed/sitemap.js +10 -7
  27. package/dist/routes/pages/archive.js +12 -11
  28. package/dist/routes/pages/collection.js +11 -5
  29. package/dist/routes/pages/home.js +53 -61
  30. package/dist/routes/pages/page.js +60 -29
  31. package/dist/routes/pages/post.js +5 -12
  32. package/dist/routes/pages/search.js +3 -4
  33. package/dist/services/collection.js +52 -64
  34. package/dist/services/index.js +5 -3
  35. package/dist/services/navigation.js +29 -53
  36. package/dist/services/page.js +80 -0
  37. package/dist/services/post.js +68 -69
  38. package/dist/services/search.js +24 -18
  39. package/dist/theme/components/MediaGallery.js +19 -91
  40. package/dist/theme/components/PageForm.js +15 -15
  41. package/dist/theme/components/PostForm.js +136 -129
  42. package/dist/theme/components/PostList.js +13 -8
  43. package/dist/theme/components/ThreadView.js +3 -3
  44. package/dist/theme/components/TypeBadge.js +3 -14
  45. package/dist/theme/components/VisibilityBadge.js +33 -23
  46. package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
  47. package/dist/themes/threads/index.js +81 -0
  48. package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
  49. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  50. package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
  51. package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
  52. package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
  53. package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
  54. package/dist/themes/threads/timeline/LinkCard.js +68 -0
  55. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  56. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  57. package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
  58. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  59. package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
  60. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  61. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  62. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  63. package/dist/types.js +24 -40
  64. package/package.json +2 -1
  65. package/src/__tests__/helpers/app.ts +4 -0
  66. package/src/__tests__/helpers/db.ts +51 -74
  67. package/src/app.tsx +4 -6
  68. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  69. package/src/db/migrations/meta/_journal.json +7 -0
  70. package/src/db/schema.ts +63 -46
  71. package/src/i18n/locales/en.po +216 -164
  72. package/src/i18n/locales/en.ts +1 -1
  73. package/src/i18n/locales/zh-Hans.po +216 -164
  74. package/src/i18n/locales/zh-Hans.ts +1 -1
  75. package/src/i18n/locales/zh-Hant.po +216 -164
  76. package/src/i18n/locales/zh-Hant.ts +1 -1
  77. package/src/index.ts +28 -12
  78. package/src/lib/__tests__/excerpt.test.ts +125 -0
  79. package/src/lib/__tests__/schemas.test.ts +166 -105
  80. package/src/lib/__tests__/theme-components.test.ts +4 -25
  81. package/src/lib/__tests__/time.test.ts +62 -0
  82. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  83. package/src/lib/__tests__/view.test.ts +199 -51
  84. package/src/lib/constants.ts +1 -4
  85. package/src/lib/excerpt.ts +87 -0
  86. package/src/lib/feed.ts +22 -7
  87. package/src/lib/navigation.ts +6 -7
  88. package/src/lib/render.tsx +1 -1
  89. package/src/lib/schemas.ts +118 -52
  90. package/src/lib/theme-components.ts +10 -13
  91. package/src/lib/time.ts +64 -0
  92. package/src/lib/timeline.ts +170 -0
  93. package/src/lib/view.ts +80 -82
  94. package/src/preset.css +45 -0
  95. package/src/routes/api/__tests__/posts.test.ts +50 -108
  96. package/src/routes/api/__tests__/search.test.ts +2 -3
  97. package/src/routes/api/posts.ts +30 -30
  98. package/src/routes/api/search.ts +4 -4
  99. package/src/routes/api/upload.ts +16 -6
  100. package/src/routes/dash/collections.tsx +18 -40
  101. package/src/routes/dash/index.tsx +2 -2
  102. package/src/routes/dash/navigation.tsx +27 -26
  103. package/src/routes/dash/pages.tsx +45 -60
  104. package/src/routes/dash/posts.tsx +44 -52
  105. package/src/routes/feed/rss.ts +2 -1
  106. package/src/routes/feed/sitemap.ts +14 -4
  107. package/src/routes/pages/archive.tsx +14 -10
  108. package/src/routes/pages/collection.tsx +17 -6
  109. package/src/routes/pages/home.tsx +56 -81
  110. package/src/routes/pages/page.tsx +64 -27
  111. package/src/routes/pages/post.tsx +5 -14
  112. package/src/routes/pages/search.tsx +2 -2
  113. package/src/services/__tests__/collection.test.ts +257 -158
  114. package/src/services/__tests__/media.test.ts +18 -18
  115. package/src/services/__tests__/navigation.test.ts +161 -87
  116. package/src/services/__tests__/post-timeline.test.ts +92 -88
  117. package/src/services/__tests__/post.test.ts +342 -206
  118. package/src/services/__tests__/search.test.ts +19 -25
  119. package/src/services/collection.ts +71 -113
  120. package/src/services/index.ts +9 -8
  121. package/src/services/navigation.ts +38 -71
  122. package/src/services/page.ts +124 -0
  123. package/src/services/post.ts +93 -103
  124. package/src/services/search.ts +38 -27
  125. package/src/theme/components/MediaGallery.tsx +27 -96
  126. package/src/theme/components/PageForm.tsx +21 -21
  127. package/src/theme/components/PostForm.tsx +122 -118
  128. package/src/theme/components/PostList.tsx +58 -49
  129. package/src/theme/components/ThreadView.tsx +6 -3
  130. package/src/theme/components/TypeBadge.tsx +9 -17
  131. package/src/theme/components/VisibilityBadge.tsx +40 -23
  132. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  133. package/src/themes/{minimal → threads}/index.ts +30 -13
  134. package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
  135. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  136. package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
  137. package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
  138. package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
  139. package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
  140. package/src/themes/threads/style.css +336 -0
  141. package/src/themes/threads/timeline/LinkCard.tsx +67 -0
  142. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  143. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  144. package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
  145. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  146. package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
  147. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  148. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  149. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  150. package/src/types.ts +242 -98
  151. package/dist/routes/api/timeline.js +0 -120
  152. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  153. package/dist/themes/minimal/index.js +0 -65
  154. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  155. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  156. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  157. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  158. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  159. package/dist/themes/minimal/timeline/QuoteCard.js +0 -48
  160. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  161. package/src/routes/api/timeline.tsx +0 -159
  162. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  163. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  164. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  165. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  166. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  167. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  168. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  169. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
package/src/types.ts CHANGED
@@ -1,48 +1,34 @@
1
1
  /**
2
- * Jant Type Definitions
2
+ * Jant Type Definitions (v2)
3
3
  */
4
4
 
5
5
  // =============================================================================
6
6
  // Content Types
7
7
  // =============================================================================
8
8
 
9
- export const POST_TYPES = [
10
- "note",
11
- "article",
12
- "link",
13
- "quote",
14
- "image",
15
- "page",
9
+ export const FORMATS = ["note", "link", "quote"] as const;
10
+ export type Format = (typeof FORMATS)[number];
11
+
12
+ export const STATUSES = ["draft", "published"] as const;
13
+ export type Status = (typeof STATUSES)[number];
14
+
15
+ export const SORT_ORDERS = [
16
+ "newest",
17
+ "oldest",
18
+ "rating_desc",
19
+ "rating_asc",
16
20
  ] as const;
17
- export type PostType = (typeof POST_TYPES)[number];
21
+ export type SortOrder = (typeof SORT_ORDERS)[number];
18
22
 
19
- export const MAX_MEDIA_ATTACHMENTS = 20;
23
+ export const NAV_ITEM_TYPES = ["page", "link"] as const;
24
+ export type NavItemType = (typeof NAV_ITEM_TYPES)[number];
20
25
 
21
- /**
22
- * Media attachment rules per post type.
23
- * Each entry is [min, max] or null (no media allowed).
24
- */
25
- export const POST_TYPE_MEDIA_RULES: Record<PostType, [number, number] | null> =
26
- {
27
- note: [0, 20],
28
- article: [0, 20],
29
- image: [1, 20],
30
- link: [0, 1],
31
- quote: [0, 20],
32
- page: null,
33
- };
26
+ export const MAX_MEDIA_ATTACHMENTS = 20;
27
+ export const MAX_PINNED_POSTS = 3;
34
28
 
35
29
  export const STORAGE_DRIVERS = ["r2", "s3"] as const;
36
30
  export type StorageDriver = (typeof STORAGE_DRIVERS)[number];
37
31
 
38
- export const VISIBILITY_LEVELS = [
39
- "featured",
40
- "quiet",
41
- "unlisted",
42
- "draft",
43
- ] as const;
44
- export type Visibility = (typeof VISIBILITY_LEVELS)[number];
45
-
46
32
  // =============================================================================
47
33
  // Cloudflare Bindings
48
34
  // =============================================================================
@@ -56,6 +42,8 @@ export interface Bindings {
56
42
  IMAGE_TRANSFORM_URL?: string;
57
43
  DEMO_EMAIL?: string;
58
44
  DEMO_PASSWORD?: string;
45
+ // Timeline
46
+ PAGE_SIZE?: string;
59
47
  // Site configuration (optional - can be overridden in DB)
60
48
  SITE_NAME?: string;
61
49
  SITE_DESCRIPTION?: string;
@@ -81,8 +69,8 @@ export interface Bindings {
81
69
  * Add new fields here, and they'll automatically work everywhere.
82
70
  *
83
71
  * Priority logic:
84
- * - envOnly: false User-configurable (DB > ENV > Default)
85
- * - envOnly: true Environment-only (ENV > Default)
72
+ * - envOnly: false -> User-configurable (DB > ENV > Default)
73
+ * - envOnly: true -> Environment-only (ENV > Default)
86
74
  */
87
75
  export const CONFIG_FIELDS = {
88
76
  // User-configurable (can be modified in dashboard)
@@ -124,6 +112,10 @@ export const CONFIG_FIELDS = {
124
112
  defaultValue: "",
125
113
  envOnly: true,
126
114
  },
115
+ PAGE_SIZE: {
116
+ defaultValue: "20",
117
+ envOnly: true,
118
+ },
127
119
  STORAGE_DRIVER: {
128
120
  defaultValue: "r2",
129
121
  envOnly: true,
@@ -162,15 +154,18 @@ export type ConfigKey = keyof typeof CONFIG_FIELDS;
162
154
 
163
155
  export interface Post {
164
156
  id: number;
165
- type: PostType;
166
- visibility: Visibility;
157
+ format: Format;
158
+ status: Status;
159
+ featured: number; // 0 | 1
160
+ pinned: number; // 0 | 1
161
+ slug: string | null;
167
162
  title: string | null;
168
- path: string | null;
169
- content: string | null;
170
- contentHtml: string | null;
171
- sourceUrl: string | null;
172
- sourceName: string | null;
173
- sourceDomain: string | null;
163
+ url: string | null;
164
+ body: string | null;
165
+ bodyHtml: string | null;
166
+ quoteText: string | null;
167
+ rating: number | null;
168
+ collectionId: number | null;
174
169
  replyToId: number | null;
175
170
  threadId: number | null;
176
171
  deletedAt: number | null;
@@ -179,6 +174,17 @@ export interface Post {
179
174
  updatedAt: number;
180
175
  }
181
176
 
177
+ export interface Page {
178
+ id: number;
179
+ slug: string;
180
+ title: string | null;
181
+ body: string | null;
182
+ bodyHtml: string | null;
183
+ status: Status;
184
+ createdAt: number;
185
+ updatedAt: number;
186
+ }
187
+
182
188
  export interface Media {
183
189
  id: string; // UUIDv7
184
190
  postId: number | null;
@@ -214,17 +220,26 @@ export interface PostWithMedia extends Post {
214
220
 
215
221
  export interface Collection {
216
222
  id: number;
223
+ slug: string;
217
224
  title: string;
218
- path: string | null;
219
225
  description: string | null;
226
+ icon: string | null;
227
+ sortOrder: SortOrder;
228
+ position: number;
229
+ showDivider: number; // 0 | 1
220
230
  createdAt: number;
221
231
  updatedAt: number;
222
232
  }
223
233
 
224
- export interface PostCollection {
225
- postId: number;
226
- collectionId: number;
227
- addedAt: number;
234
+ export interface NavItem {
235
+ id: number;
236
+ type: NavItemType;
237
+ label: string;
238
+ url: string;
239
+ pageId: number | null;
240
+ position: number;
241
+ createdAt: number;
242
+ updatedAt: number;
228
243
  }
229
244
 
230
245
  export interface Redirect {
@@ -241,54 +256,91 @@ export interface Setting {
241
256
  updatedAt: number;
242
257
  }
243
258
 
244
- export interface NavigationLink {
245
- id: number;
246
- label: string;
247
- url: string;
248
- position: number;
249
- createdAt: number;
250
- updatedAt: number;
251
- }
252
-
253
259
  // =============================================================================
254
260
  // Operation Types
255
261
  // =============================================================================
256
262
 
257
263
  export interface CreatePost {
258
- type: PostType;
259
- visibility?: Visibility;
264
+ format: Format;
265
+ status?: Status;
266
+ featured?: boolean;
267
+ pinned?: boolean;
268
+ slug?: string;
260
269
  title?: string;
261
- path?: string;
262
- content?: string;
263
- sourceUrl?: string;
264
- sourceName?: string;
270
+ url?: string;
271
+ body?: string;
272
+ quoteText?: string;
273
+ rating?: number;
274
+ collectionId?: number;
265
275
  replyToId?: number;
266
276
  publishedAt?: number;
267
277
  mediaIds?: string[];
268
278
  }
269
279
 
270
280
  export interface UpdatePost {
271
- type?: PostType;
272
- visibility?: Visibility;
281
+ format?: Format;
282
+ status?: Status;
283
+ featured?: boolean;
284
+ pinned?: boolean;
285
+ slug?: string | null;
273
286
  title?: string | null;
274
- path?: string | null;
275
- content?: string | null;
276
- sourceUrl?: string | null;
277
- sourceName?: string | null;
287
+ url?: string | null;
288
+ body?: string | null;
289
+ quoteText?: string | null;
290
+ rating?: number | null;
291
+ collectionId?: number | null;
278
292
  publishedAt?: number;
279
293
  mediaIds?: string[];
280
294
  }
281
295
 
282
- export interface CreateNavigationLink {
296
+ export interface CreatePage {
297
+ slug: string;
298
+ title?: string;
299
+ body?: string;
300
+ status?: Status;
301
+ }
302
+
303
+ export interface UpdatePage {
304
+ slug?: string;
305
+ title?: string | null;
306
+ body?: string | null;
307
+ status?: Status;
308
+ }
309
+
310
+ export interface CreateNavItem {
311
+ type: NavItemType;
283
312
  label: string;
284
313
  url: string;
314
+ pageId?: number;
285
315
  position?: number;
286
316
  }
287
317
 
288
- export interface UpdateNavigationLink {
318
+ export interface UpdateNavItem {
319
+ type?: NavItemType;
289
320
  label?: string;
290
321
  url?: string;
322
+ pageId?: number | null;
323
+ position?: number;
324
+ }
325
+
326
+ export interface CreateCollection {
327
+ slug: string;
328
+ title: string;
329
+ description?: string;
330
+ icon?: string;
331
+ sortOrder?: SortOrder;
291
332
  position?: number;
333
+ showDivider?: boolean;
334
+ }
335
+
336
+ export interface UpdateCollection {
337
+ slug?: string;
338
+ title?: string;
339
+ description?: string | null;
340
+ icon?: string | null;
341
+ sortOrder?: SortOrder;
342
+ position?: number;
343
+ showDivider?: boolean;
292
344
  }
293
345
 
294
346
  // =============================================================================
@@ -297,41 +349,54 @@ export interface UpdateNavigationLink {
297
349
 
298
350
  /**
299
351
  * Render-ready post data for theme components.
300
- * All fields are pre-computed no lib/ imports needed.
352
+ * All fields are pre-computed -- no lib/ imports needed.
301
353
  */
302
354
  export interface PostView {
303
355
  // Identity
304
356
  id: number;
305
- /** Pre-computed permalink, e.g. "/p/jR3k" */
357
+ /** Pre-computed permalink: "/{slug}" if slug set, otherwise "/p/{sqid}" */
306
358
  permalink: string;
359
+ /** Custom URL slug, if set */
360
+ slug?: string;
307
361
 
308
362
  // Content
309
363
  title?: string;
310
364
  /** Pre-sanitized HTML */
311
- contentHtml?: string;
365
+ bodyHtml?: string;
312
366
  /** Pre-computed excerpt, max 160 chars */
313
367
  excerpt?: string;
368
+ /** HTML excerpt for article previews (paragraph-aware, ~500 chars) */
369
+ summaryHtml?: string;
370
+ /** Whether summaryHtml was truncated (content continues beyond excerpt) */
371
+ summaryHasMore?: boolean;
372
+ /** URL for link/quote formats */
373
+ url?: string;
374
+ /** Quoted text for quote format */
375
+ quoteText?: string;
314
376
 
315
377
  // Metadata
316
- type: PostType;
317
- visibility: Visibility;
318
- /** Custom path for pages, e.g. "/about" */
319
- path?: string;
378
+ format: Format;
379
+ status: Status;
380
+ featured: boolean;
381
+ pinned: boolean;
382
+ rating?: number;
383
+
384
+ // Collection
385
+ collectionId?: number;
320
386
 
321
- // Time pre-formatted
387
+ // Time -- pre-formatted
322
388
  /** ISO 8601 string */
323
389
  publishedAt: string;
324
390
  /** Human-readable, e.g. "Feb 1, 2024" */
325
391
  publishedAtFormatted: string;
392
+ /** 24-hour time, e.g. "23:05" */
393
+ publishedAtTime: string;
394
+ /** Short relative time, e.g. "5m", "3h", "2d", "Feb 1" */
395
+ publishedAtRelative: string;
326
396
  /** ISO 8601 string */
327
397
  updatedAt: string;
328
398
 
329
- // Source (for link/quote types)
330
- sourceUrl?: string;
331
- sourceName?: string;
332
- sourceDomain?: string;
333
-
334
- // Media — URLs pre-computed
399
+ // Media -- URLs pre-computed
335
400
  media: MediaView[];
336
401
 
337
402
  // Thread context
@@ -339,12 +404,25 @@ export interface PostView {
339
404
  threadRootId?: number;
340
405
 
341
406
  // Raw content (for forms/editing, not typical theme use)
342
- content?: string;
407
+ body?: string;
408
+ }
409
+
410
+ /**
411
+ * Render-ready page data for theme components.
412
+ */
413
+ export interface PageView {
414
+ id: number;
415
+ slug: string;
416
+ title?: string;
417
+ bodyHtml?: string;
418
+ status: Status;
419
+ createdAt: string;
420
+ updatedAt: string;
343
421
  }
344
422
 
345
423
  /**
346
424
  * Render-ready media data for theme components.
347
- * URLs are pre-computed no lib/ imports needed.
425
+ * URLs are pre-computed -- no lib/ imports needed.
348
426
  */
349
427
  export interface MediaView {
350
428
  id: string;
@@ -360,13 +438,15 @@ export interface MediaView {
360
438
  }
361
439
 
362
440
  /**
363
- * Render-ready navigation link for theme components.
441
+ * Render-ready navigation item for theme components.
364
442
  * Active/external state pre-computed.
365
443
  */
366
- export interface NavLinkView {
444
+ export interface NavItemView {
367
445
  id: number;
446
+ type: NavItemType;
368
447
  label: string;
369
448
  url: string;
449
+ pageId?: number;
370
450
  /** Pre-computed based on currentPath */
371
451
  isActive: boolean;
372
452
  /** Pre-computed: starts with http(s):// */
@@ -406,6 +486,40 @@ export interface ArchiveGroup {
406
486
  posts: PostView[];
407
487
  }
408
488
 
489
+ // =============================================================================
490
+ // Timeline Load-More Types
491
+ // =============================================================================
492
+
493
+ /** A date-based group of timeline items (shared utility type) */
494
+ export interface DateGroup {
495
+ dateKey: string;
496
+ label: string;
497
+ items: TimelineItemView[];
498
+ }
499
+
500
+ /** A single SSE DOM patch instruction returned by timelineMore */
501
+ export interface TimelinePatch {
502
+ selector: string;
503
+ content: string;
504
+ mode?:
505
+ | "append"
506
+ | "prepend"
507
+ | "inner"
508
+ | "outer"
509
+ | "before"
510
+ | "after"
511
+ | "remove";
512
+ }
513
+
514
+ /** Props passed to the theme's timelineMore renderer */
515
+ export interface TimelineMoreProps {
516
+ items: TimelineItemView[];
517
+ lastDate?: string;
518
+ hasMore: boolean;
519
+ nextCursor?: number;
520
+ theme?: ThemeComponents;
521
+ }
522
+
409
523
  // =============================================================================
410
524
  // Configuration Types
411
525
  // =============================================================================
@@ -430,7 +544,7 @@ export interface SearchResult {
430
544
 
431
545
  export interface SiteLayoutProps {
432
546
  siteName: string;
433
- links: NavLinkView[];
547
+ links: NavItemView[];
434
548
  currentPath: string;
435
549
  }
436
550
 
@@ -441,6 +555,7 @@ export interface SiteLayoutProps {
441
555
  /** Props for the home page component */
442
556
  export interface HomePageProps {
443
557
  items: TimelineItemView[];
558
+ pinnedItems: PostView[];
444
559
  hasMore: boolean;
445
560
  nextCursor?: number;
446
561
  theme?: ThemeComponents;
@@ -454,7 +569,15 @@ export interface PostPageProps {
454
569
 
455
570
  /** Props for the custom page component */
456
571
  export interface SinglePageProps {
457
- page: PostView;
572
+ page: PageView;
573
+ theme?: ThemeComponents;
574
+ }
575
+
576
+ /** Props for the featured page component */
577
+ export interface FeaturedPageProps {
578
+ items: TimelineItemView[];
579
+ hasMore: boolean;
580
+ nextCursor?: number;
458
581
  theme?: ThemeComponents;
459
582
  }
460
583
 
@@ -463,7 +586,8 @@ export interface ArchivePageProps {
463
586
  groups: ArchiveGroup[];
464
587
  hasMore: boolean;
465
588
  nextCursor?: number;
466
- type?: PostType;
589
+ format?: Format;
590
+ featured?: boolean;
467
591
  theme?: ThemeComponents;
468
592
  }
469
593
 
@@ -477,10 +601,18 @@ export interface SearchPageProps {
477
601
  theme?: ThemeComponents;
478
602
  }
479
603
 
480
- /** Props for the collection page component */
604
+ /** Props for the single collection page component */
481
605
  export interface CollectionPageProps {
482
606
  collection: Collection;
483
607
  posts: PostView[];
608
+ hasMore: boolean;
609
+ nextCursor?: number;
610
+ theme?: ThemeComponents;
611
+ }
612
+
613
+ /** Props for the collections list page component */
614
+ export interface CollectionsPageProps {
615
+ collections: (Collection & { postCount: number })[];
484
616
  theme?: ThemeComponents;
485
617
  }
486
618
 
@@ -501,6 +633,7 @@ export interface FeedData {
501
633
  export interface SitemapData {
502
634
  siteUrl: string;
503
635
  posts: PostView[];
636
+ pages: PageView[];
504
637
  }
505
638
 
506
639
  // =============================================================================
@@ -529,6 +662,14 @@ export interface TimelineFeedProps {
529
662
  theme?: ThemeComponents;
530
663
  }
531
664
 
665
+ /** Props for the timeline load-more button */
666
+ export interface TimelineLoadMoreProps {
667
+ nextCursor: number;
668
+ /** Last visible date key (YYYY-MM-DD) for merging groups across pages */
669
+ lastDate?: string;
670
+ theme?: ThemeComponents;
671
+ }
672
+
532
673
  /**
533
674
  * Theme component overrides
534
675
  */
@@ -540,20 +681,21 @@ export interface ThemeComponents {
540
681
  HomePage?: FC<HomePageProps>;
541
682
  PostPage?: FC<PostPageProps>;
542
683
  SinglePage?: FC<SinglePageProps>;
684
+ FeaturedPage?: FC<FeaturedPageProps>;
543
685
  ArchivePage?: FC<ArchivePageProps>;
544
686
  SearchPage?: FC<SearchPageProps>;
545
687
  CollectionPage?: FC<CollectionPageProps>;
688
+ CollectionsPage?: FC<CollectionsPageProps>;
546
689
 
547
- // Timeline sub-components
690
+ // Timeline sub-components (by format)
548
691
  NoteCard?: FC<TimelineCardProps>;
549
- ArticleCard?: FC<TimelineCardProps>;
550
692
  LinkCard?: FC<TimelineCardProps>;
551
693
  QuoteCard?: FC<TimelineCardProps>;
552
- ImageCard?: FC<TimelineCardProps>;
553
694
  ThreadPreview?: FC<ThreadPreviewProps>;
554
695
  TimelineFeed?: FC<TimelineFeedProps>;
696
+ TimelineLoadMore?: FC<TimelineLoadMoreProps>;
555
697
 
556
- // Shared sub-components (re-exported real prop types from component files)
698
+ // Shared sub-components
557
699
  Pagination?: FC<PaginationComponentProps>;
558
700
  PagePagination?: FC<PagePaginationComponentProps>;
559
701
  EmptyState?: FC<EmptyStateComponentProps>;
@@ -606,13 +748,15 @@ export interface JantTheme {
606
748
  components?: ThemeComponents;
607
749
  /** Feed renderer overrides (RSS, Atom, Sitemap) */
608
750
  feed?: {
609
- /** Custom RSS 2.0 renderer returns XML string */
751
+ /** Custom RSS 2.0 renderer -- returns XML string */
610
752
  rss?: (data: FeedData) => string;
611
- /** Custom Atom renderer returns XML string */
753
+ /** Custom Atom renderer -- returns XML string */
612
754
  atom?: (data: FeedData) => string;
613
- /** Custom Sitemap renderer returns XML string */
755
+ /** Custom Sitemap renderer -- returns XML string */
614
756
  sitemap?: (data: SitemapData) => string;
615
757
  };
758
+ /** Renders SSE patches for timeline load-more responses */
759
+ timelineMore?: (props: TimelineMoreProps) => TimelinePatch[];
616
760
  /** CSS variable overrides (highest priority, always applied) */
617
761
  cssVariables?: Record<string, string>;
618
762
  /** Replace built-in color themes with a custom list */
@@ -1,120 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Timeline API Routes
4
- *
5
- * Provides load-more functionality for the timeline feed via SSE.
6
- */ import { Hono } from "hono";
7
- import { sse } from "../../lib/sse.js";
8
- import { buildMediaMap } from "../../lib/media-helpers.js";
9
- import { TimelineItem } from "../../themes/minimal/timeline/TimelineItem.js";
10
- import { ThreadPreview as DefaultThreadPreview } from "../../themes/minimal/timeline/ThreadPreview.js";
11
- import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
12
- const PAGE_SIZE = 20;
13
- export const timelineApiRoutes = new Hono();
14
- timelineApiRoutes.get("/", async (c)=>{
15
- const cursorParam = c.req.query("cursor");
16
- const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
17
- if (!cursor || isNaN(cursor)) {
18
- return c.json({
19
- error: "cursor parameter required"
20
- }, 400);
21
- }
22
- // Fetch one extra to determine if there are more
23
- const posts = await c.var.services.posts.list({
24
- visibility: [
25
- "featured",
26
- "quiet"
27
- ],
28
- excludeReplies: true,
29
- excludeTypes: [
30
- "page"
31
- ],
32
- limit: PAGE_SIZE + 1,
33
- cursor
34
- });
35
- const hasMore = posts.length > PAGE_SIZE;
36
- const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
37
- if (displayPosts.length === 0) {
38
- return sse(c, async (stream)=>{
39
- stream.remove("#load-more-container");
40
- });
41
- }
42
- // Build media map
43
- const postIds = displayPosts.map((p)=>p.id);
44
- const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
45
- const mediaCtx = createMediaContext(c);
46
- const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
47
- // Get reply counts to identify thread roots
48
- const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
49
- const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
50
- // Get thread previews
51
- const threadPreviews = await c.var.services.posts.getThreadPreviews(threadRootIds, 3);
52
- // Load media for preview replies
53
- const previewReplyIds = [];
54
- for (const replies of threadPreviews.values()){
55
- for (const reply of replies){
56
- previewReplyIds.push(reply.id);
57
- }
58
- }
59
- const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl) : new Map();
60
- // Assemble timeline items with View Models
61
- const items = displayPosts.map((post)=>{
62
- const postView = toPostView({
63
- ...post,
64
- mediaAttachments: mediaMap.get(post.id) ?? []
65
- }, mediaCtx);
66
- const replyCount = replyCounts.get(post.id) ?? 0;
67
- const previewReplies = threadPreviews.get(post.id);
68
- if (replyCount > 0 && previewReplies) {
69
- return {
70
- post: postView,
71
- threadPreview: {
72
- replies: toPostViews(previewReplies.map((r)=>({
73
- ...r,
74
- mediaAttachments: previewMediaMap.get(r.id) ?? []
75
- })), mediaCtx),
76
- totalReplyCount: replyCount
77
- }
78
- };
79
- }
80
- return {
81
- post: postView
82
- };
83
- });
84
- // Resolve theme components for card rendering
85
- const theme = c.var.config.theme?.components;
86
- const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
87
- // Render items to HTML
88
- const itemsHtml = items.map((item)=>{
89
- if (item.threadPreview) {
90
- return /*#__PURE__*/ _jsx(ResolvedThreadPreview, {
91
- rootPost: item.post,
92
- previewReplies: item.threadPreview.replies,
93
- totalReplyCount: item.threadPreview.totalReplyCount,
94
- theme: theme
95
- });
96
- }
97
- return /*#__PURE__*/ _jsx(TimelineItem, {
98
- item: item,
99
- theme: theme
100
- });
101
- }).map((jsx)=>jsx.toString()).join("");
102
- // Determine next cursor
103
- const lastPost = displayPosts[displayPosts.length - 1];
104
- const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
105
- // Build load-more button HTML
106
- const loadMoreHtml = nextCursor ? `<div id="load-more-container" class="mt-8 text-center"><button class="text-sm text-muted-foreground hover:text-foreground hover:underline" data-on:click="@get('/api/timeline?cursor=${nextCursor}')">Load more</button></div>` : "";
107
- return sse(c, async (stream)=>{
108
- // Append new items to the feed
109
- stream.patchElements(itemsHtml, {
110
- mode: "append",
111
- selector: "#timeline-feed"
112
- });
113
- // Replace or remove the load-more container
114
- if (loadMoreHtml) {
115
- stream.patchElements(loadMoreHtml);
116
- } else {
117
- stream.remove("#load-more-container");
118
- }
119
- });
120
- });