@codecademy/gamut 68.6.0 → 68.6.1-alpha.e6c390.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 +648 -0
- package/agent-tools/DESIGN.LXStudio.md +460 -0
- package/agent-tools/DESIGN.Percipio.md +463 -0
- package/agent-tools/DESIGN.md +1 -0
- package/agent-tools/agents/.gitkeep +0 -0
- package/agent-tools/commands/gamut-review.md +170 -0
- package/agent-tools/guidelines/components/buttons.md +44 -0
- package/agent-tools/guidelines/components/overview.md +44 -0
- package/agent-tools/guidelines/foundations/color.md +86 -0
- package/agent-tools/guidelines/foundations/modes.md +47 -0
- package/agent-tools/guidelines/foundations/spacing.md +66 -0
- package/agent-tools/guidelines/foundations/typography.md +50 -0
- package/agent-tools/guidelines/overview.md +38 -0
- package/agent-tools/guidelines/setup.md +42 -0
- package/agent-tools/rules/accessibility.mdc +69 -0
- package/agent-tools/skills/gamut-accessibility/SKILL.md +239 -0
- package/agent-tools/skills/gamut-color-mode/SKILL.md +99 -0
- package/agent-tools/skills/gamut-system-props/SKILL.md +181 -0
- package/agent-tools/skills/gamut-testing/SKILL.md +181 -0
- package/agent-tools/skills/gamut-theming/SKILL.md +115 -0
- package/agent-tools/skills/gamut-typography/SKILL.md +123 -0
- package/bin/commands/plugin/install.mjs +173 -0
- package/bin/commands/plugin/list.mjs +105 -0
- package/bin/commands/plugin/remove.mjs +116 -0
- package/bin/commands/plugin/update.mjs +49 -0
- package/bin/gamut.mjs +92 -0
- package/bin/lib/claude.mjs +52 -0
- package/bin/lib/cursor.mjs +40 -0
- package/bin/lib/figma.mjs +49 -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,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamut-testing
|
|
3
|
+
description: Use this skill when writing or fixing unit tests for React components that use Gamut — setupRtl, MockGamutProvider, ColorMode in tests, 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
|
+
## Core rule: never mock Gamut components
|
|
13
|
+
|
|
14
|
+
Do not use `jest.mock('@codecademy/gamut')` or manually mock individual Gamut components in test files. This silently bypasses emotion's theme context, produces tests that can't catch real rendering failures, and requires every test file to maintain its own fragile mock list.
|
|
15
|
+
|
|
16
|
+
Use `MockGamutProvider` or `setupRtl` — both are designed for exactly this purpose.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What `MockGamutProvider` does
|
|
21
|
+
|
|
22
|
+
`MockGamutProvider` wraps `GamutProvider` with test-safe settings:
|
|
23
|
+
|
|
24
|
+
- `useCache={false}` — disables emotion's style injection cache so styles are predictable across tests
|
|
25
|
+
- `useGlobals={false}` — disables global CSS (Reboot, Typography, CSS Variables) to avoid side effects between test files
|
|
26
|
+
- `theme={coreTheme}` — provides the full Gamut token set so styled components resolve correctly
|
|
27
|
+
|
|
28
|
+
You should never construct a `GamutProvider` manually in a test. Use `MockGamutProvider`.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Decision guide
|
|
33
|
+
|
|
34
|
+
| Scenario | Use |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Standard component unit test | `setupRtl` from `@codecademy/gamut-tests` |
|
|
37
|
+
| Test that needs to vary `useLogicalProperties` | `render` + `MockGamutProvider` directly |
|
|
38
|
+
| Test that needs `ColorMode` context | `render` + `MockGamutProvider` + `<ColorMode>` |
|
|
39
|
+
| Visual test mock / Storybook wrapper | `MockGamutProvider` directly |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## `setupRtl` — preferred pattern
|
|
44
|
+
|
|
45
|
+
`setupRtl` from `@codecademy/gamut-tests` wraps `setupRtl` from `component-test-setup` with `MockGamutProvider` automatically. You do not need to add `MockGamutProvider` yourself.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { setupRtl } from '@codecademy/gamut-tests';
|
|
49
|
+
|
|
50
|
+
import { MyComponent } from '../MyComponent';
|
|
51
|
+
|
|
52
|
+
const renderView = setupRtl(MyComponent, {
|
|
53
|
+
label: 'Default label',
|
|
54
|
+
onClick: jest.fn(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders the label', () => {
|
|
58
|
+
const { view } = renderView();
|
|
59
|
+
expect(view.getByText('Default label')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('accepts prop overrides', () => {
|
|
63
|
+
const { view } = renderView({ label: 'Override' });
|
|
64
|
+
expect(view.getByText('Override')).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`renderView` returns `{ view, props, update }`:
|
|
69
|
+
- `view` — RTL `RenderResult` (getByRole, getByText, etc.)
|
|
70
|
+
- `props` — the resolved props passed to the component (useful for asserting on jest.fn() mocks)
|
|
71
|
+
- `update` — re-render with updated props without remounting
|
|
72
|
+
|
|
73
|
+
### Accessing mock functions via `props`
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
it('calls onClick when clicked', async () => {
|
|
77
|
+
const { view, props } = renderView();
|
|
78
|
+
await userEvent.click(view.getByRole('button'));
|
|
79
|
+
expect(props.onClick).toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## `MockGamutProvider` directly — when you need more control
|
|
86
|
+
|
|
87
|
+
Use `render` + `MockGamutProvider` when `setupRtl` doesn't give enough control over the wrapper — for example, when testing both logical and physical CSS property modes, or when adding `ColorMode`.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { MockGamutProvider } from '@codecademy/gamut-tests';
|
|
91
|
+
import { render } from '@testing-library/react';
|
|
92
|
+
|
|
93
|
+
it('uses logical properties when enabled', () => {
|
|
94
|
+
const { container } = render(
|
|
95
|
+
<MockGamutProvider useLogicalProperties>
|
|
96
|
+
<MyComponent width="200px" />
|
|
97
|
+
</MockGamutProvider>
|
|
98
|
+
);
|
|
99
|
+
// assert on inlineSize rather than width
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Testing both logical and physical property modes
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
describe.each([
|
|
107
|
+
{ useLogicalProperties: true, widthProp: 'inlineSize' },
|
|
108
|
+
{ useLogicalProperties: false, widthProp: 'width' },
|
|
109
|
+
])(
|
|
110
|
+
'useLogicalProperties=$useLogicalProperties',
|
|
111
|
+
({ useLogicalProperties, widthProp }) => {
|
|
112
|
+
it(`uses ${widthProp}`, () => {
|
|
113
|
+
const { container } = render(
|
|
114
|
+
<MockGamutProvider useLogicalProperties={useLogicalProperties}>
|
|
115
|
+
<MyComponent width="200px" />
|
|
116
|
+
</MockGamutProvider>
|
|
117
|
+
);
|
|
118
|
+
expect(container.firstChild).toHaveStyle({ [widthProp]: '200px' });
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Emotion style assertions
|
|
127
|
+
|
|
128
|
+
Install `@emotion/jest` matchers once per test file to enable CSS-in-JS assertions:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { matchers } from '@emotion/jest';
|
|
132
|
+
|
|
133
|
+
expect.extend(matchers);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Then assert on styles:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
expect(element).toHaveStyle({ borderRadius: '2px' });
|
|
140
|
+
expect(element).toHaveStyleRule('padding', '1rem');
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Use `theme` from `@codecademy/gamut-styles` to avoid hardcoding token values:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import { theme } from '@codecademy/gamut-styles';
|
|
147
|
+
|
|
148
|
+
expect(element).toHaveStyle({ columnGap: theme.spacing[40] });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Visual test wrappers
|
|
154
|
+
|
|
155
|
+
When creating mock components for visual tests or Storybook, wrap with `MockGamutProvider` and `ColorMode`:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { MockGamutProvider } from '@codecademy/gamut-tests';
|
|
159
|
+
import { ColorMode } from '@codecademy/gamut-styles';
|
|
160
|
+
|
|
161
|
+
export const MyComponentMock: React.FC<ComponentProps<typeof MyComponent>> = (props) => (
|
|
162
|
+
<MockGamutProvider>
|
|
163
|
+
<ColorMode mode="light">
|
|
164
|
+
<MyComponent {...props} />
|
|
165
|
+
</ColorMode>
|
|
166
|
+
</MockGamutProvider>
|
|
167
|
+
);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Common anti-patterns
|
|
173
|
+
|
|
174
|
+
| Anti-pattern | Fix |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `jest.mock('@codecademy/gamut', () => ({ ... }))` | Remove mock; use `setupRtl` or `MockGamutProvider` |
|
|
177
|
+
| `jest.mock('@codecademy/gamut-styles', ...)` | Remove mock; `MockGamutProvider` handles theme context |
|
|
178
|
+
| Wrapping with `GamutProvider` directly in tests | Use `MockGamutProvider` — it sets `useCache={false}` and `useGlobals={false}` |
|
|
179
|
+
| Repeating `render(<MockGamutProvider>...</MockGamutProvider>)` in every test | Extract with `setupRtl`; define `renderView` once above the `describe` block |
|
|
180
|
+
| One `setupRtl` call per `it` block | Define `renderView` once outside `describe`, call it inside each `it` |
|
|
181
|
+
| Asserting on raw CSS token strings | Import `theme` from `@codecademy/gamut-styles` and use `theme.spacing[n]`, `theme.fontSize`, etc. |
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamut-theming
|
|
3
|
+
description: Use this skill when working with GamutProvider themes, Emotion theme tokens, or behavior that differs across Core, Admin, Platform, LX Studio, and Percipio — including new themes, token access in styled components, or debugging theme-specific styles.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut Theming
|
|
7
|
+
|
|
8
|
+
Source: `@codecademy/gamut-styles`
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Gamut uses Emotion's theme system. All styled components have access to a typed theme object containing every design token. Themes are org-specific collections of tokens; components work across all themes without modification as long as they use token aliases rather than hardcoded values.
|
|
13
|
+
|
|
14
|
+
**See also:** [`gamut-color-mode`](../gamut-color-mode/SKILL.md) for `<ColorMode>`, `<Background>`, `useColorModes`, and contrast-safe surfaces — read that before wiring light/dark or colored sections. Storybook: [ColorMode](https://gamut.codecademy.com/?path=/docs-foundations-colormode--page), [Best practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page).
|
|
15
|
+
|
|
16
|
+
## Available themes
|
|
17
|
+
|
|
18
|
+
| Theme | Used for |
|
|
19
|
+
| --------- | ---------------------------------------------------------- |
|
|
20
|
+
| Core | Codecademy default (public-facing products) |
|
|
21
|
+
| Admin | Codecademy admin tools |
|
|
22
|
+
| Platform | Codecademy learning environment / shared platform surfaces |
|
|
23
|
+
| LX Studio | Learning Experience Studio |
|
|
24
|
+
| Percipio | Skillsoft Percipio platform |
|
|
25
|
+
|
|
26
|
+
The active theme is set at the app level via `<GamutProvider>`. Components inside receive the current theme via Emotion's context.
|
|
27
|
+
|
|
28
|
+
## Accessing tokens in styled components
|
|
29
|
+
|
|
30
|
+
### Via `css()` utility (recommended for static styles)
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { css } from '@codecademy/gamut-styles';
|
|
34
|
+
import styled from '@emotion/styled';
|
|
35
|
+
|
|
36
|
+
// Static color token
|
|
37
|
+
const Box = styled.div(css({ bg: 'navy-400', p: 4 }));
|
|
38
|
+
|
|
39
|
+
// Semantic color alias (adapts to color mode and theme)
|
|
40
|
+
const Text = styled.div(css({ color: 'primary', p: 4 }));
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Via `theme` prop (for dynamic styles)
|
|
44
|
+
|
|
45
|
+
Every Emotion styled component receives `theme` as a prop:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import styled from '@emotion/styled';
|
|
49
|
+
|
|
50
|
+
const DynamicBox = styled.div`
|
|
51
|
+
color: ${({ theme }) => theme.colors.blue};
|
|
52
|
+
font-size: ${({ theme }) => theme.fontSize[16]};
|
|
53
|
+
`;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Via imported `theme` object (outside styled components)
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { css as emotionCss } from '@emotion/react';
|
|
60
|
+
import { theme } from '@codecademy/gamut-styles';
|
|
61
|
+
|
|
62
|
+
// For use in plain CSS-in-JS outside of styled components
|
|
63
|
+
const myStyles = emotionCss`
|
|
64
|
+
font-size: ${theme.fontSize[14]};
|
|
65
|
+
color: ${theme.colors['navy-400']};
|
|
66
|
+
`;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Via `useTheme()` hook
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { useTheme } from '@emotion/react';
|
|
73
|
+
|
|
74
|
+
const MyComponent = () => {
|
|
75
|
+
const theme = useTheme();
|
|
76
|
+
return <div style={{ color: theme.colors.primary }} />;
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## System props as the primary token API
|
|
81
|
+
|
|
82
|
+
For most styling needs, use **system props** (see the `gamut-system-props` skill) rather than accessing the theme directly. System props are the idiomatic way to use design tokens in Gamut components:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { variance } from '@codecademy/variance';
|
|
86
|
+
import { system } from '@codecademy/gamut-styles';
|
|
87
|
+
|
|
88
|
+
const Card = styled.div(
|
|
89
|
+
variance.compose(system.layout, system.space, system.color)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// token scale values are validated at the type level
|
|
93
|
+
<Card bg="navy-400" p={16} width="100%" />;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Theme-aware vs. color-mode-aware
|
|
97
|
+
|
|
98
|
+
| Concern | Tool |
|
|
99
|
+
| ---------------------------------------------- | ------------------------------------------ |
|
|
100
|
+
| Tokens change per theme (colors, sizes, fonts) | Access via `theme.*` or system props |
|
|
101
|
+
| Colors change per light/dark mode | Use semantic color aliases + `<ColorMode>` |
|
|
102
|
+
| Background contrast | Use `<Background>` from ColorMode |
|
|
103
|
+
|
|
104
|
+
Semantic aliases like `primary`, `secondary`, `text`, `background` serve double duty: they resolve to theme-specific values **and** switch between light/dark variants automatically.
|
|
105
|
+
|
|
106
|
+
## Creating a new theme
|
|
107
|
+
|
|
108
|
+
See `CreatingThemes.mdx` in the styleguide (`packages/styleguide/src/lib/Foundations/Theme/CreatingThemes.mdx`) for the full guide. 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
|
+
- Prefer semantic token aliases over raw token values when the style needs to respond to color mode changes.
|
|
113
|
+
- Use raw tokens (e.g. `navy-400`) only for styles that should be fixed regardless of mode.
|
|
114
|
+
- Never hardcode hex values in styled components — always go through the theme/system props.
|
|
115
|
+
- The `GamutProvider` at the app root wires up the theme, color mode, and logical properties settings; components should not need to know which theme is active.
|
|
@@ -0,0 +1,123 @@
|
|
|
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 — even if the user does not name fonts or tokens. Covers Apercu Pro, Suisse Intl Mono, scale, line heights, line length, and alignment.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut Typography
|
|
7
|
+
|
|
8
|
+
> **Scope**: This skill covers typography for **Codecademy products** using the Core, Admin, or Platform themes (Apercu + Suisse). Percipio uses Roboto for all type — see `DESIGN.md` for Percipio-specific guidance. LX Studio uses Hanken Grotesk in place of both Apercu and Suisse.
|
|
9
|
+
|
|
10
|
+
## Typefaces
|
|
11
|
+
|
|
12
|
+
Codecademy products use two typefaces:
|
|
13
|
+
|
|
14
|
+
### Apercu Pro (`fontFamily: "base"`)
|
|
15
|
+
|
|
16
|
+
The primary typeface. Geometric-ish, humanist sans-serif. Use for:
|
|
17
|
+
- Headlines (Bold weight)
|
|
18
|
+
- Body / paragraph text (Regular weight)
|
|
19
|
+
- UI labels, menu items (Regular weight)
|
|
20
|
+
- Emphasis within body copy (Italic — **not** Bold)
|
|
21
|
+
|
|
22
|
+
**Rules:**
|
|
23
|
+
- Do not use Bold to emphasize text within a Regular-weight paragraph. Use Italic instead.
|
|
24
|
+
- Set with generous line-height for body text: **150–175%** of the type size (e.g. 16px type → 24–28px line-height).
|
|
25
|
+
- Headlines should use **100–110%** line-height to appear intentional and grouped.
|
|
26
|
+
- Text should be **left-aligned** by default.
|
|
27
|
+
|
|
28
|
+
### Suisse Intl Mono (`fontFamily: "accent"`)
|
|
29
|
+
|
|
30
|
+
Monospace accent typeface. Use sparingly for:
|
|
31
|
+
- Code snippets and inline code
|
|
32
|
+
- Numbers, figures, and statistics
|
|
33
|
+
- Captions and labels that reference technical/engineering context
|
|
34
|
+
- Enumerated lists
|
|
35
|
+
- Quotations in a technical voice
|
|
36
|
+
|
|
37
|
+
**Rules:**
|
|
38
|
+
- Every character is the same width — avoid long paragraph-length prose in Suisse.
|
|
39
|
+
- It reads large for its point size: **reduce the size by ~10–15%** relative to Apercu text at the same visual scale (e.g. 14px Suisse ≈ 16px Apercu visually).
|
|
40
|
+
- Requires extra line-height to remain readable.
|
|
41
|
+
|
|
42
|
+
## Font size scale (`fontSize`)
|
|
43
|
+
|
|
44
|
+
Sizes are accessed via the theme's `fontSize` scale. Common keys:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { css } from '@codecademy/gamut-styles';
|
|
48
|
+
import styled from '@emotion/styled';
|
|
49
|
+
|
|
50
|
+
// Via system props
|
|
51
|
+
import { system } from '@codecademy/gamut-styles';
|
|
52
|
+
const Text = styled.p(system.typography);
|
|
53
|
+
<Text fontSize={16} />;
|
|
54
|
+
|
|
55
|
+
// Via css() utility
|
|
56
|
+
const Box = styled.div(css({ fontSize: 14 }));
|
|
57
|
+
|
|
58
|
+
// Via theme directly (outside styled components)
|
|
59
|
+
import { theme } from '@codecademy/gamut-styles';
|
|
60
|
+
const size = theme.fontSize[16];
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Line heights (`lineHeight`)
|
|
64
|
+
|
|
65
|
+
Line heights are limited to **multiples of 4px**. Type boxes are placed on an **8px placement grid**.
|
|
66
|
+
|
|
67
|
+
Guidelines:
|
|
68
|
+
- Body text: 150–175% of font size
|
|
69
|
+
- Headlines: 100–110% of font size
|
|
70
|
+
|
|
71
|
+
## Line length
|
|
72
|
+
|
|
73
|
+
Controlling line length is essential for readability:
|
|
74
|
+
|
|
75
|
+
| Context | Target |
|
|
76
|
+
|---|---|
|
|
77
|
+
| Single-column body text | ~66 characters (max 85) |
|
|
78
|
+
| Multi-column layouts | ≤50 characters per line |
|
|
79
|
+
| Minimum | 45 characters |
|
|
80
|
+
|
|
81
|
+
**How to control line length**: Start with the right text style for the design, then adjust the width or column count of the text container.
|
|
82
|
+
|
|
83
|
+
## Alignment
|
|
84
|
+
|
|
85
|
+
- **Left-align** paragraphs by default — this is easiest to read and supports grid alignment.
|
|
86
|
+
- **Center-align** only for short marketing headlines or specific interface components with brief text.
|
|
87
|
+
- **Never right-align** text in normal circumstances (exceptions: numbers, equations).
|
|
88
|
+
- Do not adjust letter-spacing.
|
|
89
|
+
|
|
90
|
+
## Accessing typography tokens
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// System props (recommended for styled components)
|
|
94
|
+
import { system } from '@codecademy/gamut-styles';
|
|
95
|
+
import { variance } from '@codecademy/variance';
|
|
96
|
+
|
|
97
|
+
const Heading = styled.h2(
|
|
98
|
+
variance.compose(system.typography, system.space)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
<Heading fontSize={24} fontFamily="base" fontWeight="bold" lineHeight={1.1} mb={8} />;
|
|
102
|
+
|
|
103
|
+
// css() utility (recommended for static styles)
|
|
104
|
+
import { css } from '@codecademy/gamut-styles';
|
|
105
|
+
|
|
106
|
+
const Caption = styled.span(
|
|
107
|
+
css({ fontFamily: 'accent', fontSize: 12, color: 'secondary' })
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Theme object (outside styled components)
|
|
111
|
+
import { theme } from '@codecademy/gamut-styles';
|
|
112
|
+
import { css as emotionCss } from '@emotion/react';
|
|
113
|
+
|
|
114
|
+
const myStyles = emotionCss`
|
|
115
|
+
font-size: ${theme.fontSize[14]};
|
|
116
|
+
`;
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Semantic vs. visual sizing
|
|
120
|
+
|
|
121
|
+
- The term **"Title"** distinguishes visual size from semantic HTML hierarchy (H1–H6).
|
|
122
|
+
- A visually large title may use `<h2>` or `<p>` semantically — visual scale and semantic meaning are independent in Gamut.
|
|
123
|
+
- Choose HTML heading levels for document structure, choose font size for visual hierarchy.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, rm, symlink } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { claudePluginSpec, marketplaceName } from '../../lib/claude.mjs';
|
|
5
|
+
import { cursorDestPath } from '../../lib/cursor.mjs';
|
|
6
|
+
import { resolveFigmaOutput } from '../../lib/figma.mjs';
|
|
7
|
+
import { getFlag, resolvePluginDir } from '../../lib/resolve-plugin-dir.mjs';
|
|
8
|
+
import { runCommand } from '../../lib/run-command.mjs';
|
|
9
|
+
|
|
10
|
+
export const TARGETS = ['cursor', 'claude', 'figma'];
|
|
11
|
+
export const SCOPES = ['all', 'skills', 'rules', 'commands', 'agents'];
|
|
12
|
+
|
|
13
|
+
export function help() {
|
|
14
|
+
console.log(`
|
|
15
|
+
Usage:
|
|
16
|
+
gamut plugin install [target] [options]
|
|
17
|
+
|
|
18
|
+
Install the Gamut plugin into an AI or design tool.
|
|
19
|
+
|
|
20
|
+
Arguments:
|
|
21
|
+
target Tool to install into (default: cursor)
|
|
22
|
+
cursor | claude | figma
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--scope <scope> Content to install (default: all)
|
|
26
|
+
all | skills | rules | commands | agents
|
|
27
|
+
--output <path> [figma] Explicit destination directory for guidelines/.
|
|
28
|
+
If omitted, walks up from cwd to find figma.config.json.
|
|
29
|
+
--plugin-dir <path> Override the bundled agent-tools directory
|
|
30
|
+
-h, --help Show this help message
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
gamut plugin install
|
|
34
|
+
gamut plugin install claude
|
|
35
|
+
gamut plugin install figma
|
|
36
|
+
gamut plugin install figma --output /path/to/project/guidelines
|
|
37
|
+
gamut plugin install cursor --scope skills
|
|
38
|
+
gamut plugin install cursor --plugin-dir ./my-agent-tools
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/** Directories in the plugin source that should not be installed to Cursor. */
|
|
45
|
+
const CURSOR_IGNORE = new Set([
|
|
46
|
+
'.claude-plugin', // Claude Code manifest — not a Cursor concept
|
|
47
|
+
'guidelines', // Figma Make only
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/** @param {string} sourceRoot @param {string} scope */
|
|
51
|
+
async function installCursor(sourceRoot, scope) {
|
|
52
|
+
const dest = await cursorDestPath(sourceRoot);
|
|
53
|
+
|
|
54
|
+
if ((process.env.CURSOR_INSTALL_METHOD ?? 'copy') !== 'copy') {
|
|
55
|
+
// Symlink the whole plugin dir (dev convenience)
|
|
56
|
+
await rm(dest, { recursive: true, force: true });
|
|
57
|
+
await symlink(resolve(sourceRoot), dest, 'dir');
|
|
58
|
+
console.log(`Cursor: symlinked to ${dest}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Selective copy: always include the cursor manifest, then scoped content dirs
|
|
63
|
+
await rm(dest, { recursive: true, force: true });
|
|
64
|
+
await mkdir(dest, { recursive: true });
|
|
65
|
+
|
|
66
|
+
await cp(`${sourceRoot}/.cursor-plugin`, `${dest}/.cursor-plugin`, { recursive: true });
|
|
67
|
+
|
|
68
|
+
let dirs;
|
|
69
|
+
if (scope === 'all') {
|
|
70
|
+
const entries = await readdir(sourceRoot, { withFileTypes: true });
|
|
71
|
+
dirs = entries
|
|
72
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && !CURSOR_IGNORE.has(e.name))
|
|
73
|
+
.map((e) => e.name);
|
|
74
|
+
} else {
|
|
75
|
+
dirs = [scope];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const dir of dirs) {
|
|
79
|
+
await cp(`${sourceRoot}/${dir}`, `${dest}/${dir}`, { recursive: true }).catch(() => {
|
|
80
|
+
// directory may be empty/missing — not an error
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const scopeLabel = scope === 'all' ? 'all content' : scope;
|
|
85
|
+
console.log(`Cursor: installed (${scopeLabel}) → ${dest}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Claude Code only loads from recognized plugin directories: skills/, commands/, agents/.
|
|
89
|
+
// rules/ is Cursor-specific (.mdc format); .cursor-plugin/ and guidelines/ are also
|
|
90
|
+
// present in sourceRoot but ignored by Claude Code.
|
|
91
|
+
|
|
92
|
+
/** @param {string} sourceRoot */
|
|
93
|
+
async function installClaude(sourceRoot) {
|
|
94
|
+
const spec = await claudePluginSpec(sourceRoot);
|
|
95
|
+
const mpName = marketplaceName(spec);
|
|
96
|
+
const root = resolve(sourceRoot);
|
|
97
|
+
|
|
98
|
+
let code = await runCommand('claude', ['plugin', 'marketplace', 'add', root, '--scope', 'user']);
|
|
99
|
+
if (code !== 0) {
|
|
100
|
+
console.warn(
|
|
101
|
+
`warning: "claude plugin marketplace add" exited ${code} — ` +
|
|
102
|
+
`if it's already registered this is safe to ignore.`,
|
|
103
|
+
);
|
|
104
|
+
code = await runCommand('claude', ['plugin', 'marketplace', 'update', mpName]);
|
|
105
|
+
if (code !== 0) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`claude plugin marketplace add/update failed (exit ${code}).\n` +
|
|
108
|
+
`Try manually: claude plugin marketplace add ${root}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
code = await runCommand('claude', ['plugin', 'install', spec, '--scope', 'user']);
|
|
114
|
+
if (code !== 0) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`claude plugin install failed (exit ${code}).\n` +
|
|
117
|
+
`Try manually: claude plugin install ${spec} --scope user`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`Claude Code: installed ${spec} (user scope)`);
|
|
122
|
+
console.log(` Tip: run /reload-plugins in Claude Code if skills don't appear immediately.`);
|
|
123
|
+
console.log(` One-off without install: claude --plugin-dir ${root}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {string} sourceRoot
|
|
128
|
+
* @param {string | undefined} outputArg
|
|
129
|
+
*/
|
|
130
|
+
async function installFigma(sourceRoot, outputArg) {
|
|
131
|
+
const src = join(sourceRoot, 'guidelines');
|
|
132
|
+
const { path: dest, discovered } = await resolveFigmaOutput(outputArg);
|
|
133
|
+
|
|
134
|
+
if (discovered) {
|
|
135
|
+
console.log(`Figma: found figma.config.json — installing to ${dest}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await rm(dest, { recursive: true, force: true });
|
|
139
|
+
await cp(src, dest, { recursive: true });
|
|
140
|
+
console.log(`Figma: installed guidelines/ → ${dest}`);
|
|
141
|
+
console.log(` In Figma Make, point your kit at this guidelines/ directory for design system context.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* gamut plugin install [cursor|claude|figma] [--scope all|skills|rules|commands|agents]
|
|
148
|
+
* [--plugin-dir <path>]
|
|
149
|
+
*
|
|
150
|
+
* @param {string[]} args
|
|
151
|
+
*/
|
|
152
|
+
export default async function install(args) {
|
|
153
|
+
const target = args.find((a) => !a.startsWith('-')) ?? 'cursor';
|
|
154
|
+
const scope = getFlag(args, '--scope', 'all') ?? 'all';
|
|
155
|
+
|
|
156
|
+
if (!TARGETS.includes(target)) {
|
|
157
|
+
throw new Error(`Unknown target: "${target}". Choose from: ${TARGETS.join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
if (!SCOPES.includes(scope)) {
|
|
160
|
+
throw new Error(`Unknown scope: "${scope}". Choose from: ${SCOPES.join(', ')}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const pluginDir = await resolvePluginDir(args);
|
|
164
|
+
|
|
165
|
+
if (target === 'cursor') {
|
|
166
|
+
await installCursor(pluginDir, scope);
|
|
167
|
+
} else if (target === 'claude') {
|
|
168
|
+
await installClaude(pluginDir);
|
|
169
|
+
} else if (target === 'figma') {
|
|
170
|
+
const output = getFlag(args, '--output', undefined);
|
|
171
|
+
await installFigma(pluginDir, output);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import { cursorDestPath } from '../../lib/cursor.mjs';
|
|
4
|
+
import { findFigmaConfigDir } from '../../lib/figma.mjs';
|
|
5
|
+
import { getFlag, resolvePluginDir } from '../../lib/resolve-plugin-dir.mjs';
|
|
6
|
+
|
|
7
|
+
export function help() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Usage:
|
|
10
|
+
gamut plugin list [options]
|
|
11
|
+
|
|
12
|
+
Show installation status for all supported targets.
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--output <path> [figma] Explicit path to DESIGN.md.
|
|
16
|
+
If omitted, walks up from cwd to find figma.config.json.
|
|
17
|
+
--plugin-dir <path> Override the bundled agent-tools directory
|
|
18
|
+
-h, --help Show this help message
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
gamut plugin list
|
|
22
|
+
gamut plugin list --output ./docs/DESIGN.md
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/** @param {string} sourceRoot */
|
|
29
|
+
async function cursorStatus(sourceRoot) {
|
|
30
|
+
const dest = await cursorDestPath(sourceRoot);
|
|
31
|
+
const installed = !!(await stat(dest).catch(() => null));
|
|
32
|
+
return {
|
|
33
|
+
target: 'cursor',
|
|
34
|
+
status: installed ? '✓ installed' : '✗ not installed',
|
|
35
|
+
notes: installed ? dest : 'run: gamut plugin install cursor',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function claudeStatus() {
|
|
40
|
+
// Claude Code doesn't expose a stable filesystem path we can check.
|
|
41
|
+
return {
|
|
42
|
+
target: 'claude',
|
|
43
|
+
status: '? unknown',
|
|
44
|
+
notes: 'run: claude plugin list',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {string | undefined} outputArg */
|
|
49
|
+
async function figmaStatus(outputArg) {
|
|
50
|
+
let dest;
|
|
51
|
+
if (outputArg) {
|
|
52
|
+
dest = outputArg;
|
|
53
|
+
} else {
|
|
54
|
+
const dir = await findFigmaConfigDir(process.cwd());
|
|
55
|
+
dest = dir ? `${dir}/DESIGN.md` : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!dest) {
|
|
59
|
+
return {
|
|
60
|
+
target: 'figma',
|
|
61
|
+
status: '? unknown',
|
|
62
|
+
notes: 'figma.config.json not found — run from your project root or use --output',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const installed = !!(await stat(dest).catch(() => null));
|
|
67
|
+
return {
|
|
68
|
+
target: 'figma',
|
|
69
|
+
status: installed ? '✓ installed' : '✗ not installed',
|
|
70
|
+
notes: installed ? dest : `run: gamut plugin install figma`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* gamut plugin list
|
|
78
|
+
*
|
|
79
|
+
* Shows installation status for each supported target.
|
|
80
|
+
*
|
|
81
|
+
* @param {string[]} args
|
|
82
|
+
*/
|
|
83
|
+
export default async function list(args) {
|
|
84
|
+
const pluginDir = await resolvePluginDir(args);
|
|
85
|
+
const output = getFlag(args, '--output', undefined);
|
|
86
|
+
|
|
87
|
+
const rows = await Promise.all([
|
|
88
|
+
cursorStatus(pluginDir),
|
|
89
|
+
claudeStatus(),
|
|
90
|
+
figmaStatus(output),
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
const col0 = Math.max(...rows.map((r) => r.target.length));
|
|
94
|
+
const col1 = Math.max(...rows.map((r) => r.status.length));
|
|
95
|
+
|
|
96
|
+
const header = `${'Target'.padEnd(col0)} ${'Status'.padEnd(col1)} Path / Notes`;
|
|
97
|
+
const rule = '─'.repeat(header.length);
|
|
98
|
+
|
|
99
|
+
console.log(`\n${header}`);
|
|
100
|
+
console.log(rule);
|
|
101
|
+
for (const row of rows) {
|
|
102
|
+
console.log(`${row.target.padEnd(col0)} ${row.status.padEnd(col1)} ${row.notes}`);
|
|
103
|
+
}
|
|
104
|
+
console.log();
|
|
105
|
+
}
|