diff --git a/.design-sync/NOTES.md b/.design-sync/NOTES.md new file mode 100644 index 0000000000..6634d31d0f --- /dev/null +++ b/.design-sync/NOTES.md @@ -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 `` 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`. diff --git a/.design-sync/config.json b/.design-sync/config.json new file mode 100644 index 0000000000..8c488c1439 --- /dev/null +++ b/.design-sync/config.json @@ -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" + } + } + } +} diff --git a/.design-sync/conventions.md b/.design-sync/conventions.md new file mode 100644 index 0000000000..6b20d4c576 --- /dev/null +++ b/.design-sync/conventions.md @@ -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 ( + + + {/* components here */} + Click Me + + + ); +} +``` + +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: ``. 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 + + content + + + + Primary + Secondary + +``` + +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 ( + + + + + Learn to code + + + Join millions of learners on Codecademy. + + + Get started + Sign in + + + + + ); +} +``` + +## 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 ` + ``` + +3. **Don't "fix" #2 by loading your own React UMD bundle.** That creates a second React instance — `ReactDOM` binds to one, your hooks to the other — producing `can't access property "useState", g.current is null`. Always use the runtime's own `window.React`. +4. **Because the bundle now loads after first render, guard the component that reads it.** Hold a `ready` state, poll `window.CodecademyGamut` in an effect, render a loading placeholder until it exists, and read Gamut components **at render time** (`const { FlexBox } = window.CodecademyGamut` inside the component), not by destructuring at module scope before the bundle has loaded. + +**One `GamutProvider` + `ColorMode` for the whole app.** Mounting individual Gamut components as separate x-imports, each with its own provider, won't share theme context between them. + +## Dependency protection + +Never build a local shim, wrapper, or reimplementation of a Gamut component +or theme — always use the real export from `window.CodecademyGamut`. If a +component or theme export you need doesn't appear to exist on the bundle, +say so explicitly rather than fabricating a substitute. diff --git a/.design-sync/guidelines/overview-styling.md b/.design-sync/guidelines/overview-styling.md new file mode 100644 index 0000000000..03b08eb899 --- /dev/null +++ b/.design-sync/guidelines/overview-styling.md @@ -0,0 +1,125 @@ +# Styling Rules + +- **Never use inline `style` attributes, and never use Emotion's inline `css` prop.** All styling must use system props + or Gamut tokens — never the `style` JSX attribute, an inline `css={...}` prop on an element, or hardcoded values. Use `css()`/`variant()`/`states()` composed on a `styled` component instead of writing `css={...}` inline. +- **Use system props shorthand** for layout and spacing. Prefer abbreviated + prop names over long-form equivalents. +- **Never use SCSS/CSS modules or `className`** on a Gamut component + (`Box`, `FlexBox`, `Text`, `FillButton`, etc.) for styling — bypasses + `ColorMode` token propagation and system-prop composition. `className` is + acceptable only as an integration seam a third-party library requires — + call it out explicitly when used (see Departures below), never as a + styling mechanism. +- **Avoid nested selectors** in styled/Emotion template literals (bare tag + selectors, `${Component} { }` child selectors) — style the target + directly with system props or `as`, not from a parent wrapper. + +**If a case looks like it needs inline `style`, it almost never does** — +`css()`/`variant()`/`states()` (below) cover effectively everything system +props don't, including things that feel like edge cases: + +- _Absolute-positioned icon inside an `Input`_ — not a `style={{ position: +'absolute' }}` job. Use `system.positioning` props (`position`, `top`, + `left`) directly on a wrapping `Box`, or `css({ position: 'absolute', ... })` + if the positioning is conditional/branching. +- _Fixed control widths that don't map to a spacing token_ — still a system + prop (`width="240px"` works with any CSS length, not just scale values) or + `css({ width: '240px' })`, not `style`. + +If you genuinely hit something neither system props nor `css()`/`variant()`/ +`states()` can express, that's a Departures case (below) — say so explicitly, +don't silently reach for `style`. + +## System props shorthand + +| Long form | Shorthand | +| --------------- | --------- | +| `margin` | `m` | +| `marginTop` | `mt` | +| `marginRight` | `mr` | +| `marginBottom` | `mb` | +| `marginLeft` | `ml` | +| `marginX` | `mx` | +| `marginY` | `my` | +| `padding` | `p` | +| `paddingTop` | `pt` | +| `paddingRight` | `pr` | +| `paddingBottom` | `pb` | +| `paddingLeft` | `pl` | +| `paddingX` | `px` | +| `paddingY` | `py` | + +Use `mb={16}`, not `marginBottom={16}` or `style={{ marginBottom: 16 }}`. + +## Border and color shorthand + +| Category | Props | +| -------- | ---------------------------------------------------------------------------------------------------------------- | +| Borders | `border`, `borderTop`, `borderRight`, `borderBottom`, `borderLeft`, `borderColor`, `borderWidth`, `borderRadius` | +| Colors | `bg` (background), `textColor`, `color` | + +Values are Gamut tokens — `borderColor="navy-300"`, `bg="background-primary"`, `textColor="text-secondary"`. Never raw hex. + +**Availability varies by component.** These props are fully available on layout primitives (`Box`, `FlexBox`, `Text`, `Background`). + +## System prop groups + +`Box`, `FlexBox`, and `GridBox` compose several theme-connected prop groups at once. + +| Group | Covers | Key props | +| -------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------- | +| `system.layout` | dimensions, display, overflow, flex/grid item props | `display`, `width`, `height`, `overflow*`, `flexGrow`, `gridColumn`, `alignSelf` | +| `system.space` | margin/padding on the spacing scale | see shorthand table above | +| `system.color` | foreground/background/border colors, theme-restricted | `color`, `textColor`, `bg`, `borderColor` | +| `system.typography` | text styling on theme scales | `fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `textAlign` | +| `system.border` | border width/style/radius/color | `border`, `borderRadius`, `borderWidth` | +| `system.flex` | flexbox child/container props | `flex`, `flexDirection`, `alignItems`, `justifyContent`, `gap` | +| `system.grid` | CSS Grid container/child props | `gridTemplateColumns`, `gridTemplateAreas`, `gridArea` | +| `system.positioning` | position and offsets | `position`, `top`, `left`, `zIndex` | +| `system.shadow` | box/text shadow | `boxShadow`, `textShadow` | + +### Responsive values + +All system props accept responsive values, mobile-first (min-width queries), as an object or array: + +```jsx + + +``` + +`_` is the base (no breakpoint). Container-query keys (`c_xs`…`c_xl`) apply to a containment context instead of the viewport — the parent must set `containerType="inline-size"`. + +## `css()`, `variant()`, `states()` — for anything system props can't express + +When a style can't be expressed as a system prop, compose it with `css()`, `variant()`, or `states()` (available on `window.CodecademyGamut`) alongside `styled` from Emotion — these read from the same token scales as system props (semantic colors track `ColorMode` and theme). + +```jsx +const { css, variant, states } = window.CodecademyGamut; + +// css() — static style object +const styledBox = css({ bg: 'navy-400', p: 4 }); + +// variant() — mutually exclusive modes +const linkVariant = variant({ + base: { p: 4 }, + defaultVariant: 'interface', + variants: { + interface: { color: 'text', '&:hover': { color: 'text-accent' } }, + inline: { color: 'primary', '&:hover': { color: 'secondary' } }, + }, +}); + +// states() — independent boolean toggles +const boxStates = states({ + base: { mx: 4, my: 8, p: 16 }, + disabled: { bg: 'background-disabled', color: 'text-disabled' }, +}); +``` + +Prefer semantic color keys (`primary`, `text`) over raw palette tokens (`navy-400`) so styles track `ColorMode` and theme switches. + +## Departures — call out deviations, don't ship them silently + +If a build genuinely can't follow one of the rules above — no semantic token fits a required color, a third-party integration needs `className` — say so explicitly (e.g. "using hex `#...` here because no semantic token matches this color; flag for design review"). A silent departure looks system-compliant when it isn't. + +This applies to the _request_ as much as the output: 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 explicitly that it deviates from the system before implementing it. Don't silently comply with an off-system request, and don't silently coerce it to the nearest token without saying so — either way, the person asking should know the system was departed from. diff --git a/.design-sync/guidelines/overview-validation-checklist.md b/.design-sync/guidelines/overview-validation-checklist.md new file mode 100644 index 0000000000..7b2b13e458 --- /dev/null +++ b/.design-sync/guidelines/overview-validation-checklist.md @@ -0,0 +1,28 @@ +# Validation Checklist + +Before considering output final, verify: + +- [ ] Every color resolves to a Gamut semantic or core color token — no raw hex values. Status colors (success/error/warning) use `feedback-*` tokens, never brand green/red hex. Page and section backgrounds use `background-*` tokens or ``, never a styled `div`. +- [ ] The whole tree renders inside `ColorMode` so semantic tokens resolve. +- [ ] All spacing and sizing values match the spacing token scale. +- [ ] Border radius values use one of the defined radius tokens. +- [ ] Typography uses only accepted font families, sizes, weights, and line heights. +- [ ] UI elements map to existing Gamut components where applicable — a purpose-built component (`List`, `DataList`, `DataTable`, `Toggle`, `Menu`, etc.) was used instead of recreating the pattern from `Box`/`FlexBox`. Status labels, counts, pills, and chips use `Badge`/`Tag` — not a hand-styled `Box`. +- [ ] Icons, illustrations, and patterns are sourced from the Gamut libraries. +- [ ] Icon selection was determined by visual appearance and contextual clues (tooltip text, element purpose) — not by Figma layer names, which may be stale. +- [ ] The design meets WCAG accessibility requirements for contrast and target size. +- [ ] All component imports reference the real `window.CodecademyGamut` bundle — no local shim/reimplementation exists. +- [ ] No inline `style` attributes, `className`, or SCSS/CSS modules are used on Gamut components — all styling uses system props, `css()`/`variant()`/`states()`, or Gamut tokens. +- [ ] All system props use shorthand notation (e.g., `mb` instead of `marginBottom`). +- [ ] No nested selectors (bare tag selectors, `${Component} { }`) inside styled/Emotion template literals. +- [ ] Any unavoidable departure from a rule above (forced `className`, no matching token) is called out explicitly, not shipped silently. +- [ ] Gamut component inventory was inspected before writing any custom markup for a UI pattern. +- [ ] Any custom markup includes a comment confirming no suitable Gamut component exists. +- [ ] Dark/light regions use `ColorMode` or `Background` — no custom CSS color overrides for mode switching. +- [ ] Every `DataTable` / `DataList` with `sortable: true` columns also provides `query` and `onQueryChange` props, and passes pre-sorted rows. +- [ ] Every `Menu` has an explicit `variant` prop — `fixed` for persistent navigation (sidebars, primary nav, footer nav), `popover` for temporary surfaces (overflow menus, action menus). `fixed` menus render as `