@nice-code/state 0.10.0 → 0.12.0

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