@jant/core 0.3.27 → 0.3.28

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