@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,533 @@
1
+ /**
2
+ * General settings form
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+ import type { TimezoneEntry } from "../../../lib/timezones.js";
7
+ import { SettingsNav } from "./SettingsNav.js";
8
+
9
+ /**
10
+ * Build data-signals JSON with `_orig_<key>` duplicates for cancel/reset.
11
+ * Private `_orig_*` signals store original values so Cancel can revert.
12
+ * The `dirty` signal tracks whether the user has made any changes.
13
+ */
14
+ function buildSignals(fields: Record<string, string>, dirty: string): string {
15
+ const signals: Record<string, string | boolean> = {};
16
+ for (const [key, value] of Object.entries(fields)) {
17
+ signals[key] = value;
18
+ signals[`_orig_${key}`] = value;
19
+ }
20
+ signals[dirty] = false;
21
+ return JSON.stringify(signals).replace(/</g, "\\u003c");
22
+ }
23
+
24
+ /** Spinner SVG shown inside buttons during loading */
25
+ function Spinner({ show }: { show: string }) {
26
+ return (
27
+ <svg
28
+ data-show={show}
29
+ style="display:none"
30
+ class="animate-spin size-4"
31
+ xmlns="http://www.w3.org/2000/svg"
32
+ viewBox="0 0 24 24"
33
+ fill="none"
34
+ stroke="currentColor"
35
+ stroke-width="2"
36
+ stroke-linecap="round"
37
+ stroke-linejoin="round"
38
+ role="status"
39
+ >
40
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
41
+ </svg>
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Save + Cancel button pair.
47
+ * Both are disabled when no changes (`!dirty`) or during loading.
48
+ * Cancel resets all signals to originals and clears dirty.
49
+ */
50
+ function FormActions({
51
+ indicator,
52
+ dirty,
53
+ fields,
54
+ }: {
55
+ indicator: string;
56
+ dirty: string;
57
+ fields: string[];
58
+ }) {
59
+ const { t } = useLingui();
60
+ const resetExpr = [
61
+ ...fields.map((f) => `$${f} = $_orig_${f}`),
62
+ `$${dirty} = false`,
63
+ ].join("; ");
64
+
65
+ return (
66
+ <div class="flex gap-2 mt-4">
67
+ <button
68
+ type="submit"
69
+ class="btn"
70
+ disabled
71
+ data-attr:disabled={`$${indicator} || !$${dirty}`}
72
+ >
73
+ <Spinner show={`$${indicator}`} />
74
+ {t({
75
+ message: "Save",
76
+ comment: "@context: Button to save settings",
77
+ })}
78
+ </button>
79
+ <button
80
+ type="button"
81
+ class="btn-outline"
82
+ disabled
83
+ data-attr:disabled={`$${indicator} || !$${dirty}`}
84
+ data-on:click={resetExpr}
85
+ >
86
+ {t({
87
+ message: "Cancel",
88
+ comment:
89
+ "@context: Button to cancel unsaved changes and revert to original values",
90
+ })}
91
+ </button>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ export function GeneralContent({
97
+ siteName,
98
+ siteDescription,
99
+ siteLanguage,
100
+ homeDefaultView,
101
+ siteNameFallback,
102
+ siteDescriptionFallback,
103
+ siteAvatarUrl,
104
+ showHeaderAvatar,
105
+ timeZone,
106
+ siteFooter,
107
+ noindex,
108
+ timezones,
109
+ }: {
110
+ siteName: string;
111
+ siteDescription: string;
112
+ siteLanguage: string;
113
+ homeDefaultView: string;
114
+ siteNameFallback: string;
115
+ siteDescriptionFallback: string;
116
+ siteAvatarUrl: string;
117
+ showHeaderAvatar: boolean;
118
+ timeZone: string;
119
+ siteFooter: string;
120
+ noindex: boolean;
121
+ timezones: TimezoneEntry[];
122
+ }) {
123
+ const { t } = useLingui();
124
+
125
+ const generalSignals = buildSignals(
126
+ {
127
+ siteName,
128
+ siteDescription,
129
+ siteLanguage,
130
+ homeDefaultView,
131
+ timeZone,
132
+ },
133
+ "_generalDirty",
134
+ );
135
+
136
+ const footerSignals = buildSignals({ siteFooter }, "_footerDirty");
137
+
138
+ const seoSignals = buildSignals(
139
+ { noindex: noindex ? "" : "true" },
140
+ "_seoDirty",
141
+ );
142
+
143
+ const avatarSignals = buildSignals(
144
+ { showHeaderAvatar: showHeaderAvatar ? "true" : "" },
145
+ "_avatarDisplayDirty",
146
+ );
147
+
148
+ return (
149
+ <>
150
+ <h1 class="text-2xl font-semibold mb-2">
151
+ {t({ message: "Settings", comment: "@context: Dashboard heading" })}
152
+ </h1>
153
+ <SettingsNav currentTab="general" />
154
+
155
+ <div class="flex flex-col gap-6 max-w-lg">
156
+ {/* Blog Avatar */}
157
+ <div class="card">
158
+ <header>
159
+ <h2>
160
+ {t({
161
+ message: "Blog Avatar",
162
+ comment: "@context: Settings section heading for avatar",
163
+ })}
164
+ </h2>
165
+ </header>
166
+ <section class="flex flex-col gap-4">
167
+ <div class="flex items-center gap-4">
168
+ {siteAvatarUrl ? (
169
+ <img
170
+ src={siteAvatarUrl}
171
+ alt=""
172
+ class="rounded-full object-cover"
173
+ style="width:64px;height:64px"
174
+ />
175
+ ) : (
176
+ <div
177
+ class="rounded-full bg-muted flex items-center justify-center text-muted-foreground"
178
+ style="width:64px;height:64px"
179
+ >
180
+ <svg
181
+ xmlns="http://www.w3.org/2000/svg"
182
+ width="24"
183
+ height="24"
184
+ viewBox="0 0 24 24"
185
+ fill="none"
186
+ stroke="currentColor"
187
+ stroke-width="2"
188
+ stroke-linecap="round"
189
+ stroke-linejoin="round"
190
+ >
191
+ <rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
192
+ <circle cx="9" cy="9" r="2" />
193
+ <path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
194
+ </svg>
195
+ </div>
196
+ )}
197
+ <div class="flex flex-col gap-2">
198
+ <form
199
+ action="/dash/settings/avatar"
200
+ method="post"
201
+ enctype="multipart/form-data"
202
+ class="inline"
203
+ >
204
+ <label class="btn text-sm cursor-pointer">
205
+ {t({
206
+ message: "Upload Avatar",
207
+ comment: "@context: Button to upload avatar image",
208
+ })}
209
+ <input
210
+ type="file"
211
+ name="file"
212
+ accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml"
213
+ class="hidden"
214
+ data-avatar-upload
215
+ data-text-processing={t({
216
+ message: "Processing...",
217
+ comment:
218
+ "@context: Avatar upload button text while generating favicon variants",
219
+ })}
220
+ data-text-uploading={t({
221
+ message: "Uploading...",
222
+ comment:
223
+ "@context: Avatar upload button text while uploading",
224
+ })}
225
+ data-text-error={t({
226
+ message: "Upload failed. Please try again.",
227
+ comment:
228
+ "@context: Error message when avatar upload fails",
229
+ })}
230
+ />
231
+ </label>
232
+ </form>
233
+ {siteAvatarUrl && (
234
+ <form
235
+ data-on:submit__prevent="@post('/dash/settings/avatar/remove')"
236
+ data-indicator="_removeAvatarLoading"
237
+ >
238
+ <button
239
+ type="submit"
240
+ class="btn-outline text-sm"
241
+ data-attr:disabled="$_removeAvatarLoading"
242
+ >
243
+ {t({
244
+ message: "Remove",
245
+ comment: "@context: Button to remove the blog avatar",
246
+ })}
247
+ </button>
248
+ </form>
249
+ )}
250
+ </div>
251
+ </div>
252
+ <p class="text-sm text-muted-foreground">
253
+ {t({
254
+ message:
255
+ "This is used for your favicon and apple-touch-icon. For best results, upload a square image at least 180x180 pixels.",
256
+ comment: "@context: Help text for avatar upload",
257
+ })}
258
+ </p>
259
+ <form
260
+ data-signals={avatarSignals}
261
+ data-on:submit__prevent="@post('/dash/settings/avatar/display')"
262
+ data-indicator="_avatarDisplayLoading"
263
+ >
264
+ <label class="flex items-center gap-2 cursor-pointer">
265
+ <input
266
+ type="checkbox"
267
+ class="checkbox"
268
+ data-bind="showHeaderAvatar"
269
+ data-on:change="$_avatarDisplayDirty = true"
270
+ checked={showHeaderAvatar || undefined}
271
+ value="true"
272
+ />
273
+ <span>
274
+ {t({
275
+ message: "Display avatar in my site header",
276
+ comment:
277
+ "@context: Checkbox to show avatar in the site header",
278
+ })}
279
+ </span>
280
+ </label>
281
+ <FormActions
282
+ indicator="_avatarDisplayLoading"
283
+ dirty="_avatarDisplayDirty"
284
+ fields={["showHeaderAvatar"]}
285
+ />
286
+ </form>
287
+ </section>
288
+ </div>
289
+
290
+ {/* General settings */}
291
+ <form
292
+ data-signals={generalSignals}
293
+ data-on:submit__prevent="@post('/dash/settings')"
294
+ data-indicator="_generalLoading"
295
+ >
296
+ <div class="card">
297
+ <header>
298
+ <h2>
299
+ {t({
300
+ message: "General",
301
+ comment: "@context: Settings section heading",
302
+ })}
303
+ </h2>
304
+ </header>
305
+ <section class="flex flex-col gap-4">
306
+ <div class="field">
307
+ <label class="label">
308
+ {t({
309
+ message: "Site Name",
310
+ comment: "@context: Settings form field",
311
+ })}
312
+ </label>
313
+ <input
314
+ type="text"
315
+ data-bind="siteName"
316
+ data-on:input="$_generalDirty = true"
317
+ class="input"
318
+ placeholder={siteNameFallback}
319
+ />
320
+ </div>
321
+
322
+ <div class="field">
323
+ <label class="label">
324
+ {t({
325
+ message: "About this blog",
326
+ comment:
327
+ "@context: Settings form field for site description",
328
+ })}
329
+ </label>
330
+ <textarea
331
+ data-bind="siteDescription"
332
+ data-on:input="$_generalDirty = true"
333
+ class="textarea"
334
+ rows={3}
335
+ placeholder={siteDescriptionFallback}
336
+ >
337
+ {siteDescription}
338
+ </textarea>
339
+ <p class="text-sm text-muted-foreground mt-1">
340
+ {t({
341
+ message:
342
+ "This is displayed above your blog posts on your default home page. This is also used for the meta description on your home page.",
343
+ comment: "@context: Help text for site description field",
344
+ })}
345
+ </p>
346
+ </div>
347
+
348
+ <div class="field">
349
+ <label class="label">
350
+ {t({
351
+ message: "Language",
352
+ comment: "@context: Settings form field",
353
+ })}
354
+ </label>
355
+ <select
356
+ data-bind="siteLanguage"
357
+ data-on:change="$_generalDirty = true"
358
+ class="select"
359
+ >
360
+ <option value="en" selected={siteLanguage === "en"}>
361
+ English
362
+ </option>
363
+ <option value="zh-Hans" selected={siteLanguage === "zh-Hans"}>
364
+ 简体中文
365
+ </option>
366
+ <option value="zh-Hant" selected={siteLanguage === "zh-Hant"}>
367
+ 繁體中文
368
+ </option>
369
+ </select>
370
+ </div>
371
+
372
+ <div class="field">
373
+ <label class="label">
374
+ {t({
375
+ message: "Default Homepage View",
376
+ comment: "@context: Settings form field",
377
+ })}
378
+ </label>
379
+ <select
380
+ data-bind="homeDefaultView"
381
+ data-on:change="$_generalDirty = true"
382
+ class="select"
383
+ >
384
+ <option
385
+ value="latest"
386
+ selected={homeDefaultView === "latest"}
387
+ >
388
+ {t({
389
+ message: "Latest",
390
+ comment:
391
+ "@context: Homepage view option - show latest posts",
392
+ })}
393
+ </option>
394
+ <option
395
+ value="featured"
396
+ selected={homeDefaultView === "featured"}
397
+ >
398
+ {t({
399
+ message: "Featured",
400
+ comment:
401
+ "@context: Homepage view option - show featured posts",
402
+ })}
403
+ </option>
404
+ </select>
405
+ </div>
406
+
407
+ <div class="field">
408
+ <label class="label">
409
+ {t({
410
+ message: "Time Zone",
411
+ comment: "@context: Settings form field",
412
+ })}
413
+ </label>
414
+ <select
415
+ data-bind="timeZone"
416
+ data-on:change="$_generalDirty = true"
417
+ class="select"
418
+ >
419
+ {timezones.map((tz) => (
420
+ <option
421
+ key={tz.value}
422
+ value={tz.value}
423
+ selected={timeZone === tz.value}
424
+ >
425
+ {tz.label}
426
+ </option>
427
+ ))}
428
+ </select>
429
+ </div>
430
+ <FormActions
431
+ indicator="_generalLoading"
432
+ dirty="_generalDirty"
433
+ fields={[
434
+ "siteName",
435
+ "siteDescription",
436
+ "siteLanguage",
437
+ "homeDefaultView",
438
+ "timeZone",
439
+ ]}
440
+ />
441
+ </section>
442
+ </div>
443
+ </form>
444
+
445
+ {/* Site Footer */}
446
+ <form
447
+ data-signals={footerSignals}
448
+ data-on:submit__prevent="@post('/dash/settings/footer')"
449
+ data-indicator="_footerLoading"
450
+ >
451
+ <div class="card">
452
+ <header>
453
+ <h2>
454
+ {t({
455
+ message: "Site Footer",
456
+ comment: "@context: Settings section heading for site footer",
457
+ })}
458
+ </h2>
459
+ </header>
460
+ <section class="flex flex-col gap-4">
461
+ <textarea
462
+ data-bind="siteFooter"
463
+ data-on:input="$_footerDirty = true"
464
+ class="textarea font-mono text-sm"
465
+ rows={4}
466
+ placeholder={t({
467
+ message: "Markdown supported",
468
+ comment: "@context: Placeholder for footer textarea",
469
+ })}
470
+ >
471
+ {siteFooter}
472
+ </textarea>
473
+ <p class="text-sm text-muted-foreground">
474
+ {t({
475
+ message:
476
+ "This is displayed at the bottom of all of your posts and pages. Markdown is supported.",
477
+ comment: "@context: Help text for site footer field",
478
+ })}
479
+ </p>
480
+ <FormActions
481
+ indicator="_footerLoading"
482
+ dirty="_footerDirty"
483
+ fields={["siteFooter"]}
484
+ />
485
+ </section>
486
+ </div>
487
+ </form>
488
+
489
+ {/* SEO */}
490
+ <form
491
+ data-signals={seoSignals}
492
+ data-on:submit__prevent="@post('/dash/settings/seo')"
493
+ data-indicator="_seoLoading"
494
+ >
495
+ <div class="card">
496
+ <header>
497
+ <h2>
498
+ {t({
499
+ message: "SEO",
500
+ comment: "@context: Settings section heading for SEO",
501
+ })}
502
+ </h2>
503
+ </header>
504
+ <section>
505
+ <label class="flex items-center gap-2 cursor-pointer">
506
+ <input
507
+ type="checkbox"
508
+ class="checkbox"
509
+ data-bind="noindex"
510
+ data-on:change="$_seoDirty = true"
511
+ checked={!noindex || undefined}
512
+ value="true"
513
+ />
514
+ <span>
515
+ {t({
516
+ message: "It's OK for search engines to index my site",
517
+ comment:
518
+ "@context: Checkbox for allowing search engine indexing",
519
+ })}
520
+ </span>
521
+ </label>
522
+ <FormActions
523
+ indicator="_seoLoading"
524
+ dirty="_seoDirty"
525
+ fields={["noindex"]}
526
+ />
527
+ </section>
528
+ </div>
529
+ </form>
530
+ </div>
531
+ </>
532
+ );
533
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Settings sub-navigation tabs
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+
7
+ export type SettingsTab = "general" | "appearance" | "account";
8
+
9
+ export function SettingsNav({ currentTab }: { currentTab: SettingsTab }) {
10
+ const { t } = useLingui();
11
+
12
+ const tabs: { id: SettingsTab; label: string; href: string }[] = [
13
+ {
14
+ id: "general",
15
+ label: t({
16
+ message: "General",
17
+ comment: "@context: Settings sub-navigation tab",
18
+ }),
19
+ href: "/dash/settings",
20
+ },
21
+ {
22
+ id: "appearance",
23
+ label: t({
24
+ message: "Appearance",
25
+ comment: "@context: Settings sub-navigation tab",
26
+ }),
27
+ href: "/dash/settings/appearance",
28
+ },
29
+ {
30
+ id: "account",
31
+ label: t({
32
+ message: "Account",
33
+ comment: "@context: Settings sub-navigation tab",
34
+ }),
35
+ href: "/dash/settings/account",
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
+ }
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Threads Theme - Link Card
2
+ * Link Card
3
3
  *
4
4
  * Compact link preview box — date is shown at the feed level as a group header.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
- import type { TimelineCardProps } from "../../../types.js";
8
+ import type { TimelineCardProps } from "../../types.js";
9
9
 
10
10
  export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
11
11
  // Extract domain from URL for display
@@ -19,7 +19,11 @@ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
19
19
  }
20
20
 
21
21
  return (
22
- <article class={`h-entry${compact ? " threads-compact" : ""}`}>
22
+ <article
23
+ class={`h-entry${compact ? " feed-compact" : ""}`}
24
+ data-post
25
+ data-format="link"
26
+ >
23
27
  {domain && (
24
28
  <div class="text-xs text-muted-foreground mb-1 flex items-center gap-1">
25
29
  <svg
@@ -52,10 +56,11 @@ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
52
56
  {!compact && post.bodyHtml && (
53
57
  <div
54
58
  class="e-content prose text-muted-foreground"
59
+ data-post-body
55
60
  dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
56
61
  />
57
62
  )}
58
- <footer class="mt-2 text-xs text-muted-foreground">
63
+ <footer class="mt-2 text-xs text-muted-foreground" data-post-meta>
59
64
  <a href={post.permalink} class="hover:underline">
60
65
  <time class="dt-published" datetime={post.publishedAt}>
61
66
  {post.publishedAtFormatted}
@@ -1,20 +1,24 @@
1
1
  /**
2
- * Threads Theme - Note Card
2
+ * Note Card
3
3
  *
4
- * Without title: plain text note date is shown at the feed level as a group header.
4
+ * Without title: plain text note with full date in footer.
5
5
  * With title: article-style rendering with summary excerpt and "Read more" link.
6
6
  */
7
7
 
8
8
  import type { FC } from "hono/jsx";
9
- import type { TimelineCardProps } from "../../../types.js";
10
- import { MediaGallery } from "../../../theme/index.js";
9
+ import type { TimelineCardProps } from "../../types.js";
10
+ import { MediaGallery } from "../shared/MediaGallery.js";
11
11
 
12
12
  export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
13
13
  const isArticle = !!post.title;
14
14
  const displayHtml = isArticle ? post.summaryHtml : post.bodyHtml;
15
15
 
16
16
  return (
17
- <article class={`h-entry${compact ? " threads-compact" : ""}`}>
17
+ <article
18
+ class={`h-entry${compact ? " feed-compact" : ""}`}
19
+ data-post
20
+ data-format="note"
21
+ >
18
22
  {isArticle && (
19
23
  <h2
20
24
  class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
@@ -27,11 +31,12 @@ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
27
31
  {displayHtml && (
28
32
  <div
29
33
  class={`e-content prose ${compact ? "prose-sm" : isArticle ? "text-muted-foreground" : ""}`}
34
+ data-post-body
30
35
  dangerouslySetInnerHTML={{ __html: displayHtml }}
31
36
  />
32
37
  )}
33
38
  {!compact && post.media.length > 0 && (
34
- <div class="threads-media mt-3">
39
+ <div class="mt-3" data-post-media>
35
40
  <MediaGallery attachments={post.media} />
36
41
  </div>
37
42
  )}
@@ -43,13 +48,13 @@ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
43
48
  Read more →
44
49
  </a>
45
50
  )}
46
- <footer class="mt-2">
51
+ <footer class="mt-2" data-post-meta>
47
52
  <a
48
53
  href={post.permalink}
49
54
  class="u-url text-xs text-muted-foreground hover:underline"
50
55
  >
51
56
  <time class="dt-published" datetime={post.publishedAt}>
52
- {post.publishedAtRelative}
57
+ {post.publishedAtFormatted}
53
58
  </time>
54
59
  </a>
55
60
  </footer>