@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,34 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { BUILTIN_FONT_THEMES } from "../font-themes.js";
3
+
4
+ describe("BUILTIN_FONT_THEMES", () => {
5
+ it("contains 4 themes", () => {
6
+ expect(BUILTIN_FONT_THEMES).toHaveLength(4);
7
+ });
8
+
9
+ it("has 'default' as the first theme", () => {
10
+ expect(BUILTIN_FONT_THEMES[0].id).toBe("default");
11
+ });
12
+
13
+ it("each theme has required fields", () => {
14
+ for (const theme of BUILTIN_FONT_THEMES) {
15
+ expect(theme.id).toBeTruthy();
16
+ expect(theme.name).toBeTruthy();
17
+ expect(theme.fontFamily).toBeTruthy();
18
+ expect(theme.description).toBeTruthy();
19
+ }
20
+ });
21
+
22
+ it("has no duplicate IDs", () => {
23
+ const ids = BUILTIN_FONT_THEMES.map((t) => t.id);
24
+ expect(new Set(ids).size).toBe(ids.length);
25
+ });
26
+
27
+ it("includes expected theme IDs", () => {
28
+ const ids = BUILTIN_FONT_THEMES.map((t) => t.id);
29
+ expect(ids).toContain("default");
30
+ expect(ids).toContain("serif");
31
+ expect(ids).toContain("humanist");
32
+ expect(ids).toContain("mono");
33
+ });
34
+ });
@@ -118,9 +118,40 @@ function defineTheme(opts: {
118
118
  }
119
119
 
120
120
  export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
121
+ defineTheme({
122
+ id: "halloween",
123
+ name: "Halloween",
124
+ preview: {
125
+ lightBg: "#f9f2e3",
126
+ lightText: "#352200",
127
+ lightLink: "#b84400",
128
+ darkBg: "#1e1000",
129
+ darkText: "#dfc390",
130
+ darkLink: "#ff8c00",
131
+ },
132
+ light: {
133
+ bg: "oklch(0.97 0.015 75)",
134
+ fg: "oklch(0.25 0.04 55)",
135
+ primary: "oklch(0.47 0.17 50)",
136
+ primaryFg: "oklch(0.98 0.01 75)",
137
+ muted: "oklch(0.93 0.02 75)",
138
+ mutedFg: "oklch(0.5 0.025 55)",
139
+ border: "oklch(0.88 0.025 75)",
140
+ },
141
+ dark: {
142
+ bg: "oklch(0.16 0.03 50)",
143
+ fg: "oklch(0.85 0.025 75)",
144
+ primary: "oklch(0.72 0.19 55)",
145
+ primaryFg: "oklch(0.14 0.03 50)",
146
+ muted: "oklch(0.22 0.025 50)",
147
+ mutedFg: "oklch(0.62 0.02 75)",
148
+ border: "oklch(0.28 0.025 50)",
149
+ },
150
+ }),
151
+
121
152
  {
122
153
  id: "default",
123
- name: "Default",
154
+ name: "Panda",
124
155
  light: {},
125
156
  dark: {},
126
157
  preview: {
@@ -226,37 +257,6 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
226
257
  },
227
258
  }),
228
259
 
229
- defineTheme({
230
- id: "halloween",
231
- name: "Halloween",
232
- preview: {
233
- lightBg: "#f9f2e3",
234
- lightText: "#352200",
235
- lightLink: "#cc5500",
236
- darkBg: "#1e1000",
237
- darkText: "#dfc390",
238
- darkLink: "#ff8c00",
239
- },
240
- light: {
241
- bg: "oklch(0.97 0.015 75)",
242
- fg: "oklch(0.25 0.04 55)",
243
- primary: "oklch(0.6 0.2 50)",
244
- primaryFg: "oklch(0.98 0.01 75)",
245
- muted: "oklch(0.93 0.02 75)",
246
- mutedFg: "oklch(0.5 0.025 55)",
247
- border: "oklch(0.88 0.025 75)",
248
- },
249
- dark: {
250
- bg: "oklch(0.16 0.03 50)",
251
- fg: "oklch(0.85 0.025 75)",
252
- primary: "oklch(0.72 0.19 55)",
253
- primaryFg: "oklch(0.14 0.03 50)",
254
- muted: "oklch(0.22 0.025 50)",
255
- mutedFg: "oklch(0.62 0.02 75)",
256
- border: "oklch(0.28 0.025 50)",
257
- },
258
- }),
259
-
260
260
  defineTheme({
261
261
  id: "notepad",
262
262
  name: "Notepad",
@@ -294,7 +294,7 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
294
294
  preview: {
295
295
  lightBg: "#f7eef5",
296
296
  lightText: "#2e1e2c",
297
- lightLink: "#9845c8",
297
+ lightLink: "#7a30a8",
298
298
  darkBg: "#1d1428",
299
299
  darkText: "#d4c2d0",
300
300
  darkLink: "#c080fc",
@@ -302,7 +302,7 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
302
302
  light: {
303
303
  bg: "oklch(0.97 0.012 325)",
304
304
  fg: "oklch(0.25 0.02 310)",
305
- primary: "oklch(0.55 0.2 300)",
305
+ primary: "oklch(0.45 0.2 300)",
306
306
  primaryFg: "oklch(0.98 0.008 325)",
307
307
  muted: "oklch(0.93 0.016 325)",
308
308
  mutedFg: "oklch(0.52 0.015 310)",
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Compose Dialog
3
+ *
4
+ * Full-screen compose dialog for quick post creation.
5
+ * Rendered server-side as part of SiteLayout for authenticated users.
6
+ */
7
+
8
+ import type { FC } from "hono/jsx";
9
+ import type { Collection } from "../../types.js";
10
+ import { useLingui } from "@lingui/react/macro";
11
+
12
+ export interface ComposeDialogProps {
13
+ collections?: Collection[];
14
+ }
15
+
16
+ export const ComposeDialog: FC<ComposeDialogProps> = ({ collections }) => {
17
+ const { t } = useLingui();
18
+
19
+ const signals = JSON.stringify({
20
+ format: "note",
21
+ title: "",
22
+ body: "",
23
+ url: "",
24
+ quoteText: "",
25
+ status: "published",
26
+ featured: false,
27
+ pinned: false,
28
+ rating: 0,
29
+ collectionId: 0,
30
+ mediaIds: [],
31
+ _composeLoading: false,
32
+ _showRating: false,
33
+ _showCollection: false,
34
+ }).replace(/</g, "\\u003c");
35
+
36
+ return (
37
+ <dialog
38
+ id="compose-dialog"
39
+ class="compose-dialog backdrop:bg-black/50"
40
+ onclick="event.target === this && this.close()"
41
+ >
42
+ <div class="compose-dialog-inner">
43
+ {/* Header */}
44
+ <header class="compose-dialog-header">
45
+ <button
46
+ type="button"
47
+ class="compose-dialog-close"
48
+ onclick="this.closest('dialog').close()"
49
+ >
50
+ <svg
51
+ xmlns="http://www.w3.org/2000/svg"
52
+ width="20"
53
+ height="20"
54
+ viewBox="0 0 24 24"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ stroke-width="2"
58
+ stroke-linecap="round"
59
+ stroke-linejoin="round"
60
+ >
61
+ <path d="M18 6 6 18" />
62
+ <path d="M6 6l12 12" />
63
+ </svg>
64
+ </button>
65
+ <h2 class="compose-dialog-title">
66
+ {t({
67
+ message: "New Post",
68
+ comment: "@context: Compose dialog title",
69
+ })}
70
+ </h2>
71
+ <div class="w-5" />
72
+ </header>
73
+
74
+ {/* Form */}
75
+ <section class="compose-dialog-body">
76
+ <form
77
+ data-signals={signals}
78
+ data-on:submit__prevent="@post('/compose')"
79
+ data-indicator="_composeLoading"
80
+ class="flex flex-col gap-3"
81
+ >
82
+ {/* Format tabs */}
83
+ <div class="compose-format-tabs">
84
+ <button
85
+ type="button"
86
+ class="compose-format-tab"
87
+ data-class-compose-format-tab-active="$format === 'note'"
88
+ data-on:click="$format = 'note'"
89
+ >
90
+ {t({
91
+ message: "Note",
92
+ comment: "@context: Compose format tab",
93
+ })}
94
+ </button>
95
+ <button
96
+ type="button"
97
+ class="compose-format-tab"
98
+ data-class-compose-format-tab-active="$format === 'link'"
99
+ data-on:click="$format = 'link'"
100
+ >
101
+ {t({
102
+ message: "Link",
103
+ comment: "@context: Compose format tab",
104
+ })}
105
+ </button>
106
+ <button
107
+ type="button"
108
+ class="compose-format-tab"
109
+ data-class-compose-format-tab-active="$format === 'quote'"
110
+ data-on:click="$format = 'quote'"
111
+ >
112
+ {t({
113
+ message: "Quote",
114
+ comment: "@context: Compose format tab",
115
+ })}
116
+ </button>
117
+ </div>
118
+
119
+ {/* Title input */}
120
+ <input
121
+ type="text"
122
+ data-bind="title"
123
+ class="compose-title-input"
124
+ placeholder={t({
125
+ message: "Title (optional)",
126
+ comment: "@context: Compose title placeholder",
127
+ })}
128
+ />
129
+
130
+ {/* Body textarea */}
131
+ <textarea
132
+ data-bind="body"
133
+ class="compose-body-input"
134
+ placeholder={t({
135
+ message: "What's on your mind?",
136
+ comment: "@context: Compose body placeholder",
137
+ })}
138
+ rows={4}
139
+ />
140
+
141
+ {/* URL input (link/quote) */}
142
+ <div data-show="$format === 'link' || $format === 'quote'">
143
+ <input
144
+ type="url"
145
+ data-bind="url"
146
+ class="input text-sm"
147
+ placeholder="https://..."
148
+ />
149
+ </div>
150
+
151
+ {/* Quote text (quote format) */}
152
+ <div data-show="$format === 'quote'">
153
+ <textarea
154
+ data-bind="quoteText"
155
+ class="textarea text-sm"
156
+ placeholder={t({
157
+ message: "The text being quoted...",
158
+ comment: "@context: Compose quote text placeholder",
159
+ })}
160
+ rows={2}
161
+ />
162
+ </div>
163
+
164
+ {/* Rating picker (toggleable) */}
165
+ <div data-show="$_showRating" class="field">
166
+ <label class="label text-sm">
167
+ {t({
168
+ message: "Rating",
169
+ comment: "@context: Compose rating field",
170
+ })}
171
+ </label>
172
+ <select data-bind="rating" class="select text-sm">
173
+ <option value="0">
174
+ {t({
175
+ message: "None",
176
+ comment: "@context: No rating selected",
177
+ })}
178
+ </option>
179
+ <option value="1">1</option>
180
+ <option value="2">2</option>
181
+ <option value="3">3</option>
182
+ <option value="4">4</option>
183
+ <option value="5">5</option>
184
+ </select>
185
+ </div>
186
+
187
+ {/* Collection picker (toggleable) */}
188
+ {collections && collections.length > 0 && (
189
+ <div data-show="$_showCollection" class="field">
190
+ <label class="label text-sm">
191
+ {t({
192
+ message: "Collection",
193
+ comment: "@context: Compose collection field",
194
+ })}
195
+ </label>
196
+ <select data-bind="collectionId" class="select text-sm">
197
+ <option value="0">
198
+ {t({
199
+ message: "None",
200
+ comment: "@context: No collection selected",
201
+ })}
202
+ </option>
203
+ {collections.map((col) => (
204
+ <option key={col.id} value={col.id}>
205
+ {col.title}
206
+ </option>
207
+ ))}
208
+ </select>
209
+ </div>
210
+ )}
211
+
212
+ {/* Toolbar */}
213
+ <div class="compose-toolbar">
214
+ <div class="flex gap-1">
215
+ {/* Media button */}
216
+ <button
217
+ type="button"
218
+ class="compose-toolbar-btn"
219
+ title={t({
220
+ message: "Add Media",
221
+ comment: "@context: Compose toolbar - add media",
222
+ })}
223
+ data-on:click="document.getElementById('compose-media-picker').showModal(); fetch('/dash/media/picker').then(r => r.text()).then(html => document.getElementById('compose-media-grid').innerHTML = html)"
224
+ >
225
+ <svg
226
+ xmlns="http://www.w3.org/2000/svg"
227
+ width="18"
228
+ height="18"
229
+ viewBox="0 0 24 24"
230
+ fill="none"
231
+ stroke="currentColor"
232
+ stroke-width="2"
233
+ stroke-linecap="round"
234
+ stroke-linejoin="round"
235
+ >
236
+ <rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
237
+ <circle cx="9" cy="9" r="2" />
238
+ <path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
239
+ </svg>
240
+ </button>
241
+
242
+ {/* Rating toggle */}
243
+ <button
244
+ type="button"
245
+ class="compose-toolbar-btn"
246
+ title={t({
247
+ message: "Rating",
248
+ comment: "@context: Compose toolbar - toggle rating",
249
+ })}
250
+ data-on:click="$_showRating = !$_showRating"
251
+ >
252
+ <svg
253
+ xmlns="http://www.w3.org/2000/svg"
254
+ width="18"
255
+ height="18"
256
+ viewBox="0 0 24 24"
257
+ fill="none"
258
+ stroke="currentColor"
259
+ stroke-width="2"
260
+ stroke-linecap="round"
261
+ stroke-linejoin="round"
262
+ >
263
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
264
+ </svg>
265
+ </button>
266
+
267
+ {/* Collection toggle */}
268
+ {collections && collections.length > 0 && (
269
+ <button
270
+ type="button"
271
+ class="compose-toolbar-btn"
272
+ title={t({
273
+ message: "Collection",
274
+ comment: "@context: Compose toolbar - toggle collection",
275
+ })}
276
+ data-on:click="$_showCollection = !$_showCollection"
277
+ >
278
+ <svg
279
+ xmlns="http://www.w3.org/2000/svg"
280
+ width="18"
281
+ height="18"
282
+ viewBox="0 0 24 24"
283
+ fill="none"
284
+ stroke="currentColor"
285
+ stroke-width="2"
286
+ stroke-linecap="round"
287
+ stroke-linejoin="round"
288
+ >
289
+ <path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
290
+ </svg>
291
+ </button>
292
+ )}
293
+ </div>
294
+ </div>
295
+
296
+ {/* Footer: checkboxes + submit */}
297
+ <div class="compose-dialog-footer">
298
+ <div class="flex gap-3">
299
+ <label class="flex items-center gap-1.5 text-xs text-muted-foreground">
300
+ <input
301
+ type="checkbox"
302
+ class="checkbox"
303
+ data-bind="featured"
304
+ />
305
+ {t({
306
+ message: "Featured",
307
+ comment: "@context: Compose checkbox - mark as featured",
308
+ })}
309
+ </label>
310
+ <label class="flex items-center gap-1.5 text-xs text-muted-foreground">
311
+ <input type="checkbox" class="checkbox" data-bind="pinned" />
312
+ {t({
313
+ message: "Pinned",
314
+ comment: "@context: Compose checkbox - pin to top",
315
+ })}
316
+ </label>
317
+ </div>
318
+ <div class="flex gap-2">
319
+ <button
320
+ type="button"
321
+ class="btn-outline text-sm"
322
+ data-attr:disabled="$_composeLoading"
323
+ data-on:click="$status = 'draft'; document.querySelector('#compose-dialog form').requestSubmit()"
324
+ >
325
+ <svg
326
+ data-show="$_composeLoading"
327
+ style="display:none"
328
+ class="animate-spin size-4"
329
+ xmlns="http://www.w3.org/2000/svg"
330
+ viewBox="0 0 24 24"
331
+ fill="none"
332
+ stroke="currentColor"
333
+ stroke-width="2"
334
+ stroke-linecap="round"
335
+ stroke-linejoin="round"
336
+ role="status"
337
+ >
338
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
339
+ </svg>
340
+ {t({
341
+ message: "Draft",
342
+ comment: "@context: Compose button - save as draft",
343
+ })}
344
+ </button>
345
+ <button
346
+ type="submit"
347
+ class="btn text-sm"
348
+ data-attr:disabled="$_composeLoading"
349
+ >
350
+ <svg
351
+ data-show="$_composeLoading"
352
+ style="display:none"
353
+ class="animate-spin size-4"
354
+ xmlns="http://www.w3.org/2000/svg"
355
+ viewBox="0 0 24 24"
356
+ fill="none"
357
+ stroke="currentColor"
358
+ stroke-width="2"
359
+ stroke-linecap="round"
360
+ stroke-linejoin="round"
361
+ role="status"
362
+ >
363
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
364
+ </svg>
365
+ {t({
366
+ message: "Post",
367
+ comment: "@context: Compose button - publish post",
368
+ })}
369
+ </button>
370
+ </div>
371
+ </div>
372
+ </form>
373
+ </section>
374
+ </div>
375
+
376
+ {/* Nested media picker dialog */}
377
+ <dialog
378
+ id="compose-media-picker"
379
+ class="p-6 rounded-lg max-w-2xl w-full backdrop:bg-black/50"
380
+ onclick="event.target === this && this.close()"
381
+ >
382
+ <div class="flex items-center justify-between mb-4">
383
+ <h2 class="text-lg font-semibold">
384
+ {t({
385
+ message: "Select Media",
386
+ comment: "@context: Media picker dialog title",
387
+ })}
388
+ </h2>
389
+ <button
390
+ type="button"
391
+ class="btn-outline text-sm"
392
+ onclick="this.closest('dialog').close()"
393
+ >
394
+ {t({
395
+ message: "Done",
396
+ comment: "@context: Close media picker button",
397
+ })}
398
+ </button>
399
+ </div>
400
+ <div
401
+ id="compose-media-grid"
402
+ class="grid grid-cols-4 gap-2 max-h-96 overflow-y-auto"
403
+ >
404
+ <p class="text-muted-foreground text-sm col-span-4">
405
+ {t({
406
+ message: "Loading...",
407
+ comment: "@context: Loading state for media picker",
408
+ })}
409
+ </p>
410
+ </div>
411
+ </dialog>
412
+ </dialog>
413
+ );
414
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Compose Prompt
3
+ *
4
+ * "What's new?" prompt bar at the top of the content area.
5
+ * Clicking it opens the compose dialog.
6
+ */
7
+
8
+ import type { FC } from "hono/jsx";
9
+ import { useLingui } from "@lingui/react/macro";
10
+
11
+ export const ComposePrompt: FC = () => {
12
+ const { t } = useLingui();
13
+
14
+ return (
15
+ <div class="compose-prompt">
16
+ <button
17
+ type="button"
18
+ class="compose-prompt-trigger"
19
+ onclick="document.getElementById('compose-dialog').showModal()"
20
+ >
21
+ <span class="compose-prompt-avatar">
22
+ <svg
23
+ xmlns="http://www.w3.org/2000/svg"
24
+ width="16"
25
+ height="16"
26
+ viewBox="0 0 24 24"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ stroke-width="2"
30
+ stroke-linecap="round"
31
+ stroke-linejoin="round"
32
+ >
33
+ <path d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 1 1 3.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
34
+ </svg>
35
+ </span>
36
+ <span class="compose-prompt-text">
37
+ {t({
38
+ message: "What's new?",
39
+ comment: "@context: Compose prompt placeholder text",
40
+ })}
41
+ </span>
42
+ </button>
43
+ <button
44
+ type="button"
45
+ class="compose-prompt-post-btn"
46
+ onclick="document.getElementById('compose-dialog').showModal()"
47
+ >
48
+ {t({
49
+ message: "Post",
50
+ comment: "@context: Compose prompt post button",
51
+ })}
52
+ </button>
53
+ </div>
54
+ );
55
+ };
@@ -2,18 +2,17 @@
2
2
  * Format Badge Component
3
3
  *
4
4
  * Displays a badge indicating the format of a post (note, link, quote).
5
- * Named TypeBadge for backward compatibility with theme overrides.
6
5
  */
7
6
 
8
7
  import type { FC } from "hono/jsx";
9
8
  import { useLingui } from "@lingui/react/macro";
10
9
  import type { Format } from "../../types.js";
11
10
 
12
- export interface TypeBadgeProps {
11
+ export interface FormatBadgeProps {
13
12
  type: Format;
14
13
  }
15
14
 
16
- export const TypeBadge: FC<TypeBadgeProps> = ({ type }) => {
15
+ export const FormatBadge: FC<FormatBadgeProps> = ({ type }) => {
17
16
  const { t } = useLingui();
18
17
 
19
18
  const labels: Record<Format, string> = {
@@ -147,25 +147,31 @@ export const PageForm: FC<PageFormProps> = ({
147
147
 
148
148
  {/* Submit */}
149
149
  <div class="flex gap-2">
150
- <button type="submit" class="btn" data-attr-disabled="$_loading">
151
- <span data-show="!$_loading">
152
- {isEdit
153
- ? t({
154
- message: "Update Page",
155
- comment: "@context: Button to update existing page",
156
- })
157
- : t({
158
- message: "Create Page",
159
- comment: "@context: Button to create new page",
160
- })}
161
- </span>
162
- <span data-show="$_loading">
163
- {t({
164
- message: "Processing...",
165
- comment:
166
- "@context: Loading text shown on submit button while request is in progress",
167
- })}
168
- </span>
150
+ <button type="submit" class="btn" data-attr:disabled="$_loading">
151
+ <svg
152
+ data-show="$_loading"
153
+ style="display:none"
154
+ class="animate-spin size-4"
155
+ xmlns="http://www.w3.org/2000/svg"
156
+ viewBox="0 0 24 24"
157
+ fill="none"
158
+ stroke="currentColor"
159
+ stroke-width="2"
160
+ stroke-linecap="round"
161
+ stroke-linejoin="round"
162
+ role="status"
163
+ >
164
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
165
+ </svg>
166
+ {isEdit
167
+ ? t({
168
+ message: "Update Page",
169
+ comment: "@context: Button to update existing page",
170
+ })
171
+ : t({
172
+ message: "Create Page",
173
+ comment: "@context: Button to create new page",
174
+ })}
169
175
  </button>
170
176
  <a href={cancelUrl} class="btn-outline">
171
177
  {t({