@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.
- package/README.md +9 -3
- package/components/AvatarGroup/AvatarGroup.css +61 -0
- package/components/AvatarGroup/AvatarGroup.d.ts +32 -0
- package/components/AvatarGroup/AvatarGroup.js +31 -0
- package/components/Banner/Banner.css +110 -0
- package/components/Banner/Banner.d.ts +36 -0
- package/components/Banner/Banner.js +59 -0
- package/components/ContributionGraph/ContributionGraph.css +7 -1
- package/components/ContributionGraph/ContributionGraph.d.ts +9 -1
- package/components/ContributionGraph/ContributionGraph.js +59 -43
- package/components/EmptyState/EmptyState.css +5 -0
- package/components/FilterBar/FilterBar.css +202 -0
- package/components/FilterBar/FilterBar.d.ts +55 -0
- package/components/FilterBar/FilterBar.js +249 -0
- package/components/FunnelChart/FunnelChart.css +3 -2
- package/components/FunnelChart/FunnelChart.d.ts +12 -4
- package/components/FunnelChart/FunnelChart.js +48 -28
- package/components/Gauge/Gauge.css +110 -0
- package/components/Gauge/Gauge.d.ts +64 -0
- package/components/Gauge/Gauge.js +111 -0
- package/components/HoverCard/HoverCard.css +97 -0
- package/components/HoverCard/HoverCard.d.ts +29 -0
- package/components/HoverCard/HoverCard.js +83 -0
- package/components/ImageCompare/ImageCompare.css +112 -0
- package/components/ImageCompare/ImageCompare.d.ts +40 -0
- package/components/ImageCompare/ImageCompare.js +134 -0
- package/components/LinkList/LinkList.d.ts +3 -1
- package/components/LinkList/LinkList.js +4 -3
- package/components/Meter/Meter.css +89 -0
- package/components/Meter/Meter.d.ts +36 -0
- package/components/Meter/Meter.js +48 -0
- package/components/NotificationCenter/NotificationCenter.css +11 -3
- package/components/Rating/Rating.css +74 -0
- package/components/Rating/Rating.d.ts +41 -0
- package/components/Rating/Rating.js +124 -0
- package/components/Sparkline/Sparkline.d.ts +5 -3
- package/components/Sparkline/Sparkline.js +30 -1
- package/components/SplitButton/SplitButton.css +93 -0
- package/components/SplitButton/SplitButton.d.ts +43 -0
- package/components/SplitButton/SplitButton.js +66 -0
- package/components/SplitPane/SplitPane.css +106 -0
- package/components/SplitPane/SplitPane.d.ts +36 -0
- package/components/SplitPane/SplitPane.js +130 -0
- package/components/StreamingText/StreamingText.css +40 -0
- package/components/StreamingText/StreamingText.d.ts +62 -0
- package/components/StreamingText/StreamingText.js +49 -0
- package/components/StreamingText/useStreamReveal.d.ts +70 -0
- package/components/StreamingText/useStreamReveal.js +121 -0
- package/components/registry.json +88 -0
- package/components/registry.json.d.ts +88 -0
- package/components/registry.json.js +1 -1
- package/index.d.ts +11 -0
- package/index.js +25 -0
- package/package.json +5 -1
- package/tokens/motion.d.ts +12 -3
- 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
|
|
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-
|
|
227
|
-
height: var(--gap-
|
|
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.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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 ?
|
|
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(
|