diff --git a/docs/guides/accessing-the-dom.md b/docs/guides/accessing-the-dom.md index 28fdad1049..1a942ebaa9 100644 --- a/docs/guides/accessing-the-dom.md +++ b/docs/guides/accessing-the-dom.md @@ -1,7 +1,7 @@ --- title: Accessing the DOM category: Guides -order: 3 +order: 4 relevantForAI: true --- diff --git a/docs/guides/component-versioning.md b/docs/guides/component-versioning.md new file mode 100644 index 0000000000..7b3ff6da4a --- /dev/null +++ b/docs/guides/component-versioning.md @@ -0,0 +1,108 @@ +--- +title: Component versioning +category: Guides +order: 2 +--- + +## Why components are versioned + +When InstUI needs to make a breaking change to a component (renamed props, changed behaviour, etc.), the old version is **kept alongside the new one** instead of being replaced. Upgrading the library no longer forces you to immediately rewrite every component usage — you can migrate to new versions on your own schedule. + +New component versions appear with InstUI **minor** version bumps (e.g. `11.7` → `11.8`). Patch releases never include breaking changes. + +## Import paths + +Every InstUI component package supports three import styles: + +### Default — `@instructure/ui-` + +Always points to the **oldest** still-supported component version. Upgrading the library without changing your imports will keep your code working without surprises. + +```js +--- +type: code +--- +import { Alert } from '@instructure/ui-alerts' +``` + +### Pinned — `@instructure/ui-/v11_X` + +Locks the import to a specific InstUI minor version of the component. When you are ready to adopt a breaking change, update the path to the next pinned version. + +```js +--- +type: code +--- +import { Alert } from '@instructure/ui-alerts/v11_7' +``` + +### Latest — `@instructure/ui-/latest` + +Always points to the newest component version. This may bring breaking changes when you upgrade InstUI itself. + +```js +--- +type: code +--- +import { Alert } from '@instructure/ui-alerts/latest' +``` + +### Per-package or umbrella package + +InstUI also ships an umbrella package, `@instructure/ui`, which re-exports every component from the individual `@instructure/ui-*` packages. Two equivalent import styles work — both resolve to the same component: + +```js +--- +type: code +--- +// per-package import +import { Alert } from '@instructure/ui-alerts/v11_7' + +// umbrella package import +import { Alert } from '@instructure/ui/v11_7' +``` + +The same three path styles (default / `/v11_X` / `/latest`) work on the umbrella package as well. Pick per-package imports when you want better tree-shaking and only pull in what you use, or the umbrella package when you'd rather depend on a single `@instructure/ui` entry in your `package.json`. + +## Versions and theming engines + +InstUI is in the middle of a transition between two theming systems. Which engine a component uses depends on which version you import: + +- **`v11_6` and earlier** — legacy theming. Components are configured through the Canvas theme variables and the `themeOverride` prop, which accepts a function or object that maps to the component's own theme map. See the [Legacy theme overrides](legacy-theme-overrides) guide. + +- **`v11_7` and newer** — new theming system. Components consume pre-resolved design tokens, and theming is done through the new token override structure. See the [New theme overrides](new-theme-overrides) guide. + +Mixing imports from both groups in the same app is fully supported — the two engines run side-by-side without conflict. + +### Supported themes per version + +You import themes the same way as before — from `@instructure/ui-themes` — and pass them to `InstUISettingsProvider`: + +```js +--- +type: code +--- +import { canvas } from '@instructure/ui-themes' + + + + +``` + +The `canvas` (and `canvasHighContrast`) export works for **both `v11_6` and `v11_7+` components in the same app** — internally it carries the data each engine needs. You don't need to switch theme objects when you bump a component import to `/v11_7`. + +- **`v11_6` and earlier** — supports the original two themes: + + - `canvas` — default theme used by Canvas products + - `canvasHighContrast` — same as `canvas`, with colors WCAG-tuned for high-contrast accessibility + +- **`v11_7` and newer** — supports the same two themes (rendered through the new engine, labelled `(legacy)` in the docs UI Theme selector) plus two brand-new ones: + + - `canvas` — same import as above, now driven by the new engine (labelled as `(legacy)`) + - `canvasHighContrast` — same import as above, now driven by the new engine (labelled as `(legacy)`) + - `light` — new light theme + - `dark` — new dark theme + +This means that when you move a component import from `/v11_6` to `/v11_7`, you can continue using the `canvas` theme to maintain a familiar look and feel, and opt in to `light` or `dark` only when you're ready. + +> The `@instructure/ui-themes` package also exports `legacyCanvas` and `legacyCanvasHighContrast`. These are the raw new-engine forms that `canvas` / `canvasHighContrast` wrap internally — most consumers don't need to import them directly. diff --git a/docs/guides/forms.md b/docs/guides/forms.md index 20ba36b808..83f4c4855e 100644 --- a/docs/guides/forms.md +++ b/docs/guides/forms.md @@ -1,7 +1,7 @@ --- title: Forms category: Guides -order: 4 +order: 5 relevantForAI: true --- diff --git a/docs/guides/module-federation.md b/docs/guides/module-federation.md index 9ca53786c1..efea854e4f 100644 --- a/docs/guides/module-federation.md +++ b/docs/guides/module-federation.md @@ -1,7 +1,7 @@ --- title: Module federation category: Guides -order: 2 +order: 3 relevantForAI: true --- diff --git a/docs/theming/new-theme-overrides.md b/docs/theming/new-theme-overrides.md index 5fcdc279e2..671cfaaf55 100644 --- a/docs/theming/new-theme-overrides.md +++ b/docs/theming/new-theme-overrides.md @@ -527,9 +527,52 @@ type: example ``` -### 13. Provider-level overrides cannot target a child component selectively +### 13. Independent overrides for child parts of compound components -Because `Button` uses `BaseButton`'s theme internally, a `components.Button` entry in the provider's `themeOverride` does **not** override `BaseButton`'s tokens for `Button` instances only. Both `BaseButton` and `Button` share the same `BaseButton` theme variables, so a `components.BaseButton` override affects both, regardless of whether a separate `components.Button` entry is also present. +Most compound components expose each part as a separate component with its own `componentId`. This means you can independently override each part via `components` — both overrides take effect: + +```js +--- +type: example +--- + + + + + + Row headers column - purple + Cells column - purple + + + + + TableRowHeader — deeppink + TableCell — unchanged + + + TableRowHeader — deeppink + TableCell — unchanged + + +
+
+
+``` + +**Exception — `Button` and `BaseButton`:** `Button` uses `BaseButton`'s `componentId` internally, so `components.Button` has no effect. A `components.BaseButton` override affects all `BaseButton` instances including those rendered inside `Button` — there is no way to target only one: ```js --- diff --git a/packages/__docs__/buildScripts/DataTypes.mts b/packages/__docs__/buildScripts/DataTypes.mts index f9f5f9c678..d04d721a55 100644 --- a/packages/__docs__/buildScripts/DataTypes.mts +++ b/packages/__docs__/buildScripts/DataTypes.mts @@ -149,11 +149,14 @@ type ResolvedColors = { semantic: Record } +// At runtime, build-docs.mts also attaches `resolvedComponents` (and on +// canvas / canvas-high-contrast: `key`, `description`) to the new-system +// entries. Not declared per-branch; surfaced as optional on `MainDocsData.themes`. type ThemeResource = | (BaseTheme & { resolvedComponents: Record }) // legacy-canvas, legacy-canvas-high-contrast | (NewBaseTheme & { resolvedColors: ResolvedColors }) // canvas, canvas-high-contrast - | (LightTheme & { resolvedColors: ResolvedColors }) - | (DarkTheme & { resolvedColors: ResolvedColors }) + | (LightTheme & { resolvedColors: ResolvedColors }) // light + | (DarkTheme & { resolvedColors: ResolvedColors }) // dark | SharedTokens type MainDocsData = { diff --git a/packages/__docs__/buildScripts/build-docs.mts b/packages/__docs__/buildScripts/build-docs.mts index 66b0dd3fac..399c7212a6 100644 --- a/packages/__docs__/buildScripts/build-docs.mts +++ b/packages/__docs__/buildScripts/build-docs.mts @@ -420,17 +420,44 @@ function parseThemes() { parsed['legacy-canvas-high-contrast'] = { resource: { ...canvasHighContrast, resolvedComponents: resolveComponents(legacyCanvasHighContrast) } } + // `key` is read by Document.tsx's `componentDidUpdate` to detect theme + // changes and refetch the Default Theme Variables. `legacyCanvas` / + // `legacyCanvasHighContrast` from `newThemeTokens` do not include a `key` + // field (unlike `light` / `dark`, which come through wrappers that set it). + // Without it, switching e.g. canvas (legacy) → canvas-high-contrast (legacy) + // on v11_7 leaves `themeVariables.key` `undefined` on both sides, so + // `undefined !== undefined` is false and the refetch never fires. parsed['canvas'] = { - resource: { ...legacyCanvas, resolvedColors: resolveNewThemeColors(legacyCanvas), description: canvas.description } + resource: { + ...legacyCanvas, + key: 'canvas', + resolvedColors: resolveNewThemeColors(legacyCanvas), + resolvedComponents: resolveComponents(legacyCanvas), + description: canvas.description + } } parsed['canvas-high-contrast'] = { - resource: { ...legacyCanvasHighContrast, resolvedColors: resolveNewThemeColors(legacyCanvasHighContrast), description: canvasHighContrast.description } + resource: { + ...legacyCanvasHighContrast, + key: 'canvas-high-contrast', + resolvedColors: resolveNewThemeColors(legacyCanvasHighContrast), + resolvedComponents: resolveComponents(legacyCanvasHighContrast), + description: canvasHighContrast.description + } } parsed[light.key] = { - resource: { ...light, resolvedColors: resolveNewThemeColors(light.newTheme as typeof legacyCanvas) } + resource: { + ...light, + resolvedColors: resolveNewThemeColors(light.newTheme as typeof legacyCanvas), + resolvedComponents: resolveComponents(light.newTheme as typeof legacyCanvas) + } } parsed[dark.key] = { - resource: { ...dark, resolvedColors: resolveNewThemeColors(dark.newTheme as typeof legacyCanvas) } + resource: { + ...dark, + resolvedColors: resolveNewThemeColors(dark.newTheme as typeof legacyCanvas), + resolvedComponents: resolveComponents(dark.newTheme as typeof legacyCanvas) + } } const canvasSemantics = legacyCanvas.semantics(legacyCanvas.primitives) parsed['shared-tokens'] = { resource: legacyCanvas.sharedTokens(canvasSemantics) } diff --git a/packages/__docs__/src/App/index.tsx b/packages/__docs__/src/App/index.tsx index 4a50793ded..1f5ae877bd 100644 --- a/packages/__docs__/src/App/index.tsx +++ b/packages/__docs__/src/App/index.tsx @@ -70,6 +70,7 @@ import { } from '../versionData' import { parseCurrentUrl, + navigateTo, navigateToVersion, getDeployBase, MINOR_VERSION_REGEX @@ -303,9 +304,10 @@ class App extends Component { fetchMinorVersionData(signal) .then((minorVersionsData) => { if (minorVersionsData && minorVersionsData.libraryVersions.length > 0) { - // If URL has a version, use it; otherwise use default - const selectedMinorVersion = - urlMinorVersion ?? minorVersionsData.defaultVersion + const { libraryVersions } = minorVersionsData + const latestVersion = libraryVersions[libraryVersions.length - 1] + // If URL has a version, use it; otherwise use the latest version + const selectedMinorVersion = urlMinorVersion ?? latestVersion // Update globals before fetching docs so renders use correct components updateGlobalsForVersion(selectedMinorVersion) this.setState({ @@ -343,6 +345,48 @@ class App extends Component { ) { this.handleNavigationFocusRegion() } + + if (prevState.key !== this.state.key) { + this.handleMinorVersionForPage(this.state.key) + } + + if (!prevState.minorVersionsData && this.state.minorVersionsData) { + this.handleMinorVersionForPage(this.state.key) + } + } + + getLatestMinorVersion = () => { + const { minorVersionsData } = this.state + if (!minorVersionsData) return undefined + const { libraryVersions } = minorVersionsData + return libraryVersions[libraryVersions.length - 1] + } + + handleMinorVersionForPage = (key: string | undefined) => { + const { selectedMinorVersion, minorVersionsData, docsData } = this.state + if (!minorVersionsData || !selectedMinorVersion || !key) return + + const latestVersion = this.getLatestMinorVersion()! + + if (key === 'legacy-theme-overrides') { + if (selectedMinorVersion !== 'v11_6') { + this.handleMinorVersionChange('v11_6') + } + } else if (key === 'new-theme-overrides') { + if (selectedMinorVersion !== latestVersion) { + this.handleMinorVersionChange(latestVersion) + } + } else { + const category = docsData?.docs[key]?.category + const isGuidePage = + !!category && + !category.startsWith('components') && + !category.startsWith('utilities') + if (isGuidePage && selectedMinorVersion !== latestVersion) { + // Guide pages (.md) always show with the latest version + this.handleMinorVersionChange(latestVersion) + } + } } componentWillUnmount() { @@ -554,9 +598,17 @@ class App extends Component { } renderThemeSelect() { + const { minorVersionsData, selectedMinorVersion } = this.state const allThemeKeys = Object.keys(this.state.docsData!.themes) - const showNewThemes = this.state.selectedMinorVersion !== 'v11_6' - + const showNewThemes = selectedMinorVersion !== 'v11_6' + + // The `parsed.themes` map in build-docs.mts contains both: + // - `canvas` / `canvas-high-contrast` → new-system resources (primitives/semantics/sharedTokens/components`) + // - `legacy-canvas` / `legacy-canvas-high-contrast` → legacy wrappers (full theme object) + // v11_6 components' `generateComponentTheme(theme)` reads `theme.colors`, + // `theme.spacing`, etc., so v11_6 MUST be backed by the legacy wrappers. + // We pick `legacy-*` as the actual selected keys, and strip the prefix for + // display so the user still sees "canvas" / "canvas-high-contrast". const themeKeys = showNewThemes ? allThemeKeys.filter( (k) => @@ -565,7 +617,7 @@ class App extends Component { k !== 'legacy-canvas-high-contrast' ) : allThemeKeys.filter( - (k) => k === 'canvas' || k === 'canvas-high-contrast' + (k) => k === 'legacy-canvas' || k === 'legacy-canvas-high-contrast' ) const displayThemeName = (themeKey: string) => { @@ -575,20 +627,63 @@ class App extends Component { ) { return `${themeKey} (legacy)` } + // On v11_6 strip the `legacy-` prefix so the user sees the plain names. + if (!showNewThemes) { + return themeKey.replace(/^legacy-/, '') + } return themeKey } + const formatMinorVersion = (version: string) => { + const formatted = version.replace(/_/g, '.') + if (version === 'v11_6') return `${formatted} (legacy theming)` + return formatted + } + const smallScreen = this.state.layout === 'small' const currentThemeKey = this.state.themeKey && themeKeys.includes(this.state.themeKey) ? this.state.themeKey : themeKeys[0] + const { key, docsData } = this.state + const versionLockedPages = ['legacy-theme-overrides', 'new-theme-overrides'] + if (versionLockedPages.includes(key ?? '')) return null + + // Only show on Components pages — other categories (guides, utilities, + // themes, etc.) don't need theme or component-version switching. + const category = key ? docsData?.docs[key]?.category : undefined + if (!category || !category.startsWith('components')) return null + + const showMinorVersionSelect = + minorVersionsData && minorVersionsData.libraryVersions.length > 1 + return themeKeys.length > 1 ? ( + {showMinorVersionSelect && ( + + + + )}