@sixthshift/design-system 0.7.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 +5 -4
- package/dist/components/Badge/Badge.d.ts +17 -35
- package/dist/components/Badge/Badge.d.ts.map +1 -1
- package/dist/components/Badge/Badge.js +19 -22
- package/dist/components/Badge/Badge.js.map +1 -1
- package/dist/components/Badge/index.d.ts +1 -1
- package/dist/components/Badge/index.d.ts.map +1 -1
- package/dist/components/Badge/index.js +1 -1
- package/dist/components/Badge/index.js.map +1 -1
- package/dist/components/Button/Button.d.ts +23 -41
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/Button.js +28 -35
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Button/index.d.ts +1 -1
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/Button/index.js +1 -1
- package/dist/components/Button/index.js.map +1 -1
- package/dist/components/Card/Card.d.ts +2 -2
- package/dist/components/Card/Card.js +1 -1
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts +20 -16
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -1
- package/dist/components/CheckboxGroup/CheckboxGroup.js +12 -10
- package/dist/components/CheckboxGroup/CheckboxGroup.js.map +1 -1
- package/dist/components/Code/Workspace/Toolbar.d.ts +2 -1
- package/dist/components/Code/Workspace/Toolbar.d.ts.map +1 -1
- package/dist/components/Code/Workspace/Toolbar.js.map +1 -1
- package/dist/components/DataTable/DataTable.d.ts +89 -0
- package/dist/components/DataTable/DataTable.d.ts.map +1 -0
- package/dist/components/DataTable/DataTable.js +107 -0
- package/dist/components/DataTable/DataTable.js.map +1 -0
- package/dist/components/DataTable/index.d.ts +2 -0
- package/dist/components/DataTable/index.d.ts.map +1 -0
- package/dist/components/DataTable/index.js +2 -0
- package/dist/components/DataTable/index.js.map +1 -0
- package/dist/components/FileDropzone/FileDropzone.d.ts +87 -0
- package/dist/components/FileDropzone/FileDropzone.d.ts.map +1 -0
- package/dist/components/FileDropzone/FileDropzone.js +146 -0
- package/dist/components/FileDropzone/FileDropzone.js.map +1 -0
- package/dist/components/FileDropzone/index.d.ts +2 -0
- package/dist/components/FileDropzone/index.d.ts.map +1 -0
- package/dist/components/FileDropzone/index.js +2 -0
- package/dist/components/FileDropzone/index.js.map +1 -0
- package/dist/components/FormField/FormField.d.ts +3 -1
- package/dist/components/FormField/FormField.d.ts.map +1 -1
- package/dist/components/FormField/FormField.js.map +1 -1
- package/dist/components/ImageGallery/ImageGallery.d.ts +93 -0
- package/dist/components/ImageGallery/ImageGallery.d.ts.map +1 -0
- package/dist/components/ImageGallery/ImageGallery.js +121 -0
- package/dist/components/ImageGallery/ImageGallery.js.map +1 -0
- package/dist/components/ImageGallery/index.d.ts +2 -0
- package/dist/components/ImageGallery/index.d.ts.map +1 -0
- package/dist/components/ImageGallery/index.js +2 -0
- package/dist/components/ImageGallery/index.js.map +1 -0
- package/dist/components/Input/Input.d.ts +3 -3
- package/dist/components/Input/Input.js +3 -3
- package/dist/components/Markdown/Markdown.js +1 -1
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/NumberStepper/NumberStepper.d.ts +78 -0
- package/dist/components/NumberStepper/NumberStepper.d.ts.map +1 -0
- package/dist/components/NumberStepper/NumberStepper.js +129 -0
- package/dist/components/NumberStepper/NumberStepper.js.map +1 -0
- package/dist/components/NumberStepper/index.d.ts +2 -0
- package/dist/components/NumberStepper/index.d.ts.map +1 -0
- package/dist/components/NumberStepper/index.js +2 -0
- package/dist/components/NumberStepper/index.js.map +1 -0
- package/dist/components/ProgressBar/ProgressBar.d.ts +11 -0
- package/dist/components/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/components/ProgressBar/ProgressBar.js +6 -2
- package/dist/components/ProgressBar/ProgressBar.js.map +1 -1
- package/dist/components/ProgressBar/index.d.ts +1 -1
- package/dist/components/ProgressBar/index.d.ts.map +1 -1
- package/dist/components/ProgressBar/index.js.map +1 -1
- package/dist/components/RadioButtonGroup/RadioButtonGroup.d.ts +19 -16
- package/dist/components/RadioButtonGroup/RadioButtonGroup.d.ts.map +1 -1
- package/dist/components/RadioButtonGroup/RadioButtonGroup.js +13 -12
- package/dist/components/RadioButtonGroup/RadioButtonGroup.js.map +1 -1
- package/dist/components/SearchInput/SearchInput.d.ts +3 -2
- package/dist/components/SearchInput/SearchInput.d.ts.map +1 -1
- package/dist/components/SearchInput/SearchInput.js +13 -8
- package/dist/components/SearchInput/SearchInput.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.d.ts +8 -4
- package/dist/components/Sparkline/Sparkline.d.ts.map +1 -1
- package/dist/components/Sparkline/Sparkline.js +1 -1
- package/dist/components/Sparkline/Sparkline.js.map +1 -1
- package/dist/components/Sparkline/index.d.ts +1 -1
- package/dist/components/Sparkline/index.d.ts.map +1 -1
- package/dist/components/Sparkline/index.js.map +1 -1
- package/dist/components/Steps/Steps.d.ts +65 -0
- package/dist/components/Steps/Steps.d.ts.map +1 -0
- package/dist/components/Steps/Steps.js +62 -0
- package/dist/components/Steps/Steps.js.map +1 -0
- package/dist/components/Steps/index.d.ts +2 -0
- package/dist/components/Steps/index.d.ts.map +1 -0
- package/dist/components/Steps/index.js +2 -0
- package/dist/components/Steps/index.js.map +1 -0
- package/dist/components/TagChip/TagChip.d.ts +0 -13
- package/dist/components/TagChip/TagChip.d.ts.map +1 -1
- package/dist/components/TagChip/TagChip.js +10 -4
- package/dist/components/TagChip/TagChip.js.map +1 -1
- package/dist/components/TextInline/TextInline.d.ts +2 -2
- package/dist/components/Toast/Toast.js +1 -1
- package/dist/components/Toast/Toast.js.map +1 -1
- package/dist/components/Toggle/Toggle.d.ts +29 -18
- package/dist/components/Toggle/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle/Toggle.js +48 -11
- package/dist/components/Toggle/Toggle.js.map +1 -1
- package/dist/components/Toggle/index.d.ts +1 -1
- package/dist/components/Toggle/index.d.ts.map +1 -1
- package/dist/components/Toggle/index.js +1 -1
- package/dist/components/Toggle/index.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +4 -4
- package/dist/components/ToggleGroup/ToggleGroup.js +4 -4
- package/dist/components/ToggleGroup/ToggleGroupItem.d.ts +1 -1
- package/dist/components/ToggleGroup/ToggleGroupItem.d.ts.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroupItem.js +37 -7
- package/dist/components/ToggleGroup/ToggleGroupItem.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroupMultiple.d.ts +3 -3
- package/dist/components/ToggleGroup/ToggleGroupSingle.d.ts +3 -3
- package/dist/components/ToggleGroup/index.d.ts +1 -1
- package/dist/components/ToggleGroup/index.d.ts.map +1 -1
- package/dist/components/ToggleGroup/toggleGroup.types.d.ts +18 -16
- package/dist/components/ToggleGroup/toggleGroup.types.d.ts.map +1 -1
- package/dist/typography/Code.js +1 -1
- package/dist/typography/Code.js.map +1 -1
- package/package.json +22 -1
- package/src/components/Badge/Badge.tsx +38 -45
- package/src/components/Badge/badge.recipe.css +20 -101
- package/src/components/Badge/index.ts +0 -1
- package/src/components/Button/Button.tsx +57 -51
- package/src/components/Button/button.recipe.css +39 -122
- package/src/components/Button/index.ts +0 -1
- package/src/components/Card/Card.tsx +1 -1
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +17 -15
- package/src/components/Code/Workspace/Toolbar.tsx +2 -2
- package/src/components/DataTable/DataTable.tsx +261 -0
- package/src/components/DataTable/data-table.recipe.css +28 -0
- package/src/components/DataTable/index.ts +10 -0
- package/src/components/FileDropzone/FileDropzone.tsx +239 -0
- package/src/components/FileDropzone/file-dropzone.recipe.css +38 -0
- package/src/components/FileDropzone/index.ts +1 -0
- package/src/components/FormField/FormField.tsx +3 -2
- package/src/components/ImageGallery/ImageGallery.tsx +285 -0
- package/src/components/ImageGallery/image-gallery.recipe.css +50 -0
- package/src/components/ImageGallery/index.ts +7 -0
- package/src/components/Input/Input.tsx +3 -3
- package/src/components/Markdown/Markdown.tsx +1 -1
- package/src/components/Message/message.recipe.css +13 -28
- package/src/components/NumberStepper/NumberStepper.tsx +227 -0
- package/src/components/NumberStepper/index.ts +1 -0
- package/src/components/NumberStepper/number-stepper.recipe.css +31 -0
- package/src/components/ProgressBar/ProgressBar.tsx +16 -4
- package/src/components/ProgressBar/index.ts +1 -1
- package/src/components/ProgressBar/progress-bar.recipe.css +29 -0
- package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +18 -17
- package/src/components/SearchInput/SearchInput.tsx +34 -20
- package/src/components/SearchInput/search-input.recipe.css +14 -6
- package/src/components/Sparkline/Sparkline.tsx +9 -4
- package/src/components/Sparkline/index.ts +1 -1
- package/src/components/Steps/Steps.tsx +149 -0
- package/src/components/Steps/index.ts +1 -0
- package/src/components/Steps/steps.recipe.css +51 -0
- package/src/components/TagChip/TagChip.tsx +17 -8
- package/src/components/TagChip/tag-chip.recipe.css +12 -12
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/components/Toggle/Toggle.tsx +67 -21
- package/src/components/Toggle/index.ts +9 -1
- package/src/components/Toggle/toggle.recipe.css +45 -73
- package/src/components/ToggleGroup/ToggleGroup.tsx +4 -4
- package/src/components/ToggleGroup/ToggleGroupItem.tsx +46 -11
- package/src/components/ToggleGroup/index.ts +9 -1
- package/src/components/ToggleGroup/toggle-group.recipe.css +62 -0
- package/src/components/ToggleGroup/toggleGroup.types.ts +18 -16
- package/src/theme/linen/theme.css +21 -0
- package/src/theming/intents.css +112 -0
- package/src/theming/tailwind.css +22 -1
- package/src/typography/Code.tsx +1 -1
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Message recipe — the component-token layer for Message.
|
|
3
3
|
*
|
|
4
|
-
* Message has one colour-bearing
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* here concerns it.
|
|
4
|
+
* Message has one colour-bearing axis, `intent`, and one treatment: the
|
|
5
|
+
* family's tint, bordered. So the whole recipe is a single cell reading the
|
|
6
|
+
* intent slots from src/theming/intents.css — an intent a consumer adds there
|
|
7
|
+
* reaches Message with no change here. `size` is pure geometry (padding, gap,
|
|
8
|
+
* font-size) and stays in Message.tsx's `cva` call; nothing here concerns it.
|
|
10
9
|
*
|
|
11
10
|
* The grammar is `--message-{context}`, context in {bg, fg, border}. Intent
|
|
12
11
|
* never appears in a token name — see button.css's header for why: it would
|
|
@@ -18,30 +17,16 @@
|
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
19
|
@layer components {
|
|
21
|
-
/* The floor. Applies when no cell below matches — an intent a consumer has
|
|
22
|
-
added but not yet styled still renders as a plain, legible message rather
|
|
23
|
-
than something broken. */
|
|
24
20
|
.message {
|
|
25
|
-
--message-bg: var(--bg
|
|
26
|
-
--message-fg: var(--fg
|
|
27
|
-
--message-border: var(--border
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.message[data-intent="success"] {
|
|
31
|
-
--message-bg: var(--bg-success-subtle);
|
|
32
|
-
--message-fg: var(--fg-on-success-subtle);
|
|
33
|
-
--message-border: var(--border-success);
|
|
21
|
+
--message-bg: var(--intent-tint-bg);
|
|
22
|
+
--message-fg: var(--intent-tint-fg);
|
|
23
|
+
--message-border: var(--intent-border);
|
|
34
24
|
}
|
|
35
25
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.message[data-intent="danger"] {
|
|
43
|
-
--message-bg: var(--bg-danger-subtle);
|
|
44
|
-
--message-fg: var(--fg-on-danger-subtle);
|
|
45
|
-
--message-border: var(--border-danger);
|
|
26
|
+
/* A neutral message sits on the resting surface, not the grey tint: it is a
|
|
27
|
+
plain bordered note, and a grey wash reads as disabled. The one place a
|
|
28
|
+
recipe overrides a slot for a single intent. */
|
|
29
|
+
.message[data-intent="neutral"] {
|
|
30
|
+
--message-bg: var(--bg-normal);
|
|
46
31
|
}
|
|
47
32
|
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
4
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { Minus, Plus } from "lucide-react";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Geometry only. Every colour reads a `--number-stepper-*` component token
|
|
11
|
+
* whose value is decided by src/components/NumberStepper/number-stepper.recipe.css.
|
|
12
|
+
*
|
|
13
|
+
* `size` tracks Input's and Button's heights (`sm` 32px, `md` 36px, `lg`
|
|
14
|
+
* 40px) so a stepper sits on the same baseline as the fields beside it.
|
|
15
|
+
*/
|
|
16
|
+
const numberStepperVariants = cva(
|
|
17
|
+
// One literal, deliberately — see Button.tsx for why a `+` concatenation is a trap.
|
|
18
|
+
"number-stepper border-(color:--number-stepper-border) focus-within:ring-(color:--number-stepper-ring) inline-flex w-fit items-stretch overflow-hidden rounded-md border bg-(--number-stepper-bg) text-(--number-stepper-fg) shadow-xs transition-colors focus-within:ring-2 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50",
|
|
19
|
+
{
|
|
20
|
+
variants: {
|
|
21
|
+
size: {
|
|
22
|
+
sm: "h-8 text-xs [&_svg]:size-3.5",
|
|
23
|
+
md: "h-9 text-sm [&_svg]:size-4",
|
|
24
|
+
lg: "h-10 text-sm [&_svg]:size-4",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
size: "md",
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/** Button width per size — square against the field's height. */
|
|
34
|
+
const buttonGeometry: Record<string, string> = {
|
|
35
|
+
sm: "w-8",
|
|
36
|
+
md: "w-9",
|
|
37
|
+
lg: "w-10",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Input width per size — room for three digits before it grows. */
|
|
41
|
+
const inputGeometry: Record<string, string> = {
|
|
42
|
+
sm: "w-10",
|
|
43
|
+
md: "w-12",
|
|
44
|
+
lg: "w-14",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type NumberStepperSize = NonNullable<VariantProps<typeof numberStepperVariants>["size"]>;
|
|
48
|
+
|
|
49
|
+
export type NumberStepperProps = Omit<
|
|
50
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
51
|
+
"value" | "defaultValue" | "onChange" | "size" | "min" | "max" | "step" | "type"
|
|
52
|
+
> &
|
|
53
|
+
VariantProps<typeof numberStepperVariants> & {
|
|
54
|
+
/** Controlled value. */
|
|
55
|
+
value?: number;
|
|
56
|
+
/** Initial value in uncontrolled mode. Defaults to `min`, or `0` without one. */
|
|
57
|
+
defaultValue?: number;
|
|
58
|
+
/** Called with the new, already-clamped value. */
|
|
59
|
+
onValueChange?: (value: number) => void;
|
|
60
|
+
/** Lowest reachable value. Defaults to `0` — quantities are the common case. */
|
|
61
|
+
min?: number;
|
|
62
|
+
/** Highest reachable value. Unbounded when omitted. */
|
|
63
|
+
max?: number;
|
|
64
|
+
/** Increment for the buttons and arrow keys. `PageUp`/`PageDown` move ten of them. */
|
|
65
|
+
step?: number;
|
|
66
|
+
/** Accessible name for the decrement button. */
|
|
67
|
+
decrementLabel?: string;
|
|
68
|
+
/** Accessible name for the increment button. */
|
|
69
|
+
incrementLabel?: string;
|
|
70
|
+
/** Extra classes for the inner `<input>`; `className` styles the surrounding group. */
|
|
71
|
+
inputClassName?: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function clamp(value: number, min: number, max: number | undefined): number {
|
|
75
|
+
const floored = Math.max(min, value);
|
|
76
|
+
return max === undefined ? floored : Math.min(max, floored);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A whole-number field with decrement and increment buttons — a cart
|
|
81
|
+
* quantity, a guest count, a per-day capacity.
|
|
82
|
+
*
|
|
83
|
+
* Implements the WAI-ARIA spinbutton pattern on the `<input>` itself:
|
|
84
|
+
* `role="spinbutton"` with `aria-valuenow`/`min`/`max`, `ArrowUp`/`ArrowDown`
|
|
85
|
+
* step, `PageUp`/`PageDown` step by ten, and `Home`/`End` jump to the bounds
|
|
86
|
+
* that exist. The two buttons are pointer affordances and stay out of the tab
|
|
87
|
+
* order, so the field is one tab stop — the same reason RadioButtonGroup is.
|
|
88
|
+
*
|
|
89
|
+
* Typing is free-form while focused and committed on blur or `Enter`: the
|
|
90
|
+
* draft is parsed, rounded to the nearest `step` from `min`, and clamped.
|
|
91
|
+
* Anything unparseable reverts to the last committed value rather than
|
|
92
|
+
* reporting `NaN`. Every value `onValueChange` receives is in range.
|
|
93
|
+
*
|
|
94
|
+
* Controlled or uncontrolled through the `value`/`defaultValue`/
|
|
95
|
+
* `onValueChange` triad. `name` submits the committed value with a form, since
|
|
96
|
+
* the input is a real `<input>`. The ref and every unlisted prop land on that
|
|
97
|
+
* input — so `FormField`'s `id`/`aria-describedby` wiring reaches the control
|
|
98
|
+
* — while `className` styles the surrounding group.
|
|
99
|
+
*/
|
|
100
|
+
export const NumberStepper = React.forwardRef<HTMLInputElement, NumberStepperProps>(
|
|
101
|
+
(
|
|
102
|
+
{
|
|
103
|
+
className,
|
|
104
|
+
inputClassName,
|
|
105
|
+
size = "md",
|
|
106
|
+
value: controlledValue,
|
|
107
|
+
defaultValue,
|
|
108
|
+
onValueChange,
|
|
109
|
+
min = 0,
|
|
110
|
+
max,
|
|
111
|
+
step = 1,
|
|
112
|
+
disabled = false,
|
|
113
|
+
readOnly = false,
|
|
114
|
+
decrementLabel = "Decrease",
|
|
115
|
+
incrementLabel = "Increase",
|
|
116
|
+
onKeyDown,
|
|
117
|
+
onBlur,
|
|
118
|
+
onFocus,
|
|
119
|
+
...props
|
|
120
|
+
},
|
|
121
|
+
ref
|
|
122
|
+
) => {
|
|
123
|
+
const [value, setValue] = useControllableState({
|
|
124
|
+
value: controlledValue,
|
|
125
|
+
defaultValue: clamp(defaultValue ?? min, min, max),
|
|
126
|
+
onChange: onValueChange,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// The text being typed, or `null` when the field shows the committed value.
|
|
130
|
+
const [draft, setDraft] = React.useState<string | null>(null);
|
|
131
|
+
|
|
132
|
+
const commit = (next: number) => {
|
|
133
|
+
const snapped = min + Math.round((next - min) / step) * step;
|
|
134
|
+
const clamped = clamp(snapped, min, max);
|
|
135
|
+
if (clamped !== value) setValue(clamped);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const commitDraft = () => {
|
|
139
|
+
if (draft === null) return;
|
|
140
|
+
const parsed = Number.parseFloat(draft);
|
|
141
|
+
setDraft(null);
|
|
142
|
+
if (Number.isFinite(parsed)) commit(parsed);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const interactive = !disabled && !readOnly;
|
|
146
|
+
const atMin = value <= min;
|
|
147
|
+
const atMax = max !== undefined && value >= max;
|
|
148
|
+
|
|
149
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
150
|
+
onKeyDown?.(event);
|
|
151
|
+
if (event.defaultPrevented || !interactive) return;
|
|
152
|
+
|
|
153
|
+
const moves: Record<string, (() => number) | undefined> = {
|
|
154
|
+
ArrowUp: () => value + step,
|
|
155
|
+
ArrowDown: () => value - step,
|
|
156
|
+
PageUp: () => value + step * 10,
|
|
157
|
+
PageDown: () => value - step * 10,
|
|
158
|
+
Home: () => min,
|
|
159
|
+
End: max === undefined ? undefined : () => max,
|
|
160
|
+
};
|
|
161
|
+
const move = moves[event.key];
|
|
162
|
+
|
|
163
|
+
if (event.key === "Enter") {
|
|
164
|
+
commitDraft();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!move) return;
|
|
168
|
+
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
setDraft(null);
|
|
171
|
+
commit(move());
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
|
175
|
+
commitDraft();
|
|
176
|
+
onBlur?.(event);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const stepBy = (direction: 1 | -1) => {
|
|
180
|
+
setDraft(null);
|
|
181
|
+
commit(value + step * direction);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const buttonClass = cn(
|
|
185
|
+
"number-stepper-button flex shrink-0 cursor-pointer items-center justify-center text-(--number-stepper-button-fg) transition-colors enabled:active:bg-(--number-stepper-button-bg-pressed) enabled:hover:bg-(--number-stepper-button-bg-hovered) disabled:cursor-not-allowed disabled:text-(--number-stepper-button-fg-disabled)",
|
|
186
|
+
buttonGeometry[size ?? "md"]
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
// biome-ignore lint/a11y/useSemanticElements: a <fieldset> brings a border, min-width and legend semantics this inline control does not want; role="group" only ties the buttons to the field
|
|
191
|
+
<div role="group" data-disabled={disabled} className={numberStepperVariants({ size, className })}>
|
|
192
|
+
<button type="button" tabIndex={-1} aria-label={decrementLabel} disabled={!interactive || atMin} onClick={() => stepBy(-1)} className={buttonClass}>
|
|
193
|
+
<Minus aria-hidden="true" />
|
|
194
|
+
</button>
|
|
195
|
+
<input
|
|
196
|
+
ref={ref}
|
|
197
|
+
type="text"
|
|
198
|
+
inputMode="numeric"
|
|
199
|
+
role="spinbutton"
|
|
200
|
+
autoComplete="off"
|
|
201
|
+
aria-valuenow={value}
|
|
202
|
+
aria-valuemin={min}
|
|
203
|
+
aria-valuemax={max}
|
|
204
|
+
disabled={disabled}
|
|
205
|
+
readOnly={readOnly}
|
|
206
|
+
value={draft ?? String(value)}
|
|
207
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
208
|
+
onKeyDown={handleKeyDown}
|
|
209
|
+
onBlur={handleBlur}
|
|
210
|
+
onFocus={onFocus}
|
|
211
|
+
className={cn(
|
|
212
|
+
"border-(color:--number-stepper-divider-border) min-w-0 border-x bg-transparent text-center text-[length:inherit] tabular-nums outline-hidden disabled:cursor-not-allowed",
|
|
213
|
+
inputGeometry[size ?? "md"],
|
|
214
|
+
inputClassName
|
|
215
|
+
)}
|
|
216
|
+
{...props}
|
|
217
|
+
/>
|
|
218
|
+
<button type="button" tabIndex={-1} aria-label={incrementLabel} disabled={!interactive || atMax} onClick={() => stepBy(1)} className={buttonClass}>
|
|
219
|
+
<Plus aria-hidden="true" />
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
NumberStepper.displayName = "NumberStepper";
|
|
226
|
+
|
|
227
|
+
export { numberStepperVariants };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NumberStepper, type NumberStepperProps, type NumberStepperSize, numberStepperVariants } from "./NumberStepper";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NumberStepper recipe — the component-token layer for NumberStepper.
|
|
3
|
+
*
|
|
4
|
+
* No variant/intent axis: a stepper is a form field, and like Input every
|
|
5
|
+
* colour is a fixed semantic token. The field surface deliberately mirrors
|
|
6
|
+
* input.css's values (bg-normal, border-normal, focus-ring) so a stepper next
|
|
7
|
+
* to a text input reads as the same kind of control — but through its own
|
|
8
|
+
* tokens, so a consumer can re-point one without the other.
|
|
9
|
+
*
|
|
10
|
+
* The grammar is `--number-stepper-[{part}-]{context}[-{state}]`. Parts are
|
|
11
|
+
* `button` (the two step buttons) and `divider` (the rules either side of the
|
|
12
|
+
* value). `--number-stepper-button-fg-disabled` is the colour a button takes
|
|
13
|
+
* at a bound; the whole group dims via opacity only when the field itself is
|
|
14
|
+
* disabled, the same way Input does.
|
|
15
|
+
*
|
|
16
|
+
* `@layer components` is load-bearing, not tidiness — see button.css.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
@layer components {
|
|
20
|
+
.number-stepper {
|
|
21
|
+
--number-stepper-bg: var(--bg-normal);
|
|
22
|
+
--number-stepper-fg: var(--fg-normal);
|
|
23
|
+
--number-stepper-border: var(--border-normal);
|
|
24
|
+
--number-stepper-ring: var(--focus-ring);
|
|
25
|
+
--number-stepper-divider-border: var(--border-normal);
|
|
26
|
+
--number-stepper-button-fg: var(--fg-normal);
|
|
27
|
+
--number-stepper-button-fg-disabled: var(--fg-normal-disabled);
|
|
28
|
+
--number-stepper-button-bg-hovered: var(--bg-normal-hovered);
|
|
29
|
+
--number-stepper-button-bg-pressed: var(--bg-normal-pressed);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -2,10 +2,18 @@ import { Caption } from "@sixthshift/design-system/caption";
|
|
|
2
2
|
import { cn } from "@sixthshift/design-system/utils";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
|
+
export type ProgressBarIntentName = "neutral" | "brand" | "danger" | "success" | "warning";
|
|
6
|
+
/** Widened, like every intent prop: a consumer adds an intent in CSS, not in a release. */
|
|
7
|
+
export type ProgressBarIntent = ProgressBarIntentName | (string & {});
|
|
8
|
+
|
|
5
9
|
export type ProgressBarProps = {
|
|
6
10
|
completed: number;
|
|
7
11
|
total: number;
|
|
8
12
|
showFraction?: boolean;
|
|
13
|
+
/** The fill's colour family. Defaults to `brand`. */
|
|
14
|
+
intent?: ProgressBarIntent | undefined;
|
|
15
|
+
/** Name of a brand the theme defines, rendered as `data-brand`. Omitted, the bar follows the nearest `data-brand` ancestor. */
|
|
16
|
+
brand?: string | undefined;
|
|
9
17
|
/** Accessible name for the progress bar. */
|
|
10
18
|
label?: string;
|
|
11
19
|
className?: string;
|
|
@@ -24,9 +32,13 @@ export type ProgressBarProps = {
|
|
|
24
32
|
* invalid CSS or an impossible reported value — though the fraction text
|
|
25
33
|
* still shows the raw, unclamped numbers. When `total` is not positive the
|
|
26
34
|
* bar renders as indeterminate: no `aria-valuenow`/`aria-valuemax`, 0% fill.
|
|
35
|
+
*
|
|
36
|
+
* `intent` colours the fill and defaults to `brand`: progress is not an
|
|
37
|
+
* outcome, so a bar at 40% is not `success`. Pass `success` when completion
|
|
38
|
+
* really is the good news, or `danger` for a quota running out.
|
|
27
39
|
*/
|
|
28
40
|
export const ProgressBar = React.forwardRef<HTMLDivElement, ProgressBarProps>(
|
|
29
|
-
({ completed, total, showFraction = true, label = "Progress", className }, ref) => {
|
|
41
|
+
({ completed, total, intent = "brand", brand, showFraction = true, label = "Progress", className }, ref) => {
|
|
30
42
|
// A caller can hand us anything; the bar must not render an out-of-range
|
|
31
43
|
// width (a negative `width` is invalid CSS) or report impossible progress.
|
|
32
44
|
const isMeasurable = total > 0;
|
|
@@ -34,7 +46,7 @@ export const ProgressBar = React.forwardRef<HTMLDivElement, ProgressBarProps>(
|
|
|
34
46
|
const percentage = isMeasurable ? (clamped / total) * 100 : 0;
|
|
35
47
|
|
|
36
48
|
return (
|
|
37
|
-
<div ref={ref} className={cn("flex items-center gap-3", className)}>
|
|
49
|
+
<div ref={ref} data-intent={intent} data-brand={brand} className={cn("progress-bar flex items-center gap-3", className)}>
|
|
38
50
|
<div
|
|
39
51
|
role="progressbar"
|
|
40
52
|
aria-label={label}
|
|
@@ -44,9 +56,9 @@ export const ProgressBar = React.forwardRef<HTMLDivElement, ProgressBarProps>(
|
|
|
44
56
|
// total there is no meaningful progress to report.
|
|
45
57
|
aria-valuenow={isMeasurable ? clamped : undefined}
|
|
46
58
|
aria-valuetext={isMeasurable ? `${clamped} of ${total}` : undefined}
|
|
47
|
-
className="h-2 flex-1 overflow-hidden rounded-full bg-bg
|
|
59
|
+
className="h-2 flex-1 overflow-hidden rounded-full bg-(--progress-bar-track-bg)"
|
|
48
60
|
>
|
|
49
|
-
<div className="h-full rounded-full bg-
|
|
61
|
+
<div className="h-full rounded-full bg-(--progress-bar-fill-bg) transition-all" style={{ width: `${percentage}%` }} />
|
|
50
62
|
</div>
|
|
51
63
|
{showFraction && (
|
|
52
64
|
<Caption className="shrink-0">
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ProgressBar, type ProgressBarProps } from "./ProgressBar";
|
|
1
|
+
export { ProgressBar, type ProgressBarIntent, type ProgressBarIntentName, type ProgressBarProps } from "./ProgressBar";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressBar recipe — the component-token layer for ProgressBar.
|
|
3
|
+
*
|
|
4
|
+
* Two parts: the track, which is the same grey in every intent, and the fill,
|
|
5
|
+
* which paints with the intent slots from src/theming/intents.css. The fill
|
|
6
|
+
* reads `--intent-fg`, the family's colour on a resting surface, rather than
|
|
7
|
+
* the solid slot: `--intent-solid-bg` for `neutral` is `bg-strong`, which is
|
|
8
|
+
* the track itself, so a neutral bar would vanish. `neutral` is the one
|
|
9
|
+
* override: its `--intent-fg` is `fg-normal`, which barely separates from a
|
|
10
|
+
* `bg-strong` track in either mode, so it takes the mid-grey `fg-subtle`,
|
|
11
|
+
* which separates from both the track and the page around it. (`fg-on-strong`
|
|
12
|
+
* contrasts with the track but matches the page, and reads as a gap.)
|
|
13
|
+
*
|
|
14
|
+
* The grammar is `--progress-bar-{part}-{context}`. Both tokens live on the
|
|
15
|
+
* root, which carries `data-intent`, and the fill reads them by inheritance.
|
|
16
|
+
*
|
|
17
|
+
* `@layer components` is load-bearing, not tidiness — see button.recipe.css.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
@layer components {
|
|
21
|
+
.progress-bar {
|
|
22
|
+
--progress-bar-track-bg: var(--bg-strong);
|
|
23
|
+
--progress-bar-fill-bg: var(--intent-fg);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.progress-bar[data-intent="neutral"] {
|
|
27
|
+
--progress-bar-fill-bg: var(--fg-subtle);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -27,10 +27,11 @@ export type RadioButtonGroupProps = Omit<React.HTMLAttributes<HTMLDivElement>, "
|
|
|
27
27
|
onValueChange?: (value: string) => void;
|
|
28
28
|
/** Layout orientation */
|
|
29
29
|
orientation?: "vertical" | "horizontal";
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
/**
|
|
31
|
+
* How the options render: `control` (the default) is one `RadioButton` per option;
|
|
32
|
+
* `segmented` and `separate` are a row or column of buttons, joined or apart.
|
|
33
|
+
*/
|
|
34
|
+
appearance?: "control" | "segmented" | "separate";
|
|
34
35
|
/** Input name for form submission */
|
|
35
36
|
name?: string;
|
|
36
37
|
/** Disable all options */
|
|
@@ -44,21 +45,22 @@ export type RadioButtonGroupProps = Omit<React.HTMLAttributes<HTMLDivElement>, "
|
|
|
44
45
|
* uncontrolled `string` selection, via the `value`/`defaultValue`/
|
|
45
46
|
* `onValueChange` triad (`useControllableState`).
|
|
46
47
|
*
|
|
47
|
-
* `
|
|
48
|
-
* `
|
|
49
|
-
* `RadioButton` — a row/column of plain buttons with
|
|
50
|
-
* contiguous buttons touch and share borders (`
|
|
51
|
-
*
|
|
52
|
-
*
|
|
48
|
+
* `appearance="control"` (the default) renders one `RadioButton` per option.
|
|
49
|
+
* `appearance="segmented"` or `"separate"` is a separate rendering path — not
|
|
50
|
+
* a re-skinned `RadioButton` — a row/column of plain buttons with
|
|
51
|
+
* `role="radio"`; contiguous buttons touch and share borders (`segmented`) or
|
|
52
|
+
* sit apart with their own rounded corners (`separate`). One prop rather than
|
|
53
|
+
* a `variant` + `appearance` pair, so "appearance without buttons" — which
|
|
54
|
+
* used to mean nothing — cannot be written.
|
|
53
55
|
*
|
|
54
56
|
* Implements the WAI-ARIA radiogroup keyboard pattern: the group is one tab
|
|
55
57
|
* stop, landing on the checked option (or the first enabled option if none
|
|
56
58
|
* is checked), and arrow/Home/End keys move focus *and* selection between
|
|
57
59
|
* enabled options, wrapping at the ends.
|
|
58
60
|
*
|
|
59
|
-
* Form submission only happens when `name` is set.
|
|
61
|
+
* Form submission only happens when `name` is set. With `control`
|
|
60
62
|
* that means each underlying `RadioButton` gets `name`, so one hidden
|
|
61
|
-
* native radio input is rendered per option;
|
|
63
|
+
* native radio input is rendered per option; with the button appearances the
|
|
62
64
|
* group itself renders a single hidden `<input type="hidden">` carrying the
|
|
63
65
|
* selected value.
|
|
64
66
|
*
|
|
@@ -73,8 +75,7 @@ const RadioButtonGroup = React.forwardRef<HTMLDivElement, RadioButtonGroupProps>
|
|
|
73
75
|
options,
|
|
74
76
|
onValueChange,
|
|
75
77
|
orientation = "vertical",
|
|
76
|
-
|
|
77
|
-
appearance = "segmented",
|
|
78
|
+
appearance = "control",
|
|
78
79
|
name,
|
|
79
80
|
disabled,
|
|
80
81
|
onBlur,
|
|
@@ -111,8 +112,8 @@ const RadioButtonGroup = React.forwardRef<HTMLDivElement, RadioButtonGroupProps>
|
|
|
111
112
|
const handleOptionSelect = (optionValue: string) => {
|
|
112
113
|
// Re-selecting the current option reports nothing, as a native radio does
|
|
113
114
|
// not. The guard lives here as well as in `RadioButton` because the button
|
|
114
|
-
//
|
|
115
|
-
// the two
|
|
115
|
+
// appearances are a separate rendering path that never goes through it —
|
|
116
|
+
// and the two paths must not disagree about this. `ToggleGroup` in
|
|
116
117
|
// `"single"` mode already works this way.
|
|
117
118
|
if (optionValue === value) return;
|
|
118
119
|
setValue(optionValue);
|
|
@@ -139,7 +140,7 @@ const RadioButtonGroup = React.forwardRef<HTMLDivElement, RadioButtonGroupProps>
|
|
|
139
140
|
const hasCheckedOption = options.some((option) => option.value === value);
|
|
140
141
|
const tabStopValue = hasCheckedOption ? value : firstEnabledValue;
|
|
141
142
|
|
|
142
|
-
if (
|
|
143
|
+
if (appearance === "segmented" || appearance === "separate") {
|
|
143
144
|
const isVertical = orientation === "vertical";
|
|
144
145
|
const isSegmented = appearance === "segmented";
|
|
145
146
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Input } from "@sixthshift/design-system/input";
|
|
4
3
|
import { cn } from "@sixthshift/design-system/utils";
|
|
5
4
|
import { Search, X } from "lucide-react";
|
|
6
5
|
import * as React from "react";
|
|
@@ -14,12 +13,19 @@ export type SearchInputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>,
|
|
|
14
13
|
clearLabel?: string;
|
|
15
14
|
};
|
|
16
15
|
|
|
16
|
+
/** The icon slots, duplicated from Input's. */
|
|
17
|
+
const iconSlotStyles = `absolute top-1/2 -translate-y-1/2 text-(--search-input-icon-fg)
|
|
18
|
+
[&_svg]:h-4 [&_svg]:w-4
|
|
19
|
+
[&_button]:flex [&_button]:cursor-pointer [&_button]:appearance-none
|
|
20
|
+
[&_button]:border-0 [&_button]:bg-transparent`;
|
|
21
|
+
|
|
17
22
|
/**
|
|
18
23
|
* Text input specialised for search: a search icon fixed on the left and,
|
|
19
24
|
* once there is a value, a clear button on the right.
|
|
20
25
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
26
|
+
* Its own field, not a wrapped `Input`: the markup and classes are duplicated
|
|
27
|
+
* from Input on purpose, so importing SearchInput pulls in nothing else and
|
|
28
|
+
* its colours are its own `--search-input-*` tokens. Always controlled — `value` is required and
|
|
23
29
|
* there is no `defaultValue`/uncontrolled mode. `onValueChange` receives the
|
|
24
30
|
* string value directly; the native `onChange` keeps its event signature and
|
|
25
31
|
* fires too if passed. Clearing calls `onClear` if provided, otherwise calls
|
|
@@ -36,25 +42,33 @@ const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
|
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
return (
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
<div className={cn("search-input relative h-9 w-full text-sm", className)}>
|
|
46
|
+
<span className={cn(iconSlotStyles, "left-3")}>
|
|
47
|
+
<Search />
|
|
48
|
+
</span>
|
|
49
|
+
<input
|
|
50
|
+
ref={ref}
|
|
51
|
+
type="text"
|
|
52
|
+
value={value}
|
|
53
|
+
onChange={(e) => {
|
|
54
|
+
onValueChange(e.target.value);
|
|
55
|
+
onChange?.(e);
|
|
56
|
+
}}
|
|
57
|
+
className={cn(
|
|
58
|
+
"border-(color:--search-input-border) focus-visible:ring-(color:--search-input-ring) flex h-full w-full rounded-md border bg-(--search-input-bg) py-1 text-[length:inherit] shadow-xs transition-colors placeholder:text-(--search-input-placeholder-fg) focus-visible:outline-hidden focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
59
|
+
"pl-9",
|
|
60
|
+
value ? "pr-9" : "pr-3"
|
|
61
|
+
)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
{value && (
|
|
65
|
+
<span className={cn(iconSlotStyles, "right-3")}>
|
|
66
|
+
<button type="button" onClick={handleClear} aria-label={clearLabel} className="rounded-sm hover:bg-(--search-input-clear-bg-hovered)">
|
|
51
67
|
<X />
|
|
52
68
|
</button>
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
{...props}
|
|
57
|
-
/>
|
|
69
|
+
</span>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
58
72
|
);
|
|
59
73
|
}
|
|
60
74
|
);
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SearchInput recipe — the component-token layer for SearchInput.
|
|
3
3
|
*
|
|
4
|
-
* SearchInput
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* SearchInput draws its own field rather than wrapping Input, so it owns every
|
|
5
|
+
* colour it paints. The values match Input's today, deliberately duplicated:
|
|
6
|
+
* re-pointing `--input-*` does not reach a search field, and re-pointing these
|
|
7
|
+
* does not reach a plain input.
|
|
8
|
+
*
|
|
9
|
+
* The grammar is `--search-input-[{part}-]{context}[-{state}]`, parts being
|
|
10
|
+
* the placeholder, the icons, and the clear button.
|
|
11
|
+
*
|
|
12
|
+
* `@layer components` is load-bearing, not tidiness — see button.recipe.css.
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
@layer components {
|
|
13
16
|
.search-input {
|
|
17
|
+
--search-input-bg: var(--bg-normal);
|
|
18
|
+
--search-input-border: var(--border-normal);
|
|
19
|
+
--search-input-ring: var(--focus-ring);
|
|
20
|
+
--search-input-placeholder-fg: var(--fg-subtle);
|
|
21
|
+
--search-input-icon-fg: var(--fg-subtle);
|
|
14
22
|
--search-input-clear-bg-hovered: var(--bg-subtle);
|
|
15
23
|
}
|
|
16
24
|
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import type { Interpolation } from "@sixthshift/design-system/line-chart";
|
|
2
1
|
import { cn } from "@sixthshift/design-system/utils";
|
|
3
2
|
import * as React from "react";
|
|
4
3
|
|
|
4
|
+
/**
|
|
5
|
+
* How the line joins its points. The same four as LineChart's — duplicated, not
|
|
6
|
+
* imported, so a sparkline does not pull in the chart.
|
|
7
|
+
*/
|
|
8
|
+
export type SparklineInterpolation = "linear" | "monotone" | "stepBefore" | "stepAfter";
|
|
9
|
+
|
|
5
10
|
export type SparklineProps = React.HTMLAttributes<HTMLSpanElement> & {
|
|
6
11
|
data: number[];
|
|
7
12
|
width?: number;
|
|
8
13
|
height?: number;
|
|
9
14
|
color?: string;
|
|
10
15
|
fillArea?: boolean;
|
|
11
|
-
interpolation?:
|
|
16
|
+
interpolation?: SparklineInterpolation;
|
|
12
17
|
strokeWidth?: number;
|
|
13
18
|
};
|
|
14
19
|
|
|
15
|
-
function buildPath(points: { x: number; y: number }[], interpolation:
|
|
20
|
+
function buildPath(points: { x: number; y: number }[], interpolation: SparklineInterpolation): string {
|
|
16
21
|
if (points.length === 0) return "";
|
|
17
22
|
const parts = [`M ${points[0]!.x} ${points[0]!.y}`];
|
|
18
23
|
|
|
@@ -58,7 +63,7 @@ function buildPath(points: { x: number; y: number }[], interpolation: Interpolat
|
|
|
58
63
|
*
|
|
59
64
|
* Requires at least 2 data points; with fewer it renders nothing (`null`).
|
|
60
65
|
* `color` is a single CSS color string (default `var(--fg-brand)`), and
|
|
61
|
-
* `interpolation`
|
|
66
|
+
* `interpolation` takes the same four values as `LineChart`'s. The
|
|
62
67
|
* accessible name is a static `aria-label="Trend line"` — it does not
|
|
63
68
|
* describe the actual values.
|
|
64
69
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Sparkline, type SparklineProps } from "./Sparkline";
|
|
1
|
+
export { Sparkline, type SparklineInterpolation, type SparklineProps } from "./Sparkline";
|