@robr0/design-system 0.8.0 → 0.9.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 +3 -3
- package/components/Accordion/Accordion.js +1 -0
- package/components/AgentPlan/AgentPlan.css +2 -2
- package/components/AgentPlan/AgentPlan.js +1 -0
- package/components/AgentStatus/AgentStatus.js +1 -0
- package/components/AiButton/AiButton.js +1 -0
- package/components/AlertDialog/AlertDialog.js +1 -0
- package/components/AnchorNav/AnchorNav.css +63 -0
- package/components/AnchorNav/AnchorNav.d.ts +33 -0
- package/components/AnchorNav/AnchorNav.js +82 -0
- package/components/AppLayout/AppLayout.js +1 -0
- package/components/AppSidebar/AppSidebar.js +1 -0
- package/components/Avatar/Avatar.js +1 -0
- package/components/Button/Button.js +1 -0
- package/components/Card/Card.js +1 -0
- package/components/Carousel/Carousel.js +1 -0
- package/components/ChatThread/ChatThread.js +1 -0
- package/components/Checkbox/Checkbox.js +1 -0
- package/components/CodeBlock/CodeBlock.js +1 -0
- package/components/CodeDiff/CodeDiff.css +175 -0
- package/components/CodeDiff/CodeDiff.d.ts +58 -0
- package/components/CodeDiff/CodeDiff.js +114 -0
- package/components/ColorPicker/ColorPicker.js +1 -0
- package/components/Combobox/Combobox.js +1 -0
- package/components/CommandPalette/CommandPalette.js +1 -0
- package/components/Composer/Composer.js +1 -0
- package/components/ContactCard/ContactCard.js +1 -0
- package/components/ContextMenu/ContextMenu.js +1 -0
- package/components/DataTable/DataTable.js +1 -0
- package/components/DateInput/DateInput.js +1 -0
- package/components/DatePicker/DatePicker.js +1 -0
- package/components/Dialog/Dialog.js +1 -0
- package/components/Drawer/Drawer.js +1 -0
- package/components/Dropdown/Dropdown.js +1 -0
- package/components/DropdownMenu/DropdownMenu.css +1 -1
- package/components/DropdownMenu/DropdownMenu.js +1 -0
- package/components/EventCalendar/EventCalendar.js +1 -0
- package/components/Field/Field.js +1 -0
- package/components/Field/FieldContext.js +1 -0
- package/components/FileInput/FileInput.js +1 -0
- package/components/Input/Input.js +1 -0
- package/components/InterruptCard/InterruptCard.css +11 -21
- package/components/InterruptCard/InterruptCard.d.ts +5 -6
- package/components/MessageCard/MessageCard.css +1 -2
- package/components/ModelPicker/ModelPicker.css +3 -1
- package/components/ModelPicker/ModelPicker.js +1 -0
- package/components/NavList/NavList.js +1 -0
- package/components/NotificationCenter/NotificationCenter.js +1 -0
- package/components/NumberInput/NumberInput.css +154 -0
- package/components/NumberInput/NumberInput.d.ts +44 -0
- package/components/NumberInput/NumberInput.js +147 -0
- package/components/Pagination/Pagination.js +1 -0
- package/components/PinInput/PinInput.css +92 -0
- package/components/PinInput/PinInput.d.ts +46 -0
- package/components/PinInput/PinInput.js +161 -0
- package/components/Popover/Popover.css +5 -6
- package/components/Popover/Popover.js +1 -0
- package/components/RadioButton/RadioButton.js +1 -0
- package/components/Reasoning/Reasoning.js +1 -0
- package/components/SegmentedControl/SegmentedControl.js +1 -0
- package/components/SelectionCard/SelectionCard.js +1 -0
- package/components/ShaderField/ShaderField.js +1 -0
- package/components/ShaderField/useShaderField.js +1 -0
- package/components/Slider/Slider.js +1 -0
- package/components/Sparkline/Sparkline.css +52 -0
- package/components/Sparkline/Sparkline.d.ts +49 -0
- package/components/Sparkline/Sparkline.js +86 -0
- package/components/Stepper/Stepper.css +207 -0
- package/components/Stepper/Stepper.d.ts +41 -0
- package/components/Stepper/Stepper.js +61 -0
- package/components/Tabs/Tabs.js +1 -0
- package/components/TagInput/TagInput.css +160 -0
- package/components/TagInput/TagInput.d.ts +43 -0
- package/components/TagInput/TagInput.js +137 -0
- package/components/Textarea/Textarea.js +1 -0
- package/components/TimePicker/TimePicker.css +230 -0
- package/components/TimePicker/TimePicker.d.ts +46 -0
- package/components/TimePicker/TimePicker.js +246 -0
- package/components/Toast/Toast.js +1 -0
- package/components/ToggleGroup/ToggleGroup.js +1 -0
- package/components/ToggleSwitch/ToggleSwitch.js +1 -0
- package/components/ToolCall/ToolCall.css +7 -11
- package/components/ToolCall/ToolCall.js +1 -0
- package/components/Tooltip/Tooltip.js +1 -0
- package/components/TreeView/TreeView.css +119 -0
- package/components/TreeView/TreeView.d.ts +45 -0
- package/components/TreeView/TreeView.js +232 -0
- package/components/registry.d.ts +22 -6
- package/components/registry.js +3 -1
- package/components/registry.json +206 -20
- package/components/registry.json.d.ts +206 -20
- package/components/registry.json.js +2 -2
- package/index.d.ts +11 -2
- package/index.js +21 -1
- package/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
PIN INPUT COMPONENT
|
|
3
|
+
Segmented one-time-code input
|
|
4
|
+
============================================ */
|
|
5
|
+
|
|
6
|
+
/* Base — the Field wrapper carries the root class */
|
|
7
|
+
.ds-pininput {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: var(--gap-sm); /* 8px */
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* ============================================
|
|
14
|
+
CELLS ROW
|
|
15
|
+
============================================ */
|
|
16
|
+
|
|
17
|
+
.ds-pininput__cells {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: var(--gap-sm); /* 8px */
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* ============================================
|
|
23
|
+
CELL
|
|
24
|
+
============================================ */
|
|
25
|
+
|
|
26
|
+
.ds-pininput__cell {
|
|
27
|
+
/* Fixed control geometry, like the ToggleSwitch track */
|
|
28
|
+
width: 44px;
|
|
29
|
+
height: 52px;
|
|
30
|
+
padding: 0;
|
|
31
|
+
text-align: center;
|
|
32
|
+
|
|
33
|
+
border: var(--border-xs) solid var(--color-input-border-primary);
|
|
34
|
+
border-radius: var(--radius-md); /* 12px */
|
|
35
|
+
background-color: var(--color-input-bg-primary);
|
|
36
|
+
|
|
37
|
+
/* Typography */
|
|
38
|
+
font-family: var(--font-paragraph-em-family);
|
|
39
|
+
font-size: var(--font-paragraph-em-size);
|
|
40
|
+
font-weight: var(--font-paragraph-em-weight);
|
|
41
|
+
line-height: var(--font-paragraph-em-line-height);
|
|
42
|
+
letter-spacing: var(--font-paragraph-em-letter-spacing);
|
|
43
|
+
color: var(--color-input-text-primary);
|
|
44
|
+
|
|
45
|
+
/* Transitions */
|
|
46
|
+
transition: border-color var(--motion-duration-base) var(--motion-ease-standard), background-color var(--motion-duration-base) var(--motion-ease-standard);
|
|
47
|
+
outline: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ============================================
|
|
51
|
+
STATES
|
|
52
|
+
============================================ */
|
|
53
|
+
|
|
54
|
+
.ds-pininput__cell:hover:not(:focus):not(:disabled) {
|
|
55
|
+
border-color: var(--color-input-border-hover);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.ds-pininput__cell:focus {
|
|
59
|
+
border-color: var(--color-input-border-selected);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.ds-pininput__cell:focus-visible {
|
|
63
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
64
|
+
outline-offset: 2px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ============================================
|
|
68
|
+
ERROR STATE
|
|
69
|
+
============================================ */
|
|
70
|
+
|
|
71
|
+
.ds-pininput--error .ds-pininput__cell {
|
|
72
|
+
border-color: var(--color-status-error-border);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.ds-pininput--error .ds-pininput__cell:focus {
|
|
76
|
+
border-color: var(--color-status-error-border);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ============================================
|
|
80
|
+
DISABLED STATE
|
|
81
|
+
============================================ */
|
|
82
|
+
|
|
83
|
+
.ds-pininput--disabled .ds-pininput__cells {
|
|
84
|
+
opacity: 0.4;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ds-pininput--disabled .ds-pininput__cell {
|
|
88
|
+
background-color: var(--color-input-bg-disabled);
|
|
89
|
+
border-color: var(--color-input-border-disabled);
|
|
90
|
+
color: var(--color-input-text-disabled);
|
|
91
|
+
cursor: not-allowed;
|
|
92
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by PinInput itself — everything else falls through to the group container. */
|
|
3
|
+
type PinInputOwnProps = {
|
|
4
|
+
/** Number of cells, one character each */
|
|
5
|
+
length?: number;
|
|
6
|
+
/** Current code (controlled) — character at index n renders in cell n */
|
|
7
|
+
value?: string;
|
|
8
|
+
/** Initial code (uncontrolled) */
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Convenience callback receiving the joined code whenever any cell changes.
|
|
12
|
+
* There is no single native `onChange` here: the control is a group of
|
|
13
|
+
* one-character inputs, so the value callback is the primary API.
|
|
14
|
+
*/
|
|
15
|
+
onValueChange?: (value: string) => void;
|
|
16
|
+
/** Fires once when every cell is filled, with the complete code */
|
|
17
|
+
onComplete?: (value: string) => void;
|
|
18
|
+
/** Render cells as password fields, hiding the entered characters */
|
|
19
|
+
mask?: boolean;
|
|
20
|
+
/** Accepted characters — `numeric` rejects non-digits and sets a numeric keyboard */
|
|
21
|
+
format?: 'numeric' | 'alphanumeric';
|
|
22
|
+
/** Label text rendered above the cells; also names the group for screen readers */
|
|
23
|
+
label?: string;
|
|
24
|
+
/** Helper or error message displayed below the cells */
|
|
25
|
+
helperText?: string;
|
|
26
|
+
/** Error state — recolours cell borders and the helper text */
|
|
27
|
+
error?: boolean;
|
|
28
|
+
/** Whether the whole control is disabled */
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
/** Additional CSS classes — applied to the wrapper, not the cells */
|
|
31
|
+
className?: string;
|
|
32
|
+
};
|
|
33
|
+
export interface PinInputProps extends PinInputOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof PinInputOwnProps> {
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Segmented one-time-code input: one cell per character, with auto-advance,
|
|
37
|
+
* Backspace retreat, arrow-key movement and paste distribution.
|
|
38
|
+
*
|
|
39
|
+
* The ref forwards to the FIRST cell's `<input>`, so `ref.current.focus()`
|
|
40
|
+
* focuses the start of the code. Unrecognised props spread onto the
|
|
41
|
+
* `role="group"` container `<div>` that wraps the cells — there is no single
|
|
42
|
+
* underlying input for them to land on — so `data-*` and `aria-*` attributes
|
|
43
|
+
* describe the group.
|
|
44
|
+
*/
|
|
45
|
+
export declare const PinInput: React.ForwardRefExoticComponent<PinInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Field } from "../Field/Field.js";
|
|
5
|
+
import "./PinInput.css";
|
|
6
|
+
const toChars = (value, length) => Array.from({ length }, (_, i) => value[i] ?? "");
|
|
7
|
+
const PinInput = React.forwardRef(
|
|
8
|
+
({
|
|
9
|
+
length = 6,
|
|
10
|
+
value,
|
|
11
|
+
defaultValue,
|
|
12
|
+
onValueChange,
|
|
13
|
+
onComplete,
|
|
14
|
+
mask = false,
|
|
15
|
+
format = "numeric",
|
|
16
|
+
label,
|
|
17
|
+
helperText,
|
|
18
|
+
error = false,
|
|
19
|
+
disabled = false,
|
|
20
|
+
className = "",
|
|
21
|
+
id,
|
|
22
|
+
...rest
|
|
23
|
+
}, ref) => {
|
|
24
|
+
const baseClass = "ds-pininput";
|
|
25
|
+
const generatedId = React.useId();
|
|
26
|
+
const controlId = id || generatedId;
|
|
27
|
+
const isControlled = value !== void 0;
|
|
28
|
+
const [internalChars, setInternalChars] = React.useState(
|
|
29
|
+
() => toChars(defaultValue ?? "", length)
|
|
30
|
+
);
|
|
31
|
+
const chars = isControlled ? toChars(value, length) : internalChars;
|
|
32
|
+
const cellRefs = React.useRef([]);
|
|
33
|
+
const setCellRef = (index) => (node) => {
|
|
34
|
+
cellRefs.current[index] = node;
|
|
35
|
+
if (index === 0) {
|
|
36
|
+
if (typeof ref === "function") ref(node);
|
|
37
|
+
else if (ref) ref.current = node;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const classes = [
|
|
41
|
+
baseClass,
|
|
42
|
+
error ? `${baseClass}--error` : "",
|
|
43
|
+
disabled ? `${baseClass}--disabled` : "",
|
|
44
|
+
className
|
|
45
|
+
].filter(Boolean).join(" ");
|
|
46
|
+
const sanitize = (raw) => raw.replace(format === "numeric" ? /[^0-9]/g : /[^a-zA-Z0-9]/g, "");
|
|
47
|
+
const focusCell = (index) => {
|
|
48
|
+
const clamped = Math.max(0, Math.min(index, length - 1));
|
|
49
|
+
const cell = cellRefs.current[clamped];
|
|
50
|
+
cell?.focus();
|
|
51
|
+
cell?.select();
|
|
52
|
+
};
|
|
53
|
+
const commit = (next) => {
|
|
54
|
+
if (!isControlled) setInternalChars(next);
|
|
55
|
+
onValueChange?.(next.join(""));
|
|
56
|
+
};
|
|
57
|
+
const joined = chars.join("");
|
|
58
|
+
const complete = joined.length === length;
|
|
59
|
+
const wasComplete = React.useRef(complete);
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
if (complete && !wasComplete.current) onComplete?.(joined);
|
|
62
|
+
wasComplete.current = complete;
|
|
63
|
+
}, [complete, joined, onComplete]);
|
|
64
|
+
const handleCellChange = (index) => (e) => {
|
|
65
|
+
const clean = sanitize(e.target.value);
|
|
66
|
+
if (e.target.value && !clean) return;
|
|
67
|
+
const next = [...chars];
|
|
68
|
+
if (clean.length <= 1) {
|
|
69
|
+
next[index] = clean;
|
|
70
|
+
commit(next);
|
|
71
|
+
if (clean) focusCell(index + 1);
|
|
72
|
+
} else {
|
|
73
|
+
let cursor = index;
|
|
74
|
+
for (const ch of clean) {
|
|
75
|
+
if (cursor >= length) break;
|
|
76
|
+
next[cursor] = ch;
|
|
77
|
+
cursor += 1;
|
|
78
|
+
}
|
|
79
|
+
commit(next);
|
|
80
|
+
focusCell(cursor);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const handleCellKeyDown = (index) => (e) => {
|
|
84
|
+
if (e.key === "Backspace") {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
const next = [...chars];
|
|
87
|
+
if (next[index]) {
|
|
88
|
+
next[index] = "";
|
|
89
|
+
commit(next);
|
|
90
|
+
focusCell(index - 1);
|
|
91
|
+
} else if (index > 0) {
|
|
92
|
+
next[index - 1] = "";
|
|
93
|
+
commit(next);
|
|
94
|
+
focusCell(index - 1);
|
|
95
|
+
}
|
|
96
|
+
} else if (e.key === "ArrowLeft") {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
focusCell(index - 1);
|
|
99
|
+
} else if (e.key === "ArrowRight") {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
focusCell(index + 1);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const handlePaste = (e) => {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
const clean = sanitize(e.clipboardData.getData("text")).slice(0, length);
|
|
107
|
+
if (!clean) return;
|
|
108
|
+
commit(toChars(clean, length));
|
|
109
|
+
focusCell(clean.length);
|
|
110
|
+
};
|
|
111
|
+
const describedBy = helperText ? `${controlId}-helper` : rest["aria-describedby"];
|
|
112
|
+
const characterNoun = format === "numeric" ? "Digit" : "Character";
|
|
113
|
+
return /* @__PURE__ */ jsx(
|
|
114
|
+
Field,
|
|
115
|
+
{
|
|
116
|
+
className: classes,
|
|
117
|
+
label,
|
|
118
|
+
helperText,
|
|
119
|
+
error,
|
|
120
|
+
disabled,
|
|
121
|
+
id: controlId,
|
|
122
|
+
children: /* @__PURE__ */ jsx(
|
|
123
|
+
"div",
|
|
124
|
+
{
|
|
125
|
+
...rest,
|
|
126
|
+
role: "group",
|
|
127
|
+
"aria-labelledby": label ? `${controlId}-label` : void 0,
|
|
128
|
+
"aria-label": !label ? rest["aria-label"] : void 0,
|
|
129
|
+
"aria-describedby": describedBy,
|
|
130
|
+
className: `${baseClass}__cells`,
|
|
131
|
+
children: chars.map((char, index) => /* @__PURE__ */ jsx(
|
|
132
|
+
"input",
|
|
133
|
+
{
|
|
134
|
+
ref: setCellRef(index),
|
|
135
|
+
className: `${baseClass}__cell`,
|
|
136
|
+
type: mask ? "password" : "text",
|
|
137
|
+
inputMode: format === "numeric" ? "numeric" : "text",
|
|
138
|
+
autoComplete: index === 0 ? "one-time-code" : "off",
|
|
139
|
+
maxLength: 1,
|
|
140
|
+
value: char,
|
|
141
|
+
disabled,
|
|
142
|
+
id: index === 0 ? controlId : void 0,
|
|
143
|
+
"aria-label": `${characterNoun} ${index + 1} of ${length}`,
|
|
144
|
+
"aria-invalid": error || void 0,
|
|
145
|
+
onChange: handleCellChange(index),
|
|
146
|
+
onKeyDown: handleCellKeyDown(index),
|
|
147
|
+
onPaste: handlePaste,
|
|
148
|
+
onFocus: (e) => e.target.select()
|
|
149
|
+
},
|
|
150
|
+
index
|
|
151
|
+
))
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
PinInput.displayName = "PinInput";
|
|
159
|
+
export {
|
|
160
|
+
PinInput
|
|
161
|
+
};
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
padding: var(--padding-md); /* 16px */
|
|
29
29
|
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
30
30
|
border-radius: var(--radius-sm); /* 8px */
|
|
31
|
-
background
|
|
31
|
+
/* Page background, not container: the container fill is semi-transparent
|
|
32
|
+
in dark mode, and a floating panel must not show text through itself */
|
|
33
|
+
background-color: var(--color-bg-page-primary);
|
|
32
34
|
box-shadow: var(--shadow-floating);
|
|
33
35
|
|
|
34
36
|
/* Hidden by default */
|
|
@@ -88,12 +90,9 @@
|
|
|
88
90
|
SIZE — COMPACT
|
|
89
91
|
============================================ */
|
|
90
92
|
|
|
93
|
+
/* The size class rides on the panel element itself, so this is a plain
|
|
94
|
+
compound selector — a descendant form would never match */
|
|
91
95
|
.ds-popover--compact {
|
|
92
96
|
padding: var(--padding-sm); /* 8px */
|
|
93
97
|
min-width: 160px;
|
|
94
98
|
}
|
|
95
|
-
|
|
96
|
-
.ds-popover--compact .ds-popover__panel {
|
|
97
|
-
padding: var(--padding-sm);
|
|
98
|
-
min-width: 160px;
|
|
99
|
-
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
SPARKLINE COMPONENT
|
|
3
|
+
Inline trend line for stats and table cells.
|
|
4
|
+
Colour flows through `color` — the line, fill
|
|
5
|
+
and dot all draw with currentColor, so a tone
|
|
6
|
+
is one custom property swap.
|
|
7
|
+
============================================ */
|
|
8
|
+
|
|
9
|
+
/* Base */
|
|
10
|
+
|
|
11
|
+
.ds-sparkline {
|
|
12
|
+
display: inline-block;
|
|
13
|
+
vertical-align: middle;
|
|
14
|
+
flex-shrink: 0;
|
|
15
|
+
color: var(--color-action-primary-bg);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Parts */
|
|
19
|
+
|
|
20
|
+
.ds-sparkline__line {
|
|
21
|
+
fill: none;
|
|
22
|
+
stroke: currentColor;
|
|
23
|
+
stroke-linecap: round;
|
|
24
|
+
stroke-linejoin: round;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ds-sparkline__fill {
|
|
28
|
+
fill: currentColor;
|
|
29
|
+
opacity: 0.15;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.ds-sparkline__dot {
|
|
33
|
+
fill: currentColor;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Tones */
|
|
37
|
+
|
|
38
|
+
.ds-sparkline--accent {
|
|
39
|
+
color: var(--color-action-primary-bg);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.ds-sparkline--positive {
|
|
43
|
+
color: var(--color-status-positive-text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ds-sparkline--negative {
|
|
47
|
+
color: var(--color-status-error-text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.ds-sparkline--neutral {
|
|
51
|
+
color: var(--color-text-secondary);
|
|
52
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by Sparkline itself — everything else falls through to the SVG node. */
|
|
3
|
+
type SparklineOwnProps = {
|
|
4
|
+
/** The series to plot, in order. Values are normalised into the viewBox. */
|
|
5
|
+
data: number[];
|
|
6
|
+
/** Rendering treatment — a bare line, or a line with a soft fill underneath. */
|
|
7
|
+
variant?: 'line' | 'area';
|
|
8
|
+
/**
|
|
9
|
+
* Colour role for the line and end dot. `accent` follows the chart palette's
|
|
10
|
+
* lead colour; `positive`/`negative` map to the status text tokens; `neutral`
|
|
11
|
+
* recedes to secondary text.
|
|
12
|
+
*/
|
|
13
|
+
tone?: 'accent' | 'positive' | 'negative' | 'neutral';
|
|
14
|
+
/** Marks the final point with a small dot. */
|
|
15
|
+
showDot?: boolean;
|
|
16
|
+
/** Line thickness in viewBox units — an SVG geometry attribute, not a token. */
|
|
17
|
+
strokeWidth?: number;
|
|
18
|
+
/**
|
|
19
|
+
* ViewBox width. Also the default rendered width; the SVG scales to its
|
|
20
|
+
* container when sized externally via CSS. Intentionally shadows the native
|
|
21
|
+
* SVG `width` attribute, which it sets.
|
|
22
|
+
*/
|
|
23
|
+
width?: number;
|
|
24
|
+
/**
|
|
25
|
+
* ViewBox height. Also the default rendered height; the SVG scales to its
|
|
26
|
+
* container when sized externally via CSS. Intentionally shadows the native
|
|
27
|
+
* SVG `height` attribute, which it sets.
|
|
28
|
+
*/
|
|
29
|
+
height?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Accessible description of the trend, e.g. "Revenue, trending up". When
|
|
32
|
+
* omitted the SVG is decorative and hidden from assistive technology.
|
|
33
|
+
*/
|
|
34
|
+
label?: string;
|
|
35
|
+
/** Additional CSS classes */
|
|
36
|
+
className?: string;
|
|
37
|
+
};
|
|
38
|
+
export interface SparklineProps extends SparklineOwnProps, Omit<React.ComponentPropsWithoutRef<'svg'>, keyof SparklineOwnProps | 'children'> {
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Sparkline — an inline trend line for stats and table cells, drawn without
|
|
42
|
+
* axes or chrome. Pure SVG computed from props: no charting library, no hooks,
|
|
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.
|
|
47
|
+
*/
|
|
48
|
+
export declare const Sparkline: React.ForwardRefExoticComponent<SparklineProps & React.RefAttributes<SVGSVGElement>>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import "./Sparkline.css";
|
|
4
|
+
const round = (n) => Math.round(n * 100) / 100;
|
|
5
|
+
const Sparkline = React.forwardRef(
|
|
6
|
+
({
|
|
7
|
+
data,
|
|
8
|
+
variant = "line",
|
|
9
|
+
tone = "accent",
|
|
10
|
+
showDot = true,
|
|
11
|
+
strokeWidth = 2,
|
|
12
|
+
width = 120,
|
|
13
|
+
height = 32,
|
|
14
|
+
label,
|
|
15
|
+
className = "",
|
|
16
|
+
...rest
|
|
17
|
+
}, ref) => {
|
|
18
|
+
const baseClass = "ds-sparkline";
|
|
19
|
+
const classes = [
|
|
20
|
+
baseClass,
|
|
21
|
+
`${baseClass}--${variant}`,
|
|
22
|
+
`${baseClass}--${tone}`,
|
|
23
|
+
className
|
|
24
|
+
].filter(Boolean).join(" ");
|
|
25
|
+
const dotRadius = strokeWidth * 1.5;
|
|
26
|
+
const pad = dotRadius + strokeWidth / 2;
|
|
27
|
+
const innerWidth = Math.max(width - pad * 2, 0);
|
|
28
|
+
const innerHeight = Math.max(height - pad * 2, 0);
|
|
29
|
+
const min = Math.min(...data);
|
|
30
|
+
const max = Math.max(...data);
|
|
31
|
+
const range = max - min;
|
|
32
|
+
const points = data.map((value, i) => ({
|
|
33
|
+
x: round(
|
|
34
|
+
data.length < 2 ? pad + innerWidth : pad + i / (data.length - 1) * innerWidth
|
|
35
|
+
),
|
|
36
|
+
// A flat series (range 0) draws a horizontal midline instead of dividing by zero.
|
|
37
|
+
y: round(
|
|
38
|
+
range === 0 ? pad + innerHeight / 2 : pad + (1 - (value - min) / range) * innerHeight
|
|
39
|
+
)
|
|
40
|
+
}));
|
|
41
|
+
const lastPoint = points.length > 0 ? points[points.length - 1] : void 0;
|
|
42
|
+
const hasLine = points.length >= 2;
|
|
43
|
+
const linePath = hasLine ? `M ${points.map((p) => `${p.x} ${p.y}`).join(" L ")}` : void 0;
|
|
44
|
+
const baseline = round(height - pad);
|
|
45
|
+
const areaPath = hasLine && variant === "area" ? `${linePath} L ${points[points.length - 1].x} ${baseline} L ${points[0].x} ${baseline} Z` : void 0;
|
|
46
|
+
return /* @__PURE__ */ jsxs(
|
|
47
|
+
"svg",
|
|
48
|
+
{
|
|
49
|
+
...rest,
|
|
50
|
+
ref,
|
|
51
|
+
className: classes,
|
|
52
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
53
|
+
width,
|
|
54
|
+
height,
|
|
55
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
56
|
+
role: label ? "img" : void 0,
|
|
57
|
+
"aria-label": label,
|
|
58
|
+
"aria-hidden": label ? void 0 : true,
|
|
59
|
+
children: [
|
|
60
|
+
areaPath && /* @__PURE__ */ jsx("path", { className: `${baseClass}__fill`, d: areaPath }),
|
|
61
|
+
linePath && /* @__PURE__ */ jsx(
|
|
62
|
+
"path",
|
|
63
|
+
{
|
|
64
|
+
className: `${baseClass}__line`,
|
|
65
|
+
d: linePath,
|
|
66
|
+
strokeWidth
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
showDot && lastPoint && /* @__PURE__ */ jsx(
|
|
70
|
+
"circle",
|
|
71
|
+
{
|
|
72
|
+
className: `${baseClass}__dot`,
|
|
73
|
+
cx: lastPoint.x,
|
|
74
|
+
cy: lastPoint.y,
|
|
75
|
+
r: dotRadius
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
Sparkline.displayName = "Sparkline";
|
|
84
|
+
export {
|
|
85
|
+
Sparkline
|
|
86
|
+
};
|