tosijs-ui 1.12.5 → 1.12.6
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 +119 -0
- package/dist/code-editor.d.ts +3 -0
- package/dist/code-editor.js +59 -0
- package/dist/diff.d.ts +84 -0
- package/dist/diff.js +307 -12
- package/dist/doc-system/release-notes.d.ts +0 -1
- package/dist/doc-system/release-notes.js +19 -1
- package/dist/doc-system/site/dev-server.js +27 -4
- package/dist/doc-system/site/index.d.ts +2 -0
- package/dist/doc-system/site/index.js +13 -0
- package/dist/iife.js +78 -77
- package/dist/iife.js.map +7 -7
- package/dist/live-example/component.js +20 -0
- package/dist/notifications.js +13 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,124 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.12.6
|
|
4
|
+
|
|
5
|
+
### `<tosi-diff resolvable>` — "do I want to accept this?"
|
|
6
|
+
|
|
7
|
+
The diff viewer can now be operated, not just read. Set `resolvable` and every change gets a
|
|
8
|
+
pair of buttons; `value` is the text those choices imply and `change` fires when one moves. It
|
|
9
|
+
is a review surface for a **proposed** edit — an AI suggestion, a collaborator's revision, a
|
|
10
|
+
file that moved under you while you were editing it.
|
|
11
|
+
|
|
12
|
+
The labels are yours: `originalLabel`/`modifiedLabel`. "Mine"/"Theirs" is git's framing,
|
|
13
|
+
"Current"/"Proposed" suits reviewing a suggestion, and this component's own vocabulary is
|
|
14
|
+
original/modified — none of which is right for everyone, so none of it is hardcoded. Also
|
|
15
|
+
`acceptAll()`, `rejectAll()`, `changeCount`, and a `resolutions` array you can read and write.
|
|
16
|
+
`diffBlocks()` and `resolveDiff()` are exported too, if you want the model without the DOM.
|
|
17
|
+
|
|
18
|
+
The unit of decision is a change **block**, not a line. The LCS walk can emit
|
|
19
|
+
remove/add/remove/add for a single multi-line edit, and offering four independent choices there
|
|
20
|
+
lets a reviewer accept half an edit — producing text neither side wrote.
|
|
21
|
+
|
|
22
|
+
The read-only path is deliberately unchanged: grouping reorders interleaved edits, which is
|
|
23
|
+
better to resolve and a **different rendering** for the two components already shipping this
|
|
24
|
+
one. Additive means additive.
|
|
25
|
+
|
|
26
|
+
Two notes on how this was tested, because **both** bugs that nearly shipped were invisible to
|
|
27
|
+
the unit lane, and for the same underlying reason.
|
|
28
|
+
|
|
29
|
+
The first version rendered **nothing**: `dataset: {…}` in an element creator assigns
|
|
30
|
+
`el.dataset`, a read-only accessor, so `render()` threw and left an empty shadow root.
|
|
31
|
+
Twenty-six green tests sat behind it, every one asserting `value` — which is computed from the
|
|
32
|
+
model and is correct whether or not a single element reaches the DOM. tosijs renders on an
|
|
33
|
+
animation frame and happy-dom runs none, so a unit test that merely _mounts_ an element asserts
|
|
34
|
+
against an empty shadow root; appending hydrates, and `render()` must be called explicitly.
|
|
35
|
+
|
|
36
|
+
The second was worse, because the render-level tests added for the first one _also_ passed. The
|
|
37
|
+
buttons did nothing when clicked, in every browser. `<tosi-diff>` is a shadow-DOM component and
|
|
38
|
+
tosijs delegates `on*` handlers from above the shadow boundary, so a real click is
|
|
39
|
+
**retargeted**: `event.target` is the host, not the button, and `event.target.closest(…)` — the
|
|
40
|
+
idiom this project documents, and which is correct for a light-DOM component — is `null` every
|
|
41
|
+
time. The fix is `composedPath()`, the only one of the two that survives the boundary.
|
|
42
|
+
|
|
43
|
+
The happy-dom test could not see it: it called `.click()` on a node it had already looked up and
|
|
44
|
+
then asserted the value moved. Measured against a deliberately broken build, **30 unit tests
|
|
45
|
+
pass and all 3 browser tests fail** — so `tests/diff-resolve.pw.ts` now clicks what a person
|
|
46
|
+
clicks and reads what a host reads. The rule this leaves behind: **shadow-DOM interaction needs
|
|
47
|
+
a browser test**, not a DOM-shaped unit test.
|
|
48
|
+
|
|
49
|
+
### Selective revert in live examples
|
|
50
|
+
|
|
51
|
+
The diff resolver is wired into `<tosi-example>`'s **View changes**, and into `<tosi-code>`
|
|
52
|
+
generally via a new `diffResolvable` (plus `diffOriginalLabel`/`diffModifiedLabel`).
|
|
53
|
+
|
|
54
|
+
Revert was all-or-nothing, which is the wrong shape for what actually happens: you tried four
|
|
55
|
+
things and three worked. Each change in the diff now carries keep/revert buttons — labelled
|
|
56
|
+
**Source** and **Yours** in an example — defaulting to _keep_, so the feature costs nothing to
|
|
57
|
+
ignore and doing nothing leaves your edit exactly as it was.
|
|
58
|
+
|
|
59
|
+
Choices are applied when the diff **closes**, not as you click, and a `change` event fires if
|
|
60
|
+
the text moved. Writing each choice straight back into `value` would feed the new text into the
|
|
61
|
+
overlay's `modified`, and `<tosi-diff>` resets its decisions when either input changes — so live
|
|
62
|
+
application would wipe the choices making it and renumber the hunks under the pointer.
|
|
63
|
+
|
|
64
|
+
### Fixed: `<tosi-diff>` and notifications ignored dark mode
|
|
65
|
+
|
|
66
|
+
Both hardcoded a light palette — `#fff`/`#222` for the diff, `#fafafa`/`#444` for
|
|
67
|
+
notifications — so on a dark page you got a white block, and in the notification's case the one
|
|
68
|
+
element in the library that appears unbidden over whatever you are looking at was also the one
|
|
69
|
+
that ignored your theme.
|
|
70
|
+
|
|
71
|
+
Both now fall back to the theme (`--background`/`--text-color`) with the old literals as the
|
|
72
|
+
final fallback, so an unthemed page is unchanged and every `--tosi-diff-*` /
|
|
73
|
+
`--notification-*` override still wins exactly as before.
|
|
74
|
+
|
|
75
|
+
The diff needed both halves at once: darkening the surface alone would have been **worse** than
|
|
76
|
+
leaving it, because light text on a solid `#e6ffed` row is unreadable. The add/remove tints are
|
|
77
|
+
now translucent (`color-mix` with `transparent`), so one value reads correctly as a wash over
|
|
78
|
+
either surface.
|
|
79
|
+
|
|
80
|
+
### Fixed: a consumer could not use the build lock ([#117](https://github.com/tonioloewald/tosijs-ui/issues/117), [#118](https://github.com/tonioloewald/tosijs-ui/issues/118))
|
|
81
|
+
|
|
82
|
+
1.12.5 added a build lock that records `pid`, `role`, `root` and `port` per project, so a
|
|
83
|
+
`stop` command can identify what is running instead of `pkill`-ing sibling checkouts. It worked
|
|
84
|
+
— and none of it was reachable from outside the package. `import('tosijs-ui/site')` gave
|
|
85
|
+
`currentHolder === undefined`, and the deep path is blocked by the exports map.
|
|
86
|
+
|
|
87
|
+
`currentHolder`, `describeHolder`, `lockPathFor`, `lockDecision`, `isProcessAlive` and the
|
|
88
|
+
`LockHolder`/`LockDecision` types are now re-exported from `tosijs-ui/site`.
|
|
89
|
+
|
|
90
|
+
The reporter deliberately did **not** re-derive the lock path locally, and was right not to:
|
|
91
|
+
that means copying the FNV-1a hash of the resolved root, and if the hash ever changed the
|
|
92
|
+
consumer would report "nothing running" _while a server runs_. Silently-wrong is the exact
|
|
93
|
+
failure this whole area exists to eliminate; duplicating the algorithm reintroduces it one
|
|
94
|
+
layer out.
|
|
95
|
+
|
|
96
|
+
### haltija floor raised to `^1.12.6`
|
|
97
|
+
|
|
98
|
+
The floor encodes fixes, not a date. 1.12.6 is where a `--private` instance finally gained a
|
|
99
|
+
**lifetime bound** (haltija#39, filed from here after one was found 12 days old at 5.7 GB and
|
|
100
|
+
~150% CPU on a machine at load average 212); where **LAN and Bonjour access started working at
|
|
101
|
+
all** (served scripts handed the browser `localhost:<port>`, and `localhost` in a served script
|
|
102
|
+
means the _browser's_ machine, so a phone was told to connect to itself); where the widget sends
|
|
103
|
+
`X-Haltija-Token`, so a `--token` server and a page that can talk back stopped being mutually
|
|
104
|
+
exclusive; where results carry `paintAgeMs`, making a rAF-starved tab detectable rather than
|
|
105
|
+
silently wrong; and where Electron went 40.6.1 → 43.4.1, clearing two context-isolation
|
|
106
|
+
bypasses in the thing we spawn.
|
|
107
|
+
|
|
108
|
+
(Internal, but worth recording: our own `CLAUDE.md` documented this pin as `^1.6.1` long after
|
|
109
|
+
the code had moved to `^1.11.2` — a stale number in the one place a reader would trust for it.
|
|
110
|
+
Corrected.)
|
|
111
|
+
|
|
112
|
+
### Docs: our haltija gate covers the bridge, not the port
|
|
113
|
+
|
|
114
|
+
The `haltijaDev: 'tunnel'` documentation read as a complete security story — session cookie,
|
|
115
|
+
404-on-unauthorized, relay rather than hole — and a reader could reasonably conclude the whole
|
|
116
|
+
surface was gated. It is not. Enabling `haltijaDev` at all means haltija's **own** port is
|
|
117
|
+
running, and a default instance binds beyond loopback with `Access-Control-Allow-Origin: *` and
|
|
118
|
+
no token, where `POST /terminal/command` reaches a shell. Set `HALTIJA_TOKEN` on a network you
|
|
119
|
+
do not control; it only became genuinely usable in haltija 1.12.6, which is why older advice
|
|
120
|
+
said to skip it.
|
|
121
|
+
|
|
3
122
|
## 1.12.5
|
|
4
123
|
|
|
5
124
|
### Fixed: 49 controls a screen reader announced as "button" and "edit text"
|
package/dist/code-editor.d.ts
CHANGED
|
@@ -30,6 +30,9 @@ export declare class CodeEditor extends WebComponent<CodeEditorParts> {
|
|
|
30
30
|
private diffOverlay;
|
|
31
31
|
private _pendingDiff;
|
|
32
32
|
get showingDiff(): boolean;
|
|
33
|
+
diffResolvable: boolean;
|
|
34
|
+
diffOriginalLabel: string;
|
|
35
|
+
diffModifiedLabel: string;
|
|
33
36
|
showDiff(on: boolean): void;
|
|
34
37
|
static initAttributes: {
|
|
35
38
|
mode: string;
|
package/dist/code-editor.js
CHANGED
|
@@ -24,6 +24,8 @@ body {
|
|
|
24
24
|
| `mode` | `javascript`, `typescript`, `tjs`, `ajs`, `css`, `html`, `markdown` |
|
|
25
25
|
| `disabled` | makes the editor read-only |
|
|
26
26
|
| `original` + `showDiff(on)` | diff the current `value` against a baseline, as an overlay |
|
|
27
|
+
| `diffResolvable` | make that overlay **resolvable**: each change gets keep/revert buttons, and the choices are applied to `value` when the diff closes |
|
|
28
|
+
| `diffOriginalLabel` / `diffModifiedLabel` | what the two buttons say (default `Original`/`Modified`) |
|
|
27
29
|
| `editor` | the underlying CodeMirror [`EditorView`](https://codemirror.net/docs/ref/#view.EditorView) (`undefined` until loaded) |
|
|
28
30
|
| `undo()` / `redo()` / `canUndo()` / `canRedo()` | history control |
|
|
29
31
|
| `tjsAutocomplete` | runtime-value autocomplete hooks (tjs mode) — see below |
|
|
@@ -38,6 +40,29 @@ runtime values** — including proxy members no static analysis can see:
|
|
|
38
40
|
codeEl.tjsAutocomplete = { getLiveBindings: () => ({ app, elements }) }
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
## Selective revert
|
|
44
|
+
|
|
45
|
+
`showDiff(on)` shows a read-only diff. Set `diffResolvable` first and the overlay becomes a
|
|
46
|
+
review surface instead: every change carries keep/revert buttons, defaulting to **keep**, and
|
|
47
|
+
whatever you leave alone survives. This is what `<tosi-example>`'s **View changes** uses — you
|
|
48
|
+
tried four things and three worked, so revert the one you regret rather than all four.
|
|
49
|
+
|
|
50
|
+
The choices are applied when the diff **closes**, not as you click, and a `change` event fires
|
|
51
|
+
if the text moved. That is deliberate: writing each choice straight back into `value` would
|
|
52
|
+
feed the new text into the overlay's `modified`, and `<tosi-diff>` resets its decisions when
|
|
53
|
+
either input changes — so live application would wipe the choices making it and renumber the
|
|
54
|
+
hunks under the pointer.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
codeEl.original = savedSource
|
|
58
|
+
codeEl.diffResolvable = true
|
|
59
|
+
codeEl.diffOriginalLabel = 'Source'
|
|
60
|
+
codeEl.diffModifiedLabel = 'Yours'
|
|
61
|
+
codeEl.showDiff(true)
|
|
62
|
+
// …user reverts a hunk or two…
|
|
63
|
+
codeEl.showDiff(false) // codeEl.value now reflects their choices
|
|
64
|
+
```
|
|
65
|
+
|
|
41
66
|
## Bundling
|
|
42
67
|
|
|
43
68
|
CodeMirror is a **lazy chunk**: with a bundler (ESM), a page that never uses
|
|
@@ -154,6 +179,17 @@ export class CodeEditor extends WebComponent {
|
|
|
154
179
|
return this._pendingDiff ?? false;
|
|
155
180
|
return !this.parts.diffHost.hidden;
|
|
156
181
|
}
|
|
182
|
+
/*
|
|
183
|
+
Make the diff overlay RESOLVABLE, turning it from a review into selective revert.
|
|
184
|
+
|
|
185
|
+
Off by default, so `showDiff()` stays exactly what it was. With it on, every change gets
|
|
186
|
+
keep/revert buttons and the ones you leave alone are kept — which is the useful default when
|
|
187
|
+
the "modified" side is your own edit: you are reverting the two lines you regret, not
|
|
188
|
+
re-accepting the twenty you meant.
|
|
189
|
+
*/
|
|
190
|
+
diffResolvable = false;
|
|
191
|
+
diffOriginalLabel = 'Original';
|
|
192
|
+
diffModifiedLabel = 'Modified';
|
|
157
193
|
showDiff(on) {
|
|
158
194
|
if (!this.hydrated) {
|
|
159
195
|
this._pendingDiff = on;
|
|
@@ -165,9 +201,32 @@ export class CodeEditor extends WebComponent {
|
|
|
165
201
|
this.diffOverlay = tosiDiff();
|
|
166
202
|
diffHost.append(this.diffOverlay);
|
|
167
203
|
}
|
|
204
|
+
this.diffOverlay.resolvable = this.diffResolvable;
|
|
205
|
+
this.diffOverlay.originalLabel = this.diffOriginalLabel;
|
|
206
|
+
this.diffOverlay.modifiedLabel = this.diffModifiedLabel;
|
|
168
207
|
this.diffOverlay.original = this.original;
|
|
169
208
|
this.diffOverlay.modified = this.value;
|
|
170
209
|
}
|
|
210
|
+
else if (this.diffOverlay !== undefined && this.diffResolvable) {
|
|
211
|
+
/*
|
|
212
|
+
Resolutions land on the way OUT, not as you click.
|
|
213
|
+
|
|
214
|
+
Writing each choice straight back into `value` would feed the editor's new text into the
|
|
215
|
+
overlay's `modified`, and `<tosi-diff>` resets its decisions when either input changes —
|
|
216
|
+
by design, because choice #2 of the old diff is not choice #2 of the new one. Live
|
|
217
|
+
application would therefore wipe the very choices making it, and the hunks would
|
|
218
|
+
renumber under the pointer. Applying once, when the diff closes, sidesteps that
|
|
219
|
+
completely and matches how a reviewer thinks: decide, then commit.
|
|
220
|
+
|
|
221
|
+
The `change` event matters as much as the value: it is how a host recomputes "does this
|
|
222
|
+
still differ from the source" after a partial revert.
|
|
223
|
+
*/
|
|
224
|
+
const resolved = this.diffOverlay.value;
|
|
225
|
+
if (resolved !== this.value) {
|
|
226
|
+
this.value = resolved;
|
|
227
|
+
this.dispatchEvent(new CustomEvent('change', { detail: { value: resolved } }));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
171
230
|
diffHost.hidden = !on;
|
|
172
231
|
}
|
|
173
232
|
static initAttributes = {
|
package/dist/diff.d.ts
CHANGED
|
@@ -4,6 +4,35 @@ export interface DiffLine {
|
|
|
4
4
|
op: DiffOp;
|
|
5
5
|
text: string;
|
|
6
6
|
}
|
|
7
|
+
/** Which side of a change block won. */
|
|
8
|
+
export type DiffResolution = 'original' | 'modified';
|
|
9
|
+
/**
|
|
10
|
+
* A run of unchanged lines, or one CHANGE — the unit a reviewer accepts or rejects.
|
|
11
|
+
*
|
|
12
|
+
* `diffLines` emits removes and adds in walk order, which for a multi-line edit can
|
|
13
|
+
* interleave (`r a`, `+ A`, `r b`, `+ B`). That is fine to *read* and wrong to *resolve*:
|
|
14
|
+
* accepting half of an edit is not a state anybody asked for. Grouping every consecutive
|
|
15
|
+
* non-context run into one block is what makes "do I want this change?" a single decision.
|
|
16
|
+
*/
|
|
17
|
+
export type DiffBlock = {
|
|
18
|
+
kind: 'context';
|
|
19
|
+
lines: string[];
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'change';
|
|
22
|
+
removed: string[];
|
|
23
|
+
added: string[];
|
|
24
|
+
};
|
|
25
|
+
/** Group a line diff into context runs and change blocks, in display order. */
|
|
26
|
+
export declare function diffBlocks(lines: DiffLine[]): DiffBlock[];
|
|
27
|
+
/**
|
|
28
|
+
* Rebuild the text implied by a set of per-change choices.
|
|
29
|
+
*
|
|
30
|
+
* The invariant worth holding onto, and the reason this is a pure function with its own
|
|
31
|
+
* tests: all-`original` reproduces the original EXACTLY and all-`modified` reproduces the
|
|
32
|
+
* modified exactly. A resolver that cannot round-trip its own endpoints is one you cannot
|
|
33
|
+
* trust in the middle.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveDiff(blocks: DiffBlock[], choices: DiffResolution[]): string;
|
|
7
36
|
/**
|
|
8
37
|
* Line-level diff of two strings via longest-common-subsequence, returning each
|
|
9
38
|
* line tagged `context` (unchanged), `remove` (only in `before`), or `add` (only
|
|
@@ -18,6 +47,9 @@ export declare class TosiDiff extends Component<DiffParts> {
|
|
|
18
47
|
static initAttributes: {
|
|
19
48
|
original: string;
|
|
20
49
|
modified: string;
|
|
50
|
+
resolvable: boolean;
|
|
51
|
+
originalLabel: string;
|
|
52
|
+
modifiedLabel: string;
|
|
21
53
|
};
|
|
22
54
|
static shadowStyleSpec: {
|
|
23
55
|
':host': {
|
|
@@ -53,8 +85,60 @@ export declare class TosiDiff extends Component<DiffParts> {
|
|
|
53
85
|
'.diff-remove .marker': {
|
|
54
86
|
color: string;
|
|
55
87
|
};
|
|
88
|
+
'.diff-hunk': {
|
|
89
|
+
display: string;
|
|
90
|
+
position: string;
|
|
91
|
+
margin: string;
|
|
92
|
+
borderRadius: string;
|
|
93
|
+
boxShadow: string;
|
|
94
|
+
};
|
|
95
|
+
'.diff-choices': {
|
|
96
|
+
display: string;
|
|
97
|
+
gap: string;
|
|
98
|
+
alignItems: string;
|
|
99
|
+
padding: string;
|
|
100
|
+
};
|
|
101
|
+
'.diff-choices button': {
|
|
102
|
+
font: string;
|
|
103
|
+
cursor: string;
|
|
104
|
+
padding: string;
|
|
105
|
+
borderRadius: string;
|
|
106
|
+
border: string;
|
|
107
|
+
color: string;
|
|
108
|
+
background: string;
|
|
109
|
+
};
|
|
110
|
+
'.diff-choices button[aria-pressed="true"]': {
|
|
111
|
+
background: string;
|
|
112
|
+
color: string;
|
|
113
|
+
};
|
|
114
|
+
'.diff-line.not-chosen': {
|
|
115
|
+
opacity: string;
|
|
116
|
+
textDecoration: string;
|
|
117
|
+
textDecorationColor: string;
|
|
118
|
+
};
|
|
56
119
|
};
|
|
57
120
|
content: () => HTMLDivElement[];
|
|
121
|
+
private _resolutions;
|
|
122
|
+
private resolvedOriginal;
|
|
123
|
+
private resolvedModified;
|
|
124
|
+
/** One entry per change block, in document order. */
|
|
125
|
+
get resolutions(): DiffResolution[];
|
|
126
|
+
set resolutions(choices: DiffResolution[]);
|
|
127
|
+
private get blocks();
|
|
128
|
+
/** Number of decisions this diff asks for. `0` means the two texts agree. */
|
|
129
|
+
get changeCount(): number;
|
|
130
|
+
/**
|
|
131
|
+
* The text implied by the current choices. With everything accepted this is exactly
|
|
132
|
+
* `modified`, and with everything rejected exactly `original`, so a host that never shows
|
|
133
|
+
* the controls still gets a sensible value.
|
|
134
|
+
*/
|
|
135
|
+
get value(): string;
|
|
136
|
+
private setAll;
|
|
137
|
+
acceptAll: () => void;
|
|
138
|
+
rejectAll: () => void;
|
|
139
|
+
private syncResolutions;
|
|
140
|
+
private choose;
|
|
141
|
+
private lineElement;
|
|
58
142
|
render(): void;
|
|
59
143
|
}
|
|
60
144
|
export declare const tosiDiff: ElementCreator<TosiDiff>;
|