@jant/core 0.6.11 → 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 (72) hide show
  1. package/bin/commands/import-site.js +45 -20
  2. package/dist/app-B5P1b4TF.js +6 -0
  3. package/dist/{app-CpmficmQ.js → app-D-zAhayc.js} +331 -55
  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-Dd9U383b.js → client-S1cdvulk.js} +1 -1
  7. package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-YW92ZH7u.js} +116 -55
  8. package/dist/{export-Ba7NJImL.js → export-Ckwh4BiE.js} +4 -4
  9. package/dist/{github-sync-BD4w2m8-.js → github-sync-BMmK3JZ0.js} +2 -2
  10. package/dist/{github-sync-Cb4_6_i7.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 +20 -1
  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/jant-command-palette.ts +67 -2
  25. package/src/client/components/jant-compose-dialog.ts +8 -2
  26. package/src/client/components/jant-nav-manager.ts +142 -2
  27. package/src/client/components/jant-settings-general.ts +56 -0
  28. package/src/client/components/nav-manager-types.ts +14 -0
  29. package/src/client/components/settings-types.ts +30 -0
  30. package/src/client/compose-launch.ts +30 -0
  31. package/src/client/compose-shortcuts.ts +46 -3
  32. package/src/client/tiptap/__tests__/rehost-images.test.ts +66 -13
  33. package/src/client/tiptap/rehost-images.ts +5 -1
  34. package/src/i18n/__tests__/fallback.test.ts +24 -0
  35. package/src/i18n/locales/public/zh-Hans.po +2 -2
  36. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  37. package/src/i18n/locales/public/zh-Hant.po +2 -2
  38. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  39. package/src/i18n/locales/settings/en.po +54 -0
  40. package/src/i18n/locales/settings/en.ts +1 -1
  41. package/src/i18n/locales/settings/zh-Hans.po +64 -10
  42. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  43. package/src/i18n/locales/settings/zh-Hant.po +56 -2
  44. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  45. package/src/lib/__tests__/feed.test.ts +96 -10
  46. package/src/lib/feed.ts +63 -3
  47. package/src/lib/post-display.ts +14 -8
  48. package/src/preset.css +60 -18
  49. package/src/routes/dash/settings.tsx +41 -2
  50. package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
  51. package/src/routes/pages/page.tsx +44 -12
  52. package/src/services/__tests__/about-page.test.ts +116 -0
  53. package/src/services/__tests__/navigation.test.ts +190 -1
  54. package/src/services/__tests__/post-timeline.test.ts +119 -2
  55. package/src/services/about-page.ts +143 -0
  56. package/src/services/export-theme/layouts/_default/rss.xml +2 -1
  57. package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
  58. package/src/services/export-theme/styles/main.css +46 -14
  59. package/src/services/index.ts +27 -17
  60. package/src/services/navigation.ts +155 -1
  61. package/src/services/post.ts +24 -7
  62. package/src/styles/components.css +20 -0
  63. package/src/styles/tokens.css +26 -8
  64. package/src/styles/ui.css +149 -7
  65. package/src/types/views.ts +13 -0
  66. package/src/ui/dash/appearance/NavigationContent.tsx +57 -0
  67. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
  68. package/src/ui/dash/settings/GeneralContent.tsx +44 -0
  69. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
  70. package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
  71. package/dist/app-DqKkZenB.js +0 -6
  72. package/dist/client/_assets/client-BhHHVvSY.css +0 -2
@@ -9,11 +9,50 @@ import { now } from "../../lib/time.js";
9
9
 
10
10
  const TEST_COLLECTION_ID = "col_test00000000000000000000001";
11
11
  const TEST_COLLECTION_ID_2 = "col_test00000000000000000000002";
12
+ const TEST_POST_ID = "pst_test00000000000000000000001";
13
+ const TEST_POST_ID_2 = "pst_test00000000000000000000002";
14
+
15
+ function insertTestPath(
16
+ sqlite: ReturnType<typeof createTestDatabase>["sqlite"],
17
+ input: {
18
+ id: string;
19
+ path: string;
20
+ kind: "slug" | "alias" | "redirect" | "archive";
21
+ postId?: string | null;
22
+ collectionId?: string | null;
23
+ redirectToPath?: string | null;
24
+ redirectType?: 301 | 302 | null;
25
+ archiveQuery?: string | null;
26
+ },
27
+ ) {
28
+ const ts = now();
29
+ sqlite
30
+ .prepare(
31
+ `INSERT INTO path_registry (
32
+ id, site_id, path, kind, post_id, collection_id, redirect_to_path,
33
+ redirect_type, archive_query, created_at, updated_at
34
+ )
35
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
36
+ )
37
+ .run(
38
+ input.id,
39
+ DEFAULT_TEST_SITE_ID,
40
+ input.path,
41
+ input.kind,
42
+ input.postId ?? null,
43
+ input.collectionId ?? null,
44
+ input.redirectToPath ?? null,
45
+ input.redirectType ?? null,
46
+ input.archiveQuery ?? null,
47
+ ts,
48
+ ts,
49
+ );
50
+ }
12
51
 
13
52
  function insertTestCollection(
14
53
  sqlite: ReturnType<typeof createTestDatabase>["sqlite"],
15
54
  id: string,
16
- _slug: string,
55
+ slug: string,
17
56
  title: string,
18
57
  ) {
19
58
  const ts = now();
@@ -23,6 +62,51 @@ function insertTestCollection(
23
62
  VALUES (?, ?, ?, 'newest', ?, ?)`,
24
63
  )
25
64
  .run(id, DEFAULT_TEST_SITE_ID, title, ts, ts);
65
+ insertTestPath(sqlite, {
66
+ id: `pth_col_${id.slice(4)}`,
67
+ path: slug,
68
+ kind: "slug",
69
+ collectionId: id,
70
+ });
71
+ }
72
+
73
+ function insertTestPost(
74
+ sqlite: ReturnType<typeof createTestDatabase>["sqlite"],
75
+ input: {
76
+ id: string;
77
+ slug: string;
78
+ title: string | null;
79
+ status?: "draft" | "published";
80
+ visibility?: "public" | "latest_hidden" | "private";
81
+ },
82
+ ) {
83
+ const ts = now();
84
+ sqlite
85
+ .prepare(
86
+ `INSERT INTO post (
87
+ id, site_id, format, status, visibility, title, thread_id,
88
+ published_at, last_activity_at, created_at, updated_at
89
+ )
90
+ VALUES (?, ?, 'note', ?, ?, ?, ?, ?, ?, ?, ?)`,
91
+ )
92
+ .run(
93
+ input.id,
94
+ DEFAULT_TEST_SITE_ID,
95
+ input.status ?? "published",
96
+ input.visibility ?? "latest_hidden",
97
+ input.title,
98
+ input.id,
99
+ ts,
100
+ ts,
101
+ ts,
102
+ ts,
103
+ );
104
+ insertTestPath(sqlite, {
105
+ id: `pth_pst_${input.id.slice(4)}`,
106
+ path: input.slug,
107
+ kind: "slug",
108
+ postId: input.id,
109
+ });
26
110
  }
27
111
 
28
112
  describe("NavItemService", () => {
@@ -277,6 +361,111 @@ describe("NavItemService", () => {
277
361
  });
278
362
  });
279
363
 
364
+ describe("listSuggestedLinks", () => {
365
+ it("suggests a published /about page as a normal link", async () => {
366
+ insertTestPost(sqlite, {
367
+ id: TEST_POST_ID,
368
+ slug: "about",
369
+ title: "About me",
370
+ });
371
+
372
+ const suggestions = await navItemService.listSuggestedLinks();
373
+
374
+ expect(suggestions).toEqual([
375
+ {
376
+ key: "about",
377
+ label: "About me",
378
+ url: "/about",
379
+ targetType: "page",
380
+ navItemType: "link",
381
+ },
382
+ ]);
383
+ });
384
+
385
+ it("suggests a canonical /now collection as a collection nav item", async () => {
386
+ insertTestCollection(sqlite, TEST_COLLECTION_ID, "now", "Now");
387
+
388
+ const suggestions = await navItemService.listSuggestedLinks();
389
+
390
+ expect(suggestions).toEqual([
391
+ {
392
+ key: "now",
393
+ label: "Now",
394
+ url: "/now",
395
+ targetType: "collection",
396
+ navItemType: "collection",
397
+ collectionId: TEST_COLLECTION_ID,
398
+ },
399
+ ]);
400
+ });
401
+
402
+ it("suggests a /now collection alias as a normal link to preserve the path", async () => {
403
+ insertTestCollection(sqlite, TEST_COLLECTION_ID, "updates", "Updates");
404
+ insertTestPath(sqlite, {
405
+ id: "pth_now_alias",
406
+ path: "now",
407
+ kind: "alias",
408
+ collectionId: TEST_COLLECTION_ID,
409
+ });
410
+
411
+ const suggestions = await navItemService.listSuggestedLinks();
412
+
413
+ expect(suggestions).toEqual([
414
+ {
415
+ key: "now",
416
+ label: "Updates",
417
+ url: "/now",
418
+ targetType: "collection",
419
+ navItemType: "link",
420
+ },
421
+ ]);
422
+ });
423
+
424
+ it("hides suggestions already represented by path or collection id", async () => {
425
+ insertTestPost(sqlite, {
426
+ id: TEST_POST_ID,
427
+ slug: "about",
428
+ title: "About",
429
+ });
430
+ insertTestCollection(sqlite, TEST_COLLECTION_ID, "now", "Now");
431
+
432
+ await navItemService.create({
433
+ type: "link",
434
+ label: "About",
435
+ url: "/about/",
436
+ });
437
+ await navItemService.create({
438
+ type: "collection",
439
+ collectionId: TEST_COLLECTION_ID,
440
+ label: "Now",
441
+ url: "/now",
442
+ });
443
+
444
+ const suggestions = await navItemService.listSuggestedLinks();
445
+
446
+ expect(suggestions).toEqual([]);
447
+ });
448
+
449
+ it("does not suggest draft or private posts", async () => {
450
+ insertTestPost(sqlite, {
451
+ id: TEST_POST_ID,
452
+ slug: "about",
453
+ title: "About",
454
+ status: "draft",
455
+ });
456
+ insertTestPost(sqlite, {
457
+ id: TEST_POST_ID_2,
458
+ slug: "now",
459
+ title: "Now",
460
+ visibility: "private",
461
+ });
462
+
463
+ const suggestions = await navItemService.listSuggestedLinks();
464
+
465
+ expect(suggestions).toEqual([]);
466
+ });
467
+ });
468
+
280
469
  describe("update", () => {
281
470
  it("updates a nav item's label", async () => {
282
471
  const created = await navItemService.create({
@@ -464,8 +464,86 @@ describe("PostService - Timeline features", () => {
464
464
  });
465
465
  });
466
466
 
467
+ describe("listCollectionThreadRootIds", () => {
468
+ it("bumps a hidden collection thread when a non-quiet reply is published", async () => {
469
+ const collection = await collectionService.create({
470
+ slug: "thinking",
471
+ title: "Thinking",
472
+ });
473
+ const hiddenRoot = await postService.create({
474
+ format: "note",
475
+ bodyMarkdown: "Hidden root",
476
+ visibility: "latest_hidden",
477
+ publishedAt: 1000,
478
+ });
479
+ const newerRoot = await postService.create({
480
+ format: "note",
481
+ bodyMarkdown: "Newer root",
482
+ publishedAt: 2000,
483
+ });
484
+
485
+ await collectionService.addPost(collection.id, hiddenRoot.id);
486
+ await collectionService.addPost(collection.id, newerRoot.id);
487
+
488
+ await postService.create({
489
+ format: "note",
490
+ bodyMarkdown: "Normal reply",
491
+ replyToId: hiddenRoot.id,
492
+ publishedAt: 3000,
493
+ });
494
+
495
+ const rootIds = await postService.listCollectionThreadRootIds(
496
+ collection.id,
497
+ {
498
+ status: "published",
499
+ sortOrder: "newest",
500
+ },
501
+ );
502
+
503
+ expect(rootIds).toEqual([hiddenRoot.id, newerRoot.id]);
504
+ });
505
+
506
+ it("does not bump a collection thread when a reply is quiet", async () => {
507
+ const collection = await collectionService.create({
508
+ slug: "quiet",
509
+ title: "Quiet",
510
+ });
511
+ const olderRoot = await postService.create({
512
+ format: "note",
513
+ bodyMarkdown: "Older root",
514
+ publishedAt: 1000,
515
+ });
516
+ const newerRoot = await postService.create({
517
+ format: "note",
518
+ bodyMarkdown: "Newer root",
519
+ publishedAt: 2000,
520
+ });
521
+
522
+ await collectionService.addPost(collection.id, olderRoot.id);
523
+ await collectionService.addPost(collection.id, newerRoot.id);
524
+
525
+ await postService.create({
526
+ format: "note",
527
+ bodyMarkdown: "Quiet reply",
528
+ replyToId: olderRoot.id,
529
+ publishedAt: 3000,
530
+ quietReply: true,
531
+ });
532
+
533
+ const rootIds = await postService.listCollectionThreadRootIds(
534
+ collection.id,
535
+ {
536
+ status: "published",
537
+ sortOrder: "newest",
538
+ },
539
+ );
540
+
541
+ expect(rootIds).toEqual([newerRoot.id, olderRoot.id]);
542
+ });
543
+ });
544
+
467
545
  describe("listCollectionFeedEntries", () => {
468
- it("orders collection feeds by publishedAt and returns thread roots", async () => {
546
+ it("orders collection feeds by thread activity and returns thread roots", async () => {
469
547
  const collection = await collectionService.create({
470
548
  slug: "reading",
471
549
  title: "Reading",
@@ -565,11 +643,50 @@ describe("PostService - Timeline features", () => {
565
643
  );
566
644
 
567
645
  expect(entries).toHaveLength(2);
568
- // Sorted by publishedAt DESC: secondRoot created last has latest publishedAt
569
646
  expect(entries[0]?.post.id).toBe(secondRoot.id);
570
647
  expect(entries[0]?.collectedAt).toBe(200);
571
648
  expect(entries[1]?.post.id).toBe(sharedRoot.id);
572
649
  expect(entries[1]?.collectedAt).toBe(300);
573
650
  });
651
+
652
+ it("orders collection feeds by thread activity from non-quiet replies", async () => {
653
+ const collection = await collectionService.create({
654
+ slug: "activity",
655
+ title: "Activity",
656
+ });
657
+ const hiddenRoot = await postService.create({
658
+ format: "note",
659
+ bodyMarkdown: "Hidden root",
660
+ visibility: "latest_hidden",
661
+ publishedAt: 1000,
662
+ });
663
+ const newerRoot = await postService.create({
664
+ format: "note",
665
+ bodyMarkdown: "Newer root",
666
+ publishedAt: 2000,
667
+ });
668
+
669
+ await collectionService.addPost(collection.id, hiddenRoot.id);
670
+ await collectionService.addPost(collection.id, newerRoot.id);
671
+
672
+ await postService.create({
673
+ format: "note",
674
+ bodyMarkdown: "Normal reply",
675
+ replyToId: hiddenRoot.id,
676
+ publishedAt: 3000,
677
+ });
678
+
679
+ const entries = await postService.listCollectionFeedEntries(
680
+ collection.id,
681
+ {
682
+ status: "published",
683
+ },
684
+ );
685
+
686
+ expect(entries.map((entry) => entry.post.id)).toEqual([
687
+ hiddenRoot.id,
688
+ newerRoot.id,
689
+ ]);
690
+ });
574
691
  });
575
692
  });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * About Page Service
3
+ *
4
+ * Treats `/about` as the standard site About page. The page itself remains a
5
+ * normal post; this service reads that convention and can create the page when
6
+ * the author follows the settings prompt.
7
+ */
8
+
9
+ import { ValidationError } from "../lib/errors.js";
10
+ import type { Visibility } from "../types.js";
11
+ import type { CollectionService } from "./collection.js";
12
+ import type { PathService } from "./path.js";
13
+ import type { PostService } from "./post.js";
14
+
15
+ export const ABOUT_PAGE_PATH = "/about";
16
+ export const ABOUT_PAGE_SLUG = "about";
17
+
18
+ export type AboutPageStatus =
19
+ | {
20
+ state: "missing";
21
+ path: typeof ABOUT_PAGE_PATH;
22
+ }
23
+ | {
24
+ state: "ready";
25
+ path: typeof ABOUT_PAGE_PATH;
26
+ post: {
27
+ id: string;
28
+ title: string | null;
29
+ status: "draft" | "published";
30
+ visibility: Visibility;
31
+ };
32
+ }
33
+ | {
34
+ state: "conflict";
35
+ path: typeof ABOUT_PAGE_PATH;
36
+ conflict: {
37
+ targetType: "collection" | "redirect" | "archive" | "post";
38
+ id: string | null;
39
+ title: string | null;
40
+ };
41
+ };
42
+
43
+ export interface AboutPageService {
44
+ getStatus(): Promise<AboutPageStatus>;
45
+ ensurePage(): Promise<Extract<AboutPageStatus, { state: "ready" }>["post"]>;
46
+ }
47
+
48
+ export function createAboutPageService(deps: {
49
+ paths: PathService;
50
+ posts: PostService;
51
+ collections: CollectionService;
52
+ }): AboutPageService {
53
+ async function readStatus(): Promise<AboutPageStatus> {
54
+ const resolved = await deps.paths.resolve(ABOUT_PAGE_SLUG);
55
+ const base = {
56
+ path: ABOUT_PAGE_PATH,
57
+ } as const;
58
+
59
+ if (!resolved) {
60
+ return { ...base, state: "missing" };
61
+ }
62
+
63
+ if (resolved.targetType === "post" && resolved.postId) {
64
+ const post = await deps.posts.getById(resolved.postId);
65
+ if (!post) {
66
+ return {
67
+ ...base,
68
+ state: "conflict",
69
+ conflict: {
70
+ targetType: "post",
71
+ id: resolved.postId,
72
+ title: null,
73
+ },
74
+ };
75
+ }
76
+
77
+ return {
78
+ ...base,
79
+ state: "ready",
80
+ post: {
81
+ id: post.id,
82
+ title: post.title,
83
+ status: post.status,
84
+ visibility: post.visibility,
85
+ },
86
+ };
87
+ }
88
+
89
+ if (resolved.targetType === "collection" && resolved.collectionId) {
90
+ const collection = await deps.collections.getById(resolved.collectionId);
91
+ return {
92
+ ...base,
93
+ state: "conflict",
94
+ conflict: {
95
+ targetType: "collection",
96
+ id: resolved.collectionId,
97
+ title: collection?.title ?? null,
98
+ },
99
+ };
100
+ }
101
+
102
+ return {
103
+ ...base,
104
+ state: "conflict",
105
+ conflict: {
106
+ targetType: resolved.targetType,
107
+ id: null,
108
+ title: null,
109
+ },
110
+ };
111
+ }
112
+
113
+ return {
114
+ getStatus: readStatus,
115
+
116
+ async ensurePage() {
117
+ const status = await readStatus();
118
+ if (status.state === "ready") {
119
+ return status.post;
120
+ }
121
+
122
+ if (status.state === "conflict") {
123
+ throw new ValidationError(
124
+ "/about is already used. Rename that item before creating an About page.",
125
+ );
126
+ }
127
+
128
+ const post = await deps.posts.create({
129
+ format: "note",
130
+ title: "About",
131
+ slug: ABOUT_PAGE_SLUG,
132
+ status: "published",
133
+ visibility: "latest_hidden",
134
+ });
135
+ return {
136
+ id: post.id,
137
+ title: post.title,
138
+ status: post.status,
139
+ visibility: post.visibility,
140
+ };
141
+ },
142
+ };
143
+ }
@@ -23,7 +23,8 @@
23
23
  body → Hugo-rendered .Content (stripped of <script>/<style>)
24
24
  rating → <p>★★★★☆ 4/5</p>
25
25
  link → trailing <p><a href=permalink> ★ </a></p>
26
- replies → <hr/> + <p><small><time>{dt}</time></small></p> + content
26
+ replies → <hr/> + <p><small><time>{dt}</time></small></p>
27
+ + inline title/link metadata + content
27
28
 
28
29
  Hugo's default RSS media-type override (`[outputFormats.RSS]`) makes this
29
30
  template produce application/atom+xml at /{section}/index.xml.
@@ -8,6 +8,8 @@
8
8
  link-format "★" suffix)
9
9
  includeReplies — when true, appends each public reply separated by
10
10
  <hr/> + <small><time></time></small>
11
+ inline — when true, renders title/link metadata that top-level
12
+ Atom entries expose through their entry fields
11
13
 
12
14
  Output is raw HTML suitable for embedding inside Atom
13
15
  <content type="html"><![CDATA[...]]></content>. Since CDATA ends at the
@@ -18,9 +20,31 @@
18
20
  {{- $post := .post -}}
19
21
  {{- $permalink := .permalink -}}
20
22
  {{- $includeReplies := .includeReplies -}}
23
+ {{- $inline := false -}}
24
+ {{- with .inline -}}{{- $inline = . -}}{{- end -}}
21
25
  {{- $format := $post.Params.format | default "note" -}}
22
26
  {{- $parts := slice -}}
23
27
 
28
+ {{- /* --- Inline title/link metadata for thread replies --- */ -}}
29
+ {{- if and $inline (ne $format "quote") -}}
30
+ {{- $title := $post.Title | default "" -}}
31
+ {{- $linkUrl := string ($post.Params.link_url | default "") -}}
32
+ {{- if eq $format "link" -}}
33
+ {{- if ne $linkUrl "" -}}
34
+ {{- $domain := $linkUrl -}}
35
+ {{- with urls.Parse $linkUrl -}}{{- if .Host -}}{{- $domain = replaceRE `^(www|m|mobile)\.` "" .Host -}}{{- end -}}{{- end -}}
36
+ {{- $parts = $parts | append (printf `<p><a href=%q>%s</a></p>` $linkUrl (transform.XMLEscape $domain)) -}}
37
+ {{- end -}}
38
+ {{- if ne $title "" -}}
39
+ {{- $titleHref := $permalink -}}
40
+ {{- if ne $linkUrl "" -}}{{- $titleHref = $linkUrl -}}{{- end -}}
41
+ {{- $parts = $parts | append (printf `<h2><a href=%q>%s</a></h2>` $titleHref (transform.XMLEscape $title)) -}}
42
+ {{- end -}}
43
+ {{- else if ne $title "" -}}
44
+ {{- $parts = $parts | append (printf `<h2><a href=%q>%s</a></h2>` $permalink (transform.XMLEscape $title)) -}}
45
+ {{- end -}}
46
+ {{- end -}}
47
+
24
48
  {{- /* --- Quote block (format=quote) --- */ -}}
25
49
  {{- if eq $format "quote" -}}
26
50
  {{- $quoteText := string ($post.Params.quote_text | default "") -}}
@@ -100,7 +124,7 @@
100
124
  {{- $replyLabel := .Date.Format "Jan 2, 2006 15:04" -}}
101
125
  {{- $parts = $parts | append "<hr/>" -}}
102
126
  {{- $parts = $parts | append (printf `<p><small><time datetime=%q>%s</time></small></p>` $replyDatetime (transform.XMLEscape $replyLabel)) -}}
103
- {{- $parts = $parts | append (partial "feed-post-content.xml" (dict "post" . "permalink" $replyPermalink "includeReplies" false)) -}}
127
+ {{- $parts = $parts | append (partial "feed-post-content.xml" (dict "post" . "permalink" $replyPermalink "includeReplies" false "inline" true)) -}}
104
128
  {{- end -}}
105
129
  {{- end -}}
106
130
 
@@ -84,13 +84,11 @@ h1 {
84
84
 
85
85
  h2 {
86
86
  font-size: var(--type-content-title);
87
- font-style: italic;
88
87
  margin: 2.1rem 0 1.4rem;
89
88
  }
90
89
 
91
90
  h3 {
92
91
  font-size: var(--type-content-subtitle);
93
- font-style: italic;
94
92
  margin: 2rem 0 1.4rem;
95
93
  }
96
94
 
@@ -119,6 +117,12 @@ kbd,
119
117
  samp {
120
118
  font-family: var(--font-mono);
121
119
  font-size: var(--type-code);
120
+ line-height: 1.42;
121
+ }
122
+
123
+ code {
124
+ color: var(--site-code-text);
125
+ overflow-wrap: break-word;
122
126
  }
123
127
 
124
128
  pre {
@@ -127,23 +131,26 @@ pre {
127
131
  line-height: 1.5;
128
132
  padding: 1rem;
129
133
  overflow-x: auto;
130
- background-color: var(--site-feed-card-bg);
131
- border: 1px solid var(--site-feed-card-border);
132
- border-radius: 0.375rem;
134
+ color: var(--site-code-text);
135
+ background-color: var(--site-code-block-bg);
136
+ border: 1px solid var(--site-code-block-border);
137
+ border-radius: 6px;
133
138
  }
134
139
 
135
140
  pre code {
136
141
  padding: 0;
137
142
  background: transparent;
138
143
  border: 0;
144
+ color: inherit;
139
145
  font-size: inherit;
140
146
  }
141
147
 
142
148
  :not(pre) > code {
143
- padding: 0.1em 0.35em;
144
- background-color: var(--site-feed-card-bg);
145
- border: 1px solid var(--site-feed-card-border);
146
- border-radius: 0.25rem;
149
+ padding: 0.08em 0.28em;
150
+ background-color: var(--site-code-bg);
151
+ border-radius: 0.22em;
152
+ box-decoration-break: clone;
153
+ -webkit-box-decoration-break: clone;
147
154
  }
148
155
 
149
156
  /* Blockquotes — tinted background card with quote icon (matches main site). */
@@ -217,13 +224,13 @@ blockquote cite {
217
224
 
218
225
  ul,
219
226
  ol {
220
- padding-left: 1.625em;
227
+ padding-left: 1.4em;
221
228
  margin: 1.25em 0;
222
229
  }
223
230
 
224
231
  li {
225
- margin-top: 1.2em;
226
- margin-bottom: 1.2em;
232
+ margin-top: 0.65em;
233
+ margin-bottom: 0.65em;
227
234
  }
228
235
 
229
236
  /* Normalize li > p so list spacing is controlled by li alone,
@@ -850,6 +857,31 @@ hr.feed-divider {
850
857
  color: var(--site-reading-body);
851
858
  }
852
859
 
860
+ .post-card > .post-card-summary :is(h1, h2),
861
+ .post-card > .post-card-quote-commentary :is(h1, h2) {
862
+ font-size: calc(var(--type-content-body) * 1.12);
863
+ font-weight: var(--fw-medium);
864
+ margin-top: 1.45rem;
865
+ margin-bottom: 0.55rem;
866
+ }
867
+
868
+ .post-card > .post-card-summary :is(h3, h4),
869
+ .post-card > .post-card-quote-commentary :is(h3, h4) {
870
+ font-size: var(--type-content-body);
871
+ font-weight: var(--fw-medium);
872
+ margin-top: 1.2rem;
873
+ margin-bottom: 0.45rem;
874
+ }
875
+
876
+ .post-card > .post-card-summary :is(h5, h6),
877
+ .post-card > .post-card-quote-commentary :is(h5, h6) {
878
+ font-size: var(--type-secondary);
879
+ font-weight: var(--fw-medium);
880
+ color: var(--site-text-secondary);
881
+ margin-top: 1rem;
882
+ margin-bottom: 0.35rem;
883
+ }
884
+
853
885
  /* Link card domain row — shown ABOVE the title (matches main site's
854
886
  `.feed-link-domain` pattern). Inline flex with icon + host text. */
855
887
  .post-card-link-domain,
@@ -1009,8 +1041,8 @@ hr.feed-divider {
1009
1041
  .post-card-quote-content {
1010
1042
  font-family: var(--font-serif);
1011
1043
  color: var(--site-text-primary);
1012
- font-size: var(--type-content-subtitle);
1013
- line-height: var(--type-heading-leading);
1044
+ font-size: var(--type-content-quote);
1045
+ line-height: var(--type-content-quote-leading);
1014
1046
  white-space: pre-line;
1015
1047
  text-wrap: pretty;
1016
1048
  margin: 0;