@nice-code/state 0.10.0 → 0.12.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/build/{Store-B65MojT2.d.ts → Store-B65MojT2.d.cts} +1 -1
- package/build/Store-B65MojT2.d.mts +201 -0
- package/build/{Store-CI9N0P6I.js → Store-CI9N0P6I.cjs} +1 -1
- package/build/Store-CI9N0P6I.cjs.map +1 -0
- package/build/{Store-PjfFkZ2I.js → Store-PjfFkZ2I.mjs} +1 -1
- package/build/Store-PjfFkZ2I.mjs.map +1 -0
- package/build/devtools/browser/index.cjs +2792 -0
- package/build/devtools/browser/index.cjs.map +1 -0
- package/build/devtools/browser/{index.d.ts → index.d.cts} +2 -2
- package/build/devtools/browser/index.d.mts +120 -0
- package/build/devtools/browser/{index.js → index.mjs} +1 -32
- package/build/devtools/browser/index.mjs.map +1 -0
- package/build/index.cjs +5 -0
- package/build/{index.d.ts → index.d.cts} +1 -1
- package/build/index.d.mts +2 -0
- package/build/{index.js → index.mjs} +1 -1
- package/build/react/index.cjs +72 -0
- package/build/react/index.cjs.map +1 -0
- package/build/react/{index.d.ts → index.d.cts} +2 -2
- package/build/react/index.d.mts +58 -0
- package/build/react/{index.js → index.mjs} +2 -2
- package/build/react/index.mjs.map +1 -0
- package/package.json +33 -10
- package/build/Store-CI9N0P6I.js.map +0 -1
- package/build/Store-PjfFkZ2I.js.map +0 -1
- package/build/devtools/browser/index.js.map +0 -1
- package/build/react/index.js.map +0 -1
|
@@ -0,0 +1,2792 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let react = require("react");
|
|
3
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
4
|
+
let _tanstack_react_virtual = require("@tanstack/react-virtual");
|
|
5
|
+
//#region src/devtools/core/StateDevtoolsCore.ts
|
|
6
|
+
/**
|
|
7
|
+
* Framework-agnostic collector for nice-state stores.
|
|
8
|
+
*
|
|
9
|
+
* Register any {@link Store} and the core hooks its patch + update streams,
|
|
10
|
+
* pairing the two so each committed mutation becomes a single
|
|
11
|
+
* {@link IDevtoolsStateChange} with patches and full before/after snapshots.
|
|
12
|
+
* The browser panel ({@link NiceStateDevtools}) renders whatever this exposes.
|
|
13
|
+
*
|
|
14
|
+
* Registering a store attaches a patch listener, which makes Immer compute
|
|
15
|
+
* patches for that store's updates — a negligible dev-time cost. Keep this out
|
|
16
|
+
* of production bundles (the panel is dev-gated, but the core is not).
|
|
17
|
+
*/
|
|
18
|
+
var StateDevtoolsCore = class {
|
|
19
|
+
_stores = /* @__PURE__ */ new Map();
|
|
20
|
+
_changes = [];
|
|
21
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
22
|
+
_maxChanges;
|
|
23
|
+
_paused = false;
|
|
24
|
+
_cuidCounter = 0;
|
|
25
|
+
_sourceOverride = null;
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this._maxChanges = options.maxChanges ?? 250;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Start observing a store under a human-readable label. Returns an
|
|
31
|
+
* unregister function. If the label collides with an existing store, a numeric
|
|
32
|
+
* suffix is appended to keep ids unique.
|
|
33
|
+
*/
|
|
34
|
+
registerStore(label, store) {
|
|
35
|
+
const id = this._uniqueId(label);
|
|
36
|
+
let lastSnapshot = store.getRawState();
|
|
37
|
+
let pendingPatches = [];
|
|
38
|
+
let pendingInverse = [];
|
|
39
|
+
const unsubPatches = store.listenToPatches((patches, inverse) => {
|
|
40
|
+
for (const p of patches) pendingPatches.push(p);
|
|
41
|
+
for (const p of inverse) pendingInverse.push(p);
|
|
42
|
+
});
|
|
43
|
+
const unsubUpdate = store.subscribe(() => {
|
|
44
|
+
const snapshot = store.getRawState();
|
|
45
|
+
const patches = pendingPatches;
|
|
46
|
+
const inverse = pendingInverse;
|
|
47
|
+
pendingPatches = [];
|
|
48
|
+
pendingInverse = [];
|
|
49
|
+
const reg = this._stores.get(id);
|
|
50
|
+
if (reg != null) reg.currentState = snapshot;
|
|
51
|
+
const source = this._sourceOverride ?? (patches.length > 0 ? "update" : "replace");
|
|
52
|
+
const prevSnapshot = lastSnapshot;
|
|
53
|
+
lastSnapshot = snapshot;
|
|
54
|
+
if (this._paused) {
|
|
55
|
+
this._notify();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this._recordChange(id, {
|
|
59
|
+
cuid: `chg_${++this._cuidCounter}`,
|
|
60
|
+
storeId: id,
|
|
61
|
+
storeLabel: label,
|
|
62
|
+
timestamp: Date.now(),
|
|
63
|
+
patches,
|
|
64
|
+
inversePatches: inverse,
|
|
65
|
+
prevSnapshot,
|
|
66
|
+
snapshot,
|
|
67
|
+
source
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
this._stores.set(id, {
|
|
71
|
+
id,
|
|
72
|
+
label,
|
|
73
|
+
store,
|
|
74
|
+
currentState: lastSnapshot,
|
|
75
|
+
changeCount: 0,
|
|
76
|
+
unsubscribe: () => {
|
|
77
|
+
unsubPatches();
|
|
78
|
+
unsubUpdate();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
this._notify();
|
|
82
|
+
return () => this.unregisterStore(id);
|
|
83
|
+
}
|
|
84
|
+
unregisterStore(id) {
|
|
85
|
+
const reg = this._stores.get(id);
|
|
86
|
+
if (reg == null) return;
|
|
87
|
+
reg.unsubscribe();
|
|
88
|
+
this._stores.delete(id);
|
|
89
|
+
this._changes = this._changes.filter((c) => c.storeId !== id);
|
|
90
|
+
this._notify();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Replace a registered store's state wholesale — the primary "edit directly
|
|
94
|
+
* for testing" entry point. The resulting change is tagged `devtools-edit`.
|
|
95
|
+
*/
|
|
96
|
+
applyEdit(storeId, newState) {
|
|
97
|
+
const reg = this._stores.get(storeId);
|
|
98
|
+
if (reg == null) return;
|
|
99
|
+
this._sourceOverride = "devtools-edit";
|
|
100
|
+
try {
|
|
101
|
+
reg.store.replace(newState);
|
|
102
|
+
} finally {
|
|
103
|
+
this._sourceOverride = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Undo a change by applying its inverse patches. Cleanest for the most recent
|
|
108
|
+
* change of a store; older reverts may not apply if later changes touched the
|
|
109
|
+
* same paths. The resulting change is tagged `devtools-revert`.
|
|
110
|
+
*/
|
|
111
|
+
revertChange(change) {
|
|
112
|
+
const reg = this._stores.get(change.storeId);
|
|
113
|
+
if (reg == null || change.inversePatches.length === 0) return;
|
|
114
|
+
this._sourceOverride = "devtools-revert";
|
|
115
|
+
try {
|
|
116
|
+
reg.store.applyPatches(change.inversePatches);
|
|
117
|
+
} finally {
|
|
118
|
+
this._sourceOverride = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
setPaused(paused) {
|
|
122
|
+
if (this._paused === paused) return;
|
|
123
|
+
this._paused = paused;
|
|
124
|
+
this._notify();
|
|
125
|
+
}
|
|
126
|
+
togglePaused() {
|
|
127
|
+
this.setPaused(!this._paused);
|
|
128
|
+
}
|
|
129
|
+
/** Drop the recorded timeline; registered stores and their state remain. */
|
|
130
|
+
clear() {
|
|
131
|
+
this._changes = [];
|
|
132
|
+
for (const reg of this._stores.values()) {
|
|
133
|
+
reg.changeCount = 0;
|
|
134
|
+
reg.lastChangeTime = void 0;
|
|
135
|
+
}
|
|
136
|
+
this._notify();
|
|
137
|
+
}
|
|
138
|
+
getSnapshot() {
|
|
139
|
+
return this._buildSnapshot();
|
|
140
|
+
}
|
|
141
|
+
subscribe(listener) {
|
|
142
|
+
this._listeners.add(listener);
|
|
143
|
+
listener(this._buildSnapshot());
|
|
144
|
+
return () => {
|
|
145
|
+
this._listeners.delete(listener);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
_recordChange(id, change) {
|
|
149
|
+
const reg = this._stores.get(id);
|
|
150
|
+
if (reg != null) {
|
|
151
|
+
reg.changeCount += 1;
|
|
152
|
+
reg.lastChangeTime = change.timestamp;
|
|
153
|
+
}
|
|
154
|
+
this._changes = [change, ...this._changes];
|
|
155
|
+
if (this._changes.length > this._maxChanges) this._changes.length = this._maxChanges;
|
|
156
|
+
this._notify();
|
|
157
|
+
}
|
|
158
|
+
_uniqueId(label) {
|
|
159
|
+
if (!this._stores.has(label)) return label;
|
|
160
|
+
let n = 2;
|
|
161
|
+
while (this._stores.has(`${label} (${n})`)) n += 1;
|
|
162
|
+
return `${label} (${n})`;
|
|
163
|
+
}
|
|
164
|
+
_buildSnapshot() {
|
|
165
|
+
const stores = [];
|
|
166
|
+
for (const reg of this._stores.values()) stores.push({
|
|
167
|
+
id: reg.id,
|
|
168
|
+
label: reg.label,
|
|
169
|
+
currentState: reg.currentState,
|
|
170
|
+
changeCount: reg.changeCount,
|
|
171
|
+
lastChangeTime: reg.lastChangeTime
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
stores,
|
|
175
|
+
changes: this._changes,
|
|
176
|
+
paused: this._paused
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
_notify() {
|
|
180
|
+
const snapshot = this._buildSnapshot();
|
|
181
|
+
for (const listener of this._listeners) listener(snapshot);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region ../nice-devtools-shared/src/colors.ts
|
|
186
|
+
const DEVTOOL_COLOR_SEMANTIC_ERROR = "#FF5C5C";
|
|
187
|
+
const DEVTOOL_COLOR_SEMANTIC_SUCCESS = "#A3E635";
|
|
188
|
+
const DEVTOOL_COLOR_SEMANTIC_SYSTEM = "#38BDF8";
|
|
189
|
+
const DEVTOOL_COLOR_SEMANTIC_WARNING = "#FB923C";
|
|
190
|
+
const DEVTOOL_COLOR_SEMANTIC_METADATA = "#A1A1AA";
|
|
191
|
+
const DEVTOOL_COLOR_TEXT_EMPHASIS = "#f1f5f9";
|
|
192
|
+
const DEVTOOL_COLOR_TEXT_SECONDARY = "#cbd5e1";
|
|
193
|
+
const DEVTOOL_COLOR_TEXT_MUTED = "#64748b";
|
|
194
|
+
const DEVTOOL_COLOR_TEXT_FAINT = "#334155";
|
|
195
|
+
const DEVTOOL_LIST_BASE_BACKGROUND = "#0f172a";
|
|
196
|
+
const DEVTOOL_LIST_SELECTED_BACKGROUND = "#1d2942";
|
|
197
|
+
const DEVTOOL_DETAIL_BASE_BACKGROUND = "#0d1729";
|
|
198
|
+
const DEVTOOL_DETAIL_HEADER_BACKGROUND = "#131f35";
|
|
199
|
+
const DEVTOOL_SECTION_BACKGROUND = "#1e293b";
|
|
200
|
+
const DEVTOOL_SECTION_STRING_BACKGROUND = "#0d131f";
|
|
201
|
+
const DEVTOOL_PANEL_BORDER = "#1e293b";
|
|
202
|
+
const DEVTOOL_PANEL_DIVIDER_BORDER = "#1d3352";
|
|
203
|
+
const DEVTOOL_TOOLTIP_BACKGROUND = "#0c1526";
|
|
204
|
+
const DEVTOOL_JSON_KEY = "#a5b4fc";
|
|
205
|
+
const DEVTOOL_JSON_STRING = "#fbbf24";
|
|
206
|
+
const DEVTOOL_JSON_NUMBER = "#34d399";
|
|
207
|
+
const DEVTOOL_JSON_KEYWORD = "#a78bfa";
|
|
208
|
+
const DEVTOOL_JSON_PUNCTUATION = "#475569";
|
|
209
|
+
const MONO_FONT = "ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace";
|
|
210
|
+
const SANS_FONT = "ui-sans-serif, system-ui, sans-serif";
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region ../nice-devtools-shared/src/format.ts
|
|
213
|
+
/** Pretty-print any value to JSON, degrading gracefully for cyclic / non-JSON values. */
|
|
214
|
+
function safeStringify(value, indent = 2) {
|
|
215
|
+
if (value === void 0) return "undefined";
|
|
216
|
+
if (value === null) return "null";
|
|
217
|
+
try {
|
|
218
|
+
return JSON.stringify(value, null, indent);
|
|
219
|
+
} catch {
|
|
220
|
+
return String(value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** Wall-clock time of day, e.g. `14:03:09`. */
|
|
224
|
+
function formatTimestamp(ms) {
|
|
225
|
+
return new Date(ms).toLocaleTimeString([], {
|
|
226
|
+
hour: "2-digit",
|
|
227
|
+
minute: "2-digit",
|
|
228
|
+
second: "2-digit",
|
|
229
|
+
hour12: false
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region ../nice-devtools-shared/src/json.tsx
|
|
234
|
+
const JSON_TOKEN_RE = /("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b|\bnull\b|\bundefined\b)|([{}[\],])/g;
|
|
235
|
+
/** Tokenize a JSON string into coloured <span> nodes for inline rendering. */
|
|
236
|
+
function renderColoredJson(text) {
|
|
237
|
+
const nodes = [];
|
|
238
|
+
let last = 0;
|
|
239
|
+
let i = 0;
|
|
240
|
+
JSON_TOKEN_RE.lastIndex = 0;
|
|
241
|
+
for (let m = JSON_TOKEN_RE.exec(text); m !== null; m = JSON_TOKEN_RE.exec(text)) {
|
|
242
|
+
if (m.index > last) nodes.push(text.slice(last, m.index));
|
|
243
|
+
const [, str, colon, num, kw, punct] = m;
|
|
244
|
+
if (str != null) if (colon != null) {
|
|
245
|
+
nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
246
|
+
style: { color: DEVTOOL_JSON_KEY },
|
|
247
|
+
children: str
|
|
248
|
+
}, i++));
|
|
249
|
+
nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
250
|
+
style: { color: DEVTOOL_JSON_PUNCTUATION },
|
|
251
|
+
children: colon
|
|
252
|
+
}, i++));
|
|
253
|
+
} else nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
254
|
+
style: { color: DEVTOOL_JSON_STRING },
|
|
255
|
+
children: str
|
|
256
|
+
}, i++));
|
|
257
|
+
else if (num != null) nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
258
|
+
style: { color: DEVTOOL_JSON_NUMBER },
|
|
259
|
+
children: num
|
|
260
|
+
}, i++));
|
|
261
|
+
else if (kw != null) nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
262
|
+
style: { color: DEVTOOL_JSON_KEYWORD },
|
|
263
|
+
children: kw
|
|
264
|
+
}, i++));
|
|
265
|
+
else if (punct != null) nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
266
|
+
style: { color: DEVTOOL_JSON_PUNCTUATION },
|
|
267
|
+
children: punct
|
|
268
|
+
}, i++));
|
|
269
|
+
last = JSON_TOKEN_RE.lastIndex;
|
|
270
|
+
}
|
|
271
|
+
if (last < text.length) nodes.push(text.slice(last));
|
|
272
|
+
return nodes;
|
|
273
|
+
}
|
|
274
|
+
/** A pre-formatted, syntax-highlighted JSON block. */
|
|
275
|
+
function JsonView({ value, indent = 2, style }) {
|
|
276
|
+
const text = safeStringify(value, indent);
|
|
277
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
278
|
+
style: {
|
|
279
|
+
margin: 0,
|
|
280
|
+
padding: "8px 10px",
|
|
281
|
+
borderRadius: "4px",
|
|
282
|
+
fontSize: "11px",
|
|
283
|
+
lineHeight: 1.5,
|
|
284
|
+
fontFamily: MONO_FONT,
|
|
285
|
+
background: DEVTOOL_SECTION_STRING_BACKGROUND,
|
|
286
|
+
overflowX: "auto",
|
|
287
|
+
whiteSpace: "pre-wrap",
|
|
288
|
+
wordBreak: "break-word",
|
|
289
|
+
...style
|
|
290
|
+
},
|
|
291
|
+
children: renderColoredJson(text)
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region ../nice-devtools-shared/src/dock.ts
|
|
296
|
+
const GLOBAL_KEY = "__NICE_DEVTOOLS_DOCK__";
|
|
297
|
+
const VERSION = 4;
|
|
298
|
+
function createCoordinator() {
|
|
299
|
+
const panels = /* @__PURE__ */ new Map();
|
|
300
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
301
|
+
function toRef(panel) {
|
|
302
|
+
return {
|
|
303
|
+
id: panel.id,
|
|
304
|
+
label: panel.label,
|
|
305
|
+
icon: panel.icon,
|
|
306
|
+
badge: panel.badge,
|
|
307
|
+
onOpen: panel.onOpen
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function applyBodyMargins() {
|
|
311
|
+
if (typeof document === "undefined") return;
|
|
312
|
+
const margins = {
|
|
313
|
+
top: 0,
|
|
314
|
+
bottom: 0,
|
|
315
|
+
left: 0,
|
|
316
|
+
right: 0
|
|
317
|
+
};
|
|
318
|
+
for (const panel of panels.values()) if (panel.open) margins[panel.side] += panel.size;
|
|
319
|
+
for (const side of [
|
|
320
|
+
"top",
|
|
321
|
+
"bottom",
|
|
322
|
+
"left",
|
|
323
|
+
"right"
|
|
324
|
+
]) if (margins[side] > 0) document.body.style.setProperty(`margin-${side}`, `${margins[side]}px`);
|
|
325
|
+
else document.body.style.removeProperty(`margin-${side}`);
|
|
326
|
+
}
|
|
327
|
+
function notify() {
|
|
328
|
+
applyBodyMargins();
|
|
329
|
+
for (const listener of listeners) listener();
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
version: VERSION,
|
|
333
|
+
register(panel) {
|
|
334
|
+
panels.set(panel.id, { ...panel });
|
|
335
|
+
notify();
|
|
336
|
+
return () => {
|
|
337
|
+
panels.delete(panel.id);
|
|
338
|
+
notify();
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
update(id, next) {
|
|
342
|
+
const existing = panels.get(id);
|
|
343
|
+
if (existing == null) return;
|
|
344
|
+
if (existing.side === next.side && existing.size === next.size && existing.open === next.open && existing.badge === next.badge) return;
|
|
345
|
+
panels.set(id, {
|
|
346
|
+
...existing,
|
|
347
|
+
...next
|
|
348
|
+
});
|
|
349
|
+
notify();
|
|
350
|
+
},
|
|
351
|
+
getView(id) {
|
|
352
|
+
const list = [...panels.values()];
|
|
353
|
+
const anyOpen = list.some((p) => p.open);
|
|
354
|
+
const firstId = list.length > 0 ? list[0].id : null;
|
|
355
|
+
let dockOffset = 0;
|
|
356
|
+
let stacked = false;
|
|
357
|
+
const self = panels.get(id);
|
|
358
|
+
if (self != null && self.open) {
|
|
359
|
+
let seenSelf = false;
|
|
360
|
+
for (const panel of list) {
|
|
361
|
+
if (panel.id === id) {
|
|
362
|
+
seenSelf = true;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (panel.open && panel.side === self.side) {
|
|
366
|
+
stacked = true;
|
|
367
|
+
if (!seenSelf) dockOffset += panel.size;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
dockOffset,
|
|
373
|
+
stacked,
|
|
374
|
+
anyOpen,
|
|
375
|
+
isPrimary: id === firstId,
|
|
376
|
+
devtools: list.map(toRef),
|
|
377
|
+
otherClosed: list.filter((p) => !p.open && p.id !== id).map(toRef)
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
subscribe(listener) {
|
|
381
|
+
listeners.add(listener);
|
|
382
|
+
return () => {
|
|
383
|
+
listeners.delete(listener);
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Returns the page-wide dock coordinator, installing it on `window` the first
|
|
390
|
+
* time it is requested. On the server (no `window`) a throwaway instance is
|
|
391
|
+
* returned so callers can use it unconditionally.
|
|
392
|
+
*/
|
|
393
|
+
function getDevtoolsDockCoordinator() {
|
|
394
|
+
if (typeof window === "undefined") return createCoordinator();
|
|
395
|
+
const host = window;
|
|
396
|
+
const existing = host[GLOBAL_KEY];
|
|
397
|
+
if (existing != null && existing.version === VERSION) return existing;
|
|
398
|
+
const created = createCoordinator();
|
|
399
|
+
host[GLOBAL_KEY] = created;
|
|
400
|
+
return created;
|
|
401
|
+
}
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region ../nice-devtools-shared/src/list.ts
|
|
404
|
+
/**
|
|
405
|
+
* Pin a virtualized list's viewport to a stable anchor row across list
|
|
406
|
+
* mutations. Both devtools timelines are newest-first, so new entries (and
|
|
407
|
+
* merges) prepend at the top; without this they shove whatever the user is
|
|
408
|
+
* looking at downward. The anchor is the selected row when one is visible,
|
|
409
|
+
* otherwise the first visible row, so "the thing I selected / am reading" stays
|
|
410
|
+
* put at the same screen position. Returns an `onScroll` handler that must be
|
|
411
|
+
* wired to the scroll container.
|
|
412
|
+
*
|
|
413
|
+
* `hoveringRef` is optional. When provided, the hook keeps the viewport still
|
|
414
|
+
* even across selection changes while the cursor is over the list — the caller
|
|
415
|
+
* is expected to suppress its own scroll-into-view in that case and let this
|
|
416
|
+
* hold the view rock-still (nice-action's pause-on-hover behaviour). When
|
|
417
|
+
* omitted, a selection change yields to the caller's scroll-into-view effect.
|
|
418
|
+
*/
|
|
419
|
+
function useListScrollAnchor({ containerRef, virtualizer, itemKeys, selectedKey, hoveringRef }) {
|
|
420
|
+
const anchorRef = (0, react.useRef)(null);
|
|
421
|
+
const selectedKeyRef = (0, react.useRef)(selectedKey);
|
|
422
|
+
selectedKeyRef.current = selectedKey;
|
|
423
|
+
const captureAnchor = (0, react.useCallback)(() => {
|
|
424
|
+
const el = containerRef.current;
|
|
425
|
+
if (el == null) return;
|
|
426
|
+
const items = virtualizer.getVirtualItems();
|
|
427
|
+
if (items.length === 0) {
|
|
428
|
+
anchorRef.current = null;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const scrollTop = el.scrollTop;
|
|
432
|
+
const sel = selectedKeyRef.current;
|
|
433
|
+
const chosen = (sel != null ? items.find((vi) => String(vi.key) === sel) : void 0) ?? items.find((vi) => vi.end > scrollTop) ?? items[0];
|
|
434
|
+
anchorRef.current = {
|
|
435
|
+
key: String(chosen.key),
|
|
436
|
+
delta: chosen.start - scrollTop
|
|
437
|
+
};
|
|
438
|
+
}, [containerRef, virtualizer]);
|
|
439
|
+
const prevSelectedRef = (0, react.useRef)(selectedKey);
|
|
440
|
+
(0, react.useLayoutEffect)(() => {
|
|
441
|
+
const selectionChanged = prevSelectedRef.current !== selectedKey;
|
|
442
|
+
prevSelectedRef.current = selectedKey;
|
|
443
|
+
if (selectionChanged && !(hoveringRef?.current ?? false)) return;
|
|
444
|
+
const anchor = anchorRef.current;
|
|
445
|
+
if (anchor == null) return;
|
|
446
|
+
const index = itemKeys.indexOf(anchor.key);
|
|
447
|
+
if (index < 0) return;
|
|
448
|
+
const offset = virtualizer.getOffsetForIndex(index, "start")?.[0];
|
|
449
|
+
if (offset == null) return;
|
|
450
|
+
const target = Math.max(0, offset - anchor.delta);
|
|
451
|
+
const current = virtualizer.scrollOffset ?? 0;
|
|
452
|
+
if (Math.abs(target - current) > 1) virtualizer.scrollToOffset(target);
|
|
453
|
+
}, [
|
|
454
|
+
itemKeys,
|
|
455
|
+
selectedKey,
|
|
456
|
+
virtualizer,
|
|
457
|
+
hoveringRef
|
|
458
|
+
]);
|
|
459
|
+
return captureAnchor;
|
|
460
|
+
}
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region ../nice-devtools-shared/src/components/SectionLabel.tsx
|
|
463
|
+
/** Small uppercase heading used above a detail/section block. */
|
|
464
|
+
function SectionLabel({ label, color = DEVTOOL_COLOR_SEMANTIC_SYSTEM }) {
|
|
465
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
466
|
+
style: {
|
|
467
|
+
color,
|
|
468
|
+
fontSize: "0.85em",
|
|
469
|
+
marginBottom: "3px",
|
|
470
|
+
textTransform: "uppercase",
|
|
471
|
+
letterSpacing: "0.05em",
|
|
472
|
+
fontWeight: 500,
|
|
473
|
+
textAlign: "left"
|
|
474
|
+
},
|
|
475
|
+
children: label
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region ../nice-devtools-shared/src/components/PanelChrome.tsx
|
|
480
|
+
const DOCKED_SIZE_MIN = 140;
|
|
481
|
+
const POSITION_GRID = [
|
|
482
|
+
{
|
|
483
|
+
key: "tl",
|
|
484
|
+
pos: null
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
key: "tc",
|
|
488
|
+
pos: "dock-top"
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
key: "tr",
|
|
492
|
+
pos: null
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
key: "ml",
|
|
496
|
+
pos: "dock-left"
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
key: "mc",
|
|
500
|
+
pos: null
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
key: "mr",
|
|
504
|
+
pos: "dock-right"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
key: "bl",
|
|
508
|
+
pos: null
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
key: "bc",
|
|
512
|
+
pos: "dock-bottom"
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
key: "br",
|
|
516
|
+
pos: null
|
|
517
|
+
}
|
|
518
|
+
];
|
|
519
|
+
function getDockSide(pos) {
|
|
520
|
+
switch (pos) {
|
|
521
|
+
case "dock-top": return "top";
|
|
522
|
+
case "dock-left": return "left";
|
|
523
|
+
case "dock-right": return "right";
|
|
524
|
+
default: return "bottom";
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function chromeButtonStyle(color) {
|
|
528
|
+
return {
|
|
529
|
+
background: "none",
|
|
530
|
+
border: "none",
|
|
531
|
+
color,
|
|
532
|
+
cursor: "pointer",
|
|
533
|
+
fontSize: "11px",
|
|
534
|
+
padding: 0,
|
|
535
|
+
fontFamily: SANS_FONT,
|
|
536
|
+
whiteSpace: "nowrap"
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* The panel's top chrome: a brand/title on the left (plus pills to open any other
|
|
541
|
+
* closed devtools), and the panel controls on the right. The controls column
|
|
542
|
+
* holds any caller-provided `children` (e.g. a mode switch) above a row of the
|
|
543
|
+
* pause/clear actions, then the dock-position picker and the close button.
|
|
544
|
+
*
|
|
545
|
+
* `onTogglePause` is optional — when omitted no pause control is shown (the
|
|
546
|
+
* nice-action timeline has nothing to pause), keeping a single header component
|
|
547
|
+
* consistent across both devtools suites.
|
|
548
|
+
*/
|
|
549
|
+
function PanelHeader({ title, position, onPositionChange, onClose, onClear, paused, onTogglePause, openOthers, children }) {
|
|
550
|
+
const hasActionsRow = onTogglePause != null || onClear != null;
|
|
551
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
552
|
+
style: {
|
|
553
|
+
display: "flex",
|
|
554
|
+
alignItems: "center",
|
|
555
|
+
justifyContent: "space-between",
|
|
556
|
+
padding: "6px 11px",
|
|
557
|
+
gap: "10px",
|
|
558
|
+
background: DEVTOOL_SECTION_BACKGROUND,
|
|
559
|
+
borderBottom: `1px solid ${DEVTOOL_LIST_BASE_BACKGROUND}`,
|
|
560
|
+
flexShrink: 0
|
|
561
|
+
},
|
|
562
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
563
|
+
style: {
|
|
564
|
+
display: "flex",
|
|
565
|
+
alignItems: "center",
|
|
566
|
+
gap: "8px",
|
|
567
|
+
minWidth: 0
|
|
568
|
+
},
|
|
569
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
570
|
+
style: {
|
|
571
|
+
color: DEVTOOL_COLOR_SEMANTIC_SYSTEM,
|
|
572
|
+
fontWeight: "bold",
|
|
573
|
+
fontSize: "11px",
|
|
574
|
+
whiteSpace: "nowrap"
|
|
575
|
+
},
|
|
576
|
+
children: title
|
|
577
|
+
}), openOthers?.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
578
|
+
onClick: item.onOpen,
|
|
579
|
+
title: `Open ${item.label} devtools`,
|
|
580
|
+
style: {
|
|
581
|
+
display: "flex",
|
|
582
|
+
alignItems: "center",
|
|
583
|
+
gap: "4px",
|
|
584
|
+
background: DEVTOOL_LIST_BASE_BACKGROUND,
|
|
585
|
+
border: `1px solid ${DEVTOOL_COLOR_TEXT_FAINT}`,
|
|
586
|
+
borderRadius: "999px",
|
|
587
|
+
color: DEVTOOL_COLOR_TEXT_MUTED,
|
|
588
|
+
cursor: "pointer",
|
|
589
|
+
fontSize: "10px",
|
|
590
|
+
padding: "1px 7px 1px 6px",
|
|
591
|
+
fontFamily: SANS_FONT,
|
|
592
|
+
whiteSpace: "nowrap"
|
|
593
|
+
},
|
|
594
|
+
children: [
|
|
595
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.icon }),
|
|
596
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.label }),
|
|
597
|
+
item.badge != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
598
|
+
style: { color: "#38BDF8" },
|
|
599
|
+
children: item.badge
|
|
600
|
+
})
|
|
601
|
+
]
|
|
602
|
+
}, item.id))]
|
|
603
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
604
|
+
style: {
|
|
605
|
+
display: "flex",
|
|
606
|
+
gap: "10px",
|
|
607
|
+
alignItems: "center"
|
|
608
|
+
},
|
|
609
|
+
children: [
|
|
610
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
611
|
+
style: {
|
|
612
|
+
display: "flex",
|
|
613
|
+
flexDirection: "column",
|
|
614
|
+
alignItems: "stretch"
|
|
615
|
+
},
|
|
616
|
+
children: [children, hasActionsRow && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
617
|
+
style: {
|
|
618
|
+
display: "flex",
|
|
619
|
+
gap: "10px",
|
|
620
|
+
alignItems: "center",
|
|
621
|
+
justifyContent: "space-between",
|
|
622
|
+
padding: "3px"
|
|
623
|
+
},
|
|
624
|
+
children: [onTogglePause != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
625
|
+
onClick: onTogglePause,
|
|
626
|
+
title: paused ? "Resume recording" : "Pause recording",
|
|
627
|
+
style: chromeButtonStyle(paused ? "#38BDF8" : "#64748b"),
|
|
628
|
+
children: paused ? "▶ resume" : "⏸ pause"
|
|
629
|
+
}), onClear != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
630
|
+
onClick: onClear,
|
|
631
|
+
style: chromeButtonStyle("#64748b"),
|
|
632
|
+
children: "clear"
|
|
633
|
+
})]
|
|
634
|
+
})]
|
|
635
|
+
}),
|
|
636
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PositionPicker, {
|
|
637
|
+
position,
|
|
638
|
+
onChange: onPositionChange
|
|
639
|
+
}),
|
|
640
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
641
|
+
onClick: onClose,
|
|
642
|
+
style: {
|
|
643
|
+
...chromeButtonStyle(DEVTOOL_COLOR_TEXT_MUTED),
|
|
644
|
+
fontSize: "16px",
|
|
645
|
+
lineHeight: "1"
|
|
646
|
+
},
|
|
647
|
+
children: "×"
|
|
648
|
+
})
|
|
649
|
+
]
|
|
650
|
+
})]
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
function PositionPicker({ position, onChange }) {
|
|
654
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
655
|
+
title: "Move / dock panel",
|
|
656
|
+
style: {
|
|
657
|
+
display: "grid",
|
|
658
|
+
gridTemplateColumns: "repeat(3, 9px)",
|
|
659
|
+
gap: "2px",
|
|
660
|
+
padding: "2px"
|
|
661
|
+
},
|
|
662
|
+
children: POSITION_GRID.map(({ key, pos }) => {
|
|
663
|
+
if (pos == null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
|
|
664
|
+
width: "9px",
|
|
665
|
+
height: "9px"
|
|
666
|
+
} }, key);
|
|
667
|
+
const isTopBottom = pos === "dock-top" || pos === "dock-bottom";
|
|
668
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
669
|
+
title: pos,
|
|
670
|
+
onClick: () => onChange(pos),
|
|
671
|
+
style: {
|
|
672
|
+
width: "9px",
|
|
673
|
+
height: "9px",
|
|
674
|
+
display: "flex",
|
|
675
|
+
alignItems: "center",
|
|
676
|
+
justifyContent: "center",
|
|
677
|
+
cursor: "pointer"
|
|
678
|
+
},
|
|
679
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
|
|
680
|
+
width: isTopBottom ? "9px" : "3px",
|
|
681
|
+
height: isTopBottom ? "3px" : "9px",
|
|
682
|
+
borderRadius: "1px",
|
|
683
|
+
background: pos === position ? DEVTOOL_COLOR_SEMANTIC_SYSTEM : DEVTOOL_COLOR_TEXT_FAINT
|
|
684
|
+
} })
|
|
685
|
+
}, key);
|
|
686
|
+
})
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function ResizeHandle({ dockSide, dockedSize, onChange }) {
|
|
690
|
+
const isHoriz = dockSide === "left" || dockSide === "right";
|
|
691
|
+
const onMouseDown = (e) => {
|
|
692
|
+
e.preventDefault();
|
|
693
|
+
const startCoord = isHoriz ? e.clientX : e.clientY;
|
|
694
|
+
const startSize = dockedSize;
|
|
695
|
+
const maxSize = isHoriz ? window.innerWidth * .85 : window.innerHeight * .85;
|
|
696
|
+
const sign = dockSide === "bottom" || dockSide === "right" ? -1 : 1;
|
|
697
|
+
const onMove = (me) => {
|
|
698
|
+
const delta = (isHoriz ? me.clientX : me.clientY) - startCoord;
|
|
699
|
+
onChange(Math.max(DOCKED_SIZE_MIN, Math.min(maxSize, startSize + sign * delta)));
|
|
700
|
+
};
|
|
701
|
+
const onUp = () => {
|
|
702
|
+
window.removeEventListener("mousemove", onMove);
|
|
703
|
+
window.removeEventListener("mouseup", onUp);
|
|
704
|
+
};
|
|
705
|
+
window.addEventListener("mousemove", onMove);
|
|
706
|
+
window.addEventListener("mouseup", onUp);
|
|
707
|
+
};
|
|
708
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
709
|
+
onMouseDown,
|
|
710
|
+
style: {
|
|
711
|
+
position: "absolute",
|
|
712
|
+
zIndex: 10,
|
|
713
|
+
background: "transparent",
|
|
714
|
+
...dockSide === "bottom" ? {
|
|
715
|
+
top: 0,
|
|
716
|
+
left: 0,
|
|
717
|
+
right: 0,
|
|
718
|
+
height: "5px",
|
|
719
|
+
cursor: "ns-resize"
|
|
720
|
+
} : dockSide === "top" ? {
|
|
721
|
+
bottom: 0,
|
|
722
|
+
left: 0,
|
|
723
|
+
right: 0,
|
|
724
|
+
height: "5px",
|
|
725
|
+
cursor: "ns-resize"
|
|
726
|
+
} : dockSide === "right" ? {
|
|
727
|
+
top: 0,
|
|
728
|
+
bottom: 0,
|
|
729
|
+
left: 0,
|
|
730
|
+
width: "5px",
|
|
731
|
+
cursor: "ew-resize"
|
|
732
|
+
} : {
|
|
733
|
+
top: 0,
|
|
734
|
+
bottom: 0,
|
|
735
|
+
right: 0,
|
|
736
|
+
width: "5px",
|
|
737
|
+
cursor: "ew-resize"
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
const SPLIT_RATIO_MIN = .15;
|
|
743
|
+
const SPLIT_RATIO_MAX = .85;
|
|
744
|
+
/**
|
|
745
|
+
* Draggable divider between the list and the detail pane. `horizontal` refers to
|
|
746
|
+
* the split axis: a row layout (dock top/bottom) splits horizontally and drags
|
|
747
|
+
* left/right; a column layout (dock left/right) splits vertically. The reported
|
|
748
|
+
* ratio is the fraction of the container the *detail* pane should occupy.
|
|
749
|
+
*/
|
|
750
|
+
function SplitHandle({ horizontal, onRatioChange }) {
|
|
751
|
+
const [hovered, setHovered] = (0, react.useState)(false);
|
|
752
|
+
const onMouseDown = (e) => {
|
|
753
|
+
e.preventDefault();
|
|
754
|
+
const container = e.currentTarget.parentElement;
|
|
755
|
+
if (container == null) return;
|
|
756
|
+
const onMove = (me) => {
|
|
757
|
+
const rect = container.getBoundingClientRect();
|
|
758
|
+
const ratio = horizontal ? (rect.right - me.clientX) / rect.width : (rect.bottom - me.clientY) / rect.height;
|
|
759
|
+
onRatioChange(Math.max(SPLIT_RATIO_MIN, Math.min(SPLIT_RATIO_MAX, ratio)));
|
|
760
|
+
};
|
|
761
|
+
const onUp = () => {
|
|
762
|
+
window.removeEventListener("mousemove", onMove);
|
|
763
|
+
window.removeEventListener("mouseup", onUp);
|
|
764
|
+
};
|
|
765
|
+
window.addEventListener("mousemove", onMove);
|
|
766
|
+
window.addEventListener("mouseup", onUp);
|
|
767
|
+
};
|
|
768
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
769
|
+
onMouseDown,
|
|
770
|
+
onMouseEnter: () => setHovered(true),
|
|
771
|
+
onMouseLeave: () => setHovered(false),
|
|
772
|
+
style: {
|
|
773
|
+
flex: "0 0 5px",
|
|
774
|
+
alignSelf: "stretch",
|
|
775
|
+
cursor: horizontal ? "ew-resize" : "ns-resize",
|
|
776
|
+
background: hovered ? DEVTOOL_COLOR_SEMANTIC_SYSTEM : "transparent",
|
|
777
|
+
opacity: hovered ? .6 : 1,
|
|
778
|
+
zIndex: 5
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
/** A compact segmented toggle (e.g. the nice-state Timeline / State switch). */
|
|
783
|
+
function SegmentedControl({ options, value, onChange }) {
|
|
784
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
785
|
+
style: {
|
|
786
|
+
display: "flex",
|
|
787
|
+
border: `1px solid ${DEVTOOL_COLOR_TEXT_FAINT}`,
|
|
788
|
+
borderRadius: "5px",
|
|
789
|
+
overflow: "hidden"
|
|
790
|
+
},
|
|
791
|
+
children: options.map((opt) => {
|
|
792
|
+
const active = opt.value === value;
|
|
793
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
794
|
+
onClick: () => onChange(opt.value),
|
|
795
|
+
style: {
|
|
796
|
+
background: active ? DEVTOOL_COLOR_SEMANTIC_SYSTEM : "transparent",
|
|
797
|
+
color: active ? "#0f172a" : DEVTOOL_COLOR_TEXT_MUTED,
|
|
798
|
+
border: "none",
|
|
799
|
+
cursor: "pointer",
|
|
800
|
+
fontSize: "10px",
|
|
801
|
+
fontWeight: active ? 700 : 500,
|
|
802
|
+
padding: "3px 9px",
|
|
803
|
+
fontFamily: SANS_FONT
|
|
804
|
+
},
|
|
805
|
+
children: opt.label
|
|
806
|
+
}, opt.value);
|
|
807
|
+
})
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* The combined, page-wide launcher shown while every devtool is collapsed — one
|
|
812
|
+
* grouped pill with a segment per registered devtool, so the buttons never
|
|
813
|
+
* overlap or hide behind each other. Rendered by the coordinator's "primary"
|
|
814
|
+
* devtool only.
|
|
815
|
+
*/
|
|
816
|
+
function DevtoolsLauncher({ items }) {
|
|
817
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
818
|
+
style: {
|
|
819
|
+
position: "fixed",
|
|
820
|
+
bottom: "16px",
|
|
821
|
+
right: "16px",
|
|
822
|
+
zIndex: 2147483647,
|
|
823
|
+
display: "flex",
|
|
824
|
+
fontFamily: MONO_FONT,
|
|
825
|
+
fontSize: "12px"
|
|
826
|
+
},
|
|
827
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
828
|
+
style: {
|
|
829
|
+
display: "flex",
|
|
830
|
+
background: DEVTOOL_SECTION_BACKGROUND,
|
|
831
|
+
border: `1px solid ${DEVTOOL_COLOR_TEXT_FAINT}`,
|
|
832
|
+
borderRadius: "6px",
|
|
833
|
+
overflow: "hidden",
|
|
834
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.35)"
|
|
835
|
+
},
|
|
836
|
+
children: items.map((item, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
837
|
+
onClick: item.onOpen,
|
|
838
|
+
style: {
|
|
839
|
+
display: "flex",
|
|
840
|
+
alignItems: "center",
|
|
841
|
+
gap: "5px",
|
|
842
|
+
background: "transparent",
|
|
843
|
+
color: DEVTOOL_COLOR_TEXT_SECONDARY,
|
|
844
|
+
border: "none",
|
|
845
|
+
borderLeft: i > 0 ? `1px solid ${DEVTOOL_COLOR_TEXT_FAINT}` : "none",
|
|
846
|
+
cursor: "pointer",
|
|
847
|
+
padding: "6px 11px",
|
|
848
|
+
fontFamily: MONO_FONT,
|
|
849
|
+
fontSize: "12px",
|
|
850
|
+
lineHeight: "1.5"
|
|
851
|
+
},
|
|
852
|
+
children: [
|
|
853
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.icon }),
|
|
854
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.label }),
|
|
855
|
+
item.badge != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
856
|
+
style: { color: "#38BDF8" },
|
|
857
|
+
children: item.badge
|
|
858
|
+
})
|
|
859
|
+
]
|
|
860
|
+
}, item.id))
|
|
861
|
+
})
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
//#endregion
|
|
865
|
+
//#region ../nice-devtools-shared/src/components/FollowLatestToggles.tsx
|
|
866
|
+
/**
|
|
867
|
+
* The "Follow latest" toggle pair shown above a devtools timeline list, with its
|
|
868
|
+
* indented "clicking latest re-follows" sub-option. `noun` is the thing the
|
|
869
|
+
* timeline tracks ("action" in nice-action, "change" in nice-state) and is woven
|
|
870
|
+
* into the explanatory tooltips so both suites read naturally from one component.
|
|
871
|
+
*/
|
|
872
|
+
function FollowLatestToggles({ noun, stayOnLatest, onStayOnLatestChange, followLatestOnSelect, onFollowLatestOnSelectChange }) {
|
|
873
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
874
|
+
style: {
|
|
875
|
+
display: "flex",
|
|
876
|
+
flexDirection: "column",
|
|
877
|
+
flexShrink: 0,
|
|
878
|
+
paddingBottom: "3px",
|
|
879
|
+
background: DEVTOOL_SECTION_BACKGROUND,
|
|
880
|
+
borderBottom: `1px solid ${DEVTOOL_LIST_BASE_BACKGROUND}`
|
|
881
|
+
},
|
|
882
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleLabel, {
|
|
883
|
+
title: `Auto-select the most recent ${noun} so the detail pane keeps showing the latest as new ${noun}s land`,
|
|
884
|
+
checked: stayOnLatest,
|
|
885
|
+
onChange: onStayOnLatestChange,
|
|
886
|
+
children: "Follow latest"
|
|
887
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
888
|
+
style: {
|
|
889
|
+
display: "flex",
|
|
890
|
+
alignItems: "center",
|
|
891
|
+
paddingLeft: "12px",
|
|
892
|
+
marginTop: "-4px"
|
|
893
|
+
},
|
|
894
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
895
|
+
"aria-hidden": true,
|
|
896
|
+
style: {
|
|
897
|
+
color: DEVTOOL_COLOR_TEXT_MUTED,
|
|
898
|
+
fontFamily: SANS_FONT,
|
|
899
|
+
fontSize: "10px",
|
|
900
|
+
lineHeight: 1
|
|
901
|
+
},
|
|
902
|
+
children: "└"
|
|
903
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleLabel, {
|
|
904
|
+
title: `When you click the latest ${noun}, turn 'Follow latest' back on so the view resumes tracking new ${noun}s. Turn this off to pin exactly to the ${noun} you click instead.`,
|
|
905
|
+
checked: followLatestOnSelect,
|
|
906
|
+
onChange: onFollowLatestOnSelectChange,
|
|
907
|
+
children: "clicking latest re-follows"
|
|
908
|
+
})]
|
|
909
|
+
})]
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
function ToggleLabel({ checked, onChange, title, children }) {
|
|
913
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
914
|
+
title,
|
|
915
|
+
style: {
|
|
916
|
+
display: "flex",
|
|
917
|
+
alignItems: "center",
|
|
918
|
+
gap: "6px",
|
|
919
|
+
padding: "5px 10px",
|
|
920
|
+
cursor: "pointer",
|
|
921
|
+
userSelect: "none",
|
|
922
|
+
color: checked ? DEVTOOL_COLOR_TEXT_SECONDARY : DEVTOOL_COLOR_TEXT_MUTED,
|
|
923
|
+
fontSize: "10px",
|
|
924
|
+
fontFamily: SANS_FONT
|
|
925
|
+
},
|
|
926
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
927
|
+
type: "checkbox",
|
|
928
|
+
checked,
|
|
929
|
+
onChange: (e) => onChange(e.target.checked),
|
|
930
|
+
style: {
|
|
931
|
+
accentColor: DEVTOOL_COLOR_SEMANTIC_SYSTEM,
|
|
932
|
+
cursor: "pointer",
|
|
933
|
+
margin: 0
|
|
934
|
+
}
|
|
935
|
+
}), children]
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
//#endregion
|
|
939
|
+
//#region ../nice-devtools-shared/src/components/VirtualList.tsx
|
|
940
|
+
/**
|
|
941
|
+
* The shared, virtualized timeline list used by both the nice-action and
|
|
942
|
+
* nice-state devtools. It owns everything about *how the list behaves* — windowed
|
|
943
|
+
* rendering, viewport anchoring across prepends, scroll-the-selection-into-view,
|
|
944
|
+
* and pause-on-hover with a "N new" catch-up hint — while each devtool supplies
|
|
945
|
+
* its own `renderItem` for the row content (which stays unique per devtool).
|
|
946
|
+
*
|
|
947
|
+
* Behaviour notes:
|
|
948
|
+
* • The list is assumed newest-first: new rows prepend at the top. While the
|
|
949
|
+
* cursor is over the list the viewport is held perfectly still (so reading
|
|
950
|
+
* isn't disrupted); fresh rows still render above and a sticky "↑ N new"
|
|
951
|
+
* badge hints they're there. Move the cursor away and normal follow resumes.
|
|
952
|
+
* • `selectedKey` is the stable item key of the selected row (NOT necessarily a
|
|
953
|
+
* sub-selection inside it). When it changes the row is scrolled into view —
|
|
954
|
+
* unless the cursor is hovering, in which case the view is left untouched.
|
|
955
|
+
*/
|
|
956
|
+
function DevtoolsVirtualList({ items, getItemKey, renderItem, selectedKey, estimateSize, overscan = 8, rowStyle, empty, footer, style }) {
|
|
957
|
+
const containerRef = (0, react.useRef)(null);
|
|
958
|
+
const [isHovering, setIsHovering] = (0, react.useState)(false);
|
|
959
|
+
const hoveringRef = (0, react.useRef)(false);
|
|
960
|
+
hoveringRef.current = isHovering;
|
|
961
|
+
const hoverBaselineCountRef = (0, react.useRef)(0);
|
|
962
|
+
const pendingCount = isHovering ? Math.max(0, items.length - hoverBaselineCountRef.current) : 0;
|
|
963
|
+
const hasPendingActivity = isHovering && pendingCount > 0;
|
|
964
|
+
const getItemKeyRef = (0, react.useRef)(getItemKey);
|
|
965
|
+
getItemKeyRef.current = getItemKey;
|
|
966
|
+
const itemKeys = (0, react.useMemo)(() => items.map((item, i) => getItemKeyRef.current(item, i)), [items]);
|
|
967
|
+
const virtualizer = (0, _tanstack_react_virtual.useVirtualizer)({
|
|
968
|
+
count: items.length,
|
|
969
|
+
getScrollElement: () => containerRef.current,
|
|
970
|
+
estimateSize: () => estimateSize,
|
|
971
|
+
overscan,
|
|
972
|
+
getItemKey: (index) => itemKeys[index]
|
|
973
|
+
});
|
|
974
|
+
const onScroll = useListScrollAnchor({
|
|
975
|
+
containerRef,
|
|
976
|
+
virtualizer,
|
|
977
|
+
itemKeys,
|
|
978
|
+
selectedKey,
|
|
979
|
+
hoveringRef
|
|
980
|
+
});
|
|
981
|
+
const prevSelectedRef = (0, react.useRef)(selectedKey);
|
|
982
|
+
(0, react.useEffect)(() => {
|
|
983
|
+
if (selectedKey === prevSelectedRef.current) return;
|
|
984
|
+
prevSelectedRef.current = selectedKey;
|
|
985
|
+
if (selectedKey == null) return;
|
|
986
|
+
if (hoveringRef.current) return;
|
|
987
|
+
const index = itemKeys.indexOf(selectedKey);
|
|
988
|
+
if (index >= 0) virtualizer.scrollToIndex(index, { align: "auto" });
|
|
989
|
+
}, [
|
|
990
|
+
selectedKey,
|
|
991
|
+
itemKeys,
|
|
992
|
+
virtualizer
|
|
993
|
+
]);
|
|
994
|
+
if (items.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: empty });
|
|
995
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
996
|
+
ref: containerRef,
|
|
997
|
+
style,
|
|
998
|
+
onScroll,
|
|
999
|
+
onMouseEnter: () => {
|
|
1000
|
+
hoverBaselineCountRef.current = items.length;
|
|
1001
|
+
setIsHovering(true);
|
|
1002
|
+
},
|
|
1003
|
+
onMouseLeave: () => setIsHovering(false),
|
|
1004
|
+
children: [
|
|
1005
|
+
hasPendingActivity && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PendingIndicator, { count: pendingCount }),
|
|
1006
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1007
|
+
style: {
|
|
1008
|
+
position: "relative",
|
|
1009
|
+
width: "100%",
|
|
1010
|
+
height: virtualizer.getTotalSize()
|
|
1011
|
+
},
|
|
1012
|
+
children: virtualizer.getVirtualItems().map((vItem) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1013
|
+
"data-index": vItem.index,
|
|
1014
|
+
ref: virtualizer.measureElement,
|
|
1015
|
+
style: {
|
|
1016
|
+
position: "absolute",
|
|
1017
|
+
top: 0,
|
|
1018
|
+
left: 0,
|
|
1019
|
+
width: "100%",
|
|
1020
|
+
transform: `translateY(${vItem.start}px)`,
|
|
1021
|
+
...rowStyle
|
|
1022
|
+
},
|
|
1023
|
+
children: renderItem(items[vItem.index], vItem.index)
|
|
1024
|
+
}, vItem.key))
|
|
1025
|
+
}),
|
|
1026
|
+
footer
|
|
1027
|
+
]
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* A sticky, zero-height overlay floating over the rows without taking layout space. Shown only once
|
|
1032
|
+
* new items have arrived while hovering, hinting they're above (newest-first) so the user can scroll
|
|
1033
|
+
* up without the view having jumped there.
|
|
1034
|
+
*/
|
|
1035
|
+
function PendingIndicator({ count }) {
|
|
1036
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1037
|
+
style: {
|
|
1038
|
+
position: "sticky",
|
|
1039
|
+
top: 0,
|
|
1040
|
+
height: 0,
|
|
1041
|
+
zIndex: 5,
|
|
1042
|
+
pointerEvents: "none"
|
|
1043
|
+
},
|
|
1044
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1045
|
+
style: {
|
|
1046
|
+
position: "absolute",
|
|
1047
|
+
top: "6px",
|
|
1048
|
+
right: "10px",
|
|
1049
|
+
display: "flex",
|
|
1050
|
+
alignItems: "center",
|
|
1051
|
+
gap: "5px",
|
|
1052
|
+
padding: "2px 7px",
|
|
1053
|
+
borderRadius: "10px",
|
|
1054
|
+
fontSize: "10px",
|
|
1055
|
+
fontFamily: SANS_FONT,
|
|
1056
|
+
color: DEVTOOL_COLOR_SEMANTIC_WARNING,
|
|
1057
|
+
background: DEVTOOL_TOOLTIP_BACKGROUND,
|
|
1058
|
+
border: `1px solid ${DEVTOOL_COLOR_SEMANTIC_WARNING}55`,
|
|
1059
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.5)",
|
|
1060
|
+
whiteSpace: "nowrap"
|
|
1061
|
+
},
|
|
1062
|
+
children: [
|
|
1063
|
+
"↑ ",
|
|
1064
|
+
count,
|
|
1065
|
+
" new"
|
|
1066
|
+
]
|
|
1067
|
+
})
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
//#endregion
|
|
1071
|
+
//#region src/devtools/core/devtools_colors.ts
|
|
1072
|
+
const DEVTOOL_EDITOR_BACKGROUND = "#08101f";
|
|
1073
|
+
//#endregion
|
|
1074
|
+
//#region src/devtools/browser/components/utils.ts
|
|
1075
|
+
const SOURCE_LABEL = {
|
|
1076
|
+
update: "UPDATE",
|
|
1077
|
+
replace: "REPLACE",
|
|
1078
|
+
"devtools-edit": "EDIT",
|
|
1079
|
+
"devtools-revert": "REVERT"
|
|
1080
|
+
};
|
|
1081
|
+
const SOURCE_COLOR = {
|
|
1082
|
+
update: DEVTOOL_COLOR_SEMANTIC_SYSTEM,
|
|
1083
|
+
replace: DEVTOOL_COLOR_SEMANTIC_METADATA,
|
|
1084
|
+
"devtools-edit": DEVTOOL_COLOR_SEMANTIC_WARNING,
|
|
1085
|
+
"devtools-revert": DEVTOOL_COLOR_SEMANTIC_WARNING
|
|
1086
|
+
};
|
|
1087
|
+
/** Render an Immer patch path (`["todos", 0, "done"]`) as `todos.0.done`. */
|
|
1088
|
+
function patchPathToString(path) {
|
|
1089
|
+
if (path.length === 0) return "(root)";
|
|
1090
|
+
return path.map((seg) => String(seg)).join(".");
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* A terse, single-line summary of what a change touched, for the timeline row.
|
|
1094
|
+
* Prefers a `path: prev → next` form for single scalar replaces, otherwise lists
|
|
1095
|
+
* the distinct top-level paths affected.
|
|
1096
|
+
*/
|
|
1097
|
+
function summarizeChange(change) {
|
|
1098
|
+
const { patches } = change;
|
|
1099
|
+
if (patches.length === 0) return change.source === "replace" || change.source === "devtools-edit" ? "whole state replaced" : "no patches";
|
|
1100
|
+
if (patches.length === 1) {
|
|
1101
|
+
const p = patches[0];
|
|
1102
|
+
const path = patchPathToString(p.path);
|
|
1103
|
+
if (p.op === "replace" && isScalar(p.value)) return `${path} → ${formatScalar(p.value)}`;
|
|
1104
|
+
if (p.op === "add") return `+ ${path}`;
|
|
1105
|
+
if (p.op === "remove") return `− ${path}`;
|
|
1106
|
+
return path;
|
|
1107
|
+
}
|
|
1108
|
+
const paths = patches.map((p) => topLevel(p.path)).filter((s) => s.length > 0);
|
|
1109
|
+
const unique = [...new Set(paths)];
|
|
1110
|
+
const shown = unique.slice(0, 3).join(", ");
|
|
1111
|
+
return unique.length > 3 ? `${shown} +${unique.length - 3}` : shown;
|
|
1112
|
+
}
|
|
1113
|
+
function topLevel(path) {
|
|
1114
|
+
return path.length > 0 ? String(path[0]) : "(root)";
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Structural identity of a change by its *resulting* state: same store, same
|
|
1118
|
+
* snapshot. The feature collapses updates that "didn't change the state from the
|
|
1119
|
+
* previous update", so we compare the committed state itself rather than the
|
|
1120
|
+
* mutation that produced it. Keying on patches would miss the no-op cases this is
|
|
1121
|
+
* meant to catch — a fresh object reference with identical content, or a
|
|
1122
|
+
* same-value write from a different source (e.g. a devtools edit followed by an
|
|
1123
|
+
* app update). Those carry patches yet leave the state untouched, so they belong
|
|
1124
|
+
* in the run that already reached this state regardless of source.
|
|
1125
|
+
*/
|
|
1126
|
+
function changeGroupKey(change) {
|
|
1127
|
+
return `${change.storeId}|${safeStringify(change.snapshot, 0)}`;
|
|
1128
|
+
}
|
|
1129
|
+
/** Collapse consecutive structurally-equal changes (list is newest-first). */
|
|
1130
|
+
function groupChanges(changes) {
|
|
1131
|
+
const groups = [];
|
|
1132
|
+
let lastKey = null;
|
|
1133
|
+
for (const change of changes) {
|
|
1134
|
+
const key = changeGroupKey(change);
|
|
1135
|
+
const last = groups[groups.length - 1];
|
|
1136
|
+
if (last != null && key === lastKey) {
|
|
1137
|
+
last.count++;
|
|
1138
|
+
last.oldest = change;
|
|
1139
|
+
} else groups.push({
|
|
1140
|
+
representative: change,
|
|
1141
|
+
oldest: change,
|
|
1142
|
+
count: 1
|
|
1143
|
+
});
|
|
1144
|
+
lastKey = key;
|
|
1145
|
+
}
|
|
1146
|
+
return groups;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Structural diff between two snapshots, flattened to the individual leaf/branch
|
|
1150
|
+
* paths that actually changed — the data behind the test-framework-style "Diff"
|
|
1151
|
+
* view. Unchanged branches are skipped entirely (Immer's structural sharing
|
|
1152
|
+
* means equal branches keep their reference, so this stays cheap).
|
|
1153
|
+
*/
|
|
1154
|
+
function computeDiff(before, after) {
|
|
1155
|
+
const out = [];
|
|
1156
|
+
walkDiff([], before, after, out);
|
|
1157
|
+
return out;
|
|
1158
|
+
}
|
|
1159
|
+
function isPlainObjectOrArray(value) {
|
|
1160
|
+
return value != null && typeof value === "object";
|
|
1161
|
+
}
|
|
1162
|
+
function walkDiff(path, before, after, out) {
|
|
1163
|
+
if (Object.is(before, after)) return;
|
|
1164
|
+
if (isPlainObjectOrArray(before) && isPlainObjectOrArray(after) && Array.isArray(before) === Array.isArray(after)) {
|
|
1165
|
+
const b = before;
|
|
1166
|
+
const a = after;
|
|
1167
|
+
const keys = new Set([...Object.keys(b), ...Object.keys(a)]);
|
|
1168
|
+
for (const key of keys) {
|
|
1169
|
+
const inB = key in b;
|
|
1170
|
+
const inA = key in a;
|
|
1171
|
+
const next = [...path, key];
|
|
1172
|
+
if (inB && !inA) out.push({
|
|
1173
|
+
path: formatDiffPath(next),
|
|
1174
|
+
segments: next,
|
|
1175
|
+
kind: "removed",
|
|
1176
|
+
before: b[key]
|
|
1177
|
+
});
|
|
1178
|
+
else if (!inB && inA) out.push({
|
|
1179
|
+
path: formatDiffPath(next),
|
|
1180
|
+
segments: next,
|
|
1181
|
+
kind: "added",
|
|
1182
|
+
after: a[key]
|
|
1183
|
+
});
|
|
1184
|
+
else walkDiff(next, b[key], a[key], out);
|
|
1185
|
+
}
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
out.push({
|
|
1189
|
+
path: formatDiffPath(path),
|
|
1190
|
+
segments: [...path],
|
|
1191
|
+
kind: "changed",
|
|
1192
|
+
before,
|
|
1193
|
+
after
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
function formatDiffPath(path) {
|
|
1197
|
+
return path.length === 0 ? "(root)" : path.map((seg) => String(seg)).join(".");
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Classic LCS line diff between two blocks of text — the data behind the unified
|
|
1201
|
+
* git-style "Diff View" and the per-line highlighting in the Before / After
|
|
1202
|
+
* panels. Snapshots rendered as pretty JSON stay small, so the O(n·m) table is
|
|
1203
|
+
* comfortably cheap.
|
|
1204
|
+
*/
|
|
1205
|
+
function computeLineDiff(beforeText, afterText) {
|
|
1206
|
+
const a = beforeText.split("\n");
|
|
1207
|
+
const b = afterText.split("\n");
|
|
1208
|
+
const n = a.length;
|
|
1209
|
+
const m = b.length;
|
|
1210
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
1211
|
+
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
1212
|
+
const ops = [];
|
|
1213
|
+
let i = 0;
|
|
1214
|
+
let j = 0;
|
|
1215
|
+
while (i < n && j < m) if (a[i] === b[j]) {
|
|
1216
|
+
ops.push({
|
|
1217
|
+
kind: "common",
|
|
1218
|
+
text: a[i]
|
|
1219
|
+
});
|
|
1220
|
+
i++;
|
|
1221
|
+
j++;
|
|
1222
|
+
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
1223
|
+
ops.push({
|
|
1224
|
+
kind: "removed",
|
|
1225
|
+
text: a[i]
|
|
1226
|
+
});
|
|
1227
|
+
i++;
|
|
1228
|
+
} else {
|
|
1229
|
+
ops.push({
|
|
1230
|
+
kind: "added",
|
|
1231
|
+
text: b[j]
|
|
1232
|
+
});
|
|
1233
|
+
j++;
|
|
1234
|
+
}
|
|
1235
|
+
while (i < n) ops.push({
|
|
1236
|
+
kind: "removed",
|
|
1237
|
+
text: a[i++]
|
|
1238
|
+
});
|
|
1239
|
+
while (j < m) ops.push({
|
|
1240
|
+
kind: "added",
|
|
1241
|
+
text: b[j++]
|
|
1242
|
+
});
|
|
1243
|
+
return ops;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Reorder each contiguous changed hunk so additions sit above removals when
|
|
1247
|
+
* `latestFirst` is set (the new state on top). Common lines anchor the hunks in
|
|
1248
|
+
* place; only the −/+ lines within a run are regrouped, so the surrounding
|
|
1249
|
+
* context never moves. With `latestFirst` off the ops are returned untouched.
|
|
1250
|
+
*/
|
|
1251
|
+
function orderLineDiffOps(ops, latestFirst) {
|
|
1252
|
+
if (!latestFirst) return ops;
|
|
1253
|
+
const out = [];
|
|
1254
|
+
let i = 0;
|
|
1255
|
+
while (i < ops.length) {
|
|
1256
|
+
if (ops[i].kind === "common") {
|
|
1257
|
+
out.push(ops[i]);
|
|
1258
|
+
i++;
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
const added = [];
|
|
1262
|
+
const removed = [];
|
|
1263
|
+
while (i < ops.length && ops[i].kind !== "common") {
|
|
1264
|
+
(ops[i].kind === "added" ? added : removed).push(ops[i]);
|
|
1265
|
+
i++;
|
|
1266
|
+
}
|
|
1267
|
+
out.push(...added, ...removed);
|
|
1268
|
+
}
|
|
1269
|
+
return out;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* A structure-aware "address" diff: the JSON tree pruned down to just the
|
|
1273
|
+
* branches that actually changed. Parents of a change keep their brackets so the
|
|
1274
|
+
* path stays legible, and each run of untouched siblings collapses into a single
|
|
1275
|
+
* `… N unchanged …` placeholder (which, for arrays, naturally reports the counts
|
|
1276
|
+
* before and after a change). `side` picks which document we're rebuilding —
|
|
1277
|
+
* `"unified"` shows removed (−) and added (+) together, while `"before"` /
|
|
1278
|
+
* `"after"` show only the lines that belong to that one snapshot.
|
|
1279
|
+
*/
|
|
1280
|
+
function computeCompressedDiff(before, after, side, latestFirst = false) {
|
|
1281
|
+
const lines = [];
|
|
1282
|
+
emitChangedNode(lines, 0, null, before, after, side, latestFirst);
|
|
1283
|
+
return lines;
|
|
1284
|
+
}
|
|
1285
|
+
function keyPrefix(keyLabel) {
|
|
1286
|
+
return keyLabel != null ? `${keyLabel}: ` : "";
|
|
1287
|
+
}
|
|
1288
|
+
/** Emit a node already known to differ between the two sides. */
|
|
1289
|
+
function emitChangedNode(lines, depth, keyLabel, before, after, side, latestFirst) {
|
|
1290
|
+
if (isPlainObjectOrArray(before) && isPlainObjectOrArray(after) && Array.isArray(before) === Array.isArray(after)) {
|
|
1291
|
+
emitContainer(lines, depth, keyLabel, before, after, side, latestFirst);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const removed = () => pushValueLines(lines, depth, keyLabel, before, "−", "removed");
|
|
1295
|
+
const added = () => pushValueLines(lines, depth, keyLabel, after, "+", "added");
|
|
1296
|
+
if (side === "before") removed();
|
|
1297
|
+
else if (side === "after") added();
|
|
1298
|
+
else if (latestFirst) {
|
|
1299
|
+
added();
|
|
1300
|
+
removed();
|
|
1301
|
+
} else {
|
|
1302
|
+
removed();
|
|
1303
|
+
added();
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function emitContainer(lines, depth, keyLabel, before, after, side, latestFirst) {
|
|
1307
|
+
const isArray = Array.isArray(before);
|
|
1308
|
+
lines.push({
|
|
1309
|
+
depth,
|
|
1310
|
+
sign: " ",
|
|
1311
|
+
tone: "common",
|
|
1312
|
+
text: `${keyPrefix(keyLabel)}${isArray ? "[" : "{"}`
|
|
1313
|
+
});
|
|
1314
|
+
let unchanged = 0;
|
|
1315
|
+
const flush = () => {
|
|
1316
|
+
if (unchanged > 0) {
|
|
1317
|
+
lines.push({
|
|
1318
|
+
depth: depth + 1,
|
|
1319
|
+
sign: " ",
|
|
1320
|
+
tone: "placeholder",
|
|
1321
|
+
text: placeholderText(unchanged, isArray)
|
|
1322
|
+
});
|
|
1323
|
+
unchanged = 0;
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
for (const child of childEntries(before, after, isArray)) {
|
|
1327
|
+
if (child.kind === "added" && side === "before") continue;
|
|
1328
|
+
if (child.kind === "removed" && side === "after") continue;
|
|
1329
|
+
if (child.kind === "unchanged") {
|
|
1330
|
+
unchanged++;
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
flush();
|
|
1334
|
+
if (child.kind === "changed") emitChangedNode(lines, depth + 1, child.keyLabel, child.before, child.after, side, latestFirst);
|
|
1335
|
+
else if (child.kind === "added") pushValueLines(lines, depth + 1, child.keyLabel, child.after, "+", "added");
|
|
1336
|
+
else pushValueLines(lines, depth + 1, child.keyLabel, child.before, "−", "removed");
|
|
1337
|
+
}
|
|
1338
|
+
flush();
|
|
1339
|
+
lines.push({
|
|
1340
|
+
depth,
|
|
1341
|
+
sign: " ",
|
|
1342
|
+
tone: "common",
|
|
1343
|
+
text: isArray ? "]" : "}"
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
/** Classify each direct child as unchanged / changed / added / removed. */
|
|
1347
|
+
function childEntries(before, after, isArray) {
|
|
1348
|
+
if (isArray) {
|
|
1349
|
+
const b = before;
|
|
1350
|
+
const a = after;
|
|
1351
|
+
const len = Math.max(b.length, a.length);
|
|
1352
|
+
const out = [];
|
|
1353
|
+
for (let i = 0; i < len; i++) {
|
|
1354
|
+
const inB = i < b.length;
|
|
1355
|
+
const inA = i < a.length;
|
|
1356
|
+
if (inB && inA) {
|
|
1357
|
+
const kind = Object.is(b[i], a[i]) ? "unchanged" : "changed";
|
|
1358
|
+
out.push({
|
|
1359
|
+
keyLabel: null,
|
|
1360
|
+
kind,
|
|
1361
|
+
before: b[i],
|
|
1362
|
+
after: a[i]
|
|
1363
|
+
});
|
|
1364
|
+
} else if (inB) out.push({
|
|
1365
|
+
keyLabel: null,
|
|
1366
|
+
kind: "removed",
|
|
1367
|
+
before: b[i],
|
|
1368
|
+
after: void 0
|
|
1369
|
+
});
|
|
1370
|
+
else out.push({
|
|
1371
|
+
keyLabel: null,
|
|
1372
|
+
kind: "added",
|
|
1373
|
+
before: void 0,
|
|
1374
|
+
after: a[i]
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
return out;
|
|
1378
|
+
}
|
|
1379
|
+
const b = before;
|
|
1380
|
+
const a = after;
|
|
1381
|
+
return [...new Set([...Object.keys(b), ...Object.keys(a)])].map((key) => {
|
|
1382
|
+
const keyLabel = JSON.stringify(key);
|
|
1383
|
+
const inB = key in b;
|
|
1384
|
+
const inA = key in a;
|
|
1385
|
+
if (inB && inA) return {
|
|
1386
|
+
keyLabel,
|
|
1387
|
+
kind: Object.is(b[key], a[key]) ? "unchanged" : "changed",
|
|
1388
|
+
before: b[key],
|
|
1389
|
+
after: a[key]
|
|
1390
|
+
};
|
|
1391
|
+
if (inB) return {
|
|
1392
|
+
keyLabel,
|
|
1393
|
+
kind: "removed",
|
|
1394
|
+
before: b[key],
|
|
1395
|
+
after: void 0
|
|
1396
|
+
};
|
|
1397
|
+
return {
|
|
1398
|
+
keyLabel,
|
|
1399
|
+
kind: "added",
|
|
1400
|
+
before: void 0,
|
|
1401
|
+
after: a[key]
|
|
1402
|
+
};
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
function placeholderText(count, isArray) {
|
|
1406
|
+
return `… ${count} unchanged ${isArray ? count === 1 ? "item" : "items" : count === 1 ? "property" : "properties"}`;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Render a value as pretty JSON and append it as one tone'd block, re-basing the
|
|
1410
|
+
* stringifier's own 2-space indentation onto `baseDepth` and prefixing the key
|
|
1411
|
+
* (if any) onto the first line.
|
|
1412
|
+
*/
|
|
1413
|
+
function pushValueLines(lines, baseDepth, keyLabel, value, sign, tone) {
|
|
1414
|
+
const prefix = keyPrefix(keyLabel);
|
|
1415
|
+
safeStringify(value, 2).split("\n").forEach((line, idx) => {
|
|
1416
|
+
const leading = /^ */.exec(line)[0].length;
|
|
1417
|
+
const content = line.slice(leading);
|
|
1418
|
+
const text = idx === 0 ? `${prefix}${content}` : content;
|
|
1419
|
+
lines.push({
|
|
1420
|
+
depth: baseDepth + leading / 2,
|
|
1421
|
+
sign,
|
|
1422
|
+
tone,
|
|
1423
|
+
text
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
function isScalar(value) {
|
|
1428
|
+
return value == null || typeof value !== "object";
|
|
1429
|
+
}
|
|
1430
|
+
function formatScalar(value) {
|
|
1431
|
+
if (typeof value === "string") return `"${value.length > 24 ? `${value.slice(0, 24)}…` : value}"`;
|
|
1432
|
+
return String(value);
|
|
1433
|
+
}
|
|
1434
|
+
//#endregion
|
|
1435
|
+
//#region src/devtools/browser/components/DiffView.tsx
|
|
1436
|
+
const ADDED_BG$1 = "rgba(163, 230, 53, 0.08)";
|
|
1437
|
+
const REMOVED_BG$1 = "rgba(255, 92, 92, 0.08)";
|
|
1438
|
+
const INLINE_MAX = 40;
|
|
1439
|
+
/**
|
|
1440
|
+
* Test-framework-style diff: only the paths that changed. Changed paths are
|
|
1441
|
+
* grouped into a hierarchy by their shared parents — `countHistory.0` and
|
|
1442
|
+
* `countHistory.1` nest under a single `countHistory` node, each shown by its
|
|
1443
|
+
* direct key only. Single-child chains collapse to a dotted path (`a.b.c`) so
|
|
1444
|
+
* lone deep changes stay on one line. Short values render inline with the key
|
|
1445
|
+
* (`count: 0 → 1`); long ones keep the removed/added line block.
|
|
1446
|
+
*/
|
|
1447
|
+
function DiffView({ before, after, latestFirst = false }) {
|
|
1448
|
+
const entries = computeDiff(before, after);
|
|
1449
|
+
if (entries.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1450
|
+
style: {
|
|
1451
|
+
padding: "10px",
|
|
1452
|
+
borderRadius: "4px",
|
|
1453
|
+
background: DEVTOOL_SECTION_STRING_BACKGROUND,
|
|
1454
|
+
color: DEVTOOL_COLOR_TEXT_MUTED,
|
|
1455
|
+
fontSize: "11px",
|
|
1456
|
+
fontStyle: "italic"
|
|
1457
|
+
},
|
|
1458
|
+
children: "No differences — before and after are structurally equal."
|
|
1459
|
+
});
|
|
1460
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1461
|
+
style: {
|
|
1462
|
+
display: "flex",
|
|
1463
|
+
flexDirection: "column",
|
|
1464
|
+
gap: "6px"
|
|
1465
|
+
},
|
|
1466
|
+
children: [...buildTree(entries).children.values()].map(collapseChains).map((node) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffNode, {
|
|
1467
|
+
node,
|
|
1468
|
+
latestFirst
|
|
1469
|
+
}, node.key))
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
/** Group flat diff entries into a tree keyed by their path segments. */
|
|
1473
|
+
function buildTree(entries) {
|
|
1474
|
+
const root = {
|
|
1475
|
+
key: "",
|
|
1476
|
+
children: /* @__PURE__ */ new Map()
|
|
1477
|
+
};
|
|
1478
|
+
for (const entry of entries) {
|
|
1479
|
+
const segments = entry.segments.length > 0 ? entry.segments : ["(root)"];
|
|
1480
|
+
let node = root;
|
|
1481
|
+
for (const seg of segments) {
|
|
1482
|
+
const key = String(seg);
|
|
1483
|
+
let child = node.children.get(key);
|
|
1484
|
+
if (child == null) {
|
|
1485
|
+
child = {
|
|
1486
|
+
key,
|
|
1487
|
+
children: /* @__PURE__ */ new Map()
|
|
1488
|
+
};
|
|
1489
|
+
node.children.set(key, child);
|
|
1490
|
+
}
|
|
1491
|
+
node = child;
|
|
1492
|
+
}
|
|
1493
|
+
node.entry = entry;
|
|
1494
|
+
}
|
|
1495
|
+
return root;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Collapse single-child chains into one dotted key so a lone deep change reads
|
|
1499
|
+
* as `a.b.c` rather than three nested headers; branches with siblings keep their
|
|
1500
|
+
* hierarchy.
|
|
1501
|
+
*/
|
|
1502
|
+
function collapseChains(node) {
|
|
1503
|
+
const children = /* @__PURE__ */ new Map();
|
|
1504
|
+
for (const child of node.children.values()) {
|
|
1505
|
+
const collapsed = collapseChains(child);
|
|
1506
|
+
children.set(collapsed.key, collapsed);
|
|
1507
|
+
}
|
|
1508
|
+
let current = {
|
|
1509
|
+
key: node.key,
|
|
1510
|
+
children,
|
|
1511
|
+
entry: node.entry
|
|
1512
|
+
};
|
|
1513
|
+
while (current.entry == null && current.children.size === 1) {
|
|
1514
|
+
const only = [...current.children.values()][0];
|
|
1515
|
+
current = {
|
|
1516
|
+
key: `${current.key}.${only.key}`,
|
|
1517
|
+
children: only.children,
|
|
1518
|
+
entry: only.entry
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
return current;
|
|
1522
|
+
}
|
|
1523
|
+
function DiffNode({ node, latestFirst }) {
|
|
1524
|
+
if (node.entry != null && node.children.size === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffLeaf, {
|
|
1525
|
+
label: node.key,
|
|
1526
|
+
entry: node.entry,
|
|
1527
|
+
latestFirst
|
|
1528
|
+
});
|
|
1529
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1530
|
+
style: {
|
|
1531
|
+
color: DEVTOOL_JSON_KEY,
|
|
1532
|
+
fontFamily: MONO_FONT,
|
|
1533
|
+
fontSize: "10px",
|
|
1534
|
+
fontWeight: 600
|
|
1535
|
+
},
|
|
1536
|
+
children: node.key
|
|
1537
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1538
|
+
style: {
|
|
1539
|
+
marginTop: "4px",
|
|
1540
|
+
marginLeft: "5px",
|
|
1541
|
+
paddingLeft: "8px",
|
|
1542
|
+
borderLeft: `1px solid ${DEVTOOL_SECTION_STRING_BACKGROUND}`,
|
|
1543
|
+
display: "flex",
|
|
1544
|
+
flexDirection: "column",
|
|
1545
|
+
gap: "5px"
|
|
1546
|
+
},
|
|
1547
|
+
children: [...node.children.values()].map((child) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffNode, {
|
|
1548
|
+
node: child,
|
|
1549
|
+
latestFirst
|
|
1550
|
+
}, child.key))
|
|
1551
|
+
})] });
|
|
1552
|
+
}
|
|
1553
|
+
function DiffLeaf({ label, entry, latestFirst }) {
|
|
1554
|
+
const showRemoved = entry.kind === "removed" || entry.kind === "changed";
|
|
1555
|
+
const showAdded = entry.kind === "added" || entry.kind === "changed";
|
|
1556
|
+
const removed = showRemoved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineValue, {
|
|
1557
|
+
sign: "−",
|
|
1558
|
+
color: "#FF5C5C",
|
|
1559
|
+
background: REMOVED_BG$1,
|
|
1560
|
+
value: entry.before
|
|
1561
|
+
});
|
|
1562
|
+
const added = showAdded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineValue, {
|
|
1563
|
+
sign: "+",
|
|
1564
|
+
color: "#A3E635",
|
|
1565
|
+
background: ADDED_BG$1,
|
|
1566
|
+
value: entry.after
|
|
1567
|
+
});
|
|
1568
|
+
const removedLine = showRemoved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffLine, {
|
|
1569
|
+
sign: "−",
|
|
1570
|
+
color: "#FF5C5C",
|
|
1571
|
+
background: REMOVED_BG$1,
|
|
1572
|
+
value: entry.before
|
|
1573
|
+
});
|
|
1574
|
+
const addedLine = showAdded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffLine, {
|
|
1575
|
+
sign: "+",
|
|
1576
|
+
color: "#A3E635",
|
|
1577
|
+
background: ADDED_BG$1,
|
|
1578
|
+
value: entry.after
|
|
1579
|
+
});
|
|
1580
|
+
if (isInlineLeaf(entry)) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1581
|
+
style: {
|
|
1582
|
+
display: "flex",
|
|
1583
|
+
flexWrap: "wrap",
|
|
1584
|
+
alignItems: "baseline",
|
|
1585
|
+
gap: "6px"
|
|
1586
|
+
},
|
|
1587
|
+
children: [
|
|
1588
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1589
|
+
style: {
|
|
1590
|
+
color: DEVTOOL_JSON_KEY,
|
|
1591
|
+
fontFamily: MONO_FONT,
|
|
1592
|
+
fontSize: "11px"
|
|
1593
|
+
},
|
|
1594
|
+
children: [label, ":"]
|
|
1595
|
+
}),
|
|
1596
|
+
latestFirst ? added : removed,
|
|
1597
|
+
entry.kind === "changed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1598
|
+
style: { color: "#64748b" },
|
|
1599
|
+
children: latestFirst ? "←" : "→"
|
|
1600
|
+
}),
|
|
1601
|
+
latestFirst ? removed : added
|
|
1602
|
+
]
|
|
1603
|
+
});
|
|
1604
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1605
|
+
style: {
|
|
1606
|
+
borderRadius: "4px",
|
|
1607
|
+
overflow: "hidden",
|
|
1608
|
+
border: `1px solid ${DEVTOOL_SECTION_STRING_BACKGROUND}`
|
|
1609
|
+
},
|
|
1610
|
+
children: [
|
|
1611
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1612
|
+
style: {
|
|
1613
|
+
padding: "3px 8px",
|
|
1614
|
+
background: DEVTOOL_SECTION_STRING_BACKGROUND,
|
|
1615
|
+
color: DEVTOOL_JSON_KEY,
|
|
1616
|
+
fontFamily: MONO_FONT,
|
|
1617
|
+
fontSize: "10px"
|
|
1618
|
+
},
|
|
1619
|
+
children: label
|
|
1620
|
+
}),
|
|
1621
|
+
latestFirst ? addedLine : removedLine,
|
|
1622
|
+
latestFirst ? removedLine : addedLine
|
|
1623
|
+
]
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
function InlineValue({ sign, color, background, value }) {
|
|
1627
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1628
|
+
style: {
|
|
1629
|
+
display: "inline-flex",
|
|
1630
|
+
alignItems: "baseline",
|
|
1631
|
+
gap: "4px",
|
|
1632
|
+
padding: "1px 6px",
|
|
1633
|
+
borderRadius: "3px",
|
|
1634
|
+
background,
|
|
1635
|
+
fontFamily: MONO_FONT,
|
|
1636
|
+
fontSize: "11px",
|
|
1637
|
+
whiteSpace: "pre-wrap",
|
|
1638
|
+
wordBreak: "break-word"
|
|
1639
|
+
},
|
|
1640
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1641
|
+
style: {
|
|
1642
|
+
color,
|
|
1643
|
+
fontWeight: 700,
|
|
1644
|
+
userSelect: "none"
|
|
1645
|
+
},
|
|
1646
|
+
children: sign
|
|
1647
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: renderColoredJson(compactStringify(value)) })]
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
function DiffLine({ sign, color, background, value }) {
|
|
1651
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1652
|
+
style: {
|
|
1653
|
+
display: "flex",
|
|
1654
|
+
gap: "6px",
|
|
1655
|
+
padding: "3px 8px",
|
|
1656
|
+
background
|
|
1657
|
+
},
|
|
1658
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1659
|
+
style: {
|
|
1660
|
+
color,
|
|
1661
|
+
fontWeight: 700,
|
|
1662
|
+
flexShrink: 0,
|
|
1663
|
+
userSelect: "none"
|
|
1664
|
+
},
|
|
1665
|
+
children: sign
|
|
1666
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1667
|
+
style: {
|
|
1668
|
+
flex: 1,
|
|
1669
|
+
minWidth: 0,
|
|
1670
|
+
fontFamily: MONO_FONT,
|
|
1671
|
+
fontSize: "11px",
|
|
1672
|
+
lineHeight: 1.5,
|
|
1673
|
+
whiteSpace: "pre-wrap",
|
|
1674
|
+
wordBreak: "break-word"
|
|
1675
|
+
},
|
|
1676
|
+
children: renderColoredJson(safeStringify(value, 2))
|
|
1677
|
+
})]
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
/** A leaf renders inline when every side it shows is short and single-line. */
|
|
1681
|
+
function isInlineLeaf(entry) {
|
|
1682
|
+
const sides = [];
|
|
1683
|
+
if (entry.kind !== "added") sides.push(compactStringify(entry.before));
|
|
1684
|
+
if (entry.kind !== "removed") sides.push(compactStringify(entry.after));
|
|
1685
|
+
return sides.every((s) => s.length <= INLINE_MAX && !s.includes("\n"));
|
|
1686
|
+
}
|
|
1687
|
+
/** Single-line JSON for inline rendering (`undefined` has no JSON form). */
|
|
1688
|
+
function compactStringify(value) {
|
|
1689
|
+
if (value === void 0) return "undefined";
|
|
1690
|
+
try {
|
|
1691
|
+
return JSON.stringify(value) ?? "undefined";
|
|
1692
|
+
} catch {
|
|
1693
|
+
return String(value);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
//#endregion
|
|
1697
|
+
//#region src/devtools/browser/components/JsonDiffView.tsx
|
|
1698
|
+
const ADDED_BG = "rgba(163, 230, 53, 0.13)";
|
|
1699
|
+
const REMOVED_BG = "rgba(255, 92, 92, 0.13)";
|
|
1700
|
+
const SURFACE_STYLE = {
|
|
1701
|
+
margin: 0,
|
|
1702
|
+
padding: "8px 0",
|
|
1703
|
+
borderRadius: "4px",
|
|
1704
|
+
fontSize: "11px",
|
|
1705
|
+
lineHeight: 1.5,
|
|
1706
|
+
fontFamily: MONO_FONT,
|
|
1707
|
+
background: DEVTOOL_SECTION_STRING_BACKGROUND,
|
|
1708
|
+
overflowX: "auto"
|
|
1709
|
+
};
|
|
1710
|
+
function emptyNotice(text) {
|
|
1711
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1712
|
+
style: {
|
|
1713
|
+
padding: "10px",
|
|
1714
|
+
borderRadius: "4px",
|
|
1715
|
+
background: DEVTOOL_SECTION_STRING_BACKGROUND,
|
|
1716
|
+
color: DEVTOOL_COLOR_TEXT_MUTED,
|
|
1717
|
+
fontSize: "11px",
|
|
1718
|
+
fontStyle: "italic"
|
|
1719
|
+
},
|
|
1720
|
+
children: text
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* A single rendered diff line: a fixed sign gutter (`+` / `−` / blank) followed
|
|
1725
|
+
* by the syntax-coloured JSON text, all on a highlighted background.
|
|
1726
|
+
*/
|
|
1727
|
+
function Line({ sign, color, background, text }) {
|
|
1728
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1729
|
+
style: {
|
|
1730
|
+
display: "flex",
|
|
1731
|
+
background,
|
|
1732
|
+
padding: "0 10px",
|
|
1733
|
+
whiteSpace: "pre"
|
|
1734
|
+
},
|
|
1735
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1736
|
+
style: {
|
|
1737
|
+
width: "12px",
|
|
1738
|
+
flexShrink: 0,
|
|
1739
|
+
color,
|
|
1740
|
+
fontWeight: 700,
|
|
1741
|
+
userSelect: "none"
|
|
1742
|
+
},
|
|
1743
|
+
children: sign
|
|
1744
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1745
|
+
style: {
|
|
1746
|
+
flex: 1,
|
|
1747
|
+
minWidth: 0
|
|
1748
|
+
},
|
|
1749
|
+
children: renderColoredJson(text)
|
|
1750
|
+
})]
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Unified, git-style diff of the entire state snapshot. Both sides are rendered
|
|
1755
|
+
* as pretty JSON and line-diffed, so unchanged structure stays visible for
|
|
1756
|
+
* context while removed lines (red, `−`) and added lines (green, `+`) call out
|
|
1757
|
+
* exactly which sections of the whole state moved.
|
|
1758
|
+
*/
|
|
1759
|
+
function JsonDiffView({ before, after, compress = false, latestFirst = false }) {
|
|
1760
|
+
if (compress) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompressedDiffView, {
|
|
1761
|
+
before,
|
|
1762
|
+
after,
|
|
1763
|
+
side: "unified",
|
|
1764
|
+
latestFirst
|
|
1765
|
+
});
|
|
1766
|
+
const ops = orderLineDiffOps(computeLineDiff(safeStringify(before, 2), safeStringify(after, 2)), latestFirst);
|
|
1767
|
+
if (!ops.some((op) => op.kind !== "common")) return emptyNotice("No differences — before and after are structurally equal.");
|
|
1768
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1769
|
+
style: SURFACE_STYLE,
|
|
1770
|
+
children: ops.map((op, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Line, {
|
|
1771
|
+
sign: op.kind === "removed" ? "−" : op.kind === "added" ? "+" : " ",
|
|
1772
|
+
color: op.kind === "removed" ? DEVTOOL_COLOR_SEMANTIC_ERROR : DEVTOOL_COLOR_SEMANTIC_SUCCESS,
|
|
1773
|
+
background: op.kind === "removed" ? REMOVED_BG : op.kind === "added" ? ADDED_BG : "transparent",
|
|
1774
|
+
text: op.text
|
|
1775
|
+
}, i))
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* The structure-aware "address" view: the JSON tree pruned to just the changed
|
|
1780
|
+
* branches, with runs of untouched siblings collapsed into `… N unchanged …`
|
|
1781
|
+
* placeholders. Shared by all three diff tabs via `side`.
|
|
1782
|
+
*/
|
|
1783
|
+
function CompressedDiffView({ before, after, side, latestFirst = false }) {
|
|
1784
|
+
const lines = computeCompressedDiff(before, after, side, latestFirst);
|
|
1785
|
+
if (!lines.some((line) => line.tone === "added" || line.tone === "removed")) return emptyNotice("No differences — before and after are structurally equal.");
|
|
1786
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1787
|
+
style: SURFACE_STYLE,
|
|
1788
|
+
children: lines.map((line, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompressedLine, { line }, i))
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
function CompressedLine({ line }) {
|
|
1792
|
+
const indent = " ".repeat(line.depth);
|
|
1793
|
+
if (line.tone === "placeholder") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1794
|
+
style: {
|
|
1795
|
+
display: "flex",
|
|
1796
|
+
padding: "0 10px",
|
|
1797
|
+
whiteSpace: "pre"
|
|
1798
|
+
},
|
|
1799
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
|
|
1800
|
+
width: "12px",
|
|
1801
|
+
flexShrink: 0
|
|
1802
|
+
} }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1803
|
+
style: {
|
|
1804
|
+
flex: 1,
|
|
1805
|
+
minWidth: 0,
|
|
1806
|
+
color: DEVTOOL_COLOR_TEXT_MUTED,
|
|
1807
|
+
fontStyle: "italic"
|
|
1808
|
+
},
|
|
1809
|
+
children: [indent, line.text]
|
|
1810
|
+
})]
|
|
1811
|
+
});
|
|
1812
|
+
const background = line.tone === "removed" ? REMOVED_BG : line.tone === "added" ? ADDED_BG : "transparent";
|
|
1813
|
+
const color = line.tone === "removed" ? DEVTOOL_COLOR_SEMANTIC_ERROR : DEVTOOL_COLOR_SEMANTIC_SUCCESS;
|
|
1814
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1815
|
+
style: {
|
|
1816
|
+
display: "flex",
|
|
1817
|
+
background,
|
|
1818
|
+
padding: "0 10px",
|
|
1819
|
+
whiteSpace: "pre"
|
|
1820
|
+
},
|
|
1821
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1822
|
+
style: {
|
|
1823
|
+
width: "12px",
|
|
1824
|
+
flexShrink: 0,
|
|
1825
|
+
color,
|
|
1826
|
+
fontWeight: 700,
|
|
1827
|
+
userSelect: "none"
|
|
1828
|
+
},
|
|
1829
|
+
children: line.sign
|
|
1830
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1831
|
+
style: {
|
|
1832
|
+
flex: 1,
|
|
1833
|
+
minWidth: 0
|
|
1834
|
+
},
|
|
1835
|
+
children: renderColoredJson(`${indent}${line.text}`)
|
|
1836
|
+
})]
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* The full JSON of one snapshot with the lines that differ from the other side
|
|
1841
|
+
* highlighted in place — red behind removed lines on the "before" side, green
|
|
1842
|
+
* behind added lines on the "after" side. Lines common to both render plainly so
|
|
1843
|
+
* the highlight reads as "here is what changed within the whole state".
|
|
1844
|
+
*/
|
|
1845
|
+
function HighlightedJsonView({ before, after, side, compress = false }) {
|
|
1846
|
+
if (compress) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompressedDiffView, {
|
|
1847
|
+
before,
|
|
1848
|
+
after,
|
|
1849
|
+
side
|
|
1850
|
+
});
|
|
1851
|
+
const ops = computeLineDiff(safeStringify(before, 2), safeStringify(after, 2));
|
|
1852
|
+
const dropKind = side === "before" ? "added" : "removed";
|
|
1853
|
+
const highlightKind = side === "before" ? "removed" : "added";
|
|
1854
|
+
const background = side === "before" ? REMOVED_BG : ADDED_BG;
|
|
1855
|
+
const color = side === "before" ? DEVTOOL_COLOR_SEMANTIC_ERROR : DEVTOOL_COLOR_SEMANTIC_SUCCESS;
|
|
1856
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1857
|
+
style: SURFACE_STYLE,
|
|
1858
|
+
children: ops.filter((op) => op.kind !== dropKind).map((op, i) => {
|
|
1859
|
+
const changed = op.kind === highlightKind;
|
|
1860
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Line, {
|
|
1861
|
+
sign: changed ? side === "before" ? "−" : "+" : " ",
|
|
1862
|
+
color,
|
|
1863
|
+
background: changed ? background : "transparent",
|
|
1864
|
+
text: op.text
|
|
1865
|
+
}, i);
|
|
1866
|
+
})
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
//#endregion
|
|
1870
|
+
//#region src/devtools/browser/components/ChangeDetailPanel.tsx
|
|
1871
|
+
function ChangeDetailPanel({ change, onRevert, view, onViewChange, compress, onCompressChange, latestFirst, onLatestFirstChange }) {
|
|
1872
|
+
const canRevert = change.inversePatches.length > 0;
|
|
1873
|
+
const showCompressToggle = view !== "props";
|
|
1874
|
+
const showOrderToggle = view === "props" || view === "diff";
|
|
1875
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1876
|
+
style: {
|
|
1877
|
+
flex: 1,
|
|
1878
|
+
display: "flex",
|
|
1879
|
+
flexDirection: "column",
|
|
1880
|
+
overflow: "hidden",
|
|
1881
|
+
minHeight: 0,
|
|
1882
|
+
background: DEVTOOL_DETAIL_BASE_BACKGROUND
|
|
1883
|
+
},
|
|
1884
|
+
children: [
|
|
1885
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1886
|
+
style: {
|
|
1887
|
+
padding: "8px 12px",
|
|
1888
|
+
background: DEVTOOL_DETAIL_HEADER_BACKGROUND,
|
|
1889
|
+
borderBottom: `1px solid ${DEVTOOL_LIST_BASE_BACKGROUND}`,
|
|
1890
|
+
display: "flex",
|
|
1891
|
+
alignItems: "center",
|
|
1892
|
+
gap: "8px",
|
|
1893
|
+
flexShrink: 0
|
|
1894
|
+
},
|
|
1895
|
+
children: [
|
|
1896
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1897
|
+
style: {
|
|
1898
|
+
color: DEVTOOL_COLOR_TEXT_EMPHASIS,
|
|
1899
|
+
fontWeight: 600,
|
|
1900
|
+
fontFamily: MONO_FONT,
|
|
1901
|
+
fontSize: "12px",
|
|
1902
|
+
overflow: "hidden",
|
|
1903
|
+
textOverflow: "ellipsis",
|
|
1904
|
+
whiteSpace: "nowrap"
|
|
1905
|
+
},
|
|
1906
|
+
children: change.storeLabel
|
|
1907
|
+
}),
|
|
1908
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1909
|
+
style: {
|
|
1910
|
+
padding: "1px 7px",
|
|
1911
|
+
borderRadius: "999px",
|
|
1912
|
+
background: SOURCE_COLOR[change.source],
|
|
1913
|
+
color: "#0f172a",
|
|
1914
|
+
fontSize: "8px",
|
|
1915
|
+
fontWeight: 700,
|
|
1916
|
+
letterSpacing: "0.08em",
|
|
1917
|
+
fontFamily: SANS_FONT
|
|
1918
|
+
},
|
|
1919
|
+
children: SOURCE_LABEL[change.source]
|
|
1920
|
+
}),
|
|
1921
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
|
|
1922
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1923
|
+
style: {
|
|
1924
|
+
color: DEVTOOL_COLOR_TEXT_MUTED,
|
|
1925
|
+
fontSize: "10px",
|
|
1926
|
+
fontFamily: MONO_FONT
|
|
1927
|
+
},
|
|
1928
|
+
children: formatTimestamp(change.timestamp)
|
|
1929
|
+
}),
|
|
1930
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1931
|
+
onClick: () => onRevert(change),
|
|
1932
|
+
disabled: !canRevert,
|
|
1933
|
+
title: canRevert ? "Apply this change's inverse patches to undo it" : "No inverse patches available to revert",
|
|
1934
|
+
style: {
|
|
1935
|
+
background: "transparent",
|
|
1936
|
+
border: `1px solid ${canRevert ? DEVTOOL_COLOR_SEMANTIC_WARNING : DEVTOOL_SECTION_BACKGROUND}`,
|
|
1937
|
+
color: canRevert ? DEVTOOL_COLOR_SEMANTIC_WARNING : DEVTOOL_COLOR_TEXT_MUTED,
|
|
1938
|
+
borderRadius: "4px",
|
|
1939
|
+
cursor: canRevert ? "pointer" : "not-allowed",
|
|
1940
|
+
fontSize: "10px",
|
|
1941
|
+
padding: "2px 8px",
|
|
1942
|
+
fontFamily: SANS_FONT,
|
|
1943
|
+
flexShrink: 0
|
|
1944
|
+
},
|
|
1945
|
+
children: "⟲ revert"
|
|
1946
|
+
})
|
|
1947
|
+
]
|
|
1948
|
+
}),
|
|
1949
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1950
|
+
style: {
|
|
1951
|
+
padding: "6px 12px",
|
|
1952
|
+
borderBottom: `1px solid ${DEVTOOL_LIST_BASE_BACKGROUND}`,
|
|
1953
|
+
flexShrink: 0
|
|
1954
|
+
},
|
|
1955
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SegmentedControl, {
|
|
1956
|
+
options: [
|
|
1957
|
+
{
|
|
1958
|
+
value: "props",
|
|
1959
|
+
label: "Diff Props"
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
value: "diff",
|
|
1963
|
+
label: "Diff View"
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
value: "before",
|
|
1967
|
+
label: "Before"
|
|
1968
|
+
},
|
|
1969
|
+
{
|
|
1970
|
+
value: "after",
|
|
1971
|
+
label: "After"
|
|
1972
|
+
}
|
|
1973
|
+
],
|
|
1974
|
+
value: view,
|
|
1975
|
+
onChange: onViewChange
|
|
1976
|
+
})
|
|
1977
|
+
}),
|
|
1978
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1979
|
+
style: {
|
|
1980
|
+
flex: 1,
|
|
1981
|
+
overflowY: "auto",
|
|
1982
|
+
minHeight: 0,
|
|
1983
|
+
padding: "10px 12px"
|
|
1984
|
+
},
|
|
1985
|
+
children: [
|
|
1986
|
+
(showCompressToggle || showOrderToggle) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1987
|
+
style: {
|
|
1988
|
+
display: "flex",
|
|
1989
|
+
flexWrap: "wrap",
|
|
1990
|
+
alignItems: "center",
|
|
1991
|
+
gap: "14px",
|
|
1992
|
+
marginBottom: "8px"
|
|
1993
|
+
},
|
|
1994
|
+
children: [showCompressToggle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleCheckbox, {
|
|
1995
|
+
checked: compress,
|
|
1996
|
+
onChange: onCompressChange,
|
|
1997
|
+
label: "Compress to changed paths only",
|
|
1998
|
+
title: "Collapse unchanged branches, showing only the address of what changed"
|
|
1999
|
+
}), showOrderToggle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleCheckbox, {
|
|
2000
|
+
checked: latestFirst,
|
|
2001
|
+
onChange: onLatestFirstChange,
|
|
2002
|
+
label: "Latest (+) change first",
|
|
2003
|
+
title: "Show the new value (+) above the previous value (−); off shows previous first"
|
|
2004
|
+
})]
|
|
2005
|
+
}),
|
|
2006
|
+
view === "props" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffView, {
|
|
2007
|
+
before: change.prevSnapshot,
|
|
2008
|
+
after: change.snapshot,
|
|
2009
|
+
latestFirst
|
|
2010
|
+
}),
|
|
2011
|
+
view === "diff" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonDiffView, {
|
|
2012
|
+
before: change.prevSnapshot,
|
|
2013
|
+
after: change.snapshot,
|
|
2014
|
+
compress,
|
|
2015
|
+
latestFirst
|
|
2016
|
+
}),
|
|
2017
|
+
view === "before" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HighlightedJsonView, {
|
|
2018
|
+
before: change.prevSnapshot,
|
|
2019
|
+
after: change.snapshot,
|
|
2020
|
+
side: "before",
|
|
2021
|
+
compress
|
|
2022
|
+
}),
|
|
2023
|
+
view === "after" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HighlightedJsonView, {
|
|
2024
|
+
before: change.prevSnapshot,
|
|
2025
|
+
after: change.snapshot,
|
|
2026
|
+
side: "after",
|
|
2027
|
+
compress
|
|
2028
|
+
})
|
|
2029
|
+
]
|
|
2030
|
+
})
|
|
2031
|
+
]
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
function ToggleCheckbox({ checked, onChange, label, title }) {
|
|
2035
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2036
|
+
style: {
|
|
2037
|
+
display: "flex",
|
|
2038
|
+
alignItems: "center",
|
|
2039
|
+
gap: "6px",
|
|
2040
|
+
color: DEVTOOL_COLOR_TEXT_MUTED,
|
|
2041
|
+
fontSize: "10px",
|
|
2042
|
+
fontFamily: SANS_FONT,
|
|
2043
|
+
cursor: "pointer",
|
|
2044
|
+
userSelect: "none"
|
|
2045
|
+
},
|
|
2046
|
+
title,
|
|
2047
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2048
|
+
type: "checkbox",
|
|
2049
|
+
checked,
|
|
2050
|
+
onChange: (e) => onChange(e.target.checked),
|
|
2051
|
+
style: {
|
|
2052
|
+
cursor: "pointer",
|
|
2053
|
+
margin: 0
|
|
2054
|
+
}
|
|
2055
|
+
}), label]
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
//#endregion
|
|
2059
|
+
//#region src/devtools/browser/components/ChangeList.tsx
|
|
2060
|
+
const getGroupKey = (group) => group.oldest.cuid;
|
|
2061
|
+
function ChangeList({ groups, selectedCuid, onSelect, showStore, style }) {
|
|
2062
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevtoolsVirtualList, {
|
|
2063
|
+
items: groups,
|
|
2064
|
+
getItemKey: getGroupKey,
|
|
2065
|
+
selectedKey: selectedCuid,
|
|
2066
|
+
estimateSize: 44,
|
|
2067
|
+
overscan: 12,
|
|
2068
|
+
style,
|
|
2069
|
+
empty: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2070
|
+
style: {
|
|
2071
|
+
padding: "24px",
|
|
2072
|
+
textAlign: "center",
|
|
2073
|
+
color: DEVTOOL_COLOR_TEXT_MUTED,
|
|
2074
|
+
fontSize: "11px",
|
|
2075
|
+
...style
|
|
2076
|
+
},
|
|
2077
|
+
children: "No changes recorded yet. Mutate a store to see it here."
|
|
2078
|
+
}),
|
|
2079
|
+
renderItem: (group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChangeRow, {
|
|
2080
|
+
change: group.representative,
|
|
2081
|
+
count: group.count,
|
|
2082
|
+
selected: group.oldest.cuid === selectedCuid,
|
|
2083
|
+
onClick: () => onSelect(group.oldest.cuid),
|
|
2084
|
+
showStore
|
|
2085
|
+
})
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
function ChangeRow({ change, count, selected, onClick, showStore }) {
|
|
2089
|
+
const sourceColor = SOURCE_COLOR[change.source];
|
|
2090
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2091
|
+
onClick,
|
|
2092
|
+
style: {
|
|
2093
|
+
display: "flex",
|
|
2094
|
+
flexDirection: "column",
|
|
2095
|
+
gap: "3px",
|
|
2096
|
+
padding: "6px 10px",
|
|
2097
|
+
cursor: "pointer",
|
|
2098
|
+
borderBottom: `1px solid ${DEVTOOL_SECTION_BACKGROUND}`,
|
|
2099
|
+
borderLeft: `2px solid ${selected ? DEVTOOL_COLOR_SEMANTIC_SYSTEM : "transparent"}`,
|
|
2100
|
+
background: selected ? DEVTOOL_LIST_SELECTED_BACKGROUND : DEVTOOL_LIST_BASE_BACKGROUND
|
|
2101
|
+
},
|
|
2102
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2103
|
+
style: {
|
|
2104
|
+
display: "flex",
|
|
2105
|
+
alignItems: "center",
|
|
2106
|
+
gap: "6px"
|
|
2107
|
+
},
|
|
2108
|
+
children: [
|
|
2109
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2110
|
+
style: {
|
|
2111
|
+
color: DEVTOOL_COLOR_TEXT_MUTED,
|
|
2112
|
+
fontSize: "10px",
|
|
2113
|
+
fontFamily: MONO_FONT,
|
|
2114
|
+
flexShrink: 0
|
|
2115
|
+
},
|
|
2116
|
+
children: formatTimestamp(change.timestamp)
|
|
2117
|
+
}),
|
|
2118
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
|
|
2119
|
+
color: sourceColor,
|
|
2120
|
+
children: SOURCE_LABEL[change.source]
|
|
2121
|
+
}),
|
|
2122
|
+
count > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2123
|
+
title: `${count} consecutive identical updates`,
|
|
2124
|
+
style: {
|
|
2125
|
+
flexShrink: 0,
|
|
2126
|
+
padding: "0 5px",
|
|
2127
|
+
borderRadius: "999px",
|
|
2128
|
+
background: "#1e293b",
|
|
2129
|
+
color: "#cbd5e1",
|
|
2130
|
+
fontSize: "9px",
|
|
2131
|
+
fontWeight: 700,
|
|
2132
|
+
fontFamily: "ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace"
|
|
2133
|
+
},
|
|
2134
|
+
children: ["×", count]
|
|
2135
|
+
}),
|
|
2136
|
+
showStore && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2137
|
+
style: {
|
|
2138
|
+
color: "#38BDF8",
|
|
2139
|
+
fontSize: "10px",
|
|
2140
|
+
fontFamily: "ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace",
|
|
2141
|
+
overflow: "hidden",
|
|
2142
|
+
textOverflow: "ellipsis",
|
|
2143
|
+
whiteSpace: "nowrap"
|
|
2144
|
+
},
|
|
2145
|
+
children: change.storeLabel
|
|
2146
|
+
}),
|
|
2147
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
|
|
2148
|
+
change.patches.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2149
|
+
style: {
|
|
2150
|
+
color: "#334155",
|
|
2151
|
+
fontSize: "10px",
|
|
2152
|
+
flexShrink: 0
|
|
2153
|
+
},
|
|
2154
|
+
children: [
|
|
2155
|
+
change.patches.length,
|
|
2156
|
+
" patch",
|
|
2157
|
+
change.patches.length === 1 ? "" : "es"
|
|
2158
|
+
]
|
|
2159
|
+
})
|
|
2160
|
+
]
|
|
2161
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2162
|
+
style: {
|
|
2163
|
+
color: DEVTOOL_COLOR_TEXT_SECONDARY,
|
|
2164
|
+
fontSize: "11px",
|
|
2165
|
+
fontFamily: MONO_FONT,
|
|
2166
|
+
overflow: "hidden",
|
|
2167
|
+
textOverflow: "ellipsis",
|
|
2168
|
+
whiteSpace: "nowrap"
|
|
2169
|
+
},
|
|
2170
|
+
children: summarizeChange(change)
|
|
2171
|
+
})]
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
function Badge({ color, children }) {
|
|
2175
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2176
|
+
style: {
|
|
2177
|
+
flexShrink: 0,
|
|
2178
|
+
padding: "1px 6px",
|
|
2179
|
+
borderRadius: "999px",
|
|
2180
|
+
background: color,
|
|
2181
|
+
color: "#0f172a",
|
|
2182
|
+
fontSize: "8px",
|
|
2183
|
+
fontWeight: 700,
|
|
2184
|
+
letterSpacing: "0.08em",
|
|
2185
|
+
fontFamily: SANS_FONT
|
|
2186
|
+
},
|
|
2187
|
+
children
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
//#endregion
|
|
2191
|
+
//#region src/devtools/browser/components/StateInspector.tsx
|
|
2192
|
+
/**
|
|
2193
|
+
* Live current-state view (top half) plus a direct JSON editor (bottom half) for
|
|
2194
|
+
* one store — a fixed 50/50 split so the editor is usable without manual
|
|
2195
|
+
* resizing. Editing is the "trigger edits directly for testing" capability: the
|
|
2196
|
+
* draft is parsed and handed to {@link StateDevtoolsCore.applyEdit}, which
|
|
2197
|
+
* replaces the store state.
|
|
2198
|
+
*
|
|
2199
|
+
* The draft is seeded from the live state only while it is clean — once the user
|
|
2200
|
+
* types, incoming external updates no longer clobber their edit (a "reload"
|
|
2201
|
+
* button re-syncs on demand).
|
|
2202
|
+
*/
|
|
2203
|
+
function StateInspector({ store, onApply }) {
|
|
2204
|
+
const liveText = safeStringify(store.currentState, 2);
|
|
2205
|
+
const [draft, setDraft] = (0, react.useState)(liveText);
|
|
2206
|
+
const [dirty, setDirty] = (0, react.useState)(false);
|
|
2207
|
+
const [error, setError] = (0, react.useState)(null);
|
|
2208
|
+
(0, react.useEffect)(() => {
|
|
2209
|
+
setDraft(liveText);
|
|
2210
|
+
setDirty(false);
|
|
2211
|
+
setError(null);
|
|
2212
|
+
}, [store.id]);
|
|
2213
|
+
(0, react.useEffect)(() => {
|
|
2214
|
+
if (!dirty) setDraft(liveText);
|
|
2215
|
+
}, [liveText]);
|
|
2216
|
+
const onEdit = (value) => {
|
|
2217
|
+
setDraft(value);
|
|
2218
|
+
setDirty(true);
|
|
2219
|
+
setError(null);
|
|
2220
|
+
};
|
|
2221
|
+
const reload = () => {
|
|
2222
|
+
setDraft(liveText);
|
|
2223
|
+
setDirty(false);
|
|
2224
|
+
setError(null);
|
|
2225
|
+
};
|
|
2226
|
+
const apply = () => {
|
|
2227
|
+
let parsed;
|
|
2228
|
+
try {
|
|
2229
|
+
parsed = JSON.parse(draft);
|
|
2230
|
+
} catch (e) {
|
|
2231
|
+
setError(e instanceof Error ? e.message : "Invalid JSON");
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
onApply(store.id, parsed);
|
|
2235
|
+
setDirty(false);
|
|
2236
|
+
setError(null);
|
|
2237
|
+
};
|
|
2238
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2239
|
+
style: {
|
|
2240
|
+
flex: 1,
|
|
2241
|
+
display: "flex",
|
|
2242
|
+
flexDirection: "column",
|
|
2243
|
+
overflow: "hidden",
|
|
2244
|
+
minHeight: 0,
|
|
2245
|
+
background: DEVTOOL_DETAIL_BASE_BACKGROUND
|
|
2246
|
+
},
|
|
2247
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2248
|
+
style: {
|
|
2249
|
+
flex: 1,
|
|
2250
|
+
minHeight: 0,
|
|
2251
|
+
display: "flex",
|
|
2252
|
+
flexDirection: "column",
|
|
2253
|
+
padding: "10px 12px 8px"
|
|
2254
|
+
},
|
|
2255
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionLabel, { label: "Current state" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2256
|
+
style: {
|
|
2257
|
+
flex: 1,
|
|
2258
|
+
minHeight: 0,
|
|
2259
|
+
overflow: "auto"
|
|
2260
|
+
},
|
|
2261
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonView, {
|
|
2262
|
+
value: store.currentState,
|
|
2263
|
+
style: { minHeight: "100%" }
|
|
2264
|
+
})
|
|
2265
|
+
})]
|
|
2266
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2267
|
+
style: {
|
|
2268
|
+
flex: 1,
|
|
2269
|
+
minHeight: 0,
|
|
2270
|
+
display: "flex",
|
|
2271
|
+
flexDirection: "column",
|
|
2272
|
+
padding: "8px 12px 10px",
|
|
2273
|
+
borderTop: `1px solid ${DEVTOOL_PANEL_DIVIDER_BORDER}`
|
|
2274
|
+
},
|
|
2275
|
+
children: [
|
|
2276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2277
|
+
style: {
|
|
2278
|
+
display: "flex",
|
|
2279
|
+
alignItems: "center",
|
|
2280
|
+
justifyContent: "space-between",
|
|
2281
|
+
marginBottom: "3px"
|
|
2282
|
+
},
|
|
2283
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionLabel, {
|
|
2284
|
+
label: "Edit & apply",
|
|
2285
|
+
color: DEVTOOL_COLOR_SEMANTIC_WARNING
|
|
2286
|
+
}), dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2287
|
+
onClick: reload,
|
|
2288
|
+
style: linkButtonStyle("#64748b"),
|
|
2289
|
+
children: "reload from store"
|
|
2290
|
+
})]
|
|
2291
|
+
}),
|
|
2292
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
2293
|
+
value: draft,
|
|
2294
|
+
spellCheck: false,
|
|
2295
|
+
onChange: (e) => onEdit(e.target.value),
|
|
2296
|
+
style: {
|
|
2297
|
+
flex: 1,
|
|
2298
|
+
minHeight: 0,
|
|
2299
|
+
width: "100%",
|
|
2300
|
+
resize: "none",
|
|
2301
|
+
boxSizing: "border-box",
|
|
2302
|
+
background: DEVTOOL_EDITOR_BACKGROUND,
|
|
2303
|
+
color: DEVTOOL_COLOR_TEXT_SECONDARY,
|
|
2304
|
+
border: `1px solid ${error != null ? DEVTOOL_COLOR_SEMANTIC_ERROR : DEVTOOL_SECTION_BACKGROUND}`,
|
|
2305
|
+
borderRadius: "4px",
|
|
2306
|
+
padding: "8px 10px",
|
|
2307
|
+
fontFamily: MONO_FONT,
|
|
2308
|
+
fontSize: "11px",
|
|
2309
|
+
lineHeight: 1.5
|
|
2310
|
+
}
|
|
2311
|
+
}),
|
|
2312
|
+
error != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2313
|
+
style: {
|
|
2314
|
+
marginTop: "6px",
|
|
2315
|
+
padding: "6px 8px",
|
|
2316
|
+
borderRadius: "4px",
|
|
2317
|
+
background: "#1e0a0a",
|
|
2318
|
+
color: "#FF5C5C",
|
|
2319
|
+
fontFamily: "ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace",
|
|
2320
|
+
fontSize: "10px",
|
|
2321
|
+
flexShrink: 0
|
|
2322
|
+
},
|
|
2323
|
+
children: error
|
|
2324
|
+
}),
|
|
2325
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2326
|
+
style: {
|
|
2327
|
+
display: "flex",
|
|
2328
|
+
gap: "8px",
|
|
2329
|
+
marginTop: "8px",
|
|
2330
|
+
flexShrink: 0
|
|
2331
|
+
},
|
|
2332
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2333
|
+
onClick: apply,
|
|
2334
|
+
disabled: !dirty,
|
|
2335
|
+
style: {
|
|
2336
|
+
background: dirty ? DEVTOOL_COLOR_SEMANTIC_SUCCESS : DEVTOOL_SECTION_BACKGROUND,
|
|
2337
|
+
color: dirty ? "#0f172a" : DEVTOOL_COLOR_TEXT_MUTED,
|
|
2338
|
+
border: "none",
|
|
2339
|
+
borderRadius: "4px",
|
|
2340
|
+
cursor: dirty ? "pointer" : "not-allowed",
|
|
2341
|
+
fontSize: "11px",
|
|
2342
|
+
fontWeight: 600,
|
|
2343
|
+
padding: "5px 14px",
|
|
2344
|
+
fontFamily: SANS_FONT
|
|
2345
|
+
},
|
|
2346
|
+
children: "Apply to store"
|
|
2347
|
+
})
|
|
2348
|
+
})
|
|
2349
|
+
]
|
|
2350
|
+
})]
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
function linkButtonStyle(color) {
|
|
2354
|
+
return {
|
|
2355
|
+
background: "none",
|
|
2356
|
+
border: "none",
|
|
2357
|
+
color,
|
|
2358
|
+
cursor: "pointer",
|
|
2359
|
+
fontSize: "10px",
|
|
2360
|
+
padding: 0,
|
|
2361
|
+
fontFamily: SANS_FONT
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
//#endregion
|
|
2365
|
+
//#region src/devtools/browser/components/StoreTabs.tsx
|
|
2366
|
+
/**
|
|
2367
|
+
* Horizontal store filter. `null` selection means "all stores". When
|
|
2368
|
+
* `includeAll` is false (the State inspector needs a concrete store) the All
|
|
2369
|
+
* pill is omitted.
|
|
2370
|
+
*/
|
|
2371
|
+
function StoreTabs({ stores, selectedStoreId, onSelect, includeAll = true }) {
|
|
2372
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2373
|
+
style: {
|
|
2374
|
+
display: "flex",
|
|
2375
|
+
gap: "6px",
|
|
2376
|
+
padding: "6px 10px",
|
|
2377
|
+
overflowX: "auto",
|
|
2378
|
+
background: DEVTOOL_LIST_BASE_BACKGROUND,
|
|
2379
|
+
borderBottom: `1px solid ${DEVTOOL_SECTION_BACKGROUND}`,
|
|
2380
|
+
flexWrap: "wrap",
|
|
2381
|
+
flexShrink: 0
|
|
2382
|
+
},
|
|
2383
|
+
children: [
|
|
2384
|
+
includeAll && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pill, {
|
|
2385
|
+
active: selectedStoreId == null,
|
|
2386
|
+
onClick: () => onSelect(null),
|
|
2387
|
+
label: "all"
|
|
2388
|
+
}),
|
|
2389
|
+
stores.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2390
|
+
style: {
|
|
2391
|
+
color: "#64748b",
|
|
2392
|
+
fontSize: "11px",
|
|
2393
|
+
padding: "2px 0"
|
|
2394
|
+
},
|
|
2395
|
+
children: "no stores registered"
|
|
2396
|
+
}),
|
|
2397
|
+
stores.map((store) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pill, {
|
|
2398
|
+
active: selectedStoreId === store.id,
|
|
2399
|
+
onClick: () => onSelect(store.id),
|
|
2400
|
+
label: store.label,
|
|
2401
|
+
count: store.changeCount
|
|
2402
|
+
}, store.id))
|
|
2403
|
+
]
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
function Pill({ active, onClick, label, count }) {
|
|
2407
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2408
|
+
onClick,
|
|
2409
|
+
style: {
|
|
2410
|
+
display: "flex",
|
|
2411
|
+
alignItems: "center",
|
|
2412
|
+
gap: "5px",
|
|
2413
|
+
background: active ? DEVTOOL_SECTION_BACKGROUND : "transparent",
|
|
2414
|
+
color: active ? DEVTOOL_COLOR_SEMANTIC_SYSTEM : DEVTOOL_COLOR_TEXT_MUTED,
|
|
2415
|
+
border: `1px solid ${active ? DEVTOOL_COLOR_SEMANTIC_SYSTEM : DEVTOOL_COLOR_TEXT_FAINT}`,
|
|
2416
|
+
borderRadius: "5px",
|
|
2417
|
+
cursor: "pointer",
|
|
2418
|
+
fontSize: "11px",
|
|
2419
|
+
fontFamily: MONO_FONT,
|
|
2420
|
+
padding: "2px 8px",
|
|
2421
|
+
whiteSpace: "nowrap",
|
|
2422
|
+
flexShrink: 0
|
|
2423
|
+
},
|
|
2424
|
+
children: [label, count != null && count > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2425
|
+
style: {
|
|
2426
|
+
color: active ? "#38BDF8" : "#334155",
|
|
2427
|
+
fontSize: "10px"
|
|
2428
|
+
},
|
|
2429
|
+
children: count
|
|
2430
|
+
})]
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
//#endregion
|
|
2434
|
+
//#region src/devtools/browser/NiceStateDevtools.tsx
|
|
2435
|
+
if (typeof document !== "undefined" && !document.getElementById("__nice-state-devtools-styles")) {
|
|
2436
|
+
const style = document.createElement("style");
|
|
2437
|
+
style.id = "__nice-state-devtools-styles";
|
|
2438
|
+
style.textContent = `
|
|
2439
|
+
@keyframes __nice-state-pulse {
|
|
2440
|
+
0%, 100% { opacity: 1; }
|
|
2441
|
+
50% { opacity: 0.35; }
|
|
2442
|
+
}
|
|
2443
|
+
#__nice-state-devtools-panel ::-webkit-scrollbar { width: 4px; height: 4px; }
|
|
2444
|
+
#__nice-state-devtools-panel ::-webkit-scrollbar-track { background: transparent; }
|
|
2445
|
+
#__nice-state-devtools-panel ::-webkit-scrollbar-thumb { background: #334155; border-radius: 2px; }
|
|
2446
|
+
#__nice-state-devtools-panel ::-webkit-scrollbar-thumb:hover { background: #475569; }
|
|
2447
|
+
#__nice-state-devtools-panel ::-webkit-scrollbar-corner { background: transparent; }
|
|
2448
|
+
/* Shield the panel's native form controls from the host app's global element
|
|
2449
|
+
styles (e.g. a bare \`input {}\`/\`button {}\` rule). \`all: revert\` drops them
|
|
2450
|
+
to the UA baseline the panel is authored against; the panel's own inline
|
|
2451
|
+
styles still win, so its look is unchanged across any host. */
|
|
2452
|
+
#__nice-state-devtools-panel input,
|
|
2453
|
+
#__nice-state-devtools-panel button,
|
|
2454
|
+
#__nice-state-devtools-panel select,
|
|
2455
|
+
#__nice-state-devtools-panel textarea { all: revert; font-family: inherit; }
|
|
2456
|
+
`;
|
|
2457
|
+
document.head?.appendChild(style);
|
|
2458
|
+
}
|
|
2459
|
+
const PREFS_KEY = "__nice-state-devtools-prefs";
|
|
2460
|
+
const DOCKED_HEIGHT_DEFAULT = 340;
|
|
2461
|
+
const DOCKED_WIDTH_DEFAULT = 330;
|
|
2462
|
+
const DETAIL_RATIO_DEFAULT = .5;
|
|
2463
|
+
const DOCK_POSITIONS = [
|
|
2464
|
+
"dock-bottom",
|
|
2465
|
+
"dock-top",
|
|
2466
|
+
"dock-left",
|
|
2467
|
+
"dock-right"
|
|
2468
|
+
];
|
|
2469
|
+
function isDockPosition(value) {
|
|
2470
|
+
return typeof value === "string" && DOCK_POSITIONS.includes(value);
|
|
2471
|
+
}
|
|
2472
|
+
function readPrefs(defaultPosition, initialOpen) {
|
|
2473
|
+
const fallback = {
|
|
2474
|
+
position: defaultPosition,
|
|
2475
|
+
isOpen: initialOpen,
|
|
2476
|
+
dockedHeight: DOCKED_HEIGHT_DEFAULT,
|
|
2477
|
+
dockedWidth: DOCKED_WIDTH_DEFAULT,
|
|
2478
|
+
detailRatio: DETAIL_RATIO_DEFAULT,
|
|
2479
|
+
stayOnLatest: true,
|
|
2480
|
+
followLatestOnSelect: true,
|
|
2481
|
+
compressDiff: true,
|
|
2482
|
+
detailView: "props",
|
|
2483
|
+
diffLatestFirst: true
|
|
2484
|
+
};
|
|
2485
|
+
try {
|
|
2486
|
+
if (typeof localStorage === "undefined") return fallback;
|
|
2487
|
+
const stored = localStorage.getItem(PREFS_KEY);
|
|
2488
|
+
const merged = stored != null ? {
|
|
2489
|
+
...fallback,
|
|
2490
|
+
...JSON.parse(stored)
|
|
2491
|
+
} : fallback;
|
|
2492
|
+
if (!isDockPosition(merged.position)) merged.position = defaultPosition;
|
|
2493
|
+
return merged;
|
|
2494
|
+
} catch {
|
|
2495
|
+
return fallback;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
function writePrefs(prefs) {
|
|
2499
|
+
try {
|
|
2500
|
+
localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));
|
|
2501
|
+
} catch {
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
const EMPTY_SNAPSHOT = {
|
|
2506
|
+
stores: [],
|
|
2507
|
+
changes: [],
|
|
2508
|
+
paused: false
|
|
2509
|
+
};
|
|
2510
|
+
function NiceStateDevtools({ forceEnable, ...props }) {
|
|
2511
|
+
if (!forceEnable && true) return null;
|
|
2512
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NiceStateDevtools_Panel, { ...props });
|
|
2513
|
+
}
|
|
2514
|
+
function NiceStateDevtools_Panel({ core, position: defaultPosition = "dock-right", initialOpen = false }) {
|
|
2515
|
+
const [prefs, setPrefsRaw] = (0, react.useState)(() => readPrefs(defaultPosition, initialOpen));
|
|
2516
|
+
const [snapshot, setSnapshot] = (0, react.useState)(EMPTY_SNAPSHOT);
|
|
2517
|
+
const [mode, setMode] = (0, react.useState)("timeline");
|
|
2518
|
+
const [storeFilter, setStoreFilter] = (0, react.useState)(null);
|
|
2519
|
+
const [selectedChangeCuid, setSelectedChangeCuid] = (0, react.useState)(null);
|
|
2520
|
+
(0, react.useEffect)(() => core.subscribe(setSnapshot), [core]);
|
|
2521
|
+
const setPrefs = (update) => {
|
|
2522
|
+
setPrefsRaw((prev) => ({
|
|
2523
|
+
...prev,
|
|
2524
|
+
...update
|
|
2525
|
+
}));
|
|
2526
|
+
};
|
|
2527
|
+
(0, react.useEffect)(() => {
|
|
2528
|
+
const timer = setTimeout(() => writePrefs(prefs), 250);
|
|
2529
|
+
return () => clearTimeout(timer);
|
|
2530
|
+
}, [prefs]);
|
|
2531
|
+
const { stores, changes, paused } = snapshot;
|
|
2532
|
+
const { position, isOpen, dockedHeight, dockedWidth, detailRatio, stayOnLatest, followLatestOnSelect, compressDiff, detailView, diffLatestFirst } = prefs;
|
|
2533
|
+
const dockSide = getDockSide(position);
|
|
2534
|
+
const isHorizDock = dockSide === "top" || dockSide === "bottom";
|
|
2535
|
+
const dockedSize = isHorizDock ? dockedHeight : dockedWidth;
|
|
2536
|
+
const filteredChanges = (0, react.useMemo)(() => storeFilter == null ? changes : changes.filter((c) => c.storeId === storeFilter), [changes, storeFilter]);
|
|
2537
|
+
const groups = (0, react.useMemo)(() => groupChanges(filteredChanges), [filteredChanges]);
|
|
2538
|
+
const selectedChange = (0, react.useMemo)(() => {
|
|
2539
|
+
if (selectedChangeCuid == null) return null;
|
|
2540
|
+
const group = groups.find((g) => g.oldest.cuid === selectedChangeCuid);
|
|
2541
|
+
if (group == null) return changes.find((c) => c.cuid === selectedChangeCuid) ?? null;
|
|
2542
|
+
if (group.count > 1) return {
|
|
2543
|
+
...group.representative,
|
|
2544
|
+
prevSnapshot: group.oldest.prevSnapshot,
|
|
2545
|
+
inversePatches: group.oldest.inversePatches
|
|
2546
|
+
};
|
|
2547
|
+
return group.representative;
|
|
2548
|
+
}, [
|
|
2549
|
+
selectedChangeCuid,
|
|
2550
|
+
groups,
|
|
2551
|
+
changes
|
|
2552
|
+
]);
|
|
2553
|
+
const latestCuid = groups.length > 0 ? groups[0].oldest.cuid : null;
|
|
2554
|
+
(0, react.useEffect)(() => {
|
|
2555
|
+
if (stayOnLatest && latestCuid != null) setSelectedChangeCuid(latestCuid);
|
|
2556
|
+
}, [stayOnLatest, latestCuid]);
|
|
2557
|
+
const activeStore = stores.find((s) => s.id === storeFilter) ?? (stores.length > 0 ? stores[0] : null);
|
|
2558
|
+
const dock = (0, react.useMemo)(() => getDevtoolsDockCoordinator(), []);
|
|
2559
|
+
const panelId = (0, react.useId)();
|
|
2560
|
+
const [, bumpView] = (0, react.useReducer)((n) => n + 1, 0);
|
|
2561
|
+
const badge = changes.length > 0 ? String(changes.length) : void 0;
|
|
2562
|
+
(0, react.useEffect)(() => {
|
|
2563
|
+
const unregister = dock.register({
|
|
2564
|
+
id: panelId,
|
|
2565
|
+
label: "state",
|
|
2566
|
+
icon: "🧩",
|
|
2567
|
+
side: dockSide,
|
|
2568
|
+
size: dockedSize,
|
|
2569
|
+
open: isOpen,
|
|
2570
|
+
badge,
|
|
2571
|
+
onOpen: () => setPrefs({ isOpen: true })
|
|
2572
|
+
});
|
|
2573
|
+
const unsubscribe = dock.subscribe(bumpView);
|
|
2574
|
+
return () => {
|
|
2575
|
+
unregister();
|
|
2576
|
+
unsubscribe();
|
|
2577
|
+
};
|
|
2578
|
+
}, [dock, panelId]);
|
|
2579
|
+
(0, react.useEffect)(() => {
|
|
2580
|
+
dock.update(panelId, {
|
|
2581
|
+
side: dockSide,
|
|
2582
|
+
size: dockedSize,
|
|
2583
|
+
open: isOpen,
|
|
2584
|
+
badge
|
|
2585
|
+
});
|
|
2586
|
+
}, [
|
|
2587
|
+
dock,
|
|
2588
|
+
panelId,
|
|
2589
|
+
dockSide,
|
|
2590
|
+
dockedSize,
|
|
2591
|
+
isOpen,
|
|
2592
|
+
badge
|
|
2593
|
+
]);
|
|
2594
|
+
const view = dock.getView(panelId);
|
|
2595
|
+
const baseStyle = {
|
|
2596
|
+
position: "fixed",
|
|
2597
|
+
zIndex: 2147483646,
|
|
2598
|
+
fontFamily: "ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace",
|
|
2599
|
+
fontSize: "12px"
|
|
2600
|
+
};
|
|
2601
|
+
if (!isOpen) {
|
|
2602
|
+
if (view.isPrimary && !view.anyOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevtoolsLauncher, { items: view.devtools });
|
|
2603
|
+
return null;
|
|
2604
|
+
}
|
|
2605
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2606
|
+
id: "__nice-state-devtools-panel",
|
|
2607
|
+
style: {
|
|
2608
|
+
...baseStyle,
|
|
2609
|
+
background: DEVTOOL_LIST_BASE_BACKGROUND,
|
|
2610
|
+
border: `1px solid ${DEVTOOL_PANEL_BORDER}`,
|
|
2611
|
+
color: DEVTOOL_COLOR_TEXT_SECONDARY,
|
|
2612
|
+
display: "flex",
|
|
2613
|
+
flexDirection: "column",
|
|
2614
|
+
boxShadow: "0 -4px 24px rgba(0,0,0,0.4)",
|
|
2615
|
+
overflow: "hidden",
|
|
2616
|
+
...dockSide === "bottom" ? {
|
|
2617
|
+
bottom: view.dockOffset,
|
|
2618
|
+
left: 0,
|
|
2619
|
+
right: 0,
|
|
2620
|
+
height: `${dockedSize}px`,
|
|
2621
|
+
borderRadius: view.stacked ? "0" : "8px 8px 0 0"
|
|
2622
|
+
} : dockSide === "top" ? {
|
|
2623
|
+
top: view.dockOffset,
|
|
2624
|
+
left: 0,
|
|
2625
|
+
right: 0,
|
|
2626
|
+
height: `${dockedSize}px`,
|
|
2627
|
+
borderRadius: view.stacked ? "0" : "0 0 8px 8px"
|
|
2628
|
+
} : dockSide === "left" ? {
|
|
2629
|
+
top: 0,
|
|
2630
|
+
left: view.dockOffset,
|
|
2631
|
+
bottom: 0,
|
|
2632
|
+
width: `${dockedSize}px`,
|
|
2633
|
+
borderRadius: view.stacked ? "0" : "0 8px 8px 0"
|
|
2634
|
+
} : {
|
|
2635
|
+
top: 0,
|
|
2636
|
+
right: view.dockOffset,
|
|
2637
|
+
bottom: 0,
|
|
2638
|
+
width: `${dockedSize}px`,
|
|
2639
|
+
borderRadius: view.stacked ? "0" : "8px 0 0 8px"
|
|
2640
|
+
}
|
|
2641
|
+
},
|
|
2642
|
+
children: [
|
|
2643
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResizeHandle, {
|
|
2644
|
+
dockSide,
|
|
2645
|
+
dockedSize,
|
|
2646
|
+
onChange: (size) => setPrefs(isHorizDock ? { dockedHeight: size } : { dockedWidth: size })
|
|
2647
|
+
}),
|
|
2648
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PanelHeader, {
|
|
2649
|
+
title: "🧩 state",
|
|
2650
|
+
position,
|
|
2651
|
+
onPositionChange: (p) => setPrefs({ position: p }),
|
|
2652
|
+
onClose: () => setPrefs({ isOpen: false }),
|
|
2653
|
+
onClear: changes.length > 0 ? () => {
|
|
2654
|
+
core.clear();
|
|
2655
|
+
setSelectedChangeCuid(null);
|
|
2656
|
+
} : void 0,
|
|
2657
|
+
paused,
|
|
2658
|
+
onTogglePause: () => core.togglePaused(),
|
|
2659
|
+
openOthers: view.otherClosed,
|
|
2660
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SegmentedControl, {
|
|
2661
|
+
options: [{
|
|
2662
|
+
value: "timeline",
|
|
2663
|
+
label: "Timeline"
|
|
2664
|
+
}, {
|
|
2665
|
+
value: "state",
|
|
2666
|
+
label: "State"
|
|
2667
|
+
}],
|
|
2668
|
+
value: mode,
|
|
2669
|
+
onChange: setMode
|
|
2670
|
+
})
|
|
2671
|
+
}),
|
|
2672
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StoreTabs, {
|
|
2673
|
+
stores,
|
|
2674
|
+
selectedStoreId: mode === "state" ? activeStore?.id ?? null : storeFilter,
|
|
2675
|
+
onSelect: setStoreFilter,
|
|
2676
|
+
includeAll: mode === "timeline"
|
|
2677
|
+
}),
|
|
2678
|
+
mode === "state" ? activeStore != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateInspector, {
|
|
2679
|
+
store: activeStore,
|
|
2680
|
+
onApply: (id, next) => core.applyEdit(id, next)
|
|
2681
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyMessage, { children: "No stores registered. Call core.registerStore(...)." }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2682
|
+
style: {
|
|
2683
|
+
flex: 1,
|
|
2684
|
+
display: "flex",
|
|
2685
|
+
flexDirection: isHorizDock ? "row" : "column",
|
|
2686
|
+
overflow: "hidden",
|
|
2687
|
+
minHeight: 0
|
|
2688
|
+
},
|
|
2689
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2690
|
+
style: {
|
|
2691
|
+
flexGrow: selectedChange != null ? 1 - detailRatio : 1,
|
|
2692
|
+
flexShrink: 1,
|
|
2693
|
+
flexBasis: 0,
|
|
2694
|
+
minWidth: 0,
|
|
2695
|
+
minHeight: 0,
|
|
2696
|
+
display: "flex",
|
|
2697
|
+
flexDirection: "column",
|
|
2698
|
+
overflow: "hidden"
|
|
2699
|
+
},
|
|
2700
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowLatestToggles, {
|
|
2701
|
+
noun: "change",
|
|
2702
|
+
stayOnLatest,
|
|
2703
|
+
onStayOnLatestChange: (next) => setPrefs({ stayOnLatest: next }),
|
|
2704
|
+
followLatestOnSelect,
|
|
2705
|
+
onFollowLatestOnSelectChange: (next) => {
|
|
2706
|
+
if (next && latestCuid != null && selectedChangeCuid === latestCuid && !stayOnLatest) setPrefs({
|
|
2707
|
+
followLatestOnSelect: next,
|
|
2708
|
+
stayOnLatest: true
|
|
2709
|
+
});
|
|
2710
|
+
else setPrefs({ followLatestOnSelect: next });
|
|
2711
|
+
}
|
|
2712
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2713
|
+
style: {
|
|
2714
|
+
flex: 1,
|
|
2715
|
+
minHeight: 0,
|
|
2716
|
+
display: "flex",
|
|
2717
|
+
flexDirection: "column"
|
|
2718
|
+
},
|
|
2719
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChangeList, {
|
|
2720
|
+
style: {
|
|
2721
|
+
flex: 1,
|
|
2722
|
+
minHeight: 0,
|
|
2723
|
+
overflowY: "auto"
|
|
2724
|
+
},
|
|
2725
|
+
groups,
|
|
2726
|
+
selectedCuid: selectedChangeCuid,
|
|
2727
|
+
onSelect: (cuid) => {
|
|
2728
|
+
const next = selectedChangeCuid === cuid ? null : cuid;
|
|
2729
|
+
if (next != null && next === latestCuid && followLatestOnSelect) {
|
|
2730
|
+
if (!stayOnLatest) setPrefs({ stayOnLatest: true });
|
|
2731
|
+
} else if (stayOnLatest) setPrefs({ stayOnLatest: false });
|
|
2732
|
+
setSelectedChangeCuid(next);
|
|
2733
|
+
},
|
|
2734
|
+
showStore: storeFilter == null
|
|
2735
|
+
})
|
|
2736
|
+
})]
|
|
2737
|
+
}), selectedChange != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SplitHandle, {
|
|
2738
|
+
horizontal: isHorizDock,
|
|
2739
|
+
onRatioChange: (ratio) => setPrefs({ detailRatio: ratio })
|
|
2740
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2741
|
+
style: {
|
|
2742
|
+
flexGrow: detailRatio,
|
|
2743
|
+
flexShrink: 1,
|
|
2744
|
+
flexBasis: 0,
|
|
2745
|
+
minWidth: 0,
|
|
2746
|
+
minHeight: 0,
|
|
2747
|
+
display: "flex",
|
|
2748
|
+
flexDirection: "column",
|
|
2749
|
+
overflow: "hidden",
|
|
2750
|
+
...isHorizDock ? {
|
|
2751
|
+
borderLeft: `1px solid #1d3352`,
|
|
2752
|
+
boxShadow: "inset 18px 0 36px -14px rgba(0,0,0,0.8)"
|
|
2753
|
+
} : {
|
|
2754
|
+
borderTop: `1px solid #1d3352`,
|
|
2755
|
+
boxShadow: "inset 0 18px 36px -14px rgba(0,0,0,0.8)"
|
|
2756
|
+
}
|
|
2757
|
+
},
|
|
2758
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChangeDetailPanel, {
|
|
2759
|
+
change: selectedChange,
|
|
2760
|
+
onRevert: (c) => core.revertChange(c),
|
|
2761
|
+
view: detailView,
|
|
2762
|
+
onViewChange: (v) => setPrefs({ detailView: v }),
|
|
2763
|
+
compress: compressDiff,
|
|
2764
|
+
onCompressChange: (v) => setPrefs({ compressDiff: v }),
|
|
2765
|
+
latestFirst: diffLatestFirst,
|
|
2766
|
+
onLatestFirstChange: (v) => setPrefs({ diffLatestFirst: v })
|
|
2767
|
+
})
|
|
2768
|
+
})] })]
|
|
2769
|
+
})
|
|
2770
|
+
]
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
function EmptyMessage({ children }) {
|
|
2774
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2775
|
+
style: {
|
|
2776
|
+
flex: 1,
|
|
2777
|
+
display: "flex",
|
|
2778
|
+
alignItems: "center",
|
|
2779
|
+
justifyContent: "center",
|
|
2780
|
+
padding: "24px",
|
|
2781
|
+
textAlign: "center",
|
|
2782
|
+
color: DEVTOOL_COLOR_TEXT_MUTED,
|
|
2783
|
+
fontSize: "11px"
|
|
2784
|
+
},
|
|
2785
|
+
children
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
//#endregion
|
|
2789
|
+
exports.NiceStateDevtools = NiceStateDevtools;
|
|
2790
|
+
exports.StateDevtoolsCore = StateDevtoolsCore;
|
|
2791
|
+
|
|
2792
|
+
//# sourceMappingURL=index.cjs.map
|