@neuronection/assistant-ui 0.19.0 → 0.21.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/CHANGELOG.md +17 -0
- package/README.md +3 -0
- package/dist/ai-text-transform.d.ts +44 -0
- package/dist/ai-text-transform.js +3 -0
- package/dist/ai-text-transform.js.map +1 -0
- package/dist/chunk-2PR6POP6.js +543 -0
- package/dist/chunk-2PR6POP6.js.map +1 -0
- package/dist/chunk-BKAO7BTU.js +156 -0
- package/dist/chunk-BKAO7BTU.js.map +1 -0
- package/dist/chunk-KAC2DOLH.js +362 -0
- package/dist/chunk-KAC2DOLH.js.map +1 -0
- package/dist/{chunk-AHLNXCP2.js → chunk-UJVNCZ5Z.js} +5 -4
- package/dist/chunk-UJVNCZ5Z.js.map +1 -0
- package/dist/chunk-XUDCCQRN.js +253 -0
- package/dist/chunk-XUDCCQRN.js.map +1 -0
- package/dist/dictation.d.ts +64 -0
- package/dist/dictation.js +3 -0
- package/dist/dictation.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +16 -12
- package/dist/index.js.map +1 -1
- package/dist/rich-text-editor.d.ts +51 -0
- package/dist/rich-text-editor.js +3 -0
- package/dist/rich-text-editor.js.map +1 -0
- package/dist/settings-shell.d.ts +2 -0
- package/dist/settings-shell.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/text-diff-view.d.ts +61 -0
- package/dist/text-diff-view.js +3 -0
- package/dist/text-diff-view.js.map +1 -0
- package/package.json +24 -2
- package/dist/chunk-AHLNXCP2.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @neuronection/assistant-ui
|
|
2
2
|
|
|
3
|
+
## 0.21.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`ead8dd8`](https://github.com/neuronection/assistant-ui/commit/ead8dd8297e1fe5b090fc1f92dead8d573a975c4) Thanks [@constLiakos](https://github.com/constLiakos)! - SettingsShell nav items accept an optional `trailing` node rendered at the
|
|
8
|
+
row's trailing edge — for status dots, counts or badges next to the label.
|
|
9
|
+
|
|
10
|
+
## 0.20.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- [`e973c82`](https://github.com/neuronection/assistant-ui/commit/e973c8242355efe8cf6748615d8638211274bc8f) Thanks [@constLiakos](https://github.com/constLiakos)! - New tier-2 "transport-injected hooks" modules: `dictation` (`useDictation` state machine + `DictationButton` + `DictationStrip` — the app injects `transcribe(blob)`, the library owns recording/timer/level/cancel UI) and `ai-text-transform` (`useAiTextTransform` — streaming transform state machine with delta coalescing, poll fallback, hard timeout and terminal-event guard; the app injects start/subscribe/cancel/poll closures). No fetching inside the library — ADR-006 holds.
|
|
15
|
+
|
|
16
|
+
- [`c00baa9`](https://github.com/neuronection/assistant-ui/commit/c00baa97d3c6446e624bd8b9da30af2eb3fbf743) Thanks [@constLiakos](https://github.com/constLiakos)! - New `rich-text-editor` module: controlled tiptap-based markdown editor with a configurable toolbar (groups + `toolbarExtra` app slot), label/icon props with English defaults, caret-preserving external sync, and a markdown round-trip contract. Adds `@tiptap/react`, `@tiptap/starter-kit`, `@tiptap/pm` and `tiptap-markdown` as dependencies.
|
|
17
|
+
|
|
18
|
+
- [`5d965d8`](https://github.com/neuronection/assistant-ui/commit/5d965d8425fe46f93da2ab24b52e36593fdbf965) Thanks [@constLiakos](https://github.com/constLiakos)! - New `text-diff-view` module: gitlens-style side-by-side diff view (line-level LCS diff with word-level intra-line highlighting, per-side line numbers, prev/next change navigation with active-block tint, expandable/re-collapsible unchanged-line folds, virtualized rows) plus exported `computeLineDiff` / `wordSegments` engine. Adds `@tanstack/react-virtual` as a dependency. Labels are props/callbacks with English defaults.
|
|
19
|
+
|
|
3
20
|
## 0.19.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -76,6 +76,9 @@ Visual reference: [gallery](https://neuronection.github.io/assistant-ui/).
|
|
|
76
76
|
| [`tooltip`](./docs/components/tooltip.md) | `Tooltip` + parts, `InfoTooltip` | [stories](https://neuronection.github.io/assistant-ui/?story=overlay--tooltips) |
|
|
77
77
|
| [`wizard`](./docs/components/wizard.md) | `Wizard` (steps config + `renderStep`, per-step validation gates, modal/drawer variants), `Stepper` (dots/labels) | [stories](https://neuronection.github.io/assistant-ui/?story=wizard--modal-wizard) |
|
|
78
78
|
| [`input`](./docs/components/input.md) / [`textarea`](./docs/components/textarea.md) / [`search-input`](./docs/components/search-input.md) / [`expandable-search`](./docs/components/expandable-search.md) | `Input` (label/hint/error wiring) · `Textarea` · `SearchInput` · `ExpandableSearch` | [stories](https://neuronection.github.io/assistant-ui/?story=misc--inputs) |
|
|
79
|
+
| [`rich-text-editor`](./docs/components/rich-text-editor.md) | `RichTextEditor` — controlled tiptap markdown editor: configurable toolbar groups + `toolbarExtra` slot, label/icon props, caret-preserving external sync | [stories](https://neuronection.github.io/assistant-ui/?story=rich-text-editor--default) |
|
|
80
|
+
| [`dictation`](./docs/components/dictation.md) / [`ai-text-transform`](./docs/components/ai-text-transform.md) | `useDictation` + `DictationButton`/`DictationStrip` (transport-injected STT) · `useAiTextTransform` (streaming transform state machine) | [stories](https://neuronection.github.io/assistant-ui/?story=dictation--dictation) |
|
|
81
|
+
| [`text-diff-view`](./docs/components/text-diff-view.md) | `TextDiffView` — side-by-side diff with word-level highlights, line numbers, change navigation, folds, virtualization (+ exported `computeLineDiff`/`wordSegments`) | [stories](https://neuronection.github.io/assistant-ui/?story=text-diff-view--default) |
|
|
79
82
|
| [`chip-input`](./docs/components/chip-input.md) / [`chip-list`](./docs/components/chip-list.md) | `ChipInput` (Enter/comma commit, paste-splits, Backspace-removes) · `ChipList` (variant pills, clickable, removable) | [stories](https://neuronection.github.io/assistant-ui/?story=phase2--chip-editing) |
|
|
80
83
|
| [`time-picker`](./docs/components/time-picker.md) / [`time-list`](./docs/components/time-list.md) | `TimePicker` (clock face, 24h value, 12h UI, editable fields) · `TimeList` (time chips with per-chip pickers) | [stories](https://neuronection.github.io/assistant-ui/?story=time-pickers--time-picker-story) |
|
|
81
84
|
| [`date-picker`](./docs/components/date-picker.md) | `DatePicker` (popover calendar: days/months/years views, min/max, `allowClear`, arrow-key day grid, `unstyled` variant) | [stories](https://neuronection.github.io/assistant-ui/?story=phase2--date-pickers) |
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type AiTextTransformStatus = 'idle' | 'running' | 'done' | 'error' | 'cancelled';
|
|
2
|
+
interface AiTextTransformEventHandlers {
|
|
3
|
+
onDelta: (text: string) => void;
|
|
4
|
+
onDone: (result: string) => void;
|
|
5
|
+
onError: (message: string) => void;
|
|
6
|
+
}
|
|
7
|
+
interface AiTextTransformTransport {
|
|
8
|
+
/** Begin the job and resolve with its handle (e.g. a job id). */
|
|
9
|
+
start: () => Promise<string>;
|
|
10
|
+
/** Subscribe to a job's events; returns an unsubscribe function. */
|
|
11
|
+
subscribe: (jobId: string, handlers: AiTextTransformEventHandlers) => () => void;
|
|
12
|
+
cancel?: (jobId: string) => Promise<void>;
|
|
13
|
+
/** Optional status fallback when events may be missed. */
|
|
14
|
+
poll?: (jobId: string) => Promise<{
|
|
15
|
+
status: 'running' | 'done' | 'error' | 'cancelled';
|
|
16
|
+
result?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
interface UseAiTextTransformOptions {
|
|
21
|
+
transport: AiTextTransformTransport;
|
|
22
|
+
/** Status-fallback interval when `poll` is provided. Default 800 ms. */
|
|
23
|
+
pollIntervalMs?: number;
|
|
24
|
+
/** Hard timeout for a running job. Default 90 000 ms. */
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
/** Streaming delta coalescing window. Default 50 ms. */
|
|
27
|
+
flushMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Streaming text-transform state machine (idle → running → done/error/
|
|
31
|
+
* cancelled). Transport-injected: the app provides start/subscribe/cancel/
|
|
32
|
+
* poll closures over its own gateway; this hook owns status transitions,
|
|
33
|
+
* delta coalescing, the poll fallback and the hard timeout.
|
|
34
|
+
*/
|
|
35
|
+
declare function useAiTextTransform(options: UseAiTextTransformOptions): {
|
|
36
|
+
status: AiTextTransformStatus;
|
|
37
|
+
result: string;
|
|
38
|
+
error: string | null;
|
|
39
|
+
start: () => Promise<void>;
|
|
40
|
+
stop: () => Promise<void>;
|
|
41
|
+
reset: () => void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export { type AiTextTransformEventHandlers, type AiTextTransformStatus, type AiTextTransformTransport, type UseAiTextTransformOptions, useAiTextTransform };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"ai-text-transform.js"}
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { cn } from './chunk-BLJWR4ZV.js';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
4
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
5
|
+
import { Minus, ChevronUp, ChevronDown, Plus, ChevronRight } from 'lucide-react';
|
|
6
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
// src/components/text-diff-view/lineDiff.ts
|
|
9
|
+
var MAX_LCS_CELLS = 4e6;
|
|
10
|
+
var MAX_WORD_CELLS = 4e4;
|
|
11
|
+
function at(list, index) {
|
|
12
|
+
const value = list[index];
|
|
13
|
+
if (value === void 0) {
|
|
14
|
+
throw new Error(`lineDiff: index ${index} out of bounds`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function atNum(list, index) {
|
|
19
|
+
const value = list[index];
|
|
20
|
+
if (value === void 0) {
|
|
21
|
+
throw new Error(`lineDiff: index ${index} out of bounds`);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function splitLines(text) {
|
|
26
|
+
if (text === "") {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return text.replace(/\r\n?/g, "\n").split("\n");
|
|
30
|
+
}
|
|
31
|
+
function diffOps(a, b, maxCells = MAX_LCS_CELLS) {
|
|
32
|
+
const n = a.length;
|
|
33
|
+
const m = b.length;
|
|
34
|
+
const ops = [];
|
|
35
|
+
if (n * m > maxCells) {
|
|
36
|
+
for (const text of a) {
|
|
37
|
+
ops.push({ type: "del", text });
|
|
38
|
+
}
|
|
39
|
+
for (const text of b) {
|
|
40
|
+
ops.push({ type: "add", text });
|
|
41
|
+
}
|
|
42
|
+
return ops;
|
|
43
|
+
}
|
|
44
|
+
const width = m + 1;
|
|
45
|
+
const lcs = new Int32Array((n + 1) * width);
|
|
46
|
+
for (let i2 = n - 1; i2 >= 0; i2 -= 1) {
|
|
47
|
+
for (let j2 = m - 1; j2 >= 0; j2 -= 1) {
|
|
48
|
+
lcs[i2 * width + j2] = at(a, i2) === at(b, j2) ? atNum(lcs, (i2 + 1) * width + j2 + 1) + 1 : Math.max(atNum(lcs, (i2 + 1) * width + j2), atNum(lcs, i2 * width + j2 + 1));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let i = 0;
|
|
52
|
+
let j = 0;
|
|
53
|
+
while (i < n && j < m) {
|
|
54
|
+
if (at(a, i) === at(b, j)) {
|
|
55
|
+
ops.push({ type: "equal", text: at(a, i) });
|
|
56
|
+
i += 1;
|
|
57
|
+
j += 1;
|
|
58
|
+
} else if (atNum(lcs, (i + 1) * width + j) >= atNum(lcs, i * width + j + 1)) {
|
|
59
|
+
ops.push({ type: "del", text: at(a, i) });
|
|
60
|
+
i += 1;
|
|
61
|
+
} else {
|
|
62
|
+
ops.push({ type: "add", text: at(b, j) });
|
|
63
|
+
j += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
while (i < n) {
|
|
67
|
+
ops.push({ type: "del", text: at(a, i) });
|
|
68
|
+
i += 1;
|
|
69
|
+
}
|
|
70
|
+
while (j < m) {
|
|
71
|
+
ops.push({ type: "add", text: at(b, j) });
|
|
72
|
+
j += 1;
|
|
73
|
+
}
|
|
74
|
+
return ops;
|
|
75
|
+
}
|
|
76
|
+
function tokenize(text) {
|
|
77
|
+
return text.match(/\S+|\s+/g) ?? [];
|
|
78
|
+
}
|
|
79
|
+
function isMeaningful(token) {
|
|
80
|
+
return /\S/.test(token);
|
|
81
|
+
}
|
|
82
|
+
function mergeSegments(segments) {
|
|
83
|
+
const merged = [];
|
|
84
|
+
for (const segment of segments) {
|
|
85
|
+
const last = merged[merged.length - 1];
|
|
86
|
+
if (last !== void 0 && last.changed === segment.changed) {
|
|
87
|
+
last.text += segment.text;
|
|
88
|
+
} else {
|
|
89
|
+
merged.push({ ...segment });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return merged;
|
|
93
|
+
}
|
|
94
|
+
function wordSegments(original, suggested) {
|
|
95
|
+
const a = tokenize(original);
|
|
96
|
+
const b = tokenize(suggested);
|
|
97
|
+
const ops = diffOps(a, b, MAX_WORD_CELLS);
|
|
98
|
+
const left = [];
|
|
99
|
+
const right = [];
|
|
100
|
+
for (const op of ops) {
|
|
101
|
+
if (op.type === "equal") {
|
|
102
|
+
left.push({ text: op.text, changed: false });
|
|
103
|
+
right.push({ text: op.text, changed: false });
|
|
104
|
+
} else if (op.type === "del") {
|
|
105
|
+
left.push({ text: op.text, changed: isMeaningful(op.text) });
|
|
106
|
+
} else {
|
|
107
|
+
right.push({ text: op.text, changed: isMeaningful(op.text) });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { left: mergeSegments(left), right: mergeSegments(right) };
|
|
111
|
+
}
|
|
112
|
+
function pairRows(ops) {
|
|
113
|
+
const rows = [];
|
|
114
|
+
let index = 0;
|
|
115
|
+
while (index < ops.length) {
|
|
116
|
+
const op = at(ops, index);
|
|
117
|
+
if (op.type === "equal") {
|
|
118
|
+
rows.push({
|
|
119
|
+
kind: "pair",
|
|
120
|
+
left: { kind: "context", text: op.text },
|
|
121
|
+
right: { kind: "context", text: op.text }
|
|
122
|
+
});
|
|
123
|
+
index += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const dels = [];
|
|
127
|
+
const adds = [];
|
|
128
|
+
while (index < ops.length && at(ops, index).type !== "equal") {
|
|
129
|
+
const current = at(ops, index);
|
|
130
|
+
if (current.type === "del") {
|
|
131
|
+
dels.push(current.text);
|
|
132
|
+
} else {
|
|
133
|
+
adds.push(current.text);
|
|
134
|
+
}
|
|
135
|
+
index += 1;
|
|
136
|
+
}
|
|
137
|
+
const pairCount = Math.max(dels.length, adds.length);
|
|
138
|
+
for (let k = 0; k < pairCount; k += 1) {
|
|
139
|
+
const leftText = k < dels.length ? at(dels, k) : null;
|
|
140
|
+
const rightText = k < adds.length ? at(adds, k) : null;
|
|
141
|
+
const leftCell = leftText !== null ? { kind: "del", text: leftText } : null;
|
|
142
|
+
const rightCell = rightText !== null ? { kind: "add", text: rightText } : null;
|
|
143
|
+
if (leftCell !== null && rightCell !== null && leftText !== null && rightText !== null && leftText !== rightText) {
|
|
144
|
+
const words = wordSegments(leftText, rightText);
|
|
145
|
+
leftCell.segments = words.left;
|
|
146
|
+
rightCell.segments = words.right;
|
|
147
|
+
}
|
|
148
|
+
rows.push({ kind: "pair", left: leftCell, right: rightCell });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return rows;
|
|
152
|
+
}
|
|
153
|
+
function numberRows(rows) {
|
|
154
|
+
let leftNo = 0;
|
|
155
|
+
let rightNo = 0;
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
if (row.kind !== "pair") {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (row.left !== null) {
|
|
161
|
+
leftNo += 1;
|
|
162
|
+
row.left.no = leftNo;
|
|
163
|
+
}
|
|
164
|
+
if (row.right !== null) {
|
|
165
|
+
rightNo += 1;
|
|
166
|
+
row.right.no = rightNo;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function foldRows(rows, contextLines) {
|
|
171
|
+
const hasChange = rows.some((row) => {
|
|
172
|
+
if (row.kind !== "pair") {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return row.left?.kind !== "context" || row.right?.kind !== "context";
|
|
176
|
+
});
|
|
177
|
+
if (!hasChange || contextLines < 0) {
|
|
178
|
+
return rows;
|
|
179
|
+
}
|
|
180
|
+
const runs = [];
|
|
181
|
+
let start = -1;
|
|
182
|
+
for (let index2 = 0; index2 <= rows.length; index2 += 1) {
|
|
183
|
+
const row = rows[index2];
|
|
184
|
+
const isContext = index2 < rows.length && row !== void 0 && row.kind === "pair" && row.left?.kind === "context" && row.right?.kind === "context";
|
|
185
|
+
if (isContext) {
|
|
186
|
+
if (start === -1) {
|
|
187
|
+
start = index2;
|
|
188
|
+
}
|
|
189
|
+
} else if (start !== -1) {
|
|
190
|
+
runs.push({ start, end: index2 });
|
|
191
|
+
start = -1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const hiddenAt = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const run of runs) {
|
|
196
|
+
const length = run.end - run.start;
|
|
197
|
+
const isFirstRun = run.start === 0;
|
|
198
|
+
const isLastRun = run.end === rows.length;
|
|
199
|
+
const keepBefore = isFirstRun ? 0 : contextLines;
|
|
200
|
+
const keepAfter = isLastRun ? 0 : contextLines;
|
|
201
|
+
const foldCount = length - keepBefore - keepAfter;
|
|
202
|
+
if (foldCount <= 0) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const hidden = rows.slice(run.start + keepBefore, run.start + keepBefore + foldCount);
|
|
206
|
+
hiddenAt.set(run.start + keepBefore, hidden);
|
|
207
|
+
}
|
|
208
|
+
const folded = [];
|
|
209
|
+
let index = 0;
|
|
210
|
+
while (index < rows.length) {
|
|
211
|
+
const hidden = hiddenAt.get(index);
|
|
212
|
+
if (hidden === void 0) {
|
|
213
|
+
folded.push(at(rows, index));
|
|
214
|
+
index += 1;
|
|
215
|
+
} else {
|
|
216
|
+
folded.push({ kind: "fold", count: hidden.length, hidden });
|
|
217
|
+
index += hidden.length;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return folded;
|
|
221
|
+
}
|
|
222
|
+
function computeLineDiff(original, suggested, options = {}) {
|
|
223
|
+
const contextLines = options.contextLines ?? 2;
|
|
224
|
+
const ops = diffOps(splitLines(original), splitLines(suggested));
|
|
225
|
+
const flatRows = pairRows(ops);
|
|
226
|
+
numberRows(flatRows);
|
|
227
|
+
const rows = foldRows(flatRows, contextLines);
|
|
228
|
+
let added = 0;
|
|
229
|
+
let removed = 0;
|
|
230
|
+
for (const row of flatRows) {
|
|
231
|
+
if (row.kind !== "pair") {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (row.right?.kind === "add") {
|
|
235
|
+
added += 1;
|
|
236
|
+
}
|
|
237
|
+
if (row.left?.kind === "del") {
|
|
238
|
+
removed += 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { rows, added, removed };
|
|
242
|
+
}
|
|
243
|
+
var DEFAULT_LABELS = {
|
|
244
|
+
original: "Original",
|
|
245
|
+
suggested: "Suggested",
|
|
246
|
+
unchangedLines: (count) => count === 1 ? "1 unchanged line" : `${count} unchanged lines`,
|
|
247
|
+
showLess: "Show less",
|
|
248
|
+
prevChange: "Previous change",
|
|
249
|
+
nextChange: "Next change",
|
|
250
|
+
changePosition: (index, total) => `${index}/${total}`
|
|
251
|
+
};
|
|
252
|
+
function isChangedRow(row) {
|
|
253
|
+
return row.kind === "pair" && (row.left?.kind === "del" || row.right?.kind === "add");
|
|
254
|
+
}
|
|
255
|
+
function CellContent({ cell }) {
|
|
256
|
+
if (cell.segments === void 0) {
|
|
257
|
+
return /* @__PURE__ */ jsx("span", { className: "whitespace-pre-wrap break-words", children: cell.text });
|
|
258
|
+
}
|
|
259
|
+
return /* @__PURE__ */ jsx("span", { className: "whitespace-pre-wrap break-words", children: cell.segments.map(
|
|
260
|
+
(segment, index) => segment.changed ? /* @__PURE__ */ jsx(
|
|
261
|
+
"mark",
|
|
262
|
+
{
|
|
263
|
+
className: cn(
|
|
264
|
+
"rounded-[2px] text-inherit",
|
|
265
|
+
cell.kind === "add" ? "bg-[var(--as-success)]/30" : "bg-[var(--as-danger)]/30"
|
|
266
|
+
),
|
|
267
|
+
children: segment.text
|
|
268
|
+
},
|
|
269
|
+
index
|
|
270
|
+
) : /* @__PURE__ */ jsx("span", { children: segment.text }, index)
|
|
271
|
+
) });
|
|
272
|
+
}
|
|
273
|
+
var TextDiffView = React.forwardRef(
|
|
274
|
+
function TextDiffView2({
|
|
275
|
+
original,
|
|
276
|
+
suggested,
|
|
277
|
+
contextLines = 2,
|
|
278
|
+
showHeader = true,
|
|
279
|
+
showNav = true,
|
|
280
|
+
labels,
|
|
281
|
+
className,
|
|
282
|
+
bodyClassName
|
|
283
|
+
}, ref) {
|
|
284
|
+
const mergedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
285
|
+
const [expanded, setExpanded] = useState(/* @__PURE__ */ new Set());
|
|
286
|
+
const [activeGroup, setActiveGroup] = useState(0);
|
|
287
|
+
const scrollRef = React.useRef(null);
|
|
288
|
+
const diff = useMemo(
|
|
289
|
+
() => computeLineDiff(original, suggested, { contextLines }),
|
|
290
|
+
[original, suggested, contextLines]
|
|
291
|
+
);
|
|
292
|
+
const flatRows = useMemo(() => {
|
|
293
|
+
const out = [];
|
|
294
|
+
diff.rows.forEach((row, index) => {
|
|
295
|
+
if (row.kind === "fold" && expanded.has(index)) {
|
|
296
|
+
row.hidden.forEach((hiddenRow, hiddenIndex) => {
|
|
297
|
+
out.push({ key: `${index}.${hiddenIndex}`, row: hiddenRow });
|
|
298
|
+
});
|
|
299
|
+
out.push({ key: `${index}.collapse`, row, collapseOf: index });
|
|
300
|
+
} else {
|
|
301
|
+
out.push({
|
|
302
|
+
key: String(index),
|
|
303
|
+
row,
|
|
304
|
+
foldOf: row.kind === "fold" ? index : void 0
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
return out;
|
|
309
|
+
}, [diff.rows, expanded]);
|
|
310
|
+
const changeGroups = useMemo(() => {
|
|
311
|
+
const groups = [];
|
|
312
|
+
let inGroup = false;
|
|
313
|
+
flatRows.forEach((entry, index) => {
|
|
314
|
+
const changed = isChangedRow(entry.row);
|
|
315
|
+
if (changed && !inGroup) {
|
|
316
|
+
groups.push(index);
|
|
317
|
+
}
|
|
318
|
+
inGroup = changed;
|
|
319
|
+
});
|
|
320
|
+
return groups;
|
|
321
|
+
}, [flatRows]);
|
|
322
|
+
const virtualizer = useVirtualizer({
|
|
323
|
+
count: flatRows.length,
|
|
324
|
+
getScrollElement: () => scrollRef.current,
|
|
325
|
+
estimateSize: () => 22,
|
|
326
|
+
overscan: 12,
|
|
327
|
+
getItemKey: (index) => flatRows[index]?.key ?? String(index)
|
|
328
|
+
});
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
setExpanded(/* @__PURE__ */ new Set());
|
|
331
|
+
setActiveGroup(0);
|
|
332
|
+
}, [original, suggested]);
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (activeGroup >= changeGroups.length) {
|
|
335
|
+
setActiveGroup(0);
|
|
336
|
+
}
|
|
337
|
+
}, [activeGroup, changeGroups.length]);
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
const target = changeGroups[activeGroup];
|
|
340
|
+
if (target !== void 0) {
|
|
341
|
+
virtualizer.scrollToIndex(target, { align: "center" });
|
|
342
|
+
}
|
|
343
|
+
}, [activeGroup, changeGroups, virtualizer]);
|
|
344
|
+
if (diff.added === 0 && diff.removed === 0) {
|
|
345
|
+
return /* @__PURE__ */ jsx(
|
|
346
|
+
"p",
|
|
347
|
+
{
|
|
348
|
+
ref,
|
|
349
|
+
className: cn(
|
|
350
|
+
"text-[var(--as-muted-fg)] border-[var(--as-border)] bg-[var(--as-subtle)] rounded-md border p-3 text-xs",
|
|
351
|
+
className
|
|
352
|
+
),
|
|
353
|
+
children: "No changes"
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const activeGroupStart = changeGroups[activeGroup];
|
|
358
|
+
const activeGroupEnd = activeGroupStart === void 0 ? void 0 : (() => {
|
|
359
|
+
for (let index = activeGroupStart + 1; index < flatRows.length; index += 1) {
|
|
360
|
+
const entry = flatRows[index];
|
|
361
|
+
if (entry !== void 0 && isChangedRow(entry.row)) {
|
|
362
|
+
return index;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return flatRows.length;
|
|
366
|
+
})();
|
|
367
|
+
const renderRow = (entry, index) => {
|
|
368
|
+
const row = entry.row;
|
|
369
|
+
if (row.kind === "fold") {
|
|
370
|
+
if (entry.collapseOf !== void 0) {
|
|
371
|
+
const collapseOf = entry.collapseOf;
|
|
372
|
+
return /* @__PURE__ */ jsxs(
|
|
373
|
+
"button",
|
|
374
|
+
{
|
|
375
|
+
type: "button",
|
|
376
|
+
onClick: () => setExpanded((current) => {
|
|
377
|
+
const next = new Set(current);
|
|
378
|
+
next.delete(collapseOf);
|
|
379
|
+
return next;
|
|
380
|
+
}),
|
|
381
|
+
className: "text-[var(--as-muted-fg)] hover:bg-[var(--as-subtle)] hover:text-[var(--as-fg)] flex w-full items-center gap-1 px-3 py-0.5 text-left text-[11px]",
|
|
382
|
+
children: [
|
|
383
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "size-3", "aria-hidden": true }),
|
|
384
|
+
mergedLabels.showLess
|
|
385
|
+
]
|
|
386
|
+
},
|
|
387
|
+
entry.key
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const foldOf = entry.foldOf;
|
|
391
|
+
return /* @__PURE__ */ jsxs(
|
|
392
|
+
"button",
|
|
393
|
+
{
|
|
394
|
+
type: "button",
|
|
395
|
+
onClick: () => {
|
|
396
|
+
if (foldOf !== void 0) {
|
|
397
|
+
setExpanded((current) => {
|
|
398
|
+
const next = new Set(current);
|
|
399
|
+
next.add(foldOf);
|
|
400
|
+
return next;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
className: "text-[var(--as-muted-fg)] hover:bg-[var(--as-subtle)] hover:text-[var(--as-fg)] flex w-full items-center gap-1 px-3 py-0.5 text-left text-[11px]",
|
|
405
|
+
children: [
|
|
406
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "size-3", "aria-hidden": true }),
|
|
407
|
+
mergedLabels.unchangedLines(row.count)
|
|
408
|
+
]
|
|
409
|
+
},
|
|
410
|
+
entry.key
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const changed = isChangedRow(row);
|
|
414
|
+
const active = activeGroupStart !== void 0 && activeGroupEnd !== void 0 && index >= activeGroupStart && index < activeGroupEnd;
|
|
415
|
+
return /* @__PURE__ */ jsx(
|
|
416
|
+
"div",
|
|
417
|
+
{
|
|
418
|
+
className: cn("grid grid-cols-2", active && "bg-[var(--as-primary)]/5"),
|
|
419
|
+
"data-changed": changed || void 0,
|
|
420
|
+
"data-active": active || void 0,
|
|
421
|
+
children: ["left", "right"].map((side) => {
|
|
422
|
+
const cell = row[side];
|
|
423
|
+
const isAdd = cell?.kind === "add";
|
|
424
|
+
const isDel = cell?.kind === "del";
|
|
425
|
+
return /* @__PURE__ */ jsxs(
|
|
426
|
+
"pre",
|
|
427
|
+
{
|
|
428
|
+
className: cn(
|
|
429
|
+
"flex min-w-0 gap-1.5 px-2 py-0.5",
|
|
430
|
+
isAdd && "bg-[var(--as-success)]/10 text-[var(--as-success)]",
|
|
431
|
+
isDel && "bg-[var(--as-danger)]/10 text-[var(--as-danger)]"
|
|
432
|
+
),
|
|
433
|
+
children: [
|
|
434
|
+
/* @__PURE__ */ jsx("span", { className: "w-7 shrink-0 select-none text-right text-[var(--as-muted-fg)] tabular-nums opacity-70", children: cell?.no ?? "" }),
|
|
435
|
+
/* @__PURE__ */ jsx("span", { className: "w-2 shrink-0 select-none text-center", "aria-hidden": true, children: isAdd ? "+" : isDel ? "-" : "" }),
|
|
436
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1", children: cell === null ? null : /* @__PURE__ */ jsx(CellContent, { cell }) })
|
|
437
|
+
]
|
|
438
|
+
},
|
|
439
|
+
side
|
|
440
|
+
);
|
|
441
|
+
})
|
|
442
|
+
},
|
|
443
|
+
entry.key
|
|
444
|
+
);
|
|
445
|
+
};
|
|
446
|
+
return /* @__PURE__ */ jsxs(
|
|
447
|
+
"div",
|
|
448
|
+
{
|
|
449
|
+
ref,
|
|
450
|
+
"data-as": "text-diff-view",
|
|
451
|
+
className: cn(
|
|
452
|
+
"bg-[var(--as-subtle)] border-[var(--as-border)] flex flex-col overflow-hidden rounded-md border font-mono text-xs leading-relaxed",
|
|
453
|
+
className
|
|
454
|
+
),
|
|
455
|
+
children: [
|
|
456
|
+
showHeader ? /* @__PURE__ */ jsxs("div", { className: "border-[var(--as-border)] text-[var(--as-muted-fg)] grid grid-cols-[1fr_auto_1fr] items-center gap-2 border-b px-3 py-1.5 text-[11px] font-medium tracking-wide uppercase", children: [
|
|
457
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
|
|
458
|
+
/* @__PURE__ */ jsx(Minus, { className: "size-3 text-[var(--as-danger)]", "aria-hidden": true }),
|
|
459
|
+
mergedLabels.original
|
|
460
|
+
] }),
|
|
461
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
462
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
463
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[var(--as-success)]", children: [
|
|
464
|
+
"+",
|
|
465
|
+
diff.added
|
|
466
|
+
] }),
|
|
467
|
+
" ",
|
|
468
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[var(--as-danger)]", children: [
|
|
469
|
+
"\u2212",
|
|
470
|
+
diff.removed
|
|
471
|
+
] })
|
|
472
|
+
] }),
|
|
473
|
+
showNav && changeGroups.length > 0 ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-0.5", children: [
|
|
474
|
+
/* @__PURE__ */ jsx(
|
|
475
|
+
"button",
|
|
476
|
+
{
|
|
477
|
+
type: "button",
|
|
478
|
+
"aria-label": mergedLabels.prevChange,
|
|
479
|
+
title: mergedLabels.prevChange,
|
|
480
|
+
disabled: activeGroup === 0,
|
|
481
|
+
onClick: () => setActiveGroup((value) => Math.max(0, value - 1)),
|
|
482
|
+
className: "hover:bg-[var(--as-border)] hover:text-[var(--as-fg)] rounded p-0.5 disabled:opacity-40",
|
|
483
|
+
children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-3.5", "aria-hidden": true })
|
|
484
|
+
}
|
|
485
|
+
),
|
|
486
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: mergedLabels.changePosition(activeGroup + 1, changeGroups.length) }),
|
|
487
|
+
/* @__PURE__ */ jsx(
|
|
488
|
+
"button",
|
|
489
|
+
{
|
|
490
|
+
type: "button",
|
|
491
|
+
"aria-label": mergedLabels.nextChange,
|
|
492
|
+
title: mergedLabels.nextChange,
|
|
493
|
+
disabled: activeGroup >= changeGroups.length - 1,
|
|
494
|
+
onClick: () => setActiveGroup((value) => Math.min(changeGroups.length - 1, value + 1)),
|
|
495
|
+
className: "hover:bg-[var(--as-border)] hover:text-[var(--as-fg)] rounded p-0.5 disabled:opacity-40",
|
|
496
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5", "aria-hidden": true })
|
|
497
|
+
}
|
|
498
|
+
)
|
|
499
|
+
] }) : null
|
|
500
|
+
] }),
|
|
501
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center justify-end gap-1", children: [
|
|
502
|
+
mergedLabels.suggested,
|
|
503
|
+
/* @__PURE__ */ jsx(Plus, { className: "size-3 text-[var(--as-success)]", "aria-hidden": true })
|
|
504
|
+
] })
|
|
505
|
+
] }) : null,
|
|
506
|
+
/* @__PURE__ */ jsx("div", { ref: scrollRef, className: cn("min-h-0 flex-1 overflow-y-auto", bodyClassName), children: /* @__PURE__ */ jsx(
|
|
507
|
+
"div",
|
|
508
|
+
{
|
|
509
|
+
style: {
|
|
510
|
+
height: virtualizer.getTotalSize(),
|
|
511
|
+
position: "relative",
|
|
512
|
+
width: "100%"
|
|
513
|
+
},
|
|
514
|
+
children: virtualizer.getVirtualItems().map((virtualItem) => /* @__PURE__ */ jsx(
|
|
515
|
+
"div",
|
|
516
|
+
{
|
|
517
|
+
"data-index": virtualItem.index,
|
|
518
|
+
ref: virtualizer.measureElement,
|
|
519
|
+
style: {
|
|
520
|
+
position: "absolute",
|
|
521
|
+
top: 0,
|
|
522
|
+
left: 0,
|
|
523
|
+
width: "100%",
|
|
524
|
+
transform: `translateY(${virtualItem.start}px)`
|
|
525
|
+
},
|
|
526
|
+
children: (() => {
|
|
527
|
+
const entry = flatRows[virtualItem.index];
|
|
528
|
+
return entry === void 0 ? null : renderRow(entry, virtualItem.index);
|
|
529
|
+
})()
|
|
530
|
+
},
|
|
531
|
+
virtualItem.key
|
|
532
|
+
))
|
|
533
|
+
}
|
|
534
|
+
) })
|
|
535
|
+
]
|
|
536
|
+
}
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
export { TextDiffView, computeLineDiff, wordSegments };
|
|
542
|
+
//# sourceMappingURL=chunk-2PR6POP6.js.map
|
|
543
|
+
//# sourceMappingURL=chunk-2PR6POP6.js.map
|