@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
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Icon Catalog
3
+ *
4
+ * Curated subset of Lucide icons organized by category.
5
+ * Used by the dashboard icon picker to keep the response payload small.
6
+ */
7
+
8
+ /** Curated icon names (kebab-case) organized by category */
9
+ export const ICON_CATALOG: Record<string, string[]> = {
10
+ general: [
11
+ "library",
12
+ "bookmark",
13
+ "heart",
14
+ "star",
15
+ "flag",
16
+ "tag",
17
+ "hash",
18
+ "circle",
19
+ "square",
20
+ "triangle",
21
+ "diamond",
22
+ "award",
23
+ "trophy",
24
+ "medal",
25
+ "crown",
26
+ "gem",
27
+ "sparkles",
28
+ "zap",
29
+ "flame",
30
+ ],
31
+ files: [
32
+ "file",
33
+ "file-text",
34
+ "folder",
35
+ "folder-open",
36
+ "archive",
37
+ "clipboard",
38
+ "notebook",
39
+ "book",
40
+ "book-open",
41
+ "book-marked",
42
+ "scroll",
43
+ "newspaper",
44
+ "sticky-note",
45
+ ],
46
+ media: [
47
+ "image",
48
+ "camera",
49
+ "video",
50
+ "film",
51
+ "music",
52
+ "headphones",
53
+ "mic",
54
+ "radio",
55
+ "tv",
56
+ "monitor",
57
+ "podcast",
58
+ "palette",
59
+ "brush",
60
+ "pen-tool",
61
+ ],
62
+ communication: [
63
+ "mail",
64
+ "message-circle",
65
+ "message-square",
66
+ "phone",
67
+ "at-sign",
68
+ "send",
69
+ "inbox",
70
+ "megaphone",
71
+ "bell",
72
+ "rss",
73
+ ],
74
+ nature: [
75
+ "sun",
76
+ "moon",
77
+ "cloud",
78
+ "snowflake",
79
+ "droplets",
80
+ "leaf",
81
+ "flower-2",
82
+ "trees",
83
+ "mountain",
84
+ "waves",
85
+ "bird",
86
+ "bug",
87
+ "fish",
88
+ "paw-print",
89
+ ],
90
+ tech: [
91
+ "code",
92
+ "terminal",
93
+ "cpu",
94
+ "database",
95
+ "server",
96
+ "hard-drive",
97
+ "wifi",
98
+ "globe",
99
+ "link",
100
+ "qr-code",
101
+ "smartphone",
102
+ "laptop",
103
+ "tablet",
104
+ "gamepad-2",
105
+ "bot",
106
+ ],
107
+ travel: [
108
+ "map",
109
+ "map-pin",
110
+ "compass",
111
+ "navigation",
112
+ "plane",
113
+ "car",
114
+ "bike",
115
+ "ship",
116
+ "train-front",
117
+ "building-2",
118
+ "home",
119
+ "tent",
120
+ "landmark",
121
+ ],
122
+ food: [
123
+ "coffee",
124
+ "wine",
125
+ "beer",
126
+ "utensils",
127
+ "pizza",
128
+ "cake",
129
+ "apple",
130
+ "cherry",
131
+ "grape",
132
+ "cookie",
133
+ ],
134
+ people: [
135
+ "user",
136
+ "users",
137
+ "baby",
138
+ "smile",
139
+ "laugh",
140
+ "angry",
141
+ "hand-heart",
142
+ "brain",
143
+ "dumbbell",
144
+ "stethoscope",
145
+ "graduation-cap",
146
+ "briefcase",
147
+ ],
148
+ objects: [
149
+ "key",
150
+ "lock",
151
+ "shield",
152
+ "clock",
153
+ "calendar",
154
+ "gift",
155
+ "shopping-bag",
156
+ "shopping-cart",
157
+ "wallet",
158
+ "scissors",
159
+ "wrench",
160
+ "hammer",
161
+ "lightbulb",
162
+ "rocket",
163
+ "umbrella",
164
+ "glasses",
165
+ ],
166
+ };
167
+
168
+ /**
169
+ * Get all curated icon names as a flat array.
170
+ *
171
+ * @returns Array of kebab-case icon names
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * const names = getAllCatalogIconNames();
176
+ * // ["library", "bookmark", "heart", ...]
177
+ * ```
178
+ */
179
+ export function getAllCatalogIconNames(): string[] {
180
+ return Object.values(ICON_CATALOG).flat();
181
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Collection Icon Utilities
3
+ *
4
+ * Handles structured icon data (Lucide icons with color) stored as JSON in the DB.
5
+ * Backward-compatible with legacy emoji/text icon values.
6
+ */
7
+
8
+ import * as lucideIcons from "lucide-static";
9
+
10
+ /** Structured icon data stored as JSON in the DB `icon` column */
11
+ export interface CollectionIcon {
12
+ name: string;
13
+ svg: string;
14
+ color: string;
15
+ }
16
+
17
+ /** Curated color presets for the icon picker */
18
+ export const ICON_COLOR_PRESETS = [
19
+ { name: "gray", value: "#6b7280" },
20
+ { name: "red", value: "#ef4444" },
21
+ { name: "orange", value: "#f97316" },
22
+ { name: "amber", value: "#f59e0b" },
23
+ { name: "green", value: "#22c55e" },
24
+ { name: "teal", value: "#14b8a6" },
25
+ { name: "blue", value: "#3b82f6" },
26
+ { name: "indigo", value: "#6366f1" },
27
+ { name: "purple", value: "#a855f7" },
28
+ { name: "pink", value: "#ec4899" },
29
+ ] as const;
30
+
31
+ export const DEFAULT_ICON_NAME = "library";
32
+ export const DEFAULT_ICON_COLOR = "#6b7280";
33
+
34
+ /**
35
+ * Convert a kebab-case icon name to PascalCase for lucide-static lookup.
36
+ *
37
+ * @param name - Kebab-case icon name (e.g. "book-open")
38
+ * @returns PascalCase string (e.g. "BookOpen")
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * toPascalCase("book-open") // "BookOpen"
43
+ * toPascalCase("library") // "Library"
44
+ * ```
45
+ */
46
+ function toPascalCase(name: string): string {
47
+ return name
48
+ .split("-")
49
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
50
+ .join("");
51
+ }
52
+
53
+ /**
54
+ * Get SVG string for a Lucide icon by kebab-case name.
55
+ *
56
+ * @param name - Kebab-case icon name (e.g. "book-open", "library")
57
+ * @returns SVG string or null if icon not found
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * const svg = getIconSvg("library");
62
+ * // '<svg class="lucide lucide-library" ...'
63
+ * ```
64
+ */
65
+ export function getIconSvg(name: string): string | null {
66
+ const pascalName = toPascalCase(name);
67
+ const svg = (lucideIcons as Record<string, string>)[pascalName];
68
+ return typeof svg === "string" ? svg : null;
69
+ }
70
+
71
+ /**
72
+ * Parse a collection icon value from the DB.
73
+ * Returns structured icon data or null for legacy emoji/text values or invalid JSON.
74
+ *
75
+ * @param icon - Raw icon string from the DB (JSON or legacy emoji/text)
76
+ * @returns Parsed CollectionIcon or null
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * parseCollectionIcon('{"name":"library","svg":"<svg...","color":"#6b7280"}')
81
+ * // { name: "library", svg: "<svg...", color: "#6b7280" }
82
+ *
83
+ * parseCollectionIcon("📚") // null (legacy emoji)
84
+ * parseCollectionIcon(null) // null
85
+ * ```
86
+ */
87
+ export function parseCollectionIcon(
88
+ icon: string | null,
89
+ ): CollectionIcon | null {
90
+ if (!icon || !icon.startsWith("{")) return null;
91
+ try {
92
+ const parsed = JSON.parse(icon) as Record<string, unknown>;
93
+ if (
94
+ typeof parsed.name === "string" &&
95
+ typeof parsed.svg === "string" &&
96
+ typeof parsed.color === "string"
97
+ ) {
98
+ return parsed as unknown as CollectionIcon;
99
+ }
100
+ return null;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Create a JSON string for storing a structured icon in the DB.
108
+ *
109
+ * @param name - Kebab-case icon name
110
+ * @param svg - SVG string
111
+ * @param color - Hex color string
112
+ * @returns JSON string for DB storage
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * createIconValue("library", "<svg...", "#6b7280")
117
+ * // '{"name":"library","svg":"<svg...","color":"#6b7280"}'
118
+ * ```
119
+ */
120
+ export function createIconValue(
121
+ name: string,
122
+ svg: string,
123
+ color: string,
124
+ ): string {
125
+ return JSON.stringify({ name, svg, color });
126
+ }
127
+
128
+ /**
129
+ * Render a collection icon as an HTML string.
130
+ *
131
+ * - Structured icon (JSON) -> colored SVG
132
+ * - Legacy emoji/text -> span with text
133
+ * - null + fallback -> default icon SVG
134
+ * - null without fallback -> empty string
135
+ *
136
+ * @param icon - Raw icon string from the DB
137
+ * @param opts - Rendering options
138
+ * @param opts.size - Icon size in pixels (default: 24)
139
+ * @param opts.fallback - Whether to render default icon when icon is null (default: false)
140
+ * @returns HTML string
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * renderCollectionIcon('{"name":"library","svg":"<svg...","color":"#3b82f6"}', { size: 16 })
145
+ * // '<svg ... style="color: #3b82f6" width="16" height="16">...</svg>'
146
+ *
147
+ * renderCollectionIcon("📚")
148
+ * // '<span>📚</span>'
149
+ *
150
+ * renderCollectionIcon(null, { fallback: true })
151
+ * // '<svg ... (default library icon)>'
152
+ * ```
153
+ */
154
+ export function renderCollectionIcon(
155
+ icon: string | null,
156
+ opts?: { size?: number; fallback?: boolean },
157
+ ): string {
158
+ const size = opts?.size ?? 24;
159
+
160
+ const parsed = parseCollectionIcon(icon);
161
+ if (parsed) {
162
+ return applyIconSize(parsed.svg, size, parsed.color);
163
+ }
164
+
165
+ // Legacy emoji/text value
166
+ if (icon) {
167
+ return `<span>${escapeHtml(icon)}</span>`;
168
+ }
169
+
170
+ // Null — optionally show fallback
171
+ if (opts?.fallback) {
172
+ const defaultSvg = getIconSvg(DEFAULT_ICON_NAME);
173
+ if (defaultSvg) {
174
+ return applyIconSize(defaultSvg, size, DEFAULT_ICON_COLOR);
175
+ }
176
+ }
177
+
178
+ return "";
179
+ }
180
+
181
+ /**
182
+ * Apply size and color to an SVG string by replacing width/height attributes
183
+ * and adding a style attribute for color.
184
+ */
185
+ function applyIconSize(svg: string, size: number, color?: string): string {
186
+ let result = svg
187
+ .replace(/width="24"/, `width="${size}"`)
188
+ .replace(/height="24"/, `height="${size}"`);
189
+ if (color) {
190
+ result = result.replace("<svg", `<svg style="color: ${color}"`);
191
+ }
192
+ return result;
193
+ }
194
+
195
+ /** Minimal HTML escaping for legacy emoji/text values */
196
+ function escapeHtml(str: string): string {
197
+ return str
198
+ .replace(/&/g, "&amp;")
199
+ .replace(/</g, "&lt;")
200
+ .replace(/>/g, "&gt;")
201
+ .replace(/"/g, "&quot;");
202
+ }
@@ -5,10 +5,8 @@
5
5
  */
6
6
 
7
7
  import type { Context } from "hono";
8
- import { getSiteName, getHomeDefaultView, getSiteFooter } from "./config.js";
9
8
  import type { Collection, NavItemView } from "../types.js";
10
9
  import { toNavItemViews } from "./view.js";
11
- import { getMediaUrl, getPublicUrlForProvider } from "./image.js";
12
10
  import { render as renderMarkdown } from "./markdown.js";
13
11
 
14
12
  /**
@@ -49,31 +47,20 @@ export interface NavigationData {
49
47
  export async function getNavigationData(c: Context): Promise<NavigationData> {
50
48
  const items = await c.var.services.navItems.list();
51
49
  const currentPath = new URL(c.req.url).pathname;
52
- const [siteName, homeDefaultView, siteFooter] = await Promise.all([
53
- getSiteName(c),
54
- getHomeDefaultView(c),
55
- getSiteFooter(c),
56
- ]);
50
+ const appConfig = c.var.appConfig;
51
+
52
+ const siteName = appConfig.siteName;
53
+ const homeDefaultView = appConfig.homeDefaultView;
54
+ const siteFooter = appConfig.siteFooter;
57
55
 
58
56
  // Only include description if explicitly set (DB or env), not the default
59
- const dbDescription = await c.var.services.settings.get("SITE_DESCRIPTION");
60
- const envDescription = c.env.SITE_DESCRIPTION;
61
- const siteDescription =
62
- dbDescription || (typeof envDescription === "string" ? envDescription : "");
57
+ const siteDescription = appConfig.siteDescriptionExplicit
58
+ ? appConfig.siteDescription
59
+ : "";
63
60
 
64
- // Resolve avatar URL from storage key
65
- const avatarKey = await c.var.services.settings.get("SITE_AVATAR");
66
- const showHeaderAvatar =
67
- (await c.var.services.settings.get("SHOW_HEADER_AVATAR")) === "true";
68
- let siteAvatarUrl: string | undefined;
69
- if (avatarKey) {
70
- const publicUrl = getPublicUrlForProvider(
71
- c.env.STORAGE_DRIVER || "r2",
72
- c.env.R2_PUBLIC_URL,
73
- c.env.S3_PUBLIC_URL,
74
- );
75
- siteAvatarUrl = getMediaUrl(avatarKey, publicUrl);
76
- }
61
+ // Avatar URL and display flag come from appConfig
62
+ const siteAvatarUrl = appConfig.siteAvatarUrl || undefined;
63
+ const showHeaderAvatar = appConfig.showHeaderAvatar;
77
64
 
78
65
  // Render footer markdown
79
66
  const siteFooterHtml = siteFooter ? renderMarkdown(siteFooter) : undefined;
@@ -83,15 +70,13 @@ export async function getNavigationData(c: Context): Promise<NavigationData> {
83
70
  // Check auth status for compose button
84
71
  let isAuthenticated = false;
85
72
  let collections: Collection[] = [];
86
- if (c.var.auth) {
87
- try {
88
- const session = await c.var.auth.api.getSession({
89
- headers: c.req.raw.headers,
90
- });
91
- isAuthenticated = !!session?.user;
92
- } catch {
93
- // Not authenticated
94
- }
73
+ try {
74
+ const session = await c.var.auth.api.getSession({
75
+ headers: c.req.raw.headers,
76
+ });
77
+ isAuthenticated = !!session?.user;
78
+ } catch {
79
+ // Not authenticated
95
80
  }
96
81
 
97
82
  // Only load collections when authenticated (for compose dialog)
@@ -40,10 +40,11 @@ export function getPageNumbers(
40
40
  // Insert 0 for gaps
41
41
  const result: number[] = [];
42
42
  for (let i = 0; i < sorted.length; i++) {
43
- if (i > 0 && sorted[i]! - sorted[i - 1]! > 1) {
43
+ const current = sorted[i] as number;
44
+ if (i > 0 && current - (sorted[i - 1] as number) > 1) {
44
45
  result.push(0); // ellipsis marker
45
46
  }
46
- result.push(sorted[i]!);
47
+ result.push(current);
47
48
  }
48
49
 
49
50
  return result;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Post Form Bridge
3
+ *
4
+ * Connects <jant-post-form> to the server by handling:
5
+ * - `jant:post-submit` → POST JSON and redirect on success
6
+ * - `jant:post-load-media` → fetch media picker HTML and manage selections
7
+ */
8
+
9
+ import type { PostSubmitDetail } from "../ui/components/post-form-types.js";
10
+ import type { JantPostForm } from "../ui/components/jant-post-form.js";
11
+ import { showToast } from "./toast.js";
12
+
13
+ function findPostForm(
14
+ target: globalThis.EventTarget | null,
15
+ ): JantPostForm | null {
16
+ if (target instanceof HTMLElement && target.tagName === "JANT-POST-FORM") {
17
+ return target as JantPostForm;
18
+ }
19
+ if (target instanceof HTMLElement) {
20
+ return target.closest("jant-post-form") as JantPostForm | null;
21
+ }
22
+ return document.querySelector("jant-post-form");
23
+ }
24
+
25
+ function applyMediaSelection(el: HTMLElement, selected: boolean) {
26
+ el.classList.toggle("ring-2", selected);
27
+ el.classList.toggle("ring-primary", selected);
28
+ el.classList.toggle("border-primary", selected);
29
+ }
30
+
31
+ async function handlePostSubmit(event: Event) {
32
+ const customEvent = event as CustomEvent<PostSubmitDetail>;
33
+ const detail = customEvent.detail;
34
+ if (!detail) return;
35
+
36
+ const formEl = findPostForm(customEvent.target);
37
+ if (!formEl || !detail.endpoint) return;
38
+
39
+ formEl.loading = true;
40
+
41
+ try {
42
+ const res = await fetch(detail.endpoint, {
43
+ method: "POST",
44
+ headers: {
45
+ "Content-Type": "application/json",
46
+ Accept: "application/json",
47
+ },
48
+ body: JSON.stringify(detail.data),
49
+ });
50
+
51
+ if (!res.ok) {
52
+ let message = detail.messages.error;
53
+ try {
54
+ const json = await res.json();
55
+ if (typeof json?.error === "string") message = json.error;
56
+ else if (typeof json?.message === "string") message = json.message;
57
+ } catch {
58
+ // Ignore JSON parse failure; keep fallback message.
59
+ }
60
+ throw new Error(message);
61
+ }
62
+
63
+ const json = await res.json();
64
+
65
+ if (json?.status === "redirect" && typeof json.url === "string") {
66
+ window.location.href = json.url;
67
+ return;
68
+ }
69
+
70
+ showToast(detail.messages.success);
71
+ } catch (err) {
72
+ const message =
73
+ err instanceof Error && err.message ? err.message : detail.messages.error;
74
+ showToast(message, "error");
75
+ } finally {
76
+ formEl.loading = false;
77
+ }
78
+ }
79
+
80
+ async function handleMediaLoad(event: Event) {
81
+ const customEvent = event as CustomEvent<{
82
+ endpoint: string;
83
+ selectedIds: string[];
84
+ }>;
85
+ const detail = customEvent.detail;
86
+ if (!detail?.endpoint) return;
87
+
88
+ const grid = document.getElementById("post-media-grid");
89
+ const formEl = findPostForm(customEvent.target);
90
+ if (!grid || !formEl) return;
91
+
92
+ try {
93
+ grid.innerHTML =
94
+ '<p class="text-muted-foreground text-sm col-span-4">Loading...</p>';
95
+
96
+ const res = await fetch(detail.endpoint, {
97
+ headers: { Accept: "text/html" },
98
+ });
99
+ const html = await res.text();
100
+ grid.innerHTML = html;
101
+ } catch {
102
+ grid.innerHTML =
103
+ '<p class="text-red-500 text-sm col-span-4">Failed to load media.</p>';
104
+ return;
105
+ }
106
+
107
+ const selected = new Set(detail.selectedIds);
108
+
109
+ grid.querySelectorAll<HTMLElement>("[data-media-id]").forEach((el) => {
110
+ const id = el.dataset.mediaId;
111
+ if (!id) return;
112
+ applyMediaSelection(el, selected.has(id));
113
+ });
114
+
115
+ grid.onclick = (e: Event) => {
116
+ const target = (e.target as HTMLElement).closest<HTMLElement>(
117
+ "[data-media-id]",
118
+ );
119
+ if (!target) return;
120
+ const id = target.dataset.mediaId;
121
+ if (!id) return;
122
+
123
+ const current = new Set(formEl.mediaIds);
124
+ if (current.has(id)) {
125
+ current.delete(id);
126
+ applyMediaSelection(target, false);
127
+ } else {
128
+ current.add(id);
129
+ applyMediaSelection(target, true);
130
+ }
131
+ formEl.mediaIds = [...current];
132
+ };
133
+ }
134
+
135
+ document.addEventListener("jant:post-submit", handlePostSubmit);
136
+ document.addEventListener("jant:post-load-media", handleMediaLoad);
@@ -21,6 +21,8 @@ export interface RenderPublicPageOptions {
21
21
  navData: NavigationData;
22
22
  /** Page content JSX to render inside SiteLayout */
23
23
  content: Child;
24
+ /** Optional sidebar content for sidebar layout */
25
+ sidebar?: Child;
24
26
  }
25
27
 
26
28
  /**
@@ -41,7 +43,7 @@ export interface RenderPublicPageOptions {
41
43
  * ```
42
44
  */
43
45
  export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
44
- const { title, description, navData, content } = options;
46
+ const { title, description, navData, content, sidebar } = options;
45
47
 
46
48
  const layoutProps: SiteLayoutProps = {
47
49
  siteName: navData.siteName,
@@ -54,11 +56,14 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
54
56
  siteAvatarUrl: navData.siteAvatarUrl,
55
57
  showHeaderAvatar: navData.showHeaderAvatar,
56
58
  siteFooterHtml: navData.siteFooterHtml,
59
+ sidebar,
57
60
  };
58
61
 
59
- // Read favicon and noindex from context (set by theme middleware)
60
- const faviconUrl = c.get("faviconUrl") as string | undefined;
61
- const noindex = c.get("noindex") as boolean | undefined;
62
+ // Read favicon, version, and noindex from appConfig
63
+ const appConfig = c.get("appConfig");
64
+ const faviconUrl = appConfig.siteAvatarUrl || undefined;
65
+ const faviconVersion = appConfig.faviconVersion || undefined;
66
+ const noindex = appConfig.noindex;
62
67
 
63
68
  return c.html(
64
69
  <BaseLayout
@@ -66,7 +71,9 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
66
71
  description={description}
67
72
  c={c}
68
73
  faviconUrl={faviconUrl}
74
+ faviconVersion={faviconVersion}
69
75
  noindex={noindex}
76
+ isAuthenticated={navData.isAuthenticated}
70
77
  >
71
78
  <SiteLayout {...layoutProps}>{content}</SiteLayout>
72
79
  </BaseLayout>,