@jant/core 0.6.11 → 0.6.13

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 (139) hide show
  1. package/bin/commands/import-site.js +47 -30
  2. package/bin/commands/migrate-text-attachments.js +1 -3
  3. package/bin/commands/site/export.js +1 -3
  4. package/bin/commands/telegram/register-webhooks.js +9 -3
  5. package/bin/lib/site-snapshot.js +1 -5
  6. package/dist/app-CWJFPjuR.js +6 -0
  7. package/dist/{app-CpmficmQ.js → app-HnzmsaqU.js} +721 -473
  8. package/dist/client/.vite/manifest.json +3 -3
  9. package/dist/client/_assets/{client-Dd9U383b.js → client-3_OaxhTs.js} +1 -1
  10. package/dist/client/_assets/client-DCrcHVJl.css +2 -0
  11. package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-TsYq90xm.js} +625 -561
  12. package/dist/{export-Ba7NJImL.js → export-Dfbq9aot.js} +12 -15
  13. package/dist/{github-sync-Cb4_6_i7.js → github-sync--PSoE-Ne.js} +1 -1
  14. package/dist/{github-sync-BD4w2m8-.js → github-sync-BGTLx8Mf.js} +2 -2
  15. package/dist/index.js +3 -3
  16. package/dist/node.js +4 -4
  17. package/package.json +1 -1
  18. package/src/__tests__/export-service.test.ts +1 -2
  19. package/src/__tests__/helpers/app.ts +3 -0
  20. package/src/__tests__/helpers/export-fixtures.ts +0 -1
  21. package/src/__tests__/import-site-command.test.ts +76 -2
  22. package/src/__tests__/vite-dev-plugins.test.ts +34 -0
  23. package/src/client/__tests__/compose-launch.test.ts +50 -1
  24. package/src/client/__tests__/compose-shortcuts.test.ts +74 -18
  25. package/src/client/__tests__/site-header-nav.test.ts +36 -2
  26. package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
  27. package/src/client/components/__tests__/jant-command-palette.test.ts +63 -0
  28. package/src/client/components/__tests__/jant-compose-dialog.test.ts +33 -0
  29. package/src/client/components/__tests__/jant-compose-editor-rehost-notice.test.ts +20 -1
  30. package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
  31. package/src/client/components/__tests__/jant-nav-manager.test.ts +181 -20
  32. package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
  33. package/src/client/components/__tests__/jant-settings-general.test.ts +72 -0
  34. package/src/client/components/compose-title-from-heading.ts +66 -0
  35. package/src/client/components/compose-types.ts +8 -0
  36. package/src/client/components/inline-image-issues.ts +17 -0
  37. package/src/client/components/jant-command-palette.ts +67 -2
  38. package/src/client/components/jant-compose-dialog.ts +10 -2
  39. package/src/client/components/jant-compose-editor.ts +78 -1
  40. package/src/client/components/jant-compose-fullscreen.ts +2 -0
  41. package/src/client/components/jant-nav-manager.ts +382 -152
  42. package/src/client/components/jant-settings-general.ts +56 -0
  43. package/src/client/components/nav-manager-types.ts +14 -10
  44. package/src/client/components/settings-types.ts +30 -0
  45. package/src/client/compose-launch.ts +30 -0
  46. package/src/client/compose-shortcuts.ts +46 -3
  47. package/src/client/site-header-nav.d.ts +1 -0
  48. package/src/client/site-header-nav.js +105 -47
  49. package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
  50. package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
  51. package/src/client/tiptap/__tests__/rehost-images.test.ts +66 -13
  52. package/src/client/tiptap/create-editor.ts +7 -0
  53. package/src/client/tiptap/extensions.ts +7 -2
  54. package/src/client/tiptap/image-input-rules.ts +48 -0
  55. package/src/client/tiptap/image-node.ts +189 -7
  56. package/src/client/tiptap/rehost-images.ts +5 -1
  57. package/src/client-auth.ts +0 -1
  58. package/src/i18n/__tests__/fallback.test.ts +24 -0
  59. package/src/i18n/locales/public/en.po +20 -4
  60. package/src/i18n/locales/public/en.ts +1 -1
  61. package/src/i18n/locales/public/zh-Hans.po +22 -6
  62. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  63. package/src/i18n/locales/public/zh-Hant.po +22 -6
  64. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  65. package/src/i18n/locales/settings/en.po +77 -20
  66. package/src/i18n/locales/settings/en.ts +1 -1
  67. package/src/i18n/locales/settings/zh-Hans.po +85 -28
  68. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  69. package/src/i18n/locales/settings/zh-Hant.po +79 -22
  70. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  71. package/src/lib/__tests__/feed.test.ts +96 -10
  72. package/src/lib/__tests__/navigation.test.ts +4 -37
  73. package/src/lib/__tests__/resolve-config.test.ts +6 -1
  74. package/src/lib/__tests__/view.test.ts +6 -30
  75. package/src/lib/feed.ts +63 -3
  76. package/src/lib/github-sync-site-config.ts +0 -2
  77. package/src/lib/hugo-markdown.ts +1 -1
  78. package/src/lib/navigation.ts +5 -22
  79. package/src/lib/post-display.ts +14 -8
  80. package/src/lib/resolve-config.ts +9 -2
  81. package/src/lib/site-header-fragment.tsx +37 -0
  82. package/src/lib/view.ts +2 -7
  83. package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
  84. package/src/preset.css +60 -18
  85. package/src/routes/api/__tests__/nav-items.test.ts +26 -0
  86. package/src/routes/api/export.ts +0 -2
  87. package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
  88. package/src/routes/api/internal/sites.ts +5 -5
  89. package/src/routes/api/nav-items.ts +25 -4
  90. package/src/routes/dash/settings.tsx +48 -3
  91. package/src/routes/feed/__tests__/feed.test.ts +42 -0
  92. package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
  93. package/src/routes/feed/feed.ts +1 -0
  94. package/src/routes/feed/sitemap.ts +2 -6
  95. package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
  96. package/src/routes/pages/archive.tsx +1 -0
  97. package/src/routes/pages/collection.tsx +1 -0
  98. package/src/routes/pages/featured.tsx +0 -4
  99. package/src/routes/pages/home.tsx +4 -51
  100. package/src/routes/pages/latest.tsx +2 -51
  101. package/src/routes/pages/page.tsx +44 -12
  102. package/src/services/__tests__/about-page.test.ts +116 -0
  103. package/src/services/__tests__/github-app-installations.test.ts +10 -10
  104. package/src/services/__tests__/navigation.test.ts +231 -1
  105. package/src/services/__tests__/post-timeline.test.ts +175 -2
  106. package/src/services/__tests__/post.test.ts +33 -0
  107. package/src/services/__tests__/settings.test.ts +5 -20
  108. package/src/services/about-page.ts +143 -0
  109. package/src/services/export-theme/assets/client-site.js +10 -10
  110. package/src/services/export-theme/layouts/_default/rss.xml +7 -16
  111. package/src/services/export-theme/layouts/index.html +26 -39
  112. package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
  113. package/src/services/export-theme/styles/main.css +46 -14
  114. package/src/services/export.ts +5 -10
  115. package/src/services/github-app-installations.ts +1 -3
  116. package/src/services/index.ts +27 -17
  117. package/src/services/navigation.ts +186 -1
  118. package/src/services/post.ts +103 -63
  119. package/src/services/settings.ts +0 -10
  120. package/src/services/site-admin.ts +0 -2
  121. package/src/services/upload-session.ts +4 -18
  122. package/src/styles/components.css +20 -0
  123. package/src/styles/tokens.css +28 -10
  124. package/src/styles/ui.css +349 -13
  125. package/src/types/bindings.ts +0 -1
  126. package/src/types/config.ts +3 -7
  127. package/src/types/views.ts +13 -0
  128. package/src/ui/compose/ComposeDialog.tsx +28 -0
  129. package/src/ui/dash/appearance/NavigationContent.tsx +58 -1
  130. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
  131. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  132. package/src/ui/dash/settings/GeneralContent.tsx +44 -0
  133. package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
  134. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
  135. package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
  136. package/src/ui/layouts/SiteLayout.tsx +249 -216
  137. package/dist/app-DqKkZenB.js +0 -6
  138. package/dist/client/_assets/client-BhHHVvSY.css +0 -2
  139. package/src/client/nav-manager-bridge.ts +0 -54
@@ -119,6 +119,8 @@ export interface PostFilters {
119
119
  hasReplies?: boolean;
120
120
  /** Explicit result sort order */
121
121
  sortOrder?: SortOrder;
122
+ /** Ignore global pinned ordering when sorting subscription/feed results. */
123
+ ignorePinnedSort?: boolean;
122
124
  limit?: number;
123
125
  cursor?: string;
124
126
  offset?: number; // offset for page-based pagination
@@ -137,6 +139,11 @@ interface ThreadRootPageOptions {
137
139
  offset?: number;
138
140
  }
139
141
 
142
+ interface CollectionFeedEntryOptions extends ThreadRootPageOptions {
143
+ /** Ignore per-collection pinned ordering when sorting collection feeds. */
144
+ ignoreCollectionPinnedSort?: boolean;
145
+ }
146
+
140
147
  interface CollectionThreadRootPageOptions extends ThreadRootPageOptions {
141
148
  sortOrder?: CollectionSortOrder;
142
149
  }
@@ -314,12 +321,12 @@ export interface PostService {
314
321
  /** List collection feed entries ordered by latest added-at timestamp */
315
322
  listCollectionFeedEntries(
316
323
  collectionId: string,
317
- options?: ThreadRootPageOptions,
324
+ options?: CollectionFeedEntryOptions,
318
325
  ): Promise<CollectionFeedEntry[]>;
319
326
  /** List collection feed entries for a union of collections */
320
327
  listCollectionFeedEntriesForCollections(
321
328
  collectionIds: string[],
322
- options?: ThreadRootPageOptions,
329
+ options?: CollectionFeedEntryOptions,
323
330
  ): Promise<CollectionFeedEntry[]>;
324
331
  /** Fetch all published, non-deleted posts for each requested thread root */
325
332
  getPublishedThreads(rootIds: string[]): Promise<Map<string, Post[]>>;
@@ -902,76 +909,90 @@ export function createPostService(
902
909
  const cursorSortTimestamp = getCursorSortTimestamp(cursorPost);
903
910
  const cursorRatingPresence = cursorPost.rating === null ? 0 : 1;
904
911
  const cursorRating = cursorPost.rating ?? -1;
912
+ const withPinnedSortKey = (
913
+ keys: [CursorSortKey, ...CursorSortKey[]],
914
+ ): [CursorSortKey, ...CursorSortKey[]] =>
915
+ filters.ignorePinnedSort
916
+ ? keys
917
+ : [
918
+ { direction: "desc", expr: pinnedSortExpr, value: cursorPinnedAt },
919
+ ...keys,
920
+ ];
905
921
 
906
922
  if (filters.featured || filters.sortOrder === undefined) {
907
923
  if (filters.featured) {
908
- return buildLexicographicCursorCondition([
909
- { direction: "desc", expr: pinnedSortExpr, value: cursorPinnedAt },
924
+ return buildLexicographicCursorCondition(
925
+ withPinnedSortKey([
926
+ {
927
+ direction: "desc",
928
+ expr: featuredSortExpr,
929
+ value: cursorFeaturedAt,
930
+ },
931
+ { direction: "desc", expr: posts.id, value: cursorPost.id },
932
+ ]),
933
+ );
934
+ }
935
+
936
+ return buildLexicographicCursorCondition(
937
+ withPinnedSortKey([
910
938
  {
911
939
  direction: "desc",
912
- expr: featuredSortExpr,
913
- value: cursorFeaturedAt,
940
+ expr: sortTimestampSortExpr,
941
+ value: cursorSortTimestamp,
914
942
  },
915
943
  { direction: "desc", expr: posts.id, value: cursorPost.id },
916
- ]);
917
- }
918
-
919
- return buildLexicographicCursorCondition([
920
- { direction: "desc", expr: pinnedSortExpr, value: cursorPinnedAt },
921
- {
922
- direction: "desc",
923
- expr: sortTimestampSortExpr,
924
- value: cursorSortTimestamp,
925
- },
926
- { direction: "desc", expr: posts.id, value: cursorPost.id },
927
- ]);
944
+ ]),
945
+ );
928
946
  }
929
947
 
930
948
  if (filters.sortOrder === "oldest") {
931
- return buildLexicographicCursorCondition([
932
- { direction: "desc", expr: pinnedSortExpr, value: cursorPinnedAt },
933
- {
934
- direction: "asc",
935
- expr: sortTimestampSortExpr,
936
- value: cursorSortTimestamp,
937
- },
938
- { direction: "asc", expr: posts.id, value: cursorPost.id },
939
- ]);
949
+ return buildLexicographicCursorCondition(
950
+ withPinnedSortKey([
951
+ {
952
+ direction: "asc",
953
+ expr: sortTimestampSortExpr,
954
+ value: cursorSortTimestamp,
955
+ },
956
+ { direction: "asc", expr: posts.id, value: cursorPost.id },
957
+ ]),
958
+ );
940
959
  }
941
960
 
942
961
  if (filters.sortOrder === "rating_desc") {
943
- return buildLexicographicCursorCondition([
944
- { direction: "desc", expr: pinnedSortExpr, value: cursorPinnedAt },
962
+ return buildLexicographicCursorCondition(
963
+ withPinnedSortKey([
964
+ {
965
+ direction: "desc",
966
+ expr: ratingPresenceExpr,
967
+ value: cursorRatingPresence,
968
+ },
969
+ { direction: "desc", expr: ratingSortExpr, value: cursorRating },
970
+ {
971
+ direction: "desc",
972
+ expr: sortTimestampSortExpr,
973
+ value: cursorSortTimestamp,
974
+ },
975
+ { direction: "desc", expr: posts.id, value: cursorPost.id },
976
+ ]),
977
+ );
978
+ }
979
+
980
+ return buildLexicographicCursorCondition(
981
+ withPinnedSortKey([
945
982
  {
946
983
  direction: "desc",
947
984
  expr: ratingPresenceExpr,
948
985
  value: cursorRatingPresence,
949
986
  },
950
- { direction: "desc", expr: ratingSortExpr, value: cursorRating },
987
+ { direction: "asc", expr: ratingSortExpr, value: cursorRating },
951
988
  {
952
989
  direction: "desc",
953
990
  expr: sortTimestampSortExpr,
954
991
  value: cursorSortTimestamp,
955
992
  },
956
993
  { direction: "desc", expr: posts.id, value: cursorPost.id },
957
- ]);
958
- }
959
-
960
- return buildLexicographicCursorCondition([
961
- { direction: "desc", expr: pinnedSortExpr, value: cursorPinnedAt },
962
- {
963
- direction: "desc",
964
- expr: ratingPresenceExpr,
965
- value: cursorRatingPresence,
966
- },
967
- { direction: "asc", expr: ratingSortExpr, value: cursorRating },
968
- {
969
- direction: "desc",
970
- expr: sortTimestampSortExpr,
971
- value: cursorSortTimestamp,
972
- },
973
- { direction: "desc", expr: posts.id, value: cursorPost.id },
974
- ]);
994
+ ]),
995
+ );
975
996
  }
976
997
 
977
998
  function toPost(
@@ -1105,6 +1126,21 @@ export function createPostService(
1105
1126
  return conditions;
1106
1127
  }
1107
1128
 
1129
+ function buildCollectionThreadActivityExpr(alias: string) {
1130
+ return sql<number>`MAX(
1131
+ COALESCE(
1132
+ (
1133
+ SELECT root.last_activity_at
1134
+ FROM post AS root
1135
+ WHERE root.site_id = ${siteId}
1136
+ AND root.id = ${posts.threadId}
1137
+ ),
1138
+ ${posts.publishedAt},
1139
+ ${posts.updatedAt}
1140
+ )
1141
+ )`.as(alias);
1142
+ }
1143
+
1108
1144
  function isMediaAttachmentInput(
1109
1145
  attachment: PostAttachmentInput,
1110
1146
  ): attachment is Extract<PostAttachmentInput, { type: "media" }> {
@@ -1313,6 +1349,9 @@ export function createPostService(
1313
1349
  const pinnedSortExpr = sql<number>`coalesce(${posts.pinnedAt}, -1)`;
1314
1350
  const featuredSortExpr = sql<number>`coalesce(${posts.featuredAt}, -1)`;
1315
1351
  const sortTimestampSortExpr = sql<number>`coalesce(${sortTimestamp}, -1)`;
1352
+ const pinnedOrder = filters.ignorePinnedSort
1353
+ ? []
1354
+ : [desc(pinnedSortExpr)];
1316
1355
 
1317
1356
  const baseQuery = db
1318
1357
  .select()
@@ -1323,7 +1362,7 @@ export function createPostService(
1323
1362
  let query =
1324
1363
  filters.featured || filters.sortOrder === undefined
1325
1364
  ? baseQuery.orderBy(
1326
- desc(pinnedSortExpr),
1365
+ ...pinnedOrder,
1327
1366
  filters.featured
1328
1367
  ? desc(featuredSortExpr)
1329
1368
  : desc(sortTimestampSortExpr),
@@ -1331,20 +1370,20 @@ export function createPostService(
1331
1370
  )
1332
1371
  : filters.sortOrder === "oldest"
1333
1372
  ? baseQuery.orderBy(
1334
- desc(pinnedSortExpr),
1373
+ ...pinnedOrder,
1335
1374
  asc(sortTimestampSortExpr),
1336
1375
  asc(posts.id),
1337
1376
  )
1338
1377
  : filters.sortOrder === "rating_desc"
1339
1378
  ? baseQuery.orderBy(
1340
- desc(pinnedSortExpr),
1379
+ ...pinnedOrder,
1341
1380
  desc(ratingPresence),
1342
1381
  desc(posts.rating),
1343
1382
  desc(sortTimestampSortExpr),
1344
1383
  desc(posts.id),
1345
1384
  )
1346
1385
  : baseQuery.orderBy(
1347
- desc(pinnedSortExpr),
1386
+ ...pinnedOrder,
1348
1387
  desc(ratingPresence),
1349
1388
  asc(posts.rating),
1350
1389
  desc(sortTimestampSortExpr),
@@ -2845,6 +2884,8 @@ export function createPostService(
2845
2884
  sortOrder === "oldest"
2846
2885
  ? sql<number>`MIN(${posts.publishedAt})`.as("published_at")
2847
2886
  : sql<number>`MAX(${posts.publishedAt})`.as("published_at");
2887
+ const threadActivityAt =
2888
+ buildCollectionThreadActivityExpr("thread_activity_at");
2848
2889
  const ratingPresence = sql<number>`MAX(
2849
2890
  CASE
2850
2891
  WHEN ${posts.rating} IS NULL THEN 0
@@ -2864,6 +2905,7 @@ export function createPostService(
2864
2905
  .select({
2865
2906
  threadId: posts.threadId,
2866
2907
  publishedAt,
2908
+ threadActivityAt,
2867
2909
  collectionPinnedAt,
2868
2910
  ratingPresence,
2869
2911
  ratingValue,
@@ -2891,12 +2933,12 @@ export function createPostService(
2891
2933
  desc(collectionPinnedAt),
2892
2934
  desc(ratingPresence),
2893
2935
  desc(ratingValue),
2894
- desc(publishedAt),
2936
+ desc(threadActivityAt),
2895
2937
  desc(posts.threadId),
2896
2938
  )
2897
2939
  : baseQuery.orderBy(
2898
2940
  desc(collectionPinnedAt),
2899
- desc(publishedAt),
2941
+ desc(threadActivityAt),
2900
2942
  desc(posts.threadId),
2901
2943
  );
2902
2944
 
@@ -2923,9 +2965,8 @@ export function createPostService(
2923
2965
  ...buildThreadRootPageConditions(options),
2924
2966
  buildCollectionMembershipCondition(collectionIds),
2925
2967
  ];
2926
- const publishedAt = sql<number>`MAX(${posts.publishedAt})`.as(
2927
- "published_at",
2928
- );
2968
+ const threadActivityAt =
2969
+ buildCollectionThreadActivityExpr("thread_activity_at");
2929
2970
  const collectedAt = sql<number>`MAX(${postCollections.createdAt})`.as(
2930
2971
  "collected_at",
2931
2972
  );
@@ -2933,11 +2974,14 @@ export function createPostService(
2933
2974
  sql<number>`MAX(coalesce(${postCollections.pinnedAt}, -1))`.as(
2934
2975
  "collection_pinned_at",
2935
2976
  );
2977
+ const pinnedOrder = options.ignoreCollectionPinnedSort
2978
+ ? []
2979
+ : [desc(collectionPinnedAt)];
2936
2980
 
2937
2981
  let query = db
2938
2982
  .select({
2939
2983
  threadId: posts.threadId,
2940
- publishedAt,
2984
+ threadActivityAt,
2941
2985
  collectedAt,
2942
2986
  collectionPinnedAt,
2943
2987
  })
@@ -2951,11 +2995,7 @@ export function createPostService(
2951
2995
  )
2952
2996
  .where(and(...conditions))
2953
2997
  .groupBy(posts.threadId)
2954
- .orderBy(
2955
- desc(collectionPinnedAt),
2956
- desc(publishedAt),
2957
- desc(posts.threadId),
2958
- );
2998
+ .orderBy(...pinnedOrder, desc(threadActivityAt), desc(posts.threadId));
2959
2999
 
2960
3000
  if (options.limit !== undefined) {
2961
3001
  query = query.limit(options.limit) as typeof query;
@@ -46,7 +46,6 @@ export interface GeneralSettingsData {
46
46
  dashboardLanguage?: string;
47
47
  cjkSerifFont: string;
48
48
  showJantBrandingOnHome: boolean;
49
- homeDefaultView?: FeedKind;
50
49
  mainRssFeed?: FeedKind;
51
50
  timeZone: string;
52
51
  }
@@ -404,15 +403,6 @@ export function createSettingsService(
404
403
  oldCjkSerifFont: opts.oldCjkSerifFont,
405
404
  });
406
405
 
407
- // Homepage default view: only update if provided (may be managed separately)
408
- if (data.homeDefaultView !== undefined) {
409
- if (data.homeDefaultView === "featured") {
410
- await this.set("HOME_DEFAULT_VIEW", data.homeDefaultView);
411
- } else {
412
- await this.remove("HOME_DEFAULT_VIEW");
413
- }
414
- }
415
-
416
406
  await this.updateFeedSettings({ mainRssFeed: data.mainRssFeed });
417
407
 
418
408
  return {
@@ -15,7 +15,6 @@ import { SETTINGS_KEYS } from "../lib/constants.js";
15
15
  import { ConflictError, NotFoundError } from "../lib/errors.js";
16
16
  import { createEntityId } from "../lib/ids.js";
17
17
  import { getConfiguredSingleSiteUrl } from "../lib/env.js";
18
- import { getHomeDefaultViewFromNavItems } from "../lib/navigation.js";
19
18
  import { resolveConfig } from "../lib/resolve-config.js";
20
19
  import { buildThemeStyle } from "../lib/theme.js";
21
20
  import { now } from "../lib/time.js";
@@ -793,7 +792,6 @@ export function createSiteAdminService(
793
792
  siteDescription: appConfig.siteDescription,
794
793
  siteLanguage: appConfig.siteLanguage,
795
794
  showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
796
- homeDefaultView: getHomeDefaultViewFromNavItems(navItemList),
797
795
  mainRssFeed: appConfig.mainRssFeed,
798
796
  siteFooter: appConfig.siteFooter,
799
797
  showHeaderAvatar: appConfig.showHeaderAvatar,
@@ -52,10 +52,6 @@ const RELAY_MULTIPART_THRESHOLD = 95 * 1024 * 1024;
52
52
  const RELAY_MULTIPART_PART_SIZE = 50 * 1024 * 1024;
53
53
  const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
54
54
  const DEFAULT_EXPIRED_UPLOAD_CLEANUP_LIMIT = 20;
55
- // Grace window before a finalized-but-unattached media row (uploaded during
56
- // compose, `postId IS NULL`) is reaped. Generous enough that an in-progress
57
- // compose/edit is never affected; based on `createdAt`, no heartbeat needed.
58
- const ORPHAN_MEDIA_GRACE_SECONDS = 7 * 24 * 60 * 60;
59
55
  type CleanupableUploadSessionState = "pending" | "uploaded" | "failed";
60
56
  const CLEANUPABLE_UPLOAD_SESSION_STATES = [
61
57
  "pending",
@@ -766,19 +762,6 @@ export function createUploadSessionService(
766
762
  deletedSessions += 1;
767
763
  }
768
764
 
769
- // Reap finalized media that was uploaded during compose but never
770
- // attached to a post (`postId IS NULL`) past the grace window. This now
771
- // soft-deletes via the media service: the DB row is removed and the
772
- // storage object is enqueued for deferred deletion. Bounded by the same
773
- // per-run `limit`; any backlog drains over subsequent runs.
774
- const orphanIds = await media.listOrphanedMediaIds({
775
- before: now() - ORPHAN_MEDIA_GRACE_SECONDS,
776
- limit,
777
- });
778
- if (orphanIds.length > 0) {
779
- await media.deleteByIds(orphanIds, deps.storage);
780
- }
781
-
782
765
  // Physically delete storage objects whose recycle window has elapsed.
783
766
  // Skips any object a live media row still references (re-uploads).
784
767
  const purgedStorageObjects = await media.purgeDueStorageObjects(
@@ -789,7 +772,10 @@ export function createUploadSessionService(
789
772
  return {
790
773
  abortedMultipartUploads,
791
774
  deletedSessions,
792
- deletedOrphanMedia: orphanIds.length,
775
+ // Retained for response compatibility. Finalized media may be referenced
776
+ // from post bodies without a `media.post_id`, so upload cleanup must not
777
+ // infer liveness from attachment state.
778
+ deletedOrphanMedia: 0,
793
779
  purgedStorageObjects,
794
780
  };
795
781
  },
@@ -1163,6 +1163,26 @@ svg[stroke-width].icon-fine {
1163
1163
  border-color: var(--color-border);
1164
1164
  }
1165
1165
 
1166
+ .nav-suggestions-list {
1167
+ @apply flex flex-col gap-2;
1168
+ }
1169
+
1170
+ .nav-suggestion-item {
1171
+ @apply flex items-center justify-between gap-3 rounded-lg border px-3 py-2;
1172
+ }
1173
+
1174
+ .nav-suggestion-info {
1175
+ @apply flex min-w-0 flex-col;
1176
+ }
1177
+
1178
+ .nav-suggestion-title {
1179
+ @apply truncate text-sm font-medium;
1180
+ }
1181
+
1182
+ .nav-suggestion-meta {
1183
+ @apply truncate text-xs text-muted-foreground;
1184
+ }
1185
+
1166
1186
  /* Collection picker dropdown menu */
1167
1187
  .collection-picker {
1168
1188
  @apply absolute left-0 z-10 mt-1.5 w-64 overflow-y-auto rounded-lg border py-1.5;
@@ -65,8 +65,8 @@
65
65
  * --type-subtitle 1.8rem (27px) — h3
66
66
  * --type-body 1.4rem (21px) — running text, form inputs
67
67
  * --type-secondary 1.1rem (16.5px) — meta, nav, sidenotes, UI labels
68
- * --type-base 1.0rem (15px) — code, UI buttons, controls
69
- * --type-sm 0.9rem (13.5px) — code blocks, small UI text
68
+ * --type-base 1.0rem (15px) — UI buttons, controls
69
+ * --type-sm 0.9rem (13.5px) — small UI text
70
70
  * --type-xs 0.8rem (12px) — captions, descriptions, chips
71
71
  * --type-2xs 0.65rem (9.75px) — tiny labels, file sizes, badges
72
72
  */
@@ -84,18 +84,20 @@
84
84
  mobile overrides below cap sizes for small screens.
85
85
  --type-content-scale uniformly scales all content sizes
86
86
  without affecting UI controls. */
87
- --type-content-scale: 0.78;
87
+ --type-content-scale: 0.8;
88
88
  --type-content-display: calc(var(--type-display) * var(--type-content-scale));
89
89
  --type-content-title: calc(var(--type-title) * var(--type-content-scale));
90
90
  --type-content-subtitle: calc(
91
91
  var(--type-subtitle) * var(--type-content-scale)
92
92
  );
93
93
  --type-content-body: calc(var(--type-body) * var(--type-content-scale));
94
+ --type-content-quote: calc(var(--type-content-body) * 1.16);
95
+ --type-content-quote-leading: 1.4;
94
96
 
95
97
  /* Semantic aliases */
96
- --type-code: var(--type-base);
97
- --type-code-block: var(--type-sm);
98
98
  --type-body-size: var(--type-content-body);
99
+ --type-code: calc(var(--type-body-size) * 0.94);
100
+ --type-code-block: calc(var(--type-body-size) * 0.9);
99
101
  --type-body-tracking: 0;
100
102
  --type-ui-title: var(--type-secondary);
101
103
  --type-ui-control: var(--type-base);
@@ -107,14 +109,14 @@
107
109
  --type-thread-context: var(--type-base);
108
110
  --type-thread-context-title: var(--type-secondary);
109
111
  --type-thread-context-meta: var(--type-sm);
110
- --feed-note-title-size: var(--type-content-title);
112
+ --feed-note-title-size: calc(var(--type-content-body) * 1.36);
111
113
  --feed-note-title-leading: var(--type-heading-leading);
112
- --type-body-leading: 1.5;
114
+ --type-body-leading: 1.7;
113
115
  --type-display-leading: 1.15;
114
116
  --type-heading-leading: 1.15;
115
- --type-heading-weight: var(--fw-regular);
117
+ --type-heading-weight: var(--fw-medium);
116
118
  --type-heading-tracking: 0;
117
- --type-display-weight: var(--fw-regular);
119
+ --type-display-weight: var(--fw-medium);
118
120
  --type-display-tracking: 0;
119
121
  --type-label-weight: var(--fw-medium);
120
122
  --type-label-tracking: 0.08em;
@@ -264,6 +266,22 @@
264
266
  var(--site-text-primary) 12%,
265
267
  transparent
266
268
  );
269
+ --site-code-text: color-mix(
270
+ in srgb,
271
+ var(--site-reading-heading) 86%,
272
+ var(--site-reading-body)
273
+ );
274
+ --site-code-bg: color-mix(in srgb, var(--site-reading-meta) 12%, transparent);
275
+ --site-code-block-bg: color-mix(
276
+ in srgb,
277
+ var(--site-elevated-bg) 94%,
278
+ var(--site-nav-hover-bg)
279
+ );
280
+ --site-code-block-border: color-mix(
281
+ in srgb,
282
+ var(--site-divider) 62%,
283
+ transparent
284
+ );
267
285
  --site-feed-divider-color: color-mix(
268
286
  in srgb,
269
287
  var(--site-text-secondary) 30%,
@@ -484,7 +502,7 @@
484
502
  :root {
485
503
  /* Content layer — tighter scale for mobile reading.
486
504
  Ratio ≈ 1.88 : 1.42 : 1.15 : 1 (display:title:subtitle:body).
487
- At 15px root × 0.78 content-scale: 28.7 / 21.6 / 17.6 / 15.2px.
505
+ At 15px root × 0.8 content-scale: 29.4 / 22.2 / 18 / 15.6px.
488
506
  Body is floored to a 16px minimum below (max()) for readability;
489
507
  the floor still yields to calc() when the user zooms the root up. */
490
508
  --type-content-display: calc(2.45rem * var(--type-content-scale));