Imported from Codecademy/gamut (
packages/gamut/agent-tools/skills/gamut-review/SKILL.md). Install upstream withnpx skills add Codecademy/gamut --skill gamut-review. Copyright stays with the author.
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-color-mode · gamut-system-props · gamut-style-utilities · gamut-typography · gamut-testing · gamut-z-index · gamut-component-first
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.
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-style-utilities · gamut-color-mode
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 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):
False-positive risk:^\s*\*\s*\{*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
Globalfrom@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:
- Find files that contain
from '@codecademy/gamut'. - In those files, grep for:
This matches any\$\{[A-Z][A-Za-z]+\}[^{]*\{${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, orGridBoxwith theasprop 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-style-utilities
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-propsprop groups) → ✗ error. Remediation: delete thestyled()wrapper; pass the same values as props directly on the JSX element. Flagdisplay: 'flex'/display:flexspecifically — recommendFlexBoxinstead ofBox+ adisplayprop. Note thatbackground(unlikebg) has no token scale and takes any CSS value as-is — a gradient string is already valid as a plainbackgroundprop and does not by itself justify astyled()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: keepstyled(ComponentName), but move the object intocss(),variant(), orstates()from@codecademy/gamut-stylesinstead of a raw literal — and pull the properties that are expressible (padding, display, plain colors, gradients viabackground) 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-style-utilities
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-modeskill — semantic alias tables and decision guide.- Foundations / ColorMode — aliases per mode;
<Background>behavior. - Meta / Best practices — semantic colors +
css/variant/statesfromgamut-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)
- 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>+ palettebg) vs component chrome (prefer semantics). - 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-stylespalette definitions. - 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-hoveron 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→textvsborder-primaryvssecondary): pick from property + component role.
- Body / UI foreground →
- When palette-only is OK —
bgprop 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
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 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 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
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.