@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
@@ -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>,