@codecademy/gamut

71.0.071.0.1-alpha.c0c413.0
~

Modified (7 files)

Index: package/package.json
===================================================================
--- package/package.json
+++ package/package.json
@@ -1,16 +1,16 @@
 {
   "name": "@codecademy/gamut",
   "description": "Styleguide & Component library for Codecademy",
-  "version": "71.0.0",
+  "version": "71.0.1-alpha.c0c413.0",
   "author": "Codecademy Engineering <[email protected]>",
   "bin": "./bin/gamut.mjs",
   "dependencies": {
-    "@codecademy/gamut-icons": "9.57.7",
-    "@codecademy/gamut-illustrations": "0.58.13",
-    "@codecademy/gamut-patterns": "0.10.32",
-    "@codecademy/gamut-styles": "20.0.0",
-    "@codecademy/variance": "0.26.1",
+    "@codecademy/gamut-icons": "9.57.8-alpha.c0c413.0",
+    "@codecademy/gamut-illustrations": "0.58.14-alpha.c0c413.0",
+    "@codecademy/gamut-patterns": "0.10.33-alpha.c0c413.0",
+    "@codecademy/gamut-styles": "20.0.1-alpha.c0c413.0",
+    "@codecademy/variance": "0.26.2-alpha.c0c413.0",
     "@formatjs/intl-locale": "5.3.1",
     "@react-aria/interactions": "3.25.0",
     "@types/marked": "^4.0.8",
     "@vidstack/react": "^1.12.12",
@@ -54,8 +54,9 @@
   "scripts": {
     "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": [
     "**/*.css",
Index: package/agent-tools/.claude-plugin/plugin.json
===================================================================
--- package/agent-tools/.claude-plugin/plugin.json
+++ package/agent-tools/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
 {
   "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"]
 }
Index: package/agent-tools/.cursor-plugin/plugin.json
===================================================================
--- package/agent-tools/.cursor-plugin/plugin.json
+++ package/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"]
 }
Index: package/agent-tools/skills/gamut-buttons/SKILL.md
===================================================================
--- package/agent-tools/skills/gamut-buttons/SKILL.md
+++ package/agent-tools/skills/gamut-buttons/SKILL.md
@@ -86,8 +86,62 @@
 - `href` + `disabled`: `ButtonBase` (internal) drops `href` and renders a `<button disabled>` — link-style buttons cannot stay anchors while disabled.
 - `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();
+};
+
+<IconButton icon={SettingsIcon} tip="Settings" onClick={handleClick} />;
+```
+
+Or with a ref:
+
+```tsx
+const ref = useRef<HTMLButtonElement>(null);
+
+<IconButton
+  ref={ref}
+  icon={SettingsIcon}
+  tip="Settings"
+  onClick={() => {
+    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.
 - Reserve `CTAButton` for marketing / high-visibility promotions; do not use it for standard UI actions.
Index: package/agent-tools/skills/gamut-layout/SKILL.md
===================================================================
--- package/agent-tools/skills/gamut-layout/SKILL.md
+++ package/agent-tools/skills/gamut-layout/SKILL.md
@@ -1,7 +1,7 @@
 ---
 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
Index: package/agent-tools/skills/gamut-review/SKILL.md
===================================================================
--- package/agent-tools/skills/gamut-review/SKILL.md
+++ package/agent-tools/skills/gamut-review/SKILL.md
@@ -1,7 +1,7 @@
 ---
 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
 
@@ -10,9 +10,9 @@
 When `DESIGN.md` is present at the audit root, use it as the authoritative reference for product design intent, token names, and component patterns. It is copied from `DESIGN.Codecademy.md`, `DESIGN.Percipio.md`, or `DESIGN.LXStudio.md` in `@codecademy/gamut` agent-tools (via `gamut plugin install --theme <name>`). When a finding maps to a skill, note it in the report so the developer knows where to get remediation guidance.
 
 Run Check 0 first, then Checks 1–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)
 
 ---
 
 ## Check 0 — DESIGN.md present
@@ -29,13 +29,14 @@
 ## Check 1 — Dependencies
 
 Read `package.json` at the audit root. Inspect `dependencies`, `devDependencies`, and `peerDependencies` combined.
 
-| Package                    | Expectation                                             |
-| -------------------------- | ------------------------------------------------------- |
-| `@codecademy/gamut`        | Required — core component library                       |
-| `@codecademy/gamut-styles` | Recommended — design tokens and theme primitives        |
-| `@codecademy/variance`     | Recommended — style-prop system used by Gamut internals |
+| Package                    | Expectation                                                                                                                                                                                                                                                                                                                                                                                                      |
+| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `@codecademy/gamut`        | Required — core component library                                                                                                                                                                                                                                                                                                                                                                                |
+| `@codecademy/gamut-styles` | Recommended — design tokens and theme primitives                                                                                                                                                                                                                                                                                                                                                                 |
+| `@codecademy/variance`     | Recommended — style-prop system used by Gamut internals                                                                                                                                                                                                                                                                                                                                                          |
+| `@codecademy/gamut-kit`    | Acceptable alternative meta-package — re-exports `@codecademy/gamut`, `@codecademy/gamut-styles`, `@codecademy/variance`, and more. Treat its presence as satisfying the three rows above; do not separately flag those packages as missing. **Caveat:** requires npm or yarn with `nodeLinker: node-modules`; not compatible with yarn Plug'n'Play. Flag as ⚠ warning if `.yarnrc.yml` shows `nodeLinker: pnp`. |
 
 ---
 
 ## Check 2 — Setup
@@ -52,8 +53,10 @@
 | `declare module '@emotion/react'` | **Required if TypeScript** — `Theme` must be augmented with the active theme type (e.g. `CoreTheme`) so scale props type-check correctly; grep `.d.ts` and `.ts`/`.tsx` source files for this string. **Recommended if not TypeScript** — note that adopting TypeScript is recommended and that `theme.d.ts` will be needed when it is. |
 
 For each found symbol report the first file path where it appears.
 
+**Conditional — `StyleProps` with `states()`/`variant()`**: If source files contain `states(` or `variant(` from `@codecademy/gamut-styles`, check whether component prop interfaces use `StyleProps<typeof ...>` from `@codecademy/variance`. When `states()`/`variant()` are present but `StyleProps` is absent from associated component interfaces, report as ⚠ warning: `StyleProps not used — state/variant props may be untyped`. Remediation: `import { StyleProps } from '@codecademy/variance'` and add `extends StyleProps<typeof myStates>` to the component interface. Skill reference: [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md).
+
 ---
 
 ## Check 3 — Import patterns
 
@@ -69,8 +72,110 @@
 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  <ComponentName className={...}>`.
+
+Severity note: `className` is not always forbidden — some Gamut components accept it for integration with third-party tools (e.g. passing a class to an external drag-and-drop library). Downgrade to ⚠ warning only when the usage is clearly an integration seam, not styling.
+
+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)
+
+---
+
+## Check 3c — Nested selectors
+
+Nested selectors inside styled-component or Emotion template literals cause hard-to-isolate side effects and make consistent updates difficult. The [Gamut Best Practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page) page flags two kinds as "at your own risk": tag selectors and Gamut component selectors.
+
+**Step 1 — Tag selectors**
+
+Grep source files (`.ts`, `.tsx`, `.js`, `.jsx`) for bare HTML tag names appearing as CSS selector lines inside styled-component or Emotion template literals. Skip `node_modules`, `dist`, `.next`, `build`, `.turbo`.
+
+- Pattern A (named tags):
+  ```
+  ^\s*(div|span|p|ul|li|ol|a|img|h[1-6]|table|thead|tbody|tr|td|th|form|section|header|footer|nav|main)\s*\{
+  ```
+- Pattern B (universal selector):
+  ```
+  ^\s*\*\s*\{
+  ```
+  False-positive risk: `*` appears in JSDoc comment bodies (` * {`). Post-filter matches where the line is a comment (starts with `//` or matches `\s*\*\s`). For remaining matches, verify context before marking as a violation.
+
+Do NOT include SVG primitive tag names (`path`, `rect`, `circle`, `line`, `polyline`, `svg`) — styled SVG primitives in icon and form components are normal and not the target of this rule.
+
+Exemptions (downgrade to `ℹ note`, not warning):
+
+- Files that import `Global` from `@emotion/react` — intentional global reset/injection stylesheets.
+- Files whose name matches `*reboot*`, `*reset*`, `*global*`, or `*base-styles*`.
+
+**Step 2 — Gamut component selectors**
+
+Rather than enumerating every Gamut component by name (brittle, misses new additions), scope to files that already import from `@codecademy/gamut`, then grep those files for any PascalCase identifier used as a CSS selector:
+
+1. Find files that contain `from '@codecademy/gamut'`.
+2. In those files, grep for:
+   ```
+   \$\{[A-Z][A-Za-z]+\}[^{]*\{
+   ```
+   This matches any `${PascalCaseName}` followed by a rule block — i.e., a component used as a CSS child selector.
+
+Each match means a component is being targeted from a parent styled wrapper rather than styled directly. Report as `file:line  ${ComponentName} { ... }`.
+
+Severity note: `&:pseudo ${ComponentName}` (pseudo-class combinator preceding the interpolation) is lower risk — downgrade those to ⚠ warning with a note to verify scope. Bare `${ComponentName} { }` selector blocks are the primary target.
+
+**Severity:** ⚠ warning for all matches (per Best Practices: "you may still do so, but at your own risk").
+
+**Remediation:**
+
+_Tag selectors_ — plain HTML elements do not need to become Gamut components. Two valid paths:
+
+- Use `Box`, `FlexBox`, or `GridBox` with the `as` prop to render as the intended element — no extra DOM node needed: `<Box as="section" p={16} color="text">`, `<FlexBox as="nav" gap={8}>`.
+- Style in place with `styled.div(css({ color: 'text', p: 16 }))` using semantic ColorMode tokens from `@codecademy/gamut-styles` — keeps the element but brings it into the design system token graph.
+
+Replace the parent's nested selector rule with one of the above and remove the selector block.
+
+_Gamut component selectors_ — pass system props directly to the component (`alignSelf`, `mt`, etc.) rather than targeting it from a parent wrapper. Where dynamic behavior spans multiple children, prefer `css()` with `variant()` or `states()` from `@codecademy/gamut-styles` keyed to data attributes or boolean props on the parent.
+
+Skill references: [`gamut-system-props`](../gamut-system-props/SKILL.md) · [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md)
+
+---
+
 ## Check 4 — Hardcoded colors (semantic-first)
 
 Rule: Inline hex literals in application UI code are violations. Remediation is not “replace hex with `navy-800`” — prefer semantic ColorMode tokens (`text`, `background`, `primary`, …) so light/dark and theme switches stay correct. Reserve raw palette tokens for colors that must stay fixed and for `bg` on `<Background>` from `@codecademy/gamut-styles` (section surfaces with content).
 
@@ -184,8 +289,29 @@
 | `#ca00d1` | `pink-800`                 |
 | `#006d82` | `teal-500` / `teal`        |
 | `#b3ccff` | `purple-300` / `purple`    |
 
+### Step 2 — Non-Gamut CSS custom properties (SCSS/CSS/Less files)
+
+After the hex scan, grep `.scss`, `.css`, and `.less` files for `var(--` occurrences. For each custom property name found, classify it:
+
+_Gamut-issued variables_ — skip these:
+
+- `--color-*` (ColorMode semantic aliases)
+- `--space*` (spacing scale)
+- `--font*` (font-size scale)
+- `--lineHeight*` (line-height scale)
+- `--borderWidth*` (border-width tokens)
+- `--fontFamily*` (font-family tokens)
+- `--fontWeight*` (font-weight tokens)
+
+_Non-Gamut variables_ — flag these:
+Any other name — especially camel-cased semantic names like `--darkNeutralColor`, `--whiteColor`, `--lightPrimaryColor`, `--colorNavy800`, `--borderGreyColor` — indicates a parallel token system (Skillsoft/Percipio globals, legacy design tokens, or ad-hoc project variables). These variables are NOT set by `GamutProvider`/`ColorMode` and will be undefined inside a Gamut-scoped tree unless the host shell also loads them.
+
+Severity: ✗ error for color-semantic variables (invisible in tests/Storybook without the host stylesheet); ⚠ warning for spacing/sizing variables that duplicate Gamut scale tokens.
+
+Reporting: count unique non-Gamut variable names and list the top offenders with frequency. Do not enumerate every call-site — just the variable names and usage counts. Suggest the nearest Gamut semantic alias where obvious (e.g. `--darkNeutralColor` → `--color-text`, `--whiteColor` → `--color-background`).
+
 ---
 
 ## Check 5 — Test setup
 
@@ -197,8 +323,9 @@
 | `jest.mock\(.*@codecademy/gamut-styles`               | Error                                 | Same issue as above — mocking gamut-styles breaks token resolution                                                                                                                                                                      |
 | `from '@codecademy/gamut-tests'`                      | Good — report count of files using it | Correct import for `setupRtl` and `MockGamutProvider`                                                                                                                                                                                   |
 | `from 'component-test-setup'` (without gamut-tests)   | Warning                               | Should import `setupRtl` from `@codecademy/gamut-tests`, not directly from `component-test-setup` — the gamut-tests wrapper adds `MockGamutProvider` automatically                                                                      |
 | `new GamutProvider` or `<GamutProvider` in test files | Warning                               | Prefer `setupRtl`; use `MockGamutProvider` (sets `useCache={false}`, `useGlobals={false}`) in harnesses or stories, not `GamutProvider` directly                                                                                        |
+| `jest.mock\(.*[Gg]amut[Pp]rovider`                    | Warning                               | Mocking any file whose path contains `GamutProvider` (including project-internal wrappers) strips Emotion/theme context; prefer `setupRtl` from `@codecademy/gamut-tests`                                                               |
 
 Skill reference for remediation: [`gamut-testing`](../gamut-testing/SKILL.md)
 
 ---
@@ -230,12 +357,29 @@
   ✗  Deep src imports          2 occurrences
        src/Thing.tsx:7
        src/Other.tsx:12
 
+SCSS modules & className                             [→ gamut-system-props] [→ gamut-style-utilities]
+  ✗  SCSS/CSS imports   14 files — migrate to system props and css()/variant()
+       src/components/Card/Card.scss
+       src/components/Nav/Nav.scss   (+ 12 more)
+  ✗  className on Gamut components   9 occurrences
+       src/components/Card/Card.tsx:14   <Box className={styles.wrapper}>
+       src/components/Nav/Nav.tsx:7      <Text className={styles.title}>
+
+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
   ✗  jest.mock(@codecademy/gamut)   2 occurrences — remove; prefer setupRtl (or harness + setupRtl)
Index: package/bin/lib/design.mjs
===================================================================
--- package/bin/lib/design.mjs
+++ package/bin/lib/design.mjs
@@ -1,5 +1,5 @@
-import { copyFile, stat } from 'node:fs/promises';
+import { readFile, stat, writeFile } from 'node:fs/promises';
 import { join } from 'node:path';
 
 /** @type {Record<string, { sourceFile: string, label: string }>} */
 const THEME_ALIASES = {
@@ -49,9 +49,9 @@
  * @param {{ force?: boolean }} [options]
  * @returns {Promise<{ dest: string, label: string }>}
  */
 export async function installDesignMd(sourceRoot, cwd, theme, options = {}) {
-  const { sourceFile, label } = resolveTheme(theme);
+  const { sourceFile, label, alias } = resolveTheme(theme);
   const src = join(sourceRoot, sourceFile);
   const dest = join(cwd, 'DESIGN.md');
 
   const srcStat = await stat(src).catch(() => null);
@@ -65,7 +65,17 @@
       `DESIGN.md already exists at ${dest}. Use --force to overwrite, or remove it first.`
     );
   }
 
-  await copyFile(src, dest);
+  const version = await readFile(join(sourceRoot, '..', 'package.json'), 'utf8')
+    .then((raw) => JSON.parse(raw).version ?? 'unknown')
+    .catch(() => 'unknown');
+
+  const header =
+    `<!-- Generated by @codecademy/gamut@${version}.\n` +
+    `     Do not edit this file directly — to update, re-run:\n` +
+    `     gamut plugin install --theme ${alias} --force -->\n`;
+
+  const content = await readFile(src, 'utf8');
+  await writeFile(dest, header + content, 'utf8');
   return { dest, label };
 }