@jant/core 0.3.27 → 0.3.28

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 (313) hide show
  1. package/dist/client/client.css +1 -0
  2. package/dist/client/client.js +31561 -0
  3. package/dist/index.js +15209 -15
  4. package/package.json +21 -15
  5. package/src/__tests__/helpers/app.ts +19 -3
  6. package/src/__tests__/helpers/db.ts +44 -0
  7. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  8. package/src/app.tsx +111 -174
  9. package/src/client.ts +13 -0
  10. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  11. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  12. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  13. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  14. package/src/db/schema.ts +24 -4
  15. package/src/i18n/locales/en.po +810 -385
  16. package/src/i18n/locales/en.ts +1 -1
  17. package/src/i18n/locales/zh-Hans.po +733 -522
  18. package/src/i18n/locales/zh-Hans.ts +1 -1
  19. package/src/i18n/locales/zh-Hant.po +733 -522
  20. package/src/i18n/locales/zh-Hant.ts +1 -1
  21. package/src/i18n/middleware.ts +7 -11
  22. package/src/index.ts +1 -1
  23. package/src/lib/__tests__/icons.test.ts +178 -0
  24. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  25. package/src/lib/__tests__/schemas.test.ts +12 -6
  26. package/src/lib/__tests__/theme.test.ts +62 -0
  27. package/src/lib/__tests__/timezones.test.ts +1 -1
  28. package/src/lib/__tests__/url.test.ts +12 -0
  29. package/src/lib/__tests__/view.test.ts +1 -5
  30. package/src/lib/avatar-upload.ts +18 -10
  31. package/src/lib/collection-form-bridge.ts +52 -0
  32. package/src/lib/collections-reorder.ts +28 -0
  33. package/src/lib/compose-bridge.ts +251 -0
  34. package/src/lib/errors.ts +116 -0
  35. package/src/lib/excerpt.ts +1 -1
  36. package/src/lib/favicon.ts +3 -5
  37. package/src/lib/html.ts +22 -0
  38. package/src/lib/icon-catalog.ts +181 -0
  39. package/src/lib/icons.ts +202 -0
  40. package/src/lib/navigation.ts +18 -33
  41. package/src/lib/pagination.ts +3 -2
  42. package/src/lib/post-form-bridge.ts +136 -0
  43. package/src/lib/render.tsx +11 -4
  44. package/src/lib/resolve-config.ts +157 -0
  45. package/src/lib/schemas.ts +76 -12
  46. package/src/lib/settings-bridge.ts +139 -0
  47. package/src/lib/storage.ts +37 -16
  48. package/src/lib/theme.ts +5 -7
  49. package/src/lib/timeline.ts +4 -8
  50. package/src/lib/toast.ts +134 -0
  51. package/src/lib/upload.ts +71 -0
  52. package/src/lib/url.ts +9 -1
  53. package/src/lib/version.ts +16 -0
  54. package/src/lib/view.ts +9 -10
  55. package/src/middleware/__tests__/auth.test.ts +6 -28
  56. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  57. package/src/middleware/auth.ts +6 -12
  58. package/src/middleware/config.ts +51 -0
  59. package/src/middleware/error-handler.ts +56 -0
  60. package/src/middleware/onboarding.ts +1 -1
  61. package/src/preset.css +6 -0
  62. package/src/routes/__tests__/compose.test.ts +104 -17
  63. package/src/routes/api/__tests__/collections.test.ts +93 -2
  64. package/src/routes/api/__tests__/posts.test.ts +2 -1
  65. package/src/routes/api/__tests__/settings.test.ts +1 -1
  66. package/src/routes/api/collections.ts +64 -68
  67. package/src/routes/api/nav-items.ts +21 -59
  68. package/src/routes/api/pages.ts +18 -46
  69. package/src/routes/api/posts.ts +64 -86
  70. package/src/routes/api/search.ts +6 -4
  71. package/src/routes/api/settings.ts +8 -24
  72. package/src/routes/api/upload.ts +55 -53
  73. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  74. package/src/routes/auth/reset.tsx +17 -66
  75. package/src/routes/auth/setup.tsx +67 -11
  76. package/src/routes/auth/signin.tsx +44 -8
  77. package/src/routes/compose.tsx +194 -0
  78. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  79. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  80. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  81. package/src/routes/dash/appearance.tsx +173 -0
  82. package/src/routes/dash/collections.tsx +80 -14
  83. package/src/routes/dash/index.tsx +12 -14
  84. package/src/routes/dash/media.tsx +46 -49
  85. package/src/routes/dash/pages.tsx +85 -37
  86. package/src/routes/dash/posts.tsx +60 -23
  87. package/src/routes/dash/redirects.tsx +43 -33
  88. package/src/routes/dash/settings.tsx +234 -214
  89. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  90. package/src/routes/feed/rss.ts +11 -16
  91. package/src/routes/feed/sitemap.ts +15 -9
  92. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  93. package/src/routes/pages/archive.tsx +2 -2
  94. package/src/routes/pages/collection.tsx +76 -9
  95. package/src/routes/pages/collections.tsx +3 -1
  96. package/src/routes/pages/featured.tsx +2 -2
  97. package/src/routes/pages/home.tsx +3 -3
  98. package/src/routes/pages/latest.tsx +2 -2
  99. package/src/routes/pages/page.tsx +2 -2
  100. package/src/routes/pages/post.tsx +2 -2
  101. package/src/routes/pages/search.tsx +2 -2
  102. package/src/services/__tests__/collection.test.ts +324 -34
  103. package/src/services/__tests__/media.test.ts +1 -1
  104. package/src/services/__tests__/page.test.ts +116 -1
  105. package/src/services/auth.ts +88 -0
  106. package/src/services/collection.ts +169 -30
  107. package/src/services/index.ts +8 -3
  108. package/src/services/media.ts +39 -12
  109. package/src/services/navigation.ts +17 -5
  110. package/src/services/page.ts +24 -4
  111. package/src/services/post.ts +87 -19
  112. package/src/services/search.ts +0 -1
  113. package/src/services/settings.ts +21 -13
  114. package/src/style.css +3 -0
  115. package/src/styles/components.css +42 -1
  116. package/src/styles/tokens.css +4 -0
  117. package/src/styles/ui.css +902 -73
  118. package/src/types/app-context.ts +25 -0
  119. package/src/types/bindings.ts +1 -0
  120. package/src/types/config.ts +60 -23
  121. package/src/types/entities.ts +12 -2
  122. package/src/types/lingui-react-macro.d.ts +3 -3
  123. package/src/types/operations.ts +2 -4
  124. package/src/types/views.ts +1 -3
  125. package/src/ui/__tests__/font-themes.test.ts +27 -8
  126. package/src/ui/color-themes.ts +1 -1
  127. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  128. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  129. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  130. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  131. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  132. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  133. package/src/ui/components/collection-types.ts +45 -0
  134. package/src/ui/components/compose-types.ts +75 -0
  135. package/src/ui/components/jant-collection-form.ts +512 -0
  136. package/src/ui/components/jant-compose-dialog.ts +494 -0
  137. package/src/ui/components/jant-compose-editor.ts +799 -0
  138. package/src/ui/components/jant-post-form.ts +290 -0
  139. package/src/ui/components/jant-settings-avatar.ts +231 -0
  140. package/src/ui/components/jant-settings-general.ts +436 -0
  141. package/src/ui/components/post-form-template.ts +260 -0
  142. package/src/ui/components/post-form-types.ts +87 -0
  143. package/src/ui/components/settings-types.ts +62 -0
  144. package/src/ui/compose/ComposeDialog.tsx +141 -385
  145. package/src/ui/compose/ComposePrompt.tsx +3 -3
  146. package/src/ui/dash/PostList.tsx +55 -61
  147. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  148. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  149. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  150. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  151. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  152. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  153. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  154. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  155. package/src/ui/dash/index.ts +1 -1
  156. package/src/ui/dash/posts/PostForm.tsx +248 -0
  157. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  158. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  159. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  160. package/src/ui/font-themes.ts +115 -32
  161. package/src/ui/layouts/BaseLayout.tsx +49 -19
  162. package/src/ui/layouts/DashLayout.tsx +14 -9
  163. package/src/ui/layouts/SiteLayout.tsx +38 -23
  164. package/src/ui/pages/CollectionPage.tsx +12 -2
  165. package/src/ui/pages/CollectionsPage.tsx +27 -27
  166. package/src/ui/pages/HomePage.tsx +15 -6
  167. package/src/ui/pages/SearchPage.tsx +1 -2
  168. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  169. package/src/ui/shared/Pagination.tsx +2 -2
  170. package/dist/app.js +0 -267
  171. package/dist/auth.js +0 -39
  172. package/dist/client.js +0 -13
  173. package/dist/db/index.js +0 -10
  174. package/dist/db/schema.js +0 -224
  175. package/dist/i18n/Trans.js +0 -24
  176. package/dist/i18n/context.js +0 -58
  177. package/dist/i18n/detect.js +0 -26
  178. package/dist/i18n/i18n.js +0 -49
  179. package/dist/i18n/index.js +0 -44
  180. package/dist/i18n/locales/en.js +0 -1
  181. package/dist/i18n/locales/zh-Hans.js +0 -1
  182. package/dist/i18n/locales/zh-Hant.js +0 -1
  183. package/dist/i18n/locales.js +0 -13
  184. package/dist/i18n/middleware.js +0 -30
  185. package/dist/lib/avatar-upload.js +0 -134
  186. package/dist/lib/config.js +0 -143
  187. package/dist/lib/constants.js +0 -50
  188. package/dist/lib/excerpt.js +0 -76
  189. package/dist/lib/favicon.js +0 -102
  190. package/dist/lib/feed.js +0 -123
  191. package/dist/lib/image-processor.js +0 -187
  192. package/dist/lib/image.js +0 -97
  193. package/dist/lib/index.js +0 -7
  194. package/dist/lib/markdown.js +0 -83
  195. package/dist/lib/media-helpers.js +0 -49
  196. package/dist/lib/media-upload.js +0 -104
  197. package/dist/lib/nav-reorder.js +0 -27
  198. package/dist/lib/navigation.js +0 -79
  199. package/dist/lib/pagination.js +0 -44
  200. package/dist/lib/render.js +0 -53
  201. package/dist/lib/schemas.js +0 -174
  202. package/dist/lib/sqid.js +0 -72
  203. package/dist/lib/sse.js +0 -218
  204. package/dist/lib/storage.js +0 -164
  205. package/dist/lib/theme.js +0 -65
  206. package/dist/lib/time.js +0 -159
  207. package/dist/lib/timeline.js +0 -95
  208. package/dist/lib/timezones.js +0 -388
  209. package/dist/lib/url.js +0 -89
  210. package/dist/lib/view.js +0 -217
  211. package/dist/middleware/auth.js +0 -52
  212. package/dist/middleware/onboarding.js +0 -41
  213. package/dist/routes/api/collections.js +0 -124
  214. package/dist/routes/api/nav-items.js +0 -104
  215. package/dist/routes/api/pages.js +0 -91
  216. package/dist/routes/api/posts.js +0 -218
  217. package/dist/routes/api/search.js +0 -48
  218. package/dist/routes/api/settings.js +0 -68
  219. package/dist/routes/api/upload.js +0 -246
  220. package/dist/routes/auth/reset.js +0 -221
  221. package/dist/routes/auth/setup.js +0 -194
  222. package/dist/routes/auth/signin.js +0 -176
  223. package/dist/routes/compose.js +0 -48
  224. package/dist/routes/dash/collections.js +0 -115
  225. package/dist/routes/dash/index.js +0 -118
  226. package/dist/routes/dash/media.js +0 -106
  227. package/dist/routes/dash/pages.js +0 -294
  228. package/dist/routes/dash/posts.js +0 -244
  229. package/dist/routes/dash/redirects.js +0 -257
  230. package/dist/routes/dash/settings.js +0 -379
  231. package/dist/routes/feed/rss.js +0 -62
  232. package/dist/routes/feed/sitemap.js +0 -49
  233. package/dist/routes/pages/archive.js +0 -62
  234. package/dist/routes/pages/collection.js +0 -34
  235. package/dist/routes/pages/collections.js +0 -28
  236. package/dist/routes/pages/featured.js +0 -36
  237. package/dist/routes/pages/home.js +0 -64
  238. package/dist/routes/pages/latest.js +0 -45
  239. package/dist/routes/pages/page.js +0 -68
  240. package/dist/routes/pages/post.js +0 -44
  241. package/dist/routes/pages/search.js +0 -54
  242. package/dist/services/collection.js +0 -109
  243. package/dist/services/index.js +0 -24
  244. package/dist/services/media.js +0 -117
  245. package/dist/services/navigation.js +0 -91
  246. package/dist/services/page.js +0 -84
  247. package/dist/services/post.js +0 -229
  248. package/dist/services/redirect.js +0 -48
  249. package/dist/services/search.js +0 -67
  250. package/dist/services/settings.js +0 -68
  251. package/dist/types/bindings.js +0 -3
  252. package/dist/types/config.js +0 -147
  253. package/dist/types/constants.js +0 -27
  254. package/dist/types/entities.js +0 -3
  255. package/dist/types/lingui-react-macro.d.js +0 -9
  256. package/dist/types/operations.js +0 -3
  257. package/dist/types/props.js +0 -3
  258. package/dist/types/sortablejs.d.js +0 -5
  259. package/dist/types/views.js +0 -5
  260. package/dist/types.js +0 -11
  261. package/dist/ui/color-themes.js +0 -268
  262. package/dist/ui/compose/ComposeDialog.js +0 -467
  263. package/dist/ui/compose/ComposePrompt.js +0 -55
  264. package/dist/ui/dash/ActionButtons.js +0 -46
  265. package/dist/ui/dash/CrudPageHeader.js +0 -22
  266. package/dist/ui/dash/DangerZone.js +0 -36
  267. package/dist/ui/dash/FormatBadge.js +0 -27
  268. package/dist/ui/dash/ListItemRow.js +0 -21
  269. package/dist/ui/dash/PageForm.js +0 -195
  270. package/dist/ui/dash/PostForm.js +0 -395
  271. package/dist/ui/dash/PostList.js +0 -83
  272. package/dist/ui/dash/StatusBadge.js +0 -46
  273. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  274. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  275. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  276. package/dist/ui/dash/index.js +0 -10
  277. package/dist/ui/dash/media/MediaListContent.js +0 -166
  278. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  279. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  280. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  281. package/dist/ui/dash/settings/AccountContent.js +0 -209
  282. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  283. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  284. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  285. package/dist/ui/feed/LinkCard.js +0 -72
  286. package/dist/ui/feed/NoteCard.js +0 -58
  287. package/dist/ui/feed/QuoteCard.js +0 -63
  288. package/dist/ui/feed/ThreadPreview.js +0 -48
  289. package/dist/ui/feed/TimelineFeed.js +0 -41
  290. package/dist/ui/feed/TimelineItem.js +0 -27
  291. package/dist/ui/font-themes.js +0 -36
  292. package/dist/ui/layouts/BaseLayout.js +0 -153
  293. package/dist/ui/layouts/DashLayout.js +0 -141
  294. package/dist/ui/layouts/SiteLayout.js +0 -169
  295. package/dist/ui/pages/ArchivePage.js +0 -143
  296. package/dist/ui/pages/CollectionPage.js +0 -70
  297. package/dist/ui/pages/CollectionsPage.js +0 -76
  298. package/dist/ui/pages/FeaturedPage.js +0 -24
  299. package/dist/ui/pages/HomePage.js +0 -24
  300. package/dist/ui/pages/PostPage.js +0 -55
  301. package/dist/ui/pages/SearchPage.js +0 -122
  302. package/dist/ui/pages/SinglePage.js +0 -23
  303. package/dist/ui/shared/EmptyState.js +0 -27
  304. package/dist/ui/shared/MediaGallery.js +0 -35
  305. package/dist/ui/shared/Pagination.js +0 -195
  306. package/dist/ui/shared/ThreadView.js +0 -108
  307. package/dist/ui/shared/index.js +0 -5
  308. package/dist/vendor/datastar.js +0 -1606
  309. package/src/lib/__tests__/config.test.ts +0 -192
  310. package/src/lib/config.ts +0 -167
  311. package/src/routes/compose.ts +0 -63
  312. package/src/ui/dash/PostForm.tsx +0 -360
  313. 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 (