@jant/core 0.6.8 → 0.6.10

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 (77) hide show
  1. package/bin/commands/uploads/cleanup.js +1 -0
  2. package/dist/{app-9P4rVCe2.js → app-CGHkOdme.js} +3450 -3121
  3. package/dist/app-D24n0DoH.js +6 -0
  4. package/dist/client/.vite/manifest.json +3 -3
  5. package/dist/client/_assets/{client-CXnEhyyv.js → client-DYrWuaIk.js} +1 -1
  6. package/dist/client/_assets/{client-auth-CSItbyU8.js → client-auth-B5Re0uCd.js} +187 -167
  7. package/dist/client/_assets/client-xWDl78yi.css +2 -0
  8. package/dist/{export-Be082J0n.js → export-DY1v5Iqu.js} +2 -2
  9. package/dist/{github-sync-D1Cw8mOY.js → github-sync-2_T7nbOv.js} +1 -1
  10. package/dist/{github-sync-_kPWM4m9.js → github-sync-LefaslGJ.js} +2 -2
  11. package/dist/index.js +3 -3
  12. package/dist/node.js +4 -4
  13. package/package.json +1 -1
  14. package/src/client/components/__tests__/jant-settings-avatar.test.ts +8 -2
  15. package/src/client/components/__tests__/jant-settings-general.test.ts +64 -12
  16. package/src/client/components/jant-compose-dialog.ts +12 -0
  17. package/src/client/components/jant-settings-general.ts +74 -21
  18. package/src/client/components/settings-types.ts +13 -0
  19. package/src/client/settings-bridge.ts +3 -0
  20. package/src/client/tiptap/__tests__/link-toolbar.test.ts +41 -0
  21. package/src/client/tiptap/__tests__/mark-exit.test.ts +99 -0
  22. package/src/client/tiptap/bubble-menu.ts +37 -4
  23. package/src/client/tiptap/link-toolbar.ts +63 -1
  24. package/src/db/migrations/0026_absent_rhodey.sql +14 -0
  25. package/src/db/migrations/meta/0026_snapshot.json +2511 -0
  26. package/src/db/migrations/meta/_journal.json +7 -0
  27. package/src/db/migrations/pg/0024_high_violations.sql +14 -0
  28. package/src/db/migrations/pg/meta/0024_snapshot.json +3204 -0
  29. package/src/db/migrations/pg/meta/_journal.json +7 -0
  30. package/src/db/pg/schema.ts +36 -0
  31. package/src/db/schema.ts +36 -0
  32. package/src/i18n/__tests__/middleware.test.ts +46 -0
  33. package/src/i18n/locales/settings/en.po +282 -27
  34. package/src/i18n/locales/settings/en.ts +1 -1
  35. package/src/i18n/locales/settings/zh-Hans.po +282 -27
  36. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  37. package/src/i18n/locales/settings/zh-Hant.po +282 -27
  38. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  39. package/src/i18n/middleware.ts +17 -8
  40. package/src/i18n/supported-locales.ts +5 -4
  41. package/src/lib/__tests__/feed.test.ts +5 -1
  42. package/src/lib/feed.ts +6 -3
  43. package/src/lib/ids.ts +1 -0
  44. package/src/lib/resolve-config.ts +1 -0
  45. package/src/lib/upload.ts +14 -0
  46. package/src/routes/api/__tests__/settings.test.ts +1 -4
  47. package/src/routes/api/__tests__/upload.test.ts +2 -0
  48. package/src/routes/api/internal/__tests__/uploads.test.ts +19 -1
  49. package/src/routes/api/settings.ts +2 -1
  50. package/src/routes/auth/__tests__/setup.test.ts +14 -0
  51. package/src/routes/dash/__tests__/settings-avatar.test.ts +35 -17
  52. package/src/routes/dash/settings.tsx +22 -4
  53. package/src/routes/feed/__tests__/feed.test.ts +58 -19
  54. package/src/routes/feed/feed.ts +37 -28
  55. package/src/routes/pages/featured.tsx +17 -0
  56. package/src/routes/pages/latest.tsx +25 -0
  57. package/src/services/__tests__/media.test.ts +191 -30
  58. package/src/services/__tests__/settings.test.ts +55 -0
  59. package/src/services/bootstrap.ts +7 -0
  60. package/src/services/export-theme/layouts/_default/baseof.html +2 -1
  61. package/src/services/media.ts +169 -42
  62. package/src/services/post.ts +1 -1
  63. package/src/services/settings.ts +49 -15
  64. package/src/services/upload-session.ts +13 -3
  65. package/src/styles/tokens.css +21 -4
  66. package/src/styles/ui.css +44 -1
  67. package/src/types/bindings.ts +1 -0
  68. package/src/types/config.ts +13 -0
  69. package/src/ui/__tests__/color-themes.test.ts +2 -2
  70. package/src/ui/color-themes.ts +32 -0
  71. package/src/ui/dash/appearance/ColorThemeContent.tsx +264 -29
  72. package/src/ui/dash/settings/GeneralContent.tsx +54 -4
  73. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +3 -2
  74. package/src/ui/layouts/BaseLayout.tsx +3 -2
  75. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +17 -4
  76. package/dist/app-DaxS_Cz-.js +0 -6
  77. package/dist/client/_assets/client-C6peCkkD.css +0 -2
@@ -15,6 +15,8 @@ import { createMediaService } from "../../../services/media.js";
15
15
  import { DEFAULT_APP_PORT } from "../../../lib/env.js";
16
16
  import { resolveConfig } from "../../../lib/resolve-config.js";
17
17
  import { feedRoutes } from "../feed.js";
18
+ import { latestRoutes } from "../../pages/latest.js";
19
+ import { featuredRoutes } from "../../pages/featured.js";
18
20
  import type { Database } from "../../../db/index.js";
19
21
 
20
22
  type Env = { Bindings: Bindings; Variables: AppVariables };
@@ -58,6 +60,9 @@ function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
58
60
  });
59
61
 
60
62
  app.route("/feed", feedRoutes);
63
+ // The canonical latest/featured feeds live in their page route groups.
64
+ app.route("/latest", latestRoutes);
65
+ app.route("/featured", featuredRoutes);
61
66
 
62
67
  return { app, services, db: db as unknown as Database };
63
68
  }
@@ -166,7 +171,7 @@ describe("Atom Feed Routes", () => {
166
171
  });
167
172
  });
168
173
 
169
- describe("/feed/latest — latest public posts", () => {
174
+ describe("/latest/feed — latest public posts", () => {
170
175
  it("returns public published posts and excludes hidden, private, and draft posts", async () => {
171
176
  const { app, services } = createFeedTestApp();
172
177
 
@@ -204,7 +209,7 @@ describe("Atom Feed Routes", () => {
204
209
  status: "draft",
205
210
  });
206
211
 
207
- const res = await app.request("/feed/latest");
212
+ const res = await app.request("/latest/feed");
208
213
  expect(res.status).toBe(200);
209
214
 
210
215
  const xml = await res.text();
@@ -237,7 +242,7 @@ describe("Atom Feed Routes", () => {
237
242
  status: "published",
238
243
  });
239
244
 
240
- const res = await app.request("/feed/latest?format=note");
245
+ const res = await app.request("/latest/feed?format=note");
241
246
  expect(res.status).toBe(200);
242
247
 
243
248
  const xml = await res.text();
@@ -262,7 +267,7 @@ describe("Atom Feed Routes", () => {
262
267
  status: "published",
263
268
  });
264
269
 
265
- const res = await app.request("/feed/latest?format=invalid");
270
+ const res = await app.request("/latest/feed?format=invalid");
266
271
  expect(res.status).toBe(200);
267
272
 
268
273
  const xml = await res.text();
@@ -273,14 +278,14 @@ describe("Atom Feed Routes", () => {
273
278
  it("returns Atom content type", async () => {
274
279
  const { app } = createFeedTestApp();
275
280
 
276
- const res = await app.request("/feed/latest");
281
+ const res = await app.request("/latest/feed");
277
282
  expect(res.headers.get("Content-Type")).toBe(
278
283
  "application/atom+xml; charset=utf-8",
279
284
  );
280
285
  });
281
286
  });
282
287
 
283
- describe("/feed/featured — featured posts", () => {
288
+ describe("/featured/feed — featured posts", () => {
284
289
  it("returns only featured posts", async () => {
285
290
  const { app, services } = createFeedTestApp();
286
291
 
@@ -298,7 +303,7 @@ describe("Atom Feed Routes", () => {
298
303
  featured: true,
299
304
  });
300
305
 
301
- const res = await app.request("/feed/featured");
306
+ const res = await app.request("/featured/feed");
302
307
  expect(res.status).toBe(200);
303
308
 
304
309
  const xml = await res.text();
@@ -307,6 +312,24 @@ describe("Atom Feed Routes", () => {
307
312
  });
308
313
  });
309
314
 
315
+ describe("legacy feed path redirects", () => {
316
+ it("redirects /feed/latest to /latest/feed, preserving ?format=", async () => {
317
+ const { app } = createFeedTestApp();
318
+
319
+ const res = await app.request("/feed/latest?format=note");
320
+ expect(res.status).toBe(308);
321
+ expect(res.headers.get("Location")).toBe("/latest/feed?format=note");
322
+ });
323
+
324
+ it("redirects /feed/featured to /featured/feed", async () => {
325
+ const { app } = createFeedTestApp();
326
+
327
+ const res = await app.request("/feed/featured");
328
+ expect(res.status).toBe(308);
329
+ expect(res.headers.get("Location")).toBe("/featured/feed");
330
+ });
331
+ });
332
+
310
333
  describe("legacy atom.xml redirects", () => {
311
334
  it("redirects /feed/atom.xml to /feed", async () => {
312
335
  const { app } = createFeedTestApp();
@@ -316,38 +339,54 @@ describe("Atom Feed Routes", () => {
316
339
  expect(res.headers.get("Location")).toBe("/feed");
317
340
  });
318
341
 
319
- it("redirects /feed/latest/atom.xml to /feed/latest", async () => {
342
+ it("redirects /feed/latest/atom.xml to /latest/feed", async () => {
320
343
  const { app } = createFeedTestApp();
321
344
 
322
345
  const res = await app.request("/feed/latest/atom.xml");
323
346
  expect(res.status).toBe(308);
324
- expect(res.headers.get("Location")).toBe("/feed/latest");
347
+ expect(res.headers.get("Location")).toBe("/latest/feed");
325
348
  });
326
349
 
327
- it("redirects /feed/featured/atom.xml to /feed/featured", async () => {
350
+ it("redirects /feed/featured/atom.xml to /featured/feed", async () => {
328
351
  const { app } = createFeedTestApp();
329
352
 
330
353
  const res = await app.request("/feed/featured/atom.xml");
331
354
  expect(res.status).toBe(308);
332
- expect(res.headers.get("Location")).toBe("/feed/featured");
355
+ expect(res.headers.get("Location")).toBe("/featured/feed");
356
+ });
357
+
358
+ it("redirects /latest/feed/atom.xml to /latest/feed", async () => {
359
+ const { app } = createFeedTestApp();
360
+
361
+ const res = await app.request("/latest/feed/atom.xml");
362
+ expect(res.status).toBe(308);
363
+ expect(res.headers.get("Location")).toBe("/latest/feed");
364
+ });
365
+
366
+ it("redirects /featured/feed/atom.xml to /featured/feed", async () => {
367
+ const { app } = createFeedTestApp();
368
+
369
+ const res = await app.request("/featured/feed/atom.xml");
370
+ expect(res.status).toBe(308);
371
+ expect(res.headers.get("Location")).toBe("/featured/feed");
333
372
  });
334
373
  });
335
374
 
336
375
  describe("legacy feed aliases", () => {
337
- it("redirects /feed/all to /feed/latest", async () => {
376
+ it("redirects /feed/all to /latest/feed", async () => {
338
377
  const { app } = createFeedTestApp();
339
378
 
340
379
  const res = await app.request("/feed/all?format=note");
341
380
  expect(res.status).toBe(308);
342
- expect(res.headers.get("Location")).toBe("/feed/latest?format=note");
381
+ expect(res.headers.get("Location")).toBe("/latest/feed?format=note");
343
382
  });
344
383
 
345
- it("redirects /feed/all/atom.xml to /feed/latest", async () => {
384
+ it("redirects /feed/all/atom.xml to /latest/feed", async () => {
346
385
  const { app } = createFeedTestApp();
347
386
 
348
387
  const res = await app.request("/feed/all/atom.xml?format=link");
349
388
  expect(res.status).toBe(308);
350
- expect(res.headers.get("Location")).toBe("/feed/latest");
389
+ expect(res.headers.get("Location")).toBe("/latest/feed");
351
390
  });
352
391
  });
353
392
 
@@ -388,7 +427,7 @@ describe("Atom Feed Routes", () => {
388
427
  });
389
428
  }
390
429
 
391
- const res = await app.request("/feed/latest");
430
+ const res = await app.request("/latest/feed");
392
431
  expect(res.status).toBe(200);
393
432
 
394
433
  const xml = await res.text();
@@ -413,7 +452,7 @@ describe("Atom Feed Routes", () => {
413
452
  });
414
453
  }
415
454
 
416
- const res = await app.request("/feed/latest");
455
+ const res = await app.request("/latest/feed");
417
456
  expect(res.status).toBe(200);
418
457
 
419
458
  const xml = await res.text();
@@ -436,7 +475,7 @@ describe("Atom Feed Routes", () => {
436
475
  });
437
476
  }
438
477
 
439
- const res = await app.request("/feed/featured");
478
+ const res = await app.request("/featured/feed");
440
479
  expect(res.status).toBe(200);
441
480
 
442
481
  const xml = await res.text();
@@ -463,7 +502,7 @@ describe("Atom Feed Routes", () => {
463
502
  replyToId: root.id,
464
503
  });
465
504
 
466
- const res = await app.request("/feed/latest");
505
+ const res = await app.request("/latest/feed");
467
506
  expect(res.status).toBe(200);
468
507
 
469
508
  const xml = await res.text();
@@ -1,12 +1,17 @@
1
1
  /**
2
2
  * Atom Feed Routes
3
3
  *
4
- * Feed hierarchy:
5
- * - /feed — site main feed (latest or featured, site-configurable)
6
- * - /feed/latest — latest public posts
7
- * - /feed/featured featured posts only
8
- * - /{slug}/feed single-collection feed (handled in page routes)
4
+ * Feed hierarchy (resource-first: a feed is a sub-resource of the page it
5
+ * represents, so it lives at `{page}/feed`):
6
+ * - /feed site main feed (latest or featured, site-configurable; feed of `/`)
7
+ * - /latest/feed latest public posts (handled in pages/latest)
8
+ * - /featured/feed featured posts only (handled in pages/featured)
9
+ * - /archive/feed — full archive incl. Hidden-from-Latest (handled in pages/archive)
10
+ * - /{slug}/feed — single-collection feed (handled in page routes)
9
11
  * - /collections/{slug}/feed — combined collection feed (handled in collection routes)
12
+ *
13
+ * Legacy: /feed/latest and /feed/featured 308-redirect to the canonical
14
+ * /latest/feed and /featured/feed. Kept indefinitely for old subscribers.
10
15
  */
11
16
 
12
17
  import { msg } from "@lingui/core/macro";
@@ -255,11 +260,15 @@ async function buildFeaturedFeedData(
255
260
  /**
256
261
  * Build FeedData from the Hono context.
257
262
  *
263
+ * Exported so the canonical latest/featured feeds (served from the
264
+ * `/latest/feed` and `/featured/feed` page route groups) can reuse the
265
+ * same feed-building logic.
266
+ *
258
267
  * @param c - Hono context
259
268
  * @param opts - Filter options for the feed
260
269
  * @returns Feed data ready for rendering
261
270
  */
262
- async function buildFeedData(
271
+ export async function buildFeedData(
263
272
  c: Context<Env>,
264
273
  opts: FeedOptions,
265
274
  ): Promise<FeedData> {
@@ -311,7 +320,7 @@ async function buildFeedData(
311
320
  * Parse and validate the `format` query parameter.
312
321
  * Returns a valid Format or undefined if missing/invalid.
313
322
  */
314
- function parseFormatQuery(c: Context<Env>): Format | undefined {
323
+ export function parseFormatQuery(c: Context<Env>): Format | undefined {
315
324
  const raw = c.req.query("format");
316
325
  if (raw && (FORMATS as readonly string[]).includes(raw)) {
317
326
  return raw as Format;
@@ -319,7 +328,7 @@ function parseFormatQuery(c: Context<Env>): Format | undefined {
319
328
  return undefined;
320
329
  }
321
330
 
322
- function renderFeed(xml: string) {
331
+ export function renderFeed(xml: string) {
323
332
  return new Response(xml, {
324
333
  headers: {
325
334
  "Content-Type": "application/atom+xml; charset=utf-8",
@@ -335,24 +344,24 @@ feedRoutes.get("/", async (c) => {
335
344
  return renderFeed(defaultFeedRenderer(feedData));
336
345
  });
337
346
 
338
- // Atom — /feed/latest
339
- feedRoutes.get("/latest", async (c) => {
340
- const format = parseFormatQuery(c);
341
- const feedData = await buildFeedData(c, {
342
- kind: "latest",
343
- selfPath: "/feed/latest",
344
- format,
345
- });
346
- return renderFeed(defaultFeedRenderer(feedData));
347
+ // Legacy — /feed/latest moved to the canonical /latest/feed. Kept
348
+ // indefinitely as a 308 so old subscribers don't break; preserves the
349
+ // ?format= query string.
350
+ feedRoutes.get("/latest", (c) => {
351
+ const sitePathPrefix = c.var.appConfig.sitePathPrefix;
352
+ const qs = c.req.url.includes("?")
353
+ ? c.req.url.slice(c.req.url.indexOf("?"))
354
+ : "";
355
+ return c.redirect(
356
+ `${toPublicPath("/latest/feed", sitePathPrefix)}${qs}`,
357
+ 308,
358
+ );
347
359
  });
348
360
 
349
- // Atom — /feed/featured
350
- feedRoutes.get("/featured", async (c) => {
351
- const feedData = await buildFeedData(c, {
352
- kind: "featured",
353
- selfPath: "/feed/featured",
354
- });
355
- return renderFeed(defaultFeedRenderer(feedData));
361
+ // Legacy — /feed/featured moved to the canonical /featured/feed.
362
+ feedRoutes.get("/featured", (c) => {
363
+ const sitePathPrefix = c.var.appConfig.sitePathPrefix;
364
+ return c.redirect(toPublicPath("/featured/feed", sitePathPrefix), 308);
356
365
  });
357
366
 
358
367
  // Legacy aliases
@@ -362,7 +371,7 @@ feedRoutes.get("/all", (c) => {
362
371
  ? c.req.url.slice(c.req.url.indexOf("?"))
363
372
  : "";
364
373
  return c.redirect(
365
- `${toPublicPath("/feed/latest", sitePathPrefix)}${qs}`,
374
+ `${toPublicPath("/latest/feed", sitePathPrefix)}${qs}`,
366
375
  308,
367
376
  );
368
377
  });
@@ -374,13 +383,13 @@ feedRoutes.get("/atom.xml", (c) => {
374
383
  });
375
384
  feedRoutes.get("/latest/atom.xml", (c) => {
376
385
  const sitePathPrefix = c.var.appConfig.sitePathPrefix;
377
- return c.redirect(toPublicPath("/feed/latest", sitePathPrefix), 308);
386
+ return c.redirect(toPublicPath("/latest/feed", sitePathPrefix), 308);
378
387
  });
379
388
  feedRoutes.get("/featured/atom.xml", (c) => {
380
389
  const sitePathPrefix = c.var.appConfig.sitePathPrefix;
381
- return c.redirect(toPublicPath("/feed/featured", sitePathPrefix), 308);
390
+ return c.redirect(toPublicPath("/featured/feed", sitePathPrefix), 308);
382
391
  });
383
392
  feedRoutes.get("/all/atom.xml", (c) => {
384
393
  const sitePathPrefix = c.var.appConfig.sitePathPrefix;
385
- return c.redirect(toPublicPath("/feed/latest", sitePathPrefix), 308);
394
+ return c.redirect(toPublicPath("/latest/feed", sitePathPrefix), 308);
386
395
  });
@@ -15,6 +15,8 @@ import { buildPageTitle } from "../../lib/page-title.js";
15
15
  import { renderPublicPage } from "../../lib/render.js";
16
16
  import { assembleFeaturedTimeline } from "../../lib/timeline.js";
17
17
  import { toPublicPath } from "../../lib/url.js";
18
+ import { defaultFeedRenderer } from "../../lib/feed.js";
19
+ import { buildFeedData, renderFeed } from "../feed/feed.js";
18
20
  import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
19
21
 
20
22
  type Env = { Bindings: Bindings; Variables: AppVariables };
@@ -58,3 +60,18 @@ featuredRoutes.get("/", async (c) => {
58
60
  ),
59
61
  });
60
62
  });
63
+
64
+ // Atom — /featured/feed (canonical featured feed)
65
+ featuredRoutes.get("/feed", async (c) => {
66
+ const feedData = await buildFeedData(c, {
67
+ kind: "featured",
68
+ selfPath: "/featured/feed",
69
+ });
70
+ return renderFeed(defaultFeedRenderer(feedData));
71
+ });
72
+
73
+ // Legacy atom.xml suffix → canonical /featured/feed
74
+ featuredRoutes.get("/feed/atom.xml", (c) => {
75
+ const sitePathPrefix = c.var.appConfig.sitePathPrefix;
76
+ return c.redirect(toPublicPath("/featured/feed", sitePathPrefix), 308);
77
+ });
@@ -18,6 +18,8 @@ import { buildPageTitle } from "../../lib/page-title.js";
18
18
  import { renderPublicPage } from "../../lib/render.js";
19
19
  import { assembleTimeline } from "../../lib/timeline.js";
20
20
  import { toPublicPath } from "../../lib/url.js";
21
+ import { defaultFeedRenderer } from "../../lib/feed.js";
22
+ import { buildFeedData, parseFormatQuery, renderFeed } from "../feed/feed.js";
21
23
  import { HomePage } from "../../ui/pages/HomePage.js";
22
24
 
23
25
  type Env = { Bindings: Bindings; Variables: AppVariables };
@@ -65,3 +67,26 @@ latestRoutes.get("/", async (c) => {
65
67
  ),
66
68
  });
67
69
  });
70
+
71
+ // Atom — /latest/feed (canonical latest feed; accepts ?format=note|link|quote)
72
+ latestRoutes.get("/feed", async (c) => {
73
+ const format = parseFormatQuery(c);
74
+ const feedData = await buildFeedData(c, {
75
+ kind: "latest",
76
+ selfPath: "/latest/feed",
77
+ format,
78
+ });
79
+ return renderFeed(defaultFeedRenderer(feedData));
80
+ });
81
+
82
+ // Legacy atom.xml suffix → canonical /latest/feed (preserves ?format=)
83
+ latestRoutes.get("/feed/atom.xml", (c) => {
84
+ const sitePathPrefix = c.var.appConfig.sitePathPrefix;
85
+ const qs = c.req.url.includes("?")
86
+ ? c.req.url.slice(c.req.url.indexOf("?"))
87
+ : "";
88
+ return c.redirect(
89
+ `${toPublicPath("/latest/feed", sitePathPrefix)}${qs}`,
90
+ 308,
91
+ );
92
+ });