@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,360 +0,0 @@
1
- /**
2
- * Post Creation/Edit Form
3
- */
4
-
5
- import type { FC } from "hono/jsx";
6
- import type { Post, Media, Collection } from "../../types.js";
7
- import { useLingui } from "@lingui/react/macro";
8
- import {
9
- getMediaUrl,
10
- getImageUrl,
11
- getPublicUrlForProvider,
12
- } from "../../lib/image.js";
13
-
14
- export interface PostFormProps {
15
- post?: Post;
16
- action: string;
17
- mediaAttachments?: Media[];
18
- r2PublicUrl?: string;
19
- imageTransformUrl?: string;
20
- s3PublicUrl?: string;
21
- collections?: Collection[];
22
- }
23
-
24
- export const PostForm: FC<PostFormProps> = ({
25
- post,
26
- action,
27
- mediaAttachments,
28
- r2PublicUrl,
29
- imageTransformUrl,
30
- s3PublicUrl,
31
- collections,
32
- }) => {
33
- const { t } = useLingui();
34
- const isEdit = !!post;
35
-
36
- const existingMediaIds = (mediaAttachments ?? []).map((m) => m.id);
37
-
38
- const signals = JSON.stringify({
39
- format: post?.format ?? "note",
40
- title: post?.title ?? "",
41
- body: post?.body ?? "",
42
- url: post?.url ?? "",
43
- quoteText: post?.quoteText ?? "",
44
- status: post?.status ?? "published",
45
- featured: post?.featured === 1,
46
- pinned: post?.pinned === 1,
47
- rating: post?.rating ?? 0,
48
- collectionId: post?.collectionId ?? 0,
49
- mediaIds: existingMediaIds,
50
- }).replace(/</g, "\\u003c");
51
-
52
- return (
53
- <form
54
- data-signals={signals}
55
- data-on:submit__prevent={`@post('${action}')`}
56
- data-indicator="_loading"
57
- class="flex flex-col gap-4"
58
- >
59
- <div id="post-form-message"></div>
60
-
61
- {/* Format selector */}
62
- <div class="field">
63
- <label class="label">
64
- {t({
65
- message: "Format",
66
- comment: "@context: Post form field - post format",
67
- })}
68
- </label>
69
- <select data-bind="format" class="select" required>
70
- <option value="note" selected={post?.format === "note" || !post}>
71
- {t({ message: "Note", comment: "@context: Post format option" })}
72
- </option>
73
- <option value="link" selected={post?.format === "link"}>
74
- {t({ message: "Link", comment: "@context: Post format option" })}
75
- </option>
76
- <option value="quote" selected={post?.format === "quote"}>
77
- {t({ message: "Quote", comment: "@context: Post format option" })}
78
- </option>
79
- </select>
80
- </div>
81
-
82
- {/* Title (optional) */}
83
- <div class="field">
84
- <label class="label">
85
- {t({
86
- message: "Title (optional)",
87
- comment: "@context: Post form field",
88
- })}
89
- </label>
90
- <input
91
- type="text"
92
- data-bind="title"
93
- class="input"
94
- placeholder={t({
95
- message: "Post title...",
96
- comment: "@context: Post title placeholder",
97
- })}
98
- />
99
- </div>
100
-
101
- {/* Body */}
102
- <div class="field">
103
- <label class="label">
104
- {t({ message: "Content", comment: "@context: Post form field" })}
105
- </label>
106
- <textarea
107
- data-bind="body"
108
- class="textarea min-h-32"
109
- placeholder={t({
110
- message: "What's on your mind?",
111
- comment: "@context: Post content placeholder",
112
- })}
113
- >
114
- {post?.body ?? ""}
115
- </textarea>
116
- </div>
117
-
118
- {/* URL (for link/quote formats) */}
119
- <div class="field">
120
- <label class="label">
121
- {t({
122
- message: "URL (optional)",
123
- comment: "@context: Post form field - source URL",
124
- })}
125
- </label>
126
- <input
127
- type="url"
128
- data-bind="url"
129
- class="input"
130
- placeholder="https://..."
131
- />
132
- </div>
133
-
134
- {/* Quote Text (for quote format) */}
135
- <div class="field" data-show="$format === 'quote'">
136
- <label class="label">
137
- {t({
138
- message: "Quote Text",
139
- comment: "@context: Post form field - quoted text",
140
- })}
141
- </label>
142
- <textarea
143
- data-bind="quoteText"
144
- class="textarea"
145
- placeholder={t({
146
- message: "The text being quoted...",
147
- comment: "@context: Quote text placeholder",
148
- })}
149
- rows={3}
150
- >
151
- {post?.quoteText ?? ""}
152
- </textarea>
153
- </div>
154
-
155
- {/* Media attachments */}
156
- <div class="field">
157
- <label class="label">
158
- {t({
159
- message: "Media",
160
- comment: "@context: Post form field - media attachments",
161
- })}
162
- </label>
163
- {mediaAttachments && mediaAttachments.length > 0 && (
164
- <div class="grid grid-cols-4 sm:grid-cols-6 gap-2 mb-2">
165
- {mediaAttachments.map((m) => {
166
- const pUrl = getPublicUrlForProvider(
167
- m.provider,
168
- r2PublicUrl,
169
- s3PublicUrl,
170
- );
171
- const mUrl = getMediaUrl(m.storageKey, pUrl);
172
- const thumbUrl = getImageUrl(mUrl, imageTransformUrl, {
173
- width: 150,
174
- quality: 80,
175
- format: "auto",
176
- fit: "cover",
177
- });
178
- return (
179
- <div
180
- key={m.id}
181
- class="relative group aspect-square"
182
- data-show={`$mediaIds.includes('${m.id}')`}
183
- >
184
- <img
185
- src={thumbUrl}
186
- alt={m.alt || m.originalName}
187
- class="w-full h-full object-cover rounded-lg border"
188
- loading="lazy"
189
- />
190
- <button
191
- type="button"
192
- class="absolute top-1 right-1 w-5 h-5 flex items-center justify-center bg-black/60 text-white rounded-full text-xs opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
193
- data-on:click={`$mediaIds = $mediaIds.filter(id => id !== '${m.id}')`}
194
- title={t({
195
- message: "Remove",
196
- comment: "@context: Remove media attachment button",
197
- })}
198
- >
199
- &times;
200
- </button>
201
- </div>
202
- );
203
- })}
204
- </div>
205
- )}
206
- <button
207
- type="button"
208
- class="btn-outline text-sm"
209
- data-on:click="document.getElementById('media-picker-dialog').showModal(); fetch('/dash/media/picker').then(r => r.text()).then(html => document.getElementById('media-picker-grid').innerHTML = html)"
210
- >
211
- {t({
212
- message: "Add Media",
213
- comment: "@context: Button to open media picker",
214
- })}
215
- </button>
216
- </div>
217
-
218
- {/* Status */}
219
- <div class="field">
220
- <label class="label">
221
- {t({ message: "Status", comment: "@context: Post form field" })}
222
- </label>
223
- <select data-bind="status" class="select">
224
- <option
225
- value="published"
226
- selected={post?.status === "published" || !post}
227
- >
228
- {t({
229
- message: "Published",
230
- comment: "@context: Post status option",
231
- })}
232
- </option>
233
- <option value="draft" selected={post?.status === "draft"}>
234
- {t({
235
- message: "Draft",
236
- comment: "@context: Post status option",
237
- })}
238
- </option>
239
- </select>
240
- </div>
241
-
242
- {/* Featured & Pinned */}
243
- <div class="flex gap-4">
244
- <label class="flex items-center gap-2 text-sm">
245
- <input type="checkbox" class="checkbox" data-bind="featured" />
246
- {t({
247
- message: "Featured",
248
- comment: "@context: Post form checkbox - mark as featured",
249
- })}
250
- </label>
251
- <label class="flex items-center gap-2 text-sm">
252
- <input type="checkbox" class="checkbox" data-bind="pinned" />
253
- {t({
254
- message: "Pinned",
255
- comment: "@context: Post form checkbox - pin to top",
256
- })}
257
- </label>
258
- </div>
259
-
260
- {/* Collection */}
261
- {collections && collections.length > 0 && (
262
- <div class="field">
263
- <label class="label">
264
- {t({
265
- message: "Collection (optional)",
266
- comment: "@context: Post form field - assign to collection",
267
- })}
268
- </label>
269
- <select data-bind="collectionId" class="select">
270
- <option value="0">
271
- {t({
272
- message: "None",
273
- comment: "@context: No collection selected",
274
- })}
275
- </option>
276
- {collections.map((col) => (
277
- <option
278
- key={col.id}
279
- value={col.id}
280
- selected={post?.collectionId === col.id}
281
- >
282
- {col.title}
283
- </option>
284
- ))}
285
- </select>
286
- </div>
287
- )}
288
-
289
- {/* Submit */}
290
- <div class="flex gap-2">
291
- <button type="submit" class="btn" data-attr:disabled="$_loading">
292
- <svg
293
- data-show="$_loading"
294
- style="display:none"
295
- class="animate-spin size-4"
296
- xmlns="http://www.w3.org/2000/svg"
297
- viewBox="0 0 24 24"
298
- fill="none"
299
- stroke="currentColor"
300
- stroke-width="2"
301
- stroke-linecap="round"
302
- stroke-linejoin="round"
303
- role="status"
304
- >
305
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
306
- </svg>
307
- {isEdit
308
- ? t({
309
- message: "Update",
310
- comment: "@context: Button to update existing post",
311
- })
312
- : t({
313
- message: "Publish",
314
- comment: "@context: Button to publish new post",
315
- })}
316
- </button>
317
- <a href="/dash/posts" class="btn-outline">
318
- {t({ message: "Cancel", comment: "@context: Button to cancel form" })}
319
- </a>
320
- </div>
321
-
322
- {/* Media picker dialog */}
323
- <dialog
324
- id="media-picker-dialog"
325
- class="p-6 rounded-lg max-w-2xl w-full backdrop:bg-black/50"
326
- onclick="event.target === this && this.close()"
327
- >
328
- <div class="flex items-center justify-between mb-4">
329
- <h2 class="text-lg font-semibold">
330
- {t({
331
- message: "Select Media",
332
- comment: "@context: Media picker dialog title",
333
- })}
334
- </h2>
335
- <button
336
- type="button"
337
- class="btn-outline text-sm"
338
- onclick="this.closest('dialog').close()"
339
- >
340
- {t({
341
- message: "Done",
342
- comment: "@context: Close media picker button",
343
- })}
344
- </button>
345
- </div>
346
- <div
347
- id="media-picker-grid"
348
- class="grid grid-cols-4 gap-2 max-h-96 overflow-y-auto"
349
- >
350
- <p class="text-muted-foreground text-sm col-span-4">
351
- {t({
352
- message: "Loading...",
353
- comment: "@context: Loading state for media picker",
354
- })}
355
- </p>
356
- </div>
357
- </dialog>
358
- </form>
359
- );
360
- };
@@ -1,254 +0,0 @@
1
- /**
2
- * Appearance settings: color theme picker + custom CSS form
3
- */
4
-
5
- import { useLingui } from "@lingui/react/macro";
6
- import type { ColorTheme } from "../../color-themes.js";
7
- import type { FontTheme } from "../../font-themes.js";
8
- import { SettingsNav } from "./SettingsNav.js";
9
-
10
- function ThemeCard({
11
- theme,
12
- selected,
13
- }: {
14
- theme: ColorTheme;
15
- selected: boolean;
16
- }) {
17
- const expr = `$theme === '${theme.id}'`;
18
- const { preview } = theme;
19
-
20
- return (
21
- <label
22
- class={`block cursor-pointer rounded-lg border overflow-hidden transition-colors ${selected ? "border-primary" : "border-border"}`}
23
- data-class:border-primary={expr}
24
- data-class:border-border={`$theme !== '${theme.id}'`}
25
- >
26
- <div class="grid grid-cols-2">
27
- <div
28
- class="p-5"
29
- style={`background-color:${preview.lightBg};color:${preview.lightText}`}
30
- >
31
- <input
32
- type="radio"
33
- name="theme"
34
- value={theme.id}
35
- data-bind="theme"
36
- checked={selected || undefined}
37
- class="mb-1"
38
- />
39
- <h3 class="font-bold text-lg">{theme.name}</h3>
40
- <p class="text-sm mt-2 leading-relaxed">
41
- This is the {theme.name} theme in light mode. Links{" "}
42
- <a
43
- tabIndex={-1}
44
- class="underline"
45
- style={`color:${preview.lightLink}`}
46
- >
47
- look like this
48
- </a>
49
- . We'll show the correct light or dark mode based on your visitor's
50
- settings.
51
- </p>
52
- </div>
53
- <div
54
- class="p-5"
55
- style={`background-color:${preview.darkBg};color:${preview.darkText}`}
56
- >
57
- <h3 class="font-bold text-lg">{theme.name}</h3>
58
- <p class="text-sm mt-2 leading-relaxed">
59
- This is the {theme.name} theme in dark mode. Links{" "}
60
- <a
61
- tabIndex={-1}
62
- class="underline"
63
- style={`color:${preview.darkLink}`}
64
- >
65
- look like this
66
- </a>
67
- . We'll show the correct light or dark mode based on your visitor's
68
- settings.
69
- </p>
70
- </div>
71
- </div>
72
- </label>
73
- );
74
- }
75
-
76
- export function AppearanceContent({
77
- themes,
78
- currentThemeId,
79
- fontThemes,
80
- currentFontThemeId,
81
- customCSS,
82
- }: {
83
- themes: ColorTheme[];
84
- currentThemeId: string;
85
- fontThemes: FontTheme[];
86
- currentFontThemeId: string;
87
- customCSS: string;
88
- }) {
89
- const { t } = useLingui();
90
-
91
- const themeSignals = JSON.stringify({ theme: currentThemeId }).replace(
92
- /</g,
93
- "\\u003c",
94
- );
95
-
96
- const cssSignals = JSON.stringify({ customCSS }).replace(/</g, "\\u003c");
97
-
98
- return (
99
- <>
100
- <h1 class="text-2xl font-semibold mb-2">
101
- {t({ message: "Settings", comment: "@context: Dashboard heading" })}
102
- </h1>
103
- <SettingsNav currentTab="appearance" />
104
-
105
- <div
106
- data-signals={themeSignals}
107
- data-on:change="@post('/dash/settings/appearance')"
108
- class="max-w-3xl mb-8"
109
- >
110
- <fieldset>
111
- <legend class="text-lg font-semibold">
112
- {t({
113
- message: "Color theme",
114
- comment: "@context: Appearance settings heading",
115
- })}
116
- </legend>
117
- <p class="text-sm text-muted-foreground mb-4">
118
- {t({
119
- message:
120
- "This will theme both your site and your dashboard. All color themes support dark mode.",
121
- comment: "@context: Appearance settings description",
122
- })}
123
- </p>
124
-
125
- <div class="flex flex-col gap-4">
126
- {themes.map((theme) => (
127
- <ThemeCard
128
- key={theme.id}
129
- theme={theme}
130
- selected={theme.id === currentThemeId}
131
- />
132
- ))}
133
- </div>
134
- </fieldset>
135
- </div>
136
-
137
- <div
138
- data-signals={JSON.stringify({ fontTheme: currentFontThemeId }).replace(
139
- /</g,
140
- "\\u003c",
141
- )}
142
- data-on:change="@post('/dash/settings/font-theme')"
143
- class="max-w-3xl"
144
- >
145
- <fieldset>
146
- <legend class="text-lg font-semibold">
147
- {t({
148
- message: "Font theme",
149
- comment: "@context: Appearance settings heading for font theme",
150
- })}
151
- </legend>
152
- <p class="text-sm text-muted-foreground mb-4">
153
- {t({
154
- message:
155
- "Choose a font for your site. All options use system fonts for fast loading.",
156
- comment: "@context: Font theme settings description",
157
- })}
158
- </p>
159
- <div class="flex flex-col gap-2">
160
- {fontThemes.map((ft) => (
161
- <label
162
- key={ft.id}
163
- class={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${ft.id === currentFontThemeId ? "border-primary" : "border-border"}`}
164
- data-class:border-primary={`$fontTheme === '${ft.id}'`}
165
- data-class:border-border={`$fontTheme !== '${ft.id}'`}
166
- >
167
- <input
168
- type="radio"
169
- name="fontTheme"
170
- value={ft.id}
171
- data-bind="fontTheme"
172
- checked={ft.id === currentFontThemeId || undefined}
173
- class="mt-1"
174
- />
175
- <div>
176
- <div class="font-medium">{ft.name}</div>
177
- <div class="text-sm text-muted-foreground">
178
- {ft.description}
179
- </div>
180
- <div
181
- class="mt-1 text-sm"
182
- style={`font-family:${ft.fontFamily}`}
183
- >
184
- The quick brown fox jumps over the lazy dog.{" "}
185
- 敏捷的棕色狐狸跳过了懒狗。
186
- </div>
187
- </div>
188
- </label>
189
- ))}
190
- </div>
191
- </fieldset>
192
- </div>
193
-
194
- <form
195
- data-signals={cssSignals}
196
- data-on:submit__prevent="@post('/dash/settings/custom-css')"
197
- data-indicator="_cssLoading"
198
- class="max-w-3xl mt-8"
199
- >
200
- <fieldset>
201
- <legend class="text-lg font-semibold">
202
- {t({
203
- message: "Custom CSS",
204
- comment: "@context: Appearance settings heading for custom CSS",
205
- })}
206
- </legend>
207
- <p class="text-sm text-muted-foreground mb-4">
208
- {t({
209
- message:
210
- "Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.",
211
- comment: "@context: Custom CSS settings description",
212
- })}
213
- </p>
214
- <textarea
215
- data-bind="customCSS"
216
- class="textarea font-mono text-sm min-h-32"
217
- rows={8}
218
- placeholder={t({
219
- message: "/* Your custom CSS here */",
220
- comment: "@context: Custom CSS textarea placeholder",
221
- })}
222
- >
223
- {customCSS}
224
- </textarea>
225
- </fieldset>
226
- <button
227
- type="submit"
228
- class="btn mt-4"
229
- data-attr:disabled="$_cssLoading"
230
- >
231
- <svg
232
- data-show="$_cssLoading"
233
- style="display:none"
234
- class="animate-spin size-4"
235
- xmlns="http://www.w3.org/2000/svg"
236
- viewBox="0 0 24 24"
237
- fill="none"
238
- stroke="currentColor"
239
- stroke-width="2"
240
- stroke-linecap="round"
241
- stroke-linejoin="round"
242
- role="status"
243
- >
244
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
245
- </svg>
246
- {t({
247
- message: "Save CSS",
248
- comment: "@context: Button to save custom CSS",
249
- })}
250
- </button>
251
- </form>
252
- </>
253
- );
254
- }