Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .design-sync/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Design Sync Notes — Gamut

## Setup

- Package manager: yarn (hoisted node_modules at repo root)
- Node version: v22 (.nvmrc: v22.13.1)
- Shape: storybook — `.storybook` at `packages/styleguide/.storybook`
- DS entry: `packages/gamut/dist/index.js` (already compiled — barrel re-exports)
- node_modules for converter: pass `--node-modules /Users/cass/code/cc/gamut/node_modules` (react/react-dom hoisted to root)

## Storybook build — must use NX, not plain `npx storybook build`

`main.ts` loads `require.resolve('@nx/react/plugins/storybook')` which requires NX project context.
Plain `npx storybook build` fails with: `TypeError: Cannot read properties of undefined (reading 'data')` in `@nx/react/plugins/storybook/index.js`.

**Build command** (run from repo root):

```bash
yarn nx run styleguide:build-storybook --outputDir="$(pwd)/.design-sync/sb-reference"
```

## Fonts — CDN-served, not local

Fonts are served from `https://www.codecademy.com/gamut` (defined in `packages/gamut-styles/src/remoteAssets/fonts.ts`).
Fonts loaded dynamically via Emotion `@font-face` rules injected by `<Typography>` inside `GamutProvider`.

- Core theme: Apercu (regular, italic, bold, bold-italic) + Suisse (bold, regular)
- Other themes may have different font stacks

Config key: `runtimeFontPrefixes: ["https://www.codecademy.com/gamut"]`

## Dist patch — re-apply after `yarn build`

`packages/gamut/dist/Video/lib/VidstackPlayer.js` line 4: import changed from `.scss` to `.css` for esbuild compatibility.
`packages/gamut/dist/Video/styles/vds_base_theme.css` (empty): created alongside the `.scss` so the import resolves.

`Markdown` imports `Video` transitively, so both would fail without this patch.
After any `yarn build` re-run, re-apply:

```bash
sed -i '' "s|import '../styles/vds_base_theme.scss'|import '../styles/vds_base_theme.css' // patched for design-sync: .scss unsupported by esbuild|" packages/gamut/dist/Video/lib/VidstackPlayer.js
printf '/* @ds-css-runtime: vds_base_theme.scss patched to .css stub for esbuild compatibility */\n' > packages/gamut/dist/Video/styles/vds_base_theme.css
```

The `@ds-css-runtime` marker in the CSS stub is important: esbuild extracts the stub into `_ds_bundle.css`, and the validator recognises the marker as "CSS-in-JS, no compiled stylesheet" instead of failing with `[CSS_PLACEHOLDER]`.

## Re-sync risks

- Font CDN (`https://www.codecademy.com/gamut`) must be reachable during compare runs — storybook and previews both load fonts from there
- Storybook webpack aliases point at package `src/` directly (e.g. `@codecademy/gamut → packages/gamut/src`) — storybook builds TS source, not dist; converter bundles dist

## Known grade notes (2026-06-26 sync)

### tsconfig path

`cfgPath()` resolves relative to `PKG_DIR = packages/gamut`, not repo root.
Correct value: `"../../.design-sync/tsconfig.json"` (two levels up reaches repo root).
Three levels (`../../../`) goes above the repo and fails silently with "not found".

### ConnectedForm — duplicate story display name

ConnectedForm has two stories both named "Default" (different story IDs: `connectedform--default` and `connectedforminputs--default`).
Grade file can only hold one `"Default"` key — the second overwrote the first.
Graded: `Default → close` (story 1: valid form render, viewport width difference), `Watched Fields → match`.
The second "Default" story is an sb-error (auto-handled by compare, no grade entry needed).

### SkipToContent — blank-by-design

Component is visually hidden until focused; storybook and preview both render blank at rest.
Graded `match` with note — not a render failure.

### RadialProgress/Animating — animation capture artifact

Storybook captured a black canvas frame mid-animation; preview rendered at rest (blank).
Not a real render difference — animation is non-deterministic at capture time.
Graded `close` with note. Default and Children stories confirm the component renders correctly.

### Theme export — extraEntries fix

`@codecademy/gamut-styles` dist/index.js exports only `coreTheme as theme` (not named exports).
Added `@codecademy/gamut-styles/dist/themes` as a second extraEntry to expose all 5 named themes:
`coreTheme`, `adminTheme`, `platformTheme`, `lxStudioTheme`, `percipioTheme`.
Without this, `$ref: "coreTheme"` in the provider config resolved to `undefined`.
74 changes: 74 additions & 0 deletions .design-sync/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"projectId": "266cff67-7436-40c1-9899-2f4830adbc65",
"pkg": "@codecademy/gamut",
"shape": "storybook",
"storybookConfigDir": "packages/styleguide/.storybook",
"storybookStatic": ".design-sync/sb-reference",
"buildCmd": "yarn nx run-many --target=build --projects=variance,gamut-styles,gamut-icons,gamut-patterns,gamut-illustrations,gamut && sed -i '' \"s|import '../styles/vds_base_theme.scss'|import '../styles/vds_base_theme.css' // patched for design-sync|\" packages/gamut/dist/Video/lib/VidstackPlayer.js && printf '/* @ds-css-runtime: vds_base_theme.scss patched to .css stub for esbuild compatibility */\\n' > packages/gamut/dist/Video/styles/vds_base_theme.css",
"extraEntries": [
"@codecademy/gamut-styles",
"@codecademy/gamut-styles/dist/themes"
],
"readmeHeader": ".design-sync/conventions.md",
"runtimeFontPrefixes": ["https://www.codecademy.com/gamut"],
"tsconfig": "../../.design-sync/tsconfig.json",
"libOverrides": {
"bundle.mjs": "enable minification — icons-as-code push unminified bundle to 5.2MB, over the 5MB upload limit",
"source-storybook.mjs": "group components by Storybook's top-level sidebar category (atoms/molecules/organisms/layouts/typography) instead of the segment immediately preceding the component name — full nested paths were tried and reverted, they break emit.mjs's hardcoded 3-level asset paths",
"docs.mjs": "append a fixed semantic-color/no-hex/no-inline-style footer to every ingested component doc — the fast path (setup doc -> one component's .prompt.md -> build) skips guidelines/conventions.md, so this is the one reinforcement that path can't skip"
},
"titleMap": {
"Border": null,
"Color": null,
"Layout": null,
"Positioning": null,
"Space": null,
"Responsiveproperties": null,
"Button": null,
"Mini": null,
"Regular": null,
"Illustrations": null,
"Patterns": null
},
"overrides": {
"Card": { "cardMode": "column" },
"PopoverContainer": { "cardMode": "column" },
"Tag": { "cardMode": "column" },
"GridBox": { "cardMode": "column" },
"ConnectedFormGroup": { "cardMode": "column" },
"SubmitButton": { "cardMode": "column" },
"FormGroup": { "cardMode": "column" },
"Radio": { "cardMode": "column" },
"Pagination": { "cardMode": "column" },
"Tabs": { "cardMode": "column" },
"InfoTip": { "cardMode": "column" },
"PreviewTip": { "cardMode": "column" },
"ToolTip": { "cardMode": "column" },
"Anchor": { "cardMode": "column" },
"Text": { "cardMode": "column" },
"SelectDropdown": { "cardMode": "single", "primaryStory": "Default" },
"List": { "cardMode": "single", "primaryStory": "Default" },
"DataList": { "cardMode": "single", "primaryStory": "Default" },
"DataTable": { "cardMode": "single", "primaryStory": "Default" },
"Alert": { "cardMode": "single", "primaryStory": "Default" },
"Popover": { "cardMode": "single", "primaryStory": "Default" },
"Toaster": { "cardMode": "single", "primaryStory": "Default" },
"BarChart": { "cardMode": "single", "primaryStory": "Default" },
"DatePicker": { "cardMode": "single", "primaryStory": "Default" },
"GridForm": { "cardMode": "single", "primaryStory": "Default" },
"Video": { "cardMode": "column" },
"Markdown": { "cardMode": "single", "primaryStory": "Default" }
},
"provider": {
"component": "GamutProvider",
"props": {
"theme": { "$ref": "percipioTheme" }
},
"inner": {
"component": "ColorMode",
"props": {
"mode": "light"
}
}
}
}
147 changes: 147 additions & 0 deletions .design-sync/conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Gamut Design System

> **STOP — required reading before generating any code.** Read these in order before writing a single line; this is a gate, not a suggestion:
>
> 1. `guidelines/Guidelines.md`
> 2. `guidelines/overview-components.md` (the pattern → component discovery table)
> 3. `guidelines/overview-styling.md` (the styling do/don't list)
> 4. `guidelines/design-tokens/colors.md` + `guidelines/components/color-mode.md`
> 5. The per-component `.prompt.md` for every component you will use.

This is Codecademy's Gamut design system — the same components, tokens, and
patterns used across Codecademy and Skillsoft's Percipio product. Every
component in this project is browsable below, grouped the way they're
organized in Gamut's own Storybook (Atoms, Molecules, Organisms, Layouts,
Typography). Build with these components rather than one-off equivalents —
they carry accessibility, theming, and interaction behavior that a hand-built
version would have to reimplement.

## Non-negotiables

1. **Props-only — no inline `style`, no inline `css` prop, no `className` for layout, spacing, or color.** Every system-prop-expressible style goes through a Gamut prop, `css()`, `variant()`, or `states()` — not Emotion's `css={...}` JSX prop written inline on an element. Any inline `style`/`css` or hex value is a **departure** and must be called out explicitly (see #5) — it is never a silent default.
2. **Every color is a semantic token — never hex.** `text`, `background`, `primary`, `feedback-error`, etc. See `guidelines/design-tokens/colors.md`.
3. **Wrap the tree in `ColorMode`.** Semantic tokens only resolve inside it.
4. **Prefer purpose-built components over `Box`/`FlexBox` rebuilds.** `List`/`DataList`/`DataTable`/`Tabs`/`Badge`/`Tag` carry accessibility and theming a hand-built version doesn't.
5. **Call out departures — never ship them silently.** If a build genuinely needs a hex value or a `className`, say so explicitly. This applies just as much to the _request itself_: if the designer/user asks for something outside system props, tokens, or semantic tokens (an exact hex, an arbitrary pixel value off the spacing scale, a one-off font size), tell them it deviates from the system before implementing it — don't silently comply, and don't silently substitute the nearest token without saying so.

## Wrapping and setup

Every Gamut component must be rendered inside `GamutProvider`. Without it, the theme tokens (spacing, typography, colors) are undefined and components render unstyled or throw.

```jsx
import { GamutProvider, ColorMode, FillButton } from './CodecademyGamut';

function App() {
return (
<GamutProvider theme={percipioTheme}>
<ColorMode mode="light">
{/* components here */}
<FillButton>Click Me</FillButton>
</ColorMode>
</GamutProvider>
);
}
```

Five themes are available — choose one based on the target product context. **`percipioTheme` is the default for this project.**

| Theme export | Product context |
| --------------- | -------------------------------------- |
| `percipioTheme` | Percipio integration (**default**) |
| `coreTheme` | Codecademy web (Apercu + Suisse fonts) |
| `platformTheme` | Platform / learner experience |
| `adminTheme` | Admin and internal tooling |
| `lxStudioTheme` | LX Studio |

Pass the theme object to `GamutProvider`'s `theme` prop: `<GamutProvider theme={platformTheme}>`. The `ColorMode` component controls light/dark: `mode="light"` or `mode="dark"`.

## Styling idiom

Gamut is **props-only** — no CSS utility classes. Layout, spacing, and color all go through system props on components like `Box`, `FlexBox`, and `GridBox`. Do not write `className` strings or inline `style` for design-system layout.

```jsx
<Box padding="16px" backgroundColor="background" border={1} borderRadius="sm">
content
</Box>

<FlexBox alignItems="center" gap="8px" flexDirection="row">
<FillButton>Primary</FillButton>
<StrokeButton>Secondary</StrokeButton>
</FlexBox>
```

For color values, use semantic theme keys (defined in the active theme) rather than hex. `ColorMode` injects the light or dark palette — components read from it automatically, as CSS custom properties (`--color-*`) it sets at runtime. (The stats near the bottom of this README may say "0 CSS custom properties" — that's a static-bundle scan, not a statement about runtime; `ColorMode`'s injected properties are real and readable via `var(--color-border-secondary)` etc. if you ever need to. Full mapping table in `guidelines/components/color-mode.md`.)

Most-used semantic tokens (Core theme, light/dark — see the theme-specific `guidelines/design-tokens/theme-*.md` for other themes' actual values):

| Token | Light | Dark | Use for |
| -------------------- | ----------- | ------------ | --------------------------------- |
| `text` | `navy-800` | `white` | Default body/UI text |
| `background` | `white` | `navy-800` | Default page/component background |
| `background-primary` | `beige` | `navy-900` | Slightly elevated surfaces |
| `primary` | `hyper-500` | `yellow-500` | Primary CTA, links, focus rings |
| `secondary` | `navy-800` | `white` | Secondary CTA, ghost buttons |
| `border-primary` | `navy-800` | `white` | Strong borders, dividers |
| `feedback-error` | `red-600` | `red-300` | Error messages |
| `feedback-success` | `green-700` | `green-400` | Success messages |

**Before composing a UI pattern from boxes**, check whether a purpose-built component already covers it — `List`/`DataList`/`DataTable` for rows of data, `Toggle` for on/off controls, `Menu` for navigation/actions, `Disclosure` for a single show/hide section (use `List`'s expandable-row pattern instead for two or more). Recreating one of these from `Box`/`FlexBox` and manual state loses built-in accessibility, keyboard handling, and theming the real component already has. See `guidelines/overview-components.md`'s Component Discovery section for the full pattern-to-component table.

## Idiomatic build example

A page section using Gamut components — provider at the root, system props for layout:

```jsx
const {
GamutProvider,
ColorMode,
FlexBox,
Box,
Text,
FillButton,
StrokeButton,
percipioTheme,
} = window.CodecademyGamut;

export default function HeroSection() {
return (
<GamutProvider theme={percipioTheme}>
<ColorMode mode="light">
<Box padding="48px" maxWidth="800px">
<Text as="h1" fontSize={34} fontWeight="title">
Learn to code
</Text>
<Text as="p" fontSize={16} mb="24px">
Join millions of learners on Codecademy.
</Text>
<FlexBox gap="12px">
<FillButton>Get started</FillButton>
<StrokeButton>Sign in</StrokeButton>
</FlexBox>
</Box>
</ColorMode>
</GamutProvider>
);
}
```

## Agent Information

The following is build/technical detail — useful for whoever (or whatever)
is generating code against this system, less so for a human just browsing
components.

**Runtime loading — confirmed gotchas in this environment:** never write `const React = window.React` in an x-import `.jsx` (it's already an injected function parameter — redeclaring it throws `SyntaxError: redeclaration of formal parameter React`); load `_ds_bundle.js` only after `window.React`/`window.ReactDOM` exist (a static `<script>` tag in `<helmet>` often runs first and gets `W5.exports is undefined`); never load a second React UMD bundle to "fix" that (creates a second React instance — hooks break with `g.current is null`); and read `window.CodecademyGamut` at render time behind a ready-state guard, not by destructuring at module scope. Full pattern and code in `guidelines/overview-setup.md`.

**Read `guidelines/Guidelines.md` before building anything.** It defines the
required reading order (overview files → design tokens → per-component
guides) for this system's usage rules — button variant selection, theming
setup, accessibility patterns, code style, and more. Read the relevant
per-component guide before writing code that uses that component.

**Where the rest of the truth lives:**

- **Component API**: each component folder (`components/<group>/<Name>/`) contains a `.d.ts` TypeScript declaration and a `.prompt.md` usage reference — read those for props and composition patterns before coding.
- **README**: this file's body (below, auto-generated) lists all available components grouped by category.
- **Bundle**: `_ds_bundle.js` — all components and themes are on `window.CodecademyGamut`. Import them destructured: `const { FillButton, GamutProvider, percipioTheme } = window.CodecademyGamut`.
- **Guidelines**: `guidelines/` (start at `Guidelines.md`) — category-level usage rules and design tokens.
Loading
Loading