@codecademy/gamut 68.6.1-alpha.c211a2.0 → 68.6.1-alpha.df4bce.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 +444 -0
- package/agent-tools/DESIGN.Percipio.md +435 -0
- package/agent-tools/DESIGN.md +1 -0
- package/agent-tools/agents/.gitkeep +0 -0
- package/agent-tools/commands/gamut-review.md +231 -0
- package/agent-tools/guidelines/components/buttons.md +91 -0
- package/agent-tools/guidelines/components/overview.md +52 -0
- package/agent-tools/guidelines/foundations/color.md +172 -0
- package/agent-tools/guidelines/foundations/modes.md +47 -0
- package/agent-tools/guidelines/foundations/spacing.md +107 -0
- package/agent-tools/guidelines/foundations/typography.md +83 -0
- package/agent-tools/guidelines/overview.md +40 -0
- package/agent-tools/guidelines/setup.md +81 -0
- package/agent-tools/rules/accessibility.mdc +78 -0
- package/agent-tools/skills/gamut-accessibility/SKILL.md +214 -0
- package/agent-tools/skills/gamut-color-mode/SKILL.md +138 -0
- package/agent-tools/skills/gamut-forms/SKILL.md +84 -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 +113 -0
- package/agent-tools/skills/gamut-typography/SKILL.md +75 -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,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamut-system-props
|
|
3
|
+
description: Use this skill when building or refactoring styled Gamut components that need layout, spacing, color, border, background, typography, positioning, grid, flex, shadow, list styles, or responsive values from @codecademy/gamut-styles — including composing system prop groups with variance.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut System Props
|
|
7
|
+
|
|
8
|
+
Source: `@codecademy/gamut-styles` — [`variance/config.ts`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-styles/src/variance/config.ts) (definitions) and [`variance/props.ts`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-styles/src/variance/props.ts) (`variance.create` groups). **`Box`**, **`FlexBox`**, and **`GridBox`** compose the same groups in [`packages/gamut/src/Box/props.ts`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/Box/props.ts).
|
|
9
|
+
|
|
10
|
+
**See also:** [Styleguide — Best practices](../../../../styleguide/src/lib/Meta/Best%20practices.mdx) (semantic colors, `variant` / `states`, `StyleProps`, responsive examples) and Storybook [Responsive properties](https://gamut.codecademy.com/storybook/?path=/docs-foundations-system-responsive-properties--page).
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
System props are strongly-typed, theme-connected CSS prop groups from `@codecademy/gamut-styles`. They give styled components a consistent, responsive API. All props are built on top of `@codecademy/variance`.
|
|
15
|
+
|
|
16
|
+
Each prop group has:
|
|
17
|
+
|
|
18
|
+
- **`properties`**: The CSS properties it controls
|
|
19
|
+
- **`scale`**: Token scale it's restricted to (theme colors, spacing values, etc.)
|
|
20
|
+
- **`transform`**: Optional transform applied before output (e.g. `width={0.5}` → `width: 50%`)
|
|
21
|
+
|
|
22
|
+
## Basic usage
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import styled from '@emotion/styled';
|
|
26
|
+
import { system } from '@codecademy/gamut-styles';
|
|
27
|
+
|
|
28
|
+
// Apply a single group
|
|
29
|
+
const Box = styled.div(system.layout);
|
|
30
|
+
|
|
31
|
+
// Compose multiple groups
|
|
32
|
+
import { variance } from '@codecademy/variance';
|
|
33
|
+
|
|
34
|
+
const FlexBox = styled.div(
|
|
35
|
+
variance.compose(system.layout, system.flex, system.space)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
<FlexBox display="flex" p={16} gap={8} width="100%" />;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Prop groups
|
|
42
|
+
|
|
43
|
+
### `system.layout`
|
|
44
|
+
|
|
45
|
+
Controls dimensions, display, overflow, and container behavior. This group also carries **flex/grid item** props used when laying out children: `flexGrow`, `flexShrink`, `flexBasis`, `order`, `gridColumn`, `gridRow`, `gridColumnStart`, `gridRowStart`, `gridColumnEnd`, `gridRowEnd`, `alignSelf`, `justifySelf`, `gridArea`.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
const Box = styled.div(system.layout);
|
|
49
|
+
|
|
50
|
+
<Box display="flex" width="50%" height="300px" verticalAlign="middle" />;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Key props: `containerType`, `display`, `direction`, `dimensions`, `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `overflow`, `overflowX`, `overflowY`, `verticalAlign`, plus the item props above (see `config.ts` for the full map).
|
|
54
|
+
|
|
55
|
+
### `system.space`
|
|
56
|
+
|
|
57
|
+
Margin and padding using the theme's spacing scale. Supports logical properties (switches based on `useLogicalProperties` in `<GamutProvider>`).
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
const Box = styled.div(system.space);
|
|
61
|
+
|
|
62
|
+
// Single value
|
|
63
|
+
<Box p={8} m={16} />;
|
|
64
|
+
|
|
65
|
+
// Responsive (array / object — see Responsive values)
|
|
66
|
+
<Box my={[16, 24, 32]} px={[8, 16]} />;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Key props: `p`, `pt`, `pr`, `pb`, `pl`, `px`, `py`, `m`, `mt`, `mr`, `mb`, `ml`, `mx`, `my`
|
|
70
|
+
|
|
71
|
+
### `system.color`
|
|
72
|
+
|
|
73
|
+
Foreground, background, and border colors restricted to the theme's color palette.
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
const Box = styled.div(system.color);
|
|
77
|
+
|
|
78
|
+
<Box bg="navy" color="gray-900" textColor="gray-100" borderColor="blue" />;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Key props: `color`, `textColor` (both set CSS `color`), `bg`, `borderColor`, plus directional `borderColor*` variants — see `config.ts` for the full set.
|
|
82
|
+
|
|
83
|
+
### `system.typography`
|
|
84
|
+
|
|
85
|
+
Text styling connected to theme typography scales.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const Text = styled.p(system.typography);
|
|
89
|
+
|
|
90
|
+
<Text
|
|
91
|
+
fontSize={16}
|
|
92
|
+
fontFamily="accent"
|
|
93
|
+
fontStyle="italic"
|
|
94
|
+
textTransform="uppercase"
|
|
95
|
+
lineHeight="base"
|
|
96
|
+
/>;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Key props: `fontFamily`, `fontSize`, `fontWeight`, `fontStyle`, `lineHeight`, `textAlign`, `textTransform`, `textDecoration`, `letterSpacing`, `whiteSpace` — prefer **`lineHeight`** scale keys (`base`, `title`, `spacedTitle`) from the theme over raw numbers when possible.
|
|
100
|
+
|
|
101
|
+
### `system.border`
|
|
102
|
+
|
|
103
|
+
Border width, style, radius, and color. Many **logical shorthands** exist (`borderX`, `borderColorY`, `borderRadiusTop`, …); see `config.ts` for the full map.
|
|
104
|
+
|
|
105
|
+
Key props (non-exhaustive): `border`, `borderTop`, `borderRight`, `borderBottom`, `borderLeft`, `borderRadius`, `borderWidth`, `borderStyle`
|
|
106
|
+
|
|
107
|
+
### `system.background`
|
|
108
|
+
|
|
109
|
+
Background image, size, position, and repeat (for images/patterns — use `system.color` for solid background colors).
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import myBg from './myBg.png';
|
|
113
|
+
|
|
114
|
+
const Box = styled.div(system.background);
|
|
115
|
+
|
|
116
|
+
<Box
|
|
117
|
+
background={`url(${myBg})`}
|
|
118
|
+
backgroundSize="cover"
|
|
119
|
+
backgroundPosition="center"
|
|
120
|
+
/>;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Key props: `background`, `backgroundImage`, `backgroundSize`, `backgroundPosition`, `backgroundRepeat`
|
|
124
|
+
|
|
125
|
+
### `system.flex`
|
|
126
|
+
|
|
127
|
+
Flexbox child and container properties.
|
|
128
|
+
|
|
129
|
+
Key props (non-exhaustive): `flex`, `flexDirection`, `flexWrap`, `flexGrow`, `flexShrink`, `flexBasis`, `alignItems`, `alignContent`, `alignSelf`, `justifyContent`, `justifyItems`, `justifySelf`, `gap`, `rowGap`, `columnGap`
|
|
130
|
+
|
|
131
|
+
### `system.grid`
|
|
132
|
+
|
|
133
|
+
CSS Grid container and child properties.
|
|
134
|
+
|
|
135
|
+
Key props (non-exhaustive): `gridTemplateColumns`, `gridTemplateRows`, `gridTemplateAreas`, `gridColumn`, `gridRow`, `gridArea`, `gridAutoFlow`, `gridAutoColumns`, `gridAutoRows`, `gap`, `rowGap`, `columnGap`
|
|
136
|
+
|
|
137
|
+
### `system.positioning`
|
|
138
|
+
|
|
139
|
+
Position and offset properties. Inset shorthands use **`transformSize`**; physical vs logical edges follow **`useLogicalProperties`**.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
const Overlay = styled.div(variance.compose(system.layout, system.positioning));
|
|
143
|
+
|
|
144
|
+
<Overlay position="absolute" top={0} left={0} width="100%" height="100%" />;
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Key props: `position`, `inset`, `top`, `right`, `bottom`, `left`, `zIndex`, `opacity`
|
|
148
|
+
|
|
149
|
+
### `system.shadow`
|
|
150
|
+
|
|
151
|
+
Box and text shadow.
|
|
152
|
+
|
|
153
|
+
Key props: `boxShadow`, `textShadow`
|
|
154
|
+
|
|
155
|
+
### `system.list`
|
|
156
|
+
|
|
157
|
+
List marker styling (`listStyle`, `listStyleType`, `listStylePosition`, `listStyleImage`). Included on **`Box`** alongside the other composed groups.
|
|
158
|
+
|
|
159
|
+
## Responsive values
|
|
160
|
+
|
|
161
|
+
All system props accept responsive values **mobile-first** (min-width queries). Two shapes are supported:
|
|
162
|
+
|
|
163
|
+
### Object syntax
|
|
164
|
+
|
|
165
|
+
Keys are breakpoints; **`_`** is the base (no breakpoint). Includes `xs`, `sm`, `md`, `lg`, `xl`, and container keys `c_xs` … `c_xl`.
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
<Box width={{ _: '100%', sm: '50%', md: '33%' }} px={{ _: 8, md: 16 }} />
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Array syntax
|
|
172
|
+
|
|
173
|
+
Slots map in order to: base, `xs`, `sm`, `md`, `lg`, `xl`, then `c_xs` … `c_xl`. Leave a slot **empty** (or use `undefined`) to skip a breakpoint.
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
<Box width={['100%', , '50%']} p={[8, 16, , 24]} />
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Full typings and behavior: [Responsive properties (Storybook)](https://gamut.codecademy.com/storybook/?path=/docs-foundations-system-responsive-properties--page).
|
|
180
|
+
|
|
181
|
+
## Using `css()` for styled definitions
|
|
182
|
+
|
|
183
|
+
For static styles in styled components, use **`css()`** from `@codecademy/gamut-styles` (same implementation as **`system.css`** on the `system` namespace).
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { css } from '@codecademy/gamut-styles';
|
|
187
|
+
import styled from '@emotion/styled';
|
|
188
|
+
|
|
189
|
+
// Static color using raw token
|
|
190
|
+
const Box = styled.div(css({ bg: 'navy-400', p: 4 }));
|
|
191
|
+
|
|
192
|
+
// Semantic color (adapts to color mode)
|
|
193
|
+
const Text = styled.div(css({ color: 'primary', p: 4 }));
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Key principles
|
|
197
|
+
|
|
198
|
+
- Compose `system.*` groups via `variance.compose()` — don't apply multiple groups by chaining `styled.div(system.a)(system.b)`.
|
|
199
|
+
- Prefer **semantic color names** on `system.color` (e.g. `bg="background"`, `textColor="text"`) so values track **ColorMode**; use raw palette keys only when the design should stay fixed across modes.
|
|
200
|
+
- Use **`bg`** with semantic tokens for most mode-aware surfaces; use **`<Background>`** from `@codecademy/gamut-styles` when you need its **contrast- and mode-aware** behavior, not for every tinted panel.
|
|
201
|
+
- Use `system.space` values on the **spacing scale** rather than arbitrary pixel strings to keep rhythm consistent.
|
|
202
|
+
- For background images/patterns use `system.background`; for solid fills use `system.color` / semantic **`bg`** (or **`Background`** when that component’s behavior is required).
|
|
203
|
+
- For reusable **variants** or **boolean states** on styled primitives, use **`variant`** / **`states`** from `@codecademy/gamut-styles` and expose props with **`StyleProps<typeof …>`** from `@codecademy/variance` — see [Best practices](../../../../styleguide/src/lib/Meta/Best%20practices.mdx).
|
|
@@ -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,113 @@
|
|
|
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
|
+
## Available themes
|
|
15
|
+
|
|
16
|
+
| Theme | Used for |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Core | Codecademy default (public-facing products) |
|
|
19
|
+
| Admin | Codecademy admin tools |
|
|
20
|
+
| Platform | Codecademy learning environment / shared platform surfaces |
|
|
21
|
+
| LX Studio | Learning Experience Studio |
|
|
22
|
+
| Percipio | Skillsoft Percipio platform |
|
|
23
|
+
|
|
24
|
+
The active theme is set at the app level via `<GamutProvider>`. Components inside receive the current theme via Emotion's context.
|
|
25
|
+
|
|
26
|
+
## Accessing tokens in styled components
|
|
27
|
+
|
|
28
|
+
### Via `css()` utility (recommended for static styles)
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { css } from '@codecademy/gamut-styles';
|
|
32
|
+
import styled from '@emotion/styled';
|
|
33
|
+
|
|
34
|
+
// Static color token
|
|
35
|
+
const Box = styled.div(css({ bg: 'navy-400', p: 4 }));
|
|
36
|
+
|
|
37
|
+
// Semantic color alias (adapts to color mode and theme)
|
|
38
|
+
const Text = styled.div(css({ color: 'primary', p: 4 }));
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Via `theme` prop (for dynamic styles)
|
|
42
|
+
|
|
43
|
+
Every Emotion styled component receives `theme` as a prop:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import styled from '@emotion/styled';
|
|
47
|
+
|
|
48
|
+
const DynamicBox = styled.div`
|
|
49
|
+
color: ${({ theme }) => theme.colors.blue};
|
|
50
|
+
font-size: ${({ theme }) => theme.fontSize[16]};
|
|
51
|
+
`;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Via imported `theme` object (outside styled components)
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { css as emotionCss } from '@emotion/react';
|
|
58
|
+
import { theme } from '@codecademy/gamut-styles';
|
|
59
|
+
|
|
60
|
+
// For use in plain CSS-in-JS outside of styled components
|
|
61
|
+
const myStyles = emotionCss`
|
|
62
|
+
font-size: ${theme.fontSize[14]};
|
|
63
|
+
color: ${theme.colors['navy-400']};
|
|
64
|
+
`;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Via `useTheme()` hook
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { useTheme } from '@emotion/react';
|
|
71
|
+
|
|
72
|
+
const MyComponent = () => {
|
|
73
|
+
const theme = useTheme();
|
|
74
|
+
return <div style={{ color: theme.colors.primary }} />;
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## System props as the primary token API
|
|
79
|
+
|
|
80
|
+
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:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { variance } from '@codecademy/variance';
|
|
84
|
+
import { system } from '@codecademy/gamut-styles';
|
|
85
|
+
|
|
86
|
+
const Card = styled.div(
|
|
87
|
+
variance.compose(system.layout, system.space, system.color)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// token scale values are validated at the type level
|
|
91
|
+
<Card bg="navy-400" p={16} width="100%" />;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Theme-aware vs. color-mode-aware
|
|
95
|
+
|
|
96
|
+
| Concern | Tool |
|
|
97
|
+
|---|---|
|
|
98
|
+
| Tokens change per theme (colors, sizes, fonts) | Access via `theme.*` or system props |
|
|
99
|
+
| Colors change per light/dark mode | Use semantic color aliases + `<ColorMode>` |
|
|
100
|
+
| Background contrast | Use `<Background>` from ColorMode |
|
|
101
|
+
|
|
102
|
+
Semantic aliases like `primary`, `secondary`, `text`, `background` serve double duty: they resolve to theme-specific values **and** switch between light/dark variants automatically.
|
|
103
|
+
|
|
104
|
+
## Creating a new theme
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
108
|
+
## Key principles
|
|
109
|
+
|
|
110
|
+
- Prefer semantic token aliases over raw token values when the style needs to respond to color mode changes.
|
|
111
|
+
- Use raw tokens (e.g. `navy-400`) only for styles that should be fixed regardless of mode.
|
|
112
|
+
- Never hardcode hex values in styled components — always go through the theme/system props.
|
|
113
|
+
- 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,75 @@
|
|
|
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). Agent guideline: [foundations/typography.md](../../guidelines/foundations/typography.md).
|
|
9
|
+
|
|
10
|
+
## Scope by theme
|
|
11
|
+
|
|
12
|
+
| Themes | Fonts | `fontWeight.title` |
|
|
13
|
+
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
|
14
|
+
| **Core**, **Admin**, **Platform** | `base` → Apercu stack; `accent` → Suisse + Apercu stack | **700** |
|
|
15
|
+
| **Percipio**, **LX Studio** | `base` → Skillsoft Text; `accent` → Skillsoft Sans; Percipio `monospace` → Roboto Mono; LX `monospace` matches Core stack per theme file | **500** |
|
|
16
|
+
|
|
17
|
+
Use **`fontWeight="title"`** for headlines / emphasis roles — never hardcode **`700`** on Percipio/LX unless SPECIFICALLY noted in Figma designs.
|
|
18
|
+
|
|
19
|
+
## Font size scale (`fontSize`)
|
|
20
|
+
|
|
21
|
+
Theme keys: `64`, `44`, `34`, `26`, `22`, `20`, `18`, `16`, `14`.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { css } from '@codecademy/gamut-styles';
|
|
25
|
+
import styled from '@emotion/styled';
|
|
26
|
+
import { system } from '@codecademy/gamut-styles';
|
|
27
|
+
|
|
28
|
+
const Paragraph = styled.p(system.typography);
|
|
29
|
+
<Paragraph fontSize={16} lineHeight="base" />;
|
|
30
|
+
|
|
31
|
+
const Styled = styled.div(css({ fontSize: 14, fontFamily: 'base' }));
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Line height (`lineHeight`)
|
|
35
|
+
|
|
36
|
+
Tokens: **`base`** (1.5), **`spacedTitle`** (1.3), **`title`** (1.2). Prefer tokens over raw decimals. Only specify `lineHeight` when specified by design.
|
|
37
|
+
|
|
38
|
+
## Line length
|
|
39
|
+
|
|
40
|
+
| Context | Target |
|
|
41
|
+
| ------------------ | ------------------------ |
|
|
42
|
+
| Single-column body | ~66 characters (max ~85) |
|
|
43
|
+
| Multi-column | ≤50 characters per line |
|
|
44
|
+
| Minimum | ~45 characters |
|
|
45
|
+
|
|
46
|
+
## Accessing typography tokens
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { system } from '@codecademy/gamut-styles';
|
|
50
|
+
import { variance } from '@codecademy/variance';
|
|
51
|
+
|
|
52
|
+
const Heading = styled.h2(variance.compose(system.typography, system.space));
|
|
53
|
+
|
|
54
|
+
<Heading
|
|
55
|
+
fontSize={26}
|
|
56
|
+
fontFamily="base"
|
|
57
|
+
fontWeight="title"
|
|
58
|
+
lineHeight="title"
|
|
59
|
+
mb={8}
|
|
60
|
+
/>;
|
|
61
|
+
|
|
62
|
+
import { css } from '@codecademy/gamut-styles';
|
|
63
|
+
|
|
64
|
+
const Caption = styled.span(
|
|
65
|
+
css({ fontFamily: 'accent', fontSize: 14, color: 'text-secondary' })
|
|
66
|
+
);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Prefer `<Text>` from `@codecademy/gamut` with `variant` / `as` — see Storybook [Typography / Text](https://gamut.codecademy.com/?path=/docs-typography-text--docs).
|
|
70
|
+
|
|
71
|
+
## Semantic vs visual headings
|
|
72
|
+
|
|
73
|
+
- `<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).
|
|
74
|
+
- 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">`.
|
|
75
|
+
- Still pick **`h1`–`h6`** for document structure and assistive tech; overrides are for intentional divergence between semantics and appearance.
|