@jant/core 0.3.36 → 0.3.37

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 (271) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Search Service (v2)
3
3
  *
4
- * Full-text search using FTS5
4
+ * Full-text search using FTS5 trigram for queries ≥ 3 characters.
5
+ * Falls back to LIKE for shorter queries (common in CJK languages where
6
+ * 2-character words cannot form a trigram).
5
7
  */
6
8
 
7
9
  import type { Post, Status, Format, SearchResult } from "../types.js";
10
+ import { escapeHtml } from "../lib/html.js";
8
11
 
9
12
  export type { SearchResult };
10
13
 
@@ -24,100 +27,182 @@ export interface SearchService {
24
27
  }
25
28
 
26
29
  interface RawSearchRow {
27
- id: number;
30
+ id: string;
28
31
  format: string;
29
32
  status: string;
30
- visibility: string;
31
- pinned: number;
32
- path: string | null;
33
+ visibility: string | null;
34
+ effective_visibility: string | null;
35
+ pinned_at: number | null;
36
+ featured_at: number | null;
37
+ slug: string;
33
38
  title: string | null;
34
39
  url: string | null;
35
40
  body: string | null;
36
41
  body_html: string | null;
42
+ body_text: string | null;
37
43
  quote_text: string | null;
38
44
  summary: string | null;
39
45
  rating: number | null;
40
- collection_id: number | null;
41
- reply_to_id: number | null;
42
- thread_id: number | null;
46
+ collection_id: string | null;
47
+ reply_to_id: string | null;
48
+ thread_id: string;
43
49
  deleted_at: number | null;
44
- published_at: number;
50
+ published_at: number | null;
51
+ last_activity_at: number | null;
45
52
  created_at: number;
46
53
  updated_at: number;
47
54
  rank: number;
48
- snippet: string;
55
+ snippet: string | null;
56
+ }
57
+
58
+ function mapRow(row: RawSearchRow): SearchResult {
59
+ return {
60
+ post: {
61
+ id: row.id,
62
+ format: row.format as Post["format"],
63
+ status: row.status as Post["status"],
64
+ visibility: (row.effective_visibility ??
65
+ row.visibility) as Post["visibility"],
66
+ pinnedAt: row.pinned_at,
67
+ featuredAt: row.featured_at,
68
+ slug: row.slug,
69
+ title: row.title,
70
+ url: row.url,
71
+ body: row.body,
72
+ bodyHtml: row.body_html,
73
+ bodyText: row.body_text,
74
+ quoteText: row.quote_text,
75
+ summary: row.summary,
76
+ rating: row.rating,
77
+ replyToId: row.reply_to_id,
78
+ threadId: row.thread_id,
79
+ deletedAt: row.deleted_at,
80
+ publishedAt: row.published_at,
81
+ lastActivityAt:
82
+ row.last_activity_at ?? row.published_at ?? row.updated_at,
83
+ createdAt: row.created_at,
84
+ updatedAt: row.updated_at,
85
+ },
86
+ rank: row.rank,
87
+ snippet: row.snippet
88
+ ? escapeHtml(row.snippet)
89
+ .replaceAll(String.fromCharCode(2), "<mark>")
90
+ .replaceAll(String.fromCharCode(3), "</mark>")
91
+ : undefined,
92
+ };
49
93
  }
50
94
 
51
95
  export function createSearchService(d1: D1Database): SearchService {
96
+ async function searchFts(
97
+ query: string,
98
+ options: SearchOptions,
99
+ ): Promise<SearchResult[]> {
100
+ const limit = options.limit ?? 20;
101
+ const offset = options.offset ?? 0;
102
+ const status = options.status ?? ["published"];
103
+
104
+ const ftsQuery = query
105
+ .trim()
106
+ .split(/\s+/)
107
+ .filter((term) => term.length > 0)
108
+ .map((term) => `"${term.replace(/"/g, '""')}"*`)
109
+ .join(" ");
110
+
111
+ if (!ftsQuery) return [];
112
+
113
+ const statusPlaceholders = status.map(() => "?").join(", ");
114
+ const formatFilter = options.format ? "AND post.format = ?" : "";
115
+ const formatParams = options.format ? [options.format] : [];
116
+
117
+ const stmt = d1.prepare(`
118
+ SELECT
119
+ post.*,
120
+ COALESCE(post.visibility, root_post.visibility) AS effective_visibility,
121
+ path_registry.path AS slug,
122
+ post_fts.rank AS rank,
123
+ snippet(post_fts, 1, char(2), char(3), '...', 32) AS snippet
124
+ FROM post_fts
125
+ JOIN post ON post.rowid = post_fts.rowid
126
+ JOIN post AS root_post ON root_post.id = post.thread_id
127
+ JOIN path_registry
128
+ ON path_registry.post_id = post.id
129
+ AND path_registry.kind = 'slug'
130
+ WHERE post_fts MATCH ?
131
+ AND post.deleted_at IS NULL
132
+ AND post.status IN (${statusPlaceholders})
133
+ ${formatFilter}
134
+ ORDER BY post_fts.rank
135
+ LIMIT ? OFFSET ?
136
+ `);
137
+
138
+ const { results } = await stmt
139
+ .bind(ftsQuery, ...status, ...formatParams, limit, offset)
140
+ .all<RawSearchRow>();
141
+
142
+ return (results || []).map(mapRow);
143
+ }
144
+
145
+ async function searchLike(
146
+ query: string,
147
+ options: SearchOptions,
148
+ ): Promise<SearchResult[]> {
149
+ const limit = options.limit ?? 20;
150
+ const offset = options.offset ?? 0;
151
+ const status = options.status ?? ["published"];
152
+ const like = `%${query}%`;
153
+
154
+ const statusPlaceholders = status.map(() => "?").join(", ");
155
+ const formatFilter = options.format ? "AND post.format = ?" : "";
156
+ const formatParams = options.format ? [options.format] : [];
157
+
158
+ const stmt = d1.prepare(`
159
+ SELECT
160
+ post.*,
161
+ COALESCE(post.visibility, root_post.visibility) AS effective_visibility,
162
+ path_registry.path AS slug,
163
+ 0 AS rank,
164
+ NULL AS snippet
165
+ FROM post
166
+ JOIN post AS root_post ON root_post.id = post.thread_id
167
+ JOIN path_registry
168
+ ON path_registry.post_id = post.id
169
+ AND path_registry.kind = 'slug'
170
+ WHERE (
171
+ post.title LIKE ? OR
172
+ post.body_text LIKE ? OR
173
+ post.quote_text LIKE ? OR
174
+ post.url LIKE ?
175
+ )
176
+ AND post.deleted_at IS NULL
177
+ AND post.status IN (${statusPlaceholders})
178
+ ${formatFilter}
179
+ ORDER BY post.published_at DESC
180
+ LIMIT ? OFFSET ?
181
+ `);
182
+
183
+ const { results } = await stmt
184
+ .bind(like, like, like, like, ...status, ...formatParams, limit, offset)
185
+ .all<RawSearchRow>();
186
+
187
+ return (results || []).map(mapRow);
188
+ }
189
+
52
190
  return {
53
191
  async search(query, options = {}) {
54
- const limit = options.limit ?? 20;
55
- const offset = options.offset ?? 0;
56
- const status = options.status ?? ["published"];
57
-
58
- // Escape and prepare the query for FTS5
59
- const ftsQuery = query
60
- .trim()
61
- .split(/\s+/)
62
- .filter((term) => term.length > 0)
63
- .map((term) => `"${term.replace(/"/g, '""')}"*`)
64
- .join(" ");
65
-
66
- if (!ftsQuery) {
67
- return [];
192
+ const trimmed = query.trim();
193
+ if (!trimmed) return [];
194
+
195
+ // Trigram FTS requires at least 3 characters.
196
+ // For shorter queries (common in CJK), fall back to LIKE.
197
+ const charCount = [...trimmed].length;
198
+ if (charCount < 3) {
199
+ return searchLike(trimmed, options);
68
200
  }
69
201
 
70
- // Build status placeholders
71
- const statusPlaceholders = status.map(() => "?").join(", ");
72
-
73
- // Build format filter
74
- const formatFilter = options.format ? "AND posts.format = ?" : "";
75
- const formatParams = options.format ? [options.format] : [];
76
-
77
- const stmt = d1.prepare(`
78
- SELECT
79
- posts.*,
80
- posts_fts.rank AS rank,
81
- snippet(posts_fts, 1, '<mark>', '</mark>', '...', 32) AS snippet
82
- FROM posts_fts
83
- JOIN posts ON posts.id = posts_fts.rowid
84
- WHERE posts_fts MATCH ?
85
- AND posts.deleted_at IS NULL
86
- AND posts.status IN (${statusPlaceholders})
87
- ${formatFilter}
88
- ORDER BY posts_fts.rank
89
- LIMIT ? OFFSET ?
90
- `);
91
-
92
- const { results } = await stmt
93
- .bind(ftsQuery, ...status, ...formatParams, limit, offset)
94
- .all<RawSearchRow>();
95
-
96
- return (results || []).map((row) => ({
97
- post: {
98
- id: row.id,
99
- format: row.format as Post["format"],
100
- status: row.status as Post["status"],
101
- visibility: row.visibility as Post["visibility"],
102
- pinned: row.pinned,
103
- path: row.path,
104
- title: row.title,
105
- url: row.url,
106
- body: row.body,
107
- bodyHtml: row.body_html,
108
- quoteText: row.quote_text,
109
- summary: row.summary,
110
- rating: row.rating,
111
- replyToId: row.reply_to_id,
112
- threadId: row.thread_id,
113
- deletedAt: row.deleted_at,
114
- publishedAt: row.published_at,
115
- createdAt: row.created_at,
116
- updatedAt: row.updated_at,
117
- },
118
- rank: row.rank,
119
- snippet: row.snippet,
120
- }));
202
+ const ftsResults = await searchFts(trimmed, options);
203
+ if (ftsResults.length > 0) return ftsResults;
204
+
205
+ return searchLike(trimmed, options);
121
206
  },
122
207
  };
123
208
  }
@@ -5,6 +5,20 @@
5
5
  * AFTER Tailwind is initialized in the user's CSS entry.
6
6
  */
7
7
 
8
+ /* Search result highlight */
9
+ mark {
10
+ background-color: var(--search-mark-bg);
11
+ color: var(--search-mark-color);
12
+ border-radius: 2px;
13
+ padding: 0 2px;
14
+ }
15
+
16
+ .search-snippet {
17
+ font-size: var(--text-sm);
18
+ color: var(--muted-foreground);
19
+ line-height: var(--leading);
20
+ }
21
+
8
22
  /* Icon stroke width — CSS property overrides SVG presentational attributes */
9
23
  svg[stroke-width] {
10
24
  stroke-width: var(--icon-stroke);
@@ -28,7 +42,17 @@ svg[stroke-width].icon-fine {
28
42
  /* Toast notifications */
29
43
  @layer components {
30
44
  .toast-container {
31
- @apply fixed top-4 right-4 z-50 flex flex-col gap-2 pointer-events-none;
45
+ @apply fixed flex flex-col gap-2 pointer-events-none;
46
+ /* Override browser popover defaults for both idle and open states */
47
+ @apply inset-auto border-none bg-transparent p-0 m-0 overflow-visible;
48
+ top: 1rem;
49
+ right: 1rem;
50
+
51
+ &:popover-open {
52
+ @apply inset-auto border-none bg-transparent p-0 m-0 overflow-visible;
53
+ top: 1rem;
54
+ right: 1rem;
55
+ }
32
56
  }
33
57
 
34
58
  .toast {
@@ -54,6 +78,17 @@ svg[stroke-width].icon-fine {
54
78
  color: var(--color-destructive);
55
79
  }
56
80
 
81
+ .toast-action {
82
+ @apply shrink-0 text-xs font-medium no-underline;
83
+ color: var(--color-foreground);
84
+ opacity: 0.7;
85
+ transition: opacity 0.15s;
86
+
87
+ &:hover {
88
+ opacity: 1;
89
+ }
90
+ }
91
+
57
92
  .toast-close {
58
93
  @apply shrink-0 translate-y-0.5 cursor-pointer rounded-sm p-0 border-0 bg-transparent;
59
94
  color: var(--color-muted-foreground);
@@ -92,7 +127,7 @@ svg[stroke-width].icon-fine {
92
127
  }
93
128
  }
94
129
 
95
- /* Dashboard header */
130
+ /* Admin header */
96
131
  @layer components {
97
132
  .dash-header {
98
133
  padding: 20px 0 0;
@@ -132,7 +167,7 @@ svg[stroke-width].icon-fine {
132
167
  justify-content: center;
133
168
  color: white;
134
169
  font-size: 0.625rem;
135
- font-weight: 600;
170
+ font-weight: var(--fw-semibold);
136
171
  line-height: 1;
137
172
  }
138
173
 
@@ -165,7 +200,7 @@ svg[stroke-width].icon-fine {
165
200
 
166
201
  .dash-header-link-active {
167
202
  color: var(--color-foreground);
168
- font-weight: 500;
203
+ font-weight: var(--fw-medium);
169
204
  }
170
205
 
171
206
  .dash-header-visit {
@@ -243,7 +278,7 @@ svg[stroke-width].icon-fine {
243
278
 
244
279
  .dash-breadcrumb-current {
245
280
  color: var(--color-foreground);
246
- font-weight: 500;
281
+ font-weight: var(--fw-medium);
247
282
  }
248
283
  }
249
284
 
@@ -304,12 +339,28 @@ svg[stroke-width].icon-fine {
304
339
  color: var(--color-muted-foreground);
305
340
  opacity: 0.5;
306
341
  }
342
+
343
+ .settings-export-form {
344
+ display: contents;
345
+ }
346
+
347
+ button.settings-item {
348
+ @apply w-full text-left cursor-pointer;
349
+ background: none;
350
+ border: none;
351
+ font: inherit;
352
+ border-bottom: 1px solid var(--color-border);
353
+
354
+ &:last-child {
355
+ border-bottom: none;
356
+ }
357
+ }
307
358
  }
308
359
 
309
- /* Dashboard scoped font rules */
360
+ /* Admin scoped font rules */
310
361
  @layer components {
311
362
  .dash-heading {
312
- font-family: Georgia, "Times New Roman", serif;
363
+ font-family: var(--font-serif);
313
364
  }
314
365
  }
315
366
 
@@ -7,22 +7,39 @@
7
7
  */
8
8
 
9
9
  :root {
10
- /* Typography */
11
- --font-body: system-ui, sans-serif;
10
+ /* Typography — Font families */
11
+ --font-body:
12
+ system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
13
+ "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB",
14
+ "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
12
15
  --font-heading: var(--font-body);
13
- --font-mono: ui-monospace, monospace;
16
+ --font-serif:
17
+ ui-serif, "New York Small", "New York", "Iowan Old Style", Charter, Georgia,
18
+ "Times New Roman", Times, "Songti SC", "Noto Serif CJK SC", "STSong",
19
+ "SimSun", serif;
20
+ --font-mono:
21
+ ui-monospace, Menlo, Monaco, Consolas, "Cascadia Code", "Courier New",
22
+ monospace;
23
+
24
+ /* Typography — Font weights */
25
+ --fw-light: 300;
26
+ --fw-regular: 400;
27
+ --fw-medium: 500;
28
+ --fw-semibold: 600;
29
+ --fw-bold: 700;
30
+ --fw-extrabold: 800;
14
31
  --text-sm: 0.8125rem;
15
32
  --text-base: 0.9375rem;
16
33
  --text-lg: 1.0625rem;
17
34
  --leading: 1.5;
18
35
 
19
36
  /* Layout */
20
- --site-width: 600px;
37
+ --site-width: 500px;
21
38
  --site-padding: 1.5rem;
22
39
  --content-gap: 1rem;
23
40
  --space-xl: 2rem;
24
41
 
25
- /* Sidebar layout (dashboard + site sidebar pages) */
42
+ /* Sidebar layout (admin + site sidebar pages) */
26
43
  --sidebar-width: 12rem;
27
44
  --sidebar-gap: 2rem;
28
45
 
@@ -43,7 +60,14 @@
43
60
  --icon-stroke-fine: 1.5;
44
61
 
45
62
  /* Derived color tokens (from BaseCoat variables) */
63
+ --site-accent: var(--primary);
64
+ --site-accent-text: var(--primary-foreground);
46
65
  --site-column-outline: var(--border);
66
+ --site-border-light: color-mix(
67
+ in srgb,
68
+ var(--site-column-outline) 52%,
69
+ transparent
70
+ );
47
71
  --site-threadline: var(--border);
48
72
  --site-page-bg: var(--background);
49
73
  --site-elevated-bg: var(--background);
@@ -53,14 +77,68 @@
53
77
  --site-text-placeholder: oklch(from var(--muted-foreground) l c h / 0.5);
54
78
  --site-media-outline: var(--border);
55
79
  --site-divider: var(--border);
80
+ --site-feed-card-bg: color-mix(
81
+ in srgb,
82
+ var(--site-elevated-bg) 88%,
83
+ var(--site-nav-hover-bg)
84
+ );
85
+ --site-feed-card-border: color-mix(
86
+ in srgb,
87
+ var(--site-divider) 78%,
88
+ transparent
89
+ );
90
+ --site-feed-card-shadow: color-mix(
91
+ in srgb,
92
+ var(--site-text-primary) 12%,
93
+ transparent
94
+ );
95
+ --site-feed-divider-color: color-mix(
96
+ in srgb,
97
+ var(--site-text-secondary) 30%,
98
+ transparent
99
+ );
100
+ --site-feed-link-tint: color-mix(in srgb, var(--site-accent) 7%, transparent);
101
+ --site-feed-quote-tint: color-mix(
102
+ in srgb,
103
+ var(--site-accent) 10%,
104
+ transparent
105
+ );
106
+ --site-thread-context-bg: color-mix(
107
+ in srgb,
108
+ var(--site-nav-hover-bg) 58%,
109
+ transparent
110
+ );
111
+ --site-thread-context-border: color-mix(
112
+ in srgb,
113
+ var(--site-divider) 74%,
114
+ transparent
115
+ );
116
+ --site-thread-gap-bg: color-mix(
117
+ in srgb,
118
+ var(--site-nav-hover-bg) 42%,
119
+ transparent
120
+ );
121
+ --site-thread-item-spacing: 14px;
122
+ --site-thread-context-max-height: 188px;
123
+ --site-thread-dot-ring: color-mix(
124
+ in srgb,
125
+ var(--site-accent) 16%,
126
+ transparent
127
+ );
128
+
129
+ /* Search highlight */
130
+ --search-mark-bg: oklch(0.92 0.14 90 / 0.55);
131
+ --search-mark-color: oklch(0.35 0.09 70);
56
132
 
57
- /* Dashboard */
133
+ /* Admin */
58
134
  --dash-bg: oklch(0.97 0.005 80);
59
135
  --dash-card-radius: 10px;
60
136
  }
61
137
 
62
138
  @media (prefers-color-scheme: dark) {
63
139
  :root {
140
+ --search-mark-bg: oklch(0.45 0.1 85 / 0.5);
141
+ --search-mark-color: oklch(0.92 0.08 90);
64
142
  --dash-bg: oklch(0.2 0.005 80);
65
143
  }
66
144
  }