@jant/core 0.3.34 → 0.3.36

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 (156) hide show
  1. package/dist/client/assets/module-RjUF93sV.js +716 -0
  2. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  3. package/dist/client/assets/url-8Dj-5CLW.js +1 -0
  4. package/dist/client/client.css +1 -1
  5. package/dist/client/client.js +3109 -2294
  6. package/dist/index.js +3327 -3031
  7. package/package.json +13 -4
  8. package/src/__tests__/helpers/app.ts +1 -1
  9. package/src/__tests__/helpers/db.ts +6 -0
  10. package/src/app.tsx +1 -5
  11. package/src/{lib → client}/avatar-upload.ts +1 -1
  12. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  13. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  14. package/src/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
  15. package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
  16. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
  17. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  18. package/src/client/components/collection-sidebar-types.ts +45 -0
  19. package/src/{ui → client}/components/collection-types.ts +3 -4
  20. package/src/{ui → client}/components/compose-types.ts +3 -1
  21. package/src/{ui → client}/components/jant-collection-form.ts +301 -182
  22. package/src/client/components/jant-collection-sidebar.ts +801 -0
  23. package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
  24. package/src/client/components/jant-compose-editor.ts +1249 -0
  25. package/src/client/components/jant-compose-fullscreen.ts +338 -0
  26. package/src/client/components/jant-media-lightbox.ts +257 -0
  27. package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
  28. package/src/{ui → client}/components/jant-post-form.ts +57 -8
  29. package/src/{ui → client}/components/jant-settings-general.ts +2 -2
  30. package/src/{ui → client}/components/nav-manager-types.ts +3 -0
  31. package/src/{ui → client}/components/post-form-template.ts +35 -31
  32. package/src/{ui → client}/components/post-form-types.ts +7 -3
  33. package/src/{lib → client}/compose-bridge.ts +9 -7
  34. package/src/client/lazy-slugify.ts +51 -0
  35. package/src/{lib → client}/media-upload.ts +16 -3
  36. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  37. package/src/client/page-slug-bridge.ts +42 -0
  38. package/src/{lib → client}/post-form-bridge.ts +2 -2
  39. package/src/{lib → client}/settings-bridge.ts +3 -3
  40. package/src/client/tiptap/bubble-menu.ts +205 -0
  41. package/src/client/tiptap/create-editor.ts +40 -0
  42. package/src/client/tiptap/exitable-marks.ts +73 -0
  43. package/src/client/tiptap/extensions.ts +60 -0
  44. package/src/client/tiptap/image-node.ts +488 -0
  45. package/src/client/tiptap/link-toolbar.ts +371 -0
  46. package/src/client/tiptap/more-break.ts +50 -0
  47. package/src/client/tiptap/paste-image.ts +140 -0
  48. package/src/client/tiptap/slash-commands.ts +328 -0
  49. package/src/{types → client/types}/sortablejs.d.ts +1 -1
  50. package/src/client.ts +24 -17
  51. package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
  52. package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
  53. package/src/db/schema.ts +6 -1
  54. package/src/i18n/locales/en.po +641 -215
  55. package/src/i18n/locales/en.ts +1 -1
  56. package/src/i18n/locales/zh-Hans.po +642 -204
  57. package/src/i18n/locales/zh-Hans.ts +1 -1
  58. package/src/i18n/locales/zh-Hant.po +642 -204
  59. package/src/i18n/locales/zh-Hant.ts +1 -1
  60. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  61. package/src/lib/__tests__/schemas.test.ts +9 -6
  62. package/src/lib/__tests__/url.test.ts +2 -2
  63. package/src/lib/__tests__/view.test.ts +9 -9
  64. package/src/lib/emoji-catalog.ts +146 -0
  65. package/src/lib/feed.ts +1 -1
  66. package/src/lib/media-helpers.ts +10 -9
  67. package/src/lib/render.tsx +4 -3
  68. package/src/lib/resolve-config.ts +8 -1
  69. package/src/lib/schemas.ts +2 -3
  70. package/src/lib/summary.ts +92 -0
  71. package/src/lib/timeline.ts +2 -0
  72. package/src/lib/tiptap-render.ts +196 -0
  73. package/src/lib/upload.ts +97 -9
  74. package/src/lib/url.ts +7 -23
  75. package/src/lib/view.ts +33 -19
  76. package/src/middleware/error-handler.ts +3 -3
  77. package/src/preset.css +38 -0
  78. package/src/routes/api/collections.ts +20 -3
  79. package/src/routes/api/posts.ts +48 -33
  80. package/src/routes/api/upload.ts +7 -5
  81. package/src/routes/auth/reset.tsx +5 -4
  82. package/src/routes/auth/setup.tsx +26 -11
  83. package/src/routes/auth/signin.tsx +10 -7
  84. package/src/routes/compose.tsx +20 -11
  85. package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
  86. package/src/routes/dash/index.tsx +7 -1
  87. package/src/routes/dash/media.tsx +3 -0
  88. package/src/routes/dash/pages.tsx +8 -2
  89. package/src/routes/dash/posts.tsx +6 -2
  90. package/src/routes/dash/redirects.tsx +15 -9
  91. package/src/routes/dash/settings.tsx +336 -32
  92. package/src/routes/feed/__tests__/rss.test.ts +245 -6
  93. package/src/routes/feed/rss.ts +70 -6
  94. package/src/routes/pages/__tests__/featured.test.ts +6 -7
  95. package/src/routes/pages/archive.tsx +11 -7
  96. package/src/routes/pages/collection.tsx +32 -15
  97. package/src/routes/pages/collections.tsx +11 -2
  98. package/src/routes/pages/featured.tsx +1 -1
  99. package/src/routes/pages/home.tsx +1 -1
  100. package/src/services/__tests__/post.test.ts +124 -33
  101. package/src/services/__tests__/settings.test.ts +3 -3
  102. package/src/services/page.ts +16 -3
  103. package/src/services/post.ts +96 -37
  104. package/src/services/search.ts +4 -2
  105. package/src/services/settings.ts +6 -2
  106. package/src/styles/components.css +240 -60
  107. package/src/styles/tokens.css +10 -0
  108. package/src/styles/ui.css +1157 -81
  109. package/src/types/bindings.ts +5 -0
  110. package/src/types/config.ts +23 -1
  111. package/src/types/constants.ts +3 -0
  112. package/src/types/entities.ts +9 -2
  113. package/src/types/operations.ts +9 -3
  114. package/src/types/props.ts +3 -3
  115. package/src/types/views.ts +3 -2
  116. package/src/ui/compose/ComposeDialog.tsx +24 -7
  117. package/src/ui/dash/PageForm.tsx +2 -0
  118. package/src/ui/dash/PostList.tsx +5 -5
  119. package/src/ui/dash/StatusBadge.tsx +13 -5
  120. package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
  121. package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
  122. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  123. package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
  124. package/src/ui/dash/media/MediaListContent.tsx +9 -4
  125. package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
  126. package/src/ui/dash/pages/PagesContent.tsx +2 -1
  127. package/src/ui/dash/posts/PostForm.tsx +19 -7
  128. package/src/ui/dash/settings/AccountContent.tsx +133 -138
  129. package/src/ui/dash/settings/AvatarContent.tsx +70 -0
  130. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  131. package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
  132. package/src/ui/layouts/DashLayout.tsx +157 -75
  133. package/src/ui/layouts/SiteLayout.tsx +13 -13
  134. package/src/ui/pages/ArchivePage.tsx +10 -7
  135. package/src/ui/pages/CollectionPage.tsx +6 -35
  136. package/src/ui/pages/CollectionsPage.tsx +2 -1
  137. package/src/ui/pages/FeaturedPage.tsx +2 -1
  138. package/src/ui/pages/HomePage.tsx +1 -1
  139. package/src/ui/pages/SearchPage.tsx +1 -1
  140. package/src/ui/shared/CollectionsSidebar.tsx +228 -3
  141. package/src/ui/shared/MediaGallery.tsx +179 -41
  142. package/src/lib/collections-reorder.ts +0 -28
  143. package/src/routes/dash/appearance.tsx +0 -240
  144. package/src/routes/dash/collections.tsx +0 -211
  145. package/src/ui/components/jant-compose-editor.ts +0 -814
  146. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  147. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  148. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  149. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  150. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  151. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  152. /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
  153. /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
  154. /package/src/{ui → client}/components/settings-types.ts +0 -0
  155. /package/src/{lib → client}/image-processor.ts +0 -0
  156. /package/src/{lib → client}/toast.ts +0 -0
@@ -138,7 +138,7 @@ uploadApiRoutes.post("/", async (c) => {
138
138
  if (!storage) {
139
139
  const errorText = i18n._(
140
140
  msg({
141
- message: "Storage not configured",
141
+ message: "File storage isn't set up. Check your server config.",
142
142
  comment: "@context: Error when file storage is not set up",
143
143
  }),
144
144
  );
@@ -154,7 +154,7 @@ uploadApiRoutes.post("/", async (c) => {
154
154
  if (!file) {
155
155
  const errorText = i18n._(
156
156
  msg({
157
- message: "No file provided",
157
+ message: "No file selected. Choose a file to upload.",
158
158
  comment: "@context: Error when no file was selected for upload",
159
159
  }),
160
160
  );
@@ -165,7 +165,9 @@ uploadApiRoutes.post("/", async (c) => {
165
165
  }
166
166
 
167
167
  // Validate file type and size
168
- const uploadError = validateUploadFile(file);
168
+ const uploadError = validateUploadFile(file, {
169
+ maxFileSizeMB: c.var.appConfig.uploadMaxFileSize,
170
+ });
169
171
  if (uploadError) {
170
172
  if (wantsSSE(c)) {
171
173
  return sseUploadError(c, uploadError);
@@ -215,7 +217,7 @@ uploadApiRoutes.post("/", async (c) => {
215
217
  await stream.toast(
216
218
  i18n._(
217
219
  msg({
218
- message: "Upload successful!",
220
+ message: "File uploaded.",
219
221
  comment: "@context: Toast after successful file upload",
220
222
  }),
221
223
  ),
@@ -243,7 +245,7 @@ uploadApiRoutes.post("/", async (c) => {
243
245
 
244
246
  const errorText = i18n._(
245
247
  msg({
246
- message: "Upload failed. Please try again.",
248
+ message: "Upload didn't go through. Try again in a moment.",
247
249
  comment: "@context: Error when file upload fails",
248
250
  }),
249
251
  );
@@ -37,7 +37,7 @@ const ResetContent: FC<{ token: string }> = ({ token }) => {
37
37
  </h2>
38
38
  <p>
39
39
  {t({
40
- message: "Enter your new password.",
40
+ message: "Choose a new password.",
41
41
  comment: "@context: Password reset page description",
42
42
  })}
43
43
  </p>
@@ -118,7 +118,7 @@ const ResetErrorContent: FC = () => {
118
118
  <header>
119
119
  <h2>
120
120
  {t({
121
- message: "Invalid or Expired Link",
121
+ message: "This Link Has Expired",
122
122
  comment: "@context: Password reset error heading",
123
123
  })}
124
124
  </h2>
@@ -127,7 +127,7 @@ const ResetErrorContent: FC = () => {
127
127
  <p class="text-muted-foreground">
128
128
  {t({
129
129
  message:
130
- "This password reset link is invalid or has expired. Please generate a new one.",
130
+ "This reset link is no longer valid. Request a new one to continue.",
131
131
  comment: "@context: Password reset error description",
132
132
  })}
133
133
  </p>
@@ -175,7 +175,8 @@ resetRoutes.post("/reset", async (c) => {
175
175
  parsed.error.issues[0]?.message ??
176
176
  i18n._(
177
177
  msg({
178
- message: "Invalid input",
178
+ message:
179
+ "Something doesn't look right. Check the form and try again.",
179
180
  comment:
180
181
  "@context: Fallback validation error for password reset form",
181
182
  }),
@@ -146,7 +146,8 @@ setupRoutes.post("/setup", async (c) => {
146
146
  parsed.error.issues[0]?.message ??
147
147
  i18n._(
148
148
  msg({
149
- message: "Invalid input",
149
+ message:
150
+ "Something doesn't look right. Check the form and try again.",
150
151
  comment: "@context: Fallback validation error for setup form",
151
152
  }),
152
153
  );
@@ -159,7 +160,7 @@ setupRoutes.post("/setup", async (c) => {
159
160
  return dsToast(
160
161
  i18n._(
161
162
  msg({
162
- message: "AUTH_SECRET not configured",
163
+ message: "Auth secret is missing. Check your environment variables.",
163
164
  comment:
164
165
  "@context: Error toast when authentication secret is missing from server config",
165
166
  }),
@@ -177,7 +178,8 @@ setupRoutes.post("/setup", async (c) => {
177
178
  return dsToast(
178
179
  i18n._(
179
180
  msg({
180
- message: "Failed to create account",
181
+ message:
182
+ "Couldn't create your account. Check the details and try again.",
181
183
  comment: "@context: Error toast when account creation fails",
182
184
  }),
183
185
  ),
@@ -195,19 +197,13 @@ setupRoutes.post("/setup", async (c) => {
195
197
  }
196
198
  }
197
199
 
200
+ // Seed default navigation items (order: Collections, About, Archive, RSS, Dashboard)
198
201
  await c.var.services.navItems.create({
199
202
  type: "link",
200
203
  label: "Collections",
201
204
  url: "/c",
202
205
  });
203
- // Seed default navigation items
204
- await c.var.services.navItems.create({
205
- type: "link",
206
- label: "Archive",
207
- url: "/archive",
208
- });
209
206
 
210
- // Seed default About page
211
207
  const aboutPage = await c.var.services.pages.create({
212
208
  slug: "about",
213
209
  title: "About",
@@ -228,6 +224,24 @@ setupRoutes.post("/setup", async (c) => {
228
224
  pageId: aboutPage.id,
229
225
  });
230
226
 
227
+ await c.var.services.navItems.create({
228
+ type: "link",
229
+ label: "Archive",
230
+ url: "/archive",
231
+ });
232
+
233
+ await c.var.services.navItems.create({
234
+ type: "system",
235
+ label: "RSS",
236
+ url: "/feed",
237
+ });
238
+
239
+ await c.var.services.navItems.create({
240
+ type: "system",
241
+ label: "Dashboard",
242
+ url: "/dash",
243
+ });
244
+
231
245
  return dsRedirect("/signin?setup");
232
246
  } catch (err) {
233
247
  // eslint-disable-next-line no-console -- Error logging is intentional
@@ -235,7 +249,8 @@ setupRoutes.post("/setup", async (c) => {
235
249
  return dsToast(
236
250
  i18n._(
237
251
  msg({
238
- message: "Failed to create account",
252
+ message:
253
+ "Couldn't create your account. Check the details and try again.",
239
254
  comment: "@context: Error toast when account creation fails",
240
255
  }),
241
256
  ),
@@ -40,7 +40,8 @@ const SigninContent: FC<{
40
40
  {demoEmail && demoPassword && (
41
41
  <p class="text-muted-foreground text-sm mb-4">
42
42
  {t({
43
- message: "Demo account pre-filled. Just click Sign In.",
43
+ message:
44
+ "Demo credentials are pre-filled — hit Sign In to continue.",
44
45
  comment:
45
46
  "@context: Hint shown on signin page when demo credentials are pre-filled",
46
47
  })}
@@ -110,9 +111,9 @@ signinRoutes.get("/signin", async (c) => {
110
111
  const isReset = c.req.query("reset") !== undefined;
111
112
  let toast: { message: string } | undefined;
112
113
  if (isSetup) {
113
- toast = { message: "Account created successfully. Please sign in." };
114
+ toast = { message: "Account created. Sign in to get started." };
114
115
  } else if (isReset) {
115
- toast = { message: "Password reset successfully. Please sign in." };
116
+ toast = { message: "Password reset. Sign in with your new password." };
116
117
  }
117
118
 
118
119
  return c.html(
@@ -132,7 +133,7 @@ signinRoutes.post("/signin", async (c) => {
132
133
  return dsToast(
133
134
  i18n._(
134
135
  msg({
135
- message: "Auth not configured",
136
+ message: "Authentication isn't set up. Check your server config.",
136
137
  comment:
137
138
  "@context: Error toast when authentication system is unavailable",
138
139
  }),
@@ -149,7 +150,8 @@ signinRoutes.post("/signin", async (c) => {
149
150
  parsed.error.issues[0]?.message ??
150
151
  i18n._(
151
152
  msg({
152
- message: "Invalid input",
153
+ message:
154
+ "Something doesn't look right. Check the form and try again.",
153
155
  comment: "@context: Fallback validation error for sign-in form",
154
156
  }),
155
157
  );
@@ -165,12 +167,13 @@ signinRoutes.post("/signin", async (c) => {
165
167
  headers: c.req.raw.headers,
166
168
  });
167
169
 
168
- return dsRedirect("/dash", { headers });
170
+ return dsRedirect("/", { headers });
169
171
  } catch {
170
172
  return dsToast(
171
173
  i18n._(
172
174
  msg({
173
- message: "Invalid email or password",
175
+ message:
176
+ "Wrong email or password. Check your credentials and try again.",
174
177
  comment: "@context: Error toast when sign-in credentials are wrong",
175
178
  }),
176
179
  ),
@@ -94,7 +94,8 @@ composeRoutes.post("/", async (c) => {
94
94
  result.error.issues[0]?.message ??
95
95
  i18n._(
96
96
  msg({
97
- message: "Invalid input",
97
+ message:
98
+ "Something doesn't look right. Check the form and try again.",
98
99
  comment: "@context: Fallback validation error for compose form",
99
100
  }),
100
101
  );
@@ -121,16 +122,24 @@ composeRoutes.post("/", async (c) => {
121
122
  }
122
123
  }
123
124
 
124
- const post = await c.var.services.posts.create({
125
- format: data.format,
126
- title: data.title || undefined,
127
- body: data.body || undefined,
128
- status: data.status ?? "published",
129
- url: data.url || undefined,
130
- quoteText: data.quoteText || undefined,
131
- rating: data.rating || undefined,
132
- collectionIds: data.collectionIds?.length ? data.collectionIds : undefined,
133
- });
125
+ const post = await c.var.services.posts.create(
126
+ {
127
+ format: data.format,
128
+ title: data.title || undefined,
129
+ body: data.body || undefined,
130
+ status: data.status ?? "published",
131
+ url: data.url || undefined,
132
+ quoteText: data.quoteText || undefined,
133
+ rating: data.rating || undefined,
134
+ collectionIds: data.collectionIds?.length
135
+ ? data.collectionIds
136
+ : undefined,
137
+ },
138
+ {
139
+ maxParagraphs: c.var.appConfig.summaryMaxParagraphs,
140
+ maxChars: c.var.appConfig.summaryMaxChars,
141
+ },
142
+ );
134
143
 
135
144
  // Attach media if provided
136
145
  if (data.mediaIds && data.mediaIds.length > 0) {
@@ -57,7 +57,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
57
57
 
58
58
  await settingsService.uploadAvatar(
59
59
  { file },
60
- { media: mediaService, storage, storageProvider: "r2" },
60
+ {
61
+ media: mediaService,
62
+ storage,
63
+ storageProvider: "r2",
64
+ maxFileSizeMB: 500,
65
+ },
61
66
  );
62
67
 
63
68
  const avatarKey = await settingsService.get("SITE_AVATAR");
@@ -72,7 +77,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
72
77
 
73
78
  await settingsService.uploadAvatar(
74
79
  { file },
75
- { media: mediaService, storage, storageProvider: "r2" },
80
+ {
81
+ media: mediaService,
82
+ storage,
83
+ storageProvider: "r2",
84
+ maxFileSizeMB: 500,
85
+ },
76
86
  );
77
87
 
78
88
  const mediaList = await mediaService.list();
@@ -89,7 +99,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
89
99
 
90
100
  await settingsService.uploadAvatar(
91
101
  { file, faviconIco: fakeIcoData.buffer },
92
- { media: mediaService, storage, storageProvider: "r2" },
102
+ {
103
+ media: mediaService,
104
+ storage,
105
+ storageProvider: "r2",
106
+ maxFileSizeMB: 500,
107
+ },
93
108
  );
94
109
 
95
110
  const stored = await settingsService.get("SITE_FAVICON_ICO");
@@ -105,7 +120,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
105
120
 
106
121
  await settingsService.uploadAvatar(
107
122
  { file, appleTouchIcon: appleTouchData },
108
- { media: mediaService, storage, storageProvider: "r2" },
123
+ {
124
+ media: mediaService,
125
+ storage,
126
+ storageProvider: "r2",
127
+ maxFileSizeMB: 500,
128
+ },
109
129
  );
110
130
 
111
131
  const stored = await settingsService.get("SITE_FAVICON_APPLE_TOUCH");
@@ -120,7 +140,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
120
140
 
121
141
  await settingsService.uploadAvatar(
122
142
  { file },
123
- { media: mediaService, storage, storageProvider: "r2" },
143
+ {
144
+ media: mediaService,
145
+ storage,
146
+ storageProvider: "r2",
147
+ maxFileSizeMB: 500,
148
+ },
124
149
  );
125
150
 
126
151
  const stored = await settingsService.get("SITE_FAVICON_VERSION");
@@ -135,19 +160,29 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
135
160
  await expect(
136
161
  settingsService.uploadAvatar(
137
162
  { file },
138
- { media: mediaService, storage, storageProvider: "r2" },
163
+ {
164
+ media: mediaService,
165
+ storage,
166
+ storageProvider: "r2",
167
+ maxFileSizeMB: 500,
168
+ },
139
169
  ),
140
170
  ).rejects.toThrow("File type not allowed");
141
171
  });
142
172
 
143
173
  it("throws ValidationError for oversized file", async () => {
144
174
  const storage = createMockStorage();
145
- const file = createMockFile("big.png", "image/png", 20 * 1024 * 1024);
175
+ const file = createMockFile("big.png", "image/png", 501 * 1024 * 1024);
146
176
 
147
177
  await expect(
148
178
  settingsService.uploadAvatar(
149
179
  { file },
150
- { media: mediaService, storage, storageProvider: "r2" },
180
+ {
181
+ media: mediaService,
182
+ storage,
183
+ storageProvider: "r2",
184
+ maxFileSizeMB: 500,
185
+ },
151
186
  ),
152
187
  ).rejects.toThrow("File too large");
153
188
  });
@@ -93,7 +93,13 @@ dashIndexRoutes.get("/", async (c) => {
93
93
  ]);
94
94
 
95
95
  return c.html(
96
- <DashLayout c={c} title="Dashboard" siteName={siteName} currentPath="/dash">
96
+ <DashLayout
97
+ c={c}
98
+ title="Dashboard"
99
+ siteName={siteName}
100
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
101
+ currentPath="/dash"
102
+ >
97
103
  <DashboardContent
98
104
  publishedCount={publishedCount}
99
105
  draftCount={draftCount}
@@ -29,6 +29,7 @@ mediaRoutes.get("/", async (c) => {
29
29
  c={c}
30
30
  title="Media"
31
31
  siteName={siteName}
32
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
32
33
  currentPath="/dash/media"
33
34
  >
34
35
  <MediaListContent
@@ -36,6 +37,7 @@ mediaRoutes.get("/", async (c) => {
36
37
  r2PublicUrl={c.var.appConfig.r2PublicUrl}
37
38
  imageTransformUrl={c.var.appConfig.imageTransformUrl}
38
39
  s3PublicUrl={c.var.appConfig.s3PublicUrl}
40
+ uploadMaxFileSize={c.var.appConfig.uploadMaxFileSize}
39
41
  />
40
42
  </DashLayout>,
41
43
  );
@@ -108,6 +110,7 @@ mediaRoutes.get("/:id", async (c) => {
108
110
  c={c}
109
111
  title={media.originalName}
110
112
  siteName={siteName}
113
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
111
114
  currentPath="/dash/media"
112
115
  >
113
116
  <ViewMediaContent
@@ -114,6 +114,7 @@ pagesRoutes.get("/", async (c) => {
114
114
  c={c}
115
115
  title="Pages"
116
116
  siteName={siteName}
117
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
117
118
  currentPath="/dash/pages"
118
119
  >
119
120
  <PagesContent pages={pages} />
@@ -128,6 +129,7 @@ pagesRoutes.get("/new", async (c) => {
128
129
  c={c}
129
130
  title="New Page"
130
131
  siteName={siteName}
132
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
131
133
  currentPath="/dash/pages"
132
134
  >
133
135
  <NewPageContent />
@@ -144,7 +146,8 @@ pagesRoutes.post("/", async (c) => {
144
146
  parsed.error.issues[0]?.message ??
145
147
  i18n._(
146
148
  msg({
147
- message: "Invalid input",
149
+ message:
150
+ "Something doesn't look right. Check the form and try again.",
148
151
  comment: "@context: Fallback validation error for page form",
149
152
  }),
150
153
  );
@@ -174,6 +177,7 @@ pagesRoutes.get("/:id", async (c) => {
174
177
  c={c}
175
178
  title={page.title || "Page"}
176
179
  siteName={siteName}
180
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
177
181
  currentPath="/dash/pages"
178
182
  >
179
183
  <ViewPageContent page={page} />
@@ -194,6 +198,7 @@ pagesRoutes.get("/:id/edit", async (c) => {
194
198
  c={c}
195
199
  title={`Edit: ${page.title || "Page"}`}
196
200
  siteName={siteName}
201
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
197
202
  currentPath="/dash/pages"
198
203
  >
199
204
  <EditPageContent page={page} />
@@ -213,7 +218,8 @@ pagesRoutes.post("/:id", async (c) => {
213
218
  parsed.error.issues[0]?.message ??
214
219
  i18n._(
215
220
  msg({
216
- message: "Invalid input",
221
+ message:
222
+ "Something doesn't look right. Check the form and try again.",
217
223
  comment: "@context: Fallback validation error for page form",
218
224
  }),
219
225
  );
@@ -81,6 +81,7 @@ postsRoutes.get("/", async (c) => {
81
81
  c={c}
82
82
  title="Posts"
83
83
  siteName={siteName}
84
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
84
85
  currentPath="/dash/posts"
85
86
  >
86
87
  <PostsListContent posts={postViews} />
@@ -98,6 +99,7 @@ postsRoutes.get("/new", async (c) => {
98
99
  c={c}
99
100
  title="New Post"
100
101
  siteName={siteName}
102
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
101
103
  currentPath="/dash/posts"
102
104
  >
103
105
  <NewPostContent collections={collections} />
@@ -120,7 +122,7 @@ postsRoutes.post("/", async (c) => {
120
122
  title: body.title || undefined,
121
123
  body: body.body,
122
124
  status: body.status,
123
- featured: body.featured,
125
+ visibility: body.visibility,
124
126
  pinned: body.pinned,
125
127
  url: body.url || undefined,
126
128
  quoteText: body.quoteText || undefined,
@@ -236,6 +238,7 @@ postsRoutes.get("/:id", async (c) => {
236
238
  c={c}
237
239
  title={pageTitle}
238
240
  siteName={siteName}
241
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
239
242
  currentPath="/dash/posts"
240
243
  >
241
244
  <ViewPostContent post={postView} />
@@ -265,6 +268,7 @@ postsRoutes.get("/:id/edit", async (c) => {
265
268
  c={c}
266
269
  title={`Edit: ${post.title || "Post"}`}
267
270
  siteName={siteName}
271
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
268
272
  currentPath="/dash/posts"
269
273
  >
270
274
  <EditPostContent
@@ -299,7 +303,7 @@ postsRoutes.post("/:id", async (c) => {
299
303
  title: body.title || null,
300
304
  body: body.body || null,
301
305
  status: body.status,
302
- featured: body.featured,
306
+ visibility: body.visibility,
303
307
  pinned: body.pinned,
304
308
  url: body.url || null,
305
309
  quoteText: body.quoteText || null,
@@ -11,7 +11,6 @@ import type { Bindings, Redirect } from "../../types.js";
11
11
  import type { AppVariables } from "../../types/app-context.js";
12
12
  import { DashLayout } from "../../ui/layouts/DashLayout.js";
13
13
  import { EmptyState, ListItemRow, ActionButtons } from "../../ui/dash/index.js";
14
- import { SettingsNav } from "../../ui/dash/settings/SettingsNav.js";
15
14
  import { dsRedirect } from "../../lib/sse.js";
16
15
  import { RedirectTypeSchema, parseValidated } from "../../lib/schemas.js";
17
16
 
@@ -30,8 +29,6 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
30
29
 
31
30
  return (
32
31
  <>
33
- <SettingsNav currentTab="redirects" />
34
-
35
32
  <div class="flex items-center justify-between mb-6">
36
33
  <h2 class="text-lg font-medium">
37
34
  {t({
@@ -50,7 +47,8 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
50
47
  {redirects.length === 0 ? (
51
48
  <EmptyState
52
49
  message={t({
53
- message: "No redirects configured.",
50
+ message:
51
+ "No redirects yet. Create one to forward traffic from old URLs.",
54
52
  comment: "@context: Empty state message",
55
53
  })}
56
54
  ctaText={t({
@@ -76,7 +74,7 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
76
74
  >
77
75
  <div class="flex items-center gap-2">
78
76
  <code class="text-sm bg-muted px-1 rounded">{r.fromPath}</code>
79
- <span class="text-muted-foreground">→</span>
77
+ <span class="text-muted-foreground">&rarr;</span>
80
78
  <code class="text-sm bg-muted px-1 rounded">{r.toPath}</code>
81
79
  <span class="badge-outline">{r.type}</span>
82
80
  </div>
@@ -93,8 +91,6 @@ function NewRedirectContent() {
93
91
 
94
92
  return (
95
93
  <>
96
- <SettingsNav currentTab="redirects" />
97
-
98
94
  <h2 class="text-lg font-medium mb-6">
99
95
  {t({ message: "New Redirect", comment: "@context: Page heading" })}
100
96
  </h2>
@@ -203,6 +199,12 @@ function NewRedirectContent() {
203
199
  );
204
200
  }
205
201
 
202
+ const BREADCRUMB = {
203
+ parent: "Settings",
204
+ parentHref: "/dash/settings",
205
+ current: "Redirects",
206
+ };
207
+
206
208
  // List redirects
207
209
  redirectsRoutes.get("/", async (c) => {
208
210
  const siteName = c.var.appConfig.siteName;
@@ -211,9 +213,11 @@ redirectsRoutes.get("/", async (c) => {
211
213
  return c.html(
212
214
  <DashLayout
213
215
  c={c}
214
- title="Settings"
216
+ title="Redirects"
215
217
  siteName={siteName}
218
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
216
219
  currentPath="/dash/settings"
220
+ breadcrumb={BREADCRUMB}
217
221
  >
218
222
  <RedirectsListContent redirects={redirects} />
219
223
  </DashLayout>,
@@ -227,9 +231,11 @@ redirectsRoutes.get("/new", async (c) => {
227
231
  return c.html(
228
232
  <DashLayout
229
233
  c={c}
230
- title="Settings"
234
+ title="Redirects"
231
235
  siteName={siteName}
236
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
232
237
  currentPath="/dash/settings"
238
+ breadcrumb={BREADCRUMB}
233
239
  >
234
240
  <NewRedirectContent />
235
241
  </DashLayout>,