@codecademy/gamut 68.6.0 → 68.6.1-alpha.e6c390.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-tools/.claude-plugin/marketplace.json +16 -0
- package/agent-tools/.claude-plugin/plugin.json +7 -0
- package/agent-tools/.cursor-plugin/plugin.json +7 -0
- package/agent-tools/DESIGN.Codecademy.md +648 -0
- package/agent-tools/DESIGN.LXStudio.md +460 -0
- package/agent-tools/DESIGN.Percipio.md +463 -0
- package/agent-tools/DESIGN.md +1 -0
- package/agent-tools/agents/.gitkeep +0 -0
- package/agent-tools/commands/gamut-review.md +170 -0
- package/agent-tools/guidelines/components/buttons.md +44 -0
- package/agent-tools/guidelines/components/overview.md +44 -0
- package/agent-tools/guidelines/foundations/color.md +86 -0
- package/agent-tools/guidelines/foundations/modes.md +47 -0
- package/agent-tools/guidelines/foundations/spacing.md +66 -0
- package/agent-tools/guidelines/foundations/typography.md +50 -0
- package/agent-tools/guidelines/overview.md +38 -0
- package/agent-tools/guidelines/setup.md +42 -0
- package/agent-tools/rules/accessibility.mdc +69 -0
- package/agent-tools/skills/gamut-accessibility/SKILL.md +239 -0
- package/agent-tools/skills/gamut-color-mode/SKILL.md +99 -0
- package/agent-tools/skills/gamut-system-props/SKILL.md +181 -0
- package/agent-tools/skills/gamut-testing/SKILL.md +181 -0
- package/agent-tools/skills/gamut-theming/SKILL.md +115 -0
- package/agent-tools/skills/gamut-typography/SKILL.md +123 -0
- package/bin/commands/plugin/install.mjs +173 -0
- package/bin/commands/plugin/list.mjs +105 -0
- package/bin/commands/plugin/remove.mjs +116 -0
- package/bin/commands/plugin/update.mjs +49 -0
- package/bin/gamut.mjs +92 -0
- package/bin/lib/claude.mjs +52 -0
- package/bin/lib/cursor.mjs +40 -0
- package/bin/lib/figma.mjs +49 -0
- package/bin/lib/resolve-plugin-dir.mjs +38 -0
- package/bin/lib/run-command.mjs +22 -0
- package/package.json +11 -8
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamut-accessibility
|
|
3
|
+
description: Use this skill when implementing or auditing accessibility in Gamut UIs — interactive widgets, forms, focus and keyboard flows, live regions, ARIA, or contrast — including fixes for screen readers, WCAG issues, or Gamut + React Aria patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut Accessibility
|
|
7
|
+
|
|
8
|
+
Source: `@codecademy/gamut` — interactive components wrap `react-aria-components`, `@react-aria/interactions`, and `react-focus-on`.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## General rules
|
|
13
|
+
|
|
14
|
+
Use ARIA sparingly and only when it's the best option available.
|
|
15
|
+
|
|
16
|
+
### Prefer HTML over ARIA
|
|
17
|
+
|
|
18
|
+
Unnecessary ARIA can cause harm. If a native HTML element or attribute with the semantics and behavior you need already exists, use it. Reach for ARIA only when native HTML is genuinely insufficient for the pattern.
|
|
19
|
+
|
|
20
|
+
### A Role is a Promise
|
|
21
|
+
|
|
22
|
+
ARIA roles modify the accessibility tree and _imply_ behavior. Always ensure that the implied keyboard behavior, focusability, and interactivity exists when a role is used.
|
|
23
|
+
|
|
24
|
+
### ARIA can both cloak and enhance
|
|
25
|
+
|
|
26
|
+
ARIA can augment native semantics (`aria-pressed` on a `<button>`) or override them entirely (`role="menuitem"` on an `<a>`). Both capabilities are powerful and dangerous. Override only when native HTML genuinely doesn't fit the pattern; when augmenting, don't contradict the native semantics.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Align accessible names with visible copy
|
|
30
|
+
|
|
31
|
+
Prefer wiring names through visible text and native `<label>` / control text / `alt` over using `aria-label`. Point `aria-labelledby` at the visible heading or label that should define the name if it's not possible to name elements from their content. Use bare `aria-label` when there is no suitable visible label.
|
|
32
|
+
|
|
33
|
+
### Treat missing visible labels as a design smell
|
|
34
|
+
|
|
35
|
+
When there is no visible text for a nameable element, consider this a sign that the content design could be improved, but not a requirement that it is changed. This is not an accessibility violation.
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<!-- smell: this list has no conceptual name, so we have to create one using ARIA -->
|
|
39
|
+
<ul aria-label="List heading">
|
|
40
|
+
<li>...</li>
|
|
41
|
+
</ul>
|
|
42
|
+
|
|
43
|
+
<!-- better: the list's name is visible and can be used for its accessible name -->
|
|
44
|
+
<h2 id="list-name">List heading</h2>
|
|
45
|
+
<ul aria-labelledby="list-name">
|
|
46
|
+
<li>...</li>
|
|
47
|
+
</ul>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## How Gamut handles accessibility
|
|
53
|
+
|
|
54
|
+
Gamut wraps React Aria for most interactive widget primitives. Roving tabindex, keyboard event handling, and ARIA attribute management are implemented for you in `<Tabs>`, `<Dialog>`, `<Modal>`, and form components. The developer's responsibilities are: supply accessible names, wire labels to inputs, and avoid overriding what the library already provides.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Component reference
|
|
59
|
+
|
|
60
|
+
### Button / IconButton
|
|
61
|
+
|
|
62
|
+
`<Button>` renders a semantic `<button>` — no `role` needed. For icon-only buttons use `<IconButton>` with a `tip` prop; `tip` becomes the button's `aria-label`. Do not use `<div onClick>` or `<a>` without `href` for actions.
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
// correct
|
|
66
|
+
<IconButton icon={DeleteIcon} tip="Delete item" onClick={handleDelete} />
|
|
67
|
+
|
|
68
|
+
// wrong — no accessible name, no keyboard semantics
|
|
69
|
+
<div onClick={handleDelete}><DeleteIcon /></div>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Dialog / Modal
|
|
73
|
+
|
|
74
|
+
Both use `<FocusTrap>` (`react-focus-on`) internally. Focus locks to the dialog on open and returns to the trigger on close. Escape closes the dialog automatically.
|
|
75
|
+
|
|
76
|
+
Always provide an accessible name. **Prefer `aria-labelledby`** when a visible heading or title defines the dialog name; use **`aria-label`** only when there is no suitable visible title string.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
<Dialog aria-labelledby="confirm-title">
|
|
80
|
+
<h2 id="confirm-title">Confirm deletion</h2>
|
|
81
|
+
</Dialog>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Alert
|
|
85
|
+
|
|
86
|
+
Renders with `aria-live="polite"` and `role="status"` by default. Override with `aria-live="assertive"` only for time-sensitive errors requiring immediate interruption. Do not nest `<Alert>` inside another live region.
|
|
87
|
+
|
|
88
|
+
### Tabs
|
|
89
|
+
|
|
90
|
+
Built on `react-aria-components`. Follows the [ARIA Tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/): arrow keys navigate tabs, Tab moves focus into the active panel, Home/End jump to first/last tab. The tablist is a composite — only the active tab is in the tab sequence (roving tabindex). No manual `aria-selected` or keyboard handling needed.
|
|
91
|
+
|
|
92
|
+
### Forms
|
|
93
|
+
|
|
94
|
+
`<FormGroup>` + `<FormGroupLabel>` + input element is the complete accessible pattern:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
<FormGroup htmlFor="email-input" description="Used for login" error={errors.email}>
|
|
98
|
+
<FormGroupLabel htmlFor="email-input">Email</FormGroupLabel>
|
|
99
|
+
<Input id="email-input" type="email" />
|
|
100
|
+
</FormGroup>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- `htmlFor` on `<FormGroupLabel>` and `id` on the input must match
|
|
104
|
+
- `error` prop on `<FormGroup>` renders into an `aria-live="assertive"` region automatically
|
|
105
|
+
- `description` prop renders into an `aria-live="polite"` region automatically
|
|
106
|
+
- Do not add `aria-describedby` or `aria-errormessage` manually — `<FormGroup>` manages these
|
|
107
|
+
|
|
108
|
+
**Checkbox / Radio**: `htmlFor` is required; the input is visually hidden via `screenReaderOnly` from `@codecademy/gamut-styles` but remains in the accessibility tree.
|
|
109
|
+
|
|
110
|
+
### InfoTip
|
|
111
|
+
|
|
112
|
+
Unlike `<IconButton>`, `<InfoTip>` has no automatic label fallback. Always provide `ariaLabel` or `ariaLabelledby` (camelCase props).
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
<InfoTip ariaLabel="More information about billing" />
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### SkipToContent
|
|
119
|
+
|
|
120
|
+
Include `<SkipToContent>` as the first focusable element in the page shell. The main content region must have a matching `id` for the skip link to target.
|
|
121
|
+
|
|
122
|
+
### Text (screen-reader-only)
|
|
123
|
+
|
|
124
|
+
`<Text screenreader>` is the supported pattern for visually hidden but announced content. `<HiddenText>` is deprecated.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
<Text screenreader>Loading results…</Text>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Focus management
|
|
133
|
+
|
|
134
|
+
`<FocusTrap>` is available for custom overlay patterns not covered by `<Dialog>` or `<Modal>`.
|
|
135
|
+
|
|
136
|
+
Key props:
|
|
137
|
+
- `active` — enable/disable the trap dynamically
|
|
138
|
+
- `onEscapeKey` — close handler
|
|
139
|
+
- `onClickOutside` — dismiss on outside click
|
|
140
|
+
- `allowPageInteraction` — permit scrolling outside the trap without closing
|
|
141
|
+
|
|
142
|
+
Always return focus to the trigger on close. React Aria components do this automatically; custom implementations must store a ref to the trigger and call `.focus()` on unmount.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Composite widgets and managed focus
|
|
147
|
+
|
|
148
|
+
ARIA composite roles (`listbox`, `menu`, `tree`, `grid`, `tablist`) use **managed focus**: only one element in the composite is in the tab sequence at a time. Tab moves focus to the next element outside the composite; arrow keys move focus within it.
|
|
149
|
+
|
|
150
|
+
Implementation pattern — roving tabindex:
|
|
151
|
+
- Set `tabIndex={0}` on the currently active item
|
|
152
|
+
- Set `tabIndex={-1}` on all other items
|
|
153
|
+
- On arrow key, update which item holds `tabIndex={0}` and call `.focus()` on it
|
|
154
|
+
|
|
155
|
+
Gamut's `<Tabs>` implements this via React Aria. If you build a custom composite widget, you must implement roving tabindex manually. A flat `tabIndex={0}` on every item is wrong — it puts every item in the sequential tab order, which is not the composite pattern.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Device-independent events
|
|
160
|
+
|
|
161
|
+
Use `click` for activation, not `mousedown`. `click` fires for both pointer and keyboard (Space/Enter on native buttons and links). `mousedown` is pointer-only and silently breaks keyboard access.
|
|
162
|
+
|
|
163
|
+
For custom elements with `role="button"`, `click` alone is not sufficient — it only fires on keyboard when the element is a native `<button>` or `<a href>`. You must also handle `keydown` for Space and Enter explicitly:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
167
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
handleActivation();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This is another reason to use `<Button>` — it handles this correctly and you don't.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Live regions
|
|
179
|
+
|
|
180
|
+
| Scenario | Pattern |
|
|
181
|
+
|---|---|
|
|
182
|
+
| Status updates, non-critical notifications | `aria-live="polite"` |
|
|
183
|
+
| Form validation errors on submit | `aria-live="assertive"` |
|
|
184
|
+
| Frequently updating counts or progress | `aria-live="polite"` + `aria-atomic="true"` |
|
|
185
|
+
|
|
186
|
+
Inject live regions into the DOM before they need to announce. A region added simultaneously with its first announcement may be ignored by some assistive technologies.
|
|
187
|
+
|
|
188
|
+
`<FormGroup>` already uses `aria-live="assertive"` for the `error` prop. Don't elevate unrelated inline errors to assertive — reserve it for interruptions the user didn't directly trigger.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## ARIA authoring rules
|
|
193
|
+
|
|
194
|
+
- **No redundant roles**: don't set `role="button"` on `<button>` or `role="heading"` on `<h2>`
|
|
195
|
+
- **aria-hidden cascades**: placing `aria-hidden="true"` on a parent removes the entire subtree from the accessibility tree, including focusable descendants — never put it on an ancestor of a focusable element
|
|
196
|
+
- **role="presentation" and aria-hidden on focusable elements**: both are prohibited on elements that can receive focus — they remove semantics while leaving the element keyboard-reachable, producing an operable but unnamed control
|
|
197
|
+
- **Labelling vs describing**: `aria-label` / `aria-labelledby` name the control. `aria-describedby` provides supplementary context. Both can coexist on the same element
|
|
198
|
+
- **Required fields**: use `aria-required="true"` or the HTML `required` attribute. Visual asterisks must have an explanatory text string visible on the page; the asterisk glyph itself should carry `aria-hidden="true"` — `<FormGroupLabel>` already handles this
|
|
199
|
+
- **display:none vs aria-hidden**: elements with `display:none` are already removed from the accessibility tree; adding `aria-hidden` is redundant. Use `aria-hidden="true"` only when an element is visually present but should be hidden from assistive technology
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Color and contrast
|
|
204
|
+
|
|
205
|
+
Gamut's `ColorMode` and semantic color tokens are designed to meet WCAG AA (4.5:1 for normal text, 3:1 for large text and non-text UI components). Hardcoding hex values bypasses this guarantee and breaks in dark mode. See the `gamut-color-mode` skill for semantic token usage.
|
|
206
|
+
|
|
207
|
+
Non-text contrast (focus indicators, input borders, icon-only controls) requires a minimum 3:1 ratio against adjacent colors per WCAG 1.4.11.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Testing checklist
|
|
212
|
+
|
|
213
|
+
- [ ] Full keyboard navigation: every interactive element reachable and operable without a mouse
|
|
214
|
+
- [ ] Focus is always visible and never lost or unexpectedly trapped
|
|
215
|
+
- [ ] Dialogs trap focus correctly; Escape closes; focus returns to the trigger
|
|
216
|
+
- [ ] Composite widgets (tabs, menus, listboxes) use arrow keys internally, not Tab
|
|
217
|
+
- [ ] All form inputs have programmatically associated labels (not placeholder-only)
|
|
218
|
+
- [ ] Form errors are announced via live region
|
|
219
|
+
- [ ] Icon-only controls have accessible names
|
|
220
|
+
- [ ] No content relies solely on color to convey meaning
|
|
221
|
+
- [ ] Screen reader matrix: VoiceOver + Safari (iOS), VoiceOver + Chrome (macOS), NVDA + Chrome (Windows)
|
|
222
|
+
- [ ] 200% zoom: layout intact, no content overflow or disappearance
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Common anti-patterns
|
|
227
|
+
|
|
228
|
+
| Anti-pattern | Fix |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `<div onClick={…}>` for actions | `<Button>` |
|
|
231
|
+
| `placeholder` as the only label | `<FormGroupLabel>` with matching `htmlFor`/`id` |
|
|
232
|
+
| `aria-label` on a `<div>` with no role | Add a meaningful `role` or use a semantic element |
|
|
233
|
+
| `role="button"` without Space/Enter handlers | Use `<Button>`, or add `keydown` handler |
|
|
234
|
+
| `tabIndex={0}` on every item in a composite | Roving tabindex: `0` on active item, `-1` on rest |
|
|
235
|
+
| Tooltip as the only accessible name for a control | Set `aria-label` directly on the control as well |
|
|
236
|
+
| `aria-hidden="true"` on a focusable element | Also remove from tab order (`tabIndex={-1}`) or restructure |
|
|
237
|
+
| `mousedown` for activation | Use `click` |
|
|
238
|
+
| `outline: none` without a replacement | Use Gamut's built-in focus styles |
|
|
239
|
+
| Multiple `aria-live` regions for the same content stream | One region per logical stream; reuse it across updates |
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamut-color-mode
|
|
3
|
+
description: Use this skill when implementing light/dark behavior, semantic color aliases, the Background component for contrast-safe surfaces, or color-mode hooks in Gamut — including replacing hardcoded hex, fixing mode bugs, or reviewing color usage.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut ColorMode
|
|
7
|
+
|
|
8
|
+
Source: `@codecademy/gamut-styles` — [`ColorMode.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-styles/src/ColorMode.tsx)
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Gamut's color system uses **semantic aliases** instead of raw color tokens. This means components automatically adapt across light and dark modes without configuration.
|
|
13
|
+
|
|
14
|
+
### Semantic color aliases
|
|
15
|
+
|
|
16
|
+
| Alias | Purpose |
|
|
17
|
+
| ------------ | ------------------------------------------ |
|
|
18
|
+
| `text` | Standard text color for all type |
|
|
19
|
+
| `background` | Base background color |
|
|
20
|
+
| `primary` | Interactive elements with primary action |
|
|
21
|
+
| `secondary` | Interactive elements with secondary action |
|
|
22
|
+
|
|
23
|
+
**Key principle**: Always use these aliases in component styles — never hardcode specific color tokens like `navy-400` for anything that needs to change per mode.
|
|
24
|
+
|
|
25
|
+
**Storybook:** [Foundations / ColorMode](https://gamut.codecademy.com/?path=/docs-foundations-colormode--page) — interactive reference. [Meta / Best practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page) — semantic tokens, `css` / `variant` / `states`, and system props with ColorMode.
|
|
26
|
+
|
|
27
|
+
## `<ColorMode />`
|
|
28
|
+
|
|
29
|
+
Wraps content in a color mode context. Nest these to create scoped mode regions.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { ColorMode } from '@codecademy/gamut-styles';
|
|
33
|
+
|
|
34
|
+
// Explicit light or dark mode
|
|
35
|
+
const Page = ({ children }) => <ColorMode mode="light">{children}</ColorMode>;
|
|
36
|
+
|
|
37
|
+
// Follows the user's OS preference (prefers-color-scheme)
|
|
38
|
+
const Page = ({ children }) => <ColorMode mode="system">{children}</ColorMode>;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Props**: `mode="light" | "dark" | "system"`
|
|
42
|
+
|
|
43
|
+
## `<Background />`
|
|
44
|
+
|
|
45
|
+
Use `<Background>` — not raw `bg` prop — whenever you need a specific background color for a section (card, landing page, hero, etc.). It **automatically switches the color mode** to ensure the content inside meets contrast requirements.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { Background } from '@codecademy/gamut-styles';
|
|
49
|
+
|
|
50
|
+
// Single background — mode switches automatically if needed
|
|
51
|
+
const Card = ({ children }) => <Background bg="hyper">{children}</Background>;
|
|
52
|
+
|
|
53
|
+
// Nested backgrounds — each creates its own accessible color context
|
|
54
|
+
const Page = () => (
|
|
55
|
+
<Background bg="black">
|
|
56
|
+
<Background bg="paleGreen" />
|
|
57
|
+
</Background>
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `background-current` CSS variable
|
|
62
|
+
|
|
63
|
+
When `<Background>` is rendered, it sets a `background-current` CSS variable on the theme. Use this to reference an ancestor's background color (e.g. for simulating transparency or masking content).
|
|
64
|
+
|
|
65
|
+
## Color mode hooks
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import {
|
|
69
|
+
useColorModes,
|
|
70
|
+
useCurrentMode,
|
|
71
|
+
usePrefersDarkMode,
|
|
72
|
+
} from '@codecademy/gamut-styles';
|
|
73
|
+
|
|
74
|
+
// Returns [activeModeKey, currentModeColorMap, allModesConfig, getColorValue]
|
|
75
|
+
const [mode, modeColors, modes, getColorValue] = useColorModes();
|
|
76
|
+
|
|
77
|
+
// Returns just the active mode key: "light" | "dark"
|
|
78
|
+
const activeMode = useCurrentMode();
|
|
79
|
+
|
|
80
|
+
// Returns boolean from window.matchMedia('(prefers-color-scheme: dark)')
|
|
81
|
+
const prefersDark = usePrefersDarkMode();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Decision guide
|
|
85
|
+
|
|
86
|
+
| Need | Use |
|
|
87
|
+
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
88
|
+
| Set a page or section to a specific mode | `<ColorMode mode="light">`, `mode="dark"`, or `mode="system"` |
|
|
89
|
+
| Place content on a colored background with automatic contrast | `<Background bg="...">` |
|
|
90
|
+
| Read the current mode in JavaScript | `useCurrentMode()` |
|
|
91
|
+
| Access active mode, resolved semantic colors for that mode, all mode configs, and `getColorValue` | `useColorModes()` |
|
|
92
|
+
| Detect OS dark mode preference | `usePrefersDarkMode()` |
|
|
93
|
+
| Access full Emotion theme | `useTheme()` from `@emotion/react` |
|
|
94
|
+
|
|
95
|
+
## Common mistakes to avoid
|
|
96
|
+
|
|
97
|
+
- Do not use raw color tokens (e.g. `color: 'navy-400'`) for text, backgrounds, or borders that need to be accessible across modes — use semantic aliases instead.
|
|
98
|
+
- Do not use raw `bg` prop for colored section backgrounds that contain text or interactive elements — use `<Background>` so contrast is guaranteed.
|
|
99
|
+
- Do not manually toggle modes based on `usePrefersDarkMode()` — use `<ColorMode mode="system">` instead.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamut-system-props
|
|
3
|
+
description: Use this skill when building or refactoring styled Gamut components that need layout, spacing, color, border, background, typography, positioning, grid, flex, shadow, or responsive values from @codecademy/gamut-styles — including composing system prop groups with variance.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut System Props
|
|
7
|
+
|
|
8
|
+
Source: `@codecademy/gamut-styles` — [`variance/config.ts`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-styles/src/variance/config.ts)
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
System props are strongly-typed, theme-connected CSS prop groups from `@codecademy/gamut-styles`. They give styled components a consistent, responsive API. All props are built on top of `@codecademy/variance`. Semantic color props assume the subtree is under the correct **ColorMode** / **Background** context when those surfaces need to adapt — see [`gamut-color-mode`](../gamut-color-mode/SKILL.md).
|
|
13
|
+
|
|
14
|
+
Each prop group has:
|
|
15
|
+
|
|
16
|
+
- **`properties`**: The CSS properties it controls
|
|
17
|
+
- **`scale`**: Token scale it's restricted to (theme colors, spacing values, etc.)
|
|
18
|
+
- **`transform`**: Optional transform applied before output (e.g. `width={0.5}` → `width: 50%`)
|
|
19
|
+
|
|
20
|
+
## Basic usage
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import styled from '@emotion/styled';
|
|
24
|
+
import { system } from '@codecademy/gamut-styles';
|
|
25
|
+
|
|
26
|
+
// Apply a single group
|
|
27
|
+
const Box = styled.div(system.layout);
|
|
28
|
+
|
|
29
|
+
// Compose multiple groups
|
|
30
|
+
import { variance } from '@codecademy/variance';
|
|
31
|
+
|
|
32
|
+
const FlexBox = styled.div(
|
|
33
|
+
variance.compose(system.layout, system.flex, system.space)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
<FlexBox display="flex" p={16} gap={8} width="100%" />;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Prop groups
|
|
40
|
+
|
|
41
|
+
### `system.layout`
|
|
42
|
+
|
|
43
|
+
Controls dimensions, display, overflow, and container behavior.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
const Box = styled.div(system.layout);
|
|
47
|
+
|
|
48
|
+
<Box display="flex" width="50%" height="300px" verticalAlign="middle" />;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Key props: `display`, `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `overflow`, `overflowX`, `overflowY`, `verticalAlign`
|
|
52
|
+
|
|
53
|
+
### `system.space`
|
|
54
|
+
|
|
55
|
+
Margin and padding using the theme's spacing scale. Supports logical properties (switches based on `useLogicalProperties` in `<GamutProvider>`).
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
const Box = styled.div(system.space);
|
|
59
|
+
|
|
60
|
+
// Single value
|
|
61
|
+
<Box p={8} m={16} />;
|
|
62
|
+
|
|
63
|
+
// Responsive array syntax: [mobile, tablet, desktop]
|
|
64
|
+
<Box my={[16, 24, 32]} px={[8, 16]} />;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Key props: `p`, `pt`, `pr`, `pb`, `pl`, `px`, `py`, `m`, `mt`, `mr`, `mb`, `ml`, `mx`, `my`
|
|
68
|
+
|
|
69
|
+
### `system.color`
|
|
70
|
+
|
|
71
|
+
Foreground, background, and border colors restricted to the theme's color palette.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
const Box = styled.div(system.color);
|
|
75
|
+
|
|
76
|
+
<Box bg="navy" textColor="gray-100" borderColor="blue" />;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Key props: `bg` (background-color shorthand), `textColor`, `borderColor`
|
|
80
|
+
|
|
81
|
+
### `system.typography`
|
|
82
|
+
|
|
83
|
+
Text styling connected to theme typography scales.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
const Text = styled.p(system.typography);
|
|
87
|
+
|
|
88
|
+
<Text
|
|
89
|
+
fontSize={16}
|
|
90
|
+
fontFamily="accent"
|
|
91
|
+
textTransform="uppercase"
|
|
92
|
+
lineHeight={1.5}
|
|
93
|
+
/>;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Key props: `fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `textAlign`, `textTransform`, `textDecoration`, `letterSpacing`, `whiteSpace`
|
|
97
|
+
|
|
98
|
+
### `system.border`
|
|
99
|
+
|
|
100
|
+
Border width, style, radius, and color.
|
|
101
|
+
|
|
102
|
+
Key props: `border`, `borderTop`, `borderRight`, `borderBottom`, `borderLeft`, `borderRadius`, `borderWidth`, `borderStyle`
|
|
103
|
+
|
|
104
|
+
### `system.background`
|
|
105
|
+
|
|
106
|
+
Background image, size, position, and repeat (for images/patterns — use `system.color` for solid background colors).
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import myBg from './myBg.png';
|
|
110
|
+
|
|
111
|
+
const Box = styled.div(system.background);
|
|
112
|
+
|
|
113
|
+
<Box
|
|
114
|
+
background={`url(${myBg})`}
|
|
115
|
+
backgroundSize="cover"
|
|
116
|
+
backgroundPosition="center"
|
|
117
|
+
/>;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Key props: `background`, `backgroundImage`, `backgroundSize`, `backgroundPosition`, `backgroundRepeat`
|
|
121
|
+
|
|
122
|
+
### `system.flex`
|
|
123
|
+
|
|
124
|
+
Flexbox child and container properties.
|
|
125
|
+
|
|
126
|
+
Key props: `flex`, `flexDirection`, `flexWrap`, `flexGrow`, `flexShrink`, `flexBasis`, `alignItems`, `alignSelf`, `justifyContent`, `justifySelf`, `gap`, `rowGap`, `columnGap`
|
|
127
|
+
|
|
128
|
+
### `system.grid`
|
|
129
|
+
|
|
130
|
+
CSS Grid container and child properties.
|
|
131
|
+
|
|
132
|
+
Key props: `gridTemplateColumns`, `gridTemplateRows`, `gridTemplateAreas`, `gridColumn`, `gridRow`, `gridArea`, `gridAutoFlow`
|
|
133
|
+
|
|
134
|
+
### `system.positioning`
|
|
135
|
+
|
|
136
|
+
Position and offset properties.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
const Overlay = styled.div(variance.compose(system.layout, system.positioning));
|
|
140
|
+
|
|
141
|
+
<Overlay position="absolute" top={0} left={0} width="100%" height="100%" />;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Key props: `position`, `top`, `right`, `bottom`, `left`, `zIndex`
|
|
145
|
+
|
|
146
|
+
### `system.shadow`
|
|
147
|
+
|
|
148
|
+
Box and text shadow.
|
|
149
|
+
|
|
150
|
+
Key props: `boxShadow`, `textShadow`
|
|
151
|
+
|
|
152
|
+
## Responsive values
|
|
153
|
+
|
|
154
|
+
All system props accept an **array of values** for responsive breakpoints (Gamut uses a mobile-first approach):
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
// [mobile, tablet, desktop]
|
|
158
|
+
<Box width={['100%', '50%', '33%']} p={[8, 16, 24]} />
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Using `css()` for styled definitions
|
|
162
|
+
|
|
163
|
+
For static styles in styled components, use `css()` from `@codecademy/gamut-styles`:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
import { css } from '@codecademy/gamut-styles';
|
|
167
|
+
import styled from '@emotion/styled';
|
|
168
|
+
|
|
169
|
+
// Static color using raw token
|
|
170
|
+
const Box = styled.div(css({ bg: 'navy-400', p: 4 }));
|
|
171
|
+
|
|
172
|
+
// Semantic color (adapts to color mode)
|
|
173
|
+
const Text = styled.div(css({ color: 'primary', p: 4 }));
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Key principles
|
|
177
|
+
|
|
178
|
+
- Compose `system.*` groups via `variance.compose()` — don't apply multiple groups by chaining `styled.div(system.a)(system.b)`.
|
|
179
|
+
- Use `system.color` (semantic aliases) for colors that need to adapt to dark/light mode; use raw tokens only for colors that should stay fixed regardless of mode.
|
|
180
|
+
- Use `system.space` values that reference the spacing scale rather than arbitrary pixel values to maintain visual rhythm.
|
|
181
|
+
- For background images/patterns use `system.background`; for solid background colors that may switch mode use `<Background>` from ColorMode.
|