@jant/core 0.6.10 → 0.6.12

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 (92) hide show
  1. package/bin/commands/import-site.js +45 -20
  2. package/dist/app-B5P1b4TF.js +6 -0
  3. package/dist/{app-CGHkOdme.js → app-D-zAhayc.js} +847 -244
  4. package/dist/client/.vite/manifest.json +3 -3
  5. package/dist/client/_assets/client-BUxR-E8O.css +2 -0
  6. package/dist/client/_assets/{client-DYrWuaIk.js → client-S1cdvulk.js} +1 -1
  7. package/dist/client/_assets/{client-auth-B5Re0uCd.js → client-auth-YW92ZH7u.js} +158 -97
  8. package/dist/{export-DY1v5Iqu.js → export-Ckwh4BiE.js} +96 -96
  9. package/dist/{github-sync-LefaslGJ.js → github-sync-BMmK3JZ0.js} +2 -2
  10. package/dist/{github-sync-2_T7nbOv.js → github-sync-Dl9wQMar.js} +1 -1
  11. package/dist/index.js +3 -3
  12. package/dist/node.js +4 -4
  13. package/package.json +1 -1
  14. package/src/__tests__/import-site-command.test.ts +75 -0
  15. package/src/__tests__/vite-dev-plugins.test.ts +34 -0
  16. package/src/client/__tests__/compose-launch.test.ts +50 -1
  17. package/src/client/__tests__/compose-shortcuts.test.ts +74 -18
  18. package/src/client/components/__tests__/jant-command-palette.test.ts +63 -0
  19. package/src/client/components/__tests__/jant-compose-dialog.test.ts +33 -0
  20. package/src/client/components/__tests__/jant-compose-editor-rehost-notice.test.ts +81 -0
  21. package/src/client/components/__tests__/jant-nav-manager.test.ts +77 -1
  22. package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
  23. package/src/client/components/__tests__/jant-settings-general.test.ts +71 -0
  24. package/src/client/components/compose-types.ts +4 -0
  25. package/src/client/components/jant-command-palette.ts +67 -2
  26. package/src/client/components/jant-compose-dialog.ts +8 -2
  27. package/src/client/components/jant-compose-editor.ts +111 -0
  28. package/src/client/components/jant-nav-manager.ts +142 -2
  29. package/src/client/components/jant-settings-general.ts +56 -0
  30. package/src/client/components/nav-manager-types.ts +14 -0
  31. package/src/client/components/settings-types.ts +30 -0
  32. package/src/client/compose-bridge.ts +25 -8
  33. package/src/client/compose-launch.ts +30 -0
  34. package/src/client/compose-shortcuts.ts +46 -3
  35. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +143 -0
  36. package/src/client/tiptap/__tests__/paste-rehost-e2e.test.ts +65 -0
  37. package/src/client/tiptap/__tests__/rehost-images.test.ts +192 -0
  38. package/src/client/tiptap/create-editor.ts +3 -0
  39. package/src/client/tiptap/extensions.ts +4 -0
  40. package/src/client/tiptap/inline-image-upload.ts +174 -50
  41. package/src/client/tiptap/rehost-images.ts +108 -0
  42. package/src/i18n/__tests__/fallback.test.ts +24 -0
  43. package/src/i18n/locales/public/en.po +10 -0
  44. package/src/i18n/locales/public/en.ts +1 -1
  45. package/src/i18n/locales/public/zh-Hans.po +10 -0
  46. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  47. package/src/i18n/locales/public/zh-Hant.po +10 -0
  48. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  49. package/src/i18n/locales/settings/en.po +54 -0
  50. package/src/i18n/locales/settings/en.ts +1 -1
  51. package/src/i18n/locales/settings/zh-Hans.po +64 -10
  52. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  53. package/src/i18n/locales/settings/zh-Hant.po +56 -2
  54. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  55. package/src/lib/__tests__/feed.test.ts +96 -10
  56. package/src/lib/__tests__/upload-sideload.test.ts +78 -0
  57. package/src/lib/__tests__/url-fetch.test.ts +181 -0
  58. package/src/lib/feed.ts +63 -3
  59. package/src/lib/post-display.ts +14 -8
  60. package/src/lib/upload.ts +111 -0
  61. package/src/lib/url-fetch.ts +263 -0
  62. package/src/preset.css +60 -18
  63. package/src/routes/api/__tests__/uploads.test.ts +63 -1
  64. package/src/routes/api/uploads.ts +52 -0
  65. package/src/routes/dash/settings.tsx +41 -2
  66. package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
  67. package/src/routes/pages/page.tsx +44 -12
  68. package/src/services/__tests__/about-page.test.ts +116 -0
  69. package/src/services/__tests__/media.test.ts +168 -1
  70. package/src/services/__tests__/navigation.test.ts +190 -1
  71. package/src/services/__tests__/post-timeline.test.ts +119 -2
  72. package/src/services/about-page.ts +143 -0
  73. package/src/services/export-theme/layouts/_default/rss.xml +2 -1
  74. package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
  75. package/src/services/export-theme/styles/main.css +46 -14
  76. package/src/services/index.ts +27 -17
  77. package/src/services/media.ts +111 -0
  78. package/src/services/navigation.ts +155 -1
  79. package/src/services/post.ts +24 -7
  80. package/src/styles/components.css +20 -0
  81. package/src/styles/tokens.css +26 -8
  82. package/src/styles/ui.css +149 -7
  83. package/src/types/views.ts +13 -0
  84. package/src/ui/compose/ComposeDialog.tsx +16 -0
  85. package/src/ui/dash/appearance/NavigationContent.tsx +57 -0
  86. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
  87. package/src/ui/dash/settings/GeneralContent.tsx +44 -0
  88. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
  89. package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
  90. package/src/ui/layouts/BaseLayout.tsx +12 -0
  91. package/dist/app-D24n0DoH.js +0 -6
  92. package/dist/client/_assets/client-xWDl78yi.css +0 -2
@@ -15,6 +15,7 @@ import {
15
15
  import { createEntityId } from "../lib/ids.js";
16
16
  import { ValidationError } from "../lib/errors.js";
17
17
  import { now } from "../lib/time.js";
18
+ import { normalizePath } from "../lib/url.js";
18
19
  import { COLLECTION_FRESHNESS_WINDOW_SECONDS } from "../types.js";
19
20
  import type {
20
21
  NavItem,
@@ -23,10 +24,15 @@ import type {
23
24
  CreateNavItem,
24
25
  UpdateNavItem,
25
26
  SystemNavKey,
27
+ SuggestedNavLink,
26
28
  } from "../types.js";
27
29
  import { SYSTEM_NAV_KEYS } from "../types.js";
28
30
 
29
31
  const POSITION_RETRY_ATTEMPTS = 5;
32
+ const SUGGESTED_NAV_LINK_CANDIDATES = [
33
+ { key: "about", path: "/about", label: "About" },
34
+ { key: "now", path: "/now", label: "Now" },
35
+ ] as const;
30
36
 
31
37
  // Re-export shared constraint detection — see db/dialect.ts
32
38
  import { isUniqueConstraintError } from "../db/dialect.js";
@@ -45,6 +51,7 @@ export interface NavItemService {
45
51
  afterId: string | null,
46
52
  beforeId: string | null,
47
53
  ): Promise<NavItem | null>;
54
+ listSuggestedLinks(): Promise<SuggestedNavLink[]>;
48
55
  getCollectionFreshness(collectionIds: string[]): Promise<Map<string, number>>;
49
56
  }
50
57
 
@@ -53,7 +60,8 @@ export function createNavItemService(
53
60
  siteId: string,
54
61
  databaseSchema: DatabaseSchema = sqliteSchemaBundle,
55
62
  ): NavItemService {
56
- const { navItems, postCollections, posts } = databaseSchema;
63
+ const { navItems, postCollections, posts, pathRegistry, collections } =
64
+ databaseSchema;
57
65
 
58
66
  const defaultSystemOrder = [
59
67
  "latest",
@@ -121,6 +129,39 @@ export function createNavItemService(
121
129
  };
122
130
  }
123
131
 
132
+ function withLeadingSlash(path: string): string {
133
+ const normalized = normalizePath(path);
134
+ return normalized ? `/${normalized}` : "/";
135
+ }
136
+
137
+ function getComparableInternalPath(url: string): string | null {
138
+ if (
139
+ url.startsWith("http://") ||
140
+ url.startsWith("https://") ||
141
+ url.startsWith("//") ||
142
+ url.startsWith("mailto:") ||
143
+ url.startsWith("tel:") ||
144
+ url.startsWith("#")
145
+ ) {
146
+ return null;
147
+ }
148
+
149
+ try {
150
+ const pathname = new URL(url, "https://jant.invalid").pathname;
151
+ return withLeadingSlash(pathname);
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ function normalizeSuggestedLabel(
158
+ label: string | null | undefined,
159
+ fallback: string,
160
+ ): string {
161
+ const trimmed = label?.trim();
162
+ return trimmed && trimmed.length > 0 ? trimmed : fallback;
163
+ }
164
+
124
165
  async function getLastPosition(): Promise<string | null> {
125
166
  const rows = await db
126
167
  .select({ position: navItems.position })
@@ -414,6 +455,119 @@ export function createNavItemService(
414
455
  throw new Error("Failed to assign a unique nav item position");
415
456
  },
416
457
 
458
+ async listSuggestedLinks() {
459
+ const existingRows = await db
460
+ .select({
461
+ url: navItems.url,
462
+ collectionId: navItems.collectionId,
463
+ })
464
+ .from(navItems)
465
+ .where(eq(navItems.siteId, siteId));
466
+ const existingPaths = new Set(
467
+ existingRows.flatMap((item) => {
468
+ const path = getComparableInternalPath(item.url);
469
+ return path ? [path] : [];
470
+ }),
471
+ );
472
+ const existingCollectionIds = new Set(
473
+ existingRows.flatMap((item) =>
474
+ item.collectionId ? [item.collectionId] : [],
475
+ ),
476
+ );
477
+
478
+ const suggestions: SuggestedNavLink[] = [];
479
+
480
+ for (const candidate of SUGGESTED_NAV_LINK_CANDIDATES) {
481
+ const path = withLeadingSlash(candidate.path);
482
+ if (existingPaths.has(path)) continue;
483
+
484
+ const pathRows = await db
485
+ .select()
486
+ .from(pathRegistry)
487
+ .where(
488
+ and(
489
+ eq(pathRegistry.siteId, siteId),
490
+ eq(pathRegistry.path, normalizePath(candidate.path)),
491
+ ),
492
+ )
493
+ .limit(1);
494
+ const record = pathRows[0];
495
+ if (!record || record.kind === "redirect") continue;
496
+
497
+ if (record.postId) {
498
+ const postRows = await db
499
+ .select({
500
+ title: posts.title,
501
+ status: posts.status,
502
+ visibility: posts.visibility,
503
+ })
504
+ .from(posts)
505
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, record.postId)))
506
+ .limit(1);
507
+ const post = postRows[0];
508
+ if (
509
+ !post ||
510
+ post.status !== "published" ||
511
+ post.visibility === "private"
512
+ ) {
513
+ continue;
514
+ }
515
+
516
+ suggestions.push({
517
+ key: candidate.key,
518
+ label: normalizeSuggestedLabel(post.title, candidate.label),
519
+ url: path,
520
+ targetType: "page",
521
+ navItemType: "link",
522
+ });
523
+ continue;
524
+ }
525
+
526
+ if (record.collectionId) {
527
+ if (existingCollectionIds.has(record.collectionId)) continue;
528
+
529
+ const collectionRows = await db
530
+ .select({
531
+ title: collections.title,
532
+ })
533
+ .from(collections)
534
+ .where(
535
+ and(
536
+ eq(collections.siteId, siteId),
537
+ eq(collections.id, record.collectionId),
538
+ ),
539
+ )
540
+ .limit(1);
541
+ const collection = collectionRows[0];
542
+ if (!collection) continue;
543
+
544
+ suggestions.push({
545
+ key: candidate.key,
546
+ label: normalizeSuggestedLabel(collection.title, candidate.label),
547
+ url: path,
548
+ targetType: "collection",
549
+ navItemType: record.kind === "slug" ? "collection" : "link",
550
+ ...(record.kind === "slug" && {
551
+ collectionId: record.collectionId,
552
+ }),
553
+ });
554
+ continue;
555
+ }
556
+
557
+ if (record.kind === "archive") {
558
+ suggestions.push({
559
+ key: candidate.key,
560
+ label: candidate.label,
561
+ url: path,
562
+ targetType: "archive",
563
+ navItemType: "link",
564
+ });
565
+ }
566
+ }
567
+
568
+ return suggestions;
569
+ },
570
+
417
571
  async getCollectionFreshness(collectionIds) {
418
572
  if (collectionIds.length === 0) return new Map<string, number>();
419
573
 
@@ -1105,6 +1105,21 @@ export function createPostService(
1105
1105
  return conditions;
1106
1106
  }
1107
1107
 
1108
+ function buildCollectionThreadActivityExpr(alias: string) {
1109
+ return sql<number>`MAX(
1110
+ COALESCE(
1111
+ (
1112
+ SELECT root.last_activity_at
1113
+ FROM post AS root
1114
+ WHERE root.site_id = ${siteId}
1115
+ AND root.id = ${posts.threadId}
1116
+ ),
1117
+ ${posts.publishedAt},
1118
+ ${posts.updatedAt}
1119
+ )
1120
+ )`.as(alias);
1121
+ }
1122
+
1108
1123
  function isMediaAttachmentInput(
1109
1124
  attachment: PostAttachmentInput,
1110
1125
  ): attachment is Extract<PostAttachmentInput, { type: "media" }> {
@@ -2845,6 +2860,8 @@ export function createPostService(
2845
2860
  sortOrder === "oldest"
2846
2861
  ? sql<number>`MIN(${posts.publishedAt})`.as("published_at")
2847
2862
  : sql<number>`MAX(${posts.publishedAt})`.as("published_at");
2863
+ const threadActivityAt =
2864
+ buildCollectionThreadActivityExpr("thread_activity_at");
2848
2865
  const ratingPresence = sql<number>`MAX(
2849
2866
  CASE
2850
2867
  WHEN ${posts.rating} IS NULL THEN 0
@@ -2864,6 +2881,7 @@ export function createPostService(
2864
2881
  .select({
2865
2882
  threadId: posts.threadId,
2866
2883
  publishedAt,
2884
+ threadActivityAt,
2867
2885
  collectionPinnedAt,
2868
2886
  ratingPresence,
2869
2887
  ratingValue,
@@ -2891,12 +2909,12 @@ export function createPostService(
2891
2909
  desc(collectionPinnedAt),
2892
2910
  desc(ratingPresence),
2893
2911
  desc(ratingValue),
2894
- desc(publishedAt),
2912
+ desc(threadActivityAt),
2895
2913
  desc(posts.threadId),
2896
2914
  )
2897
2915
  : baseQuery.orderBy(
2898
2916
  desc(collectionPinnedAt),
2899
- desc(publishedAt),
2917
+ desc(threadActivityAt),
2900
2918
  desc(posts.threadId),
2901
2919
  );
2902
2920
 
@@ -2923,9 +2941,8 @@ export function createPostService(
2923
2941
  ...buildThreadRootPageConditions(options),
2924
2942
  buildCollectionMembershipCondition(collectionIds),
2925
2943
  ];
2926
- const publishedAt = sql<number>`MAX(${posts.publishedAt})`.as(
2927
- "published_at",
2928
- );
2944
+ const threadActivityAt =
2945
+ buildCollectionThreadActivityExpr("thread_activity_at");
2929
2946
  const collectedAt = sql<number>`MAX(${postCollections.createdAt})`.as(
2930
2947
  "collected_at",
2931
2948
  );
@@ -2937,7 +2954,7 @@ export function createPostService(
2937
2954
  let query = db
2938
2955
  .select({
2939
2956
  threadId: posts.threadId,
2940
- publishedAt,
2957
+ threadActivityAt,
2941
2958
  collectedAt,
2942
2959
  collectionPinnedAt,
2943
2960
  })
@@ -2953,7 +2970,7 @@ export function createPostService(
2953
2970
  .groupBy(posts.threadId)
2954
2971
  .orderBy(
2955
2972
  desc(collectionPinnedAt),
2956
- desc(publishedAt),
2973
+ desc(threadActivityAt),
2957
2974
  desc(posts.threadId),
2958
2975
  );
2959
2976
 
@@ -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,9 +109,9 @@
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
117
  --type-heading-weight: var(--fw-regular);
@@ -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));
package/src/styles/ui.css CHANGED
@@ -2237,13 +2237,13 @@
2237
2237
  }
2238
2238
 
2239
2239
  .post-header-block {
2240
- margin-bottom: 1rem;
2240
+ margin-bottom: 1.7rem;
2241
2241
  }
2242
2242
 
2243
2243
  .post-header-block-detail {
2244
2244
  display: flex;
2245
2245
  flex-direction: column;
2246
- gap: 0.55rem;
2246
+ gap: 0.7rem;
2247
2247
  }
2248
2248
 
2249
2249
  .post-header-block .feed-link-title,
@@ -2944,8 +2944,8 @@
2944
2944
  padding-left: 0;
2945
2945
  font-family: var(--font-serif);
2946
2946
  color: var(--site-text-primary);
2947
- font-size: var(--type-content-subtitle);
2948
- line-height: var(--type-heading-leading);
2947
+ font-size: var(--type-content-quote);
2948
+ line-height: var(--type-content-quote-leading);
2949
2949
  white-space: pre-line;
2950
2950
  text-wrap: pretty;
2951
2951
  }
@@ -4535,6 +4535,7 @@
4535
4535
  --compose-title-input-size: var(--type-content-subtitle);
4536
4536
  --compose-title-input-leading: var(--type-display-leading);
4537
4537
  --compose-quote-input-size: var(--type-content-subtitle);
4538
+ --compose-quote-input-leading: 1.32;
4538
4539
  --compose-inline-input-size: var(--type-ui-input);
4539
4540
  --compose-panel-label-size: var(--type-ui-control);
4540
4541
  --compose-panel-hint-size: var(--type-ui-hint);
@@ -5201,7 +5202,7 @@
5201
5202
  font-size: var(--compose-quote-input-size);
5202
5203
  font-style: normal;
5203
5204
  font-weight: var(--fw-regular);
5204
- line-height: 1.32;
5205
+ line-height: var(--compose-quote-input-leading);
5205
5206
  letter-spacing: -0.02em;
5206
5207
  min-height: 9rem;
5207
5208
  padding-top: 0.5rem;
@@ -7428,6 +7429,53 @@
7428
7429
  position: relative;
7429
7430
  }
7430
7431
 
7432
+ .compose-reply-compose-layout {
7433
+ --compose-title-input-size: var(--type-content-subtitle);
7434
+ --compose-title-input-leading: 1.35;
7435
+ --compose-quote-input-size: calc(var(--type-content-body) * 1.06);
7436
+ --compose-quote-input-leading: 1.42;
7437
+ --compose-inline-input-size: var(--type-base);
7438
+ }
7439
+
7440
+ .compose-reply-compose-layout .compose-quote-wrap {
7441
+ margin-top: 0.4rem;
7442
+ padding: 24px 18px 18px;
7443
+ border-radius: 0.95rem;
7444
+ }
7445
+
7446
+ .compose-reply-compose-layout .compose-link-url-wrap {
7447
+ margin-top: 0.4rem;
7448
+ }
7449
+
7450
+ .compose-reply-compose-layout .compose-quote-mark {
7451
+ top: 0.58rem;
7452
+ left: 0.9rem;
7453
+ width: 1.55rem;
7454
+ }
7455
+
7456
+ .compose-reply-compose-layout .compose-quote-text {
7457
+ min-height: 6.5rem;
7458
+ padding-top: 0.35rem;
7459
+ }
7460
+
7461
+ @media (min-width: 700px) {
7462
+ .compose-reply-compose-layout .compose-quote-wrap {
7463
+ padding: 25px 20px 18px;
7464
+ border-radius: 1rem;
7465
+ }
7466
+
7467
+ .compose-reply-compose-layout .compose-quote-mark {
7468
+ top: 0.62rem;
7469
+ left: 1rem;
7470
+ width: 1.6rem;
7471
+ }
7472
+
7473
+ .compose-reply-compose-layout .compose-quote-text {
7474
+ min-height: 6.5rem;
7475
+ padding-top: 0.35rem;
7476
+ }
7477
+ }
7478
+
7431
7479
  /* Continuous vertical line through the rail */
7432
7480
  .compose-thread-layout::before {
7433
7481
  content: "";
@@ -7490,6 +7538,78 @@
7490
7538
  background: transparent;
7491
7539
  }
7492
7540
 
7541
+ .compose-reply-compose-layout .compose-editor-row .compose-attachments-dock {
7542
+ --compose-reply-attachment-width: 96px;
7543
+ --compose-reply-attachment-height: 72px;
7544
+
7545
+ max-height: min(116px, 18dvh);
7546
+ padding-block: 4px 2px;
7547
+ }
7548
+
7549
+ .compose-reply-compose-layout .compose-editor-row .compose-attachments {
7550
+ gap: 6px;
7551
+ padding-block: 0;
7552
+ }
7553
+
7554
+ .compose-reply-compose-layout .compose-editor-row .compose-attachment {
7555
+ gap: 2px;
7556
+ max-width: none;
7557
+ }
7558
+
7559
+ .compose-reply-compose-layout
7560
+ .compose-editor-row
7561
+ .compose-attachment:only-child
7562
+ .compose-attachment-img,
7563
+ .compose-reply-compose-layout
7564
+ .compose-editor-row
7565
+ .compose-attachment:not(:only-child)
7566
+ .compose-attachment-img {
7567
+ width: var(--compose-reply-attachment-width);
7568
+ height: var(--compose-reply-attachment-height);
7569
+ min-width: 0;
7570
+ max-width: var(--compose-reply-attachment-width);
7571
+ max-height: var(--compose-reply-attachment-height);
7572
+ object-fit: cover;
7573
+ object-position: center;
7574
+ }
7575
+
7576
+ .compose-reply-compose-layout
7577
+ .compose-editor-row
7578
+ .compose-attachment-preview-fallback,
7579
+ .compose-reply-compose-layout
7580
+ .compose-editor-row
7581
+ .compose-attachment-processing {
7582
+ width: var(--compose-reply-attachment-width);
7583
+ height: var(--compose-reply-attachment-height);
7584
+ aspect-ratio: auto;
7585
+ }
7586
+
7587
+ .compose-reply-compose-layout
7588
+ .compose-editor-row
7589
+ .compose-attachment-file-card,
7590
+ .compose-reply-compose-layout
7591
+ .compose-editor-row
7592
+ .compose-attachment:not(:only-child)
7593
+ .compose-attachment-file-card {
7594
+ width: 112px;
7595
+ height: var(--compose-reply-attachment-height);
7596
+ gap: 4px;
7597
+ padding: 8px 10px;
7598
+ }
7599
+
7600
+ .compose-reply-compose-layout .compose-editor-row .compose-attachment-alt {
7601
+ padding: 1px 3px;
7602
+ font-size: var(--type-ui-micro);
7603
+ line-height: 1.2;
7604
+ }
7605
+
7606
+ .compose-reply-compose-layout .compose-editor-row .compose-attachment-remove {
7607
+ top: 4px;
7608
+ right: 4px;
7609
+ width: 20px;
7610
+ height: 20px;
7611
+ }
7612
+
7493
7613
  /* Dot marker for thread positions */
7494
7614
  .compose-thread-dot {
7495
7615
  width: var(--site-thread-marker-size);
@@ -7977,6 +8097,10 @@
7977
8097
  padding-top: 8px;
7978
8098
  }
7979
8099
 
8100
+ .compose-reply-compose-layout .compose-thread-post-header + .compose-body {
8101
+ padding-top: 12px;
8102
+ }
8103
+
7980
8104
  /* Thread format selector: pill-tag style (lighter than the main segmented) */
7981
8105
  .compose-thread-segmented {
7982
8106
  background: transparent;
@@ -8700,6 +8824,11 @@
8700
8824
  font-family: var(--font-body);
8701
8825
  }
8702
8826
 
8827
+ .compose-reply-compose-layout .compose-tiptap-body .tiptap {
8828
+ font-size: var(--type-content-body);
8829
+ line-height: var(--type-body-leading);
8830
+ }
8831
+
8703
8832
  .compose-tiptap-thoughts .tiptap {
8704
8833
  min-height: 72px;
8705
8834
  }
@@ -8738,23 +8867,36 @@
8738
8867
 
8739
8868
  .compose-tiptap-body .tiptap h2 {
8740
8869
  font-size: var(--type-content-title);
8741
- font-style: italic;
8742
8870
  margin-top: 2.1rem;
8743
8871
  margin-bottom: 1.4rem;
8744
8872
  }
8745
8873
 
8746
8874
  .compose-tiptap-body .tiptap h3 {
8747
8875
  font-size: var(--type-content-subtitle);
8748
- font-style: italic;
8749
8876
  margin-top: 2rem;
8750
8877
  margin-bottom: 1.4rem;
8751
8878
  }
8752
8879
 
8880
+ .compose-reply-compose-layout
8881
+ .compose-tiptap-body
8882
+ .tiptap
8883
+ :is(h1, h2, h3, h4) {
8884
+ font-size: var(--type-secondary);
8885
+ line-height: 1.32;
8886
+ margin-top: 1.4rem;
8887
+ margin-bottom: 0.75rem;
8888
+ }
8889
+
8753
8890
  .compose-tiptap-body .tiptap p {
8754
8891
  margin-top: 1.4rem;
8755
8892
  margin-bottom: 1.4rem;
8756
8893
  }
8757
8894
 
8895
+ .compose-reply-compose-layout .compose-tiptap-body .tiptap p {
8896
+ margin-top: 1rem;
8897
+ margin-bottom: 1rem;
8898
+ }
8899
+
8758
8900
  /* In inline-format compose (reply / edit-reply / thread — the editors wrapped
8759
8901
  in .compose-editor-row), the format header sits right above the body, so the
8760
8902
  first block's 1.4rem top margin reads as a gap between the format pills and
@@ -171,6 +171,19 @@ export interface NavItemView {
171
171
  freshAt?: number;
172
172
  }
173
173
 
174
+ export type SuggestedNavLinkTargetType = "page" | "collection" | "archive";
175
+
176
+ export type SuggestedNavLinkNavItemType = "link" | "collection";
177
+
178
+ export interface SuggestedNavLink {
179
+ key: string;
180
+ label: string;
181
+ url: string;
182
+ targetType: SuggestedNavLinkTargetType;
183
+ navItemType: SuggestedNavLinkNavItemType;
184
+ collectionId?: string;
185
+ }
186
+
174
187
  /**
175
188
  * Search result from FTS5
176
189
  */
@@ -43,6 +43,22 @@ export const ComposeForm: FC<ComposeFormProps> = ({
43
43
  comment: "@context: Close compose dialog",
44
44
  }),
45
45
  ),
46
+ imageNotRehosted: i18n._(
47
+ msg({
48
+ message:
49
+ "An image couldn't be saved to your library — its original link was kept.",
50
+ comment:
51
+ "@context: Toast when a single pasted remote image couldn't be rehosted (e.g. blocked by the source's hotlink protection)",
52
+ }),
53
+ ),
54
+ imagesNotRehosted: i18n._(
55
+ msg({
56
+ message:
57
+ "{count} images couldn't be saved to your library — their original links were kept.",
58
+ comment:
59
+ "@context: Toast when several pasted remote images couldn't be rehosted; {count} is the number of images",
60
+ }),
61
+ ),
46
62
  note: i18n._(
47
63
  msg({
48
64
  message: "Note",