@robr0/design-system 0.12.0 → 0.14.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 (56) hide show
  1. package/README.md +9 -3
  2. package/components/AvatarGroup/AvatarGroup.css +61 -0
  3. package/components/AvatarGroup/AvatarGroup.d.ts +32 -0
  4. package/components/AvatarGroup/AvatarGroup.js +31 -0
  5. package/components/Banner/Banner.css +110 -0
  6. package/components/Banner/Banner.d.ts +36 -0
  7. package/components/Banner/Banner.js +59 -0
  8. package/components/ContributionGraph/ContributionGraph.css +7 -1
  9. package/components/ContributionGraph/ContributionGraph.d.ts +9 -1
  10. package/components/ContributionGraph/ContributionGraph.js +59 -43
  11. package/components/EmptyState/EmptyState.css +5 -0
  12. package/components/FilterBar/FilterBar.css +202 -0
  13. package/components/FilterBar/FilterBar.d.ts +55 -0
  14. package/components/FilterBar/FilterBar.js +249 -0
  15. package/components/FunnelChart/FunnelChart.css +3 -2
  16. package/components/FunnelChart/FunnelChart.d.ts +12 -4
  17. package/components/FunnelChart/FunnelChart.js +48 -28
  18. package/components/Gauge/Gauge.css +110 -0
  19. package/components/Gauge/Gauge.d.ts +64 -0
  20. package/components/Gauge/Gauge.js +111 -0
  21. package/components/HoverCard/HoverCard.css +97 -0
  22. package/components/HoverCard/HoverCard.d.ts +29 -0
  23. package/components/HoverCard/HoverCard.js +83 -0
  24. package/components/ImageCompare/ImageCompare.css +112 -0
  25. package/components/ImageCompare/ImageCompare.d.ts +40 -0
  26. package/components/ImageCompare/ImageCompare.js +134 -0
  27. package/components/LinkList/LinkList.d.ts +3 -1
  28. package/components/LinkList/LinkList.js +4 -3
  29. package/components/Meter/Meter.css +89 -0
  30. package/components/Meter/Meter.d.ts +36 -0
  31. package/components/Meter/Meter.js +48 -0
  32. package/components/NotificationCenter/NotificationCenter.css +11 -3
  33. package/components/Rating/Rating.css +74 -0
  34. package/components/Rating/Rating.d.ts +41 -0
  35. package/components/Rating/Rating.js +124 -0
  36. package/components/Sparkline/Sparkline.d.ts +5 -3
  37. package/components/Sparkline/Sparkline.js +30 -1
  38. package/components/SplitButton/SplitButton.css +93 -0
  39. package/components/SplitButton/SplitButton.d.ts +43 -0
  40. package/components/SplitButton/SplitButton.js +66 -0
  41. package/components/SplitPane/SplitPane.css +106 -0
  42. package/components/SplitPane/SplitPane.d.ts +36 -0
  43. package/components/SplitPane/SplitPane.js +130 -0
  44. package/components/StreamingText/StreamingText.css +40 -0
  45. package/components/StreamingText/StreamingText.d.ts +62 -0
  46. package/components/StreamingText/StreamingText.js +49 -0
  47. package/components/StreamingText/useStreamReveal.d.ts +70 -0
  48. package/components/StreamingText/useStreamReveal.js +121 -0
  49. package/components/registry.json +88 -0
  50. package/components/registry.json.d.ts +88 -0
  51. package/components/registry.json.js +1 -1
  52. package/index.d.ts +11 -0
  53. package/index.js +25 -0
  54. package/package.json +5 -1
  55. package/tokens/motion.d.ts +12 -3
  56. package/tokens/motion.js +7 -1
@@ -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,89 @@
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-secondary);
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
+ }
63
+
64
+ /* ============================================
65
+ STATUS VARIANTS
66
+ The strong border-weight status colour, not
67
+ the tinted bg — the fill is a thin ribbon
68
+ and needs the full-strength hue to read.
69
+ ============================================ */
70
+
71
+ .ds-meter--info .ds-meter__fill {
72
+ background-color: var(--color-status-info-border);
73
+ }
74
+
75
+ .ds-meter--positive .ds-meter__fill {
76
+ background-color: var(--color-status-positive-border);
77
+ }
78
+
79
+ .ds-meter--warning .ds-meter__fill {
80
+ background-color: var(--color-status-warning-border);
81
+ }
82
+
83
+ .ds-meter--error .ds-meter__fill {
84
+ background-color: var(--color-status-error-border);
85
+ }
86
+
87
+ .ds-meter--neutral .ds-meter__fill {
88
+ background-color: var(--color-status-neutral-border);
89
+ }
@@ -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
  }
@@ -0,0 +1,74 @@
1
+ /* ============================================
2
+ RATING COMPONENT
3
+ Star-scale rating control
4
+ ============================================ */
5
+
6
+ .ds-rating {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ gap: var(--gap-xxs);
10
+ }
11
+
12
+ /* ============================================
13
+ STEPS
14
+ ============================================ */
15
+
16
+ .ds-rating__step {
17
+ display: inline-flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ padding: 0;
21
+ border: none;
22
+ background: none;
23
+ cursor: pointer;
24
+ color: var(--color-icon-secondary);
25
+ border-radius: var(--radius-xs);
26
+ transition: color var(--motion-duration-fast) var(--motion-ease-standard),
27
+ transform var(--motion-duration-fast) var(--motion-ease-standard);
28
+ }
29
+
30
+ .ds-rating__icon {
31
+ --icon-size: var(--icon-size-md);
32
+ }
33
+
34
+ .ds-rating--compact .ds-rating__icon {
35
+ --icon-size: var(--icon-size-sm);
36
+ }
37
+
38
+ .ds-rating__step--filled {
39
+ color: var(--color-core-accent-gold);
40
+ }
41
+
42
+ .ds-rating__step--filled .ds-rating__icon {
43
+ --material-symbols-fill: 1;
44
+ }
45
+
46
+ /* ============================================
47
+ STATES
48
+ ============================================ */
49
+
50
+ .ds-rating__step:hover {
51
+ transform: scale(1.1);
52
+ }
53
+
54
+ .ds-rating__step:focus-visible {
55
+ outline: 2px solid var(--color-action-primary-bg);
56
+ outline-offset: 2px;
57
+ }
58
+
59
+ .ds-rating--read-only .ds-rating__step {
60
+ cursor: default;
61
+ }
62
+
63
+ .ds-rating--read-only .ds-rating__step:hover {
64
+ transform: none;
65
+ }
66
+
67
+ .ds-rating--disabled .ds-rating__step {
68
+ opacity: 0.4;
69
+ cursor: not-allowed;
70
+ }
71
+
72
+ .ds-rating--disabled .ds-rating__step:hover {
73
+ transform: none;
74
+ }
@@ -0,0 +1,41 @@
1
+ import { default as React } from 'react';
2
+ /** Props owned by Rating itself — everything else falls through to the root node. */
3
+ type RatingOwnProps = {
4
+ /** Current rating (controlled). 0 means no rating. */
5
+ value?: number;
6
+ /** Initial rating for uncontrolled use. 0 means no rating. */
7
+ defaultValue?: number;
8
+ /** Number of steps on the scale */
9
+ max?: number;
10
+ /**
11
+ * Convenience callback receiving the new rating directly.
12
+ * Fires on every selection, including a clear back to 0 via `allowClear`.
13
+ */
14
+ onValueChange?: (value: number) => void;
15
+ /** Display-only mode — renders the current rating with no interaction */
16
+ readOnly?: boolean;
17
+ /** Whether the control is disabled */
18
+ disabled?: boolean;
19
+ /** Selecting the already-selected step clears the rating back to 0 */
20
+ allowClear?: boolean;
21
+ /** Material Symbol drawn for each step */
22
+ icon?: string;
23
+ /** Component size */
24
+ size?: 'default' | 'compact';
25
+ /** Accessible name for the group, and the base of each step's label */
26
+ label?: string;
27
+ /** Additional CSS classes */
28
+ className?: string;
29
+ };
30
+ export interface RatingProps extends RatingOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof RatingOwnProps> {
31
+ }
32
+ /**
33
+ * Star-scale rating control. Behaves as a radio group: each step is a
34
+ * `role="radio"` button, arrow keys move the selection, and the current
35
+ * step holds the roving tab stop. `readOnly` renders the same row as a
36
+ * static image with a spoken "N out of M" label.
37
+ *
38
+ * Forwards a ref to the root element and spreads unrecognised props onto it.
39
+ */
40
+ export declare const Rating: React.ForwardRefExoticComponent<RatingProps & React.RefAttributes<HTMLDivElement>>;
41
+ export {};
@@ -0,0 +1,124 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import React, { useState } from "react";
4
+ import "./Rating.css";
5
+ import "../../fonts/material-symbols.css";
6
+ const Rating = React.forwardRef(
7
+ ({
8
+ value,
9
+ defaultValue = 0,
10
+ max = 5,
11
+ onValueChange,
12
+ readOnly = false,
13
+ disabled = false,
14
+ allowClear = false,
15
+ icon = "star",
16
+ size = "default",
17
+ label = "Rating",
18
+ className = "",
19
+ ...rest
20
+ }, ref) => {
21
+ const [internalValue, setInternalValue] = useState(defaultValue);
22
+ const [hoverValue, setHoverValue] = useState(0);
23
+ const baseClass = "ds-rating";
24
+ const currentValue = value ?? internalValue;
25
+ const displayValue = !readOnly && !disabled && hoverValue > 0 ? hoverValue : currentValue;
26
+ const classes = [
27
+ baseClass,
28
+ `${baseClass}--${size}`,
29
+ readOnly ? `${baseClass}--read-only` : "",
30
+ disabled ? `${baseClass}--disabled` : "",
31
+ className
32
+ ].filter(Boolean).join(" ");
33
+ const setValue = (next) => {
34
+ const resolved = allowClear && next === currentValue ? 0 : next;
35
+ if (value === void 0) setInternalValue(resolved);
36
+ onValueChange?.(resolved);
37
+ };
38
+ const handleKeyDown = (event) => {
39
+ let next = null;
40
+ switch (event.key) {
41
+ case "ArrowRight":
42
+ case "ArrowUp":
43
+ next = Math.min(max, currentValue + 1);
44
+ break;
45
+ case "ArrowLeft":
46
+ case "ArrowDown":
47
+ next = Math.max(1, currentValue - 1);
48
+ break;
49
+ case "Home":
50
+ next = 1;
51
+ break;
52
+ case "End":
53
+ next = max;
54
+ break;
55
+ }
56
+ if (next !== null) {
57
+ event.preventDefault();
58
+ if (value === void 0) setInternalValue(next);
59
+ onValueChange?.(next);
60
+ const steps2 = event.currentTarget.querySelectorAll('[role="radio"]');
61
+ steps2[next - 1]?.focus();
62
+ }
63
+ };
64
+ const steps = Array.from({ length: max }, (_, i) => i + 1);
65
+ if (readOnly) {
66
+ return /* @__PURE__ */ jsx(
67
+ "div",
68
+ {
69
+ ...rest,
70
+ ref,
71
+ className: classes,
72
+ role: "img",
73
+ "aria-label": rest["aria-label"] ?? `${label}: ${currentValue} out of ${max}`,
74
+ children: steps.map((step) => /* @__PURE__ */ jsx(
75
+ "span",
76
+ {
77
+ className: `${baseClass}__step ${step <= displayValue ? `${baseClass}__step--filled` : ""}`,
78
+ "aria-hidden": "true",
79
+ children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, children: icon })
80
+ },
81
+ step
82
+ ))
83
+ }
84
+ );
85
+ }
86
+ return /* @__PURE__ */ jsx(
87
+ "div",
88
+ {
89
+ ...rest,
90
+ ref,
91
+ className: classes,
92
+ role: "radiogroup",
93
+ "aria-label": rest["aria-label"] ?? label,
94
+ "aria-disabled": disabled || void 0,
95
+ onKeyDown: disabled ? void 0 : handleKeyDown,
96
+ onMouseLeave: () => setHoverValue(0),
97
+ children: steps.map((step) => {
98
+ const checked = step === currentValue;
99
+ const tabStop = checked || currentValue === 0 && step === 1;
100
+ return /* @__PURE__ */ jsx(
101
+ "button",
102
+ {
103
+ type: "button",
104
+ role: "radio",
105
+ "aria-checked": checked,
106
+ "aria-label": label ? `${label}: ${step} out of ${max}` : `${step} out of ${max}`,
107
+ tabIndex: tabStop ? 0 : -1,
108
+ disabled,
109
+ className: `${baseClass}__step ${step <= displayValue ? `${baseClass}__step--filled` : ""}`,
110
+ onClick: () => setValue(step),
111
+ onMouseEnter: () => setHoverValue(step),
112
+ children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon })
113
+ },
114
+ step
115
+ );
116
+ })
117
+ }
118
+ );
119
+ }
120
+ );
121
+ Rating.displayName = "Rating";
122
+ export {
123
+ Rating
124
+ };
@@ -41,9 +41,11 @@ export interface SparklineProps extends SparklineOwnProps, Omit<React.ComponentP
41
41
  * Sparkline — an inline trend line for stats and table cells, drawn without
42
42
  * axes or chrome. Pure SVG computed from props: no charting library, no hooks,
43
43
  * no dependencies, so it renders from a Server Component and costs nothing in
44
- * a dense table. Degenerate data never breaks the path: an all-equal series
45
- * renders a horizontal midline, and fewer than two points render just the
46
- * end dot, or nothing.
44
+ * a dense table. The line bends through a monotone curve, the same family as
45
+ * the recharts charts' `monotone`, so it reads as a miniature LineChart
46
+ * rather than a jagged polyline. Degenerate data never breaks the path: an
47
+ * all-equal series renders a horizontal midline, and fewer than two points
48
+ * render just the end dot, or nothing.
47
49
  */
48
50
  export declare const Sparkline: React.ForwardRefExoticComponent<SparklineProps & React.RefAttributes<SVGSVGElement>>;
49
51
  export {};
@@ -2,6 +2,35 @@ import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import "./Sparkline.css";
4
4
  const round = (n) => Math.round(n * 100) / 100;
5
+ const buildMonotonePath = (pts) => {
6
+ const n = pts.length;
7
+ const dx = [];
8
+ const slope = [];
9
+ for (let i = 0; i < n - 1; i++) {
10
+ dx[i] = pts[i + 1].x - pts[i].x;
11
+ slope[i] = (pts[i + 1].y - pts[i].y) / dx[i];
12
+ }
13
+ const tangent = [slope[0]];
14
+ for (let i = 1; i < n - 1; i++) {
15
+ if (slope[i - 1] * slope[i] <= 0) {
16
+ tangent[i] = 0;
17
+ } else {
18
+ const w1 = 2 * dx[i] + dx[i - 1];
19
+ const w2 = dx[i] + 2 * dx[i - 1];
20
+ tangent[i] = (w1 + w2) / (w1 / slope[i - 1] + w2 / slope[i]);
21
+ }
22
+ }
23
+ tangent[n - 1] = slope[n - 2];
24
+ let d = `M ${round(pts[0].x)} ${round(pts[0].y)}`;
25
+ for (let i = 0; i < n - 1; i++) {
26
+ const c1x = pts[i].x + dx[i] / 3;
27
+ const c1y = pts[i].y + tangent[i] * dx[i] / 3;
28
+ const c2x = pts[i + 1].x - dx[i] / 3;
29
+ const c2y = pts[i + 1].y - tangent[i + 1] * dx[i] / 3;
30
+ d += ` C ${round(c1x)} ${round(c1y)} ${round(c2x)} ${round(c2y)} ${round(pts[i + 1].x)} ${round(pts[i + 1].y)}`;
31
+ }
32
+ return d;
33
+ };
5
34
  const Sparkline = React.forwardRef(
6
35
  ({
7
36
  data,
@@ -40,7 +69,7 @@ const Sparkline = React.forwardRef(
40
69
  }));
41
70
  const lastPoint = points.length > 0 ? points[points.length - 1] : void 0;
42
71
  const hasLine = points.length >= 2;
43
- const linePath = hasLine ? `M ${points.map((p) => `${p.x} ${p.y}`).join(" L ")}` : void 0;
72
+ const linePath = hasLine ? buildMonotonePath(points) : void 0;
44
73
  const baseline = round(height - pad);
45
74
  const areaPath = hasLine && variant === "area" ? `${linePath} L ${points[points.length - 1].x} ${baseline} L ${points[0].x} ${baseline} Z` : void 0;
46
75
  return /* @__PURE__ */ jsxs(