From 1d51d4c892662417c4d7628599cab9bf9a2497c2 Mon Sep 17 00:00:00 2001 From: Toppanto Bence Date: Tue, 4 Aug 2026 19:08:26 +0200 Subject: [PATCH] feat(docs-app): limit the version select to a component's own versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The component version dropdown listed every library version regardless of which ones the viewed component actually existed in. Build a per-package map of the versions each component appears in, emit it in docs-versions.json, and drive the select from that instead of the full library version list. Packages that don't declare `./v11_x` export keys (ui-motion, ui-position, ui-dialog, ui-focusable, ui-i18n, ui-portal, ui-selectable, ui-svg-images, ui-truncate-list) are absent from the version map and have no entry, so the select is omitted for them rather than rendered with no options. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/__docs__/buildScripts/DataTypes.mts | 50 ++++++----- packages/__docs__/buildScripts/build-docs.mts | 88 +++++++++++++------ packages/__docs__/src/App/index.tsx | 28 ++++-- packages/__docs__/src/versionData.ts | 3 +- 4 files changed, 117 insertions(+), 52 deletions(-) diff --git a/packages/__docs__/buildScripts/DataTypes.mts b/packages/__docs__/buildScripts/DataTypes.mts index d04d721a55..a18cb3333b 100644 --- a/packages/__docs__/buildScripts/DataTypes.mts +++ b/packages/__docs__/buildScripts/DataTypes.mts @@ -32,12 +32,15 @@ import type { SharedTokens } from '@instructure/ui-themes' -type ProcessedFile = - Documentation & +type ProcessedFile = Documentation & YamlMetaInfo & JsDocResult & - PackagePathData & - { title: string, id:string, componentVersion?: string, componentDirName?: string } + PackagePathData & { + title: string + id: string + componentVersion?: string + componentDirName?: string + } type PackagePathData = { extension: string @@ -73,10 +76,10 @@ type YamlMetaInfo = { type JsDocResult = { // the comment without the comment characters ("/*" etc) - description?: string, + description?: string // If it's a function it will be the function's name, otherwise // it's either the string after the '@module' annotation or the variable's name - name?: string, + name?: string // function params. undefined if the comment is e.g. above imports params?: { name: string @@ -85,14 +88,14 @@ type JsDocResult = { optional?: boolean // the description of the param description?: string - }[], + }[] genericParameters?: { name: string defaultValue?: string constraint?: string }[] // function return value. undefined if the comment is e.g. above imports - returns?: JSDocFunctionReturns, + returns?: JSDocFunctionReturns } type JSDocFunctionReturns = { @@ -126,13 +129,16 @@ export type ParsedDoc = { docs: ParsedDocSummary } -export type ParsedDocSummary = Record +export type ParsedDocSummary = Record< + string, + { + title: string + order?: string + category?: string + isWIP?: boolean + tags?: string + } +> type Glyph = { bidirectional: boolean @@ -153,10 +159,10 @@ type ResolvedColors = { // 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 }) // light - | (DarkTheme & { resolvedColors: ResolvedColors }) // dark + | (BaseTheme & { resolvedComponents: Record }) // legacy-canvas, legacy-canvas-high-contrast + | (NewBaseTheme & { resolvedColors: ResolvedColors }) // canvas, canvas-high-contrast + | (LightTheme & { resolvedColors: ResolvedColors }) // light + | (DarkTheme & { resolvedColors: ResolvedColors }) // dark | SharedTokens type MainDocsData = { @@ -167,7 +173,10 @@ type MainDocsData = { // (BaseTheme & { resolvedComponents }) → has key ✓ // (NewBaseTheme & { resolvedColors }) → gains key? via intersection ✓ // SharedTokens → gains key? via intersection ✓ - themes: Record + themes: Record< + string, + { resource: ThemeResource & { key?: string; description?: string } } + > library: LibraryOptions } & ParsedDoc @@ -186,6 +195,7 @@ type VersionMap = { type MinorVersionData = { libraryVersions: string[] defaultVersion: string + activeVersions: Record> } export type { diff --git a/packages/__docs__/buildScripts/build-docs.mts b/packages/__docs__/buildScripts/build-docs.mts index b137ee08cd..6a265c9303 100644 --- a/packages/__docs__/buildScripts/build-docs.mts +++ b/packages/__docs__/buildScripts/build-docs.mts @@ -37,12 +37,12 @@ import { legacyCanvas, legacyCanvasHighContrast } from '@instructure/ui-themes' -import type { - MainDocsData, - ProcessedFile, - VersionMap -} from './DataTypes.mjs' -import { buildVersionMap, getPackageShortName, isDocIncludedInVersion } from './utils/buildVersionMap.mjs' +import type { MainDocsData, ProcessedFile, VersionMap } from './DataTypes.mjs' +import { + buildVersionMap, + getPackageShortName, + isDocIncludedInVersion +} from './utils/buildVersionMap.mjs' import { fileURLToPath, pathToFileURL } from 'url' import { generateAIAccessibleMarkdowns } from './ai-accessible-documentation/generate-ai-accessible-markdowns.mjs' import { generateAIAccessibleLlmsFile } from './ai-accessible-documentation/generate-ai-accessible-llms-file.mjs' @@ -172,7 +172,9 @@ async function buildDocs() { const versionDocs = filterDocsForVersion(allDocs, libVersion, versionMap) // eslint-disable-next-line no-console - console.log(`Generated docs for ${libVersion} (${versionDocs.length} entries)`) + console.log( + `Generated docs for ${libVersion} (${versionDocs.length} entries)` + ) const clientProps = getClientProps(versionDocs) const mainDocsData: MainDocsData = { @@ -208,7 +210,11 @@ async function buildDocs() { // Write default version's per-doc JSONs to root docs/ as a backward-compatible // fallback (no version prefix in the path). - const defaultVersionDocs = filterDocsForVersion(allDocs, defaultVersion, versionMap) + const defaultVersionDocs = filterDocsForVersion( + allDocs, + defaultVersion, + versionMap + ) for (const doc of defaultVersionDocs) { fs.writeFileSync( buildDir + 'docs/' + doc.id + '.json', @@ -216,9 +222,23 @@ async function buildDocs() { ) } + // builds a map with all the components and collects all the versions a component has in the library. e.g.: the component was introduced only in v11.8 and removed in 11.13, it should not be presented pre 11.8 and post 11.13 + const getActiveVersions = () => { + const { libraryVersions, mapping } = versionMap + return libraryVersions.reduce((acc, version) => { + const componentsInThisVersion = Object.keys(mapping[version]) + const res: Record> = { ...acc } + componentsInThisVersion.forEach((component) => { + res[component] = [...(acc[component] ? acc[component] : []), version] + }) + return res + }, {} as Record>) + } + // Write version manifest (client only needs versions + default, not the full map) const docsVersionsManifest = { libraryVersions: versionMap.libraryVersions, + activeVersions: getActiveVersions(), defaultVersion } fs.writeFileSync( @@ -260,12 +280,15 @@ async function buildDocs() { }) fs.copyFileSync( - projectRoot + '/packages/ui-icons/src/generated/legacy/legacy-icons-data.json', + projectRoot + + '/packages/ui-icons/src/generated/legacy/legacy-icons-data.json', buildDir + 'legacy-icons-data.json' ) // eslint-disable-next-line no-console - console.log(`Docs built in ${((Date.now() - startedAt) / 1000).toFixed(1)}s`) + console.log( + `Docs built in ${((Date.now() - startedAt) / 1000).toFixed(1)}s` + ) if (shouldDoTheVersionCopy) { const versionFilePath = path.resolve(__dirname, '..', 'versions.json') @@ -408,7 +431,9 @@ async function parseFilesInParallel( return slots.filter(Boolean) as ProcessedFile[] } -function resolveComponents(theme: typeof legacyCanvas | typeof legacyCanvasHighContrast) { +function resolveComponents( + theme: typeof legacyCanvas | typeof legacyCanvasHighContrast +) { const sem = theme.semantics(theme.primitives) const resolved: Record = {} for (const [key, fn] of Object.entries(theme.components)) { @@ -420,7 +445,9 @@ function resolveComponents(theme: typeof legacyCanvas | typeof legacyCanvasHighC } /** Recursively flatten a nested color object, keeping only string values, using the innermost key (e.g. grey.grey10 → grey10) */ -function flattenPrimitiveColors(obj: Record): Record { +function flattenPrimitiveColors( + obj: Record +): Record { const result: Record = {} for (const [key, value] of Object.entries(obj)) { if (typeof value === 'string') { @@ -433,7 +460,10 @@ function flattenPrimitiveColors(obj: Record): Record, prefix = ''): Record { +function flattenSemanticColors( + obj: Record, + prefix = '' +): Record { const result: Record = {} for (const [key, value] of Object.entries(obj)) { const fullKey = prefix @@ -464,7 +494,10 @@ function parseThemes() { resource: { ...canvas, resolvedComponents: resolveComponents(legacyCanvas) } } parsed['legacy-canvas-high-contrast'] = { - resource: { ...canvasHighContrast, resolvedComponents: resolveComponents(legacyCanvasHighContrast) } + resource: { + ...canvasHighContrast, + resolvedComponents: resolveComponents(legacyCanvasHighContrast) + } } // `key` is read by Document.tsx's `componentDidUpdate` to detect theme // changes and refetch the Default Theme Variables. `legacyCanvas` / @@ -494,25 +527,30 @@ function parseThemes() { parsed[light.key] = { resource: { ...light, - resolvedColors: resolveNewThemeColors(light.newTheme as typeof legacyCanvas), - resolvedComponents: resolveComponents(light.newTheme as typeof legacyCanvas) + 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), - resolvedComponents: resolveComponents(dark.newTheme as typeof legacyCanvas) + 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) } + parsed['shared-tokens'] = { + resource: legacyCanvas.sharedTokens(canvasSemantics) + } return parsed } -export { - pathsToProcess, - pathsToIgnore, - buildDocs, - filterDocsForVersion -} +export { pathsToProcess, pathsToIgnore, buildDocs, filterDocsForVersion } diff --git a/packages/__docs__/src/App/index.tsx b/packages/__docs__/src/App/index.tsx index af441b046d..ca3f9a3381 100644 --- a/packages/__docs__/src/App/index.tsx +++ b/packages/__docs__/src/App/index.tsx @@ -568,10 +568,17 @@ class App extends Component { } renderThemeSelect() { + const currentComponentPackageName = + this.state.currentDocData?.esPath.split('/')[1] + const { minorVersionsData, selectedMinorVersion } = this.state const allThemeKeys = Object.keys(this.state.docsData!.themes) const showNewThemes = selectedMinorVersion !== 'v11_6' + const activeComponentVersions = currentComponentPackageName + ? minorVersionsData?.activeVersions[currentComponentPackageName] + : [] + // 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) @@ -625,8 +632,14 @@ class App extends Component { const category = key ? docsData?.docs[key]?.category : undefined if (!category || !category.startsWith('components')) return null + // Gate on the current component's own versions, not the library's. Packages + // that don't declare `./v11_x` export keys (e.g. ui-motion, ui-position) + // are absent from the version map and so have no `activeVersions` entry — + // hide the select for those rather than rendering it with no options. A + // single applicable version still renders, as a read-out of what the page + // is showing. const showMinorVersionSelect = - minorVersionsData && minorVersionsData.libraryVersions.length > 1 + minorVersionsData && (activeComponentVersions?.length ?? 0) > 0 return themeKeys.length > 1 ? ( { value={selectedMinorVersion ?? this.getLatestMinorVersion()} width="16.5rem" > - {[...minorVersionsData!.libraryVersions].reverse().map((ver) => ( - - ))} + {(activeComponentVersions ?? []) + .slice() + .reverse() + .map((ver) => ( + + ))} )} diff --git a/packages/__docs__/src/versionData.ts b/packages/__docs__/src/versionData.ts index 78a7366831..4bf9abdb6b 100644 --- a/packages/__docs__/src/versionData.ts +++ b/packages/__docs__/src/versionData.ts @@ -75,7 +75,8 @@ const fetchMinorVersionData = async ( const data = await result.json() return { libraryVersions: data.libraryVersions, - defaultVersion: data.defaultVersion + defaultVersion: data.defaultVersion, + activeVersions: data.activeVersions } } catch { return undefined