@tanishqkancharla/diffmap 0.4.0 → 0.6.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.
@@ -9,7 +9,6 @@ import {
9
9
  RotateAntiClockwise,
10
10
  Text,
11
11
  Tooltip,
12
- borderColor,
13
12
  colors,
14
13
  flex,
15
14
  focusRing,
@@ -19,6 +18,7 @@ import {
19
18
  } from "maui";
20
19
  import { style, useStyles } from "purse-styles";
21
20
  import type { SourceNavigation } from "../annotations.js";
21
+ import { fenceTitle, pierreShell } from "./pierre.js";
22
22
  import { ServiceStage, TRAVEL_MS } from "./ServiceStage.tsx";
23
23
  import {
24
24
  serviceStateAt,
@@ -28,10 +28,16 @@ import {
28
28
 
29
29
  const DWELL_MS = 1200;
30
30
 
31
+ /** Names a diff step's side before its caption. */
32
+ const signLabel = { "+": "Added: ", "-": "Removed: ", " ": "" } as const;
33
+
31
34
  type Phase = "travel" | "settled";
32
35
 
33
36
  export function ServiceDiagramBlock(
34
- props: { source: string; parsed: ServicesParse } & SourceNavigation,
37
+ props: {
38
+ source: string;
39
+ parsed: ServicesParse;
40
+ } & SourceNavigation,
35
41
  ) {
36
42
  const error = useStyles(styles.error);
37
43
  if (!props.parsed.ok)
@@ -61,13 +67,16 @@ function ServicePlayer(props: { diagram: ServiceDiagram } & SourceNavigation) {
61
67
  const [playing, setPlaying] = useState(false);
62
68
  const states = useMemo(
63
69
  () =>
64
- Array.from({ length: last + 1 }, (_, step) =>
65
- serviceStateAt(diagram, step),
66
- ),
70
+ Array.from({ length: last + 1 }, (_, step) => ({
71
+ today: serviceStateAt(diagram, step, "today"),
72
+ proposed: serviceStateAt(diagram, step, "proposed"),
73
+ })),
67
74
  [diagram, last],
68
75
  );
69
76
  const step = position.step;
70
77
  const current = step === 0 ? undefined : diagram.steps[step - 1]!;
78
+ // Before step 1, the author's start caption, if any, explains the state.
79
+ const captioned = current ?? diagram.start;
71
80
  const settled = position.phase === "settled";
72
81
  // In flight, the events have not landed, so show the previous state.
73
82
  const shownState = states[settled ? step : step - 1]!;
@@ -129,33 +138,51 @@ function ServicePlayer(props: { diagram: ServiceDiagram } & SourceNavigation) {
129
138
  }
130
139
  };
131
140
 
132
- const frame = useStyles(styles.frame);
141
+ const frame = useStyles(pierreShell, styles.frame);
142
+ const body = useStyles(styles.body);
133
143
  const controls = useStyles(styles.controls);
134
144
  const scrubber = useStyles(styles.scrubber);
135
145
  const segment = useStyles(styles.segment);
136
- const detail = useStyles(styles.detail);
146
+ const detail = useStyles(fenceTitle, styles.detail);
137
147
  const counter = useStyles(styles.counter);
138
148
 
139
149
  return (
140
- <div
150
+ <figure
141
151
  className={frame}
142
152
  data-diffmap-kind="services"
143
153
  // Capture: Maui's Button stops arrow keys from bubbling.
144
154
  onKeyDownCapture={onKeyDown}
145
155
  >
146
- <ServiceStage
147
- {...props}
148
- state={shownState}
149
- previousState={states[Math.max(step - 1, 0)]!}
150
- changed={changed}
151
- active={active}
152
- events={current?.events ?? []}
153
- updates={current?.updates ?? []}
154
- phase={position.phase}
155
- stepKey={step}
156
- />
157
- {last > 0 && (
158
- <>
156
+ {/* The caption leads: before step 1 it is the start caption, which
157
+ captions the whole diagram. */}
158
+ {(captioned !== undefined || last > 0) && (
159
+ <figcaption className={detail} aria-live="polite">
160
+ {captioned !== undefined && (
161
+ <Text size="sm" fontWeight={500}>
162
+ {current !== undefined && current.sign !== " " && (
163
+ <Text
164
+ size="sm"
165
+ fontWeight={600}
166
+ style={{
167
+ color:
168
+ current.sign === "+" ? colors.green[11] : colors.red[11],
169
+ }}
170
+ >
171
+ {signLabel[current.sign]}
172
+ </Text>
173
+ )}
174
+ {captioned.caption}
175
+ </Text>
176
+ )}
177
+ {captioned?.description !== undefined && (
178
+ <Text size="xs" color="lowContrast">
179
+ {captioned.description}
180
+ </Text>
181
+ )}
182
+ </figcaption>
183
+ )}
184
+ <div className={body}>
185
+ {last > 0 && (
159
186
  <div className={controls}>
160
187
  <Tooltip content="Restart" placement="top">
161
188
  <Button
@@ -220,6 +247,7 @@ function ServicePlayer(props: { diagram: ServiceDiagram } & SourceNavigation) {
220
247
  <button
221
248
  type="button"
222
249
  className={segment}
250
+ data-change={entry.sign}
223
251
  data-state={
224
252
  index + 1 === step
225
253
  ? "current"
@@ -227,7 +255,7 @@ function ServicePlayer(props: { diagram: ServiceDiagram } & SourceNavigation) {
227
255
  ? "done"
228
256
  : "todo"
229
257
  }
230
- aria-label={`Step ${index + 1}: ${entry.caption}`}
258
+ aria-label={`Step ${index + 1}: ${signLabel[entry.sign]}${entry.caption}`}
231
259
  aria-current={index + 1 === step ? "step" : undefined}
232
260
  onClick={() => {
233
261
  setPlaying(false);
@@ -241,24 +269,20 @@ function ServicePlayer(props: { diagram: ServiceDiagram } & SourceNavigation) {
241
269
  {step} / {last}
242
270
  </Text>
243
271
  </div>
244
- <div className={detail} aria-live="polite">
245
- <Text size="sm" fontWeight={500}>
246
- {current?.caption ?? "Start"}
247
- </Text>
248
- {current === undefined && (
249
- <Text size="xs" color="lowContrast">
250
- Initial state. Press Play, or step with ← and →.
251
- </Text>
252
- )}
253
- {current?.description !== undefined && (
254
- <Text size="xs" color="lowContrast">
255
- {current.description}
256
- </Text>
257
- )}
258
- </div>
259
- </>
260
- )}
261
- </div>
272
+ )}
273
+ <ServiceStage
274
+ {...props}
275
+ state={shownState}
276
+ previousState={states[Math.max(step - 1, 0)]!}
277
+ changed={changed}
278
+ active={active}
279
+ events={current?.events ?? []}
280
+ updates={current?.updates ?? []}
281
+ phase={position.phase}
282
+ stepKey={step}
283
+ />
284
+ </div>
285
+ </figure>
262
286
  );
263
287
  }
264
288
 
@@ -273,17 +297,16 @@ function positionFor(
273
297
 
274
298
  const styles = {
275
299
  error: style(text({ size: "sm", color: "lowContrast" })),
276
- frame: style(
300
+ // A card like a code block: the caption row, then the controls and stage.
301
+ frame: style({ width: "100%", margin: 0 }),
302
+ body: style(
277
303
  flex({ direction: "column", gap: 4 }),
278
- radius.lg,
279
304
  spacing.padding({ all: 6 }),
280
- {
281
- width: "100%",
282
- minWidth: 0,
283
- border: `1px solid ${borderColor.outline}`,
284
- },
285
305
  ),
286
- controls: style(flex({ direction: "row", alignItems: "center", gap: 2 })),
306
+ controls: style(flex({ direction: "row", alignItems: "center", gap: 2 }), {
307
+ // Buttons keep their full label; only the step scrubber gives up width.
308
+ "& > button": { flexShrink: 0, overflow: "visible" },
309
+ }),
287
310
  scrubber: style(flex({ direction: "row", alignItems: "center", gap: 1 }), {
288
311
  flex: "1 1 auto",
289
312
  minWidth: 0,
@@ -312,12 +335,24 @@ const styles = {
312
335
  "&:hover::after": { backgroundColor: colors.gray[7] },
313
336
  "&[data-state='done']::after": { backgroundColor: colors.accent[7] },
314
337
  "&[data-state='current']::after": { backgroundColor: colors.accent[9] },
338
+ "&[data-change='+']::after": { backgroundColor: colors.green[6] },
339
+ "&[data-change='+'][data-state='done']::after": {
340
+ backgroundColor: colors.green[8],
341
+ },
342
+ "&[data-change='+'][data-state='current']::after": {
343
+ backgroundColor: colors.green[10],
344
+ },
345
+ "&[data-change='-']::after": { backgroundColor: colors.red[6] },
346
+ "&[data-change='-'][data-state='done']::after": {
347
+ backgroundColor: colors.red[8],
348
+ },
349
+ "&[data-change='-'][data-state='current']::after": {
350
+ backgroundColor: colors.red[10],
351
+ },
315
352
  },
316
353
  focusRing(),
317
354
  ),
318
355
  counter: style({ flexShrink: 0, minWidth: "5ch", textAlign: "end" }),
319
- detail: style(flex({ direction: "column", gap: 2 }), {
320
- minHeight: 44,
321
- paddingInline: spacing.value(2),
322
- }),
356
+ // Holds two lines so the controls below don't move between steps.
357
+ detail: style(flex({ direction: "column", gap: 2 }), { minHeight: 44 }),
323
358
  };
@@ -22,14 +22,17 @@ import { style, useStyles } from "purse-styles";
22
22
  import type { SourceNavigation } from "../annotations.js";
23
23
  import {
24
24
  flattenServices,
25
+ serviceStateAt,
25
26
  type Service,
26
27
  type ServiceDiagram,
27
28
  type ServiceEvent,
28
29
  type ServiceUpdate,
30
+ type Sign,
29
31
  } from "../services.js";
30
32
  import {
31
33
  layoutServices,
32
34
  linkId,
35
+ pillText,
33
36
  serviceLinks,
34
37
  type Point,
35
38
  type Rect,
@@ -53,10 +56,15 @@ type StepNote = {
53
56
 
54
57
  type State = Map<string, Map<string, string>>;
55
58
 
59
+ /** State replayed on each side of a diff; the sides match outside a diff. */
60
+ export type SidedState = { today: State; proposed: State };
61
+
62
+ type Values = { today: Map<string, string>; proposed: Map<string, string> };
63
+
56
64
  export type StageProps = {
57
65
  diagram: ServiceDiagram;
58
- state: State;
59
- previousState: State;
66
+ state: SidedState;
67
+ previousState: SidedState;
60
68
  changed: Set<string>;
61
69
  active: Set<string>;
62
70
  events: ServiceEvent[];
@@ -306,7 +314,10 @@ export function ServiceStage(props: StageProps) {
306
314
  <ServiceCard
307
315
  {...props}
308
316
  service={service}
309
- values={widest.get(service.name)!}
317
+ values={{
318
+ today: widest.get(service.name)!,
319
+ proposed: widest.get(service.name)!,
320
+ }}
310
321
  previous={undefined}
311
322
  changed={new Set()}
312
323
  />
@@ -339,6 +350,7 @@ export function ServiceStage(props: StageProps) {
339
350
  key={service.name}
340
351
  className={box}
341
352
  data-depth={depth.get(service.name)}
353
+ data-change={service.sign}
342
354
  data-active={props.active.has(service.name)}
343
355
  aria-label={service.name}
344
356
  style={{
@@ -351,8 +363,8 @@ export function ServiceStage(props: StageProps) {
351
363
  <ServiceCard
352
364
  {...props}
353
365
  service={service}
354
- values={props.state.get(service.name)!}
355
- previous={props.previousState.get(service.name)!}
366
+ values={valuesOf(props.state, service.name)}
367
+ previous={valuesOf(props.previousState, service.name)}
356
368
  />
357
369
  </section>
358
370
  );
@@ -364,10 +376,10 @@ export function ServiceStage(props: StageProps) {
364
376
  aria-hidden="true"
365
377
  >
366
378
  <defs>
367
- {(["idle", "active"] as const).map((kind) => (
379
+ {tones.map((tone) => (
368
380
  <marker
369
- key={kind}
370
- id={`${markerId}-${kind}`}
381
+ key={tone.id}
382
+ id={`${markerId}-${tone.id}`}
371
383
  viewBox="0 0 10 10"
372
384
  refX="9"
373
385
  refY="5"
@@ -375,26 +387,26 @@ export function ServiceStage(props: StageProps) {
375
387
  markerHeight="7"
376
388
  orient="auto-start-reverse"
377
389
  >
378
- <path
379
- d="M0,1 L9,5 L0,9 z"
380
- style={{
381
- fill:
382
- kind === "active" ? colors.accent[9] : colors.gray[8],
383
- }}
384
- />
390
+ <path d="M0,1 L9,5 L0,9 z" style={{ fill: tone.head }} />
385
391
  </marker>
386
392
  ))}
387
393
  </defs>
388
394
  {links.map((link) => {
389
395
  const route = trimmedRoute(layout, link, parents);
390
396
  const sending = (from: string, to: string) =>
391
- props.events.some(
397
+ props.events.filter(
392
398
  (event) => event.from === from && event.to === to,
393
399
  );
394
400
  const forward = sending(link.a, link.b);
395
401
  const backward = sending(link.b, link.a);
396
- const marker = (active: boolean) =>
397
- `url(#${markerId}-${active ? "active" : "idle"})`;
402
+ const active = forward.length > 0 || backward.length > 0;
403
+ // While its events travel, a link takes their sign.
404
+ const tone = toneFor(
405
+ active,
406
+ active ? commonSign([...forward, ...backward]) : link.sign,
407
+ );
408
+ const marker = (sent: ServiceEvent[]) =>
409
+ `url(#${markerId}-${(sent.length > 0 ? tone : toneFor(false, link.sign)).id})`;
398
410
  return (
399
411
  <path
400
412
  key={link.id}
@@ -403,10 +415,9 @@ export function ServiceStage(props: StageProps) {
403
415
  markerEnd={link.forward ? marker(forward) : undefined}
404
416
  markerStart={link.backward ? marker(backward) : undefined}
405
417
  style={{
406
- stroke:
407
- forward || backward ? colors.accent[9] : colors.gray[7],
408
- strokeWidth: forward || backward ? 2 : 1,
409
- strokeDasharray: forward || backward ? undefined : "4 4",
418
+ stroke: tone.line,
419
+ strokeWidth: active ? 2 : 1,
420
+ strokeDasharray: active ? undefined : "4 4",
410
421
  }}
411
422
  />
412
423
  );
@@ -522,7 +533,7 @@ function EventMarks(
522
533
  const labelAnchor = useStyles(styles.labelAnchor);
523
534
  const pill = useStyles(styles.pill);
524
535
  const annotation = props.event.annotation;
525
- const label = props.event.label === "" ? undefined : props.event.label;
536
+ const label = props.event.label === "" ? undefined : pillText(props.event);
526
537
  const lengths = cumulativeLengths(props.points);
527
538
  const total = lengths.at(-1)! || 1;
528
539
  return (
@@ -530,6 +541,7 @@ function EventMarks(
530
541
  {props.phase === "travel" && (
531
542
  <motion.div
532
543
  className={packet}
544
+ data-change={props.event.sign}
533
545
  initial={{ x: props.points[0]!.x, y: props.points[0]!.y }}
534
546
  animate={{
535
547
  x: props.points.map((point) => point.x),
@@ -554,17 +566,20 @@ function EventMarks(
554
566
  >
555
567
  {label !== undefined &&
556
568
  (annotation === undefined ? (
557
- <div className={pill}>{label}</div>
569
+ <div className={pill} data-change={props.event.sign}>
570
+ {label}
571
+ </div>
558
572
  ) : (
559
573
  <button
560
574
  type="button"
561
575
  className={pill}
576
+ data-change={props.event.sign}
562
577
  aria-pressed={props.selectedAnnotation === annotation}
563
578
  aria-controls="source-diff-panel"
564
579
  aria-label={`Open source: ${annotation.text}`}
565
580
  onClick={() => props.onSelectAnnotation(annotation)}
566
581
  >
567
- {label} ↗
582
+ {label}
568
583
  </button>
569
584
  ))}
570
585
  </motion.div>
@@ -577,8 +592,8 @@ function EventMarks(
577
592
  function ServiceCard(
578
593
  props: {
579
594
  service: Service;
580
- values: Map<string, string>;
581
- previous: Map<string, string> | undefined;
595
+ values: Values;
596
+ previous: Values | undefined;
582
597
  changed: Set<string>;
583
598
  } & SourceNavigation,
584
599
  ) {
@@ -595,7 +610,10 @@ function ServiceCard(
595
610
  <Text
596
611
  size="sm"
597
612
  fontWeight={600}
598
- style={{ whiteSpace: "nowrap" }}
613
+ style={{
614
+ whiteSpace: "nowrap",
615
+ textDecorationLine: service.sign === "-" ? "line-through" : undefined,
616
+ }}
599
617
  data-described={described}
600
618
  >
601
619
  {service.name}
@@ -639,13 +657,18 @@ function ServiceCard(
639
657
  ["--key-width" as string]: `${Math.max(...service.state.map(({ key }) => key.length))}ch`,
640
658
  }}
641
659
  >
642
- {service.state.map(({ key, description }) => {
660
+ {service.state.map(({ key, description, sign }) => {
643
661
  const changed = props.changed.has(`${service.name}.${key}`);
662
+ // A row added or removed in a diff shows only its own side.
663
+ const side = sign === "-" ? "today" : "proposed";
664
+ const today = props.values.today.get(key);
665
+ const proposed = props.values.proposed.get(key);
644
666
  return (
645
667
  <div
646
668
  key={key}
647
669
  className={stateRow}
648
670
  data-changed={changed}
671
+ data-change={sign}
649
672
  data-state-row={`${service.name}.${key}`}
650
673
  >
651
674
  <Described description={description}>
@@ -657,18 +680,39 @@ function ServiceCard(
657
680
  </dt>
658
681
  </Described>
659
682
  <dd>
660
- {changed && (
661
- <Text size="xs" monospace color="lowContrast">
662
- <s>{props.previous?.get(key)}</s>{" "}
663
- </Text>
683
+ {sign === " " && today !== proposed ? (
684
+ <>
685
+ <Text
686
+ size="xs"
687
+ monospace
688
+ style={{ color: colors.red[11] }}
689
+ >
690
+ <s>{today}</s>{" "}
691
+ </Text>
692
+ <Text
693
+ size="xs"
694
+ monospace
695
+ style={{ color: colors.green[11] }}
696
+ >
697
+ {proposed}
698
+ </Text>
699
+ </>
700
+ ) : (
701
+ <>
702
+ {changed && (
703
+ <Text size="xs" monospace color="lowContrast">
704
+ <s>{props.previous?.[side].get(key)}</s>{" "}
705
+ </Text>
706
+ )}
707
+ <Text
708
+ size="xs"
709
+ monospace
710
+ color={changed ? "accent" : undefined}
711
+ >
712
+ {props.values[side].get(key)}
713
+ </Text>
714
+ </>
664
715
  )}
665
- <Text
666
- size="xs"
667
- monospace
668
- color={changed ? "accent" : undefined}
669
- >
670
- {props.values.get(key)}
671
- </Text>
672
716
  </dd>
673
717
  </div>
674
718
  );
@@ -694,30 +738,91 @@ export function Described(props: {
694
738
 
695
739
  /**
696
740
  * Each state value at its widest, including the struck-through old value a
697
- * changed row shows beside the new one.
741
+ * changed row shows beside the new one, and a diff's Today value beside its
742
+ * Proposed one.
698
743
  */
699
744
  function widestState(diagram: ServiceDiagram) {
700
- const current = new Map(
701
- flattenServices(diagram.services).map(({ service }) => [
702
- service.name,
703
- new Map(service.state.map((entry) => [entry.key, entry.value])),
704
- ]),
705
- );
745
+ const flat = flattenServices(diagram.services);
706
746
  const widest = new Map(
707
- [...current].map(([name, values]) => [name, new Map(values)]),
747
+ flat.map(({ service }) => [service.name, new Map<string, string>()]),
708
748
  );
709
- for (const step of diagram.steps)
710
- for (const update of step.updates) {
711
- const values = current.get(update.service)!;
712
- const shown = `${values.get(update.key)} ${update.value}`;
713
- const best = widest.get(update.service)!;
714
- if (shown.length > best.get(update.key)!.length)
715
- best.set(update.key, shown);
716
- values.set(update.key, update.value);
717
- }
749
+ const at = (step: number) => ({
750
+ today: serviceStateAt(diagram, step, "today"),
751
+ proposed: serviceStateAt(diagram, step, "proposed"),
752
+ });
753
+ let before = at(0);
754
+ for (let step = 0; step <= diagram.steps.length; step++) {
755
+ const now = step === 0 ? before : at(step);
756
+ // A row updated in this step shows its old value struck through.
757
+ const updated = new Set(
758
+ diagram.steps[step - 1]?.updates.map(
759
+ (update) => `${update.service}.${update.key}`,
760
+ ),
761
+ );
762
+ for (const { service } of flat)
763
+ for (const { key } of service.state) {
764
+ const best = widest.get(service.name)!;
765
+ const value = (state: State) => state.get(service.name)!.get(key);
766
+ const today = value(now.today);
767
+ const proposed = value(now.proposed);
768
+ const shown = [
769
+ today === undefined || proposed === undefined || today === proposed
770
+ ? (proposed ?? today ?? "")
771
+ : `${today} ${proposed}`,
772
+ ];
773
+ if (updated.has(`${service.name}.${key}`))
774
+ shown.push(
775
+ `${value(before.today) ?? ""} ${today ?? ""}`,
776
+ `${value(before.proposed) ?? ""} ${proposed ?? ""}`,
777
+ );
778
+ for (const candidate of shown)
779
+ if (candidate.length > (best.get(key)?.length ?? -1))
780
+ best.set(key, candidate);
781
+ }
782
+ before = now;
783
+ }
718
784
  return widest;
719
785
  }
720
786
 
787
+ function valuesOf(state: SidedState, service: string): Values {
788
+ return {
789
+ today: state.today.get(service)!,
790
+ proposed: state.proposed.get(service)!,
791
+ };
792
+ }
793
+
794
+ /** The sign every event shares, or " " when they differ. */
795
+ function commonSign(events: ServiceEvent[]): Sign {
796
+ return events.every((event) => event.sign === events[0]!.sign)
797
+ ? events[0]!.sign
798
+ : " ";
799
+ }
800
+
801
+ /** Link colors: idle or carrying events, and Today only, Proposed only, or both. */
802
+ const tones = (["idle", "active"] as const).flatMap((state) =>
803
+ (["+", "-", " "] as const).map((sign) => {
804
+ const scale =
805
+ sign === "+" ? colors.green : sign === "-" ? colors.red : undefined;
806
+ const active = state === "active";
807
+ const line = active
808
+ ? (scale?.[9] ?? colors.accent[9])
809
+ : (scale?.[8] ?? colors.gray[7]);
810
+ return {
811
+ id: `${state}-${{ "+": "added", "-": "removed", " ": "same" }[sign]}`,
812
+ state,
813
+ sign,
814
+ line,
815
+ head: active || scale !== undefined ? line : colors.gray[8],
816
+ };
817
+ }),
818
+ );
819
+
820
+ function toneFor(active: boolean, sign: Sign) {
821
+ return tones.find(
822
+ (tone) => tone.sign === sign && (tone.state === "active") === active,
823
+ )!;
824
+ }
825
+
721
826
  function isAncestor(
722
827
  parents: Map<string, string | undefined>,
723
828
  ancestor: string,
@@ -878,7 +983,18 @@ const styles = {
878
983
  "&[data-depth='1'], &[data-depth='3']": {
879
984
  backgroundColor: colors.gray[2],
880
985
  },
986
+ "&[data-change='+']": {
987
+ borderColor: colors.green[8],
988
+ backgroundColor: colors.green[2],
989
+ },
990
+ "&[data-change='-']": {
991
+ borderColor: colors.red[8],
992
+ borderStyle: "dashed",
993
+ backgroundColor: colors.red[2],
994
+ },
881
995
  "&[data-active='true']": { borderColor: colors.accent[8] },
996
+ "&[data-active='true'][data-change='+']": { borderColor: colors.green[10] },
997
+ "&[data-active='true'][data-change='-']": { borderColor: colors.red[10] },
882
998
  }),
883
999
  header: style(
884
1000
  flex({ direction: "row", alignItems: "baseline", gap: 3, wrap: true }),
@@ -928,6 +1044,14 @@ const styles = {
928
1044
  columnGap: spacing.value(4),
929
1045
  paddingInline: spacing.value(2),
930
1046
  marginInline: `-${spacing.value(2)}`,
1047
+ "&[data-change='+']": {
1048
+ backgroundColor: colors.greenAlpha[3],
1049
+ boxShadow: `inset 2px 0 ${colors.green[9]}`,
1050
+ },
1051
+ "&[data-change='-']": {
1052
+ backgroundColor: colors.redAlpha[3],
1053
+ boxShadow: `inset 2px 0 ${colors.red[9]}`,
1054
+ },
931
1055
  "&[data-changed='true']": { backgroundColor: colors.accentAlpha[3] },
932
1056
  }),
933
1057
  packet: style(radius.circle, {
@@ -940,6 +1064,14 @@ const styles = {
940
1064
  boxShadow: `0 0 0 3px ${colors.accentAlpha[5]}`,
941
1065
  pointerEvents: "none",
942
1066
  zIndex: 2,
1067
+ "&[data-change='+']": {
1068
+ backgroundColor: colors.green[9],
1069
+ boxShadow: `0 0 0 3px ${colors.greenAlpha[5]}`,
1070
+ },
1071
+ "&[data-change='-']": {
1072
+ backgroundColor: colors.red[9],
1073
+ boxShadow: `0 0 0 3px ${colors.redAlpha[5]}`,
1074
+ },
943
1075
  }),
944
1076
  labelAnchor: style({
945
1077
  position: "absolute",
@@ -960,6 +1092,15 @@ const styles = {
960
1092
  border: `1px solid ${colors.accent[7]}`,
961
1093
  backgroundColor: backgroundColor.app,
962
1094
  color: colors.accent[11],
1095
+ "&[data-change='+']": {
1096
+ borderColor: colors.green[7],
1097
+ color: colors.green[11],
1098
+ },
1099
+ "&[data-change='-']": {
1100
+ borderColor: colors.red[7],
1101
+ borderStyle: "dashed",
1102
+ color: colors.red[11],
1103
+ },
963
1104
  "&:is(button)": { cursor: "pointer" },
964
1105
  "&:is(button):hover": { backgroundColor: colors.accent[3] },
965
1106
  "&[aria-pressed='true']": { backgroundColor: colors.blue[3] },