@jant/core 0.3.27 → 0.3.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/bin/reset-password.js +22 -0
  2. package/dist/client/client.css +1 -0
  3. package/dist/client/client.js +31561 -0
  4. package/dist/index.js +15209 -15
  5. package/package.json +25 -15
  6. package/src/__tests__/helpers/app.ts +19 -3
  7. package/src/__tests__/helpers/db.ts +44 -0
  8. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  9. package/src/app.tsx +111 -174
  10. package/src/client.ts +13 -0
  11. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  12. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  13. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  14. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  15. package/src/db/schema.ts +24 -4
  16. package/src/i18n/locales/en.po +810 -385
  17. package/src/i18n/locales/en.ts +1 -1
  18. package/src/i18n/locales/zh-Hans.po +733 -522
  19. package/src/i18n/locales/zh-Hans.ts +1 -1
  20. package/src/i18n/locales/zh-Hant.po +733 -522
  21. package/src/i18n/locales/zh-Hant.ts +1 -1
  22. package/src/i18n/middleware.ts +7 -11
  23. package/src/index.ts +1 -1
  24. package/src/lib/__tests__/icons.test.ts +178 -0
  25. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  26. package/src/lib/__tests__/schemas.test.ts +12 -6
  27. package/src/lib/__tests__/theme.test.ts +62 -0
  28. package/src/lib/__tests__/timezones.test.ts +1 -1
  29. package/src/lib/__tests__/url.test.ts +12 -0
  30. package/src/lib/__tests__/view.test.ts +1 -5
  31. package/src/lib/avatar-upload.ts +18 -10
  32. package/src/lib/collection-form-bridge.ts +52 -0
  33. package/src/lib/collections-reorder.ts +28 -0
  34. package/src/lib/compose-bridge.ts +251 -0
  35. package/src/lib/errors.ts +116 -0
  36. package/src/lib/excerpt.ts +1 -1
  37. package/src/lib/favicon.ts +3 -5
  38. package/src/lib/html.ts +22 -0
  39. package/src/lib/icon-catalog.ts +181 -0
  40. package/src/lib/icons.ts +202 -0
  41. package/src/lib/navigation.ts +18 -33
  42. package/src/lib/pagination.ts +3 -2
  43. package/src/lib/post-form-bridge.ts +136 -0
  44. package/src/lib/render.tsx +11 -4
  45. package/src/lib/resolve-config.ts +157 -0
  46. package/src/lib/schemas.ts +76 -12
  47. package/src/lib/settings-bridge.ts +139 -0
  48. package/src/lib/storage.ts +37 -16
  49. package/src/lib/theme.ts +5 -7
  50. package/src/lib/timeline.ts +4 -8
  51. package/src/lib/toast.ts +134 -0
  52. package/src/lib/upload.ts +71 -0
  53. package/src/lib/url.ts +9 -1
  54. package/src/lib/version.ts +16 -0
  55. package/src/lib/view.ts +9 -10
  56. package/src/middleware/__tests__/auth.test.ts +6 -28
  57. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  58. package/src/middleware/auth.ts +6 -12
  59. package/src/middleware/config.ts +51 -0
  60. package/src/middleware/error-handler.ts +56 -0
  61. package/src/middleware/onboarding.ts +1 -1
  62. package/src/preset.css +6 -0
  63. package/src/routes/__tests__/compose.test.ts +104 -17
  64. package/src/routes/api/__tests__/collections.test.ts +93 -2
  65. package/src/routes/api/__tests__/posts.test.ts +2 -1
  66. package/src/routes/api/__tests__/settings.test.ts +1 -1
  67. package/src/routes/api/collections.ts +64 -68
  68. package/src/routes/api/nav-items.ts +21 -59
  69. package/src/routes/api/pages.ts +18 -46
  70. package/src/routes/api/posts.ts +64 -86
  71. package/src/routes/api/search.ts +6 -4
  72. package/src/routes/api/settings.ts +8 -24
  73. package/src/routes/api/upload.ts +55 -53
  74. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  75. package/src/routes/auth/reset.tsx +17 -66
  76. package/src/routes/auth/setup.tsx +67 -11
  77. package/src/routes/auth/signin.tsx +44 -8
  78. package/src/routes/compose.tsx +194 -0
  79. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  80. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  81. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  82. package/src/routes/dash/appearance.tsx +173 -0
  83. package/src/routes/dash/collections.tsx +80 -14
  84. package/src/routes/dash/index.tsx +12 -14
  85. package/src/routes/dash/media.tsx +46 -49
  86. package/src/routes/dash/pages.tsx +85 -37
  87. package/src/routes/dash/posts.tsx +60 -23
  88. package/src/routes/dash/redirects.tsx +43 -33
  89. package/src/routes/dash/settings.tsx +234 -214
  90. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  91. package/src/routes/feed/rss.ts +11 -16
  92. package/src/routes/feed/sitemap.ts +15 -9
  93. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  94. package/src/routes/pages/archive.tsx +2 -2
  95. package/src/routes/pages/collection.tsx +76 -9
  96. package/src/routes/pages/collections.tsx +3 -1
  97. package/src/routes/pages/featured.tsx +2 -2
  98. package/src/routes/pages/home.tsx +3 -3
  99. package/src/routes/pages/latest.tsx +2 -2
  100. package/src/routes/pages/page.tsx +2 -2
  101. package/src/routes/pages/post.tsx +2 -2
  102. package/src/routes/pages/search.tsx +2 -2
  103. package/src/services/__tests__/collection.test.ts +324 -34
  104. package/src/services/__tests__/media.test.ts +1 -1
  105. package/src/services/__tests__/page.test.ts +116 -1
  106. package/src/services/auth.ts +88 -0
  107. package/src/services/collection.ts +169 -30
  108. package/src/services/index.ts +8 -3
  109. package/src/services/media.ts +39 -12
  110. package/src/services/navigation.ts +17 -5
  111. package/src/services/page.ts +24 -4
  112. package/src/services/post.ts +87 -19
  113. package/src/services/search.ts +0 -1
  114. package/src/services/settings.ts +21 -13
  115. package/src/style.css +3 -0
  116. package/src/styles/components.css +42 -1
  117. package/src/styles/tokens.css +4 -0
  118. package/src/styles/ui.css +902 -73
  119. package/src/types/app-context.ts +25 -0
  120. package/src/types/bindings.ts +1 -0
  121. package/src/types/config.ts +60 -23
  122. package/src/types/entities.ts +12 -2
  123. package/src/types/lingui-react-macro.d.ts +3 -3
  124. package/src/types/operations.ts +2 -4
  125. package/src/types/views.ts +1 -3
  126. package/src/ui/__tests__/font-themes.test.ts +27 -8
  127. package/src/ui/color-themes.ts +1 -1
  128. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  129. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  130. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  131. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  132. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  133. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  134. package/src/ui/components/collection-types.ts +45 -0
  135. package/src/ui/components/compose-types.ts +75 -0
  136. package/src/ui/components/jant-collection-form.ts +512 -0
  137. package/src/ui/components/jant-compose-dialog.ts +494 -0
  138. package/src/ui/components/jant-compose-editor.ts +799 -0
  139. package/src/ui/components/jant-post-form.ts +290 -0
  140. package/src/ui/components/jant-settings-avatar.ts +231 -0
  141. package/src/ui/components/jant-settings-general.ts +436 -0
  142. package/src/ui/components/post-form-template.ts +260 -0
  143. package/src/ui/components/post-form-types.ts +87 -0
  144. package/src/ui/components/settings-types.ts +62 -0
  145. package/src/ui/compose/ComposeDialog.tsx +141 -385
  146. package/src/ui/compose/ComposePrompt.tsx +3 -3
  147. package/src/ui/dash/PostList.tsx +55 -61
  148. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  149. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  150. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  151. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  152. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  153. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  154. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  155. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  156. package/src/ui/dash/index.ts +1 -1
  157. package/src/ui/dash/posts/PostForm.tsx +248 -0
  158. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  159. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  160. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  161. package/src/ui/font-themes.ts +115 -32
  162. package/src/ui/layouts/BaseLayout.tsx +49 -19
  163. package/src/ui/layouts/DashLayout.tsx +14 -9
  164. package/src/ui/layouts/SiteLayout.tsx +38 -23
  165. package/src/ui/pages/CollectionPage.tsx +12 -2
  166. package/src/ui/pages/CollectionsPage.tsx +27 -27
  167. package/src/ui/pages/HomePage.tsx +15 -6
  168. package/src/ui/pages/SearchPage.tsx +1 -2
  169. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  170. package/src/ui/shared/Pagination.tsx +2 -2
  171. package/dist/app.js +0 -267
  172. package/dist/auth.js +0 -39
  173. package/dist/client.js +0 -13
  174. package/dist/db/index.js +0 -10
  175. package/dist/db/schema.js +0 -224
  176. package/dist/i18n/Trans.js +0 -24
  177. package/dist/i18n/context.js +0 -58
  178. package/dist/i18n/detect.js +0 -26
  179. package/dist/i18n/i18n.js +0 -49
  180. package/dist/i18n/index.js +0 -44
  181. package/dist/i18n/locales/en.js +0 -1
  182. package/dist/i18n/locales/zh-Hans.js +0 -1
  183. package/dist/i18n/locales/zh-Hant.js +0 -1
  184. package/dist/i18n/locales.js +0 -13
  185. package/dist/i18n/middleware.js +0 -30
  186. package/dist/lib/avatar-upload.js +0 -134
  187. package/dist/lib/config.js +0 -143
  188. package/dist/lib/constants.js +0 -50
  189. package/dist/lib/excerpt.js +0 -76
  190. package/dist/lib/favicon.js +0 -102
  191. package/dist/lib/feed.js +0 -123
  192. package/dist/lib/image-processor.js +0 -187
  193. package/dist/lib/image.js +0 -97
  194. package/dist/lib/index.js +0 -7
  195. package/dist/lib/markdown.js +0 -83
  196. package/dist/lib/media-helpers.js +0 -49
  197. package/dist/lib/media-upload.js +0 -104
  198. package/dist/lib/nav-reorder.js +0 -27
  199. package/dist/lib/navigation.js +0 -79
  200. package/dist/lib/pagination.js +0 -44
  201. package/dist/lib/render.js +0 -53
  202. package/dist/lib/schemas.js +0 -174
  203. package/dist/lib/sqid.js +0 -72
  204. package/dist/lib/sse.js +0 -218
  205. package/dist/lib/storage.js +0 -164
  206. package/dist/lib/theme.js +0 -65
  207. package/dist/lib/time.js +0 -159
  208. package/dist/lib/timeline.js +0 -95
  209. package/dist/lib/timezones.js +0 -388
  210. package/dist/lib/url.js +0 -89
  211. package/dist/lib/view.js +0 -217
  212. package/dist/middleware/auth.js +0 -52
  213. package/dist/middleware/onboarding.js +0 -41
  214. package/dist/routes/api/collections.js +0 -124
  215. package/dist/routes/api/nav-items.js +0 -104
  216. package/dist/routes/api/pages.js +0 -91
  217. package/dist/routes/api/posts.js +0 -218
  218. package/dist/routes/api/search.js +0 -48
  219. package/dist/routes/api/settings.js +0 -68
  220. package/dist/routes/api/upload.js +0 -246
  221. package/dist/routes/auth/reset.js +0 -221
  222. package/dist/routes/auth/setup.js +0 -194
  223. package/dist/routes/auth/signin.js +0 -176
  224. package/dist/routes/compose.js +0 -48
  225. package/dist/routes/dash/collections.js +0 -115
  226. package/dist/routes/dash/index.js +0 -118
  227. package/dist/routes/dash/media.js +0 -106
  228. package/dist/routes/dash/pages.js +0 -294
  229. package/dist/routes/dash/posts.js +0 -244
  230. package/dist/routes/dash/redirects.js +0 -257
  231. package/dist/routes/dash/settings.js +0 -379
  232. package/dist/routes/feed/rss.js +0 -62
  233. package/dist/routes/feed/sitemap.js +0 -49
  234. package/dist/routes/pages/archive.js +0 -62
  235. package/dist/routes/pages/collection.js +0 -34
  236. package/dist/routes/pages/collections.js +0 -28
  237. package/dist/routes/pages/featured.js +0 -36
  238. package/dist/routes/pages/home.js +0 -64
  239. package/dist/routes/pages/latest.js +0 -45
  240. package/dist/routes/pages/page.js +0 -68
  241. package/dist/routes/pages/post.js +0 -44
  242. package/dist/routes/pages/search.js +0 -54
  243. package/dist/services/collection.js +0 -109
  244. package/dist/services/index.js +0 -24
  245. package/dist/services/media.js +0 -117
  246. package/dist/services/navigation.js +0 -91
  247. package/dist/services/page.js +0 -84
  248. package/dist/services/post.js +0 -229
  249. package/dist/services/redirect.js +0 -48
  250. package/dist/services/search.js +0 -67
  251. package/dist/services/settings.js +0 -68
  252. package/dist/types/bindings.js +0 -3
  253. package/dist/types/config.js +0 -147
  254. package/dist/types/constants.js +0 -27
  255. package/dist/types/entities.js +0 -3
  256. package/dist/types/lingui-react-macro.d.js +0 -9
  257. package/dist/types/operations.js +0 -3
  258. package/dist/types/props.js +0 -3
  259. package/dist/types/sortablejs.d.js +0 -5
  260. package/dist/types/views.js +0 -5
  261. package/dist/types.js +0 -11
  262. package/dist/ui/color-themes.js +0 -268
  263. package/dist/ui/compose/ComposeDialog.js +0 -467
  264. package/dist/ui/compose/ComposePrompt.js +0 -55
  265. package/dist/ui/dash/ActionButtons.js +0 -46
  266. package/dist/ui/dash/CrudPageHeader.js +0 -22
  267. package/dist/ui/dash/DangerZone.js +0 -36
  268. package/dist/ui/dash/FormatBadge.js +0 -27
  269. package/dist/ui/dash/ListItemRow.js +0 -21
  270. package/dist/ui/dash/PageForm.js +0 -195
  271. package/dist/ui/dash/PostForm.js +0 -395
  272. package/dist/ui/dash/PostList.js +0 -83
  273. package/dist/ui/dash/StatusBadge.js +0 -46
  274. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  275. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  276. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  277. package/dist/ui/dash/index.js +0 -10
  278. package/dist/ui/dash/media/MediaListContent.js +0 -166
  279. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  280. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  281. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  282. package/dist/ui/dash/settings/AccountContent.js +0 -209
  283. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  284. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  285. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  286. package/dist/ui/feed/LinkCard.js +0 -72
  287. package/dist/ui/feed/NoteCard.js +0 -58
  288. package/dist/ui/feed/QuoteCard.js +0 -63
  289. package/dist/ui/feed/ThreadPreview.js +0 -48
  290. package/dist/ui/feed/TimelineFeed.js +0 -41
  291. package/dist/ui/feed/TimelineItem.js +0 -27
  292. package/dist/ui/font-themes.js +0 -36
  293. package/dist/ui/layouts/BaseLayout.js +0 -153
  294. package/dist/ui/layouts/DashLayout.js +0 -141
  295. package/dist/ui/layouts/SiteLayout.js +0 -169
  296. package/dist/ui/pages/ArchivePage.js +0 -143
  297. package/dist/ui/pages/CollectionPage.js +0 -70
  298. package/dist/ui/pages/CollectionsPage.js +0 -76
  299. package/dist/ui/pages/FeaturedPage.js +0 -24
  300. package/dist/ui/pages/HomePage.js +0 -24
  301. package/dist/ui/pages/PostPage.js +0 -55
  302. package/dist/ui/pages/SearchPage.js +0 -122
  303. package/dist/ui/pages/SinglePage.js +0 -23
  304. package/dist/ui/shared/EmptyState.js +0 -27
  305. package/dist/ui/shared/MediaGallery.js +0 -35
  306. package/dist/ui/shared/Pagination.js +0 -195
  307. package/dist/ui/shared/ThreadView.js +0 -108
  308. package/dist/ui/shared/index.js +0 -5
  309. package/dist/vendor/datastar.js +0 -1606
  310. package/src/lib/__tests__/config.test.ts +0 -192
  311. package/src/lib/config.ts +0 -167
  312. package/src/routes/compose.ts +0 -63
  313. package/src/ui/dash/PostForm.tsx +0 -360
  314. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
@@ -1,28 +1,24 @@
1
1
  /**
2
- * Shared collection form (new + edit)
2
+ * Collection Form
3
+ *
4
+ * Server-rendered shell that provides data/labels to the Lit component
5
+ * `<jant-collection-form>`. Includes heading and SSR fallback skeleton.
3
6
  */
4
7
 
5
8
  import { useLingui } from "@lingui/react/macro";
9
+ import type { FC } from "hono/jsx";
6
10
  import type { Collection } from "../../../types.js";
7
11
 
8
- export function CollectionForm({
9
- collection,
10
- isEdit,
11
- }: {
12
+ interface CollectionFormProps {
12
13
  collection?: Collection;
13
14
  isEdit?: boolean;
14
- }) {
15
- const { t } = useLingui();
16
-
17
- const signals = JSON.stringify({
18
- title: collection?.title ?? "",
19
- slug: collection?.slug ?? "",
20
- description: collection?.description ?? "",
21
- }).replace(/</g, "\\u003c");
15
+ }
22
16
 
23
- const action = isEdit
24
- ? `/dash/collections/${collection?.id}`
25
- : "/dash/collections";
17
+ export const CollectionForm: FC<CollectionFormProps> = ({
18
+ collection,
19
+ isEdit,
20
+ }) => {
21
+ const { t } = useLingui();
26
22
 
27
23
  const heading = isEdit
28
24
  ? t({ message: "Edit Collection", comment: "@context: Page heading" })
@@ -38,6 +34,95 @@ export function CollectionForm({
38
34
  comment: "@context: Button to save new collection",
39
35
  });
40
36
 
37
+ const labels = JSON.stringify({
38
+ titleLabel: t({
39
+ message: "Title",
40
+ comment: "@context: Collection form field",
41
+ }),
42
+ titlePlaceholder: t({
43
+ message: "My Collection",
44
+ comment: "@context: Collection title placeholder",
45
+ }),
46
+ slugLabel: t({
47
+ message: "Slug",
48
+ comment: "@context: Collection form field",
49
+ }),
50
+ slugHelp: t({
51
+ message:
52
+ "URL-safe identifier (lowercase, numbers, hyphens). For CJK titles, slug will be auto-generated on the server.",
53
+ comment: "@context: Collection path help text",
54
+ }),
55
+ descriptionLabel: t({
56
+ message: "Description (optional)",
57
+ comment: "@context: Collection form field",
58
+ }),
59
+ descriptionPlaceholder: t({
60
+ message: "What's this collection about?",
61
+ comment: "@context: Collection description placeholder",
62
+ }),
63
+ iconLabel: t({
64
+ message: "Icon (optional)",
65
+ comment: "@context: Collection form field",
66
+ }),
67
+ chooseIcon: t({
68
+ message: "Choose Icon",
69
+ comment: "@context: Button to open icon picker",
70
+ }),
71
+ removeIcon: t({
72
+ message: "Remove",
73
+ comment: "@context: Button to remove icon",
74
+ }),
75
+ dialogTitle: t({
76
+ message: "Choose Icon",
77
+ comment: "@context: Icon picker dialog title",
78
+ }),
79
+ dialogClose: t({
80
+ message: "Close",
81
+ comment: "@context: Button to close icon picker",
82
+ }),
83
+ searchIconsPlaceholder: t({
84
+ message: "Search icons...",
85
+ comment: "@context: Icon picker search placeholder",
86
+ }),
87
+ sortOrderLabel: t({
88
+ message: "Sort Order",
89
+ comment: "@context: Collection form field",
90
+ }),
91
+ sortNewest: t({
92
+ message: "Newest first",
93
+ comment: "@context: Collection sort order option",
94
+ }),
95
+ sortOldest: t({
96
+ message: "Oldest first",
97
+ comment: "@context: Collection sort order option",
98
+ }),
99
+ sortRatingDesc: t({
100
+ message: "Highest rated",
101
+ comment: "@context: Collection sort order option",
102
+ }),
103
+ sortRatingAsc: t({
104
+ message: "Lowest rated",
105
+ comment: "@context: Collection sort order option",
106
+ }),
107
+ submitLabel,
108
+ cancelLabel: t({
109
+ message: "Cancel",
110
+ comment: "@context: Button to cancel form",
111
+ }),
112
+ }).replace(/</g, "\\u003c");
113
+
114
+ const initial = JSON.stringify({
115
+ title: collection?.title ?? "",
116
+ slug: collection?.slug ?? "",
117
+ description: collection?.description ?? "",
118
+ sortOrder: collection?.sortOrder ?? "newest",
119
+ icon: collection?.icon ?? "",
120
+ }).replace(/</g, "\\u003c");
121
+
122
+ const action = isEdit
123
+ ? `/dash/collections/${collection?.id}`
124
+ : "/dash/collections";
125
+
41
126
  const cancelHref = isEdit
42
127
  ? `/dash/collections/${collection?.id}`
43
128
  : "/dash/collections";
@@ -46,108 +131,36 @@ export function CollectionForm({
46
131
  <>
47
132
  <h1 class="text-2xl font-semibold mb-6">{heading}</h1>
48
133
 
49
- <form
50
- data-signals={signals}
51
- data-on:submit__prevent={`@post('${action}')`}
52
- data-indicator="_loading"
53
- class="flex flex-col gap-4 max-w-lg"
134
+ <jant-collection-form
135
+ labels={labels}
136
+ initial={initial}
137
+ action={action}
138
+ cancel-href={cancelHref}
139
+ is-edit={isEdit ? "true" : undefined}
54
140
  >
55
- <div class="field">
56
- <label class="label">
57
- {t({
58
- message: "Title",
59
- comment: "@context: Collection form field",
60
- })}
61
- </label>
62
- <input
63
- type="text"
64
- data-bind="title"
65
- class="input"
66
- required
67
- placeholder={
68
- isEdit
69
- ? undefined
70
- : t({
71
- message: "My Collection",
72
- comment: "@context: Collection title placeholder",
73
- })
74
- }
75
- />
76
- </div>
77
-
78
- <div class="field">
79
- <label class="label">
80
- {t({ message: "Slug", comment: "@context: Collection form field" })}
81
- </label>
82
- <input
83
- type="text"
84
- data-bind="slug"
85
- class="input"
86
- required
87
- pattern="[a-z0-9-]+"
88
- placeholder={isEdit ? undefined : "my-collection"}
89
- />
90
- {!isEdit && (
91
- <p class="text-xs text-muted-foreground mt-1">
92
- {t({
93
- message: "URL-safe identifier (lowercase, numbers, hyphens)",
94
- comment: "@context: Collection path help text",
95
- })}
96
- </p>
97
- )}
98
- </div>
99
-
100
- <div class="field">
101
- <label class="label">
102
- {t({
103
- message: "Description (optional)",
104
- comment: "@context: Collection form field",
105
- })}
106
- </label>
107
- <textarea
108
- data-bind="description"
109
- class="textarea"
110
- rows={3}
111
- placeholder={
112
- isEdit
113
- ? undefined
114
- : t({
115
- message: "What's this collection about?",
116
- comment: "@context: Collection description placeholder",
117
- })
118
- }
119
- >
120
- {collection?.description ?? ""}
121
- </textarea>
122
- </div>
123
-
124
- <div class="flex gap-2">
125
- <button type="submit" class="btn" data-attr:disabled="$_loading">
126
- <svg
127
- data-show="$_loading"
128
- style="display:none"
129
- class="animate-spin size-4"
130
- xmlns="http://www.w3.org/2000/svg"
131
- viewBox="0 0 24 24"
132
- fill="none"
133
- stroke="currentColor"
134
- stroke-width="2"
135
- stroke-linecap="round"
136
- stroke-linejoin="round"
137
- role="status"
138
- >
139
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
140
- </svg>
141
- {submitLabel}
142
- </button>
143
- <a href={cancelHref} class="btn-outline">
144
- {t({
145
- message: "Cancel",
146
- comment: "@context: Button to cancel form",
147
- })}
148
- </a>
141
+ <div class="flex flex-col gap-4 max-w-lg">
142
+ <div class="field">
143
+ <div class="label skel-label"></div>
144
+ <div class="input skel-input"></div>
145
+ </div>
146
+ <div class="field">
147
+ <div class="label skel-label"></div>
148
+ <div class="input skel-input"></div>
149
+ </div>
150
+ <div class="field">
151
+ <div class="label skel-label"></div>
152
+ <div class="textarea skel-textarea"></div>
153
+ </div>
154
+ <div class="field">
155
+ <div class="label skel-label"></div>
156
+ <div class="input skel-input"></div>
157
+ </div>
158
+ <div class="flex gap-2">
159
+ <div class="btn skel-input min-w-28"></div>
160
+ <div class="btn-outline skel-input min-w-20"></div>
161
+ </div>
149
162
  </div>
150
- </form>
163
+ </jant-collection-form>
151
164
  </>
152
165
  );
153
- }
166
+ };
@@ -1,23 +1,34 @@
1
1
  /**
2
- * Collections list view
2
+ * Collections list view with drag-and-drop reordering
3
3
  */
4
4
 
5
5
  import { useLingui } from "@lingui/react/macro";
6
- import type { Collection } from "../../../types.js";
7
- import {
8
- EmptyState,
9
- ListItemRow,
10
- ActionButtons,
11
- CrudPageHeader,
12
- } from "../index.js";
6
+ import type { Collection, CollectionDivider } from "../../../types.js";
7
+ import { EmptyState, ActionButtons, CrudPageHeader } from "../index.js";
8
+ import { renderCollectionIcon } from "../../../lib/icons.js";
9
+
10
+ type ListItem =
11
+ | { type: "collection"; data: Collection }
12
+ | { type: "divider"; data: CollectionDivider };
13
13
 
14
14
  export function CollectionsListContent({
15
15
  collections,
16
+ dividers,
17
+ postCounts,
16
18
  }: {
17
19
  collections: Collection[];
20
+ dividers: CollectionDivider[];
21
+ postCounts: Map<number, number>;
18
22
  }) {
19
23
  const { t } = useLingui();
20
24
 
25
+ const items: ListItem[] = [
26
+ ...collections.map((c) => ({ type: "collection", data: c }) as ListItem),
27
+ ...dividers.map((d) => ({ type: "divider", data: d }) as ListItem),
28
+ ].sort((a, b) => a.data.position - b.data.position);
29
+
30
+ const hasItems = collections.length > 0 || dividers.length > 0;
31
+
21
32
  return (
22
33
  <>
23
34
  <CrudPageHeader
@@ -25,14 +36,26 @@ export function CollectionsListContent({
25
36
  message: "Collections",
26
37
  comment: "@context: Dashboard heading",
27
38
  })}
28
- ctaLabel={t({
29
- message: "New Collection",
30
- comment: "@context: Button to create new collection",
31
- })}
32
- ctaHref="/dash/collections/new"
33
- />
39
+ >
40
+ <div class="flex items-center gap-2">
41
+ <form method="post" action="/dash/collections/dividers">
42
+ <button type="submit" class="btn-sm-outline">
43
+ {t({
44
+ message: "New Divider",
45
+ comment: "@context: Button to add divider between collections",
46
+ })}
47
+ </button>
48
+ </form>
49
+ <a href="/dash/collections/new" class="btn-sm">
50
+ {t({
51
+ message: "New Collection",
52
+ comment: "@context: Button to create new collection",
53
+ })}
54
+ </a>
55
+ </div>
56
+ </CrudPageHeader>
34
57
 
35
- {collections.length === 0 ? (
58
+ {!hasItems ? (
36
59
  <EmptyState
37
60
  message={t({
38
61
  message: "No collections yet.",
@@ -45,39 +68,77 @@ export function CollectionsListContent({
45
68
  ctaHref="/dash/collections/new"
46
69
  />
47
70
  ) : (
48
- <div class="flex flex-col divide-y">
49
- {collections.map((col) => (
50
- <ListItemRow
51
- key={col.id}
52
- actions={
71
+ <div id="collections-list" class="flex flex-col">
72
+ {items.map((item) => {
73
+ if (item.type === "divider") {
74
+ return (
75
+ <div
76
+ key={`d-${item.data.id}`}
77
+ class="py-2 flex items-center gap-4"
78
+ >
79
+ <div
80
+ class="flex-1 min-w-0 flex items-center gap-3 cursor-grab"
81
+ data-id={`d-${item.data.id}`}
82
+ >
83
+ <span class="text-muted-foreground select-none">⠿</span>
84
+ <hr class="flex-1 border-border" />
85
+ </div>
86
+ <form
87
+ method="post"
88
+ action={`/dash/collections/dividers/${item.data.id}/delete`}
89
+ >
90
+ <button
91
+ type="submit"
92
+ class="btn-sm-ghost text-muted-foreground hover:text-destructive"
93
+ title={t({
94
+ message: "Remove divider",
95
+ comment: "@context: Button to delete a divider",
96
+ })}
97
+ >
98
+
99
+ </button>
100
+ </form>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ const col = item.data;
106
+ const count = postCounts.get(col.id) ?? 0;
107
+ return (
108
+ <div key={`c-${col.id}`} class="py-2 flex items-center gap-4">
109
+ <div
110
+ class="flex-1 min-w-0 flex items-center gap-3 cursor-grab"
111
+ data-id={`c-${col.id}`}
112
+ >
113
+ <span class="text-muted-foreground select-none">⠿</span>
114
+ {col.icon && (
115
+ <span
116
+ class="flex items-center justify-center w-5 h-5 shrink-0"
117
+ dangerouslySetInnerHTML={{
118
+ __html: renderCollectionIcon(col.icon, {
119
+ size: 18,
120
+ }),
121
+ }}
122
+ />
123
+ )}
124
+ <a
125
+ href={`/dash/collections/${col.id}`}
126
+ class="font-medium hover:underline"
127
+ >
128
+ {col.title}
129
+ </a>
130
+ <span class="badge-secondary">{count}</span>
131
+ </div>
53
132
  <ActionButtons
54
133
  editHref={`/dash/collections/${col.id}/edit`}
55
134
  editLabel={t({
56
135
  message: "Edit",
57
136
  comment: "@context: Button to edit collection",
58
137
  })}
59
- viewHref={`/c/${col.slug}`}
60
- viewLabel={t({
61
- message: "View",
62
- comment: "@context: Button to view collection",
63
- })}
64
138
  />
65
- }
66
- >
67
- <a
68
- href={`/dash/collections/${col.id}`}
69
- class="font-medium hover:underline"
70
- >
71
- {col.title}
72
- </a>
73
- <p class="text-sm text-muted-foreground">/{col.slug}</p>
74
- {col.description && (
75
- <p class="text-sm text-muted-foreground mt-1">
76
- {col.description}
77
- </p>
78
- )}
79
- </ListItemRow>
80
- ))}
139
+ </div>
140
+ );
141
+ })}
81
142
  </div>
82
143
  )}
83
144
  </>
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Icon Picker Grid
3
+ *
4
+ * HTML fragment returned by GET /dash/collections/icons.
5
+ * Renders a grid of icon buttons organized by category.
6
+ */
7
+
8
+ import type { FC } from "hono/jsx";
9
+ import { ICON_CATALOG } from "../../../lib/icon-catalog.js";
10
+ import { getIconSvg } from "../../../lib/icons.js";
11
+
12
+ export const IconPickerGrid: FC = () => {
13
+ return (
14
+ <div class="flex flex-col gap-4">
15
+ {Object.entries(ICON_CATALOG).map(([category, names]) => (
16
+ <div key={category} data-category={category}>
17
+ <h3 class="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
18
+ {category}
19
+ </h3>
20
+ <div class="grid grid-cols-8 gap-1">
21
+ {names.map((name) => {
22
+ const svg = getIconSvg(name);
23
+ if (!svg) return null;
24
+ return (
25
+ <button
26
+ key={name}
27
+ type="button"
28
+ class="flex items-center justify-center w-9 h-9 rounded-md hover:bg-accent transition-colors"
29
+ data-icon-name={name}
30
+ data-icon-svg={svg}
31
+ title={name}
32
+ data-on:click={`$iconName = el.dataset.iconName; $iconSvg = el.dataset.iconSvg; $icon = JSON.stringify({ name: $iconName, svg: $iconSvg, color: $iconColor }); const p = document.getElementById('icon-preview'); if (p) p.innerHTML = el.dataset.iconSvg; document.getElementById('icon-picker-dialog')?.close()`}
33
+ >
34
+ <span
35
+ class="w-5 h-5 flex items-center justify-center"
36
+ dangerouslySetInnerHTML={{
37
+ __html: svg
38
+ .replace(/width="24"/, 'width="20"')
39
+ .replace(/height="24"/, 'height="20"'),
40
+ }}
41
+ />
42
+ </button>
43
+ );
44
+ })}
45
+ </div>
46
+ </div>
47
+ ))}
48
+ </div>
49
+ );
50
+ };
@@ -6,6 +6,7 @@ import { useLingui } from "@lingui/react/macro";
6
6
  import type { Collection, PostView } from "../../../types.js";
7
7
  import { ActionButtons } from "../index.js";
8
8
  import { encode } from "../../../lib/sqid.js";
9
+ import { renderCollectionIcon } from "../../../lib/icons.js";
9
10
 
10
11
  export function ViewCollectionContent({
11
12
  collection,
@@ -15,17 +16,27 @@ export function ViewCollectionContent({
15
16
  posts: PostView[];
16
17
  }) {
17
18
  const { t } = useLingui();
19
+ const count = String(posts.length);
18
20
  const postsHeader = t({
19
- message: "Posts in Collection ({count})",
21
+ message: `Posts in Collection (${count})`,
20
22
  comment: "@context: Collection posts section heading",
21
- values: { count: String(posts.length) },
22
23
  });
23
24
 
24
25
  return (
25
26
  <>
26
27
  <div class="flex items-center justify-between mb-6">
27
28
  <div>
28
- <h1 class="text-2xl font-semibold">{collection.title}</h1>
29
+ <h1 class="text-2xl font-semibold flex items-center gap-2">
30
+ {collection.icon && (
31
+ <span
32
+ class="shrink-0"
33
+ dangerouslySetInnerHTML={{
34
+ __html: renderCollectionIcon(collection.icon, { size: 24 }),
35
+ }}
36
+ />
37
+ )}
38
+ {collection.title}
39
+ </h1>
29
40
  <p class="text-sm text-muted-foreground">/{collection.slug}</p>
30
41
  </div>
31
42
  <ActionButtons
@@ -5,6 +5,6 @@ export { EmptyState, type EmptyStateProps } from "../shared/EmptyState.js";
5
5
  export { FormatBadge, type FormatBadgeProps } from "./FormatBadge.js";
6
6
  export { ListItemRow, type ListItemRowProps } from "./ListItemRow.js";
7
7
  export { PageForm, type PageFormProps } from "./PageForm.js";
8
- export { PostForm, type PostFormProps } from "./PostForm.js";
8
+ export { PostForm, type PostFormProps } from "./posts/PostForm.js";
9
9
  export { PostList, type PostListProps } from "./PostList.js";
10
10
  export { StatusBadge, type StatusBadgeProps } from "./StatusBadge.js";