@robr0/design-system 0.13.0 → 0.15.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 (91) hide show
  1. package/README.md +15 -6
  2. package/charts.d.ts +1 -0
  3. package/charts.js +2 -0
  4. package/components/AnchorNav/AnchorNav.css +74 -0
  5. package/components/AnchorNav/AnchorNav.d.ts +7 -0
  6. package/components/AnchorNav/AnchorNav.js +74 -21
  7. package/components/Banner/Banner.css +110 -0
  8. package/components/Banner/Banner.d.ts +36 -0
  9. package/components/Banner/Banner.js +59 -0
  10. package/components/Button/Button.css +51 -0
  11. package/components/Button/Button.d.ts +2 -2
  12. package/components/Chart/Chart.css +4 -1
  13. package/components/CircularButton/CircularButton.css +46 -0
  14. package/components/CircularButton/CircularButton.d.ts +13 -2
  15. package/components/CircularButton/CircularButton.js +19 -16
  16. package/components/CommandPalette/CommandPalette.css +26 -1
  17. package/components/CommandPalette/CommandPalette.js +11 -7
  18. package/components/Composer/Composer.css +18 -0
  19. package/components/Composer/Composer.d.ts +4 -2
  20. package/components/Composer/Composer.js +1 -0
  21. package/components/ContributionGraph/ContributionGraph.css +28 -1
  22. package/components/ContributionGraph/ContributionGraph.d.ts +9 -1
  23. package/components/ContributionGraph/ContributionGraph.js +66 -42
  24. package/components/EmptyState/EmptyState.css +5 -0
  25. package/components/Figure/Figure.css +14 -4
  26. package/components/Figure/Figure.js +11 -20
  27. package/components/FunnelChart/FunnelChart.css +11 -35
  28. package/components/FunnelChart/FunnelChart.d.ts +26 -15
  29. package/components/FunnelChart/FunnelChart.js +82 -34
  30. package/components/Gauge/Gauge.css +39 -12
  31. package/components/Gauge/Gauge.d.ts +16 -8
  32. package/components/Gauge/Gauge.js +78 -62
  33. package/components/HoverCard/HoverCard.css +97 -0
  34. package/components/HoverCard/HoverCard.d.ts +29 -0
  35. package/components/HoverCard/HoverCard.js +83 -0
  36. package/components/ImageCompare/ImageCompare.css +112 -0
  37. package/components/ImageCompare/ImageCompare.d.ts +40 -0
  38. package/components/ImageCompare/ImageCompare.js +134 -0
  39. package/components/LinkList/LinkList.d.ts +3 -1
  40. package/components/LinkList/LinkList.js +4 -3
  41. package/components/Meter/Meter.css +98 -0
  42. package/components/Meter/Meter.d.ts +36 -0
  43. package/components/Meter/Meter.js +48 -0
  44. package/components/NotificationCenter/NotificationCenter.css +11 -3
  45. package/components/ProgressBar/ProgressBar.css +1 -1
  46. package/components/Rating/Rating.css +74 -0
  47. package/components/Rating/Rating.d.ts +41 -0
  48. package/components/Rating/Rating.js +124 -0
  49. package/components/SegmentedControl/SegmentedControl.css +23 -0
  50. package/components/SegmentedControl/SegmentedControl.d.ts +3 -1
  51. package/components/SegmentedControl/SegmentedControl.js +3 -1
  52. package/components/Slider/Slider.css +1 -1
  53. package/components/Slider/Slider.js +1 -1
  54. package/components/SourceTrail/SourceTrail.css +209 -0
  55. package/components/SourceTrail/SourceTrail.d.ts +49 -0
  56. package/components/SourceTrail/SourceTrail.js +108 -0
  57. package/components/Sparkline/Sparkline.css +26 -2
  58. package/components/Sparkline/Sparkline.d.ts +5 -3
  59. package/components/Sparkline/Sparkline.js +32 -2
  60. package/components/Spinner/Spinner.css +1 -1
  61. package/components/SplitButton/SplitButton.css +93 -0
  62. package/components/SplitButton/SplitButton.d.ts +43 -0
  63. package/components/SplitButton/SplitButton.js +67 -0
  64. package/components/StreamingText/StreamingText.d.ts +27 -9
  65. package/components/StreamingText/StreamingText.js +17 -29
  66. package/components/StreamingText/useStreamReveal.d.ts +70 -0
  67. package/components/StreamingText/useStreamReveal.js +121 -0
  68. package/components/ToggleGroup/ToggleGroup.css +17 -0
  69. package/components/ToggleGroup/ToggleGroup.d.ts +2 -0
  70. package/components/ToggleGroup/ToggleGroup.js +2 -0
  71. package/components/ToolCall/ToolCall.css +7 -2
  72. package/components/Tooltip/Tooltip.css +24 -7
  73. package/components/Tooltip/Tooltip.d.ts +2 -1
  74. package/components/UsageCard/UsageCard.css +30 -0
  75. package/components/UsageCard/UsageCard.d.ts +48 -0
  76. package/components/UsageCard/UsageCard.js +54 -0
  77. package/components/registry.d.ts +8 -2
  78. package/components/registry.json +89 -14
  79. package/components/registry.json.d.ts +89 -14
  80. package/components/registry.json.js +1 -1
  81. package/index.d.ts +8 -1
  82. package/index.js +19 -2
  83. package/package.json +5 -1
  84. package/tokens/motion.d.ts +9 -4
  85. package/tokens/motion.js +5 -1
  86. package/tokens/registry.json +4 -0
  87. package/tokens/registry.json.d.ts +4 -0
  88. package/tokens/registry.json.js +1 -1
  89. package/tokens/tokens-dark.css +11 -2
  90. package/tokens/tokens-light.css +25 -11
  91. package/tokens/tokens-primitives.css +3 -0
@@ -0,0 +1,83 @@
1
+ "use client";
2
+ import { jsxs, jsx } from "react/jsx-runtime";
3
+ import React, { useState, useRef, useId, useEffect } from "react";
4
+ import "./HoverCard.css";
5
+ import { MOTION_HOVER_SHOW_DELAY_MS, MOTION_HOVER_HIDE_DELAY_MS } from "../../tokens/motion.js";
6
+ const HoverCard = React.forwardRef(
7
+ ({
8
+ children,
9
+ content,
10
+ position = "bottom",
11
+ showDelay = MOTION_HOVER_SHOW_DELAY_MS,
12
+ hideDelay = MOTION_HOVER_HIDE_DELAY_MS,
13
+ className = "",
14
+ ...rest
15
+ }, ref) => {
16
+ const [visible, setVisible] = useState(false);
17
+ const showTimeoutRef = useRef(void 0);
18
+ const hideTimeoutRef = useRef(void 0);
19
+ const panelId = useId();
20
+ const baseClass = "ds-hover-card";
21
+ const show = () => {
22
+ clearTimeout(hideTimeoutRef.current);
23
+ showTimeoutRef.current = setTimeout(() => setVisible(true), showDelay);
24
+ };
25
+ const hide = () => {
26
+ clearTimeout(showTimeoutRef.current);
27
+ hideTimeoutRef.current = setTimeout(() => setVisible(false), hideDelay);
28
+ };
29
+ useEffect(() => {
30
+ return () => {
31
+ clearTimeout(showTimeoutRef.current);
32
+ clearTimeout(hideTimeoutRef.current);
33
+ };
34
+ }, []);
35
+ useEffect(() => {
36
+ if (!visible) return;
37
+ const onKeyDown = (e) => {
38
+ if (e.key === "Escape") {
39
+ clearTimeout(showTimeoutRef.current);
40
+ clearTimeout(hideTimeoutRef.current);
41
+ setVisible(false);
42
+ }
43
+ };
44
+ document.addEventListener("keydown", onKeyDown);
45
+ return () => document.removeEventListener("keydown", onKeyDown);
46
+ }, [visible]);
47
+ const handleBlur = (event) => {
48
+ if (!event.currentTarget.contains(event.relatedTarget)) {
49
+ hide();
50
+ }
51
+ };
52
+ const trigger = React.isValidElement(children) ? React.cloneElement(children, {
53
+ "aria-expanded": visible,
54
+ "aria-controls": visible ? panelId : void 0
55
+ }) : children;
56
+ const containerClasses = [baseClass, className].filter(Boolean).join(" ");
57
+ const panelClasses = [
58
+ `${baseClass}__panel`,
59
+ `${baseClass}__panel--${position}`,
60
+ visible ? `${baseClass}__panel--visible` : ""
61
+ ].filter(Boolean).join(" ");
62
+ return /* @__PURE__ */ jsxs(
63
+ "span",
64
+ {
65
+ ...rest,
66
+ ref,
67
+ className: containerClasses,
68
+ onMouseEnter: show,
69
+ onMouseLeave: hide,
70
+ onFocus: show,
71
+ onBlur: handleBlur,
72
+ children: [
73
+ trigger,
74
+ /* @__PURE__ */ jsx("span", { className: panelClasses, id: panelId, children: content })
75
+ ]
76
+ }
77
+ );
78
+ }
79
+ );
80
+ HoverCard.displayName = "HoverCard";
81
+ export {
82
+ HoverCard
83
+ };
@@ -0,0 +1,112 @@
1
+ /* ============================================
2
+ IMAGE COMPARE COMPONENT
3
+ Before-and-after images behind one divider
4
+ ============================================ */
5
+
6
+ .ds-image-compare {
7
+ position: relative;
8
+ width: 100%;
9
+ overflow: hidden;
10
+ border-radius: var(--radius-lg);
11
+ border: var(--border-xs) solid var(--color-bg-container-border);
12
+ background-color: var(--color-bg-container-secondary);
13
+ /* pan-y keeps the page scrollable over the frame on touch; horizontal
14
+ drags stay with the slider. */
15
+ touch-action: pan-y;
16
+ user-select: none;
17
+ cursor: col-resize;
18
+ }
19
+
20
+ /* ============================================
21
+ IMAGES
22
+ ============================================ */
23
+
24
+ .ds-image-compare__after,
25
+ .ds-image-compare__before {
26
+ position: absolute;
27
+ inset: 0;
28
+ width: 100%;
29
+ height: 100%;
30
+ object-fit: cover;
31
+ display: block;
32
+ }
33
+
34
+ /* The before layer sits on top at full frame size and is clipped to the
35
+ left of the divider. clip-path leaves the image's own geometry alone,
36
+ so the two layers stay pixel-aligned as the split moves. */
37
+ .ds-image-compare__before-clip {
38
+ position: absolute;
39
+ inset: 0;
40
+ clip-path: inset(0 calc(100% - var(--ds-image-compare-position, 50%)) 0 0);
41
+ }
42
+
43
+ /* ============================================
44
+ LABELS
45
+ ============================================ */
46
+
47
+ .ds-image-compare__label {
48
+ position: absolute;
49
+ top: var(--gap-md);
50
+ z-index: 2;
51
+ padding: var(--padding-xxxs) var(--padding-sm);
52
+ border-radius: var(--radius-full);
53
+ background-color: var(--color-bg-container-inverse);
54
+ color: var(--color-text-on-inverse);
55
+ font-family: var(--font-paragraph-sm-em-family);
56
+ font-size: var(--font-paragraph-sm-em-size);
57
+ font-weight: var(--font-paragraph-sm-em-weight);
58
+ line-height: var(--font-paragraph-sm-em-line-height);
59
+ pointer-events: none;
60
+ }
61
+
62
+ .ds-image-compare__label--before {
63
+ left: var(--gap-md);
64
+ }
65
+
66
+ .ds-image-compare__label--after {
67
+ right: var(--gap-md);
68
+ }
69
+
70
+ /* ============================================
71
+ DIVIDER + HANDLE
72
+ ============================================ */
73
+
74
+ .ds-image-compare__divider {
75
+ position: absolute;
76
+ top: 0;
77
+ bottom: 0;
78
+ left: var(--ds-image-compare-position);
79
+ width: var(--border-md);
80
+ transform: translateX(-50%);
81
+ background-color: var(--color-bg-page-primary);
82
+ z-index: 1;
83
+ pointer-events: none;
84
+ }
85
+
86
+ .ds-image-compare__handle {
87
+ position: absolute;
88
+ top: 50%;
89
+ left: var(--ds-image-compare-position);
90
+ transform: translate(-50%, -50%);
91
+ z-index: 3;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ width: 32px;
96
+ height: 32px;
97
+ border-radius: var(--radius-full);
98
+ background-color: var(--color-bg-page-primary);
99
+ color: var(--color-icon-primary);
100
+ border: var(--border-xs) solid var(--color-bg-container-border);
101
+ box-shadow: var(--shadow-floating);
102
+ cursor: col-resize;
103
+ }
104
+
105
+ .ds-image-compare__handle .material-symbols-rounded {
106
+ --icon-size: var(--icon-size-sm);
107
+ }
108
+
109
+ .ds-image-compare__handle:focus-visible {
110
+ outline: 2px solid var(--color-action-primary-bg);
111
+ outline-offset: 2px;
112
+ }
@@ -0,0 +1,40 @@
1
+ import { default as React } from 'react';
2
+ /** Props owned by ImageCompare itself — everything else falls through to the root node. */
3
+ type ImageCompareOwnProps = {
4
+ /** Source of the image revealed on the left of the divider */
5
+ beforeSrc: string;
6
+ /** Source of the image revealed on the right of the divider */
7
+ afterSrc: string;
8
+ /** Alt text for the before image */
9
+ beforeAlt: string;
10
+ /** Alt text for the after image */
11
+ afterAlt: string;
12
+ /** Corner label over the before side */
13
+ beforeLabel?: string;
14
+ /** Corner label over the after side */
15
+ afterLabel?: string;
16
+ /** Whether the corner labels render */
17
+ showLabels?: boolean;
18
+ /** Divider position as a percentage from the left (controlled) */
19
+ position?: number;
20
+ /** Initial divider position for uncontrolled use */
21
+ defaultPosition?: number;
22
+ /** Convenience callback receiving the divider position on every change */
23
+ onPositionChange?: (position: number) => void;
24
+ /** CSS aspect-ratio of the frame (both images are sized to cover it) */
25
+ aspectRatio?: string;
26
+ /** Additional CSS classes */
27
+ className?: string;
28
+ };
29
+ export interface ImageCompareProps extends ImageCompareOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof ImageCompareOwnProps> {
30
+ }
31
+ /**
32
+ * Before-and-after comparison of two images sharing one frame. Dragging the
33
+ * divider (or clicking anywhere in the frame) sweeps the split; the handle is
34
+ * a `role="slider"` with arrow-key, Home/End and PageUp/PageDown control, so
35
+ * the comparison works without a pointer.
36
+ *
37
+ * Forwards a ref to the root element and spreads unrecognised props onto it.
38
+ */
39
+ export declare const ImageCompare: React.ForwardRefExoticComponent<ImageCompareProps & React.RefAttributes<HTMLDivElement>>;
40
+ export {};
@@ -0,0 +1,134 @@
1
+ "use client";
2
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
+ import React, { useState, useRef } from "react";
4
+ import "./ImageCompare.css";
5
+ import "../../fonts/material-symbols.css";
6
+ const clamp = (n) => Math.max(0, Math.min(100, n));
7
+ const ImageCompare = React.forwardRef(
8
+ ({
9
+ beforeSrc,
10
+ afterSrc,
11
+ beforeAlt,
12
+ afterAlt,
13
+ beforeLabel = "Before",
14
+ afterLabel = "After",
15
+ showLabels = true,
16
+ position,
17
+ defaultPosition = 50,
18
+ onPositionChange,
19
+ aspectRatio = "16 / 10",
20
+ className = "",
21
+ style,
22
+ ...rest
23
+ }, ref) => {
24
+ const [internalPosition, setInternalPosition] = useState(clamp(defaultPosition));
25
+ const [dragging, setDragging] = useState(false);
26
+ const frameRef = useRef(null);
27
+ const baseClass = "ds-image-compare";
28
+ const currentPosition = clamp(position ?? internalPosition);
29
+ const classes = [baseClass, dragging ? `${baseClass}--dragging` : "", className].filter(Boolean).join(" ");
30
+ const setRef = (node) => {
31
+ frameRef.current = node;
32
+ if (typeof ref === "function") ref(node);
33
+ else if (ref) ref.current = node;
34
+ };
35
+ const setPosition = (next) => {
36
+ const resolved = clamp(next);
37
+ if (position === void 0) setInternalPosition(resolved);
38
+ onPositionChange?.(resolved);
39
+ };
40
+ const positionFromPointer = (clientX) => {
41
+ const frame = frameRef.current;
42
+ if (!frame) return;
43
+ const rect = frame.getBoundingClientRect();
44
+ if (rect.width === 0) return;
45
+ setPosition((clientX - rect.left) / rect.width * 100);
46
+ };
47
+ const handlePointerDown = (event) => {
48
+ if (event.button !== 0) return;
49
+ event.currentTarget.setPointerCapture(event.pointerId);
50
+ setDragging(true);
51
+ positionFromPointer(event.clientX);
52
+ };
53
+ const handlePointerMove = (event) => {
54
+ if (!dragging) return;
55
+ positionFromPointer(event.clientX);
56
+ };
57
+ const endDrag = () => setDragging(false);
58
+ const handleKeyDown = (event) => {
59
+ let next = null;
60
+ switch (event.key) {
61
+ case "ArrowRight":
62
+ case "ArrowUp":
63
+ next = currentPosition + 1;
64
+ break;
65
+ case "ArrowLeft":
66
+ case "ArrowDown":
67
+ next = currentPosition - 1;
68
+ break;
69
+ case "PageUp":
70
+ next = currentPosition + 10;
71
+ break;
72
+ case "PageDown":
73
+ next = currentPosition - 10;
74
+ break;
75
+ case "Home":
76
+ next = 0;
77
+ break;
78
+ case "End":
79
+ next = 100;
80
+ break;
81
+ }
82
+ if (next !== null) {
83
+ event.preventDefault();
84
+ setPosition(next);
85
+ }
86
+ };
87
+ return /* @__PURE__ */ jsxs(
88
+ "div",
89
+ {
90
+ ...rest,
91
+ ref: setRef,
92
+ className: classes,
93
+ style: {
94
+ ...style,
95
+ aspectRatio,
96
+ ["--ds-image-compare-position"]: `${currentPosition}%`
97
+ },
98
+ onPointerDown: handlePointerDown,
99
+ onPointerMove: handlePointerMove,
100
+ onPointerUp: endDrag,
101
+ onPointerCancel: endDrag,
102
+ children: [
103
+ /* @__PURE__ */ jsx("img", { className: `${baseClass}__after`, src: afterSrc, alt: afterAlt, draggable: false }),
104
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__before-clip`, children: /* @__PURE__ */ jsx("img", { className: `${baseClass}__before`, src: beforeSrc, alt: beforeAlt, draggable: false }) }),
105
+ showLabels && /* @__PURE__ */ jsxs(Fragment, { children: [
106
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__label ${baseClass}__label--before`, "aria-hidden": "true", children: beforeLabel }),
107
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__label ${baseClass}__label--after`, "aria-hidden": "true", children: afterLabel })
108
+ ] }),
109
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__divider`, "aria-hidden": "true" }),
110
+ /* @__PURE__ */ jsx(
111
+ "div",
112
+ {
113
+ className: `${baseClass}__handle`,
114
+ role: "slider",
115
+ tabIndex: 0,
116
+ "aria-label": `${beforeLabel} and ${afterLabel} comparison`,
117
+ "aria-valuemin": 0,
118
+ "aria-valuemax": 100,
119
+ "aria-valuenow": Math.round(currentPosition),
120
+ "aria-valuetext": `${Math.round(currentPosition)}% ${beforeLabel.toLowerCase()}`,
121
+ "aria-orientation": "horizontal",
122
+ onKeyDown: handleKeyDown,
123
+ children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", "aria-hidden": "true", children: "drag_indicator" })
124
+ }
125
+ )
126
+ ]
127
+ }
128
+ );
129
+ }
130
+ );
131
+ ImageCompare.displayName = "ImageCompare";
132
+ export {
133
+ ImageCompare
134
+ };
@@ -11,9 +11,11 @@ export interface LinkListItem {
11
11
  logoAlt?: string;
12
12
  /** Material Symbol name used when no logo is provided (e.g. "emoji_events") */
13
13
  icon?: string;
14
+ /** Open in a new tab. Defaults to true; set false for links inside the same site, which swaps the open_in_new indicator for arrow_forward. */
15
+ newTab?: boolean;
14
16
  }
15
17
  export interface LinkListProps {
16
- /** Links to render, in display order — each opens in a new tab */
18
+ /** Links to render, in display order */
17
19
  items: LinkListItem[];
18
20
  /** Additional CSS classes */
19
21
  className?: string;
@@ -5,12 +5,13 @@ const LinkList = ({ items, className = "" }) => {
5
5
  const classes = ["ds-link-list", className].filter(Boolean).join(" ");
6
6
  return /* @__PURE__ */ jsx("div", { className: classes, children: items.map((item) => {
7
7
  const subs = item.sub ? Array.isArray(item.sub) ? item.sub : [item.sub] : [];
8
+ const newTab = item.newTab ?? true;
8
9
  return /* @__PURE__ */ jsxs(
9
10
  "a",
10
11
  {
11
12
  href: item.href,
12
- target: "_blank",
13
- rel: "noopener noreferrer",
13
+ target: newTab ? "_blank" : void 0,
14
+ rel: newTab ? "noopener noreferrer" : void 0,
14
15
  className: "ds-link-list__item",
15
16
  children: [
16
17
  /* @__PURE__ */ jsx("span", { className: "ds-link-list__icon-wrap", "aria-hidden": "true", children: item.logo ? /* @__PURE__ */ jsx(
@@ -26,7 +27,7 @@ const LinkList = ({ items, className = "" }) => {
26
27
  /* @__PURE__ */ jsxs("div", { className: "ds-link-list__content", children: [
27
28
  /* @__PURE__ */ jsxs("div", { className: "ds-link-list__title", children: [
28
29
  /* @__PURE__ */ jsx("span", { children: item.label }),
29
- /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded ds-link-list__open-icon", "aria-hidden": "true", children: "open_in_new" })
30
+ /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded ds-link-list__open-icon", "aria-hidden": "true", children: newTab ? "open_in_new" : "arrow_forward" })
30
31
  ] }),
31
32
  subs.map((s, i) => /* @__PURE__ */ jsx("span", { className: "ds-link-list__sub", children: s }, i))
32
33
  ] })
@@ -0,0 +1,98 @@
1
+ /* ============================================
2
+ METER COMPONENT
3
+ Level indicator for a known quantity
4
+ ============================================ */
5
+
6
+ .ds-meter {
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: var(--gap-xs);
10
+ width: 100%;
11
+ }
12
+
13
+ /* ============================================
14
+ HEADER
15
+ ============================================ */
16
+
17
+ .ds-meter__header {
18
+ display: flex;
19
+ align-items: baseline;
20
+ justify-content: space-between;
21
+ gap: var(--gap-md);
22
+ }
23
+
24
+ .ds-meter__label {
25
+ font-family: var(--font-paragraph-sm-em-family);
26
+ font-size: var(--font-paragraph-sm-em-size);
27
+ font-weight: var(--font-paragraph-sm-em-weight);
28
+ line-height: var(--font-paragraph-sm-em-line-height);
29
+ color: var(--color-text-primary);
30
+ }
31
+
32
+ .ds-meter__value {
33
+ font-family: var(--font-paragraph-sm-family);
34
+ font-size: var(--font-paragraph-sm-size);
35
+ font-weight: var(--font-paragraph-sm-weight);
36
+ line-height: var(--font-paragraph-sm-line-height);
37
+ color: var(--color-text-secondary);
38
+ font-variant-numeric: tabular-nums;
39
+ margin-left: auto;
40
+ }
41
+
42
+ /* ============================================
43
+ TRACK + FILL
44
+ ============================================ */
45
+
46
+ .ds-meter__track {
47
+ width: 100%;
48
+ height: 8px;
49
+ border-radius: var(--radius-full);
50
+ background-color: var(--color-bg-container-tertiary);
51
+ overflow: hidden;
52
+ }
53
+
54
+ .ds-meter--compact .ds-meter__track {
55
+ height: 4px;
56
+ }
57
+
58
+ .ds-meter__fill {
59
+ height: 100%;
60
+ border-radius: var(--radius-full);
61
+ transition: width var(--motion-duration-base) var(--motion-ease-standard);
62
+ /* Grows to the current level on mount — the single keyframe animates to
63
+ the inline width, and the duration token collapses under reduced motion. */
64
+ animation: ds-meter-grow var(--motion-duration-slower) var(--motion-ease-emphasized);
65
+ }
66
+
67
+ @keyframes ds-meter-grow {
68
+ from {
69
+ width: 0;
70
+ }
71
+ }
72
+
73
+ /* ============================================
74
+ STATUS VARIANTS
75
+ The strong border-weight status colour, not
76
+ the tinted bg — the fill is a thin ribbon
77
+ and needs the full-strength hue to read.
78
+ ============================================ */
79
+
80
+ .ds-meter--info .ds-meter__fill {
81
+ background-color: var(--color-status-info-border);
82
+ }
83
+
84
+ .ds-meter--positive .ds-meter__fill {
85
+ background-color: var(--color-status-positive-border);
86
+ }
87
+
88
+ .ds-meter--warning .ds-meter__fill {
89
+ background-color: var(--color-status-warning-border);
90
+ }
91
+
92
+ .ds-meter--error .ds-meter__fill {
93
+ background-color: var(--color-status-error-border);
94
+ }
95
+
96
+ .ds-meter--neutral .ds-meter__fill {
97
+ background-color: var(--color-status-neutral-border);
98
+ }
@@ -0,0 +1,36 @@
1
+ import { default as React } from 'react';
2
+ /** Props owned by Meter itself — everything else falls through to the root node. */
3
+ type MeterOwnProps = {
4
+ /** Current level */
5
+ value?: number;
6
+ /** Lower bound of the range */
7
+ min?: number;
8
+ /** Upper bound of the range */
9
+ max?: number;
10
+ /** Visible label naming what is measured, doubling as the accessible name */
11
+ label?: string;
12
+ /** Shows the value readout on the trailing edge of the label row */
13
+ showValue?: boolean;
14
+ /** Readout override — replaces the default percentage, spoken via aria-valuetext */
15
+ valueText?: string;
16
+ /** Status role colouring the fill */
17
+ variant?: 'info' | 'positive' | 'warning' | 'error' | 'neutral';
18
+ /** Component size (bar height) */
19
+ size?: 'default' | 'compact';
20
+ /** Additional CSS classes */
21
+ className?: string;
22
+ };
23
+ export interface MeterProps extends MeterOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof MeterOwnProps> {
24
+ }
25
+ /**
26
+ * Level indicator for a known quantity — storage used, tokens spent,
27
+ * password strength, battery left. The counterpart to ProgressBar, which
28
+ * shows a task moving toward completion; a Meter shows how full something
29
+ * is right now, and its status colour says whether that is good news.
30
+ *
31
+ * Purely presentational (no 'use client'), so it renders from a Server
32
+ * Component. Forwards a ref to the root element and spreads unrecognised
33
+ * props onto it.
34
+ */
35
+ export declare const Meter: React.ForwardRefExoticComponent<MeterProps & React.RefAttributes<HTMLDivElement>>;
36
+ export {};
@@ -0,0 +1,48 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import "./Meter.css";
4
+ const Meter = React.forwardRef(
5
+ ({
6
+ value = 0,
7
+ min = 0,
8
+ max = 100,
9
+ label,
10
+ showValue = false,
11
+ valueText,
12
+ variant = "info",
13
+ size = "default",
14
+ className = "",
15
+ "aria-label": ariaLabel,
16
+ ...rest
17
+ }, ref) => {
18
+ const baseClass = "ds-meter";
19
+ const range = max - min;
20
+ const fraction = range > 0 ? Math.max(0, Math.min(1, (value - min) / range)) : 0;
21
+ const percent = Math.round(fraction * 100);
22
+ const readout = valueText ?? `${percent}%`;
23
+ const classes = [baseClass, `${baseClass}--${variant}`, `${baseClass}--${size}`, className].filter(Boolean).join(" ");
24
+ return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
25
+ (label || showValue) && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__header`, children: [
26
+ label && /* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: label }),
27
+ showValue && /* @__PURE__ */ jsx("span", { className: `${baseClass}__value`, children: readout })
28
+ ] }),
29
+ /* @__PURE__ */ jsx(
30
+ "div",
31
+ {
32
+ className: `${baseClass}__track`,
33
+ role: "meter",
34
+ "aria-valuenow": value,
35
+ "aria-valuemin": min,
36
+ "aria-valuemax": max,
37
+ "aria-valuetext": valueText,
38
+ "aria-label": ariaLabel ?? label ?? "Level",
39
+ children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__fill`, style: { width: `${fraction * 100}%` } })
40
+ }
41
+ )
42
+ ] });
43
+ }
44
+ );
45
+ Meter.displayName = "Meter";
46
+ export {
47
+ Meter
48
+ };
@@ -165,11 +165,16 @@
165
165
  padding: var(--padding-sm) var(--padding-sm-md);
166
166
  }
167
167
 
168
+ /* The slot is a fixed column as wide as its largest medium — an sm Avatar
169
+ (32px, the --icon-size-lg rung) — with smaller media centred in it, so
170
+ every row's text starts on the same edge whether its medium is an avatar
171
+ or a bare icon. */
168
172
  .ds-notification-item__media {
169
173
  display: inline-flex;
170
174
  align-items: center;
171
175
  justify-content: center;
172
176
  flex: none;
177
+ width: var(--icon-size-lg);
173
178
  color: var(--color-icon-primary);
174
179
  }
175
180
 
@@ -220,11 +225,14 @@
220
225
  }
221
226
 
222
227
  /* Unread dot — informational, so it takes the info status colour rather
223
- than the action teal */
228
+ than the action teal, at the full-strength border weight for the same
229
+ reason Meter's fill does: a tint vanishes in a small mark. 8px because
230
+ a 4px dot is sub-perceptual at reading distance — the dot is the row's
231
+ one unmistakable unread signal, so it has to register at a glance. */
224
232
  .ds-notification-item__dot {
225
233
  flex: none;
226
- width: var(--gap-xs);
227
- height: var(--gap-xs);
234
+ width: var(--gap-sm);
235
+ height: var(--gap-sm);
228
236
  border-radius: var(--radius-full);
229
237
  background-color: var(--color-status-info-border);
230
238
  }
@@ -17,7 +17,7 @@
17
17
  .ds-progress-bar__track {
18
18
  flex: 1;
19
19
  height: 8px;
20
- background-color: var(--color-bg-container-secondary);
20
+ background-color: var(--color-bg-container-tertiary);
21
21
  border-radius: var(--radius-full);
22
22
  overflow: hidden;
23
23
  }