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