From 13a0376688c63a9faf9f516f018f876226c813b6 Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Mon, 8 Jun 2026 12:24:26 -0400 Subject: [PATCH 01/10] add tests --- .../agent-tools/skills/gamut-buttons/SKILL.md | 54 +++++++ .../agent-tools/skills/gamut-review/SKILL.md | 146 +++++++++++++++++- packages/gamut/bin/__tests__/design.test.mjs | 138 +++++++++++++++++ packages/gamut/bin/lib/design.mjs | 16 +- packages/gamut/package.json | 1 + packages/gamut/project.json | 9 +- 6 files changed, 353 insertions(+), 11 deletions(-) create mode 100644 packages/gamut/bin/__tests__/design.test.mjs diff --git a/packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md b/packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md index 5f50aaec562..18a5e0825df 100644 --- a/packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md @@ -87,6 +87,60 @@ Hover, active, and disabled colors are handled by the component. Do not override - `IconButton`: provide an accessible name via `tip` (used as `aria-label` when `aria-label` is omitted). See ToolTip / IconButton Storybook pages. - `ButtonBase` is not exported from `@codecademy/gamut` (only the `ButtonBaseElements` type is). Prefer stock atoms; custom button styling belongs in Gamut itself or via `css` / `variant` from `gamut-styles`, not by importing `ButtonBase`. +## Focus management — buttons with ToolTip + +`ToolTip` opens on **hover or focus** and closes when neither is active. The two rendering paths behave differently: + +- **Inline (default):** CSS-only via `:hover` and `:focus-within` on the wrapper. No JS involved; tooltip visibility tracks pointer and focus state automatically. +- **Floating (`placement="floating"`):** JS-driven. Tracks hover (`mouseenter`/`mouseleave`) and focus (`focus`/`blur`) separately with a small delay on each. Escape key always closes the tooltip by calling `.blur()` on the trigger automatically. + +**When the tooltip lingers after a click:** This only occurs with `FloatingTip` when the button was **keyboard-focused before the click**. `FloatingTip` keeps an `isFocused` flag; while that flag is true, `mouseleave` does not close the tooltip. If the click action does not naturally move DOM focus elsewhere, the button stays focused and the tooltip stays open. + +Mouse-initiated clicks do not have this problem: `TargetContainer` has `onMouseDown={(e) => e.preventDefault()}` which prevents the button from gaining focus via mouse, so `isFocused` stays `false` and the tooltip closes when the pointer moves away. + +**Pattern — explicit blur when focus won't move naturally:** + +```tsx +const handleClick = () => { + (document.activeElement as HTMLElement)?.blur(); + openPanel(); +}; + +; +``` + +Or with a ref: + +```tsx +const ref = useRef(null); + + { + ref.current?.blur(); + openPanel(); + }} +/>; +``` + +**When to apply (floating placement, keyboard-triggered clicks only):** + +- Click opens a modal, drawer, or panel that does NOT auto-focus an element inside it +- Click triggers an in-place state toggle (e.g. show/hide inline editor) +- Click dispatches a mutation with no focus side-effect + +**When NOT needed:** + +- Click opens a modal with a proper focus trap — the trap moves focus automatically, blurring the button +- Click navigates to a new route — component unmounts +- Click reveals a `Popover` or `FloatingTip`-managed dropdown — focus is moved by that system +- Tooltip uses the default inline (non-floating) placement — CSS handles visibility, no lingering issue +- User pressed Escape — built-in `escapeKeyPressHandler` already calls `.blur()` + +Call `.blur()` synchronously before the action; this keeps tooltip dismissal atomic with the user interaction. + ## Rules - Use `FillButton` for primary actions and `StrokeButton` for secondary — do not use both at equal weight on the same screen. diff --git a/packages/gamut/agent-tools/skills/gamut-review/SKILL.md b/packages/gamut/agent-tools/skills/gamut-review/SKILL.md index b42c10d01a0..c1cee91a807 100644 --- a/packages/gamut/agent-tools/skills/gamut-review/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-review/SKILL.md @@ -1,6 +1,6 @@ --- name: gamut-review -description: Use this skill when auditing existing code for Gamut usage — dependencies, GamutProvider, deep imports, hardcoded hex colors, and test patterns — and you need a consolidated report with pointers to matching Gamut skills. +description: Use this skill when auditing existing code for Gamut usage — dependencies, GamutProvider, deep imports, SCSS modules, className on Gamut components, nested selectors, hardcoded hex colors, non-Gamut CSS variables, and test patterns — and you need a consolidated report with pointers to matching Gamut skills. --- # Gamut Review @@ -11,7 +11,7 @@ When `DESIGN.md` is present at the audit root, use it as the authoritative refer Run Check 0 first, then Checks 1–5, 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-testing`](../gamut-testing/SKILL.md) +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) --- @@ -30,11 +30,12 @@ Resolve the audit root from the user's path if provided, otherwise the current w 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 | +| 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`. | --- @@ -53,6 +54,8 @@ Search source files (`.ts`, `.tsx`, `.js`, `.jsx`) for these symbols. Skip `node 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` 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` to the component interface. Skill reference: [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md). + --- ## Check 3 — Import patterns @@ -70,6 +73,96 @@ Report each violation as `file:line`. --- +## Check 3b — SCSS/CSS module imports and className on Gamut components + +Gamut components are styled via the variance system (system props, `css()`, `variant()`, `states()` from `@codecademy/gamut-styles`). Importing SCSS/CSS modules and passing `className` to Gamut components bypasses this system entirely, breaks ColorMode token propagation, and prevents 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 `. + +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. + +Remediation: replace SCSS module rules with system props directly on the Gamut component (`padding`, `margin`, `color`, `bg`, `borderColor`, `fontSize`, `fontWeight`, etc.); use `css()` or `variant()` from `@codecademy/gamut-styles` for anything not covered by 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** + +Grep source files for a Gamut component name appearing as a CSS selector (i.e., followed by a rule block) inside a template literal: + +``` +\$\{(Box|FlexBox|GridBox|Column|LayoutGrid|Card|Text|Anchor|FillButton|StrokeButton|TextButton|CTAButton|IconButton|Toggle|List|ListRow|ListCol|Background|Disclosure)\}[^{]*\{ +``` + +Each match means the component is being targeted as a child CSS selector 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:** For tag-selector violations, replace the nested wrapper with a Gamut layout component (`FlexBox`, `GridBox`) and move styles to system props on each child. For Gamut component selector violations, 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()` 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 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 `` from `@codecademy/gamut-styles` (section surfaces with content). @@ -185,6 +278,27 @@ Case-insensitive. Use to label `palette:` in the report; do not stop at this ste | `#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 @@ -198,6 +312,7 @@ Grep test files (`**/__tests__/**/*.{ts,tsx}`, `**/*.test.{ts,tsx}`, `**/*.spec. | `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 ` + src/components/Nav/Nav.tsx:7 + +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) + 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 diff --git a/packages/gamut/bin/__tests__/design.test.mjs b/packages/gamut/bin/__tests__/design.test.mjs new file mode 100644 index 00000000000..6dadb2fdedc --- /dev/null +++ b/packages/gamut/bin/__tests__/design.test.mjs @@ -0,0 +1,138 @@ +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { after, before, describe, it } from 'node:test'; + +import { installDesignMd } from '../lib/design.mjs'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Create a minimal agent-tools directory with one DESIGN source file. */ +async function makeSourceRoot(dir, pkgVersion = '71.0.0') { + const sourceRoot = join(dir, 'agent-tools'); + await mkdir(sourceRoot); + + // Minimal package.json adjacent to agent-tools + if (pkgVersion !== null) { + await writeFile( + join(dir, 'package.json'), + JSON.stringify({ name: '@codecademy/gamut', version: pkgVersion }), + 'utf8' + ); + } + + // Minimal DESIGN source file + await writeFile( + join(sourceRoot, 'DESIGN.Codecademy.md'), + '---\nversion: alpha\nname: Codecademy\n', + 'utf8' + ); + + return sourceRoot; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('installDesignMd', () => { + let tmp; + + before(async () => { + tmp = await mkdtemp(join(tmpdir(), 'gamut-design-test-')); + }); + + after(async () => { + await rm(tmp, { recursive: true, force: true }); + }); + + it('stamps the gamut version in an HTML comment at the top of DESIGN.md', async () => { + const dir = join(tmp, 'stamp'); + await mkdir(dir); + const sourceRoot = await makeSourceRoot(dir, '99.0.0'); + const cwd = join(dir, 'app'); + await mkdir(cwd); + + await installDesignMd(sourceRoot, cwd, 'core'); + + const content = await readFile(join(cwd, 'DESIGN.md'), 'utf8'); + assert.match(content, /^\n`; + + const content = await readFile(src, 'utf8'); + await writeFile(dest, header + content, 'utf8'); return { dest, label }; } diff --git a/packages/gamut/package.json b/packages/gamut/package.json index b6a9b298c79..c6bfab918e7 100644 --- a/packages/gamut/package.json +++ b/packages/gamut/package.json @@ -55,6 +55,7 @@ "build": "nx build @codecademy/gamut", "build:watch": "yarn build && onchange ./src -- yarn build", "compile": "babel ./src --out-dir ./dist --extensions \".ts,.tsx\"", + "test:bin": "node --test bin/__tests__/design.test.mjs", "verify": "tsc --noEmit && tsc --project tsconfig.bin.json" }, "sideEffects": [ diff --git a/packages/gamut/project.json b/packages/gamut/project.json index 3e2bef14b11..f94a7846862 100644 --- a/packages/gamut/project.json +++ b/packages/gamut/project.json @@ -19,8 +19,15 @@ "parallel": false } }, + "test-bin": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/gamut", + "command": "node --test bin/__tests__/design.test.mjs" + } + }, "test": { - "dependsOn": ["^build"], + "dependsOn": ["^build", "test-bin"], "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/packages/gamut"], "options": { From d23b9d0d99161921bba3d38e4f51a0c49703f7e4 Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Mon, 8 Jun 2026 13:34:53 -0400 Subject: [PATCH 02/10] add types to mjs --- packages/gamut/bin/__tests__/design.test.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/gamut/bin/__tests__/design.test.mjs b/packages/gamut/bin/__tests__/design.test.mjs index 6dadb2fdedc..1a50f096f46 100644 --- a/packages/gamut/bin/__tests__/design.test.mjs +++ b/packages/gamut/bin/__tests__/design.test.mjs @@ -10,7 +10,11 @@ import { installDesignMd } from '../lib/design.mjs'; // Helpers // --------------------------------------------------------------------------- -/** Create a minimal agent-tools directory with one DESIGN source file. */ +/** + * Create a minimal agent-tools directory with one DESIGN source file. + * @param {string} dir + * @param {string | null} [pkgVersion] + */ async function makeSourceRoot(dir, pkgVersion = '71.0.0') { const sourceRoot = join(dir, 'agent-tools'); await mkdir(sourceRoot); @@ -39,6 +43,7 @@ async function makeSourceRoot(dir, pkgVersion = '71.0.0') { // --------------------------------------------------------------------------- describe('installDesignMd', () => { + /** @type {string} */ let tmp; before(async () => { From 78fc23eed1b0aaf0e51c0e8bfa960f76ea9f0a3e Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Mon, 8 Jun 2026 13:38:56 -0400 Subject: [PATCH 03/10] plan release --- .nx/version-plans/version-plan-1780940298971.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .nx/version-plans/version-plan-1780940298971.md diff --git a/.nx/version-plans/version-plan-1780940298971.md b/.nx/version-plans/version-plan-1780940298971.md new file mode 100644 index 00000000000..c71c5c19e33 --- /dev/null +++ b/.nx/version-plans/version-plan-1780940298971.md @@ -0,0 +1,5 @@ +--- +gamut: minor +--- + +Update ai-skills with better setup instructions + DESIGN.md generation From 5d69ea2d4ecffe1ed343cdceb0140068b659e02e Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Tue, 9 Jun 2026 08:46:57 -0400 Subject: [PATCH 04/10] stricter styling guideline --- packages/gamut/agent-tools/skills/gamut-review/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gamut/agent-tools/skills/gamut-review/SKILL.md b/packages/gamut/agent-tools/skills/gamut-review/SKILL.md index c1cee91a807..7a4028cbb9a 100644 --- a/packages/gamut/agent-tools/skills/gamut-review/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-review/SKILL.md @@ -112,7 +112,7 @@ Each match is an error. Report as `file:line `. 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. -Remediation: replace SCSS module rules with system props directly on the Gamut component (`padding`, `margin`, `color`, `bg`, `borderColor`, `fontSize`, `fontWeight`, etc.); use `css()` or `variant()` from `@codecademy/gamut-styles` for anything not covered by system props; delete the SCSS file when all rules are migrated. +Remediation: replace SCSS module rules 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 or palette names; 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) From 780786c8a72d29682f2bf2b531f53a2a2d974031 Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Tue, 9 Jun 2026 09:10:22 -0400 Subject: [PATCH 05/10] components instruction tweak --- .../agent-tools/skills/gamut-review/SKILL.md | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/gamut/agent-tools/skills/gamut-review/SKILL.md b/packages/gamut/agent-tools/skills/gamut-review/SKILL.md index 7a4028cbb9a..49c9152dfbf 100644 --- a/packages/gamut/agent-tools/skills/gamut-review/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-review/SKILL.md @@ -145,19 +145,31 @@ Exemptions (downgrade to `ℹ note`, not warning): **Step 2 — Gamut component selectors** -Grep source files for a Gamut component name appearing as a CSS selector (i.e., followed by a rule block) inside a template literal: +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: -``` -\$\{(Box|FlexBox|GridBox|Column|LayoutGrid|Card|Text|Anchor|FillButton|StrokeButton|TextButton|CTAButton|IconButton|Toggle|List|ListRow|ListCol|Background|Disclosure)\}[^{]*\{ -``` +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 the component is being targeted as a child CSS selector from a parent styled wrapper rather than styled directly. Report as `file:line ${ComponentName} { ... }`. +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:** For tag-selector violations, replace the nested wrapper with a Gamut layout component (`FlexBox`, `GridBox`) and move styles to system props on each child. For Gamut component selector violations, 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()` keyed to data attributes or boolean props on the parent. +**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: ``, ``. +- 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) From b2eacf113e349842a8884b425b5d10053d0dc56f Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Tue, 9 Jun 2026 11:31:07 -0400 Subject: [PATCH 06/10] up plugin --- packages/gamut/agent-tools/.claude-plugin/plugin.json | 2 +- packages/gamut/agent-tools/.cursor-plugin/plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gamut/agent-tools/.claude-plugin/plugin.json b/packages/gamut/agent-tools/.claude-plugin/plugin.json index 1061b0b02f3..28d0f3ec686 100644 --- a/packages/gamut/agent-tools/.claude-plugin/plugin.json +++ b/packages/gamut/agent-tools/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "gamut-design-system", - "version": "0.0.1", + "version": "0.0.2", "description": "Gamut design system agent tools: skills and rules for AI-assisted development.", "license": "MIT", "keywords": ["codecademy", "gamut", "design-system", "agent-skills"] diff --git a/packages/gamut/agent-tools/.cursor-plugin/plugin.json b/packages/gamut/agent-tools/.cursor-plugin/plugin.json index daa7f1bfab6..075b1a75d1c 100644 --- a/packages/gamut/agent-tools/.cursor-plugin/plugin.json +++ b/packages/gamut/agent-tools/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "gamut-design-system", "displayName": "Gamut Design System", - "version": "0.0.1", + "version": "0.0.2", "description": "Gamut design system agent tools: skills and rules for AI-assisted development.", "keywords": ["codecademy", "gamut", "design-system", "agent-skills"] } From 1dd550719bfb6241cd19b58b314377ed3f2567f8 Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Tue, 9 Jun 2026 15:05:55 -0400 Subject: [PATCH 07/10] more tweaks for media-queries --- packages/gamut/agent-tools/skills/gamut-layout/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gamut/agent-tools/skills/gamut-layout/SKILL.md b/packages/gamut/agent-tools/skills/gamut-layout/SKILL.md index c9fcca8c13b..bc342cf577d 100644 --- a/packages/gamut/agent-tools/skills/gamut-layout/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-layout/SKILL.md @@ -1,6 +1,6 @@ --- name: gamut-layout -description: Use this skill when applying Gamut spacing scale, border radii, viewport or container breakpoints, or page layout grid (LayoutGrid vs GridBox) — complements gamut-system-props for system.space and responsive props. +description: Use this skill when applying Gamut spacing scale, border radii, viewport or container breakpoints, screen sizes, responsive layouts, media queries, or page layout grid (LayoutGrid vs GridBox) — including migrating breakpoint or screen-size logic, responsive prop patterns, useWindowSize / useBreakpoint hooks, and mobile-first design. Complements gamut-system-props for system.space and responsive props. --- # Gamut Layout From acd8827d15d73e2d67c875754b741b8a67f6ab54 Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Wed, 17 Jun 2026 12:43:50 -0400 Subject: [PATCH 08/10] datatable skill add --- .../skills/gamut-datalist/SKILL.md | 274 ++++++++++++++++ .../skills/gamut-datatable/SKILL.md | 306 ++++++++++++++++++ .../agent-tools/skills/gamut-list/SKILL.md | 12 +- 3 files changed, 590 insertions(+), 2 deletions(-) create mode 100644 packages/gamut/agent-tools/skills/gamut-datalist/SKILL.md create mode 100644 packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md diff --git a/packages/gamut/agent-tools/skills/gamut-datalist/SKILL.md b/packages/gamut/agent-tools/skills/gamut-datalist/SKILL.md new file mode 100644 index 00000000000..83a1e3e35b5 --- /dev/null +++ b/packages/gamut/agent-tools/skills/gamut-datalist/SKILL.md @@ -0,0 +1,274 @@ +--- +name: gamut-datalist +description: Use this skill when building a DataList for item-focused layouts with row expansion, row selection, or rich per-item content — including expandedContent, onRowSelect, onRowExpand, variant (default/card), useLocalQuery, and empty/loading states. Do not use for bulk data comparison tables (see gamut-datatable), or for small static lists (see gamut-list). +--- + +# Gamut DataList + +Item-focused list for managing, engaging with, and expanding individual rows. Use when users interact with items — opening details, selecting for bulk actions, or viewing expanded layouts — rather than scanning and comparing data across rows. + +Source: `@codecademy/gamut` — [DataList.tsx](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/DataList/DataList.tsx) + +See also: [`gamut-datatable`](../gamut-datatable/SKILL.md) — query-focused table for bulk data comparison. [`gamut-list`](../gamut-list/SKILL.md) — lower-level list primitives for fully custom layouts. [`gamut-accessibility`](../gamut-accessibility/SKILL.md) — ARIA and keyboard interaction. + +Storybook: [Organisms / Lists & Tables / DataList](https://gamut.codecademy.com/?path=/docs-organisms-lists-tables-datalist--docs) + +## Components + +```tsx +import { DataList } from '@codecademy/gamut'; +import { useLocalQuery } from '@codecademy/gamut'; +``` + +| Symbol | Role | +| --------------- | ---------------------------------------------------------------------------------------- | +| `DataList` | Root component; supports expansion, selection, and card/default variants | +| `useLocalQuery` | Client-side hook for sort/filter state; spread its return value directly into `DataList` | + +## When to use DataList + +- Users **open, expand, or engage** with individual items (content libraries, assignment lists, course catalogs). +- Rows need **expandable detail panels** with rich layouts. +- Users need to **select rows** for bulk actions. +- Items contain **icons, graphics, or complex layouts** rather than plain metrics. +- Optional filtering or sorting across shared attributes is needed at the list level. + +**Do not use DataList when:** + +- The goal is to **compare data across rows** (scores, metrics, statuses in columns) → use [`DataTable`](../gamut-datatable/SKILL.md). +- The list is small, static, and needs fully custom row layouts → use [`List`](../gamut-list/SKILL.md) directly. +- You need a table with horizontal scrolling → use `DataTable` (DataList is not scrollable). + +## Design principles + +- **Engage with individual items**: design each row to support the action users take on it (open, launch, select, expand). +- **Customize items with rich content**: icons, progress indicators, graphics, and other atoms are appropriate within rows. +- **Keep item controls visible**: action controls should be on the right side of the row. +- **Place lists inside main containers**: avoid overflow by placing DataList in appropriately sized parent layouts. +- **Use DataTable for comparison-first designs**: if the design is primarily about scanning data between items, reach for DataTable instead. + +## Variants + +```tsx + +``` + +| `variant` | Use for | +| --------- | ---------------------------------------------------------------------------------- | +| `default` | Standard bordered rows; best for most item-management layouts | +| `card` | Rows with vertical gap; best for content that doesn't need to be visually adjacent | + +## Props + +| Prop | Type | Default | Notes | +| ----------------------- | ------------------------------------------------------ | ------------------- | ------------------------------------------------------------------------ | +| `id` | `string` | required | Unique ID for the list | +| `idKey` | `keyof Row` | required | Row identifier — must be a `string \| number` field | +| `rows` | `Row[]` | required | Data array | +| `columns` | `ColumnConfig[]` | required | Column definitions | +| `query` | `Query` | — | Current sort/filter state | +| `onQueryChange` | `OnQueryChange` | — | Called when sort or filter changes | +| `selected` | `Row[IdKey][]` | — | Array of selected row IDs | +| `onRowSelect` | `RowStateChange<'select' \| 'select-all', Row[IdKey]>` | — | Called on row or select-all toggle | +| `expanded` | `Row[IdKey][]` | — | Array of expanded row IDs | +| `onRowExpand` | `RowStateChange<'expand', Row[IdKey]>` | — | Called when a row is expanded or collapsed | +| `expandedContent` | `ExpandRow` | — | Render function for expanded row content; receives `{ row, onCollapse }` | +| `variant` | `'default' \| 'card'` | `'default'` | Row visual style | +| `header` | `boolean` | — | Whether to show a header row | +| `hideSelectAll` | `boolean` | `false` | Hides the select-all checkbox in the header | +| `loading` | `boolean` | `false` | Replaces row content with shimmer placeholders | +| `spacing` | `'condensed' \| 'normal'` | `'condensed'` | Row padding | +| `emptyMessage` | `ReactNode` | default empty state | Rendered when `rows` is empty | +| `disableContainerQuery` | `boolean` | `false` | Falls back to media queries instead of container queries | + +> DataList always sets `scrollable={false}`. For a horizontally scrollable table, use `DataTable`. + +## ColumnConfig + +Identical to DataTable — see [`gamut-datatable`](../gamut-datatable/SKILL.md#columnconfig) for the full table. + +## Basic usage + +```tsx +import { DataList, useLocalQuery } from '@codecademy/gamut'; + +const columns = [ + { key: 'title', header: 'Title', size: 'md', type: 'header', fill: true }, + { key: 'status', header: 'Status', size: 'sm' }, +]; + +const MyList = ({ data }) => { + const queryData = useLocalQuery({ idKey: 'id', rows: data, columns }); + + return ; +}; +``` + +## Expandable rows + +Pass `expandedContent`, `expanded`, and `onRowExpand` together. The `expandedContent` render function receives `{ row, onCollapse }` — call `onCollapse` from within the expanded panel to close it programmatically. + +```tsx +const [expanded, setExpanded] = useState([]); + +const handleExpand = ({ type, payload: { rowId, toggle } }) => { + if (type === 'expand') { + setExpanded((prev) => + toggle ? prev.filter((id) => id !== rowId) : [...prev, rowId] + ); + } +}; + + ( + + {row.title}: additional details here + Collapse + + )} +/>; +``` + +## Selectable rows + +Pass `selected` and `onRowSelect` to enable checkboxes. The callback receives `{ type, payload: { rowId, toggle } }` where `type` is `'select'` or `'select-all'`. + +```tsx +const [selected, setSelected] = useState([]); + +const handleSelect = ({ type, payload: { rowId, toggle } }) => { + if (type === 'select-all') { + setSelected((prev) => (prev.length > 0 ? [] : data.map((r) => r.id))); + } else if (type === 'select') { + setSelected((prev) => + toggle ? prev.filter((id) => id !== rowId) : [...prev, rowId] + ); + } +}; + +; +``` + +To hide row checkboxes entirely, omit `onRowSelect` and `selected`. To hide only the select-all checkbox, pass `hideSelectAll`. + +## Expansion + selection combined + +Both can be active at once — pass all four props together. + +```tsx + } +/> +``` + +## Sorting and filtering with useLocalQuery + +`useLocalQuery` handles client-side sort and filter state. Spread its return value directly into `DataList`. Mark columns as `sortable` or provide `filters` in the column config. + +```tsx +const columns = [ + { key: 'title', header: 'Title', size: 'md', sortable: true }, + { + key: 'type', + header: 'Type', + size: 'sm', + filters: ['Course', 'Path', 'Project'], + }, +]; + +const queryData = useLocalQuery({ idKey: 'id', rows: data, columns }); + +; +``` + +For server-side filtering or pagination, manage `query` and `onQueryChange` externally instead of using `useLocalQuery`. + +## Empty state + +DataList shows a default empty state when `rows` is empty. Override with `emptyMessage`. + +```tsx + + + + No items yet. + + + + } +/> +``` + +## Loading state + +Pass `loading` to replace row content with shimmer placeholders while data fetches. + +```tsx + +``` + +## Container queries + +DataList uses CSS container queries by default. Disable only when the list lives in a constrained container or you are managing your own responsive logic. + +```tsx +
+ +
+``` + +## Accessibility + +- DataList renders as a semantic `` via the underlying `List as="table"`. +- Expanded rows receive an `aria-live="polite"` region automatically — do not add a duplicate live region. +- The expanded content region is labeled by the row's header column text; provide meaningful header column values. +- Selection checkboxes are grouped as a checkbox list; the select-all checkbox is in the ``. +- For custom `emptyMessage`, use `tbody > tr > th` structure for valid table semantics. diff --git a/packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md b/packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md new file mode 100644 index 00000000000..bdbe3ff4be1 --- /dev/null +++ b/packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md @@ -0,0 +1,306 @@ +--- +name: gamut-datatable +description: Use this skill when building a DataTable for bulk data analysis, comparison, sorting, or filtering — including column configuration, the useLocalQuery hook, row action menus, empty/loading states, and color mode. Do not use for item management with row expansion or selection (see gamut-datalist), or for small static lists (see gamut-list). +--- + +# Gamut DataTable + +Structured, query-capable table for bulk data analysis and comparison. Sorting, filtering, loading, and empty states are built in. Use when the goal is to scan and compare information across rows — not to manage individual items. + +Source: `@codecademy/gamut` — [DataTable.tsx](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/DataList/DataTable.tsx) + +See also: [`gamut-datalist`](../gamut-datalist/SKILL.md) — item-focused list with expansion and selection. [`gamut-list`](../gamut-list/SKILL.md) — lower-level list primitives for fully custom layouts. [`gamut-accessibility`](../gamut-accessibility/SKILL.md) — ARIA and keyboard interaction. [`gamut-color-mode`](../gamut-color-mode/SKILL.md) — dark/light mode with `Background`. + +Storybook: [Organisms / Lists & Tables / DataTable](https://gamut.codecademy.com/?path=/docs-organisms-lists-tables-datatable--docs) + +## Components + +```tsx +import { DataTable } from '@codecademy/gamut'; +import { useLocalQuery } from '@codecademy/gamut'; +``` + +| Symbol | Role | +| --------------- | ----------------------------------------------------------------------------------------- | +| `DataTable` | Root component; always renders as a `table`-variant `List` with scrolling enabled | +| `useLocalQuery` | Client-side hook for sort/filter state; spread its return value directly into `DataTable` | + +## When to use DataTable + +- Displaying data that users need to **compare across rows** (metrics, scores, statuses, dates). +- When the data set needs **sorting, filtering, or both** and that logic lives client-side. +- Dashboards, admin tables, reports, and data-dense views. +- When rows are **not individually selectable or expandable** — use `DataList` if you need those. + +**Do not use DataTable when:** + +- Users engage with items individually (expand for details, select for bulk actions) → use [`DataList`](../gamut-datalist/SKILL.md). +- The list is small, static, and needs fully custom row layouts → use [`List`](../gamut-list/SKILL.md) directly. +- There is no data at all and you just need a layout container. + +## Design principles + +- **Prioritize comparison**: arrange columns to encourage scanning and finding patterns, not storytelling. +- **Surface secondary info on drill-down**: use Coachmarks, Tooltips, Modals, or Flyovers rather than cramming detail into table cells. +- **Avoid information overload**: determine what belongs en-masse vs. what should live on a detail surface. Order columns by priority; collapse lower-priority columns at smaller sizes. +- **Use cell-level interactions carefully**: anchors and links for navigation; popovers for in-context actions. + +## Props + +| Prop | Type | Default | Notes | +| ----------------------- | ------------------------- | ------------------- | ------------------------------------------------------------------- | +| `id` | `string` | required | Unique ID for the table | +| `idKey` | `keyof Row` | required | Row identifier — must be a `string \| number` field | +| `rows` | `Row[]` | required | Data array | +| `columns` | `ColumnConfig[]` | required | Column definitions | +| `query` | `Query` | — | Current sort/filter state; use `useLocalQuery` or manage externally | +| `onQueryChange` | `OnQueryChange` | — | Called when sort or filter changes | +| `loading` | `boolean` | `false` | Replaces row content with shimmer placeholders | +| `spacing` | `'condensed' \| 'normal'` | `'condensed'` | Row padding | +| `scrollable` | `boolean` | `true` | Enables horizontal scroll with sticky first column | +| `shadow` | `boolean` | `false` | Shows a right-side scroll-indicator shadow | +| `height` | `string` | `'100%'` | Container height when `scrollable` is true | +| `minHeight` | `number` | `0` | Minimum container height | +| `emptyMessage` | `ReactNode` | default empty state | Rendered when `rows` is empty | +| `showOverflow` | `boolean` | — | Shows overflow indicators in cells | +| `disableContainerQuery` | `boolean` | `false` | Falls back to media queries instead of container queries | +| `scrollToTopOnUpdate` | `boolean` | `false` | Scrolls to top when rows change | +| `wrapperWidth` | `string` | — | Custom wrapper width override | + +## ColumnConfig + +| Field | Type | Notes | +| ---------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `key` | `keyof Row` | Required; maps to a row data field | +| `header` | `string` | Column header label | +| `type` | `'header' \| 'control'` | `'header'` makes the column sticky when scrollable; `'control'` right-aligns and removes padding for action buttons | +| `size` | `'sm' \| 'md' \| 'lg' \| 'xl'` | Column width; omit to fit content | +| `fill` | `boolean` | Expands the column to fill remaining width | +| `justify` | `'left' \| 'right'` | Cell alignment | +| `sortable` | `boolean` | Adds a sort toggle to the column header | +| `filters` | `string[]` | Adds a filter dropdown with these string values | +| `options` | `Array` | Alternative to `filters` when display text differs from value | +| `render` | `(row: Row) => ReactElement \| null` | Custom cell renderer | + +## Basic usage + +```tsx +import { DataTable, useLocalQuery } from '@codecademy/gamut'; + +const columns = [ + { key: 'name', header: 'Name', size: 'md', sortable: true }, + { + key: 'role', + header: 'Role', + size: 'md', + filters: ['Engineer', 'Design', 'Product'], + }, + { + key: 'score', + header: 'Score', + size: 'sm', + sortable: true, + justify: 'right', + }, +]; + +const MyTable = ({ data }) => { + const queryData = useLocalQuery({ idKey: 'id', rows: data, columns }); + + return ( + + ); +}; +``` + +## Custom cell render + +Use `render` for cells that need non-text content (status badges, progress bars, action buttons). + +```tsx +const columns = [ + { key: 'name', header: 'Name', size: 'md', type: 'header' }, + { + key: 'status', + header: 'Status', + size: 'sm', + render: (row) => ( + + {row.status} + + ), + }, +]; +``` + +## Row action menus + +Use a `type: 'control'` column with `PopoverContainer` and `Menu` for row-level actions. + +```tsx +import { + DataTable, + IconButton, + Menu, + MenuItem, + Modal, + PopoverContainer, +} from '@codecademy/gamut'; +import { MiniKebabMenuIcon } from '@codecademy/gamut-icons'; +import { useRef, useState } from 'react'; + +const RowActions = ({ rowId }) => { + const [isOpen, setIsOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const ref = useRef(null); + + return ( + + setIsOpen(!isOpen)} + /> + setIsOpen(false)} + > + + setIsOpen(false)}>Edit + { + setIsOpen(false); + setIsModalOpen(true); + }} + > + View details + + + + setIsModalOpen(false)} + > + {/* detail content */} + + + ); +}; + +const columns = [ + { key: 'name', header: 'Name', size: 'md', type: 'header' }, + { + key: 'id', + header: '', + size: 'sm', + type: 'control', + justify: 'right', + render: (row) => , + }, +]; +``` + +Key points: + +- `closeOnViewportExit` on `PopoverContainer` closes the menu when its row scrolls out of view. +- `allowPageInteraction` lets users interact with the table while the menu is open. +- Modals opened from menu items render at `zIndex={3}` by default, above the table header. + +## Scrollable table + +`scrollable` defaults to `true` on DataTable. The first `type: 'header'` column sticks to the left. Add `shadow` for a visual overflow indicator. + +```tsx + +``` + +## Empty state + +DataTable shows a default empty state when `rows` is empty. Override with `emptyMessage`. + +```tsx + + + + No results found. + + + + } +/> +``` + +## Loading state + +Pass `loading` to replace row content with shimmer placeholders while data fetches. + +```tsx + +``` + +## Color mode + +DataTable inherits background color from the `current-background` token. Wrap in `Background` from `@codecademy/gamut-styles` to apply a surface color and automatically switch to dark mode contrast. + +```tsx +import { Background } from '@codecademy/gamut-styles'; + + + +; +``` + +## Container queries + +DataTable uses CSS container queries by default for responsive column stacking. Disable only when: + +- The table lives in a container narrower than its breakpoint. +- You are managing your own responsive logic. + +```tsx +
+ +
+``` + +## Accessibility + +- DataTable renders as a semantic `
` element automatically. +- Sort controls receive `aria-sort` automatically when `sortable` is set on a column. +- Filter controls are keyboard-accessible via the column header dropdowns. +- For custom `emptyMessage`, use `tbody > tr > th` structure for valid table semantics (see empty state example above). diff --git a/packages/gamut/agent-tools/skills/gamut-list/SKILL.md b/packages/gamut/agent-tools/skills/gamut-list/SKILL.md index d99dacdba87..ffafc25c819 100644 --- a/packages/gamut/agent-tools/skills/gamut-list/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-list/SKILL.md @@ -9,7 +9,7 @@ Structured, repeating layouts built from `List`, `ListRow`, `ListCol`, and `Tabl Source: `@codecademy/gamut` — [List.tsx](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/List/List.tsx) -See also: [`gamut-accessibility`](../gamut-accessibility/SKILL.md) — ARIA, focus, and keyboard interaction rules. [`gamut-layout`](../gamut-layout/SKILL.md) — spacing tokens and system props. +See also: [`gamut-datatable`](../gamut-datatable/SKILL.md) — use instead of List when data needs sorting, filtering, or query state. [`gamut-datalist`](../gamut-datalist/SKILL.md) — use instead of List when rows need expansion or selection. [`gamut-accessibility`](../gamut-accessibility/SKILL.md) — ARIA, focus, and keyboard interaction rules. [`gamut-layout`](../gamut-layout/SKILL.md) — spacing tokens and system props. Storybook: @@ -32,8 +32,16 @@ import { List, ListRow, ListCol, TableHeader } from '@codecademy/gamut'; ## When to use List +List is for **fully custom, manually composed layouts**. Reach for a higher-level component first: + +- Data that needs **sorting, filtering, or query state** → use [`DataTable`](../gamut-datatable/SKILL.md) or [`DataList`](../gamut-datalist/SKILL.md) instead of wiring these up manually in List. +- Rows that need **expansion or row selection** → use [`DataList`](../gamut-datalist/SKILL.md). + +Use List directly when: + +- You need fully custom row/column composition that DataTable or DataList cannot accommodate. - Displaying repetitive content where individual rows may contain interactive elements, metrics, or controls — use List, not Card. -- Comparing data across rows — use `variant="table"` (or `as="table"`) rather than a plain `
`. +- Comparing data across rows with a fully custom layout — use `variant="table"` (or `as="table"`) rather than a plain `
`. - Needing numbered rows — use `as="ol"`. - **Needing multiple expandable/disclosure-style items** — use List's expandable row pattern (see [Expandable rows](#expandable-rows)), not multiple standalone `Disclosure` components. From 3aba03298150cddd56c8ffa533b00817a15f8487 Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Thu, 18 Jun 2026 08:21:44 -0400 Subject: [PATCH 09/10] new skills --- .../skills/gamut-create-skill/SKILL.md | 244 ++++++++++++++++++ .../lib/Atoms/Buttons/CTAButton/CTAButton.mdx | 4 +- .../Atoms/Buttons/FillButton/FillButton.mdx | 4 +- .../Atoms/Buttons/IconButton/IconButton.mdx | 4 +- .../Buttons/StrokeButton/StrokeButton.mdx | 4 +- .../Atoms/Buttons/TextButton/TextButton.mdx | 4 +- .../lib/Foundations/ColorMode/ColorMode.mdx | 4 +- .../Gamut plugin/Best practices.mdx | 4 + .../src/lib/Organisms/GridForm/About.mdx | 3 + .../Lists & Tables/DataList/DataList.mdx | 2 + .../Lists & Tables/DataTable/DataTable.mdx | 2 + .../Organisms/Lists & Tables/List/List.mdx | 2 + .../src/lib/Typography/Text/Text.mdx | 4 +- 13 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 packages/gamut/agent-tools/skills/gamut-create-skill/SKILL.md diff --git a/packages/gamut/agent-tools/skills/gamut-create-skill/SKILL.md b/packages/gamut/agent-tools/skills/gamut-create-skill/SKILL.md new file mode 100644 index 00000000000..f24ca7fb588 --- /dev/null +++ b/packages/gamut/agent-tools/skills/gamut-create-skill/SKILL.md @@ -0,0 +1,244 @@ +--- +name: gamut-create-skill +description: Use this skill when creating a new Gamut agent skill from scratch — including researching the component source and Storybook docs, writing SKILL.md, validating structure and token weight, checking consistency against existing skills, updating the Storybook AI Tools list, and adding a callout to the relevant component page. Do not use for general Gamut development tasks — see individual component skills for that. +--- + +# Gamut Create Skill + +Workflow playbook for authoring a new Gamut agent skill. Covers research, structure, token discipline, consistency validation, and Storybook integration. Intended for Gamut contributors adding skill coverage for a new or under-documented component. + +Reference skills: [`gamut-buttons`](../gamut-buttons/SKILL.md) — reference for a tight component skill. [`gamut-datatable`](../gamut-datatable/SKILL.md) — reference for an organism skill with full API surface. [`gamut-review`](../gamut-review/SKILL.md) — reference for an audit/workflow skill. + +Storybook: [Meta / AI Tooling / Gamut plugin / Best practices](https://gamut.codecademy.com/?path=/docs-meta-ai-tooling-gamut-plugin-best-practices--page) + +Claude Code skill authoring reference: [Claude Code skills documentation](https://docs.anthropic.com/en/docs/claude-code/skills) + +--- + +## Phase 1 — Research before writing + +Do not write until you have read all of the following. + +**Component source** + +Read the component's TypeScript source and type definitions: + +``` +packages/gamut/src/{ComponentName}/ +``` + +Extract: exported symbols, prop interfaces, default values, generic constraints, any hooks exported alongside the component. + +**Storybook MDX** + +Read the component's MDX documentation page: + +``` +packages/styleguide/src/lib/{Category}/{ComponentName}/{ComponentName}.mdx +``` + +Extract: design principles, "When to use" / "Do not use" guidance, usage notes. These belong in the skill verbatim or paraphrased — they encode the designer's intent. + +**Related skills** + +Identify 2–3 existing skills that overlap. Check for: + +- Components this one wraps or is used alongside +- Skills that already cover adjacent concepts (e.g. `gamut-accessibility` for ARIA, `gamut-color-mode` for theming) + +**Existing callout** + +Check whether the component's Storybook MDX already has a `` for the skill. If so, you only need to update the text, not add a new import. + +--- + +## Skill directory structure + +``` +packages/gamut/agent-tools/skills/ + {skill-name}/ + SKILL.md ← only file; no README, no index +``` + +Rules: + +- Directory name is kebab-case and must exactly match the `name:` frontmatter field +- No other files in the directory + +--- + +## Frontmatter spec + +```yaml +--- +name: gamut-{kebab-case-name} +description: Use this skill when {trigger 1}, {trigger 2}[, {trigger 3}] — {scope and features}[; not for {anti-pattern} (see {gamut-other-skill})]. +--- +``` + +`description` rules: + +- Must start with `"Use this skill when"` +- List **2–6 concrete trigger scenarios** (comma-separated) +- Follow with `—` then scope/features the skill teaches +- Add anti-patterns where the boundary with another skill is non-obvious +- Target: 1–2 sentences, ≤200 characters per sentence +- Use inline backticks for component/function names + +--- + +## Section template + +Use this ordered structure. Omit sections that don't apply; do not reorder. + +```markdown +# {Title-Cased Skill Name} + +{1–2 sentence purpose and scope. No "this skill covers…" framing.} + +Source: `@codecademy/gamut` — [ComponentName.tsx]({github-url}) + +See also: [`gamut-x`](../gamut-x/SKILL.md) — {why}. [`gamut-y`](../gamut-y/SKILL.md) — {why}. + +Storybook: [{Path / ComponentName}]({gamut.codecademy.com url}) + +## Components + +\`\`\`tsx +import { ComponentName } from '@codecademy/gamut'; +\`\`\` + +| Symbol | Role | +| --------------- | -------------- | +| `ComponentName` | {what it does} | + +## When to use {ComponentName} + +- {Positive case 1} +- {Positive case 2} + +**Do not use {ComponentName} when:** + +- {Anti-pattern 1} → use [{alternative}]({link}) +- {Anti-pattern 2} + +## Design principles + +{Lifted from Storybook MDX. 3–5 bullets. Imperative, design-voice.} + +## Props + +| Prop | Type | Default | Notes | +| ---------- | ------ | --------- | ------ | +| `propName` | `type` | `default` | {note} | + +> **{Non-obvious gotcha title}**: {explanation of surprising behavior, constraint, or invariant}. +> Only add this block when the behavior would genuinely surprise a reader. + +## Basic usage + +\`\`\`tsx +// {describe what this shows} + +\`\`\` + +## {Pattern name} (repeat per pattern) + +\`\`\`tsx +// correct + + +// wrong — {reason} + +\`\`\` + +## Accessibility + +{Only if the component has ARIA or focus concerns beyond the universal rules.} + +Do not repeat content from [`accessibility.mdc`](../../rules/accessibility.mdc) — link to it instead. +``` + +--- + +## Token and context guidance + +The `description` field is loaded on every skill invocation — token waste here is paid on every use. + +| Principle | Rule | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Reference, don't repeat | Point to `accessibility.mdc`, related skills, or Storybook rather than copying content | +| Tables > prose | Use tables for prop references, option lists, and decision trees | +| Front-load | Lead each section with the most common case; edge cases last | +| Flat structure | Max one level of H3 subsections under each H2 | +| Line target | 100–180 lines for a component skill; 200–400 for a workflow skill | +| Gotcha blocks | Only for things that would genuinely surprise a reader (layout behavior, type constraints, focus side-effects) | +| Code examples | Language marker required (`tsx`, `ts`, `jsx`); show both correct and incorrect when the wrong pattern is common | +| Semantic tokens | All code examples use semantic color tokens (`color="text"`, `bg="background"`) — never hardcoded hex or raw palette names | +| No AI-speak | Write imperatively. No "this skill will help you…", "certainly", or meta-commentary | + +--- + +## Consistency checklist + +Run this against your draft before committing: + +- [ ] `name` field matches directory name exactly (kebab-case) +- [ ] `description` starts with "Use this skill when" and lists ≥2 concrete trigger scenarios +- [ ] H1 title is present and matches skill purpose +- [ ] `See also:` block with ≥1 related skill and a one-line reason +- [ ] ≥1 Storybook link using a full `gamut.codecademy.com` URL +- [ ] All code blocks have a language marker +- [ ] "When to use" has both positive cases and a "Do not use when" sub-list +- [ ] No hardcoded hex (`#...`) or raw palette names in code examples +- [ ] No duplication of `accessibility.mdc` — link to it instead +- [ ] Props table includes: name, type, default, notes +- [ ] Non-obvious gotchas have a callout block +- [ ] No trailing "See also" list that duplicates the top `See also:` block +- [ ] Source code GitHub link present + +--- + +## Storybook integration + +Both steps are required after writing the skill. + +### Step 1 — Add to Best practices list + +File: `packages/styleguide/src/lib/Meta/AI Tooling/Gamut plugin/Best practices.mdx` + +```md +- `gamut-{name}` — {one-line description, matching the skill's overview sentence} +``` + +Group the entry near related skills (e.g. new list/table skills go after `gamut-datalist`; new form skills go after `gamut-forms`). + +### Step 2 — Add Callout to component Storybook page + +File: the component's MDX (e.g. `packages/styleguide/src/lib/Organisms/.../ComponentName.mdx`) + +If `Callout` is not yet imported, add it to the `~styleguide/blocks` import: + +```diff +- import { ComponentHeader } from '~styleguide/blocks'; ++ import { Callout, ComponentHeader } from '~styleguide/blocks'; +``` + +Place the callout immediately after `` or ``: + +```mdx + +``` + +For skills covering a concept rather than a single component (e.g. `gamut-color-mode`, `gamut-layout`), add the callout to the primary concept page (`ColorMode.mdx`, `Layout.mdx`, etc.) — not every page that tangentially touches the concept. + +--- + +## Reference skills by type + +| Type | Skill | Read it for | +| ---------------------- | -------------------------------------------------- | ------------------------------------------------- | +| Tight component | [`gamut-buttons`](../gamut-buttons/SKILL.md) | Scope discipline, clean "When to use / not use" | +| Organism with full API | [`gamut-datatable`](../gamut-datatable/SKILL.md) | Props table + ColumnConfig + gotcha note pattern | +| Audit / workflow | [`gamut-review`](../gamut-review/SKILL.md) | Multi-phase check structure, remediation pointers | +| System / concept | [`gamut-color-mode`](../gamut-color-mode/SKILL.md) | Semantic-first framing, cross-skill boundaries | diff --git a/packages/styleguide/src/lib/Atoms/Buttons/CTAButton/CTAButton.mdx b/packages/styleguide/src/lib/Atoms/Buttons/CTAButton/CTAButton.mdx index 72f80f9162d..03fafd7d284 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/CTAButton/CTAButton.mdx +++ b/packages/styleguide/src/lib/Atoms/Buttons/CTAButton/CTAButton.mdx @@ -1,6 +1,6 @@ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; -import { ComponentHeader } from '~styleguide/blocks'; +import { Callout, ComponentHeader } from '~styleguide/blocks'; import * as CTAButtonStories from './CTAButton.stories'; @@ -23,6 +23,8 @@ export const parameters = { + + ## Usage Use this for high-visibility marketing actions. Include a leading or trailing icons from our mini icon set to clarify an action. diff --git a/packages/styleguide/src/lib/Atoms/Buttons/FillButton/FillButton.mdx b/packages/styleguide/src/lib/Atoms/Buttons/FillButton/FillButton.mdx index 0a38d47aad6..433dde09567 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/FillButton/FillButton.mdx +++ b/packages/styleguide/src/lib/Atoms/Buttons/FillButton/FillButton.mdx @@ -1,6 +1,6 @@ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; -import { ComponentHeader } from '~styleguide/blocks'; +import { Callout, ComponentHeader } from '~styleguide/blocks'; import * as FillButtonStories from './FillButton.stories'; @@ -23,6 +23,8 @@ export const parameters = { + + ## Usage Use this for primary actions. Include a leading or trailing icons from our mini icon set to clarify an action. diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx index 8bafaccb195..6c21691cfcc 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx @@ -1,6 +1,6 @@ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; -import { ComponentHeader, LinkTo } from '~styleguide/blocks'; +import { Callout, ComponentHeader, LinkTo } from '~styleguide/blocks'; import * as IconButtonStories from './IconButton.stories'; @@ -23,6 +23,8 @@ export const parameters = { + + ## Usage Use for secondary or space-constrained actions with recognizable icons. Use regular icons for normal buttons and mini icons for small buttons. Icon-only buttons must include a `tip` prop, which will also appear in the tooltip on hover and focus. diff --git a/packages/styleguide/src/lib/Atoms/Buttons/StrokeButton/StrokeButton.mdx b/packages/styleguide/src/lib/Atoms/Buttons/StrokeButton/StrokeButton.mdx index ad65bf8f380..4407ae961e7 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/StrokeButton/StrokeButton.mdx +++ b/packages/styleguide/src/lib/Atoms/Buttons/StrokeButton/StrokeButton.mdx @@ -1,6 +1,6 @@ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; -import { ComponentHeader } from '~styleguide/blocks'; +import { Callout, ComponentHeader } from '~styleguide/blocks'; import * as StrokeButtonStories from './StrokeButton.stories'; @@ -23,6 +23,8 @@ export const parameters = { + + ## Usage Use this for secondary actions. Include a leading or trailing icons from our mini icon set to clarify an action. diff --git a/packages/styleguide/src/lib/Atoms/Buttons/TextButton/TextButton.mdx b/packages/styleguide/src/lib/Atoms/Buttons/TextButton/TextButton.mdx index dc7e7ea2b0a..d944de7c559 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/TextButton/TextButton.mdx +++ b/packages/styleguide/src/lib/Atoms/Buttons/TextButton/TextButton.mdx @@ -1,6 +1,6 @@ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; -import { ComponentHeader } from '~styleguide/blocks'; +import { Callout, ComponentHeader } from '~styleguide/blocks'; import * as TextButtonStories from './TextButton.stories'; @@ -23,6 +23,8 @@ export const parameters = { + + ## Usage Use this for tertiary actions. Include a leading or trailing icons from our mini icon set to clarify an action or to distinguish the button from other bold text. diff --git a/packages/styleguide/src/lib/Foundations/ColorMode/ColorMode.mdx b/packages/styleguide/src/lib/Foundations/ColorMode/ColorMode.mdx index c7383fc9d36..376818aa831 100644 --- a/packages/styleguide/src/lib/Foundations/ColorMode/ColorMode.mdx +++ b/packages/styleguide/src/lib/Foundations/ColorMode/ColorMode.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/addon-docs/blocks'; -import { AboutHeader } from '~styleguide/blocks'; +import { AboutHeader, Callout } from '~styleguide/blocks'; import { DarkModeTable, LightModeTable } from '../shared/elements'; import { @@ -25,6 +25,8 @@ export const parameters = { + + We've created several color modes by default for light and dark context. Each color mode consists of a set of aliased color tokens. Each alias has semantic meaning for how the color is used throughout our design system: diff --git a/packages/styleguide/src/lib/Meta/AI Tooling/Gamut plugin/Best practices.mdx b/packages/styleguide/src/lib/Meta/AI Tooling/Gamut plugin/Best practices.mdx index 1de86b0d52f..caa15b11332 100644 --- a/packages/styleguide/src/lib/Meta/AI Tooling/Gamut plugin/Best practices.mdx +++ b/packages/styleguide/src/lib/Meta/AI Tooling/Gamut plugin/Best practices.mdx @@ -85,10 +85,14 @@ Related skills under [`packages/gamut/agent-tools/skills/`](https://github.com/C - `gamut-theming` — theme selection, `GamutProvider`, `theme.d.ts` - `gamut-color-mode` — ColorMode and semantic color - `gamut-buttons` — button atoms, variants, disabled patterns +- `gamut-list` — List, ListRow, ListCol primitives for custom layouts +- `gamut-datatable` — DataTable for sortable/filterable bulk data comparison +- `gamut-datalist` — DataList for item management with expansion and selection - `gamut-layout` — spacing scale, breakpoints, page grid - `gamut-system-props` — `system.*` / `Box` - `gamut-style-utilities` — `css`, `variant`, `states` - `gamut-typography`, `gamut-forms`, `gamut-accessibility`, `gamut-testing` +- `gamut-create-skill` — workflow playbook for authoring a new Gamut agent skill from scratch ## Design-to-code (Figma MCP) diff --git a/packages/styleguide/src/lib/Organisms/GridForm/About.mdx b/packages/styleguide/src/lib/Organisms/GridForm/About.mdx index fc8fad31947..5776cfd215f 100644 --- a/packages/styleguide/src/lib/Organisms/GridForm/About.mdx +++ b/packages/styleguide/src/lib/Organisms/GridForm/About.mdx @@ -3,6 +3,7 @@ import { Meta } from '@storybook/addon-docs/blocks'; import { AboutHeader, addParentPath, + Callout, LinkTo, TableOfContents, } from '~styleguide/blocks'; @@ -24,6 +25,8 @@ export const parameters = { + + ## Getting started If you're new to `GridForm`, start with Usage to see an overview of how `GridForm` works and interact with a live playground. diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.mdx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.mdx index c7647d40c47..5011e38d2d3 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.mdx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.mdx @@ -19,6 +19,8 @@ export const parameters = { + + ## Design principles **Recommended for engaging with individual Items** diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.mdx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.mdx index e951eacee32..876a1d8139e 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.mdx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.mdx @@ -19,6 +19,8 @@ export const parameters = { + + ## Design principles **Design principles** diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/List/List.mdx b/packages/styleguide/src/lib/Organisms/Lists & Tables/List/List.mdx index 1103502a880..88d7ff1422e 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/List/List.mdx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/List/List.mdx @@ -26,6 +26,8 @@ export const parameters = { + + ## Overview `List` is the root container and it provides context to child rows and columns. diff --git a/packages/styleguide/src/lib/Typography/Text/Text.mdx b/packages/styleguide/src/lib/Typography/Text/Text.mdx index 782563acdab..cf814375fd1 100644 --- a/packages/styleguide/src/lib/Typography/Text/Text.mdx +++ b/packages/styleguide/src/lib/Typography/Text/Text.mdx @@ -1,6 +1,6 @@ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; -import { ComponentHeader, LinkTo } from '~styleguide/blocks'; +import { Callout, ComponentHeader, LinkTo } from '~styleguide/blocks'; import * as TextStories from './Text.stories'; @@ -24,6 +24,8 @@ export const parameters = { + + ## Usage This is the preferred component for customizing typography. The API is progressively specific. In order to achieve the effect you are looking for there are 3 levels of precedence. From cca5497f80e2a1b6c8919914a8d0050374828708 Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Thu, 18 Jun 2026 10:36:12 -0400 Subject: [PATCH 10/10] code skill + audit all skills with writing-great-skills --- .../skills/gamut-create-skill/SKILL.md | 41 ++- .cursor/rules/gamut-create-skill.mdc | 263 ++++++++++++++++++ .husky/pre-commit | 2 + local-skills/gamut-create-skill/SKILL.md | 263 ++++++++++++++++++ package.json | 2 + .../skills/gamut-accessibility/SKILL.md | 2 +- .../agent-tools/skills/gamut-buttons/SKILL.md | 54 +--- .../skills/gamut-buttons/TOOLTIP_FOCUS.md | 55 ++++ .../skills/gamut-color-mode/SKILL.md | 93 +------ .../skills/gamut-datatable/SKILL.md | 52 +--- .../agent-tools/skills/gamut-forms/SKILL.md | 2 +- .../agent-tools/skills/gamut-layout/SKILL.md | 2 +- .../agent-tools/skills/gamut-review/SKILL.md | 2 +- .../skills/gamut-style-utilities/SKILL.md | 8 +- .../skills/gamut-system-props/SKILL.md | 13 +- .../agent-tools/skills/gamut-testing/SKILL.md | 2 - .../agent-tools/skills/gamut-theming/SKILL.md | 7 - .../skills/gamut-typography/SKILL.md | 2 +- .../Gamut plugin/Best practices.mdx | 11 +- scripts/sync-local-skills.mjs | 115 ++++++++ 20 files changed, 754 insertions(+), 237 deletions(-) rename {packages/gamut/agent-tools => .claude}/skills/gamut-create-skill/SKILL.md (76%) create mode 100644 .cursor/rules/gamut-create-skill.mdc create mode 100644 local-skills/gamut-create-skill/SKILL.md create mode 100644 packages/gamut/agent-tools/skills/gamut-buttons/TOOLTIP_FOCUS.md create mode 100644 scripts/sync-local-skills.mjs diff --git a/packages/gamut/agent-tools/skills/gamut-create-skill/SKILL.md b/.claude/skills/gamut-create-skill/SKILL.md similarity index 76% rename from packages/gamut/agent-tools/skills/gamut-create-skill/SKILL.md rename to .claude/skills/gamut-create-skill/SKILL.md index f24ca7fb588..08d2889dbaf 100644 --- a/packages/gamut/agent-tools/skills/gamut-create-skill/SKILL.md +++ b/.claude/skills/gamut-create-skill/SKILL.md @@ -1,18 +1,21 @@ --- name: gamut-create-skill -description: Use this skill when creating a new Gamut agent skill from scratch — including researching the component source and Storybook docs, writing SKILL.md, validating structure and token weight, checking consistency against existing skills, updating the Storybook AI Tools list, and adding a callout to the relevant component page. Do not use for general Gamut development tasks — see individual component skills for that. +description: 'Blueprint — use this skill when authoring a new Gamut agent skill from scratch, from component source research through Storybook integration. Not for general Gamut development (see individual component skills).' +alwaysApply: false --- # Gamut Create Skill -Workflow playbook for authoring a new Gamut agent skill. Covers research, structure, token discipline, consistency validation, and Storybook integration. Intended for Gamut contributors adding skill coverage for a new or under-documented component. +Blueprint playbook for authoring a new Gamut agent skill. -Reference skills: [`gamut-buttons`](../gamut-buttons/SKILL.md) — reference for a tight component skill. [`gamut-datatable`](../gamut-datatable/SKILL.md) — reference for an organism skill with full API surface. [`gamut-review`](../gamut-review/SKILL.md) — reference for an audit/workflow skill. +Reference skills: [`gamut-buttons`](../../packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md) — reference for a tight component skill. [`gamut-datatable`](../../packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md) — reference for an organism skill with full API surface. [`gamut-review`](../../packages/gamut/agent-tools/skills/gamut-review/SKILL.md) — reference for an audit/workflow skill. Storybook: [Meta / AI Tooling / Gamut plugin / Best practices](https://gamut.codecademy.com/?path=/docs-meta-ai-tooling-gamut-plugin-best-practices--page) Claude Code skill authoring reference: [Claude Code skills documentation](https://docs.anthropic.com/en/docs/claude-code/skills) +Skill quality review: use `/writing-great-skills` in Claude Code to audit a draft against information hierarchy, leading words, no-ops, and pruning before committing. + --- ## Phase 1 — Research before writing @@ -54,16 +57,31 @@ Check whether the component's Storybook MDX already has a `` for the sk ## Skill directory structure +There are two locations, depending on the intended audience: + +**Exported skills** — shipped to app repos via `gamut plugin install`: + ``` packages/gamut/agent-tools/skills/ {skill-name}/ - SKILL.md ← only file; no README, no index + SKILL.md ``` -Rules: +**Contributor-only skills** — available in this repo only, never exported: + +``` +local-skills/ + {skill-name}/ + SKILL.md ← canonical source +``` + +Contributor skills are synced automatically to `.claude/skills/` (Claude Code) and `.cursor/rules/` (Cursor) by the pre-commit hook. To sync manually: `yarn sync-skills`. To watch during development: `yarn sync-skills:watch`. + +Rules (apply to both locations): - Directory name is kebab-case and must exactly match the `name:` frontmatter field - No other files in the directory +- Contributor skills must also include `alwaysApply: false` in frontmatter (required by Cursor) --- @@ -196,6 +214,7 @@ Run this against your draft before committing: - [ ] Non-obvious gotchas have a callout block - [ ] No trailing "See also" list that duplicates the top `See also:` block - [ ] Source code GitHub link present +- [ ] Draft reviewed with `/writing-great-skills` before committing --- @@ -236,9 +255,9 @@ For skills covering a concept rather than a single component (e.g. `gamut-color- ## Reference skills by type -| Type | Skill | Read it for | -| ---------------------- | -------------------------------------------------- | ------------------------------------------------- | -| Tight component | [`gamut-buttons`](../gamut-buttons/SKILL.md) | Scope discipline, clean "When to use / not use" | -| Organism with full API | [`gamut-datatable`](../gamut-datatable/SKILL.md) | Props table + ColumnConfig + gotcha note pattern | -| Audit / workflow | [`gamut-review`](../gamut-review/SKILL.md) | Multi-phase check structure, remediation pointers | -| System / concept | [`gamut-color-mode`](../gamut-color-mode/SKILL.md) | Semantic-first framing, cross-skill boundaries | +| Type | Skill | Read it for | +| ---------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------- | +| Tight component | [`gamut-buttons`](../../packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md) | Scope discipline, clean "When to use / not use" | +| Organism with full API | [`gamut-datatable`](../../packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md) | Props table + ColumnConfig + gotcha note pattern | +| Audit / workflow | [`gamut-review`](../../packages/gamut/agent-tools/skills/gamut-review/SKILL.md) | Multi-phase check structure, remediation pointers | +| System / concept | [`gamut-color-mode`](../../packages/gamut/agent-tools/skills/gamut-color-mode/SKILL.md) | Semantic-first framing, cross-skill boundaries | diff --git a/.cursor/rules/gamut-create-skill.mdc b/.cursor/rules/gamut-create-skill.mdc new file mode 100644 index 00000000000..08d2889dbaf --- /dev/null +++ b/.cursor/rules/gamut-create-skill.mdc @@ -0,0 +1,263 @@ +--- +name: gamut-create-skill +description: 'Blueprint — use this skill when authoring a new Gamut agent skill from scratch, from component source research through Storybook integration. Not for general Gamut development (see individual component skills).' +alwaysApply: false +--- + +# Gamut Create Skill + +Blueprint playbook for authoring a new Gamut agent skill. + +Reference skills: [`gamut-buttons`](../../packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md) — reference for a tight component skill. [`gamut-datatable`](../../packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md) — reference for an organism skill with full API surface. [`gamut-review`](../../packages/gamut/agent-tools/skills/gamut-review/SKILL.md) — reference for an audit/workflow skill. + +Storybook: [Meta / AI Tooling / Gamut plugin / Best practices](https://gamut.codecademy.com/?path=/docs-meta-ai-tooling-gamut-plugin-best-practices--page) + +Claude Code skill authoring reference: [Claude Code skills documentation](https://docs.anthropic.com/en/docs/claude-code/skills) + +Skill quality review: use `/writing-great-skills` in Claude Code to audit a draft against information hierarchy, leading words, no-ops, and pruning before committing. + +--- + +## Phase 1 — Research before writing + +Do not write until you have read all of the following. + +**Component source** + +Read the component's TypeScript source and type definitions: + +``` +packages/gamut/src/{ComponentName}/ +``` + +Extract: exported symbols, prop interfaces, default values, generic constraints, any hooks exported alongside the component. + +**Storybook MDX** + +Read the component's MDX documentation page: + +``` +packages/styleguide/src/lib/{Category}/{ComponentName}/{ComponentName}.mdx +``` + +Extract: design principles, "When to use" / "Do not use" guidance, usage notes. These belong in the skill verbatim or paraphrased — they encode the designer's intent. + +**Related skills** + +Identify 2–3 existing skills that overlap. Check for: + +- Components this one wraps or is used alongside +- Skills that already cover adjacent concepts (e.g. `gamut-accessibility` for ARIA, `gamut-color-mode` for theming) + +**Existing callout** + +Check whether the component's Storybook MDX already has a `` for the skill. If so, you only need to update the text, not add a new import. + +--- + +## Skill directory structure + +There are two locations, depending on the intended audience: + +**Exported skills** — shipped to app repos via `gamut plugin install`: + +``` +packages/gamut/agent-tools/skills/ + {skill-name}/ + SKILL.md +``` + +**Contributor-only skills** — available in this repo only, never exported: + +``` +local-skills/ + {skill-name}/ + SKILL.md ← canonical source +``` + +Contributor skills are synced automatically to `.claude/skills/` (Claude Code) and `.cursor/rules/` (Cursor) by the pre-commit hook. To sync manually: `yarn sync-skills`. To watch during development: `yarn sync-skills:watch`. + +Rules (apply to both locations): + +- Directory name is kebab-case and must exactly match the `name:` frontmatter field +- No other files in the directory +- Contributor skills must also include `alwaysApply: false` in frontmatter (required by Cursor) + +--- + +## Frontmatter spec + +```yaml +--- +name: gamut-{kebab-case-name} +description: Use this skill when {trigger 1}, {trigger 2}[, {trigger 3}] — {scope and features}[; not for {anti-pattern} (see {gamut-other-skill})]. +--- +``` + +`description` rules: + +- Must start with `"Use this skill when"` +- List **2–6 concrete trigger scenarios** (comma-separated) +- Follow with `—` then scope/features the skill teaches +- Add anti-patterns where the boundary with another skill is non-obvious +- Target: 1–2 sentences, ≤200 characters per sentence +- Use inline backticks for component/function names + +--- + +## Section template + +Use this ordered structure. Omit sections that don't apply; do not reorder. + +```markdown +# {Title-Cased Skill Name} + +{1–2 sentence purpose and scope. No "this skill covers…" framing.} + +Source: `@codecademy/gamut` — [ComponentName.tsx]({github-url}) + +See also: [`gamut-x`](../gamut-x/SKILL.md) — {why}. [`gamut-y`](../gamut-y/SKILL.md) — {why}. + +Storybook: [{Path / ComponentName}]({gamut.codecademy.com url}) + +## Components + +\`\`\`tsx +import { ComponentName } from '@codecademy/gamut'; +\`\`\` + +| Symbol | Role | +| --------------- | -------------- | +| `ComponentName` | {what it does} | + +## When to use {ComponentName} + +- {Positive case 1} +- {Positive case 2} + +**Do not use {ComponentName} when:** + +- {Anti-pattern 1} → use [{alternative}]({link}) +- {Anti-pattern 2} + +## Design principles + +{Lifted from Storybook MDX. 3–5 bullets. Imperative, design-voice.} + +## Props + +| Prop | Type | Default | Notes | +| ---------- | ------ | --------- | ------ | +| `propName` | `type` | `default` | {note} | + +> **{Non-obvious gotcha title}**: {explanation of surprising behavior, constraint, or invariant}. +> Only add this block when the behavior would genuinely surprise a reader. + +## Basic usage + +\`\`\`tsx +// {describe what this shows} + +\`\`\` + +## {Pattern name} (repeat per pattern) + +\`\`\`tsx +// correct + + +// wrong — {reason} + +\`\`\` + +## Accessibility + +{Only if the component has ARIA or focus concerns beyond the universal rules.} + +Do not repeat content from [`accessibility.mdc`](../../rules/accessibility.mdc) — link to it instead. +``` + +--- + +## Token and context guidance + +The `description` field is loaded on every skill invocation — token waste here is paid on every use. + +| Principle | Rule | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Reference, don't repeat | Point to `accessibility.mdc`, related skills, or Storybook rather than copying content | +| Tables > prose | Use tables for prop references, option lists, and decision trees | +| Front-load | Lead each section with the most common case; edge cases last | +| Flat structure | Max one level of H3 subsections under each H2 | +| Line target | 100–180 lines for a component skill; 200–400 for a workflow skill | +| Gotcha blocks | Only for things that would genuinely surprise a reader (layout behavior, type constraints, focus side-effects) | +| Code examples | Language marker required (`tsx`, `ts`, `jsx`); show both correct and incorrect when the wrong pattern is common | +| Semantic tokens | All code examples use semantic color tokens (`color="text"`, `bg="background"`) — never hardcoded hex or raw palette names | +| No AI-speak | Write imperatively. No "this skill will help you…", "certainly", or meta-commentary | + +--- + +## Consistency checklist + +Run this against your draft before committing: + +- [ ] `name` field matches directory name exactly (kebab-case) +- [ ] `description` starts with "Use this skill when" and lists ≥2 concrete trigger scenarios +- [ ] H1 title is present and matches skill purpose +- [ ] `See also:` block with ≥1 related skill and a one-line reason +- [ ] ≥1 Storybook link using a full `gamut.codecademy.com` URL +- [ ] All code blocks have a language marker +- [ ] "When to use" has both positive cases and a "Do not use when" sub-list +- [ ] No hardcoded hex (`#...`) or raw palette names in code examples +- [ ] No duplication of `accessibility.mdc` — link to it instead +- [ ] Props table includes: name, type, default, notes +- [ ] Non-obvious gotchas have a callout block +- [ ] No trailing "See also" list that duplicates the top `See also:` block +- [ ] Source code GitHub link present +- [ ] Draft reviewed with `/writing-great-skills` before committing + +--- + +## Storybook integration + +Both steps are required after writing the skill. + +### Step 1 — Add to Best practices list + +File: `packages/styleguide/src/lib/Meta/AI Tooling/Gamut plugin/Best practices.mdx` + +```md +- `gamut-{name}` — {one-line description, matching the skill's overview sentence} +``` + +Group the entry near related skills (e.g. new list/table skills go after `gamut-datalist`; new form skills go after `gamut-forms`). + +### Step 2 — Add Callout to component Storybook page + +File: the component's MDX (e.g. `packages/styleguide/src/lib/Organisms/.../ComponentName.mdx`) + +If `Callout` is not yet imported, add it to the `~styleguide/blocks` import: + +```diff +- import { ComponentHeader } from '~styleguide/blocks'; ++ import { Callout, ComponentHeader } from '~styleguide/blocks'; +``` + +Place the callout immediately after `` or ``: + +```mdx + +``` + +For skills covering a concept rather than a single component (e.g. `gamut-color-mode`, `gamut-layout`), add the callout to the primary concept page (`ColorMode.mdx`, `Layout.mdx`, etc.) — not every page that tangentially touches the concept. + +--- + +## Reference skills by type + +| Type | Skill | Read it for | +| ---------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------- | +| Tight component | [`gamut-buttons`](../../packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md) | Scope discipline, clean "When to use / not use" | +| Organism with full API | [`gamut-datatable`](../../packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md) | Props table + ColumnConfig + gotcha note pattern | +| Audit / workflow | [`gamut-review`](../../packages/gamut/agent-tools/skills/gamut-review/SKILL.md) | Multi-phase check structure, remediation pointers | +| System / concept | [`gamut-color-mode`](../../packages/gamut/agent-tools/skills/gamut-color-mode/SKILL.md) | Semantic-first framing, cross-skill boundaries | diff --git a/.husky/pre-commit b/.husky/pre-commit index 2312dc587f6..9db48e9e1b6 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,3 @@ npx lint-staged +node scripts/sync-local-skills.mjs +git add .claude/skills .cursor/rules diff --git a/local-skills/gamut-create-skill/SKILL.md b/local-skills/gamut-create-skill/SKILL.md new file mode 100644 index 00000000000..08d2889dbaf --- /dev/null +++ b/local-skills/gamut-create-skill/SKILL.md @@ -0,0 +1,263 @@ +--- +name: gamut-create-skill +description: 'Blueprint — use this skill when authoring a new Gamut agent skill from scratch, from component source research through Storybook integration. Not for general Gamut development (see individual component skills).' +alwaysApply: false +--- + +# Gamut Create Skill + +Blueprint playbook for authoring a new Gamut agent skill. + +Reference skills: [`gamut-buttons`](../../packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md) — reference for a tight component skill. [`gamut-datatable`](../../packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md) — reference for an organism skill with full API surface. [`gamut-review`](../../packages/gamut/agent-tools/skills/gamut-review/SKILL.md) — reference for an audit/workflow skill. + +Storybook: [Meta / AI Tooling / Gamut plugin / Best practices](https://gamut.codecademy.com/?path=/docs-meta-ai-tooling-gamut-plugin-best-practices--page) + +Claude Code skill authoring reference: [Claude Code skills documentation](https://docs.anthropic.com/en/docs/claude-code/skills) + +Skill quality review: use `/writing-great-skills` in Claude Code to audit a draft against information hierarchy, leading words, no-ops, and pruning before committing. + +--- + +## Phase 1 — Research before writing + +Do not write until you have read all of the following. + +**Component source** + +Read the component's TypeScript source and type definitions: + +``` +packages/gamut/src/{ComponentName}/ +``` + +Extract: exported symbols, prop interfaces, default values, generic constraints, any hooks exported alongside the component. + +**Storybook MDX** + +Read the component's MDX documentation page: + +``` +packages/styleguide/src/lib/{Category}/{ComponentName}/{ComponentName}.mdx +``` + +Extract: design principles, "When to use" / "Do not use" guidance, usage notes. These belong in the skill verbatim or paraphrased — they encode the designer's intent. + +**Related skills** + +Identify 2–3 existing skills that overlap. Check for: + +- Components this one wraps or is used alongside +- Skills that already cover adjacent concepts (e.g. `gamut-accessibility` for ARIA, `gamut-color-mode` for theming) + +**Existing callout** + +Check whether the component's Storybook MDX already has a `` for the skill. If so, you only need to update the text, not add a new import. + +--- + +## Skill directory structure + +There are two locations, depending on the intended audience: + +**Exported skills** — shipped to app repos via `gamut plugin install`: + +``` +packages/gamut/agent-tools/skills/ + {skill-name}/ + SKILL.md +``` + +**Contributor-only skills** — available in this repo only, never exported: + +``` +local-skills/ + {skill-name}/ + SKILL.md ← canonical source +``` + +Contributor skills are synced automatically to `.claude/skills/` (Claude Code) and `.cursor/rules/` (Cursor) by the pre-commit hook. To sync manually: `yarn sync-skills`. To watch during development: `yarn sync-skills:watch`. + +Rules (apply to both locations): + +- Directory name is kebab-case and must exactly match the `name:` frontmatter field +- No other files in the directory +- Contributor skills must also include `alwaysApply: false` in frontmatter (required by Cursor) + +--- + +## Frontmatter spec + +```yaml +--- +name: gamut-{kebab-case-name} +description: Use this skill when {trigger 1}, {trigger 2}[, {trigger 3}] — {scope and features}[; not for {anti-pattern} (see {gamut-other-skill})]. +--- +``` + +`description` rules: + +- Must start with `"Use this skill when"` +- List **2–6 concrete trigger scenarios** (comma-separated) +- Follow with `—` then scope/features the skill teaches +- Add anti-patterns where the boundary with another skill is non-obvious +- Target: 1–2 sentences, ≤200 characters per sentence +- Use inline backticks for component/function names + +--- + +## Section template + +Use this ordered structure. Omit sections that don't apply; do not reorder. + +```markdown +# {Title-Cased Skill Name} + +{1–2 sentence purpose and scope. No "this skill covers…" framing.} + +Source: `@codecademy/gamut` — [ComponentName.tsx]({github-url}) + +See also: [`gamut-x`](../gamut-x/SKILL.md) — {why}. [`gamut-y`](../gamut-y/SKILL.md) — {why}. + +Storybook: [{Path / ComponentName}]({gamut.codecademy.com url}) + +## Components + +\`\`\`tsx +import { ComponentName } from '@codecademy/gamut'; +\`\`\` + +| Symbol | Role | +| --------------- | -------------- | +| `ComponentName` | {what it does} | + +## When to use {ComponentName} + +- {Positive case 1} +- {Positive case 2} + +**Do not use {ComponentName} when:** + +- {Anti-pattern 1} → use [{alternative}]({link}) +- {Anti-pattern 2} + +## Design principles + +{Lifted from Storybook MDX. 3–5 bullets. Imperative, design-voice.} + +## Props + +| Prop | Type | Default | Notes | +| ---------- | ------ | --------- | ------ | +| `propName` | `type` | `default` | {note} | + +> **{Non-obvious gotcha title}**: {explanation of surprising behavior, constraint, or invariant}. +> Only add this block when the behavior would genuinely surprise a reader. + +## Basic usage + +\`\`\`tsx +// {describe what this shows} + +\`\`\` + +## {Pattern name} (repeat per pattern) + +\`\`\`tsx +// correct + + +// wrong — {reason} + +\`\`\` + +## Accessibility + +{Only if the component has ARIA or focus concerns beyond the universal rules.} + +Do not repeat content from [`accessibility.mdc`](../../rules/accessibility.mdc) — link to it instead. +``` + +--- + +## Token and context guidance + +The `description` field is loaded on every skill invocation — token waste here is paid on every use. + +| Principle | Rule | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Reference, don't repeat | Point to `accessibility.mdc`, related skills, or Storybook rather than copying content | +| Tables > prose | Use tables for prop references, option lists, and decision trees | +| Front-load | Lead each section with the most common case; edge cases last | +| Flat structure | Max one level of H3 subsections under each H2 | +| Line target | 100–180 lines for a component skill; 200–400 for a workflow skill | +| Gotcha blocks | Only for things that would genuinely surprise a reader (layout behavior, type constraints, focus side-effects) | +| Code examples | Language marker required (`tsx`, `ts`, `jsx`); show both correct and incorrect when the wrong pattern is common | +| Semantic tokens | All code examples use semantic color tokens (`color="text"`, `bg="background"`) — never hardcoded hex or raw palette names | +| No AI-speak | Write imperatively. No "this skill will help you…", "certainly", or meta-commentary | + +--- + +## Consistency checklist + +Run this against your draft before committing: + +- [ ] `name` field matches directory name exactly (kebab-case) +- [ ] `description` starts with "Use this skill when" and lists ≥2 concrete trigger scenarios +- [ ] H1 title is present and matches skill purpose +- [ ] `See also:` block with ≥1 related skill and a one-line reason +- [ ] ≥1 Storybook link using a full `gamut.codecademy.com` URL +- [ ] All code blocks have a language marker +- [ ] "When to use" has both positive cases and a "Do not use when" sub-list +- [ ] No hardcoded hex (`#...`) or raw palette names in code examples +- [ ] No duplication of `accessibility.mdc` — link to it instead +- [ ] Props table includes: name, type, default, notes +- [ ] Non-obvious gotchas have a callout block +- [ ] No trailing "See also" list that duplicates the top `See also:` block +- [ ] Source code GitHub link present +- [ ] Draft reviewed with `/writing-great-skills` before committing + +--- + +## Storybook integration + +Both steps are required after writing the skill. + +### Step 1 — Add to Best practices list + +File: `packages/styleguide/src/lib/Meta/AI Tooling/Gamut plugin/Best practices.mdx` + +```md +- `gamut-{name}` — {one-line description, matching the skill's overview sentence} +``` + +Group the entry near related skills (e.g. new list/table skills go after `gamut-datalist`; new form skills go after `gamut-forms`). + +### Step 2 — Add Callout to component Storybook page + +File: the component's MDX (e.g. `packages/styleguide/src/lib/Organisms/.../ComponentName.mdx`) + +If `Callout` is not yet imported, add it to the `~styleguide/blocks` import: + +```diff +- import { ComponentHeader } from '~styleguide/blocks'; ++ import { Callout, ComponentHeader } from '~styleguide/blocks'; +``` + +Place the callout immediately after `` or ``: + +```mdx + +``` + +For skills covering a concept rather than a single component (e.g. `gamut-color-mode`, `gamut-layout`), add the callout to the primary concept page (`ColorMode.mdx`, `Layout.mdx`, etc.) — not every page that tangentially touches the concept. + +--- + +## Reference skills by type + +| Type | Skill | Read it for | +| ---------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------- | +| Tight component | [`gamut-buttons`](../../packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md) | Scope discipline, clean "When to use / not use" | +| Organism with full API | [`gamut-datatable`](../../packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md) | Props table + ColumnConfig + gotcha note pattern | +| Audit / workflow | [`gamut-review`](../../packages/gamut/agent-tools/skills/gamut-review/SKILL.md) | Multi-phase check structure, remediation pointers | +| System / concept | [`gamut-color-mode`](../../packages/gamut/agent-tools/skills/gamut-color-mode/SKILL.md) | Semantic-first framing, cross-skill boundaries | diff --git a/package.json b/package.json index c09a7f053d9..a5ee7f4dfec 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,8 @@ "prettier": "prettier --ignore-path .prettierignore \"./**/*.{mdx,js,ts,tsx,json,css,scss}\"", "start": "yarn && yarn start:storybook", "start:storybook": "nx storybook styleguide", + "sync-skills": "node scripts/sync-local-skills.mjs", + "sync-skills:watch": "onchange 'local-skills/**' -- node scripts/sync-local-skills.mjs", "test": "nx run-many --target=test --all", "test:storybook": "nx run styleguide:storybook-test", "verify": "nx run-many --target=verify --parallel=3 --all", diff --git a/packages/gamut/agent-tools/skills/gamut-accessibility/SKILL.md b/packages/gamut/agent-tools/skills/gamut-accessibility/SKILL.md index 0bbf5a116ad..0b95afe1136 100644 --- a/packages/gamut/agent-tools/skills/gamut-accessibility/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-accessibility/SKILL.md @@ -1,6 +1,6 @@ --- name: gamut-accessibility -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. +description: Use this skill when implementing accessibility for a specific Gamut component, building a custom overlay or composite widget, or auditing component usage against WCAG — complements the always-loaded `accessibility.mdc` with Gamut component-specific patterns. Form wiring lives in `gamut-forms`. --- # Gamut Accessibility diff --git a/packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md b/packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md index 18a5e0825df..294065ac78d 100644 --- a/packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-buttons/SKILL.md @@ -5,7 +5,7 @@ description: Use this skill when choosing Gamut button atoms (FillButton, Stroke # Gamut Buttons -Which button component and which `variant` to use. Colors are wired inside each atom — consumers do not pass `color`, `bg`, hex, or semantic token names on stock buttons. +Which button component and which `variant` to use. See also: [`gamut-color-mode`](../gamut-color-mode/SKILL.md) — semantic tokens for custom styled controls only, not stock button atoms; ColorMode / `` when placing buttons on colored surfaces. [`gamut-accessibility`](../gamut-accessibility/SKILL.md) — universal action and naming rules. @@ -89,57 +89,7 @@ Hover, active, and disabled colors are handled by the component. Do not override ## Focus management — buttons with ToolTip -`ToolTip` opens on **hover or focus** and closes when neither is active. The two rendering paths behave differently: - -- **Inline (default):** CSS-only via `:hover` and `:focus-within` on the wrapper. No JS involved; tooltip visibility tracks pointer and focus state automatically. -- **Floating (`placement="floating"`):** JS-driven. Tracks hover (`mouseenter`/`mouseleave`) and focus (`focus`/`blur`) separately with a small delay on each. Escape key always closes the tooltip by calling `.blur()` on the trigger automatically. - -**When the tooltip lingers after a click:** This only occurs with `FloatingTip` when the button was **keyboard-focused before the click**. `FloatingTip` keeps an `isFocused` flag; while that flag is true, `mouseleave` does not close the tooltip. If the click action does not naturally move DOM focus elsewhere, the button stays focused and the tooltip stays open. - -Mouse-initiated clicks do not have this problem: `TargetContainer` has `onMouseDown={(e) => e.preventDefault()}` which prevents the button from gaining focus via mouse, so `isFocused` stays `false` and the tooltip closes when the pointer moves away. - -**Pattern — explicit blur when focus won't move naturally:** - -```tsx -const handleClick = () => { - (document.activeElement as HTMLElement)?.blur(); - openPanel(); -}; - -; -``` - -Or with a ref: - -```tsx -const ref = useRef(null); - - { - ref.current?.blur(); - openPanel(); - }} -/>; -``` - -**When to apply (floating placement, keyboard-triggered clicks only):** - -- Click opens a modal, drawer, or panel that does NOT auto-focus an element inside it -- Click triggers an in-place state toggle (e.g. show/hide inline editor) -- Click dispatches a mutation with no focus side-effect - -**When NOT needed:** - -- Click opens a modal with a proper focus trap — the trap moves focus automatically, blurring the button -- Click navigates to a new route — component unmounts -- Click reveals a `Popover` or `FloatingTip`-managed dropdown — focus is moved by that system -- Tooltip uses the default inline (non-floating) placement — CSS handles visibility, no lingering issue -- User pressed Escape — built-in `escapeKeyPressHandler` already calls `.blur()` - -Call `.blur()` synchronously before the action; this keeps tooltip dismissal atomic with the user interaction. +When a button uses `placement="floating"` and the click does not naturally move DOM focus elsewhere, the tooltip may linger after a keyboard-triggered click. See [TOOLTIP_FOCUS.md](./TOOLTIP_FOCUS.md) for the pattern, when to apply `.blur()`, and when it is not needed. ## Rules diff --git a/packages/gamut/agent-tools/skills/gamut-buttons/TOOLTIP_FOCUS.md b/packages/gamut/agent-tools/skills/gamut-buttons/TOOLTIP_FOCUS.md new file mode 100644 index 00000000000..82cbc5f3b51 --- /dev/null +++ b/packages/gamut/agent-tools/skills/gamut-buttons/TOOLTIP_FOCUS.md @@ -0,0 +1,55 @@ +# ToolTip focus management — buttons with floating placement + +`ToolTip` opens on hover or focus and closes when neither is active. Two rendering paths: + +- **Inline (default):** CSS-only via `:hover` and `:focus-within`. No JS involved; tooltip tracks pointer and focus automatically. +- **Floating (`placement="floating"`):** JS-driven. Tracks hover (`mouseenter`/`mouseleave`) and focus (`focus`/`blur`) separately with a small delay. Escape key calls `.blur()` on the trigger automatically. + +## When the tooltip lingers after a click + +Only occurs with `FloatingTip` when the button was **keyboard-focused before the click**. `FloatingTip` keeps an `isFocused` flag; while true, `mouseleave` does not close the tooltip. If the click action does not naturally move DOM focus elsewhere, the button stays focused and the tooltip stays open. + +Mouse-initiated clicks do not have this problem: `TargetContainer` has `onMouseDown={(e) => e.preventDefault()}` which prevents the button from gaining focus via mouse, so `isFocused` stays `false` and the tooltip closes when the pointer moves away. + +## Pattern — explicit blur when focus won't move naturally + +```tsx +const handleClick = () => { + (document.activeElement as HTMLElement)?.blur(); + openPanel(); +}; + +; +``` + +Or with a ref: + +```tsx +const ref = useRef(null); + + { + ref.current?.blur(); + openPanel(); + }} +/>; +``` + +Call `.blur()` synchronously before the action; this keeps tooltip dismissal atomic with the user interaction. + +## When to apply (floating placement, keyboard-triggered clicks only) + +- Click opens a modal, drawer, or panel that does NOT auto-focus an element inside it +- Click triggers an in-place state toggle (e.g. show/hide inline editor) +- Click dispatches a mutation with no focus side-effect + +## When NOT needed + +- Click opens a modal with a proper focus trap — the trap moves focus automatically, blurring the button +- Click navigates to a new route — component unmounts +- Click reveals a `Popover` or `FloatingTip`-managed dropdown — focus is moved by that system +- Tooltip uses the default inline (non-floating) placement — CSS handles visibility, no lingering issue +- User pressed Escape — built-in `escapeKeyPressHandler` already calls `.blur()` diff --git a/packages/gamut/agent-tools/skills/gamut-color-mode/SKILL.md b/packages/gamut/agent-tools/skills/gamut-color-mode/SKILL.md index 6c4adbb0d4d..0a982fc8c09 100644 --- a/packages/gamut/agent-tools/skills/gamut-color-mode/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-color-mode/SKILL.md @@ -122,102 +122,15 @@ const forced = useCurrentMode('light'); const prefersDark = usePrefersDarkMode(); ``` -## Decision guide - -| Need | Use | -| ------------------------------------------------------------- | ---------------------------------- | ---- | --------- | -| Set a page or section to a specific mode | `` | -| Place content on a colored background with automatic contrast | `` | -| Read the current mode in JavaScript | `useCurrentMode()` | -| Access all modes, variables, and resolve raw colors | `useColorModes()` | -| Detect OS dark mode preference | `usePrefersDarkMode()` | -| Access full emotion theme | `useTheme()` from `@emotion/react` | - ## Common mistakes to avoid - 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. - Do not use a raw `bg` prop for colored section backgrounds that need static, ColorMode-agnostic, background colors — use `` so mode selection is handled for you. - 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). -## Semantic aliases (theme-stable names) - -These tokens describe roles. Actual colors come from the active theme + ColorMode. Never assume Codecademy Core hex when advising another product. - -### Text - -| Token | Use for | -| ---------------- | --------------------------- | -| `text` | Default body and UI text | -| `text-accent` | Stronger emphasis text | -| `text-secondary` | Supporting / secondary copy | -| `text-disabled` | Disabled state labels | - -### Background - -| Token | Use for | -| --------------------- | --------------------------------- | -| `background` | Default page/component background | -| `background-primary` | Slightly elevated surfaces | -| `background-contrast` | Maximum contrast surface | -| `background-selected` | Selected row / item | -| `background-hover` | Hover state overlay | -| `background-disabled` | Disabled surface | -| `background-success` | Success state container | -| `background-warning` | Warning state container | -| `background-error` | Error state container | - -### Interactive - -| Token | Use for | -| ----------------- | ----------------------------------------- | -| `primary` | Primary CTA, links, focus accents | -| `primary-hover` | Hover on primary interactive | -| `primary-inverse` | Accent on top of primary-colored surfaces | -| `secondary` | Secondary CTA, ghost buttons | -| `secondary-hover` | Hover on secondary interactive | -| `danger` | Destructive actions, error emphasis | -| `danger-hover` | Hover on danger interactive | - -### Border - -| Token | Use for | -| ------------------ | -------------------------- | -| `border-primary` | Strong borders, dividers | -| `border-secondary` | Medium-weight borders | -| `border-tertiary` | Subtle borders, separators | -| `border-disabled` | Disabled input borders | - -### Feedback - -| Token | Use for | -| ------------------ | -------------------------- | -| `feedback-error` | Error messages, validation | -| `feedback-success` | Success messages | -| `feedback-warning` | Warning messages | - -## Where resolved colors are documented - -- Storybook [ColorMode](https://gamut.codecademy.com/?path=/docs-foundations-colormode--page) -- [Core](https://gamut.codecademy.com/?path=/docs-foundations-theme-core-theme--docs) · [Admin](https://gamut.codecademy.com/?path=/docs-foundations-theme-admin-theme--docs) · [Platform](https://gamut.codecademy.com/?path=/docs-foundations-theme-platform-theme--docs) · [Percipio](https://gamut.codecademy.com/?path=/docs-foundations-theme-percipio-theme--docs) · [LX Studio](https://gamut.codecademy.com/?path=/docs-foundations-theme-lx-studio-theme--docs) theme pages -- Root `DESIGN.md` from agent-tools (`DESIGN.Codecademy.md`, `DESIGN.Percipio.md`, `DESIGN.LXStudio.md`) -- Source: [`packages/gamut-styles/src/themes`](https://github.com/Codecademy/gamut/tree/main/packages/gamut-styles/src/themes) - -## Codecademy Core — illustrative light/dark hex only - -Not valid for Percipio, LX Studio, or other themes. Quick mental model for Core defaults only. - -| Token | Light | Dark | -| ---------------- | --------- | --------- | -| `text` | `#10162F` | `#ffffff` | -| `text-accent` | `#0A0D1C` | `#FFF0E5` | -| `background` | `#ffffff` | `#10162F` | -| `primary` | `#3A10E5` | `#FFD300` | -| `primary-hover` | `#5533FF` | `#CCA900` | -| `secondary` | `#10162F` | `#ffffff` | -| `danger` | `#E91C11` | `#E85D7F` | -| `feedback-error` | `#BE1809` | `#E85D7F` | - -Full tables: Storybook theme pages or audit with [`gamut-review`](../gamut-review/SKILL.md) Appendix A/B for hex triage. +## Semantic aliases + +These tokens describe roles; actual colors come from the active theme + ColorMode. Full alias tables: [Foundations / ColorMode](https://gamut.codecademy.com/?path=/docs-foundations-colormode--page) — and per-theme breakdowns at [Core](https://gamut.codecademy.com/?path=/docs-foundations-theme-core-theme--docs) · [Admin](https://gamut.codecademy.com/?path=/docs-foundations-theme-admin-theme--docs) · [Platform](https://gamut.codecademy.com/?path=/docs-foundations-theme-platform-theme--docs) · [Percipio](https://gamut.codecademy.com/?path=/docs-foundations-theme-percipio-theme--docs) · [LX Studio](https://gamut.codecademy.com/?path=/docs-foundations-theme-lx-studio-theme--docs). Source: [`packages/gamut-styles/src/themes`](https://github.com/Codecademy/gamut/tree/main/packages/gamut-styles/src/themes). ## Raw palette (Core-centric reference) diff --git a/packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md b/packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md index bdbe3ff4be1..a106149ca57 100644 --- a/packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-datatable/SKILL.md @@ -224,40 +224,7 @@ Key points: ## Empty state -DataTable shows a default empty state when `rows` is empty. Override with `emptyMessage`. - -```tsx - - - - No results found. - - - - } -/> -``` +DataTable shows a default empty state when `rows` is empty. Override with `emptyMessage`. For the `tbody > tr > th` layout pattern required for valid table semantics, see [`gamut-datalist` — Empty state](../gamut-datalist/SKILL.md#empty-state). ## Loading state @@ -281,22 +248,7 @@ import { Background } from '@codecademy/gamut-styles'; ## Container queries -DataTable uses CSS container queries by default for responsive column stacking. Disable only when: - -- The table lives in a container narrower than its breakpoint. -- You are managing your own responsive logic. - -```tsx -
- -
-``` +DataTable uses CSS container queries by default for responsive column stacking. Pass `disableContainerQuery` when the table lives in a constrained container or you are managing your own responsive logic — same pattern as [`gamut-datalist`](../gamut-datalist/SKILL.md#container-queries). ## Accessibility diff --git a/packages/gamut/agent-tools/skills/gamut-forms/SKILL.md b/packages/gamut/agent-tools/skills/gamut-forms/SKILL.md index ea61d364efa..12cbee94e49 100644 --- a/packages/gamut/agent-tools/skills/gamut-forms/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-forms/SKILL.md @@ -1,6 +1,6 @@ --- name: gamut-forms -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. +description: Use this skill when 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. --- # Gamut forms diff --git a/packages/gamut/agent-tools/skills/gamut-layout/SKILL.md b/packages/gamut/agent-tools/skills/gamut-layout/SKILL.md index bc342cf577d..79cb8522995 100644 --- a/packages/gamut/agent-tools/skills/gamut-layout/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-layout/SKILL.md @@ -1,6 +1,6 @@ --- name: gamut-layout -description: Use this skill when applying Gamut spacing scale, border radii, viewport or container breakpoints, screen sizes, responsive layouts, media queries, or page layout grid (LayoutGrid vs GridBox) — including migrating breakpoint or screen-size logic, responsive prop patterns, useWindowSize / useBreakpoint hooks, and mobile-first design. Complements gamut-system-props for system.space and responsive props. +description: Use this skill when applying Gamut spacing scale, border radii, responsive breakpoints (viewport or container), or the page layout grid (LayoutGrid vs GridBox) — including responsive prop patterns and mobile-first design. Complements gamut-system-props for system.space and responsive props. --- # Gamut Layout diff --git a/packages/gamut/agent-tools/skills/gamut-review/SKILL.md b/packages/gamut/agent-tools/skills/gamut-review/SKILL.md index 49c9152dfbf..ae46627ba5b 100644 --- a/packages/gamut/agent-tools/skills/gamut-review/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-review/SKILL.md @@ -1,6 +1,6 @@ --- name: gamut-review -description: Use this skill when auditing existing code for Gamut usage — dependencies, GamutProvider, deep imports, SCSS modules, className on Gamut components, nested selectors, hardcoded hex colors, non-Gamut CSS variables, and test patterns — and you need a consolidated report with pointers to matching Gamut skills. +description: Use this skill when auditing existing code for Gamut usage and you need a consolidated report — checks dependencies, setup, import patterns, hardcoded colors, and test setup, with pointers to remediation skills. --- # Gamut Review diff --git a/packages/gamut/agent-tools/skills/gamut-style-utilities/SKILL.md b/packages/gamut/agent-tools/skills/gamut-style-utilities/SKILL.md index 1e6c3cd058d..01d51fe6668 100644 --- a/packages/gamut/agent-tools/skills/gamut-style-utilities/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-style-utilities/SKILL.md @@ -7,7 +7,7 @@ description: Use this skill when authoring Gamut styles with @codecademy/gamut-s Source: `@codecademy/gamut-styles` — [`variance/props.ts`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-styles/src/variance/props.ts) (`css`, `variant`, `states` built on `PROPERTIES.all`). -See also: [`gamut-theming`](../gamut-theming/SKILL.md) (which theme, `GamutProvider`, new themes). [`gamut-system-props`](../gamut-system-props/SKILL.md) (`system.*`, responsive props, `Box`). [`gamut-color-mode`](../gamut-color-mode/SKILL.md) (semantic color, ``, ``). [Best practices](../../../../styleguide/src/lib/Meta/Best%20practices.mdx) and [system compose](https://gamut.codecademy.com/?path=/docs-foundations-system-compose--page). +See also: [`gamut-theming`](../gamut-theming/SKILL.md) (which theme, `GamutProvider`, new themes). [`gamut-system-props`](../gamut-system-props/SKILL.md) (`system.*`, responsive props, `Box`). [`gamut-color-mode`](../gamut-color-mode/SKILL.md) (semantic color, ``, ``). [Best practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page) and [system compose](https://gamut.codecademy.com/?path=/docs-foundations-system-compose--page). ## Overview @@ -99,9 +99,3 @@ const Sparkline = () => { return ; }; ``` - -## Key principles - -- Prefer semantic color keys in `css` / `variant` / `states` so ColorMode and theme switches apply; see `gamut-color-mode`. -- Never hardcode hex in component styles — use tokens / semantic aliases. -- Prefer `variant` / `states` for modes and toggles instead of ad-hoc `theme` interpolation in template literals. diff --git a/packages/gamut/agent-tools/skills/gamut-system-props/SKILL.md b/packages/gamut/agent-tools/skills/gamut-system-props/SKILL.md index fb9d102b46c..d0e20dc2ca6 100644 --- a/packages/gamut/agent-tools/skills/gamut-system-props/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-system-props/SKILL.md @@ -1,13 +1,13 @@ --- name: gamut-system-props -description: Use this skill when building or refactoring styled Gamut components that need layout, spacing, color, border, background, typography, positioning, grid, flex, shadow, list styles, or responsive values from @codecademy/gamut-styles — including composing system prop groups with variance. +description: 'Use this skill when composing system prop groups (`system.*`) on styled components, selecting which group covers a CSS property, or building responsive props with `variance.compose()` — not for `css()`, `variant()`, or `states()` (see gamut-style-utilities).' --- # Gamut System Props Source: `@codecademy/gamut-styles` — [`variance/config.ts`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-styles/src/variance/config.ts) (definitions) and [`variance/props.ts`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-styles/src/variance/props.ts) (`variance.create` groups). `Box`, `FlexBox`, and `GridBox` compose the same groups in [`packages/gamut/src/Box/props.ts`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/Box/props.ts). -See also: [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md) (`css`, `variant`, `states`, `StyleProps`). [Styleguide — Best practices](../../../../styleguide/src/lib/Meta/Best%20practices.mdx) (semantic colors, responsive examples) and Storybook [Responsive properties](https://gamut.codecademy.com/storybook/?path=/docs-foundations-system-responsive-properties--page). +See also: [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md) (`css`, `variant`, `states`, `StyleProps`). [Styleguide — Best practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page) (semantic colors, responsive examples) and Storybook [Responsive properties](https://gamut.codecademy.com/storybook/?path=/docs-foundations-system-responsive-properties--page). ## Overview @@ -192,12 +192,3 @@ const Box = styled.div(css({ bg: 'navy-400', p: 4 })); // Semantic color (adapts to color mode) const Text = styled.div(css({ color: 'primary', p: 4 })); ``` - -## Key principles - -- Compose `system.*` groups via `variance.compose()` — don't apply multiple groups by chaining `styled.div(system.a)(system.b)`. -- Prefer semantic color names on `system.color` (e.g. `bg="background"`, `textColor="text"`) so values track ColorMode; use raw palette keys only when the design should stay fixed across modes. -- Use `bg` with semantic tokens for most mode-aware surfaces; use `` from `@codecademy/gamut-styles` when you need its contrast- and mode-aware behavior, not for every tinted panel. -- Use `system.space` values on the spacing scale rather than arbitrary pixel strings to keep rhythm consistent. -- For background images/patterns use `system.background`; for solid fills use `system.color` / semantic `bg` (or `Background` when that component’s behavior is required). -- For reusable variants or boolean states on styled primitives, use `variant` / `states` from `@codecademy/gamut-styles` and expose props with `StyleProps` from `@codecademy/variance` — see [Best practices](../../../../styleguide/src/lib/Meta/Best%20practices.mdx). diff --git a/packages/gamut/agent-tools/skills/gamut-testing/SKILL.md b/packages/gamut/agent-tools/skills/gamut-testing/SKILL.md index 125dfc357bf..be092a75c21 100644 --- a/packages/gamut/agent-tools/skills/gamut-testing/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-testing/SKILL.md @@ -9,8 +9,6 @@ Source: `@codecademy/gamut-tests` — [`index.tsx`](https://github.com/Codecadem --- ---- - ## What `MockGamutProvider` does (under `setupRtl`) `MockGamutProvider` forwards to `GamutProvider` with: diff --git a/packages/gamut/agent-tools/skills/gamut-theming/SKILL.md b/packages/gamut/agent-tools/skills/gamut-theming/SKILL.md index 57ffe9899ce..3c5fe587c25 100644 --- a/packages/gamut/agent-tools/skills/gamut-theming/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-theming/SKILL.md @@ -106,10 +106,3 @@ See Emotion’s [TypeScript / define a theme](https://emotion.sh/docs/typescript ## Creating a new theme See [Creating Themes](https://gamut.codecademy.com/?path=/docs-foundations-theme-creating-themes--docs) in Storybook. Themes are defined in `@codecademy/gamut-styles` and must extend the base theme shape with all required token keys. - -## Key principles - -- Pick the correct theme export for the product so tokens and fonts match design intent. -- Align `theme.d.ts` / `Theme extends …` with the same theme interface you pass to `GamutProvider`. -- Components stay portable across themes when they use token and semantic aliases rather than one-off hex; authoring rules live in `gamut-style-utilities` and `gamut-color-mode`. -- `GamutProvider` wires theme, color mode, and logical-properties settings at the root; individual components should not hard-code which org theme is active. diff --git a/packages/gamut/agent-tools/skills/gamut-typography/SKILL.md b/packages/gamut/agent-tools/skills/gamut-typography/SKILL.md index 4594569bd02..f568695072f 100644 --- a/packages/gamut/agent-tools/skills/gamut-typography/SKILL.md +++ b/packages/gamut/agent-tools/skills/gamut-typography/SKILL.md @@ -1,6 +1,6 @@ --- name: gamut-typography -description: Use this skill when creating or reviewing UI text in Gamut apps — headlines, body, captions, labels, code snippets, or text-heavy layouts. Covers theme-specific stacks (Core Apercu/Suisse vs Percipio/LX Skillsoft), fontSize / lineHeight tokens, semantic fontWeight title (700 vs 500), line length, and alignment for Codecademy-branded surfaces. +description: Use this skill when applying Gamut typography tokens — font family, font size, line height, or semantic font weight (including the `fontWeight="title"` difference between Core 700 and Percipio/LX 500). See `gamut-system-props` for system.typography props. --- # Gamut Typography diff --git a/packages/styleguide/src/lib/Meta/AI Tooling/Gamut plugin/Best practices.mdx b/packages/styleguide/src/lib/Meta/AI Tooling/Gamut plugin/Best practices.mdx index caa15b11332..97bf5ee641a 100644 --- a/packages/styleguide/src/lib/Meta/AI Tooling/Gamut plugin/Best practices.mdx +++ b/packages/styleguide/src/lib/Meta/AI Tooling/Gamut plugin/Best practices.mdx @@ -79,7 +79,9 @@ Without `DESIGN.md` at the repo root, color findings are lower confidence — in ## Agent skills -Related skills under [`packages/gamut/agent-tools/skills/`](https://github.com/Codecademy/gamut/tree/main/packages/gamut/agent-tools/skills/): +### Exported skills + +Installed into app repos via `gamut plugin install`. Source: [`packages/gamut/agent-tools/skills/`](https://github.com/Codecademy/gamut/tree/main/packages/gamut/agent-tools/skills/) - `gamut-review` — codebase audit playbook - `gamut-theming` — theme selection, `GamutProvider`, `theme.d.ts` @@ -92,7 +94,12 @@ Related skills under [`packages/gamut/agent-tools/skills/`](https://github.com/C - `gamut-system-props` — `system.*` / `Box` - `gamut-style-utilities` — `css`, `variant`, `states` - `gamut-typography`, `gamut-forms`, `gamut-accessibility`, `gamut-testing` -- `gamut-create-skill` — workflow playbook for authoring a new Gamut agent skill from scratch + +### Contributor-only skills + +Available in this repo only — never exported. Source: [`local-skills/`](https://github.com/Codecademy/gamut/tree/main/local-skills/). Auto-synced to `.claude/skills/` and `.cursor/rules/` via the pre-commit hook; sync manually with `yarn sync-skills`. + +- `gamut-create-skill` — blueprint playbook for authoring a new Gamut agent skill from scratch ## Design-to-code (Figma MCP) diff --git a/scripts/sync-local-skills.mjs b/scripts/sync-local-skills.mjs new file mode 100644 index 00000000000..01eb4368c4f --- /dev/null +++ b/scripts/sync-local-skills.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +/* Syncs local-skills/ → .claude/skills/ and .cursor/rules/ + * + * Usage: + * node scripts/sync-local-skills.mjs # write/remove generated files + * node scripts/sync-local-skills.mjs --check # exit 1 if generated files are stale + */ +import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const SOURCE_DIR = join(ROOT, 'local-skills'); +const CLAUDE_SKILLS_DIR = join(ROOT, '.claude', 'skills'); +const CURSOR_RULES_DIR = join(ROOT, '.cursor', 'rules'); + +const isCheck = process.argv.includes('--check'); + +async function readSkills() { + const entries = await readdir(SOURCE_DIR, { withFileTypes: true }); + const results = await Promise.all( + entries + .filter((e) => e.isDirectory()) + .map(async (entry) => { + const content = await readFile( + join(SOURCE_DIR, entry.name, 'SKILL.md'), + 'utf8' + ).catch(() => null); + return content !== null ? { name: entry.name, content } : null; + }) + ); + return results.filter(Boolean); +} + +async function checkFile(path, expected) { + const actual = await readFile(path, 'utf8').catch(() => null); + return actual === expected; +} + +async function main() { + const skills = await readSkills(); + const activeNames = new Set(skills.map((s) => s.name)); + + // Orphans: directories in .claude/skills/ with no matching local-skills/ source + const claudeEntries = await readdir(CLAUDE_SKILLS_DIR, { + withFileTypes: true, + }).catch(() => []); + const orphans = claudeEntries + .filter((e) => e.isDirectory() && !activeNames.has(e.name)) + .map((e) => e.name); + + if (isCheck) { + const staleArrays = await Promise.all( + skills.map(async ({ name, content }) => { + const claudeDest = join(CLAUDE_SKILLS_DIR, name, 'SKILL.md'); + const cursorDest = join(CURSOR_RULES_DIR, `${name}.mdc`); + const [claudeOk, cursorOk] = await Promise.all([ + checkFile(claudeDest, content), + checkFile(cursorDest, content), + ]); + const stale = []; + if (!claudeOk) stale.push(claudeDest); + if (!cursorOk) stale.push(cursorDest); + return stale; + }) + ); + + const stale = [ + ...staleArrays.flat(), + ...orphans.flatMap((name) => [ + join(CLAUDE_SKILLS_DIR, name, 'SKILL.md'), + join(CURSOR_RULES_DIR, `${name}.mdc`), + ]), + ]; + + if (stale.length > 0) { + console.error( + 'Stale generated files (run `yarn sync-skills` to update):' + ); + stale.forEach((f) => console.error(` ${f}`)); + process.exit(1); + } else { + console.log('All generated skill files are up to date.'); + } + return; + } + + await Promise.all( + orphans.map((name) => + Promise.all([ + rm(join(CLAUDE_SKILLS_DIR, name), { recursive: true, force: true }), + rm(join(CURSOR_RULES_DIR, `${name}.mdc`), { force: true }), + ]).then(() => console.log(`removed: ${name}`)) + ) + ); + + await Promise.all( + skills.map(async ({ name, content }) => { + const claudeDest = join(CLAUDE_SKILLS_DIR, name, 'SKILL.md'); + const cursorDest = join(CURSOR_RULES_DIR, `${name}.mdc`); + await mkdir(dirname(claudeDest), { recursive: true }); + await Promise.all([ + writeFile(claudeDest, content, 'utf8'), + writeFile(cursorDest, content, 'utf8'), + ]); + console.log(`synced: ${name}`); + }) + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});