@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,11 +1,17 @@
1
1
  /**
2
2
  * Button recipe — the component-token layer for Button.
3
3
  *
4
- * This is the mapping from `(variant, intent, state)` to a semantic token. It
5
- * used to live in `compoundVariants` inside Button.tsx, where it was compiled
6
- * into class-name literals and no consumer could reach it. Here it is data, so
7
- * a consumer can re-point a cell, scope a change to a subtree, or add a variant
8
- * or intent the library never shipped.
4
+ * This is the mapping from `(variant, state)` to a colour. It used to live in
5
+ * `compoundVariants` inside Button.tsx, where it was compiled into class-name
6
+ * literals and no consumer could reach it. Here it is data, so a consumer can
7
+ * re-point a cell, scope a change to a subtree, or add a variant the library
8
+ * never shipped.
9
+ *
10
+ * Intent does not appear here at all. Each cell below paints with the intent
11
+ * slots (`--intent-solid-bg`, `--intent-fg`, …) that src/theming/intents.css
12
+ * declares per `data-intent`, so every variant works in every intent — including
13
+ * one a consumer adds — and this file has one cell per variant, not one per
14
+ * `(variant, intent)` pair.
9
15
  *
10
16
  * The grammar is `--button-{context}[-{state}]` — the same `context` and
11
17
  * `state` words the semantic layer uses (src/theming/vocabulary.ts). Note what is
@@ -37,131 +43,42 @@
37
43
  --button-ring: var(--focus-ring);
38
44
  }
39
45
 
40
- /* solid — a filled surface, with text read on top of it. The `fg-on-*`
41
- partner is the pairing check-contrast.ts verifies, so these four cells
42
- inherit that guarantee rather than restating it. */
43
- .btn[data-variant="solid"][data-intent="brand"] {
44
- --button-bg: var(--bg-brand);
45
- --button-bg-hovered: var(--bg-brand-hovered);
46
- --button-bg-pressed: var(--bg-brand-pressed);
47
- --button-fg: var(--fg-on-brand);
48
- }
49
-
50
- /* `neutral` means *no colour family* — the grey ramp. It used to resolve to
51
- the brand fill, which made `intent="neutral"` mean one thing on Button and
52
- another on Badge and Message, where it was already grey. Button
53
- was the outlier; this is the cell that ends that. A brand-coloured button
54
- is now `intent="brand"`, which is also Button's default, so a bare
55
- `<Button>` is unchanged. */
56
- .btn[data-variant="solid"][data-intent="neutral"] {
57
- --button-bg: var(--bg-strong);
58
- --button-bg-hovered: var(--bg-strong-hovered);
59
- --button-bg-pressed: var(--bg-strong-pressed);
60
- --button-fg: var(--fg-on-strong);
61
- }
62
-
63
- .btn[data-variant="solid"][data-intent="danger"] {
64
- --button-bg: var(--bg-danger);
65
- --button-bg-hovered: var(--bg-danger-hovered);
66
- --button-bg-pressed: var(--bg-danger-pressed);
67
- --button-fg: var(--fg-on-danger);
68
- }
69
-
70
- .btn[data-variant="solid"][data-intent="success"] {
71
- --button-bg: var(--bg-success);
72
- --button-bg-hovered: var(--bg-success-hovered);
73
- --button-bg-pressed: var(--bg-success-pressed);
74
- --button-fg: var(--fg-on-success);
75
- }
76
-
77
- .btn[data-variant="solid"][data-intent="warning"] {
78
- --button-bg: var(--bg-warning);
79
- --button-bg-hovered: var(--bg-warning-hovered);
80
- --button-bg-pressed: var(--bg-warning-pressed);
81
- --button-fg: var(--fg-on-warning);
82
- }
83
-
84
- /* outline carries a resting surface; ghost does not. Beyond that their
85
- colour recipes are identical, so the text colour and the hover/pressed wash
86
- are shared per intent and only the two lines below differ. */
87
- .btn[data-variant="outline"] {
88
- --button-bg: var(--bg-normal);
89
- }
90
-
91
- /* `neutral` here needed no change: outline and ghost were already grey — it
92
- was only the solid and link cells that read as brand. */
93
- .btn[data-variant="outline"][data-intent="neutral"],
94
- .btn[data-variant="ghost"][data-intent="neutral"] {
95
- --button-bg-hovered: var(--bg-subtle-hovered);
96
- --button-bg-pressed: var(--bg-subtle-pressed);
97
- --button-fg: var(--fg-normal);
98
- }
99
-
100
- .btn[data-variant="outline"][data-intent="brand"],
101
- .btn[data-variant="ghost"][data-intent="brand"] {
102
- --button-bg-hovered: var(--bg-brand-subtle-hovered);
103
- --button-bg-pressed: var(--bg-brand-subtle-pressed);
104
- --button-fg: var(--fg-brand);
105
- }
106
-
107
- .btn[data-variant="outline"][data-intent="danger"],
108
- .btn[data-variant="ghost"][data-intent="danger"] {
109
- --button-bg-hovered: var(--bg-danger-subtle-hovered);
110
- --button-bg-pressed: var(--bg-danger-subtle-pressed);
111
- --button-fg: var(--fg-danger);
46
+ /* solid — a filled surface, with text read on top of it. The slot pair
47
+ resolves to a `bg-*` / `fg-on-*` partnership, which is the pairing
48
+ check-contrast.ts verifies, so this cell inherits that guarantee. For
49
+ `neutral`, the default, that is the grey `bg-strong` fill. */
50
+ .btn[data-variant="solid"] {
51
+ --button-bg: var(--intent-solid-bg);
52
+ --button-bg-hovered: var(--intent-solid-bg-hovered);
53
+ --button-bg-pressed: var(--intent-solid-bg-pressed);
54
+ --button-fg: var(--intent-solid-fg);
112
55
  }
113
56
 
114
- .btn[data-variant="outline"][data-intent="success"],
115
- .btn[data-variant="ghost"][data-intent="success"] {
116
- --button-bg-hovered: var(--bg-success-subtle-hovered);
117
- --button-bg-pressed: var(--bg-success-subtle-pressed);
118
- --button-fg: var(--fg-success);
119
- }
120
-
121
- .btn[data-variant="outline"][data-intent="warning"],
122
- .btn[data-variant="ghost"][data-intent="warning"] {
123
- --button-bg-hovered: var(--bg-warning-subtle-hovered);
124
- --button-bg-pressed: var(--bg-warning-subtle-pressed);
125
- --button-fg: var(--fg-warning);
57
+ /* outline carries a resting surface; ghost does not. Beyond that they share
58
+ the family's text colour and its tint wash on hover and press. */
59
+ .btn[data-variant="outline"],
60
+ .btn[data-variant="ghost"] {
61
+ --button-bg-hovered: var(--intent-tint-bg-hovered);
62
+ --button-bg-pressed: var(--intent-tint-bg-pressed);
63
+ --button-fg: var(--intent-fg);
126
64
  }
127
65
 
128
66
  /* The border belongs to outline alone. Ghost has no border width, so folding
129
- these into the blocks above would be invisible today — and a silent
67
+ this into the block above would be invisible today — and a silent
130
68
  semantic change the moment anyone gave ghost a border. A consumer who wants
131
69
  that can still say so in one line; it should be their decision, not a
132
- side-effect of how this file was factored. `neutral` needs no rule: the
133
- floor's `border-normal` is already right. */
134
- .btn[data-variant="outline"][data-intent="brand"] {
135
- --button-border: var(--border-brand);
136
- }
137
- .btn[data-variant="outline"][data-intent="danger"] {
138
- --button-border: var(--border-danger);
139
- }
140
- .btn[data-variant="outline"][data-intent="success"] {
141
- --button-border: var(--border-success);
142
- }
143
- .btn[data-variant="outline"][data-intent="warning"] {
144
- --button-border: var(--border-warning);
70
+ side-effect of how this file was factored. */
71
+ .btn[data-variant="outline"] {
72
+ --button-bg: var(--bg-normal);
73
+ --button-border: var(--intent-border);
145
74
  }
146
75
 
147
- /* link — no surface in any state; only the text carries colour, so the
148
- floor's transparent background and its no-shift-on-hover chain both stand.
149
- A link button reads as brand-coloured by default because `brand` is
150
- Button's default intent — not because `neutral` was quietly redefined to
151
- mean brand, which is what this cell used to do. */
152
- .btn[data-variant="link"][data-intent="brand"] {
153
- --button-fg: var(--fg-brand);
154
- }
155
- .btn[data-variant="link"][data-intent="neutral"] {
156
- --button-fg: var(--fg-normal);
157
- }
158
- .btn[data-variant="link"][data-intent="danger"] {
159
- --button-fg: var(--fg-danger);
160
- }
161
- .btn[data-variant="link"][data-intent="success"] {
162
- --button-fg: var(--fg-success);
163
- }
164
- .btn[data-variant="link"][data-intent="warning"] {
165
- --button-fg: var(--fg-warning);
76
+ /* inline — not a variant: inline text with no box (Button drops
77
+ `data-variant` for it, so no cell above can match). Only the text carries
78
+ colour, so the floor's transparent background and its no-shift-on-hover
79
+ chain both stand. It is brand-coloured only with `intent="brand"`;
80
+ `neutral`, the default, is plain text colour. */
81
+ .btn[data-inline="true"] {
82
+ --button-fg: var(--intent-fg);
166
83
  }
167
84
  }
@@ -5,6 +5,5 @@ export {
5
5
  type ButtonProps,
6
6
  type ButtonVariant,
7
7
  type ButtonVariantName,
8
- buttonRecipe,
9
8
  buttonVariants,
10
9
  } from "./Button";
@@ -49,7 +49,7 @@ export const cardVariants = cva(
49
49
  sm: "rounded-lg p-3",
50
50
  md: "rounded-xl p-4",
51
51
  lg: "rounded-xl p-6",
52
- xl: "rounded-2xl p-8",
52
+ xl: "rounded-xl p-8",
53
53
  },
54
54
  interactive: {
55
55
  true: "hover:border-(color:--card-border-hovered) focus-visible:outline-(color:--card-ring) cursor-pointer transition-colors focus-visible:outline-2 focus-visible:outline-offset-2",
@@ -26,10 +26,11 @@ export type CheckboxGroupProps = Omit<React.HTMLAttributes<HTMLDivElement>, "onC
26
26
  onValueChange?: (value: string[]) => void;
27
27
  /** Layout orientation */
28
28
  orientation?: "vertical" | "horizontal";
29
- /** Visual variant */
30
- variant?: "default" | "button";
31
- /** Button appearance (only applies when variant="button") */
32
- appearance?: "segmented" | "separate";
29
+ /**
30
+ * How the options render: `control` (the default) is one `Checkbox` per option;
31
+ * `segmented` and `separate` are a row or column of buttons, joined or apart.
32
+ */
33
+ appearance?: "control" | "segmented" | "separate";
33
34
  /** Input name for form submission */
34
35
  name?: string;
35
36
  /** Disable all options */
@@ -43,17 +44,19 @@ export type CheckboxGroupProps = Omit<React.HTMLAttributes<HTMLDivElement>, "onC
43
44
  * selection, via the `value`/`defaultValue`/`onValueChange` triad
44
45
  * (`useControllableState`).
45
46
  *
46
- * `variant="default"` (the default) renders one `Checkbox` per option, each
47
+ * `appearance="control"` (the default) renders one `Checkbox` per option, each
47
48
  * with its own visible label, laid out in a column or row per
48
- * `orientation`. `variant="button"` is a separate rendering path — not a
49
- * re-skinned `Checkbox` — a row/column of plain buttons with
50
- * `role="checkbox"` and no separate label; contiguous buttons touch and
51
- * share borders (`appearance="segmented"`, the default) or sit apart with
52
- * their own rounded corners (`appearance="separate"`).
49
+ * `orientation`. `appearance="segmented"` or `"separate"` is a separate
50
+ * rendering path — not a re-skinned `Checkbox` — a row/column of plain
51
+ * buttons with `role="checkbox"` and no separate label; contiguous buttons
52
+ * touch and share borders (`segmented`) or sit apart with their own rounded
53
+ * corners (`separate`). One prop rather than a
54
+ * `variant` + `appearance` pair, so "appearance without buttons" — which
55
+ * used to mean nothing — cannot be written.
53
56
  *
54
- * Form submission only happens when `name` is set. In the default variant
57
+ * Form submission only happens when `name` is set. With `control`
55
58
  * that means each underlying `Checkbox` gets `name`, so one hidden native
56
- * checkbox input is rendered per option; in the button variant the group
59
+ * checkbox input is rendered per option; with the button appearances the group
57
60
  * itself renders one hidden `<input type="checkbox">` per *currently
58
61
  * selected* value.
59
62
  *
@@ -68,8 +71,7 @@ const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
68
71
  options,
69
72
  onValueChange,
70
73
  orientation = "vertical",
71
- variant = "default",
72
- appearance = "segmented",
74
+ appearance = "control",
73
75
  name,
74
76
  disabled,
75
77
  onBlur,
@@ -111,7 +113,7 @@ const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
111
113
  }
112
114
  };
113
115
 
114
- if (variant === "button") {
116
+ if (appearance === "segmented" || appearance === "separate") {
115
117
  const isVertical = orientation === "vertical";
116
118
  const isSegmented = appearance === "segmented";
117
119
 
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import type { HTMLAttributes, ReactNode } from "react";
4
- import { Button } from "../../Button";
4
+ import { Button, type ButtonVariant } from "../../Button";
5
5
 
6
6
  export type Action = {
7
7
  id: string;
@@ -9,7 +9,7 @@ export type Action = {
9
9
  icon?: ReactNode;
10
10
  onClick: () => void;
11
11
  disabled?: boolean;
12
- variant?: "solid" | "outline" | "ghost" | "link";
12
+ variant?: ButtonVariant;
13
13
  };
14
14
 
15
15
  export type ToolbarProps = HTMLAttributes<HTMLDivElement> & {
@@ -1,14 +1,15 @@
1
1
  "use client";
2
2
 
3
3
  import { Label } from "@sixthshift/design-system/label";
4
- import { Message } from "@sixthshift/design-system/message";
4
+ import { Message, type MessageIntentName } from "@sixthshift/design-system/message";
5
5
  import { Muted } from "@sixthshift/design-system/muted";
6
6
  import { cn } from "@sixthshift/design-system/utils";
7
7
  import * as React from "react";
8
8
 
9
9
  export type FormFieldFeedback = {
10
10
  message: string;
11
- intent: "danger" | "success" | "warning";
11
+ /** Message's intents, less `neutral` — feedback always reports an outcome. */
12
+ intent: Exclude<MessageIntentName, "neutral">;
12
13
  };
13
14
 
14
15
  export type FormFieldProps = React.HTMLAttributes<HTMLDivElement> & {
@@ -16,15 +16,15 @@ const iconSlotStyles = `absolute top-1/2 -translate-y-1/2 text-(--input-icon-fg)
16
16
  *
17
17
  * Renders a plain `<input>` unless `iconLeft` or `iconRight` is supplied, in
18
18
  * which case it wraps the input in a relatively positioned container so an
19
- * icon — or an icon-styled button, as `SearchInput` uses for its clear
20
- * button — can sit inside the field's padding without affecting layout.
19
+ * icon — or an icon-styled button — can sit inside the field's padding
20
+ * without affecting layout.
21
21
  *
22
22
  * Controlled or uncontrolled exactly like a native input (`value`/`onChange`
23
23
  * or `defaultValue`), and `type` is forwarded as-is, so `type="password"`,
24
24
  * `type="number"`, etc. all work.
25
25
  *
26
26
  * Meant to be wrapped by `FormField` for label, description and validation
27
- * wiring, or by `SearchInput` for a search affordance.
27
+ * wiring. For search, use `SearchInput`, which draws its own field.
28
28
  */
29
29
  const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, iconLeft, iconRight, ...props }, ref) => {
30
30
  if (iconLeft || iconRight) {
@@ -47,7 +47,7 @@ export const Markdown = React.forwardRef<HTMLDivElement, MarkdownProps>(({ child
47
47
  ul: ({ children }) => <ul className="mt-2 flex list-disc flex-col gap-1 pl-5">{children}</ul>,
48
48
  ol: ({ children }) => <ol className="mt-2 flex list-decimal flex-col gap-1 pl-5">{children}</ol>,
49
49
  li: ({ children }) => <li className="text-fg-default text-sm">{children}</li>,
50
- code: ({ children }) => <code className="rounded bg-bg-subtle px-1.5 py-0.5 font-mono text-fg-emphasis text-xs">{children}</code>,
50
+ code: ({ children }) => <code className="rounded-sm bg-bg-subtle px-1.5 py-0.5 font-mono text-fg-emphasis text-xs">{children}</code>,
51
51
  pre: ({ children }) => <pre className="mt-2 overflow-x-auto rounded-md bg-bg-subtle p-3 font-mono text-xs">{children}</pre>,
52
52
  }}
53
53
  >
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * Message recipe — the component-token layer for Message.
3
3
  *
4
- * Message has one colour-bearing variant, `intent`, with four values. Each
5
- * cell sets border + bg + fg together, exactly as the old `cva` `variants.intent`
6
- * map did — this file is that map, moved to CSS so a consumer can re-point a
7
- * cell or add an intent the library never shipped. `size` is pure geometry
8
- * (padding, gap, font-size) and stays in Message.tsx's `cva` call; nothing
9
- * here concerns it.
4
+ * Message has one colour-bearing axis, `intent`, and one treatment: the
5
+ * family's tint, bordered. So the whole recipe is a single cell reading the
6
+ * intent slots from src/theming/intents.css — an intent a consumer adds there
7
+ * reaches Message with no change here. `size` is pure geometry (padding, gap,
8
+ * font-size) and stays in Message.tsx's `cva` call; nothing here concerns it.
10
9
  *
11
10
  * The grammar is `--message-{context}`, context in {bg, fg, border}. Intent
12
11
  * never appears in a token name — see button.css's header for why: it would
@@ -18,30 +17,16 @@
18
17
  */
19
18
 
20
19
  @layer components {
21
- /* The floor. Applies when no cell below matches — an intent a consumer has
22
- added but not yet styled still renders as a plain, legible message rather
23
- than something broken. */
24
20
  .message {
25
- --message-bg: var(--bg-normal);
26
- --message-fg: var(--fg-normal);
27
- --message-border: var(--border-normal);
28
- }
29
-
30
- .message[data-intent="success"] {
31
- --message-bg: var(--bg-success-subtle);
32
- --message-fg: var(--fg-on-success-subtle);
33
- --message-border: var(--border-success);
21
+ --message-bg: var(--intent-tint-bg);
22
+ --message-fg: var(--intent-tint-fg);
23
+ --message-border: var(--intent-border);
34
24
  }
35
25
 
36
- .message[data-intent="warning"] {
37
- --message-bg: var(--bg-warning-subtle);
38
- --message-fg: var(--fg-on-warning-subtle);
39
- --message-border: var(--border-warning);
40
- }
41
-
42
- .message[data-intent="danger"] {
43
- --message-bg: var(--bg-danger-subtle);
44
- --message-fg: var(--fg-on-danger-subtle);
45
- --message-border: var(--border-danger);
26
+ /* A neutral message sits on the resting surface, not the grey tint: it is a
27
+ plain bordered note, and a grey wash reads as disabled. The one place a
28
+ recipe overrides a slot for a single intent. */
29
+ .message[data-intent="neutral"] {
30
+ --message-bg: var(--bg-normal);
46
31
  }
47
32
  }
@@ -2,10 +2,18 @@ import { Caption } from "@sixthshift/design-system/caption";
2
2
  import { cn } from "@sixthshift/design-system/utils";
3
3
  import * as React from "react";
4
4
 
5
+ export type ProgressBarIntentName = "neutral" | "brand" | "danger" | "success" | "warning";
6
+ /** Widened, like every intent prop: a consumer adds an intent in CSS, not in a release. */
7
+ export type ProgressBarIntent = ProgressBarIntentName | (string & {});
8
+
5
9
  export type ProgressBarProps = {
6
10
  completed: number;
7
11
  total: number;
8
12
  showFraction?: boolean;
13
+ /** The fill's colour family. Defaults to `brand`. */
14
+ intent?: ProgressBarIntent | undefined;
15
+ /** Name of a brand the theme defines, rendered as `data-brand`. Omitted, the bar follows the nearest `data-brand` ancestor. */
16
+ brand?: string | undefined;
9
17
  /** Accessible name for the progress bar. */
10
18
  label?: string;
11
19
  className?: string;
@@ -24,9 +32,13 @@ export type ProgressBarProps = {
24
32
  * invalid CSS or an impossible reported value — though the fraction text
25
33
  * still shows the raw, unclamped numbers. When `total` is not positive the
26
34
  * bar renders as indeterminate: no `aria-valuenow`/`aria-valuemax`, 0% fill.
35
+ *
36
+ * `intent` colours the fill and defaults to `brand`: progress is not an
37
+ * outcome, so a bar at 40% is not `success`. Pass `success` when completion
38
+ * really is the good news, or `danger` for a quota running out.
27
39
  */
28
40
  export const ProgressBar = React.forwardRef<HTMLDivElement, ProgressBarProps>(
29
- ({ completed, total, showFraction = true, label = "Progress", className }, ref) => {
41
+ ({ completed, total, intent = "brand", brand, showFraction = true, label = "Progress", className }, ref) => {
30
42
  // A caller can hand us anything; the bar must not render an out-of-range
31
43
  // width (a negative `width` is invalid CSS) or report impossible progress.
32
44
  const isMeasurable = total > 0;
@@ -34,7 +46,7 @@ export const ProgressBar = React.forwardRef<HTMLDivElement, ProgressBarProps>(
34
46
  const percentage = isMeasurable ? (clamped / total) * 100 : 0;
35
47
 
36
48
  return (
37
- <div ref={ref} className={cn("flex items-center gap-3", className)}>
49
+ <div ref={ref} data-intent={intent} data-brand={brand} className={cn("progress-bar flex items-center gap-3", className)}>
38
50
  <div
39
51
  role="progressbar"
40
52
  aria-label={label}
@@ -44,9 +56,9 @@ export const ProgressBar = React.forwardRef<HTMLDivElement, ProgressBarProps>(
44
56
  // total there is no meaningful progress to report.
45
57
  aria-valuenow={isMeasurable ? clamped : undefined}
46
58
  aria-valuetext={isMeasurable ? `${clamped} of ${total}` : undefined}
47
- className="h-2 flex-1 overflow-hidden rounded-full bg-bg-strong"
59
+ className="h-2 flex-1 overflow-hidden rounded-full bg-(--progress-bar-track-bg)"
48
60
  >
49
- <div className="h-full rounded-full bg-fg-success transition-all" style={{ width: `${percentage}%` }} />
61
+ <div className="h-full rounded-full bg-(--progress-bar-fill-bg) transition-all" style={{ width: `${percentage}%` }} />
50
62
  </div>
51
63
  {showFraction && (
52
64
  <Caption className="shrink-0">
@@ -1 +1 @@
1
- export { ProgressBar, type ProgressBarProps } from "./ProgressBar";
1
+ export { ProgressBar, type ProgressBarIntent, type ProgressBarIntentName, type ProgressBarProps } from "./ProgressBar";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ProgressBar recipe — the component-token layer for ProgressBar.
3
+ *
4
+ * Two parts: the track, which is the same grey in every intent, and the fill,
5
+ * which paints with the intent slots from src/theming/intents.css. The fill
6
+ * reads `--intent-fg`, the family's colour on a resting surface, rather than
7
+ * the solid slot: `--intent-solid-bg` for `neutral` is `bg-strong`, which is
8
+ * the track itself, so a neutral bar would vanish. `neutral` is the one
9
+ * override: its `--intent-fg` is `fg-normal`, which barely separates from a
10
+ * `bg-strong` track in either mode, so it takes the mid-grey `fg-subtle`,
11
+ * which separates from both the track and the page around it. (`fg-on-strong`
12
+ * contrasts with the track but matches the page, and reads as a gap.)
13
+ *
14
+ * The grammar is `--progress-bar-{part}-{context}`. Both tokens live on the
15
+ * root, which carries `data-intent`, and the fill reads them by inheritance.
16
+ *
17
+ * `@layer components` is load-bearing, not tidiness — see button.recipe.css.
18
+ */
19
+
20
+ @layer components {
21
+ .progress-bar {
22
+ --progress-bar-track-bg: var(--bg-strong);
23
+ --progress-bar-fill-bg: var(--intent-fg);
24
+ }
25
+
26
+ .progress-bar[data-intent="neutral"] {
27
+ --progress-bar-fill-bg: var(--fg-subtle);
28
+ }
29
+ }
@@ -27,10 +27,11 @@ export type RadioButtonGroupProps = Omit<React.HTMLAttributes<HTMLDivElement>, "
27
27
  onValueChange?: (value: string) => void;
28
28
  /** Layout orientation */
29
29
  orientation?: "vertical" | "horizontal";
30
- /** Visual variant */
31
- variant?: "default" | "button";
32
- /** Button appearance (only applies when variant="button") */
33
- appearance?: "segmented" | "separate";
30
+ /**
31
+ * How the options render: `control` (the default) is one `RadioButton` per option;
32
+ * `segmented` and `separate` are a row or column of buttons, joined or apart.
33
+ */
34
+ appearance?: "control" | "segmented" | "separate";
34
35
  /** Input name for form submission */
35
36
  name?: string;
36
37
  /** Disable all options */
@@ -44,21 +45,22 @@ export type RadioButtonGroupProps = Omit<React.HTMLAttributes<HTMLDivElement>, "
44
45
  * uncontrolled `string` selection, via the `value`/`defaultValue`/
45
46
  * `onValueChange` triad (`useControllableState`).
46
47
  *
47
- * `variant="default"` (the default) renders one `RadioButton` per option.
48
- * `variant="button"` is a separate rendering path — not a re-skinned
49
- * `RadioButton` — a row/column of plain buttons with `role="radio"`;
50
- * contiguous buttons touch and share borders (`appearance="segmented"`, the
51
- * default) or sit apart with their own rounded corners
52
- * (`appearance="separate"`).
48
+ * `appearance="control"` (the default) renders one `RadioButton` per option.
49
+ * `appearance="segmented"` or `"separate"` is a separate rendering path — not
50
+ * a re-skinned `RadioButton` — a row/column of plain buttons with
51
+ * `role="radio"`; contiguous buttons touch and share borders (`segmented`) or
52
+ * sit apart with their own rounded corners (`separate`). One prop rather than
53
+ * a `variant` + `appearance` pair, so "appearance without buttons" — which
54
+ * used to mean nothing — cannot be written.
53
55
  *
54
56
  * Implements the WAI-ARIA radiogroup keyboard pattern: the group is one tab
55
57
  * stop, landing on the checked option (or the first enabled option if none
56
58
  * is checked), and arrow/Home/End keys move focus *and* selection between
57
59
  * enabled options, wrapping at the ends.
58
60
  *
59
- * Form submission only happens when `name` is set. In the default variant
61
+ * Form submission only happens when `name` is set. With `control`
60
62
  * that means each underlying `RadioButton` gets `name`, so one hidden
61
- * native radio input is rendered per option; in the button variant the
63
+ * native radio input is rendered per option; with the button appearances the
62
64
  * group itself renders a single hidden `<input type="hidden">` carrying the
63
65
  * selected value.
64
66
  *
@@ -73,8 +75,7 @@ const RadioButtonGroup = React.forwardRef<HTMLDivElement, RadioButtonGroupProps>
73
75
  options,
74
76
  onValueChange,
75
77
  orientation = "vertical",
76
- variant = "default",
77
- appearance = "segmented",
78
+ appearance = "control",
78
79
  name,
79
80
  disabled,
80
81
  onBlur,
@@ -111,8 +112,8 @@ const RadioButtonGroup = React.forwardRef<HTMLDivElement, RadioButtonGroupProps>
111
112
  const handleOptionSelect = (optionValue: string) => {
112
113
  // Re-selecting the current option reports nothing, as a native radio does
113
114
  // not. The guard lives here as well as in `RadioButton` because the button
114
- // variant is a separate rendering path that never goes through it — and
115
- // the two variants must not disagree about this. `ToggleGroup` in
115
+ // appearances are a separate rendering path that never goes through it —
116
+ // and the two paths must not disagree about this. `ToggleGroup` in
116
117
  // `"single"` mode already works this way.
117
118
  if (optionValue === value) return;
118
119
  setValue(optionValue);
@@ -139,7 +140,7 @@ const RadioButtonGroup = React.forwardRef<HTMLDivElement, RadioButtonGroupProps>
139
140
  const hasCheckedOption = options.some((option) => option.value === value);
140
141
  const tabStopValue = hasCheckedOption ? value : firstEnabledValue;
141
142
 
142
- if (variant === "button") {
143
+ if (appearance === "segmented" || appearance === "separate") {
143
144
  const isVertical = orientation === "vertical";
144
145
  const isSegmented = appearance === "segmented";
145
146
 
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
 
3
- import { Input } from "@sixthshift/design-system/input";
4
3
  import { cn } from "@sixthshift/design-system/utils";
5
4
  import { Search, X } from "lucide-react";
6
5
  import * as React from "react";
@@ -14,12 +13,19 @@ export type SearchInputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>,
14
13
  clearLabel?: string;
15
14
  };
16
15
 
16
+ /** The icon slots, duplicated from Input's. */
17
+ const iconSlotStyles = `absolute top-1/2 -translate-y-1/2 text-(--search-input-icon-fg)
18
+ [&_svg]:h-4 [&_svg]:w-4
19
+ [&_button]:flex [&_button]:cursor-pointer [&_button]:appearance-none
20
+ [&_button]:border-0 [&_button]:bg-transparent`;
21
+
17
22
  /**
18
23
  * Text input specialised for search: a search icon fixed on the left and,
19
24
  * once there is a value, a clear button on the right.
20
25
  *
21
- * Wraps `Input`, using its `iconLeft`/`iconRight` slots, rather than
22
- * reimplementing the field. Always controlled — `value` is required and
26
+ * Its own field, not a wrapped `Input`: the markup and classes are duplicated
27
+ * from Input on purpose, so importing SearchInput pulls in nothing else and
28
+ * its colours are its own `--search-input-*` tokens. Always controlled — `value` is required and
23
29
  * there is no `defaultValue`/uncontrolled mode. `onValueChange` receives the
24
30
  * string value directly; the native `onChange` keeps its event signature and
25
31
  * fires too if passed. Clearing calls `onClear` if provided, otherwise calls
@@ -36,25 +42,33 @@ const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
36
42
  };
37
43
 
38
44
  return (
39
- <Input
40
- ref={ref}
41
- type="text"
42
- value={value}
43
- onChange={(e) => {
44
- onValueChange(e.target.value);
45
- onChange?.(e);
46
- }}
47
- iconLeft={<Search />}
48
- iconRight={
49
- value ? (
50
- <button type="button" onClick={handleClear} aria-label={clearLabel} className="rounded hover:bg-(--search-input-clear-bg-hovered)">
45
+ <div className={cn("search-input relative h-9 w-full text-sm", className)}>
46
+ <span className={cn(iconSlotStyles, "left-3")}>
47
+ <Search />
48
+ </span>
49
+ <input
50
+ ref={ref}
51
+ type="text"
52
+ value={value}
53
+ onChange={(e) => {
54
+ onValueChange(e.target.value);
55
+ onChange?.(e);
56
+ }}
57
+ className={cn(
58
+ "border-(color:--search-input-border) focus-visible:ring-(color:--search-input-ring) flex h-full w-full rounded-md border bg-(--search-input-bg) py-1 text-[length:inherit] shadow-xs transition-colors placeholder:text-(--search-input-placeholder-fg) focus-visible:outline-hidden focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50",
59
+ "pl-9",
60
+ value ? "pr-9" : "pr-3"
61
+ )}
62
+ {...props}
63
+ />
64
+ {value && (
65
+ <span className={cn(iconSlotStyles, "right-3")}>
66
+ <button type="button" onClick={handleClear} aria-label={clearLabel} className="rounded-sm hover:bg-(--search-input-clear-bg-hovered)">
51
67
  <X />
52
68
  </button>
53
- ) : undefined
54
- }
55
- className={cn("search-input", className)}
56
- {...props}
57
- />
69
+ </span>
70
+ )}
71
+ </div>
58
72
  );
59
73
  }
60
74
  );