@codecademy/gamut 68.6.1-alpha.c211a2.0 → 68.6.1-alpha.df4bce.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 (36) hide show
  1. package/agent-tools/.claude-plugin/marketplace.json +16 -0
  2. package/agent-tools/.claude-plugin/plugin.json +7 -0
  3. package/agent-tools/.cursor-plugin/plugin.json +7 -0
  4. package/agent-tools/DESIGN.Codecademy.md +643 -0
  5. package/agent-tools/DESIGN.LXStudio.md +444 -0
  6. package/agent-tools/DESIGN.Percipio.md +435 -0
  7. package/agent-tools/DESIGN.md +1 -0
  8. package/agent-tools/agents/.gitkeep +0 -0
  9. package/agent-tools/commands/gamut-review.md +231 -0
  10. package/agent-tools/guidelines/components/buttons.md +91 -0
  11. package/agent-tools/guidelines/components/overview.md +52 -0
  12. package/agent-tools/guidelines/foundations/color.md +172 -0
  13. package/agent-tools/guidelines/foundations/modes.md +47 -0
  14. package/agent-tools/guidelines/foundations/spacing.md +107 -0
  15. package/agent-tools/guidelines/foundations/typography.md +83 -0
  16. package/agent-tools/guidelines/overview.md +40 -0
  17. package/agent-tools/guidelines/setup.md +81 -0
  18. package/agent-tools/rules/accessibility.mdc +78 -0
  19. package/agent-tools/skills/gamut-accessibility/SKILL.md +214 -0
  20. package/agent-tools/skills/gamut-color-mode/SKILL.md +138 -0
  21. package/agent-tools/skills/gamut-forms/SKILL.md +84 -0
  22. package/agent-tools/skills/gamut-system-props/SKILL.md +203 -0
  23. package/agent-tools/skills/gamut-testing/SKILL.md +221 -0
  24. package/agent-tools/skills/gamut-theming/SKILL.md +113 -0
  25. package/agent-tools/skills/gamut-typography/SKILL.md +75 -0
  26. package/bin/commands/plugin/install.mjs +173 -0
  27. package/bin/commands/plugin/list.mjs +105 -0
  28. package/bin/commands/plugin/remove.mjs +116 -0
  29. package/bin/commands/plugin/update.mjs +49 -0
  30. package/bin/gamut.mjs +92 -0
  31. package/bin/lib/claude.mjs +52 -0
  32. package/bin/lib/cursor.mjs +40 -0
  33. package/bin/lib/figma.mjs +49 -0
  34. package/bin/lib/resolve-plugin-dir.mjs +38 -0
  35. package/bin/lib/run-command.mjs +22 -0
  36. package/package.json +11 -8
@@ -0,0 +1,81 @@
1
+ # Setup
2
+
3
+ ## Install
4
+
5
+ ```sh
6
+ yarn add @codecademy/gamut-kit @emotion/react @emotion/styled
7
+ ```
8
+
9
+ `gamut-kit` bundles `gamut`, `gamut-icons`, `gamut-illustrations`, `gamut-patterns`, `gamut-styles`, `variance`, and `gamut-tests`.
10
+
11
+ **Full guide:** [Meta / Installation](https://gamut.codecademy.com/?path=/docs-meta-installation--page) in Storybook (CSP `nonce` on `GamutProvider`, Jest, Next/Gatsby entry points). For Emotion + TypeScript, add `theme.d.ts` as in [TypeScript (`theme.d.ts`)](#typescript-themedts) below.
12
+
13
+ Optionally add a `peerDependencies` block in `package.json` listing `@codecademy/gamut`, `@codecademy/gamut-icons`, `@codecademy/gamut-illustrations`, `@codecademy/gamut-patterns`, `@codecademy/gamut-styles`, `@codecademy/gamut-tests`, and `@codecademy/variance` (e.g. `"*"`) so editors surface those packages — see Meta / Installation for the JSON snippet.
14
+
15
+ ## Required wrapper
16
+
17
+ Wrap the app root in `<GamutProvider>` from `@codecademy/gamut-styles`. This wires up the theme, color mode, and logical properties for all child components.
18
+
19
+ At runtime, `GamutProvider` defaults to Core when `theme` is omitted (`theme = coreTheme` in the implementation). For non-Core products and for TypeScript (`theme` is required on `GamutProviderProps`), pass `theme` explicitly using the table below.
20
+
21
+ ```tsx
22
+ import { GamutProvider, theme } from '@codecademy/gamut-styles';
23
+
24
+ const App = () => (
25
+ <GamutProvider theme={theme}>{/* app content */}</GamutProvider>
26
+ );
27
+ ```
28
+
29
+ ## Theme selection
30
+
31
+ | Product | Theme to import |
32
+ | ------------------- | ----------------------------- |
33
+ | Codecademy public | `coreTheme` (default `theme`) |
34
+ | Codecademy admin | `adminTheme` |
35
+ | Codecademy platform | `platformTheme` |
36
+ | LX Studio | `lxStudioTheme` |
37
+ | Percipio | `percipioTheme` |
38
+
39
+ All themes are exported from `@codecademy/gamut-styles`.
40
+
41
+ ## TypeScript (`theme.d.ts`)
42
+
43
+ Augment `@emotion/react` so `props.theme` in `styled` / `css` matches the **same theme object** you pass to `<GamutProvider theme={...}>`. If the types disagree, system props and token autocomplete will not line up with runtime.
44
+
45
+ Add a root `theme.d.ts` (or merge into your existing global types):
46
+
47
+ ```tsx
48
+ // theme.d.ts
49
+ import '@emotion/react';
50
+
51
+ import type { CoreTheme } from '@codecademy/gamut-styles';
52
+
53
+ declare module '@emotion/react' {
54
+ export interface Theme extends CoreTheme {}
55
+ }
56
+ ```
57
+
58
+ Use the **theme interface that matches your provider** — same row as the [theme selection](#theme-selection) table:
59
+
60
+ | `GamutProvider` `theme` prop | Import for `Theme extends …` |
61
+ | ---------------------------- | ---------------------------- |
62
+ | `theme` or `coreTheme` | `CoreTheme` |
63
+ | `adminTheme` | `AdminTheme` |
64
+ | `platformTheme` | `PlatformTheme` |
65
+ | `lxStudioTheme` | `LxStudioTheme` |
66
+ | `percipioTheme` | `PercipioTheme` |
67
+
68
+ Example when the app uses Percipio:
69
+
70
+ ```tsx
71
+ // theme.d.ts
72
+ import '@emotion/react';
73
+
74
+ import type { PercipioTheme } from '@codecademy/gamut-styles';
75
+
76
+ declare module '@emotion/react' {
77
+ export interface Theme extends PercipioTheme {}
78
+ }
79
+ ```
80
+
81
+ See Emotion’s [TypeScript / define a theme](https://emotion.sh/docs/typescript#define-a-theme) for details.
@@ -0,0 +1,78 @@
1
+ ---
2
+ description: Apply these guardrails when editing Gamut UI in TS/JS/TSX/JSX. Universal rules (always loaded). Form wiring depth: **`gamut-forms`** skill; other component matrix and audit detail: **`gamut-accessibility`** — those skills do not repeat this rule set.
3
+ alwaysApply: true
4
+ globs: ['*.tsx', '*.ts', '*.jsx', '*.js']
5
+ ---
6
+
7
+ # Gamut Accessibility Rules
8
+
9
+ ## Prefer HTML over ARIA
10
+
11
+ 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.
12
+
13
+ ## A Role is a Promise
14
+
15
+ 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.
16
+
17
+ ## ARIA can both cloak and enhance
18
+
19
+ 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.
20
+
21
+ ## Align accessible names with visible copy
22
+
23
+ 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.
24
+
25
+ ## Treat missing visible labels as a design smell
26
+
27
+ 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.
28
+
29
+ ```html
30
+ <!-- smell: this list has no conceptual name, so we have to create one using ARIA -->
31
+ <ul aria-label="List heading">
32
+ <li>...</li>
33
+ </ul>
34
+
35
+ <!-- better: the list's name is visible and can be used for its accessible name -->
36
+ <h2 id="list-name">List heading</h2>
37
+ <ul aria-labelledby="list-name">
38
+ <li>...</li>
39
+ </ul>
40
+ ```
41
+
42
+ ## Use Gamut primitives — do not fake buttons or dialogs
43
+
44
+ - **Actions:** use Gamut button atoms (`FillButton`, `TextButton`, `StrokeButton`, `CTAButton`, `IconButton`) — not `<div onClick>`, not `<span role="button">`, not `<a>` without `href` for actions. Variant and `tip` guidance: [`guidelines/components/buttons.md`](../guidelines/components/buttons.md).
45
+ - **Tabs / overlays:** `Tabs`, `Dialog`, `Modal`, and related primitives implement keyboard and focus patterns in code — still supply labels, titles, and trigger semantics as documented in the skill.
46
+
47
+ ## Every interactive control needs an accessible name
48
+
49
+ - **`IconButton`** — provide `tip` (accessible name for icon-only).
50
+ - **`InfoTip`** — provide `ariaLabel` or `ariaLabelledby`; there is no automatic fallback.
51
+ - Decorative icon SVGs next to visible text — `aria-hidden="true"` on the icon.
52
+
53
+ ## Form label association
54
+
55
+ Match **`htmlFor`** on **`<FormGroupLabel>`** with the **`id`** on the control. Base **`<FormGroup>`** renders live regions for **`error`** and **`description`**; **`GridForm`** and **`ConnectedForm`** add field wiring (**`aria-describedby`**, **`aria-invalid`**, first-error **`aria-live`** behavior) — do not add redundant duplicate regions. Depth: **[`skills/gamut-forms/SKILL.md`](../skills/gamut-forms/SKILL.md)**.
56
+
57
+ ## Screen-reader-only text
58
+
59
+ Use `<Text screenreader>` for visually hidden but announced content. `<HiddenText>` is deprecated.
60
+
61
+ ## Color and contrast
62
+
63
+ Do not hardcode hex for adaptive UI. Prefer semantic tokens and **`ColorMode` / `<Background>`** so surfaces track theme and mode — see the **`gamut-color-mode`** skill and [`foundations/modes.md`](../guidelines/foundations/modes.md). Default pairings support accessible UI, but **tokens do not guarantee WCAG compliance** for every layout; validate non-standard combinations.
64
+
65
+ ## Focus visibility
66
+
67
+ Never suppress focus indicators with `outline: none` or `outline: 0` without a visible replacement. Gamut’s focus styles are intentional (WCAG 2.4.7).
68
+
69
+ ## Where to read more (minimal index)
70
+
71
+ | Topic | Primary doc |
72
+ | --- | --- |
73
+ | Forms (`GridForm`, `ConnectedForm`, `FormGroup`, validation, live regions) | [`skills/gamut-forms/SKILL.md`](../skills/gamut-forms/SKILL.md) |
74
+ | Component matrix (tips, overlays, composites, checklists; not form wiring) | [`skills/gamut-accessibility/SKILL.md`](../skills/gamut-accessibility/SKILL.md) |
75
+ | Button variants, `IconButton` `tip`, `disabled` vs `aria-disabled` | [`guidelines/components/buttons.md`](../guidelines/components/buttons.md) |
76
+ | ColorMode, `Background`, semantic color roles | **`gamut-color-mode`** skill · [`foundations/modes.md`](../guidelines/foundations/modes.md) · [`foundations/color.md`](../guidelines/foundations/color.md) |
77
+ | Tokens, `css` / `variant` / `states` | Storybook [Meta / Best practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page) |
78
+ | Install, `GamutProvider`, CSP | Storybook [Meta / Installation](https://gamut.codecademy.com/?path=/docs-meta-installation--page) |
@@ -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 — verify behavior in Storybook and source, do not assume React Aria.
9
+
10
+ **Product-oriented button variants and props:** [`guidelines/components/buttons.md`](../../guidelines/components/buttons.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 [`buttons.md`](../../guidelines/components/buttons.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,138 @@
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
+ ## `<ColorMode />`
30
+
31
+ 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.
32
+
33
+ ```tsx
34
+ import { ColorMode } from '@codecademy/gamut-styles';
35
+
36
+ // Explicit light or dark
37
+ <ColorMode mode="light">{children}</ColorMode>
38
+
39
+ // Follow OS light/dark preference (see mode="system" below)
40
+ <ColorMode mode="system">{children}</ColorMode>
41
+ ```
42
+
43
+ **Props**: `mode="light" | "dark" | "system"`
44
+
45
+ ### `mode="system"` (OS preference)
46
+
47
+ `system` is **not** a third color theme. It always resolves to `"light"` or `"dark"` based on the user's OS setting.
48
+
49
+ **How it works**
50
+
51
+ 1. `ColorMode` calls `usePrefersDarkMode()`, which reads `window.matchMedia('(prefers-color-scheme: dark)')`.
52
+ 2. If the query matches → active mode is `"dark"`; otherwise `"light"`.
53
+ 3. Descendants receive that mode's semantic color variables (`text`, `background`, `primary`, etc.) — same as passing `mode="light"` or `mode="dark"` directly.
54
+
55
+ **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.
56
+
57
+ **What it does not do**
58
+
59
+ - Read in-app preferences (account settings, a theme toggle in localStorage). For those, pass `mode="light"` or `mode="dark"` yourself from your own state.
60
+ - Replace `<Background>`. A colored band still needs `<Background bg="hyper">` if you want contrast-based mode selection for that surface.
61
+
62
+ **Prefer `mode="system"` over wiring the hook yourself**
63
+
64
+ ```tsx
65
+ // Prefer — ColorMode owns light/dark resolution
66
+ <ColorMode mode="system">{children}</ColorMode>;
67
+
68
+ // Avoid — duplicates what mode="system" already does
69
+ const prefersDark = usePrefersDarkMode();
70
+ <ColorMode mode={prefersDark ? 'dark' : 'light'}>{children}</ColorMode>;
71
+ ```
72
+
73
+ 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).
74
+
75
+ 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>`.
76
+
77
+ ## `<Background />`
78
+
79
+ 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`).
80
+
81
+ `<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.
82
+
83
+ ```tsx
84
+ import { Background } from '@codecademy/gamut-styles';
85
+
86
+ // Single background — mode switches automatically if needed
87
+ const Card = ({ children }) => <Background bg="hyper">{children}</Background>;
88
+
89
+ // Nested backgrounds — each creates its own color context
90
+ const Page = () => (
91
+ <Background bg="black" p={24}>
92
+ <Background bg="paleGreen" p={24}>
93
+ {/* content inside inner Background uses its own mode */}
94
+ </Background>
95
+ </Background>
96
+ );
97
+ ```
98
+
99
+ ### `background-current` CSS variable
100
+
101
+ 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).
102
+
103
+ ## Color mode hooks
104
+
105
+ ```tsx
106
+ import {
107
+ useColorModes,
108
+ useCurrentMode,
109
+ usePrefersDarkMode,
110
+ } from '@codecademy/gamut-styles';
111
+
112
+ // [activeModeKey, activeModeColors, allModes, getColorValue]
113
+ const [current, currentColors, modes, getColorValue] = useColorModes();
114
+
115
+ // Active mode key: "light" | "dark" (optional override argument)
116
+ const current = useCurrentMode();
117
+ const forced = useCurrentMode('light');
118
+
119
+ // Boolean from window.matchMedia('(prefers-color-scheme: dark)')
120
+ const prefersDark = usePrefersDarkMode();
121
+ ```
122
+
123
+ ## Decision guide
124
+
125
+ | Need | Use |
126
+ | ------------------------------------------------------------- | ---------------------------------- | ---- | --------- |
127
+ | Set a page or section to a specific mode | `<ColorMode mode="light | dark | system">` |
128
+ | Place content on a colored background with automatic contrast | `<Background bg="...">` |
129
+ | Read the current mode in JavaScript | `useCurrentMode()` |
130
+ | Access all modes, variables, and resolve raw colors | `useColorModes()` |
131
+ | Detect OS dark mode preference | `usePrefersDarkMode()` |
132
+ | Access full emotion theme | `useTheme()` from `@emotion/react` |
133
+
134
+ ## Common mistakes to avoid
135
+
136
+ - 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.
137
+ - 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.
138
+ - 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,84 @@
1
+ ---
2
+ name: gamut-forms
3
+ description: Implementing or auditing Gamut forms — FormGroup, ConnectedForm, ConnectedFormGroup, GridForm, react-hook-form wiring, labels, and accessible error/description regions. Pair with **`gamut-accessibility`** for non-form widgets and **`accessibility.mdc`** for universal HTML/ARIA rules.
4
+ ---
5
+
6
+ # Gamut forms
7
+
8
+ Canonical wiring for **`FormGroup`**, **`ConnectedForm`**, **`ConnectedFormGroup`**, **`GridForm`**, and field renderers. Source: **`packages/gamut/src/Form/`**, **`ConnectedForm/`**, **`GridForm/`**.
9
+
10
+ Universal label and primitive guidance: **[`accessibility.mdc`](../../rules/accessibility.mdc)** · overlay and composite patterns: **[`gamut-accessibility`](../gamut-accessibility/SKILL.md)**.
11
+
12
+ ---
13
+
14
+ ## Prefer connected layouts
15
+
16
+ For typical product forms, prefer **`GridForm`** (declarative **`fields`**, **`LayoutGrid`**, submit/cancel) or **`ConnectedForm`** with **`ConnectedFormGroup`** / **`useConnectedForm`**. Use raw **`FormGroup`** + atoms only when the layout is simple and you fully own **`id`**, **`htmlFor`**, invalid state, and any **`aria-describedby`** (see below).
17
+
18
+ ---
19
+
20
+ ## Labels and controls
21
+
22
+ **`htmlFor`** / **`id`** pairing — universal rule in **[`accessibility.mdc`](../../rules/accessibility.mdc)** (Form label association). Form-specific notes:
23
+
24
+ - **`FormGroupLabel`** → control **`id`** (or stable **`name`** when that is your field’s id convention).
25
+ - **Checkbox**, **Radio**, **Select**: same pairing; checkbox/radio use the visually hidden input pattern from **`@codecademy/gamut-styles`** where applicable.
26
+
27
+ ---
28
+
29
+ ## `FormGroup` (baseline)
30
+
31
+ [`FormGroup.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/Form/elements/FormGroup.tsx)
32
+
33
+ - **`description`** → **`FormGroupDescription`** with **`aria-live="assertive"`**.
34
+ - **`error`** (string) → **`FormError`** with **`aria-live="polite"`** and **`role="alert"`**.
35
+
36
+ Raw **`FormGroup`** does **not** set **`aria-describedby`** or **`aria-invalid`** on **`children`**. If you compose fields outside **`ConnectedFormGroup`** / **`GridForm`**, wire those yourself or accept that only the live regions above communicate errors/descriptions.
37
+
38
+ ---
39
+
40
+ ## `ConnectedFormGroup`
41
+
42
+ [`ConnectedFormGroup.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/ConnectedForm/ConnectedFormGroup.tsx)
43
+
44
+ - Passes **`aria-describedby`** (error region id when shown) and **`aria-invalid`** on the rendered field component.
45
+ - **`FormError`**: **`aria-live="assertive"`** and **`role="alert"`** only when **`isFirstError`**; otherwise **`aria-live="off"`** and **`role="status"`** so subsequent errors do not interrupt repeatedly.
46
+
47
+ ---
48
+
49
+ ## `GridForm`
50
+
51
+ [`GridFormInputGroup/index.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/GridForm/GridFormInputGroup/index.tsx) · [`GridFormTextInput`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/GridForm/GridFormInputGroup/GridFormTextInput/index.tsx)
52
+
53
+ - Composes **`ConnectedForm`**, **`LayoutGrid`**, **`GridFormButtons`**, and field metadata. **`FormError`** uses the same **first-error assertive** pattern as **`ConnectedFormGroup`** (**`aria-live`** assertive vs off, **`role`** alert vs status).
54
+ - Built-in text inputs set **`aria-invalid`** and register with **react-hook-form** via **`register`**. Custom / **`custom`** / **`custom-group`** renderers must still expose correct **`id`**, **`label`**, and error surfacing consistent with this pattern.
55
+
56
+ ---
57
+
58
+ ## Live regions — do not double up
59
+
60
+ **`FormGroup`**, **`ConnectedFormGroup`**, and **`GridForm`** already render **`FormError`** (and base **`FormGroup`** renders **`FormGroupDescription`**) with live-region attributes. Do not add a second **`aria-live`** wrapper for the same message stream.
61
+
62
+ ---
63
+
64
+ ## Storybook
65
+
66
+ - [Organisms / GridForm / About](https://gamut.codecademy.com/?path=/docs-organisms-gridform-about--docs) · [Usage](https://gamut.codecademy.com/?path=/docs-organisms-gridform-usage--docs) · [Validation](https://gamut.codecademy.com/?path=/docs-organisms-gridform-validation--docs) · [Fields](https://gamut.codecademy.com/?path=/docs-organisms-gridform-fields--docs)
67
+ - [Organisms / ConnectedForm / ConnectedForm](https://gamut.codecademy.com/?path=/docs-organisms-connectedform-connectedform--docs) · [ConnectedFormGroup](https://gamut.codecademy.com/?path=/docs-organisms-connectedform-connectedformgroup--docs)
68
+
69
+ ---
70
+
71
+ ## Example — baseline `FormGroup`
72
+
73
+ ```tsx
74
+ <FormGroup
75
+ htmlFor="email-input"
76
+ description="Used for login"
77
+ error={errors.email}
78
+ >
79
+ <FormGroupLabel htmlFor="email-input">Email</FormGroupLabel>
80
+ <Input id="email-input" type="email" />
81
+ </FormGroup>
82
+ ```
83
+
84
+ When using **`ConnectedFormGroup`** or **`GridForm`**, prefer their docs and defaults over hand-rolling the above for every field.