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