@tanishqkancharla/diffmap 0.4.0 → 0.6.1

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]!;
@@ -83,14 +92,15 @@ function ServicePlayer(props: { diagram: ServiceDiagram } & SourceNavigation) {
83
92
  const goTo = (target: number, animate: boolean) =>
84
93
  setPosition(positionFor(diagram, target, animate && !reduceMotion));
85
94
 
95
+ // Events travel one after another, so the step settles when the last lands.
86
96
  useEffect(() => {
87
97
  if (position.phase !== "travel") return;
88
98
  const timer = setTimeout(
89
99
  () => setPosition({ step: position.step, phase: "settled" }),
90
- TRAVEL_MS,
100
+ TRAVEL_MS * (diagram.steps[position.step - 1]?.events.length ?? 1),
91
101
  );
92
102
  return () => clearTimeout(timer);
93
- }, [position]);
103
+ }, [position, diagram]);
94
104
 
95
105
  useEffect(() => {
96
106
  if (!playing || position.phase !== "settled") return;
@@ -129,33 +139,51 @@ function ServicePlayer(props: { diagram: ServiceDiagram } & SourceNavigation) {
129
139
  }
130
140
  };
131
141
 
132
- const frame = useStyles(styles.frame);
142
+ const frame = useStyles(pierreShell, styles.frame);
143
+ const body = useStyles(styles.body);
133
144
  const controls = useStyles(styles.controls);
134
145
  const scrubber = useStyles(styles.scrubber);
135
146
  const segment = useStyles(styles.segment);
136
- const detail = useStyles(styles.detail);
147
+ const detail = useStyles(fenceTitle, styles.detail);
137
148
  const counter = useStyles(styles.counter);
138
149
 
139
150
  return (
140
- <div
151
+ <figure
141
152
  className={frame}
142
153
  data-diffmap-kind="services"
143
154
  // Capture: Maui's Button stops arrow keys from bubbling.
144
155
  onKeyDownCapture={onKeyDown}
145
156
  >
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
- <>
157
+ {/* The caption leads: before step 1 it is the start caption, which
158
+ captions the whole diagram. */}
159
+ {(captioned !== undefined || last > 0) && (
160
+ <figcaption className={detail} aria-live="polite">
161
+ {captioned !== undefined && (
162
+ <Text size="sm" fontWeight={500}>
163
+ {current !== undefined && current.sign !== " " && (
164
+ <Text
165
+ size="sm"
166
+ fontWeight={600}
167
+ style={{
168
+ color:
169
+ current.sign === "+" ? colors.green[11] : colors.red[11],
170
+ }}
171
+ >
172
+ {signLabel[current.sign]}
173
+ </Text>
174
+ )}
175
+ {captioned.caption}
176
+ </Text>
177
+ )}
178
+ {captioned?.description !== undefined && (
179
+ <Text size="xs" color="lowContrast">
180
+ {captioned.description}
181
+ </Text>
182
+ )}
183
+ </figcaption>
184
+ )}
185
+ <div className={body}>
186
+ {last > 0 && (
159
187
  <div className={controls}>
160
188
  <Tooltip content="Restart" placement="top">
161
189
  <Button
@@ -220,6 +248,7 @@ function ServicePlayer(props: { diagram: ServiceDiagram } & SourceNavigation) {
220
248
  <button
221
249
  type="button"
222
250
  className={segment}
251
+ data-change={entry.sign}
223
252
  data-state={
224
253
  index + 1 === step
225
254
  ? "current"
@@ -227,7 +256,7 @@ function ServicePlayer(props: { diagram: ServiceDiagram } & SourceNavigation) {
227
256
  ? "done"
228
257
  : "todo"
229
258
  }
230
- aria-label={`Step ${index + 1}: ${entry.caption}`}
259
+ aria-label={`Step ${index + 1}: ${signLabel[entry.sign]}${entry.caption}`}
231
260
  aria-current={index + 1 === step ? "step" : undefined}
232
261
  onClick={() => {
233
262
  setPlaying(false);
@@ -241,24 +270,20 @@ function ServicePlayer(props: { diagram: ServiceDiagram } & SourceNavigation) {
241
270
  {step} / {last}
242
271
  </Text>
243
272
  </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>
273
+ )}
274
+ <ServiceStage
275
+ {...props}
276
+ state={shownState}
277
+ previousState={states[Math.max(step - 1, 0)]!}
278
+ changed={changed}
279
+ active={active}
280
+ events={current?.events ?? []}
281
+ updates={current?.updates ?? []}
282
+ phase={position.phase}
283
+ stepKey={step}
284
+ />
285
+ </div>
286
+ </figure>
262
287
  );
263
288
  }
264
289
 
@@ -273,17 +298,16 @@ function positionFor(
273
298
 
274
299
  const styles = {
275
300
  error: style(text({ size: "sm", color: "lowContrast" })),
276
- frame: style(
301
+ // A card like a code block: the caption row, then the controls and stage.
302
+ frame: style({ width: "100%", margin: 0 }),
303
+ body: style(
277
304
  flex({ direction: "column", gap: 4 }),
278
- radius.lg,
279
305
  spacing.padding({ all: 6 }),
280
- {
281
- width: "100%",
282
- minWidth: 0,
283
- border: `1px solid ${borderColor.outline}`,
284
- },
285
306
  ),
286
- controls: style(flex({ direction: "row", alignItems: "center", gap: 2 })),
307
+ controls: style(flex({ direction: "row", alignItems: "center", gap: 2 }), {
308
+ // Buttons keep their full label; only the step scrubber gives up width.
309
+ "& > button": { flexShrink: 0, overflow: "visible" },
310
+ }),
287
311
  scrubber: style(flex({ direction: "row", alignItems: "center", gap: 1 }), {
288
312
  flex: "1 1 auto",
289
313
  minWidth: 0,
@@ -312,12 +336,24 @@ const styles = {
312
336
  "&:hover::after": { backgroundColor: colors.gray[7] },
313
337
  "&[data-state='done']::after": { backgroundColor: colors.accent[7] },
314
338
  "&[data-state='current']::after": { backgroundColor: colors.accent[9] },
339
+ "&[data-change='+']::after": { backgroundColor: colors.green[6] },
340
+ "&[data-change='+'][data-state='done']::after": {
341
+ backgroundColor: colors.green[8],
342
+ },
343
+ "&[data-change='+'][data-state='current']::after": {
344
+ backgroundColor: colors.green[10],
345
+ },
346
+ "&[data-change='-']::after": { backgroundColor: colors.red[6] },
347
+ "&[data-change='-'][data-state='done']::after": {
348
+ backgroundColor: colors.red[8],
349
+ },
350
+ "&[data-change='-'][data-state='current']::after": {
351
+ backgroundColor: colors.red[10],
352
+ },
315
353
  },
316
354
  focusRing(),
317
355
  ),
318
356
  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
- }),
357
+ // Holds two lines so the controls below don't move between steps.
358
+ detail: style(flex({ direction: "column", gap: 2 }), { minHeight: 44 }),
323
359
  };