@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.
Files changed (177) 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/DataTable/DataTable.d.ts +89 -0
  29. package/dist/components/DataTable/DataTable.d.ts.map +1 -0
  30. package/dist/components/DataTable/DataTable.js +107 -0
  31. package/dist/components/DataTable/DataTable.js.map +1 -0
  32. package/dist/components/DataTable/index.d.ts +2 -0
  33. package/dist/components/DataTable/index.d.ts.map +1 -0
  34. package/dist/components/DataTable/index.js +2 -0
  35. package/dist/components/DataTable/index.js.map +1 -0
  36. package/dist/components/FileDropzone/FileDropzone.d.ts +87 -0
  37. package/dist/components/FileDropzone/FileDropzone.d.ts.map +1 -0
  38. package/dist/components/FileDropzone/FileDropzone.js +146 -0
  39. package/dist/components/FileDropzone/FileDropzone.js.map +1 -0
  40. package/dist/components/FileDropzone/index.d.ts +2 -0
  41. package/dist/components/FileDropzone/index.d.ts.map +1 -0
  42. package/dist/components/FileDropzone/index.js +2 -0
  43. package/dist/components/FileDropzone/index.js.map +1 -0
  44. package/dist/components/FormField/FormField.d.ts +3 -1
  45. package/dist/components/FormField/FormField.d.ts.map +1 -1
  46. package/dist/components/FormField/FormField.js.map +1 -1
  47. package/dist/components/ImageGallery/ImageGallery.d.ts +93 -0
  48. package/dist/components/ImageGallery/ImageGallery.d.ts.map +1 -0
  49. package/dist/components/ImageGallery/ImageGallery.js +121 -0
  50. package/dist/components/ImageGallery/ImageGallery.js.map +1 -0
  51. package/dist/components/ImageGallery/index.d.ts +2 -0
  52. package/dist/components/ImageGallery/index.d.ts.map +1 -0
  53. package/dist/components/ImageGallery/index.js +2 -0
  54. package/dist/components/ImageGallery/index.js.map +1 -0
  55. package/dist/components/Input/Input.d.ts +3 -3
  56. package/dist/components/Input/Input.js +3 -3
  57. package/dist/components/Markdown/Markdown.js +1 -1
  58. package/dist/components/Markdown/Markdown.js.map +1 -1
  59. package/dist/components/NumberStepper/NumberStepper.d.ts +78 -0
  60. package/dist/components/NumberStepper/NumberStepper.d.ts.map +1 -0
  61. package/dist/components/NumberStepper/NumberStepper.js +129 -0
  62. package/dist/components/NumberStepper/NumberStepper.js.map +1 -0
  63. package/dist/components/NumberStepper/index.d.ts +2 -0
  64. package/dist/components/NumberStepper/index.d.ts.map +1 -0
  65. package/dist/components/NumberStepper/index.js +2 -0
  66. package/dist/components/NumberStepper/index.js.map +1 -0
  67. package/dist/components/ProgressBar/ProgressBar.d.ts +11 -0
  68. package/dist/components/ProgressBar/ProgressBar.d.ts.map +1 -1
  69. package/dist/components/ProgressBar/ProgressBar.js +6 -2
  70. package/dist/components/ProgressBar/ProgressBar.js.map +1 -1
  71. package/dist/components/ProgressBar/index.d.ts +1 -1
  72. package/dist/components/ProgressBar/index.d.ts.map +1 -1
  73. package/dist/components/ProgressBar/index.js.map +1 -1
  74. package/dist/components/RadioButtonGroup/RadioButtonGroup.d.ts +19 -16
  75. package/dist/components/RadioButtonGroup/RadioButtonGroup.d.ts.map +1 -1
  76. package/dist/components/RadioButtonGroup/RadioButtonGroup.js +13 -12
  77. package/dist/components/RadioButtonGroup/RadioButtonGroup.js.map +1 -1
  78. package/dist/components/SearchInput/SearchInput.d.ts +3 -2
  79. package/dist/components/SearchInput/SearchInput.d.ts.map +1 -1
  80. package/dist/components/SearchInput/SearchInput.js +13 -8
  81. package/dist/components/SearchInput/SearchInput.js.map +1 -1
  82. package/dist/components/Sparkline/Sparkline.d.ts +8 -4
  83. package/dist/components/Sparkline/Sparkline.d.ts.map +1 -1
  84. package/dist/components/Sparkline/Sparkline.js +1 -1
  85. package/dist/components/Sparkline/Sparkline.js.map +1 -1
  86. package/dist/components/Sparkline/index.d.ts +1 -1
  87. package/dist/components/Sparkline/index.d.ts.map +1 -1
  88. package/dist/components/Sparkline/index.js.map +1 -1
  89. package/dist/components/Steps/Steps.d.ts +65 -0
  90. package/dist/components/Steps/Steps.d.ts.map +1 -0
  91. package/dist/components/Steps/Steps.js +62 -0
  92. package/dist/components/Steps/Steps.js.map +1 -0
  93. package/dist/components/Steps/index.d.ts +2 -0
  94. package/dist/components/Steps/index.d.ts.map +1 -0
  95. package/dist/components/Steps/index.js +2 -0
  96. package/dist/components/Steps/index.js.map +1 -0
  97. package/dist/components/TagChip/TagChip.d.ts +0 -13
  98. package/dist/components/TagChip/TagChip.d.ts.map +1 -1
  99. package/dist/components/TagChip/TagChip.js +10 -4
  100. package/dist/components/TagChip/TagChip.js.map +1 -1
  101. package/dist/components/TextInline/TextInline.d.ts +2 -2
  102. package/dist/components/Toast/Toast.js +1 -1
  103. package/dist/components/Toast/Toast.js.map +1 -1
  104. package/dist/components/Toggle/Toggle.d.ts +29 -18
  105. package/dist/components/Toggle/Toggle.d.ts.map +1 -1
  106. package/dist/components/Toggle/Toggle.js +48 -11
  107. package/dist/components/Toggle/Toggle.js.map +1 -1
  108. package/dist/components/Toggle/index.d.ts +1 -1
  109. package/dist/components/Toggle/index.d.ts.map +1 -1
  110. package/dist/components/Toggle/index.js +1 -1
  111. package/dist/components/Toggle/index.js.map +1 -1
  112. package/dist/components/ToggleGroup/ToggleGroup.d.ts +4 -4
  113. package/dist/components/ToggleGroup/ToggleGroup.js +4 -4
  114. package/dist/components/ToggleGroup/ToggleGroupItem.d.ts +1 -1
  115. package/dist/components/ToggleGroup/ToggleGroupItem.d.ts.map +1 -1
  116. package/dist/components/ToggleGroup/ToggleGroupItem.js +37 -7
  117. package/dist/components/ToggleGroup/ToggleGroupItem.js.map +1 -1
  118. package/dist/components/ToggleGroup/ToggleGroupMultiple.d.ts +3 -3
  119. package/dist/components/ToggleGroup/ToggleGroupSingle.d.ts +3 -3
  120. package/dist/components/ToggleGroup/index.d.ts +1 -1
  121. package/dist/components/ToggleGroup/index.d.ts.map +1 -1
  122. package/dist/components/ToggleGroup/toggleGroup.types.d.ts +18 -16
  123. package/dist/components/ToggleGroup/toggleGroup.types.d.ts.map +1 -1
  124. package/dist/typography/Code.js +1 -1
  125. package/dist/typography/Code.js.map +1 -1
  126. package/package.json +22 -1
  127. package/src/components/Badge/Badge.tsx +38 -45
  128. package/src/components/Badge/badge.recipe.css +20 -101
  129. package/src/components/Badge/index.ts +0 -1
  130. package/src/components/Button/Button.tsx +57 -51
  131. package/src/components/Button/button.recipe.css +39 -122
  132. package/src/components/Button/index.ts +0 -1
  133. package/src/components/Card/Card.tsx +1 -1
  134. package/src/components/CheckboxGroup/CheckboxGroup.tsx +17 -15
  135. package/src/components/Code/Workspace/Toolbar.tsx +2 -2
  136. package/src/components/DataTable/DataTable.tsx +261 -0
  137. package/src/components/DataTable/data-table.recipe.css +28 -0
  138. package/src/components/DataTable/index.ts +10 -0
  139. package/src/components/FileDropzone/FileDropzone.tsx +239 -0
  140. package/src/components/FileDropzone/file-dropzone.recipe.css +38 -0
  141. package/src/components/FileDropzone/index.ts +1 -0
  142. package/src/components/FormField/FormField.tsx +3 -2
  143. package/src/components/ImageGallery/ImageGallery.tsx +285 -0
  144. package/src/components/ImageGallery/image-gallery.recipe.css +50 -0
  145. package/src/components/ImageGallery/index.ts +7 -0
  146. package/src/components/Input/Input.tsx +3 -3
  147. package/src/components/Markdown/Markdown.tsx +1 -1
  148. package/src/components/Message/message.recipe.css +13 -28
  149. package/src/components/NumberStepper/NumberStepper.tsx +227 -0
  150. package/src/components/NumberStepper/index.ts +1 -0
  151. package/src/components/NumberStepper/number-stepper.recipe.css +31 -0
  152. package/src/components/ProgressBar/ProgressBar.tsx +16 -4
  153. package/src/components/ProgressBar/index.ts +1 -1
  154. package/src/components/ProgressBar/progress-bar.recipe.css +29 -0
  155. package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +18 -17
  156. package/src/components/SearchInput/SearchInput.tsx +34 -20
  157. package/src/components/SearchInput/search-input.recipe.css +14 -6
  158. package/src/components/Sparkline/Sparkline.tsx +9 -4
  159. package/src/components/Sparkline/index.ts +1 -1
  160. package/src/components/Steps/Steps.tsx +149 -0
  161. package/src/components/Steps/index.ts +1 -0
  162. package/src/components/Steps/steps.recipe.css +51 -0
  163. package/src/components/TagChip/TagChip.tsx +17 -8
  164. package/src/components/TagChip/tag-chip.recipe.css +12 -12
  165. package/src/components/Toast/Toast.tsx +1 -1
  166. package/src/components/Toggle/Toggle.tsx +67 -21
  167. package/src/components/Toggle/index.ts +9 -1
  168. package/src/components/Toggle/toggle.recipe.css +45 -73
  169. package/src/components/ToggleGroup/ToggleGroup.tsx +4 -4
  170. package/src/components/ToggleGroup/ToggleGroupItem.tsx +46 -11
  171. package/src/components/ToggleGroup/index.ts +9 -1
  172. package/src/components/ToggleGroup/toggle-group.recipe.css +62 -0
  173. package/src/components/ToggleGroup/toggleGroup.types.ts +18 -16
  174. package/src/theme/linen/theme.css +21 -0
  175. package/src/theming/intents.css +112 -0
  176. package/src/theming/tailwind.css +22 -1
  177. package/src/typography/Code.tsx +1 -1
@@ -0,0 +1,149 @@
1
+ "use client";
2
+
3
+ import { cn } from "@sixthshift/design-system/utils";
4
+ import { cva } from "class-variance-authority";
5
+ import { Check } from "lucide-react";
6
+ import * as React from "react";
7
+
8
+ /**
9
+ * Geometry only. Every colour reads a `--steps-*` component token whose value
10
+ * is decided by src/components/Steps/steps.recipe.css, selected by the
11
+ * `data-state` each item renders (`complete`, `current`, `upcoming`).
12
+ */
13
+ const stepsVariants = cva("steps flex", {
14
+ variants: {
15
+ orientation: {
16
+ horizontal: "w-full flex-row items-start",
17
+ vertical: "flex-col",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ orientation: "horizontal",
22
+ },
23
+ });
24
+
25
+ export type StepsOrientation = "horizontal" | "vertical";
26
+
27
+ /** Where a step sits relative to `current`. Rendered as `data-state`. */
28
+ export type StepState = "complete" | "current" | "upcoming";
29
+
30
+ export type StepItem = {
31
+ /** Visible step name. */
32
+ label: React.ReactNode;
33
+ /** Secondary line under the label. */
34
+ description?: React.ReactNode;
35
+ };
36
+
37
+ export type StepsProps = Omit<React.OlHTMLAttributes<HTMLOListElement>, "children"> & {
38
+ /** The steps, in order. */
39
+ steps: readonly StepItem[];
40
+ /** Zero-based index of the step in progress. `steps.length` marks every step complete. */
41
+ current: number;
42
+ /**
43
+ * Makes completed steps clickable, for going back in a wizard. Upcoming
44
+ * steps never are — skipping ahead is the flow's decision, not the
45
+ * indicator's.
46
+ */
47
+ onStepClick?: (index: number) => void;
48
+ /** Layout direction. `horizontal` collapses labels to the current step's on narrow screens. */
49
+ orientation?: StepsOrientation;
50
+ };
51
+
52
+ function stateOf(index: number, current: number): StepState {
53
+ if (index < current) return "complete";
54
+ if (index === current) return "current";
55
+ return "upcoming";
56
+ }
57
+
58
+ /**
59
+ * Progress through a fixed sequence — checkout, onboarding, a multi-page
60
+ * form. Display-only: the step in progress is the `current` prop, owned by
61
+ * whatever runs the flow, so there is no internal state to control.
62
+ *
63
+ * Renders an `<ol>` (pass `aria-label`, e.g. "Checkout progress"); the
64
+ * current item carries `aria-current="step"`, and each indicator has a
65
+ * visually hidden status ("completed", "current") so the sequence reads
66
+ * correctly without the colours.
67
+ *
68
+ * With `onStepClick`, completed steps become buttons. Current and upcoming
69
+ * steps stay plain text: a step indicator that let you jump forward would be
70
+ * making the flow's validation decisions for it.
71
+ *
72
+ * Horizontally, every label shows from `sm` up; below that only the current
73
+ * step's label does, so four steps still fit a phone.
74
+ */
75
+ export const Steps = React.forwardRef<HTMLOListElement, StepsProps>(({ className, steps, current, onStepClick, orientation = "horizontal", ...props }, ref) => {
76
+ const isHorizontal = orientation === "horizontal";
77
+
78
+ return (
79
+ <ol ref={ref} data-orientation={orientation} className={stepsVariants({ orientation, className })} {...props}>
80
+ {steps.map((step, index) => {
81
+ const state = stateOf(index, current);
82
+ const isLast = index === steps.length - 1;
83
+ const clickable = state === "complete" && onStepClick !== undefined;
84
+
85
+ const indicator = (
86
+ <span className="steps-indicator border-(color:--steps-indicator-border) relative z-10 flex size-8 shrink-0 items-center justify-center rounded-full border-2 bg-(--steps-indicator-bg) font-semibold text-(--steps-indicator-fg) text-sm transition-colors">
87
+ {state === "complete" ? <Check aria-hidden="true" className="size-4" strokeWidth={3} /> : index + 1}
88
+ </span>
89
+ );
90
+
91
+ const text = (
92
+ <span
93
+ className={cn("flex min-w-0 flex-col", isHorizontal ? "items-center text-center" : "pt-1", isHorizontal && state !== "current" && "max-sm:sr-only")}
94
+ >
95
+ <span className="font-medium text-(--steps-label-fg) text-sm">{step.label}</span>
96
+ {step.description !== undefined && <span className="text-(--steps-description-fg) text-xs">{step.description}</span>}
97
+ {state !== "upcoming" && <span className="sr-only">{state === "complete" ? "(completed)" : "(current)"}</span>}
98
+ </span>
99
+ );
100
+
101
+ const body = (
102
+ <>
103
+ {indicator}
104
+ {text}
105
+ </>
106
+ );
107
+
108
+ const bodyClass = cn("flex gap-2", isHorizontal ? "flex-col items-center" : "flex-row items-start gap-3");
109
+
110
+ return (
111
+ <li
112
+ // biome-ignore lint/suspicious/noArrayIndexKey: a step is its position — the sequence is fixed and never reorders
113
+ key={index}
114
+ data-state={state}
115
+ aria-current={state === "current" ? "step" : undefined}
116
+ className={cn("steps-item relative", isHorizontal ? "flex flex-1 justify-center" : "flex pb-6 last:pb-0")}
117
+ >
118
+ {!isLast && (
119
+ <span
120
+ aria-hidden="true"
121
+ className={cn(
122
+ "steps-connector absolute bg-(--steps-connector-bg)",
123
+ isHorizontal ? "top-4 left-1/2 h-0.5 w-full -translate-y-1/2" : "top-8 bottom-0 left-4 w-0.5 -translate-x-1/2"
124
+ )}
125
+ />
126
+ )}
127
+ {clickable ? (
128
+ <button
129
+ type="button"
130
+ onClick={() => onStepClick(index)}
131
+ className={cn(
132
+ bodyClass,
133
+ "focus-visible:ring-(color:--steps-ring) cursor-pointer rounded-md focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 [&:hover_.steps-indicator]:bg-(--steps-indicator-bg-hovered)"
134
+ )}
135
+ >
136
+ {body}
137
+ </button>
138
+ ) : (
139
+ <span className={bodyClass}>{body}</span>
140
+ )}
141
+ </li>
142
+ );
143
+ })}
144
+ </ol>
145
+ );
146
+ });
147
+ Steps.displayName = "Steps";
148
+
149
+ export { stepsVariants };
@@ -0,0 +1 @@
1
+ export { type StepItem, type StepState, Steps, type StepsOrientation, type StepsProps, stepsVariants } from "./Steps";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Steps recipe — the component-token layer for Steps.
3
+ *
4
+ * One axis, and it is not `intent`: a step's colour says where it sits in the
5
+ * sequence, which the component renders as `data-state` on each item
6
+ * (`complete`, `current`, `upcoming`). Same shape as tabs.css keying its
7
+ * selected cell on `data-selected` rather than a variant.
8
+ *
9
+ * The grammar is `--steps-[{part}-]{context}[-{state}]`. Parts are the
10
+ * `indicator` (the numbered circle), the `connector` (the rule to the next
11
+ * step), and the `label`/`description` text. `-hovered` on the indicator only
12
+ * ever applies to a completed step with `onStepClick`, the one clickable case.
13
+ *
14
+ * The connector belongs to the step it leaves, so a completed step's
15
+ * connector takes the brand colour: the line fills up to the current step.
16
+ *
17
+ * `@layer components` is load-bearing, not tidiness — see button.css.
18
+ */
19
+
20
+ @layer components {
21
+ /* The floor, and also the upcoming look: an outlined, quiet step. An item
22
+ with a state a consumer added but did not style renders like this. */
23
+ .steps-item {
24
+ --steps-indicator-bg: var(--bg-normal);
25
+ --steps-indicator-bg-hovered: var(--steps-indicator-bg);
26
+ --steps-indicator-fg: var(--fg-subtle);
27
+ --steps-indicator-border: var(--border-normal);
28
+ --steps-connector-bg: var(--border-normal);
29
+ --steps-label-fg: var(--fg-subtle);
30
+ --steps-description-fg: var(--fg-subtle);
31
+ --steps-ring: var(--focus-ring);
32
+ }
33
+
34
+ /* current — outlined in brand, the label at full strength. */
35
+ .steps-item[data-state="current"] {
36
+ --steps-indicator-bg: var(--bg-brand-subtle);
37
+ --steps-indicator-fg: var(--fg-on-brand-subtle);
38
+ --steps-indicator-border: var(--border-brand);
39
+ --steps-label-fg: var(--fg-normal);
40
+ }
41
+
42
+ /* complete — a filled brand disc with the check reversed out. */
43
+ .steps-item[data-state="complete"] {
44
+ --steps-indicator-bg: var(--bg-brand);
45
+ --steps-indicator-bg-hovered: var(--bg-brand-hovered);
46
+ --steps-indicator-fg: var(--fg-on-brand);
47
+ --steps-indicator-border: var(--steps-indicator-bg);
48
+ --steps-connector-bg: var(--border-brand);
49
+ --steps-label-fg: var(--fg-normal);
50
+ }
51
+ }
@@ -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
  */