@jant/core 0.3.35 → 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 (307) 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/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * Collection Page
3
3
  *
4
- * Collection header with icon and divider-separated post list.
4
+ * Collection header with icon and timeline feed of posts.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
9
  import type { CollectionPageProps } from "../../types.js";
10
10
  import { renderCollectionIcon } from "../../lib/icons.js";
11
+ import { TimelineFeed } from "../feed/TimelineFeed.js";
11
12
 
12
13
  export const CollectionPage: FC<CollectionPageProps> = ({
13
14
  collection,
14
- posts,
15
+ items,
15
16
  }) => {
16
17
  const { t } = useLingui();
17
18
  const iconHtml = renderCollectionIcon(collection.icon, { size: 28 });
@@ -34,45 +35,15 @@ export const CollectionPage: FC<CollectionPageProps> = ({
34
35
  </header>
35
36
 
36
37
  <main>
37
- {posts.length === 0 ? (
38
+ {items.length === 0 ? (
38
39
  <p class="text-muted-foreground">
39
40
  {t({
40
- message: "No posts in this collection.",
41
+ message: "This collection is empty. Add posts from the editor.",
41
42
  comment: "@context: Empty state message",
42
43
  })}
43
44
  </p>
44
45
  ) : (
45
- <div class="divide-y divide-border">
46
- {posts.map((post) => (
47
- <article
48
- key={post.id}
49
- class="h-entry py-4"
50
- data-post
51
- data-format={post.format}
52
- >
53
- {post.title && (
54
- <h2 class="p-name text-lg font-medium mb-2">
55
- <a href={post.permalink} class="u-url hover:underline">
56
- {post.title}
57
- </a>
58
- </h2>
59
- )}
60
- <div
61
- class="e-content prose prose-sm"
62
- data-post-body
63
- dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
64
- />
65
- <footer
66
- class="mt-2 text-sm text-muted-foreground"
67
- data-post-meta
68
- >
69
- <time class="dt-published" datetime={post.publishedAt}>
70
- {post.publishedAtFormatted}
71
- </time>
72
- </footer>
73
- </article>
74
- ))}
75
- </div>
46
+ <TimelineFeed items={items} />
76
47
  )}
77
48
  </main>
78
49
  </div>
@@ -27,7 +27,8 @@ export const CollectionsPage: FC<CollectionsPageProps> = ({ collections }) => {
27
27
  {collections.length === 0 ? (
28
28
  <p class="text-muted-foreground">
29
29
  {t({
30
- message: "No collections yet.",
30
+ message:
31
+ "No collections yet. Start one to organize posts by topic.",
31
32
  comment: "@context: Empty state message on collections page",
32
33
  })}
33
34
  </p>
@@ -0,0 +1,54 @@
1
+ import type { FC } from "hono/jsx";
2
+ import { useLingui } from "@lingui/react/macro";
3
+ import type { Collection } from "../../types.js";
4
+ import { ComposeForm } from "../compose/ComposeDialog.js";
5
+
6
+ export interface ComposePageProps {
7
+ collections?: Collection[];
8
+ uploadMaxFileSize?: number;
9
+ closeHref?: string;
10
+ }
11
+
12
+ export const ComposePage: FC<ComposePageProps> = ({
13
+ collections,
14
+ uploadMaxFileSize,
15
+ closeHref = "/",
16
+ }) => {
17
+ const { t } = useLingui();
18
+ const backLabel = t({
19
+ message: "Back",
20
+ comment: "@context: Link back from the new post page",
21
+ });
22
+
23
+ return (
24
+ <section class="compose-page" data-page="compose">
25
+ <div class="compose-page-shell">
26
+ <div class="compose-page-intro">
27
+ <div class="compose-page-intro-row">
28
+ <h1 class="compose-page-title">
29
+ {t({
30
+ message: "New post",
31
+ comment: "@context: Page title for the new post page",
32
+ })}
33
+ </h1>
34
+ <button
35
+ type="button"
36
+ class="compose-page-back-link"
37
+ aria-label={backLabel}
38
+ data-on:click="el.closest('.compose-page-shell')?.querySelector('jant-compose-dialog')?.requestCloseAndLeave()"
39
+ >
40
+ <span>{`← ${backLabel}`}</span>
41
+ </button>
42
+ </div>
43
+ </div>
44
+ <ComposeForm
45
+ collections={collections}
46
+ uploadMaxFileSize={uploadMaxFileSize}
47
+ pageMode
48
+ closeHref={closeHref}
49
+ autoRestoreDraft
50
+ />
51
+ </div>
52
+ </section>
53
+ );
54
+ };
@@ -18,7 +18,8 @@ export const FeaturedPage: FC<FeaturedPageProps> = ({ items }) => {
18
18
  {items.length === 0 ? (
19
19
  <p class="text-muted-foreground">
20
20
  {t({
21
- message: "No featured posts yet.",
21
+ message:
22
+ "No featured posts. Mark a post as featured to highlight it here.",
22
23
  comment: "@context: Empty state message on featured page",
23
24
  })}
24
25
  </p>
@@ -27,7 +27,7 @@ export const HomePage: FC<HomePageProps> = ({
27
27
  class="py-12 text-center text-muted-foreground"
28
28
  >
29
29
  {t({
30
- message: "No posts yet.",
30
+ message: "Nothing here yet.",
31
31
  comment: "@context: Empty state message on home page",
32
32
  })}
33
33
  </p>
@@ -2,55 +2,40 @@
2
2
  * Single Post Page
3
3
  *
4
4
  * Single post view — clean, no card border, with divider footer.
5
+ * When `threadPosts` is provided, renders the full thread with the current
6
+ * post highlighted and scroll-targeted.
5
7
  */
6
8
 
7
9
  import type { FC } from "hono/jsx";
8
- import { useLingui } from "@lingui/react/macro";
9
- import type { PostPageProps } from "../../types.js";
10
- import { MediaGallery } from "../shared/MediaGallery.js";
11
-
12
- export const PostPage: FC<PostPageProps> = ({ post }) => {
13
- const { t } = useLingui();
10
+ import type { PostPageProps, PostView } from "../../types.js";
11
+ import { TimelineItemFromPost } from "../feed/TimelineItem.js";
14
12
 
13
+ const ThreadDetail: FC<{ post: PostView; threadPosts: PostView[] }> = ({
14
+ post,
15
+ threadPosts,
16
+ }) => {
15
17
  return (
16
- <article
17
- class="h-entry py-6"
18
- data-page="post"
19
- data-post
20
- data-format={post.format}
21
- >
22
- {post.title && (
23
- <h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
24
- )}
25
-
26
- {post.bodyHtml && (
27
- <div
28
- class="e-content prose"
29
- data-post-body
30
- dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
31
- />
32
- )}
33
-
34
- {post.media.length > 0 && (
35
- <div class="mt-4" data-post-media>
36
- <MediaGallery attachments={post.media} />
37
- </div>
38
- )}
39
-
40
- <footer
41
- class="mt-6 pt-4 border-t text-sm text-muted-foreground"
42
- data-post-meta
43
- >
44
- <time class="dt-published" datetime={post.publishedAt}>
45
- {post.publishedAtFormatted}
46
- </time>
47
- <a href={post.permalink} class="u-url ml-4">
48
- {t({
49
- message: "Permalink",
50
- comment: "@context: Link to permanent URL of post",
51
- })}
52
- </a>
53
- </footer>
54
- </article>
18
+ <div class="thread-group thread-group-detail" data-page="post">
19
+ {threadPosts.map((tp) => {
20
+ const isCurrent = tp.id === post.id;
21
+ return (
22
+ <div
23
+ key={tp.id}
24
+ id={`post-${tp.id}`}
25
+ class={`thread-item thread-detail-item${isCurrent ? " thread-item-current" : ""}`}
26
+ {...(isCurrent ? { "data-post-current": "" } : {})}
27
+ >
28
+ <TimelineItemFromPost post={tp} />
29
+ </div>
30
+ );
31
+ })}
32
+ </div>
55
33
  );
56
34
  };
35
+
36
+ export const PostPage: FC<PostPageProps> = ({ post, threadPosts }) => {
37
+ if (threadPosts && threadPosts.length > 1) {
38
+ return <ThreadDetail post={post} threadPosts={threadPosts} />;
39
+ }
40
+ return <TimelineItemFromPost post={post} mode="detail" />;
41
+ };
@@ -1,14 +1,184 @@
1
1
  /**
2
2
  * Search Page
3
3
  *
4
- * Search form and resultsdivider-separated instead of bordered cards.
4
+ * Dedicated search result UIcompact per-type cards, not full timeline cards.
5
+ * Each card shows only what's relevant: title/domain/quote + FTS snippet.
5
6
  */
6
7
 
7
8
  import type { FC } from "hono/jsx";
8
9
  import { useLingui } from "@lingui/react/macro";
9
- import type { SearchPageProps } from "../../types.js";
10
+ import type { SearchPageProps, SearchResultView } from "../../types.js";
10
11
  import { PagePagination } from "../shared/Pagination.js";
11
12
 
13
+ // External link icon (shared by LinkCard)
14
+ const ExternalLinkIcon = () => (
15
+ <svg
16
+ class="size-3 shrink-0"
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ fill="none"
19
+ viewBox="0 0 24 24"
20
+ stroke-width="2"
21
+ stroke="currentColor"
22
+ >
23
+ <path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
24
+ </svg>
25
+ );
26
+
27
+ const SearchResultCard: FC<{ result: SearchResultView }> = ({ result }) => {
28
+ const { post, snippet, titleHighlighted, quoteHighlighted } = result;
29
+
30
+ // Extract domain for link posts
31
+ let domain: string | undefined;
32
+ if (post.format === "link" && post.url) {
33
+ try {
34
+ domain = new URL(post.url).hostname.replace(/^www\./, "");
35
+ } catch {
36
+ // Invalid URL, skip
37
+ }
38
+ }
39
+
40
+ const footer = (
41
+ <footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
42
+ <span class="badge-outline">{post.format}</span>
43
+ <a href={post.permalink} class="hover:underline">
44
+ <time datetime={post.publishedAt}>{post.publishedAtFormatted}</time>
45
+ </a>
46
+ </footer>
47
+ );
48
+
49
+ // ── Link ──────────────────────────────────────────────────────────────────
50
+ if (post.format === "link") {
51
+ return (
52
+ <article data-post data-format="link">
53
+ {domain && (
54
+ <div class="flex items-center gap-1 text-xs text-muted-foreground mb-1">
55
+ <ExternalLinkIcon />
56
+ <span>{domain}</span>
57
+ </div>
58
+ )}
59
+ {(titleHighlighted ?? post.title) && (
60
+ <h3 class="font-semibold text-base mb-1">
61
+ {titleHighlighted ? (
62
+ <a
63
+ href={post.url || post.permalink}
64
+ target={post.url ? "_blank" : undefined}
65
+ rel={post.url ? "noopener noreferrer" : undefined}
66
+ class="hover:underline"
67
+ dangerouslySetInnerHTML={{ __html: titleHighlighted }}
68
+ />
69
+ ) : (
70
+ <a
71
+ href={post.url || post.permalink}
72
+ target={post.url ? "_blank" : undefined}
73
+ rel={post.url ? "noopener noreferrer" : undefined}
74
+ class="hover:underline"
75
+ >
76
+ {post.title}
77
+ </a>
78
+ )}
79
+ </h3>
80
+ )}
81
+ {snippet && (
82
+ <p
83
+ class="search-snippet"
84
+ dangerouslySetInnerHTML={{ __html: snippet }}
85
+ />
86
+ )}
87
+ {footer}
88
+ </article>
89
+ );
90
+ }
91
+
92
+ // ── Quote ─────────────────────────────────────────────────────────────────
93
+ if (post.format === "quote") {
94
+ return (
95
+ <article data-post data-format="quote">
96
+ {quoteHighlighted && (
97
+ <blockquote class="feed-quote mb-1">
98
+ <p
99
+ class="text-sm"
100
+ dangerouslySetInnerHTML={{ __html: quoteHighlighted }}
101
+ />
102
+ {post.title && (
103
+ <footer class="text-xs text-muted-foreground mt-1">
104
+ {post.url ? (
105
+ <a
106
+ href={post.url}
107
+ target="_blank"
108
+ rel="noopener noreferrer"
109
+ class="hover:underline"
110
+ >
111
+ — {post.title}
112
+ </a>
113
+ ) : (
114
+ <span>— {post.title}</span>
115
+ )}
116
+ </footer>
117
+ )}
118
+ </blockquote>
119
+ )}
120
+ {snippet && (
121
+ <p
122
+ class="search-snippet"
123
+ dangerouslySetInnerHTML={{ __html: snippet }}
124
+ />
125
+ )}
126
+ {footer}
127
+ </article>
128
+ );
129
+ }
130
+
131
+ // ── Note with title (article) ─────────────────────────────────────────────
132
+ if (post.title) {
133
+ return (
134
+ <article data-post data-format="note">
135
+ <h3 class="font-semibold text-base mb-1">
136
+ {titleHighlighted ? (
137
+ <a
138
+ href={post.permalink}
139
+ class="hover:underline"
140
+ dangerouslySetInnerHTML={{ __html: titleHighlighted }}
141
+ />
142
+ ) : (
143
+ <a href={post.permalink} class="hover:underline">
144
+ {post.title}
145
+ </a>
146
+ )}
147
+ </h3>
148
+ {snippet && (
149
+ <p
150
+ class="search-snippet"
151
+ dangerouslySetInnerHTML={{ __html: snippet }}
152
+ />
153
+ )}
154
+ {footer}
155
+ </article>
156
+ );
157
+ }
158
+
159
+ // ── Untitled note ─────────────────────────────────────────────────────────
160
+ return (
161
+ <article data-post data-format="note">
162
+ {snippet ? (
163
+ <a href={post.permalink} class="block hover:opacity-80">
164
+ <p
165
+ class="search-snippet"
166
+ dangerouslySetInnerHTML={{ __html: snippet }}
167
+ />
168
+ </a>
169
+ ) : (
170
+ <a
171
+ href={post.permalink}
172
+ class="block text-sm text-muted-foreground hover:underline"
173
+ >
174
+ {post.publishedAtFormatted}
175
+ </a>
176
+ )}
177
+ {footer}
178
+ </article>
179
+ );
180
+ };
181
+
12
182
  export const SearchPage: FC<SearchPageProps> = ({
13
183
  query,
14
184
  results,
@@ -17,14 +187,12 @@ export const SearchPage: FC<SearchPageProps> = ({
17
187
  page,
18
188
  }) => {
19
189
  const { t } = useLingui();
20
- const searchTitle = t({
21
- message: "Search",
22
- comment: "@context: Search page title",
23
- });
24
190
 
25
191
  return (
26
192
  <div class="py-6" data-page="search">
27
- <h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
193
+ <h1 class="text-2xl font-semibold mb-6">
194
+ {t({ message: "Search", comment: "@context: Search page title" })}
195
+ </h1>
28
196
 
29
197
  {/* Search form */}
30
198
  <form method="get" action="/search" class="mb-8">
@@ -62,7 +230,7 @@ export const SearchPage: FC<SearchPageProps> = ({
62
230
  <p class="text-sm text-muted-foreground mb-4">
63
231
  {results.length === 0
64
232
  ? t({
65
- message: "No results found.",
233
+ message: "No results. Try different keywords.",
66
234
  comment: "@context: Search empty results",
67
235
  })
68
236
  : results.length === 1
@@ -78,36 +246,12 @@ export const SearchPage: FC<SearchPageProps> = ({
78
246
 
79
247
  {results.length > 0 && (
80
248
  <>
81
- <div class="divide-y divide-border">
82
- {results.map((result) => (
83
- <article
84
- key={result.post.id}
85
- class="py-4"
86
- data-post
87
- data-format={result.post.format}
88
- >
89
- <a href={result.post.permalink} class="block">
90
- <h2 class="font-medium hover:underline">
91
- {result.post.title ||
92
- result.post.excerpt?.slice(0, 60) ||
93
- "Post #" + result.post.id}
94
- </h2>
95
-
96
- {result.snippet && (
97
- <p
98
- class="text-sm text-muted-foreground mt-2 line-clamp-2"
99
- dangerouslySetInnerHTML={{ __html: result.snippet }}
100
- />
101
- )}
102
-
103
- <footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
104
- <span class="badge-outline">{result.post.format}</span>
105
- <time datetime={result.post.publishedAt}>
106
- {result.post.publishedAtFormatted}
107
- </time>
108
- </footer>
109
- </a>
110
- </article>
249
+ <div class="flex flex-col">
250
+ {results.map((result, i) => (
251
+ <div key={result.post.id}>
252
+ {i > 0 && <hr class="feed-divider" />}
253
+ <SearchResultCard result={result} />
254
+ </div>
111
255
  ))}
112
256
  </div>
113
257
 
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Admin Breadcrumb Component
3
+ *
4
+ * Reuses the existing dash-breadcrumb CSS classes.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+
9
+ export interface AdminBreadcrumbProps {
10
+ parent: string;
11
+ parentHref: string;
12
+ current: string;
13
+ }
14
+
15
+ export const AdminBreadcrumb: FC<AdminBreadcrumbProps> = ({
16
+ parent,
17
+ parentHref,
18
+ current,
19
+ }) => {
20
+ return (
21
+ <nav class="dash-breadcrumb mb-6">
22
+ <a href={parentHref} class="dash-breadcrumb-parent">
23
+ {parent}
24
+ </a>
25
+ <span class="dash-breadcrumb-sep">/</span>
26
+ <span class="dash-breadcrumb-current">{current}</span>
27
+ </nav>
28
+ );
29
+ };