Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b549159
bumped react, react-dom, types, and types/react-dom
LinKCoding Jan 22, 2026
cedf8b7
fix linting issue
LinKCoding Jan 22, 2026
ac4a529
bumped react-test-renderer types and fixed SVG linting errors
LinKCoding Jan 22, 2026
57dc037
resolve linting issue with patterns
LinKCoding Jan 22, 2026
d6ac116
more linting issues
LinKCoding Jan 22, 2026
92bb265
another try to address same linting issue
LinKCoding Jan 22, 2026
b9ac907
added resolution since older packages aren't playing nice with their …
LinKCoding Jan 22, 2026
c63f3f9
misc react 19 type fixes
LinKCoding Jan 22, 2026
c722e00
added JSX to gamut-tests index file
LinKCoding Jan 22, 2026
11fa4b2
fix snapshot tests and update snapshots
LinKCoding Jan 22, 2026
d3a90cd
cleaned up tests for assetprovider
LinKCoding Jan 22, 2026
d726437
more cleanup
LinKCoding Jan 22, 2026
7eb54ba
DRY up code and clean up
LinKCoding Jan 22, 2026
91246ad
update peerDependencies
LinKCoding Jan 23, 2026
d950213
update peerDependencies with types
LinKCoding Jan 23, 2026
8afef4a
revert back to react 18 types
LinKCoding Jan 23, 2026
ccfb162
revert some types to support react 18
LinKCoding Jan 23, 2026
f99ea8c
more lint fixes
LinKCoding Jan 23, 2026
41992e0
chore: 18 + 19
dreamwasp Feb 5, 2026
1d3ea6d
test resize config
dreamwasp Feb 5, 2026
56580dc
test resize script
dreamwasp Feb 6, 2026
520bf3b
fix refs
dreamwasp Feb 6, 2026
f528271
test legacy ref build
dreamwasp Feb 6, 2026
9da2126
more messing around
dreamwasp Feb 6, 2026
c6c5268
test null issues
dreamwasp Feb 6, 2026
6ecfe45
compat testr
dreamwasp Feb 6, 2026
e31ecf9
box flexbox gridbox nonul
dreamwasp Feb 6, 2026
5d16f59
next round of changes
dreamwasp Feb 6, 2026
9c81dd1
Merge branch 'main' into cass-gmt-1473
dreamwasp Feb 9, 2026
1d7b933
remove 17 support;
dreamwasp Feb 9, 2026
28ffb75
Merge branch 'cass-gmt-1473' of github.com:Codecademy/gamut into cass…
dreamwasp Feb 9, 2026
1c88467
upd8 motion
dreamwasp Feb 9, 2026
4e56ec4
build
dreamwasp Feb 9, 2026
e067f43
test more
dreamwasp Feb 9, 2026
3d45c8a
move things over
dreamwasp Feb 10, 2026
36f3467
start with form + connectedform stuff
dreamwasp Feb 10, 2026
2b141db
input + text
dreamwasp Feb 10, 2026
6655270
ref issue
dreamwasp Feb 11, 2026
03db3d9
Merge branch 'main' into cass-gmt-1473
dreamwasp Feb 11, 2026
c7ed890
ref fix
dreamwasp Feb 11, 2026
2817c66
Merge branch 'cass-gmt-1473' of github.com:Codecademy/gamut into cass…
dreamwasp Feb 11, 2026
33c2b0b
fix return type
dreamwasp Feb 11, 2026
5cae12a
ref polyfill
dreamwasp Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions docs/react-19-typecheck-response-to-mono.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# React 19 typecheck – Gamut response to Mono (portal-app)

**Purpose:** Status of Gamut changes from Mono’s React 19 typecheck feedback and what Mono should do on their side.

**Your last test:** 2025-02-09, `@codecademy/gamut-kit` **0.6.581-alpha.9fa994.0** (and synced gamut packages at `*-alpha.9fa994.0`). That alpha introduced **35 additional errors** (~86 → ~121); we’ve fixed the regression (see below).

---

## Regression fix (alpha.9fa994.0 → next alpha)

Alpha **9fa994.0** added ~35 errors because the ref type was **intersected** with the styled component’s existing ref (`ComponentProps<typeof StyledBox>` etc.), which can be non-null and narrowed the type so `RefObject<El | null>` was no longer assignable.

**Fix in Gamut:** We now **omit `ref`** before adding `RefAttributes<El | null>` everywhere:

- **Box / FlexBox / GridBox** – default overload uses `Omit<ComponentProps<typeof StyledBox>, 'ref'> & RefAttributes<HTMLDivElement | null>` (and same for FlexBox/GridBox).
- **Form / Input / TextArea** – export cast uses `Omit<FormProps, 'ref'> & RefAttributes<...>` (and same for InputWrapperProps / TextAreaProps).

The next alpha after 9fa994.0 will include this; you should see the regression disappear and ref nullability work as intended.

---

## What’s in Gamut (main) – next alpha after 9fa994.0

These are implemented in Gamut and will ship in the next alpha you pick up.

### 1. Box / FlexBox / GridBox – intrinsic props when `as="img"` / `as="form"` / `as="input"` (~55 errors)

- **Done in Gamut.** Polymorphic call signatures so intrinsic `as` gets the right HTML props:
- `as="img"` → `src`, `alt`, `loading`, etc.
- `as="form"` → `action`, `method`, etc.
- `as="input"` → `name`, `type`, etc.
- **Files:** `Box/Box.tsx`, `Box/FlexBox.tsx`, `Box/GridBox.tsx`.

### 2. Ref nullability – accept `RefObject<T | null>` (~25 errors)

- **Done in Gamut** for the components you listed:
- **Box / FlexBox / GridBox** – ref in polymorphic typings is `RefAttributes<... | null>`.
- **Form** – `RefAttributes<HTMLFormElement | null>` (and ConnectedForm overload).
- **ButtonBase** – `RefAttributes<ButtonBaseElements | null>`.
- **Anchor** – `RefAttributes<HTMLAnchorElement | HTMLButtonElement | null>`.
- **Card** – `forwardRef<HTMLDivElement | null, CardProps>`.
- **TextArea** – export cast to `RefAttributes<HTMLTextAreaElement | null>`.
- **Input** – export cast to `RefAttributes<HTMLInputElement | null>`.

### 3. Anchor / Button polymorphic ref – union, not intersection (3 errors)

- **Done in Gamut.** You confirmed this is fixed at alpha.26b7a6.0 (VideoDescription, AppHeaderLink, AppHeaderNavButton). Ref is a union so you can pass either anchor or button ref.

---

## What we’re not changing in Gamut

### 4. Styled Input – CardDetails assignability (2 errors)

- **No Gamut change.** We are not adding a `style` prop to `StyledInputProps`. Fix on your side: type your styled Input wrapper with the component’s actual inferred type (or a cast), not `StyledComponent<StyledInputProps, any, {}>`.

### 5. useRef / overload – nullable refs (3 errors)

- **5a. DropdownList ref array** – Not in Gamut. Widen the ref-array type in the hook/component that consumes it (Mono or the package that defines it) to accept `RefObject<HTMLAnchorElement | null>[]`.
- **5b. NotificationsDropdown / useHeaderNotifications** – ButtonBase already accepts `RefObject<ButtonBaseElements | null>`. If the dropdown/hook prop type is still non-null, that type likely lives in **gamut-kit**; we can widen it there. Otherwise widen it in Mono.

### 6. CourseCardProps – `isProUser` (1 error)

- **No Gamut change.** `CourseCard` / `CourseCardProps` are not in the Gamut repo. Add `isProUser` to the component’s props in the package that defines it, or stop passing it.

---

## What you should do in Mono

1. **Bump** to the next Gamut alpha that includes the commits for items 1 and 2 (and TextArea/Input ref casts) once it’s published.
2. **Re-run** portal-app typecheck:
`yarn exec tsc --noEmit -p apps/portal-app`
3. **CardDetails (item 4):** Type your styled Input wrapper using the actual styled component type or a cast; don’t rely on `StyledInputProps` + `style`.
4. **DropdownList (5a) / NotificationsDropdown (5b):** Widen ref types to `RefObject<... | null>` (and nullable ref arrays) in the defining package (Mono or gamut-kit).
5. **CourseCard (item 6):** Add `isProUser` to the defining component’s props type, or stop passing it.

If anything still fails after bumping to the next alpha, share the exact error and file/line and we can adjust.
27 changes: 13 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"@emotion/styled": "11.14.0",
"@vidstack/react": "^1.12.12",
"core-js": "3.7.0",
"lodash": "^4.17.23",
"react": "18.3.1",
"react-dom": "18.3.1",
"lodash": "^4.17.5",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-helmet-async": "^2.0.5"
},
"devDependencies": {
Expand Down Expand Up @@ -44,18 +44,17 @@
"@storybook/react-webpack5": "^8.6.15",
"@storybook/theming": "^8.6.15",
"@svgr/cli": "5.5.0",
"@testing-library/dom": "^8.11.1",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "15.0.6",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.5.2",
"@types/classnames": "2.2.10",
"@types/invariant": "2.2.29",
"@types/konami-code-js": "^0.8.0",
"@types/lodash": "4.17.23",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"@types/react-test-renderer": "^17.0.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/react-test-renderer": "^19.1.0",
"@types/stylis": "^4.2.0",
"@typescript-eslint/eslint-plugin": "^5.15.0",
"@typescript-eslint/parser": "^5.15.0",
Expand Down Expand Up @@ -84,7 +83,7 @@
"nx-cloud": "^19.1.0",
"onchange": "^7.0.2",
"prettier": "^2.8.7",
"react-test-renderer": "18.3.1",
"react-test-renderer": "^19.0.0",
"storybook": "^8.6.15",
"storybook-addon-deep-controls": "^0.9.5",
"style-loader": "^4.0.0",
Expand Down Expand Up @@ -114,10 +113,10 @@
"repository": "git@github.com:Codecademy/gamut.git",
"resolutions": {
"@typescript-eslint/utils": "^5.15.0",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"error-ex": "1.3.4"
},
"scripts": {
Expand Down
5 changes: 3 additions & 2 deletions packages/gamut-icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
"peerDependencies": {
"@emotion/react": "^11.4.0",
"@emotion/styled": "^11.3.0",
"lodash": "^4.17.23",
"react": "^17.0.2 || ^18.2.0"
"@types/react": "^18.0.0 || ^19.0.0",
"lodash": "^4.17.5",
"react": "^18.2.0 || ^19.0.0"
},
"publishConfig": {
"access": "public"
Expand Down
7 changes: 5 additions & 2 deletions packages/gamut-icons/src/props.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { styledOptions, system } from '@codecademy/gamut-styles';
import { StyleProps, variance } from '@codecademy/variance';
import styled from '@emotion/styled';
import styled, { StyledComponent } from '@emotion/styled';

export interface IconStyleProps extends StyleProps<typeof iconProps> {
/**
Expand Down Expand Up @@ -46,4 +46,7 @@ export const iconProps = variance.compose(
system.border
);

export const Svg = styled('svg', styledOptions<'svg'>())(iconProps);
export const Svg = styled(
'svg',
styledOptions<'svg'>()
)(iconProps) as StyledComponent<GamutBaseIconProps, {}, {}>;
5 changes: 3 additions & 2 deletions packages/gamut-illustrations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"peerDependencies": {
"@emotion/react": "^11.4.0",
"@emotion/styled": "^11.3.0",
"react": "^17.0.2 || ^18.2.0",
"react-dom": "^17.0.2 || ^18.2.0"
"@types/react": "^18.0.0 || ^19.0.0",
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0"
},
"publishConfig": {
"access": "public"
Expand Down
5 changes: 3 additions & 2 deletions packages/gamut-patterns/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
"peerDependencies": {
"@emotion/react": "^11.4.0",
"@emotion/styled": "^11.3.0",
"react": "^17.0.2 || ^18.2.0",
"react-dom": "^17.0.2 || ^18.2.0"
"@types/react": "^18.0.0 || ^19.0.0",
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0"
},
"publishConfig": {
"access": "public"
Expand Down
8 changes: 4 additions & 4 deletions packages/gamut-patterns/src/props.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { styledOptions, system } from '@codecademy/gamut-styles';
import { StyleProps, variance } from '@codecademy/variance';
import styled from '@emotion/styled';
import { ComponentProps, forwardRef } from 'react';
import styled, { StyledComponent } from '@emotion/styled';
import { forwardRef } from 'react';

const patternStyles = variance.compose(
system.layout,
Expand All @@ -24,9 +24,9 @@ export interface PatternProps
const StyledSvg = styled(
'svg',
styledOptions<'svg'>()
)<PatternProps>(patternStyles);
)(patternStyles) as StyledComponent<PatternProps, {}, {}>;

export const Svg = forwardRef<SVGSVGElement, ComponentProps<typeof StyledSvg>>(
export const Svg = forwardRef<SVGSVGElement, PatternProps>(
({ height = '100%', width = '100%', ...rest }, ref) => (
<StyledSvg height={height} ref={ref} width={width} {...rest} />
)
Expand Down
5 changes: 3 additions & 2 deletions packages/gamut-styles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.0",
"@emotion/styled": "^11.3.0",
"lodash": "^4.17.23",
"react": "^17.0.2 || ^18.2.0",
"@types/react": "^18.0.0 || ^19.0.0",
"lodash": "^4.17.5",
"react": "^18.2.0 || ^19.0.0",
"stylis": "^4.0.7"
},
"publishConfig": {
Expand Down
40 changes: 20 additions & 20 deletions packages/gamut-styles/src/__tests__/AssetProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { render } from '@testing-library/react';

import { AssetProvider, createFontLinks } from '../AssetProvider';
import { coreTheme, percipioTheme } from '../themes';
import { cleanupPreloadLinks, getPreloadLinks, mockGetFonts } from './helpers';

const renderView = setupRtl(AssetProvider, {});

Expand Down Expand Up @@ -43,11 +44,10 @@ jest.mock('../remoteAssets/fonts', () => ({
},
}));

const mockGetFonts = require('../utils/fontUtils').getFonts;

describe('AssetProvider', () => {
beforeEach(() => {
jest.clearAllMocks();
cleanupPreloadLinks();
});

describe('createFontLinks', () => {
Expand All @@ -66,7 +66,7 @@ describe('AssetProvider', () => {
];

const { container } = render(<>{createFontLinks(fonts)}</>);
const links = container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(container);

expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
Expand All @@ -80,13 +80,13 @@ describe('AssetProvider', () => {

it('should handle empty fonts array', () => {
const { container } = render(<>{createFontLinks([])}</>);
const links = container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(container);
expect(links).toHaveLength(0);
});

it('should handle undefined fonts parameter', () => {
const { container } = render(<>{createFontLinks(undefined)}</>);
const links = container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(container);
expect(links).toHaveLength(2);
});

Expand All @@ -110,7 +110,7 @@ describe('AssetProvider', () => {
];

const { container } = render(<>{createFontLinks(fonts)}</>);
const links = container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(container);
expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
'href',
Expand Down Expand Up @@ -139,7 +139,7 @@ describe('AssetProvider', () => {
];

const { container } = render(<>{createFontLinks(fonts)}</>);
const links = container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(container);
expect(links).toHaveLength(2);
});
});
Expand All @@ -155,7 +155,7 @@ describe('AssetProvider', () => {
]);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(view.container);

expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
Expand All @@ -175,7 +175,7 @@ describe('AssetProvider', () => {
]);

const { view } = renderView({ theme: percipioTheme as any });
const links = view.container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(view.container);

expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
Expand All @@ -186,7 +186,7 @@ describe('AssetProvider', () => {
});

it('should handle theme without name property', () => {
const themeWithoutName = { ...coreTheme, name: undefined };
const themeWithoutName = { ...coreTheme, name: undefined } as any;
mockGetFonts.mockReturnValue([]);

renderView({ theme: themeWithoutName });
Expand All @@ -207,31 +207,31 @@ describe('AssetProvider', () => {
});

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(view.container);
expect(links).toHaveLength(0);
});

it('should fallback to core fonts when getFonts returns undefined', () => {
mockGetFonts.mockReturnValue(undefined);
mockGetFonts.mockReturnValue(undefined as any);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(view.container);
expect(links).toHaveLength(2);
});

it('should fallback to core fonts when getFonts returns null', () => {
mockGetFonts.mockReturnValue(null);
mockGetFonts.mockReturnValue(null as any);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(view.container);
expect(links).toHaveLength(2);
});

it('should fallback to core fonts when getFonts returns non-array', () => {
mockGetFonts.mockReturnValue('not-an-array');
mockGetFonts.mockReturnValue('not-an-array' as any);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(view.container);
expect(links).toHaveLength(0);
});

Expand All @@ -255,7 +255,7 @@ describe('AssetProvider', () => {
]);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(view.container);

expect(links).toHaveLength(3);
expect(links[0]).toHaveAttribute(
Expand Down Expand Up @@ -292,7 +292,7 @@ describe('AssetProvider', () => {
]);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(view.container);

expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
Expand Down Expand Up @@ -321,7 +321,7 @@ describe('AssetProvider', () => {
]);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
const links = getPreloadLinks(view.container);

expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
Expand Down
Loading
Loading