@codecademy/gamut 68.6.2-alpha.1fc7ca.0 → 68.6.2-alpha.f8b396.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-tools/.claude-plugin/marketplace.json +16 -0
- package/agent-tools/.claude-plugin/plugin.json +7 -0
- package/agent-tools/.cursor-plugin/plugin.json +7 -0
- package/agent-tools/DESIGN.Codecademy.md +643 -0
- package/agent-tools/DESIGN.LXStudio.md +437 -0
- package/agent-tools/DESIGN.Percipio.md +433 -0
- package/agent-tools/DESIGN.md +1 -0
- package/agent-tools/agents/.gitkeep +0 -0
- package/agent-tools/rules/accessibility.mdc +78 -0
- package/agent-tools/skills/gamut-accessibility/SKILL.md +214 -0
- package/agent-tools/skills/gamut-buttons/SKILL.md +96 -0
- package/agent-tools/skills/gamut-color-mode/SKILL.md +257 -0
- package/agent-tools/skills/gamut-forms/SKILL.md +84 -0
- package/agent-tools/skills/gamut-layout/SKILL.md +109 -0
- package/agent-tools/skills/gamut-list/SKILL.md +273 -0
- package/agent-tools/skills/gamut-review/SKILL.md +254 -0
- package/agent-tools/skills/gamut-style-utilities/SKILL.md +107 -0
- package/agent-tools/skills/gamut-system-props/SKILL.md +203 -0
- package/agent-tools/skills/gamut-testing/SKILL.md +221 -0
- package/agent-tools/skills/gamut-theming/SKILL.md +115 -0
- package/agent-tools/skills/gamut-typography/SKILL.md +98 -0
- package/bin/commands/plugin/install.mjs +212 -0
- package/bin/commands/plugin/list.mjs +73 -0
- package/bin/commands/plugin/remove.mjs +108 -0
- package/bin/commands/plugin/update.mjs +59 -0
- package/bin/gamut.mjs +96 -0
- package/bin/lib/claude.mjs +52 -0
- package/bin/lib/cursor.mjs +40 -0
- package/bin/lib/design.mjs +71 -0
- package/bin/lib/io.mjs +14 -0
- package/bin/lib/resolve-plugin-dir.mjs +38 -0
- package/bin/lib/run-command.mjs +22 -0
- package/package.json +11 -8
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamut-testing
|
|
3
|
+
description: Use this skill when writing or fixing unit tests for React components that use Gamut — prefer setupRtl from @codecademy/gamut-tests, harness patterns for useLogicalProperties and ColorMode, RTL/dir testing, emotion matchers, or removing jest.mock of @codecademy/gamut / gamut-styles.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut Testing
|
|
7
|
+
|
|
8
|
+
Source: `@codecademy/gamut-tests` — [`index.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-tests/src/index.tsx)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What `MockGamutProvider` does (under `setupRtl`)
|
|
15
|
+
|
|
16
|
+
`MockGamutProvider` forwards to `GamutProvider` with:
|
|
17
|
+
|
|
18
|
+
- `useCache={false}` — stable Emotion output across tests
|
|
19
|
+
- `useGlobals={false}` — no global Reboot/Typography bleed between files
|
|
20
|
+
- `theme={theme}` — full token theme for styled components
|
|
21
|
+
- Optional `useLogicalProperties` — forwarded for logical vs physical CSS in variance
|
|
22
|
+
|
|
23
|
+
You normally do not import `MockGamutProvider` for plain component tests; `setupRtl` already wraps the component under test once. Import it inside a harness when the SUT needs a non-default provider flag or extra wrappers (see below).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Decision guide
|
|
28
|
+
|
|
29
|
+
| Scenario | Prefer |
|
|
30
|
+
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
31
|
+
| Default unit test for a Gamut (or app) component | `setupRtl(Component, defaultProps)` once per file / describe |
|
|
32
|
+
| Vary `useLogicalProperties` across cases | Harness that accepts `useLogicalProperties` and wraps `MockGamutProvider`, then `setupRtl(Harness, defaults)`; pass overrides per `it` / `describe.each` |
|
|
33
|
+
| Need `ColorMode` (or other context) around the SUT | Harness with `<ColorMode>` inside the tree, then `setupRtl(Harness)` — no need for raw `render` unless you are testing the provider itself |
|
|
34
|
+
| `dir` / RTL behavior (e.g. mirrored layout, `useElementDir`) | Keep using `setupRtl` for the component; set `document.documentElement.setAttribute('dir', 'rtl' \| 'ltr')` (and scroll/viewport stubs if needed) in `beforeEach` / `afterEach`; reset `dir` after tests so suites do not leak |
|
|
35
|
+
| Storybook-only mock, chromatic-style wrapper, or non-RTL harness | `MockGamutProvider` (± `ColorMode`) in the exported wrapper component |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## `setupRtl` — primary pattern
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { setupRtl } from '@codecademy/gamut-tests';
|
|
43
|
+
|
|
44
|
+
import { MyComponent } from '../MyComponent';
|
|
45
|
+
|
|
46
|
+
const renderView = setupRtl(MyComponent, {
|
|
47
|
+
label: 'Default label',
|
|
48
|
+
onClick: jest.fn(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders the label', () => {
|
|
52
|
+
const { view } = renderView();
|
|
53
|
+
expect(view.getByText('Default label')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('accepts prop overrides', () => {
|
|
57
|
+
const { view } = renderView({ label: 'Override' });
|
|
58
|
+
expect(view.getByText('Override')).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`renderView` returns `{ view, props, update }`:
|
|
63
|
+
|
|
64
|
+
- `view` — RTL `RenderResult` (`getByRole`, `getByLabelText`, `getByText`, …)
|
|
65
|
+
- `props` — resolved props (handy for `jest.fn()` assertions)
|
|
66
|
+
- `update` — re-render with new props without remounting
|
|
67
|
+
|
|
68
|
+
### Query and interaction habits (RTL)
|
|
69
|
+
|
|
70
|
+
- Prefer `getByRole`, `getByLabelText`, and accessible names over CSS selectors or snapshotting class strings unless you are explicitly testing styling.
|
|
71
|
+
- Prefer `@testing-library/user-event` over `fireEvent` when simulating real input (import `userEvent` from `@testing-library/user-event` in current major versions).
|
|
72
|
+
|
|
73
|
+
### Accessing mock functions via `props`
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import userEvent from '@testing-library/user-event';
|
|
77
|
+
|
|
78
|
+
it('calls onClick when clicked', async () => {
|
|
79
|
+
const { view, props } = renderView();
|
|
80
|
+
await userEvent.click(view.getByRole('button'));
|
|
81
|
+
expect(props.onClick).toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Harness + `setupRtl` when the wrapper is not default
|
|
88
|
+
|
|
89
|
+
`setupRtl` always wraps with `MockGamutProvider` with default props. To vary `useLogicalProperties`, add `ColorMode`, or compose other providers, define a small harness and pass `setupRtl` that harness — still one `renderView` factory, still `props` / `update` ergonomics.
|
|
90
|
+
|
|
91
|
+
### Varying `useLogicalProperties` (logical vs physical CSS)
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { MockGamutProvider, setupRtl } from '@codecademy/gamut-tests';
|
|
95
|
+
|
|
96
|
+
import { MyComponent } from '../MyComponent';
|
|
97
|
+
|
|
98
|
+
type HarnessProps = React.ComponentProps<typeof MyComponent> & {
|
|
99
|
+
useLogicalProperties?: boolean;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const MyHarness = ({ useLogicalProperties, ...rest }: HarnessProps) => (
|
|
103
|
+
<MockGamutProvider useLogicalProperties={useLogicalProperties}>
|
|
104
|
+
<MyComponent {...rest} />
|
|
105
|
+
</MockGamutProvider>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const renderView = setupRtl(MyHarness, { width: '200px' });
|
|
109
|
+
|
|
110
|
+
describe.each([
|
|
111
|
+
{ useLogicalProperties: true as const, widthProp: 'inlineSize' as const },
|
|
112
|
+
{ useLogicalProperties: false as const, widthProp: 'width' as const },
|
|
113
|
+
])(
|
|
114
|
+
'useLogicalProperties=$useLogicalProperties',
|
|
115
|
+
({ useLogicalProperties, widthProp }) => {
|
|
116
|
+
it(`uses ${widthProp}`, () => {
|
|
117
|
+
const { view } = renderView({ useLogicalProperties });
|
|
118
|
+
expect(view.getByTestId('my-component-root')).toHaveStyle({
|
|
119
|
+
[widthProp]: '200px',
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The outer `setupRtl` wrapper adds a default `MockGamutProvider`; the harness’s inner `MockGamutProvider` sets `useLogicalProperties` for the subtree under test (nested `GamutProvider` / theme is the nearest one Emotion and variance see).
|
|
127
|
+
|
|
128
|
+
### `ColorMode` without abandoning `setupRtl`
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { ColorMode } from '@codecademy/gamut-styles';
|
|
132
|
+
import { setupRtl } from '@codecademy/gamut-tests';
|
|
133
|
+
|
|
134
|
+
const DarkHarness = (props: React.ComponentProps<typeof MyComponent>) => (
|
|
135
|
+
<ColorMode mode="dark">
|
|
136
|
+
<MyComponent {...props} />
|
|
137
|
+
</ColorMode>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const renderDark = setupRtl(DarkHarness, { title: 'Hi' });
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Use `MockGamutProvider` only inside the harness if you also need a non-default Gamut flag and `ColorMode` in the same tree; otherwise `setupRtl(DarkHarness)` is enough.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Raw `render` + `MockGamutProvider` — rare
|
|
148
|
+
|
|
149
|
+
Reserve `render` from `@testing-library/react` + manual `MockGamutProvider` for cases where a harness would be more obscure than a single inline tree (e.g. highly dynamic one-off trees). If the same wrapper appears more than once, switch to a harness + `setupRtl`.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## RTL / `dir` and document-level behavior
|
|
154
|
+
|
|
155
|
+
Some components (e.g. overlays that call `useElementDir`) resolve direction from `document.documentElement` when there is no real target node. For those tests:
|
|
156
|
+
|
|
157
|
+
- Set `document.documentElement.setAttribute('dir', 'rtl')` (or `'ltr'`) around the scenario, `unmount` between LTR and RTL assertions when re-rendering, and restore `dir` in `afterEach` so other tests start clean.
|
|
158
|
+
- Combine with the harness pattern above when `useLogicalProperties` affects which longhand wins (`left` vs `insetInlineStart`, etc.).
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Emotion style assertions
|
|
163
|
+
|
|
164
|
+
Install `@emotion/jest` matchers if you absolutely need to enable CSS-in-JS assertions:
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
import { matchers } from '@emotion/jest';
|
|
168
|
+
|
|
169
|
+
expect.extend(matchers);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Then assert on styles:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
expect(element).toHaveStyle({ borderRadius: '2px' });
|
|
176
|
+
expect(element).toHaveStyleRule('padding', '1rem');
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Use `theme` from `@codecademy/gamut-styles` instead of hardcoding token strings:
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
import { theme } from '@codecademy/gamut-styles';
|
|
183
|
+
|
|
184
|
+
expect(element).toHaveStyle({ columnGap: theme.spacing[40] });
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Visual test wrappers and Storybook
|
|
190
|
+
|
|
191
|
+
Exported mocks and stories may wrap with `MockGamutProvider` and `ColorMode` explicitly (no `setupRtl` in Storybook):
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { MockGamutProvider } from '@codecademy/gamut-tests';
|
|
195
|
+
import { ColorMode } from '@codecademy/gamut-styles';
|
|
196
|
+
|
|
197
|
+
export const MyComponentMock: React.FC<ComponentProps<typeof MyComponent>> = (
|
|
198
|
+
props
|
|
199
|
+
) => (
|
|
200
|
+
<MockGamutProvider>
|
|
201
|
+
<ColorMode mode="light">
|
|
202
|
+
<MyComponent {...props} />
|
|
203
|
+
</ColorMode>
|
|
204
|
+
</MockGamutProvider>
|
|
205
|
+
);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Common anti-patterns
|
|
211
|
+
|
|
212
|
+
| Anti-pattern | Fix |
|
|
213
|
+
| ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
214
|
+
| `jest.mock('@codecademy/gamut', () => ({ ... }))` | Remove; use `setupRtl` (or harness + `setupRtl`) |
|
|
215
|
+
| `jest.mock('@codecademy/gamut-styles', ...)` | Remove; `MockGamutProvider` / `setupRtl` supplies theme |
|
|
216
|
+
| `GamutProvider` in test files | Use `MockGamutProvider` only when building a harness or story; default tests go through `setupRtl` |
|
|
217
|
+
| `import { setupRtl } from 'component-test-setup'` in Gamut / apps | Import `setupRtl` from `@codecademy/gamut-tests` so `MockGamutProvider` is applied |
|
|
218
|
+
| Repeated `render(<MockGamutProvider>…` | Harness + `setupRtl`, or a shared `renderView` factory |
|
|
219
|
+
| One `setupRtl` call per `it` | Define `renderView` once outside `describe`, call it inside each `it` |
|
|
220
|
+
| Asserting raw CSS strings for tokens | Use `theme` from `@codecademy/gamut-styles` |
|
|
221
|
+
| Leaking `dir="rtl"` between tests | Reset `document.documentElement` in `afterEach` |
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamut-theming
|
|
3
|
+
description: Use this skill when choosing or extending Gamut themes (Core, Admin, Platform, LX Studio, Percipio), wiring GamutProvider and Emotion theme TypeScript augmentation, or following CreatingThemes — not for day-to-day css(), variant(), or states() patterns (see gamut-style-utilities).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut Theming
|
|
7
|
+
|
|
8
|
+
Source: `@codecademy/gamut-styles`
|
|
9
|
+
|
|
10
|
+
See also: [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md) (`css`, `variant`, `states`, `StyleProps`, `useTheme` escape hatch). [`gamut-color-mode`](../gamut-color-mode/SKILL.md) (semantic color, `<ColorMode>`, `<Background>`). [`gamut-system-props`](../gamut-system-props/SKILL.md) (`system.*`, responsive `Box` props).
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Gamut uses Emotion's theme system. Themes are org-specific token bundles (colors, typography, spacing, etc.). The active theme is set at the app root with `<GamutProvider theme={...}>`; child styled components read tokens through Emotion context.
|
|
15
|
+
|
|
16
|
+
For authoring component styles (`css`, `variant`, `states`, system props, ColorMode), use the skills linked above and the styleguide [Best practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page).
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
yarn add @codecademy/gamut-kit @emotion/react @emotion/styled
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`gamut-kit` bundles `gamut`, `gamut-icons`, `gamut-illustrations`, `gamut-patterns`, `gamut-styles`, `variance`, and `gamut-tests`.
|
|
25
|
+
|
|
26
|
+
Full guide: [Meta / Installation](https://gamut.codecademy.com/?path=/docs-meta-installation--page) in Storybook (CSP `nonce` on `GamutProvider`, Jest, Next/Gatsby entry points).
|
|
27
|
+
|
|
28
|
+
Optionally add a `peerDependencies` block in `package.json` listing `@codecademy/gamut`, `@codecademy/gamut-icons`, `@codecademy/gamut-illustrations`, `@codecademy/gamut-patterns`, `@codecademy/gamut-styles`, `@codecademy/gamut-tests`, and `@codecademy/variance` (e.g. `"*"`) so editors surface those packages — see Meta / Installation for the JSON snippet.
|
|
29
|
+
|
|
30
|
+
## Required wrapper
|
|
31
|
+
|
|
32
|
+
Wrap the app root in `<GamutProvider>` from `@codecademy/gamut-styles`. This wires up the theme, color mode, and logical properties for all child components.
|
|
33
|
+
|
|
34
|
+
At runtime, `GamutProvider` defaults to Core when `theme` is omitted (`theme = coreTheme` in the implementation). For non-Core products and for TypeScript (`theme` is required on `GamutProviderProps`), pass `theme` explicitly using the table below.
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { GamutProvider, theme } from '@codecademy/gamut-styles';
|
|
38
|
+
|
|
39
|
+
const App = () => (
|
|
40
|
+
<GamutProvider theme={theme}>{/* app content */}</GamutProvider>
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Available themes
|
|
45
|
+
|
|
46
|
+
| Theme | Used for | Import from `@codecademy/gamut-styles` |
|
|
47
|
+
| --------- | ---------------------------------------------------------- | -------------------------------------- |
|
|
48
|
+
| Core | Codecademy default | `coreTheme` or `theme` (default) |
|
|
49
|
+
| Admin | Codecademy admin tools | `adminTheme` |
|
|
50
|
+
| Platform | Codecademy learning environment / shared platform surfaces | `platformTheme` |
|
|
51
|
+
| LX Studio | Learning Experience Studio | `lxStudioTheme` |
|
|
52
|
+
| Percipio | Skillsoft Percipio platform | `percipioTheme` |
|
|
53
|
+
|
|
54
|
+
## TypeScript (`theme.d.ts`)
|
|
55
|
+
|
|
56
|
+
Augment `@emotion/react` so `props.theme` in `styled` / `css` matches the same theme object you pass to `<GamutProvider theme={...}>`. If the types disagree, system props and token autocomplete will not line up with runtime.
|
|
57
|
+
|
|
58
|
+
Add a root `theme.d.ts` (or merge into your existing global types):
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// theme.d.ts
|
|
62
|
+
import '@emotion/react';
|
|
63
|
+
|
|
64
|
+
import type { CoreTheme } from '@codecademy/gamut-styles';
|
|
65
|
+
|
|
66
|
+
declare module '@emotion/react' {
|
|
67
|
+
export interface Theme extends CoreTheme {}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use the theme interface that matches your provider:
|
|
72
|
+
|
|
73
|
+
| `GamutProvider` `theme` prop | Import for `Theme extends …` |
|
|
74
|
+
| ---------------------------- | ---------------------------- |
|
|
75
|
+
| `theme` or `coreTheme` | `CoreTheme` |
|
|
76
|
+
| `adminTheme` | `AdminTheme` |
|
|
77
|
+
| `platformTheme` | `PlatformTheme` |
|
|
78
|
+
| `lxStudioTheme` | `LxStudioTheme` |
|
|
79
|
+
| `percipioTheme` | `PercipioTheme` |
|
|
80
|
+
|
|
81
|
+
Example when the app uses Percipio:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
// theme.d.ts
|
|
85
|
+
import '@emotion/react';
|
|
86
|
+
|
|
87
|
+
import type { PercipioTheme } from '@codecademy/gamut-styles';
|
|
88
|
+
|
|
89
|
+
declare module '@emotion/react' {
|
|
90
|
+
export interface Theme extends PercipioTheme {}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See Emotion’s [TypeScript / define a theme](https://emotion.sh/docs/typescript#define-a-theme) for details.
|
|
95
|
+
|
|
96
|
+
## Theme vs color mode vs style API
|
|
97
|
+
|
|
98
|
+
| Concern | Where to read |
|
|
99
|
+
| ------------------------------------------------------- | ----------------------- |
|
|
100
|
+
| Which `theme` object to pass to `GamutProvider` | This skill |
|
|
101
|
+
| Light / dark semantic colors, `ColorMode`, `Background` | `gamut-color-mode` |
|
|
102
|
+
| `css` / `variant` / `states`, `useTheme` for non-CSS JS | `gamut-style-utilities` |
|
|
103
|
+
| Composed `system.*` props on styled primitives / `Box` | `gamut-system-props` |
|
|
104
|
+
| Spacing, breakpoints, page grid | `gamut-layout` |
|
|
105
|
+
|
|
106
|
+
## Creating a new theme
|
|
107
|
+
|
|
108
|
+
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.
|
|
109
|
+
|
|
110
|
+
## Key principles
|
|
111
|
+
|
|
112
|
+
- Pick the correct theme export for the product so tokens and fonts match design intent.
|
|
113
|
+
- Align `theme.d.ts` / `Theme extends …` with the same theme interface you pass to `GamutProvider`.
|
|
114
|
+
- 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`.
|
|
115
|
+
- `GamutProvider` wires theme, color mode, and logical-properties settings at the root; individual components should not hard-code which org theme is active.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamut-typography
|
|
3
|
+
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut Typography
|
|
7
|
+
|
|
8
|
+
Implementation source of truth: [`packages/gamut-styles/src/variables/typography.ts`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-styles/src/variables/typography.ts) and themes under [`packages/gamut-styles/src/themes`](https://github.com/Codecademy/gamut/tree/main/packages/gamut-styles/src/themes).
|
|
9
|
+
|
|
10
|
+
## Scope by theme
|
|
11
|
+
|
|
12
|
+
| Themes | `fontFamily.base` | `fontFamily.accent` | `fontWeight.title` |
|
|
13
|
+
| --------------------- | ---------------------------------------- | ----------------------------------------- | ------------------ |
|
|
14
|
+
| Core, Admin, Platform | Apercu stack | Suisse + Apercu stack | 700 |
|
|
15
|
+
| Percipio | Skillsoft Text | Skillsoft Sans; `monospace` → Roboto Mono | 500 |
|
|
16
|
+
| LX Studio | Skillsoft Text / Sans (same as Percipio) | Same | 500 |
|
|
17
|
+
|
|
18
|
+
Admin and Platform extend Core for colors / modes only — typography matches Core.
|
|
19
|
+
|
|
20
|
+
Licensing: Apercu is licensed for Codecademy surfaces only; Skillsoft products use Percipio/LX stacks.
|
|
21
|
+
|
|
22
|
+
Use `fontWeight="title"` for headlines / emphasis roles — never hardcode `700` on Percipio/LX unless SPECIFICALLY noted in Figma designs.
|
|
23
|
+
|
|
24
|
+
## Font weight (semantic)
|
|
25
|
+
|
|
26
|
+
| Token | Core / Admin / Platform | Percipio / LX Studio |
|
|
27
|
+
| ------- | ----------------------- | -------------------- |
|
|
28
|
+
| `base` | 400 | 400 |
|
|
29
|
+
| `title` | 700 | 500 |
|
|
30
|
+
|
|
31
|
+
Headlines, CTAs, and buttons should use `fontWeight="title"` so Percipio/LX get 500, Core gets 700. Literal `700` breaks Skillsoft branding on those themes.
|
|
32
|
+
|
|
33
|
+
## Font size scale (`fontSize`)
|
|
34
|
+
|
|
35
|
+
Theme keys: `64`, `44`, `34`, `26`, `22`, `20`, `18`, `16`, `14`.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { css } from '@codecademy/gamut-styles';
|
|
39
|
+
import styled from '@emotion/styled';
|
|
40
|
+
import { system } from '@codecademy/gamut-styles';
|
|
41
|
+
|
|
42
|
+
const Paragraph = styled.p(system.typography);
|
|
43
|
+
<Paragraph fontSize={16} lineHeight="base" />;
|
|
44
|
+
|
|
45
|
+
const Styled = styled.div(css({ fontSize: 14, fontFamily: 'base' }));
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Line height (`lineHeight`)
|
|
49
|
+
|
|
50
|
+
Tokens: `base` (1.5), `spacedTitle` (1.3), `title` (1.2). Prefer tokens over raw decimals. Only specify `lineHeight` when specified by design.
|
|
51
|
+
|
|
52
|
+
## Line length
|
|
53
|
+
|
|
54
|
+
| Context | Target |
|
|
55
|
+
| ------------------ | ------------------------ |
|
|
56
|
+
| Single-column body | ~66 characters (max ~85) |
|
|
57
|
+
| Multi-column | ≤50 characters per line |
|
|
58
|
+
| Minimum | ~45 characters |
|
|
59
|
+
|
|
60
|
+
## Accessing typography tokens
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { system } from '@codecademy/gamut-styles';
|
|
64
|
+
import { variance } from '@codecademy/variance';
|
|
65
|
+
|
|
66
|
+
const Heading = styled.h2(variance.compose(system.typography, system.space));
|
|
67
|
+
|
|
68
|
+
<Heading
|
|
69
|
+
fontSize={26}
|
|
70
|
+
fontFamily="base"
|
|
71
|
+
fontWeight="title"
|
|
72
|
+
lineHeight="title"
|
|
73
|
+
mb={8}
|
|
74
|
+
/>;
|
|
75
|
+
|
|
76
|
+
import { css } from '@codecademy/gamut-styles';
|
|
77
|
+
|
|
78
|
+
const Caption = styled.span(
|
|
79
|
+
css({ fontFamily: 'accent', fontSize: 14, color: 'text-secondary' })
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Prefer `<Text>` from `@codecademy/gamut` with `variant` / `as` — see Storybook [Typography / Text](https://gamut.codecademy.com/?path=/docs-typography-text--docs).
|
|
84
|
+
|
|
85
|
+
## Codecademy (Core / Admin / Platform) — voice and layout
|
|
86
|
+
|
|
87
|
+
Do not blindly apply to Percipio/LX without brand guidance.
|
|
88
|
+
|
|
89
|
+
- `fontFamily="base"` (Apercu): default UI and marketing type. Emphasis inside body copy: Italic, not Bold for intra-paragraph stress.
|
|
90
|
+
- `fontFamily="accent"` (Suisse stack): technical accent — code snippets, captions, labels. Use sparingly; glyph box reads larger — step down ~10–15% vs equivalent `base` size.
|
|
91
|
+
- Alignment: left-align by default; center only short marketing headlines; avoid right-align except tabs / numerics.
|
|
92
|
+
- Letter-spacing: do not tweak tracking unless design specifies.
|
|
93
|
+
|
|
94
|
+
## Semantic vs visual headings
|
|
95
|
+
|
|
96
|
+
- `<Text as="h1">` … `<Text as="h6">` gets default heading styles: each tag maps to the same scale as `variant="title-xxl"` … `variant="title-xs"` (`h1` largest through `h6` smallest). Plain `<Text>` defaults to `as="span"` (inherits font size).
|
|
97
|
+
- Use `variant` plus `fontSize` / `fontWeight` / `lineHeight` (and other system props) to override element defaults when the outline needs one heading level but the UI needs another visual weight — e.g. `<Text as="h2" variant="title-sm">`.
|
|
98
|
+
- Still pick `h1`–`h6` for document structure and assistive tech; overrides are for intentional divergence between semantics and appearance.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, rm, symlink } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { claudePluginSpec, marketplaceName } from '../../lib/claude.mjs';
|
|
5
|
+
import { cursorDestPath } from '../../lib/cursor.mjs';
|
|
6
|
+
import {
|
|
7
|
+
installDesignMd,
|
|
8
|
+
listCanonicalThemes,
|
|
9
|
+
resolveTheme,
|
|
10
|
+
} from '../../lib/design.mjs';
|
|
11
|
+
import { log, warn } from '../../lib/io.mjs';
|
|
12
|
+
import { getFlag, resolvePluginDir } from '../../lib/resolve-plugin-dir.mjs';
|
|
13
|
+
import { runCommand } from '../../lib/run-command.mjs';
|
|
14
|
+
|
|
15
|
+
export const TARGETS = ['cursor', 'claude'];
|
|
16
|
+
export const SCOPES = ['all', 'skills', 'rules', 'agents'];
|
|
17
|
+
|
|
18
|
+
export function help() {
|
|
19
|
+
log(`
|
|
20
|
+
Usage:
|
|
21
|
+
gamut plugin install [target] [options]
|
|
22
|
+
|
|
23
|
+
Install the Gamut plugin into an AI tool.
|
|
24
|
+
|
|
25
|
+
Arguments:
|
|
26
|
+
target Tool to install into (default: cursor)
|
|
27
|
+
cursor | claude
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--scope <scope> Content to install (default: all)
|
|
31
|
+
all | skills | rules | agents
|
|
32
|
+
--theme <theme> Copy DESIGN.*.md to ./DESIGN.md in the current directory
|
|
33
|
+
core | admin | platform | percipio | lxstudio
|
|
34
|
+
(admin/platform use Codecademy DESIGN; aliases: codecademy, cc, lx-studio)
|
|
35
|
+
--force Overwrite existing DESIGN.md when using --theme
|
|
36
|
+
--plugin-dir <path> Override the bundled agent-tools directory
|
|
37
|
+
-h, --help Show this help message
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
gamut plugin install
|
|
41
|
+
gamut plugin install claude
|
|
42
|
+
gamut plugin install cursor --theme core
|
|
43
|
+
gamut plugin install cursor --theme percipio --force
|
|
44
|
+
gamut plugin install cursor --scope skills
|
|
45
|
+
gamut plugin install cursor --plugin-dir ./my-agent-tools
|
|
46
|
+
`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/** Directories in the plugin source that should not be installed to Cursor. */
|
|
52
|
+
const CURSOR_IGNORE = new Set([
|
|
53
|
+
'.claude-plugin', // Claude Code manifest — not a Cursor concept
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/** @param {string} sourceRoot @param {string} scope */
|
|
57
|
+
async function installCursor(sourceRoot, scope) {
|
|
58
|
+
const dest = await cursorDestPath(sourceRoot);
|
|
59
|
+
|
|
60
|
+
if ((process.env.CURSOR_INSTALL_METHOD ?? 'copy') !== 'copy') {
|
|
61
|
+
// Symlink the whole plugin dir (dev convenience)
|
|
62
|
+
await rm(dest, { recursive: true, force: true });
|
|
63
|
+
await symlink(resolve(sourceRoot), dest, 'dir');
|
|
64
|
+
log(`Cursor: symlinked to ${dest}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Selective copy: always include the cursor manifest, then scoped content dirs
|
|
69
|
+
await rm(dest, { recursive: true, force: true });
|
|
70
|
+
await mkdir(dest, { recursive: true });
|
|
71
|
+
|
|
72
|
+
await cp(`${sourceRoot}/.cursor-plugin`, `${dest}/.cursor-plugin`, {
|
|
73
|
+
recursive: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let dirs;
|
|
77
|
+
if (scope === 'all') {
|
|
78
|
+
const entries = await readdir(sourceRoot, { withFileTypes: true });
|
|
79
|
+
dirs = entries
|
|
80
|
+
.filter(
|
|
81
|
+
(e) =>
|
|
82
|
+
e.isDirectory() &&
|
|
83
|
+
!e.name.startsWith('.') &&
|
|
84
|
+
!CURSOR_IGNORE.has(e.name)
|
|
85
|
+
)
|
|
86
|
+
.map((e) => e.name);
|
|
87
|
+
} else {
|
|
88
|
+
dirs = [scope];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await Promise.all(
|
|
92
|
+
dirs.map((dir) =>
|
|
93
|
+
cp(`${sourceRoot}/${dir}`, `${dest}/${dir}`, { recursive: true }).catch(
|
|
94
|
+
() => {
|
|
95
|
+
// directory may be empty/missing — not an error
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const scopeLabel = scope === 'all' ? 'all content' : scope;
|
|
102
|
+
log(`Cursor: installed (${scopeLabel}) → ${dest}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Claude Code loads skills/, agents/ from the plugin source. rules/ is Cursor-specific
|
|
106
|
+
// (.mdc format). Claude marketplace registers the full sourceRoot.
|
|
107
|
+
|
|
108
|
+
/** @param {string} sourceRoot */
|
|
109
|
+
async function installClaude(sourceRoot) {
|
|
110
|
+
const spec = await claudePluginSpec(sourceRoot);
|
|
111
|
+
const mpName = marketplaceName(spec);
|
|
112
|
+
const root = resolve(sourceRoot);
|
|
113
|
+
|
|
114
|
+
let code = await runCommand('claude', [
|
|
115
|
+
'plugin',
|
|
116
|
+
'marketplace',
|
|
117
|
+
'add',
|
|
118
|
+
root,
|
|
119
|
+
'--scope',
|
|
120
|
+
'user',
|
|
121
|
+
]);
|
|
122
|
+
if (code !== 0) {
|
|
123
|
+
warn(
|
|
124
|
+
`warning: "claude plugin marketplace add" exited ${code} — ` +
|
|
125
|
+
`if it's already registered this is safe to ignore.`
|
|
126
|
+
);
|
|
127
|
+
code = await runCommand('claude', [
|
|
128
|
+
'plugin',
|
|
129
|
+
'marketplace',
|
|
130
|
+
'update',
|
|
131
|
+
mpName,
|
|
132
|
+
]);
|
|
133
|
+
if (code !== 0) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`claude plugin marketplace add/update failed (exit ${code}).\n` +
|
|
136
|
+
`Try manually: claude plugin marketplace add ${root}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
code = await runCommand('claude', [
|
|
142
|
+
'plugin',
|
|
143
|
+
'install',
|
|
144
|
+
spec,
|
|
145
|
+
'--scope',
|
|
146
|
+
'user',
|
|
147
|
+
]);
|
|
148
|
+
if (code !== 0) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`claude plugin install failed (exit ${code}).\n` +
|
|
151
|
+
`Try manually: claude plugin install ${spec} --scope user`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log(`Claude Code: installed ${spec} (user scope)`);
|
|
156
|
+
log(
|
|
157
|
+
` Tip: run /reload-plugins in Claude Code if skills don't appear immediately.`
|
|
158
|
+
);
|
|
159
|
+
log(` One-off without install: claude --plugin-dir ${root}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* gamut plugin install [cursor|claude] [--scope all|skills|rules|agents]
|
|
166
|
+
* [--plugin-dir <path>]
|
|
167
|
+
*
|
|
168
|
+
* @param {string[]} args
|
|
169
|
+
*/
|
|
170
|
+
export default async function install(args) {
|
|
171
|
+
const target = args.find((a) => !a.startsWith('-')) ?? 'cursor';
|
|
172
|
+
const scope = getFlag(args, '--scope', 'all') ?? 'all';
|
|
173
|
+
|
|
174
|
+
if (!TARGETS.includes(target)) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Unknown target: "${target}". Choose from: ${TARGETS.join(', ')}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (!SCOPES.includes(scope)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Unknown scope: "${scope}". Choose from: ${SCOPES.join(', ')}`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const pluginDir = await resolvePluginDir(args);
|
|
186
|
+
const theme = getFlag(args, '--theme');
|
|
187
|
+
const force = args.includes('--force');
|
|
188
|
+
|
|
189
|
+
if (theme) {
|
|
190
|
+
resolveTheme(theme);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (target === 'cursor') {
|
|
194
|
+
await installCursor(pluginDir, scope);
|
|
195
|
+
} else if (target === 'claude') {
|
|
196
|
+
await installClaude(pluginDir);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (theme) {
|
|
200
|
+
const { dest, label } = await installDesignMd(
|
|
201
|
+
pluginDir,
|
|
202
|
+
process.cwd(),
|
|
203
|
+
theme,
|
|
204
|
+
{
|
|
205
|
+
force,
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
log(`DESIGN.md: installed (${label}) → ${dest}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export { listCanonicalThemes };
|