Use this skill when auditing existing code for Gamut usage and you need a consolidated report — checks dependencies, setup, import patterns, styled() wrapping that bypasses system props, hardcoded colors, bespoke component duplication, and test setup, with pointers to remediation skills.
Scanned 9/1/2026
Install to Claude Code
npx -y skills add Codecademy/gamut --skill gamut-review --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Gamut Review?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/codecademy-gamut-review)More formats (shields.io, HTML) on the badges page.
---
name: gamut-review
description: Use this skill when auditing existing code for Gamut usage and you need a consolidated report — checks dependencies, setup, import patterns, styled() wrapping that bypasses system props, hardcoded colors, bespoke component duplication, and test setup, with pointers to remediation skills.
---
# Gamut Review
Audit existing code at the path the user provides (default: current working directory). Find violations and misuse; do not generate new code.
When `DESIGN.md` is present at the audit root, use it as the authoritative reference for product design intent, token names, and component patterns. It is copied from `DESIGN.Codecademy.md`, `DESIGN.Percipio.md`, or `DESIGN.LXStudio.md` in `@codecademy/gamut` agent-tools (via `gamut plugin install --theme <name>`). When a finding maps to a skill, note it in the report so the developer knows where to get remediation guidance.
Run Check 0 first, then Checks 1–6, then print a single consolidated report using the format at the end of this file.
Remediation skills: [`gamut-theming`](../gamut-theming/SKILL.md) · [`gamut-color-mode`](../gamut-color-mode/SKILL.md) · [`gamut-system-props`](../gamut-system-props/SKILL.md) · [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md) · [`gamut-typography`](../gamut-typography/SKILL.md) · [`gamut-testing`](../gamut-testing/SKILL.md) · [`gamut-z-index`](../gamut-z-index/SKILL.md) · [`gamut-component-first`](../gamut-component-first/SKILL.md)
---
## Check 0 — DESIGN.md present
Resolve the audit root from the user's path if provided, otherwise the current working directory. Look for `DESIGN.md` at that root (not inside `node_modules` or package subfolders unless the audit path is explicitly that folder).
| Result | Action |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Found | Report `✓ DESIGN.md present (<path>)`. Proceed with Checks 1–5 using this file for product/theme context. |
| Missing | Report `✗ DESIGN.md not found` as a blocking finding. Include remediation: from the repo root run `gamut plugin install cursor --theme core` (or `percipio`, `lxstudio`, `admin`, `platform`; also `claude`), or manually copy the matching `DESIGN.*.md` from `@codecademy/gamut` agent-tools and rename to `DESIGN.md`. Still run Checks 1–3 and 5. For Check 4, list hex violations with `palette:` / `semantic:` only where Appendix A/B apply without product YAML — prefix the Hardcoded colors section with `⚠ low confidence — no DESIGN.md` and do not assume Codecademy Core semantics; do not use Appendix B shortcuts as authoritative. |
---
## Check 1 — Dependencies
Read `package.json` at the audit root. Inspect `dependencies`, `devDependencies`, and `peerDependencies` combined.
| Package | Expectation |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@codecademy/gamut` | Required — core component library |
| `@codecademy/gamut-styles` | Recommended — design tokens and theme primitives |
| `@codecademy/variance` | Recommended — style-prop system used by Gamut internals |
| `@codecademy/gamut-kit` | Acceptable alternative meta-package — re-exports `@codecademy/gamut`, `@codecademy/gamut-styles`, `@codecademy/variance`, and more. Treat its presence as satisfying the three rows above; do not separately flag those packages as missing. **Caveat:** requires npm or yarn with `nodeLinker: node-modules`; not compatible with yarn Plug'n'Play. Flag as ⚠ warning if `.yarnrc.yml` shows `nodeLinker: pnp`. |
---
## Check 2 — Setup
First detect whether the project uses TypeScript by checking for `tsconfig.json` at the audit root or `typescript` in `package.json` `dependencies`/`devDependencies`. Use this to set severity for the Theme augmentation row below.
Search source files (`.ts`, `.tsx`, `.js`, `.jsx`) for these symbols. Skip `node_modules`, `dist`, `.next`, `build`, `.turbo`.
| Symbol | Expectation |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GamutProvider` | Required — must appear at least once (app root wrapper) |
| `ColorMode` | Recommended — enables semantic light/dark theming |
| `Background` | Recommended — semantic surface color via `@codecademy/gamut-styles` |
| `declare module '@emotion/react'` | **Required if TypeScript** — `Theme` must be augmented with the active theme type (e.g. `CoreTheme`) so scale props type-check correctly; grep `.d.ts` and `.ts`/`.tsx` source files for this string. **Recommended if not TypeScript** — note that adopting TypeScript is recommended and that `theme.d.ts` will be needed when it is. |
For each found symbol report the first file path where it appears.
**Conditional — `StyleProps` with `states()`/`variant()`**: If source files contain `states(` or `variant(` from `@codecademy/gamut-styles`, check whether component prop interfaces use `StyleProps<typeof ...>` from `@codecademy/variance`. When `states()`/`variant()` are present but `StyleProps` is absent from associated component interfaces, report as ⚠ warning: `StyleProps not used — state/variant props may be untyped`. Remediation: `import { StyleProps } from '@codecademy/variance'` and add `extends StyleProps<typeof myStates>` to the component interface. Skill reference: [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md).
---
## Check 3 — Import patterns
Grep source files for any of these patterns. Each match is an error.
| Pattern | Reason |
| ------------------------------- | ----------------------------------------------------------------------- |
| `@codecademy/gamut/dist/` | Deep dist import — bypasses public API and breaks on internal refactors |
| `@codecademy/gamut/src/` | Deep src import — not part of the published package |
| `@codecademy/gamut-styles/src/` | Deep src import — use the package root |
| `@codecademy/variance/src/` | Deep src import — use the package root |
Report each violation as `file:line`.
---
## Check 3b — SCSS/CSS module imports, className, and inline styles on Gamut components
Gamut components are styled via the variance system (system props, `css()`, `variant()`, `states()` from `@codecademy/gamut-styles`). Importing SCSS/CSS modules, passing `className`, and passing an inline `style` prop to Gamut components all bypass this system, break ColorMode token propagation, and prevent system props from composing correctly.
**Step 1 — SCSS/CSS module imports**
Grep source files (`.ts`, `.tsx`, `.js`, `.jsx`) for:
```
import .* from '.*\.(scss|css)'
```
Skip `node_modules`, `dist`. Each match is an error unless:
- The import targets a third-party stylesheet (e.g. a carousel or date-picker vendor sheet that cannot be replaced) — flag as ⚠ warning with note "third-party vendor styles".
- The file is a global reset or application shell (not a component) — flag as ⚠ warning.
Report the count and list of files. If there are more than 5 files, group by directory and report totals rather than listing every file.
**Step 2 — className on Gamut components**
Grep source files for `className=` appearing on any of the core Gamut component names in the same JSX element opening tag. The known Gamut components to check:
```
Box, FlexBox, Column, LayoutGrid, GridBox, Card, Text, Anchor,
FillButton, StrokeButton, TextButton, CTAButton, IconButton, Toggle,
List, ListRow, ListCol, Background, Disclosure
```
Pattern (grep, case-sensitive):
```
<(Box|FlexBox|Column|LayoutGrid|GridBox|Card|Text|Anchor|FillButton|StrokeButton|TextButton|CTAButton|IconButton|Toggle|List|ListRow|ListCol|Background|Disclosure)\b[^>]*\bclassName=
```
Each match is an error. Report as `file:line <ComponentName className={...}>`.
Severity note: `className` is not always forbidden — some Gamut components accept it for integration with third-party tools (e.g. passing a class to an external drag-and-drop library). Downgrade to ⚠ warning only when the usage is clearly an integration seam, not styling.
**Step 3 — inline `style` prop**
Grep source files (`.ts`, `.tsx`, `.js`, `.jsx`) for an inline `style` prop on any JSX element, and specifically on the known Gamut component names from Step 2:
```
style=\{\{
```
and
```
<(Box|FlexBox|Column|LayoutGrid|GridBox|Card|Text|Anchor|FillButton|StrokeButton|TextButton|CTAButton|IconButton|Toggle|List|ListRow|ListCol|Background|Disclosure)\b[^>]*\bstyle=\{
```
Skip `node_modules`, `dist`. Each match is an error — an inline `style` object silently bypasses ColorMode and the variance system exactly like `className` does, and its values are almost always hardcoded hex or pixel literals rather than tokens.
Before reporting a match, check the matched line and the line immediately above it for an `eslint-disable` / `eslint-disable-next-line` comment referencing a style-related rule (e.g. `react/forbid-component-props`, `react/forbid-dom-props`, or any bare `eslint-disable(-next-line)` directly above the match). If found, still report the match but annotate it `(eslint-disabled)` and downgrade to ⚠ warning — someone made a deliberate, reviewed exception, but the suppression may be stale or broader than intended, so surface it rather than silently skip it.
Report as `file:line <ComponentName style={{...}}>` (or the bare JSX tag when not a known Gamut component).
Severity note: like `className`, downgrade to ⚠ warning only for a clear third-party integration seam (e.g. a style object required by an external widget's API) — not for layout or color values that a Gamut component or system prop could express.
Remediation: replace SCSS module rules and inline `style` objects with system props directly on the Gamut component — use semantic ColorMode tokens as values (`color="text"`, `bg="background"`, `borderColor="border-primary"`, etc.) rather than hardcoded hex, pixel literals, or inline objects; use `css()`, `variant()`, or `states()` from `@codecademy/gamut-styles` (with `styled` from `@emotion/styled`) for styles not expressible as system props; delete the SCSS file when all rules are migrated.
Skill references: [`gamut-system-props`](../gamut-system-props/SKILL.md) · [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md) · [`gamut-color-mode`](../gamut-color-mode/SKILL.md)
---
## Check 3c — Nested selectors
Nested selectors inside styled-component or Emotion template literals cause hard-to-isolate side effects and make consistent updates difficult. The [Gamut Best Practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page) page flags two kinds as "at your own risk": tag selectors and Gamut component selectors.
**Step 1 — Tag selectors**
Grep source files (`.ts`, `.tsx`, `.js`, `.jsx`) for bare HTML tag names appearing as CSS selector lines inside styled-component or Emotion template literals. Skip `node_modules`, `dist`, `.next`, `build`, `.turbo`.
- Pattern A (named tags):
```
^\s*(div|span|p|ul|li|ol|a|img|h[1-6]|table|thead|tbody|tr|td|th|form|section|header|footer|nav|main)\s*\{
```
- Pattern B (universal selector):
```
^\s*\*\s*\{
```
False-positive risk: `*` appears in JSDoc comment bodies (` * {`). Post-filter matches where the line is a comment (starts with `//` or matches `\s*\*\s`). For remaining matches, verify context before marking as a violation.
Do NOT include SVG primitive tag names (`path`, `rect`, `circle`, `line`, `polyline`, `svg`) — styled SVG primitives in icon and form components are normal and not the target of this rule.
Exemptions (downgrade to `ℹ note`, not warning):
- Files that import `Global` from `@emotion/react` — intentional global reset/injection stylesheets.
- Files whose name matches `*reboot*`, `*reset*`, `*global*`, or `*base-styles*`.
**Step 2 — Gamut component selectors**
Rather than enumerating every Gamut component by name (brittle, misses new additions), scope to files that already import from `@codecademy/gamut`, then grep those files for any PascalCase identifier used as a CSS selector:
1. Find files that contain `from '@codecademy/gamut'`.
2. In those files, grep for:
```
\$\{[A-Z][A-Za-z]+\}[^{]*\{
```
This matches any `${PascalCaseName}` followed by a rule block — i.e., a component used as a CSS child selector.
Each match means a component is being targeted from a parent styled wrapper rather than styled directly. Report as `file:line ${ComponentName} { ... }`.
Severity note: `&:pseudo ${ComponentName}` (pseudo-class combinator preceding the interpolation) is lower risk — downgrade those to ⚠ warning with a note to verify scope. Bare `${ComponentName} { }` selector blocks are the primary target.
**Severity:** ⚠ warning for all matches (per Best Practices: "you may still do so, but at your own risk").
**Remediation:**
_Tag selectors_ — plain HTML elements do not need to become Gamut components. Two valid paths:
- Use `Box`, `FlexBox`, or `GridBox` with the `as` prop to render as the intended element — no extra DOM node needed: `<Box as="section" p={16} color="text">`, `<FlexBox as="nav" gap={8}>`.
- Style in place with `styled.div(css({ color: 'text', p: 16 }))` using semantic ColorMode tokens from `@codecademy/gamut-styles` — keeps the element but brings it into the design system token graph.
Replace the parent's nested selector rule with one of the above and remove the selector block.
_Gamut component selectors_ — pass system props directly to the component (`alignSelf`, `mt`, etc.) rather than targeting it from a parent wrapper. Where dynamic behavior spans multiple children, prefer `css()` with `variant()` or `states()` from `@codecademy/gamut-styles` keyed to data attributes or boolean props on the parent.
Skill references: [`gamut-system-props`](../gamut-system-props/SKILL.md) · [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md)
---
## Check 3d — `styled(GamutComponent)` bypassing system props
Wrapping an already-Gamut component in `styled()` and writing raw CSS (a tagged template or plain object, not `css()`/`variant()`/`states()`) is the same bypass as `className`/inline `style` on a Gamut component wearing a different hat — none of it gets ColorMode token resolution, responsive-prop scaling, or the variance pipeline, and it duplicates an API the wrapped component already exposes as props.
**Step 1 — scope to files that already import from `@codecademy/gamut`**
Same technique as Check 3c Step 2 (enumerating every component name by hand is brittle and misses new additions). Find files containing:
```
from '@codecademy/gamut'
```
**Step 2 — grep those files for `styled(PascalCaseName)` not wrapped in `css()`/`variant()`/`states()`**
Two patterns, both violations:
```
styled\([A-Z][A-Za-z]+\)`
```
```
styled\([A-Z][A-Za-z]+\)\(\s*\{
```
The first catches the tagged-template form (`` styled(Box)`display: flex;` ``); the second catches the plain-object function-call form (`styled(Box)({ display: 'flex' })`). `styled(Box)(css({...}))`, `styled(Box)(variant({...}))`, and `styled(Box)(states({...}))` do **not** match either pattern — the character right after the opening `(` is a letter, not a backtick or `{`. That's the compliant form; don't flag it.
Confirm the matched name is actually the Gamut import in that file (not an unrelated same-named local component) before reporting.
**Step 3 — classify each match**
Read the properties inside the flagged block:
- **Every property has a direct system-prop equivalent** (layout/flex/space/color/border/positioning/typography — see [`gamut-system-props`](../gamut-system-props/SKILL.md) prop groups) → ✗ error. Remediation: delete the `styled()` wrapper; pass the same values as props directly on the JSX element. Flag `display: 'flex'`/`display:flex` specifically — recommend `FlexBox` instead of `Box` + a `display` prop. Note that `background` (unlike `bg`) has no token scale and takes any CSS value as-is — a gradient string is already valid as a plain `background` prop and does **not** by itself justify a `styled()` wrapper.
- **Something in the block isn't expressible as a prop** (`background-clip`, `background-blend-mode`, a variant that should branch on a prop, pseudo-selectors) → ⚠ warning. Remediation: keep `styled(ComponentName)`, but move the object into `css()`, `variant()`, or `states()` from `@codecademy/gamut-styles` instead of a raw literal — and pull the properties that _are_ expressible (padding, display, plain colors, gradients via `background`) back out to props rather than leaving them in the escape hatch just because one sibling property forced it.
**Step 4 — raw CSS property names inside `css()` silently skip the scale lookup**
Being wrapped in `css()` (the compliant form from Step 2) isn't enough on its own to guarantee token-scale treatment. `css()`'s scale-aware behavior only applies to the _alias_ keys variance defines (`p`, `m`, `bg`, `borderRadius`, `fontSize`, `fontWeight`, `lineHeight`, …) — not the literal CSS property name. `padding: 24` inside a `css({...})` call is **not** the same as `p: 24`: variance's static-CSS extraction (`getStaticCss` in `packages/variance/src/core.ts`) passes any key it doesn't recognize as an alias straight through as literal, unscaled CSS. A block that _looks_ correct because it's wrapped in `css()` can still bypass the spacing/typography scale entirely if the property inside is spelled out as the raw CSS name instead of its alias.
Grep for raw CSS property names as keys inside a `css({...})` call, where a shorter alias exists:
```
css\(\s*\{[^}]*\b(padding|margin|background-color|border-radius|font-size|font-weight|line-height)\s*:
```
(non-exhaustive — the pattern is: any CSS property name used as a key where its alias — `p`/`m`/`bg`/`borderRadius`/`fontSize`/`fontWeight`/`lineHeight` — belongs instead.)
Each match is **✗ error**, not a style nit — the value silently doesn't get the intended token/rem conversion regardless of what number is written, which is a functional bug, not a preference. This check applies independently of Step 3's classification — a block can pass Step 3 (genuinely needs `css()` for one property) and still fail Step 4 (a _different_ property inside that same block used the wrong key name).
**Step 5 — plain `<Box display="flex">`/`<Box display="grid">` (heuristic, not a bypass)**
Unlike Steps 1–4, this isn't a system-props bypass — `display` is a real system prop and the code works correctly. It's a semantic-consistency nit: `FlexBox`/`GridBox` already default to `display: flex`/`display: grid`, so reaching for `Box` + a `display` prop says the same thing more verbosely and loses the more legible component name at the call site.
Grep files already confirmed to import `Box` from `@codecademy/gamut` (Step 1) for:
```
<Box\b[^>]*\bdisplay=\{?["']flex["']\}?
<Box\b[^>]*\bdisplay=\{?["']grid["']\}?
```
Always **⚠ warning**, never ✗ — don't flag `display="inline-flex"`/`"inline-grid"`, or a conditionally-computed `display` (e.g. `display={isOpen ? 'flex' : 'none'}`); both are legitimate reasons to stay on `Box`.
Report as `file:line styled(ComponentName)` with the classification and the specific properties found, e.g.:
```
src/HeroSection.tsx:14 styled(Box)`...` — display, flex-direction, padding, color: white → delete wrapper, use FlexBox + props (color: use a semantic token)
src/ColumnTitle.tsx:8 styled(Box)`...` — background-clip: text + padding → wrap in css(), move padding to a prop
src/GlowShell.tsx:15 css({ padding: 24, ... }) — 'padding' is not a recognized alias; use 'p' or the value bypasses the spacing scale entirely (renders as an unscaled literal px value), even though the block correctly stays in css() for the gradient
src/Panel.tsx:9 <Box display="flex" ...> — same defaults as FlexBox, more legible as FlexBox
src/Grid.tsx:14 <Box display="grid" ...> — same defaults as GridBox, more legible as GridBox
```
Skill references: [`gamut-system-props`](../gamut-system-props/SKILL.md#dont-wrap-a-gamut-component-in-styled-to-hand-write-css) · [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md)
---
## Check 4 — Hardcoded colors (semantic-first)
Rule: Inline hex literals in application UI code are violations. Remediation is not “replace hex with `navy-800`” — prefer semantic ColorMode tokens (`text`, `background`, `primary`, …) so light/dark and theme switches stay correct. Reserve raw palette tokens for colors that must stay fixed and for `bg` on `<Background>` from `@codecademy/gamut-styles` (section surfaces with content).
Align findings with project docs and Storybook:
- [`gamut-color-mode`](../gamut-color-mode/SKILL.md) skill — semantic alias tables and decision guide.
- [Foundations / ColorMode](https://gamut.codecademy.com/?path=/docs-foundations-colormode--page) — aliases per mode; `<Background>` behavior.
- [Meta / Best practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page) — semantic colors + `css` / `variant` / `states` from `gamut-styles`.
- Foundations / Theme stories (Core, Admin, Platform, Percipio, LX Studio) — verify hex ↔ semantic if the product is not Codecademy Core.
Theme context: If Check 0 passed, infer product/theme from root `DESIGN.md` and `GamutProvider` / app config. If Check 0 failed, follow the low-confidence rules in Check 0 — do not assume Codecademy Core semantics. If `DESIGN.md` exists but theme is still unclear, add a report note to confirm against the correct theme Storybook page.
Discovery: Grep source files (`.ts`, `.tsx`, `.js`, `.jsx`, `.css`, `.scss`, `.less`) for inline hex literals (`#RGB` or `#RRGGBB`). Comparison is case-insensitive. Skip `node_modules`, `dist`, `.next`, `build`, `.turbo` (same spirit as other checks).
### Workflow (each hex match)
1. Context — Inspect the surrounding line(s): CSS property (`color`, `background`, `border-color`, …), JSX prop (`color`, `bg`, `borderColor`, SVG fill), or asset. Note whether the subtree is a section with content (candidate for `<Background>` + palette `bg`) vs component chrome (prefer semantics).
2. Identify palette — Normalize hex (case-insensitive); map to a Gamut palette name using Appendix A below. If missing from the appendix, match against `DESIGN.md` / `packages/gamut-styles` palette definitions.
3. Recommend semantic first — Use Appendix B (Core light literals) plus role:
- Body / UI foreground → `text`; strong emphasis → `text-accent`.
- Page or card fill → `background` / `background-primary` / state surfaces (`background-success`, `background-warning`, `background-error`).
- CTAs, links, hyper accents → `primary` (+ `primary-hover` on hover).
- Ghost / secondary buttons → `secondary`.
- Destructive → `danger` / `danger-hover`.
- Dividers / outlines → `border-primary` / `border-secondary` / `border-tertiary`.
- Inline feedback copy → `feedback-error` / `feedback-success` / `feedback-warning`.
- Disambiguation: `#FFD300` — warning copy → `feedback-warning`; yellow accent on top of primary-colored surfaces → `primary-inverse`.
- Same hex can map to multiple semantics (e.g. `#10162F` → `text` vs `border-primary` vs `secondary`): pick from property + component role.
4. When palette-only is OK — `bg` prop on `<Background>` (`<Background bg="hyper">`, etc.) is the primary place for fixed surface palette colors on sections. After replacing hex there, use a named palette token, not hex. Exceptions (flag with rationale): charts/data viz, third-party widgets, exported static illustrations — still prefer tokens over hex when feasible.
Severity: Hex on adaptive UI (random wrappers, `styled-components`, inline `style`) → error. Hex inside documented exceptions → warning with note.
Reporting: For each match outside token definition files:
`file:line 'HEX' → semantic: <token(s)> | palette: <token> | note: <theme/disambiguation>`
Use `semantic: (n/a)` only when no semantic applies (e.g. pure illustration); still give `palette: …`. If unmappable: `→ no Gamut token`.
Ignore hex inside design token definition files (e.g. `variables/colors.ts`, `_colors.scss`) — source of truth, not violations.
### Appendix B — Core light: hex → suggested semantic (shortcut)
Use with step 3; verify for non-Core themes.
| Hex (normalized) | Typical semantic direction |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `#10162f` | `text`, `border-primary`, or `secondary` (by role) |
| `#0a0d1c` | `text-accent` |
| `#ffffff` | `background` (fills), `secondary` (inverse ghost on dark — rare in light-only grep context) |
| `#fff0e5` | `background-primary` |
| `#f5ffe3` | `background-success` |
| `#fffae5` | `background-warning` |
| `#fbf1f0` | `background-error` |
| `#3a10e5` | `primary` |
| `#5533ff` | `primary-hover` |
| `#ffd300` | `feedback-warning` or `primary-inverse` (see disambiguation above) |
| `#cca900` | Often pairs with hover in dark mode; in light UI as literal hex → check palette appendix (`yellow-400`) then assign role |
| `#e91c11` | `danger` |
| `#be1809` | `danger-hover`, `feedback-error` |
| `#008a27` | `feedback-success` |
Hexes with no row above still get Appendix A palette id + role-based semantic guess (e.g. blue scale → often decorative or legacy marketing; prefer design review unless mapping clearly to `primary`).
### Appendix A — Hex → palette token (identification only)
Case-insensitive. Use to label `palette:` in the report; do not stop at this step without Appendix B / role triage.
| Hex | Token |
| --------- | -------------------------- |
| `#000000` | `black` |
| `#ffffff` | `white` |
| `#10162f` | `navy` / `navy-800` |
| `#0a0d1c` | `navy-900` |
| `#fff0e5` | `beige-100` |
| `#f5fcff` | `blue-0` |
| `#d3f2ff` | `blue-100` |
| `#66c4ff` | `blue-300` |
| `#3388ff` | `blue-400` |
| `#1557ff` | `blue-500` |
| `#1d2340` | `blue-800` |
| `#f5ffe3` | `green-0` |
| `#eafdc6` | `green-100` |
| `#aee938` | `green-400` / `lightGreen` |
| `#008a27` | `green-700` |
| `#151c07` | `green-900` |
| `#fffae5` | `yellow-0` |
| `#cca900` | `yellow-400` |
| `#ffd300` | `yellow-500` / `yellow` |
| `#211b00` | `yellow-900` |
| `#fff5ff` | `pink-0` |
| `#f966ff` | `pink-400` / `pink` |
| `#fbf1f0` | `red-0` |
| `#e85d7f` | `red-300` |
| `#dc5879` | `red-400` / `paleRed` |
| `#e91c11` | `red-500` / `red` |
| `#be1809` | `red-600` |
| `#280503` | `red-900` |
| `#ffe8cc` | `orange-100` |
| `#ff8c00` | `orange-500` / `orange` |
| `#5533ff` | `hyper-400` |
| `#3a10e5` | `hyper-500` / `hyper` |
| `#f5f5f5` | `gray-100` |
| `#eeeeee` | `gray-200` |
| `#e0e0e0` | `gray-300` |
| `#9e9e9e` | `gray-600` |
| `#616161` | `gray-800` |
| `#424242` | `gray-900` |
| `#fffbf8` | `beige-0` |
| `#8a7300` | `gold-800` / `gold` |
| `#d14900` | `orange-800` |
| `#ca00d1` | `pink-800` |
| `#006d82` | `teal-500` / `teal` |
| `#b3ccff` | `purple-300` / `purple` |
### Step 2 — Non-Gamut CSS custom properties (SCSS/CSS/Less files)
After the hex scan, grep `.scss`, `.css`, and `.less` files for `var(--` occurrences. For each custom property name found, classify it:
_Gamut-issued variables_ — skip these:
- `--color-*` (ColorMode semantic aliases)
- `--space*` (spacing scale)
- `--font*` (font-size scale)
- `--lineHeight*` (line-height scale)
- `--borderWidth*` (border-width tokens)
- `--fontFamily*` (font-family tokens)
- `--fontWeight*` (font-weight tokens)
_Non-Gamut variables_ — flag these:
Any other name — especially camel-cased semantic names like `--darkNeutralColor`, `--whiteColor`, `--lightPrimaryColor`, `--colorNavy800`, `--borderGreyColor` — indicates a parallel token system (Skillsoft/Percipio globals, legacy design tokens, or ad-hoc project variables). These variables are NOT set by `GamutProvider`/`ColorMode` and will be undefined inside a Gamut-scoped tree unless the host shell also loads them.
Severity: ✗ error for color-semantic variables (invisible in tests/Storybook without the host stylesheet); ⚠ warning for spacing/sizing variables that duplicate Gamut scale tokens.
Reporting: count unique non-Gamut variable names and list the top offenders with frequency. Do not enumerate every call-site — just the variable names and usage counts. Suggest the nearest Gamut semantic alias where obvious (e.g. `--darkNeutralColor` → `--color-text`, `--whiteColor` → `--color-background`).
---
## Check 5 — Test setup
Grep test files (`**/__tests__/**/*.{ts,tsx}`, `**/*.test.{ts,tsx}`, `**/*.spec.{ts,tsx}`) for these patterns. Skip `node_modules`, `dist`.
| Pattern | Verdict | Reason |
| ----------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `jest.mock\(.*@codecademy/gamut` | Error | Manual mocking bypasses theme context and produces false-positive tests; prefer `setupRtl` from `@codecademy/gamut-tests` (or a harness + `setupRtl`); use raw `MockGamutProvider` + `render` only for rare one-offs or Storybook mocks |
| `jest.mock\(.*@codecademy/gamut-styles` | Error | Same issue as above — mocking gamut-styles breaks token resolution |
| `from '@codecademy/gamut-tests'` | Good — report count of files using it | Correct import for `setupRtl` and `MockGamutProvider` |
| `from 'component-test-setup'` (without gamut-tests) | Warning | Should import `setupRtl` from `@codecademy/gamut-tests`, not directly from `component-test-setup` — the gamut-tests wrapper adds `MockGamutProvider` automatically |
| `new GamutProvider` or `<GamutProvider` in test files | Warning | Prefer `setupRtl`; use `MockGamutProvider` (sets `useCache={false}`, `useGlobals={false}`) in harnesses or stories, not `GamutProvider` directly |
| `jest.mock\(.*[Gg]amut[Pp]rovider` | Warning | Mocking any file whose path contains `GamutProvider` (including project-internal wrappers) strips Emotion/theme context; prefer `setupRtl` from `@codecademy/gamut-tests` |
Skill reference for remediation: [`gamut-testing`](../gamut-testing/SKILL.md)
---
## Check 6 — Bespoke component duplication
Unlike Checks 1–5, this check is heuristic, not deterministic — every match needs a human glance before acting. **Every Check 6 finding is reported with `⚠`. There is no `✗` option for this check** — if a match feels like a clear-cut violation, that feeling is exactly the failure mode this rule exists to catch (a confident-looking ARIA-role/hand-rolled-listener match is still just a pattern match, not a certainty).
The goal: find custom-built UI that duplicates something `@codecademy/gamut` already provides — a hand-rolled modal, dropdown, tooltip, or focus trap living next to the library that already solves it. See [`gamut-component-first`](../gamut-component-first/SKILL.md) for the full decision table and the "signals" list this check is built from.
**Step 1 — Suspicious ARIA roles without a matching Gamut import**
Grep source files (`.ts`, `.tsx`, `.js`, `.jsx`) for hand-set roles:
```
role=["']?(dialog|menu|tooltip|listbox|alert)["']?
```
For each match, check whether the same file imports the corresponding component from `@codecademy/gamut` (`Modal`/`Dialog` for `dialog`, `Menu` for `menu`, `ToolTip`/`PreviewTip`/`InfoTip`/`Tip` for `tooltip`, `SelectDropdown` for `listbox`, `Alert` for `alert`). If the import is absent, report as `⚠ <file>:<line> role="..." with no matching Gamut import`.
**Step 2 — Component files named after a Gamut component that don't import it**
Look for source files whose filename (not import path) matches a known Gamut component name — `Modal`, `Dialog`, `Dropdown`, `Tooltip`, `Popover`, `Menu`, `Toast`, `Accordion`, `Tabs`, `Pagination`, `Avatar`, `Badge`, `Tag` — and check whether that file imports the matching name from `@codecademy/gamut`. A same-named local file that does _not_ import from the library is a strong signal of a parallel implementation. Report as `⚠ <file> filename matches a Gamut component; no @codecademy/gamut import found`.
**Step 3 — Hand-rolled dismiss/focus-trap logic**
Grep for manual Escape-key or outside-click dismiss handling that isn't going through Gamut's `Overlay`/`FocusTrap`/`PopoverContainer`:
```
(key === ['"]Escape['"]|addEventListener\(['"]keydown)
```
in files that don't import `Overlay`, `FocusTrap`, or `PopoverContainer` from `@codecademy/gamut`. Same idea for manual outside-click listeners (`addEventListener('click'` at the document/window level alongside a "contains" check). Report as `⚠ <file>:<line> hand-rolled dismiss logic, no FocusTrap/Overlay/PopoverContainer import`.
**Step 4 — SCSS/CSS module files named after a Gamut component**
Cross-reference with Check 3b's SCSS import list: a stylesheet named `Modal.scss`, `Dropdown.module.css`, `Tooltip.scss`, etc. is worth a second look even if Check 3b already flagged the import generically — the filename is the signal that this isn't just "some CSS," it's a parallel version of a specific Gamut component. Report as `⚠ <file> stylesheet named after a Gamut component`.
**Reporting:** for each match, name the likely Gamut component from the [decision table](../gamut-component-first/SKILL.md#decision-table-common-needs) and note this needs manual confirmation — a real product-specific one-off will look identical to a grep tool.
**Before finalizing the report**, re-scan every line under this section specifically for a `✗` icon. If you find one, that's a mistake — change it to `⚠`. **When computing the final `<N> error(s), <N> warning(s)` tally, count every Check 6 match toward the warning total, never the error total, even if a `✗` slipped through above** — this is the one place a stray icon can't corrupt the report's headline numbers.
Skill reference for remediation: [`gamut-component-first`](../gamut-component-first/SKILL.md)
---
## Output format
```
Gamut Review — <absolute path>
══════════════════════════════════════════════════
DESIGN.md
✓ present <path>/DESIGN.md
✗ missing run: gamut plugin install cursor --theme <core|percipio|lxstudio|…> [blocking for color audit]
Dependencies
✓ @codecademy/gamut <version>
⚠ @codecademy/gamut-styles not found — recommended
✗ @codecademy/variance not found — recommended
Setup
✓ GamutProvider found (src/App.tsx)
⚠ ColorMode not found — use ColorMode for light/dark theming [→ gamut-color-mode]
⚠ Background not found — use <Background> for semantic surfaces [→ gamut-color-mode]
✗ Theme augmentation not found — create src/theme.d.ts extending CoreTheme [→ gamut-theming]
(⚠ if TypeScript not detected: TypeScript not detected — recommended to adopt TypeScript; add src/theme.d.ts extending CoreTheme when you do [→ gamut-theming])
Import patterns
✓ Deep dist imports none found
✗ Deep src imports 2 occurrences
src/Thing.tsx:7
src/Other.tsx:12
SCSS modules, className & inline style [→ gamut-system-props] [→ gamut-style-utilities] [→ gamut-color-mode]
✗ SCSS/CSS imports 14 files — migrate to system props and css()/variant()
src/components/Card/Card.scss
src/components/Nav/Nav.scss (+ 12 more)
✗ className on Gamut components 9 occurrences
src/components/Card/Card.tsx:14 <Box className={styles.wrapper}>
src/components/Nav/Nav.tsx:7 <Text className={styles.title}>
✗ inline style on Gamut components 3 occurrences — use system props or css()/variant()/states() with semantic tokens
src/components/Hero/Hero.tsx:31 <Box style={{ color: '#10162F' }}>
src/components/Nav/Nav.tsx:19 <Text style={{ marginTop: 8 }}> (eslint-disabled) ⚠
Nested selectors [→ gamut-system-props] [→ gamut-style-utilities]
⚠ Tag selectors 3 occurrences — replace with system props or layout components (FlexBox, GridBox)
src/components/Nav/Nav.tsx:18 div { ... }
src/components/Hero/Hero.tsx:9 * { ... } (verify scope — may be JSDoc false positive)
⚠ Gamut component selectors 1 occurrence — use system props directly instead
src/components/Layout/Layout.tsx:12 ${Box} { align-self: start; }
(or: ✓ none found)
styled(GamutComponent) bypassing system props [→ gamut-system-props] [→ gamut-style-utilities]
✗ styled(Box) raw CSS 1 occurrence — every property has a prop equivalent
src/HeroSection.tsx:14 display, flex-direction, padding, color: white → delete wrapper, use FlexBox + props
⚠ styled(Box) raw CSS 1 occurrence — partially expressible, needs css() not a raw literal
src/ColumnTitle.tsx:8 background-clip: text + padding → wrap in css(), move padding to a prop
⚠ Box used where FlexBox/GridBox fits 2 occurrences — same defaults, more legible name
src/Panel.tsx:9 <Box display="flex" ...> → FlexBox
src/Grid.tsx:14 <Box display="grid" ...> → GridBox
(or: ✓ none found)
Hardcoded colors [→ gamut-color-mode]
✗ src/Card.tsx:22 '#10162F' → semantic: text | palette: navy-800 | note: Core light body copy
⚠ src/Hero.tsx:14 '#1557FF' → semantic: primary (if link/CTA) | palette: blue-500 | note: no exact semantic; confirm theme
⚠ src/Nav.tsx:8 '#BADA55' → semantic: (n/a) | palette: — | note: no Gamut token
✗ Non-Gamut CSS vars --darkNeutralColor (8 uses), --whiteColor (5 uses) → --color-text, --color-background
Test setup [→ gamut-testing]
✓ @codecademy/gamut-tests used in 12 test files
✗ jest.mock(@codecademy/gamut) 2 occurrences — remove; prefer setupRtl (or harness + setupRtl)
src/components/Foo/__tests__/Foo.test.tsx:3
src/components/Bar/__tests__/Bar.test.tsx:5
⚠ direct component-test-setup import 1 occurrence — import from @codecademy/gamut-tests
src/components/Baz/__tests__/Baz.test.tsx:2
Bespoke component duplication (heuristic — confirm manually) [→ gamut-component-first]
⚠ src/components/ConfirmDialog/ConfirmDialog.tsx:9 role="dialog" with no Modal/Dialog import — likely reinventing gamut-modal
⚠ src/components/Dropdown/Dropdown.tsx filename matches a Gamut component; no @codecademy/gamut import found — compare against SelectDropdown
(or: ✓ none found)
══════════════════════════════════════════════════
<N> error(s), <N> warning(s) found. (or "All checks passed." if none)
```
Icons: `✓` = pass, `⚠` = warning (recommended, not required), `✗` = error (required).
`[→ skill-name]` annotations indicate which Gamut skill has remediation guidance for that category.
After printing the report, offer one sentence of prioritized next-step advice based on what was found, then ask the user whether they'd like you to fix the findings — do not apply any remediation unprompted.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!