From f68c4a5b1aca304757458f01311e23a98e6a7ccd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 6 Feb 2026 21:27:48 +0000 Subject: [PATCH] feat(token-metrics): add design token metrics tool with baseline report Add a new tool at tools/token-metrics that extracts quantitative metrics from @adobe/spectrum-tokens to support the OKR: 'Token metrics are defined and included into our metrics system & have a baseline to compare moving forward.' Metrics extracted from the current token data: - Token inventory: 2,338 total (2,150 active, 188 deprecated) - Token scope: 965 global, 1,373 component-scoped across 95 components - Architecture: 654 aliases, 459 direct-value, 1,225 set-based - Data quality: 100% UUID coverage, max alias depth 4 - Component coverage: 44/54 registered components have tokens (81.5%) - Semantic categories: layout (741), content (289), typography (284), color (227), background (151), border (58), shadow (23) Includes: - src/index.js: Core metrics extraction library with exported functions - src/cli.js: CLI tool for generating reports - test/index.test.js: 21 unit and integration tests (all passing) - README.md: Documents all metrics, their meaning, and recommended timeline for Alison and the team - token-metrics-report.json: Baseline snapshot from current data - moon.yml, ava.config.js, package.json: Standard tooling config --- pnpm-lock.yaml | 10 + tools/token-metrics/README.md | 142 ++ tools/token-metrics/ava.config.js | 7 + tools/token-metrics/moon.yml | 7 + tools/token-metrics/package.json | 43 + tools/token-metrics/src/cli.js | 130 ++ tools/token-metrics/src/index.js | 592 +++++++++ tools/token-metrics/test/index.test.js | 375 ++++++ tools/token-metrics/token-metrics-report.json | 1149 +++++++++++++++++ 9 files changed, 2455 insertions(+) create mode 100644 tools/token-metrics/README.md create mode 100644 tools/token-metrics/ava.config.js create mode 100644 tools/token-metrics/moon.yml create mode 100644 tools/token-metrics/package.json create mode 100644 tools/token-metrics/src/cli.js create mode 100644 tools/token-metrics/src/index.js create mode 100644 tools/token-metrics/test/index.test.js create mode 100644 tools/token-metrics/token-metrics-report.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a3dad42..8916e107 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -682,6 +682,16 @@ importers: tools/token-manifest-builder: {} + tools/token-metrics: + dependencies: + commander: + specifier: ^13.1.0 + version: 13.1.0 + devDependencies: + ava: + specifier: ^6.0.0 + version: 6.4.0(@ava/typescript@6.0.0)(rollup@4.44.1) + tools/transform-tokens-json: dependencies: jsonpath-plus: diff --git a/tools/token-metrics/README.md b/tools/token-metrics/README.md new file mode 100644 index 00000000..7dd1b6bd --- /dev/null +++ b/tools/token-metrics/README.md @@ -0,0 +1,142 @@ +# Spectrum Token Metrics + +Extracts and reports design token metrics from `@adobe/spectrum-tokens` for tracking the health, coverage, and growth of the Spectrum design token system. + +This tool supports the OKR: **"Token metrics are defined and included into our metrics system & have a baseline to compare moving forward."** + +## What This Answers for Leadership + +**"What does 'token metrics' mean specifically, and when should this be done by?"** + +Token metrics are quantitative measurements extracted from the design token data in `packages/tokens` that give us visibility into the health, maturity, and coverage of Spectrum's design data layer. They establish a baseline so we can measure progress over time and make data-informed decisions about where to invest in the token system. + +## Metrics Available Today (Baseline) + +These metrics are extracted directly from the current token source files with zero additional instrumentation: + +### 1. Token Inventory (Health) + +| Metric | Current Value | Why It Matters | +| ---------------------------------- | ------------------------ | ------------------------------------------------------------------- | +| **Total tokens** | 2,338 | Size of the design data surface area | +| **Active tokens** | 2,150 | Tokens currently in use | +| **Deprecated tokens** | 188 (8.0%) | Technical debt in the token layer | +| **Deprecated with migration path** | 57 (30.3% of deprecated) | How well we support migration when tokens change | +| **Private tokens** | 372 | Internal implementation tokens not intended for direct consumer use | + +### 2. Token Scope (Architecture) + +| Metric | Current Value | Why It Matters | +| --------------------------------- | ------------- | ------------------------------------------------ | +| **Global (system) tokens** | 965 | Foundation tokens available to all components | +| **Component tokens** | 1,373 | Tokens scoped to specific components | +| **Unique components with tokens** | 95 | How many components have dedicated token support | + +### 3. Token Architecture (Complexity) + +| Metric | Current Value | Why It Matters | +| ---------------------------- | ------------- | -------------------------------------------------- | +| **Alias (reference) tokens** | 654 | Tokens that reference other tokens for consistency | +| **Direct-value tokens** | 459 | Tokens with hard-coded values | +| **Color theme set tokens** | 470 | Tokens with light/dark/wireframe variants | +| **Scale set tokens** | 755 | Tokens with desktop/mobile variants | +| **Max alias chain depth** | 4 | Deepest nesting of token references | + +### 4. Data Quality + +| Metric | Current Value | Why It Matters | +| --------------------------- | -------------------------- | ------------------------------------------------ | +| **UUID coverage** | 100% | Every token has a unique identifier for tracking | +| **Migration path coverage** | 30.3% of deprecated tokens | Gap in providing clear upgrade guidance | + +### 5. Component Coverage + +| Metric | Current Value | Why It Matters | +| ------------------------- | ------------- | ---------------------------------------------- | +| **Registered components** | 54 | Components in the design system registry | +| **With tokens** | 44 (81.5%) | Components that have design token support | +| **With schema** | 54 (100%) | Components that have formal schema definitions | + +### 6. Semantic Categories + +| Category | Count | What It Covers | +| ---------- | ----- | ------------------------------------------------- | +| Layout | 741 | Spacing, sizing, dimensions, margins | +| Other | 550 | Component-specific tokens not in other categories | +| Content | 289 | Text and content-related colors/styles | +| Typography | 284 | Font families, sizes, weights, line heights | +| Color | 227 | Color values and visual tokens | +| Background | 151 | Background colors and opacity | +| Border | 58 | Border colors and styles | +| Shadow | 23 | Drop shadows and elevation | +| Opacity | 9 | Opacity values | +| Icon | 6 | Icon-specific tokens | + +## Additional Metrics Worth Working On + +These require additional instrumentation or cross-system integration: + +### Phase 2 (Recommended for Q2) + +1. **Token adoption rate** - Track which tokens are actually used in downstream implementations (Spectrum CSS, React Spectrum, S2) via automated source code scanning. + +2. **Token change velocity** - Tokens added/modified/deleted per release. The existing `release-analyzer` tool partially supports this; it can be extended to track token-level changes per version. + +3. **Deprecation lifecycle duration** - How long deprecated tokens remain before removal. This measures how effectively we communicate and complete migrations. + +4. **Cross-platform token parity** - Compare tokens available for web vs iOS vs Android to measure multi-platform readiness. This directly supports the "multi-platform philosophy" OKR item. + +### Phase 3 (Q3+) + +5. **Consumer migration completeness** - For each deprecated token, what percentage of known consumers have migrated. Requires integration with downstream repos. + +6. **Token request/gap tracking** - Track feature requests for new tokens and measure time-to-delivery. + +7. **Design-to-code token fidelity** - Measure how well design tool tokens map to implementation tokens (requires Figma API integration). + +8. **Custom token prevalence** - Track how often teams create custom tokens outside the system, indicating gaps in the core offering. + +## Usage + +### Generate a full metrics report + +```bash +node src/cli.js +``` + +### Print summary only + +```bash +node src/cli.js --summary +``` + +### Output to a specific file + +```bash +node src/cli.js --output metrics-2026-Q1.json +``` + +### Using moon + +```bash +moon run token-metrics:metrics +``` + +## Integration with Metrics Systems + +The tool outputs a JSON report that can be: + +* Stored in the repo as a baseline snapshot (commit the output file) +* Ingested by dashboarding tools (Grafana, Datadog, etc.) +* Compared across releases to show trends +* Included in CI to track regressions (e.g., deprecation rate exceeding threshold) + +## Suggested Timeline + +| Milestone | Target | Description | +| --------------------- | ----------------- | --------------------------------------------- | +| Baseline established | **Now (Q1 2026)** | This tool generates the initial baseline | +| CI integration | Q1 2026 | Run metrics on every PR to detect regressions | +| Dashboard | Q2 2026 | Visualize metrics trends over time | +| Adoption metrics | Q2 2026 | Cross-reference with downstream consumers | +| Multi-platform parity | Q2-Q3 2026 | Compare web/iOS/Android token coverage | diff --git a/tools/token-metrics/ava.config.js b/tools/token-metrics/ava.config.js new file mode 100644 index 00000000..9330844f --- /dev/null +++ b/tools/token-metrics/ava.config.js @@ -0,0 +1,7 @@ +export default { + files: ["test/**/*.test.js"], + verbose: true, + environmentVariables: { + NODE_ENV: "test", + }, +}; diff --git a/tools/token-metrics/moon.yml b/tools/token-metrics/moon.yml new file mode 100644 index 00000000..63e60cb6 --- /dev/null +++ b/tools/token-metrics/moon.yml @@ -0,0 +1,7 @@ +tasks: + test: + command: pnpm ava test + platform: node + metrics: + command: node src/cli.js + platform: node diff --git a/tools/token-metrics/package.json b/tools/token-metrics/package.json new file mode 100644 index 00000000..9140082e --- /dev/null +++ b/tools/token-metrics/package.json @@ -0,0 +1,43 @@ +{ + "name": "@adobe/spectrum-tokens-metrics", + "private": true, + "version": "0.1.0", + "description": "Extracts and reports design token metrics from @adobe/spectrum-tokens for tracking health, coverage, and growth of the Spectrum design token system", + "type": "module", + "main": "./src/index.js", + "bin": { + "token-metrics": "./src/cli.js" + }, + "scripts": { + "test": "ava" + }, + "engines": { + "node": ">=20.12.0", + "pnpm": ">=10.17.1" + }, + "packageManager": "pnpm@10.17.1", + "repository": { + "type": "git", + "url": "git+https://github.com/adobe/spectrum-design-data.git", + "directory": "tools/token-metrics" + }, + "author": "Garth Braithwaite (http://garthdb.com/)", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/adobe/spectrum-design-data/issues" + }, + "homepage": "https://github.com/adobe/spectrum-design-data/tree/main/tools/token-metrics#readme", + "keywords": [ + "spectrum", + "tokens", + "metrics", + "design-system", + "analysis" + ], + "dependencies": { + "commander": "^13.1.0" + }, + "devDependencies": { + "ava": "^6.0.0" + } +} diff --git a/tools/token-metrics/src/cli.js b/tools/token-metrics/src/cli.js new file mode 100644 index 00000000..f011fd93 --- /dev/null +++ b/tools/token-metrics/src/cli.js @@ -0,0 +1,130 @@ +#!/usr/bin/env node + +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { Command } from "commander"; +import { writeFile } from "fs/promises"; +import { resolve } from "path"; +import generateMetricsReport from "./index.js"; + +const program = new Command(); + +program + .name("token-metrics") + .description( + "Extract and report design token metrics from @adobe/spectrum-tokens", + ) + .version("0.1.0") + .option( + "-o, --output ", + "Output file path for JSON report", + "token-metrics-report.json", + ) + .option("--tokens-src ", "Path to tokens/src directory") + .option("--registry ", "Path to design-system-registry components.json") + .option("--schemas ", "Path to component-schemas/schemas/components") + .option("--summary", "Print only the summary to stdout", false) + .action(async (options) => { + try { + const reportOptions = {}; + if (options.tokensSrc) + reportOptions.tokensSrc = resolve(options.tokensSrc); + if (options.registry) + reportOptions.registryPath = resolve(options.registry); + if (options.schemas) reportOptions.schemasDir = resolve(options.schemas); + + console.log("Generating token metrics report...\n"); + const report = await generateMetricsReport(reportOptions); + + if (options.summary) { + printSummary(report); + } else { + printSummary(report); + const outputPath = resolve(options.output); + await writeFile(outputPath, JSON.stringify(report, null, 2)); + console.log(`\nFull report written to: ${outputPath}`); + } + } catch (error) { + console.error("Error generating metrics:", error.message); + process.exit(1); + } + }); + +function printSummary(report) { + const { summary, aliasAnalysis, uuidCoverage, componentCoverage } = report; + + console.log("=== Spectrum Design Token Metrics ===\n"); + console.log(`Generated: ${report.generatedAt}\n`); + + console.log("--- Token Inventory ---"); + console.log(` Total tokens: ${summary.totalTokens}`); + console.log(` Active tokens: ${summary.activeTokens}`); + console.log( + ` Deprecated tokens: ${summary.deprecatedTokens} (${summary.deprecationRate}%)`, + ); + console.log( + ` Deprecated w/ migration: ${summary.deprecatedWithMigrationPath} (${summary.migrationPathCoverage}% coverage)`, + ); + console.log(` Private tokens: ${summary.privateTokens}`); + + console.log("\n--- Token Scope ---"); + console.log(` Global tokens: ${summary.globalTokenCount}`); + console.log(` Component tokens: ${summary.componentTokenCount}`); + console.log(` Unique components: ${summary.uniqueComponents}`); + + console.log("\n--- Token Architecture ---"); + console.log(` Alias (reference) tokens: ${aliasAnalysis.aliasTokens}`); + console.log( + ` Direct-value tokens: ${aliasAnalysis.directValueTokens}`, + ); + console.log( + ` Set-based tokens: ${aliasAnalysis.setBasedTokens.total}`, + ); + console.log( + ` Color theme sets: ${aliasAnalysis.setBasedTokens.colorTheme}`, + ); + console.log( + ` Scale sets: ${aliasAnalysis.setBasedTokens.scale}`, + ); + + console.log("\n--- Data Quality ---"); + console.log( + ` UUID coverage: ${uuidCoverage.coveragePercent}% (${uuidCoverage.withUuid}/${uuidCoverage.withUuid + uuidCoverage.withoutUuid})`, + ); + console.log( + ` Max alias chain depth: ${report.aliasChainDepth.maxDepth} (${report.aliasChainDepth.maxDepthToken})`, + ); + + if (componentCoverage) { + console.log("\n--- Component Coverage ---"); + console.log( + ` Registered components: ${componentCoverage.registeredComponentCount}`, + ); + console.log( + ` With tokens: ${componentCoverage.componentsWithTokens} (${componentCoverage.tokenCoveragePercent}%)`, + ); + console.log( + ` With schema: ${componentCoverage.componentsWithSchema} (${componentCoverage.schemaCoveragePercent}%)`, + ); + console.log( + ` With both: ${componentCoverage.componentsWithBoth}`, + ); + } + + console.log("\n--- Semantic Categories ---"); + for (const [category, count] of Object.entries(report.semanticCategories)) { + console.log(` ${category.padEnd(24)} ${count}`); + } +} + +program.parse(); diff --git a/tools/token-metrics/src/index.js b/tools/token-metrics/src/index.js new file mode 100644 index 00000000..a1ea8c1f --- /dev/null +++ b/tools/token-metrics/src/index.js @@ -0,0 +1,592 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { readFile } from "fs/promises"; +import { resolve, basename } from "path"; +import { glob } from "glob"; +import * as url from "url"; + +const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); + +/** + * Default path to the tokens package source directory. + */ +const DEFAULT_TOKENS_SRC = resolve( + __dirname, + "../../..", + "packages/tokens/src", +); + +/** + * Default path to the component-schemas package. + */ +const DEFAULT_COMPONENT_SCHEMAS = resolve( + __dirname, + "../../..", + "packages/component-schemas/schemas/components", +); + +/** + * Default path to the design-system-registry components list. + */ +const DEFAULT_REGISTRY_COMPONENTS = resolve( + __dirname, + "../../..", + "packages/design-system-registry/registry/components.json", +); + +/** + * Read and parse a JSON file. + * @param {string} filePath - Path to JSON file + * @returns {Promise} Parsed JSON + */ +async function readJson(filePath) { + return JSON.parse(await readFile(filePath, "utf8")); +} + +/** + * Extract the token type from a $schema URL. + * @param {string} schemaUrl - The $schema value from a token + * @returns {string} The token type name (e.g., "alias", "color", "dimension") + */ +export function extractTokenType(schemaUrl) { + if (!schemaUrl) return "unknown"; + const match = schemaUrl.match(/token-types\/(.+)\.json$/); + return match ? match[1] : "unknown"; +} + +/** + * Check whether a token value is an alias reference (e.g., "{gray-800}"). + * @param {*} value - The token value + * @returns {boolean} + */ +export function isAliasValue(value) { + return typeof value === "string" && /^\{[\w-]+\}$/.test(value); +} + +/** + * Extract the referenced token name from an alias value. + * @param {string} value - Alias value like "{gray-800}" + * @returns {string|null} The referenced token name, or null + */ +export function extractAliasReference(value) { + if (!isAliasValue(value)) return null; + return value.slice(1, -1); +} + +/** + * Determine whether a token is set-based (has `sets` property at the top level). + * @param {object} token - Token definition + * @returns {boolean} + */ +export function isSetToken(token) { + return Object.hasOwn(token, "sets") && typeof token.sets === "object"; +} + +/** + * Determine the set type (color theme set vs scale set vs other). + * @param {object} token - Token definition with sets + * @returns {string} "color-theme" | "scale" | "other" + */ +export function getSetType(token) { + if (!isSetToken(token)) return "none"; + const keys = Object.keys(token.sets); + const hasThemeKeys = keys.some((k) => + ["light", "dark", "wireframe"].includes(k), + ); + const hasScaleKeys = keys.some((k) => ["desktop", "mobile"].includes(k)); + if (hasThemeKeys) return "color-theme"; + if (hasScaleKeys) return "scale"; + return "other"; +} + +/** + * Check whether a token is deprecated. + * @param {object} token - Token definition + * @returns {boolean} + */ +export function isDeprecated(token) { + if (Object.hasOwn(token, "deprecated") && token.deprecated === true) { + return true; + } + if (isSetToken(token)) { + return Object.values(token.sets).every( + (setValue) => + Object.hasOwn(setValue, "deprecated") && setValue.deprecated === true, + ); + } + return false; +} + +/** + * Check whether a deprecated token has a `renamed` migration path. + * @param {object} token - Token definition + * @returns {boolean} + */ +export function hasRenamedPath(token) { + return isDeprecated(token) && Object.hasOwn(token, "renamed"); +} + +/** + * Load all token files from the source directory. + * @param {string} srcDir - Path to the tokens/src directory + * @returns {Promise>} Map of filename to parsed token data + */ +export async function loadTokenFiles(srcDir = DEFAULT_TOKENS_SRC) { + const files = await glob(`${srcDir}/**/*.json`); + const tokenFiles = new Map(); + for (const file of files) { + const data = await readJson(file); + tokenFiles.set(basename(file), data); + } + return tokenFiles; +} + +/** + * Flatten all tokens from all files into a single Map keyed by token name. + * @param {Map} tokenFiles - Map of filename to token data + * @returns {Map} + */ +export function flattenTokens(tokenFiles) { + const allTokens = new Map(); + for (const [fileName, fileData] of tokenFiles) { + for (const [tokenName, tokenDef] of Object.entries(fileData)) { + allTokens.set(tokenName, { token: tokenDef, sourceFile: fileName }); + } + } + return allTokens; +} + +/** + * Compute all metrics from the token data. + * @param {Map} tokenFiles - Map of filename to token data + * @returns {object} Comprehensive metrics report + */ +export function computeMetrics(tokenFiles) { + const allTokens = flattenTokens(tokenFiles); + + // ---- 1. Total token count ---- + const totalTokens = allTokens.size; + + // ---- 2. Tokens by source file ---- + const tokensByFile = {}; + for (const [fileName, fileData] of tokenFiles) { + tokensByFile[fileName] = Object.keys(fileData).length; + } + + // ---- 3. Tokens by schema type ---- + const tokensByType = {}; + for (const [, { token }] of allTokens) { + const type = extractTokenType(token.$schema); + tokensByType[type] = (tokensByType[type] || 0) + 1; + } + + // ---- 4. Component tokens vs global tokens ---- + const componentTokens = new Map(); + const globalTokens = new Map(); + for (const [name, entry] of allTokens) { + if (entry.token.component) { + componentTokens.set(name, entry); + } else { + globalTokens.set(name, entry); + } + } + + // ---- 5. Component breakdown ---- + const componentBreakdown = {}; + for (const [, entry] of componentTokens) { + const comp = entry.token.component; + if (!componentBreakdown[comp]) { + componentBreakdown[comp] = { count: 0, deprecated: 0 }; + } + componentBreakdown[comp].count++; + if (isDeprecated(entry.token)) { + componentBreakdown[comp].deprecated++; + } + } + + // ---- 6. Deprecation metrics ---- + let deprecatedCount = 0; + let deprecatedWithRenamedCount = 0; + const deprecatedTokenNames = []; + for (const [name, { token }] of allTokens) { + if (isDeprecated(token)) { + deprecatedCount++; + deprecatedTokenNames.push(name); + if (hasRenamedPath(token)) { + deprecatedWithRenamedCount++; + } + } + } + + // ---- 7. Alias analysis ---- + let aliasCount = 0; + let directValueCount = 0; + const aliasReferences = {}; + for (const [, { token }] of allTokens) { + if (isSetToken(token)) { + // Check set values + for (const setValue of Object.values(token.sets)) { + if (isAliasValue(setValue.value)) { + const ref = extractAliasReference(setValue.value); + aliasReferences[ref] = (aliasReferences[ref] || 0) + 1; + } + } + } else if (isAliasValue(token.value)) { + aliasCount++; + const ref = extractAliasReference(token.value); + aliasReferences[ref] = (aliasReferences[ref] || 0) + 1; + } else { + directValueCount++; + } + } + + // ---- 8. Set-based token analysis ---- + let colorThemeSetCount = 0; + let scaleSetCount = 0; + let otherSetCount = 0; + for (const [, { token }] of allTokens) { + const setType = getSetType(token); + if (setType === "color-theme") colorThemeSetCount++; + else if (setType === "scale") scaleSetCount++; + else if (setType === "other") otherSetCount++; + } + + // ---- 9. UUID coverage ---- + let tokensWithUuid = 0; + let tokensWithoutUuid = 0; + for (const [, { token }] of allTokens) { + if (isSetToken(token)) { + // For set tokens, check inner values + const allHaveUuids = Object.values(token.sets).every( + (sv) => sv.uuid !== undefined, + ); + if (allHaveUuids) tokensWithUuid++; + else tokensWithoutUuid++; + } else { + if (token.uuid) tokensWithUuid++; + else tokensWithoutUuid++; + } + } + + // ---- 10. Private tokens ---- + let privateCount = 0; + for (const [, { token }] of allTokens) { + if (token.private === true) privateCount++; + } + + // ---- 11. Most-referenced tokens (alias targets) ---- + const topAliasTargets = Object.entries(aliasReferences) + .sort((a, b) => b[1] - a[1]) + .slice(0, 20) + .map(([name, count]) => ({ name, referenceCount: count })); + + // ---- 12. Naming pattern analysis ---- + const namingCategories = {}; + for (const [name] of allTokens) { + // Extract first segment as category + const firstSegment = name.split("-")[0]; + namingCategories[firstSegment] = (namingCategories[firstSegment] || 0) + 1; + } + const topNamingCategories = Object.entries(namingCategories) + .sort((a, b) => b[1] - a[1]) + .slice(0, 20) + .map(([prefix, count]) => ({ prefix, count })); + + // ---- 13. Alias chain depth analysis ---- + const aliasDepths = computeAliasDepths(allTokens); + + // ---- 14. Token categories (semantic grouping) ---- + const semanticCategories = categorizeTokens(allTokens); + + return { + generatedAt: new Date().toISOString(), + summary: { + totalTokens, + activeTokens: totalTokens - deprecatedCount, + deprecatedTokens: deprecatedCount, + deprecationRate: parseFloat( + ((deprecatedCount / totalTokens) * 100).toFixed(1), + ), + deprecatedWithMigrationPath: deprecatedWithRenamedCount, + migrationPathCoverage: + deprecatedCount > 0 + ? parseFloat( + ((deprecatedWithRenamedCount / deprecatedCount) * 100).toFixed(1), + ) + : 100, + componentTokenCount: componentTokens.size, + globalTokenCount: globalTokens.size, + uniqueComponents: Object.keys(componentBreakdown).length, + privateTokens: privateCount, + }, + tokensByFile, + tokensByType, + componentBreakdown, + deprecatedTokens: deprecatedTokenNames, + aliasAnalysis: { + aliasTokens: aliasCount, + directValueTokens: directValueCount, + setBasedTokens: { + colorTheme: colorThemeSetCount, + scale: scaleSetCount, + other: otherSetCount, + total: colorThemeSetCount + scaleSetCount + otherSetCount, + }, + topAliasTargets, + }, + uuidCoverage: { + withUuid: tokensWithUuid, + withoutUuid: tokensWithoutUuid, + coveragePercent: parseFloat( + ((tokensWithUuid / totalTokens) * 100).toFixed(1), + ), + }, + namingPatterns: topNamingCategories, + aliasChainDepth: aliasDepths, + semanticCategories, + }; +} + +/** + * Compute alias chain depths to find deeply nested references. + * @param {Map} allTokens + * @returns {object} Depth analysis + */ +function computeAliasDepths(allTokens) { + const depthCache = new Map(); + + function getDepth(tokenName, visited = new Set()) { + if (depthCache.has(tokenName)) return depthCache.get(tokenName); + if (visited.has(tokenName)) return 0; // Circular reference guard + visited.add(tokenName); + + const entry = allTokens.get(tokenName); + if (!entry) return 0; + + const { token } = entry; + if (isSetToken(token)) { + // For set tokens, take the max depth of inner values + let maxDepth = 0; + for (const setValue of Object.values(token.sets)) { + if (isAliasValue(setValue.value)) { + const ref = extractAliasReference(setValue.value); + maxDepth = Math.max(maxDepth, 1 + getDepth(ref, new Set(visited))); + } + } + depthCache.set(tokenName, maxDepth); + return maxDepth; + } + + if (isAliasValue(token.value)) { + const ref = extractAliasReference(token.value); + const depth = 1 + getDepth(ref, new Set(visited)); + depthCache.set(tokenName, depth); + return depth; + } + + depthCache.set(tokenName, 0); + return 0; + } + + for (const [name] of allTokens) { + getDepth(name); + } + + // Distribution + const distribution = {}; + let maxDepth = 0; + let maxDepthToken = ""; + for (const [name, depth] of depthCache) { + distribution[depth] = (distribution[depth] || 0) + 1; + if (depth > maxDepth) { + maxDepth = depth; + maxDepthToken = name; + } + } + + return { + maxDepth, + maxDepthToken, + distribution, + }; +} + +/** + * Categorize tokens into semantic groups based on naming patterns. + * @param {Map} allTokens + * @returns {object} Category counts + */ +function categorizeTokens(allTokens) { + const categories = { + background: 0, + border: 0, + content: 0, + typography: 0, + layout: 0, + color: 0, + shadow: 0, + opacity: 0, + icon: 0, + other: 0, + }; + + for (const [name] of allTokens) { + if (name.includes("background")) categories.background++; + else if (name.includes("border")) categories.border++; + else if (name.includes("content") || name.includes("text")) + categories.content++; + else if ( + name.includes("font") || + name.includes("letter") || + name.includes("line-height") + ) + categories.typography++; + else if ( + name.includes("spacing") || + name.includes("size") || + name.includes("width") || + name.includes("height") || + name.includes("radius") || + name.includes("margin") || + name.includes("padding") || + name.includes("edge") || + name.includes("top-to") || + name.includes("bottom-to") + ) + categories.layout++; + else if (name.includes("color") || name.includes("visual")) + categories.color++; + else if (name.includes("shadow") || name.includes("elevation")) + categories.shadow++; + else if (name.includes("opacity")) categories.opacity++; + else if (name.includes("icon")) categories.icon++; + else categories.other++; + } + + return categories; +} + +/** + * Load registered components from the design-system-registry. + * @param {string} registryPath - Path to components.json + * @returns {Promise} Array of component IDs + */ +export async function loadRegisteredComponents( + registryPath = DEFAULT_REGISTRY_COMPONENTS, +) { + try { + const data = await readJson(registryPath); + return data.values.map((c) => c.id); + } catch { + return []; + } +} + +/** + * Load component schemas to identify which components have formal schema definitions. + * @param {string} schemasDir - Path to component-schemas/schemas/components + * @returns {Promise} Array of component IDs with schemas + */ +export async function loadComponentSchemas( + schemasDir = DEFAULT_COMPONENT_SCHEMAS, +) { + try { + const files = await glob(`${schemasDir}/*.json`); + return files.map((f) => basename(f, ".json")); + } catch { + return []; + } +} + +/** + * Compute component coverage: which registered components have tokens and/or schemas. + * @param {Map} tokenFiles - Token file data + * @param {string} registryPath - Path to components.json + * @param {string} schemasDir - Path to component schemas directory + * @returns {Promise} Component coverage report + */ +export async function computeComponentCoverage( + tokenFiles, + registryPath = DEFAULT_REGISTRY_COMPONENTS, + schemasDir = DEFAULT_COMPONENT_SCHEMAS, +) { + const allTokens = flattenTokens(tokenFiles); + const registeredComponents = await loadRegisteredComponents(registryPath); + const schemaComponents = await loadComponentSchemas(schemasDir); + + // Components that have tokens + const componentsWithTokens = new Set(); + for (const [, { token }] of allTokens) { + if (token.component) { + componentsWithTokens.add(token.component); + } + } + + const coverage = registeredComponents.map((compId) => ({ + id: compId, + hasTokens: componentsWithTokens.has(compId), + hasSchema: schemaComponents.includes(compId), + })); + + const withTokens = coverage.filter((c) => c.hasTokens).length; + const withSchema = coverage.filter((c) => c.hasSchema).length; + const withBoth = coverage.filter((c) => c.hasTokens && c.hasSchema).length; + + return { + registeredComponentCount: registeredComponents.length, + componentsWithTokens: withTokens, + componentsWithSchema: withSchema, + componentsWithBoth: withBoth, + tokenCoveragePercent: parseFloat( + ((withTokens / registeredComponents.length) * 100).toFixed(1), + ), + schemaCoveragePercent: parseFloat( + ((withSchema / registeredComponents.length) * 100).toFixed(1), + ), + details: coverage, + tokensOnlyComponents: [...componentsWithTokens].filter( + (c) => !registeredComponents.includes(c), + ), + }; +} + +/** + * Generate the full metrics report including component coverage. + * @param {object} options - Configuration options + * @param {string} options.tokensSrc - Path to tokens/src directory + * @param {string} options.registryPath - Path to components.json + * @param {string} options.schemasDir - Path to component schemas + * @returns {Promise} Full metrics report + */ +export async function generateMetricsReport(options = {}) { + const { + tokensSrc = DEFAULT_TOKENS_SRC, + registryPath = DEFAULT_REGISTRY_COMPONENTS, + schemasDir = DEFAULT_COMPONENT_SCHEMAS, + } = options; + + const tokenFiles = await loadTokenFiles(tokensSrc); + const metrics = computeMetrics(tokenFiles); + const componentCoverage = await computeComponentCoverage( + tokenFiles, + registryPath, + schemasDir, + ); + + return { + ...metrics, + componentCoverage, + }; +} + +export default generateMetricsReport; diff --git a/tools/token-metrics/test/index.test.js b/tools/token-metrics/test/index.test.js new file mode 100644 index 00000000..a1451771 --- /dev/null +++ b/tools/token-metrics/test/index.test.js @@ -0,0 +1,375 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import test from "ava"; +import { + extractTokenType, + isAliasValue, + extractAliasReference, + isSetToken, + getSetType, + isDeprecated, + hasRenamedPath, + flattenTokens, + computeMetrics, +} from "../src/index.js"; + +// --- extractTokenType --- + +test("extractTokenType extracts type from schema URL", (t) => { + t.is( + extractTokenType( + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json", + ), + "alias", + ); + t.is( + extractTokenType( + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color.json", + ), + "color", + ); + t.is( + extractTokenType( + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/dimension.json", + ), + "dimension", + ); + t.is( + extractTokenType( + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json", + ), + "color-set", + ); +}); + +test("extractTokenType returns unknown for missing or invalid schema", (t) => { + t.is(extractTokenType(undefined), "unknown"); + t.is(extractTokenType(null), "unknown"); + t.is(extractTokenType(""), "unknown"); + t.is(extractTokenType("not-a-url"), "unknown"); +}); + +// --- isAliasValue --- + +test("isAliasValue identifies alias references", (t) => { + t.true(isAliasValue("{gray-800}")); + t.true(isAliasValue("{blue-100}")); + t.true(isAliasValue("{accent-color-900}")); +}); + +test("isAliasValue rejects non-alias values", (t) => { + t.false(isAliasValue("10px")); + t.false(isAliasValue("rgba(0, 0, 0, 0.12)")); + t.false(isAliasValue("0.4")); + t.false(isAliasValue(42)); + t.false(isAliasValue(null)); + t.false(isAliasValue(undefined)); +}); + +// --- extractAliasReference --- + +test("extractAliasReference extracts the token name from alias value", (t) => { + t.is(extractAliasReference("{gray-800}"), "gray-800"); + t.is(extractAliasReference("{accent-color-900}"), "accent-color-900"); +}); + +test("extractAliasReference returns null for non-alias values", (t) => { + t.is(extractAliasReference("10px"), null); + t.is(extractAliasReference(null), null); +}); + +// --- isSetToken --- + +test("isSetToken identifies set-based tokens", (t) => { + t.true( + isSetToken({ + sets: { + light: { value: "{gray-100}", uuid: "abc" }, + dark: { value: "{gray-300}", uuid: "def" }, + }, + }), + ); +}); + +test("isSetToken rejects non-set tokens", (t) => { + t.false(isSetToken({ value: "{gray-800}", uuid: "abc" })); + t.false(isSetToken({ value: "10px", uuid: "abc" })); +}); + +// --- getSetType --- + +test("getSetType identifies color theme sets", (t) => { + t.is( + getSetType({ + sets: { + light: { value: "{gray-100}" }, + dark: { value: "{gray-300}" }, + wireframe: { value: "{gray-100}" }, + }, + }), + "color-theme", + ); +}); + +test("getSetType identifies scale sets", (t) => { + t.is( + getSetType({ + sets: { + desktop: { value: "14px" }, + mobile: { value: "18px" }, + }, + }), + "scale", + ); +}); + +test("getSetType returns none for non-set tokens", (t) => { + t.is(getSetType({ value: "10px", uuid: "abc" }), "none"); +}); + +// --- isDeprecated --- + +test("isDeprecated identifies deprecated tokens", (t) => { + t.true(isDeprecated({ deprecated: true, value: "{foo}", uuid: "abc" })); +}); + +test("isDeprecated identifies deprecated set tokens where all sets are deprecated", (t) => { + t.true( + isDeprecated({ + sets: { + light: { deprecated: true, value: "{a}", uuid: "1" }, + dark: { deprecated: true, value: "{b}", uuid: "2" }, + }, + }), + ); +}); + +test("isDeprecated returns false for active tokens", (t) => { + t.false(isDeprecated({ value: "{gray-800}", uuid: "abc" })); + t.false( + isDeprecated({ deprecated: false, value: "{gray-800}", uuid: "abc" }), + ); +}); + +// --- hasRenamedPath --- + +test("hasRenamedPath identifies tokens with migration path", (t) => { + t.true( + hasRenamedPath({ + deprecated: true, + renamed: "new-token-name", + value: "{new-token-name}", + uuid: "abc", + }), + ); +}); + +test("hasRenamedPath returns false for deprecated without renamed", (t) => { + t.false(hasRenamedPath({ deprecated: true, value: "{foo}", uuid: "abc" })); +}); + +test("hasRenamedPath returns false for non-deprecated", (t) => { + t.false(hasRenamedPath({ value: "{foo}", uuid: "abc" })); +}); + +// --- flattenTokens --- + +test("flattenTokens merges all token files into a single map", (t) => { + const tokenFiles = new Map([ + [ + "file1.json", + { + "token-a": { value: "10px", uuid: "1" }, + "token-b": { value: "{gray-800}", uuid: "2" }, + }, + ], + [ + "file2.json", + { + "token-c": { value: "bold", uuid: "3" }, + }, + ], + ]); + + const flat = flattenTokens(tokenFiles); + t.is(flat.size, 3); + t.is(flat.get("token-a").sourceFile, "file1.json"); + t.is(flat.get("token-c").sourceFile, "file2.json"); +}); + +// --- computeMetrics --- + +test("computeMetrics produces a valid metrics report from token data", (t) => { + const tokenFiles = new Map([ + [ + "colors.json", + { + "bg-color": { + $schema: + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json", + value: "{gray-800}", + uuid: "1", + }, + "deprecated-color": { + $schema: + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json", + value: "{new-color}", + uuid: "2", + deprecated: true, + renamed: "new-color", + }, + "theme-color": { + $schema: + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json", + sets: { + light: { + $schema: + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json", + value: "{gray-100}", + uuid: "3a", + }, + dark: { + $schema: + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json", + value: "{gray-300}", + uuid: "3b", + }, + }, + }, + }, + ], + [ + "layout.json", + { + "button-width": { + $schema: + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/dimension.json", + value: "120px", + uuid: "4", + component: "button", + }, + "scale-token": { + $schema: + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/scale-set.json", + sets: { + desktop: { + $schema: + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/dimension.json", + value: "14px", + uuid: "5a", + }, + mobile: { + $schema: + "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/dimension.json", + value: "18px", + uuid: "5b", + }, + }, + }, + }, + ], + ]); + + const metrics = computeMetrics(tokenFiles); + + // Total tokens + t.is(metrics.summary.totalTokens, 5); + + // Active tokens (5 - 1 deprecated = 4) + t.is(metrics.summary.activeTokens, 4); + + // Deprecated + t.is(metrics.summary.deprecatedTokens, 1); + t.is(metrics.summary.deprecatedWithMigrationPath, 1); + t.is(metrics.summary.migrationPathCoverage, 100); + + // Component tokens + t.is(metrics.summary.componentTokenCount, 1); + t.is(metrics.summary.globalTokenCount, 4); + t.is(metrics.summary.uniqueComponents, 1); + + // Tokens by file + t.is(metrics.tokensByFile["colors.json"], 3); + t.is(metrics.tokensByFile["layout.json"], 2); + + // Token types + t.is(metrics.tokensByType["alias"], 2); + t.is(metrics.tokensByType["color-set"], 1); + t.is(metrics.tokensByType["dimension"], 1); + t.is(metrics.tokensByType["scale-set"], 1); + + // Alias analysis + t.is(metrics.aliasAnalysis.aliasTokens, 2); // bg-color and deprecated-color + t.is(metrics.aliasAnalysis.directValueTokens, 1); // button-width + t.is(metrics.aliasAnalysis.setBasedTokens.colorTheme, 1); + t.is(metrics.aliasAnalysis.setBasedTokens.scale, 1); + + // UUID coverage + t.is(metrics.uuidCoverage.coveragePercent, 100); + + // Component breakdown + t.truthy(metrics.componentBreakdown["button"]); + t.is(metrics.componentBreakdown["button"].count, 1); + + // Semantic categories + t.truthy(metrics.semanticCategories); + + // Alias chain depth + t.truthy(metrics.aliasChainDepth); + t.is(typeof metrics.aliasChainDepth.maxDepth, "number"); +}); + +test("computeMetrics handles empty token files", (t) => { + const tokenFiles = new Map(); + const metrics = computeMetrics(tokenFiles); + t.is(metrics.summary.totalTokens, 0); + t.is(metrics.summary.activeTokens, 0); +}); + +// --- Integration: metrics from real token data --- + +test("computeMetrics with real token data produces reasonable results", async (t) => { + // This test uses the actual token files to validate the tool against the real codebase + const { loadTokenFiles, computeMetrics: compute } = + await import("../src/index.js"); + const tokenFiles = await loadTokenFiles(); + const metrics = compute(tokenFiles); + + // Sanity checks against the real data + t.true(metrics.summary.totalTokens > 2000, "should have > 2000 tokens"); + t.true( + metrics.summary.activeTokens > 1800, + "should have > 1800 active tokens", + ); + t.true( + metrics.summary.deprecatedTokens > 0, + "should have some deprecated tokens", + ); + t.true( + metrics.summary.deprecationRate < 20, + "deprecation rate should be < 20%", + ); + t.true( + metrics.summary.uniqueComponents > 30, + "should have > 30 unique components with tokens", + ); + t.is( + metrics.uuidCoverage.coveragePercent, + 100, + "all tokens should have UUIDs", + ); + t.true( + metrics.aliasChainDepth.maxDepth <= 10, + "alias chain depth should be reasonable", + ); +}); diff --git a/tools/token-metrics/token-metrics-report.json b/tools/token-metrics/token-metrics-report.json new file mode 100644 index 00000000..c52bfeea --- /dev/null +++ b/tools/token-metrics/token-metrics-report.json @@ -0,0 +1,1149 @@ +{ + "generatedAt": "2026-02-06T21:23:53.346Z", + "summary": { + "totalTokens": 2338, + "activeTokens": 2150, + "deprecatedTokens": 188, + "deprecationRate": 8, + "deprecatedWithMigrationPath": 57, + "migrationPathCoverage": 30.3, + "componentTokenCount": 1373, + "globalTokenCount": 965, + "uniqueComponents": 95, + "privateTokens": 372 + }, + "tokensByFile": { + "typography.json": 312, + "semantic-color-palette.json": 94, + "layout.json": 242, + "layout-component.json": 997, + "icons.json": 79, + "color-palette.json": 372, + "color-component.json": 73, + "color-aliases.json": 169 + }, + "tokensByType": { + "alias": 654, + "dimension": 292, + "text-align": 3, + "font-family": 4, + "font-weight": 6, + "font-style": 2, + "scale-set": 755, + "multiplier": 24, + "text-transform": 2, + "typography": 15, + "color-set": 470, + "gradient-stop": 9, + "opacity": 30, + "color": 68, + "drop-shadow": 4 + }, + "componentBreakdown": { + "heading": { + "count": 91, + "deprecated": 26 + }, + "body": { + "count": 38, + "deprecated": 0 + }, + "detail": { + "count": 69, + "deprecated": 27 + }, + "code": { + "count": 31, + "deprecated": 0 + }, + "checkbox": { + "count": 8, + "deprecated": 0 + }, + "switch": { + "count": 20, + "deprecated": 0 + }, + "radio-button": { + "count": 9, + "deprecated": 0 + }, + "field-label": { + "count": 17, + "deprecated": 4 + }, + "help-text": { + "count": 5, + "deprecated": 4 + }, + "status-light": { + "count": 12, + "deprecated": 0 + }, + "action-button": { + "count": 5, + "deprecated": 0 + }, + "button": { + "count": 1, + "deprecated": 0 + }, + "tooltip": { + "count": 4, + "deprecated": 0 + }, + "divider": { + "count": 5, + "deprecated": 0 + }, + "progress-circle": { + "count": 6, + "deprecated": 0 + }, + "toast": { + "count": 5, + "deprecated": 0 + }, + "action-bar": { + "count": 10, + "deprecated": 0 + }, + "swatch": { + "count": 13, + "deprecated": 0 + }, + "progress-bar": { + "count": 6, + "deprecated": 0 + }, + "meter": { + "count": 8, + "deprecated": 1 + }, + "in-line-alert": { + "count": 1, + "deprecated": 0 + }, + "tag": { + "count": 17, + "deprecated": 1 + }, + "popover": { + "count": 6, + "deprecated": 1 + }, + "menu": { + "count": 18, + "deprecated": 1 + }, + "slider": { + "count": 45, + "deprecated": 18 + }, + "picker": { + "count": 8, + "deprecated": 1 + }, + "text-field": { + "count": 1, + "deprecated": 0 + }, + "text-area": { + "count": 2, + "deprecated": 0 + }, + "combo-box": { + "count": 8, + "deprecated": 6 + }, + "thumbnail": { + "count": 17, + "deprecated": 0 + }, + "alert-dialog": { + "count": 6, + "deprecated": 2 + }, + "opacity-checkerboard": { + "count": 5, + "deprecated": 1 + }, + "contextual-help": { + "count": 5, + "deprecated": 2 + }, + "breadcrumbs": { + "count": 32, + "deprecated": 13 + }, + "avatar": { + "count": 27, + "deprecated": 0 + }, + "alert-banner": { + "count": 9, + "deprecated": 3 + }, + "rating": { + "count": 12, + "deprecated": 2 + }, + "color-area": { + "count": 8, + "deprecated": 0 + }, + "color-wheel": { + "count": 5, + "deprecated": 0 + }, + "color-slider": { + "count": 6, + "deprecated": 0 + }, + "floating-action-button": { + "count": 4, + "deprecated": 1 + }, + "illustrated-message": { + "count": 15, + "deprecated": 4 + }, + "search-field": { + "count": 1, + "deprecated": 0 + }, + "color-loupe": { + "count": 10, + "deprecated": 3 + }, + "card": { + "count": 49, + "deprecated": 1 + }, + "drop-zone": { + "count": 13, + "deprecated": 4 + }, + "coach-mark": { + "count": 14, + "deprecated": 3 + }, + "accordion": { + "count": 53, + "deprecated": 11 + }, + "color-handle": { + "count": 13, + "deprecated": 1 + }, + "table": { + "count": 108, + "deprecated": 20 + }, + "tab-item": { + "count": 49, + "deprecated": 0 + }, + "side-navigation": { + "count": 23, + "deprecated": 3 + }, + "tray": { + "count": 1, + "deprecated": 0 + }, + "in-field-button": { + "count": 23, + "deprecated": 1 + }, + "arrow-icon": { + "count": 7, + "deprecated": 0 + }, + "asterisk-icon": { + "count": 4, + "deprecated": 0 + }, + "checkmark-icon": { + "count": 8, + "deprecated": 0 + }, + "chevron-icon": { + "count": 8, + "deprecated": 0 + }, + "cross-icon": { + "count": 7, + "deprecated": 0 + }, + "dash-icon": { + "count": 8, + "deprecated": 0 + }, + "title": { + "count": 45, + "deprecated": 0 + }, + "field": { + "count": 4, + "deprecated": 0 + }, + "in-field-progress-circle": { + "count": 5, + "deprecated": 0 + }, + "standard-dialog": { + "count": 6, + "deprecated": 0 + }, + "link-out-icon": { + "count": 5, + "deprecated": 0 + }, + "menu-item": { + "count": 14, + "deprecated": 0 + }, + "coach-indicator": { + "count": 8, + "deprecated": 0 + }, + "swatch-group": { + "count": 3, + "deprecated": 0 + }, + "avatar-group": { + "count": 7, + "deprecated": 0 + }, + "standard-panel": { + "count": 12, + "deprecated": 0 + }, + "bar-panel": { + "count": 6, + "deprecated": 0 + }, + "select-box": { + "count": 14, + "deprecated": 0 + }, + "segmented-control": { + "count": 3, + "deprecated": 0 + }, + "in-field-stepper": { + "count": 4, + "deprecated": 0 + }, + "number-field": { + "count": 9, + "deprecated": 0 + }, + "takeover-dialog": { + "count": 2, + "deprecated": 0 + }, + "tree-view": { + "count": 28, + "deprecated": 1 + }, + "collection-card": { + "count": 10, + "deprecated": 0 + }, + "user-card": { + "count": 8, + "deprecated": 0 + }, + "steplist": { + "count": 18, + "deprecated": 0 + }, + "card-horizontal": { + "count": 3, + "deprecated": 0 + }, + "single-calendar": { + "count": 2, + "deprecated": 0 + }, + "double-calendar": { + "count": 2, + "deprecated": 0 + }, + "triple-calendar": { + "count": 2, + "deprecated": 0 + }, + "date-field": { + "count": 2, + "deprecated": 0 + }, + "date-picker": { + "count": 3, + "deprecated": 0 + }, + "time-field": { + "count": 2, + "deprecated": 0 + }, + "segmented-text-field": { + "count": 2, + "deprecated": 0 + }, + "add-icon": { + "count": 5, + "deprecated": 0 + }, + "drag-handle-icon": { + "count": 4, + "deprecated": 0 + }, + "gripper-icon": { + "count": 1, + "deprecated": 0 + }, + "tag-field": { + "count": 10, + "deprecated": 0 + }, + "list-view": { + "count": 5, + "deprecated": 0 + }, + "stack-item": { + "count": 21, + "deprecated": 0 + }, + "icon": { + "count": 79, + "deprecated": 1 + } + }, + "deprecatedTokens": [ + "heading-sans-serif-light-font-weight", + "heading-sans-serif-light-font-style", + "heading-serif-light-font-weight", + "heading-serif-light-font-style", + "heading-cjk-light-font-weight", + "heading-cjk-light-font-style", + "heading-sans-serif-light-strong-font-weight", + "heading-sans-serif-light-strong-font-style", + "heading-serif-light-strong-font-weight", + "heading-serif-light-strong-font-style", + "heading-cjk-light-strong-font-weight", + "heading-cjk-light-strong-font-style", + "heading-sans-serif-light-emphasized-font-weight", + "heading-sans-serif-light-emphasized-font-style", + "heading-serif-light-emphasized-font-weight", + "heading-serif-light-emphasized-font-style", + "heading-cjk-light-emphasized-font-weight", + "heading-cjk-light-emphasized-font-style", + "heading-sans-serif-light-strong-emphasized-font-weight", + "heading-sans-serif-light-strong-emphasized-font-style", + "heading-serif-light-strong-emphasized-font-weight", + "heading-serif-light-strong-emphasized-font-style", + "heading-cjk-light-strong-emphasized-font-weight", + "heading-cjk-light-strong-emphasized-font-style", + "heading-size-xxs", + "heading-cjk-size-xxs", + "detail-sans-serif-light-font-weight", + "detail-sans-serif-light-font-style", + "detail-serif-light-font-weight", + "detail-serif-light-font-style", + "detail-cjk-light-font-weight", + "detail-cjk-light-font-style", + "detail-sans-serif-light-strong-font-weight", + "detail-sans-serif-light-strong-font-style", + "detail-serif-light-strong-font-weight", + "detail-serif-light-strong-font-style", + "detail-cjk-light-strong-font-weight", + "detail-cjk-light-strong-font-style", + "detail-sans-serif-light-emphasized-font-weight", + "detail-sans-serif-light-emphasized-font-style", + "detail-serif-light-emphasized-font-weight", + "detail-serif-light-emphasized-font-style", + "detail-cjk-light-emphasized-font-weight", + "detail-cjk-light-emphasized-font-style", + "detail-sans-serif-light-strong-emphasized-font-weight", + "detail-sans-serif-light-strong-emphasized-font-style", + "detail-serif-light-strong-emphasized-font-weight", + "detail-serif-light-strong-emphasized-font-style", + "detail-cjk-light-strong-emphasized-font-weight", + "detail-cjk-light-strong-emphasized-font-style", + "detail-letter-spacing", + "detail-sans-serif-text-transform", + "detail-serif-text-transform", + "negative-subdued-background-color-default", + "negative-subdued-background-color-hover", + "negative-subdued-background-color-down", + "negative-subdued-background-color-key-focus", + "drop-shadow-x", + "drop-shadow-y", + "drop-shadow-blur", + "field-edge-to-text-quiet", + "field-edge-to-border-quiet", + "field-edge-to-alert-icon-quiet", + "field-edge-to-validation-icon-quiet", + "field-width", + "field-width-small", + "character-count-to-field-quiet-small", + "character-count-to-field-quiet-medium", + "character-count-to-field-quiet-large", + "character-count-to-field-quiet-extra-large", + "field-width-medium", + "field-width-large", + "field-width-extra-large", + "field-label-to-component-quiet-small", + "field-label-to-component-quiet-medium", + "field-label-to-component-quiet-large", + "field-label-to-component-quiet-extra-large", + "help-text-top-to-workflow-icon-small", + "help-text-top-to-workflow-icon-medium", + "help-text-top-to-workflow-icon-large", + "help-text-top-to-workflow-icon-extra-large", + "meter-default-width", + "popover-top-to-content-area", + "menu-item-label-to-description", + "slider-track-thickness", + "slider-control-height-small", + "slider-control-height-medium", + "slider-control-height-large", + "slider-control-height-extra-large", + "slider-handle-size-small", + "slider-handle-size-medium", + "slider-handle-size-large", + "slider-handle-size-extra-large", + "slider-handle-border-width-down-small", + "slider-handle-border-width-down-medium", + "slider-handle-border-width-down-large", + "slider-handle-border-width-down-extra-large", + "slider-handle-gap", + "slider-bottom-to-handle-small", + "slider-bottom-to-handle-medium", + "slider-bottom-to-handle-large", + "slider-bottom-to-handle-extra-large", + "picker-end-edge-to-disclousure-icon-quiet", + "combo-box-quiet-minimum-width-multiplier", + "combo-box-visual-to-field-button-small", + "combo-box-visual-to-field-button-medium", + "combo-box-visual-to-field-button-large", + "combo-box-visual-to-field-button-extra-large", + "combo-box-visual-to-field-button-quiet", + "alert-dialog-title-size", + "alert-dialog-description-size", + "opacity-checkerboard-square-size", + "contextual-help-title-size", + "contextual-help-body-size", + "breadcrumbs-height", + "breadcrumbs-height-compact", + "breadcrumbs-top-to-text", + "breadcrumbs-top-to-text-compact", + "breadcrumbs-bottom-to-text", + "breadcrumbs-bottom-to-text-compact", + "breadcrumbs-start-edge-to-text", + "breadcrumbs-top-to-separator-icon", + "breadcrumbs-top-to-separator-icon-compact", + "breadcrumbs-top-to-separator-icon-multiline", + "breadcrumbs-separator-icon-to-bottom-text-multiline", + "breadcrumbs-truncated-menu-to-separator-icon", + "breadcrumbs-top-to-truncated-menu-compact", + "alert-banner-to-top-workflow-icon", + "alert-banner-to-top-text", + "alert-banner-to-bottom-text", + "rating-indicator-width", + "rating-indicator-to-icon", + "illustrated-message-maximum-width", + "illustrated-message-title-size", + "illustrated-message-cjk-title-size", + "illustrated-message-body-size", + "card-minimum-width", + "drop-zone-content-maximum-width", + "drop-zone-title-size", + "drop-zone-cjk-title-size", + "drop-zone-body-size", + "coach-mark-title-size", + "coach-mark-body-size", + "coach-mark-pagination-body-size", + "accordion-top-to-text-regular-small", + "accordion-small-top-to-text-spacious", + "accordion-top-to-text-regular-medium", + "accordion-top-to-text-regular-large", + "accordion-top-to-text-regular-extra-large", + "accordion-bottom-to-text-regular-small", + "accordion-bottom-to-text-regular-medium", + "accordion-bottom-to-text-regular-large", + "accordion-bottom-to-text-regular-extra-large", + "accordion-disclosure-indicator-to-text", + "accordion-edge-to-disclosure-indicator", + "table-row-height-small-regular", + "table-row-height-medium-regular", + "table-row-height-large-regular", + "table-row-height-extra-large-regular", + "table-row-top-to-text-small-regular", + "table-row-top-to-text-medium-regular", + "table-row-top-to-text-large-regular", + "table-row-top-to-text-extra-large-regular", + "table-row-bottom-to-text-small-regular", + "table-row-bottom-to-text-medium-regular", + "table-row-bottom-to-text-large-regular", + "table-row-bottom-to-text-extra-large-regular", + "table-row-checkbox-to-top-small-regular", + "table-row-checkbox-to-top-medium-regular", + "table-row-checkbox-to-top-large-regular", + "table-row-checkbox-to-top-extra-large-regular", + "table-thumbnail-to-top-minimum-small-regular", + "table-thumbnail-to-top-minimum-medium-regular", + "table-thumbnail-to-top-minimum-large-regular", + "table-thumbnail-to-top-minimum-extra-large-regular", + "side-navigation-maximum-width", + "side-navigation-with-icon-second-level-edge-to-text", + "side-navigation-with-icon-third-level-edge-to-text", + "in-field-button-edge-to-fill", + "tag-minimum-width-multiplier", + "tree-view-item-to-item", + "icon-color-inverse", + "color-loupe-drop-shadow-color", + "color-loupe-drop-shadow-y", + "color-loupe-drop-shadow-blur", + "color-handle-drop-shadow-color", + "floating-action-button-shadow-color", + "drop-shadow-color" + ], + "aliasAnalysis": { + "aliasTokens": 654, + "directValueTokens": 459, + "setBasedTokens": { + "colorTheme": 470, + "scale": 755, + "other": 0, + "total": 1225 + }, + "topAliasTargets": [ + { + "name": "default-font-style", + "referenceCount": 62 + }, + { + "name": "italic-font-style", + "referenceCount": 30 + }, + { + "name": "gray-100", + "referenceCount": 26 + }, + { + "name": "bold-font-weight", + "referenceCount": 25 + }, + { + "name": "regular-font-weight", + "referenceCount": 21 + }, + { + "name": "black-font-weight", + "referenceCount": 20 + }, + { + "name": "extra-bold-font-weight", + "referenceCount": 19 + }, + { + "name": "gray-800", + "referenceCount": 19 + }, + { + "name": "gray-900", + "referenceCount": 13 + }, + { + "name": "gray-200", + "referenceCount": 12 + }, + { + "name": "negative-color-1000", + "referenceCount": 12 + }, + { + "name": "font-size-100", + "referenceCount": 10 + }, + { + "name": "accent-color-1000", + "referenceCount": 9 + }, + { + "name": "font-size-300", + "referenceCount": 8 + }, + { + "name": "font-size-200", + "referenceCount": 8 + }, + { + "name": "blue-800", + "referenceCount": 8 + }, + { + "name": "blue-900", + "referenceCount": 8 + }, + { + "name": "font-size-400", + "referenceCount": 7 + }, + { + "name": "font-size-75", + "referenceCount": 7 + }, + { + "name": "body-size-s", + "referenceCount": 7 + } + ] + }, + "uuidCoverage": { + "withUuid": 2338, + "withoutUuid": 0, + "coveragePercent": 100 + }, + "namingPatterns": [ + { + "prefix": "table", + "count": 108 + }, + { + "prefix": "heading", + "count": 98 + }, + { + "prefix": "icon", + "count": 84 + }, + { + "prefix": "field", + "count": 83 + }, + { + "prefix": "component", + "count": 77 + }, + { + "prefix": "detail", + "count": 69 + }, + { + "prefix": "drop", + "count": 55 + }, + { + "prefix": "accordion", + "count": 53 + }, + { + "prefix": "card", + "count": 52 + }, + { + "prefix": "tab", + "count": 49 + }, + { + "prefix": "static", + "count": 48 + }, + { + "prefix": "body", + "count": 47 + }, + { + "prefix": "title", + "count": 46 + }, + { + "prefix": "slider", + "count": 45 + }, + { + "prefix": "color", + "count": 43 + }, + { + "prefix": "negative", + "count": 36 + }, + { + "prefix": "avatar", + "count": 34 + }, + { + "prefix": "in", + "count": 33 + }, + { + "prefix": "menu", + "count": 32 + }, + { + "prefix": "breadcrumbs", + "count": 32 + } + ], + "aliasChainDepth": { + "maxDepth": 4, + "maxDepthToken": "drop-zone-title-size", + "distribution": { + "0": 1479, + "1": 744, + "2": 88, + "3": 24, + "4": 3 + } + }, + "semanticCategories": { + "background": 151, + "border": 58, + "content": 289, + "typography": 284, + "layout": 741, + "color": 227, + "shadow": 23, + "opacity": 9, + "icon": 6, + "other": 550 + }, + "componentCoverage": { + "registeredComponentCount": 54, + "componentsWithTokens": 44, + "componentsWithSchema": 54, + "componentsWithBoth": 44, + "tokenCoveragePercent": 81.5, + "schemaCoveragePercent": 100, + "details": [ + { + "id": "accordion", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "action-bar", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "action-button", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "action-group", + "hasTokens": false, + "hasSchema": true + }, + { + "id": "alert-banner", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "alert-dialog", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "avatar", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "avatar-group", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "badge", + "hasTokens": false, + "hasSchema": true + }, + { + "id": "breadcrumbs", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "button", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "button-group", + "hasTokens": false, + "hasSchema": true + }, + { + "id": "calendar", + "hasTokens": false, + "hasSchema": true + }, + { + "id": "checkbox", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "checkbox-group", + "hasTokens": false, + "hasSchema": true + }, + { + "id": "close-button", + "hasTokens": false, + "hasSchema": true + }, + { + "id": "color-area", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "color-slider", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "color-wheel", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "combo-box", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "contextual-help", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "date-picker", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "divider", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "drop-zone", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "field-label", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "help-text", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "illustrated-message", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "in-line-alert", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "link", + "hasTokens": false, + "hasSchema": true + }, + { + "id": "menu", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "meter", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "number-field", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "picker", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "popover", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "progress-bar", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "progress-circle", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "radio-button", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "radio-group", + "hasTokens": false, + "hasSchema": true + }, + { + "id": "rating", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "search-field", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "select-box", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "slider", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "status-light", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "swatch", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "switch", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "table", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "tabs", + "hasTokens": false, + "hasSchema": true + }, + { + "id": "tag", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "tag-group", + "hasTokens": false, + "hasSchema": true + }, + { + "id": "text-area", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "text-field", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "toast", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "tooltip", + "hasTokens": true, + "hasSchema": true + }, + { + "id": "tray", + "hasTokens": true, + "hasSchema": true + } + ], + "tokensOnlyComponents": [ + "heading", + "body", + "detail", + "code", + "thumbnail", + "opacity-checkerboard", + "floating-action-button", + "color-loupe", + "card", + "coach-mark", + "color-handle", + "tab-item", + "side-navigation", + "in-field-button", + "arrow-icon", + "asterisk-icon", + "checkmark-icon", + "chevron-icon", + "cross-icon", + "dash-icon", + "title", + "field", + "in-field-progress-circle", + "standard-dialog", + "link-out-icon", + "menu-item", + "coach-indicator", + "swatch-group", + "standard-panel", + "bar-panel", + "segmented-control", + "in-field-stepper", + "takeover-dialog", + "tree-view", + "collection-card", + "user-card", + "steplist", + "card-horizontal", + "single-calendar", + "double-calendar", + "triple-calendar", + "date-field", + "time-field", + "segmented-text-field", + "add-icon", + "drag-handle-icon", + "gripper-icon", + "tag-field", + "list-view", + "stack-item", + "icon" + ] + } +}