@jant/core 0.3.36 → 0.3.38

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 (271) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
@@ -15,7 +15,7 @@ export interface ColorTheme {
15
15
  name: string;
16
16
  /** CSS variable overrides for :root (light mode) */
17
17
  light: Record<string, string>;
18
- /** CSS variable overrides for .dark (dark mode) */
18
+ /** CSS variable overrides for dark mode (prefers-color-scheme: dark) */
19
19
  dark: Record<string, string>;
20
20
  /** Preview colors (hex) for theme picker cards */
21
21
  preview: {
@@ -28,32 +28,55 @@ export interface ColorTheme {
28
28
  };
29
29
  }
30
30
 
31
+ interface ThemeModeColors {
32
+ bg: string;
33
+ fg: string;
34
+ primary: string;
35
+ primaryFg: string;
36
+ muted: string;
37
+ mutedFg: string;
38
+ border: string;
39
+ /** Destructive action color (defaults to BaseCoat red) */
40
+ destructive?: string;
41
+ /** Success indicator color (defaults to preset green) */
42
+ success?: string;
43
+ /** Search highlight background */
44
+ searchMarkBg?: string;
45
+ /** Search highlight text color */
46
+ searchMarkColor?: string;
47
+ /** Admin dashboard background */
48
+ dashBg?: string;
49
+ }
50
+
51
+ /** Default destructive/success colors that harmonize with most themes */
52
+ const DEFAULTS = {
53
+ light: {
54
+ destructive: "oklch(0.577 0.245 27.325)",
55
+ success: "oklch(0.518 0.16 145.071)",
56
+ searchMarkBg: "oklch(0.92 0.14 90 / 0.55)",
57
+ searchMarkColor: "oklch(0.35 0.09 70)",
58
+ dashBg: "oklch(0.97 0.005 80)",
59
+ },
60
+ dark: {
61
+ destructive: "oklch(0.704 0.191 22.216)",
62
+ success: "oklch(0.627 0.194 149.214)",
63
+ searchMarkBg: "oklch(0.45 0.1 85 / 0.5)",
64
+ searchMarkColor: "oklch(0.92 0.08 90)",
65
+ dashBg: "oklch(0.2 0.005 80)",
66
+ },
67
+ };
68
+
31
69
  /**
32
70
  * Create a comprehensive color theme from key colors.
33
71
  * Derives card, popover, muted, secondary, accent, and sidebar variables.
72
+ * Also sets --destructive, --success, --search-mark-*, and --dash-bg.
34
73
  */
35
74
  function defineTheme(opts: {
36
75
  id: string;
37
76
  name: string;
38
77
  preview: ColorTheme["preview"];
39
- light: {
40
- bg: string;
41
- fg: string;
42
- primary: string;
43
- primaryFg: string;
44
- muted: string;
45
- mutedFg: string;
46
- border: string;
47
- };
48
- dark: {
49
- bg: string;
50
- fg: string;
51
- primary: string;
52
- primaryFg: string;
53
- muted: string;
54
- mutedFg: string;
55
- border: string;
56
- };
78
+ light: ThemeModeColors;
79
+ dark: ThemeModeColors;
57
80
  }): ColorTheme {
58
81
  const { light, dark } = opts;
59
82
  return {
@@ -75,6 +98,8 @@ function defineTheme(opts: {
75
98
  "--muted-foreground": light.mutedFg,
76
99
  "--accent": light.muted,
77
100
  "--accent-foreground": light.fg,
101
+ "--destructive": light.destructive ?? DEFAULTS.light.destructive,
102
+ "--success": light.success ?? DEFAULTS.light.success,
78
103
  "--border": light.border,
79
104
  "--input": light.border,
80
105
  "--ring": light.primary,
@@ -86,6 +111,10 @@ function defineTheme(opts: {
86
111
  "--sidebar-accent-foreground": light.fg,
87
112
  "--sidebar-border": light.border,
88
113
  "--sidebar-ring": light.primary,
114
+ "--search-mark-bg": light.searchMarkBg ?? DEFAULTS.light.searchMarkBg,
115
+ "--search-mark-color":
116
+ light.searchMarkColor ?? DEFAULTS.light.searchMarkColor,
117
+ "--dash-bg": light.dashBg ?? DEFAULTS.light.dashBg,
89
118
  },
90
119
  dark: {
91
120
  "--background": dark.bg,
@@ -102,6 +131,8 @@ function defineTheme(opts: {
102
131
  "--muted-foreground": dark.mutedFg,
103
132
  "--accent": dark.muted,
104
133
  "--accent-foreground": dark.fg,
134
+ "--destructive": dark.destructive ?? DEFAULTS.dark.destructive,
135
+ "--success": dark.success ?? DEFAULTS.dark.success,
105
136
  "--border": dark.border,
106
137
  "--input": dark.border,
107
138
  "--ring": dark.primary,
@@ -113,6 +144,10 @@ function defineTheme(opts: {
113
144
  "--sidebar-accent-foreground": dark.fg,
114
145
  "--sidebar-border": dark.border,
115
146
  "--sidebar-ring": dark.primary,
147
+ "--search-mark-bg": dark.searchMarkBg ?? DEFAULTS.dark.searchMarkBg,
148
+ "--search-mark-color":
149
+ dark.searchMarkColor ?? DEFAULTS.dark.searchMarkColor,
150
+ "--dash-bg": dark.dashBg ?? DEFAULTS.dark.dashBg,
116
151
  },
117
152
  };
118
153
  }
@@ -137,6 +172,9 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
137
172
  muted: "oklch(0.94 0.022 95)",
138
173
  mutedFg: "oklch(0.52 0 0)",
139
174
  border: "oklch(0.88 0.025 95)",
175
+ destructive: "oklch(0.55 0.22 25)",
176
+ success: "oklch(0.5 0.14 150)",
177
+ dashBg: "oklch(0.96 0.015 95)",
140
178
  },
141
179
  dark: {
142
180
  bg: "oklch(0.2 0.02 90)",
@@ -146,6 +184,9 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
146
184
  muted: "oklch(0.26 0.018 90)",
147
185
  mutedFg: "oklch(0.62 0.012 95)",
148
186
  border: "oklch(0.32 0.018 90)",
187
+ destructive: "oklch(0.68 0.19 22)",
188
+ success: "oklch(0.62 0.15 150)",
189
+ dashBg: "oklch(0.18 0.015 90)",
149
190
  },
150
191
  }),
151
192
 
@@ -168,6 +209,11 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
168
209
  muted: "oklch(0.93 0.02 75)",
169
210
  mutedFg: "oklch(0.5 0.025 55)",
170
211
  border: "oklch(0.88 0.025 75)",
212
+ destructive: "oklch(0.5 0.2 15)",
213
+ success: "oklch(0.5 0.14 155)",
214
+ searchMarkBg: "oklch(0.88 0.12 75 / 0.6)",
215
+ searchMarkColor: "oklch(0.3 0.06 55)",
216
+ dashBg: "oklch(0.95 0.012 75)",
171
217
  },
172
218
  dark: {
173
219
  bg: "oklch(0.16 0.03 50)",
@@ -177,14 +223,17 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
177
223
  muted: "oklch(0.22 0.025 50)",
178
224
  mutedFg: "oklch(0.62 0.02 75)",
179
225
  border: "oklch(0.28 0.025 50)",
226
+ destructive: "oklch(0.65 0.2 15)",
227
+ success: "oklch(0.62 0.15 155)",
228
+ searchMarkBg: "oklch(0.42 0.1 60 / 0.55)",
229
+ searchMarkColor: "oklch(0.9 0.06 75)",
230
+ dashBg: "oklch(0.14 0.025 50)",
180
231
  },
181
232
  }),
182
233
 
183
- {
234
+ defineTheme({
184
235
  id: "panda",
185
236
  name: "Panda",
186
- light: {},
187
- dark: {},
188
237
  preview: {
189
238
  lightBg: "#ffffff",
190
239
  lightText: "#1e1e1e",
@@ -193,7 +242,31 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
193
242
  darkText: "#fafafa",
194
243
  darkLink: "#eaeaea",
195
244
  },
196
- },
245
+ light: {
246
+ bg: "oklch(1 0 0)",
247
+ fg: "oklch(0.2 0 0)",
248
+ primary: "oklch(0.2 0 0)",
249
+ primaryFg: "oklch(1 0 0)",
250
+ muted: "oklch(0.96 0 0)",
251
+ mutedFg: "oklch(0.5 0 0)",
252
+ border: "oklch(0.9 0 0)",
253
+ destructive: "oklch(0.55 0.22 25)",
254
+ success: "oklch(0.5 0.15 145)",
255
+ dashBg: "oklch(0.97 0 0)",
256
+ },
257
+ dark: {
258
+ bg: "oklch(0.21 0 0)",
259
+ fg: "oklch(0.98 0 0)",
260
+ primary: "oklch(0.93 0 0)",
261
+ primaryFg: "oklch(0.15 0 0)",
262
+ muted: "oklch(0.27 0 0)",
263
+ mutedFg: "oklch(0.65 0 0)",
264
+ border: "oklch(0.33 0 0)",
265
+ destructive: "oklch(0.68 0.18 22)",
266
+ success: "oklch(0.62 0.16 150)",
267
+ dashBg: "oklch(0.18 0 0)",
268
+ },
269
+ }),
197
270
 
198
271
  defineTheme({
199
272
  id: "beach",
@@ -214,6 +287,9 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
214
287
  muted: "oklch(0.93 0.015 85)",
215
288
  mutedFg: "oklch(0.52 0.015 65)",
216
289
  border: "oklch(0.88 0.018 85)",
290
+ destructive: "oklch(0.52 0.2 22)",
291
+ success: "oklch(0.5 0.12 170)",
292
+ dashBg: "oklch(0.95 0.008 85)",
217
293
  },
218
294
  dark: {
219
295
  bg: "oklch(0.27 0.03 210)",
@@ -223,6 +299,9 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
223
299
  muted: "oklch(0.33 0.025 210)",
224
300
  mutedFg: "oklch(0.65 0.015 80)",
225
301
  border: "oklch(0.38 0.02 210)",
302
+ destructive: "oklch(0.68 0.18 20)",
303
+ success: "oklch(0.65 0.12 170)",
304
+ dashBg: "oklch(0.24 0.025 210)",
226
305
  },
227
306
  }),
228
307
 
@@ -245,6 +324,12 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
245
324
  muted: "oklch(0.83 0.035 130)",
246
325
  mutedFg: "oklch(0.48 0.03 140)",
247
326
  border: "oklch(0.79 0.035 130)",
327
+ destructive: "oklch(0.52 0.18 25)",
328
+ // Use blue-teal to differentiate from the green primary
329
+ success: "oklch(0.48 0.1 220)",
330
+ searchMarkBg: "oklch(0.8 0.06 100 / 0.6)",
331
+ searchMarkColor: "oklch(0.3 0.04 130)",
332
+ dashBg: "oklch(0.85 0.028 130)",
248
333
  },
249
334
  dark: {
250
335
  bg: "oklch(0.18 0.02 140)",
@@ -254,6 +339,11 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
254
339
  muted: "oklch(0.24 0.02 140)",
255
340
  mutedFg: "oklch(0.58 0.02 130)",
256
341
  border: "oklch(0.3 0.02 140)",
342
+ destructive: "oklch(0.65 0.17 22)",
343
+ success: "oklch(0.6 0.1 220)",
344
+ searchMarkBg: "oklch(0.4 0.06 110 / 0.55)",
345
+ searchMarkColor: "oklch(0.88 0.04 130)",
346
+ dashBg: "oklch(0.16 0.018 140)",
257
347
  },
258
348
  }),
259
349
 
@@ -276,6 +366,11 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
276
366
  muted: "oklch(0.92 0 0)",
277
367
  mutedFg: "oklch(0.55 0 0)",
278
368
  border: "oklch(0.87 0 0)",
369
+ destructive: "oklch(0.55 0.22 25)",
370
+ success: "oklch(0.5 0.15 145)",
371
+ searchMarkBg: "oklch(0.88 0 0 / 0.7)",
372
+ searchMarkColor: "oklch(0.25 0 0)",
373
+ dashBg: "oklch(0.94 0 0)",
279
374
  },
280
375
  dark: {
281
376
  bg: "oklch(0.18 0 0)",
@@ -285,6 +380,11 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
285
380
  muted: "oklch(0.24 0 0)",
286
381
  mutedFg: "oklch(0.6 0 0)",
287
382
  border: "oklch(0.3 0 0)",
383
+ destructive: "oklch(0.68 0.18 22)",
384
+ success: "oklch(0.62 0.16 150)",
385
+ searchMarkBg: "oklch(0.35 0 0 / 0.6)",
386
+ searchMarkColor: "oklch(0.9 0 0)",
387
+ dashBg: "oklch(0.16 0 0)",
288
388
  },
289
389
  }),
290
390
 
@@ -307,6 +407,11 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
307
407
  muted: "oklch(0.93 0.016 325)",
308
408
  mutedFg: "oklch(0.52 0.015 310)",
309
409
  border: "oklch(0.88 0.016 325)",
410
+ destructive: "oklch(0.55 0.2 15)",
411
+ success: "oklch(0.5 0.14 155)",
412
+ searchMarkBg: "oklch(0.9 0.08 310 / 0.45)",
413
+ searchMarkColor: "oklch(0.3 0.04 300)",
414
+ dashBg: "oklch(0.95 0.01 325)",
310
415
  },
311
416
  dark: {
312
417
  bg: "oklch(0.18 0.025 300)",
@@ -316,6 +421,11 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
316
421
  muted: "oklch(0.24 0.022 300)",
317
422
  mutedFg: "oklch(0.62 0.012 325)",
318
423
  border: "oklch(0.3 0.022 300)",
424
+ destructive: "oklch(0.68 0.18 18)",
425
+ success: "oklch(0.62 0.15 155)",
426
+ searchMarkBg: "oklch(0.38 0.08 290 / 0.5)",
427
+ searchMarkColor: "oklch(0.9 0.04 325)",
428
+ dashBg: "oklch(0.16 0.02 300)",
319
429
  },
320
430
  }),
321
431
  ];
@@ -18,9 +18,18 @@ export interface ComposeDialogProps {
18
18
  uploadMaxFileSize?: number;
19
19
  }
20
20
 
21
- export const ComposeDialog: FC<ComposeDialogProps> = ({
21
+ export interface ComposeFormProps extends ComposeDialogProps {
22
+ pageMode?: boolean;
23
+ closeHref?: string;
24
+ autoRestoreDraft?: boolean;
25
+ }
26
+
27
+ export const ComposeForm: FC<ComposeFormProps> = ({
22
28
  collections,
23
29
  uploadMaxFileSize,
30
+ pageMode = false,
31
+ closeHref,
32
+ autoRestoreDraft = false,
24
33
  }) => {
25
34
  const { t } = useLingui();
26
35
 
@@ -74,7 +83,7 @@ export const ComposeDialog: FC<ComposeDialogProps> = ({
74
83
  comment: "@context: Compose quote source link placeholder",
75
84
  }),
76
85
  attachedText: t({
77
- message: "Attached Text",
86
+ message: "Text attachment",
78
87
  comment: "@context: Attached text panel title",
79
88
  }),
80
89
  attachedTextPlaceholder: t({
@@ -116,7 +125,13 @@ export const ComposeDialog: FC<ComposeDialogProps> = ({
116
125
  }),
117
126
  noCollections: t({
118
127
  message: "No matching collections.",
119
- comment: "@context: Compose collection combobox empty state",
128
+ comment:
129
+ "@context: Compose collection combobox empty state when search has no results",
130
+ }),
131
+ emptyCollections: t({
132
+ message: "Create a collection to get started.",
133
+ comment:
134
+ "@context: Compose collection combobox empty state when no collections exist",
120
135
  }),
121
136
  post: t({
122
137
  message: "Post",
@@ -150,11 +165,203 @@ export const ComposeDialog: FC<ComposeDialogProps> = ({
150
165
  message: "Published!",
151
166
  comment: "@context: Toast shown after successful deferred publish",
152
167
  }),
168
+ view: t({
169
+ message: "View",
170
+ comment: "@context: Toast action button to view the published post",
171
+ }),
153
172
  retryAll: t({
154
- message: "Click to retry all",
173
+ message: "Tap to retry",
174
+ comment:
175
+ "@context: Label on failed upload overlay button, tells user tapping retries the upload",
176
+ }),
177
+ editPost: t({
178
+ message: "Edit post",
179
+ comment: "@context: Compose dialog header title in edit mode",
180
+ }),
181
+ update: t({
182
+ message: "Done",
183
+ comment: "@context: Compose button - update existing post",
184
+ }),
185
+ confirmCloseTitle: t({
186
+ message: "Save to drafts?",
187
+ comment: "@context: Confirm close action sheet title",
188
+ }),
189
+ confirmCloseSubtitle: t({
190
+ message: "Save to drafts to edit and post at a later time.",
191
+ comment: "@context: Confirm close action sheet subtitle",
192
+ }),
193
+ confirmCloseSave: t({
194
+ message: "Save",
195
+ comment: "@context: Confirm close action sheet - save draft button",
196
+ }),
197
+ confirmCloseCancel: t({
198
+ message: "Cancel",
199
+ comment:
200
+ "@context: Confirm close action sheet - cancel and return to editor",
201
+ }),
202
+ confirmCloseDiscard: t({
203
+ message: "Don't save",
204
+ comment: "@context: Confirm close action sheet - discard button",
205
+ }),
206
+ confirmEditTitle: t({
207
+ message: "You have unsaved changes",
208
+ comment:
209
+ "@context: Confirm close action sheet title when editing a published post",
210
+ }),
211
+ confirmEditSubtitle: t({
212
+ message: "Do you want to publish your changes or discard them?",
213
+ comment:
214
+ "@context: Confirm close action sheet subtitle when editing a published post",
215
+ }),
216
+ confirmEditPublish: t({
217
+ message: "Publish",
218
+ comment:
219
+ "@context: Confirm close action sheet - publish update button for editing published post",
220
+ }),
221
+ confirmEditDiscard: t({
222
+ message: "Discard",
155
223
  comment:
156
- "@context: Tooltip hint on failed upload overlay, tells user clicking retries all failed uploads",
224
+ "@context: Confirm close action sheet - discard changes button for editing published post",
225
+ }),
226
+ drafts: t({ message: "Drafts", comment: "@context: Drafts panel title" }),
227
+ draftsEmpty: t({
228
+ message: "No drafts yet. Save a draft to find it here.",
229
+ comment: "@context: Drafts panel empty state",
230
+ }),
231
+ deleteDraft: t({
232
+ message: "Delete Draft",
233
+ comment: "@context: Draft item action",
234
+ }),
235
+ draftDeleted: t({
236
+ message: "Draft deleted.",
237
+ comment: "@context: Toast after draft deletion",
157
238
  }),
239
+ publishFailedDraft: t({
240
+ message: "Couldn't publish. Saved as draft.",
241
+ comment:
242
+ "@context: Toast when publish fails and post is auto-saved as draft",
243
+ }),
244
+ uploadFailedDraft: t({
245
+ message: "Some uploads failed. Saved as draft.",
246
+ comment:
247
+ "@context: Toast when uploads fail and post is auto-saved as draft",
248
+ }),
249
+ reply: t({
250
+ message: "Reply",
251
+ comment: "@context: Compose button - reply to post",
252
+ }),
253
+ publishFeatured: t({
254
+ message: "Post as Featured",
255
+ comment:
256
+ "@context: Compose dropdown option - publish post and mark it as featured",
257
+ }),
258
+ publishUnlisted: t({
259
+ message: "Post Unlisted",
260
+ comment:
261
+ "@context: Compose dropdown option - publish post with unlisted visibility, hidden from main feed",
262
+ }),
263
+ publishPrivate: t({
264
+ message: "Post as Private",
265
+ comment:
266
+ "@context: Compose dropdown option - publish post visible only when logged in",
267
+ }),
268
+ showMore: t({
269
+ message: "Show more",
270
+ comment: "@context: Expand reply context",
271
+ }),
272
+ showLess: t({
273
+ message: "Show less",
274
+ comment: "@context: Collapse reply context",
275
+ }),
276
+ addCollection: t({
277
+ message: "Add Collection",
278
+ comment: "@context: Action to create a new collection from compose",
279
+ }),
280
+ collectionCountLabel: t({
281
+ message: "%name% + %count% more",
282
+ comment:
283
+ "@context: Compose collection trigger label when multiple collections selected. %name% is the first collection name, %count% is how many more",
284
+ }),
285
+ draftRestored: t({
286
+ message: "Draft restored.",
287
+ comment:
288
+ "@context: Toast shown when a local draft is restored on compose open",
289
+ }),
290
+ collectionFormLabels: {
291
+ titleLabel: t({
292
+ message: "Title",
293
+ comment: "@context: Collection form field",
294
+ }),
295
+ titlePlaceholder: t({
296
+ message: "My Collection",
297
+ comment: "@context: Collection title placeholder",
298
+ }),
299
+ slugLabel: t({
300
+ message: "Slug",
301
+ comment: "@context: Collection form field",
302
+ }),
303
+ slugHelp: t({
304
+ message:
305
+ "URL-safe identifier (lowercase, numbers, hyphens). For CJK titles, slug will be auto-generated on the server.",
306
+ comment: "@context: Collection path help text",
307
+ }),
308
+ descriptionLabel: t({
309
+ message: "Description (optional)",
310
+ comment: "@context: Collection form field",
311
+ }),
312
+ descriptionPlaceholder: t({
313
+ message: "What's this collection about?",
314
+ comment: "@context: Collection description placeholder",
315
+ }),
316
+ removeIcon: t({
317
+ message: "Remove",
318
+ comment: "@context: Button to remove icon",
319
+ }),
320
+ iconsTab: t({
321
+ message: "Icons",
322
+ comment: "@context: Icon picker tab label",
323
+ }),
324
+ emojisTab: t({
325
+ message: "Emojis",
326
+ comment: "@context: Emoji picker tab label",
327
+ }),
328
+ searchIconsPlaceholder: t({
329
+ message: "Search icons...",
330
+ comment: "@context: Icon picker search placeholder",
331
+ }),
332
+ searchEmojisPlaceholder: t({
333
+ message: "Search emojis...",
334
+ comment: "@context: Emoji picker search placeholder",
335
+ }),
336
+ sortOrderLabel: t({
337
+ message: "Sort Order",
338
+ comment: "@context: Collection form field",
339
+ }),
340
+ sortNewest: t({
341
+ message: "Newest first",
342
+ comment: "@context: Collection sort order option",
343
+ }),
344
+ sortOldest: t({
345
+ message: "Oldest first",
346
+ comment: "@context: Collection sort order option",
347
+ }),
348
+ sortRatingDesc: t({
349
+ message: "Highest rated",
350
+ comment: "@context: Collection sort order option",
351
+ }),
352
+ sortRatingAsc: t({
353
+ message: "Lowest rated",
354
+ comment: "@context: Collection sort order option",
355
+ }),
356
+ submitLabel: t({
357
+ message: "Save",
358
+ comment: "@context: Button to save collection",
359
+ }),
360
+ cancelLabel: t({
361
+ message: "Cancel",
362
+ comment: "@context: Button to cancel form",
363
+ }),
364
+ },
158
365
  }).replace(/</g, "\\u003c");
159
366
 
160
367
  const collectionsJson = JSON.stringify(
@@ -165,23 +372,38 @@ export const ComposeDialog: FC<ComposeDialogProps> = ({
165
372
  })),
166
373
  ).replace(/</g, "\\u003c");
167
374
 
375
+ return (
376
+ <jant-compose-dialog
377
+ collections={collectionsJson}
378
+ labels={labels}
379
+ upload-max-file-size={uploadMaxFileSize ?? 500}
380
+ {...(pageMode ? { "page-mode": "" } : {})}
381
+ {...(closeHref ? { "close-href": closeHref } : {})}
382
+ {...(autoRestoreDraft ? { "auto-restore-draft": "" } : {})}
383
+ >
384
+ {/* SSR fallback skeleton */}
385
+ <div class="compose-dialog-inner">
386
+ <div class="compose-dialog-header" />
387
+ <div class="compose-body skel-section-md" />
388
+ </div>
389
+ </jant-compose-dialog>
390
+ );
391
+ };
392
+
393
+ export const ComposeDialog: FC<ComposeDialogProps> = ({
394
+ collections,
395
+ uploadMaxFileSize,
396
+ }) => {
168
397
  return (
169
398
  <dialog
170
399
  id="compose-dialog"
171
400
  class="compose-dialog"
172
- onclick="event.target === this && this.close()"
401
+ data-on:click="evt.target === el && el.querySelector('jant-compose-dialog')?.requestClose()"
173
402
  >
174
- <jant-compose-dialog
175
- collections={collectionsJson}
176
- labels={labels}
177
- upload-max-file-size={uploadMaxFileSize ?? 500}
178
- >
179
- {/* SSR fallback skeleton */}
180
- <div class="compose-dialog-inner">
181
- <div class="compose-dialog-header" />
182
- <div class="compose-body skel-section-md" />
183
- </div>
184
- </jant-compose-dialog>
403
+ <ComposeForm
404
+ collections={collections}
405
+ uploadMaxFileSize={uploadMaxFileSize}
406
+ />
185
407
  </dialog>
186
408
  );
187
409
  };
@@ -16,7 +16,7 @@ export const ComposePrompt: FC = () => {
16
16
  <button
17
17
  type="button"
18
18
  class="compose-prompt-trigger"
19
- onclick="const d=document.getElementById('compose-dialog');d.showModal();d.querySelector('jant-compose-editor')?.focusInput()"
19
+ data-on:click="document.getElementById('compose-dialog').querySelector('jant-compose-dialog')?.restoreLocalDraft(); document.getElementById('compose-dialog').showModal(); document.getElementById('compose-dialog').querySelector('jant-compose-editor')?.focusInput()"
20
20
  >
21
21
  <span class="compose-prompt-avatar">
22
22
  <svg
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CRUD Page Header Component
3
3
  *
4
- * Provides consistent header layout for dashboard CRUD list pages
4
+ * Provides consistent header layout for admin CRUD list pages
5
5
  * with title and primary action button
6
6
  */
7
7
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * List Item Row Component
3
3
  *
4
- * Provides consistent layout for list items in dashboard CRUD pages.
4
+ * Provides consistent layout for list items in admin CRUD pages.
5
5
  * Handles responsive spacing, overflow, and action button placement.
6
6
  */
7
7
 
@@ -11,12 +11,14 @@ import type { Status, Visibility } from "../../types.js";
11
11
  export interface StatusBadgeProps {
12
12
  status: Status;
13
13
  visibility?: Visibility;
14
+ featured?: boolean;
14
15
  pinned?: boolean;
15
16
  }
16
17
 
17
18
  export const StatusBadge: FC<StatusBadgeProps> = ({
18
19
  status,
19
20
  visibility,
21
+ featured,
20
22
  pinned,
21
23
  }) => {
22
24
  const { t } = useLingui();
@@ -40,7 +42,7 @@ export const StatusBadge: FC<StatusBadgeProps> = ({
40
42
  return (
41
43
  <span class="flex items-center gap-1">
42
44
  <span class={statusVariants[status]}>{statusLabels[status]}</span>
43
- {visibility === "featured" && (
45
+ {featured && (
44
46
  <span class="badge-primary">
45
47
  {t({
46
48
  message: "Featured",
@@ -4,6 +4,9 @@
4
4
 
5
5
  import { useLingui } from "@lingui/react/macro";
6
6
 
7
+ const THEMING_DOCS_URL =
8
+ "https://github.com/jant-me/jant/blob/main/docs/theming.md";
9
+
7
10
  export function AdvancedContent({ customCSS }: { customCSS: string }) {
8
11
  const { t } = useLingui();
9
12
 
@@ -12,7 +15,7 @@ export function AdvancedContent({ customCSS }: { customCSS: string }) {
12
15
  return (
13
16
  <form
14
17
  data-signals={cssSignals}
15
- data-on:submit__prevent="@post('/dash/settings/custom-css')"
18
+ data-on:submit__prevent="@post('/settings/custom-css')"
16
19
  data-indicator="_cssLoading"
17
20
  class="max-w-3xl"
18
21
  >
@@ -28,6 +31,24 @@ export function AdvancedContent({ customCSS }: { customCSS: string }) {
28
31
  message:
29
32
  "Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.",
30
33
  comment: "@context: Custom CSS settings description",
34
+ })}{" "}
35
+ <a
36
+ href={THEMING_DOCS_URL}
37
+ target="_blank"
38
+ rel="noopener noreferrer"
39
+ class="underline hover:text-foreground transition-colors"
40
+ >
41
+ {t({
42
+ message: "Theming guide",
43
+ comment:
44
+ "@context: Link to theming documentation on Custom CSS page",
45
+ })}
46
+ </a>
47
+ {" — "}
48
+ {t({
49
+ message: "available CSS variables, data attributes, and examples.",
50
+ comment:
51
+ "@context: Description after theming guide link on Custom CSS page",
31
52
  })}
32
53
  </p>
33
54
  <textarea