@codecademy/gamut 68.6.1 → 68.6.2-alpha.f8b396.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 +643 -0
- package/agent-tools/DESIGN.LXStudio.md +437 -0
- package/agent-tools/DESIGN.Percipio.md +433 -0
- package/agent-tools/DESIGN.md +1 -0
- package/agent-tools/agents/.gitkeep +0 -0
- package/agent-tools/rules/accessibility.mdc +78 -0
- package/agent-tools/skills/gamut-accessibility/SKILL.md +214 -0
- package/agent-tools/skills/gamut-buttons/SKILL.md +96 -0
- package/agent-tools/skills/gamut-color-mode/SKILL.md +257 -0
- package/agent-tools/skills/gamut-forms/SKILL.md +84 -0
- package/agent-tools/skills/gamut-layout/SKILL.md +109 -0
- package/agent-tools/skills/gamut-list/SKILL.md +273 -0
- package/agent-tools/skills/gamut-review/SKILL.md +254 -0
- package/agent-tools/skills/gamut-style-utilities/SKILL.md +107 -0
- package/agent-tools/skills/gamut-system-props/SKILL.md +203 -0
- package/agent-tools/skills/gamut-testing/SKILL.md +221 -0
- package/agent-tools/skills/gamut-theming/SKILL.md +115 -0
- package/agent-tools/skills/gamut-typography/SKILL.md +98 -0
- package/bin/commands/plugin/install.mjs +212 -0
- package/bin/commands/plugin/list.mjs +73 -0
- package/bin/commands/plugin/remove.mjs +108 -0
- package/bin/commands/plugin/update.mjs +59 -0
- package/bin/gamut.mjs +96 -0
- package/bin/lib/claude.mjs +52 -0
- package/bin/lib/cursor.mjs +40 -0
- package/bin/lib/design.mjs +71 -0
- package/bin/lib/io.mjs +14 -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,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamut-accessibility
|
|
3
|
+
description: Deep Gamut accessibility reference (component matrix, overlays, tips, live regions, checklists). Form wiring and validation UX live in `gamut-forms`. Universal HTML/ARIA/focus/color rules: always-loaded `accessibility.mdc` — read that first; this skill does not duplicate them.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut Accessibility
|
|
7
|
+
|
|
8
|
+
Source: `@codecademy/gamut` — `react-aria-components` is used only in [`packages/gamut/src/Tabs/`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/Tabs/) (`Tabs.tsx`, `TabList.tsx`, `Tab.tsx`, `TabPanel.tsx`). `react-focus-on` powers `FocusTrap` ([`packages/gamut/src/FocusTrap/index.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/FocusTrap/index.tsx)), used by overlays such as `Overlay` ([`packages/gamut/src/Overlay/index.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/Overlay/index.tsx)) and `Popover`. Other widgets (e.g. `Menu`, `DatePicker`) implement keyboard and ARIA in Gamut code.
|
|
9
|
+
|
|
10
|
+
Product-oriented button variants and props: [`gamut-buttons`](../gamut-buttons/SKILL.md)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Universal rules
|
|
15
|
+
|
|
16
|
+
Prefer native HTML, minimal ARIA, correct roles, visible names, focus visibility, semantic color / `ColorMode`, and Gamut primitives — see the always-loaded Gamut Accessibility Rules: [`accessibility.mdc`](../../rules/accessibility.mdc). This skill adds Gamut component behavior and audit detail below.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## How Gamut handles accessibility
|
|
21
|
+
|
|
22
|
+
Tabs use `react-aria-components` (see `packages/gamut/src/Tabs/*.tsx`) for roving tabindex and keyboard navigation. Overlays (e.g. `Overlay`, `Popover`) use `FocusTrap` → `react-focus-on` for focus containment and Escape/outside close. Other interactive components (`Menu`, `DatePicker`, `Modal`, `Dialog`, etc.) rely on in-repo implementations — supply accessible names, wire labels to controls, and avoid duplicating what a component already sets (`aria-live`, `aria-describedby`, tabindex, etc.); confirm in source when auditing.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Component reference (index)
|
|
27
|
+
|
|
28
|
+
There is no exported `<Button>` — use `FillButton`, `TextButton`, `StrokeButton`, `CTAButton`, and `IconButton` (shared `ButtonProps` type). Prefer these over `<div onClick>` or `<span role="button">`.
|
|
29
|
+
|
|
30
|
+
Forms — `FormGroup`, `ConnectedForm` / `ConnectedFormGroup`, `GridForm`, field atoms (`Select`, `Checkbox`, `Radio`), validation, `aria-live` / `aria-describedby`: canonical reference is [`gamut-forms`](../gamut-forms/SKILL.md).
|
|
31
|
+
|
|
32
|
+
| Component(s) | Handled in library | App / author responsibilities |
|
|
33
|
+
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
34
|
+
| `FillButton`, `TextButton`, `StrokeButton`, `CTAButton` | Render `<button>` (or `<a>` when `href` is set); native click + keyboard activation | Visible text or `href` purpose; follow [`gamut-buttons`](../gamut-buttons/SKILL.md) for variants and `disabled` vs `aria-disabled`. |
|
|
35
|
+
| `IconButton` | `tip` feeds the accessible name for icon-only controls | Always pass `tip` when the button has no visible text. |
|
|
36
|
+
| `Dialog` | `Overlay` (shroud, Escape, focus), `role="dialog"`, `aria-modal`, close control with configurable `closeButtonProps.tip` | Provide a clear `title` (and meaningful body copy). Confirm naming with [Molecules / Modals / Dialog](https://gamut.codecademy.com/?path=/docs-molecules-modals-dialog--docs). |
|
|
37
|
+
| `Modal` | Same overlay/focus stack; optional `aria-label`; multi-`views` support | `title` / view titles; pass `aria-label` when there is no visible title string. [Molecules / Modals / Modal](https://gamut.codecademy.com/?path=/docs-molecules-modals-modal--docs). |
|
|
38
|
+
| `Alert` | Default `aria-live="polite"`, `role="status"` | Use `aria-live="assertive"` only for urgent interruptions; do not nest inside another live region. |
|
|
39
|
+
| `Tabs`, `Tab`, `TabList`, `TabPanel` | `react-aria-components` in `packages/gamut/src/Tabs/` — roving tabindex, arrows, Home/End | Name each tab; Tab moves into the active panel per APG. |
|
|
40
|
+
| Forms | See Forms above | [`gamut-forms`](../gamut-forms/SKILL.md) |
|
|
41
|
+
| `DatePicker` + `DatePickerInput` | Segmented input + calendar behavior inside `FormGroup` | Provide `label` / `name` / `form` as for any input; keep `DatePickerInput` inside `DatePicker`. When embedded in `FormGroup` / `GridForm`, follow [`gamut-forms`](../gamut-forms/SKILL.md). [Organisms / DatePicker](https://gamut.codecademy.com/?path=/docs-organisms-datepicker--docs). |
|
|
42
|
+
| `Menu`, `MenuItem`, `MenuSeparator` | List + `MenuProvider` (keyboard / roles depend on variant) | Label `Menu` / menubar per pattern; follow Storybook [Molecules / Menu](https://gamut.codecademy.com/?path=/docs-molecules-menu--docs). |
|
|
43
|
+
| `Popover` | `FocusTrap` when open (unless `skipFocusTrap`), positioning | `onRequestClose`, meaningful `role` when needed; do not trap focus unnecessarily when `skipFocusTrap`. |
|
|
44
|
+
| `Flyout` | `Overlay`, `Drawer`, visible `title`, close `IconButton` with `tip={closeLabel}` | Pass `title` and `closeLabel`; name panel content. |
|
|
45
|
+
| `Drawer` | Focuses container when `expanded`, `tabIndex={-1}` on shell | Drawer is a surface, not a full dialog — supply headings/labels inside for screen readers. |
|
|
46
|
+
| `Disclosure` | `DisclosureButton` drives expand/collapse | Provide `heading` / structure so the control’s purpose is clear. |
|
|
47
|
+
| `Toggle` | `ToggleLabel` + `htmlFor` wired to control `id` | With no visible `label`, pass `ariaLabel` (or use `as="button"` pattern per props). |
|
|
48
|
+
| `ToolTip` | Floating mode renders a screen-reader `role="tooltip"` branch with `id` | Pass the same `id` to the trigger’s `aria-describedby` when you rely on the tooltip as supplementary description (see component `id` JSDoc). |
|
|
49
|
+
| `InfoTip` | — | `ariaLabel` or `ariaLabelledby` (camelCase) — no automatic fallback. |
|
|
50
|
+
| `PreviewTip` | `Anchor`-based preview, focus-driven content | `linkDescription` and visible anchor text; do not use the preview as the sole name for an unrelated control. |
|
|
51
|
+
| `SkipToContent` | Skip link behavior | Place early in the tab order; `href` target `id` must exist on main content. |
|
|
52
|
+
| `Toast` + `Toaster` | `Toaster` wraps the stack in `aria-live="polite"` | Keep messages concise; avoid stacking many simultaneous assertive announcements. |
|
|
53
|
+
| `Pagination` | Page / control buttons | Ensure current page and actions are perceivable (labels / `aria-current` patterns per Storybook). [Molecules / Pagination](https://gamut.codecademy.com/?path=/docs-molecules-pagination--docs). |
|
|
54
|
+
| `FocusTrap` | Escape, outside click, `allowPageInteraction` | Return focus to trigger on close for custom overlays. |
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// correct
|
|
58
|
+
<IconButton icon={DeleteIcon} tip="Delete item" onClick={handleDelete} />
|
|
59
|
+
|
|
60
|
+
// wrong — no accessible name, no keyboard semantics
|
|
61
|
+
<div onClick={handleDelete}><DeleteIcon /></div>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Dialog / Modal (detail)
|
|
65
|
+
|
|
66
|
+
Both use `Overlay` and `FocusTrap` (`react-focus-on`) patterns: focus moves into the surface, Escape closes (when enabled), focus should return to the trigger on close.
|
|
67
|
+
|
|
68
|
+
Prefer a visible title so the dialog has a clear name; on `Modal`, pass `aria-label` when there is no suitable visible title string. Close control: `IconButton` with `closeButtonProps.tip` (defaults documented in source).
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
<Dialog
|
|
72
|
+
title="Confirm deletion"
|
|
73
|
+
confirmCta={{ children: 'Delete', onClick: handleDelete }}
|
|
74
|
+
onRequestClose={handleClose}
|
|
75
|
+
isOpen={open}
|
|
76
|
+
/>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Alert (detail)
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
### Tabs (detail)
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
### InfoTip (example)
|
|
88
|
+
|
|
89
|
+
`<InfoTip>` needs `ariaLabel` or `ariaLabelledby` — see also the always-loaded rules.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<InfoTip ariaLabel="More information about billing" />
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### ToolTip (detail)
|
|
96
|
+
|
|
97
|
+
When `placement="floating"`, the component renders a screen-reader-only branch with `role="tooltip"` and an optional `id`. Pass the same `id` to the described element’s `aria-describedby` so assistive tech associates the tooltip copy with the trigger. Inline placement uses the wrapper differently — see [Molecules / Tips / ToolTip](https://gamut.codecademy.com/?path=/docs-molecules-tips-tooltip--docs).
|
|
98
|
+
|
|
99
|
+
### SkipToContent (detail)
|
|
100
|
+
|
|
101
|
+
Include `<SkipToContent>` as the first focusable element in the page shell. The main content region must expose a matching `id` for the skip target.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Focus management
|
|
106
|
+
|
|
107
|
+
`<FocusTrap>` is for custom overlay patterns not covered by `Dialog` / `Modal`.
|
|
108
|
+
|
|
109
|
+
Key props:
|
|
110
|
+
|
|
111
|
+
- `active` — enable/disable the trap dynamically
|
|
112
|
+
- `onEscapeKey` — close handler
|
|
113
|
+
- `onClickOutside` — dismiss on outside click
|
|
114
|
+
- `allowPageInteraction` — permit scrolling outside the trap without closing
|
|
115
|
+
|
|
116
|
+
Always return focus to the trigger on close. `react-focus-on` (via `FocusTrap`) and overlay flows handle much of this for dialogs/popovers; `Tabs` inherit focus behavior from `react-aria-components`. Custom surfaces must store a ref to the trigger and call `.focus()` on close when the library does not.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Composite widgets and managed focus
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
124
|
+
Implementation pattern — roving tabindex:
|
|
125
|
+
|
|
126
|
+
- Set `tabIndex={0}` on the currently active item
|
|
127
|
+
- Set `tabIndex={-1}` on all other items
|
|
128
|
+
- On arrow key, update which item holds `tabIndex={0}` and call `.focus()` on it
|
|
129
|
+
|
|
130
|
+
`Tabs` (`react-aria-components`) implement roving tabindex for the tablist pattern. `Menu` and other composites implement focus in Gamut — if you build a custom composite, implement roving tabindex yourself. A flat `tabIndex={0}` on every item is wrong — it puts every item in the sequential tab order.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Device-independent events
|
|
135
|
+
|
|
136
|
+
Use `click` for activation, not `mousedown`. `click` follows pointer activation; native `<button>` (and similar controls) also fire `click` from keyboard (Space and Enter). A focused `<a href>` is usually activated with Enter, which fires `click` — Space often scrolls the page instead of activating the link. `mousedown` does not represent keyboard activation, so relying on it alone breaks keyboard-only use.
|
|
137
|
+
|
|
138
|
+
For custom elements with `role="button"`, do not assume the browser will synthesize `click` from the keyboard the way it does for native interactive elements (`<button>`, `<a href>`, and other built-ins). Handle `keydown` for Space and Enter explicitly:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
142
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
handleActivation();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Prefer Gamut `*Button` components (or `Anchor` with a real `href`) so you do not reimplement this.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Live regions
|
|
154
|
+
|
|
155
|
+
| Scenario | Pattern |
|
|
156
|
+
| ------------------------------------------ | ------------------------------------------- |
|
|
157
|
+
| Status updates, non-critical notifications | `aria-live="polite"` |
|
|
158
|
+
| Urgent global interruptions | `aria-live="assertive"` (use sparingly) |
|
|
159
|
+
| Frequently updating counts or progress | `aria-live="polite"` + `aria-atomic="true"` |
|
|
160
|
+
|
|
161
|
+
Form-bound `aria-live` and `FormError` patterns: see Forms above (do not assume assertive on every field error).
|
|
162
|
+
|
|
163
|
+
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.
|
|
164
|
+
|
|
165
|
+
Do not elevate unrelated inline errors to `assertive` — reserve assertive for urgent interruptions the user did not directly trigger.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## ARIA authoring rules
|
|
170
|
+
|
|
171
|
+
- No redundant roles: don't set `role="button"` on `<button>` or `role="heading"` on `<h2>`
|
|
172
|
+
- `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
|
|
173
|
+
- `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
|
|
174
|
+
- Labelling vs describing: `aria-label` / `aria-labelledby` name the control. `aria-describedby` provides supplementary context. Both can coexist on the same element
|
|
175
|
+
- 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
|
|
176
|
+
- `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
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Color and contrast (non-text)
|
|
181
|
+
|
|
182
|
+
Semantic tokens, `ColorMode`, and `<Background>` are covered in the always-loaded `accessibility.mdc` rule and the `gamut-color-mode` skill. Here: non-text contrast (focus rings, input borders, icon affordances) should meet ~3:1 vs adjacent colors where WCAG 1.4.11 applies — validate in your layout.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Testing checklist
|
|
187
|
+
|
|
188
|
+
- [ ] Full keyboard navigation: every interactive element reachable and operable without a mouse
|
|
189
|
+
- [ ] Focus is always visible and never lost or unexpectedly trapped
|
|
190
|
+
- [ ] Dialogs trap focus correctly; Escape closes; focus returns to the trigger
|
|
191
|
+
- [ ] Composite widgets (tabs, menus, listboxes) use arrow keys internally, not Tab
|
|
192
|
+
- [ ] All form inputs have programmatically associated labels (not placeholder-only)
|
|
193
|
+
- [ ] Form errors surface through the library’s `FormError` / live-region patterns (Forms above)
|
|
194
|
+
- [ ] Icon-only controls have accessible names
|
|
195
|
+
- [ ] No content relies solely on color to convey meaning
|
|
196
|
+
- [ ] Screen reader matrix: VoiceOver + Safari (iOS), VoiceOver + Chrome (macOS), NVDA + Chrome (Windows)
|
|
197
|
+
- [ ] 200% zoom: layout intact, no content overflow or disappearance
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Common anti-patterns
|
|
202
|
+
|
|
203
|
+
| Anti-pattern | Fix |
|
|
204
|
+
| -------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
205
|
+
| `<div onClick={…}>` for actions | `FillButton`, `TextButton`, `StrokeButton`, `CTAButton`, or `IconButton` (with `tip`) |
|
|
206
|
+
| `placeholder` as the only label | `FormGroupLabel` with matching `htmlFor` / `id` |
|
|
207
|
+
| `aria-label` on a `<div>` with no role | Add a meaningful `role` or use a semantic element |
|
|
208
|
+
| `role="button"` without Space/Enter handlers | Use a Gamut `*Button`, `Anchor` with `href`, or add `keydown` |
|
|
209
|
+
| `tabIndex={0}` on every item in a composite | Roving tabindex: `0` on active item, `-1` on rest |
|
|
210
|
+
| Tooltip as the only accessible name for a control | Set `aria-label` (or visible text) on the control as well |
|
|
211
|
+
| `aria-hidden="true"` on a focusable element | Also remove from tab order (`tabIndex={-1}`) or restructure |
|
|
212
|
+
| `mousedown` for activation | Use `click` |
|
|
213
|
+
| `outline: none` without a replacement | Use Gamut’s built-in focus styles |
|
|
214
|
+
| Multiple `aria-live` regions for the same content stream | One region per logical stream; reuse it across updates |
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamut-buttons
|
|
3
|
+
description: Use this skill when choosing Gamut button atoms (FillButton, StrokeButton, TextButton, IconButton, CTAButton), variant and size props, or disabled vs aria-disabled patterns — not for custom styled controls (see gamut-color-mode and gamut-style-utilities).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut Buttons
|
|
7
|
+
|
|
8
|
+
Which button component and which `variant` to use. Colors are wired inside each atom — consumers do not pass `color`, `bg`, hex, or semantic token names on stock buttons.
|
|
9
|
+
|
|
10
|
+
See also: [`gamut-color-mode`](../gamut-color-mode/SKILL.md) — semantic tokens for custom styled controls only, not stock button atoms; ColorMode / `<Background>` when placing buttons on colored surfaces. [`gamut-accessibility`](../gamut-accessibility/SKILL.md) — universal action and naming rules.
|
|
11
|
+
|
|
12
|
+
Storybook:
|
|
13
|
+
|
|
14
|
+
- [Atoms / Buttons / Button](https://gamut.codecademy.com/?path=/docs-atoms-buttons-button--docs) — variants, light/dark examples
|
|
15
|
+
- [FillButton](https://gamut.codecademy.com/?path=/docs-atoms-buttons-fillbutton--docs) · [StrokeButton](https://gamut.codecademy.com/?path=/docs-atoms-buttons-strokebutton--docs) · [TextButton](https://gamut.codecademy.com/?path=/docs-atoms-buttons-textbutton--docs) · [IconButton](https://gamut.codecademy.com/?path=/docs-atoms-buttons-iconbutton--docs) · [CTAButton](https://gamut.codecademy.com/?path=/docs-atoms-buttons-ctabutton--docs)
|
|
16
|
+
- [Meta / Best practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page) — semantic tokens for custom `css` / `variant` / `states`, not prebuilt atoms
|
|
17
|
+
- [UX Writing / Component guidelines / Buttons](https://gamut.codecademy.com/?path=/docs-ux-writing-component-guidelines-buttons--docs) — label copy
|
|
18
|
+
|
|
19
|
+
## Component selection
|
|
20
|
+
|
|
21
|
+
| Component | Use for | Default `variant` |
|
|
22
|
+
| -------------- | --------------------------------------------- | ------------------------------------------------ |
|
|
23
|
+
| `FillButton` | Primary / high-emphasis actions | `primary` |
|
|
24
|
+
| `StrokeButton` | Secondary / outlined actions | `primary` (often pass `secondary` per Storybook) |
|
|
25
|
+
| `TextButton` | Low-emphasis, inline actions | `primary` |
|
|
26
|
+
| `IconButton` | Icon-only; requires `tip` and accessible name | `secondary` |
|
|
27
|
+
| `CTAButton` | Marketing / high-visibility CTA only | `primary` (only option) |
|
|
28
|
+
|
|
29
|
+
## `variant` prop
|
|
30
|
+
|
|
31
|
+
Shared by `FillButton`, `StrokeButton`, `TextButton`, and `IconButton`. `CTAButton` only supports `primary`.
|
|
32
|
+
|
|
33
|
+
| `variant` | Typical use |
|
|
34
|
+
| ----------- | ------------------------------ |
|
|
35
|
+
| `primary` | Submit, main CTA |
|
|
36
|
+
| `secondary` | Close, cancel, low priority |
|
|
37
|
+
| `danger` | Destructive actions |
|
|
38
|
+
| `interface` | Controls styled like UI chrome |
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<FillButton variant="primary">Submit</FillButton>
|
|
42
|
+
<StrokeButton variant="secondary">Cancel</StrokeButton>
|
|
43
|
+
<IconButton icon={SearchIcon} tip="Search" variant="secondary" />
|
|
44
|
+
<CTAButton>Try Pro for free</CTAButton>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Sizes
|
|
48
|
+
|
|
49
|
+
`small` | `normal` (default) | `large`
|
|
50
|
+
|
|
51
|
+
## Key props
|
|
52
|
+
|
|
53
|
+
| Prop | Type | Effect |
|
|
54
|
+
| -------------- | ----------------------------------------------------- | ------------------------------------------------ |
|
|
55
|
+
| `variant` | `"primary" \| "secondary" \| "danger" \| "interface"` | Color emphasis (see table above) |
|
|
56
|
+
| `size` | `"small" \| "normal" \| "large"` | Padding and font size |
|
|
57
|
+
| `icon` | Icon component | Leading or trailing icon (Fill, Stroke, Text) |
|
|
58
|
+
| `iconPosition` | `"left" \| "right"` | Defaults to left |
|
|
59
|
+
| `disabled` | boolean | Disabled state styling |
|
|
60
|
+
| `href` | string | Renders as `<a>` tag |
|
|
61
|
+
| `tip` | string | Required on `IconButton` (tooltip + hover label) |
|
|
62
|
+
|
|
63
|
+
## States
|
|
64
|
+
|
|
65
|
+
Hover, active, and disabled colors are handled by the component. Do not override state colors with `color` / `bg` props.
|
|
66
|
+
|
|
67
|
+
## Accessibility — `disabled` vs `aria-disabled`
|
|
68
|
+
|
|
69
|
+
| Situation | Use | Why |
|
|
70
|
+
| --------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
71
|
+
| Button should not be activatable (default) | `disabled` prop | Renders native `disabled` on `<button>`; removed from tab order; correct for most forms and actions |
|
|
72
|
+
| Disabled button with a tooltip that must stay readable on focus | `aria-disabled` only — do not also pass `disabled` | Native `disabled` blocks keyboard focus, so the tooltip cannot be reached. Gamut disabled styles also match `[aria-disabled='true']`. See [ToolTip — With a disabled Button](https://gamut.codecademy.com/?path=/docs-molecules-tips-tooltip--docs) |
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// Default — not interactive
|
|
76
|
+
<FillButton disabled>Submit</FillButton>
|
|
77
|
+
|
|
78
|
+
// Disabled but focusable (e.g. wrapped in ToolTip explaining why)
|
|
79
|
+
<ToolTip id="why-disabled" info="Complete the lesson first">
|
|
80
|
+
<FillButton aria-describedby="why-disabled" aria-disabled>
|
|
81
|
+
Submit
|
|
82
|
+
</FillButton>
|
|
83
|
+
</ToolTip>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- `href` + `disabled`: `ButtonBase` (internal) drops `href` and renders a `<button disabled>` — link-style buttons cannot stay anchors while disabled.
|
|
87
|
+
- `IconButton`: provide an accessible name via `tip` (used as `aria-label` when `aria-label` is omitted). See ToolTip / IconButton Storybook pages.
|
|
88
|
+
- `ButtonBase` is not exported from `@codecademy/gamut` (only the `ButtonBaseElements` type is). Prefer stock atoms; custom button styling belongs in Gamut itself or via `css` / `variant` from `gamut-styles`, not by importing `ButtonBase`.
|
|
89
|
+
|
|
90
|
+
## Rules
|
|
91
|
+
|
|
92
|
+
- Use `FillButton` for primary actions and `StrokeButton` for secondary — do not use both at equal weight on the same screen.
|
|
93
|
+
- Reserve `CTAButton` for marketing / high-visibility promotions; do not use it for standard UI actions.
|
|
94
|
+
- Avoid placing buttons in the wrong color-mode context (e.g. light-mode buttons on a navy band without `<Background>`). See [`gamut-color-mode`](../gamut-color-mode/SKILL.md).
|
|
95
|
+
- Every interactive `Card` wrapped in `<Anchor>` should have `isInteractive` — not a button inside.
|
|
96
|
+
- Do not set `color`, `bg`, or hex on stock button components. For custom styled controls, follow [`gamut-color-mode`](../gamut-color-mode/SKILL.md) and `gamut-styles` utilities — do not import internal `ButtonBase`.
|
|
@@ -0,0 +1,257 @@
|
|
|
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
|
+
This set is not exhaustive (e.g. `text-accent`, `background-disabled`, `danger` — see the light/dark tables in Storybook).
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
|
|
29
|
+
Agent skill: [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md) — `css` / `variant` / `states` with semantic colors alongside ColorMode.
|
|
30
|
+
|
|
31
|
+
## `<ColorMode />`
|
|
32
|
+
|
|
33
|
+
Wraps content in a color mode context. Place `<ColorMode />` as high in the app tree as practical. For a nested or static themed area on a page, use `<Background />` instead.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { ColorMode } from '@codecademy/gamut-styles';
|
|
37
|
+
|
|
38
|
+
// Explicit light or dark
|
|
39
|
+
<ColorMode mode="light">{children}</ColorMode>
|
|
40
|
+
|
|
41
|
+
// Follow OS light/dark preference (see mode="system" below)
|
|
42
|
+
<ColorMode mode="system">{children}</ColorMode>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Props: `mode="light" | "dark" | "system"`
|
|
46
|
+
|
|
47
|
+
### `mode="system"` (OS preference)
|
|
48
|
+
|
|
49
|
+
`system` is not a third color theme. It always resolves to `"light"` or `"dark"` based on the user's OS setting.
|
|
50
|
+
|
|
51
|
+
How it works
|
|
52
|
+
|
|
53
|
+
1. `ColorMode` calls `usePrefersDarkMode()`, which reads `window.matchMedia('(prefers-color-scheme: dark)')`.
|
|
54
|
+
2. If the query matches → active mode is `"dark"`; otherwise `"light"`.
|
|
55
|
+
3. Descendants receive that mode's semantic color variables (`text`, `background`, `primary`, etc.) — same as passing `mode="light"` or `mode="dark"` directly.
|
|
56
|
+
|
|
57
|
+
When the OS changes (e.g. user toggles system appearance), the media query fires a `change` event, `ColorMode` re-renders, and semantic colors update for everything inside the wrapper.
|
|
58
|
+
|
|
59
|
+
What it does not do
|
|
60
|
+
|
|
61
|
+
- Read in-app preferences (account settings, a theme toggle in localStorage). For those, pass `mode="light"` or `mode="dark"` yourself from your own state.
|
|
62
|
+
- Replace `<Background>`. A colored band still needs `<Background bg="hyper">` if you want contrast-based mode selection for that surface.
|
|
63
|
+
|
|
64
|
+
Prefer `mode="system"` over wiring the hook yourself
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// Prefer — ColorMode owns light/dark resolution
|
|
68
|
+
<ColorMode mode="system">{children}</ColorMode>;
|
|
69
|
+
|
|
70
|
+
// Avoid — duplicates what mode="system" already does
|
|
71
|
+
const prefersDark = usePrefersDarkMode();
|
|
72
|
+
<ColorMode mode={prefersDark ? 'dark' : 'light'}>{children}</ColorMode>;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Use `usePrefersDarkMode()` only when you need the OS preference for something other than setting `ColorMode`'s mode (e.g. picking a decorative palette `bg` in a demo).
|
|
76
|
+
|
|
77
|
+
Place `<ColorMode mode="system">` high in the tree (often inside `GamutProvider`) so the whole app follows system appearance unless a subtree overrides with `mode="light"`, `mode="dark"`, or `<Background>`.
|
|
78
|
+
|
|
79
|
+
## `<Background />`
|
|
80
|
+
|
|
81
|
+
Use `<Background>` instead of putting `bg` on a layout component when a section needs a fixed palette color (card, hero, landing band, etc.) — independent of the parent color mode. Pass a palette token to `bg` (e.g. `hyper`, `navy`, `paleGreen`), not a semantic alias (`text`, `background`, `primary`).
|
|
82
|
+
|
|
83
|
+
`<Background>` switches light/dark to whichever mode gives the highest contrast between that surface and body `text`. Nested Gamut components inherit readable colors without extra setup.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { Background } from '@codecademy/gamut-styles';
|
|
87
|
+
|
|
88
|
+
// Single background — mode switches automatically if needed
|
|
89
|
+
const Card = ({ children }) => <Background bg="hyper">{children}</Background>;
|
|
90
|
+
|
|
91
|
+
// Nested backgrounds — each creates its own color context
|
|
92
|
+
const Page = () => (
|
|
93
|
+
<Background bg="black" p={24}>
|
|
94
|
+
<Background bg="paleGreen" p={24}>
|
|
95
|
+
{/* content inside inner Background uses its own mode */}
|
|
96
|
+
</Background>
|
|
97
|
+
</Background>
|
|
98
|
+
);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `background-current` CSS variable
|
|
102
|
+
|
|
103
|
+
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).
|
|
104
|
+
|
|
105
|
+
## Color mode hooks
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import {
|
|
109
|
+
useColorModes,
|
|
110
|
+
useCurrentMode,
|
|
111
|
+
usePrefersDarkMode,
|
|
112
|
+
} from '@codecademy/gamut-styles';
|
|
113
|
+
|
|
114
|
+
// [activeModeKey, activeModeColors, allModes, getColorValue]
|
|
115
|
+
const [current, currentColors, modes, getColorValue] = useColorModes();
|
|
116
|
+
|
|
117
|
+
// Active mode key: "light" | "dark" (optional override argument)
|
|
118
|
+
const current = useCurrentMode();
|
|
119
|
+
const forced = useCurrentMode('light');
|
|
120
|
+
|
|
121
|
+
// Boolean from window.matchMedia('(prefers-color-scheme: dark)')
|
|
122
|
+
const prefersDark = usePrefersDarkMode();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Decision guide
|
|
126
|
+
|
|
127
|
+
| Need | Use |
|
|
128
|
+
| ------------------------------------------------------------- | ---------------------------------- | ---- | --------- |
|
|
129
|
+
| Set a page or section to a specific mode | `<ColorMode mode="light | dark | system">` |
|
|
130
|
+
| Place content on a colored background with automatic contrast | `<Background bg="...">` |
|
|
131
|
+
| Read the current mode in JavaScript | `useCurrentMode()` |
|
|
132
|
+
| Access all modes, variables, and resolve raw colors | `useColorModes()` |
|
|
133
|
+
| Detect OS dark mode preference | `usePrefersDarkMode()` |
|
|
134
|
+
| Access full emotion theme | `useTheme()` from `@emotion/react` |
|
|
135
|
+
|
|
136
|
+
## Common mistakes to avoid
|
|
137
|
+
|
|
138
|
+
- 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.
|
|
139
|
+
- Do not use a raw `bg` prop for colored section backgrounds that need static, ColorMode-agnostic, background colors — use `<Background>` so mode selection is handled for you.
|
|
140
|
+
- Do not manually set `ColorMode`'s `mode` from `usePrefersDarkMode()` when `mode="system"` is enough. The hook is still useful for non-mode concerns (e.g. choosing a decorative `bg` in Storybook demos).
|
|
141
|
+
|
|
142
|
+
## Semantic aliases (theme-stable names)
|
|
143
|
+
|
|
144
|
+
These tokens describe roles. Actual colors come from the active theme + ColorMode. Never assume Codecademy Core hex when advising another product.
|
|
145
|
+
|
|
146
|
+
### Text
|
|
147
|
+
|
|
148
|
+
| Token | Use for |
|
|
149
|
+
| ---------------- | --------------------------- |
|
|
150
|
+
| `text` | Default body and UI text |
|
|
151
|
+
| `text-accent` | Stronger emphasis text |
|
|
152
|
+
| `text-secondary` | Supporting / secondary copy |
|
|
153
|
+
| `text-disabled` | Disabled state labels |
|
|
154
|
+
|
|
155
|
+
### Background
|
|
156
|
+
|
|
157
|
+
| Token | Use for |
|
|
158
|
+
| --------------------- | --------------------------------- |
|
|
159
|
+
| `background` | Default page/component background |
|
|
160
|
+
| `background-primary` | Slightly elevated surfaces |
|
|
161
|
+
| `background-contrast` | Maximum contrast surface |
|
|
162
|
+
| `background-selected` | Selected row / item |
|
|
163
|
+
| `background-hover` | Hover state overlay |
|
|
164
|
+
| `background-disabled` | Disabled surface |
|
|
165
|
+
| `background-success` | Success state container |
|
|
166
|
+
| `background-warning` | Warning state container |
|
|
167
|
+
| `background-error` | Error state container |
|
|
168
|
+
|
|
169
|
+
### Interactive
|
|
170
|
+
|
|
171
|
+
| Token | Use for |
|
|
172
|
+
| ----------------- | ----------------------------------------- |
|
|
173
|
+
| `primary` | Primary CTA, links, focus accents |
|
|
174
|
+
| `primary-hover` | Hover on primary interactive |
|
|
175
|
+
| `primary-inverse` | Accent on top of primary-colored surfaces |
|
|
176
|
+
| `secondary` | Secondary CTA, ghost buttons |
|
|
177
|
+
| `secondary-hover` | Hover on secondary interactive |
|
|
178
|
+
| `danger` | Destructive actions, error emphasis |
|
|
179
|
+
| `danger-hover` | Hover on danger interactive |
|
|
180
|
+
|
|
181
|
+
### Border
|
|
182
|
+
|
|
183
|
+
| Token | Use for |
|
|
184
|
+
| ------------------ | -------------------------- |
|
|
185
|
+
| `border-primary` | Strong borders, dividers |
|
|
186
|
+
| `border-secondary` | Medium-weight borders |
|
|
187
|
+
| `border-tertiary` | Subtle borders, separators |
|
|
188
|
+
| `border-disabled` | Disabled input borders |
|
|
189
|
+
|
|
190
|
+
### Feedback
|
|
191
|
+
|
|
192
|
+
| Token | Use for |
|
|
193
|
+
| ------------------ | -------------------------- |
|
|
194
|
+
| `feedback-error` | Error messages, validation |
|
|
195
|
+
| `feedback-success` | Success messages |
|
|
196
|
+
| `feedback-warning` | Warning messages |
|
|
197
|
+
|
|
198
|
+
## Where resolved colors are documented
|
|
199
|
+
|
|
200
|
+
- Storybook [ColorMode](https://gamut.codecademy.com/?path=/docs-foundations-colormode--page)
|
|
201
|
+
- [Core](https://gamut.codecademy.com/?path=/docs-foundations-theme-core-theme--docs) · [Admin](https://gamut.codecademy.com/?path=/docs-foundations-theme-admin-theme--docs) · [Platform](https://gamut.codecademy.com/?path=/docs-foundations-theme-platform-theme--docs) · [Percipio](https://gamut.codecademy.com/?path=/docs-foundations-theme-percipio-theme--docs) · [LX Studio](https://gamut.codecademy.com/?path=/docs-foundations-theme-lx-studio-theme--docs) theme pages
|
|
202
|
+
- Root `DESIGN.md` from agent-tools (`DESIGN.Codecademy.md`, `DESIGN.Percipio.md`, `DESIGN.LXStudio.md`)
|
|
203
|
+
- Source: [`packages/gamut-styles/src/themes`](https://github.com/Codecademy/gamut/tree/main/packages/gamut-styles/src/themes)
|
|
204
|
+
|
|
205
|
+
## Codecademy Core — illustrative light/dark hex only
|
|
206
|
+
|
|
207
|
+
Not valid for Percipio, LX Studio, or other themes. Quick mental model for Core defaults only.
|
|
208
|
+
|
|
209
|
+
| Token | Light | Dark |
|
|
210
|
+
| ---------------- | --------- | --------- |
|
|
211
|
+
| `text` | `#10162F` | `#ffffff` |
|
|
212
|
+
| `text-accent` | `#0A0D1C` | `#FFF0E5` |
|
|
213
|
+
| `background` | `#ffffff` | `#10162F` |
|
|
214
|
+
| `primary` | `#3A10E5` | `#FFD300` |
|
|
215
|
+
| `primary-hover` | `#5533FF` | `#CCA900` |
|
|
216
|
+
| `secondary` | `#10162F` | `#ffffff` |
|
|
217
|
+
| `danger` | `#E91C11` | `#E85D7F` |
|
|
218
|
+
| `feedback-error` | `#BE1809` | `#E85D7F` |
|
|
219
|
+
|
|
220
|
+
Full tables: Storybook theme pages or audit with [`gamut-review`](../gamut-review/SKILL.md) Appendix A/B for hex triage.
|
|
221
|
+
|
|
222
|
+
## Raw palette (Core-centric reference)
|
|
223
|
+
|
|
224
|
+
Raw tokens name fixed swatches for `<Background bg="…">`, illustration, and surfaces. Confirm allowed keys in the active theme or `DESIGN.md` before using in non-Core apps.
|
|
225
|
+
|
|
226
|
+
Named shorthand aliases on Core: `beige`, `blue`, `green`, `hyper`, `navy`, `orange`, `pink`, `red`, `yellow`, `black`, `white`
|
|
227
|
+
|
|
228
|
+
## Color decision guide
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
Which product theme is GamutProvider using?
|
|
232
|
+
└─ Unknown → check DESIGN.md / gamut-theming / Storybook theme page
|
|
233
|
+
|
|
234
|
+
Coloring UI text or backgrounds?
|
|
235
|
+
└─ Must adapt to light/dark OR theme? → semantic alias (text, background, primary, …)
|
|
236
|
+
└─ Must stay fixed regardless of mode? → raw palette token (confirm key in that theme)
|
|
237
|
+
└─ Section background with content? → <Background bg="…">
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Card and alert patterns
|
|
241
|
+
|
|
242
|
+
### Cards
|
|
243
|
+
|
|
244
|
+
- Background variants: `default` (ColorMode-responsive), `white`, `yellow`, `beige`, `navy`, `hyper`
|
|
245
|
+
- Shadow variants: `none` (default), `outline`, `patternLeft`, `patternRight`
|
|
246
|
+
- Add `isInteractive` when wrapping in `<Anchor>` — enables hover shadow + `borderRadius: md`
|
|
247
|
+
- Default `borderRadius` is `none`; override with `borderRadius` prop
|
|
248
|
+
|
|
249
|
+
### Alerts
|
|
250
|
+
|
|
251
|
+
| Variant | Tokens |
|
|
252
|
+
| ------- | ----------------------------------------- |
|
|
253
|
+
| Error | `feedback-error` + `background-error` |
|
|
254
|
+
| Success | `feedback-success` + `background-success` |
|
|
255
|
+
| Warning | `feedback-warning` + `background-warning` |
|
|
256
|
+
|
|
257
|
+
52 components have Figma ↔ code mappings via Code Connect (`packages/code-connect/`).
|