@nice-code/state 0.4.4 → 0.4.6

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.
@@ -161,7 +161,7 @@ class StateDevtoolsCore {
161
161
  }
162
162
  }
163
163
  // src/devtools/browser/NiceStateDevtools.tsx
164
- import { useEffect as useEffect2, useId, useMemo, useReducer, useState as useState4 } from "react";
164
+ import { useEffect as useEffect2, useId, useMemo as useMemo2, useReducer, useState as useState3 } from "react";
165
165
 
166
166
  // src/devtools/core/devtools_colors.ts
167
167
  var DEVTOOL_COLOR_SEMANTIC_ERROR = "#FF5C5C";
@@ -191,9 +191,6 @@ var DEVTOOL_JSON_PUNCTUATION = "#475569";
191
191
  var MONO_FONT = "ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace";
192
192
  var SANS_FONT = "ui-sans-serif, system-ui, sans-serif";
193
193
 
194
- // src/devtools/browser/components/ChangeDetailPanel.tsx
195
- import { useState as useState2 } from "react";
196
-
197
194
  // src/devtools/browser/components/utils.ts
198
195
  function safeStringify(value, indent = 2) {
199
196
  if (value === undefined)
@@ -327,6 +324,143 @@ function computeLineDiff(beforeText, afterText) {
327
324
  ops.push({ kind: "added", text: b[j++] });
328
325
  return ops;
329
326
  }
327
+ function orderLineDiffOps(ops, latestFirst) {
328
+ if (!latestFirst)
329
+ return ops;
330
+ const out = [];
331
+ let i = 0;
332
+ while (i < ops.length) {
333
+ if (ops[i].kind === "common") {
334
+ out.push(ops[i]);
335
+ i++;
336
+ continue;
337
+ }
338
+ const added = [];
339
+ const removed = [];
340
+ while (i < ops.length && ops[i].kind !== "common") {
341
+ (ops[i].kind === "added" ? added : removed).push(ops[i]);
342
+ i++;
343
+ }
344
+ out.push(...added, ...removed);
345
+ }
346
+ return out;
347
+ }
348
+ function computeCompressedDiff(before, after, side, latestFirst = false) {
349
+ const lines = [];
350
+ emitChangedNode(lines, 0, null, before, after, side, latestFirst);
351
+ return lines;
352
+ }
353
+ function keyPrefix(keyLabel) {
354
+ return keyLabel != null ? `${keyLabel}: ` : "";
355
+ }
356
+ function emitChangedNode(lines, depth, keyLabel, before, after, side, latestFirst) {
357
+ const bothContainers = isPlainObjectOrArray(before) && isPlainObjectOrArray(after);
358
+ const sameShape = bothContainers && Array.isArray(before) === Array.isArray(after);
359
+ if (sameShape) {
360
+ emitContainer(lines, depth, keyLabel, before, after, side, latestFirst);
361
+ return;
362
+ }
363
+ const removed = () => pushValueLines(lines, depth, keyLabel, before, "−", "removed");
364
+ const added = () => pushValueLines(lines, depth, keyLabel, after, "+", "added");
365
+ if (side === "before") {
366
+ removed();
367
+ } else if (side === "after") {
368
+ added();
369
+ } else if (latestFirst) {
370
+ added();
371
+ removed();
372
+ } else {
373
+ removed();
374
+ added();
375
+ }
376
+ }
377
+ function emitContainer(lines, depth, keyLabel, before, after, side, latestFirst) {
378
+ const isArray = Array.isArray(before);
379
+ lines.push({ depth, sign: " ", tone: "common", text: `${keyPrefix(keyLabel)}${isArray ? "[" : "{"}` });
380
+ let unchanged = 0;
381
+ const flush = () => {
382
+ if (unchanged > 0) {
383
+ lines.push({
384
+ depth: depth + 1,
385
+ sign: " ",
386
+ tone: "placeholder",
387
+ text: placeholderText(unchanged, isArray)
388
+ });
389
+ unchanged = 0;
390
+ }
391
+ };
392
+ for (const child of childEntries(before, after, isArray)) {
393
+ if (child.kind === "added" && side === "before")
394
+ continue;
395
+ if (child.kind === "removed" && side === "after")
396
+ continue;
397
+ if (child.kind === "unchanged") {
398
+ unchanged++;
399
+ continue;
400
+ }
401
+ flush();
402
+ if (child.kind === "changed") {
403
+ emitChangedNode(lines, depth + 1, child.keyLabel, child.before, child.after, side, latestFirst);
404
+ } else if (child.kind === "added") {
405
+ pushValueLines(lines, depth + 1, child.keyLabel, child.after, "+", "added");
406
+ } else {
407
+ pushValueLines(lines, depth + 1, child.keyLabel, child.before, "−", "removed");
408
+ }
409
+ }
410
+ flush();
411
+ lines.push({ depth, sign: " ", tone: "common", text: isArray ? "]" : "}" });
412
+ }
413
+ function childEntries(before, after, isArray) {
414
+ if (isArray) {
415
+ const b2 = before;
416
+ const a2 = after;
417
+ const len = Math.max(b2.length, a2.length);
418
+ const out = [];
419
+ for (let i = 0;i < len; i++) {
420
+ const inB = i < b2.length;
421
+ const inA = i < a2.length;
422
+ if (inB && inA) {
423
+ const kind = Object.is(b2[i], a2[i]) ? "unchanged" : "changed";
424
+ out.push({ keyLabel: null, kind, before: b2[i], after: a2[i] });
425
+ } else if (inB) {
426
+ out.push({ keyLabel: null, kind: "removed", before: b2[i], after: undefined });
427
+ } else {
428
+ out.push({ keyLabel: null, kind: "added", before: undefined, after: a2[i] });
429
+ }
430
+ }
431
+ return out;
432
+ }
433
+ const b = before;
434
+ const a = after;
435
+ const keys = [...new Set([...Object.keys(b), ...Object.keys(a)])];
436
+ return keys.map((key) => {
437
+ const keyLabel = JSON.stringify(key);
438
+ const inB = key in b;
439
+ const inA = key in a;
440
+ if (inB && inA) {
441
+ const kind = Object.is(b[key], a[key]) ? "unchanged" : "changed";
442
+ return { keyLabel, kind, before: b[key], after: a[key] };
443
+ }
444
+ if (inB)
445
+ return { keyLabel, kind: "removed", before: b[key], after: undefined };
446
+ return { keyLabel, kind: "added", before: undefined, after: a[key] };
447
+ });
448
+ }
449
+ function placeholderText(count, isArray) {
450
+ const noun = isArray ? count === 1 ? "item" : "items" : count === 1 ? "property" : "properties";
451
+ return `… ${count} unchanged ${noun}`;
452
+ }
453
+ function pushValueLines(lines, baseDepth, keyLabel, value, sign, tone) {
454
+ const prefix = keyPrefix(keyLabel);
455
+ const raw = safeStringify(value, 2).split(`
456
+ `);
457
+ raw.forEach((line, idx) => {
458
+ const leading = /^ */.exec(line)[0].length;
459
+ const content = line.slice(leading);
460
+ const text = idx === 0 ? `${prefix}${content}` : content;
461
+ lines.push({ depth: baseDepth + leading / 2, sign, tone, text });
462
+ });
463
+ }
330
464
  function isScalar(value) {
331
465
  return value == null || typeof value !== "object";
332
466
  }
@@ -415,7 +549,11 @@ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
415
549
  var ADDED_BG = "rgba(163, 230, 53, 0.08)";
416
550
  var REMOVED_BG = "rgba(255, 92, 92, 0.08)";
417
551
  var INLINE_MAX = 40;
418
- function DiffView({ before, after }) {
552
+ function DiffView({
553
+ before,
554
+ after,
555
+ latestFirst = false
556
+ }) {
419
557
  const entries = computeDiff(before, after);
420
558
  if (entries.length === 0) {
421
559
  return /* @__PURE__ */ jsxDEV2("div", {
@@ -434,7 +572,8 @@ function DiffView({ before, after }) {
434
572
  return /* @__PURE__ */ jsxDEV2("div", {
435
573
  style: { display: "flex", flexDirection: "column", gap: "6px" },
436
574
  children: roots.map((node) => /* @__PURE__ */ jsxDEV2(DiffNode, {
437
- node
575
+ node,
576
+ latestFirst
438
577
  }, node.key, false, undefined, this))
439
578
  }, undefined, false, undefined, this);
440
579
  }
@@ -469,11 +608,12 @@ function collapseChains(node) {
469
608
  }
470
609
  return current;
471
610
  }
472
- function DiffNode({ node }) {
611
+ function DiffNode({ node, latestFirst }) {
473
612
  if (node.entry != null && node.children.size === 0) {
474
613
  return /* @__PURE__ */ jsxDEV2(DiffLeaf, {
475
614
  label: node.key,
476
- entry: node.entry
615
+ entry: node.entry,
616
+ latestFirst
477
617
  }, undefined, false, undefined, this);
478
618
  }
479
619
  return /* @__PURE__ */ jsxDEV2("div", {
@@ -493,15 +633,44 @@ function DiffNode({ node }) {
493
633
  gap: "5px"
494
634
  },
495
635
  children: [...node.children.values()].map((child) => /* @__PURE__ */ jsxDEV2(DiffNode, {
496
- node: child
636
+ node: child,
637
+ latestFirst
497
638
  }, child.key, false, undefined, this))
498
639
  }, undefined, false, undefined, this)
499
640
  ]
500
641
  }, undefined, true, undefined, this);
501
642
  }
502
- function DiffLeaf({ label, entry }) {
643
+ function DiffLeaf({
644
+ label,
645
+ entry,
646
+ latestFirst
647
+ }) {
503
648
  const showRemoved = entry.kind === "removed" || entry.kind === "changed";
504
649
  const showAdded = entry.kind === "added" || entry.kind === "changed";
650
+ const removed = showRemoved && /* @__PURE__ */ jsxDEV2(InlineValue, {
651
+ sign: "−",
652
+ color: DEVTOOL_COLOR_SEMANTIC_ERROR,
653
+ background: REMOVED_BG,
654
+ value: entry.before
655
+ }, undefined, false, undefined, this);
656
+ const added = showAdded && /* @__PURE__ */ jsxDEV2(InlineValue, {
657
+ sign: "+",
658
+ color: DEVTOOL_COLOR_SEMANTIC_SUCCESS,
659
+ background: ADDED_BG,
660
+ value: entry.after
661
+ }, undefined, false, undefined, this);
662
+ const removedLine = showRemoved && /* @__PURE__ */ jsxDEV2(DiffLine, {
663
+ sign: "−",
664
+ color: DEVTOOL_COLOR_SEMANTIC_ERROR,
665
+ background: REMOVED_BG,
666
+ value: entry.before
667
+ }, undefined, false, undefined, this);
668
+ const addedLine = showAdded && /* @__PURE__ */ jsxDEV2(DiffLine, {
669
+ sign: "+",
670
+ color: DEVTOOL_COLOR_SEMANTIC_SUCCESS,
671
+ background: ADDED_BG,
672
+ value: entry.after
673
+ }, undefined, false, undefined, this);
505
674
  if (isInlineLeaf(entry)) {
506
675
  return /* @__PURE__ */ jsxDEV2("div", {
507
676
  style: { display: "flex", flexWrap: "wrap", alignItems: "baseline", gap: "6px" },
@@ -513,22 +682,12 @@ function DiffLeaf({ label, entry }) {
513
682
  ":"
514
683
  ]
515
684
  }, 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),
685
+ latestFirst ? added : removed,
522
686
  entry.kind === "changed" && /* @__PURE__ */ jsxDEV2("span", {
523
687
  style: { color: DEVTOOL_COLOR_TEXT_MUTED },
524
- children: "→"
688
+ children: latestFirst ? "←" : "→"
525
689
  }, 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)
690
+ latestFirst ? removed : added
532
691
  ]
533
692
  }, undefined, true, undefined, this);
534
693
  }
@@ -549,18 +708,8 @@ function DiffLeaf({ label, entry }) {
549
708
  },
550
709
  children: label
551
710
  }, undefined, false, undefined, this),
552
- showRemoved && /* @__PURE__ */ jsxDEV2(DiffLine, {
553
- sign: "−",
554
- color: DEVTOOL_COLOR_SEMANTIC_ERROR,
555
- background: REMOVED_BG,
556
- value: entry.before
557
- }, undefined, false, undefined, this),
558
- showAdded && /* @__PURE__ */ jsxDEV2(DiffLine, {
559
- sign: "+",
560
- color: DEVTOOL_COLOR_SEMANTIC_SUCCESS,
561
- background: ADDED_BG,
562
- value: entry.after
563
- }, undefined, false, undefined, this)
711
+ latestFirst ? addedLine : removedLine,
712
+ latestFirst ? removedLine : addedLine
564
713
  ]
565
714
  }, undefined, true, undefined, this);
566
715
  }
@@ -683,8 +832,21 @@ function Line({ sign, color, background, text }) {
683
832
  ]
684
833
  }, undefined, true, undefined, this);
685
834
  }
686
- function JsonDiffView({ before, after }) {
687
- const ops = computeLineDiff(safeStringify(before, 2), safeStringify(after, 2));
835
+ function JsonDiffView({
836
+ before,
837
+ after,
838
+ compress = false,
839
+ latestFirst = false
840
+ }) {
841
+ if (compress) {
842
+ return /* @__PURE__ */ jsxDEV3(CompressedDiffView, {
843
+ before,
844
+ after,
845
+ side: "unified",
846
+ latestFirst
847
+ }, undefined, false, undefined, this);
848
+ }
849
+ const ops = orderLineDiffOps(computeLineDiff(safeStringify(before, 2), safeStringify(after, 2)), latestFirst);
688
850
  if (!ops.some((op) => op.kind !== "common")) {
689
851
  return emptyNotice("No differences — before and after are structurally equal.");
690
852
  }
@@ -698,11 +860,70 @@ function JsonDiffView({ before, after }) {
698
860
  }, i, false, undefined, this))
699
861
  }, undefined, false, undefined, this);
700
862
  }
863
+ function CompressedDiffView({
864
+ before,
865
+ after,
866
+ side,
867
+ latestFirst = false
868
+ }) {
869
+ const lines = computeCompressedDiff(before, after, side, latestFirst);
870
+ if (!lines.some((line) => line.tone === "added" || line.tone === "removed")) {
871
+ return emptyNotice("No differences — before and after are structurally equal.");
872
+ }
873
+ return /* @__PURE__ */ jsxDEV3("pre", {
874
+ style: SURFACE_STYLE,
875
+ children: lines.map((line, i) => /* @__PURE__ */ jsxDEV3(CompressedLine, {
876
+ line
877
+ }, i, false, undefined, this))
878
+ }, undefined, false, undefined, this);
879
+ }
880
+ function CompressedLine({ line }) {
881
+ const indent = " ".repeat(line.depth);
882
+ if (line.tone === "placeholder") {
883
+ return /* @__PURE__ */ jsxDEV3("div", {
884
+ style: { display: "flex", padding: "0 10px", whiteSpace: "pre" },
885
+ children: [
886
+ /* @__PURE__ */ jsxDEV3("span", {
887
+ style: { width: "12px", flexShrink: 0 }
888
+ }, undefined, false, undefined, this),
889
+ /* @__PURE__ */ jsxDEV3("span", {
890
+ style: { flex: 1, minWidth: 0, color: DEVTOOL_COLOR_TEXT_MUTED, fontStyle: "italic" },
891
+ children: [
892
+ indent,
893
+ line.text
894
+ ]
895
+ }, undefined, true, undefined, this)
896
+ ]
897
+ }, undefined, true, undefined, this);
898
+ }
899
+ const background = line.tone === "removed" ? REMOVED_BG2 : line.tone === "added" ? ADDED_BG2 : "transparent";
900
+ const color = line.tone === "removed" ? DEVTOOL_COLOR_SEMANTIC_ERROR : DEVTOOL_COLOR_SEMANTIC_SUCCESS;
901
+ return /* @__PURE__ */ jsxDEV3("div", {
902
+ style: { display: "flex", background, padding: "0 10px", whiteSpace: "pre" },
903
+ children: [
904
+ /* @__PURE__ */ jsxDEV3("span", {
905
+ style: { width: "12px", flexShrink: 0, color, fontWeight: 700, userSelect: "none" },
906
+ children: line.sign
907
+ }, undefined, false, undefined, this),
908
+ /* @__PURE__ */ jsxDEV3("span", {
909
+ style: { flex: 1, minWidth: 0 },
910
+ children: renderColoredJson(`${indent}${line.text}`)
911
+ }, undefined, false, undefined, this)
912
+ ]
913
+ }, undefined, true, undefined, this);
914
+ }
701
915
  function HighlightedJsonView({
702
916
  before,
703
917
  after,
704
- side
918
+ side,
919
+ compress = false
705
920
  }) {
921
+ if (compress)
922
+ return /* @__PURE__ */ jsxDEV3(CompressedDiffView, {
923
+ before,
924
+ after,
925
+ side
926
+ }, undefined, false, undefined, this);
706
927
  const ops = computeLineDiff(safeStringify(before, 2), safeStringify(after, 2));
707
928
  const dropKind = side === "before" ? "added" : "removed";
708
929
  const highlightKind = side === "before" ? "removed" : "added";
@@ -1055,10 +1276,17 @@ function DevtoolsLauncher({ items }) {
1055
1276
  import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
1056
1277
  function ChangeDetailPanel({
1057
1278
  change,
1058
- onRevert
1279
+ onRevert,
1280
+ view,
1281
+ onViewChange,
1282
+ compress,
1283
+ onCompressChange,
1284
+ latestFirst,
1285
+ onLatestFirstChange
1059
1286
  }) {
1060
- const [view, setView] = useState2("props");
1061
1287
  const canRevert = change.inversePatches.length > 0;
1288
+ const showCompressToggle = view !== "props";
1289
+ const showOrderToggle = view === "props" || view === "diff";
1062
1290
  return /* @__PURE__ */ jsxDEV5("div", {
1063
1291
  style: {
1064
1292
  flex: 1,
@@ -1139,13 +1367,13 @@ function ChangeDetailPanel({
1139
1367
  },
1140
1368
  children: /* @__PURE__ */ jsxDEV5(SegmentedControl, {
1141
1369
  options: [
1142
- { value: "props", label: "Diff Props" },
1143
1370
  { value: "diff", label: "Diff View" },
1371
+ { value: "props", label: "Diff Props" },
1144
1372
  { value: "before", label: "Before" },
1145
1373
  { value: "after", label: "After" }
1146
1374
  ],
1147
1375
  value: view,
1148
- onChange: setView
1376
+ onChange: onViewChange
1149
1377
  }, undefined, false, undefined, this)
1150
1378
  }, undefined, false, undefined, this),
1151
1379
  /* @__PURE__ */ jsxDEV5("div", {
@@ -1156,32 +1384,110 @@ function ChangeDetailPanel({
1156
1384
  padding: "10px 12px"
1157
1385
  },
1158
1386
  children: [
1159
- view === "props" && /* @__PURE__ */ jsxDEV5(DiffView, {
1387
+ (showCompressToggle || showOrderToggle) && /* @__PURE__ */ jsxDEV5("div", {
1388
+ style: {
1389
+ display: "flex",
1390
+ flexWrap: "wrap",
1391
+ alignItems: "center",
1392
+ gap: "14px",
1393
+ marginBottom: "8px"
1394
+ },
1395
+ children: [
1396
+ showCompressToggle && /* @__PURE__ */ jsxDEV5(ToggleCheckbox, {
1397
+ checked: compress,
1398
+ onChange: onCompressChange,
1399
+ label: "Compress to changed paths only",
1400
+ title: "Collapse unchanged branches, showing only the address of what changed"
1401
+ }, undefined, false, undefined, this),
1402
+ showOrderToggle && /* @__PURE__ */ jsxDEV5(ToggleCheckbox, {
1403
+ checked: latestFirst,
1404
+ onChange: onLatestFirstChange,
1405
+ label: "Latest (+) change first",
1406
+ title: "Show the new value (+) above the previous value (−); off shows previous first"
1407
+ }, undefined, false, undefined, this)
1408
+ ]
1409
+ }, undefined, true, undefined, this),
1410
+ view === "diff" && /* @__PURE__ */ jsxDEV5(JsonDiffView, {
1160
1411
  before: change.prevSnapshot,
1161
- after: change.snapshot
1412
+ after: change.snapshot,
1413
+ compress,
1414
+ latestFirst
1162
1415
  }, undefined, false, undefined, this),
1163
- view === "diff" && /* @__PURE__ */ jsxDEV5(JsonDiffView, {
1416
+ view === "props" && /* @__PURE__ */ jsxDEV5(DiffView, {
1164
1417
  before: change.prevSnapshot,
1165
- after: change.snapshot
1418
+ after: change.snapshot,
1419
+ latestFirst
1166
1420
  }, undefined, false, undefined, this),
1167
1421
  view === "before" && /* @__PURE__ */ jsxDEV5(HighlightedJsonView, {
1168
1422
  before: change.prevSnapshot,
1169
1423
  after: change.snapshot,
1170
- side: "before"
1424
+ side: "before",
1425
+ compress
1171
1426
  }, undefined, false, undefined, this),
1172
1427
  view === "after" && /* @__PURE__ */ jsxDEV5(HighlightedJsonView, {
1173
1428
  before: change.prevSnapshot,
1174
1429
  after: change.snapshot,
1175
- side: "after"
1430
+ side: "after",
1431
+ compress
1176
1432
  }, undefined, false, undefined, this)
1177
1433
  ]
1178
1434
  }, undefined, true, undefined, this)
1179
1435
  ]
1180
1436
  }, undefined, true, undefined, this);
1181
1437
  }
1438
+ function ToggleCheckbox({
1439
+ checked,
1440
+ onChange,
1441
+ label,
1442
+ title
1443
+ }) {
1444
+ return /* @__PURE__ */ jsxDEV5("label", {
1445
+ style: {
1446
+ display: "flex",
1447
+ alignItems: "center",
1448
+ gap: "6px",
1449
+ color: DEVTOOL_COLOR_TEXT_MUTED,
1450
+ fontSize: "10px",
1451
+ fontFamily: SANS_FONT,
1452
+ cursor: "pointer",
1453
+ userSelect: "none"
1454
+ },
1455
+ title,
1456
+ children: [
1457
+ /* @__PURE__ */ jsxDEV5("input", {
1458
+ type: "checkbox",
1459
+ checked,
1460
+ onChange: (e) => onChange(e.target.checked),
1461
+ style: { cursor: "pointer", margin: 0 }
1462
+ }, undefined, false, undefined, this),
1463
+ label
1464
+ ]
1465
+ }, undefined, true, undefined, this);
1466
+ }
1182
1467
 
1183
1468
  // src/devtools/browser/components/ChangeList.tsx
1469
+ import { useMemo } from "react";
1184
1470
  import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
1471
+ function changeGroupKey(change) {
1472
+ const shape = change.patches.length > 0 ? safeStringify(change.patches, 0) : safeStringify(change.snapshot, 0);
1473
+ return `${change.storeId}|${change.source}|${shape}`;
1474
+ }
1475
+ function groupChanges(changes) {
1476
+ const groups = [];
1477
+ let lastKey = null;
1478
+ for (const change of changes) {
1479
+ const key = changeGroupKey(change);
1480
+ const last = groups[groups.length - 1];
1481
+ if (last != null && key === lastKey) {
1482
+ last.count++;
1483
+ last.oldestCuid = change.cuid;
1484
+ } else {
1485
+ groups.push({ representative: change, oldestCuid: change.cuid, count: 1 });
1486
+ }
1487
+ lastKey = key;
1488
+ }
1489
+ return groups;
1490
+ }
1185
1491
  function ChangeList({
1186
1492
  changes,
1187
1493
  selectedCuid,
@@ -1189,6 +1495,7 @@ function ChangeList({
1189
1495
  showStore,
1190
1496
  style
1191
1497
  }) {
1498
+ const groups = useMemo(() => groupChanges(changes), [changes]);
1192
1499
  if (changes.length === 0) {
1193
1500
  return /* @__PURE__ */ jsxDEV6("div", {
1194
1501
  style: {
@@ -1203,16 +1510,18 @@ function ChangeList({
1203
1510
  }
1204
1511
  return /* @__PURE__ */ jsxDEV6("div", {
1205
1512
  style,
1206
- children: changes.map((change) => /* @__PURE__ */ jsxDEV6(ChangeRow, {
1207
- change,
1208
- selected: change.cuid === selectedCuid,
1209
- onClick: () => onSelect(change.cuid),
1513
+ children: groups.map((group) => /* @__PURE__ */ jsxDEV6(ChangeRow, {
1514
+ change: group.representative,
1515
+ count: group.count,
1516
+ selected: group.representative.cuid === selectedCuid,
1517
+ onClick: () => onSelect(group.representative.cuid),
1210
1518
  showStore
1211
- }, change.cuid, false, undefined, this))
1519
+ }, group.oldestCuid, false, undefined, this))
1212
1520
  }, undefined, false, undefined, this);
1213
1521
  }
1214
1522
  function ChangeRow({
1215
1523
  change,
1524
+ count,
1216
1525
  selected,
1217
1526
  onClick,
1218
1527
  showStore
@@ -1247,6 +1556,23 @@ function ChangeRow({
1247
1556
  color: sourceColor,
1248
1557
  children: SOURCE_LABEL[change.source]
1249
1558
  }, undefined, false, undefined, this),
1559
+ count > 1 && /* @__PURE__ */ jsxDEV6("span", {
1560
+ title: `${count} consecutive identical updates`,
1561
+ style: {
1562
+ flexShrink: 0,
1563
+ padding: "0 5px",
1564
+ borderRadius: "999px",
1565
+ background: DEVTOOL_SECTION_BACKGROUND,
1566
+ color: DEVTOOL_COLOR_TEXT_SECONDARY,
1567
+ fontSize: "9px",
1568
+ fontWeight: 700,
1569
+ fontFamily: MONO_FONT
1570
+ },
1571
+ children: [
1572
+ "×",
1573
+ count
1574
+ ]
1575
+ }, undefined, true, undefined, this),
1250
1576
  showStore && /* @__PURE__ */ jsxDEV6("span", {
1251
1577
  style: {
1252
1578
  color: DEVTOOL_COLOR_SEMANTIC_SYSTEM,
@@ -1303,7 +1629,7 @@ function Badge({ color, children }) {
1303
1629
  }
1304
1630
 
1305
1631
  // src/devtools/browser/components/StateInspector.tsx
1306
- import { useEffect, useState as useState3 } from "react";
1632
+ import { useEffect, useState as useState2 } from "react";
1307
1633
 
1308
1634
  // src/devtools/browser/components/SectionLabel.tsx
1309
1635
  import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
@@ -1332,9 +1658,9 @@ function StateInspector({
1332
1658
  onApply
1333
1659
  }) {
1334
1660
  const liveText = safeStringify(store.currentState, 2);
1335
- const [draft, setDraft] = useState3(liveText);
1336
- const [dirty, setDirty] = useState3(false);
1337
- const [error, setError] = useState3(null);
1661
+ const [draft, setDraft] = useState2(liveText);
1662
+ const [dirty, setDirty] = useState2(false);
1663
+ const [error, setError] = useState2(null);
1338
1664
  useEffect(() => {
1339
1665
  setDraft(liveText);
1340
1666
  setDirty(false);
@@ -1708,7 +2034,10 @@ function readPrefs(defaultPosition, initialOpen) {
1708
2034
  dockedHeight: DOCKED_HEIGHT_DEFAULT,
1709
2035
  dockedWidth: DOCKED_WIDTH_DEFAULT,
1710
2036
  detailRatio: DETAIL_RATIO_DEFAULT,
1711
- stayOnLatest: false
2037
+ stayOnLatest: true,
2038
+ compressDiff: true,
2039
+ detailView: "diff",
2040
+ diffLatestFirst: true
1712
2041
  };
1713
2042
  try {
1714
2043
  if (typeof localStorage === "undefined")
@@ -1741,11 +2070,11 @@ function NiceStateDevtools_Panel({
1741
2070
  position: defaultPosition = "dock-bottom",
1742
2071
  initialOpen = false
1743
2072
  }) {
1744
- const [prefs, setPrefsRaw] = useState4(() => readPrefs(defaultPosition, initialOpen));
1745
- const [snapshot, setSnapshot] = useState4(EMPTY_SNAPSHOT);
1746
- const [mode, setMode] = useState4("timeline");
1747
- const [storeFilter, setStoreFilter] = useState4(null);
1748
- const [selectedChangeCuid, setSelectedChangeCuid] = useState4(null);
2073
+ const [prefs, setPrefsRaw] = useState3(() => readPrefs(defaultPosition, initialOpen));
2074
+ const [snapshot, setSnapshot] = useState3(EMPTY_SNAPSHOT);
2075
+ const [mode, setMode] = useState3("timeline");
2076
+ const [storeFilter, setStoreFilter] = useState3(null);
2077
+ const [selectedChangeCuid, setSelectedChangeCuid] = useState3(null);
1749
2078
  useEffect2(() => core.subscribe(setSnapshot), [core]);
1750
2079
  const setPrefs = (update) => {
1751
2080
  setPrefsRaw((prev) => ({ ...prev, ...update }));
@@ -1755,11 +2084,21 @@ function NiceStateDevtools_Panel({
1755
2084
  return () => clearTimeout(timer);
1756
2085
  }, [prefs]);
1757
2086
  const { stores, changes, paused } = snapshot;
1758
- const { position, isOpen, dockedHeight, dockedWidth, detailRatio, stayOnLatest } = prefs;
2087
+ const {
2088
+ position,
2089
+ isOpen,
2090
+ dockedHeight,
2091
+ dockedWidth,
2092
+ detailRatio,
2093
+ stayOnLatest,
2094
+ compressDiff,
2095
+ detailView,
2096
+ diffLatestFirst
2097
+ } = prefs;
1759
2098
  const dockSide = getDockSide(position);
1760
2099
  const isHorizDock = dockSide === "top" || dockSide === "bottom";
1761
2100
  const dockedSize = isHorizDock ? dockedHeight : dockedWidth;
1762
- const filteredChanges = useMemo(() => storeFilter == null ? changes : changes.filter((c) => c.storeId === storeFilter), [changes, storeFilter]);
2101
+ const filteredChanges = useMemo2(() => storeFilter == null ? changes : changes.filter((c) => c.storeId === storeFilter), [changes, storeFilter]);
1763
2102
  const selectedChange = selectedChangeCuid != null ? changes.find((c) => c.cuid === selectedChangeCuid) ?? null : null;
1764
2103
  const latestCuid = filteredChanges.length > 0 ? filteredChanges[0].cuid : null;
1765
2104
  useEffect2(() => {
@@ -1767,7 +2106,7 @@ function NiceStateDevtools_Panel({
1767
2106
  setSelectedChangeCuid(latestCuid);
1768
2107
  }, [stayOnLatest, latestCuid]);
1769
2108
  const activeStore = stores.find((s) => s.id === storeFilter) ?? (stores.length > 0 ? stores[0] : null);
1770
- const dock = useMemo(() => getDevtoolsDockCoordinator(), []);
2109
+ const dock = useMemo2(() => getDevtoolsDockCoordinator(), []);
1771
2110
  const panelId = useId();
1772
2111
  const [, bumpView] = useReducer((n) => n + 1, 0);
1773
2112
  const badge = changes.length > 0 ? String(changes.length) : undefined;
@@ -1947,7 +2286,13 @@ function NiceStateDevtools_Panel({
1947
2286
  },
1948
2287
  children: /* @__PURE__ */ jsxDEV10(ChangeDetailPanel, {
1949
2288
  change: selectedChange,
1950
- onRevert: (c) => core.revertChange(c)
2289
+ onRevert: (c) => core.revertChange(c),
2290
+ view: detailView,
2291
+ onViewChange: (v) => setPrefs({ detailView: v }),
2292
+ compress: compressDiff,
2293
+ onCompressChange: (v) => setPrefs({ compressDiff: v }),
2294
+ latestFirst: diffLatestFirst,
2295
+ onLatestFirstChange: (v) => setPrefs({ diffLatestFirst: v })
1951
2296
  }, undefined, false, undefined, this)
1952
2297
  }, undefined, false, undefined, this)
1953
2298
  ]
@@ -1,5 +1,12 @@
1
1
  import type { IDevtoolsStateChange } from "../../core/StateDevtools.types";
2
- export declare function ChangeDetailPanel({ change, onRevert, }: {
2
+ export type TChangeView = "diff" | "props" | "before" | "after";
3
+ export declare function ChangeDetailPanel({ change, onRevert, view, onViewChange, compress, onCompressChange, latestFirst, onLatestFirstChange, }: {
3
4
  change: IDevtoolsStateChange;
4
5
  onRevert: (change: IDevtoolsStateChange) => void;
6
+ view: TChangeView;
7
+ onViewChange: (view: TChangeView) => void;
8
+ compress: boolean;
9
+ onCompressChange: (compress: boolean) => void;
10
+ latestFirst: boolean;
11
+ onLatestFirstChange: (latestFirst: boolean) => void;
5
12
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,4 @@
1
- import type { CSSProperties } from "react";
1
+ import { type CSSProperties } from "react";
2
2
  import type { IDevtoolsStateChange } from "../../core/StateDevtools.types";
3
3
  export declare function ChangeList({ changes, selectedCuid, onSelect, showStore, style, }: {
4
4
  changes: IDevtoolsStateChange[];
@@ -6,7 +6,8 @@
6
6
  * lone deep changes stay on one line. Short values render inline with the key
7
7
  * (`count: 0 → 1`); long ones keep the removed/added line block.
8
8
  */
9
- export declare function DiffView({ before, after }: {
9
+ export declare function DiffView({ before, after, latestFirst, }: {
10
10
  before: unknown;
11
11
  after: unknown;
12
+ latestFirst?: boolean;
12
13
  }): import("react/jsx-runtime").JSX.Element;
@@ -4,9 +4,11 @@
4
4
  * context while removed lines (red, `−`) and added lines (green, `+`) call out
5
5
  * exactly which sections of the whole state moved.
6
6
  */
7
- export declare function JsonDiffView({ before, after }: {
7
+ export declare function JsonDiffView({ before, after, compress, latestFirst, }: {
8
8
  before: unknown;
9
9
  after: unknown;
10
+ compress?: boolean;
11
+ latestFirst?: boolean;
10
12
  }): import("react/jsx-runtime").JSX.Element;
11
13
  /**
12
14
  * The full JSON of one snapshot with the lines that differ from the other side
@@ -14,8 +16,9 @@ export declare function JsonDiffView({ before, after }: {
14
16
  * behind added lines on the "after" side. Lines common to both render plainly so
15
17
  * the highlight reads as "here is what changed within the whole state".
16
18
  */
17
- export declare function HighlightedJsonView({ before, after, side, }: {
19
+ export declare function HighlightedJsonView({ before, after, side, compress, }: {
18
20
  before: unknown;
19
21
  after: unknown;
20
22
  side: "before" | "after";
23
+ compress?: boolean;
21
24
  }): import("react/jsx-runtime").JSX.Element;
@@ -40,3 +40,32 @@ export interface ILineDiffOp {
40
40
  * comfortably cheap.
41
41
  */
42
42
  export declare function computeLineDiff(beforeText: string, afterText: string): ILineDiffOp[];
43
+ /**
44
+ * Reorder each contiguous changed hunk so additions sit above removals when
45
+ * `latestFirst` is set (the new state on top). Common lines anchor the hunks in
46
+ * place; only the −/+ lines within a run are regrouped, so the surrounding
47
+ * context never moves. With `latestFirst` off the ops are returned untouched.
48
+ */
49
+ export declare function orderLineDiffOps(ops: ILineDiffOp[], latestFirst: boolean): ILineDiffOp[];
50
+ export type TCompressedTone = "common" | "added" | "removed" | "placeholder";
51
+ /** Which document a compressed diff is being rendered for. */
52
+ export type TDiffSide = "unified" | "before" | "after";
53
+ export interface ICompressedLine {
54
+ /** Nesting depth — the renderer turns this into leading indentation. */
55
+ depth: number;
56
+ /** Gutter glyph: `+` added, `−` removed, or a space for structure/placeholder. */
57
+ sign: string;
58
+ tone: TCompressedTone;
59
+ /** The line's JSON (or placeholder) text, without indentation. */
60
+ text: string;
61
+ }
62
+ /**
63
+ * A structure-aware "address" diff: the JSON tree pruned down to just the
64
+ * branches that actually changed. Parents of a change keep their brackets so the
65
+ * path stays legible, and each run of untouched siblings collapses into a single
66
+ * `… N unchanged …` placeholder (which, for arrays, naturally reports the counts
67
+ * before and after a change). `side` picks which document we're rebuilding —
68
+ * `"unified"` shows removed (−) and added (+) together, while `"before"` /
69
+ * `"after"` show only the lines that belong to that one snapshot.
70
+ */
71
+ export declare function computeCompressedDiff(before: unknown, after: unknown, side: TDiffSide, latestFirst?: boolean): ICompressedLine[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nice-code/state",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {