@dowel-ui/react 0.2.0 → 0.4.0
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.
- package/README.md +14 -7
- package/dist/components/ai-agent-plan/ai-agent-plan.d.ts +50 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.d.ts.map +1 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.js +156 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -0
- package/dist/components/ai-agent-plan/index.d.ts +2 -0
- package/dist/components/ai-agent-plan/index.js +2 -0
- package/dist/components/ai-agent-plan/meta.js +17 -0
- package/dist/components/ai-agent-plan/meta.js.map +1 -0
- package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
- package/dist/components/ai-approval-request/ai-approval-request.d.ts +67 -0
- package/dist/components/ai-approval-request/ai-approval-request.d.ts.map +1 -0
- package/dist/components/ai-approval-request/ai-approval-request.js +218 -0
- package/dist/components/ai-approval-request/ai-approval-request.js.map +1 -0
- package/dist/components/ai-approval-request/index.d.ts +2 -0
- package/dist/components/ai-approval-request/index.js +2 -0
- package/dist/components/ai-approval-request/meta.js +17 -0
- package/dist/components/ai-approval-request/meta.js.map +1 -0
- package/dist/components/ai-disclosure/ai-disclosure.d.ts +92 -0
- package/dist/components/ai-disclosure/ai-disclosure.d.ts.map +1 -0
- package/dist/components/ai-disclosure/ai-disclosure.js +122 -0
- package/dist/components/ai-disclosure/ai-disclosure.js.map +1 -0
- package/dist/components/ai-disclosure/index.d.ts +2 -0
- package/dist/components/ai-disclosure/index.js +2 -0
- package/dist/components/ai-disclosure/meta.js +17 -0
- package/dist/components/ai-disclosure/meta.js.map +1 -0
- package/dist/components/ai-response/ai-response.d.ts.map +1 -1
- package/dist/components/ai-response/ai-response.js +1 -0
- package/dist/components/ai-response/ai-response.js.map +1 -1
- package/dist/components/ai-structured-output/ai-structured-output.d.ts.map +1 -1
- package/dist/components/ai-structured-output/ai-structured-output.js +13 -2
- package/dist/components/ai-structured-output/ai-structured-output.js.map +1 -1
- package/dist/components/ai-tool/ai-tool.d.ts +1 -1
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/date-picker/date-picker.js +1 -1
- package/dist/components/diff-viewer/diff-model.d.ts +69 -0
- package/dist/components/diff-viewer/diff-model.d.ts.map +1 -0
- package/dist/components/diff-viewer/diff-model.js +180 -0
- package/dist/components/diff-viewer/diff-model.js.map +1 -0
- package/dist/components/diff-viewer/diff-viewer.d.ts +42 -0
- package/dist/components/diff-viewer/diff-viewer.d.ts.map +1 -0
- package/dist/components/diff-viewer/diff-viewer.js +212 -0
- package/dist/components/diff-viewer/diff-viewer.js.map +1 -0
- package/dist/components/diff-viewer/index.d.ts +3 -0
- package/dist/components/diff-viewer/index.js +3 -0
- package/dist/components/diff-viewer/meta.js +17 -0
- package/dist/components/diff-viewer/meta.js.map +1 -0
- package/dist/components/file-upload/file-upload.d.ts +46 -0
- package/dist/components/file-upload/file-upload.d.ts.map +1 -0
- package/dist/components/file-upload/file-upload.js +204 -0
- package/dist/components/file-upload/file-upload.js.map +1 -0
- package/dist/components/file-upload/index.d.ts +3 -0
- package/dist/components/file-upload/index.js +3 -0
- package/dist/components/file-upload/meta.js +17 -0
- package/dist/components/file-upload/meta.js.map +1 -0
- package/dist/components/file-upload/upload-queue.d.ts +87 -0
- package/dist/components/file-upload/upload-queue.d.ts.map +1 -0
- package/dist/components/file-upload/upload-queue.js +249 -0
- package/dist/components/file-upload/upload-queue.js.map +1 -0
- package/dist/components/log-viewer/index.d.ts +3 -0
- package/dist/components/log-viewer/index.js +3 -0
- package/dist/components/log-viewer/log-stream.d.ts +68 -0
- package/dist/components/log-viewer/log-stream.d.ts.map +1 -0
- package/dist/components/log-viewer/log-stream.js +120 -0
- package/dist/components/log-viewer/log-stream.js.map +1 -0
- package/dist/components/log-viewer/log-viewer.d.ts +51 -0
- package/dist/components/log-viewer/log-viewer.d.ts.map +1 -0
- package/dist/components/log-viewer/log-viewer.js +284 -0
- package/dist/components/log-viewer/log-viewer.js.map +1 -0
- package/dist/components/log-viewer/meta.js +17 -0
- package/dist/components/log-viewer/meta.js.map +1 -0
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/progress/progress.js +1 -0
- package/dist/components/progress/progress.js.map +1 -1
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/spinner/spinner.js +1 -0
- package/dist/components/spinner/spinner.js.map +1 -1
- package/dist/components/tags-input/index.d.ts +2 -0
- package/dist/components/tags-input/index.js +2 -0
- package/dist/components/tags-input/meta.js +17 -0
- package/dist/components/tags-input/meta.js.map +1 -0
- package/dist/components/tags-input/tags-input.d.ts +47 -0
- package/dist/components/tags-input/tags-input.d.ts.map +1 -0
- package/dist/components/tags-input/tags-input.js +182 -0
- package/dist/components/tags-input/tags-input.js.map +1 -0
- package/dist/components/time-range-picker/index.d.ts +3 -0
- package/dist/components/time-range-picker/index.js +3 -0
- package/dist/components/time-range-picker/meta.js +22 -0
- package/dist/components/time-range-picker/meta.js.map +1 -0
- package/dist/components/time-range-picker/time-expression.d.ts +88 -0
- package/dist/components/time-range-picker/time-expression.d.ts.map +1 -0
- package/dist/components/time-range-picker/time-expression.js +335 -0
- package/dist/components/time-range-picker/time-expression.js.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.d.ts +65 -0
- package/dist/components/time-range-picker/time-range-picker.d.ts.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.js +377 -0
- package/dist/components/time-range-picker/time-range-picker.js.map +1 -0
- package/dist/components/toast/toast.d.ts +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +14 -2
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +66 -50
- package/dist/registry/components.js.map +1 -1
- package/package.json +6 -4
- package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
- package/src/components/ai-agent-plan/index.ts +10 -0
- package/src/components/ai-agent-plan/meta.ts +19 -0
- package/src/components/ai-approval-request/ai-approval-request.tsx +368 -0
- package/src/components/ai-approval-request/index.ts +7 -0
- package/src/components/ai-approval-request/meta.ts +21 -0
- package/src/components/ai-disclosure/ai-disclosure.tsx +244 -0
- package/src/components/ai-disclosure/index.ts +9 -0
- package/src/components/ai-disclosure/meta.ts +23 -0
- package/src/components/ai-response/ai-response.tsx +1 -0
- package/src/components/ai-structured-output/ai-structured-output.tsx +25 -1
- package/src/components/diff-viewer/diff-model.ts +256 -0
- package/src/components/diff-viewer/diff-viewer.tsx +339 -0
- package/src/components/diff-viewer/index.ts +17 -0
- package/src/components/diff-viewer/meta.ts +20 -0
- package/src/components/file-upload/file-upload.tsx +322 -0
- package/src/components/file-upload/index.ts +20 -0
- package/src/components/file-upload/meta.ts +21 -0
- package/src/components/file-upload/upload-queue.ts +337 -0
- package/src/components/log-viewer/index.ts +21 -0
- package/src/components/log-viewer/log-stream.ts +176 -0
- package/src/components/log-viewer/log-viewer.tsx +424 -0
- package/src/components/log-viewer/meta.ts +21 -0
- package/src/components/progress/progress.tsx +3 -0
- package/src/components/spinner/spinner.tsx +5 -0
- package/src/components/tags-input/index.ts +1 -0
- package/src/components/tags-input/meta.ts +23 -0
- package/src/components/tags-input/tags-input.tsx +300 -0
- package/src/components/time-range-picker/index.ts +25 -0
- package/src/components/time-range-picker/meta.ts +22 -0
- package/src/components/time-range-picker/time-expression.ts +491 -0
- package/src/components/time-range-picker/time-range-picker.tsx +522 -0
- package/src/index.ts +8 -0
- package/src/registry/components.ts +16 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//#region src/components/diff-viewer/diff-model.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Turning two versions of a file into rows a diff can render.
|
|
4
|
+
*
|
|
5
|
+
* The algorithm is not the interesting part and is not reimplemented here —
|
|
6
|
+
* jsdiff does Myers properly, is BSD-licensed, has no dependencies of its own,
|
|
7
|
+
* and getting an O(ND) diff subtly wrong is a poor use of anybody's afternoon.
|
|
8
|
+
* What every packaged *viewer* welds on is a styling strategy: emotion in
|
|
9
|
+
* react-diff-viewer-continued, HTML strings and a stylesheet in diff2html. That
|
|
10
|
+
* is what cannot be reached by design tokens, and it is why this exists.
|
|
11
|
+
*
|
|
12
|
+
* Everything below is pure, so the grouping, the context collapsing and the
|
|
13
|
+
* word-level pairing can be tested without rendering anything.
|
|
14
|
+
*/
|
|
15
|
+
type RowKind = "context" | "added" | "removed";
|
|
16
|
+
interface WordSegment {
|
|
17
|
+
text: string;
|
|
18
|
+
changed: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface DiffRow {
|
|
21
|
+
kind: RowKind;
|
|
22
|
+
/** 1-based line number in the original. Absent on an added line. */
|
|
23
|
+
before?: number;
|
|
24
|
+
/** 1-based line number in the result. Absent on a removed line. */
|
|
25
|
+
after?: number;
|
|
26
|
+
content: string;
|
|
27
|
+
/**
|
|
28
|
+
* Word-level split, present only where a removed line pairs with an added
|
|
29
|
+
* one. Without a pair there is nothing to compare against, and highlighting
|
|
30
|
+
* the whole line as "changed" would be noise.
|
|
31
|
+
*/
|
|
32
|
+
segments?: WordSegment[];
|
|
33
|
+
}
|
|
34
|
+
interface DiffHunk {
|
|
35
|
+
id: string;
|
|
36
|
+
rows: DiffRow[];
|
|
37
|
+
/** Context lines hidden before this hunk, if any. */
|
|
38
|
+
skippedBefore: number;
|
|
39
|
+
}
|
|
40
|
+
interface BuildDiffOptions {
|
|
41
|
+
/** Unchanged lines kept either side of a change. */
|
|
42
|
+
context?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Compare word by word inside a changed line. Off for very large diffs,
|
|
45
|
+
* where the extra pass costs more than it explains.
|
|
46
|
+
*/
|
|
47
|
+
words?: boolean;
|
|
48
|
+
}
|
|
49
|
+
declare function buildDiff(before: string, after: string, options?: BuildDiffOptions): DiffHunk[];
|
|
50
|
+
/**
|
|
51
|
+
* Groups rows into hunks, dropping context beyond `context` lines.
|
|
52
|
+
*
|
|
53
|
+
* A file with one changed line is otherwise thousands of rows of identical
|
|
54
|
+
* text, and the reader has to find the change in it.
|
|
55
|
+
*/
|
|
56
|
+
declare function groupIntoHunks(rows: DiffRow[], context: number): DiffHunk[];
|
|
57
|
+
/** Row pairs for a side-by-side view, aligning removals against additions. */
|
|
58
|
+
declare function toSplitRows(rows: DiffRow[]): {
|
|
59
|
+
left: DiffRow | null;
|
|
60
|
+
right: DiffRow | null;
|
|
61
|
+
}[];
|
|
62
|
+
/** Counts, for a summary that says what the diff does before it is read. */
|
|
63
|
+
declare function countChanges(hunks: DiffHunk[]): {
|
|
64
|
+
added: number;
|
|
65
|
+
removed: number;
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
export { BuildDiffOptions, DiffHunk, DiffRow, RowKind, WordSegment, buildDiff, countChanges, groupIntoHunks, toSplitRows };
|
|
69
|
+
//# sourceMappingURL=diff-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-model.d.ts","names":[],"sources":["../../../src/components/diff-viewer/diff-model.ts"],"mappings":";;;;;;;;;;;;;;KAgBY;UAEK;EACf;EACA;;UAGe;EACf,MAAM;;EAEN;;EAEA;EACA;;;;;;EAMA,WAAW;;UAGI;EACf;EACA,MAAM;;EAEN;;UAGe;;EAEf;;;;;EAKA;;iBAoDc,UACd,gBACA,eACA,UAAS,mBACR;;;;;;;iBAiEa,eAAe,MAAM,WAAW,kBAAkB;;iBA+BlD,YACd,MAAM;EACH,MAAM;EAAgB,OAAO;;;iBAuClB,aAAa,OAAO;EAAe;EAAe"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { diffLines, diffWordsWithSpace } from "diff";
|
|
2
|
+
//#region src/components/diff-viewer/diff-model.ts
|
|
3
|
+
function splitLines(text) {
|
|
4
|
+
const lines = text.split("\n");
|
|
5
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
6
|
+
return lines;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Gives both sides a trailing newline before they are compared.
|
|
10
|
+
*
|
|
11
|
+
* jsdiff's line tokens carry their own newline, so a final line written as `a`
|
|
12
|
+
* and one written as `a\n` are different tokens. Appending a line to a file
|
|
13
|
+
* that did not end in a newline would otherwise be reported as the last line
|
|
14
|
+
* being removed and re-added — a change the author did not make, sitting on
|
|
15
|
+
* top of the one they did.
|
|
16
|
+
*/
|
|
17
|
+
function withTrailingNewline(text) {
|
|
18
|
+
return text.endsWith("\n") ? text : `${text}\n`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pairs removed lines with added ones inside a single change block.
|
|
22
|
+
*
|
|
23
|
+
* jsdiff reports a change as a run of removals followed by a run of additions.
|
|
24
|
+
* Comparing the first removal against the first addition is what turns "this
|
|
25
|
+
* line went away and another arrived" into "this word changed" — but only while
|
|
26
|
+
* the runs line up. Beyond the shorter run there is no counterpart, and
|
|
27
|
+
* inventing one produces confident nonsense.
|
|
28
|
+
*/
|
|
29
|
+
function pairWords(removed, added) {
|
|
30
|
+
const pairs = Math.min(removed.length, added.length);
|
|
31
|
+
for (let index = 0; index < pairs; index += 1) {
|
|
32
|
+
const from = removed[index];
|
|
33
|
+
const to = added[index];
|
|
34
|
+
if (!from || !to) continue;
|
|
35
|
+
const parts = diffWordsWithSpace(from.content, to.content);
|
|
36
|
+
from.segments = parts.filter((part) => !part.added).map((part) => ({
|
|
37
|
+
text: part.value,
|
|
38
|
+
changed: Boolean(part.removed)
|
|
39
|
+
}));
|
|
40
|
+
to.segments = parts.filter((part) => !part.removed).map((part) => ({
|
|
41
|
+
text: part.value,
|
|
42
|
+
changed: Boolean(part.added)
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function buildDiff(before, after, options = {}) {
|
|
47
|
+
const { context = 3, words = true } = options;
|
|
48
|
+
const changes = diffLines(withTrailingNewline(before), withTrailingNewline(after));
|
|
49
|
+
const rows = [];
|
|
50
|
+
let beforeLine = 1;
|
|
51
|
+
let afterLine = 1;
|
|
52
|
+
let pendingRemoved = [];
|
|
53
|
+
let pendingAdded = [];
|
|
54
|
+
const flushPair = () => {
|
|
55
|
+
if (words && pendingRemoved.length > 0 && pendingAdded.length > 0) pairWords(pendingRemoved, pendingAdded);
|
|
56
|
+
pendingRemoved = [];
|
|
57
|
+
pendingAdded = [];
|
|
58
|
+
};
|
|
59
|
+
for (const change of changes) {
|
|
60
|
+
const lines = splitLines(change.value);
|
|
61
|
+
if (change.added) {
|
|
62
|
+
for (const content of lines) {
|
|
63
|
+
const row = {
|
|
64
|
+
kind: "added",
|
|
65
|
+
after: afterLine,
|
|
66
|
+
content
|
|
67
|
+
};
|
|
68
|
+
afterLine += 1;
|
|
69
|
+
rows.push(row);
|
|
70
|
+
pendingAdded.push(row);
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (change.removed) {
|
|
75
|
+
if (pendingAdded.length > 0) flushPair();
|
|
76
|
+
for (const content of lines) {
|
|
77
|
+
const row = {
|
|
78
|
+
kind: "removed",
|
|
79
|
+
before: beforeLine,
|
|
80
|
+
content
|
|
81
|
+
};
|
|
82
|
+
beforeLine += 1;
|
|
83
|
+
rows.push(row);
|
|
84
|
+
pendingRemoved.push(row);
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
flushPair();
|
|
89
|
+
for (const content of lines) {
|
|
90
|
+
rows.push({
|
|
91
|
+
kind: "context",
|
|
92
|
+
before: beforeLine,
|
|
93
|
+
after: afterLine,
|
|
94
|
+
content
|
|
95
|
+
});
|
|
96
|
+
beforeLine += 1;
|
|
97
|
+
afterLine += 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
flushPair();
|
|
101
|
+
return groupIntoHunks(rows, context);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Groups rows into hunks, dropping context beyond `context` lines.
|
|
105
|
+
*
|
|
106
|
+
* A file with one changed line is otherwise thousands of rows of identical
|
|
107
|
+
* text, and the reader has to find the change in it.
|
|
108
|
+
*/
|
|
109
|
+
function groupIntoHunks(rows, context) {
|
|
110
|
+
const changedIndexes = rows.map((row, index) => row.kind === "context" ? -1 : index).filter((index) => index >= 0);
|
|
111
|
+
if (changedIndexes.length === 0) return [];
|
|
112
|
+
const ranges = [];
|
|
113
|
+
for (const index of changedIndexes) {
|
|
114
|
+
const start = Math.max(0, index - context);
|
|
115
|
+
const end = Math.min(rows.length - 1, index + context);
|
|
116
|
+
const last = ranges[ranges.length - 1];
|
|
117
|
+
if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end);
|
|
118
|
+
else ranges.push([start, end]);
|
|
119
|
+
}
|
|
120
|
+
let previousEnd = -1;
|
|
121
|
+
return ranges.map(([start, end], index) => {
|
|
122
|
+
const skippedBefore = start - previousEnd - 1;
|
|
123
|
+
previousEnd = end;
|
|
124
|
+
return {
|
|
125
|
+
id: `hunk-${String(index)}`,
|
|
126
|
+
rows: rows.slice(start, end + 1),
|
|
127
|
+
skippedBefore: Math.max(0, skippedBefore)
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Row pairs for a side-by-side view, aligning removals against additions. */
|
|
132
|
+
function toSplitRows(rows) {
|
|
133
|
+
const pairs = [];
|
|
134
|
+
let index = 0;
|
|
135
|
+
while (index < rows.length) {
|
|
136
|
+
const row = rows[index];
|
|
137
|
+
if (!row) break;
|
|
138
|
+
if (row.kind === "context") {
|
|
139
|
+
pairs.push({
|
|
140
|
+
left: row,
|
|
141
|
+
right: row
|
|
142
|
+
});
|
|
143
|
+
index += 1;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const removed = [];
|
|
147
|
+
while (rows[index]?.kind === "removed") {
|
|
148
|
+
removed.push(rows[index]);
|
|
149
|
+
index += 1;
|
|
150
|
+
}
|
|
151
|
+
const added = [];
|
|
152
|
+
while (rows[index]?.kind === "added") {
|
|
153
|
+
added.push(rows[index]);
|
|
154
|
+
index += 1;
|
|
155
|
+
}
|
|
156
|
+
const height = Math.max(removed.length, added.length);
|
|
157
|
+
for (let offset = 0; offset < height; offset += 1) pairs.push({
|
|
158
|
+
left: removed[offset] ?? null,
|
|
159
|
+
right: added[offset] ?? null
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return pairs;
|
|
163
|
+
}
|
|
164
|
+
/** Counts, for a summary that says what the diff does before it is read. */
|
|
165
|
+
function countChanges(hunks) {
|
|
166
|
+
let added = 0;
|
|
167
|
+
let removed = 0;
|
|
168
|
+
for (const hunk of hunks) for (const row of hunk.rows) {
|
|
169
|
+
if (row.kind === "added") added += 1;
|
|
170
|
+
if (row.kind === "removed") removed += 1;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
added,
|
|
174
|
+
removed
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
export { buildDiff, countChanges, groupIntoHunks, toSplitRows };
|
|
179
|
+
|
|
180
|
+
//# sourceMappingURL=diff-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-model.js","names":[],"sources":["../../../src/components/diff-viewer/diff-model.ts"],"sourcesContent":["import { diffLines, diffWordsWithSpace } from \"diff\";\n\n/**\n * Turning two versions of a file into rows a diff can render.\n *\n * The algorithm is not the interesting part and is not reimplemented here —\n * jsdiff does Myers properly, is BSD-licensed, has no dependencies of its own,\n * and getting an O(ND) diff subtly wrong is a poor use of anybody's afternoon.\n * What every packaged *viewer* welds on is a styling strategy: emotion in\n * react-diff-viewer-continued, HTML strings and a stylesheet in diff2html. That\n * is what cannot be reached by design tokens, and it is why this exists.\n *\n * Everything below is pure, so the grouping, the context collapsing and the\n * word-level pairing can be tested without rendering anything.\n */\n\nexport type RowKind = \"context\" | \"added\" | \"removed\";\n\nexport interface WordSegment {\n text: string;\n changed: boolean;\n}\n\nexport interface DiffRow {\n kind: RowKind;\n /** 1-based line number in the original. Absent on an added line. */\n before?: number;\n /** 1-based line number in the result. Absent on a removed line. */\n after?: number;\n content: string;\n /**\n * Word-level split, present only where a removed line pairs with an added\n * one. Without a pair there is nothing to compare against, and highlighting\n * the whole line as \"changed\" would be noise.\n */\n segments?: WordSegment[];\n}\n\nexport interface DiffHunk {\n id: string;\n rows: DiffRow[];\n /** Context lines hidden before this hunk, if any. */\n skippedBefore: number;\n}\n\nexport interface BuildDiffOptions {\n /** Unchanged lines kept either side of a change. */\n context?: number;\n /**\n * Compare word by word inside a changed line. Off for very large diffs,\n * where the extra pass costs more than it explains.\n */\n words?: boolean;\n}\n\nfunction splitLines(text: string): string[] {\n const lines = text.split(\"\\n\");\n // A trailing newline produces a final empty element that is not a line.\n if (lines.length > 1 && lines[lines.length - 1] === \"\") lines.pop();\n return lines;\n}\n\n/**\n * Gives both sides a trailing newline before they are compared.\n *\n * jsdiff's line tokens carry their own newline, so a final line written as `a`\n * and one written as `a\\n` are different tokens. Appending a line to a file\n * that did not end in a newline would otherwise be reported as the last line\n * being removed and re-added — a change the author did not make, sitting on\n * top of the one they did.\n */\nfunction withTrailingNewline(text: string): string {\n return text.endsWith(\"\\n\") ? text : `${text}\\n`;\n}\n\n/**\n * Pairs removed lines with added ones inside a single change block.\n *\n * jsdiff reports a change as a run of removals followed by a run of additions.\n * Comparing the first removal against the first addition is what turns \"this\n * line went away and another arrived\" into \"this word changed\" — but only while\n * the runs line up. Beyond the shorter run there is no counterpart, and\n * inventing one produces confident nonsense.\n */\nfunction pairWords(removed: DiffRow[], added: DiffRow[]): void {\n const pairs = Math.min(removed.length, added.length);\n\n for (let index = 0; index < pairs; index += 1) {\n const from = removed[index];\n const to = added[index];\n if (!from || !to) continue;\n\n const parts = diffWordsWithSpace(from.content, to.content);\n\n from.segments = parts\n .filter((part) => !part.added)\n .map((part) => ({ text: part.value, changed: Boolean(part.removed) }));\n\n to.segments = parts\n .filter((part) => !part.removed)\n .map((part) => ({ text: part.value, changed: Boolean(part.added) }));\n }\n}\n\nexport function buildDiff(\n before: string,\n after: string,\n options: BuildDiffOptions = {},\n): DiffHunk[] {\n const { context = 3, words = true } = options;\n\n const changes = diffLines(withTrailingNewline(before), withTrailingNewline(after));\n const rows: DiffRow[] = [];\n let beforeLine = 1;\n let afterLine = 1;\n\n // Flat rows first. Grouping into hunks is a separate concern and mixing the\n // two is how these implementations become unreadable.\n let pendingRemoved: DiffRow[] = [];\n let pendingAdded: DiffRow[] = [];\n\n const flushPair = () => {\n if (words && pendingRemoved.length > 0 && pendingAdded.length > 0) {\n pairWords(pendingRemoved, pendingAdded);\n }\n pendingRemoved = [];\n pendingAdded = [];\n };\n\n for (const change of changes) {\n const lines = splitLines(change.value);\n\n if (change.added) {\n for (const content of lines) {\n const row: DiffRow = { kind: \"added\", after: afterLine, content };\n afterLine += 1;\n rows.push(row);\n pendingAdded.push(row);\n }\n continue;\n }\n\n if (change.removed) {\n // A removal run that follows an addition run starts a new pairing.\n if (pendingAdded.length > 0) flushPair();\n for (const content of lines) {\n const row: DiffRow = { kind: \"removed\", before: beforeLine, content };\n beforeLine += 1;\n rows.push(row);\n pendingRemoved.push(row);\n }\n continue;\n }\n\n flushPair();\n for (const content of lines) {\n rows.push({ kind: \"context\", before: beforeLine, after: afterLine, content });\n beforeLine += 1;\n afterLine += 1;\n }\n }\n\n flushPair();\n\n return groupIntoHunks(rows, context);\n}\n\n/**\n * Groups rows into hunks, dropping context beyond `context` lines.\n *\n * A file with one changed line is otherwise thousands of rows of identical\n * text, and the reader has to find the change in it.\n */\nexport function groupIntoHunks(rows: DiffRow[], context: number): DiffHunk[] {\n const changedIndexes = rows\n .map((row, index) => (row.kind === \"context\" ? -1 : index))\n .filter((index) => index >= 0);\n\n if (changedIndexes.length === 0) return [];\n\n // Ranges of rows to keep, then merged where their context overlaps —\n // otherwise two nearby changes produce two hunks separated by nothing.\n const ranges: [number, number][] = [];\n for (const index of changedIndexes) {\n const start = Math.max(0, index - context);\n const end = Math.min(rows.length - 1, index + context);\n const last = ranges[ranges.length - 1];\n if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end);\n else ranges.push([start, end]);\n }\n\n let previousEnd = -1;\n return ranges.map(([start, end], index) => {\n const skippedBefore = start - previousEnd - 1;\n previousEnd = end;\n return {\n id: `hunk-${String(index)}`,\n rows: rows.slice(start, end + 1),\n skippedBefore: Math.max(0, skippedBefore),\n };\n });\n}\n\n/** Row pairs for a side-by-side view, aligning removals against additions. */\nexport function toSplitRows(\n rows: DiffRow[],\n): { left: DiffRow | null; right: DiffRow | null }[] {\n const pairs: { left: DiffRow | null; right: DiffRow | null }[] = [];\n let index = 0;\n\n while (index < rows.length) {\n const row = rows[index];\n if (!row) break;\n\n if (row.kind === \"context\") {\n pairs.push({ left: row, right: row });\n index += 1;\n continue;\n }\n\n // Take the whole removal run and the whole addition run that follows, then\n // lay them alongside each other. Emitting them in document order instead\n // would put every removal above every addition, which is the unified view\n // wearing a two-column costume.\n const removed: DiffRow[] = [];\n while (rows[index]?.kind === \"removed\") {\n removed.push(rows[index] as DiffRow);\n index += 1;\n }\n const added: DiffRow[] = [];\n while (rows[index]?.kind === \"added\") {\n added.push(rows[index] as DiffRow);\n index += 1;\n }\n\n const height = Math.max(removed.length, added.length);\n for (let offset = 0; offset < height; offset += 1) {\n pairs.push({ left: removed[offset] ?? null, right: added[offset] ?? null });\n }\n }\n\n return pairs;\n}\n\n/** Counts, for a summary that says what the diff does before it is read. */\nexport function countChanges(hunks: DiffHunk[]): { added: number; removed: number } {\n let added = 0;\n let removed = 0;\n for (const hunk of hunks) {\n for (const row of hunk.rows) {\n if (row.kind === \"added\") added += 1;\n if (row.kind === \"removed\") removed += 1;\n }\n }\n return { added, removed };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { DiffHunk } from "./diff-model.js";
|
|
2
|
+
import { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
|
+
//#region src/components/diff-viewer/diff-viewer.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A diff, and a decision about it.
|
|
6
|
+
*
|
|
7
|
+
* The rendering half exists because every packaged viewer brings its own
|
|
8
|
+
* styling system — emotion, or HTML strings plus a stylesheet — which design
|
|
9
|
+
* tokens cannot reach and which is awkward under RSC. The deciding half exists
|
|
10
|
+
* because Dowel is for AI products, and an agent proposing a change to a file is
|
|
11
|
+
* the case that needs it: what every coding agent ships, and no component
|
|
12
|
+
* library does.
|
|
13
|
+
*
|
|
14
|
+
* Decisions are controlled. The component never mutates the diff or applies
|
|
15
|
+
* anything; it reports which hunks were accepted and rejected and leaves the
|
|
16
|
+
* consequences to the application, the same way ai-action-ledger does.
|
|
17
|
+
*
|
|
18
|
+
* Not a grid. A code listing is a table of text, and role="grid" would promise
|
|
19
|
+
* cell-by-cell arrow navigation that neither exists here nor makes sense for
|
|
20
|
+
* reading code. Line kind is carried in text, because a plus sign and a green
|
|
21
|
+
* background are not information.
|
|
22
|
+
*/
|
|
23
|
+
type HunkDecision = "accepted" | "rejected";
|
|
24
|
+
interface DiffViewerProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
|
|
25
|
+
hunks: DiffHunk[];
|
|
26
|
+
/** Names the diff — usually the path of the file being changed. */
|
|
27
|
+
label: string;
|
|
28
|
+
view?: "unified" | "split";
|
|
29
|
+
/** Per-hunk decisions, keyed by hunk id. Controlled. */
|
|
30
|
+
decisions?: Record<string, HunkDecision>;
|
|
31
|
+
onDecision?: (hunkId: string, decision: HunkDecision) => void;
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
}
|
|
34
|
+
declare function DiffViewer({ className, hunks, label, view, decisions, onDecision, children, ...props }: DiffViewerProps): import("react").JSX.Element;
|
|
35
|
+
/** Switches between unified and side-by-side. */
|
|
36
|
+
declare function DiffViewerToolbar({ className, view, onViewChange, ...props }: Omit<ComponentPropsWithRef<"div">, "onChange"> & {
|
|
37
|
+
view: "unified" | "split";
|
|
38
|
+
onViewChange: (view: "unified" | "split") => void;
|
|
39
|
+
}): import("react").JSX.Element;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { DiffViewer, DiffViewerProps, DiffViewerToolbar, HunkDecision };
|
|
42
|
+
//# sourceMappingURL=diff-viewer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-viewer.d.ts","names":[],"sources":["../../../src/components/diff-viewer/diff-viewer.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;KA6BY;UAQK,wBAAwB,KAAK;EAC5C,OAAO;;EAEP;EACA;;EAEA,YAAY,eAAe;EAC3B,cAAc,gBAAgB,UAAU;EACxC,WAAW;;iBAGG,aACd,WACA,OACA,OACA,MACA,WACA,YACA,aACG,SACF,kCAAe,IAAA;;iBAmPF,oBACd,WACA,MACA,iBACG,SACF,KAAK;EACN;EACA,eAAe;oBAChB,IAAA"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { disabledStyles, focusRing } from "../../lib/styles.js";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import { countChanges, toSplitRows } from "./diff-model.js";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
//#region src/components/diff-viewer/diff-viewer.tsx
|
|
8
|
+
const KIND_LABEL = {
|
|
9
|
+
added: "Added",
|
|
10
|
+
removed: "Removed",
|
|
11
|
+
context: "Unchanged"
|
|
12
|
+
};
|
|
13
|
+
function DiffViewer({ className, hunks, label, view = "unified", decisions, onDecision, children, ...props }) {
|
|
14
|
+
const counts = useMemo(() => countChanges(hunks), [hunks]);
|
|
15
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
16
|
+
"data-slot": "diff-viewer",
|
|
17
|
+
"data-view": view,
|
|
18
|
+
className: cn("flex flex-col gap-2", className),
|
|
19
|
+
...props,
|
|
20
|
+
children: [
|
|
21
|
+
/* @__PURE__ */ jsxs("div", {
|
|
22
|
+
className: "flex flex-wrap items-baseline justify-between gap-2",
|
|
23
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
24
|
+
className: "font-mono text-sm font-medium",
|
|
25
|
+
children: label
|
|
26
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
27
|
+
"data-slot": "diff-viewer-summary",
|
|
28
|
+
className: "text-xs text-muted-foreground",
|
|
29
|
+
children: [
|
|
30
|
+
/* @__PURE__ */ jsxs("span", {
|
|
31
|
+
className: "text-success",
|
|
32
|
+
children: ["+", counts.added]
|
|
33
|
+
}),
|
|
34
|
+
" ",
|
|
35
|
+
/* @__PURE__ */ jsxs("span", {
|
|
36
|
+
className: "text-destructive",
|
|
37
|
+
children: ["−", counts.removed]
|
|
38
|
+
}),
|
|
39
|
+
/* @__PURE__ */ jsx("span", {
|
|
40
|
+
className: "sr-only",
|
|
41
|
+
children: ` — ${String(counts.added)} lines added, ${String(counts.removed)} removed`
|
|
42
|
+
})
|
|
43
|
+
]
|
|
44
|
+
})]
|
|
45
|
+
}),
|
|
46
|
+
children,
|
|
47
|
+
hunks.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
48
|
+
className: "rounded-lg border border-border bg-muted/30 p-3 text-sm text-muted-foreground",
|
|
49
|
+
children: "No changes."
|
|
50
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
51
|
+
className: "overflow-hidden rounded-lg border border-border",
|
|
52
|
+
children: hunks.map((hunk) => /* @__PURE__ */ jsx(DiffHunkView, {
|
|
53
|
+
hunk,
|
|
54
|
+
label,
|
|
55
|
+
view,
|
|
56
|
+
decision: decisions?.[hunk.id],
|
|
57
|
+
onDecision
|
|
58
|
+
}, hunk.id))
|
|
59
|
+
})
|
|
60
|
+
]
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function DiffHunkView({ hunk, label, view, decision, onDecision }) {
|
|
64
|
+
const splitRows = useMemo(() => view === "split" ? toSplitRows(hunk.rows) : [], [view, hunk.rows]);
|
|
65
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
66
|
+
"data-slot": "diff-hunk",
|
|
67
|
+
"data-decision": decision,
|
|
68
|
+
"aria-label": `${label}, hunk ${hunk.id}`,
|
|
69
|
+
className: cn("border-b border-border last:border-b-0", decision === "rejected" && "opacity-55"),
|
|
70
|
+
children: [
|
|
71
|
+
hunk.skippedBefore > 0 ? /* @__PURE__ */ jsxs("p", {
|
|
72
|
+
"data-slot": "diff-hunk-skipped",
|
|
73
|
+
className: "border-b border-border bg-muted/40 px-3 py-1 font-mono text-2xs text-muted-foreground",
|
|
74
|
+
children: [
|
|
75
|
+
hunk.skippedBefore,
|
|
76
|
+
" unchanged ",
|
|
77
|
+
hunk.skippedBefore === 1 ? "line" : "lines",
|
|
78
|
+
" hidden"
|
|
79
|
+
]
|
|
80
|
+
}) : null,
|
|
81
|
+
onDecision ? /* @__PURE__ */ jsxs("div", {
|
|
82
|
+
className: "flex flex-wrap items-center gap-2 border-b border-border bg-muted/20 px-3 py-1.5",
|
|
83
|
+
children: [
|
|
84
|
+
/* @__PURE__ */ jsx("span", {
|
|
85
|
+
className: "flex-1 text-xs text-muted-foreground",
|
|
86
|
+
children: decision === "accepted" ? "Accepted" : decision === "rejected" ? "Rejected" : "Not decided"
|
|
87
|
+
}),
|
|
88
|
+
/* @__PURE__ */ jsx(HunkButton, {
|
|
89
|
+
pressed: decision === "accepted",
|
|
90
|
+
onClick: () => {
|
|
91
|
+
onDecision(hunk.id, "accepted");
|
|
92
|
+
},
|
|
93
|
+
children: "Accept"
|
|
94
|
+
}),
|
|
95
|
+
/* @__PURE__ */ jsx(HunkButton, {
|
|
96
|
+
pressed: decision === "rejected",
|
|
97
|
+
onClick: () => {
|
|
98
|
+
onDecision(hunk.id, "rejected");
|
|
99
|
+
},
|
|
100
|
+
children: "Reject"
|
|
101
|
+
})
|
|
102
|
+
]
|
|
103
|
+
}) : null,
|
|
104
|
+
/* @__PURE__ */ jsx("div", {
|
|
105
|
+
className: "overflow-x-auto",
|
|
106
|
+
children: /* @__PURE__ */ jsx("table", {
|
|
107
|
+
"data-slot": "diff-table",
|
|
108
|
+
className: "w-full border-collapse font-mono text-xs",
|
|
109
|
+
children: /* @__PURE__ */ jsx("tbody", { children: view === "split" ? splitRows.map((pair, index) => /* @__PURE__ */ jsxs("tr", {
|
|
110
|
+
"data-slot": "diff-row",
|
|
111
|
+
children: [/* @__PURE__ */ jsx(SplitCell, {
|
|
112
|
+
row: pair.left,
|
|
113
|
+
side: "before"
|
|
114
|
+
}), /* @__PURE__ */ jsx(SplitCell, {
|
|
115
|
+
row: pair.right,
|
|
116
|
+
side: "after"
|
|
117
|
+
})]
|
|
118
|
+
}, index)) : hunk.rows.map((row, index) => /* @__PURE__ */ jsx(UnifiedRow, { row }, index)) })
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const ROW_STYLES = {
|
|
125
|
+
added: "bg-success/10",
|
|
126
|
+
removed: "bg-destructive/10",
|
|
127
|
+
context: ""
|
|
128
|
+
};
|
|
129
|
+
function UnifiedRow({ row }) {
|
|
130
|
+
return /* @__PURE__ */ jsxs("tr", {
|
|
131
|
+
"data-slot": "diff-row",
|
|
132
|
+
"data-kind": row.kind,
|
|
133
|
+
className: ROW_STYLES[row.kind],
|
|
134
|
+
children: [
|
|
135
|
+
/* @__PURE__ */ jsx(LineNumber, { value: row.before }),
|
|
136
|
+
/* @__PURE__ */ jsx(LineNumber, { value: row.after }),
|
|
137
|
+
/* @__PURE__ */ jsx("td", {
|
|
138
|
+
className: "w-4 pr-1 text-center text-muted-foreground select-none",
|
|
139
|
+
"aria-hidden": "true",
|
|
140
|
+
children: row.kind === "added" ? "+" : row.kind === "removed" ? "−" : ""
|
|
141
|
+
}),
|
|
142
|
+
/* @__PURE__ */ jsxs("td", {
|
|
143
|
+
className: "w-full py-0.5 pr-3 break-all whitespace-pre-wrap",
|
|
144
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
145
|
+
className: "sr-only",
|
|
146
|
+
children: [KIND_LABEL[row.kind], ": "]
|
|
147
|
+
}), /* @__PURE__ */ jsx(RowContent, { row })]
|
|
148
|
+
})
|
|
149
|
+
]
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function SplitCell({ row, side }) {
|
|
153
|
+
if (!row) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("td", {
|
|
154
|
+
"aria-hidden": "true",
|
|
155
|
+
className: "w-10 bg-muted/40"
|
|
156
|
+
}), /* @__PURE__ */ jsx("td", {
|
|
157
|
+
"aria-hidden": "true",
|
|
158
|
+
className: "w-1/2 bg-muted/20"
|
|
159
|
+
})] });
|
|
160
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LineNumber, { value: side === "before" ? row.before : row.after }), /* @__PURE__ */ jsxs("td", {
|
|
161
|
+
className: cn("w-1/2 py-0.5 pr-3 break-all whitespace-pre-wrap", ROW_STYLES[row.kind]),
|
|
162
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
163
|
+
className: "sr-only",
|
|
164
|
+
children: [KIND_LABEL[row.kind], ": "]
|
|
165
|
+
}), /* @__PURE__ */ jsx(RowContent, { row })]
|
|
166
|
+
})] });
|
|
167
|
+
}
|
|
168
|
+
function RowContent({ row }) {
|
|
169
|
+
if (!row.segments) return /* @__PURE__ */ jsx(Fragment, { children: row.content });
|
|
170
|
+
return /* @__PURE__ */ jsx(Fragment, { children: row.segments.map((segment, index) => segment.changed ? /* @__PURE__ */ jsx("mark", {
|
|
171
|
+
className: cn("rounded-[2px] text-inherit", row.kind === "added" ? "bg-success/30" : "bg-destructive/30"),
|
|
172
|
+
children: segment.text
|
|
173
|
+
}, index) : /* @__PURE__ */ jsx("span", { children: segment.text }, index)) });
|
|
174
|
+
}
|
|
175
|
+
function LineNumber({ value }) {
|
|
176
|
+
return /* @__PURE__ */ jsx("td", {
|
|
177
|
+
"aria-hidden": "true",
|
|
178
|
+
className: "w-10 border-r border-border bg-muted/40 px-2 py-0.5 text-right text-muted-foreground tabular-nums select-none",
|
|
179
|
+
children: value ?? ""
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function HunkButton({ className, pressed, ...props }) {
|
|
183
|
+
return /* @__PURE__ */ jsx("button", {
|
|
184
|
+
type: "button",
|
|
185
|
+
"aria-pressed": pressed,
|
|
186
|
+
className: cn("rounded-md border px-2 py-0.5 text-xs font-medium transition-colors", pressed ? "border-primary bg-primary text-primary-foreground" : "border-input bg-background hover:bg-accent hover:text-accent-foreground", focusRing, disabledStyles, className),
|
|
187
|
+
...props
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/** Switches between unified and side-by-side. */
|
|
191
|
+
function DiffViewerToolbar({ className, view, onViewChange, ...props }) {
|
|
192
|
+
return /* @__PURE__ */ jsx("div", {
|
|
193
|
+
"data-slot": "diff-viewer-toolbar",
|
|
194
|
+
role: "group",
|
|
195
|
+
"aria-label": "Diff layout",
|
|
196
|
+
className: cn("flex items-center gap-1", className),
|
|
197
|
+
...props,
|
|
198
|
+
children: ["unified", "split"].map((option) => /* @__PURE__ */ jsx("button", {
|
|
199
|
+
type: "button",
|
|
200
|
+
"aria-pressed": view === option,
|
|
201
|
+
onClick: () => {
|
|
202
|
+
onViewChange(option);
|
|
203
|
+
},
|
|
204
|
+
className: cn("rounded-md border px-2 py-0.5 text-xs font-medium capitalize transition-colors", view === option ? "border-transparent bg-secondary text-secondary-foreground" : "border-input bg-background text-muted-foreground hover:text-foreground", focusRing),
|
|
205
|
+
children: option
|
|
206
|
+
}, option))
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
export { DiffViewer, DiffViewerToolbar };
|
|
211
|
+
|
|
212
|
+
//# sourceMappingURL=diff-viewer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-viewer.js","names":[],"sources":["../../../src/components/diff-viewer/diff-viewer.tsx"],"sourcesContent":["\"use client\";\n\nimport { useMemo, type ComponentPropsWithRef, type ReactNode } from \"react\";\n\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nimport { countChanges, toSplitRows, type DiffHunk, type DiffRow } from \"./diff-model\";\n\n/**\n * A diff, and a decision about it.\n *\n * The rendering half exists because every packaged viewer brings its own\n * styling system — emotion, or HTML strings plus a stylesheet — which design\n * tokens cannot reach and which is awkward under RSC. The deciding half exists\n * because Dowel is for AI products, and an agent proposing a change to a file is\n * the case that needs it: what every coding agent ships, and no component\n * library does.\n *\n * Decisions are controlled. The component never mutates the diff or applies\n * anything; it reports which hunks were accepted and rejected and leaves the\n * consequences to the application, the same way ai-action-ledger does.\n *\n * Not a grid. A code listing is a table of text, and role=\"grid\" would promise\n * cell-by-cell arrow navigation that neither exists here nor makes sense for\n * reading code. Line kind is carried in text, because a plus sign and a green\n * background are not information.\n */\n\nexport type HunkDecision = \"accepted\" | \"rejected\";\n\nconst KIND_LABEL: Record<DiffRow[\"kind\"], string> = {\n added: \"Added\",\n removed: \"Removed\",\n context: \"Unchanged\",\n};\n\nexport interface DiffViewerProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n hunks: DiffHunk[];\n /** Names the diff — usually the path of the file being changed. */\n label: string;\n view?: \"unified\" | \"split\";\n /** Per-hunk decisions, keyed by hunk id. Controlled. */\n decisions?: Record<string, HunkDecision>;\n onDecision?: (hunkId: string, decision: HunkDecision) => void;\n children?: ReactNode;\n}\n\nexport function DiffViewer({\n className,\n hunks,\n label,\n view = \"unified\",\n decisions,\n onDecision,\n children,\n ...props\n}: DiffViewerProps) {\n const counts = useMemo(() => countChanges(hunks), [hunks]);\n\n return (\n <div\n data-slot=\"diff-viewer\"\n data-view={view}\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n >\n <div className=\"flex flex-wrap items-baseline justify-between gap-2\">\n <span className=\"font-mono text-sm font-medium\">{label}</span>\n {/* Said in words as well as coloured, and before the diff rather than\n after it: a reader deserves to know the size of the change before\n they start reading it. */}\n <span data-slot=\"diff-viewer-summary\" className=\"text-xs text-muted-foreground\">\n <span className=\"text-success\">+{counts.added}</span>{\" \"}\n <span className=\"text-destructive\">−{counts.removed}</span>\n <span className=\"sr-only\">\n {` — ${String(counts.added)} lines added, ${String(counts.removed)} removed`}\n </span>\n </span>\n </div>\n\n {children}\n\n {hunks.length === 0 ? (\n <p className=\"rounded-lg border border-border bg-muted/30 p-3 text-sm text-muted-foreground\">\n No changes.\n </p>\n ) : (\n <div className=\"overflow-hidden rounded-lg border border-border\">\n {hunks.map((hunk) => (\n <DiffHunkView\n key={hunk.id}\n hunk={hunk}\n label={label}\n view={view}\n decision={decisions?.[hunk.id]}\n onDecision={onDecision}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nfunction DiffHunkView({\n hunk,\n label,\n view,\n decision,\n onDecision,\n}: {\n hunk: DiffHunk;\n label: string;\n view: \"unified\" | \"split\";\n decision?: HunkDecision;\n onDecision?: (hunkId: string, decision: HunkDecision) => void;\n}) {\n const splitRows = useMemo(\n () => (view === \"split\" ? toSplitRows(hunk.rows) : []),\n [view, hunk.rows],\n );\n\n return (\n <section\n data-slot=\"diff-hunk\"\n data-decision={decision}\n aria-label={`${label}, hunk ${hunk.id}`}\n className={cn(\n \"border-b border-border last:border-b-0\",\n decision === \"rejected\" && \"opacity-55\",\n )}\n >\n {hunk.skippedBefore > 0 ? (\n <p\n data-slot=\"diff-hunk-skipped\"\n className=\"border-b border-border bg-muted/40 px-3 py-1 font-mono text-2xs text-muted-foreground\"\n >\n {hunk.skippedBefore} unchanged {hunk.skippedBefore === 1 ? \"line\" : \"lines\"} hidden\n </p>\n ) : null}\n\n {onDecision ? (\n <div className=\"flex flex-wrap items-center gap-2 border-b border-border bg-muted/20 px-3 py-1.5\">\n <span className=\"flex-1 text-xs text-muted-foreground\">\n {decision === \"accepted\"\n ? \"Accepted\"\n : decision === \"rejected\"\n ? \"Rejected\"\n : \"Not decided\"}\n </span>\n <HunkButton\n pressed={decision === \"accepted\"}\n onClick={() => {\n onDecision(hunk.id, \"accepted\");\n }}\n >\n Accept\n </HunkButton>\n <HunkButton\n pressed={decision === \"rejected\"}\n onClick={() => {\n onDecision(hunk.id, \"rejected\");\n }}\n >\n Reject\n </HunkButton>\n </div>\n ) : null}\n\n <div className=\"overflow-x-auto\">\n <table data-slot=\"diff-table\" className=\"w-full border-collapse font-mono text-xs\">\n <tbody>\n {view === \"split\"\n ? splitRows.map((pair, index) => (\n <tr key={index} data-slot=\"diff-row\">\n <SplitCell row={pair.left} side=\"before\" />\n <SplitCell row={pair.right} side=\"after\" />\n </tr>\n ))\n : hunk.rows.map((row, index) => <UnifiedRow key={index} row={row} />)}\n </tbody>\n </table>\n </div>\n </section>\n );\n}\n\nconst ROW_STYLES: Record<DiffRow[\"kind\"], string> = {\n added: \"bg-success/10\",\n removed: \"bg-destructive/10\",\n context: \"\",\n};\n\nfunction UnifiedRow({ row }: { row: DiffRow }) {\n return (\n <tr data-slot=\"diff-row\" data-kind={row.kind} className={ROW_STYLES[row.kind]}>\n <LineNumber value={row.before} />\n <LineNumber value={row.after} />\n <td className=\"w-4 pr-1 text-center text-muted-foreground select-none\" aria-hidden=\"true\">\n {row.kind === \"added\" ? \"+\" : row.kind === \"removed\" ? \"−\" : \"\"}\n </td>\n <td className=\"w-full py-0.5 pr-3 break-all whitespace-pre-wrap\">\n {/* The kind, for anyone who cannot see the sign or the tint. Reading a\n diff aloud without it is reading the same file twice. */}\n <span className=\"sr-only\">{KIND_LABEL[row.kind]}: </span>\n <RowContent row={row} />\n </td>\n </tr>\n );\n}\n\nfunction SplitCell({ row, side }: { row: DiffRow | null; side: \"before\" | \"after\" }) {\n if (!row) {\n // An empty half of a pair, not a blank line of code. Hidden from assistive\n // technology so a reader is not read padding.\n return (\n <>\n <td aria-hidden=\"true\" className=\"w-10 bg-muted/40\" />\n <td aria-hidden=\"true\" className=\"w-1/2 bg-muted/20\" />\n </>\n );\n }\n\n return (\n <>\n <LineNumber value={side === \"before\" ? row.before : row.after} />\n <td\n className={cn(\"w-1/2 py-0.5 pr-3 break-all whitespace-pre-wrap\", ROW_STYLES[row.kind])}\n >\n <span className=\"sr-only\">{KIND_LABEL[row.kind]}: </span>\n <RowContent row={row} />\n </td>\n </>\n );\n}\n\nfunction RowContent({ row }: { row: DiffRow }) {\n if (!row.segments) return <>{row.content}</>;\n\n return (\n <>\n {row.segments.map((segment, index) =>\n segment.changed ? (\n // A mark element, so the changed words survive as structure rather\n // than existing only as a slightly stronger background.\n <mark\n key={index}\n className={cn(\n \"rounded-[2px] text-inherit\",\n row.kind === \"added\" ? \"bg-success/30\" : \"bg-destructive/30\",\n )}\n >\n {segment.text}\n </mark>\n ) : (\n <span key={index}>{segment.text}</span>\n ),\n )}\n </>\n );\n}\n\nfunction LineNumber({ value }: { value?: number }) {\n return (\n <td\n // Decorative: the line number is orientation for a sighted reader, and\n // announcing two numbers before every line makes the diff unlistenable.\n aria-hidden=\"true\"\n className=\"w-10 border-r border-border bg-muted/40 px-2 py-0.5 text-right text-muted-foreground tabular-nums select-none\"\n >\n {value ?? \"\"}\n </td>\n );\n}\n\nfunction HunkButton({\n className,\n pressed,\n ...props\n}: ComponentPropsWithRef<\"button\"> & { pressed: boolean }) {\n return (\n <button\n type=\"button\"\n aria-pressed={pressed}\n className={cn(\n \"rounded-md border px-2 py-0.5 text-xs font-medium transition-colors\",\n pressed\n ? \"border-primary bg-primary text-primary-foreground\"\n : \"border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n disabledStyles,\n className,\n )}\n {...props}\n />\n );\n}\n\n/** Switches between unified and side-by-side. */\nexport function DiffViewerToolbar({\n className,\n view,\n onViewChange,\n ...props\n}: Omit<ComponentPropsWithRef<\"div\">, \"onChange\"> & {\n view: \"unified\" | \"split\";\n onViewChange: (view: \"unified\" | \"split\") => void;\n}) {\n return (\n <div\n data-slot=\"diff-viewer-toolbar\"\n role=\"group\"\n aria-label=\"Diff layout\"\n className={cn(\"flex items-center gap-1\", className)}\n {...props}\n >\n {([\"unified\", \"split\"] as const).map((option) => (\n <button\n key={option}\n type=\"button\"\n aria-pressed={view === option}\n onClick={() => {\n onViewChange(option);\n }}\n className={cn(\n \"rounded-md border px-2 py-0.5 text-xs font-medium capitalize transition-colors\",\n view === option\n ? \"border-transparent bg-secondary text-secondary-foreground\"\n : \"border-input bg-background text-muted-foreground hover:text-foreground\",\n focusRing,\n )}\n >\n {option}\n </button>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { BuildDiffOptions, DiffHunk, DiffRow, RowKind, WordSegment, buildDiff, countChanges, groupIntoHunks, toSplitRows } from "./diff-model.js";
|
|
2
|
+
import { DiffViewer, DiffViewerProps, DiffViewerToolbar, HunkDecision } from "./diff-viewer.js";
|
|
3
|
+
export { type BuildDiffOptions, type DiffHunk, type DiffRow, DiffViewer, type DiffViewerProps, DiffViewerToolbar, type HunkDecision, type RowKind, type WordSegment, buildDiff, countChanges, groupIntoHunks, toSplitRows };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/diff-viewer/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "diff-viewer",
|
|
5
|
+
title: "Diff Viewer",
|
|
6
|
+
description: "A diff you can style, and accept or reject hunk by hunk.",
|
|
7
|
+
category: "data",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: ["diff"],
|
|
10
|
+
registryDependencies: [],
|
|
11
|
+
files: ["diff-model.ts", "diff-viewer.tsx"],
|
|
12
|
+
a11y: "A semantic table, not a grid. role=\"grid\" would promise cell-by-cell arrow navigation that does not exist here and makes no sense for reading code. Every row states added, removed or unchanged in text, because a plus sign and a green tint are not information — a diff read aloud without it is just the same file twice. Line numbers are aria-hidden: they orient a sighted reader, and announcing two numbers before every line makes the diff unlistenable. Changed words are marked with mark elements so they survive as structure, and the empty half of a split pair is hidden rather than read as a blank line of code."
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { meta };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/diff-viewer/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"diff-viewer\",\n title: \"Diff Viewer\",\n description: \"A diff you can style, and accept or reject hunk by hunk.\",\n category: \"data\",\n status: \"stable\",\n dependencies: [\"diff\"],\n registryDependencies: [],\n files: [\"diff-model.ts\", \"diff-viewer.tsx\"],\n a11y:\n 'A semantic table, not a grid. role=\"grid\" would promise cell-by-cell arrow navigation that ' +\n \"does not exist here and makes no sense for reading code. Every row states added, removed or \" +\n \"unchanged in text, because a plus sign and a green tint are not information — a diff read \" +\n \"aloud without it is just the same file twice. Line numbers are aria-hidden: they orient a \" +\n \"sighted reader, and announcing two numbers before every line makes the diff unlistenable. \" +\n \"Changed words are marked with mark elements so they survive as structure, and the empty half \" +\n \"of a split pair is hidden rather than read as a blank line of code.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aAAa;CACb,UAAU;CACV,QAAQ;CACR,cAAc,CAAC,MAAM;CACrB,sBAAsB,CAAC;CACvB,OAAO,CAAC,iBAAiB,iBAAiB;CAC1C,MACE;AAOJ,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { QueuedFile } from "./upload-queue.js";
|
|
2
|
+
import { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
|
+
//#region src/components/file-upload/file-upload.d.ts
|
|
4
|
+
interface FileUploadProps extends Omit<ComponentPropsWithRef<"div">, "onDrop"> {
|
|
5
|
+
/** Names the control. */
|
|
6
|
+
label: string;
|
|
7
|
+
onFiles: (files: File[]) => void;
|
|
8
|
+
accept?: string;
|
|
9
|
+
multiple?: boolean;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** Shown under the prompt: accepted types, size limit. */
|
|
12
|
+
hint?: ReactNode;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
declare function FileUpload({ className, label, onFiles, accept, multiple, disabled, hint, children, ...props }: FileUploadProps): import("react").JSX.Element;
|
|
16
|
+
interface FileUploadListProps extends ComponentPropsWithRef<"ul"> {
|
|
17
|
+
files: QueuedFile[];
|
|
18
|
+
onCancel?: (id: string) => void;
|
|
19
|
+
onRetry?: (id: string) => void;
|
|
20
|
+
onRemove?: (id: string) => void;
|
|
21
|
+
}
|
|
22
|
+
declare function FileUploadList({ className, files, onCancel, onRetry, onRemove, ...props }: FileUploadListProps): import("react").JSX.Element | null;
|
|
23
|
+
interface FileUploadItemProps extends Omit<ComponentPropsWithRef<"li">, "children"> {
|
|
24
|
+
entry: QueuedFile;
|
|
25
|
+
onCancel?: (id: string) => void;
|
|
26
|
+
onRetry?: (id: string) => void;
|
|
27
|
+
onRemove?: (id: string) => void;
|
|
28
|
+
}
|
|
29
|
+
declare function FileUploadItem({ className, entry, onCancel, onRetry, onRemove, ...props }: FileUploadItemProps): import("react").JSX.Element;
|
|
30
|
+
/**
|
|
31
|
+
* One sentence covering the whole queue, announced politely.
|
|
32
|
+
*
|
|
33
|
+
* Per-file live regions would talk over each other the moment two uploads run
|
|
34
|
+
* at once; one summary of the set is readable where six competing ones are not.
|
|
35
|
+
*/
|
|
36
|
+
declare function FileUploadStatus({ className, stats, ...props }: ComponentPropsWithRef<"p"> & {
|
|
37
|
+
stats: {
|
|
38
|
+
total: number;
|
|
39
|
+
active: number;
|
|
40
|
+
done: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
};
|
|
43
|
+
}): import("react").JSX.Element;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { FileUpload, FileUploadItem, FileUploadItemProps, FileUploadList, FileUploadListProps, FileUploadProps, FileUploadStatus };
|
|
46
|
+
//# sourceMappingURL=file-upload.d.ts.map
|