@robr0/design-system 0.15.0 → 0.16.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.
Files changed (45) hide show
  1. package/README.md +3 -3
  2. package/behaviors/focusable.d.ts +26 -0
  3. package/behaviors/focusable.js +18 -0
  4. package/behaviors/useFocusScope.d.ts +38 -0
  5. package/behaviors/useFocusScope.js +82 -0
  6. package/behaviors/useLayer.d.ts +43 -0
  7. package/behaviors/useLayer.js +55 -0
  8. package/behaviors/useMounted.d.ts +7 -0
  9. package/behaviors/useMounted.js +14 -0
  10. package/behaviors/useScrollLock.d.ts +55 -0
  11. package/behaviors/useScrollLock.js +59 -0
  12. package/components/AgentPlan/AgentPlan.css +12 -3
  13. package/components/AiButton/AiButton.css +327 -0
  14. package/components/AiButton/AiButton.d.ts +42 -0
  15. package/components/AiButton/AiButton.js +140 -16
  16. package/components/AlertDialog/AlertDialog.js +13 -58
  17. package/components/Button/Button.css +3 -0
  18. package/components/Combobox/Combobox.js +2 -1
  19. package/components/CommandPalette/CommandPalette.css +10 -7
  20. package/components/CommandPalette/CommandPalette.js +9 -58
  21. package/components/Composer/Composer.css +54 -0
  22. package/components/Composer/Composer.d.ts +21 -4
  23. package/components/Composer/Composer.js +7 -0
  24. package/components/Dialog/Dialog.js +9 -62
  25. package/components/Drawer/Drawer.js +9 -62
  26. package/components/Dropdown/Dropdown.js +2 -1
  27. package/components/DropdownMenu/DropdownMenu.js +11 -3
  28. package/components/ModelPicker/ModelPicker.js +2 -1
  29. package/components/Popover/Popover.js +3 -0
  30. package/components/SegmentedControl/SegmentedControl.css +4 -2
  31. package/components/SourceTrail/SourceTrail.css +2 -2
  32. package/components/TimePicker/TimePicker.js +2 -1
  33. package/components/ToolCall/ToolCall.css +9 -1
  34. package/components/Tooltip/Tooltip.js +6 -0
  35. package/components/registry.json +2 -2
  36. package/components/registry.json.d.ts +2 -2
  37. package/components/registry.json.js +1 -1
  38. package/package.json +5 -1
  39. package/tokens/motion.d.ts +10 -4
  40. package/tokens/motion.js +7 -1
  41. package/tokens/registry.json +6 -0
  42. package/tokens/registry.json.d.ts +6 -0
  43. package/tokens/registry.json.js +1 -1
  44. package/tokens/tokens-dark.css +11 -0
  45. package/tokens/tokens-light.css +13 -0
package/README.md CHANGED
@@ -153,17 +153,17 @@ The same generated data is served as files too: every component's prop contract
153
153
  - **Vite 7**: dev server and library build
154
154
  - **Next.js 16**: portfolio site and design system documentation
155
155
  - **Storybook 10**: component explorer
156
- - **Vitest + Playwright + axe**: every Storybook story runs as a render test in headless Chromium, with an accessibility audit on each
156
+ - **Vitest + Playwright + axe**: every Storybook story runs as a render test in headless Chromium, with an accessibility audit on each and interaction assertions where a story carries a play function
157
157
  - **CSS custom properties**: all theming via semantic tokens, no CSS-in-JS
158
158
 
159
159
  ---
160
160
 
161
161
  ## Quality & CI
162
162
 
163
- Every push and pull request runs a GitHub Actions pipeline ([`ci.yml`](.github/workflows/ci.yml)) with four jobs: lint + library build, story tests, Storybook build, and website lint + build followed by the two built-HTML checks (rendered spacing, chat-corpus coverage). The story tests render every Storybook story in headless Chromium via Vitest and run an axe accessibility audit on each, so a violation fails the build exactly like a render error. The same checklist runs locally with one command:
163
+ Every push and pull request runs a GitHub Actions pipeline ([`ci.yml`](.github/workflows/ci.yml)) with four jobs: lint + library build + the package-publish lint, story tests, Storybook build, and website lint + build followed by the built-HTML checks (rendered spacing, chat-corpus coverage, internal links) and the served-site checks: a hydration smoke that loads the built site in a real browser, and a page-level axe pass over the served pages in both themes. The story tests render every Storybook story in headless Chromium via Vitest and run an axe accessibility audit on each, so a violation fails the build exactly like a render error; the overlay stories also assert their keyboard and focus behaviour with play functions, enforced the same way. A scheduled workflow ([`uptime.yml`](.github/workflows/uptime.yml)) re-runs the hydration smoke against production every four hours, because incremental regeneration means the served site can change with no deploy. The same checklist runs locally with one command:
164
164
 
165
165
  ```bash
166
- npm run verify # lint + library type-check + package build + story tests + Storybook build + website lint + build + built-HTML checks
166
+ npm run verify # lint + library type-check + package build + publish lint + story tests + Storybook build + website lint + build + the built-HTML and served-site checks
167
167
  ```
168
168
 
169
169
  CI also guards against documentation drift: every generated surface (the `validate-registry` entry in the root `package.json` is the authoritative list, from this README's component count to the per-component markdown and the consumer agent skill) is rebuilt from its source registries on every build, and CI fails if the committed copies are stale. The numbers on the site are never hand-written.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared focusability queries for the overlay behavior layer.
3
+ *
4
+ * One home for the selector the modal components previously each carried a
5
+ * copy of. Plain DOM utilities — no React, safe to import anywhere.
6
+ */
7
+ /** Everything a focus trap must cycle through. */
8
+ export declare const FOCUSABLE = "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
9
+ /**
10
+ * The subset worth landing initial focus on: real controls, not arbitrary
11
+ * tabindex carriers (a scrollable body region is tabbable but focusing it on
12
+ * open would strand a keyboard user on a non-control).
13
+ */
14
+ export declare const INITIAL_FOCUS = "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])";
15
+ /**
16
+ * The focusable elements inside a container, in DOM order, excluding
17
+ * invisible ones — a control hidden by `display: none` must not count as the
18
+ * trap's first or last stop.
19
+ */
20
+ export declare function getFocusable(container: HTMLElement): HTMLElement[];
21
+ /**
22
+ * Where initial focus should land when a scope activates: the first visible
23
+ * real control, then the first visible focusable of any kind, else null (the
24
+ * caller falls back to the container itself).
25
+ */
26
+ export declare function getInitialFocus(container: HTMLElement): HTMLElement | null;
@@ -0,0 +1,18 @@
1
+ const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
2
+ const INITIAL_FOCUS = "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])";
3
+ const isVisible = (el) => typeof el.checkVisibility === "function" ? el.checkVisibility() : el.offsetParent !== null;
4
+ function getFocusable(container) {
5
+ return Array.from(container.querySelectorAll(FOCUSABLE)).filter(isVisible);
6
+ }
7
+ function getInitialFocus(container) {
8
+ const preferred = Array.from(container.querySelectorAll(INITIAL_FOCUS)).find(
9
+ isVisible
10
+ );
11
+ return preferred ?? getFocusable(container)[0] ?? null;
12
+ }
13
+ export {
14
+ FOCUSABLE,
15
+ INITIAL_FOCUS,
16
+ getFocusable,
17
+ getInitialFocus
18
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Focus containment for modal overlays: initial focus, Tab wrapping, focus
3
+ * restore, and background isolation.
4
+ *
5
+ * Restore is guarded by `document.contains` — the element that opened a
6
+ * dialog can be gone by the time it closes (a row the dialog itself deleted),
7
+ * and restoring into a detached node silently drops focus to `<body>`. It
8
+ * also runs when the component unmounts while open, which the per-component
9
+ * implementations this replaces never handled.
10
+ *
11
+ * Background isolation uses `inert`, not `aria-hidden`: a JS focus trap only
12
+ * fences the Tab key, while a screen reader's virtual cursor walks the page
13
+ * behind the dialog freely. `inert` closes all three doors at once —
14
+ * assistive-tech traversal, focusability, and pointer events. Scopes stack:
15
+ * only the topmost active scope's overlay is interactive, and closing it
16
+ * hands the page back to the scope below, not to the whole document. The
17
+ * inert set is recomputed only when a scope activates or deactivates, so a
18
+ * portal that mounts *while* a modal is open (a toast, say) is not inerted
19
+ * until the stack next changes — under `aria-modal` that content is outside
20
+ * the dialog's world anyway.
21
+ */
22
+ type FocusScopeOptions = {
23
+ /** Whether the scope is active — the overlay's open state. */
24
+ active: boolean;
25
+ /** Wrap Tab / Shift+Tab within the scope. Default true. */
26
+ trap?: boolean;
27
+ /** Restore focus to the previously focused element on deactivate. Default true. */
28
+ restoreFocus?: boolean;
29
+ /**
30
+ * Where initial focus lands. Defaults to the first visible real control,
31
+ * then the first focusable of any kind, then the container itself.
32
+ */
33
+ initialFocus?: () => HTMLElement | null;
34
+ /** Make everything outside the scope inert while active. Default true. */
35
+ inertOutside?: boolean;
36
+ };
37
+ export declare function useFocusScope(ref: React.RefObject<HTMLElement | null>, options: FocusScopeOptions): void;
38
+ export {};
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { useRef, useEffect } from "react";
3
+ import { getInitialFocus, getFocusable } from "./focusable.js";
4
+ const inertScopes = [];
5
+ const inerted = /* @__PURE__ */ new Set();
6
+ const applyInert = () => {
7
+ const top = inertScopes[inertScopes.length - 1];
8
+ for (const child of Array.from(document.body.children)) {
9
+ if (!(child instanceof HTMLElement)) continue;
10
+ if (child.tagName === "SCRIPT" || child.tagName === "STYLE") continue;
11
+ const shouldBeInert = top !== void 0 && !child.contains(top.panel);
12
+ if (shouldBeInert && !child.inert) {
13
+ child.inert = true;
14
+ inerted.add(child);
15
+ } else if (!shouldBeInert && inerted.has(child)) {
16
+ child.inert = false;
17
+ inerted.delete(child);
18
+ }
19
+ }
20
+ if (top === void 0 && inerted.size > 0) {
21
+ for (const el of inerted) el.inert = false;
22
+ inerted.clear();
23
+ }
24
+ };
25
+ function useFocusScope(ref, options) {
26
+ const optionsRef = useRef(options);
27
+ useEffect(() => {
28
+ optionsRef.current = options;
29
+ });
30
+ const { active } = options;
31
+ useEffect(() => {
32
+ if (!active) return;
33
+ const panel = ref.current;
34
+ if (!panel) return;
35
+ const previousFocus = document.activeElement;
36
+ let focusRaf = 0;
37
+ let focusAttempts = 0;
38
+ const tryInitialFocus = () => {
39
+ const initial = optionsRef.current.initialFocus?.() ?? getInitialFocus(panel) ?? panel;
40
+ initial.focus();
41
+ if (document.activeElement === initial) return;
42
+ focusAttempts += 1;
43
+ if (focusAttempts < 5) focusRaf = requestAnimationFrame(tryInitialFocus);
44
+ };
45
+ tryInitialFocus();
46
+ const scope = { panel };
47
+ if (optionsRef.current.inertOutside !== false) {
48
+ inertScopes.push(scope);
49
+ applyInert();
50
+ }
51
+ const handleKeyDown = (event) => {
52
+ if (event.key !== "Tab" || optionsRef.current.trap === false) return;
53
+ const focusable = getFocusable(panel);
54
+ if (focusable.length === 0) return;
55
+ const first = focusable[0];
56
+ const last = focusable[focusable.length - 1];
57
+ if (event.shiftKey && document.activeElement === first) {
58
+ event.preventDefault();
59
+ last.focus();
60
+ } else if (!event.shiftKey && document.activeElement === last) {
61
+ event.preventDefault();
62
+ first.focus();
63
+ }
64
+ };
65
+ document.addEventListener("keydown", handleKeyDown);
66
+ return () => {
67
+ cancelAnimationFrame(focusRaf);
68
+ document.removeEventListener("keydown", handleKeyDown);
69
+ const index = inertScopes.indexOf(scope);
70
+ if (index !== -1) {
71
+ inertScopes.splice(index, 1);
72
+ applyInert();
73
+ }
74
+ if (optionsRef.current.restoreFocus !== false && previousFocus && document.contains(previousFocus)) {
75
+ previousFocus.focus();
76
+ }
77
+ };
78
+ }, [active, ref]);
79
+ }
80
+ export {
81
+ useFocusScope
82
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The dismissal layer stack — the single owner of the document-level Escape
3
+ * and outside-press listeners for every overlay in the library.
4
+ *
5
+ * Before this existed, each open overlay registered its own document keydown,
6
+ * so Escape over a stacked Dialog-in-Drawer closed both at once. The stack
7
+ * routes each interaction to the **topmost** open layer only: Escape
8
+ * dismisses it (or is swallowed when it is not Escape-dismissible — a lower
9
+ * layer must never close through a blocking one), and an outside press
10
+ * dismisses it only when the press lands outside every element the layer
11
+ * claims. Layers below are untouched; one press, one layer.
12
+ *
13
+ * Exactly two document listeners exist while any layer is open, and none
14
+ * while the stack is empty. Registration order is stack order — overlays
15
+ * portal into `document.body` in the order they open, so stack order and
16
+ * paint order agree without reading z-index.
17
+ */
18
+ type LayerOptions = {
19
+ /** The overlay's open state — the layer registers while true. */
20
+ open: boolean;
21
+ /** Called when the stack decides this layer should dismiss. */
22
+ onDismiss?: () => void;
23
+ /** Whether Escape dismisses this layer. Default true. */
24
+ dismissOnEscape?: boolean;
25
+ /**
26
+ * Whether a pointerdown outside `getElements()` dismisses this layer.
27
+ * Default false — modal components keep their own backdrop click handler,
28
+ * which is part of their DOM and needs no document listener.
29
+ */
30
+ dismissOnOutsidePress?: boolean;
31
+ /**
32
+ * The elements that count as "inside" for outside-press checks — typically
33
+ * the panel and its trigger. Only read when `dismissOnOutsidePress` is on.
34
+ */
35
+ getElements?: () => (HTMLElement | null | undefined)[];
36
+ };
37
+ /**
38
+ * Register an overlay on the dismissal stack while `open` is true. Options
39
+ * are read at event time, so a `dismissOnEscape` toggle takes effect without
40
+ * re-registering (which would wrongly move the layer to the top).
41
+ */
42
+ export declare function useLayer(options: LayerOptions): void;
43
+ export {};
@@ -0,0 +1,55 @@
1
+ "use client";
2
+ import { useRef, useEffect } from "react";
3
+ const layers = [];
4
+ let listenersInstalled = false;
5
+ const handleKeyDown = (event) => {
6
+ if (event.key !== "Escape") return;
7
+ const top = layers[layers.length - 1];
8
+ if (!top) return;
9
+ const options = top.optionsRef.current;
10
+ if (options.dismissOnEscape === false) return;
11
+ event.preventDefault();
12
+ options.onDismiss?.();
13
+ };
14
+ const handlePointerDown = (event) => {
15
+ const top = layers[layers.length - 1];
16
+ if (!top) return;
17
+ const options = top.optionsRef.current;
18
+ if (!options.dismissOnOutsidePress) return;
19
+ const target = event.target instanceof Node ? event.target : null;
20
+ if (!target) return;
21
+ const inside = (options.getElements?.() ?? []).some((el) => el?.contains(target));
22
+ if (!inside) options.onDismiss?.();
23
+ };
24
+ const syncListeners = () => {
25
+ if (layers.length > 0 && !listenersInstalled) {
26
+ document.addEventListener("keydown", handleKeyDown);
27
+ document.addEventListener("pointerdown", handlePointerDown);
28
+ listenersInstalled = true;
29
+ } else if (layers.length === 0 && listenersInstalled) {
30
+ document.removeEventListener("keydown", handleKeyDown);
31
+ document.removeEventListener("pointerdown", handlePointerDown);
32
+ listenersInstalled = false;
33
+ }
34
+ };
35
+ function useLayer(options) {
36
+ const optionsRef = useRef(options);
37
+ useEffect(() => {
38
+ optionsRef.current = options;
39
+ });
40
+ const { open } = options;
41
+ useEffect(() => {
42
+ if (!open) return;
43
+ const layer = { optionsRef };
44
+ layers.push(layer);
45
+ syncListeners();
46
+ return () => {
47
+ const index = layers.indexOf(layer);
48
+ if (index !== -1) layers.splice(index, 1);
49
+ syncListeners();
50
+ };
51
+ }, [open]);
52
+ }
53
+ export {
54
+ useLayer
55
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * False during SSR and the hydration render, true afterwards — the guard a
3
+ * portal component needs before touching `document.body`. Uses
4
+ * `useSyncExternalStore` rather than a mount effect so the client's first
5
+ * render matches the server HTML without an extra render pass.
6
+ */
7
+ export declare function useMounted(): boolean;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { useSyncExternalStore } from "react";
3
+ const emptySubscribe = () => () => {
4
+ };
5
+ function useMounted() {
6
+ return useSyncExternalStore(
7
+ emptySubscribe,
8
+ () => true,
9
+ () => false
10
+ );
11
+ }
12
+ export {
13
+ useMounted
14
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Page scroll locking for modal overlays, with two strategies and one owner.
3
+ *
4
+ * `pin` (the default) fixes the body in place rather than hiding its
5
+ * overflow. iOS Safari ignores `overflow: hidden` on the body: a touch still
6
+ * scrolls the document, and focusing a field scrolls it programmatically to
7
+ * bring the field above the keyboard — which slides the page out from under
8
+ * the overlay. A fixed body has no scrollable overflow at all, so neither can
9
+ * move it. The body is pinned at its current scroll offset so the page behind
10
+ * the overlay does not visibly jump, and the offset is restored when the last
11
+ * holder releases. `overflow: hidden` rides along so the pinned body cannot
12
+ * grow scrollbars of its own.
13
+ *
14
+ * The pin is **counted**, and the counter is the whole point: closing a
15
+ * Dialog opened over a Drawer must not unlock the Drawer's scroll. The
16
+ * imperative pair (`acquireScrollLock` / `releaseScrollLock`) exposes the
17
+ * same counter to host chrome — a site's nav drawer, a bespoke chat panel —
18
+ * because two locks that cannot see each other each believe they own
19
+ * `document.body`, and whichever releases first unlocks the page under the
20
+ * overlay the other still holds (or restores styles the other already
21
+ * changed). One counter means the first acquire captures and pins, only the
22
+ * last release restores, whichever module it belongs to. The captured inline
23
+ * styles are restored verbatim, never blanked, so a host's own body styling
24
+ * survives a lock cycle.
25
+ *
26
+ * `cancel-events` keeps the document scrollbar rendered and cancels scroll
27
+ * *input* instead — wheel and touch gestures work only inside `allowWithin`.
28
+ * This is CommandPalette's documented design decision (unscrollable-body
29
+ * treatments blink the scrollbar rail out, which reads as a layout jump on
30
+ * classic-scrollbar platforms); it is deliberately not the default because
31
+ * swapping the modals onto it would change their scrollbar rendering.
32
+ * Instances are independent — overlapping cancel-events locks stack
33
+ * harmlessly.
34
+ */
35
+ type ScrollLockOptions = {
36
+ /** Which locking strategy to use. Default `'pin'`. */
37
+ strategy?: 'pin' | 'cancel-events';
38
+ /** For `cancel-events`: the scrollable region that keeps receiving input. */
39
+ allowWithin?: React.RefObject<HTMLElement | null>;
40
+ };
41
+ /**
42
+ * Take one hold on the shared page scroll lock — the counter behind the
43
+ * hook's `pin` strategy. For overlays outside this library (host chrome)
44
+ * that must not fight the modal components over `document.body`. Every
45
+ * acquire needs exactly one matching release.
46
+ */
47
+ export declare function acquireScrollLock(): void;
48
+ /**
49
+ * Release one hold on the shared page scroll lock. The body styles and the
50
+ * scroll offset are restored only when the last hold anywhere releases; an
51
+ * unbalanced release never touches the page.
52
+ */
53
+ export declare function releaseScrollLock(): void;
54
+ export declare function useScrollLock(active: boolean, options?: ScrollLockOptions): void;
55
+ export {};
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import { useRef, useEffect } from "react";
3
+ const PINNED_PROPS = ["position", "top", "left", "right", "overflow"];
4
+ let pinCount = 0;
5
+ let pinnedScrollY = 0;
6
+ let previousBodyStyle = {};
7
+ function acquireScrollLock() {
8
+ if (pinCount === 0) {
9
+ const { style } = document.body;
10
+ pinnedScrollY = window.scrollY;
11
+ previousBodyStyle = {};
12
+ for (const prop of PINNED_PROPS) previousBodyStyle[prop] = style[prop];
13
+ style.position = "fixed";
14
+ style.top = `-${pinnedScrollY}px`;
15
+ style.left = "0";
16
+ style.right = "0";
17
+ style.overflow = "hidden";
18
+ }
19
+ pinCount += 1;
20
+ }
21
+ function releaseScrollLock() {
22
+ if (pinCount === 0) return;
23
+ pinCount -= 1;
24
+ if (pinCount > 0) return;
25
+ const { style } = document.body;
26
+ for (const prop of PINNED_PROPS) style[prop] = previousBodyStyle[prop] ?? "";
27
+ window.scrollTo({ top: pinnedScrollY, behavior: "instant" });
28
+ }
29
+ function useScrollLock(active, options) {
30
+ const optionsRef = useRef(options);
31
+ useEffect(() => {
32
+ optionsRef.current = options;
33
+ });
34
+ const strategy = options?.strategy ?? "pin";
35
+ useEffect(() => {
36
+ if (!active) return;
37
+ if (strategy === "pin") {
38
+ acquireScrollLock();
39
+ return releaseScrollLock;
40
+ }
41
+ const block = (event) => {
42
+ const allow = optionsRef.current?.allowWithin?.current;
43
+ const target = event.target instanceof Node ? event.target : null;
44
+ if (allow && target && allow.contains(target)) return;
45
+ event.preventDefault();
46
+ };
47
+ document.addEventListener("wheel", block, { passive: false });
48
+ document.addEventListener("touchmove", block, { passive: false });
49
+ return () => {
50
+ document.removeEventListener("wheel", block);
51
+ document.removeEventListener("touchmove", block);
52
+ };
53
+ }, [active, strategy]);
54
+ }
55
+ export {
56
+ acquireScrollLock,
57
+ releaseScrollLock,
58
+ useScrollLock
59
+ };
@@ -151,6 +151,11 @@
151
151
  align-items: center;
152
152
  justify-content: center;
153
153
  flex: none;
154
+ /* Fixed box: the active step's spinner (16px) is smaller than the step
155
+ icons (20px), so without this the column narrows and the spinner drifts
156
+ off the connector's centre line. */
157
+ width: var(--icon-size-sm);
158
+ height: var(--icon-size-sm);
154
159
  color: var(--color-icon-secondary);
155
160
  }
156
161
 
@@ -188,8 +193,12 @@
188
193
  STEP STATES
189
194
  ============================================ */
190
195
 
196
+ .ds-agent-plan__step--pending .ds-agent-plan__indicator {
197
+ color: var(--color-status-neutral-icon);
198
+ }
199
+
191
200
  .ds-agent-plan__step--completed .ds-agent-plan__indicator {
192
- color: var(--color-status-positive-text);
201
+ color: var(--color-status-positive-icon);
193
202
  }
194
203
 
195
204
  .ds-agent-plan__step--completed .ds-agent-plan__label {
@@ -197,7 +206,7 @@
197
206
  }
198
207
 
199
208
  .ds-agent-plan__step--active .ds-agent-plan__indicator {
200
- color: var(--color-status-info-text);
209
+ color: var(--color-status-info-icon);
201
210
  }
202
211
 
203
212
  .ds-agent-plan__step--active .ds-agent-plan__label {
@@ -207,7 +216,7 @@
207
216
  }
208
217
 
209
218
  .ds-agent-plan__step--failed .ds-agent-plan__indicator {
210
- color: var(--color-status-error-text);
219
+ color: var(--color-status-error-icon);
211
220
  }
212
221
 
213
222
  .ds-agent-plan__step--failed .ds-agent-plan__label {