@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.
Files changed (123) hide show
  1. package/README.md +5 -4
  2. package/dist/components/Badge/Badge.d.ts +17 -35
  3. package/dist/components/Badge/Badge.d.ts.map +1 -1
  4. package/dist/components/Badge/Badge.js +19 -22
  5. package/dist/components/Badge/Badge.js.map +1 -1
  6. package/dist/components/Badge/index.d.ts +1 -1
  7. package/dist/components/Badge/index.d.ts.map +1 -1
  8. package/dist/components/Badge/index.js +1 -1
  9. package/dist/components/Badge/index.js.map +1 -1
  10. package/dist/components/Button/Button.d.ts +23 -41
  11. package/dist/components/Button/Button.d.ts.map +1 -1
  12. package/dist/components/Button/Button.js +28 -35
  13. package/dist/components/Button/Button.js.map +1 -1
  14. package/dist/components/Button/index.d.ts +1 -1
  15. package/dist/components/Button/index.d.ts.map +1 -1
  16. package/dist/components/Button/index.js +1 -1
  17. package/dist/components/Button/index.js.map +1 -1
  18. package/dist/components/Card/Card.d.ts +2 -2
  19. package/dist/components/Card/Card.js +1 -1
  20. package/dist/components/Card/Card.js.map +1 -1
  21. package/dist/components/CheckboxGroup/CheckboxGroup.d.ts +20 -16
  22. package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -1
  23. package/dist/components/CheckboxGroup/CheckboxGroup.js +12 -10
  24. package/dist/components/CheckboxGroup/CheckboxGroup.js.map +1 -1
  25. package/dist/components/Code/Workspace/Toolbar.d.ts +2 -1
  26. package/dist/components/Code/Workspace/Toolbar.d.ts.map +1 -1
  27. package/dist/components/Code/Workspace/Toolbar.js.map +1 -1
  28. package/dist/components/FormField/FormField.d.ts +3 -1
  29. package/dist/components/FormField/FormField.d.ts.map +1 -1
  30. package/dist/components/FormField/FormField.js.map +1 -1
  31. package/dist/components/Input/Input.d.ts +3 -3
  32. package/dist/components/Input/Input.js +3 -3
  33. package/dist/components/Markdown/Markdown.js +1 -1
  34. package/dist/components/Markdown/Markdown.js.map +1 -1
  35. package/dist/components/NumberStepper/NumberStepper.d.ts +1 -1
  36. package/dist/components/ProgressBar/ProgressBar.d.ts +11 -0
  37. package/dist/components/ProgressBar/ProgressBar.d.ts.map +1 -1
  38. package/dist/components/ProgressBar/ProgressBar.js +6 -2
  39. package/dist/components/ProgressBar/ProgressBar.js.map +1 -1
  40. package/dist/components/ProgressBar/index.d.ts +1 -1
  41. package/dist/components/ProgressBar/index.d.ts.map +1 -1
  42. package/dist/components/ProgressBar/index.js.map +1 -1
  43. package/dist/components/RadioButtonGroup/RadioButtonGroup.d.ts +19 -16
  44. package/dist/components/RadioButtonGroup/RadioButtonGroup.d.ts.map +1 -1
  45. package/dist/components/RadioButtonGroup/RadioButtonGroup.js +13 -12
  46. package/dist/components/RadioButtonGroup/RadioButtonGroup.js.map +1 -1
  47. package/dist/components/SearchInput/SearchInput.d.ts +3 -2
  48. package/dist/components/SearchInput/SearchInput.d.ts.map +1 -1
  49. package/dist/components/SearchInput/SearchInput.js +13 -8
  50. package/dist/components/SearchInput/SearchInput.js.map +1 -1
  51. package/dist/components/Sparkline/Sparkline.d.ts +8 -4
  52. package/dist/components/Sparkline/Sparkline.d.ts.map +1 -1
  53. package/dist/components/Sparkline/Sparkline.js +1 -1
  54. package/dist/components/Sparkline/Sparkline.js.map +1 -1
  55. package/dist/components/Sparkline/index.d.ts +1 -1
  56. package/dist/components/Sparkline/index.d.ts.map +1 -1
  57. package/dist/components/Sparkline/index.js.map +1 -1
  58. package/dist/components/TagChip/TagChip.d.ts +0 -13
  59. package/dist/components/TagChip/TagChip.d.ts.map +1 -1
  60. package/dist/components/TagChip/TagChip.js +10 -4
  61. package/dist/components/TagChip/TagChip.js.map +1 -1
  62. package/dist/components/TextInline/TextInline.d.ts +2 -2
  63. package/dist/components/Toast/Toast.js +1 -1
  64. package/dist/components/Toast/Toast.js.map +1 -1
  65. package/dist/components/Toggle/Toggle.d.ts +29 -18
  66. package/dist/components/Toggle/Toggle.d.ts.map +1 -1
  67. package/dist/components/Toggle/Toggle.js +48 -11
  68. package/dist/components/Toggle/Toggle.js.map +1 -1
  69. package/dist/components/Toggle/index.d.ts +1 -1
  70. package/dist/components/Toggle/index.d.ts.map +1 -1
  71. package/dist/components/Toggle/index.js +1 -1
  72. package/dist/components/Toggle/index.js.map +1 -1
  73. package/dist/components/ToggleGroup/ToggleGroup.d.ts +4 -4
  74. package/dist/components/ToggleGroup/ToggleGroup.js +4 -4
  75. package/dist/components/ToggleGroup/ToggleGroupItem.d.ts +1 -1
  76. package/dist/components/ToggleGroup/ToggleGroupItem.d.ts.map +1 -1
  77. package/dist/components/ToggleGroup/ToggleGroupItem.js +37 -7
  78. package/dist/components/ToggleGroup/ToggleGroupItem.js.map +1 -1
  79. package/dist/components/ToggleGroup/ToggleGroupMultiple.d.ts +3 -3
  80. package/dist/components/ToggleGroup/ToggleGroupSingle.d.ts +3 -3
  81. package/dist/components/ToggleGroup/index.d.ts +1 -1
  82. package/dist/components/ToggleGroup/index.d.ts.map +1 -1
  83. package/dist/components/ToggleGroup/toggleGroup.types.d.ts +18 -16
  84. package/dist/components/ToggleGroup/toggleGroup.types.d.ts.map +1 -1
  85. package/dist/typography/Code.js +1 -1
  86. package/dist/typography/Code.js.map +1 -1
  87. package/package.json +2 -1
  88. package/src/components/Badge/Badge.tsx +38 -45
  89. package/src/components/Badge/badge.recipe.css +20 -101
  90. package/src/components/Badge/index.ts +0 -1
  91. package/src/components/Button/Button.tsx +57 -51
  92. package/src/components/Button/button.recipe.css +39 -122
  93. package/src/components/Button/index.ts +0 -1
  94. package/src/components/Card/Card.tsx +1 -1
  95. package/src/components/CheckboxGroup/CheckboxGroup.tsx +17 -15
  96. package/src/components/Code/Workspace/Toolbar.tsx +2 -2
  97. package/src/components/FormField/FormField.tsx +3 -2
  98. package/src/components/Input/Input.tsx +3 -3
  99. package/src/components/Markdown/Markdown.tsx +1 -1
  100. package/src/components/Message/message.recipe.css +13 -28
  101. package/src/components/ProgressBar/ProgressBar.tsx +16 -4
  102. package/src/components/ProgressBar/index.ts +1 -1
  103. package/src/components/ProgressBar/progress-bar.recipe.css +29 -0
  104. package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +18 -17
  105. package/src/components/SearchInput/SearchInput.tsx +34 -20
  106. package/src/components/SearchInput/search-input.recipe.css +14 -6
  107. package/src/components/Sparkline/Sparkline.tsx +9 -4
  108. package/src/components/Sparkline/index.ts +1 -1
  109. package/src/components/TagChip/TagChip.tsx +17 -8
  110. package/src/components/TagChip/tag-chip.recipe.css +12 -12
  111. package/src/components/Toast/Toast.tsx +1 -1
  112. package/src/components/Toggle/Toggle.tsx +67 -21
  113. package/src/components/Toggle/index.ts +9 -1
  114. package/src/components/Toggle/toggle.recipe.css +45 -73
  115. package/src/components/ToggleGroup/ToggleGroup.tsx +4 -4
  116. package/src/components/ToggleGroup/ToggleGroupItem.tsx +46 -11
  117. package/src/components/ToggleGroup/index.ts +9 -1
  118. package/src/components/ToggleGroup/toggle-group.recipe.css +62 -0
  119. package/src/components/ToggleGroup/toggleGroup.types.ts +18 -16
  120. package/src/theme/linen/theme.css +21 -0
  121. package/src/theming/intents.css +112 -0
  122. package/src/theming/tailwind.css +17 -1
  123. 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 renders `Input` for its field, so every input-surface colour
5
- * (background, border, ring, placeholder, icon) already comes from
6
- * `.input`'s tokens in input.css — duplicating them here would just be a
7
- * second name for the same cell. The only colour SearchInput contributes
8
- * itself is the clear button's hover wash, which is why this file is a single
9
- * token rather than empty.
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?: Interpolation;
16
+ interpolation?: SparklineInterpolation;
12
17
  strokeWidth?: number;
13
18
  };
14
19
 
15
- function buildPath(points: { x: number; y: number }[], interpolation: Interpolation): string {
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` reuses the same `Interpolation` type as `LineChart`. The
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). Built on `Badge`
28
- * (`variant="outline" intent="muted"`).
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
- <Badge
43
+ <span
37
44
  ref={ref}
38
- variant="outline"
39
- intent="muted"
40
- className={cn("tag-chip gap-1 font-normal", size === "sm" ? "px-1.5 py-0 text-[10px]" : "px-2 py-0.5 text-xs", className)}
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
- </Badge>
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's surface (background, border, foreground) is entirely Badge's:
5
- * it renders `<Badge variant="outline" intent="muted">`, so that colour
6
- * already comes from `.badge`'s tokens in badge.css — duplicating it here
7
- * would just be a second name for the same cell. The only colours TagChip
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}]`. Both tokens live
12
- * on `.tag-chip` (the root Badge element) and are read by its descendants via
13
- * ordinary CSS custom-property inheritance — the namespace `<span>` and the
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 for the
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 variant="link" size="sm" className="h-auto p-0 font-medium" onClick={onAction}>
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 { VariantProps } from "class-variance-authority";
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
- * `variant` and `intent` are no longer CVA variants on `buttonVariants` — they
11
- * select a recipe cell in CSS — so they are declared here rather than inherited
12
- * through `VariantProps`, which now yields `size` alone.
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 buttonVariants> & {
16
- variant?: ButtonVariant | undefined;
17
- intent?: ButtonIntent | undefined;
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
- * Built directly on Button: it calls `buttonRecipe(...)` and takes the same
37
- * `variant`/`intent`/`size` axes Button does, plus `iconOnly` for a toolbar
38
- * toggle whose whole content is an icon. The
39
- * pressed-state colour mapping lives in `src/components/Toggle/toggle.recipe.css`,
40
- * keyed off the `data-state="on" | "off"` attribute this component renders
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
- {...recipe}
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 +1,9 @@
1
- export { Toggle, type ToggleProps } from "./Toggle";
1
+ export {
2
+ Toggle,
3
+ type ToggleIntent,
4
+ type ToggleIntentName,
5
+ type ToggleProps,
6
+ type ToggleVariant,
7
+ type ToggleVariantName,
8
+ toggleVariants,
9
+ } from "./Toggle";
@@ -1,92 +1,64 @@
1
1
  /**
2
- * Toggle recipe — the pressed-state layer for Toggle and ToggleGroupItem.
2
+ * Toggle recipe — the component-token layer for Toggle.
3
3
  *
4
- * These cells target `.btn`, not a `.toggle` class of their own, because both
5
- * components render Button's class and read Button's `--button-*` tokens
6
- * (via `buttonRecipe`) — a toggle is a button with a third axis, `state`, not
7
- * a different look. Reusing `.btn` here is deliberate, not a shortcut.
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
- * The grammar extends button.css's `(variant, intent)` with a third selector,
10
- * `[data-state="on"]` — the same attribute Toggle already renders
11
- * (`data-state={pressed ? "on" : "off"}`) and ToggleGroupItem now renders for
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
- * Only solid, outline and ghost get cells: a link toggle has no surface to
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
- /* solid — the main -pressed background carries through as both resting and
30
- hovered, so pressing never lightens or darkens further on hover. */
31
- .btn[data-state="on"][data-variant="solid"][data-intent="brand"] {
32
- --button-bg: var(--bg-brand-pressed);
33
- --button-bg-hovered: var(--bg-brand-pressed);
34
- }
35
-
36
- .btn[data-state="on"][data-variant="solid"][data-intent="neutral"] {
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
- /* outline and ghost — same subtle-pressed background and foreground per
57
- intent, matching button.css's own pairing of these two variants. */
58
- .btn[data-state="on"][data-variant="outline"][data-intent="neutral"],
59
- .btn[data-state="on"][data-variant="ghost"][data-intent="neutral"] {
60
- --button-bg: var(--bg-subtle-pressed);
61
- --button-bg-hovered: var(--bg-subtle-pressed);
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
- .btn[data-state="on"][data-variant="outline"][data-intent="brand"],
66
- .btn[data-state="on"][data-variant="ghost"][data-intent="brand"] {
67
- --button-bg: var(--bg-brand-subtle-pressed);
68
- --button-bg-hovered: var(--bg-brand-subtle-pressed);
69
- --button-fg: var(--fg-on-brand-subtle-pressed);
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
- .btn[data-state="on"][data-variant="outline"][data-intent="danger"],
73
- .btn[data-state="on"][data-variant="ghost"][data-intent="danger"] {
74
- --button-bg: var(--bg-danger-subtle-pressed);
75
- --button-bg-hovered: var(--bg-danger-subtle-pressed);
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
- .btn[data-state="on"][data-variant="outline"][data-intent="success"],
80
- .btn[data-state="on"][data-variant="ghost"][data-intent="success"] {
81
- --button-bg: var(--bg-success-subtle-pressed);
82
- --button-bg-hovered: var(--bg-success-subtle-pressed);
83
- --button-fg: var(--fg-on-success-subtle-pressed);
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
- .btn[data-state="on"][data-variant="outline"][data-intent="warning"],
87
- .btn[data-state="on"][data-variant="ghost"][data-intent="warning"] {
88
- --button-bg: var(--bg-warning-subtle-pressed);
89
- --button-bg-hovered: var(--bg-warning-subtle-pressed);
90
- --button-fg: var(--fg-on-warning-subtle-pressed);
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 are built on the same `buttonRecipe` as `Button`/`Toggle` and take
17
- * the same `variant`/`intent`, with two differences: `variant` excludes
18
- * `"link"` (no clear pressed state for an underline in a group) and `size`
19
- * excludes `"xl"` (too large for grouped toggles) — see
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 modes share the same base: buttonVariants + the pressed-state cells
24
- // in recipes/toggle.css. Segmented just overrides border/shadow/rounding
25
- // so items join cleanly.
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
- {...recipe}
65
+ data-variant={variant}
66
+ data-intent={intent}
32
67
  type="button"
33
- // Same attribute Toggle renders for `pressed` — recipes/toggle.css
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
- recipe.className,
40
- // Segmented: only override rounding (let buttonVariants handle border/color).
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 { ToggleGroupOption, ToggleGroupProps } from "./toggleGroup.types";
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
+ }