@dogsbay/format-astro 0.2.0-beta.124 → 0.2.0-beta.126

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.
@@ -21,58 +21,15 @@
21
21
  * decoration layer is invisible until a DiffTree arrives.
22
22
  */
23
23
  import type { TreeNode } from "@dogsbay/types";
24
- /** Structural mirror of @dogsbay/tree-diff's DiffNode fields. */
25
- export interface DiffMarks {
26
- diff?: "added" | "removed" | "changed" | "moved";
27
- diffInline?: {
28
- op: "eq" | "ins" | "del";
29
- text: string;
30
- }[];
31
- diffPeer?: TreeNode;
32
- diffMovedFrom?: string[];
33
- }
34
- export declare const diffMarksOf: (node: TreeNode) => DiffMarks;
35
- export declare function hasDiffMarks(node: TreeNode): boolean;
24
+ export { DIFF_CSS, buildMergedCodeDiff, diffMarksOf, hasDiffMarks, inlineSegmentsToHtml, type DiffMarks, type MergedCodeDiff, } from "@dogsbay/render-html/diff";
36
25
  /**
37
26
  * Inject attributes into the FIRST opening tag of a rendered
38
27
  * snippet. Handles components (`<TableRow`), plain tags (`<p`), and
39
28
  * skips leading whitespace/JSX expressions.
40
29
  */
41
30
  export declare function injectAttrs(rendered: string, attrs: string): string;
42
- /**
43
- * Render the merged word-level segments of a changed prose node.
44
- * Formatting inside changed prose flattens to text in this view —
45
- * the marks are the content (the unchanged siblings keep their full
46
- * inline rendering).
47
- */
48
- export declare function inlineSegmentsToHtml(segments: NonNullable<DiffMarks["diffInline"]>): string;
49
- export interface MergedCodeDiff {
50
- /** Unified code: unchanged lines, removed lines, added lines. */
51
- code: string;
52
- /** 1-based line numbers, for CodeRich's diffAdd/diffRemove props. */
53
- addLines: number[];
54
- removeLines: number[];
55
- }
56
- /**
57
- * Merge two code texts into ONE unified block, reporting the changed
58
- * lines BY NUMBER rather than by `[!code]` comment markers.
59
- *
60
- * Markers were the first design and they leak: Shiki can only strip a
61
- * marker it recognizes as a comment in the grammar, so in `plaintext`
62
- * — which every unknown language (`package-install`, doc-tool tokens)
63
- * falls back to — the marker renders as literal text in the reader's
64
- * code. Line numbers are language-independent and can't leak.
65
- *
66
- * Returns null over the line cap; callers fall back to before/after.
67
- */
68
- export declare function buildMergedCodeDiff(oldCode: string, newCode: string): MergedCodeDiff | null;
69
31
  /**
70
32
  * Decorate one rendered node. `renderPeer` renders the other side of
71
33
  * a changed pair through the same serializer (code before/after).
72
34
  */
73
35
  export declare function decorateDiff(node: TreeNode, rendered: string, renderPeer: (peer: TreeNode) => string): string;
74
- /**
75
- * The diff stylesheet the generated site carries — theme-token based,
76
- * both color modes, no hardcoded palette (key-decisions doctrine).
77
- */
78
- export declare const DIFF_CSS = "/* tree-diff decorations \u2014 one DiffTree, rendered by the normal components.\n Mark styling is GUARDED by html:not([data-diff-hidden]) rather than\n force-reset under hidden mode: a reset like \"border-left: none\n !important\" also clobbers a component's OWN border (an added warning\n callout lost its left edge \u2014 user find, adapters/mysql). Guarded\n rules simply stop applying, and component styling survives. */\n[data-diff] { position: relative; }\nhtml:not([data-diff-hidden]) [data-diff=\"added\"] {\n background: color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 12%, transparent);\n border-left: 3px solid var(--color-success, oklch(0.72 0.19 150));\n padding-left: 0.75rem;\n}\nhtml:not([data-diff-hidden]) [data-diff=\"removed\"] {\n background: color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 10%, transparent);\n border-left: 3px solid var(--color-destructive, oklch(0.64 0.21 25));\n padding-left: 0.75rem;\n opacity: 0.75;\n}\nhtml:not([data-diff-hidden]) [data-diff=\"removed\"] :is(p, li, code, pre) { text-decoration: line-through; text-decoration-thickness: 1px; }\nhtml:not([data-diff-hidden]) [data-diff=\"changed\"] {\n border-left: 3px solid var(--color-warning, oklch(0.8 0.16 85));\n padding-left: 0.75rem;\n}\nhtml:not([data-diff-hidden]) [data-diff=\"moved\"] {\n border-left: 3px dashed var(--color-info, oklch(0.7 0.14 240));\n padding-left: 0.75rem;\n}\nhtml:not([data-diff-hidden]) [data-diff=\"moved\"][data-diff-from]::after {\n content: \"moved from \" attr(data-diff-from);\n display: block;\n font-size: 0.75rem;\n color: var(--color-muted-foreground, inherit);\n opacity: 0.8;\n}\n\n/* Explicit \"added\"/\"removed\" label pills straddling the block's top\n edge. Components with their own chrome (callouts, code blocks)\n camouflage a tint-only mark \u2014 a green-tinted warning callout reads\n as a callout variant, not as new content (user feedback,\n better-auth adapters/mysql). Table rows are excluded (a pill can't\n straddle a tr); nested marks defer to the outermost label. */\nhtml:not([data-diff-hidden]) [data-diff=\"added\"]:not(tr, th, td)::before,\nhtml:not([data-diff-hidden]) [data-diff=\"removed\"]:not(tr, th, td)::before {\n position: absolute;\n top: 0;\n right: 0.6rem;\n transform: translateY(-55%);\n font-size: 0.65rem;\n font-weight: 600;\n letter-spacing: 0.04em;\n line-height: 1.4;\n padding: 0 0.5rem;\n border-radius: 99px;\n z-index: 1;\n text-decoration: none;\n}\nhtml:not([data-diff-hidden]) [data-diff=\"added\"]:not(tr, th, td)::before {\n content: \"added\";\n background: color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 25%, var(--color-background, Canvas));\n border: 1px solid color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 45%, transparent);\n}\nhtml:not([data-diff-hidden]) [data-diff=\"removed\"]:not(tr, th, td)::before {\n content: \"removed\";\n background: color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 20%, var(--color-background, Canvas));\n border: 1px solid color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 45%, transparent);\n}\n[data-diff] [data-diff]::before { content: none !important; }\n\n/* Nav change markers (the sidebar is client-rendered from nav.json, so\n the styles ship here rather than with the component). Same contract as\n the tab markers: a GLYPH carries the meaning, colour reinforces it,\n visually-hidden text announces it. */\n.db-nav-mark {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n margin-left: auto;\n border-radius: 99px;\n font-size: 0.75rem;\n font-weight: 700;\n line-height: 1;\n color: var(--color-muted-foreground, currentColor);\n}\n.db-nav-mark[data-nav-mark=\"added\"] { color: var(--color-success, oklch(0.72 0.19 150)); }\n.db-nav-mark[data-nav-mark=\"changed\"] { color: var(--color-warning, oklch(0.8 0.16 85)); }\n.db-nav-mark[data-nav-mark=\"removed\"] { color: var(--color-destructive, oklch(0.64 0.21 25)); }\n.db-nav-mark[data-nav-mark=\"moved\"] { color: var(--color-info, oklch(0.7 0.14 240)); }\n/* a subtree marker is a hint about descendants, not a claim about this row */\n.db-nav-mark[data-nav-mark-subtree] { opacity: 0.55; }\n/* removed pages get a SECOND non-colour signal */\n/* Belt-and-braces: a subtree mark must never strike through its own row. */\n[data-nav-mark-row=\"removed\"]:not([data-nav-mark-subtree]) { text-decoration: line-through; opacity: 0.7; }\n@media (forced-colors: active) {\n .db-nav-mark { color: CanvasText; }\n}\n\n/* Which tab changed? A marked panel is invisible until opened, so its\n trigger carries a marker (the group's bar says \"in here\", the marker\n says \"this one\"). The marker is a GLYPH, not a colored dot: colour\n alone fails WCAG 1.4.1 and background-tinted dots vanish entirely in\n forced-colors mode. Colour reinforces; shape carries the meaning.\n Visually-hidden text rides along (emitted by the serializer). */\n.db-tab-mark {\n display: inline-block;\n margin-left: 0.35rem;\n font-size: 0.75rem;\n font-weight: 700;\n line-height: 1;\n vertical-align: middle;\n color: var(--color-muted-foreground, currentColor);\n}\n[data-diff-hidden] .db-tab-mark { display: none; }\n.db-tab-mark[data-diff-tab=\"changed\"] { color: var(--color-warning, oklch(0.8 0.16 85)); }\n.db-tab-mark[data-diff-tab=\"added\"] { color: var(--color-success, oklch(0.72 0.19 150)); }\n.db-tab-mark[data-diff-tab=\"removed\"] { color: var(--color-destructive, oklch(0.64 0.21 25)); }\n.db-tab-mark[data-diff-tab=\"moved\"] { color: var(--color-info, oklch(0.7 0.14 240)); }\n@media (forced-colors: active) {\n .db-tab-mark { color: CanvasText; }\n}\nins.db-diff-ins {\n text-decoration: none;\n background: color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 22%, transparent);\n border-radius: 2px;\n padding: 0 1px;\n}\ndel.db-diff-del {\n background: color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 18%, transparent);\n border-radius: 2px;\n padding: 0 1px;\n}\n.db-diff-pair { display: grid; gap: 0.25rem; margin-block: 1rem; }\n.db-diff-pair-label {\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--color-muted-foreground, inherit);\n opacity: 0.8;\n}\ntr[data-diff] { border-left-width: 3px; }\n\n/* Folded unchanged sections are CHROME, not content callouts:\n dashed, dimmed, compact \u2014 visually \"collapsed context\". */\n.db-diff-fold {\n border: 1px dashed color-mix(in oklch, currentColor 25%, transparent) !important;\n border-left-width: 1px !important;\n background: transparent !important;\n opacity: 0.7;\n margin-block: 0.5rem !important;\n}\n.db-diff-fold:hover, .db-diff-fold[open] { opacity: 1; }\n.db-diff-fold summary, .db-diff-fold [data-slot=\"trigger\"] {\n font-size: 0.85rem;\n}\n\n/* The comparison banner at the top of each reader page. */\n.db-diff-banner-row { display: flex; flex-wrap: wrap; gap: 0.4rem 0.8rem; align-items: center; width: 100%; }\n.db-diff-banner {\n border: 1px solid color-mix(in oklch, currentColor 15%, transparent);\n border-radius: 8px;\n padding: 0.75rem 1rem;\n margin-block-end: 1.5rem;\n font-size: 0.85rem;\n display: flex;\n flex-wrap: wrap;\n gap: 0.4rem 1rem;\n align-items: center;\n}\n.db-diff-banner .db-chip {\n display: inline-block;\n border-radius: 99px;\n padding: 0.05rem 0.6rem;\n font-size: 0.75rem;\n border: 1px solid transparent;\n}\n.db-chip-added { background: color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 15%, transparent); }\n.db-chip-removed { background: color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 12%, transparent); }\n.db-chip-changed { background: color-mix(in oklch, var(--color-warning, oklch(0.8 0.16 85)) 15%, transparent); }\n.db-chip-moved { background: color-mix(in oklch, var(--color-info, oklch(0.7 0.14 240)) 12%, transparent); }\n.db-chip-muted { border-color: color-mix(in oklch, currentColor 25%, transparent); opacity: 0.8; }\n\n/* The persistent view-switcher bar (Reader / Docs-ops / Agent):\n perspectives on ONE comparison \u2014 tabs are links, verification\n lives in the chrome, the Monitor CTA sits next to the value. */\n.db-cmp-bar {\n position: sticky;\n /* BELOW the docs header, not under it: both were sticky at top:0 and\n the header's higher z-index simply swallowed this bar on scroll \u2014\n the chrome carrying the trust contract was the chrome that vanished.\n --db-header-height is declared by DocsLayout (the header sizes itself\n from the same variable, so they cannot drift). */\n top: var(--db-header-height, 3.25rem);\n z-index: 30;\n display: flex;\n flex-wrap: wrap;\n gap: 0.4rem 0.9rem;\n align-items: center;\n padding: 0.5rem 0.9rem;\n margin-block-end: 1rem;\n border: 1px solid color-mix(in oklch, currentColor 15%, transparent);\n border-radius: 8px;\n background: var(--color-background, Canvas);\n font-size: 0.8rem;\n}\n.db-cmp-bar .db-chip {\n display: inline-block;\n border-radius: 99px;\n padding: 0.05rem 0.6rem;\n font-size: 0.75rem;\n border: 1px solid transparent;\n}\n.db-cmp-bar button.db-chip { cursor: pointer; background: transparent; font: inherit; font-size: 0.75rem; color: inherit; }\n.db-cmp-tabs {\n display: inline-flex;\n gap: 0.15rem;\n border: 1px solid color-mix(in oklch, currentColor 20%, transparent);\n border-radius: 99px;\n padding: 0.1rem;\n}\n.db-cmp-tab {\n padding: 0.1rem 0.7rem;\n border-radius: 99px;\n text-decoration: none;\n color: inherit;\n}\n.db-cmp-tab[aria-current=\"page\"] {\n background: color-mix(in oklch, currentColor 12%, transparent);\n font-weight: 600;\n}\n.db-cmp-pairs { position: relative; display: inline-block; }\n.db-cmp-pairs summary { cursor: pointer; }\n.db-cmp-pairs ul {\n position: absolute;\n top: 100%;\n left: 0;\n margin: 0.25rem 0 0;\n padding: 0.35rem 0.75rem;\n list-style: none;\n background: var(--color-background, Canvas);\n border: 1px solid color-mix(in oklch, currentColor 20%, transparent);\n border-radius: 8px;\n white-space: nowrap;\n z-index: 40;\n}\n.db-cmp-monitor { text-decoration: none; border-color: color-mix(in oklch, currentColor 30%, transparent) !important; }\n.db-cmp-monitor:hover { background: color-mix(in oklch, currentColor 10%, transparent); }\n\n/* A wholly added/removed PAGE carries one mark around all its content.\n The per-block tint would be overwhelming at page scale, so page level\n gets a stronger edge and a much lighter wash \u2014 legible at a glance,\n still readable as prose. The \"added\"/\"removed\" pill (the ::before on\n [data-diff]) rides on this wrapper, which is exactly right: it labels\n the page, once. */\nhtml:not([data-diff-hidden]) [data-diff].db-diff-page {\n border-left-width: 5px;\n padding-left: 1rem;\n padding-block: 0.5rem;\n border-radius: 0 6px 6px 0;\n}\nhtml:not([data-diff-hidden]) [data-diff=\"added\"].db-diff-page {\n background: color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 5%, transparent);\n}\nhtml:not([data-diff-hidden]) [data-diff=\"removed\"].db-diff-page {\n background: color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 5%, transparent);\n opacity: 1; /* the page is READABLE \u2014 the mark says removed, not the dimming */\n}\n/* \u2026and a removed PAGE is not struck through \u2014 that is for a deleted line,\n not for a document the reader still needs to read. */\n[data-diff=\"removed\"].db-diff-page :is(p, li, code, pre) { text-decoration: none; }\n\n/* Changed raw HTML: the faithful new render, plus the previous version\n one click away. Without it a deletion inside opaque markup (a dropped\n table column) is simply invisible \u2014 the reader is told \"something\n changed\" and shown no evidence. */\n.db-diff-html-before {\n margin-block-start: 0.5rem;\n font-size: 0.8rem;\n border: 1px dashed color-mix(in oklch, currentColor 25%, transparent);\n border-radius: 6px;\n padding: 0.25rem 0.6rem;\n}\n/* Full-strength text: the summary is a CONTROL \u2014 dimming it (opacity or\n muted-foreground) drops it below 4.5:1 and axe rightly fails it. */\n.db-diff-html-before summary { cursor: pointer; color: inherit; }\n.db-diff-html-before[open] summary { margin-block-end: 0.5rem; }\n[data-diff-hidden] .db-diff-html-before { display: none; }\n\n/* Merged code diffs carry a hidden clean twin (the post-change\n render); Hide highlights swaps them. */\n.db-diff-code { position: relative; }\n.db-diff-code-clean { display: none; }\n[data-diff-hidden] .db-diff-code-clean { display: block; }\n[data-diff-hidden] .db-diff-code-merged { display: none; }\n\n/* Whitespace-only code edits are still marked (indentation is\n semantic in YAML/Python/markdown) \u2014 but NAMED, so the reader\n is not hunting an invisible 3-space shift. */\nhtml:not([data-diff-hidden]) .db-diff-code-ws .db-diff-code-merged::before {\n content: \"whitespace only\";\n position: absolute;\n top: 0;\n right: 0.6rem;\n transform: translateY(-55%);\n z-index: 1;\n font-size: 0.65rem;\n font-weight: 600;\n letter-spacing: 0.04em;\n line-height: 1.4;\n padding: 0 0.5rem;\n border-radius: 99px;\n background: color-mix(in oklch, var(--color-warning, oklch(0.8 0.16 85)) 22%, var(--color-background, Canvas));\n border: 1px solid color-mix(in oklch, var(--color-warning, oklch(0.8 0.16 85)) 45%, transparent);\n}\n\n/* Hide highlights \u2014 read the target version clean. One attribute on\n <html>: the guarded mark rules above stop applying by themselves;\n only content that does not exist in the target version (removed\n blocks, del runs, the \"before\" code side) needs hiding, plus the\n ins tint. */\n[data-diff-hidden] :is([data-diff=\"removed\"], del.db-diff-del, .db-diff-pair-label) { display: none !important; }\n[data-diff-hidden] ins.db-diff-ins { background: transparent; padding: 0; }\n";
@@ -1,8 +1,6 @@
1
- export const diffMarksOf = (node) => node;
2
- export function hasDiffMarks(node) {
3
- const m = diffMarksOf(node);
4
- return Boolean(m.diff || m.diffInline);
5
- }
1
+ import { diffMarksOf, inlineSegmentsToHtml, mergedCodeNode } from "@dogsbay/render-html/diff";
2
+ // Shared with render-html, which marks the same DiffTree in its HTML
3
+ export { DIFF_CSS, buildMergedCodeDiff, diffMarksOf, hasDiffMarks, inlineSegmentsToHtml, } from "@dogsbay/render-html/diff";
6
4
  /**
7
5
  * Inject attributes into the FIRST opening tag of a rendered
8
6
  * snippet. Handles components (`<TableRow`), plain tags (`<p`), and
@@ -15,103 +13,6 @@ export function injectAttrs(rendered, attrs) {
15
13
  const insertAt = m.index + 1 + m[1].length;
16
14
  return `${rendered.slice(0, insertAt)} ${attrs}${rendered.slice(insertAt)}`;
17
15
  }
18
- const escapeHtmlText = (s) => s
19
- .replace(/&/g, "&amp;")
20
- .replace(/</g, "&lt;")
21
- .replace(/>/g, "&gt;")
22
- .replace(/\{/g, "&#123;")
23
- .replace(/\}/g, "&#125;");
24
- /**
25
- * Render the merged word-level segments of a changed prose node.
26
- * Formatting inside changed prose flattens to text in this view —
27
- * the marks are the content (the unchanged siblings keep their full
28
- * inline rendering).
29
- */
30
- export function inlineSegmentsToHtml(segments) {
31
- return segments
32
- .map((s) => {
33
- const text = escapeHtmlText(s.text);
34
- if (s.op === "ins")
35
- return `<ins class="db-diff-ins">${text}</ins>`;
36
- if (s.op === "del")
37
- return `<del class="db-diff-del">${text}</del>`;
38
- return text;
39
- })
40
- .join("");
41
- }
42
- /** Quadratic-LCS guard, same rationale as tree-diff's word differ. */
43
- const CODE_DIFF_MAX_LINES = 1500;
44
- /**
45
- * Merge two code texts into ONE unified block, reporting the changed
46
- * lines BY NUMBER rather than by `[!code]` comment markers.
47
- *
48
- * Markers were the first design and they leak: Shiki can only strip a
49
- * marker it recognizes as a comment in the grammar, so in `plaintext`
50
- * — which every unknown language (`package-install`, doc-tool tokens)
51
- * falls back to — the marker renders as literal text in the reader's
52
- * code. Line numbers are language-independent and can't leak.
53
- *
54
- * Returns null over the line cap; callers fall back to before/after.
55
- */
56
- export function buildMergedCodeDiff(oldCode, newCode) {
57
- const a = oldCode.split("\n");
58
- const b = newCode.split("\n");
59
- if (a.length > CODE_DIFF_MAX_LINES || b.length > CODE_DIFF_MAX_LINES)
60
- return null;
61
- const m = a.length;
62
- const n = b.length;
63
- const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
64
- for (let i = m - 1; i >= 0; i--) {
65
- for (let j = n - 1; j >= 0; j--) {
66
- lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
67
- }
68
- }
69
- const out = [];
70
- const addLines = [];
71
- const removeLines = [];
72
- let i = 0;
73
- let j = 0;
74
- const emit = (text, kind) => {
75
- out.push(text);
76
- if (kind === "add")
77
- addLines.push(out.length);
78
- else if (kind === "remove")
79
- removeLines.push(out.length);
80
- };
81
- while (i < m && j < n) {
82
- if (a[i] === b[j]) {
83
- emit(b[j]);
84
- i++;
85
- j++;
86
- }
87
- else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
88
- emit(a[i], "remove");
89
- i++;
90
- }
91
- else {
92
- emit(b[j], "add");
93
- j++;
94
- }
95
- }
96
- while (i < m)
97
- emit(a[i++], "remove");
98
- while (j < n)
99
- emit(b[j++], "add");
100
- return { code: out.join("\n"), addLines, removeLines };
101
- }
102
- /** [2,3,4,7] → "2-4,7" */
103
- const toLineSpec = (lines) => {
104
- const parts = [];
105
- for (let i = 0; i < lines.length;) {
106
- let j = i;
107
- while (j + 1 < lines.length && lines[j + 1] === lines[j] + 1)
108
- j++;
109
- parts.push(i === j ? String(lines[i]) : `${lines[i]}-${lines[j]}`);
110
- i = j + 1;
111
- }
112
- return parts.join(",");
113
- };
114
- const codeTextOf = (node) => node.props?.code ?? node.html ?? "";
115
16
  /**
116
17
  * Decorate one rendered node. `renderPeer` renders the other side of
117
18
  * a changed pair through the same serializer (code before/after).
@@ -130,26 +31,14 @@ export function decorateDiff(node, rendered, renderPeer) {
130
31
  // post-change code only. Falls back to a labeled before/after
131
32
  // pair when merging is unsafe (size cap, marker collision).
132
33
  if (marks.diff === "changed" && marks.diffPeer && node.type === "code") {
133
- const oldCode = codeTextOf(marks.diffPeer);
134
- const newCode = codeTextOf(node);
135
- const merged = buildMergedCodeDiff(oldCode, newCode);
34
+ const merged = mergedCodeNode(node);
136
35
  if (merged !== null) {
137
- const mergedNode = {
138
- ...node,
139
- html: undefined,
140
- props: {
141
- ...node.props,
142
- code: merged.code,
143
- copyText: newCode,
144
- diffAdd: toLineSpec(merged.addLines),
145
- diffRemove: toLineSpec(merged.removeLines),
146
- },
147
- };
36
+ const mergedNode = merged.node;
148
37
  // Whitespace-only edits stay FULLY marked (indentation is
149
38
  // semantic in YAML/Python/markdown — never silently dropped),
150
39
  // but they get NAMED: without the label the reader hunts a
151
40
  // 3-space shift across two near-identical lines.
152
- const wsOnly = oldCode !== newCode && oldCode.replace(/\s+/g, "") === newCode.replace(/\s+/g, "");
41
+ const wsOnly = merged.whitespaceOnly;
153
42
  const cls = `db-diff-code${wsOnly ? " db-diff-code-ws" : ""}`;
154
43
  // The clean twin is the already-rendered new side: shiki line
155
44
  // spans keep their newlines OUTSIDE the span, so hiding removed
@@ -210,331 +99,3 @@ export function decorateDiff(node, rendered, renderPeer) {
210
99
  }
211
100
  return injectAttrs(rendered, attrs);
212
101
  }
213
- /**
214
- * The diff stylesheet the generated site carries — theme-token based,
215
- * both color modes, no hardcoded palette (key-decisions doctrine).
216
- */
217
- export const DIFF_CSS = `/* tree-diff decorations — one DiffTree, rendered by the normal components.
218
- Mark styling is GUARDED by html:not([data-diff-hidden]) rather than
219
- force-reset under hidden mode: a reset like "border-left: none
220
- !important" also clobbers a component's OWN border (an added warning
221
- callout lost its left edge — user find, adapters/mysql). Guarded
222
- rules simply stop applying, and component styling survives. */
223
- [data-diff] { position: relative; }
224
- html:not([data-diff-hidden]) [data-diff="added"] {
225
- background: color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 12%, transparent);
226
- border-left: 3px solid var(--color-success, oklch(0.72 0.19 150));
227
- padding-left: 0.75rem;
228
- }
229
- html:not([data-diff-hidden]) [data-diff="removed"] {
230
- background: color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 10%, transparent);
231
- border-left: 3px solid var(--color-destructive, oklch(0.64 0.21 25));
232
- padding-left: 0.75rem;
233
- opacity: 0.75;
234
- }
235
- html:not([data-diff-hidden]) [data-diff="removed"] :is(p, li, code, pre) { text-decoration: line-through; text-decoration-thickness: 1px; }
236
- html:not([data-diff-hidden]) [data-diff="changed"] {
237
- border-left: 3px solid var(--color-warning, oklch(0.8 0.16 85));
238
- padding-left: 0.75rem;
239
- }
240
- html:not([data-diff-hidden]) [data-diff="moved"] {
241
- border-left: 3px dashed var(--color-info, oklch(0.7 0.14 240));
242
- padding-left: 0.75rem;
243
- }
244
- html:not([data-diff-hidden]) [data-diff="moved"][data-diff-from]::after {
245
- content: "moved from " attr(data-diff-from);
246
- display: block;
247
- font-size: 0.75rem;
248
- color: var(--color-muted-foreground, inherit);
249
- opacity: 0.8;
250
- }
251
-
252
- /* Explicit "added"/"removed" label pills straddling the block's top
253
- edge. Components with their own chrome (callouts, code blocks)
254
- camouflage a tint-only mark — a green-tinted warning callout reads
255
- as a callout variant, not as new content (user feedback,
256
- better-auth adapters/mysql). Table rows are excluded (a pill can't
257
- straddle a tr); nested marks defer to the outermost label. */
258
- html:not([data-diff-hidden]) [data-diff="added"]:not(tr, th, td)::before,
259
- html:not([data-diff-hidden]) [data-diff="removed"]:not(tr, th, td)::before {
260
- position: absolute;
261
- top: 0;
262
- right: 0.6rem;
263
- transform: translateY(-55%);
264
- font-size: 0.65rem;
265
- font-weight: 600;
266
- letter-spacing: 0.04em;
267
- line-height: 1.4;
268
- padding: 0 0.5rem;
269
- border-radius: 99px;
270
- z-index: 1;
271
- text-decoration: none;
272
- }
273
- html:not([data-diff-hidden]) [data-diff="added"]:not(tr, th, td)::before {
274
- content: "added";
275
- background: color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 25%, var(--color-background, Canvas));
276
- border: 1px solid color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 45%, transparent);
277
- }
278
- html:not([data-diff-hidden]) [data-diff="removed"]:not(tr, th, td)::before {
279
- content: "removed";
280
- background: color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 20%, var(--color-background, Canvas));
281
- border: 1px solid color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 45%, transparent);
282
- }
283
- [data-diff] [data-diff]::before { content: none !important; }
284
-
285
- /* Nav change markers (the sidebar is client-rendered from nav.json, so
286
- the styles ship here rather than with the component). Same contract as
287
- the tab markers: a GLYPH carries the meaning, colour reinforces it,
288
- visually-hidden text announces it. */
289
- .db-nav-mark {
290
- display: inline-flex;
291
- align-items: center;
292
- justify-content: center;
293
- flex-shrink: 0;
294
- width: 1rem;
295
- height: 1rem;
296
- margin-left: auto;
297
- border-radius: 99px;
298
- font-size: 0.75rem;
299
- font-weight: 700;
300
- line-height: 1;
301
- color: var(--color-muted-foreground, currentColor);
302
- }
303
- .db-nav-mark[data-nav-mark="added"] { color: var(--color-success, oklch(0.72 0.19 150)); }
304
- .db-nav-mark[data-nav-mark="changed"] { color: var(--color-warning, oklch(0.8 0.16 85)); }
305
- .db-nav-mark[data-nav-mark="removed"] { color: var(--color-destructive, oklch(0.64 0.21 25)); }
306
- .db-nav-mark[data-nav-mark="moved"] { color: var(--color-info, oklch(0.7 0.14 240)); }
307
- /* a subtree marker is a hint about descendants, not a claim about this row */
308
- .db-nav-mark[data-nav-mark-subtree] { opacity: 0.55; }
309
- /* removed pages get a SECOND non-colour signal */
310
- /* Belt-and-braces: a subtree mark must never strike through its own row. */
311
- [data-nav-mark-row="removed"]:not([data-nav-mark-subtree]) { text-decoration: line-through; opacity: 0.7; }
312
- @media (forced-colors: active) {
313
- .db-nav-mark { color: CanvasText; }
314
- }
315
-
316
- /* Which tab changed? A marked panel is invisible until opened, so its
317
- trigger carries a marker (the group's bar says "in here", the marker
318
- says "this one"). The marker is a GLYPH, not a colored dot: colour
319
- alone fails WCAG 1.4.1 and background-tinted dots vanish entirely in
320
- forced-colors mode. Colour reinforces; shape carries the meaning.
321
- Visually-hidden text rides along (emitted by the serializer). */
322
- .db-tab-mark {
323
- display: inline-block;
324
- margin-left: 0.35rem;
325
- font-size: 0.75rem;
326
- font-weight: 700;
327
- line-height: 1;
328
- vertical-align: middle;
329
- color: var(--color-muted-foreground, currentColor);
330
- }
331
- [data-diff-hidden] .db-tab-mark { display: none; }
332
- .db-tab-mark[data-diff-tab="changed"] { color: var(--color-warning, oklch(0.8 0.16 85)); }
333
- .db-tab-mark[data-diff-tab="added"] { color: var(--color-success, oklch(0.72 0.19 150)); }
334
- .db-tab-mark[data-diff-tab="removed"] { color: var(--color-destructive, oklch(0.64 0.21 25)); }
335
- .db-tab-mark[data-diff-tab="moved"] { color: var(--color-info, oklch(0.7 0.14 240)); }
336
- @media (forced-colors: active) {
337
- .db-tab-mark { color: CanvasText; }
338
- }
339
- ins.db-diff-ins {
340
- text-decoration: none;
341
- background: color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 22%, transparent);
342
- border-radius: 2px;
343
- padding: 0 1px;
344
- }
345
- del.db-diff-del {
346
- background: color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 18%, transparent);
347
- border-radius: 2px;
348
- padding: 0 1px;
349
- }
350
- .db-diff-pair { display: grid; gap: 0.25rem; margin-block: 1rem; }
351
- .db-diff-pair-label {
352
- font-size: 0.7rem;
353
- text-transform: uppercase;
354
- letter-spacing: 0.08em;
355
- color: var(--color-muted-foreground, inherit);
356
- opacity: 0.8;
357
- }
358
- tr[data-diff] { border-left-width: 3px; }
359
-
360
- /* Folded unchanged sections are CHROME, not content callouts:
361
- dashed, dimmed, compact — visually "collapsed context". */
362
- .db-diff-fold {
363
- border: 1px dashed color-mix(in oklch, currentColor 25%, transparent) !important;
364
- border-left-width: 1px !important;
365
- background: transparent !important;
366
- opacity: 0.7;
367
- margin-block: 0.5rem !important;
368
- }
369
- .db-diff-fold:hover, .db-diff-fold[open] { opacity: 1; }
370
- .db-diff-fold summary, .db-diff-fold [data-slot="trigger"] {
371
- font-size: 0.85rem;
372
- }
373
-
374
- /* The comparison banner at the top of each reader page. */
375
- .db-diff-banner-row { display: flex; flex-wrap: wrap; gap: 0.4rem 0.8rem; align-items: center; width: 100%; }
376
- .db-diff-banner {
377
- border: 1px solid color-mix(in oklch, currentColor 15%, transparent);
378
- border-radius: 8px;
379
- padding: 0.75rem 1rem;
380
- margin-block-end: 1.5rem;
381
- font-size: 0.85rem;
382
- display: flex;
383
- flex-wrap: wrap;
384
- gap: 0.4rem 1rem;
385
- align-items: center;
386
- }
387
- .db-diff-banner .db-chip {
388
- display: inline-block;
389
- border-radius: 99px;
390
- padding: 0.05rem 0.6rem;
391
- font-size: 0.75rem;
392
- border: 1px solid transparent;
393
- }
394
- .db-chip-added { background: color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 15%, transparent); }
395
- .db-chip-removed { background: color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 12%, transparent); }
396
- .db-chip-changed { background: color-mix(in oklch, var(--color-warning, oklch(0.8 0.16 85)) 15%, transparent); }
397
- .db-chip-moved { background: color-mix(in oklch, var(--color-info, oklch(0.7 0.14 240)) 12%, transparent); }
398
- .db-chip-muted { border-color: color-mix(in oklch, currentColor 25%, transparent); opacity: 0.8; }
399
-
400
- /* The persistent view-switcher bar (Reader / Docs-ops / Agent):
401
- perspectives on ONE comparison — tabs are links, verification
402
- lives in the chrome, the Monitor CTA sits next to the value. */
403
- .db-cmp-bar {
404
- position: sticky;
405
- /* BELOW the docs header, not under it: both were sticky at top:0 and
406
- the header's higher z-index simply swallowed this bar on scroll —
407
- the chrome carrying the trust contract was the chrome that vanished.
408
- --db-header-height is declared by DocsLayout (the header sizes itself
409
- from the same variable, so they cannot drift). */
410
- top: var(--db-header-height, 3.25rem);
411
- z-index: 30;
412
- display: flex;
413
- flex-wrap: wrap;
414
- gap: 0.4rem 0.9rem;
415
- align-items: center;
416
- padding: 0.5rem 0.9rem;
417
- margin-block-end: 1rem;
418
- border: 1px solid color-mix(in oklch, currentColor 15%, transparent);
419
- border-radius: 8px;
420
- background: var(--color-background, Canvas);
421
- font-size: 0.8rem;
422
- }
423
- .db-cmp-bar .db-chip {
424
- display: inline-block;
425
- border-radius: 99px;
426
- padding: 0.05rem 0.6rem;
427
- font-size: 0.75rem;
428
- border: 1px solid transparent;
429
- }
430
- .db-cmp-bar button.db-chip { cursor: pointer; background: transparent; font: inherit; font-size: 0.75rem; color: inherit; }
431
- .db-cmp-tabs {
432
- display: inline-flex;
433
- gap: 0.15rem;
434
- border: 1px solid color-mix(in oklch, currentColor 20%, transparent);
435
- border-radius: 99px;
436
- padding: 0.1rem;
437
- }
438
- .db-cmp-tab {
439
- padding: 0.1rem 0.7rem;
440
- border-radius: 99px;
441
- text-decoration: none;
442
- color: inherit;
443
- }
444
- .db-cmp-tab[aria-current="page"] {
445
- background: color-mix(in oklch, currentColor 12%, transparent);
446
- font-weight: 600;
447
- }
448
- .db-cmp-pairs { position: relative; display: inline-block; }
449
- .db-cmp-pairs summary { cursor: pointer; }
450
- .db-cmp-pairs ul {
451
- position: absolute;
452
- top: 100%;
453
- left: 0;
454
- margin: 0.25rem 0 0;
455
- padding: 0.35rem 0.75rem;
456
- list-style: none;
457
- background: var(--color-background, Canvas);
458
- border: 1px solid color-mix(in oklch, currentColor 20%, transparent);
459
- border-radius: 8px;
460
- white-space: nowrap;
461
- z-index: 40;
462
- }
463
- .db-cmp-monitor { text-decoration: none; border-color: color-mix(in oklch, currentColor 30%, transparent) !important; }
464
- .db-cmp-monitor:hover { background: color-mix(in oklch, currentColor 10%, transparent); }
465
-
466
- /* A wholly added/removed PAGE carries one mark around all its content.
467
- The per-block tint would be overwhelming at page scale, so page level
468
- gets a stronger edge and a much lighter wash — legible at a glance,
469
- still readable as prose. The "added"/"removed" pill (the ::before on
470
- [data-diff]) rides on this wrapper, which is exactly right: it labels
471
- the page, once. */
472
- html:not([data-diff-hidden]) [data-diff].db-diff-page {
473
- border-left-width: 5px;
474
- padding-left: 1rem;
475
- padding-block: 0.5rem;
476
- border-radius: 0 6px 6px 0;
477
- }
478
- html:not([data-diff-hidden]) [data-diff="added"].db-diff-page {
479
- background: color-mix(in oklch, var(--color-success, oklch(0.72 0.19 150)) 5%, transparent);
480
- }
481
- html:not([data-diff-hidden]) [data-diff="removed"].db-diff-page {
482
- background: color-mix(in oklch, var(--color-destructive, oklch(0.64 0.21 25)) 5%, transparent);
483
- opacity: 1; /* the page is READABLE — the mark says removed, not the dimming */
484
- }
485
- /* …and a removed PAGE is not struck through — that is for a deleted line,
486
- not for a document the reader still needs to read. */
487
- [data-diff="removed"].db-diff-page :is(p, li, code, pre) { text-decoration: none; }
488
-
489
- /* Changed raw HTML: the faithful new render, plus the previous version
490
- one click away. Without it a deletion inside opaque markup (a dropped
491
- table column) is simply invisible — the reader is told "something
492
- changed" and shown no evidence. */
493
- .db-diff-html-before {
494
- margin-block-start: 0.5rem;
495
- font-size: 0.8rem;
496
- border: 1px dashed color-mix(in oklch, currentColor 25%, transparent);
497
- border-radius: 6px;
498
- padding: 0.25rem 0.6rem;
499
- }
500
- /* Full-strength text: the summary is a CONTROL — dimming it (opacity or
501
- muted-foreground) drops it below 4.5:1 and axe rightly fails it. */
502
- .db-diff-html-before summary { cursor: pointer; color: inherit; }
503
- .db-diff-html-before[open] summary { margin-block-end: 0.5rem; }
504
- [data-diff-hidden] .db-diff-html-before { display: none; }
505
-
506
- /* Merged code diffs carry a hidden clean twin (the post-change
507
- render); Hide highlights swaps them. */
508
- .db-diff-code { position: relative; }
509
- .db-diff-code-clean { display: none; }
510
- [data-diff-hidden] .db-diff-code-clean { display: block; }
511
- [data-diff-hidden] .db-diff-code-merged { display: none; }
512
-
513
- /* Whitespace-only code edits are still marked (indentation is
514
- semantic in YAML/Python/markdown) — but NAMED, so the reader
515
- is not hunting an invisible 3-space shift. */
516
- html:not([data-diff-hidden]) .db-diff-code-ws .db-diff-code-merged::before {
517
- content: "whitespace only";
518
- position: absolute;
519
- top: 0;
520
- right: 0.6rem;
521
- transform: translateY(-55%);
522
- z-index: 1;
523
- font-size: 0.65rem;
524
- font-weight: 600;
525
- letter-spacing: 0.04em;
526
- line-height: 1.4;
527
- padding: 0 0.5rem;
528
- border-radius: 99px;
529
- background: color-mix(in oklch, var(--color-warning, oklch(0.8 0.16 85)) 22%, var(--color-background, Canvas));
530
- border: 1px solid color-mix(in oklch, var(--color-warning, oklch(0.8 0.16 85)) 45%, transparent);
531
- }
532
-
533
- /* Hide highlights — read the target version clean. One attribute on
534
- <html>: the guarded mark rules above stop applying by themselves;
535
- only content that does not exist in the target version (removed
536
- blocks, del runs, the "before" code side) needs hiding, plus the
537
- ins tint. */
538
- [data-diff-hidden] :is([data-diff="removed"], del.db-diff-del, .db-diff-pair-label) { display: none !important; }
539
- [data-diff-hidden] ins.db-diff-ins { background: transparent; padding: 0; }
540
- `;
package/dist/serialize.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { walkInline, applyTextFlags, renderLeaf, indent, isSafeHref, apiPageLayout, apiSymbolLevels, renderAroundGroup, isDetailsSheet, } from "@dogsbay/serialize-core";
2
2
  import { parseInline } from "@dogsbay/format-dogsbay-md";
3
3
  import { decorateDiff, hasDiffMarks } from "./diff-decoration.js";
4
+ import { TAB_GLYPH, tabDiffSignal } from "@dogsbay/render-html/diff";
4
5
  import { TONE_CLASSES, CARD_TONE_CLASSES, TAB_VARIANTS, THEME_SCOPES, lookup } from "./tone-classes.js";
5
6
  import { apiBlockHtml, apiPropertiesHtml } from "./api-markdown.js";
6
7
  export { TONE_CLASSES };
@@ -1056,24 +1057,12 @@ function tabsToAstro(node, ctx) {
1056
1057
  // as a colored dot by DIFF_CSS) telling the reader WHICH tab to
1057
1058
  // open. Structural read of @dogsbay/tree-diff marks, same as
1058
1059
  // diff-decoration; unmarked trees are unaffected.
1059
- const tabDiffSignal = (tab) => {
1060
- const own = tab.diff;
1061
- if (own)
1062
- return own;
1063
- const deep = (n) => Boolean(n.diff || n.diffInline) || (n.children ?? []).some(deep);
1064
- return (tab.children ?? []).some(deep) ? "changed" : undefined;
1065
- };
1066
1060
  // The indicator must not live in COLOUR alone (WCAG 1.4.1), and a
1067
1061
  // background-tinted dot disappears in forced-colors mode. So it is a
1068
1062
  // GLYPH (shape carries the meaning) plus visually-hidden text, so the
1069
1063
  // trigger announces "Astro, changed in this comparison" rather than
1070
- // relying on a `title` screen readers may never speak.
1071
- const TAB_GLYPH = {
1072
- added: "+",
1073
- changed: "•",
1074
- removed: "−",
1075
- moved: "→",
1076
- };
1064
+ // relying on a `title` screen readers may never speak. (TAB_GLYPH and
1065
+ // tabDiffSignal are shared with render-html.)
1077
1066
  // A variant's class strings are complete (TAB_VARIANTS): TabsList and
1078
1067
  // TabsTrigger merge a `class` over their base with tailwind-variants
1079
1068
  const variant = lookup(TAB_VARIANTS, node.props?.variant);
package/dist/theme-css.js CHANGED
@@ -98,10 +98,11 @@ export function generateGlobalCss() {
98
98
  .docs-prose {
99
99
  line-height: 1.7;
100
100
 
101
- & h1:not(.dba-api *) { font-family: var(--font-heading); font-size: 2rem; font-weight: 700; margin-top: 0; margin-bottom: 1rem; line-height: 1.2; letter-spacing: -0.025em; }
102
- & h2:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.5rem; font-weight: 600; margin-top: 2.5rem; margin-bottom: 0.75rem; line-height: 1.3; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; letter-spacing: -0.015em; }
103
- & h3:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; line-height: 1.4; }
104
- & h4:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.1rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; }
101
+ /* A heading that is part of a component (a card title: h3.not-prose) keeps the component look */
102
+ & h1:not(.dba-api *):not(.not-prose) { font-family: var(--font-heading); font-size: 2rem; font-weight: 700; margin-top: 0; margin-bottom: 1rem; line-height: 1.2; letter-spacing: -0.025em; }
103
+ & h2:not(.dba-api *):not(.not-prose) { font-family: var(--font-heading); font-size: 1.5rem; font-weight: 600; margin-top: 2.5rem; margin-bottom: 0.75rem; line-height: 1.3; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; letter-spacing: -0.015em; }
104
+ & h3:not(.dba-api *):not(.not-prose) { font-family: var(--font-heading); font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; line-height: 1.4; }
105
+ & h4:not(.dba-api *):not(.not-prose) { font-family: var(--font-heading); font-size: 1.1rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; }
105
106
 
106
107
  & p:not(.dba-api *) { margin-top: 0.75rem; margin-bottom: 0.75rem; }
107
108
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/format-astro",
3
- "version": "0.2.0-beta.124",
3
+ "version": "0.2.0-beta.126",
4
4
  "description": "Astro format plugin for Dogsbay — export TreeNode[] to Astro component markup",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,12 +32,12 @@
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
34
  "markdown-it": "^14.3.0",
35
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.124",
36
- "@dogsbay/render-html": "0.2.0-beta.124",
37
- "@dogsbay/serialize-core": "0.2.0-beta.124",
38
- "@dogsbay/site-files": "0.2.0-beta.124",
39
- "@dogsbay/types": "0.2.0-beta.124",
40
- "@dogsbay/ui": "0.2.0-beta.124"
35
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.126",
36
+ "@dogsbay/serialize-core": "0.2.0-beta.126",
37
+ "@dogsbay/site-files": "0.2.0-beta.126",
38
+ "@dogsbay/render-html": "0.2.0-beta.126",
39
+ "@dogsbay/types": "0.2.0-beta.126",
40
+ "@dogsbay/ui": "0.2.0-beta.126"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",