@mediaradar/design-components 0.18.1 → 0.18.3

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.
package/dist/index.d.cts CHANGED
@@ -14,12 +14,17 @@ import { VariantProps } from 'class-variance-authority';
14
14
  * @dependencies lucide-react
15
15
  * @behavior 'click' mode toggles on click only. 'hover' mode also opens on
16
16
  * keyboard focus and closes on blur, so it stays reachable without a mouse —
17
- * not just a decorative hover effect. Both modes close on Escape and on any
17
+ * not just a decorative hover effect. It closes 120ms after the pointer
18
+ * leaves, and stays open while the pointer is on the bubble, so the text can
19
+ * be hovered (WCAG 1.4.13). Both modes close on Escape and on any
18
20
  * outside click. The bubble is portaled to document.body and repositions
19
21
  * itself to stay clear of the right viewport edge.
20
- * @a11y The trigger is a <button> reporting aria-expanded; the bubble is
21
- * role="tooltip" and Escape dismisses it. It describes its anchor rather
22
- * than naming it, so the anchor still needs its own accessible name.
22
+ * @a11y The trigger is a <button> reporting aria-expanded, and its
23
+ * aria-describedby points at a hidden copy of the text beside it, so a screen
24
+ * reader reads the text on focus, open or not. (The visible bubble is
25
+ * portaled to the end of the page and appears only after a click, so it is
26
+ * aria-hidden rather than described-by.) Escape dismisses it. `label` names
27
+ * the button; the text describes it.
23
28
  * @usage
24
29
  * ```tsx
25
30
  * <InfoTip text="Trims time periods to data that has finalized across the selected media formats." />
@@ -291,7 +296,7 @@ declare function Delta({ value, suffix, arrow, period, className }: DeltaProps):
291
296
  * be dropped into a row that already has its own click-to-toggle handler
292
297
  * (a common list/table pattern) without double-firing. Where the row itself
293
298
  * is the control, use `CheckBoxBox` instead.
294
- * @tokens bg-brand-primaryAction, border-border-interactive, bg-input,
299
+ * @tokens bg-brand-primaryAction, text-brand-onprimary, border-border-interactive, bg-input,
295
300
  * hover:border-brand-primaryAction, focus-visible:outline-border-focus
296
301
  * @dependencies lucide-react
297
302
  * @behavior `aria-checked` is `"mixed"` when indeterminate, otherwise the
@@ -364,7 +369,7 @@ declare function CheckBox({ checked, indeterminate, onChange, ariaLabel, disable
364
369
  * the same control is worse for keyboard users, not better. The owning
365
370
  * element supplies `role="checkbox"` and `aria-checked`; this is
366
371
  * `aria-hidden` so the state isn't announced twice.
367
- * @tokens bg-brand-primaryAction, border-border-interactive, bg-input
372
+ * @tokens bg-brand-primaryAction, text-brand-onprimary, border-border-interactive, bg-input
368
373
  * @dependencies lucide-react
369
374
  * @behavior Renders through the same `boxClass` as `CheckBox`, so geometry
370
375
  * and fill can't drift. Takes neither the focus ring nor the hover border,
@@ -404,7 +409,7 @@ declare function CheckBoxBox({ checked, indeterminate, disabled, className }: Ch
404
409
  * dual-month calendar with range highlighting on the right, Start/End fields,
405
410
  * and Cancel/Apply. Shared by Creative Intel and the Account Review.
406
411
  * @tokens rounded-control, rounded-card, border-border-default, border-border-strong,
407
- * bg-surface, bg-input, bg-subtle, bg-ai-tint, bg-brand-primaryAction,
412
+ * bg-surface, bg-input, bg-subtle, bg-ai-tint, bg-brand-primaryAction, text-brand-onprimary,
408
413
  * shadow-overlay, text-text-primary, text-text-secondary, text-text-tertiary,
409
414
  * hover:bg-subtle, hover:text-text-primary, focus-visible:outline-border-focus
410
415
  * @dependencies lucide-react
@@ -761,7 +766,7 @@ interface NoResultsAction {
761
766
  /** Rendered as the filled button; every other action renders as the quiet link/button. */
762
767
  primary?: boolean;
763
768
  }
764
- declare const PRIMARY_ACTION_CLASS = "rounded-control bg-brand-primaryAction px-3.5 py-2 text-[13px] font-semibold text-white transition-colors hover:opacity-90";
769
+ declare const PRIMARY_ACTION_CLASS = "rounded-control bg-brand-primaryAction px-3.5 py-2 text-[13px] font-semibold text-brand-onprimary transition-colors hover:opacity-90";
765
770
  declare const QUIET_ACTION_CLASS = "rounded-control text-[13px] font-medium text-text-link transition-colors hover:underline focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus";
766
771
  /**
767
772
  * @pattern NoResultsEmptyState
@@ -775,7 +780,7 @@ declare const QUIET_ACTION_CLASS = "rounded-control text-[13px] font-medium text
775
780
  * in a colSpan table cell (or a card, or a chart body) that already owns
776
781
  * that chrome, without imposing its own.
777
782
  * @tokens bg-subtle, text-text-tertiary, text-base, text-text-primary,
778
- * text-text-secondary, rounded-control, bg-brand-primaryAction, text-text-link,
783
+ * text-text-secondary, rounded-control, bg-brand-primaryAction, text-brand-onprimary, text-text-link,
779
784
  * focus-visible:outline-border-focus
780
785
  * @dependencies lucide-react
781
786
  * @behavior The Clear button shows when `onClearFilters` is given AND
@@ -1082,8 +1087,11 @@ declare function StackedColumnPlaceholder({ height, width, legendRows, axis, }:
1082
1087
  * right-aligned to the anchor's right edge -- both meant for `anchorRef`
1083
1088
  * anchors near the viewport's right edge, where a centred box would run
1084
1089
  * off-screen or read awkwardly beside a narrow icon.
1085
- * @a11y Renders role="tooltip" and is wired to its anchor by aria-describedby,
1086
- * so the anchor must exist and keep a stable id. It describes rather than
1090
+ * @a11y Renders role="tooltip" and is wired by aria-describedby to the
1091
+ * element that has focus: the `anchorRef` element, or inline, the child that
1092
+ * took focus (not the wrapper span: a screen reader reads the description of
1093
+ * the focused element only). An aria-describedby the caller set is kept.
1094
+ * Escape hides it without moving focus or the pointer. It describes rather than
1087
1095
  * labels: it is announced in addition to the anchor's own name, not
1088
1096
  * instead of it, so never put the only label for a control in here.
1089
1097
  * @usage
@@ -1522,7 +1530,7 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantPr
1522
1530
  * styling to a different element, e.g. a router `<Link>`. Also exports the
1523
1531
  * raw `buttonVariants` cva function for consumers composing button styling
1524
1532
  * onto something other than this component.
1525
- * @tokens bg-brand-primaryAction, text-brand-onprimary, bg-surface, text-text-primary, border-border-strong, hover:border-border-interactive, hover:bg-subtle, bg-status-danger, text-status-onstatus, text-text-secondary, hover:text-text-danger, text-text-link, rounded-control, duration-(--mr-motion-duration-interactive), ease-interactive
1533
+ * @tokens bg-brand-primaryAction, text-brand-onprimary, bg-surface, text-text-primary, border-border-strong, hover:border-border-interactive, hover:bg-subtle, bg-status-danger, text-status-onstatus, text-text-secondary, hover:text-text-danger, text-text-link, rounded-control, duration-(--mr-motion-duration-interactive), ease-interactive, focus-visible:outline-border-focus
1526
1534
  * @dependencies class-variance-authority, @radix-ui/react-slot, lucide-react
1527
1535
  * @variant primary - The one primary action on a screen. Solid brand fill.
1528
1536
  * @variant secondary - Any other real action competing for attention (Cancel, Export). Bordered, neutral fill.
@@ -1531,10 +1539,11 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantPr
1531
1539
  * @variant dangerGhost - Neutral at rest, red on hover. For a destructive affordance inline in a row or toolbar -- what the product reaches for far more often than filled danger.
1532
1540
  * @variant link - Reads as inline text, not a control. Use inside body copy or where a full button would overpower the surrounding content.
1533
1541
  * @behavior `disabled` removes pointer events entirely (not just a style
1534
- * change) and dims to 40% opacity. Focus handling is a deliberate exception:
1535
- * most interactive components use a visible `focus-visible:outline-2` ring,
1536
- * but Button suppresses it (`focus-visible:outline-hidden focus-visible:ring-0`)
1537
- * and relies on its per-variant hover/brightness treatment instead. `loading`
1542
+ * change) and dims to 40% opacity. Keyboard focus shows the same 2px
1543
+ * `border-focus` outline as every other control here. It used to be
1544
+ * suppressed in favour of the hover treatment, which a keyboard user never
1545
+ * sees, and the suppression also cancelled the apps' own global focus ring,
1546
+ * so a focused Button had no indicator at all (WCAG 2.4.7). `loading`
1538
1547
  * implies `disabled` (and sets `aria-busy`), since a control mid-submit
1539
1548
  * shouldn't accept another click. `asChild` renders only the child element,
1540
1549
  * through Radix `Slot`, with the button classes, ref and remaining props
@@ -1643,10 +1652,10 @@ interface NameListModalProps {
1643
1652
  * When `options` are given they render as CheckBox rows below the field, and
1644
1653
  * their state comes back as `onSubmit`'s second argument -- a handler that
1645
1654
  * only takes the name stays valid.
1646
- * @a11y role="dialog" with aria-modal, labelled by its title. The name field
1647
- * takes focus on open and Escape closes via onCancel. Focus is NOT trapped
1648
- * and NOT restored to the opener on close -- confirmed in the source -- so
1649
- * a consumer that needs either must supply it around this component.
1655
+ * @a11y role="dialog" with aria-modal, labelled by its title (aria-labelledby
1656
+ * on the heading). The name field takes focus on open and Escape closes via
1657
+ * onCancel. Tab and Shift+Tab stay inside the dialog, and on close focus
1658
+ * returns to the element that had it when the dialog opened.
1650
1659
  * @usage
1651
1660
  * ```tsx
1652
1661
  * <NameListModal title="Rename list" initialName={list.name} submitLabel="Save" onCancel={close} onSubmit={rename} />
@@ -1794,7 +1803,7 @@ interface SortButtonProps<K extends string> {
1794
1803
  * neutral ArrowUpDown in text-tertiary at half opacity, coming up to full on
1795
1804
  * hover so the column reads as clickable; the active column swaps to a
1796
1805
  * direction arrow (ArrowUp/ArrowDown) colored with the brand action blue.
1797
- * @tokens text-text-tertiary, text-brand-primaryAction, hover:text-text-primary
1806
+ * @tokens text-text-tertiary, text-brand-primaryAction, hover:text-text-primary, focus-visible:outline-border-focus
1798
1807
  * @dependencies lucide-react
1799
1808
  * @behavior Pair with `cycleSort`/`sortProp` (exported alongside) for the
1800
1809
  * standard three-state cycle — sort, reverse, then unsorted. `disabled` keeps
package/dist/index.d.ts CHANGED
@@ -14,12 +14,17 @@ import { VariantProps } from 'class-variance-authority';
14
14
  * @dependencies lucide-react
15
15
  * @behavior 'click' mode toggles on click only. 'hover' mode also opens on
16
16
  * keyboard focus and closes on blur, so it stays reachable without a mouse —
17
- * not just a decorative hover effect. Both modes close on Escape and on any
17
+ * not just a decorative hover effect. It closes 120ms after the pointer
18
+ * leaves, and stays open while the pointer is on the bubble, so the text can
19
+ * be hovered (WCAG 1.4.13). Both modes close on Escape and on any
18
20
  * outside click. The bubble is portaled to document.body and repositions
19
21
  * itself to stay clear of the right viewport edge.
20
- * @a11y The trigger is a <button> reporting aria-expanded; the bubble is
21
- * role="tooltip" and Escape dismisses it. It describes its anchor rather
22
- * than naming it, so the anchor still needs its own accessible name.
22
+ * @a11y The trigger is a <button> reporting aria-expanded, and its
23
+ * aria-describedby points at a hidden copy of the text beside it, so a screen
24
+ * reader reads the text on focus, open or not. (The visible bubble is
25
+ * portaled to the end of the page and appears only after a click, so it is
26
+ * aria-hidden rather than described-by.) Escape dismisses it. `label` names
27
+ * the button; the text describes it.
23
28
  * @usage
24
29
  * ```tsx
25
30
  * <InfoTip text="Trims time periods to data that has finalized across the selected media formats." />
@@ -291,7 +296,7 @@ declare function Delta({ value, suffix, arrow, period, className }: DeltaProps):
291
296
  * be dropped into a row that already has its own click-to-toggle handler
292
297
  * (a common list/table pattern) without double-firing. Where the row itself
293
298
  * is the control, use `CheckBoxBox` instead.
294
- * @tokens bg-brand-primaryAction, border-border-interactive, bg-input,
299
+ * @tokens bg-brand-primaryAction, text-brand-onprimary, border-border-interactive, bg-input,
295
300
  * hover:border-brand-primaryAction, focus-visible:outline-border-focus
296
301
  * @dependencies lucide-react
297
302
  * @behavior `aria-checked` is `"mixed"` when indeterminate, otherwise the
@@ -364,7 +369,7 @@ declare function CheckBox({ checked, indeterminate, onChange, ariaLabel, disable
364
369
  * the same control is worse for keyboard users, not better. The owning
365
370
  * element supplies `role="checkbox"` and `aria-checked`; this is
366
371
  * `aria-hidden` so the state isn't announced twice.
367
- * @tokens bg-brand-primaryAction, border-border-interactive, bg-input
372
+ * @tokens bg-brand-primaryAction, text-brand-onprimary, border-border-interactive, bg-input
368
373
  * @dependencies lucide-react
369
374
  * @behavior Renders through the same `boxClass` as `CheckBox`, so geometry
370
375
  * and fill can't drift. Takes neither the focus ring nor the hover border,
@@ -404,7 +409,7 @@ declare function CheckBoxBox({ checked, indeterminate, disabled, className }: Ch
404
409
  * dual-month calendar with range highlighting on the right, Start/End fields,
405
410
  * and Cancel/Apply. Shared by Creative Intel and the Account Review.
406
411
  * @tokens rounded-control, rounded-card, border-border-default, border-border-strong,
407
- * bg-surface, bg-input, bg-subtle, bg-ai-tint, bg-brand-primaryAction,
412
+ * bg-surface, bg-input, bg-subtle, bg-ai-tint, bg-brand-primaryAction, text-brand-onprimary,
408
413
  * shadow-overlay, text-text-primary, text-text-secondary, text-text-tertiary,
409
414
  * hover:bg-subtle, hover:text-text-primary, focus-visible:outline-border-focus
410
415
  * @dependencies lucide-react
@@ -761,7 +766,7 @@ interface NoResultsAction {
761
766
  /** Rendered as the filled button; every other action renders as the quiet link/button. */
762
767
  primary?: boolean;
763
768
  }
764
- declare const PRIMARY_ACTION_CLASS = "rounded-control bg-brand-primaryAction px-3.5 py-2 text-[13px] font-semibold text-white transition-colors hover:opacity-90";
769
+ declare const PRIMARY_ACTION_CLASS = "rounded-control bg-brand-primaryAction px-3.5 py-2 text-[13px] font-semibold text-brand-onprimary transition-colors hover:opacity-90";
765
770
  declare const QUIET_ACTION_CLASS = "rounded-control text-[13px] font-medium text-text-link transition-colors hover:underline focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus";
766
771
  /**
767
772
  * @pattern NoResultsEmptyState
@@ -775,7 +780,7 @@ declare const QUIET_ACTION_CLASS = "rounded-control text-[13px] font-medium text
775
780
  * in a colSpan table cell (or a card, or a chart body) that already owns
776
781
  * that chrome, without imposing its own.
777
782
  * @tokens bg-subtle, text-text-tertiary, text-base, text-text-primary,
778
- * text-text-secondary, rounded-control, bg-brand-primaryAction, text-text-link,
783
+ * text-text-secondary, rounded-control, bg-brand-primaryAction, text-brand-onprimary, text-text-link,
779
784
  * focus-visible:outline-border-focus
780
785
  * @dependencies lucide-react
781
786
  * @behavior The Clear button shows when `onClearFilters` is given AND
@@ -1082,8 +1087,11 @@ declare function StackedColumnPlaceholder({ height, width, legendRows, axis, }:
1082
1087
  * right-aligned to the anchor's right edge -- both meant for `anchorRef`
1083
1088
  * anchors near the viewport's right edge, where a centred box would run
1084
1089
  * off-screen or read awkwardly beside a narrow icon.
1085
- * @a11y Renders role="tooltip" and is wired to its anchor by aria-describedby,
1086
- * so the anchor must exist and keep a stable id. It describes rather than
1090
+ * @a11y Renders role="tooltip" and is wired by aria-describedby to the
1091
+ * element that has focus: the `anchorRef` element, or inline, the child that
1092
+ * took focus (not the wrapper span: a screen reader reads the description of
1093
+ * the focused element only). An aria-describedby the caller set is kept.
1094
+ * Escape hides it without moving focus or the pointer. It describes rather than
1087
1095
  * labels: it is announced in addition to the anchor's own name, not
1088
1096
  * instead of it, so never put the only label for a control in here.
1089
1097
  * @usage
@@ -1522,7 +1530,7 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantPr
1522
1530
  * styling to a different element, e.g. a router `<Link>`. Also exports the
1523
1531
  * raw `buttonVariants` cva function for consumers composing button styling
1524
1532
  * onto something other than this component.
1525
- * @tokens bg-brand-primaryAction, text-brand-onprimary, bg-surface, text-text-primary, border-border-strong, hover:border-border-interactive, hover:bg-subtle, bg-status-danger, text-status-onstatus, text-text-secondary, hover:text-text-danger, text-text-link, rounded-control, duration-(--mr-motion-duration-interactive), ease-interactive
1533
+ * @tokens bg-brand-primaryAction, text-brand-onprimary, bg-surface, text-text-primary, border-border-strong, hover:border-border-interactive, hover:bg-subtle, bg-status-danger, text-status-onstatus, text-text-secondary, hover:text-text-danger, text-text-link, rounded-control, duration-(--mr-motion-duration-interactive), ease-interactive, focus-visible:outline-border-focus
1526
1534
  * @dependencies class-variance-authority, @radix-ui/react-slot, lucide-react
1527
1535
  * @variant primary - The one primary action on a screen. Solid brand fill.
1528
1536
  * @variant secondary - Any other real action competing for attention (Cancel, Export). Bordered, neutral fill.
@@ -1531,10 +1539,11 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantPr
1531
1539
  * @variant dangerGhost - Neutral at rest, red on hover. For a destructive affordance inline in a row or toolbar -- what the product reaches for far more often than filled danger.
1532
1540
  * @variant link - Reads as inline text, not a control. Use inside body copy or where a full button would overpower the surrounding content.
1533
1541
  * @behavior `disabled` removes pointer events entirely (not just a style
1534
- * change) and dims to 40% opacity. Focus handling is a deliberate exception:
1535
- * most interactive components use a visible `focus-visible:outline-2` ring,
1536
- * but Button suppresses it (`focus-visible:outline-hidden focus-visible:ring-0`)
1537
- * and relies on its per-variant hover/brightness treatment instead. `loading`
1542
+ * change) and dims to 40% opacity. Keyboard focus shows the same 2px
1543
+ * `border-focus` outline as every other control here. It used to be
1544
+ * suppressed in favour of the hover treatment, which a keyboard user never
1545
+ * sees, and the suppression also cancelled the apps' own global focus ring,
1546
+ * so a focused Button had no indicator at all (WCAG 2.4.7). `loading`
1538
1547
  * implies `disabled` (and sets `aria-busy`), since a control mid-submit
1539
1548
  * shouldn't accept another click. `asChild` renders only the child element,
1540
1549
  * through Radix `Slot`, with the button classes, ref and remaining props
@@ -1643,10 +1652,10 @@ interface NameListModalProps {
1643
1652
  * When `options` are given they render as CheckBox rows below the field, and
1644
1653
  * their state comes back as `onSubmit`'s second argument -- a handler that
1645
1654
  * only takes the name stays valid.
1646
- * @a11y role="dialog" with aria-modal, labelled by its title. The name field
1647
- * takes focus on open and Escape closes via onCancel. Focus is NOT trapped
1648
- * and NOT restored to the opener on close -- confirmed in the source -- so
1649
- * a consumer that needs either must supply it around this component.
1655
+ * @a11y role="dialog" with aria-modal, labelled by its title (aria-labelledby
1656
+ * on the heading). The name field takes focus on open and Escape closes via
1657
+ * onCancel. Tab and Shift+Tab stay inside the dialog, and on close focus
1658
+ * returns to the element that had it when the dialog opened.
1650
1659
  * @usage
1651
1660
  * ```tsx
1652
1661
  * <NameListModal title="Rename list" initialName={list.name} submitLabel="Save" onCancel={close} onSubmit={rename} />
@@ -1794,7 +1803,7 @@ interface SortButtonProps<K extends string> {
1794
1803
  * neutral ArrowUpDown in text-tertiary at half opacity, coming up to full on
1795
1804
  * hover so the column reads as clickable; the active column swaps to a
1796
1805
  * direction arrow (ArrowUp/ArrowDown) colored with the brand action blue.
1797
- * @tokens text-text-tertiary, text-brand-primaryAction, hover:text-text-primary
1806
+ * @tokens text-text-tertiary, text-brand-primaryAction, hover:text-text-primary, focus-visible:outline-border-focus
1798
1807
  * @dependencies lucide-react
1799
1808
  * @behavior Pair with `cycleSort`/`sortProp` (exported alongside) for the
1800
1809
  * standard three-state cycle — sort, reverse, then unsorted. `disabled` keeps
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
+ "use client";
2
+
1
3
  // src/InfoTip.tsx
2
- import { useEffect, useRef, useState } from "react";
4
+ import { useEffect, useId, useRef, useState } from "react";
3
5
  import { createPortal } from "react-dom";
4
6
  import { Info } from "lucide-react";
5
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -7,6 +9,17 @@ function InfoTip({ text, size = 11, label = "More information", trigger = "click
7
9
  const [open, setOpen] = useState(false);
8
10
  const [pos, setPos] = useState(null);
9
11
  const btnRef = useRef(null);
12
+ const closeTimer = useRef(null);
13
+ const textId = useId();
14
+ const cancelClose = () => {
15
+ if (closeTimer.current) clearTimeout(closeTimer.current);
16
+ closeTimer.current = null;
17
+ };
18
+ const closeSoon = () => {
19
+ cancelClose();
20
+ closeTimer.current = setTimeout(() => setOpen(false), 120);
21
+ };
22
+ useEffect(() => cancelClose, []);
10
23
  useEffect(() => {
11
24
  if (!open) return;
12
25
  const onDown = (e) => {
@@ -28,6 +41,7 @@ function InfoTip({ text, size = 11, label = "More information", trigger = "click
28
41
  setPos({ top: r.bottom + 6, left, width });
29
42
  };
30
43
  const show = () => {
44
+ cancelClose();
31
45
  place();
32
46
  setOpen(true);
33
47
  };
@@ -38,7 +52,7 @@ function InfoTip({ text, size = 11, label = "More information", trigger = "click
38
52
  };
39
53
  const hoverProps = trigger === "hover" ? {
40
54
  onMouseEnter: show,
41
- onMouseLeave: () => setOpen(false),
55
+ onMouseLeave: closeSoon,
42
56
  onFocus: show,
43
57
  onBlur: () => setOpen(false)
44
58
  } : {};
@@ -51,16 +65,20 @@ function InfoTip({ text, size = 11, label = "More information", trigger = "click
51
65
  onClick: toggle,
52
66
  "aria-label": label,
53
67
  "aria-expanded": open,
68
+ "aria-describedby": textId,
54
69
  ...hoverProps,
55
70
  className: "inline-flex cursor-pointer items-center text-text-tertiary transition-colors hover:text-text-primary focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus",
56
71
  children: /* @__PURE__ */ jsx(Info, { size, "aria-hidden": "true" })
57
72
  }
58
73
  ),
74
+ /* @__PURE__ */ jsx("span", { id: textId, hidden: true, children: text }),
59
75
  open && pos && createPortal(
60
76
  /* @__PURE__ */ jsx(
61
77
  "div",
62
78
  {
63
- role: "tooltip",
79
+ "aria-hidden": "true",
80
+ onMouseEnter: trigger === "hover" ? cancelClose : void 0,
81
+ onMouseLeave: trigger === "hover" ? closeSoon : void 0,
64
82
  style: { position: "fixed", top: pos.top, left: pos.left, width: pos.width },
65
83
  className: "z-[90] rounded-control border border-border-default bg-surface px-3 py-2 text-[12px] font-normal not-italic normal-case leading-relaxed tracking-normal text-text-secondary shadow-overlay",
66
84
  children: text
@@ -509,7 +527,7 @@ import { jsx as jsx6 } from "react/jsx-runtime";
509
527
  function boxClass(on, disabled) {
510
528
  return cn(
511
529
  "flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-[5px] border transition-colors",
512
- on ? "border-transparent bg-brand-primaryAction text-white" : "border-border-interactive bg-input",
530
+ on ? "border-transparent bg-brand-primaryAction text-brand-onprimary" : "border-border-interactive bg-input",
513
531
  disabled && "cursor-not-allowed opacity-50"
514
532
  );
515
533
  }
@@ -849,7 +867,7 @@ function DateRangeFilter({
849
867
  /* @__PURE__ */ jsx7(
850
868
  "span",
851
869
  {
852
- className: `relative z-10 flex h-8 w-8 items-center justify-center rounded-full text-[13px] tabular-nums ${endpoint ? "bg-brand-primaryAction font-semibold text-white" : inMonth ? "text-text-primary hover:bg-subtle" : "text-text-tertiary"}`,
870
+ className: `relative z-10 flex h-8 w-8 items-center justify-center rounded-full text-[13px] tabular-nums ${endpoint ? "bg-brand-primaryAction font-semibold text-brand-onprimary" : inMonth ? "text-text-primary hover:bg-subtle" : "text-text-tertiary"}`,
853
871
  children: d.getDate()
854
872
  }
855
873
  )
@@ -860,7 +878,7 @@ function DateRangeFilter({
860
878
  }) }),
861
879
  /* @__PURE__ */ jsxs6("div", { className: "mt-4 flex justify-end gap-2", children: [
862
880
  /* @__PURE__ */ jsx7("button", { type: "button", "data-testid": testIdPrefix && `${testIdPrefix}-cancel`, onClick: () => setOpen(false), className: "rounded-control border border-border-default bg-surface px-5 py-2 text-[13px] font-semibold text-text-primary transition-colors hover:bg-subtle", children: "Cancel" }),
863
- /* @__PURE__ */ jsx7("button", { type: "button", "data-testid": testIdPrefix && `${testIdPrefix}-apply`, onClick: apply, className: "rounded-control bg-brand-primaryAction px-5 py-2 text-[13px] font-semibold text-white transition-[filter] hover:brightness-110", children: "Apply" })
881
+ /* @__PURE__ */ jsx7("button", { type: "button", "data-testid": testIdPrefix && `${testIdPrefix}-apply`, onClick: apply, className: "rounded-control bg-brand-primaryAction px-5 py-2 text-[13px] font-semibold text-brand-onprimary transition-[filter] hover:brightness-110", children: "Apply" })
864
882
  ] })
865
883
  ] })
866
884
  ]
@@ -879,7 +897,7 @@ function NavBtn({ children, onClick, ...rest }) {
879
897
  }
880
898
 
881
899
  // src/SearchBar.tsx
882
- import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
900
+ import { useEffect as useEffect4, useId as useId2, useRef as useRef4, useState as useState4 } from "react";
883
901
  import { AlertCircle, Check as Check3, ChevronDown as ChevronDown3, Search as Search3 } from "lucide-react";
884
902
  import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
885
903
  var DEFAULT_SEARCH_MATCH = { mode: "anyWord", wordJoin: "Or" };
@@ -1140,6 +1158,7 @@ function RadioOption({
1140
1158
  }
1141
1159
  function RowInfoTip({ text }) {
1142
1160
  const [open, setOpen] = useState4(false);
1161
+ const id = useId2();
1143
1162
  const ref = useRef4(null);
1144
1163
  useEffect4(() => {
1145
1164
  if (!open) return;
@@ -1160,14 +1179,16 @@ function RowInfoTip({ text }) {
1160
1179
  },
1161
1180
  "aria-label": "More information",
1162
1181
  "aria-expanded": open,
1163
- className: "inline-flex text-text-tertiary transition-colors hover:text-text-primary",
1182
+ "aria-describedby": id,
1183
+ className: "inline-flex text-text-tertiary transition-colors hover:text-text-primary focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus",
1164
1184
  children: /* @__PURE__ */ jsx8(AlertCircle, { size: 13, "aria-hidden": "true" })
1165
1185
  }
1166
1186
  ),
1167
- open && /* @__PURE__ */ jsx8(
1187
+ /* @__PURE__ */ jsx8(
1168
1188
  "span",
1169
1189
  {
1170
- role: "tooltip",
1190
+ id,
1191
+ hidden: !open,
1171
1192
  className: "absolute left-0 top-[calc(100%+6px)] z-40 w-[220px] rounded-control border border-border-default bg-surface px-2.5 py-1.5 text-[11.5px] font-normal leading-snug text-text-secondary shadow-overlay",
1172
1193
  children: text
1173
1194
  }
@@ -1519,7 +1540,7 @@ var NO_RESULTS_BODY = "Your filters may be too narrow, conflict with each other,
1519
1540
  function noResultsBody(workspaceName) {
1520
1541
  return workspaceName ? `Nothing in the \u201C${workspaceName}\u201D workspace matches. ${NO_RESULTS_BODY}` : NO_RESULTS_BODY;
1521
1542
  }
1522
- var PRIMARY_ACTION_CLASS = "rounded-control bg-brand-primaryAction px-3.5 py-2 text-[13px] font-semibold text-white transition-colors hover:opacity-90";
1543
+ var PRIMARY_ACTION_CLASS = "rounded-control bg-brand-primaryAction px-3.5 py-2 text-[13px] font-semibold text-brand-onprimary transition-colors hover:opacity-90";
1523
1544
  var QUIET_ACTION_CLASS = "rounded-control text-[13px] font-medium text-text-link transition-colors hover:underline focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus";
1524
1545
  function NoResultsEmptyState({
1525
1546
  title = NO_RESULTS_TITLE,
@@ -1854,12 +1875,21 @@ function StackedColumnPlaceholder({
1854
1875
  }
1855
1876
 
1856
1877
  // src/Tooltip.tsx
1857
- import { useEffect as useEffect8, useId, useLayoutEffect as useLayoutEffect2, useRef as useRef9, useState as useState8 } from "react";
1878
+ import { useEffect as useEffect8, useId as useId3, useLayoutEffect as useLayoutEffect2, useRef as useRef9, useState as useState8 } from "react";
1858
1879
  import { createPortal as createPortal3 } from "react-dom";
1859
1880
  import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
1860
1881
  function cx6(...parts) {
1861
1882
  return parts.filter(Boolean).join(" ");
1862
1883
  }
1884
+ function addDescribedBy(el, id) {
1885
+ const ids = (el.getAttribute("aria-describedby") ?? "").split(" ").filter(Boolean);
1886
+ if (!ids.includes(id)) el.setAttribute("aria-describedby", [...ids, id].join(" "));
1887
+ }
1888
+ function removeDescribedBy(el, id) {
1889
+ const ids = (el.getAttribute("aria-describedby") ?? "").split(" ").filter((x) => x && x !== id);
1890
+ if (ids.length) el.setAttribute("aria-describedby", ids.join(" "));
1891
+ else el.removeAttribute("aria-describedby");
1892
+ }
1863
1893
  function exitDurationMs(el) {
1864
1894
  if (typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return 0;
1865
1895
  const cs = window.getComputedStyle(el);
@@ -1896,7 +1926,8 @@ function Tooltip({
1896
1926
  const [shown, setShown] = useState8(false);
1897
1927
  const openRef = useRef9(false);
1898
1928
  const mountedRef = useRef9(false);
1899
- const id = useId();
1929
+ const id = useId3();
1930
+ const describedRef = useRef9(null);
1900
1931
  const clearHideTimer = () => {
1901
1932
  if (hideTimer.current) {
1902
1933
  clearTimeout(hideTimer.current);
@@ -1933,7 +1964,8 @@ function Tooltip({
1933
1964
  if (!mountedRef.current) setBoxWidth(null);
1934
1965
  mountedRef.current = true;
1935
1966
  setAnchor({ top: r.top, left: r.left, width: r.width, height: r.height, bottom: r.bottom, right: r.right });
1936
- if (anchorRef) el.setAttribute("aria-describedby", id);
1967
+ const target = anchorRef ? el : describedRef.current;
1968
+ if (target) addDescribedBy(target, id);
1937
1969
  };
1938
1970
  const show = () => {
1939
1971
  clearHideTimer();
@@ -1950,7 +1982,9 @@ function Tooltip({
1950
1982
  mountedRef.current = false;
1951
1983
  setAnchor(null);
1952
1984
  setBoxWidth(null);
1953
- if (anchorRef?.current) anchorRef.current.removeAttribute("aria-describedby");
1985
+ if (anchorRef?.current) removeDescribedBy(anchorRef.current, id);
1986
+ if (describedRef.current) removeDescribedBy(describedRef.current, id);
1987
+ describedRef.current = null;
1954
1988
  };
1955
1989
  const hideNow = () => {
1956
1990
  clearShowTimer();
@@ -2001,6 +2035,12 @@ function Tooltip({
2001
2035
  el.removeEventListener("blur", hide);
2002
2036
  };
2003
2037
  }, [anchorRef, placement, delay]);
2038
+ useEffect8(() => {
2039
+ if (!anchor) return;
2040
+ const onKey = (e) => e.key === "Escape" && hideNow();
2041
+ window.addEventListener("keydown", onKey);
2042
+ return () => window.removeEventListener("keydown", onKey);
2043
+ }, [anchor]);
2004
2044
  useLayoutEffect2(() => {
2005
2045
  if (!anchor) return;
2006
2046
  const el = boxRef.current;
@@ -2064,9 +2104,11 @@ function Tooltip({
2064
2104
  className,
2065
2105
  onMouseEnter: show,
2066
2106
  onMouseLeave: hide,
2067
- onFocus: show,
2107
+ onFocus: (e) => {
2108
+ describedRef.current = e.target;
2109
+ show();
2110
+ },
2068
2111
  onBlur: hide,
2069
- "aria-describedby": anchor ? id : void 0,
2070
2112
  children: [
2071
2113
  children,
2072
2114
  floating
@@ -2761,7 +2803,7 @@ import { cva } from "class-variance-authority";
2761
2803
  import { Loader2 as Loader22 } from "lucide-react";
2762
2804
  import { jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
2763
2805
  var buttonVariants = cva(
2764
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-control transition-all duration-(--mr-motion-duration-interactive) ease-interactive disabled:pointer-events-none disabled:opacity-40 focus-visible:outline-hidden focus-visible:ring-0",
2806
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-control transition-all duration-(--mr-motion-duration-interactive) ease-interactive disabled:pointer-events-none disabled:opacity-40 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-focus",
2765
2807
  {
2766
2808
  variants: {
2767
2809
  variant: {
@@ -2859,7 +2901,7 @@ function FilterDropdownFooter({
2859
2901
  }
2860
2902
 
2861
2903
  // src/NameListModal.tsx
2862
- import { useEffect as useEffect10, useState as useState11 } from "react";
2904
+ import { useEffect as useEffect10, useId as useId4, useRef as useRef11, useState as useState11 } from "react";
2863
2905
  import { createPortal as createPortal5 } from "react-dom";
2864
2906
  import { jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
2865
2907
  function NameListModal({
@@ -2877,11 +2919,38 @@ function NameListModal({
2877
2919
  () => Object.fromEntries((options ?? []).map((o) => [o.key, !!o.initial]))
2878
2920
  );
2879
2921
  const toggle = (key) => setChecked((c) => ({ ...c, [key]: !c[key] }));
2922
+ const dialogRef = useRef11(null);
2923
+ const titleId = useId4();
2924
+ const fieldId = useId4();
2880
2925
  useEffect10(() => {
2881
2926
  const onKey = (e) => e.key === "Escape" && onCancel();
2882
2927
  window.addEventListener("keydown", onKey);
2883
2928
  return () => window.removeEventListener("keydown", onKey);
2884
2929
  }, [onCancel]);
2930
+ const [opener] = useState11(() => typeof document === "undefined" ? null : document.activeElement);
2931
+ useEffect10(
2932
+ () => () => {
2933
+ const active = document.activeElement;
2934
+ if (!active || active === document.body) opener?.focus?.();
2935
+ },
2936
+ [opener]
2937
+ );
2938
+ const trapTab = (e) => {
2939
+ if (e.key !== "Tab" || !dialogRef.current) return;
2940
+ const items = [
2941
+ ...dialogRef.current.querySelectorAll("button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex='-1'])")
2942
+ ];
2943
+ if (items.length === 0) return;
2944
+ const first = items[0];
2945
+ const last = items[items.length - 1];
2946
+ if (e.shiftKey && document.activeElement === first) {
2947
+ e.preventDefault();
2948
+ last.focus();
2949
+ } else if (!e.shiftKey && document.activeElement === last) {
2950
+ e.preventDefault();
2951
+ first.focus();
2952
+ }
2953
+ };
2885
2954
  return createPortal5(
2886
2955
  // bg-scrim, not a literal: design-tokens gained a scrim token in 2.5.0.
2887
2956
  // No in-data-[theme=dark] override needed any more -- the token redefines
@@ -2894,18 +2963,20 @@ function NameListModal({
2894
2963
  children: /* @__PURE__ */ jsxs26(
2895
2964
  "div",
2896
2965
  {
2966
+ ref: dialogRef,
2897
2967
  role: "dialog",
2898
2968
  "aria-modal": "true",
2899
- "aria-label": title,
2969
+ "aria-labelledby": titleId,
2970
+ onKeyDown: trapTab,
2900
2971
  className: "w-full max-w-[420px] rounded-card border border-border-default bg-surface p-5 shadow-overlay",
2901
2972
  children: [
2902
- /* @__PURE__ */ jsx28("h2", { className: "text-[16px] font-semibold text-text-primary", children: title }),
2973
+ /* @__PURE__ */ jsx28("h2", { id: titleId, className: "text-[16px] font-semibold text-text-primary", children: title }),
2903
2974
  /* @__PURE__ */ jsxs26("div", { className: "mt-4 flex flex-col gap-1.5", children: [
2904
- /* @__PURE__ */ jsx28("label", { htmlFor: "account-list-name", className: "text-[12px] font-medium text-text-secondary", children: fieldLabel }),
2975
+ /* @__PURE__ */ jsx28("label", { htmlFor: fieldId, className: "text-[12px] font-medium text-text-secondary", children: fieldLabel }),
2905
2976
  /* @__PURE__ */ jsx28(
2906
2977
  "input",
2907
2978
  {
2908
- id: "account-list-name",
2979
+ id: fieldId,
2909
2980
  autoFocus: true,
2910
2981
  value: name,
2911
2982
  onChange: (e) => setName(e.target.value),
@@ -3063,7 +3134,7 @@ function SortButton({
3063
3134
  // subtly tighter than the fixed headers next to them. A control
3064
3135
  // should carry exactly the properties that do not inherit, and
3065
3136
  // nothing more.
3066
- "group inline-flex items-center gap-1 uppercase transition-colors",
3137
+ "group inline-flex items-center gap-1 uppercase transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-focus",
3067
3138
  ALIGN[align],
3068
3139
  !disabled && "hover:text-text-primary"
3069
3140
  ),
@@ -3335,7 +3406,7 @@ function TablePagination({
3335
3406
  }
3336
3407
 
3337
3408
  // src/DateRangePicker.tsx
3338
- import { useEffect as useEffect11, useMemo as useMemo4, useRef as useRef11, useState as useState12 } from "react";
3409
+ import { useEffect as useEffect11, useMemo as useMemo4, useRef as useRef12, useState as useState12 } from "react";
3339
3410
  import { ChevronDown as ChevronDown10 } from "lucide-react";
3340
3411
  import { jsx as jsx36, jsxs as jsxs32 } from "react/jsx-runtime";
3341
3412
  var MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
@@ -3402,7 +3473,7 @@ function DateRangePicker({
3402
3473
  const [open, setOpen] = useState12(false);
3403
3474
  const [value, setValue] = useState12(defaultValue);
3404
3475
  const [draft, setDraft] = useState12(defaultValue);
3405
- const ref = useRef11(null);
3476
+ const ref = useRef12(null);
3406
3477
  const [fromMo, setFromMo] = useState12(end.getMonth());
3407
3478
  const [fromYr, setFromYr] = useState12(end.getFullYear() - 1);
3408
3479
  const [toMo, setToMo] = useState12(end.getMonth());
@@ -3509,7 +3580,7 @@ function DateRangePicker({
3509
3580
  }
3510
3581
 
3511
3582
  // src/MultiSelectFilter.tsx
3512
- import { useCallback as useCallback4, useEffect as useEffect12, useRef as useRef12, useState as useState13 } from "react";
3583
+ import { useCallback as useCallback4, useEffect as useEffect12, useRef as useRef13, useState as useState13 } from "react";
3513
3584
  import { ChevronDown as ChevronDown11, Check as Check7, Minus as Minus2, Search as Search5, X as X4 } from "lucide-react";
3514
3585
  import { jsx as jsx37, jsxs as jsxs33 } from "react/jsx-runtime";
3515
3586
  function MultiSelectFilter({
@@ -3538,7 +3609,7 @@ function MultiSelectFilter({
3538
3609
  const [open, setOpen] = useState13(false);
3539
3610
  const [q, setQ] = useState13("");
3540
3611
  const [draft, setDraft] = useState13(selected);
3541
- const ref = useRef12(null);
3612
+ const ref = useRef13(null);
3542
3613
  const close = useCallback4(() => {
3543
3614
  setOpen(false);
3544
3615
  setQ("");
@@ -3792,11 +3863,11 @@ function Toggle({ checked, onChange, label, size = "md", disabled, className, te
3792
3863
  }
3793
3864
 
3794
3865
  // src/AISpark.tsx
3795
- import { useId as useId2 } from "react";
3866
+ import { useId as useId5 } from "react";
3796
3867
  import { Sparkles } from "lucide-react";
3797
3868
  import { Fragment as Fragment5, jsx as jsx39, jsxs as jsxs34 } from "react/jsx-runtime";
3798
3869
  function AISpark({ size = 16, className }) {
3799
- const id = useId2();
3870
+ const id = useId5();
3800
3871
  return /* @__PURE__ */ jsxs34(Fragment5, { children: [
3801
3872
  /* @__PURE__ */ jsx39("svg", { width: "0", height: "0", "aria-hidden": "true", style: { position: "absolute" }, children: /* @__PURE__ */ jsx39("defs", { children: /* @__PURE__ */ jsxs34("linearGradient", { id, x1: "0", y1: "0", x2: "1", y2: "1", children: [
3802
3873
  /* @__PURE__ */ jsx39("stop", { offset: "0%", stopColor: "var(--mr-color-brand-primary)" }),