From 0e8235ce54fe5cbb922733c1dbd8cc663802768a Mon Sep 17 00:00:00 2001 From: Toppanto Bence Date: Tue, 22 Sep 2026 11:03:29 +0200 Subject: [PATCH] chore(many): freeze Alert and Pill v2 against design tokens v1 Alert v2 and Pill v2 now resolve their component tokens from a frozen snapshot of design tokens v1 instead of the live theme, so their styling stays put while the shared token set keeps moving. Both pass frozenThemesDesignTokensV1 to withStyleNew and type themeOverride against DesignTokensV1ComponentTypes. For a frozen component, themeOverride.components now accepts either the frozen token shape or the current one, but rejects a mix of the two. Keys from the wrong shape used to typecheck and then get silently ignored at runtime. Bumps @instructure/instructure-design-tokens from v1.5.0 to v1.9.0. Co-Authored-By: Claude Opus 5 (1M context) --- docs/contributing/freezing-themes.md | 83 +++++++++++++++++++ packages/emotion/src/useStyleNew.ts | 2 +- packages/emotion/src/withStyleNew.tsx | 2 +- packages/ui-alerts/src/Alert/v2/index.tsx | 3 +- packages/ui-alerts/src/Alert/v2/props.ts | 7 +- packages/ui-alerts/src/Alert/v2/styles.ts | 5 +- packages/ui-pill/src/Pill/v2/index.tsx | 3 +- packages/ui-pill/src/Pill/v2/props.ts | 4 +- packages/ui-pill/src/Pill/v2/styles.ts | 4 +- packages/ui-scripts/package.json | 2 +- .../src/Select/__tests__/Select.test.tsx | 15 +++- packages/ui-themes/src/index.ts | 34 +++++++- .../designTokensV1/components/alert/canvas.ts | 51 ++++++++++++ .../components/alert/canvasHighContrast.ts | 51 ++++++++++++ .../designTokensV1/components/alert/dark.ts | 51 ++++++++++++ .../designTokensV1/components/alert/light.ts | 51 ++++++++++++ .../designTokensV1/components/alert/type.ts | 50 +++++++++++ .../designTokensV1/components/index.ts | 56 +++++++++++++ .../designTokensV1/components/pill/canvas.ts | 51 ++++++++++++ .../components/pill/canvasHighContrast.ts | 51 ++++++++++++ .../designTokensV1/components/pill/dark.ts | 51 ++++++++++++ .../designTokensV1/components/pill/light.ts | 51 ++++++++++++ .../designTokensV1/components/pill/type.ts | 50 +++++++++++ .../frozenThemes/designTokensV1/index.ts | 76 +++++++++++++++++ .../frozenThemes/designTokensV1/primitives.ts | 47 +++++++++++ .../frozenThemes/designTokensV1/semantics.ts | 46 ++++++++++ .../designTokensV1/sharedTokens.ts | 35 ++++++++ .../src/themes/frozenThemes/index.ts | 28 +++++++ pnpm-lock.yaml | 18 ++-- 29 files changed, 946 insertions(+), 32 deletions(-) create mode 100644 docs/contributing/freezing-themes.md create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/components/alert/canvas.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/components/alert/canvasHighContrast.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/components/alert/dark.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/components/alert/light.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/components/alert/type.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/components/index.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/components/pill/canvas.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/components/pill/canvasHighContrast.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/components/pill/dark.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/components/pill/light.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/components/pill/type.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/index.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/primitives.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/semantics.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/designTokensV1/sharedTokens.ts create mode 100644 packages/ui-themes/src/themes/frozenThemes/index.ts diff --git a/docs/contributing/freezing-themes.md b/docs/contributing/freezing-themes.md new file mode 100644 index 0000000000..54308f33e0 --- /dev/null +++ b/docs/contributing/freezing-themes.md @@ -0,0 +1,83 @@ +--- +title: Freezing themes +category: Contributing +order: 9 +--- + +# Freezing themes + +## Why freeze? + +Component themes in the new theming system are generated from `@instructure/instructure-design-tokens`. That package has its own semantic version, so a major bump can rename, reshape, or drop tokens a component relies on. + +If a component's theme variables change in a way that breaks the component, we freeze the component's current theme structure. The component keeps resolving its tokens from that frozen snapshot, so it renders the same after the breaking token change. This is the theming counterpart of the [Multi-Version System](#multi-version-system): the old component version stays intact, and the frozen theme is what keeps it intact. + +## How to freeze + +When design tokens release a new major version, snapshot the previous one. For a 4.0.0 release, the previous major is 3, so add: + +```sh +--- +type: code +--- +packages/ui-themes/src/themes/frozenThemes/designTokensV3/ +├── index.ts +├── primitives.ts +├── semantics.ts +├── sharedTokens.ts +└── components/ + ├── index.ts + └── / + ├── type.ts + ├── canvas.ts + ├── canvasHighContrast.ts + ├── dark.ts + └── light.ts +``` + +The folder is a partial theme. Include only what broke: + +- the themes of the components that broke in 4.0.0, one folder per component with a file per theme key (`canvas`, `canvas-high-contrast`, `dark`, `light`) +- primitives, semantics, and shared tokens, but only if those broke too + +Then export the snapshot and its component types from `frozenThemes/index.ts` and from the `ui-themes` entry point, and register the component value types in the `FrozenComponentValues` union in `packages/ui-themes/src/index.ts`. That union is what makes `themeOverride.components` accept the frozen token shape. + +See `packages/ui-themes/src/themes/frozenThemes/designTokensV1` for a worked example. + +## What's next? + +A frozen component opts in by passing the snapshot to `withStyleNew` or `useStyleNew` and typing its theme against the snapshot's component types. + +```js +--- +type: code +--- +// class component +import { frozenThemesDesignTokensV1 } from '@instructure/ui-themes' + +@withStyleNew(generateStyle, null, frozenThemesDesignTokensV1) +class Alert extends Component { /* ... */ } + +// function component +const styles = useStyleNew({ + generateStyle, + componentId: 'Pill', + themeOverride: props.themeOverride, + frozenTheme: frozenThemesDesignTokensV1 +}) +``` + +`props.ts` and `styles.ts` switch from `NewComponentTypes` to the frozen types: + +```js +--- +type: code +--- +import type { DesignTokensV1ComponentTypes } from '@instructure/ui-themes' + +const generateStyle = ( + componentTheme: ReturnType, + props: AlertProps, + sharedTokens: SharedTokens +): AlertStyle => { /* ... */ } +``` diff --git a/packages/emotion/src/useStyleNew.ts b/packages/emotion/src/useStyleNew.ts index 7d36fcab61..daa26f0a26 100644 --- a/packages/emotion/src/useStyleNew.ts +++ b/packages/emotion/src/useStyleNew.ts @@ -87,7 +87,7 @@ const useStyleNew = < // if a new theme has been added to the lib since this component version has been frozen, it can't be used with this // theme, so we throw an error. Solution: upgrade to the latest version, it will support it if (frozenTheme && !frozenTheme[themeKey]) { - console.error( + throw new Error( `The version of ${componentId} you are using does not support the currently applied "${themeKey}" theme. ` + `Please upgrade to the latest version of ${componentId}.` ) diff --git a/packages/emotion/src/withStyleNew.tsx b/packages/emotion/src/withStyleNew.tsx index 8fb1548376..940e146a39 100644 --- a/packages/emotion/src/withStyleNew.tsx +++ b/packages/emotion/src/withStyleNew.tsx @@ -153,7 +153,7 @@ const withStyleNew = decorator( // if a new theme has been added to the lib since this component version has been frozen, it can't be used with this // theme, so we throw an error. Solution: upgrade to the latest version, it will support it if (frozenTheme && !frozenTheme[themeKey]) { - console.error( + throw new Error( `The version of ${displayName} you are using does not support the currently applied "${themeKey}" theme. ` + `Please upgrade to the latest version of ${displayName}.` ) diff --git a/packages/ui-alerts/src/Alert/v2/index.tsx b/packages/ui-alerts/src/Alert/v2/index.tsx index bbb8af133a..873875e91e 100644 --- a/packages/ui-alerts/src/Alert/v2/index.tsx +++ b/packages/ui-alerts/src/Alert/v2/index.tsx @@ -49,6 +49,7 @@ import generateStyle from './styles.js' import { allowedProps } from './props.js' import type { AlertProps, AlertState } from './props' +import { frozenThemesDesignTokensV1 } from '@instructure/ui-themes' /** --- @@ -56,7 +57,7 @@ category: components --- **/ @withDeterministicId() -@withStyleNew(generateStyle) +@withStyleNew(generateStyle, null, frozenThemesDesignTokensV1) class Alert extends Component { static displayName = 'Alert' static readonly componentId = 'Alert' diff --git a/packages/ui-alerts/src/Alert/v2/props.ts b/packages/ui-alerts/src/Alert/v2/props.ts index 162104dc1a..5aa18ffcc9 100644 --- a/packages/ui-alerts/src/Alert/v2/props.ts +++ b/packages/ui-alerts/src/Alert/v2/props.ts @@ -29,9 +29,9 @@ import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' -import type { NewComponentTypes } from '@instructure/ui-themes' import type { Renderable } from '@instructure/shared-types' import type { WithDeterministicIdProps } from '@instructure/ui-react-utils' +import type { DesignTokensV1ComponentTypes } from '@instructure/ui-themes' type AlertOwnProps = { /** @@ -122,7 +122,10 @@ type PropKeys = keyof AlertOwnProps type AllowedPropKeys = Readonly> type AlertProps = AlertOwnProps & - WithStyleProps, AlertStyle> & + WithStyleProps< + ReturnType, + AlertStyle + > & WithDeterministicIdProps type AlertStyle = ComponentStyle< diff --git a/packages/ui-alerts/src/Alert/v2/styles.ts b/packages/ui-alerts/src/Alert/v2/styles.ts index b69da3b19b..27bb621b1b 100644 --- a/packages/ui-alerts/src/Alert/v2/styles.ts +++ b/packages/ui-alerts/src/Alert/v2/styles.ts @@ -22,8 +22,9 @@ * SOFTWARE. */ import { boxShadowObjectsToCSSString } from '@instructure/ui-themes' -import type { NewComponentTypes, SharedTokens } from '@instructure/ui-themes' +import type { SharedTokens } from '@instructure/ui-themes' import type { AlertProps, AlertStyle } from './props' +import type { DesignTokensV1ComponentTypes } from '@instructure/ui-themes' /** * --- @@ -36,7 +37,7 @@ import type { AlertProps, AlertStyle } from './props' * @return {Object} The final style object, which will be used in the component */ const generateStyle = ( - componentTheme: ReturnType, + componentTheme: ReturnType, props: AlertProps, sharedTokens: SharedTokens ): AlertStyle => { diff --git a/packages/ui-pill/src/Pill/v2/index.tsx b/packages/ui-pill/src/Pill/v2/index.tsx index c3ad573ddf..b9722f42dd 100644 --- a/packages/ui-pill/src/Pill/v2/index.tsx +++ b/packages/ui-pill/src/Pill/v2/index.tsx @@ -35,6 +35,7 @@ import generateStyle from './styles.js' import type { PillProps, PillState } from './props' import { allowedProps } from './props.js' +import { frozenThemesDesignTokensV1 } from '@instructure/ui-themes' /** --- @@ -42,7 +43,7 @@ category: components --- **/ -@withStyleNew(generateStyle) +@withStyleNew(generateStyle, null, frozenThemesDesignTokensV1) class Pill extends Component { static displayName = 'Pill' static readonly componentId = 'Pill' diff --git a/packages/ui-pill/src/Pill/v2/props.ts b/packages/ui-pill/src/Pill/v2/props.ts index fdd95aa045..598f84417a 100644 --- a/packages/ui-pill/src/Pill/v2/props.ts +++ b/packages/ui-pill/src/Pill/v2/props.ts @@ -27,11 +27,11 @@ import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' -import type { NewComponentTypes } from '@instructure/ui-themes' import type { AsElementType, OtherHTMLAttributes } from '@instructure/shared-types' +import type { DesignTokensV1ComponentTypes } from '@instructure/ui-themes' type PillOwnProps = { as?: AsElementType @@ -63,7 +63,7 @@ type PropKeys = keyof PillOwnProps type AllowedPropKeys = Readonly> type PillProps = PillOwnProps & - WithStyleProps, PillStyle> & + WithStyleProps, PillStyle> & OtherHTMLAttributes type PillStyle = ComponentStyle< diff --git a/packages/ui-pill/src/Pill/v2/styles.ts b/packages/ui-pill/src/Pill/v2/styles.ts index a8b0a7506b..ecdc772be5 100644 --- a/packages/ui-pill/src/Pill/v2/styles.ts +++ b/packages/ui-pill/src/Pill/v2/styles.ts @@ -22,8 +22,8 @@ * SOFTWARE. */ -import type { NewComponentTypes } from '@instructure/ui-themes' import type { PillProps, PillStyle } from './props' +import type { DesignTokensV1ComponentTypes } from '@instructure/ui-themes' /** * --- @@ -35,7 +35,7 @@ import type { PillProps, PillStyle } from './props' * @return {Object} The final style object, which will be used in the component */ const generateStyle = ( - componentTheme: ReturnType, + componentTheme: ReturnType, props: PillProps ): PillStyle => { const { color } = props diff --git a/packages/ui-scripts/package.json b/packages/ui-scripts/package.json index 301ed51787..7e2f7996d8 100644 --- a/packages/ui-scripts/package.json +++ b/packages/ui-scripts/package.json @@ -23,7 +23,7 @@ "dependencies": { "@babel/cli": "^7.27.2", "@instructure/command-utils": "workspace:*", - "@instructure/instructure-design-tokens": "github:instructure/instructure-design-tokens#v1.5.0", + "@instructure/instructure-design-tokens": "github:instructure/instructure-design-tokens#v1.9.0", "dprint": "^0.57.1", "http-server": "^14.1.1", "inquirer": "^14.2.1", diff --git a/packages/ui-select/src/Select/__tests__/Select.test.tsx b/packages/ui-select/src/Select/__tests__/Select.test.tsx index d2a5cdc8ef..b98f38c2d6 100644 --- a/packages/ui-select/src/Select/__tests__/Select.test.tsx +++ b/packages/ui-select/src/Select/__tests__/Select.test.tsx @@ -27,6 +27,10 @@ import { render } from 'vitest-browser-react' import { page, userEvent } from 'vitest/browser' import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import type { MockInstance } from 'vitest' + +import { color2hex } from '@instructure/ui-color-utils' +import canvas from '@instructure/ui-themes' + import { Select } from '@instructure/ui-select/latest' import { CheckInstUIIcon, @@ -1327,6 +1331,9 @@ describe('