@jant/core 0.3.24 → 0.3.26

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 (277) hide show
  1. package/dist/app.js +101 -571
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +1 -1
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/index.js +3 -9
  8. package/dist/lib/avatar-upload.js +134 -0
  9. package/dist/lib/config.js +39 -0
  10. package/dist/lib/constants.js +10 -9
  11. package/dist/lib/favicon.js +102 -0
  12. package/dist/lib/image.js +13 -17
  13. package/dist/lib/media-helpers.js +2 -2
  14. package/dist/lib/nav-reorder.js +1 -1
  15. package/dist/lib/navigation.js +48 -3
  16. package/dist/lib/pagination.js +44 -0
  17. package/dist/lib/render.js +16 -11
  18. package/dist/lib/schemas.js +34 -3
  19. package/dist/lib/theme.js +4 -4
  20. package/dist/lib/timeline.js +24 -48
  21. package/dist/lib/timezones.js +388 -0
  22. package/dist/lib/view.js +3 -3
  23. package/dist/routes/api/collections.js +124 -0
  24. package/dist/routes/api/nav-items.js +104 -0
  25. package/dist/routes/api/pages.js +91 -0
  26. package/dist/routes/api/posts.js +3 -3
  27. package/dist/routes/api/search.js +2 -2
  28. package/dist/routes/api/settings.js +68 -0
  29. package/dist/routes/api/upload.js +3 -3
  30. package/dist/routes/auth/reset.js +221 -0
  31. package/dist/routes/auth/setup.js +194 -0
  32. package/dist/routes/auth/signin.js +176 -0
  33. package/dist/routes/compose.js +48 -0
  34. package/dist/routes/dash/collections.js +24 -416
  35. package/dist/routes/dash/index.js +1 -1
  36. package/dist/routes/dash/media.js +13 -393
  37. package/dist/routes/dash/pages.js +112 -86
  38. package/dist/routes/dash/posts.js +3 -5
  39. package/dist/routes/dash/redirects.js +20 -14
  40. package/dist/routes/dash/settings.js +213 -518
  41. package/dist/routes/feed/rss.js +4 -3
  42. package/dist/routes/feed/sitemap.js +5 -3
  43. package/dist/routes/pages/archive.js +3 -6
  44. package/dist/routes/pages/collection.js +3 -6
  45. package/dist/routes/pages/collections.js +28 -0
  46. package/dist/routes/pages/featured.js +36 -0
  47. package/dist/routes/pages/home.js +33 -49
  48. package/dist/routes/pages/latest.js +45 -0
  49. package/dist/routes/pages/page.js +29 -32
  50. package/dist/routes/pages/post.js +3 -6
  51. package/dist/routes/pages/search.js +3 -6
  52. package/dist/services/page.js +5 -1
  53. package/dist/services/post.js +45 -31
  54. package/dist/services/search.js +1 -1
  55. package/dist/types/bindings.js +3 -0
  56. package/dist/types/config.js +147 -0
  57. package/dist/types/constants.js +27 -0
  58. package/dist/types/entities.js +3 -0
  59. package/dist/types/operations.js +3 -0
  60. package/dist/types/props.js +3 -0
  61. package/dist/types/views.js +5 -0
  62. package/dist/types.js +8 -111
  63. package/dist/{theme → ui}/color-themes.js +33 -33
  64. package/dist/ui/compose/ComposeDialog.js +467 -0
  65. package/dist/ui/compose/ComposePrompt.js +55 -0
  66. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  67. package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
  68. package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
  69. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  70. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  71. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  72. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  73. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  74. package/dist/{theme/components → ui/dash}/index.js +3 -6
  75. package/dist/ui/dash/media/MediaListContent.js +166 -0
  76. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  77. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  78. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  79. package/dist/ui/dash/settings/AccountContent.js +209 -0
  80. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  81. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  82. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  83. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  84. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  85. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  86. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  87. package/dist/ui/feed/TimelineFeed.js +41 -0
  88. package/dist/ui/feed/TimelineItem.js +27 -0
  89. package/dist/ui/font-themes.js +36 -0
  90. package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
  91. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  92. package/dist/ui/layouts/SiteLayout.js +169 -0
  93. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  94. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  95. package/dist/ui/pages/CollectionsPage.js +76 -0
  96. package/dist/ui/pages/FeaturedPage.js +24 -0
  97. package/dist/ui/pages/HomePage.js +24 -0
  98. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  99. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  100. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  101. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  102. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  103. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  104. package/dist/ui/shared/index.js +5 -0
  105. package/package.json +1 -9
  106. package/src/__tests__/helpers/db.ts +3 -0
  107. package/src/app.tsx +131 -561
  108. package/src/client.ts +1 -0
  109. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  110. package/src/db/migrations/meta/_journal.json +7 -0
  111. package/src/db/schema.ts +1 -1
  112. package/src/i18n/locales/en.po +477 -261
  113. package/src/i18n/locales/en.ts +1 -1
  114. package/src/i18n/locales/zh-Hans.po +477 -261
  115. package/src/i18n/locales/zh-Hans.ts +1 -1
  116. package/src/i18n/locales/zh-Hant.po +477 -261
  117. package/src/i18n/locales/zh-Hant.ts +1 -1
  118. package/src/index.ts +7 -36
  119. package/src/lib/__tests__/config.test.ts +192 -0
  120. package/src/lib/__tests__/favicon.test.ts +151 -0
  121. package/src/lib/__tests__/image.test.ts +2 -6
  122. package/src/lib/__tests__/schemas.test.ts +60 -19
  123. package/src/lib/__tests__/timeline.test.ts +45 -81
  124. package/src/lib/__tests__/timezones.test.ts +61 -0
  125. package/src/lib/__tests__/view.test.ts +15 -9
  126. package/src/lib/avatar-upload.ts +165 -0
  127. package/src/lib/config.ts +47 -0
  128. package/src/lib/constants.ts +19 -10
  129. package/src/lib/favicon.ts +115 -0
  130. package/src/lib/image.ts +13 -21
  131. package/src/lib/media-helpers.ts +2 -2
  132. package/src/lib/nav-reorder.ts +1 -1
  133. package/src/lib/navigation.ts +73 -4
  134. package/src/lib/pagination.ts +50 -0
  135. package/src/lib/render.tsx +22 -15
  136. package/src/lib/schemas.ts +47 -6
  137. package/src/lib/theme.ts +5 -5
  138. package/src/lib/timeline.ts +28 -57
  139. package/src/lib/timezones.ts +325 -0
  140. package/src/lib/view.ts +3 -3
  141. package/src/preset.css +2 -1
  142. package/src/routes/__tests__/compose.test.ts +199 -0
  143. package/src/routes/api/__tests__/collections.test.ts +249 -0
  144. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  145. package/src/routes/api/__tests__/pages.test.ts +218 -0
  146. package/src/routes/api/__tests__/settings.test.ts +132 -0
  147. package/src/routes/api/collections.ts +143 -0
  148. package/src/routes/api/nav-items.ts +115 -0
  149. package/src/routes/api/pages.ts +101 -0
  150. package/src/routes/api/posts.ts +3 -3
  151. package/src/routes/api/search.ts +2 -2
  152. package/src/routes/api/settings.ts +91 -0
  153. package/src/routes/api/upload.ts +2 -3
  154. package/src/routes/auth/reset.tsx +239 -0
  155. package/src/routes/auth/setup.tsx +189 -0
  156. package/src/routes/auth/signin.tsx +163 -0
  157. package/src/routes/compose.ts +63 -0
  158. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  159. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  160. package/src/routes/dash/collections.tsx +18 -367
  161. package/src/routes/dash/index.tsx +1 -1
  162. package/src/routes/dash/media.tsx +13 -415
  163. package/src/routes/dash/pages.tsx +131 -98
  164. package/src/routes/dash/posts.tsx +3 -7
  165. package/src/routes/dash/redirects.tsx +22 -16
  166. package/src/routes/dash/settings.tsx +265 -478
  167. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  168. package/src/routes/feed/rss.ts +5 -3
  169. package/src/routes/feed/sitemap.ts +5 -3
  170. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  171. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  172. package/src/routes/pages/archive.tsx +2 -6
  173. package/src/routes/pages/collection.tsx +2 -6
  174. package/src/routes/pages/collections.tsx +36 -0
  175. package/src/routes/pages/featured.tsx +44 -0
  176. package/src/routes/pages/home.tsx +30 -53
  177. package/src/routes/pages/latest.tsx +59 -0
  178. package/src/routes/pages/page.tsx +28 -30
  179. package/src/routes/pages/post.tsx +2 -5
  180. package/src/routes/pages/search.tsx +2 -6
  181. package/src/services/__tests__/page.test.ts +106 -0
  182. package/src/services/__tests__/post.test.ts +114 -15
  183. package/src/services/page.ts +13 -1
  184. package/src/services/post.ts +58 -40
  185. package/src/services/search.ts +2 -2
  186. package/src/styles/components.css +0 -65
  187. package/src/styles/tokens.css +47 -0
  188. package/src/styles/ui.css +475 -0
  189. package/src/types/bindings.ts +30 -0
  190. package/src/types/config.ts +183 -0
  191. package/src/types/constants.ts +26 -0
  192. package/src/types/entities.ts +109 -0
  193. package/src/types/operations.ts +88 -0
  194. package/src/types/props.ts +115 -0
  195. package/src/types/views.ts +172 -0
  196. package/src/types.ts +8 -774
  197. package/src/ui/__tests__/font-themes.test.ts +34 -0
  198. package/src/{theme → ui}/color-themes.ts +34 -34
  199. package/src/ui/compose/ComposeDialog.tsx +414 -0
  200. package/src/ui/compose/ComposePrompt.tsx +55 -0
  201. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  202. package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
  203. package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
  204. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  205. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  206. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  207. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  208. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  209. package/src/ui/dash/index.ts +10 -0
  210. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  211. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  212. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  213. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  214. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  215. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  216. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  217. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  218. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  219. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  220. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  221. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  222. package/src/ui/feed/TimelineFeed.tsx +49 -0
  223. package/src/ui/feed/TimelineItem.tsx +45 -0
  224. package/src/ui/font-themes.ts +54 -0
  225. package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
  226. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  227. package/src/ui/layouts/SiteLayout.tsx +164 -0
  228. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  229. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  230. package/src/ui/pages/CollectionsPage.tsx +73 -0
  231. package/src/ui/pages/FeaturedPage.tsx +31 -0
  232. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  233. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  234. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  235. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  236. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  237. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  238. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  239. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  240. package/src/ui/shared/index.ts +12 -0
  241. package/bin/jant.js +0 -185
  242. package/dist/lib/theme-components.js +0 -46
  243. package/dist/routes/dash/navigation.js +0 -289
  244. package/dist/theme/index.js +0 -18
  245. package/dist/theme/layouts/index.js +0 -2
  246. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  247. package/dist/themes/threads/index.js +0 -81
  248. package/dist/themes/threads/pages/HomePage.js +0 -25
  249. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  250. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  251. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  252. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  253. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  254. package/src/lib/__tests__/theme-components.test.ts +0 -105
  255. package/src/lib/theme-components.ts +0 -65
  256. package/src/routes/dash/navigation.tsx +0 -317
  257. package/src/theme/components/index.ts +0 -23
  258. package/src/theme/index.ts +0 -22
  259. package/src/theme/layouts/index.ts +0 -7
  260. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  261. package/src/themes/threads/index.ts +0 -100
  262. package/src/themes/threads/style.css +0 -336
  263. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  264. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  265. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  266. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  267. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  268. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  269. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  270. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  271. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  272. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  273. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  274. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  275. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  276. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  277. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Account settings: profile + password change forms
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+ import { SettingsNav } from "./SettingsNav.js";
7
+
8
+ export function AccountContent({ userName }: { userName: string }) {
9
+ const { t } = useLingui();
10
+
11
+ const profileSignals = JSON.stringify({ userName }).replace(/</g, "\\u003c");
12
+
13
+ return (
14
+ <>
15
+ <h1 class="text-2xl font-semibold mb-2">
16
+ {t({ message: "Settings", comment: "@context: Dashboard heading" })}
17
+ </h1>
18
+ <SettingsNav currentTab="account" />
19
+
20
+ <div class="flex flex-col gap-6 max-w-lg">
21
+ <form
22
+ data-signals={profileSignals}
23
+ data-on:submit__prevent="@post('/dash/settings/account')"
24
+ data-indicator="_profileLoading"
25
+ >
26
+ <div class="card">
27
+ <header>
28
+ <h2>
29
+ {t({
30
+ message: "Profile",
31
+ comment: "@context: Account settings section heading",
32
+ })}
33
+ </h2>
34
+ </header>
35
+ <section class="flex flex-col gap-4">
36
+ <div class="field">
37
+ <label class="label">
38
+ {t({
39
+ message: "Name",
40
+ comment: "@context: Account settings form field",
41
+ })}
42
+ </label>
43
+ <input
44
+ type="text"
45
+ data-bind="userName"
46
+ class="input"
47
+ required
48
+ />
49
+ </div>
50
+ </section>
51
+ </div>
52
+
53
+ <button
54
+ type="submit"
55
+ class="btn mt-4"
56
+ data-attr:disabled="$_profileLoading"
57
+ >
58
+ <svg
59
+ data-show="$_profileLoading"
60
+ style="display:none"
61
+ class="animate-spin size-4"
62
+ xmlns="http://www.w3.org/2000/svg"
63
+ viewBox="0 0 24 24"
64
+ fill="none"
65
+ stroke="currentColor"
66
+ stroke-width="2"
67
+ stroke-linecap="round"
68
+ stroke-linejoin="round"
69
+ role="status"
70
+ >
71
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
72
+ </svg>
73
+ {t({
74
+ message: "Save Profile",
75
+ comment: "@context: Button to save profile",
76
+ })}
77
+ </button>
78
+ </form>
79
+
80
+ <form
81
+ data-signals="{currentPassword: '', newPassword: '', confirmPassword: ''}"
82
+ data-on:submit__prevent="@post('/dash/settings/password')"
83
+ data-indicator="_passwordLoading"
84
+ >
85
+ <div class="card">
86
+ <header>
87
+ <h2>
88
+ {t({
89
+ message: "Change Password",
90
+ comment: "@context: Settings section heading",
91
+ })}
92
+ </h2>
93
+ </header>
94
+ <section class="flex flex-col gap-4">
95
+ <div class="field">
96
+ <label class="label">
97
+ {t({
98
+ message: "Current Password",
99
+ comment: "@context: Password form field",
100
+ })}
101
+ </label>
102
+ <input
103
+ type="password"
104
+ data-bind="currentPassword"
105
+ class="input"
106
+ required
107
+ autocomplete="current-password"
108
+ />
109
+ </div>
110
+
111
+ <div class="field">
112
+ <label class="label">
113
+ {t({
114
+ message: "New Password",
115
+ comment: "@context: Password form field",
116
+ })}
117
+ </label>
118
+ <input
119
+ type="password"
120
+ data-bind="newPassword"
121
+ class="input"
122
+ required
123
+ minlength={8}
124
+ autocomplete="new-password"
125
+ />
126
+ </div>
127
+
128
+ <div class="field">
129
+ <label class="label">
130
+ {t({
131
+ message: "Confirm New Password",
132
+ comment: "@context: Password form field",
133
+ })}
134
+ </label>
135
+ <input
136
+ type="password"
137
+ data-bind="confirmPassword"
138
+ class="input"
139
+ required
140
+ minlength={8}
141
+ autocomplete="new-password"
142
+ />
143
+ </div>
144
+ </section>
145
+ </div>
146
+
147
+ <button
148
+ type="submit"
149
+ class="btn mt-4"
150
+ data-attr:disabled="$_passwordLoading"
151
+ >
152
+ <svg
153
+ data-show="$_passwordLoading"
154
+ style="display:none"
155
+ class="animate-spin size-4"
156
+ xmlns="http://www.w3.org/2000/svg"
157
+ viewBox="0 0 24 24"
158
+ fill="none"
159
+ stroke="currentColor"
160
+ stroke-width="2"
161
+ stroke-linecap="round"
162
+ stroke-linejoin="round"
163
+ role="status"
164
+ >
165
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
166
+ </svg>
167
+ {t({
168
+ message: "Change Password",
169
+ comment: "@context: Button to change password",
170
+ })}
171
+ </button>
172
+ </form>
173
+ </div>
174
+ </>
175
+ );
176
+ }
@@ -0,0 +1,254 @@
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
+ }