@jant/core 0.3.22 → 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 (178) hide show
  1. package/dist/app.js +23 -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 +5 -6
  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 +62 -73
  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/theme/components/index.js +0 -2
  47. package/dist/theme/index.js +10 -16
  48. package/dist/theme/layouts/index.js +0 -1
  49. package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
  50. package/dist/themes/threads/index.js +81 -0
  51. package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
  52. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  53. package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
  54. package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
  55. package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
  56. package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
  57. package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
  58. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  59. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  60. package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
  61. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  62. package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
  63. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  64. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  65. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  66. package/dist/types.js +24 -40
  67. package/package.json +2 -1
  68. package/src/__tests__/helpers/app.ts +4 -0
  69. package/src/__tests__/helpers/db.ts +51 -74
  70. package/src/app.tsx +27 -6
  71. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  72. package/src/db/migrations/meta/_journal.json +7 -0
  73. package/src/db/schema.ts +63 -46
  74. package/src/i18n/locales/en.po +216 -164
  75. package/src/i18n/locales/en.ts +1 -1
  76. package/src/i18n/locales/zh-Hans.po +216 -164
  77. package/src/i18n/locales/zh-Hans.ts +1 -1
  78. package/src/i18n/locales/zh-Hant.po +216 -164
  79. package/src/i18n/locales/zh-Hant.ts +1 -1
  80. package/src/index.ts +30 -15
  81. package/src/lib/__tests__/excerpt.test.ts +125 -0
  82. package/src/lib/__tests__/schemas.test.ts +166 -105
  83. package/src/lib/__tests__/theme-components.test.ts +4 -25
  84. package/src/lib/__tests__/time.test.ts +62 -0
  85. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  86. package/src/lib/__tests__/view.test.ts +217 -67
  87. package/src/lib/constants.ts +1 -4
  88. package/src/lib/excerpt.ts +87 -0
  89. package/src/lib/feed.ts +22 -7
  90. package/src/lib/navigation.ts +6 -7
  91. package/src/lib/render.tsx +1 -1
  92. package/src/lib/schemas.ts +118 -52
  93. package/src/lib/theme-components.ts +10 -13
  94. package/src/lib/time.ts +64 -0
  95. package/src/lib/timeline.ts +170 -0
  96. package/src/lib/view.ts +81 -83
  97. package/src/preset.css +45 -0
  98. package/src/routes/api/__tests__/posts.test.ts +50 -108
  99. package/src/routes/api/__tests__/search.test.ts +2 -3
  100. package/src/routes/api/posts.ts +30 -30
  101. package/src/routes/api/search.ts +4 -4
  102. package/src/routes/api/upload.ts +16 -6
  103. package/src/routes/dash/collections.tsx +18 -40
  104. package/src/routes/dash/index.tsx +2 -2
  105. package/src/routes/dash/navigation.tsx +27 -26
  106. package/src/routes/dash/pages.tsx +45 -60
  107. package/src/routes/dash/posts.tsx +44 -52
  108. package/src/routes/feed/rss.ts +2 -1
  109. package/src/routes/feed/sitemap.ts +14 -4
  110. package/src/routes/pages/archive.tsx +14 -10
  111. package/src/routes/pages/collection.tsx +17 -6
  112. package/src/routes/pages/home.tsx +56 -81
  113. package/src/routes/pages/page.tsx +64 -27
  114. package/src/routes/pages/post.tsx +5 -14
  115. package/src/routes/pages/search.tsx +2 -2
  116. package/src/services/__tests__/collection.test.ts +257 -158
  117. package/src/services/__tests__/media.test.ts +18 -18
  118. package/src/services/__tests__/navigation.test.ts +161 -87
  119. package/src/services/__tests__/post-timeline.test.ts +92 -88
  120. package/src/services/__tests__/post.test.ts +342 -206
  121. package/src/services/__tests__/search.test.ts +19 -25
  122. package/src/services/collection.ts +71 -113
  123. package/src/services/index.ts +9 -8
  124. package/src/services/navigation.ts +38 -71
  125. package/src/services/page.ts +124 -0
  126. package/src/services/post.ts +93 -103
  127. package/src/services/search.ts +38 -27
  128. package/src/styles/components.css +0 -54
  129. package/src/theme/components/MediaGallery.tsx +27 -96
  130. package/src/theme/components/PageForm.tsx +21 -21
  131. package/src/theme/components/PostForm.tsx +122 -118
  132. package/src/theme/components/PostList.tsx +58 -49
  133. package/src/theme/components/ThreadView.tsx +6 -3
  134. package/src/theme/components/TypeBadge.tsx +9 -17
  135. package/src/theme/components/VisibilityBadge.tsx +40 -23
  136. package/src/theme/components/index.ts +0 -13
  137. package/src/theme/index.ts +10 -16
  138. package/src/theme/layouts/index.ts +0 -1
  139. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  140. package/src/themes/threads/index.ts +100 -0
  141. package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
  142. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  143. package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
  144. package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
  145. package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
  146. package/src/themes/threads/pages/SinglePage.tsx +23 -0
  147. package/src/themes/threads/style.css +336 -0
  148. package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
  149. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  150. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  151. package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
  152. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  153. package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
  154. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  155. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  156. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  157. package/src/types.ts +242 -98
  158. package/dist/routes/api/timeline.js +0 -120
  159. package/dist/theme/components/timeline/ArticleCard.js +0 -46
  160. package/dist/theme/components/timeline/ImageCard.js +0 -83
  161. package/dist/theme/components/timeline/NoteCard.js +0 -34
  162. package/dist/theme/components/timeline/QuoteCard.js +0 -48
  163. package/dist/theme/components/timeline/TimelineFeed.js +0 -46
  164. package/dist/theme/components/timeline/index.js +0 -8
  165. package/dist/theme/layouts/SiteLayout.js +0 -131
  166. package/dist/theme/pages/CollectionPage.js +0 -63
  167. package/dist/theme/pages/index.js +0 -11
  168. package/src/routes/api/timeline.tsx +0 -159
  169. package/src/theme/components/timeline/ArticleCard.tsx +0 -45
  170. package/src/theme/components/timeline/ImageCard.tsx +0 -70
  171. package/src/theme/components/timeline/NoteCard.tsx +0 -34
  172. package/src/theme/components/timeline/QuoteCard.tsx +0 -48
  173. package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
  174. package/src/theme/components/timeline/index.ts +0 -8
  175. package/src/theme/layouts/SiteLayout.tsx +0 -132
  176. package/src/theme/pages/CollectionPage.tsx +0 -60
  177. package/src/theme/pages/SinglePage.tsx +0 -24
  178. package/src/theme/pages/index.ts +0 -13
@@ -1,26 +1,29 @@
1
1
  /**
2
- * Post Service
2
+ * Post Service (v2)
3
3
  *
4
- * CRUD operations for posts with Thread support
5
- */ import { eq, and, isNull, desc, or, inArray, notInArray, sql } from "drizzle-orm";
4
+ * CRUD operations for posts with Thread support.
5
+ * Posts have format (note/link/quote), status (draft/published),
6
+ * featured flag, and pinned flag.
7
+ */ import { eq, and, isNull, desc, or, inArray, sql } from "drizzle-orm";
6
8
  import { posts } from "../db/schema.js";
7
9
  import { now } from "../lib/time.js";
8
- import { extractDomain } from "../lib/url.js";
9
10
  import { render as renderMarkdown } from "../lib/markdown.js";
10
11
  export function createPostService(db) {
11
- // Helper to map DB row to Post type
12
12
  function toPost(row) {
13
13
  return {
14
14
  id: row.id,
15
- type: row.type,
16
- visibility: row.visibility,
15
+ format: row.format,
16
+ status: row.status,
17
+ featured: row.featured,
18
+ pinned: row.pinned,
19
+ slug: row.slug,
17
20
  title: row.title,
18
- path: row.path,
19
- content: row.content,
20
- contentHtml: row.contentHtml,
21
- sourceUrl: row.sourceUrl,
22
- sourceName: row.sourceName,
23
- sourceDomain: row.sourceDomain,
21
+ url: row.url,
22
+ body: row.body,
23
+ bodyHtml: row.bodyHtml,
24
+ quoteText: row.quoteText,
25
+ rating: row.rating,
26
+ collectionId: row.collectionId,
24
27
  replyToId: row.replyToId,
25
28
  threadId: row.threadId,
26
29
  deletedAt: row.deletedAt,
@@ -34,41 +37,36 @@ export function createPostService(db) {
34
37
  const result = await db.select().from(posts).where(and(eq(posts.id, id), isNull(posts.deletedAt))).limit(1);
35
38
  return result[0] ? toPost(result[0]) : null;
36
39
  },
37
- async getByPath (path) {
38
- const result = await db.select().from(posts).where(and(eq(posts.path, path), isNull(posts.deletedAt))).limit(1);
40
+ async getBySlug (slug) {
41
+ const result = await db.select().from(posts).where(and(eq(posts.slug, slug), isNull(posts.deletedAt))).limit(1);
39
42
  return result[0] ? toPost(result[0]) : null;
40
43
  },
41
44
  async list (filters = {}) {
42
45
  const conditions = [];
43
- // Visibility filter
44
- if (filters.visibility) {
45
- if (Array.isArray(filters.visibility)) {
46
- conditions.push(inArray(posts.visibility, filters.visibility));
47
- } else {
48
- conditions.push(eq(posts.visibility, filters.visibility));
49
- }
46
+ if (filters.status) {
47
+ conditions.push(eq(posts.status, filters.status));
48
+ }
49
+ if (filters.featured !== undefined) {
50
+ conditions.push(eq(posts.featured, filters.featured ? 1 : 0));
50
51
  }
51
- // Type filter
52
- if (filters.type) {
53
- conditions.push(eq(posts.type, filters.type));
52
+ if (filters.pinned !== undefined) {
53
+ conditions.push(eq(posts.pinned, filters.pinned ? 1 : 0));
54
54
  }
55
- // Exclude types filter
56
- if (filters.excludeTypes && filters.excludeTypes.length > 0) {
57
- conditions.push(notInArray(posts.type, filters.excludeTypes));
55
+ if (filters.format) {
56
+ conditions.push(eq(posts.format, filters.format));
57
+ }
58
+ if (filters.collectionId !== undefined) {
59
+ conditions.push(eq(posts.collectionId, filters.collectionId));
58
60
  }
59
- // Thread filter
60
61
  if (filters.threadId) {
61
62
  conditions.push(eq(posts.threadId, filters.threadId));
62
63
  }
63
- // Exclude replies (posts that are part of a thread but not the root)
64
64
  if (filters.excludeReplies) {
65
65
  conditions.push(isNull(posts.threadId));
66
66
  }
67
- // Exclude deleted unless specified
68
67
  if (!filters.includeDeleted) {
69
68
  conditions.push(isNull(posts.deletedAt));
70
69
  }
71
- // Cursor pagination
72
70
  if (filters.cursor) {
73
71
  conditions.push(sql`${posts.id} < ${filters.cursor}`);
74
72
  }
@@ -78,35 +76,36 @@ export function createPostService(db) {
78
76
  },
79
77
  async create (data) {
80
78
  const timestamp = now();
81
- // Process content
82
- const contentHtml = data.content ? renderMarkdown(data.content) : null;
83
- // Extract domain from source URL
84
- const sourceDomain = data.sourceUrl ? extractDomain(data.sourceUrl) : null;
79
+ const bodyHtml = data.body ? renderMarkdown(data.body) : null;
85
80
  // Handle thread relationship
86
81
  let threadId = null;
87
- let visibility = data.visibility ?? "quiet";
82
+ let status = data.status ?? "published";
83
+ let featured = data.featured ?? false;
88
84
  if (data.replyToId) {
89
85
  const parent = await this.getById(data.replyToId);
90
86
  if (parent) {
91
- // thread_id = parent's thread_id or parent's id (if parent is root)
92
87
  threadId = parent.threadId ?? parent.id;
93
- // Inherit visibility from root
88
+ // Inherit status and featured from root
94
89
  const root = parent.threadId ? await this.getById(parent.threadId) : parent;
95
90
  if (root) {
96
- visibility = root.visibility;
91
+ status = root.status;
92
+ featured = root.featured === 1;
97
93
  }
98
94
  }
99
95
  }
100
96
  const result = await db.insert(posts).values({
101
- type: data.type,
102
- visibility,
97
+ format: data.format,
98
+ status,
99
+ featured: featured ? 1 : 0,
100
+ pinned: data.pinned ? 1 : 0,
101
+ slug: data.slug ?? null,
103
102
  title: data.title ?? null,
104
- path: data.path ?? null,
105
- content: data.content ?? null,
106
- contentHtml,
107
- sourceUrl: data.sourceUrl ?? null,
108
- sourceName: data.sourceName ?? null,
109
- sourceDomain,
103
+ url: data.url ?? null,
104
+ body: data.body ?? null,
105
+ bodyHtml,
106
+ quoteText: data.quoteText ?? null,
107
+ rating: data.rating ?? null,
108
+ collectionId: data.collectionId ?? null,
110
109
  replyToId: data.replyToId ?? null,
111
110
  threadId,
112
111
  publishedAt: data.publishedAt ?? timestamp,
@@ -123,26 +122,27 @@ export function createPostService(db) {
123
122
  const updates = {
124
123
  updatedAt: timestamp
125
124
  };
126
- if (data.type !== undefined) updates.type = data.type;
125
+ if (data.format !== undefined) updates.format = data.format;
126
+ if (data.slug !== undefined) updates.slug = data.slug;
127
127
  if (data.title !== undefined) updates.title = data.title;
128
- if (data.path !== undefined) updates.path = data.path;
128
+ if (data.url !== undefined) updates.url = data.url;
129
+ if (data.quoteText !== undefined) updates.quoteText = data.quoteText;
130
+ if (data.rating !== undefined) updates.rating = data.rating;
131
+ if (data.collectionId !== undefined) updates.collectionId = data.collectionId;
129
132
  if (data.publishedAt !== undefined) updates.publishedAt = data.publishedAt;
130
- if (data.sourceUrl !== undefined) {
131
- updates.sourceUrl = data.sourceUrl;
132
- updates.sourceDomain = data.sourceUrl ? extractDomain(data.sourceUrl) : null;
133
- }
134
- if (data.sourceName !== undefined) updates.sourceName = data.sourceName;
135
- if (data.content !== undefined) {
136
- updates.content = data.content;
137
- updates.contentHtml = data.content ? renderMarkdown(data.content) : null;
133
+ if (data.pinned !== undefined) updates.pinned = data.pinned ? 1 : 0;
134
+ if (data.body !== undefined) {
135
+ updates.body = data.body;
136
+ updates.bodyHtml = data.body ? renderMarkdown(data.body) : null;
138
137
  }
139
- // Handle visibility change - cascade to thread if this is root
140
- if (data.visibility !== undefined && data.visibility !== existing.visibility) {
141
- updates.visibility = data.visibility;
142
- // If this is a root post, cascade visibility to all thread posts
143
- if (!existing.threadId) {
144
- await this.updateThreadVisibility(id, data.visibility);
145
- }
138
+ // Handle status/featured change - cascade to thread if this is root
139
+ const statusChanged = data.status !== undefined && data.status !== existing.status;
140
+ const featuredChanged = data.featured !== undefined && (data.featured ? 1 : 0) !== existing.featured;
141
+ if (statusChanged) updates.status = data.status;
142
+ if (featuredChanged) updates.featured = data.featured ? 1 : 0;
143
+ // If this is a root post and status/featured changed, cascade to thread
144
+ if ((statusChanged || featuredChanged) && !existing.threadId) {
145
+ await this.updateThreadStatusAndFeatured(id, data.status ?? existing.status, data.featured !== undefined ? data.featured : existing.featured === 1);
146
146
  }
147
147
  const result = await db.update(posts).set(updates).where(eq(posts.id, id)).returning();
148
148
  return result[0] ? toPost(result[0]) : null;
@@ -158,7 +158,6 @@ export function createPostService(db) {
158
158
  updatedAt: timestamp
159
159
  }).where(or(eq(posts.id, id), eq(posts.threadId, id)));
160
160
  } else {
161
- // Just delete this single post
162
161
  await db.update(posts).set({
163
162
  deletedAt: timestamp,
164
163
  updatedAt: timestamp
@@ -170,10 +169,11 @@ export function createPostService(db) {
170
169
  const rows = await db.select().from(posts).where(and(or(eq(posts.id, rootId), eq(posts.threadId, rootId)), isNull(posts.deletedAt))).orderBy(posts.createdAt);
171
170
  return rows.map(toPost);
172
171
  },
173
- async updateThreadVisibility (rootId, visibility) {
172
+ async updateThreadStatusAndFeatured (rootId, status, featured) {
174
173
  const timestamp = now();
175
174
  await db.update(posts).set({
176
- visibility,
175
+ status,
176
+ featured: featured ? 1 : 0,
177
177
  updatedAt: timestamp
178
178
  }).where(eq(posts.threadId, rootId));
179
179
  },
@@ -194,7 +194,6 @@ export function createPostService(db) {
194
194
  async getThreadPreviews (rootIds, previewCount = 3) {
195
195
  if (rootIds.length === 0) return new Map();
196
196
  const rows = await db.select().from(posts).where(and(inArray(posts.threadId, rootIds), isNull(posts.deletedAt))).orderBy(posts.threadId, posts.createdAt);
197
- // Partition by threadId, take first previewCount per thread
198
197
  const result = new Map();
199
198
  for (const row of rows){
200
199
  const post = toPost(row);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Search Service
2
+ * Search Service (v2)
3
3
  *
4
4
  * Full-text search using FTS5
5
5
  */ export function createSearchService(d1) {
@@ -7,19 +7,21 @@
7
7
  async search (query, options = {}) {
8
8
  const limit = options.limit ?? 20;
9
9
  const offset = options.offset ?? 0;
10
- const visibility = options.visibility ?? [
11
- "featured",
12
- "quiet"
10
+ const status = options.status ?? [
11
+ "published"
13
12
  ];
14
13
  // Escape and prepare the query for FTS5
15
- // FTS5 uses * for prefix matching
16
14
  const ftsQuery = query.trim().split(/\s+/).filter((term)=>term.length > 0).map((term)=>`"${term.replace(/"/g, '""')}"*`).join(" ");
17
15
  if (!ftsQuery) {
18
16
  return [];
19
17
  }
20
- // Build visibility placeholders
21
- const visibilityPlaceholders = visibility.map(()=>"?").join(", ");
22
- // Query FTS5 table and join with posts using raw D1 query
18
+ // Build status placeholders
19
+ const statusPlaceholders = status.map(()=>"?").join(", ");
20
+ // Build format filter
21
+ const formatFilter = options.format ? "AND posts.format = ?" : "";
22
+ const formatParams = options.format ? [
23
+ options.format
24
+ ] : [];
23
25
  const stmt = d1.prepare(`
24
26
  SELECT
25
27
  posts.*,
@@ -29,23 +31,27 @@
29
31
  JOIN posts ON posts.id = posts_fts.rowid
30
32
  WHERE posts_fts MATCH ?
31
33
  AND posts.deleted_at IS NULL
32
- AND posts.visibility IN (${visibilityPlaceholders})
34
+ AND posts.status IN (${statusPlaceholders})
35
+ ${formatFilter}
33
36
  ORDER BY posts_fts.rank
34
37
  LIMIT ? OFFSET ?
35
38
  `);
36
- const { results } = await stmt.bind(ftsQuery, ...visibility, limit, offset).all();
39
+ const { results } = await stmt.bind(ftsQuery, ...status, ...formatParams, limit, offset).all();
37
40
  return (results || []).map((row)=>({
38
41
  post: {
39
42
  id: row.id,
40
- type: row.type,
41
- visibility: row.visibility,
43
+ format: row.format,
44
+ status: row.status,
45
+ featured: row.featured,
46
+ pinned: row.pinned,
47
+ slug: row.slug,
42
48
  title: row.title,
43
- path: row.path,
44
- content: row.content,
45
- contentHtml: row.content_html,
46
- sourceUrl: row.source_url,
47
- sourceName: row.source_name,
48
- sourceDomain: row.source_domain,
49
+ url: row.url,
50
+ body: row.body,
51
+ bodyHtml: row.body_html,
52
+ quoteText: row.quote_text,
53
+ rating: row.rating,
54
+ collectionId: row.collection_id,
49
55
  replyToId: row.reply_to_id,
50
56
  threadId: row.thread_id,
51
57
  deletedAt: row.deleted_at,
@@ -1,107 +1,35 @@
1
1
  /**
2
2
  * Media Gallery Component
3
3
  *
4
- * Renders media attachments on public post pages.
5
- * Layout adapts based on the number of images.
6
- */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
4
+ * Renders media attachments in a horizontal scrollable row,
5
+ * similar to Threads.net's image carousel.
6
+ */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
7
7
  export const MediaGallery = ({ attachments })=>{
8
8
  const images = attachments.filter((a)=>a.mimeType.startsWith("image/"));
9
9
  if (images.length === 0) return null;
10
- if (images.length === 1) {
11
- const [img] = images;
12
- if (!img) return null;
13
- return /*#__PURE__*/ _jsx("div", {
14
- class: "mt-3",
15
- children: /*#__PURE__*/ _jsx("a", {
10
+ const single = images.length === 1;
11
+ return /*#__PURE__*/ _jsx("div", {
12
+ class: `mt-3 flex gap-2 ${single ? "" : "overflow-x-auto scroll-smooth snap-x snap-mandatory"}`,
13
+ style: single ? undefined : "scrollbar-width: none; -ms-overflow-style: none;",
14
+ children: images.map((img)=>{
15
+ const aspectRatio = img.width && img.height ? img.width / img.height : 4 / 3;
16
+ const itemWidth = single ? undefined : `${Math.round(320 * Math.min(Math.max(aspectRatio, 0.6), 1.6))}px`;
17
+ return /*#__PURE__*/ _jsx("a", {
16
18
  href: img.url,
17
19
  target: "_blank",
18
20
  rel: "noopener noreferrer",
21
+ class: `${single ? "" : "shrink-0 snap-start"} block rounded-lg overflow-hidden`,
22
+ style: single ? undefined : {
23
+ width: itemWidth,
24
+ maxWidth: "85%"
25
+ },
19
26
  children: /*#__PURE__*/ _jsx("img", {
20
27
  src: img.thumbnailUrl,
21
28
  alt: img.altText || "",
22
- width: img.width ?? undefined,
23
- height: img.height ?? undefined,
24
- class: "rounded-lg max-w-full h-auto",
29
+ class: single ? "rounded-lg max-w-full max-h-96 h-auto object-contain" : "h-80 w-full object-cover",
25
30
  loading: "lazy"
26
31
  })
27
- })
28
- });
29
- }
30
- if (images.length === 2) {
31
- return /*#__PURE__*/ _jsx("div", {
32
- class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
33
- children: images.map((img)=>/*#__PURE__*/ _jsx("a", {
34
- href: img.url,
35
- target: "_blank",
36
- rel: "noopener noreferrer",
37
- class: "aspect-square",
38
- children: /*#__PURE__*/ _jsx("img", {
39
- src: img.thumbnailUrl,
40
- alt: img.altText || "",
41
- class: "w-full h-full object-cover",
42
- loading: "lazy"
43
- })
44
- }, img.id))
45
- });
46
- }
47
- if (images.length === 3) {
48
- const [first, ...rest] = images;
49
- if (!first) return null;
50
- return /*#__PURE__*/ _jsxs("div", {
51
- class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
52
- children: [
53
- /*#__PURE__*/ _jsx("a", {
54
- href: first.url,
55
- target: "_blank",
56
- rel: "noopener noreferrer",
57
- class: "row-span-2",
58
- children: /*#__PURE__*/ _jsx("img", {
59
- src: first.thumbnailUrl,
60
- alt: first.altText || "",
61
- class: "w-full h-full object-cover",
62
- loading: "lazy"
63
- })
64
- }),
65
- rest.map((img)=>/*#__PURE__*/ _jsx("a", {
66
- href: img.url,
67
- target: "_blank",
68
- rel: "noopener noreferrer",
69
- class: "aspect-square",
70
- children: /*#__PURE__*/ _jsx("img", {
71
- src: img.thumbnailUrl,
72
- alt: img.altText || "",
73
- class: "w-full h-full object-cover",
74
- loading: "lazy"
75
- })
76
- }, img.id))
77
- ]
78
- });
79
- }
80
- // 4+ images: 2-column grid, show first 4 with remaining count
81
- const shown = images.slice(0, 4);
82
- const remaining = images.length - 4;
83
- return /*#__PURE__*/ _jsx("div", {
84
- class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
85
- children: shown.map((img, i)=>/*#__PURE__*/ _jsxs("a", {
86
- href: img.url,
87
- target: "_blank",
88
- rel: "noopener noreferrer",
89
- class: "relative aspect-square",
90
- children: [
91
- /*#__PURE__*/ _jsx("img", {
92
- src: img.thumbnailUrl,
93
- alt: img.altText || "",
94
- class: "w-full h-full object-cover",
95
- loading: "lazy"
96
- }),
97
- i === 3 && remaining > 0 && /*#__PURE__*/ _jsxs("div", {
98
- class: "absolute inset-0 bg-black/50 flex items-center justify-center text-white text-xl font-semibold",
99
- children: [
100
- "+",
101
- remaining
102
- ]
103
- })
104
- ]
105
- }, img.id))
32
+ }, img.id);
33
+ })
106
34
  });
107
35
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Page Creation/Edit Form
3
3
  *
4
- * For managing custom pages (posts with type="page")
4
+ * For managing standalone pages (about, now, etc.)
5
5
  */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
6
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
7
  export const PageForm = ({ page, action, cancelUrl = "/dash/pages" })=>{
@@ -9,9 +9,9 @@ export const PageForm = ({ page, action, cancelUrl = "/dash/pages" })=>{
9
9
  const isEdit = !!page;
10
10
  const signals = JSON.stringify({
11
11
  title: page?.title ?? "",
12
- path: page?.path ?? "",
13
- content: page?.content ?? "",
14
- visibility: page?.visibility ?? "unlisted"
12
+ slug: page?.slug ?? "",
13
+ body: page?.body ?? "",
14
+ status: page?.status ?? "published"
15
15
  }).replace(/</g, "\\u003c");
16
16
  return /*#__PURE__*/ _jsxs("form", {
17
17
  "data-signals": signals,
@@ -50,8 +50,8 @@ export const PageForm = ({ page, action, cancelUrl = "/dash/pages" })=>{
50
50
  /*#__PURE__*/ _jsx("label", {
51
51
  class: "label",
52
52
  children: $__i18n._({
53
- id: "I6gXOa",
54
- message: "Path"
53
+ id: "L85WcV",
54
+ message: "Slug"
55
55
  })
56
56
  }),
57
57
  /*#__PURE__*/ _jsxs("div", {
@@ -63,7 +63,7 @@ export const PageForm = ({ page, action, cancelUrl = "/dash/pages" })=>{
63
63
  }),
64
64
  /*#__PURE__*/ _jsx("input", {
65
65
  type: "text",
66
- "data-bind": "path",
66
+ "data-bind": "slug",
67
67
  class: "input flex-1",
68
68
  placeholder: "about",
69
69
  pattern: "[a-z0-9\\-]+",
@@ -95,14 +95,14 @@ export const PageForm = ({ page, action, cancelUrl = "/dash/pages" })=>{
95
95
  })
96
96
  }),
97
97
  /*#__PURE__*/ _jsx("textarea", {
98
- "data-bind": "content",
98
+ "data-bind": "body",
99
99
  class: "textarea min-h-48",
100
100
  placeholder: $__i18n._({
101
101
  id: "7G4SBz",
102
102
  message: "Page content (Markdown supported)..."
103
103
  }),
104
104
  required: true,
105
- children: page?.content ?? ""
105
+ children: page?.body ?? ""
106
106
  })
107
107
  ]
108
108
  }),
@@ -117,12 +117,12 @@ export const PageForm = ({ page, action, cancelUrl = "/dash/pages" })=>{
117
117
  })
118
118
  }),
119
119
  /*#__PURE__*/ _jsxs("select", {
120
- "data-bind": "visibility",
120
+ "data-bind": "status",
121
121
  class: "select",
122
122
  children: [
123
123
  /*#__PURE__*/ _jsx("option", {
124
- value: "unlisted",
125
- selected: page?.visibility === "unlisted" || !page,
124
+ value: "published",
125
+ selected: page?.status === "published" || !page,
126
126
  children: $__i18n._({
127
127
  id: "u3wRF+",
128
128
  message: "Published"
@@ -130,7 +130,7 @@ export const PageForm = ({ page, action, cancelUrl = "/dash/pages" })=>{
130
130
  }),
131
131
  /*#__PURE__*/ _jsx("option", {
132
132
  value: "draft",
133
- selected: page?.visibility === "draft",
133
+ selected: page?.status === "draft",
134
134
  children: $__i18n._({
135
135
  id: "eneWvv",
136
136
  message: "Draft"
@@ -141,8 +141,8 @@ export const PageForm = ({ page, action, cancelUrl = "/dash/pages" })=>{
141
141
  /*#__PURE__*/ _jsx("p", {
142
142
  class: "text-xs text-muted-foreground mt-1",
143
143
  children: $__i18n._({
144
- id: "JiP4aa",
145
- message: "Published pages are accessible via their path. Drafts are not visible."
144
+ id: "jSRrXo",
145
+ message: "Published pages are accessible via their slug. Drafts are not visible."
146
146
  })
147
147
  })
148
148
  ]