@sixthshift/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 +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/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/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 +1 -1
- 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/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 +2 -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/FormField/FormField.tsx +3 -2
- 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/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/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 +17 -1
- package/src/typography/Code.tsx +1 -1
|
@@ -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";
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Badge } from "@sixthshift/design-system/badge";
|
|
4
3
|
import { cn } from "@sixthshift/design-system/utils";
|
|
5
4
|
import { X } from "lucide-react";
|
|
6
5
|
import * as React from "react";
|
|
@@ -24,20 +23,30 @@ export type TagChipProps = {
|
|
|
24
23
|
*
|
|
25
24
|
* Two disjoint modes, chosen by whether `onRemove` is passed: navigable (no
|
|
26
25
|
* `onRemove`; the caller wraps it in a link) or removable (`onRemove` renders
|
|
27
|
-
* an × — used by `TagInput` and edit surfaces).
|
|
28
|
-
*
|
|
26
|
+
* an × — used by `TagInput` and edit surfaces). It looks like an outline
|
|
27
|
+
* Badge but is not one: the shape is duplicated here and every colour is its
|
|
28
|
+
* own `--tag-chip-*` token, so importing TagChip pulls in nothing else. A tag
|
|
29
|
+
* is metadata, not a status, so it takes no intent.
|
|
29
30
|
*/
|
|
31
|
+
/** Duplicated from Badge's sizes, which TagChip used to borrow. */
|
|
32
|
+
const sizeStyles = {
|
|
33
|
+
sm: "px-1.5 py-0 text-[10px]",
|
|
34
|
+
md: "px-2.5 py-0.5 text-xs",
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
30
37
|
export const TagChip = React.forwardRef<HTMLSpanElement, TagChipProps>(({ tag, onRemove, size = "sm", className }, ref) => {
|
|
31
38
|
const sep = tag.indexOf(":");
|
|
32
39
|
const namespace = sep > 0 ? tag.slice(0, sep) : null;
|
|
33
40
|
const value = sep > 0 ? tag.slice(sep + 1) : tag;
|
|
34
41
|
|
|
35
42
|
return (
|
|
36
|
-
<
|
|
43
|
+
<span
|
|
37
44
|
ref={ref}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
className={cn(
|
|
46
|
+
"tag-chip border-(color:--tag-chip-border) focus:ring-(color:--tag-chip-ring) inline-flex items-center gap-1 rounded-md border bg-(--tag-chip-bg) font-normal text-(--tag-chip-fg) transition-colors focus:outline-hidden focus:ring-2 focus:ring-offset-2",
|
|
47
|
+
sizeStyles[size],
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
41
50
|
>
|
|
42
51
|
{namespace && <span className="text-(--tag-chip-namespace-fg)">{namespace}:</span>}
|
|
43
52
|
<span>{value}</span>
|
|
@@ -51,7 +60,7 @@ export const TagChip = React.forwardRef<HTMLSpanElement, TagChipProps>(({ tag, o
|
|
|
51
60
|
<X className="h-3 w-3" />
|
|
52
61
|
</button>
|
|
53
62
|
)}
|
|
54
|
-
</
|
|
63
|
+
</span>
|
|
55
64
|
);
|
|
56
65
|
});
|
|
57
66
|
TagChip.displayName = "TagChip";
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TagChip recipe — the component-token layer for TagChip.
|
|
3
3
|
*
|
|
4
|
-
* TagChip
|
|
5
|
-
* it
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* contributes itself are the muted namespace prefix and the remove ("×")
|
|
9
|
-
* button, which is why this file covers just those two.
|
|
4
|
+
* TagChip draws its own chip rather than rendering a Badge, so it owns every
|
|
5
|
+
* colour it paints: the surface (a resting background and a neutral border,
|
|
6
|
+
* like an outline badge), the quieter text a tag gets as metadata, the
|
|
7
|
+
* namespace prefix and the remove ("×") button.
|
|
10
8
|
*
|
|
11
|
-
* The grammar is `--tag-chip-{part}-{context}[-{state}]`.
|
|
12
|
-
* on `.tag-chip
|
|
13
|
-
*
|
|
14
|
-
* remove `<button>` need no class hooks of their own for that.
|
|
9
|
+
* The grammar is `--tag-chip-[{part}-]{context}[-{state}]`. Every token lives
|
|
10
|
+
* on `.tag-chip`, the root, and the namespace `<span>` and remove `<button>`
|
|
11
|
+
* read theirs by ordinary inheritance.
|
|
15
12
|
*
|
|
16
|
-
* `@layer components` is load-bearing, not tidiness — see button.css
|
|
17
|
-
* cascade reasoning.
|
|
13
|
+
* `@layer components` is load-bearing, not tidiness — see button.recipe.css.
|
|
18
14
|
*/
|
|
19
15
|
|
|
20
16
|
@layer components {
|
|
21
17
|
.tag-chip {
|
|
18
|
+
--tag-chip-bg: var(--bg-normal);
|
|
19
|
+
--tag-chip-border: var(--border-normal);
|
|
20
|
+
--tag-chip-ring: var(--border-brand);
|
|
21
|
+
--tag-chip-fg: var(--fg-subtle);
|
|
22
22
|
--tag-chip-namespace-fg: var(--fg-subtle);
|
|
23
23
|
--tag-chip-remove-fg: var(--fg-subtle);
|
|
24
24
|
--tag-chip-remove-fg-hovered: var(--fg-normal);
|
|
@@ -132,7 +132,7 @@ export const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
|
|
|
132
132
|
{children}
|
|
133
133
|
{action && (
|
|
134
134
|
<div className="mt-2">
|
|
135
|
-
<Button
|
|
135
|
+
<Button inline size="sm" className="font-medium" onClick={onAction}>
|
|
136
136
|
{action}
|
|
137
137
|
</Button>
|
|
138
138
|
</div>
|
|
@@ -2,19 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
4
4
|
import { cn } from "@sixthshift/design-system/utils";
|
|
5
|
-
import type
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
6
|
import * as React from "react";
|
|
7
|
-
import { type ButtonIntent, type ButtonVariant, buttonRecipe, type buttonVariants } from "../Button/Button";
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Geometry and behaviour only; every colour reads a `--toggle-*` token decided
|
|
10
|
+
* by src/components/Toggle/toggle.recipe.css.
|
|
11
|
+
*
|
|
12
|
+
* Duplicated from Button on purpose, not shared: a toggle looks like a button
|
|
13
|
+
* but is its own component, so importing Toggle must not pull in Button. The
|
|
14
|
+
* two start identical and are free to drift.
|
|
13
15
|
*/
|
|
16
|
+
const toggleVariants = cva(
|
|
17
|
+
// One literal, deliberately — see Button.tsx for the sorter trap.
|
|
18
|
+
"toggle border-(color:--toggle-border) focus-visible:ring-(color:--toggle-ring) inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md bg-(--toggle-bg) font-medium text-(--toggle-fg) text-sm transition-colors hover:bg-(--toggle-bg-hovered) focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 active:bg-(--toggle-bg-pressed) disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
19
|
+
{
|
|
20
|
+
variants: {
|
|
21
|
+
size: {
|
|
22
|
+
xs: "h-7 rounded-md px-2 text-xs",
|
|
23
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
24
|
+
md: "h-9 px-4 py-2",
|
|
25
|
+
lg: "h-10 rounded-md px-8",
|
|
26
|
+
xl: "h-12 rounded-md px-10 text-base",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
size: "md",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/** The non-colour half of `variant` — border width and elevation. */
|
|
36
|
+
const variantStructure: Record<string, string> = {
|
|
37
|
+
solid: "shadow",
|
|
38
|
+
outline: "border shadow-xs",
|
|
39
|
+
ghost: "",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Squares the box at whatever size is set, and drops the horizontal padding. */
|
|
43
|
+
const iconOnlyGeometry: Record<string, string> = {
|
|
44
|
+
xs: "w-7 px-0",
|
|
45
|
+
sm: "w-8 px-0",
|
|
46
|
+
md: "w-9 px-0",
|
|
47
|
+
lg: "w-10 px-0",
|
|
48
|
+
xl: "w-12 px-0",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Widened deliberately, as on every component: a consumer adds a variant or intent in CSS. */
|
|
52
|
+
type Loose<T extends string> = T | (string & {});
|
|
53
|
+
|
|
54
|
+
export type ToggleVariantName = "solid" | "outline" | "ghost";
|
|
55
|
+
export type ToggleIntentName = "neutral" | "brand" | "danger" | "success" | "warning";
|
|
56
|
+
export type ToggleVariant = Loose<ToggleVariantName>;
|
|
57
|
+
export type ToggleIntent = Loose<ToggleIntentName>;
|
|
58
|
+
|
|
14
59
|
export type ToggleProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange" | "value"> &
|
|
15
|
-
VariantProps<typeof
|
|
16
|
-
variant?:
|
|
17
|
-
intent?:
|
|
60
|
+
VariantProps<typeof toggleVariants> & {
|
|
61
|
+
variant?: ToggleVariant | undefined;
|
|
62
|
+
intent?: ToggleIntent | undefined;
|
|
18
63
|
/** Name of a brand the theme defines, rendered as `data-brand`. Omitted, the toggle follows the nearest `data-brand` ancestor. */
|
|
19
64
|
brand?: string | undefined;
|
|
20
65
|
/** Square the toggle at its current size, for an icon with no label. */
|
|
@@ -33,12 +78,12 @@ export type ToggleProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "o
|
|
|
33
78
|
* for a set of mutually exclusive or multi-select options, use `ToggleGroup`
|
|
34
79
|
* instead.
|
|
35
80
|
*
|
|
36
|
-
*
|
|
37
|
-
* `
|
|
38
|
-
*
|
|
39
|
-
* pressed
|
|
40
|
-
* keyed off the `data-state="on" |
|
|
41
|
-
* alongside `aria-pressed`.
|
|
81
|
+
* Looks like a Button and takes the same `variant`/`intent`/`size` axes, plus
|
|
82
|
+
* `iconOnly` for a toolbar toggle whose whole content is an icon — but it is
|
|
83
|
+
* its own component, with its own classes and `--toggle-*` tokens, and
|
|
84
|
+
* imports nothing from Button. Every colour, pressed state included, lives in
|
|
85
|
+
* `src/components/Toggle/toggle.recipe.css`, keyed off the `data-state="on" |
|
|
86
|
+
* "off"` attribute this component renders alongside `aria-pressed`.
|
|
42
87
|
*
|
|
43
88
|
* Controlled via `pressed`/`defaultPressed`/`onPressedChange`
|
|
44
89
|
* (`useControllableState`).
|
|
@@ -47,8 +92,8 @@ const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
|
47
92
|
(
|
|
48
93
|
{
|
|
49
94
|
className,
|
|
50
|
-
variant,
|
|
51
|
-
intent,
|
|
95
|
+
variant = "solid",
|
|
96
|
+
intent = "neutral",
|
|
52
97
|
brand,
|
|
53
98
|
size,
|
|
54
99
|
iconOnly = false,
|
|
@@ -67,18 +112,19 @@ const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
|
67
112
|
onChange: onPressedChange,
|
|
68
113
|
});
|
|
69
114
|
|
|
70
|
-
const recipe = buttonRecipe({ variant, intent, brand, size, iconOnly });
|
|
71
|
-
|
|
72
115
|
return (
|
|
73
116
|
<button
|
|
74
117
|
type="button"
|
|
75
118
|
aria-pressed={pressed}
|
|
119
|
+
// What the recipe selects on; `data-brand` is omitted unless set.
|
|
76
120
|
data-state={pressed ? "on" : "off"}
|
|
121
|
+
data-variant={variant}
|
|
122
|
+
data-intent={intent}
|
|
123
|
+
data-brand={brand}
|
|
77
124
|
disabled={disabled}
|
|
78
125
|
ref={ref}
|
|
79
126
|
onClick={() => setPressed(!pressed)}
|
|
80
|
-
{
|
|
81
|
-
className={cn(recipe.className, className)}
|
|
127
|
+
className={cn(variantStructure[variant], toggleVariants({ size, className: cn(iconOnly ? iconOnlyGeometry[size ?? "md"] : undefined, className) }))}
|
|
82
128
|
{...props}
|
|
83
129
|
>
|
|
84
130
|
{children}
|
|
@@ -88,4 +134,4 @@ const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
|
88
134
|
);
|
|
89
135
|
Toggle.displayName = "Toggle";
|
|
90
136
|
|
|
91
|
-
export { Toggle };
|
|
137
|
+
export { Toggle, toggleVariants };
|
|
@@ -1,92 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Toggle recipe — the
|
|
2
|
+
* Toggle recipe — the component-token layer for Toggle.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Toggle's own `--toggle-*` tokens on its own `.toggle` hook. It looks like a
|
|
5
|
+
* Button, and these cells start as copies of button.recipe.css's, but it is a
|
|
6
|
+
* separate component: re-pointing `--button-*` does not reach a toggle, and
|
|
7
|
+
* re-pointing these does not reach a button. Duplicated on purpose, so each is
|
|
8
|
+
* free to change without dragging the other.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
* `[data-state="on"]
|
|
11
|
-
*
|
|
12
|
-
* `selected`, rather than a new attribute invented for this file. `state`
|
|
13
|
-
* never appears in a token *name* any more than `intent` does — see
|
|
14
|
-
* button.css's header for why: it selects, via the attribute, which existing
|
|
15
|
-
* `--button-bg`/`--button-bg-hovered` values apply.
|
|
10
|
+
* Every cell paints with the intent slots (src/theming/intents.css), so each
|
|
11
|
+
* variant works in every intent. The third selector is `[data-state="on"]`,
|
|
12
|
+
* the attribute Toggle renders alongside `aria-pressed`.
|
|
16
13
|
*
|
|
17
|
-
*
|
|
18
|
-
* press, so "on" carries no colour change.
|
|
19
|
-
*
|
|
20
|
-
* Every cell repeats its own resting value into `--button-bg-hovered`: the old
|
|
21
|
-
* classes paired `bg-*` with an identical `hover:bg-*`, i.e. a pressed toggle
|
|
22
|
-
* deliberately does not shift on hover. Restating it here (rather than relying
|
|
23
|
-
* on button.css's floor, which chains `-hovered` from `--button-bg`) keeps
|
|
24
|
-
* that "no shift" behaviour explicit and correct regardless of cascade order
|
|
25
|
-
* against the button.css cells these compound with.
|
|
14
|
+
* `@layer components` is load-bearing, not tidiness — see button.recipe.css.
|
|
26
15
|
*/
|
|
27
16
|
|
|
28
17
|
@layer components {
|
|
29
|
-
/*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
--button-bg: var(--bg-strong-pressed);
|
|
38
|
-
--button-bg-hovered: var(--bg-strong-pressed);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.btn[data-state="on"][data-variant="solid"][data-intent="danger"] {
|
|
42
|
-
--button-bg: var(--bg-danger-pressed);
|
|
43
|
-
--button-bg-hovered: var(--bg-danger-pressed);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.btn[data-state="on"][data-variant="solid"][data-intent="success"] {
|
|
47
|
-
--button-bg: var(--bg-success-pressed);
|
|
48
|
-
--button-bg-hovered: var(--bg-success-pressed);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.btn[data-state="on"][data-variant="solid"][data-intent="warning"] {
|
|
52
|
-
--button-bg: var(--bg-warning-pressed);
|
|
53
|
-
--button-bg-hovered: var(--bg-warning-pressed);
|
|
18
|
+
/* The floor — a plain, legible toggle when no cell below matches. */
|
|
19
|
+
.toggle {
|
|
20
|
+
--toggle-bg: transparent;
|
|
21
|
+
--toggle-bg-hovered: var(--toggle-bg); /* no shift on hover */
|
|
22
|
+
--toggle-bg-pressed: var(--toggle-bg-hovered);
|
|
23
|
+
--toggle-fg: var(--fg-normal);
|
|
24
|
+
--toggle-border: var(--border-normal);
|
|
25
|
+
--toggle-ring: var(--focus-ring);
|
|
54
26
|
}
|
|
55
27
|
|
|
56
|
-
/*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
--
|
|
61
|
-
--
|
|
62
|
-
--button-fg: var(--fg-normal);
|
|
28
|
+
/* solid — a filled surface, with text read on top of it. */
|
|
29
|
+
.toggle[data-variant="solid"] {
|
|
30
|
+
--toggle-bg: var(--intent-solid-bg);
|
|
31
|
+
--toggle-bg-hovered: var(--intent-solid-bg-hovered);
|
|
32
|
+
--toggle-bg-pressed: var(--intent-solid-bg-pressed);
|
|
33
|
+
--toggle-fg: var(--intent-solid-fg);
|
|
63
34
|
}
|
|
64
35
|
|
|
65
|
-
.
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
--
|
|
69
|
-
--
|
|
36
|
+
/* outline and ghost share the family's text colour and its tint wash. */
|
|
37
|
+
.toggle[data-variant="outline"],
|
|
38
|
+
.toggle[data-variant="ghost"] {
|
|
39
|
+
--toggle-bg-hovered: var(--intent-tint-bg-hovered);
|
|
40
|
+
--toggle-bg-pressed: var(--intent-tint-bg-pressed);
|
|
41
|
+
--toggle-fg: var(--intent-fg);
|
|
70
42
|
}
|
|
71
43
|
|
|
72
|
-
.
|
|
73
|
-
.
|
|
74
|
-
--
|
|
75
|
-
--
|
|
76
|
-
--button-fg: var(--fg-on-danger-subtle-pressed);
|
|
44
|
+
/* outline alone carries a resting surface and a border. */
|
|
45
|
+
.toggle[data-variant="outline"] {
|
|
46
|
+
--toggle-bg: var(--bg-normal);
|
|
47
|
+
--toggle-border: var(--intent-border);
|
|
77
48
|
}
|
|
78
49
|
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
--
|
|
50
|
+
/* On. Every cell repeats its resting value into `-hovered`: a pressed toggle
|
|
51
|
+
deliberately does not shift on hover. solid holds the fill's -pressed
|
|
52
|
+
step; outline and ghost hold the tint's, with its text partner. */
|
|
53
|
+
.toggle[data-state="on"][data-variant="solid"] {
|
|
54
|
+
--toggle-bg: var(--intent-solid-bg-pressed);
|
|
55
|
+
--toggle-bg-hovered: var(--intent-solid-bg-pressed);
|
|
84
56
|
}
|
|
85
57
|
|
|
86
|
-
.
|
|
87
|
-
.
|
|
88
|
-
--
|
|
89
|
-
--
|
|
90
|
-
--
|
|
58
|
+
.toggle[data-state="on"][data-variant="outline"],
|
|
59
|
+
.toggle[data-state="on"][data-variant="ghost"] {
|
|
60
|
+
--toggle-bg: var(--intent-tint-bg-pressed);
|
|
61
|
+
--toggle-bg-hovered: var(--intent-tint-bg-pressed);
|
|
62
|
+
--toggle-fg: var(--intent-tint-fg-pressed);
|
|
91
63
|
}
|
|
92
64
|
}
|
|
@@ -13,10 +13,10 @@ import type { ToggleGroupProps } from "./toggleGroup.types";
|
|
|
13
13
|
* `defaultValue`/`onValueChange` follow `type`: a string for single, a
|
|
14
14
|
* string array for multiple (`useControllableState`).
|
|
15
15
|
*
|
|
16
|
-
* Items
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Items look like Toggles but carry their own duplicated styling and
|
|
17
|
+
* `--toggle-group-item-*` tokens (toggle-group.recipe.css), and take
|
|
18
|
+
* the same `variant`/`intent`, with one difference: `size` excludes `"xl"`
|
|
19
|
+
* (too large for grouped toggles) — see
|
|
20
20
|
* `toggleGroup.types.ts`. `appearance` chooses `"segmented"` (items joined
|
|
21
21
|
* into one border) or `"separate"` (individually bordered, gapped) rendering.
|
|
22
22
|
*/
|
|
@@ -1,8 +1,44 @@
|
|
|
1
1
|
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
2
3
|
import * as React from "react";
|
|
3
|
-
import { buttonRecipe } from "../Button/Button";
|
|
4
4
|
import type { ToggleGroupBaseProps, ToggleGroupOption } from "./toggleGroup.types";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* An item's geometry. Duplicated from Button's on purpose — ToggleGroup is its
|
|
8
|
+
* own component and imports neither Button nor Toggle. Every colour reads a
|
|
9
|
+
* `--toggle-group-item-*` token from toggle-group.recipe.css.
|
|
10
|
+
*/
|
|
11
|
+
const itemVariants = cva(
|
|
12
|
+
// One literal, deliberately — see Button.tsx for the sorter trap.
|
|
13
|
+
"toggle-group-item border-(color:--toggle-group-item-border) focus-visible:ring-(color:--toggle-group-item-ring) inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md bg-(--toggle-group-item-bg) font-medium text-(--toggle-group-item-fg) text-sm transition-colors hover:bg-(--toggle-group-item-bg-hovered) focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 active:bg-(--toggle-group-item-bg-pressed) disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
14
|
+
{
|
|
15
|
+
variants: {
|
|
16
|
+
size: {
|
|
17
|
+
xs: "h-7 rounded-md px-2 text-xs",
|
|
18
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
19
|
+
md: "h-9 px-4 py-2",
|
|
20
|
+
lg: "h-10 rounded-md px-8",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: { size: "md" },
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/** The non-colour half of `variant` — border width and elevation. */
|
|
28
|
+
const variantStructure: Record<string, string> = {
|
|
29
|
+
solid: "shadow",
|
|
30
|
+
outline: "border shadow-xs",
|
|
31
|
+
ghost: "",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Squares the item at whatever size is set, and drops the horizontal padding. */
|
|
35
|
+
const iconOnlyGeometry: Record<string, string> = {
|
|
36
|
+
xs: "w-7 px-0",
|
|
37
|
+
sm: "w-8 px-0",
|
|
38
|
+
md: "w-9 px-0",
|
|
39
|
+
lg: "w-10 px-0",
|
|
40
|
+
};
|
|
41
|
+
|
|
6
42
|
type ItemProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
7
43
|
Required<Pick<ToggleGroupBaseProps, "appearance" | "orientation" | "variant" | "intent" | "size" | "iconOnly">> & {
|
|
8
44
|
option: ToggleGroupOption;
|
|
@@ -20,24 +56,23 @@ const ToggleGroupItem = React.forwardRef<HTMLButtonElement, ItemProps>(
|
|
|
20
56
|
const isVertical = orientation === "vertical";
|
|
21
57
|
const isSegmented = appearance === "segmented";
|
|
22
58
|
|
|
23
|
-
// Both
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
const recipe = buttonRecipe({ variant, intent, size, iconOnly });
|
|
27
|
-
|
|
59
|
+
// Both appearances share the same base and the pressed-state cells in
|
|
60
|
+
// toggle-group.recipe.css. Segmented just overrides the rounding so items
|
|
61
|
+
// join cleanly.
|
|
28
62
|
return (
|
|
29
63
|
<button
|
|
30
64
|
ref={ref}
|
|
31
|
-
{
|
|
65
|
+
data-variant={variant}
|
|
66
|
+
data-intent={intent}
|
|
32
67
|
type="button"
|
|
33
|
-
//
|
|
34
|
-
// selects on it, so selected/unselected items pick up the same cells.
|
|
68
|
+
// toggle-group.recipe.css selects the selected state on this.
|
|
35
69
|
data-state={selected ? "on" : "off"}
|
|
36
70
|
disabled={isDisabled}
|
|
37
71
|
aria-label={option.ariaLabel}
|
|
38
72
|
className={cn(
|
|
39
|
-
|
|
40
|
-
|
|
73
|
+
variantStructure[variant],
|
|
74
|
+
itemVariants({ size, className: iconOnly ? iconOnlyGeometry[size] : undefined }),
|
|
75
|
+
// Segmented: only override rounding (the variant handles border/colour).
|
|
41
76
|
// Negative margin collapses double borders between outline items.
|
|
42
77
|
isSegmented &&
|
|
43
78
|
(isVertical
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
export { ToggleGroup } from "./ToggleGroup";
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
ToggleGroupIntent,
|
|
4
|
+
ToggleGroupIntentName,
|
|
5
|
+
ToggleGroupOption,
|
|
6
|
+
ToggleGroupProps,
|
|
7
|
+
ToggleGroupSize,
|
|
8
|
+
ToggleGroupVariant,
|
|
9
|
+
ToggleGroupVariantName,
|
|
10
|
+
} from "./toggleGroup.types";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToggleGroup recipe — the component-token layer for ToggleGroup's items.
|
|
3
|
+
*
|
|
4
|
+
* Its own `--toggle-group-item-*` tokens on its own `.toggle-group-item` hook.
|
|
5
|
+
* The cells start as copies of Toggle's, which start as copies of Button's —
|
|
6
|
+
* duplicated on purpose: each is a separate component, re-pointing one does
|
|
7
|
+
* not reach the others, and each is free to change alone.
|
|
8
|
+
*
|
|
9
|
+
* Every cell paints with the intent slots (src/theming/intents.css). The
|
|
10
|
+
* selected state is `[data-state="on"]`.
|
|
11
|
+
*
|
|
12
|
+
* `@layer components` is load-bearing, not tidiness — see button.recipe.css.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
@layer components {
|
|
16
|
+
/* The floor — a plain, legible item when no cell below matches. */
|
|
17
|
+
.toggle-group-item {
|
|
18
|
+
--toggle-group-item-bg: transparent;
|
|
19
|
+
--toggle-group-item-bg-hovered: var(--toggle-group-item-bg); /* no shift on hover */
|
|
20
|
+
--toggle-group-item-bg-pressed: var(--toggle-group-item-bg-hovered);
|
|
21
|
+
--toggle-group-item-fg: var(--fg-normal);
|
|
22
|
+
--toggle-group-item-border: var(--border-normal);
|
|
23
|
+
--toggle-group-item-ring: var(--focus-ring);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* solid — a filled surface, with text read on top of it. */
|
|
27
|
+
.toggle-group-item[data-variant="solid"] {
|
|
28
|
+
--toggle-group-item-bg: var(--intent-solid-bg);
|
|
29
|
+
--toggle-group-item-bg-hovered: var(--intent-solid-bg-hovered);
|
|
30
|
+
--toggle-group-item-bg-pressed: var(--intent-solid-bg-pressed);
|
|
31
|
+
--toggle-group-item-fg: var(--intent-solid-fg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* outline and ghost share the family's text colour and its tint wash. */
|
|
35
|
+
.toggle-group-item[data-variant="outline"],
|
|
36
|
+
.toggle-group-item[data-variant="ghost"] {
|
|
37
|
+
--toggle-group-item-bg-hovered: var(--intent-tint-bg-hovered);
|
|
38
|
+
--toggle-group-item-bg-pressed: var(--intent-tint-bg-pressed);
|
|
39
|
+
--toggle-group-item-fg: var(--intent-fg);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* outline alone carries a resting surface and a border. */
|
|
43
|
+
.toggle-group-item[data-variant="outline"] {
|
|
44
|
+
--toggle-group-item-bg: var(--bg-normal);
|
|
45
|
+
--toggle-group-item-border: var(--intent-border);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* On. Every cell repeats its resting value into `-hovered`: a selected item
|
|
49
|
+
deliberately does not shift on hover. solid holds the fill's -pressed
|
|
50
|
+
step; outline and ghost hold the tint's, with its text partner. */
|
|
51
|
+
.toggle-group-item[data-state="on"][data-variant="solid"] {
|
|
52
|
+
--toggle-group-item-bg: var(--intent-solid-bg-pressed);
|
|
53
|
+
--toggle-group-item-bg-hovered: var(--intent-solid-bg-pressed);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.toggle-group-item[data-state="on"][data-variant="outline"],
|
|
57
|
+
.toggle-group-item[data-state="on"][data-variant="ghost"] {
|
|
58
|
+
--toggle-group-item-bg: var(--intent-tint-bg-pressed);
|
|
59
|
+
--toggle-group-item-bg-hovered: var(--intent-tint-bg-pressed);
|
|
60
|
+
--toggle-group-item-fg: var(--intent-tint-fg-pressed);
|
|
61
|
+
}
|
|
62
|
+
}
|