@jant/core 0.3.27 → 0.3.29

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 (314) hide show
  1. package/bin/reset-password.js +22 -0
  2. package/dist/client/client.css +1 -0
  3. package/dist/client/client.js +31561 -0
  4. package/dist/index.js +15209 -15
  5. package/package.json +25 -15
  6. package/src/__tests__/helpers/app.ts +19 -3
  7. package/src/__tests__/helpers/db.ts +44 -0
  8. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  9. package/src/app.tsx +111 -174
  10. package/src/client.ts +13 -0
  11. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  12. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  13. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  14. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  15. package/src/db/schema.ts +24 -4
  16. package/src/i18n/locales/en.po +810 -385
  17. package/src/i18n/locales/en.ts +1 -1
  18. package/src/i18n/locales/zh-Hans.po +733 -522
  19. package/src/i18n/locales/zh-Hans.ts +1 -1
  20. package/src/i18n/locales/zh-Hant.po +733 -522
  21. package/src/i18n/locales/zh-Hant.ts +1 -1
  22. package/src/i18n/middleware.ts +7 -11
  23. package/src/index.ts +1 -1
  24. package/src/lib/__tests__/icons.test.ts +178 -0
  25. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  26. package/src/lib/__tests__/schemas.test.ts +12 -6
  27. package/src/lib/__tests__/theme.test.ts +62 -0
  28. package/src/lib/__tests__/timezones.test.ts +1 -1
  29. package/src/lib/__tests__/url.test.ts +12 -0
  30. package/src/lib/__tests__/view.test.ts +1 -5
  31. package/src/lib/avatar-upload.ts +18 -10
  32. package/src/lib/collection-form-bridge.ts +52 -0
  33. package/src/lib/collections-reorder.ts +28 -0
  34. package/src/lib/compose-bridge.ts +251 -0
  35. package/src/lib/errors.ts +116 -0
  36. package/src/lib/excerpt.ts +1 -1
  37. package/src/lib/favicon.ts +3 -5
  38. package/src/lib/html.ts +22 -0
  39. package/src/lib/icon-catalog.ts +181 -0
  40. package/src/lib/icons.ts +202 -0
  41. package/src/lib/navigation.ts +18 -33
  42. package/src/lib/pagination.ts +3 -2
  43. package/src/lib/post-form-bridge.ts +136 -0
  44. package/src/lib/render.tsx +11 -4
  45. package/src/lib/resolve-config.ts +157 -0
  46. package/src/lib/schemas.ts +76 -12
  47. package/src/lib/settings-bridge.ts +139 -0
  48. package/src/lib/storage.ts +37 -16
  49. package/src/lib/theme.ts +5 -7
  50. package/src/lib/timeline.ts +4 -8
  51. package/src/lib/toast.ts +134 -0
  52. package/src/lib/upload.ts +71 -0
  53. package/src/lib/url.ts +9 -1
  54. package/src/lib/version.ts +16 -0
  55. package/src/lib/view.ts +9 -10
  56. package/src/middleware/__tests__/auth.test.ts +6 -28
  57. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  58. package/src/middleware/auth.ts +6 -12
  59. package/src/middleware/config.ts +51 -0
  60. package/src/middleware/error-handler.ts +56 -0
  61. package/src/middleware/onboarding.ts +1 -1
  62. package/src/preset.css +6 -0
  63. package/src/routes/__tests__/compose.test.ts +104 -17
  64. package/src/routes/api/__tests__/collections.test.ts +93 -2
  65. package/src/routes/api/__tests__/posts.test.ts +2 -1
  66. package/src/routes/api/__tests__/settings.test.ts +1 -1
  67. package/src/routes/api/collections.ts +64 -68
  68. package/src/routes/api/nav-items.ts +21 -59
  69. package/src/routes/api/pages.ts +18 -46
  70. package/src/routes/api/posts.ts +64 -86
  71. package/src/routes/api/search.ts +6 -4
  72. package/src/routes/api/settings.ts +8 -24
  73. package/src/routes/api/upload.ts +55 -53
  74. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  75. package/src/routes/auth/reset.tsx +17 -66
  76. package/src/routes/auth/setup.tsx +67 -11
  77. package/src/routes/auth/signin.tsx +44 -8
  78. package/src/routes/compose.tsx +194 -0
  79. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  80. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  81. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  82. package/src/routes/dash/appearance.tsx +173 -0
  83. package/src/routes/dash/collections.tsx +80 -14
  84. package/src/routes/dash/index.tsx +12 -14
  85. package/src/routes/dash/media.tsx +46 -49
  86. package/src/routes/dash/pages.tsx +85 -37
  87. package/src/routes/dash/posts.tsx +60 -23
  88. package/src/routes/dash/redirects.tsx +43 -33
  89. package/src/routes/dash/settings.tsx +234 -214
  90. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  91. package/src/routes/feed/rss.ts +11 -16
  92. package/src/routes/feed/sitemap.ts +15 -9
  93. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  94. package/src/routes/pages/archive.tsx +2 -2
  95. package/src/routes/pages/collection.tsx +76 -9
  96. package/src/routes/pages/collections.tsx +3 -1
  97. package/src/routes/pages/featured.tsx +2 -2
  98. package/src/routes/pages/home.tsx +3 -3
  99. package/src/routes/pages/latest.tsx +2 -2
  100. package/src/routes/pages/page.tsx +2 -2
  101. package/src/routes/pages/post.tsx +2 -2
  102. package/src/routes/pages/search.tsx +2 -2
  103. package/src/services/__tests__/collection.test.ts +324 -34
  104. package/src/services/__tests__/media.test.ts +1 -1
  105. package/src/services/__tests__/page.test.ts +116 -1
  106. package/src/services/auth.ts +88 -0
  107. package/src/services/collection.ts +169 -30
  108. package/src/services/index.ts +8 -3
  109. package/src/services/media.ts +39 -12
  110. package/src/services/navigation.ts +17 -5
  111. package/src/services/page.ts +24 -4
  112. package/src/services/post.ts +87 -19
  113. package/src/services/search.ts +0 -1
  114. package/src/services/settings.ts +21 -13
  115. package/src/style.css +3 -0
  116. package/src/styles/components.css +42 -1
  117. package/src/styles/tokens.css +4 -0
  118. package/src/styles/ui.css +902 -73
  119. package/src/types/app-context.ts +25 -0
  120. package/src/types/bindings.ts +1 -0
  121. package/src/types/config.ts +60 -23
  122. package/src/types/entities.ts +12 -2
  123. package/src/types/lingui-react-macro.d.ts +3 -3
  124. package/src/types/operations.ts +2 -4
  125. package/src/types/views.ts +1 -3
  126. package/src/ui/__tests__/font-themes.test.ts +27 -8
  127. package/src/ui/color-themes.ts +1 -1
  128. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  129. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  130. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  131. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  132. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  133. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  134. package/src/ui/components/collection-types.ts +45 -0
  135. package/src/ui/components/compose-types.ts +75 -0
  136. package/src/ui/components/jant-collection-form.ts +512 -0
  137. package/src/ui/components/jant-compose-dialog.ts +494 -0
  138. package/src/ui/components/jant-compose-editor.ts +799 -0
  139. package/src/ui/components/jant-post-form.ts +290 -0
  140. package/src/ui/components/jant-settings-avatar.ts +231 -0
  141. package/src/ui/components/jant-settings-general.ts +436 -0
  142. package/src/ui/components/post-form-template.ts +260 -0
  143. package/src/ui/components/post-form-types.ts +87 -0
  144. package/src/ui/components/settings-types.ts +62 -0
  145. package/src/ui/compose/ComposeDialog.tsx +141 -385
  146. package/src/ui/compose/ComposePrompt.tsx +3 -3
  147. package/src/ui/dash/PostList.tsx +55 -61
  148. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  149. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  150. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  151. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  152. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  153. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  154. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  155. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  156. package/src/ui/dash/index.ts +1 -1
  157. package/src/ui/dash/posts/PostForm.tsx +248 -0
  158. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  159. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  160. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  161. package/src/ui/font-themes.ts +115 -32
  162. package/src/ui/layouts/BaseLayout.tsx +49 -19
  163. package/src/ui/layouts/DashLayout.tsx +14 -9
  164. package/src/ui/layouts/SiteLayout.tsx +38 -23
  165. package/src/ui/pages/CollectionPage.tsx +12 -2
  166. package/src/ui/pages/CollectionsPage.tsx +27 -27
  167. package/src/ui/pages/HomePage.tsx +15 -6
  168. package/src/ui/pages/SearchPage.tsx +1 -2
  169. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  170. package/src/ui/shared/Pagination.tsx +2 -2
  171. package/dist/app.js +0 -267
  172. package/dist/auth.js +0 -39
  173. package/dist/client.js +0 -13
  174. package/dist/db/index.js +0 -10
  175. package/dist/db/schema.js +0 -224
  176. package/dist/i18n/Trans.js +0 -24
  177. package/dist/i18n/context.js +0 -58
  178. package/dist/i18n/detect.js +0 -26
  179. package/dist/i18n/i18n.js +0 -49
  180. package/dist/i18n/index.js +0 -44
  181. package/dist/i18n/locales/en.js +0 -1
  182. package/dist/i18n/locales/zh-Hans.js +0 -1
  183. package/dist/i18n/locales/zh-Hant.js +0 -1
  184. package/dist/i18n/locales.js +0 -13
  185. package/dist/i18n/middleware.js +0 -30
  186. package/dist/lib/avatar-upload.js +0 -134
  187. package/dist/lib/config.js +0 -143
  188. package/dist/lib/constants.js +0 -50
  189. package/dist/lib/excerpt.js +0 -76
  190. package/dist/lib/favicon.js +0 -102
  191. package/dist/lib/feed.js +0 -123
  192. package/dist/lib/image-processor.js +0 -187
  193. package/dist/lib/image.js +0 -97
  194. package/dist/lib/index.js +0 -7
  195. package/dist/lib/markdown.js +0 -83
  196. package/dist/lib/media-helpers.js +0 -49
  197. package/dist/lib/media-upload.js +0 -104
  198. package/dist/lib/nav-reorder.js +0 -27
  199. package/dist/lib/navigation.js +0 -79
  200. package/dist/lib/pagination.js +0 -44
  201. package/dist/lib/render.js +0 -53
  202. package/dist/lib/schemas.js +0 -174
  203. package/dist/lib/sqid.js +0 -72
  204. package/dist/lib/sse.js +0 -218
  205. package/dist/lib/storage.js +0 -164
  206. package/dist/lib/theme.js +0 -65
  207. package/dist/lib/time.js +0 -159
  208. package/dist/lib/timeline.js +0 -95
  209. package/dist/lib/timezones.js +0 -388
  210. package/dist/lib/url.js +0 -89
  211. package/dist/lib/view.js +0 -217
  212. package/dist/middleware/auth.js +0 -52
  213. package/dist/middleware/onboarding.js +0 -41
  214. package/dist/routes/api/collections.js +0 -124
  215. package/dist/routes/api/nav-items.js +0 -104
  216. package/dist/routes/api/pages.js +0 -91
  217. package/dist/routes/api/posts.js +0 -218
  218. package/dist/routes/api/search.js +0 -48
  219. package/dist/routes/api/settings.js +0 -68
  220. package/dist/routes/api/upload.js +0 -246
  221. package/dist/routes/auth/reset.js +0 -221
  222. package/dist/routes/auth/setup.js +0 -194
  223. package/dist/routes/auth/signin.js +0 -176
  224. package/dist/routes/compose.js +0 -48
  225. package/dist/routes/dash/collections.js +0 -115
  226. package/dist/routes/dash/index.js +0 -118
  227. package/dist/routes/dash/media.js +0 -106
  228. package/dist/routes/dash/pages.js +0 -294
  229. package/dist/routes/dash/posts.js +0 -244
  230. package/dist/routes/dash/redirects.js +0 -257
  231. package/dist/routes/dash/settings.js +0 -379
  232. package/dist/routes/feed/rss.js +0 -62
  233. package/dist/routes/feed/sitemap.js +0 -49
  234. package/dist/routes/pages/archive.js +0 -62
  235. package/dist/routes/pages/collection.js +0 -34
  236. package/dist/routes/pages/collections.js +0 -28
  237. package/dist/routes/pages/featured.js +0 -36
  238. package/dist/routes/pages/home.js +0 -64
  239. package/dist/routes/pages/latest.js +0 -45
  240. package/dist/routes/pages/page.js +0 -68
  241. package/dist/routes/pages/post.js +0 -44
  242. package/dist/routes/pages/search.js +0 -54
  243. package/dist/services/collection.js +0 -109
  244. package/dist/services/index.js +0 -24
  245. package/dist/services/media.js +0 -117
  246. package/dist/services/navigation.js +0 -91
  247. package/dist/services/page.js +0 -84
  248. package/dist/services/post.js +0 -229
  249. package/dist/services/redirect.js +0 -48
  250. package/dist/services/search.js +0 -67
  251. package/dist/services/settings.js +0 -68
  252. package/dist/types/bindings.js +0 -3
  253. package/dist/types/config.js +0 -147
  254. package/dist/types/constants.js +0 -27
  255. package/dist/types/entities.js +0 -3
  256. package/dist/types/lingui-react-macro.d.js +0 -9
  257. package/dist/types/operations.js +0 -3
  258. package/dist/types/props.js +0 -3
  259. package/dist/types/sortablejs.d.js +0 -5
  260. package/dist/types/views.js +0 -5
  261. package/dist/types.js +0 -11
  262. package/dist/ui/color-themes.js +0 -268
  263. package/dist/ui/compose/ComposeDialog.js +0 -467
  264. package/dist/ui/compose/ComposePrompt.js +0 -55
  265. package/dist/ui/dash/ActionButtons.js +0 -46
  266. package/dist/ui/dash/CrudPageHeader.js +0 -22
  267. package/dist/ui/dash/DangerZone.js +0 -36
  268. package/dist/ui/dash/FormatBadge.js +0 -27
  269. package/dist/ui/dash/ListItemRow.js +0 -21
  270. package/dist/ui/dash/PageForm.js +0 -195
  271. package/dist/ui/dash/PostForm.js +0 -395
  272. package/dist/ui/dash/PostList.js +0 -83
  273. package/dist/ui/dash/StatusBadge.js +0 -46
  274. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  275. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  276. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  277. package/dist/ui/dash/index.js +0 -10
  278. package/dist/ui/dash/media/MediaListContent.js +0 -166
  279. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  280. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  281. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  282. package/dist/ui/dash/settings/AccountContent.js +0 -209
  283. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  284. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  285. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  286. package/dist/ui/feed/LinkCard.js +0 -72
  287. package/dist/ui/feed/NoteCard.js +0 -58
  288. package/dist/ui/feed/QuoteCard.js +0 -63
  289. package/dist/ui/feed/ThreadPreview.js +0 -48
  290. package/dist/ui/feed/TimelineFeed.js +0 -41
  291. package/dist/ui/feed/TimelineItem.js +0 -27
  292. package/dist/ui/font-themes.js +0 -36
  293. package/dist/ui/layouts/BaseLayout.js +0 -153
  294. package/dist/ui/layouts/DashLayout.js +0 -141
  295. package/dist/ui/layouts/SiteLayout.js +0 -169
  296. package/dist/ui/pages/ArchivePage.js +0 -143
  297. package/dist/ui/pages/CollectionPage.js +0 -70
  298. package/dist/ui/pages/CollectionsPage.js +0 -76
  299. package/dist/ui/pages/FeaturedPage.js +0 -24
  300. package/dist/ui/pages/HomePage.js +0 -24
  301. package/dist/ui/pages/PostPage.js +0 -55
  302. package/dist/ui/pages/SearchPage.js +0 -122
  303. package/dist/ui/pages/SinglePage.js +0 -23
  304. package/dist/ui/shared/EmptyState.js +0 -27
  305. package/dist/ui/shared/MediaGallery.js +0 -35
  306. package/dist/ui/shared/Pagination.js +0 -195
  307. package/dist/ui/shared/ThreadView.js +0 -108
  308. package/dist/ui/shared/index.js +0 -5
  309. package/dist/vendor/datastar.js +0 -1606
  310. package/src/lib/__tests__/config.test.ts +0 -192
  311. package/src/lib/config.ts +0 -167
  312. package/src/routes/compose.ts +0 -63
  313. package/src/ui/dash/PostForm.tsx +0 -360
  314. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
@@ -1,54 +1,137 @@
1
1
  /**
2
2
  * Built-in Font Themes
3
3
  *
4
- * System-font-only presets — no external font loading required.
4
+ * Heading + body font pairings using system fonts only — no external font
5
+ * loading required. Each theme sets `--font-heading` and `--font-body`
6
+ * independently to enable classic editorial combinations.
7
+ *
8
+ * Name and description are MessageDescriptor objects for i18n support.
9
+ * Pass them to t() from useLingui() when rendering.
5
10
  */
6
11
 
12
+ import type { MessageDescriptor } from "@lingui/core";
13
+
7
14
  /**
8
- * A font theme definition with display metadata.
15
+ * A font theme definition with heading + body pairing.
9
16
  */
10
17
  export interface FontTheme {
11
- /** Stored in DB settings, e.g. "serif" */
18
+ /** Stored in DB settings, e.g. "classic-editorial" */
12
19
  id: string;
13
- /** Display name, e.g. "Serif" */
14
- name: string;
15
- /** CSS font-family stack */
16
- fontFamily: string;
17
- /** Short description for the picker UI */
18
- description: string;
20
+ /** Display name pass to t() for translation */
21
+ name: MessageDescriptor;
22
+ /** CSS font-family stack for headings (h1-h6, site logo) */
23
+ headingFontFamily: string;
24
+ /** CSS font-family stack for body text */
25
+ bodyFontFamily: string;
26
+ /** Short description for the picker UI — pass to t() for translation */
27
+ description: MessageDescriptor;
19
28
  }
20
29
 
30
+ /** System sans-serif stack */
31
+ const SANS =
32
+ 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif';
33
+
34
+ /**
35
+ * Editorial serif stack
36
+ *
37
+ * ui-serif → New York (macOS 10.15+); Iowan Old Style (macOS/iOS);
38
+ * Charter (macOS); Cambria / Sitka Text (Windows); Georgia (universal)
39
+ */
40
+ const EDITORIAL_SERIF =
41
+ 'ui-serif, "Iowan Old Style", Charter, "Bitstream Charter", Cambria, "Sitka Text", Georgia, "Songti SC", "Noto Serif CJK SC", "STSong", "SimSun", serif';
42
+
43
+ /**
44
+ * Classical serif stack
45
+ *
46
+ * Palatino (macOS); Palatino Linotype / Book Antiqua (Windows);
47
+ * Old-style serif with calligraphic warmth
48
+ */
49
+ const CLASSICAL_SERIF =
50
+ 'Palatino, "Palatino Linotype", "Book Antiqua", "Songti SC", "Noto Serif CJK SC", "STSong", "SimSun", serif';
51
+
52
+ /**
53
+ * Geometric sans stack
54
+ *
55
+ * Futura (macOS); Century Gothic (Windows); clean geometric proportions
56
+ */
57
+ const GEOMETRIC_SANS =
58
+ 'Futura, "Century Gothic", "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif';
59
+
21
60
  export const BUILTIN_FONT_THEMES: FontTheme[] = [
22
61
  {
23
62
  id: "default",
24
- name: "System Default",
25
- // 现代系统字体栈:先英文,后 Mac/iOS 中文,再 Win 中文
26
- fontFamily:
27
- 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Source Han Sans CN", sans-serif',
28
- description: "与你的操作系统保持一致,最稳定的阅读体验",
63
+ name: {
64
+ id: "System Default",
65
+ message: "System Default",
66
+ comment: "@context: Font theme name",
67
+ },
68
+ headingFontFamily: SANS,
69
+ bodyFontFamily: SANS,
70
+ description: {
71
+ id: "Matches your OS native font for consistent reading",
72
+ message: "Matches your OS native font for consistent reading",
73
+ comment: "@context: Font theme description",
74
+ },
75
+ },
76
+
77
+ {
78
+ id: "modern-editorial",
79
+ name: {
80
+ id: "Modern Editorial",
81
+ message: "Modern Editorial",
82
+ comment: "@context: Font theme name",
83
+ },
84
+ headingFontFamily: SANS,
85
+ bodyFontFamily: EDITORIAL_SERIF,
86
+ description: {
87
+ id: "Clean sans-serif headings with elegant serif body text",
88
+ message: "Clean sans-serif headings with elegant serif body text",
89
+ comment: "@context: Font theme description",
90
+ },
29
91
  },
30
92
  {
31
- id: "serif",
32
- name: "Classic Serif",
33
- // Charter 是 Apple 系统自带的极品衬线体
34
- fontFamily:
35
- 'Charter, "Bitstream Charter", "Sitka Text", Georgia, "Songti SC", "Source Han Serif CN", "STSong", "SimSun", serif',
36
- description: "传统的衬线体,适合深度长文阅读",
93
+ id: "classic-editorial",
94
+ name: {
95
+ id: "Classic Editorial",
96
+ message: "Classic Editorial",
97
+ comment: "@context: Font theme name",
98
+ },
99
+ headingFontFamily: EDITORIAL_SERIF,
100
+ bodyFontFamily: SANS,
101
+ description: {
102
+ id: "Serif headings with clean sans-serif body text",
103
+ message: "Serif headings with clean sans-serif body text",
104
+ comment: "@context: Font theme description",
105
+ },
37
106
  },
38
107
  {
39
- id: "humanist",
40
- name: "Humanist",
41
- // Optima 具有书法韵味,Candara 是 Windows 上的优质人文体
42
- fontFamily:
43
- 'Optima, Candara, "Noto Sans", "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif',
44
- description: "温润如玉的字体风格,兼具现代感与书法美感",
108
+ id: "literary",
109
+ name: {
110
+ id: "Literary",
111
+ message: "Literary",
112
+ comment: "@context: Font theme name",
113
+ },
114
+ headingFontFamily: CLASSICAL_SERIF,
115
+ bodyFontFamily: EDITORIAL_SERIF,
116
+ description: {
117
+ id: "Full serif pairing for immersive long-form reading",
118
+ message: "Full serif pairing for immersive long-form reading",
119
+ comment: "@context: Font theme description",
120
+ },
45
121
  },
46
122
  {
47
- id: "mono",
48
- name: "Monospace",
49
- // 优先使用 JetBrains Mono 或 SF Mono
50
- fontFamily:
51
- '"JetBrains Mono", "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", "PingFang SC", "Microsoft YaHei", monospace',
52
- description: "等宽字体,适合技术内容或代码展示",
123
+ id: "geometric",
124
+ name: {
125
+ id: "Geometric",
126
+ message: "Geometric",
127
+ comment: "@context: Font theme name",
128
+ },
129
+ headingFontFamily: GEOMETRIC_SANS,
130
+ bodyFontFamily: SANS,
131
+ description: {
132
+ id: "Bold geometric headings with clean sans-serif body text",
133
+ message: "Bold geometric headings with clean sans-serif body text",
134
+ comment: "@context: Font theme description",
135
+ },
53
136
  },
54
137
  ];
@@ -4,12 +4,13 @@
4
4
  * Provides the HTML shell with meta tags, styles, and scripts.
5
5
  * If Context is provided, automatically wraps children with I18nProvider.
6
6
  *
7
- * Uses vite-ssr-components for automatic dev/prod asset path resolution.
7
+ * In dev mode (Vite), serves assets via Vite's dev server.
8
+ * In production, serves pre-built assets with version cache-busting.
8
9
  */
9
10
 
10
11
  import type { FC, PropsWithChildren } from "hono/jsx";
11
12
  import type { Context } from "hono";
12
- import { Script, Link, ViteClient } from "vite-ssr-components/hono";
13
+ import { CORE_VERSION, IS_VITE_DEV } from "../../lib/version.js";
13
14
  import { I18nProvider } from "../../i18n/index.js";
14
15
 
15
16
  export interface ToastProps {
@@ -24,7 +25,9 @@ export interface BaseLayoutProps {
24
25
  c?: Context;
25
26
  toast?: ToastProps;
26
27
  faviconUrl?: string;
28
+ faviconVersion?: string;
27
29
  noindex?: boolean;
30
+ isAuthenticated?: boolean;
28
31
  }
29
32
 
30
33
  export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
@@ -34,17 +37,21 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
34
37
  c,
35
38
  toast,
36
39
  faviconUrl,
40
+ faviconVersion,
37
41
  noindex,
42
+ isAuthenticated = false,
38
43
  children,
39
44
  }) => {
40
45
  // Read lang from Hono context if available, otherwise use prop or default
41
46
  const resolvedLang = lang ?? (c ? c.get("lang") : "en");
42
47
 
43
- // Read faviconUrl from context when not provided as prop (fixes dashboard favicon)
44
- const resolvedFaviconUrl = faviconUrl ?? (c ? c.get("faviconUrl") : undefined);
45
-
46
- // Read noindex from context when not provided as prop
47
- const resolvedNoindex = noindex ?? (c ? c.get("noindex") : undefined);
48
+ // Read favicon/noindex from appConfig when not provided as prop
49
+ const appConfig = c ? c.get("appConfig") : undefined;
50
+ const resolvedFaviconUrl =
51
+ faviconUrl ?? (appConfig?.siteAvatarUrl || undefined);
52
+ const resolvedFaviconVersion =
53
+ faviconVersion ?? (appConfig?.faviconVersion || undefined);
54
+ const resolvedNoindex = noindex ?? appConfig?.noindex;
48
55
 
49
56
  // Automatically wrap with I18nProvider if Context is provided
50
57
  const content = c ? <I18nProvider c={c}>{children}</I18nProvider> : children;
@@ -52,11 +59,8 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
52
59
  // Read theme style from Hono context if available
53
60
  const themeStyle = c ? c.get("themeStyle") : undefined;
54
61
 
55
- // Read custom CSS from Hono context if available
56
- const customCSS = c ? c.get("customCSS") : undefined;
57
-
58
- // Check authentication status for data attribute
59
- const isAuthenticated = c ? c.get("isAuthenticated") : false;
62
+ // Read custom CSS from appConfig
63
+ const customCSS = appConfig?.customCSS || undefined;
60
64
 
61
65
  return (
62
66
  <html lang={resolvedLang}>
@@ -68,15 +72,41 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
68
72
  {resolvedNoindex && <meta name="robots" content="noindex, nofollow" />}
69
73
  {resolvedFaviconUrl && (
70
74
  <>
71
- <link rel="icon" href="/favicon.ico" sizes="16x16 32x32" />
72
- <link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
75
+ <link
76
+ rel="icon"
77
+ href={
78
+ resolvedFaviconVersion
79
+ ? `/favicon.ico?v=${resolvedFaviconVersion}`
80
+ : "/favicon.ico"
81
+ }
82
+ sizes="16x16 32x32"
83
+ />
84
+ <link
85
+ rel="apple-touch-icon"
86
+ href={
87
+ resolvedFaviconVersion
88
+ ? `/apple-touch-icon.png?v=${resolvedFaviconVersion}`
89
+ : "/apple-touch-icon.png"
90
+ }
91
+ sizes="180x180"
92
+ />
73
93
  </>
74
94
  )}
75
- <ViteClient />
76
- <Link href="/src/style.css" rel="stylesheet" />
77
- {themeStyle && <style>{themeStyle}</style>}
78
- {customCSS && <style>{customCSS}</style>}
79
- <Script src="/src/client.ts" />
95
+ {IS_VITE_DEV && <script type="module" src="/@vite/client" />}
96
+ <link
97
+ rel="stylesheet"
98
+ href={
99
+ IS_VITE_DEV ? "/src/style.css" : `/client.css?v=${CORE_VERSION}`
100
+ }
101
+ />
102
+ {themeStyle && (
103
+ <style dangerouslySetInnerHTML={{ __html: themeStyle }} />
104
+ )}
105
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
106
+ <script
107
+ type="module"
108
+ src={IS_VITE_DEV ? "/src/client.ts" : `/client.js?v=${CORE_VERSION}`}
109
+ />
80
110
  </head>
81
111
  <body
82
112
  class="bg-background text-foreground antialiased"
@@ -41,7 +41,7 @@ function DashLayoutContent({
41
41
  <div class="min-h-screen">
42
42
  {/* Header */}
43
43
  <header class="border-b bg-card">
44
- <div class="container flex h-14 items-center justify-between">
44
+ <div class="container-sidebar flex h-14 items-center justify-between">
45
45
  <a id="site-name" href="/dash" class="font-semibold">
46
46
  {siteName}
47
47
  </a>
@@ -70,9 +70,9 @@ function DashLayoutContent({
70
70
  </header>
71
71
 
72
72
  {/* Sidebar + Main */}
73
- <div class="container flex gap-8 py-8">
73
+ <div class="container-sidebar sidebar-layout py-8">
74
74
  {/* Sidebar */}
75
- <aside class="w-48 shrink-0">
75
+ <aside class="sidebar-nav">
76
76
  <nav class="flex flex-col gap-1">
77
77
  <a href="/dash" class={navClass("/dash", /^\/dash$/)}>
78
78
  {t({
@@ -118,12 +118,12 @@ function DashLayoutContent({
118
118
  })}
119
119
  </a>
120
120
  <a
121
- href="/dash/redirects"
122
- class={navClass("/dash/redirects", /^\/dash\/redirects/)}
121
+ href="/dash/appearance"
122
+ class={navClass("/dash/appearance", /^\/dash\/appearance/)}
123
123
  >
124
124
  {t({
125
- message: "Redirects",
126
- comment: "@context: Dashboard navigation - URL redirects",
125
+ message: "Appearance",
126
+ comment: "@context: Dashboard navigation - appearance settings",
127
127
  })}
128
128
  </a>
129
129
  <a
@@ -139,7 +139,7 @@ function DashLayoutContent({
139
139
  </aside>
140
140
 
141
141
  {/* Main content */}
142
- <main class="flex-1 min-w-0">{children}</main>
142
+ <main class="sidebar-main">{children}</main>
143
143
  </div>
144
144
  </div>
145
145
  );
@@ -154,7 +154,12 @@ export const DashLayout: FC<PropsWithChildren<DashLayoutProps>> = ({
154
154
  children,
155
155
  }) => {
156
156
  return (
157
- <BaseLayout title={`${title} - ${siteName}`} c={c} toast={toast}>
157
+ <BaseLayout
158
+ title={`${title} - ${siteName}`}
159
+ c={c}
160
+ toast={toast}
161
+ isAuthenticated={true}
162
+ >
158
163
  <DashLayoutContent siteName={siteName} currentPath={currentPath}>
159
164
  {children}
160
165
  </DashLayoutContent>
@@ -36,6 +36,7 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
36
36
  siteAvatarUrl,
37
37
  showHeaderAvatar,
38
38
  siteFooterHtml,
39
+ sidebar,
39
40
  children,
40
41
  }) => {
41
42
  const { t } = useLingui();
@@ -76,7 +77,7 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
76
77
 
77
78
  return (
78
79
  <div class="site-page">
79
- <header class="site-header">
80
+ <header class={`site-header ${sidebar ? "site-header-sidebar" : ""}`}>
80
81
  <div class="site-header-inner">
81
82
  <div class="site-header-top site-header-top-bordered">
82
83
  <a href="/" class="site-logo">
@@ -123,32 +124,46 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
123
124
  </header>
124
125
 
125
126
  <main class="site-main">
126
- <div class="site-container">
127
- <div class="site-content">
128
- {isHomePage && (
129
- <nav class="site-browse-nav">
130
- {browseLinks.map((link, i) => (
131
- <>
132
- {i > 0 && <span class="site-browse-sep">/</span>}
133
- <a
134
- key={link.href}
135
- href={link.href}
136
- class={`site-browse-link ${currentPath === link.href ? "site-browse-link-active" : ""}`}
137
- >
138
- {link.label}
139
- </a>
140
- </>
141
- ))}
142
- </nav>
143
- )}
144
- {isHomePage && isAuthenticated && <ComposePrompt />}
145
- {children}
127
+ {sidebar ? (
128
+ <div class="container-sidebar">
129
+ <div class="sidebar-layout">
130
+ <aside class="sidebar-nav">{sidebar}</aside>
131
+ <div class="sidebar-main">
132
+ <div class="site-content">{children}</div>
133
+ </div>
134
+ </div>
146
135
  </div>
147
- </div>
136
+ ) : (
137
+ <div class="site-container">
138
+ <div class="site-content">
139
+ {isHomePage && (
140
+ <nav class="site-browse-nav">
141
+ {browseLinks.map((link, i) => (
142
+ <>
143
+ {i > 0 && <span class="site-browse-sep">/</span>}
144
+ <a
145
+ key={link.href}
146
+ href={link.href}
147
+ class={`site-browse-link ${currentPath === link.href ? "site-browse-link-active" : ""}`}
148
+ >
149
+ {link.label}
150
+ </a>
151
+ </>
152
+ ))}
153
+ </nav>
154
+ )}
155
+ {isHomePage && isAuthenticated && <ComposePrompt />}
156
+ {children}
157
+ </div>
158
+ </div>
159
+ )}
148
160
  </main>
149
161
 
150
162
  {siteFooterHtml && (
151
- <footer class="site-footer" data-footer>
163
+ <footer
164
+ class={`site-footer ${sidebar ? "site-footer-sidebar" : ""}`}
165
+ data-footer
166
+ >
152
167
  <div class="site-container">
153
168
  <div
154
169
  class="prose"
@@ -1,23 +1,33 @@
1
1
  /**
2
2
  * Collection Page
3
3
  *
4
- * Collection header with divider-separated post list.
4
+ * Collection header with icon and divider-separated post list.
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
+ import { renderCollectionIcon } from "../../lib/icons.js";
10
11
 
11
12
  export const CollectionPage: FC<CollectionPageProps> = ({
12
13
  collection,
13
14
  posts,
14
15
  }) => {
15
16
  const { t } = useLingui();
17
+ const iconHtml = renderCollectionIcon(collection.icon, { size: 28 });
16
18
 
17
19
  return (
18
20
  <div class="py-6" data-page="collection">
19
21
  <header class="mb-8">
20
- <h1 class="text-2xl font-semibold">{collection.title}</h1>
22
+ <h1 class="text-2xl font-semibold flex items-center gap-3">
23
+ {iconHtml && (
24
+ <span
25
+ class="shrink-0"
26
+ dangerouslySetInnerHTML={{ __html: iconHtml }}
27
+ />
28
+ )}
29
+ {collection.title}
30
+ </h1>
21
31
  {collection.description && (
22
32
  <p class="text-muted-foreground mt-2">{collection.description}</p>
23
33
  )}
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Collections Listing Page
3
3
  *
4
- * Lists all collections with titles, descriptions, and post counts.
4
+ * 2-column card grid of collections with icons and post counts.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
9
  import type { CollectionsPageProps } from "../../types.js";
10
+ import { renderCollectionIcon } from "../../lib/icons.js";
10
11
 
11
12
  export const CollectionsPage: FC<CollectionsPageProps> = ({ collections }) => {
12
13
  const { t } = useLingui();
@@ -31,38 +32,37 @@ export const CollectionsPage: FC<CollectionsPageProps> = ({ collections }) => {
31
32
  })}
32
33
  </p>
33
34
  ) : (
34
- <div class="divide-y divide-border">
35
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
35
36
  {collections.map((collection) => (
36
37
  <a
37
38
  key={collection.id}
38
- href={"/c/" + collection.slug}
39
- class="block py-4 hover:bg-accent/50 -mx-4 px-4 rounded-md transition-colors"
39
+ href={`/c/${collection.slug}`}
40
+ class="collection-card"
40
41
  >
41
42
  <div class="flex items-center gap-3">
42
- {collection.icon && (
43
- <span class="text-2xl">{collection.icon}</span>
44
- )}
45
- <div class="flex-1 min-w-0">
46
- <h2 class="font-medium">{collection.title}</h2>
47
- {collection.description && (
48
- <p class="text-sm text-muted-foreground mt-1">
49
- {collection.description}
50
- </p>
51
- )}
52
- </div>
53
- <span class="text-sm text-muted-foreground shrink-0">
54
- {collection.postCount}{" "}
55
- {collection.postCount === 1
56
- ? t({
57
- message: "post",
58
- comment: "@context: Singular post count label",
59
- })
60
- : t({
61
- message: "posts",
62
- comment: "@context: Plural post count label",
63
- })}
64
- </span>
43
+ <span
44
+ class="collection-card-icon"
45
+ dangerouslySetInnerHTML={{
46
+ __html: renderCollectionIcon(collection.icon, {
47
+ size: 20,
48
+ fallback: true,
49
+ }),
50
+ }}
51
+ />
52
+ <span class="font-medium">{collection.title}</span>
65
53
  </div>
54
+ <p class="text-sm text-muted-foreground mt-1">
55
+ {collection.postCount}{" "}
56
+ {collection.postCount === 1
57
+ ? t({
58
+ message: "entry",
59
+ comment: "@context: Singular entry count label",
60
+ })
61
+ : t({
62
+ message: "entries",
63
+ comment: "@context: Plural entry count label",
64
+ })}
65
+ </p>
66
66
  </a>
67
67
  ))}
68
68
  </div>
@@ -19,12 +19,21 @@ export const HomePage: FC<HomePageProps> = ({
19
19
  return (
20
20
  <div data-page="home">
21
21
  {items.length === 0 ? (
22
- <p class="py-12 text-center text-muted-foreground">
23
- {t({
24
- message: "No posts yet.",
25
- comment: "@context: Empty state message on home page",
26
- })}
27
- </p>
22
+ <div data-feed>
23
+ <div id="timeline-feed">
24
+ <div id="timeline-items" class="flex flex-col">
25
+ <p
26
+ id="empty-timeline"
27
+ class="py-12 text-center text-muted-foreground"
28
+ >
29
+ {t({
30
+ message: "No posts yet.",
31
+ comment: "@context: Empty state message on home page",
32
+ })}
33
+ </p>
34
+ </div>
35
+ </div>
36
+ </div>
28
37
  ) : (
29
38
  <TimelineFeed
30
39
  items={items}
@@ -71,9 +71,8 @@ export const SearchPage: FC<SearchPageProps> = ({
71
71
  comment: "@context: Search results count - single",
72
72
  })
73
73
  : t({
74
- message: "Found {count} results",
74
+ message: `Found ${String(results.length)} results`,
75
75
  comment: "@context: Search results count - multiple",
76
- values: { count: String(results.length) },
77
76
  })}
78
77
  </p>
79
78
 
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Collections Sidebar
3
+ *
4
+ * Shared sidebar navigation for public collection pages.
5
+ * Shows all collections with icons and active state.
6
+ */
7
+
8
+ import type { FC } from "hono/jsx";
9
+ import { useLingui } from "@lingui/react/macro";
10
+ import type { Collection } from "../../types.js";
11
+ import { renderCollectionIcon } from "../../lib/icons.js";
12
+
13
+ export interface CollectionsSidebarProps {
14
+ collections: Collection[];
15
+ activeSlug?: string;
16
+ }
17
+
18
+ export const CollectionsSidebar: FC<CollectionsSidebarProps> = ({
19
+ collections,
20
+ activeSlug,
21
+ }) => {
22
+ const { t } = useLingui();
23
+
24
+ return (
25
+ <nav class="flex flex-col gap-1 pt-6">
26
+ <h2 class="px-3 pb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
27
+ {t({
28
+ message: "Collections",
29
+ comment: "@context: Sidebar heading for collections nav",
30
+ })}
31
+ </h2>
32
+ {collections.map((col) => {
33
+ const isActive = col.slug === activeSlug;
34
+ return (
35
+ <a
36
+ key={col.id}
37
+ href={`/c/${col.slug}`}
38
+ class={`flex items-center gap-2.5 px-3 py-2 text-sm rounded-md truncate ${
39
+ isActive
40
+ ? "bg-accent text-accent-foreground font-medium"
41
+ : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
42
+ }`}
43
+ >
44
+ <span
45
+ class="flex items-center justify-center w-4 h-4 shrink-0"
46
+ dangerouslySetInnerHTML={{
47
+ __html: renderCollectionIcon(col.icon, {
48
+ size: 16,
49
+ fallback: true,
50
+ }),
51
+ }}
52
+ />
53
+ <span class="truncate">{col.title}</span>
54
+ </a>
55
+ );
56
+ })}
57
+ </nav>
58
+ );
59
+ };
@@ -224,10 +224,10 @@ export const PagePagination: FC<PagePaginationProps> = ({
224
224
  }
225
225
 
226
226
  // Simple prev/next fallback when totalPages is unknown
227
+ const page = String(currentPage);
227
228
  const pageText = t({
228
- message: "Page {page}",
229
+ message: `Page ${page}`,
229
230
  comment: "@context: Pagination - current page indicator",
230
- values: { page: String(currentPage) },
231
231
  });
232
232
 
233
233
  return (