@codecademy/gamut 68.6.1-alpha.edab62.0 → 68.6.1-alpha.f6b2ce.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 (46) hide show
  1. package/agent-tools/.cursor-plugin/plugin.json +1 -1
  2. package/agent-tools/DESIGN.Codecademy.md +466 -413
  3. package/agent-tools/DESIGN.LXStudio.md +350 -282
  4. package/agent-tools/DESIGN.Percipio.md +357 -279
  5. package/agent-tools/commands/gamut-review.md +176 -87
  6. package/agent-tools/guidelines/components/animations.md +74 -0
  7. package/agent-tools/guidelines/components/buttons.md +74 -23
  8. package/agent-tools/guidelines/components/card.md +19 -0
  9. package/agent-tools/guidelines/components/coachmark.md +21 -0
  10. package/agent-tools/guidelines/components/data-table.md +79 -0
  11. package/agent-tools/guidelines/components/forms.md +106 -0
  12. package/agent-tools/guidelines/components/loading-states.md +17 -0
  13. package/agent-tools/guidelines/components/menu.md +58 -0
  14. package/agent-tools/guidelines/components/overview.md +97 -17
  15. package/agent-tools/guidelines/components/radial-progress.md +13 -0
  16. package/agent-tools/guidelines/components/select.md +23 -0
  17. package/agent-tools/guidelines/components/tooltips.md +22 -0
  18. package/agent-tools/guidelines/components/video.md +29 -0
  19. package/agent-tools/guidelines/foundations/color.md +140 -58
  20. package/agent-tools/guidelines/foundations/modes.md +41 -17
  21. package/agent-tools/guidelines/foundations/spacing.md +78 -37
  22. package/agent-tools/guidelines/foundations/typography.md +69 -37
  23. package/agent-tools/guidelines/overview-icons.md +19 -0
  24. package/agent-tools/guidelines/overview-illustrations.md +7 -0
  25. package/agent-tools/guidelines/overview-patterns.md +7 -0
  26. package/agent-tools/guidelines/overview.md +71 -22
  27. package/agent-tools/guidelines/setup.md +59 -18
  28. package/agent-tools/rules/accessibility.mdc +22 -13
  29. package/agent-tools/skills/gamut-accessibility/SKILL.md +97 -112
  30. package/agent-tools/skills/gamut-color-mode/SKILL.md +91 -41
  31. package/agent-tools/skills/gamut-components/SKILL.md +46 -0
  32. package/agent-tools/skills/gamut-forms/SKILL.md +101 -0
  33. package/agent-tools/skills/gamut-style-utilities/SKILL.md +111 -0
  34. package/agent-tools/skills/gamut-system-props/SKILL.md +81 -29
  35. package/agent-tools/skills/gamut-testing/SKILL.md +106 -62
  36. package/agent-tools/skills/gamut-theming/SKILL.md +36 -86
  37. package/agent-tools/skills/gamut-typography/SKILL.md +36 -80
  38. package/bin/commands/plugin/install.mjs +96 -56
  39. package/bin/commands/plugin/list.mjs +11 -43
  40. package/bin/commands/plugin/remove.mjs +30 -38
  41. package/bin/commands/plugin/update.mjs +15 -5
  42. package/bin/gamut.mjs +17 -13
  43. package/bin/lib/design.mjs +71 -0
  44. package/bin/lib/io.mjs +14 -0
  45. package/package.json +6 -6
  46. package/bin/lib/figma.mjs +0 -49
@@ -1,65 +1,67 @@
1
1
  ---
2
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.
3
+ description: Gamut a11y matrix, overlays, FocusTrap, tips, live regions. When invoked, read guidelines/components/buttons.md, menu.md, tooltips.md as needed. Forms: gamut-forms. Cursor: accessibility.mdc.
4
4
  ---
5
5
 
6
6
  # Gamut Accessibility
7
7
 
8
- Source: `@codecademy/gamut` — interactive components wrap `react-aria-components`, `@react-aria/interactions`, and `react-focus-on`.
8
+ ## Read first
9
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.
10
+ When this skill applies, read the linked component guides for widgets you are auditing (not all at once):
23
11
 
24
- ### ARIA can both cloak and enhance
12
+ - [`guidelines/components/buttons.md`](../../guidelines/components/buttons.md)
13
+ - [`guidelines/components/menu.md`](../../guidelines/components/menu.md)
14
+ - [`guidelines/components/tooltips.md`](../../guidelines/components/tooltips.md)
25
15
 
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.
16
+ Universal rules: [`accessibility.mdc`](../../rules/accessibility.mdc) (Cursor, always-on). Form wiring: `gamut-forms`.
27
17
 
18
+ 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 — verify behavior in Storybook and source, do not assume React Aria.
28
19
 
29
- ### Align accessible names with visible copy
20
+ Product-oriented button variants and props: [`guidelines/components/buttons.md`](../../guidelines/components/buttons.md) · Menus: [`guidelines/components/menu.md`](../../guidelines/components/menu.md) · Tooltips: [`guidelines/components/tooltips.md`](../../guidelines/components/tooltips.md)
30
21
 
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.
22
+ ---
36
23
 
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>
24
+ ## Universal rules
42
25
 
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
- ```
26
+ 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.
49
27
 
50
28
  ---
51
29
 
52
30
  ## How Gamut handles accessibility
53
31
 
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.
32
+ 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.
55
33
 
56
34
  ---
57
35
 
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.
36
+ ## Component reference (index)
37
+
38
+ There is no exported `<Button>` use `FillButton`, `TextButton`, `StrokeButton`, `CTAButton`, and `IconButton` (shared `ButtonProps` type). Prefer these over `<div onClick>` or `<span role="button">`.
39
+
40
+ Forms`FormGroup`, `ConnectedForm` / `ConnectedFormGroup`, `GridForm`, field atoms (`Select`, `Checkbox`, `Radio`), validation, `aria-live` / `aria-describedby`: canonical reference is [`gamut-forms`](../gamut-forms/SKILL.md).
41
+
42
+ | Component(s) | Handled in library | App / author responsibilities |
43
+ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
44
+ | `FillButton`, `TextButton`, `StrokeButton`, `CTAButton` | Render `<button>` (or `<a>` when `href` is set); native click + keyboard activation | Visible text or `href` purpose; follow [`buttons.md`](../../guidelines/components/buttons.md) for variants and `disabled` vs `aria-disabled`. |
45
+ | `IconButton` | `tip` feeds the accessible name for icon-only controls | Always pass `tip` when the button has no visible text. |
46
+ | `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). |
47
+ | `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). |
48
+ | `Alert` | Default `aria-live="polite"`, `role="status"` | Use `aria-live="assertive"` only for urgent interruptions; do not nest inside another live region. |
49
+ | `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. |
50
+ | Forms | See Forms above | [`gamut-forms`](../gamut-forms/SKILL.md) |
51
+ | `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). |
52
+ | `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). |
53
+ | `Popover` | `FocusTrap` when open (unless `skipFocusTrap`), positioning | `onRequestClose`, meaningful `role` when needed; do not trap focus unnecessarily when `skipFocusTrap`. |
54
+ | `Flyout` | `Overlay`, `Drawer`, visible `title`, close `IconButton` with `tip={closeLabel}` | Pass `title` and `closeLabel`; name panel content. |
55
+ | `Drawer` | Focuses container when `expanded`, `tabIndex={-1}` on shell | Drawer is a surface, not a full dialog — supply headings/labels inside for screen readers. |
56
+ | `Disclosure` | `DisclosureButton` drives expand/collapse | Provide `heading` / structure so the control’s purpose is clear. |
57
+ | `Toggle` | `ToggleLabel` + `htmlFor` wired to control `id` | With no visible `label`, pass `ariaLabel` (or use `as="button"` pattern per props). |
58
+ | `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). |
59
+ | `InfoTip` | — | `ariaLabel` or `ariaLabelledby` (camelCase) — no automatic fallback. |
60
+ | `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. |
61
+ | `SkipToContent` | Skip link behavior | Place early in the tab order; `href` target `id` must exist on main content. |
62
+ | `Toast` + `Toaster` | `Toaster` wraps the stack in `aria-live="polite"` | Keep messages concise; avoid stacking many simultaneous assertive announcements. |
63
+ | `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). |
64
+ | `FocusTrap` | Escape, outside click, `allowPageInteraction` | Return focus to trigger on close for custom overlays. |
63
65
 
64
66
  ```tsx
65
67
  // correct
@@ -69,98 +71,81 @@ Gamut wraps React Aria for most interactive widget primitives. Roving tabindex,
69
71
  <div onClick={handleDelete}><DeleteIcon /></div>
70
72
  ```
71
73
 
72
- ### Dialog / Modal
74
+ ### Dialog / Modal (detail)
73
75
 
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.
76
+ 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.
75
77
 
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.
78
+ 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).
77
79
 
78
80
  ```tsx
79
- <Dialog aria-labelledby="confirm-title">
80
- <h2 id="confirm-title">Confirm deletion</h2>
81
- </Dialog>
81
+ <Dialog
82
+ title="Confirm deletion"
83
+ confirmCta={{ children: 'Delete', onClick: handleDelete }}
84
+ onRequestClose={handleClose}
85
+ isOpen={open}
86
+ />
82
87
  ```
83
88
 
84
- ### Alert
89
+ ### Alert (detail)
85
90
 
86
91
  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
92
 
88
- ### Tabs
93
+ ### Tabs (detail)
89
94
 
90
95
  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
96
 
92
- ### Forms
97
+ ### InfoTip (example)
93
98
 
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).
99
+ `<InfoTip>` needs `ariaLabel` or `ariaLabelledby` see also the always-loaded rules.
113
100
 
114
101
  ```tsx
115
102
  <InfoTip ariaLabel="More information about billing" />
116
103
  ```
117
104
 
118
- ### SkipToContent
105
+ ### ToolTip (detail)
119
106
 
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.
107
+ 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).
121
108
 
122
- ### Text (screen-reader-only)
109
+ ### SkipToContent (detail)
123
110
 
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
- ```
111
+ Include `<SkipToContent>` as the first focusable element in the page shell. The main content region must expose a matching `id` for the skip target.
129
112
 
130
113
  ---
131
114
 
132
115
  ## Focus management
133
116
 
134
- `<FocusTrap>` is available for custom overlay patterns not covered by `<Dialog>` or `<Modal>`.
117
+ `<FocusTrap>` is for custom overlay patterns not covered by `Dialog` / `Modal`.
135
118
 
136
119
  Key props:
120
+
137
121
  - `active` — enable/disable the trap dynamically
138
122
  - `onEscapeKey` — close handler
139
123
  - `onClickOutside` — dismiss on outside click
140
124
  - `allowPageInteraction` — permit scrolling outside the trap without closing
141
125
 
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.
126
+ 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.
143
127
 
144
128
  ---
145
129
 
146
130
  ## Composite widgets and managed focus
147
131
 
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.
132
+ 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
133
 
150
134
  Implementation pattern — roving tabindex:
135
+
151
136
  - Set `tabIndex={0}` on the currently active item
152
137
  - Set `tabIndex={-1}` on all other items
153
138
  - On arrow key, update which item holds `tabIndex={0}` and call `.focus()` on it
154
139
 
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.
140
+ `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.
156
141
 
157
142
  ---
158
143
 
159
144
  ## Device-independent events
160
145
 
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.
146
+ 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.
162
147
 
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:
148
+ 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:
164
149
 
165
150
  ```tsx
166
151
  const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -171,40 +156,40 @@ const handleKeyDown = (e: React.KeyboardEvent) => {
171
156
  };
172
157
  ```
173
158
 
174
- This is another reason to use `<Button>` it handles this correctly and you don't.
159
+ Prefer Gamut `*Button` components (or `Anchor` with a real `href`) so you do not reimplement this.
175
160
 
176
161
  ---
177
162
 
178
163
  ## Live regions
179
164
 
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"` |
165
+ | Scenario | Pattern |
166
+ | ------------------------------------------ | ------------------------------------------- |
167
+ | Status updates, non-critical notifications | `aria-live="polite"` |
168
+ | Urgent global interruptions | `aria-live="assertive"` (use sparingly) |
169
+ | Frequently updating counts or progress | `aria-live="polite"` + `aria-atomic="true"` |
170
+
171
+ Form-bound `aria-live` and `FormError` patterns: see Forms above (do not assume assertive on every field error).
185
172
 
186
173
  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
174
 
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.
175
+ Do not elevate unrelated inline errors to `assertive` — reserve assertive for urgent interruptions the user did not directly trigger.
189
176
 
190
177
  ---
191
178
 
192
179
  ## ARIA authoring rules
193
180
 
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
181
+ - No redundant roles: don't set `role="button"` on `<button>` or `role="heading"` on `<h2>`
182
+ - `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
183
+ - `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
184
+ - Labelling vs describing: `aria-label` / `aria-labelledby` name the control. `aria-describedby` provides supplementary context. Both can coexist on the same element
185
+ - 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
186
+ - `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
187
 
201
188
  ---
202
189
 
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.
190
+ ## Color and contrast (non-text)
206
191
 
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.
192
+ 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.
208
193
 
209
194
  ---
210
195
 
@@ -215,7 +200,7 @@ Non-text contrast (focus indicators, input borders, icon-only controls) requires
215
200
  - [ ] Dialogs trap focus correctly; Escape closes; focus returns to the trigger
216
201
  - [ ] Composite widgets (tabs, menus, listboxes) use arrow keys internally, not Tab
217
202
  - [ ] All form inputs have programmatically associated labels (not placeholder-only)
218
- - [ ] Form errors are announced via live region
203
+ - [ ] Form errors surface through the library’s `FormError` / live-region patterns (Forms above)
219
204
  - [ ] Icon-only controls have accessible names
220
205
  - [ ] No content relies solely on color to convey meaning
221
206
  - [ ] Screen reader matrix: VoiceOver + Safari (iOS), VoiceOver + Chrome (macOS), NVDA + Chrome (Windows)
@@ -225,15 +210,15 @@ Non-text contrast (focus indicators, input borders, icon-only controls) requires
225
210
 
226
211
  ## Common anti-patterns
227
212
 
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 |
213
+ | Anti-pattern | Fix |
214
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------------- |
215
+ | `<div onClick={…}>` for actions | `FillButton`, `TextButton`, `StrokeButton`, `CTAButton`, or `IconButton` (with `tip`) |
216
+ | `placeholder` as the only label | `FormGroupLabel` with matching `htmlFor` / `id` |
217
+ | `aria-label` on a `<div>` with no role | Add a meaningful `role` or use a semantic element |
218
+ | `role="button"` without Space/Enter handlers | Use a Gamut `*Button`, `Anchor` with `href`, or add `keydown` |
219
+ | `tabIndex={0}` on every item in a composite | Roving tabindex: `0` on active item, `-1` on rest |
220
+ | Tooltip as the only accessible name for a control | Set `aria-label` (or visible text) on the control as well |
221
+ | `aria-hidden="true"` on a focusable element | Also remove from tab order (`tabIndex={-1}`) or restructure |
222
+ | `mousedown` for activation | Use `click` |
223
+ | `outline: none` without a replacement | Use Gamuts built-in focus styles |
224
+ | Multiple `aria-live` regions for the same content stream | One region per logical stream; reuse it across updates |
@@ -1,63 +1,108 @@
1
1
  ---
2
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.
3
+ description: Light/dark behavior, semantic color aliases, Background, color-mode hooks, hardcoded hex fixes. When invoked, read guidelines/foundations/modes.md and color.md first.
4
4
  ---
5
5
 
6
6
  # Gamut ColorMode
7
7
 
8
+ ## Read first
9
+
10
+ When this skill applies, read before writing code:
11
+
12
+ - [`guidelines/foundations/modes.md`](../../guidelines/foundations/modes.md)
13
+ - [`guidelines/foundations/color.md`](../../guidelines/foundations/color.md)
14
+
15
+ Confirm token mappings against root `DESIGN.md` and the active theme — do not assume Codecademy Core values.
16
+
8
17
  Source: `@codecademy/gamut-styles` — [`ColorMode.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-styles/src/ColorMode.tsx)
9
18
 
10
19
  ## Overview
11
20
 
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.
21
+ 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
22
 
14
23
  ### Semantic color aliases
15
24
 
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 |
25
+ | Alias | Purpose |
26
+ | ------------ | ------------------------------------------ |
27
+ | `text` | Standard text color for all type |
28
+ | `background` | Base background color |
29
+ | `primary` | Interactive elements with primary action |
30
+ | `secondary` | Interactive elements with secondary action |
31
+
32
+ This set is not exhaustive (e.g. `text-accent`, `background-disabled`, `danger` — see the light/dark tables in Storybook).
33
+
34
+ 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.
35
+
36
+ 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.
22
37
 
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.
38
+ Agent skill: [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md)`css` / `variant` / `states` with semantic colors alongside ColorMode.
24
39
 
25
40
  ## `<ColorMode />`
26
41
 
27
- Wraps content in a color mode context. Nest these to create scoped mode regions.
42
+ 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.
28
43
 
29
44
  ```tsx
30
45
  import { ColorMode } from '@codecademy/gamut-styles';
31
46
 
32
- // Explicit light or dark mode
33
- const Page = ({ children }) => (
34
- <ColorMode mode="light">{children}</ColorMode>
35
- );
47
+ // Explicit light or dark
48
+ <ColorMode mode="light">{children}</ColorMode>
36
49
 
37
- // Follows the user's OS preference (prefers-color-scheme)
38
- const Page = ({ children }) => (
39
- <ColorMode mode="system">{children}</ColorMode>
40
- );
50
+ // Follow OS light/dark preference (see mode="system" below)
51
+ <ColorMode mode="system">{children}</ColorMode>
52
+ ```
53
+
54
+ Props: `mode="light" | "dark" | "system"`
55
+
56
+ ### `mode="system"` (OS preference)
57
+
58
+ `system` is not a third color theme. It always resolves to `"light"` or `"dark"` based on the user's OS setting.
59
+
60
+ How it works
61
+
62
+ 1. `ColorMode` calls `usePrefersDarkMode()`, which reads `window.matchMedia('(prefers-color-scheme: dark)')`.
63
+ 2. If the query matches → active mode is `"dark"`; otherwise `"light"`.
64
+ 3. Descendants receive that mode's semantic color variables (`text`, `background`, `primary`, etc.) — same as passing `mode="light"` or `mode="dark"` directly.
65
+
66
+ 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.
67
+
68
+ What it does not do
69
+
70
+ - Read in-app preferences (account settings, a theme toggle in localStorage). For those, pass `mode="light"` or `mode="dark"` yourself from your own state.
71
+ - Replace `<Background>`. A colored band still needs `<Background bg="hyper">` if you want contrast-based mode selection for that surface.
72
+
73
+ Prefer `mode="system"` over wiring the hook yourself
74
+
75
+ ```tsx
76
+ // Prefer — ColorMode owns light/dark resolution
77
+ <ColorMode mode="system">{children}</ColorMode>;
78
+
79
+ // Avoid — duplicates what mode="system" already does
80
+ const prefersDark = usePrefersDarkMode();
81
+ <ColorMode mode={prefersDark ? 'dark' : 'light'}>{children}</ColorMode>;
41
82
  ```
42
83
 
43
- **Props**: `mode="light" | "dark" | "system"`
84
+ 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).
85
+
86
+ 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>`.
44
87
 
45
88
  ## `<Background />`
46
89
 
47
- 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.
90
+ 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`).
91
+
92
+ `<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.
48
93
 
49
94
  ```tsx
50
95
  import { Background } from '@codecademy/gamut-styles';
51
96
 
52
97
  // Single background — mode switches automatically if needed
53
- const Card = ({ children }) => (
54
- <Background bg="hyper">{children}</Background>
55
- );
98
+ const Card = ({ children }) => <Background bg="hyper">{children}</Background>;
56
99
 
57
- // Nested backgrounds — each creates its own accessible color context
100
+ // Nested backgrounds — each creates its own color context
58
101
  const Page = () => (
59
- <Background bg="black">
60
- <Background bg="paleGreen" />
102
+ <Background bg="black" p={24}>
103
+ <Background bg="paleGreen" p={24}>
104
+ {/* content inside inner Background uses its own mode */}
105
+ </Background>
61
106
  </Background>
62
107
  );
63
108
  ```
@@ -69,31 +114,36 @@ When `<Background>` is rendered, it sets a `background-current` CSS variable on
69
114
  ## Color mode hooks
70
115
 
71
116
  ```tsx
72
- import { useColorMode, useCurrentMode, usePrefersDarkMode } from '@codecademy/gamut-styles';
117
+ import {
118
+ useColorModes,
119
+ useCurrentMode,
120
+ usePrefersDarkMode,
121
+ } from '@codecademy/gamut-styles';
73
122
 
74
- // Returns [currentModeKey, currentModeColors, allModes]
75
- const [current, currentColors, modes] = useColorMode();
123
+ // [activeModeKey, activeModeColors, allModes, getColorValue]
124
+ const [current, currentColors, modes, getColorValue] = useColorModes();
76
125
 
77
- // Returns just the active mode key: "light" | "dark"
126
+ // Active mode key: "light" | "dark" (optional override argument)
78
127
  const current = useCurrentMode();
128
+ const forced = useCurrentMode('light');
79
129
 
80
- // Returns boolean from window.matchMedia('(prefers-color-scheme: dark)')
130
+ // Boolean from window.matchMedia('(prefers-color-scheme: dark)')
81
131
  const prefersDark = usePrefersDarkMode();
82
132
  ```
83
133
 
84
134
  ## Decision guide
85
135
 
86
- | Need | Use |
87
- |---|---|
88
- | Set a page or section to a specific mode | `<ColorMode mode="light|dark|system">` |
89
- | Place content on a colored background with automatic contrast | `<Background bg="...">` |
90
- | Read the current mode in JavaScript | `useCurrentMode()` |
91
- | Access all modes and their color variables | `useColorMode()` |
92
- | Detect OS dark mode preference | `usePrefersDarkMode()` |
93
- | Access full emotion theme | `useTheme()` from `@emotion/react` |
136
+ | Need | Use |
137
+ | ------------------------------------------------------------- | ---------------------------------- | ---- | --------- |
138
+ | Set a page or section to a specific mode | `<ColorMode mode="light | dark | system">` |
139
+ | Place content on a colored background with automatic contrast | `<Background bg="...">` |
140
+ | Read the current mode in JavaScript | `useCurrentMode()` |
141
+ | Access all modes, variables, and resolve raw colors | `useColorModes()` |
142
+ | Detect OS dark mode preference | `usePrefersDarkMode()` |
143
+ | Access full emotion theme | `useTheme()` from `@emotion/react` |
94
144
 
95
145
  ## Common mistakes to avoid
96
146
 
97
147
  - 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.
148
+ - 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.
149
+ - 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).
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: gamut-components
3
+ description: Implements or reviews Gamut UI components in TSX — Menu, DataTable, DataList, Card, Select, Video, Coachmark, Shimmer, animations, tooltips. Use when no narrower gamut-* skill applies. When invoked, read only the relevant guidelines/components/*.md files.
4
+ paths: '**/*.tsx'
5
+ ---
6
+
7
+ # Gamut components (guideline router)
8
+
9
+ Use when building or changing Gamut components in `.tsx` files and a narrower skill does not already cover the task (`gamut-forms`, `gamut-color-mode`, etc.).
10
+
11
+ ## Read first
12
+
13
+ 1. Read [`guidelines/overview.md`](../../guidelines/overview.md) Step 2 table only (component → guide mapping).
14
+ 2. Read only the `guidelines/components/*.md` files for components you will touch in this task — not all guides.
15
+
16
+ Do not load every guideline file. Skip guides for components you are not using.
17
+
18
+ ## Component → guideline
19
+
20
+ | Component(s) | Guideline |
21
+ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
22
+ | `FillButton`, `StrokeButton`, `TextButton`, `IconButton`, `CTAButton` | [`buttons.md`](../../guidelines/components/buttons.md) |
23
+ | `GridForm`, `ConnectedForm` | [`forms.md`](../../guidelines/components/forms.md) — prefer `gamut-forms` skill |
24
+ | `DataTable`, `DataList` | [`data-table.md`](../../guidelines/components/data-table.md) |
25
+ | `Menu`, `MenuItem`, `MenuSeparator` | [`menu.md`](../../guidelines/components/menu.md) |
26
+ | `Card` | [`card.md`](../../guidelines/components/card.md) |
27
+ | `Select`, `SelectDropdown` | [`select.md`](../../guidelines/components/select.md) |
28
+ | `RadialProgress` | [`radial-progress.md`](../../guidelines/components/radial-progress.md) |
29
+ | `Shimmer`, `Spinner`, `FeatureShimmer` | [`loading-states.md`](../../guidelines/components/loading-states.md) |
30
+ | `Coachmark` | [`coachmark.md`](../../guidelines/components/coachmark.md) |
31
+ | `ToolTip`, `InfoTip`, `PreviewTip` | [`tooltips.md`](../../guidelines/components/tooltips.md) |
32
+ | `Video` | [`video.md`](../../guidelines/components/video.md) |
33
+ | `Rotation`, `ExpandInCollapseOut`, `FadeInSlideOut` | [`animations.md`](../../guidelines/components/animations.md) |
34
+ | `ColorMode`, `Background` | [`modes.md`](../../guidelines/foundations/modes.md) — prefer `gamut-color-mode` skill |
35
+
36
+ Discovery and forms vs. atoms: [`components/overview.md`](../../guidelines/components/overview.md).
37
+
38
+ ## Related skills
39
+
40
+ | Topic | Skill |
41
+ | ---------------------- | ---------------------------------- |
42
+ | Forms | `gamut-forms` |
43
+ | Color / modes | `gamut-color-mode` |
44
+ | Layout / spacing props | `gamut-system-props` |
45
+ | Accessibility | `gamut-accessibility` |
46
+ | Product theme | `gamut-theming` + root `DESIGN.md` |