@nice-code/state 0.4.3 → 0.4.5

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.
@@ -278,20 +278,55 @@ function walkDiff(path, before, after, out) {
278
278
  const inA = key in a;
279
279
  const next = [...path, key];
280
280
  if (inB && !inA) {
281
- out.push({ path: formatDiffPath(next), kind: "removed", before: b[key] });
281
+ out.push({ path: formatDiffPath(next), segments: next, kind: "removed", before: b[key] });
282
282
  } else if (!inB && inA) {
283
- out.push({ path: formatDiffPath(next), kind: "added", after: a[key] });
283
+ out.push({ path: formatDiffPath(next), segments: next, kind: "added", after: a[key] });
284
284
  } else {
285
285
  walkDiff(next, b[key], a[key], out);
286
286
  }
287
287
  }
288
288
  return;
289
289
  }
290
- out.push({ path: formatDiffPath(path), kind: "changed", before, after });
290
+ out.push({ path: formatDiffPath(path), segments: [...path], kind: "changed", before, after });
291
291
  }
292
292
  function formatDiffPath(path) {
293
293
  return path.length === 0 ? "(root)" : path.map((seg) => String(seg)).join(".");
294
294
  }
295
+ function computeLineDiff(beforeText, afterText) {
296
+ const a = beforeText.split(`
297
+ `);
298
+ const b = afterText.split(`
299
+ `);
300
+ const n = a.length;
301
+ const m = b.length;
302
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
303
+ for (let i2 = n - 1;i2 >= 0; i2--) {
304
+ for (let j2 = m - 1;j2 >= 0; j2--) {
305
+ lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
306
+ }
307
+ }
308
+ const ops = [];
309
+ let i = 0;
310
+ let j = 0;
311
+ while (i < n && j < m) {
312
+ if (a[i] === b[j]) {
313
+ ops.push({ kind: "common", text: a[i] });
314
+ i++;
315
+ j++;
316
+ } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
317
+ ops.push({ kind: "removed", text: a[i] });
318
+ i++;
319
+ } else {
320
+ ops.push({ kind: "added", text: b[j] });
321
+ j++;
322
+ }
323
+ }
324
+ while (i < n)
325
+ ops.push({ kind: "removed", text: a[i++] });
326
+ while (j < m)
327
+ ops.push({ kind: "added", text: b[j++] });
328
+ return ops;
329
+ }
295
330
  function isScalar(value) {
296
331
  return value == null || typeof value !== "object";
297
332
  }
@@ -379,6 +414,7 @@ function JsonView({
379
414
  import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
380
415
  var ADDED_BG = "rgba(163, 230, 53, 0.08)";
381
416
  var REMOVED_BG = "rgba(255, 92, 92, 0.08)";
417
+ var INLINE_MAX = 40;
382
418
  function DiffView({ before, after }) {
383
419
  const entries = computeDiff(before, after);
384
420
  if (entries.length === 0) {
@@ -394,16 +430,108 @@ function DiffView({ before, after }) {
394
430
  children: "No differences — before and after are structurally equal."
395
431
  }, undefined, false, undefined, this);
396
432
  }
433
+ const roots = [...buildTree(entries).children.values()].map(collapseChains);
397
434
  return /* @__PURE__ */ jsxDEV2("div", {
398
435
  style: { display: "flex", flexDirection: "column", gap: "6px" },
399
- children: entries.map((entry) => /* @__PURE__ */ jsxDEV2(DiffRow, {
400
- entry
401
- }, `${entry.kind}:${entry.path}`, false, undefined, this))
436
+ children: roots.map((node) => /* @__PURE__ */ jsxDEV2(DiffNode, {
437
+ node
438
+ }, node.key, false, undefined, this))
402
439
  }, undefined, false, undefined, this);
403
440
  }
404
- function DiffRow({ entry }) {
441
+ function buildTree(entries) {
442
+ const root = { key: "", children: new Map };
443
+ for (const entry of entries) {
444
+ const segments = entry.segments.length > 0 ? entry.segments : ["(root)"];
445
+ let node = root;
446
+ for (const seg of segments) {
447
+ const key = String(seg);
448
+ let child = node.children.get(key);
449
+ if (child == null) {
450
+ child = { key, children: new Map };
451
+ node.children.set(key, child);
452
+ }
453
+ node = child;
454
+ }
455
+ node.entry = entry;
456
+ }
457
+ return root;
458
+ }
459
+ function collapseChains(node) {
460
+ const children = new Map;
461
+ for (const child of node.children.values()) {
462
+ const collapsed = collapseChains(child);
463
+ children.set(collapsed.key, collapsed);
464
+ }
465
+ let current = { key: node.key, children, entry: node.entry };
466
+ while (current.entry == null && current.children.size === 1) {
467
+ const only = [...current.children.values()][0];
468
+ current = { key: `${current.key}.${only.key}`, children: only.children, entry: only.entry };
469
+ }
470
+ return current;
471
+ }
472
+ function DiffNode({ node }) {
473
+ if (node.entry != null && node.children.size === 0) {
474
+ return /* @__PURE__ */ jsxDEV2(DiffLeaf, {
475
+ label: node.key,
476
+ entry: node.entry
477
+ }, undefined, false, undefined, this);
478
+ }
479
+ return /* @__PURE__ */ jsxDEV2("div", {
480
+ children: [
481
+ /* @__PURE__ */ jsxDEV2("div", {
482
+ style: { color: DEVTOOL_JSON_KEY, fontFamily: MONO_FONT, fontSize: "10px", fontWeight: 600 },
483
+ children: node.key
484
+ }, undefined, false, undefined, this),
485
+ /* @__PURE__ */ jsxDEV2("div", {
486
+ style: {
487
+ marginTop: "4px",
488
+ marginLeft: "5px",
489
+ paddingLeft: "8px",
490
+ borderLeft: `1px solid ${DEVTOOL_SECTION_STRING_BACKGROUND}`,
491
+ display: "flex",
492
+ flexDirection: "column",
493
+ gap: "5px"
494
+ },
495
+ children: [...node.children.values()].map((child) => /* @__PURE__ */ jsxDEV2(DiffNode, {
496
+ node: child
497
+ }, child.key, false, undefined, this))
498
+ }, undefined, false, undefined, this)
499
+ ]
500
+ }, undefined, true, undefined, this);
501
+ }
502
+ function DiffLeaf({ label, entry }) {
405
503
  const showRemoved = entry.kind === "removed" || entry.kind === "changed";
406
504
  const showAdded = entry.kind === "added" || entry.kind === "changed";
505
+ if (isInlineLeaf(entry)) {
506
+ return /* @__PURE__ */ jsxDEV2("div", {
507
+ style: { display: "flex", flexWrap: "wrap", alignItems: "baseline", gap: "6px" },
508
+ children: [
509
+ /* @__PURE__ */ jsxDEV2("span", {
510
+ style: { color: DEVTOOL_JSON_KEY, fontFamily: MONO_FONT, fontSize: "11px" },
511
+ children: [
512
+ label,
513
+ ":"
514
+ ]
515
+ }, undefined, true, undefined, this),
516
+ showRemoved && /* @__PURE__ */ jsxDEV2(InlineValue, {
517
+ sign: "−",
518
+ color: DEVTOOL_COLOR_SEMANTIC_ERROR,
519
+ background: REMOVED_BG,
520
+ value: entry.before
521
+ }, undefined, false, undefined, this),
522
+ entry.kind === "changed" && /* @__PURE__ */ jsxDEV2("span", {
523
+ style: { color: DEVTOOL_COLOR_TEXT_MUTED },
524
+ children: "→"
525
+ }, undefined, false, undefined, this),
526
+ showAdded && /* @__PURE__ */ jsxDEV2(InlineValue, {
527
+ sign: "+",
528
+ color: DEVTOOL_COLOR_SEMANTIC_SUCCESS,
529
+ background: ADDED_BG,
530
+ value: entry.after
531
+ }, undefined, false, undefined, this)
532
+ ]
533
+ }, undefined, true, undefined, this);
534
+ }
407
535
  return /* @__PURE__ */ jsxDEV2("div", {
408
536
  style: {
409
537
  borderRadius: "4px",
@@ -415,11 +543,11 @@ function DiffRow({ entry }) {
415
543
  style: {
416
544
  padding: "3px 8px",
417
545
  background: DEVTOOL_SECTION_STRING_BACKGROUND,
418
- color: DEVTOOL_COLOR_TEXT_SECONDARY,
546
+ color: DEVTOOL_JSON_KEY,
419
547
  fontFamily: MONO_FONT,
420
548
  fontSize: "10px"
421
549
  },
422
- children: entry.path
550
+ children: label
423
551
  }, undefined, false, undefined, this),
424
552
  showRemoved && /* @__PURE__ */ jsxDEV2(DiffLine, {
425
553
  sign: "−",
@@ -436,6 +564,36 @@ function DiffRow({ entry }) {
436
564
  ]
437
565
  }, undefined, true, undefined, this);
438
566
  }
567
+ function InlineValue({
568
+ sign,
569
+ color,
570
+ background,
571
+ value
572
+ }) {
573
+ return /* @__PURE__ */ jsxDEV2("span", {
574
+ style: {
575
+ display: "inline-flex",
576
+ alignItems: "baseline",
577
+ gap: "4px",
578
+ padding: "1px 6px",
579
+ borderRadius: "3px",
580
+ background,
581
+ fontFamily: MONO_FONT,
582
+ fontSize: "11px",
583
+ whiteSpace: "pre-wrap",
584
+ wordBreak: "break-word"
585
+ },
586
+ children: [
587
+ /* @__PURE__ */ jsxDEV2("span", {
588
+ style: { color, fontWeight: 700, userSelect: "none" },
589
+ children: sign
590
+ }, undefined, false, undefined, this),
591
+ /* @__PURE__ */ jsxDEV2("span", {
592
+ children: renderColoredJson(compactStringify(value))
593
+ }, undefined, false, undefined, this)
594
+ ]
595
+ }, undefined, true, undefined, this);
596
+ }
439
597
  function DiffLine({
440
598
  sign,
441
599
  color,
@@ -464,12 +622,112 @@ function DiffLine({
464
622
  ]
465
623
  }, undefined, true, undefined, this);
466
624
  }
625
+ function isInlineLeaf(entry) {
626
+ const sides = [];
627
+ if (entry.kind !== "added")
628
+ sides.push(compactStringify(entry.before));
629
+ if (entry.kind !== "removed")
630
+ sides.push(compactStringify(entry.after));
631
+ return sides.every((s) => s.length <= INLINE_MAX && !s.includes(`
632
+ `));
633
+ }
634
+ function compactStringify(value) {
635
+ if (value === undefined)
636
+ return "undefined";
637
+ try {
638
+ return JSON.stringify(value) ?? "undefined";
639
+ } catch {
640
+ return String(value);
641
+ }
642
+ }
643
+
644
+ // src/devtools/browser/components/JsonDiffView.tsx
645
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
646
+ var ADDED_BG2 = "rgba(163, 230, 53, 0.13)";
647
+ var REMOVED_BG2 = "rgba(255, 92, 92, 0.13)";
648
+ var SURFACE_STYLE = {
649
+ margin: 0,
650
+ padding: "8px 0",
651
+ borderRadius: "4px",
652
+ fontSize: "11px",
653
+ lineHeight: 1.5,
654
+ fontFamily: MONO_FONT,
655
+ background: DEVTOOL_SECTION_STRING_BACKGROUND,
656
+ overflowX: "auto"
657
+ };
658
+ function emptyNotice(text) {
659
+ return /* @__PURE__ */ jsxDEV3("div", {
660
+ style: {
661
+ padding: "10px",
662
+ borderRadius: "4px",
663
+ background: DEVTOOL_SECTION_STRING_BACKGROUND,
664
+ color: DEVTOOL_COLOR_TEXT_MUTED,
665
+ fontSize: "11px",
666
+ fontStyle: "italic"
667
+ },
668
+ children: text
669
+ }, undefined, false, undefined, this);
670
+ }
671
+ function Line({ sign, color, background, text }) {
672
+ return /* @__PURE__ */ jsxDEV3("div", {
673
+ style: { display: "flex", background, padding: "0 10px", whiteSpace: "pre" },
674
+ children: [
675
+ /* @__PURE__ */ jsxDEV3("span", {
676
+ style: { width: "12px", flexShrink: 0, color, fontWeight: 700, userSelect: "none" },
677
+ children: sign
678
+ }, undefined, false, undefined, this),
679
+ /* @__PURE__ */ jsxDEV3("span", {
680
+ style: { flex: 1, minWidth: 0 },
681
+ children: renderColoredJson(text)
682
+ }, undefined, false, undefined, this)
683
+ ]
684
+ }, undefined, true, undefined, this);
685
+ }
686
+ function JsonDiffView({ before, after }) {
687
+ const ops = computeLineDiff(safeStringify(before, 2), safeStringify(after, 2));
688
+ if (!ops.some((op) => op.kind !== "common")) {
689
+ return emptyNotice("No differences — before and after are structurally equal.");
690
+ }
691
+ return /* @__PURE__ */ jsxDEV3("pre", {
692
+ style: SURFACE_STYLE,
693
+ children: ops.map((op, i) => /* @__PURE__ */ jsxDEV3(Line, {
694
+ sign: op.kind === "removed" ? "−" : op.kind === "added" ? "+" : " ",
695
+ color: op.kind === "removed" ? DEVTOOL_COLOR_SEMANTIC_ERROR : DEVTOOL_COLOR_SEMANTIC_SUCCESS,
696
+ background: op.kind === "removed" ? REMOVED_BG2 : op.kind === "added" ? ADDED_BG2 : "transparent",
697
+ text: op.text
698
+ }, i, false, undefined, this))
699
+ }, undefined, false, undefined, this);
700
+ }
701
+ function HighlightedJsonView({
702
+ before,
703
+ after,
704
+ side
705
+ }) {
706
+ const ops = computeLineDiff(safeStringify(before, 2), safeStringify(after, 2));
707
+ const dropKind = side === "before" ? "added" : "removed";
708
+ const highlightKind = side === "before" ? "removed" : "added";
709
+ const background = side === "before" ? REMOVED_BG2 : ADDED_BG2;
710
+ const color = side === "before" ? DEVTOOL_COLOR_SEMANTIC_ERROR : DEVTOOL_COLOR_SEMANTIC_SUCCESS;
711
+ const visible = ops.filter((op) => op.kind !== dropKind);
712
+ return /* @__PURE__ */ jsxDEV3("pre", {
713
+ style: SURFACE_STYLE,
714
+ children: visible.map((op, i) => {
715
+ const changed = op.kind === highlightKind;
716
+ return /* @__PURE__ */ jsxDEV3(Line, {
717
+ sign: changed ? side === "before" ? "−" : "+" : " ",
718
+ color,
719
+ background: changed ? background : "transparent",
720
+ text: op.text
721
+ }, i, false, undefined, this);
722
+ })
723
+ }, undefined, false, undefined, this);
724
+ }
467
725
 
468
726
  // src/devtools/browser/components/PanelChrome.tsx
469
727
  import {
470
728
  useState
471
729
  } from "react";
472
- import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
730
+ import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
473
731
  var DOCKED_SIZE_MIN = 160;
474
732
  var POSITION_GRID = [
475
733
  { key: "tl", pos: null },
@@ -504,7 +762,7 @@ function PanelHeader({
504
762
  openOthers,
505
763
  children
506
764
  }) {
507
- return /* @__PURE__ */ jsxDEV3("div", {
765
+ return /* @__PURE__ */ jsxDEV4("div", {
508
766
  style: {
509
767
  display: "flex",
510
768
  alignItems: "center",
@@ -516,10 +774,10 @@ function PanelHeader({
516
774
  flexShrink: 0
517
775
  },
518
776
  children: [
519
- /* @__PURE__ */ jsxDEV3("div", {
777
+ /* @__PURE__ */ jsxDEV4("div", {
520
778
  style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0 },
521
779
  children: [
522
- /* @__PURE__ */ jsxDEV3("span", {
780
+ /* @__PURE__ */ jsxDEV4("span", {
523
781
  style: {
524
782
  color: DEVTOOL_COLOR_SEMANTIC_SYSTEM,
525
783
  fontWeight: "bold",
@@ -528,7 +786,7 @@ function PanelHeader({
528
786
  },
529
787
  children: "\uD83E\uDDE9 state"
530
788
  }, undefined, false, undefined, this),
531
- openOthers?.map((item) => /* @__PURE__ */ jsxDEV3("button", {
789
+ openOthers?.map((item) => /* @__PURE__ */ jsxDEV4("button", {
532
790
  onClick: item.onOpen,
533
791
  title: `Open ${item.label} devtools`,
534
792
  style: {
@@ -546,13 +804,13 @@ function PanelHeader({
546
804
  whiteSpace: "nowrap"
547
805
  },
548
806
  children: [
549
- /* @__PURE__ */ jsxDEV3("span", {
807
+ /* @__PURE__ */ jsxDEV4("span", {
550
808
  children: item.icon
551
809
  }, undefined, false, undefined, this),
552
- /* @__PURE__ */ jsxDEV3("span", {
810
+ /* @__PURE__ */ jsxDEV4("span", {
553
811
  children: item.label
554
812
  }, undefined, false, undefined, this),
555
- item.badge != null && /* @__PURE__ */ jsxDEV3("span", {
813
+ item.badge != null && /* @__PURE__ */ jsxDEV4("span", {
556
814
  style: { color: DEVTOOL_COLOR_SEMANTIC_SYSTEM },
557
815
  children: item.badge
558
816
  }, undefined, false, undefined, this)
@@ -560,26 +818,26 @@ function PanelHeader({
560
818
  }, item.id, true, undefined, this))
561
819
  ]
562
820
  }, undefined, true, undefined, this),
563
- /* @__PURE__ */ jsxDEV3("div", {
821
+ /* @__PURE__ */ jsxDEV4("div", {
564
822
  style: { display: "flex", gap: "10px", alignItems: "center" },
565
823
  children: [
566
824
  children,
567
- /* @__PURE__ */ jsxDEV3("button", {
825
+ /* @__PURE__ */ jsxDEV4("button", {
568
826
  onClick: onTogglePause,
569
827
  title: paused ? "Resume recording" : "Pause recording",
570
828
  style: chromeButtonStyle(paused ? DEVTOOL_COLOR_SEMANTIC_SYSTEM : DEVTOOL_COLOR_TEXT_MUTED),
571
829
  children: paused ? "▶ resume" : "⏸ pause"
572
830
  }, undefined, false, undefined, this),
573
- onClear != null && /* @__PURE__ */ jsxDEV3("button", {
831
+ onClear != null && /* @__PURE__ */ jsxDEV4("button", {
574
832
  onClick: onClear,
575
833
  style: chromeButtonStyle(DEVTOOL_COLOR_TEXT_MUTED),
576
834
  children: "clear"
577
835
  }, undefined, false, undefined, this),
578
- /* @__PURE__ */ jsxDEV3(PositionPicker, {
836
+ /* @__PURE__ */ jsxDEV4(PositionPicker, {
579
837
  position,
580
838
  onChange: onPositionChange
581
839
  }, undefined, false, undefined, this),
582
- /* @__PURE__ */ jsxDEV3("button", {
840
+ /* @__PURE__ */ jsxDEV4("button", {
583
841
  onClick: onClose,
584
842
  style: {
585
843
  ...chromeButtonStyle(DEVTOOL_COLOR_TEXT_MUTED),
@@ -609,17 +867,17 @@ function PositionPicker({
609
867
  position,
610
868
  onChange
611
869
  }) {
612
- return /* @__PURE__ */ jsxDEV3("div", {
870
+ return /* @__PURE__ */ jsxDEV4("div", {
613
871
  title: "Move / dock panel",
614
872
  style: { display: "grid", gridTemplateColumns: "repeat(3, 9px)", gap: "2px", padding: "2px" },
615
873
  children: POSITION_GRID.map(({ key, pos }) => {
616
874
  if (pos == null)
617
- return /* @__PURE__ */ jsxDEV3("div", {
875
+ return /* @__PURE__ */ jsxDEV4("div", {
618
876
  style: { width: "9px", height: "9px" }
619
877
  }, key, false, undefined, this);
620
878
  const isTopBottom = pos === "dock-top" || pos === "dock-bottom";
621
879
  const isActive = pos === position;
622
- return /* @__PURE__ */ jsxDEV3("div", {
880
+ return /* @__PURE__ */ jsxDEV4("div", {
623
881
  title: pos,
624
882
  onClick: () => onChange(pos),
625
883
  style: {
@@ -630,7 +888,7 @@ function PositionPicker({
630
888
  justifyContent: "center",
631
889
  cursor: "pointer"
632
890
  },
633
- children: /* @__PURE__ */ jsxDEV3("div", {
891
+ children: /* @__PURE__ */ jsxDEV4("div", {
634
892
  style: {
635
893
  width: isTopBottom ? "9px" : "3px",
636
894
  height: isTopBottom ? "3px" : "9px",
@@ -666,7 +924,7 @@ function ResizeHandle({
666
924
  window.addEventListener("mouseup", onUp);
667
925
  };
668
926
  const edgeStyle = dockSide === "bottom" ? { top: 0, left: 0, right: 0, height: "5px", cursor: "ns-resize" } : dockSide === "top" ? { bottom: 0, left: 0, right: 0, height: "5px", cursor: "ns-resize" } : dockSide === "right" ? { top: 0, bottom: 0, left: 0, width: "5px", cursor: "ew-resize" } : { top: 0, bottom: 0, right: 0, width: "5px", cursor: "ew-resize" };
669
- return /* @__PURE__ */ jsxDEV3("div", {
927
+ return /* @__PURE__ */ jsxDEV4("div", {
670
928
  onMouseDown,
671
929
  style: { position: "absolute", zIndex: 10, background: "transparent", ...edgeStyle }
672
930
  }, undefined, false, undefined, this);
@@ -695,7 +953,7 @@ function SplitHandle({
695
953
  window.addEventListener("mousemove", onMove);
696
954
  window.addEventListener("mouseup", onUp);
697
955
  };
698
- return /* @__PURE__ */ jsxDEV3("div", {
956
+ return /* @__PURE__ */ jsxDEV4("div", {
699
957
  onMouseDown,
700
958
  onMouseEnter: () => setHovered(true),
701
959
  onMouseLeave: () => setHovered(false),
@@ -714,7 +972,7 @@ function SegmentedControl({
714
972
  value,
715
973
  onChange
716
974
  }) {
717
- return /* @__PURE__ */ jsxDEV3("div", {
975
+ return /* @__PURE__ */ jsxDEV4("div", {
718
976
  style: {
719
977
  display: "flex",
720
978
  border: `1px solid ${DEVTOOL_COLOR_TEXT_FAINT}`,
@@ -723,7 +981,7 @@ function SegmentedControl({
723
981
  },
724
982
  children: options.map((opt) => {
725
983
  const active = opt.value === value;
726
- return /* @__PURE__ */ jsxDEV3("button", {
984
+ return /* @__PURE__ */ jsxDEV4("button", {
727
985
  onClick: () => onChange(opt.value),
728
986
  style: {
729
987
  background: active ? DEVTOOL_COLOR_SEMANTIC_SYSTEM : "transparent",
@@ -741,7 +999,7 @@ function SegmentedControl({
741
999
  }, undefined, false, undefined, this);
742
1000
  }
743
1001
  function DevtoolsLauncher({ items }) {
744
- return /* @__PURE__ */ jsxDEV3("div", {
1002
+ return /* @__PURE__ */ jsxDEV4("div", {
745
1003
  style: {
746
1004
  position: "fixed",
747
1005
  bottom: "16px",
@@ -751,7 +1009,7 @@ function DevtoolsLauncher({ items }) {
751
1009
  fontFamily: MONO_FONT,
752
1010
  fontSize: "12px"
753
1011
  },
754
- children: /* @__PURE__ */ jsxDEV3("div", {
1012
+ children: /* @__PURE__ */ jsxDEV4("div", {
755
1013
  style: {
756
1014
  display: "flex",
757
1015
  background: DEVTOOL_SECTION_BACKGROUND,
@@ -760,7 +1018,7 @@ function DevtoolsLauncher({ items }) {
760
1018
  overflow: "hidden",
761
1019
  boxShadow: "0 8px 24px rgba(0,0,0,0.35)"
762
1020
  },
763
- children: items.map((item, i) => /* @__PURE__ */ jsxDEV3("button", {
1021
+ children: items.map((item, i) => /* @__PURE__ */ jsxDEV4("button", {
764
1022
  onClick: item.onOpen,
765
1023
  style: {
766
1024
  display: "flex",
@@ -777,13 +1035,13 @@ function DevtoolsLauncher({ items }) {
777
1035
  lineHeight: "1.5"
778
1036
  },
779
1037
  children: [
780
- /* @__PURE__ */ jsxDEV3("span", {
1038
+ /* @__PURE__ */ jsxDEV4("span", {
781
1039
  children: item.icon
782
1040
  }, undefined, false, undefined, this),
783
- /* @__PURE__ */ jsxDEV3("span", {
1041
+ /* @__PURE__ */ jsxDEV4("span", {
784
1042
  children: item.label
785
1043
  }, undefined, false, undefined, this),
786
- item.badge != null && /* @__PURE__ */ jsxDEV3("span", {
1044
+ item.badge != null && /* @__PURE__ */ jsxDEV4("span", {
787
1045
  style: { color: DEVTOOL_COLOR_SEMANTIC_SYSTEM },
788
1046
  children: item.badge
789
1047
  }, undefined, false, undefined, this)
@@ -794,14 +1052,14 @@ function DevtoolsLauncher({ items }) {
794
1052
  }
795
1053
 
796
1054
  // src/devtools/browser/components/ChangeDetailPanel.tsx
797
- import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
1055
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
798
1056
  function ChangeDetailPanel({
799
1057
  change,
800
1058
  onRevert
801
1059
  }) {
802
- const [view, setView] = useState2("diff");
1060
+ const [view, setView] = useState2("props");
803
1061
  const canRevert = change.inversePatches.length > 0;
804
- return /* @__PURE__ */ jsxDEV4("div", {
1062
+ return /* @__PURE__ */ jsxDEV5("div", {
805
1063
  style: {
806
1064
  flex: 1,
807
1065
  display: "flex",
@@ -811,7 +1069,7 @@ function ChangeDetailPanel({
811
1069
  background: DEVTOOL_DETAIL_BASE_BACKGROUND
812
1070
  },
813
1071
  children: [
814
- /* @__PURE__ */ jsxDEV4("div", {
1072
+ /* @__PURE__ */ jsxDEV5("div", {
815
1073
  style: {
816
1074
  padding: "8px 12px",
817
1075
  background: DEVTOOL_DETAIL_HEADER_BACKGROUND,
@@ -822,7 +1080,7 @@ function ChangeDetailPanel({
822
1080
  flexShrink: 0
823
1081
  },
824
1082
  children: [
825
- /* @__PURE__ */ jsxDEV4("span", {
1083
+ /* @__PURE__ */ jsxDEV5("span", {
826
1084
  style: {
827
1085
  color: DEVTOOL_COLOR_TEXT_EMPHASIS,
828
1086
  fontWeight: 600,
@@ -834,7 +1092,7 @@ function ChangeDetailPanel({
834
1092
  },
835
1093
  children: change.storeLabel
836
1094
  }, undefined, false, undefined, this),
837
- /* @__PURE__ */ jsxDEV4("span", {
1095
+ /* @__PURE__ */ jsxDEV5("span", {
838
1096
  style: {
839
1097
  padding: "1px 7px",
840
1098
  borderRadius: "999px",
@@ -847,14 +1105,14 @@ function ChangeDetailPanel({
847
1105
  },
848
1106
  children: SOURCE_LABEL[change.source]
849
1107
  }, undefined, false, undefined, this),
850
- /* @__PURE__ */ jsxDEV4("span", {
1108
+ /* @__PURE__ */ jsxDEV5("span", {
851
1109
  style: { flex: 1 }
852
1110
  }, undefined, false, undefined, this),
853
- /* @__PURE__ */ jsxDEV4("span", {
1111
+ /* @__PURE__ */ jsxDEV5("span", {
854
1112
  style: { color: DEVTOOL_COLOR_TEXT_MUTED, fontSize: "10px", fontFamily: MONO_FONT },
855
1113
  children: formatTimestamp(change.timestamp)
856
1114
  }, undefined, false, undefined, this),
857
- /* @__PURE__ */ jsxDEV4("button", {
1115
+ /* @__PURE__ */ jsxDEV5("button", {
858
1116
  onClick: () => onRevert(change),
859
1117
  disabled: !canRevert,
860
1118
  title: canRevert ? "Apply this change's inverse patches to undo it" : "No inverse patches available to revert",
@@ -873,14 +1131,15 @@ function ChangeDetailPanel({
873
1131
  }, undefined, false, undefined, this)
874
1132
  ]
875
1133
  }, undefined, true, undefined, this),
876
- /* @__PURE__ */ jsxDEV4("div", {
1134
+ /* @__PURE__ */ jsxDEV5("div", {
877
1135
  style: {
878
1136
  padding: "6px 12px",
879
1137
  borderBottom: `1px solid ${DEVTOOL_LIST_BASE_BACKGROUND}`,
880
1138
  flexShrink: 0
881
1139
  },
882
- children: /* @__PURE__ */ jsxDEV4(SegmentedControl, {
1140
+ children: /* @__PURE__ */ jsxDEV5(SegmentedControl, {
883
1141
  options: [
1142
+ { value: "props", label: "Diff Props" },
884
1143
  { value: "diff", label: "Diff View" },
885
1144
  { value: "before", label: "Before" },
886
1145
  { value: "after", label: "After" }
@@ -889,7 +1148,7 @@ function ChangeDetailPanel({
889
1148
  onChange: setView
890
1149
  }, undefined, false, undefined, this)
891
1150
  }, undefined, false, undefined, this),
892
- /* @__PURE__ */ jsxDEV4("div", {
1151
+ /* @__PURE__ */ jsxDEV5("div", {
893
1152
  style: {
894
1153
  flex: 1,
895
1154
  overflowY: "auto",
@@ -897,15 +1156,23 @@ function ChangeDetailPanel({
897
1156
  padding: "10px 12px"
898
1157
  },
899
1158
  children: [
900
- view === "diff" && /* @__PURE__ */ jsxDEV4(DiffView, {
1159
+ view === "props" && /* @__PURE__ */ jsxDEV5(DiffView, {
901
1160
  before: change.prevSnapshot,
902
1161
  after: change.snapshot
903
1162
  }, undefined, false, undefined, this),
904
- view === "before" && /* @__PURE__ */ jsxDEV4(JsonView, {
905
- value: change.prevSnapshot
1163
+ view === "diff" && /* @__PURE__ */ jsxDEV5(JsonDiffView, {
1164
+ before: change.prevSnapshot,
1165
+ after: change.snapshot
1166
+ }, undefined, false, undefined, this),
1167
+ view === "before" && /* @__PURE__ */ jsxDEV5(HighlightedJsonView, {
1168
+ before: change.prevSnapshot,
1169
+ after: change.snapshot,
1170
+ side: "before"
906
1171
  }, undefined, false, undefined, this),
907
- view === "after" && /* @__PURE__ */ jsxDEV4(JsonView, {
908
- value: change.snapshot
1172
+ view === "after" && /* @__PURE__ */ jsxDEV5(HighlightedJsonView, {
1173
+ before: change.prevSnapshot,
1174
+ after: change.snapshot,
1175
+ side: "after"
909
1176
  }, undefined, false, undefined, this)
910
1177
  ]
911
1178
  }, undefined, true, undefined, this)
@@ -914,7 +1181,7 @@ function ChangeDetailPanel({
914
1181
  }
915
1182
 
916
1183
  // src/devtools/browser/components/ChangeList.tsx
917
- import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
1184
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
918
1185
  function ChangeList({
919
1186
  changes,
920
1187
  selectedCuid,
@@ -923,7 +1190,7 @@ function ChangeList({
923
1190
  style
924
1191
  }) {
925
1192
  if (changes.length === 0) {
926
- return /* @__PURE__ */ jsxDEV5("div", {
1193
+ return /* @__PURE__ */ jsxDEV6("div", {
927
1194
  style: {
928
1195
  padding: "24px",
929
1196
  textAlign: "center",
@@ -934,9 +1201,9 @@ function ChangeList({
934
1201
  children: "No changes recorded yet. Mutate a store to see it here."
935
1202
  }, undefined, false, undefined, this);
936
1203
  }
937
- return /* @__PURE__ */ jsxDEV5("div", {
1204
+ return /* @__PURE__ */ jsxDEV6("div", {
938
1205
  style,
939
- children: changes.map((change) => /* @__PURE__ */ jsxDEV5(ChangeRow, {
1206
+ children: changes.map((change) => /* @__PURE__ */ jsxDEV6(ChangeRow, {
940
1207
  change,
941
1208
  selected: change.cuid === selectedCuid,
942
1209
  onClick: () => onSelect(change.cuid),
@@ -951,7 +1218,7 @@ function ChangeRow({
951
1218
  showStore
952
1219
  }) {
953
1220
  const sourceColor = SOURCE_COLOR[change.source];
954
- return /* @__PURE__ */ jsxDEV5("div", {
1221
+ return /* @__PURE__ */ jsxDEV6("div", {
955
1222
  onClick,
956
1223
  style: {
957
1224
  display: "flex",
@@ -964,10 +1231,10 @@ function ChangeRow({
964
1231
  background: selected ? DEVTOOL_LIST_SELECTED_BACKGROUND : DEVTOOL_LIST_BASE_BACKGROUND
965
1232
  },
966
1233
  children: [
967
- /* @__PURE__ */ jsxDEV5("div", {
1234
+ /* @__PURE__ */ jsxDEV6("div", {
968
1235
  style: { display: "flex", alignItems: "center", gap: "6px" },
969
1236
  children: [
970
- /* @__PURE__ */ jsxDEV5("span", {
1237
+ /* @__PURE__ */ jsxDEV6("span", {
971
1238
  style: {
972
1239
  color: DEVTOOL_COLOR_TEXT_MUTED,
973
1240
  fontSize: "10px",
@@ -976,11 +1243,11 @@ function ChangeRow({
976
1243
  },
977
1244
  children: formatTimestamp(change.timestamp)
978
1245
  }, undefined, false, undefined, this),
979
- /* @__PURE__ */ jsxDEV5(Badge, {
1246
+ /* @__PURE__ */ jsxDEV6(Badge, {
980
1247
  color: sourceColor,
981
1248
  children: SOURCE_LABEL[change.source]
982
1249
  }, undefined, false, undefined, this),
983
- showStore && /* @__PURE__ */ jsxDEV5("span", {
1250
+ showStore && /* @__PURE__ */ jsxDEV6("span", {
984
1251
  style: {
985
1252
  color: DEVTOOL_COLOR_SEMANTIC_SYSTEM,
986
1253
  fontSize: "10px",
@@ -991,10 +1258,10 @@ function ChangeRow({
991
1258
  },
992
1259
  children: change.storeLabel
993
1260
  }, undefined, false, undefined, this),
994
- /* @__PURE__ */ jsxDEV5("span", {
1261
+ /* @__PURE__ */ jsxDEV6("span", {
995
1262
  style: { flex: 1 }
996
1263
  }, undefined, false, undefined, this),
997
- change.patches.length > 0 && /* @__PURE__ */ jsxDEV5("span", {
1264
+ change.patches.length > 0 && /* @__PURE__ */ jsxDEV6("span", {
998
1265
  style: { color: DEVTOOL_COLOR_TEXT_FAINT, fontSize: "10px", flexShrink: 0 },
999
1266
  children: [
1000
1267
  change.patches.length,
@@ -1004,7 +1271,7 @@ function ChangeRow({
1004
1271
  }, undefined, true, undefined, this)
1005
1272
  ]
1006
1273
  }, undefined, true, undefined, this),
1007
- /* @__PURE__ */ jsxDEV5("div", {
1274
+ /* @__PURE__ */ jsxDEV6("div", {
1008
1275
  style: {
1009
1276
  color: DEVTOOL_COLOR_TEXT_SECONDARY,
1010
1277
  fontSize: "11px",
@@ -1019,7 +1286,7 @@ function ChangeRow({
1019
1286
  }, undefined, true, undefined, this);
1020
1287
  }
1021
1288
  function Badge({ color, children }) {
1022
- return /* @__PURE__ */ jsxDEV5("span", {
1289
+ return /* @__PURE__ */ jsxDEV6("span", {
1023
1290
  style: {
1024
1291
  flexShrink: 0,
1025
1292
  padding: "1px 6px",
@@ -1039,12 +1306,12 @@ function Badge({ color, children }) {
1039
1306
  import { useEffect, useState as useState3 } from "react";
1040
1307
 
1041
1308
  // src/devtools/browser/components/SectionLabel.tsx
1042
- import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
1309
+ import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
1043
1310
  function SectionLabel({
1044
1311
  label,
1045
1312
  color = DEVTOOL_COLOR_SEMANTIC_SYSTEM
1046
1313
  }) {
1047
- return /* @__PURE__ */ jsxDEV6("div", {
1314
+ return /* @__PURE__ */ jsxDEV7("div", {
1048
1315
  style: {
1049
1316
  color,
1050
1317
  fontSize: "0.85em",
@@ -1059,7 +1326,7 @@ function SectionLabel({
1059
1326
  }
1060
1327
 
1061
1328
  // src/devtools/browser/components/StateInspector.tsx
1062
- import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
1329
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
1063
1330
  function StateInspector({
1064
1331
  store,
1065
1332
  onApply
@@ -1099,7 +1366,7 @@ function StateInspector({
1099
1366
  setDirty(false);
1100
1367
  setError(null);
1101
1368
  };
1102
- return /* @__PURE__ */ jsxDEV7("div", {
1369
+ return /* @__PURE__ */ jsxDEV8("div", {
1103
1370
  style: {
1104
1371
  flex: 1,
1105
1372
  display: "flex",
@@ -1109,7 +1376,7 @@ function StateInspector({
1109
1376
  background: DEVTOOL_DETAIL_BASE_BACKGROUND
1110
1377
  },
1111
1378
  children: [
1112
- /* @__PURE__ */ jsxDEV7("div", {
1379
+ /* @__PURE__ */ jsxDEV8("div", {
1113
1380
  style: {
1114
1381
  flex: 1,
1115
1382
  minHeight: 0,
@@ -1118,19 +1385,19 @@ function StateInspector({
1118
1385
  padding: "10px 12px 8px"
1119
1386
  },
1120
1387
  children: [
1121
- /* @__PURE__ */ jsxDEV7(SectionLabel, {
1388
+ /* @__PURE__ */ jsxDEV8(SectionLabel, {
1122
1389
  label: "Current state"
1123
1390
  }, undefined, false, undefined, this),
1124
- /* @__PURE__ */ jsxDEV7("div", {
1391
+ /* @__PURE__ */ jsxDEV8("div", {
1125
1392
  style: { flex: 1, minHeight: 0, overflow: "auto" },
1126
- children: /* @__PURE__ */ jsxDEV7(JsonView, {
1393
+ children: /* @__PURE__ */ jsxDEV8(JsonView, {
1127
1394
  value: store.currentState,
1128
1395
  style: { minHeight: "100%" }
1129
1396
  }, undefined, false, undefined, this)
1130
1397
  }, undefined, false, undefined, this)
1131
1398
  ]
1132
1399
  }, undefined, true, undefined, this),
1133
- /* @__PURE__ */ jsxDEV7("div", {
1400
+ /* @__PURE__ */ jsxDEV8("div", {
1134
1401
  style: {
1135
1402
  flex: 1,
1136
1403
  minHeight: 0,
@@ -1140,7 +1407,7 @@ function StateInspector({
1140
1407
  borderTop: `1px solid ${DEVTOOL_PANEL_DIVIDER_BORDER}`
1141
1408
  },
1142
1409
  children: [
1143
- /* @__PURE__ */ jsxDEV7("div", {
1410
+ /* @__PURE__ */ jsxDEV8("div", {
1144
1411
  style: {
1145
1412
  display: "flex",
1146
1413
  alignItems: "center",
@@ -1148,18 +1415,18 @@ function StateInspector({
1148
1415
  marginBottom: "3px"
1149
1416
  },
1150
1417
  children: [
1151
- /* @__PURE__ */ jsxDEV7(SectionLabel, {
1418
+ /* @__PURE__ */ jsxDEV8(SectionLabel, {
1152
1419
  label: "Edit & apply",
1153
1420
  color: DEVTOOL_COLOR_SEMANTIC_WARNING
1154
1421
  }, undefined, false, undefined, this),
1155
- dirty && /* @__PURE__ */ jsxDEV7("button", {
1422
+ dirty && /* @__PURE__ */ jsxDEV8("button", {
1156
1423
  onClick: reload,
1157
1424
  style: linkButtonStyle(DEVTOOL_COLOR_TEXT_MUTED),
1158
1425
  children: "reload from store"
1159
1426
  }, undefined, false, undefined, this)
1160
1427
  ]
1161
1428
  }, undefined, true, undefined, this),
1162
- /* @__PURE__ */ jsxDEV7("textarea", {
1429
+ /* @__PURE__ */ jsxDEV8("textarea", {
1163
1430
  value: draft,
1164
1431
  spellCheck: false,
1165
1432
  onChange: (e) => onEdit(e.target.value),
@@ -1179,7 +1446,7 @@ function StateInspector({
1179
1446
  lineHeight: 1.5
1180
1447
  }
1181
1448
  }, undefined, false, undefined, this),
1182
- error != null && /* @__PURE__ */ jsxDEV7("div", {
1449
+ error != null && /* @__PURE__ */ jsxDEV8("div", {
1183
1450
  style: {
1184
1451
  marginTop: "6px",
1185
1452
  padding: "6px 8px",
@@ -1192,9 +1459,9 @@ function StateInspector({
1192
1459
  },
1193
1460
  children: error
1194
1461
  }, undefined, false, undefined, this),
1195
- /* @__PURE__ */ jsxDEV7("div", {
1462
+ /* @__PURE__ */ jsxDEV8("div", {
1196
1463
  style: { display: "flex", gap: "8px", marginTop: "8px", flexShrink: 0 },
1197
- children: /* @__PURE__ */ jsxDEV7("button", {
1464
+ children: /* @__PURE__ */ jsxDEV8("button", {
1198
1465
  onClick: apply,
1199
1466
  disabled: !dirty,
1200
1467
  style: {
@@ -1229,14 +1496,14 @@ function linkButtonStyle(color) {
1229
1496
  }
1230
1497
 
1231
1498
  // src/devtools/browser/components/StoreTabs.tsx
1232
- import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
1499
+ import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
1233
1500
  function StoreTabs({
1234
1501
  stores,
1235
1502
  selectedStoreId,
1236
1503
  onSelect,
1237
1504
  includeAll = true
1238
1505
  }) {
1239
- return /* @__PURE__ */ jsxDEV8("div", {
1506
+ return /* @__PURE__ */ jsxDEV9("div", {
1240
1507
  style: {
1241
1508
  display: "flex",
1242
1509
  gap: "6px",
@@ -1247,16 +1514,16 @@ function StoreTabs({
1247
1514
  flexShrink: 0
1248
1515
  },
1249
1516
  children: [
1250
- includeAll && /* @__PURE__ */ jsxDEV8(Pill, {
1517
+ includeAll && /* @__PURE__ */ jsxDEV9(Pill, {
1251
1518
  active: selectedStoreId == null,
1252
1519
  onClick: () => onSelect(null),
1253
1520
  label: "all"
1254
1521
  }, undefined, false, undefined, this),
1255
- stores.length === 0 && /* @__PURE__ */ jsxDEV8("span", {
1522
+ stores.length === 0 && /* @__PURE__ */ jsxDEV9("span", {
1256
1523
  style: { color: DEVTOOL_COLOR_TEXT_MUTED, fontSize: "11px", padding: "2px 0" },
1257
1524
  children: "no stores registered"
1258
1525
  }, undefined, false, undefined, this),
1259
- stores.map((store) => /* @__PURE__ */ jsxDEV8(Pill, {
1526
+ stores.map((store) => /* @__PURE__ */ jsxDEV9(Pill, {
1260
1527
  active: selectedStoreId === store.id,
1261
1528
  onClick: () => onSelect(store.id),
1262
1529
  label: store.label,
@@ -1271,7 +1538,7 @@ function Pill({
1271
1538
  label,
1272
1539
  count
1273
1540
  }) {
1274
- return /* @__PURE__ */ jsxDEV8("button", {
1541
+ return /* @__PURE__ */ jsxDEV9("button", {
1275
1542
  onClick,
1276
1543
  style: {
1277
1544
  display: "flex",
@@ -1290,7 +1557,7 @@ function Pill({
1290
1557
  },
1291
1558
  children: [
1292
1559
  label,
1293
- count != null && count > 0 && /* @__PURE__ */ jsxDEV8("span", {
1560
+ count != null && count > 0 && /* @__PURE__ */ jsxDEV9("span", {
1294
1561
  style: {
1295
1562
  color: active ? DEVTOOL_COLOR_SEMANTIC_SYSTEM : DEVTOOL_COLOR_TEXT_FAINT,
1296
1563
  fontSize: "10px"
@@ -1303,7 +1570,7 @@ function Pill({
1303
1570
 
1304
1571
  // src/devtools/browser/devtools_dock.ts
1305
1572
  var GLOBAL_KEY = "__NICE_DEVTOOLS_DOCK__";
1306
- var VERSION = 2;
1573
+ var VERSION = 4;
1307
1574
  function createCoordinator() {
1308
1575
  const panels = new Map;
1309
1576
  const listeners = new Set;
@@ -1363,17 +1630,25 @@ function createCoordinator() {
1363
1630
  const anyOpen = list.some((p) => p.open);
1364
1631
  const firstId = list.length > 0 ? list[0].id : null;
1365
1632
  let dockOffset = 0;
1633
+ let stacked = false;
1366
1634
  const self = panels.get(id);
1367
1635
  if (self != null && self.open) {
1636
+ let seenSelf = false;
1368
1637
  for (const panel of list) {
1369
- if (panel.id === id)
1370
- break;
1371
- if (panel.open && panel.side === self.side)
1372
- dockOffset += panel.size;
1638
+ if (panel.id === id) {
1639
+ seenSelf = true;
1640
+ continue;
1641
+ }
1642
+ if (panel.open && panel.side === self.side) {
1643
+ stacked = true;
1644
+ if (!seenSelf)
1645
+ dockOffset += panel.size;
1646
+ }
1373
1647
  }
1374
1648
  }
1375
1649
  return {
1376
1650
  dockOffset,
1651
+ stacked,
1377
1652
  anyOpen,
1378
1653
  isPrimary: id === firstId,
1379
1654
  devtools: list.map(toRef),
@@ -1401,7 +1676,7 @@ function getDevtoolsDockCoordinator() {
1401
1676
  }
1402
1677
 
1403
1678
  // src/devtools/browser/NiceStateDevtools.tsx
1404
- import { jsxDEV as jsxDEV9, Fragment } from "react/jsx-dev-runtime";
1679
+ import { jsxDEV as jsxDEV10, Fragment } from "react/jsx-dev-runtime";
1405
1680
  if (typeof document !== "undefined" && !document.getElementById("__nice-state-devtools-styles")) {
1406
1681
  const style = document.createElement("style");
1407
1682
  style.id = "__nice-state-devtools-styles";
@@ -1432,7 +1707,8 @@ function readPrefs(defaultPosition, initialOpen) {
1432
1707
  isOpen: initialOpen,
1433
1708
  dockedHeight: DOCKED_HEIGHT_DEFAULT,
1434
1709
  dockedWidth: DOCKED_WIDTH_DEFAULT,
1435
- detailRatio: DETAIL_RATIO_DEFAULT
1710
+ detailRatio: DETAIL_RATIO_DEFAULT,
1711
+ stayOnLatest: false
1436
1712
  };
1437
1713
  try {
1438
1714
  if (typeof localStorage === "undefined")
@@ -1456,7 +1732,7 @@ function writePrefs(prefs) {
1456
1732
  var EMPTY_SNAPSHOT = { stores: [], changes: [], paused: false };
1457
1733
  function NiceStateDevtools(props) {
1458
1734
  if (false) {}
1459
- return /* @__PURE__ */ jsxDEV9(NiceStateDevtools_Panel, {
1735
+ return /* @__PURE__ */ jsxDEV10(NiceStateDevtools_Panel, {
1460
1736
  ...props
1461
1737
  }, undefined, false, undefined, this);
1462
1738
  }
@@ -1479,12 +1755,17 @@ function NiceStateDevtools_Panel({
1479
1755
  return () => clearTimeout(timer);
1480
1756
  }, [prefs]);
1481
1757
  const { stores, changes, paused } = snapshot;
1482
- const { position, isOpen, dockedHeight, dockedWidth, detailRatio } = prefs;
1758
+ const { position, isOpen, dockedHeight, dockedWidth, detailRatio, stayOnLatest } = prefs;
1483
1759
  const dockSide = getDockSide(position);
1484
1760
  const isHorizDock = dockSide === "top" || dockSide === "bottom";
1485
1761
  const dockedSize = isHorizDock ? dockedHeight : dockedWidth;
1486
1762
  const filteredChanges = useMemo(() => storeFilter == null ? changes : changes.filter((c) => c.storeId === storeFilter), [changes, storeFilter]);
1487
1763
  const selectedChange = selectedChangeCuid != null ? changes.find((c) => c.cuid === selectedChangeCuid) ?? null : null;
1764
+ const latestCuid = filteredChanges.length > 0 ? filteredChanges[0].cuid : null;
1765
+ useEffect2(() => {
1766
+ if (stayOnLatest && latestCuid != null)
1767
+ setSelectedChangeCuid(latestCuid);
1768
+ }, [stayOnLatest, latestCuid]);
1488
1769
  const activeStore = stores.find((s) => s.id === storeFilter) ?? (stores.length > 0 ? stores[0] : null);
1489
1770
  const dock = useMemo(() => getDevtoolsDockCoordinator(), []);
1490
1771
  const panelId = useId();
@@ -1519,7 +1800,7 @@ function NiceStateDevtools_Panel({
1519
1800
  };
1520
1801
  if (!isOpen) {
1521
1802
  if (view.isPrimary && !view.anyOpen) {
1522
- return /* @__PURE__ */ jsxDEV9(DevtoolsLauncher, {
1803
+ return /* @__PURE__ */ jsxDEV10(DevtoolsLauncher, {
1523
1804
  items: view.devtools
1524
1805
  }, undefined, false, undefined, this);
1525
1806
  }
@@ -1539,37 +1820,37 @@ function NiceStateDevtools_Panel({
1539
1820
  left: 0,
1540
1821
  right: 0,
1541
1822
  height: `${dockedSize}px`,
1542
- borderRadius: "8px 8px 0 0"
1823
+ borderRadius: view.stacked ? "0" : "8px 8px 0 0"
1543
1824
  } : dockSide === "top" ? {
1544
1825
  top: view.dockOffset,
1545
1826
  left: 0,
1546
1827
  right: 0,
1547
1828
  height: `${dockedSize}px`,
1548
- borderRadius: "0 0 8px 8px"
1829
+ borderRadius: view.stacked ? "0" : "0 0 8px 8px"
1549
1830
  } : dockSide === "left" ? {
1550
1831
  top: 0,
1551
1832
  left: view.dockOffset,
1552
1833
  bottom: 0,
1553
1834
  width: `${dockedSize}px`,
1554
- borderRadius: "0 8px 8px 0"
1835
+ borderRadius: view.stacked ? "0" : "0 8px 8px 0"
1555
1836
  } : {
1556
1837
  top: 0,
1557
1838
  right: view.dockOffset,
1558
1839
  bottom: 0,
1559
1840
  width: `${dockedSize}px`,
1560
- borderRadius: "8px 0 0 8px"
1841
+ borderRadius: view.stacked ? "0" : "8px 0 0 8px"
1561
1842
  }
1562
1843
  };
1563
- return /* @__PURE__ */ jsxDEV9("div", {
1844
+ return /* @__PURE__ */ jsxDEV10("div", {
1564
1845
  id: "__nice-state-devtools-panel",
1565
1846
  style: panelStyle,
1566
1847
  children: [
1567
- /* @__PURE__ */ jsxDEV9(ResizeHandle, {
1848
+ /* @__PURE__ */ jsxDEV10(ResizeHandle, {
1568
1849
  dockSide,
1569
1850
  dockedSize,
1570
1851
  onChange: (size) => setPrefs(isHorizDock ? { dockedHeight: size } : { dockedWidth: size })
1571
1852
  }, undefined, false, undefined, this),
1572
- /* @__PURE__ */ jsxDEV9(PanelHeader, {
1853
+ /* @__PURE__ */ jsxDEV10(PanelHeader, {
1573
1854
  position,
1574
1855
  onPositionChange: (p) => setPrefs({ position: p }),
1575
1856
  onClose: () => setPrefs({ isOpen: false }),
@@ -1580,7 +1861,7 @@ function NiceStateDevtools_Panel({
1580
1861
  paused,
1581
1862
  onTogglePause: () => core.togglePaused(),
1582
1863
  openOthers: view.otherClosed,
1583
- children: /* @__PURE__ */ jsxDEV9(SegmentedControl, {
1864
+ children: /* @__PURE__ */ jsxDEV10(SegmentedControl, {
1584
1865
  options: [
1585
1866
  { value: "timeline", label: "Timeline" },
1586
1867
  { value: "state", label: "State" }
@@ -1589,18 +1870,18 @@ function NiceStateDevtools_Panel({
1589
1870
  onChange: setMode
1590
1871
  }, undefined, false, undefined, this)
1591
1872
  }, undefined, false, undefined, this),
1592
- /* @__PURE__ */ jsxDEV9(StoreTabs, {
1873
+ /* @__PURE__ */ jsxDEV10(StoreTabs, {
1593
1874
  stores,
1594
1875
  selectedStoreId: mode === "state" ? activeStore?.id ?? null : storeFilter,
1595
1876
  onSelect: setStoreFilter,
1596
1877
  includeAll: mode === "timeline"
1597
1878
  }, undefined, false, undefined, this),
1598
- mode === "state" ? activeStore != null ? /* @__PURE__ */ jsxDEV9(StateInspector, {
1879
+ mode === "state" ? activeStore != null ? /* @__PURE__ */ jsxDEV10(StateInspector, {
1599
1880
  store: activeStore,
1600
1881
  onApply: (id, next) => core.applyEdit(id, next)
1601
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV9(EmptyMessage, {
1882
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV10(EmptyMessage, {
1602
1883
  children: "No stores registered. Call core.registerStore(...)."
1603
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV9("div", {
1884
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV10("div", {
1604
1885
  style: {
1605
1886
  flex: 1,
1606
1887
  display: "flex",
@@ -1609,29 +1890,44 @@ function NiceStateDevtools_Panel({
1609
1890
  minHeight: 0
1610
1891
  },
1611
1892
  children: [
1612
- /* @__PURE__ */ jsxDEV9("div", {
1893
+ /* @__PURE__ */ jsxDEV10("div", {
1613
1894
  style: {
1614
1895
  flexGrow: selectedChange != null ? 1 - detailRatio : 1,
1615
1896
  flexShrink: 1,
1616
1897
  flexBasis: 0,
1617
1898
  minWidth: 0,
1618
1899
  minHeight: 0,
1619
- overflowY: "auto"
1900
+ display: "flex",
1901
+ flexDirection: "column",
1902
+ overflow: "hidden"
1620
1903
  },
1621
- children: /* @__PURE__ */ jsxDEV9(ChangeList, {
1622
- changes: filteredChanges,
1623
- selectedCuid: selectedChangeCuid,
1624
- onSelect: (cuid) => setSelectedChangeCuid((prev) => prev === cuid ? null : cuid),
1625
- showStore: storeFilter == null
1626
- }, undefined, false, undefined, this)
1627
- }, undefined, false, undefined, this),
1628
- selectedChange != null && /* @__PURE__ */ jsxDEV9(Fragment, {
1629
1904
  children: [
1630
- /* @__PURE__ */ jsxDEV9(SplitHandle, {
1905
+ /* @__PURE__ */ jsxDEV10(StayOnLatestToggle, {
1906
+ checked: stayOnLatest,
1907
+ onChange: (next) => setPrefs({ stayOnLatest: next })
1908
+ }, undefined, false, undefined, this),
1909
+ /* @__PURE__ */ jsxDEV10("div", {
1910
+ style: { flex: 1, minHeight: 0, overflowY: "auto" },
1911
+ children: /* @__PURE__ */ jsxDEV10(ChangeList, {
1912
+ changes: filteredChanges,
1913
+ selectedCuid: selectedChangeCuid,
1914
+ onSelect: (cuid) => {
1915
+ if (stayOnLatest)
1916
+ setPrefs({ stayOnLatest: false });
1917
+ setSelectedChangeCuid((prev) => prev === cuid ? null : cuid);
1918
+ },
1919
+ showStore: storeFilter == null
1920
+ }, undefined, false, undefined, this)
1921
+ }, undefined, false, undefined, this)
1922
+ ]
1923
+ }, undefined, true, undefined, this),
1924
+ selectedChange != null && /* @__PURE__ */ jsxDEV10(Fragment, {
1925
+ children: [
1926
+ /* @__PURE__ */ jsxDEV10(SplitHandle, {
1631
1927
  horizontal: isHorizDock,
1632
1928
  onRatioChange: (ratio) => setPrefs({ detailRatio: ratio })
1633
1929
  }, undefined, false, undefined, this),
1634
- /* @__PURE__ */ jsxDEV9("div", {
1930
+ /* @__PURE__ */ jsxDEV10("div", {
1635
1931
  style: {
1636
1932
  flexGrow: detailRatio,
1637
1933
  flexShrink: 1,
@@ -1649,10 +1945,10 @@ function NiceStateDevtools_Panel({
1649
1945
  boxShadow: "inset 0 18px 36px -14px rgba(0,0,0,0.8)"
1650
1946
  }
1651
1947
  },
1652
- children: /* @__PURE__ */ jsxDEV9(ChangeDetailPanel, {
1948
+ children: /* @__PURE__ */ jsxDEV10(ChangeDetailPanel, {
1653
1949
  change: selectedChange,
1654
1950
  onRevert: (c) => core.revertChange(c)
1655
- }, selectedChange.cuid, false, undefined, this)
1951
+ }, undefined, false, undefined, this)
1656
1952
  }, undefined, false, undefined, this)
1657
1953
  ]
1658
1954
  }, undefined, true, undefined, this)
@@ -1661,8 +1957,39 @@ function NiceStateDevtools_Panel({
1661
1957
  ]
1662
1958
  }, undefined, true, undefined, this);
1663
1959
  }
1960
+ function StayOnLatestToggle({
1961
+ checked,
1962
+ onChange
1963
+ }) {
1964
+ return /* @__PURE__ */ jsxDEV10("label", {
1965
+ title: "Always select the most recent change so the detail pane follows it",
1966
+ style: {
1967
+ display: "flex",
1968
+ alignItems: "center",
1969
+ gap: "6px",
1970
+ padding: "5px 10px",
1971
+ flexShrink: 0,
1972
+ cursor: "pointer",
1973
+ userSelect: "none",
1974
+ background: DEVTOOL_SECTION_BACKGROUND,
1975
+ borderBottom: `1px solid ${DEVTOOL_LIST_BASE_BACKGROUND}`,
1976
+ color: checked ? DEVTOOL_COLOR_TEXT_SECONDARY : DEVTOOL_COLOR_TEXT_MUTED,
1977
+ fontSize: "10px",
1978
+ fontFamily: SANS_FONT
1979
+ },
1980
+ children: [
1981
+ /* @__PURE__ */ jsxDEV10("input", {
1982
+ type: "checkbox",
1983
+ checked,
1984
+ onChange: (e) => onChange(e.target.checked),
1985
+ style: { accentColor: DEVTOOL_COLOR_SEMANTIC_SYSTEM, cursor: "pointer", margin: 0 }
1986
+ }, undefined, false, undefined, this),
1987
+ "stay on latest"
1988
+ ]
1989
+ }, undefined, true, undefined, this);
1990
+ }
1664
1991
  function EmptyMessage({ children }) {
1665
- return /* @__PURE__ */ jsxDEV9("div", {
1992
+ return /* @__PURE__ */ jsxDEV10("div", {
1666
1993
  style: {
1667
1994
  flex: 1,
1668
1995
  display: "flex",
@@ -1,7 +1,10 @@
1
1
  /**
2
- * Test-framework-style diff: only the paths that changed, with removed values in
3
- * error red on a `−` line and added values in success green on a `+` line. A
4
- * `changed` entry renders both lines so old new reads top-to-bottom.
2
+ * Test-framework-style diff: only the paths that changed. Changed paths are
3
+ * grouped into a hierarchy by their shared parents `countHistory.0` and
4
+ * `countHistory.1` nest under a single `countHistory` node, each shown by its
5
+ * direct key only. Single-child chains collapse to a dotted path (`a.b.c`) so
6
+ * lone deep changes stay on one line. Short values render inline with the key
7
+ * (`count: 0 → 1`); long ones keep the removed/added line block.
5
8
  */
6
9
  export declare function DiffView({ before, after }: {
7
10
  before: unknown;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Unified, git-style diff of the entire state snapshot. Both sides are rendered
3
+ * as pretty JSON and line-diffed, so unchanged structure stays visible for
4
+ * context while removed lines (red, `−`) and added lines (green, `+`) call out
5
+ * exactly which sections of the whole state moved.
6
+ */
7
+ export declare function JsonDiffView({ before, after }: {
8
+ before: unknown;
9
+ after: unknown;
10
+ }): import("react/jsx-runtime").JSX.Element;
11
+ /**
12
+ * The full JSON of one snapshot with the lines that differ from the other side
13
+ * highlighted in place — red behind removed lines on the "before" side, green
14
+ * behind added lines on the "after" side. Lines common to both render plainly so
15
+ * the highlight reads as "here is what changed within the whole state".
16
+ */
17
+ export declare function HighlightedJsonView({ before, after, side, }: {
18
+ before: unknown;
19
+ after: unknown;
20
+ side: "before" | "after";
21
+ }): import("react/jsx-runtime").JSX.Element;
@@ -16,6 +16,7 @@ export declare function summarizeChange(change: IDevtoolsStateChange): string;
16
16
  export type TDiffKind = "added" | "removed" | "changed";
17
17
  export interface IDiffEntry {
18
18
  path: string;
19
+ segments: (string | number)[];
19
20
  kind: TDiffKind;
20
21
  before?: unknown;
21
22
  after?: unknown;
@@ -27,3 +28,15 @@ export interface IDiffEntry {
27
28
  * means equal branches keep their reference, so this stays cheap).
28
29
  */
29
30
  export declare function computeDiff(before: unknown, after: unknown): IDiffEntry[];
31
+ export type TLineDiffKind = "common" | "removed" | "added";
32
+ export interface ILineDiffOp {
33
+ kind: TLineDiffKind;
34
+ text: string;
35
+ }
36
+ /**
37
+ * Classic LCS line diff between two blocks of text — the data behind the unified
38
+ * git-style "Diff View" and the per-line highlighting in the Before / After
39
+ * panels. Snapshots rendered as pretty JSON stay small, so the O(n·m) table is
40
+ * comfortably cheap.
41
+ */
42
+ export declare function computeLineDiff(beforeText: string, afterText: string): ILineDiffOp[];
@@ -23,6 +23,13 @@ export interface IDockDevtoolInput extends IDockDevtoolSync {
23
23
  export interface IDockView {
24
24
  /** Offset (px) from the docked edge — stacks open panels on the same side. */
25
25
  dockOffset: number;
26
+ /**
27
+ * True when this panel shares its dock side with another open panel (stacked
28
+ * either nearer or further from the edge). Stacked panels square off all their
29
+ * corners so they sit flush as one continuous block — only a panel alone on
30
+ * its side keeps its rounded, page-facing corners.
31
+ */
32
+ stacked: boolean;
26
33
  /** Is any devtool on the page currently open? */
27
34
  anyOpen: boolean;
28
35
  /** First-registered devtool — the one that renders the combined launcher. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nice-code/state",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {