@jant/core 0.3.26 → 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 (314) 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 +112 -173
  9. package/src/auth.ts +4 -1
  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 -265
  172. package/dist/auth.js +0 -36
  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
@@ -16,7 +16,7 @@ export const ComposePrompt: FC = () => {
16
16
  <button
17
17
  type="button"
18
18
  class="compose-prompt-trigger"
19
- onclick="document.getElementById('compose-dialog').showModal()"
19
+ onclick="const d=document.getElementById('compose-dialog');d.showModal();d.querySelector('jant-compose-editor')?.focusInput()"
20
20
  >
21
21
  <span class="compose-prompt-avatar">
22
22
  <svg
@@ -35,7 +35,7 @@ export const ComposePrompt: FC = () => {
35
35
  </span>
36
36
  <span class="compose-prompt-text">
37
37
  {t({
38
- message: "What's new?",
38
+ message: "What's on your mind?",
39
39
  comment: "@context: Compose prompt placeholder text",
40
40
  })}
41
41
  </span>
@@ -43,7 +43,7 @@ export const ComposePrompt: FC = () => {
43
43
  <button
44
44
  type="button"
45
45
  class="compose-prompt-post-btn"
46
- onclick="document.getElementById('compose-dialog').showModal()"
46
+ onclick="const d=document.getElementById('compose-dialog');d.showModal();d.querySelector('jant-compose-editor')?.focusInput()"
47
47
  >
48
48
  {t({
49
49
  message: "Post",
@@ -4,9 +4,8 @@
4
4
 
5
5
  import type { FC } from "hono/jsx";
6
6
  import { useLingui } from "@lingui/react/macro";
7
- import type { Post } from "../../types.js";
7
+ import type { PostView } from "../../types.js";
8
8
  import * as sqid from "../../lib/sqid.js";
9
- import * as time from "../../lib/time.js";
10
9
  import { StatusBadge } from "./StatusBadge.js";
11
10
  import { FormatBadge } from "./FormatBadge.js";
12
11
  import { EmptyState } from "../shared/EmptyState.js";
@@ -14,7 +13,7 @@ import { ListItemRow } from "./ListItemRow.js";
14
13
  import { ActionButtons } from "./ActionButtons.js";
15
14
 
16
15
  export interface PostListProps {
17
- posts: Post[];
16
+ posts: PostView[];
18
17
  }
19
18
 
20
19
  export const PostList: FC<PostListProps> = ({ posts }) => {
@@ -37,65 +36,60 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
37
36
 
38
37
  return (
39
38
  <div class="flex flex-col divide-y">
40
- {posts.map((post) => {
41
- const permalink = post.path
42
- ? `/${post.path}`
43
- : `/p/${sqid.encode(post.id)}`;
44
- return (
45
- <ListItemRow
46
- key={post.id}
47
- actions={
48
- <ActionButtons
49
- editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
50
- editLabel={t({
51
- message: "Edit",
52
- comment: "@context: Button to edit post",
53
- })}
54
- viewHref={permalink}
55
- viewLabel={t({
56
- message: "View",
57
- comment: "@context: Button to view post on public site",
58
- })}
59
- deleteAction={`/dash/posts/${sqid.encode(post.id)}/delete`}
60
- deleteConfirm={t({
61
- message:
62
- "Are you sure you want to delete this post? This cannot be undone.",
63
- comment:
64
- "@context: Confirmation dialog when deleting a post from the list",
65
- })}
66
- />
67
- }
39
+ {posts.map((post) => (
40
+ <ListItemRow
41
+ key={post.id}
42
+ actions={
43
+ <ActionButtons
44
+ editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
45
+ editLabel={t({
46
+ message: "Edit",
47
+ comment: "@context: Button to edit post",
48
+ })}
49
+ viewHref={post.permalink}
50
+ viewLabel={t({
51
+ message: "View",
52
+ comment: "@context: Button to view post on public site",
53
+ })}
54
+ deleteAction={`/dash/posts/${sqid.encode(post.id)}/delete`}
55
+ deleteConfirm={t({
56
+ message:
57
+ "Are you sure you want to delete this post? This cannot be undone.",
58
+ comment:
59
+ "@context: Confirmation dialog when deleting a post from the list",
60
+ })}
61
+ />
62
+ }
63
+ >
64
+ <div class="flex items-center gap-2 mb-1">
65
+ <FormatBadge type={post.format} />
66
+ <StatusBadge
67
+ status={post.status}
68
+ featured={post.featured}
69
+ pinned={post.pinned}
70
+ />
71
+ <span class="text-xs text-muted-foreground">
72
+ {post.publishedAtFormatted}
73
+ </span>
74
+ </div>
75
+ <a
76
+ href={`/dash/posts/${sqid.encode(post.id)}`}
77
+ class="font-medium hover:underline"
68
78
  >
69
- <div class="flex items-center gap-2 mb-1">
70
- <FormatBadge type={post.format} />
71
- <StatusBadge
72
- status={post.status}
73
- featured={post.featured === 1}
74
- pinned={post.pinned === 1}
75
- />
76
- <span class="text-xs text-muted-foreground">
77
- {time.formatDate(post.publishedAt)}
78
- </span>
79
- </div>
80
- <a
81
- href={`/dash/posts/${sqid.encode(post.id)}`}
82
- class="font-medium hover:underline"
83
- >
84
- {post.title ||
85
- post.body?.slice(0, 60) ||
86
- t({
87
- message: "Untitled",
88
- comment: "@context: Default title for untitled post",
89
- })}
90
- </a>
91
- {post.body && !post.title && (
92
- <p class="text-sm text-muted-foreground mt-1 line-clamp-2">
93
- {post.body.slice(0, 120)}
94
- </p>
95
- )}
96
- </ListItemRow>
97
- );
98
- })}
79
+ {post.title ||
80
+ post.body?.slice(0, 60) ||
81
+ t({
82
+ message: "Untitled",
83
+ comment: "@context: Default title for untitled post",
84
+ })}
85
+ </a>
86
+ {post.body && !post.title && (
87
+ <p class="text-sm text-muted-foreground mt-1 line-clamp-2">
88
+ {post.body.slice(0, 120)}
89
+ </p>
90
+ )}
91
+ </ListItemRow>
92
+ ))}
99
93
  </div>
100
94
  );
101
95
  };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Advanced appearance: custom CSS editor
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+ import { AppearanceNav } from "./AppearanceNav.js";
7
+
8
+ export function AdvancedContent({ customCSS }: { customCSS: string }) {
9
+ const { t } = useLingui();
10
+
11
+ const cssSignals = JSON.stringify({ customCSS }).replace(/</g, "\\u003c");
12
+
13
+ return (
14
+ <>
15
+ <h1 class="text-2xl font-semibold mb-2">
16
+ {t({ message: "Appearance", comment: "@context: Dashboard heading" })}
17
+ </h1>
18
+ <AppearanceNav currentTab="advanced" />
19
+
20
+ <form
21
+ data-signals={cssSignals}
22
+ data-on:submit__prevent="@post('/dash/appearance/custom-css')"
23
+ data-indicator="_cssLoading"
24
+ class="max-w-3xl"
25
+ >
26
+ <fieldset>
27
+ <legend class="text-lg font-semibold">
28
+ {t({
29
+ message: "Custom CSS",
30
+ comment: "@context: Appearance settings heading for custom CSS",
31
+ })}
32
+ </legend>
33
+ <p class="text-sm text-muted-foreground mb-4">
34
+ {t({
35
+ message:
36
+ "Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.",
37
+ comment: "@context: Custom CSS settings description",
38
+ })}
39
+ </p>
40
+ <textarea
41
+ data-bind="customCSS"
42
+ class="textarea font-mono text-sm min-h-32"
43
+ rows={8}
44
+ placeholder={t({
45
+ message: "/* Your custom CSS here */",
46
+ comment: "@context: Custom CSS textarea placeholder",
47
+ })}
48
+ >
49
+ {customCSS}
50
+ </textarea>
51
+ </fieldset>
52
+ <button
53
+ type="submit"
54
+ class="btn mt-4"
55
+ data-attr:disabled="$_cssLoading"
56
+ >
57
+ <svg
58
+ data-show="$_cssLoading"
59
+ style="display:none"
60
+ class="animate-spin size-4"
61
+ xmlns="http://www.w3.org/2000/svg"
62
+ viewBox="0 0 24 24"
63
+ fill="none"
64
+ stroke="currentColor"
65
+ stroke-width="2"
66
+ stroke-linecap="round"
67
+ stroke-linejoin="round"
68
+ role="status"
69
+ >
70
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
71
+ </svg>
72
+ {t({
73
+ message: "Save CSS",
74
+ comment: "@context: Button to save custom CSS",
75
+ })}
76
+ </button>
77
+ </form>
78
+ </>
79
+ );
80
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Appearance sub-navigation tabs
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+
7
+ export type AppearanceTab = "color" | "fonts" | "advanced";
8
+
9
+ export function AppearanceNav({ currentTab }: { currentTab: AppearanceTab }) {
10
+ const { t } = useLingui();
11
+
12
+ const tabs: { id: AppearanceTab; label: string; href: string }[] = [
13
+ {
14
+ id: "color",
15
+ label: t({
16
+ message: "Color Theme",
17
+ comment: "@context: Appearance sub-navigation tab",
18
+ }),
19
+ href: "/dash/appearance",
20
+ },
21
+ {
22
+ id: "fonts",
23
+ label: t({
24
+ message: "Font Theme",
25
+ comment: "@context: Appearance sub-navigation tab",
26
+ }),
27
+ href: "/dash/appearance/fonts",
28
+ },
29
+ {
30
+ id: "advanced",
31
+ label: t({
32
+ message: "Advanced",
33
+ comment: "@context: Appearance sub-navigation tab",
34
+ }),
35
+ href: "/dash/appearance/advanced",
36
+ },
37
+ ];
38
+
39
+ return (
40
+ <nav class="flex gap-1 mb-6">
41
+ {tabs.map((tab) => (
42
+ <a
43
+ key={tab.id}
44
+ href={tab.href}
45
+ class={`px-3 py-2 text-sm rounded-md ${
46
+ tab.id === currentTab
47
+ ? "bg-accent text-accent-foreground font-medium"
48
+ : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
49
+ }`}
50
+ >
51
+ {tab.label}
52
+ </a>
53
+ ))}
54
+ </nav>
55
+ );
56
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Color theme picker
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+ import type { ColorTheme } from "../../color-themes.js";
7
+ import { AppearanceNav } from "./AppearanceNav.js";
8
+
9
+ function ThemeCard({
10
+ theme,
11
+ selected,
12
+ }: {
13
+ theme: ColorTheme;
14
+ selected: boolean;
15
+ }) {
16
+ const expr = `$theme === '${theme.id}'`;
17
+ const { preview } = theme;
18
+
19
+ return (
20
+ <label
21
+ class={`block cursor-pointer rounded-lg border overflow-hidden transition-colors ${selected ? "border-primary" : "border-border"}`}
22
+ data-class:border-primary={expr}
23
+ data-class:border-border={`$theme !== '${theme.id}'`}
24
+ >
25
+ <div class="grid grid-cols-2">
26
+ <div
27
+ class="p-5"
28
+ style={`background-color:${preview.lightBg};color:${preview.lightText}`}
29
+ >
30
+ <input
31
+ type="radio"
32
+ name="theme"
33
+ value={theme.id}
34
+ data-bind="theme"
35
+ checked={selected || undefined}
36
+ class="mb-1"
37
+ />
38
+ <h3 class="font-bold text-lg">{theme.name}</h3>
39
+ <p class="text-sm mt-2 leading-relaxed">
40
+ This is the {theme.name} theme in light mode. Links{" "}
41
+ <a
42
+ tabIndex={-1}
43
+ class="underline"
44
+ style={`color:${preview.lightLink}`}
45
+ >
46
+ look like this
47
+ </a>
48
+ . We'll show the correct light or dark mode based on your visitor's
49
+ settings.
50
+ </p>
51
+ </div>
52
+ <div
53
+ class="p-5"
54
+ style={`background-color:${preview.darkBg};color:${preview.darkText}`}
55
+ >
56
+ <h3 class="font-bold text-lg">{theme.name}</h3>
57
+ <p class="text-sm mt-2 leading-relaxed">
58
+ This is the {theme.name} theme in dark mode. Links{" "}
59
+ <a
60
+ tabIndex={-1}
61
+ class="underline"
62
+ style={`color:${preview.darkLink}`}
63
+ >
64
+ look like this
65
+ </a>
66
+ . We'll show the correct light or dark mode based on your visitor's
67
+ settings.
68
+ </p>
69
+ </div>
70
+ </div>
71
+ </label>
72
+ );
73
+ }
74
+
75
+ export function ColorThemeContent({
76
+ themes,
77
+ currentThemeId,
78
+ }: {
79
+ themes: ColorTheme[];
80
+ currentThemeId: string;
81
+ }) {
82
+ const { t } = useLingui();
83
+
84
+ const themeSignals = JSON.stringify({ theme: currentThemeId }).replace(
85
+ /</g,
86
+ "\\u003c",
87
+ );
88
+
89
+ return (
90
+ <>
91
+ <h1 class="text-2xl font-semibold mb-2">
92
+ {t({ message: "Appearance", comment: "@context: Dashboard heading" })}
93
+ </h1>
94
+ <AppearanceNav currentTab="color" />
95
+
96
+ <div
97
+ data-signals={themeSignals}
98
+ data-on:change="@post('/dash/appearance')"
99
+ class="max-w-3xl"
100
+ >
101
+ <fieldset>
102
+ <legend class="text-lg font-semibold">
103
+ {t({
104
+ message: "Color theme",
105
+ comment: "@context: Appearance settings heading",
106
+ })}
107
+ </legend>
108
+ <p class="text-sm text-muted-foreground mb-4">
109
+ {t({
110
+ message:
111
+ "This will theme both your site and your dashboard. All color themes support dark mode.",
112
+ comment: "@context: Appearance settings description",
113
+ })}
114
+ </p>
115
+
116
+ <div class="flex flex-col gap-4">
117
+ {themes.map((theme) => (
118
+ <ThemeCard
119
+ key={theme.id}
120
+ theme={theme}
121
+ selected={theme.id === currentThemeId}
122
+ />
123
+ ))}
124
+ </div>
125
+ </fieldset>
126
+ </div>
127
+ </>
128
+ );
129
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Font theme picker
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+ import type { FontTheme } from "../../font-themes.js";
7
+ import { AppearanceNav } from "./AppearanceNav.js";
8
+
9
+ export function FontThemeContent({
10
+ fontThemes,
11
+ currentFontThemeId,
12
+ }: {
13
+ fontThemes: FontTheme[];
14
+ currentFontThemeId: string;
15
+ }) {
16
+ const { t } = useLingui();
17
+
18
+ return (
19
+ <>
20
+ <h1 class="text-2xl font-semibold mb-2">
21
+ {t({ message: "Appearance", comment: "@context: Dashboard heading" })}
22
+ </h1>
23
+ <AppearanceNav currentTab="fonts" />
24
+
25
+ <div
26
+ data-signals={JSON.stringify({ fontTheme: currentFontThemeId }).replace(
27
+ /</g,
28
+ "\\u003c",
29
+ )}
30
+ data-on:change="@post('/dash/appearance/font-theme')"
31
+ class="max-w-3xl"
32
+ >
33
+ <fieldset>
34
+ <legend class="text-lg font-semibold">
35
+ {t({
36
+ message: "Font theme",
37
+ comment: "@context: Appearance settings heading for font theme",
38
+ })}
39
+ </legend>
40
+ <p class="text-sm text-muted-foreground mb-4">
41
+ {t({
42
+ message:
43
+ "Choose a font pairing for your site. All options use system fonts for fast loading.",
44
+ comment: "@context: Font theme settings description",
45
+ })}
46
+ </p>
47
+ <div class="flex flex-col gap-2">
48
+ {fontThemes.map((ft) => (
49
+ <label
50
+ key={ft.id}
51
+ class={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${ft.id === currentFontThemeId ? "border-primary" : "border-border"}`}
52
+ data-class:border-primary={`$fontTheme === '${ft.id}'`}
53
+ data-class:border-border={`$fontTheme !== '${ft.id}'`}
54
+ >
55
+ <input
56
+ type="radio"
57
+ name="fontTheme"
58
+ value={ft.id}
59
+ data-bind="fontTheme"
60
+ checked={ft.id === currentFontThemeId || undefined}
61
+ class="mt-1"
62
+ />
63
+ <div>
64
+ <div class="font-medium">{t(ft.name)}</div>
65
+ <div class="text-sm text-muted-foreground">
66
+ {t(ft.description)}
67
+ </div>
68
+ <div class="mt-1 text-sm leading-relaxed">
69
+ <div
70
+ class="font-semibold"
71
+ style={`font-family:${ft.headingFontFamily}`}
72
+ >
73
+ {t({
74
+ message: "The quick brown fox jumps over the lazy dog.",
75
+ comment:
76
+ "@context: Font theme preview sentence for headings",
77
+ })}
78
+ </div>
79
+ <div
80
+ class="mt-2"
81
+ style={`font-family:${ft.bodyFontFamily}`}
82
+ >
83
+ {t({
84
+ message: "The quick brown fox jumps over the lazy dog.",
85
+ comment:
86
+ "@context: Font theme preview sentence for body text",
87
+ })}
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </label>
92
+ ))}
93
+ </div>
94
+ </fieldset>
95
+ </div>
96
+ </>
97
+ );
98
+ }