@codecademy/gamut
72.3.072.3.1-alpha.f273fd.0
agent-tools/skills/gamut-style-utilities/SKILL.md~
agent-tools/skills/gamut-style-utilities/SKILL.mdModified+36
Index: package/agent-tools/skills/gamut-style-utilities/SKILL.md
===================================================================
--- package/agent-tools/skills/gamut-style-utilities/SKILL.md
+++ package/agent-tools/skills/gamut-style-utilities/SKILL.md
@@ -14,18 +14,37 @@
Use `css()`, `variant()`, and `states()` from `@codecademy/gamut-styles` for typed, token-scaled style objects (same scales as composed `system.*` props). Prefer semantic color keys so styles track ColorMode and theme.
For layout-heavy styled components, prefer composing `system.*` via `variance.compose()` (see `gamut-system-props`) instead of re-stating every longhand in `css()`.
+**Wrapping an existing Gamut component** (`styled(Box)`, `styled(Text)`, …) in a raw template literal or plain object instead of `css()`/`variant()`/`states()` is a common bypass — before reaching for any of the APIs below on an already-Gamut component, read [`gamut-system-props`](../gamut-system-props/SKILL.md#dont-wrap-a-gamut-component-in-styled-to-hand-write-css) first. Most of the time the fix is deleting the `styled()` wrapper entirely, not picking a different one of these three functions.
+
+**Every value inside `css()`/`variant()`/`states()` is only as good as the scale key you type — and a plausible-sounding key that doesn't exist won't warn you, it just silently fails to apply.** Don't infer a key name from what it should logically be called (e.g. `borderRadius: 'max'` for "fully rounded" — the real key is `full`); check the actual scale before typing it:
+
+| Prop group | Scale file | Real keys (non-exhaustive) |
+| -------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
+| `borderRadius` | `packages/gamut-styles/src/variables/borderRadii.ts` | `none`, `sm`, `md`, `lg`, `xl`, `full` |
+| `p`/`m`/spacing | `packages/gamut-styles/src/variables/spacing.ts` | `0`, `4`, `8`, `12`, `16`, `24`, `32`, `40`, `48`, `64`, `96` |
+| `fontSize` | `packages/gamut-styles/src/variables/typography.ts` | `14`, `16`, `18`, `20`, `22`, … |
+| `fontWeight` | same file | `base`, `title`, `400`, `700` |
+| `lineHeight` | same file | `base`, `title`, `spacedTitle` |
+| `color`/`bg`/`borderColor` | `packages/gamut-styles/src/variables/colors.ts` + theme `modes` in `packages/gamut-styles/src/themes/*.ts` | palette names and the semantic aliases covered above |
+
+If you're not certain a key exists, grep the relevant file rather than guessing — a wrong key compiles (these are typically loosely typed as strings at the call site) and produces no error, it just doesn't render the intended style.
+
## `css()` — static style objects
```tsx
import { css } from '@codecademy/gamut-styles';
import styled from '@emotion/styled';
+// fixed color — deliberately does NOT adapt to color mode (rare; see caveat below)
const Box = styled.div(css({ bg: 'navy-400', p: 4 }));
+// semantic color — adapts to color mode (the default choice, not the exception)
const Text = styled.div(css({ color: 'primary', p: 4 }));
```
+**Read [`gamut-color-mode`](../gamut-color-mode/SKILL.md) before typing a raw palette name (`navy-400`, `green-0`, `yellow-900`, …) into a `css()`/`variant()`/`states()` color value.** The `navy-400` example above is the _exception_, not a template to copy — it's there to show the fixed-color escape hatch exists, not to model the default path. If you're defining a status/feedback color (success, warning, error, info-adjacent), check first whether a semantic token already covers it — `background-success`/`feedback-success`, `background-warning`/`feedback-warning`, etc. already exist and adapt correctly between light/dark mode; a same-hue raw palette pair (`green-0`/`green-900`) looks similar in whichever mode you tested in but silently stops adapting. Reach for a raw palette value only when the color must **not** adapt (illustrations, brand marks, a permanently fixed surface) — the same exception `gamut-review` Check 4 and `gamut-system-props` use for this exact judgment call.
+
## `variant()` and `states()` — branching and toggles
- `variant()` — mutually exclusive modes: `base`, `defaultVariant`, and a `variants` map (semantic colors, spacing shorthands, nested selectors such as `'&:hover'`).
@@ -98,4 +117,21 @@
const theme = useTheme();
return <path strokeWidth={theme.spacing[4]} d="M0 0 L10 10" />;
};
```
+
+**For colors, `theme.colors[key]` is usually enough — it works with aliases and doesn't need a second lookup step.** `theme.colors[key]` (including semantic aliases like `'primary'` or `'background-current'`) returns a live CSS variable reference (`var(--color-key)`), which the browser resolves through the normal light/dark cascade. This works as a plain attribute value on DOM-rendered elements — including SVG presentation attributes like `stroke`/`fill` — exactly like `RadialProgress` does it in production: `stroke={theme.colors['background-current']}` (`packages/gamut/src/RadialProgress/index.tsx`). Reach for this first; it's simpler and it's what real Gamut components actually do for imperative SVG color.
+
+**The exception**: a `var(...)` string doesn't mean anything to a consumer outside the DOM/CSS rendering pipeline — canvas 2D `fillStyle`, WebGL, or any color-math (contrast calculations, blending) that needs the actual resolved value. For those, use `useColorModes()` instead of bare `useTheme()` — but its `getColorValue()` only accepts a **raw palette key** (`hyper-500`, `navy-800`, …), not an alias: `getColorValue('primary')` silently returns `undefined`, since aliases are never merged into the raw-key map `getColorValue` reads. Resolve the alias first with `isColorAlias`, exactly like `packages/gamut-styles/src/Background.tsx` does before computing contrast:
+
+```tsx
+import { isColorAlias, useColorModes } from '@codecademy/gamut-styles';
+
+const useResolvedColor = (color: Colors) => {
+ const [, activeColors, , getColorValue] = useColorModes();
+ return isColorAlias(activeColors, color)
+ ? getColorValue(activeColors[color])
+ : getColorValue(color);
+};
+```
+
+Don't reach for this chain by default — it's for the narrower case where you need a literal value, not for every plain-JS color read.