From 24d659c4f013d34f8a43804d093b7fd05565e5d9 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 20 Jan 2026 09:16:57 -0500 Subject: [PATCH 1/8] feat(API): added support for design tokens --- .../api/__tests__/[version]/tokens.test.ts | 106 ++++++++ .../[version]/tokens/[category].test.ts | 201 ++++++++++++++ .../__tests__/[version]/tokens/all.test.ts | 223 ++++++++++++++++ src/pages/api/[version]/tokens.ts | 32 +++ src/pages/api/[version]/tokens/[category].ts | 56 ++++ src/pages/api/[version]/tokens/all.ts | 41 +++ src/pages/api/openapi.json.ts | 246 ++++++++++++++++++ src/utils/tokens.ts | 150 +++++++++++ 8 files changed, 1055 insertions(+) create mode 100644 src/__tests__/pages/api/__tests__/[version]/tokens.test.ts create mode 100644 src/__tests__/pages/api/__tests__/[version]/tokens/[category].test.ts create mode 100644 src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts create mode 100644 src/pages/api/[version]/tokens.ts create mode 100644 src/pages/api/[version]/tokens/[category].ts create mode 100644 src/pages/api/[version]/tokens/all.ts create mode 100644 src/utils/tokens.ts diff --git a/src/__tests__/pages/api/__tests__/[version]/tokens.test.ts b/src/__tests__/pages/api/__tests__/[version]/tokens.test.ts new file mode 100644 index 0000000..d442497 --- /dev/null +++ b/src/__tests__/pages/api/__tests__/[version]/tokens.test.ts @@ -0,0 +1,106 @@ +import { GET } from '../../../../../pages/api/[version]/tokens' + +const mockApiIndex = { + versions: ['v5', 'v6'], + sections: {}, + pages: {}, + tabs: {}, +} + +jest.mock('../../../../../utils/tokens', () => ({ + getTokenCategories: jest.fn(() => [ + 'c', + 'chart', + 'global', + 'hidden', + 'l', + 't', + ]), +})) + +it('returns sorted token categories for valid version', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/tokens'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(response.headers.get('Content-Type')).toBe( + 'application/json; charset=utf-8', + ) + expect(Array.isArray(body)).toBe(true) + expect(body).toEqual(['c', 'chart', 'global', 'hidden', 'l', 't']) + + jest.restoreAllMocks() +}) + +it('returns categories alphabetically sorted', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/tokens'), + } as any) + const body = await response.json() + + const sorted = [...body].sort() + expect(body).toEqual(sorted) + + jest.restoreAllMocks() +}) + +it('returns 404 error for nonexistent version', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v99' }, + url: new URL('http://localhost:4321/api/v99/tokens'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('v99') + expect(body.error).toContain('not found') + + jest.restoreAllMocks() +}) + +it('returns 400 error when version parameter is missing', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: {}, + url: new URL('http://localhost:4321/api/tokens'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Version parameter is required') + + jest.restoreAllMocks() +}) diff --git a/src/__tests__/pages/api/__tests__/[version]/tokens/[category].test.ts b/src/__tests__/pages/api/__tests__/[version]/tokens/[category].test.ts new file mode 100644 index 0000000..3dad238 --- /dev/null +++ b/src/__tests__/pages/api/__tests__/[version]/tokens/[category].test.ts @@ -0,0 +1,201 @@ +import { GET } from '../../../../../../pages/api/[version]/tokens/[category]' + +const mockApiIndex = { + versions: ['v5', 'v6'], + sections: {}, + pages: {}, + tabs: {}, +} + +const mockTokens = { + c: [ + { + name: '--pf-v6-c-alert--Color', + value: '#000', + var: 'var(--pf-v6-c-alert--Color)', + }, + { + name: '--pf-v6-c-button--Color', + value: '#fff', + var: 'var(--pf-v6-c-button--Color)', + }, + ], + t: [ + { + name: '--pf-v6-t-global--Color', + value: '#333', + var: 'var(--pf-v6-t-global--Color)', + }, + ], +} + +jest.mock('../../../../../../utils/tokens', () => ({ + getTokenCategories: jest.fn(() => ['c', 't']), + getTokensForCategory: jest.fn( + (category: string) => mockTokens[category as keyof typeof mockTokens], + ), + filterTokens: jest.fn((tokens, filter) => + tokens.filter((token: any) => + token.name.toLowerCase().includes(filter.toLowerCase()), + ), + ), +})) + +it('returns tokens for valid category', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6', category: 'c' }, + url: new URL('http://localhost:4321/api/v6/tokens/c'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(response.headers.get('Content-Type')).toBe( + 'application/json; charset=utf-8', + ) + expect(Array.isArray(body)).toBe(true) + expect(body).toHaveLength(2) + expect(body[0]).toHaveProperty('name') + expect(body[0]).toHaveProperty('value') + expect(body[0]).toHaveProperty('var') + + jest.restoreAllMocks() +}) + +it('filters tokens when filter parameter is provided', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6', category: 'c' }, + url: new URL('http://localhost:4321/api/v6/tokens/c?filter=alert'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(Array.isArray(body)).toBe(true) + expect(body).toHaveLength(1) + expect(body[0].name).toContain('alert') + + jest.restoreAllMocks() +}) + +it('returns empty array when filter yields no matches', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6', category: 'c' }, + url: new URL('http://localhost:4321/api/v6/tokens/c?filter=nonexistent'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(Array.isArray(body)).toBe(true) + expect(body).toHaveLength(0) + + jest.restoreAllMocks() +}) + +it('returns 404 error for invalid category with valid categories list', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6', category: 'invalid' }, + url: new URL('http://localhost:4321/api/v6/tokens/invalid'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('invalid') + expect(body.error).toContain('not found') + expect(body).toHaveProperty('validCategories') + expect(Array.isArray(body.validCategories)).toBe(true) + expect(body.validCategories).toContain('c') + expect(body.validCategories).toContain('t') + + jest.restoreAllMocks() +}) + +it('returns 404 error for nonexistent version', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v99', category: 'c' }, + url: new URL('http://localhost:4321/api/v99/tokens/c'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('v99') + + jest.restoreAllMocks() +}) + +it('returns 400 error when parameters are missing', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: {}, + url: new URL('http://localhost:4321/api/tokens/'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('required') + + jest.restoreAllMocks() +}) + +it('filter is case-insensitive', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6', category: 'c' }, + url: new URL('http://localhost:4321/api/v6/tokens/c?filter=ALERT'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(Array.isArray(body)).toBe(true) + expect(body).toHaveLength(1) + + jest.restoreAllMocks() +}) diff --git a/src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts b/src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts new file mode 100644 index 0000000..164854b --- /dev/null +++ b/src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts @@ -0,0 +1,223 @@ +import { GET } from '../../../../../../pages/api/[version]/tokens/all' + +const mockApiIndex = { + versions: ['v5', 'v6'], + sections: {}, + pages: {}, + tabs: {}, +} + +const mockTokensByCategory = { + c: [ + { + name: '--pf-v6-c-alert--Color', + value: '#000', + var: 'var(--pf-v6-c-alert--Color)', + }, + { + name: '--pf-v6-c-button--Color', + value: '#fff', + var: 'var(--pf-v6-c-button--Color)', + }, + ], + t: [ + { + name: '--pf-v6-t-global--Color', + value: '#333', + var: 'var(--pf-v6-t-global--Color)', + }, + ], + chart: [ + { + name: '--pf-v6-chart-global--Color', + value: '#666', + var: 'var(--pf-v6-chart-global--Color)', + }, + ], +} + +jest.mock('../../../../../../utils/tokens', () => ({ + getTokensByCategory: jest.fn(() => mockTokensByCategory), + filterTokensByCategory: jest.fn((byCategory, filter) => { + const filtered: any = {} + for (const [category, tokens] of Object.entries(byCategory)) { + const filteredTokens = (tokens as any[]).filter((token: any) => + token.name.toLowerCase().includes(filter.toLowerCase()), + ) + if (filteredTokens.length > 0) { + filtered[category] = filteredTokens + } + } + return filtered + }), +})) + +it('returns all tokens grouped by category', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/tokens/all'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(response.headers.get('Content-Type')).toBe( + 'application/json; charset=utf-8', + ) + expect(typeof body).toBe('object') + expect(body).toHaveProperty('c') + expect(body).toHaveProperty('t') + expect(body).toHaveProperty('chart') + expect(Array.isArray(body.c)).toBe(true) + expect(body.c).toHaveLength(2) + expect(body.t).toHaveLength(1) + + jest.restoreAllMocks() +}) + +it('filters tokens across all categories when filter parameter is provided', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/tokens/all?filter=alert'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(typeof body).toBe('object') + expect(body).toHaveProperty('c') + expect(body.c).toHaveLength(1) + expect(body.c[0].name).toContain('alert') + expect(body).not.toHaveProperty('t') + expect(body).not.toHaveProperty('chart') + + jest.restoreAllMocks() +}) + +it('returns empty object when filter yields no matches', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/tokens/all?filter=nonexistent'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(typeof body).toBe('object') + expect(Object.keys(body)).toHaveLength(0) + + jest.restoreAllMocks() +}) + +it('filter is case-insensitive', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/tokens/all?filter=GLOBAL'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(typeof body).toBe('object') + expect(Object.keys(body).length).toBeGreaterThan(0) + + jest.restoreAllMocks() +}) + +it('returns 404 error for nonexistent version', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v99' }, + url: new URL('http://localhost:4321/api/v99/tokens/all'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('v99') + expect(body.error).toContain('not found') + + jest.restoreAllMocks() +}) + +it('returns 400 error when version parameter is missing', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: {}, + url: new URL('http://localhost:4321/api/tokens/all'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Version parameter is required') + + jest.restoreAllMocks() +}) + +it('each token has required properties', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/tokens/all'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + + for (const [category, tokens] of Object.entries(body)) { + expect(Array.isArray(tokens)).toBe(true) + for (const token of tokens as any[]) { + expect(token).toHaveProperty('name') + expect(token).toHaveProperty('value') + expect(token).toHaveProperty('var') + expect(typeof token.name).toBe('string') + expect(typeof token.value).toBe('string') + expect(typeof token.var).toBe('string') + } + } + + jest.restoreAllMocks() +}) diff --git a/src/pages/api/[version]/tokens.ts b/src/pages/api/[version]/tokens.ts new file mode 100644 index 0000000..4039426 --- /dev/null +++ b/src/pages/api/[version]/tokens.ts @@ -0,0 +1,32 @@ +import type { APIRoute } from 'astro' +import { createJsonResponse } from '../../../utils/apiHelpers' +import { fetchApiIndex } from '../../../utils/apiIndex/fetch' +import { getTokenCategories } from '../../../utils/tokens' + +export const prerender = false + +export const GET: APIRoute = async ({ params, url }) => { + const { version } = params + + if (!version) { + return createJsonResponse({ error: 'Version parameter is required' }, 400) + } + + try { + const index = await fetchApiIndex(url) + if (!index.versions.includes(version)) { + return createJsonResponse( + { error: `Version '${version}' not found` }, + 404, + ) + } + + const categories = getTokenCategories() + return createJsonResponse(categories) + } catch (error) { + return createJsonResponse( + { error: 'Failed to load tokens', details: error }, + 500, + ) + } +} diff --git a/src/pages/api/[version]/tokens/[category].ts b/src/pages/api/[version]/tokens/[category].ts new file mode 100644 index 0000000..e692116 --- /dev/null +++ b/src/pages/api/[version]/tokens/[category].ts @@ -0,0 +1,56 @@ +import type { APIRoute } from 'astro' +import { createJsonResponse } from '../../../../utils/apiHelpers' +import { fetchApiIndex } from '../../../../utils/apiIndex/fetch' +import { + getTokenCategories, + getTokensForCategory, + filterTokens, +} from '../../../../utils/tokens' + +export const prerender = false + +export const GET: APIRoute = async ({ params, url }) => { + const { version, category } = params + + if (!version || !category) { + return createJsonResponse( + { error: 'Version and category parameters are required' }, + 400, + ) + } + + try { + const index = await fetchApiIndex(url) + if (!index.versions.includes(version)) { + return createJsonResponse( + { error: `Version '${version}' not found` }, + 404, + ) + } + + const tokens = getTokensForCategory(category) + + if (!tokens) { + const validCategories = getTokenCategories() + return createJsonResponse( + { + error: `Category '${category}' not found`, + validCategories, + }, + 404, + ) + } + + const filterParam = url.searchParams.get('filter') + const filteredTokens = filterParam + ? filterTokens(tokens, filterParam) + : tokens + + return createJsonResponse(filteredTokens) + } catch (error) { + return createJsonResponse( + { error: 'Failed to load tokens', details: error }, + 500, + ) + } +} diff --git a/src/pages/api/[version]/tokens/all.ts b/src/pages/api/[version]/tokens/all.ts new file mode 100644 index 0000000..bd19e75 --- /dev/null +++ b/src/pages/api/[version]/tokens/all.ts @@ -0,0 +1,41 @@ +import type { APIRoute } from 'astro' +import { createJsonResponse } from '../../../../utils/apiHelpers' +import { fetchApiIndex } from '../../../../utils/apiIndex/fetch' +import { + getTokensByCategory, + filterTokensByCategory, +} from '../../../../utils/tokens' + +export const prerender = false + +export const GET: APIRoute = async ({ params, url }) => { + const { version } = params + + if (!version) { + return createJsonResponse({ error: 'Version parameter is required' }, 400) + } + + try { + const index = await fetchApiIndex(url) + if (!index.versions.includes(version)) { + return createJsonResponse( + { error: `Version '${version}' not found` }, + 404, + ) + } + + const tokensByCategory = getTokensByCategory() + + const filterParam = url.searchParams.get('filter') + const filteredTokens = filterParam + ? filterTokensByCategory(tokensByCategory, filterParam) + : tokensByCategory + + return createJsonResponse(filteredTokens) + } catch (error) { + return createJsonResponse( + { error: 'Failed to load tokens', details: error }, + 500, + ) + } +} diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index 0262f0d..14483e6 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -641,6 +641,252 @@ export const GET: APIRoute = async ({ url }) => { }, }, }, + '/{version}/tokens': { + get: { + summary: 'List token categories', + description: + 'Returns an alphabetically sorted array of available design token categories from @patternfly/react-tokens. Categories are determined by token name prefixes (e.g., c_, t_, chart_). Optimized for MCP/LLM consumption.', + operationId: 'getTokenCategories', + parameters: [ + { + name: 'version', + in: 'path', + required: true, + description: 'Documentation version', + schema: { + type: 'string', + enum: versions, + }, + example: 'v6', + }, + ], + responses: { + '200': { + description: 'List of token categories', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'string', + }, + }, + example: ['c', 'chart', 'global', 'hidden', 'l', 't'], + }, + }, + }, + '404': { + description: 'Version not found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + '/{version}/tokens/{category}': { + get: { + summary: 'Get tokens for a category', + description: + 'Returns design tokens for a specific category with optional filtering. Each token includes name (CSS variable name), value (resolved value), and var (CSS var() reference). Use the filter query parameter for case-insensitive substring matching to minimize response size.', + operationId: 'getTokensByCategory', + parameters: [ + { + name: 'version', + in: 'path', + required: true, + description: 'Documentation version', + schema: { + type: 'string', + enum: versions, + }, + example: 'v6', + }, + { + name: 'category', + in: 'path', + required: true, + description: 'Token category (e.g., c, t, chart)', + schema: { + type: 'string', + }, + example: 'c', + }, + { + name: 'filter', + in: 'query', + required: false, + description: 'Case-insensitive substring filter to match against token names', + schema: { + type: 'string', + }, + example: 'alert', + }, + ], + responses: { + '200': { + description: 'Array of tokens matching the criteria', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'CSS variable name', + }, + value: { + type: 'string', + description: 'Resolved CSS value', + }, + var: { + type: 'string', + description: 'CSS var() reference', + }, + }, + required: ['name', 'value', 'var'], + }, + }, + example: [ + { + name: '--pf-v6-c-alert--Color', + value: '#000', + var: 'var(--pf-v6-c-alert--Color)', + }, + ], + }, + }, + }, + '404': { + description: 'Category not found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string', + }, + validCategories: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + '/{version}/tokens/all': { + get: { + summary: 'Get all tokens grouped by category', + description: + 'Returns all design tokens organized by category with optional filtering. Use the filter query parameter to minimize response size for MCP/LLM consumption. Empty categories are excluded from filtered results.', + operationId: 'getAllTokens', + parameters: [ + { + name: 'version', + in: 'path', + required: true, + description: 'Documentation version', + schema: { + type: 'string', + enum: versions, + }, + example: 'v6', + }, + { + name: 'filter', + in: 'query', + required: false, + description: 'Case-insensitive substring filter to match against token names across all categories', + schema: { + type: 'string', + }, + example: 'color', + }, + ], + responses: { + '200': { + description: 'Object with category keys and token arrays as values', + content: { + 'application/json': { + schema: { + type: 'object', + additionalProperties: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'CSS variable name', + }, + value: { + type: 'string', + description: 'Resolved CSS value', + }, + var: { + type: 'string', + description: 'CSS var() reference', + }, + }, + required: ['name', 'value', 'var'], + }, + }, + }, + example: { + c: [ + { + name: '--pf-v6-c-alert--Color', + value: '#000', + var: 'var(--pf-v6-c-alert--Color)', + }, + ], + t: [ + { + name: '--pf-v6-t-global--Color', + value: '#333', + var: 'var(--pf-v6-t-global--Color)', + }, + ], + }, + }, + }, + }, + '404': { + description: 'Version not found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, }, tags: [ { diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts new file mode 100644 index 0000000..9a7a77d --- /dev/null +++ b/src/utils/tokens.ts @@ -0,0 +1,150 @@ +import * as allTokens from '@patternfly/react-tokens' + +export interface Token { + name: string + value: string + var: string +} + +export interface TokensByCategory { + [category: string]: Token[] +} + +let cachedTokens: Token[] | null = null +let cachedCategories: string[] | null = null +let cachedTokensByCategory: TokensByCategory | null = null + +/** + * Extracts the category from a token name + * Categories are determined by the first prefix before underscore + * Examples: + * c_alert_Title -> c + * t_global_color -> t + * chart_color_blue -> chart + */ +function getCategoryFromTokenName(tokenName: string): string { + const firstUnderscore = tokenName.indexOf('_') + if (firstUnderscore === -1) { + return tokenName + } + return tokenName.substring(0, firstUnderscore) +} + +/** + * Loads all tokens from @patternfly/react-tokens + * Returns an array of token objects with { name, value, var } + */ +export function getAllTokens(): Token[] { + if (cachedTokens) { + return cachedTokens + } + + const tokens: Token[] = [] + + for (const [exportName, tokenValue] of Object.entries(allTokens)) { + if (typeof tokenValue === 'object' && tokenValue !== null) { + const token = tokenValue as Token + + if (token.name && token.value && token.var) { + tokens.push({ + name: token.name, + value: token.value, + var: token.var, + }) + } + } + } + + cachedTokens = tokens + return tokens +} + +/** + * Gets a sorted array of unique token categories + */ +export function getTokenCategories(): string[] { + if (cachedCategories) { + return cachedCategories + } + + const tokens = getAllTokens() + const categorySet = new Set() + + for (const token of tokens) { + const category = getCategoryFromTokenName(token.name) + categorySet.add(category) + } + + cachedCategories = Array.from(categorySet).sort() + return cachedCategories +} + +/** + * Gets all tokens organized by category + */ +export function getTokensByCategory(): TokensByCategory { + if (cachedTokensByCategory) { + return cachedTokensByCategory + } + + const tokens = getAllTokens() + const byCategory: TokensByCategory = {} + + for (const token of tokens) { + const category = getCategoryFromTokenName(token.name) + if (!byCategory[category]) { + byCategory[category] = [] + } + byCategory[category].push(token) + } + + cachedTokensByCategory = byCategory + return byCategory +} + +/** + * Gets tokens for a specific category + * Returns undefined if category doesn't exist + */ +export function getTokensForCategory(category: string): Token[] | undefined { + const byCategory = getTokensByCategory() + return byCategory[category] +} + +/** + * Filters tokens by substring match (case-insensitive) + * Matches against the token name field + */ +export function filterTokens(tokens: Token[], filter: string): Token[] { + if (!filter) { + return tokens + } + + const lowerFilter = filter.toLowerCase() + return tokens.filter((token) => + token.name.toLowerCase().includes(lowerFilter), + ) +} + +/** + * Filters tokens by category (case-insensitive) + * Matches against the category name + */ +export function filterTokensByCategory( + byCategory: TokensByCategory, + filter: string, +): TokensByCategory { + if (!filter) { + return byCategory + } + + const filtered: TokensByCategory = {} + for (const [category, tokens] of Object.entries(byCategory)) { + const filteredTokens = filterTokens(tokens, filter) + if (filteredTokens.length > 0) { + filtered[category] = filteredTokens + } + } + + return filtered +} From 136953420af8f6e61fff6ec2d15f9c26796f1e8b Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 20 Jan 2026 09:26:10 -0500 Subject: [PATCH 2/8] Lint errors --- src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts | 2 +- src/utils/tokens.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts b/src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts index 164854b..e7d71ac 100644 --- a/src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts @@ -207,7 +207,7 @@ it('each token has required properties', async () => { expect(response.status).toBe(200) - for (const [category, tokens] of Object.entries(body)) { + for (const [_category, tokens] of Object.entries(body)) { expect(Array.isArray(tokens)).toBe(true) for (const token of tokens as any[]) { expect(token).toHaveProperty('name') diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 9a7a77d..7f46f67 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -41,7 +41,7 @@ export function getAllTokens(): Token[] { const tokens: Token[] = [] - for (const [exportName, tokenValue] of Object.entries(allTokens)) { + for (const [_exportName, tokenValue] of Object.entries(allTokens)) { if (typeof tokenValue === 'object' && tokenValue !== null) { const token = tokenValue as Token From 0989da9a3a0865334fa5edf89912188cc54a1b44 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 20 Jan 2026 09:34:07 -0500 Subject: [PATCH 3/8] Ignore lint error --- src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts | 1 + src/utils/tokens.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts b/src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts index e7d71ac..760de5a 100644 --- a/src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/tokens/all.test.ts @@ -207,6 +207,7 @@ it('each token has required properties', async () => { expect(response.status).toBe(200) + // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const [_category, tokens] of Object.entries(body)) { expect(Array.isArray(tokens)).toBe(true) for (const token of tokens as any[]) { diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 7f46f67..615be95 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -41,6 +41,7 @@ export function getAllTokens(): Token[] { const tokens: Token[] = [] + // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const [_exportName, tokenValue] of Object.entries(allTokens)) { if (typeof tokenValue === 'object' && tokenValue !== null) { const token = tokenValue as Token From dffb76bd7597a59cf985ce7eb7f76b2bcb393634 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Fri, 23 Jan 2026 08:09:09 -0500 Subject: [PATCH 4/8] Token util tests --- src/__tests__/utils/tokens.test.ts | 320 +++++++++++++++++++++++++++++ src/utils/tokens.ts | 54 ++--- 2 files changed, 340 insertions(+), 34 deletions(-) create mode 100644 src/__tests__/utils/tokens.test.ts diff --git a/src/__tests__/utils/tokens.test.ts b/src/__tests__/utils/tokens.test.ts new file mode 100644 index 0000000..baa2280 --- /dev/null +++ b/src/__tests__/utils/tokens.test.ts @@ -0,0 +1,320 @@ +jest.mock('@patternfly/react-tokens', () => ({ + c_alert_BackgroundColor: { + name: '--pf-v6-c-alert--BackgroundColor', + value: '#ffffff', + var: 'var(--pf-v6-c-alert--BackgroundColor)', + }, + c_alert_Color: { + name: '--pf-v6-c-alert--Color', + value: '#000000', + var: 'var(--pf-v6-c-alert--Color)', + }, + c_button_BackgroundColor: { + name: '--pf-v6-c-button--BackgroundColor', + value: '#0066cc', + var: 'var(--pf-v6-c-button--BackgroundColor)', + }, + t_global_color_100: { + name: '--pf-v6-t-global--color--100', + value: '#f0f0f0', + var: 'var(--pf-v6-t-global--color--100)', + }, + t_global_color_200: { + name: '--pf-v6-t-global--color--200', + value: '#e0e0e0', + var: 'var(--pf-v6-t-global--color--200)', + }, + chart_global_Fill: { + name: '--pf-v6-chart-global--Fill', + value: '#06c', + var: 'var(--pf-v6-chart-global--Fill)', + }, + l_grid_gutter: { + name: '--pf-v6-l-grid--gutter', + value: '1rem', + var: 'var(--pf-v6-l-grid--gutter)', + }, + invalidToken: { + name: '--invalid', + }, + default: 'should be ignored', +})) + +import { + getAllTokens, + getTokenCategories, + getTokensByCategory, + getTokensForCategory, + filterTokens, + filterTokensByCategory, +} from '../../utils/tokens' + +describe('getAllTokens', () => { + it('returns all valid tokens', () => { + const tokens = getAllTokens() + + expect(Array.isArray(tokens)).toBe(true) + expect(tokens.length).toBe(7) // 7 valid tokens in mockTokens + }) + + it('each token has required properties', () => { + const tokens = getAllTokens() + + tokens.forEach((token) => { + expect(token).toHaveProperty('name') + expect(token).toHaveProperty('value') + expect(token).toHaveProperty('var') + expect(typeof token.name).toBe('string') + expect(typeof token.value).toBe('string') + expect(typeof token.var).toBe('string') + }) + }) + + it('filters out invalid tokens', () => { + const tokens = getAllTokens() + + // Should not include the invalid token or non-object exports + const tokenNames = tokens.map((t) => t.name) + expect(tokenNames).not.toContain('--invalid') + }) + + it('returns cached tokens on subsequent calls', () => { + const tokens1 = getAllTokens() + const tokens2 = getAllTokens() + + // Should return the same array reference (cached) + expect(tokens1).toBe(tokens2) + }) +}) + +describe('getTokenCategories', () => { + it('returns sorted array of categories', () => { + const categories = getTokenCategories() + + expect(Array.isArray(categories)).toBe(true) + expect(categories).toEqual(['c', 'chart', 'l', 't']) + }) + + it('categories are alphabetically sorted', () => { + const categories = getTokenCategories() + const sorted = [...categories].sort() + + expect(categories).toEqual(sorted) + }) + + it('categories are unique', () => { + const categories = getTokenCategories() + const unique = [...new Set(categories)] + + expect(categories).toEqual(unique) + }) + + it('returns cached categories on subsequent calls', () => { + const categories1 = getTokenCategories() + const categories2 = getTokenCategories() + + expect(categories1).toBe(categories2) + }) +}) + +describe('getTokensByCategory', () => { + it('returns object with category keys', () => { + const byCategory = getTokensByCategory() + + expect(typeof byCategory).toBe('object') + expect(byCategory).toHaveProperty('c') + expect(byCategory).toHaveProperty('t') + expect(byCategory).toHaveProperty('chart') + expect(byCategory).toHaveProperty('l') + }) + + it('groups tokens correctly by category', () => { + const byCategory = getTokensByCategory() + + expect(byCategory.c).toHaveLength(3) // c_alert_BackgroundColor, c_alert_Color, c_button_BackgroundColor + expect(byCategory.t).toHaveLength(2) // t_global_color_100, t_global_color_200 + expect(byCategory.chart).toHaveLength(1) // chart_global_Fill + expect(byCategory.l).toHaveLength(1) // l_grid_gutter + }) + + it('all tokens in a category have correct prefix', () => { + const byCategory = getTokensByCategory() + + Object.entries(byCategory).forEach(([category, tokens]) => { + tokens.forEach((token) => { + const prefix = token.name.split('-')[4] // --pf-v6-{prefix}-... + expect(prefix).toBe(category) + }) + }) + }) + + it('returns cached result on subsequent calls', () => { + const byCategory1 = getTokensByCategory() + const byCategory2 = getTokensByCategory() + + expect(byCategory1).toBe(byCategory2) + }) +}) + +describe('getTokensForCategory', () => { + it('returns tokens for valid category', () => { + const cTokens = getTokensForCategory('c') + + expect(Array.isArray(cTokens)).toBe(true) + expect(cTokens).toHaveLength(3) + }) + + it('returns undefined for invalid category', () => { + const tokens = getTokensForCategory('invalid') + + expect(tokens).toBeUndefined() + }) + + it('returns correct tokens for each category', () => { + const cTokens = getTokensForCategory('c') + const tTokens = getTokensForCategory('t') + const chartTokens = getTokensForCategory('chart') + const lTokens = getTokensForCategory('l') + + expect(cTokens?.length).toBe(3) + expect(tTokens?.length).toBe(2) + expect(chartTokens?.length).toBe(1) + expect(lTokens?.length).toBe(1) + }) +}) + +describe('filterTokens', () => { + const testTokens = [ + { + name: '--pf-v6-c-alert--BackgroundColor', + value: '#fff', + var: 'var(--pf-v6-c-alert--BackgroundColor)', + }, + { + name: '--pf-v6-c-button--Color', + value: '#000', + var: 'var(--pf-v6-c-button--Color)', + }, + { + name: '--pf-v6-c-card--BackgroundColor', + value: '#f5f5f5', + var: 'var(--pf-v6-c-card--BackgroundColor)', + }, + ] + + it('returns all tokens when filter is empty', () => { + const filtered = filterTokens(testTokens, '') + + expect(filtered).toEqual(testTokens) + }) + + it('filters tokens by substring match', () => { + const filtered = filterTokens(testTokens, 'alert') + + expect(filtered).toHaveLength(1) + expect(filtered[0].name).toContain('alert') + }) + + it('filter is case-insensitive', () => { + const filtered = filterTokens(testTokens, 'ALERT') + + expect(filtered).toHaveLength(1) + expect(filtered[0].name).toContain('alert') + }) + + it('returns multiple matches', () => { + const filtered = filterTokens(testTokens, 'BackgroundColor') + + expect(filtered).toHaveLength(2) + filtered.forEach((token) => { + expect(token.name).toContain('BackgroundColor') + }) + }) + + it('returns empty array when no matches', () => { + const filtered = filterTokens(testTokens, 'nonexistent') + + expect(filtered).toHaveLength(0) + }) + + it('handles partial matches', () => { + const filtered = filterTokens(testTokens, 'c-c') + + expect(filtered).toHaveLength(1) + expect(filtered[0].name).toContain('c-card') + }) +}) + +describe('filterTokensByCategory', () => { + const testTokensByCategory = { + c: [ + { + name: '--pf-v6-c-alert--Color', + value: '#000', + var: 'var(--pf-v6-c-alert--Color)', + }, + { + name: '--pf-v6-c-button--Color', + value: '#fff', + var: 'var(--pf-v6-c-button--Color)', + }, + ], + t: [ + { + name: '--pf-v6-t-global--color--100', + value: '#f0f0f0', + var: 'var(--pf-v6-t-global--color--100)', + }, + ], + chart: [ + { + name: '--pf-v6-chart-global--Fill', + value: '#06c', + var: 'var(--pf-v6-chart-global--Fill)', + }, + ], + } + + it('returns all categories when filter is empty', () => { + const filtered = filterTokensByCategory(testTokensByCategory, '') + + expect(filtered).toEqual(testTokensByCategory) + }) + + it('filters across all categories', () => { + const filtered = filterTokensByCategory(testTokensByCategory, 'alert') + + expect(Object.keys(filtered)).toHaveLength(1) + expect(filtered).toHaveProperty('c') + expect(filtered.c).toHaveLength(1) + expect(filtered.c[0].name).toContain('alert') + }) + + it('excludes categories with no matches', () => { + const filtered = filterTokensByCategory(testTokensByCategory, 'button') + + expect(Object.keys(filtered)).toHaveLength(1) + expect(filtered).toHaveProperty('c') + expect(filtered).not.toHaveProperty('t') + expect(filtered).not.toHaveProperty('chart') + }) + + it('returns empty object when no matches', () => { + const filtered = filterTokensByCategory(testTokensByCategory, 'nonexistent') + + expect(Object.keys(filtered)).toHaveLength(0) + }) + + it('filter is case-insensitive', () => { + const filtered = filterTokensByCategory(testTokensByCategory, 'GLOBAL') + + expect(Object.keys(filtered).length).toBeGreaterThan(0) + }) + + it('can match tokens in multiple categories', () => { + const filtered = filterTokensByCategory(testTokensByCategory, 'global') + + expect(filtered).toHaveProperty('t') + expect(filtered).toHaveProperty('chart') + }) +}) diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 615be95..2bb4b15 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -14,26 +14,30 @@ let cachedTokens: Token[] | null = null let cachedCategories: string[] | null = null let cachedTokensByCategory: TokensByCategory | null = null -/** - * Extracts the category from a token name - * Categories are determined by the first prefix before underscore - * Examples: - * c_alert_Title -> c - * t_global_color -> t - * chart_color_blue -> chart - */ function getCategoryFromTokenName(tokenName: string): string { - const firstUnderscore = tokenName.indexOf('_') - if (firstUnderscore === -1) { - return tokenName + // CSS variable format: --pf-v6-{category}-... + // Split by hyphen and get the category part (index 4 after splitting) + const parts = tokenName.split('-') + if (parts.length >= 5) { + // Handle multi-word categories like 'chart' + // Find where the component name starts (after category) + // Categories can be: c, t, l, chart, global, hidden, patternfly + let category = parts[4] + + // Check if this might be a multi-word category + // For patterns like: --pf-v6-chart-global-... + // we want to check if parts[4] + parts[5] forms a known longer category + if (parts.length >= 6 && parts[4] === 'chart' && parts[5] !== '') { + // For chart tokens, the category is just 'chart' + return 'chart' + } + + return category } - return tokenName.substring(0, firstUnderscore) + + return tokenName } -/** - * Loads all tokens from @patternfly/react-tokens - * Returns an array of token objects with { name, value, var } - */ export function getAllTokens(): Token[] { if (cachedTokens) { return cachedTokens @@ -60,9 +64,6 @@ export function getAllTokens(): Token[] { return tokens } -/** - * Gets a sorted array of unique token categories - */ export function getTokenCategories(): string[] { if (cachedCategories) { return cachedCategories @@ -80,9 +81,6 @@ export function getTokenCategories(): string[] { return cachedCategories } -/** - * Gets all tokens organized by category - */ export function getTokensByCategory(): TokensByCategory { if (cachedTokensByCategory) { return cachedTokensByCategory @@ -103,19 +101,11 @@ export function getTokensByCategory(): TokensByCategory { return byCategory } -/** - * Gets tokens for a specific category - * Returns undefined if category doesn't exist - */ export function getTokensForCategory(category: string): Token[] | undefined { const byCategory = getTokensByCategory() return byCategory[category] } -/** - * Filters tokens by substring match (case-insensitive) - * Matches against the token name field - */ export function filterTokens(tokens: Token[], filter: string): Token[] { if (!filter) { return tokens @@ -127,10 +117,6 @@ export function filterTokens(tokens: Token[], filter: string): Token[] { ) } -/** - * Filters tokens by category (case-insensitive) - * Matches against the category name - */ export function filterTokensByCategory( byCategory: TokensByCategory, filter: string, From d04f1d54c2739435307d54ea9a86af75ede3b775 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Fri, 23 Jan 2026 08:16:38 -0500 Subject: [PATCH 5/8] Lint errors --- src/__tests__/utils/tokens.test.ts | 1 + src/utils/tokens.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/__tests__/utils/tokens.test.ts b/src/__tests__/utils/tokens.test.ts index baa2280..8eb3a1c 100644 --- a/src/__tests__/utils/tokens.test.ts +++ b/src/__tests__/utils/tokens.test.ts @@ -1,3 +1,4 @@ +// eslint-disable @typescript-eslint/camelcase jest.mock('@patternfly/react-tokens', () => ({ c_alert_BackgroundColor: { name: '--pf-v6-c-alert--BackgroundColor', diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 2bb4b15..e1cc3e3 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -22,7 +22,7 @@ function getCategoryFromTokenName(tokenName: string): string { // Handle multi-word categories like 'chart' // Find where the component name starts (after category) // Categories can be: c, t, l, chart, global, hidden, patternfly - let category = parts[4] + const category = parts[4] // Check if this might be a multi-word category // For patterns like: --pf-v6-chart-global-... From 0aaa7145e208766b6d96a7a95bcdb46903b7d012 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Fri, 23 Jan 2026 08:24:05 -0500 Subject: [PATCH 6/8] Actual lint errors resolved --- src/__tests__/utils/tokens.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/__tests__/utils/tokens.test.ts b/src/__tests__/utils/tokens.test.ts index 8eb3a1c..b441bfc 100644 --- a/src/__tests__/utils/tokens.test.ts +++ b/src/__tests__/utils/tokens.test.ts @@ -1,35 +1,41 @@ -// eslint-disable @typescript-eslint/camelcase jest.mock('@patternfly/react-tokens', () => ({ + // eslint-disable-next-line camelcase c_alert_BackgroundColor: { name: '--pf-v6-c-alert--BackgroundColor', value: '#ffffff', var: 'var(--pf-v6-c-alert--BackgroundColor)', }, + // eslint-disable-next-line camelcase c_alert_Color: { name: '--pf-v6-c-alert--Color', value: '#000000', var: 'var(--pf-v6-c-alert--Color)', }, + // eslint-disable-next-line camelcase c_button_BackgroundColor: { name: '--pf-v6-c-button--BackgroundColor', value: '#0066cc', var: 'var(--pf-v6-c-button--BackgroundColor)', }, + // eslint-disable-next-line camelcase t_global_color_100: { name: '--pf-v6-t-global--color--100', value: '#f0f0f0', var: 'var(--pf-v6-t-global--color--100)', }, + // eslint-disable-next-line camelcase t_global_color_200: { name: '--pf-v6-t-global--color--200', value: '#e0e0e0', var: 'var(--pf-v6-t-global--color--200)', }, + // eslint-disable-next-line camelcase chart_global_Fill: { name: '--pf-v6-chart-global--Fill', value: '#06c', var: 'var(--pf-v6-chart-global--Fill)', }, + // eslint-disable-next-line camelcase l_grid_gutter: { name: '--pf-v6-l-grid--gutter', value: '1rem', From 45ebd3be4956be859e0c334faeaff8061bed39b3 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Fri, 23 Jan 2026 15:22:07 -0500 Subject: [PATCH 7/8] Updated logic to capture category names --- src/__tests__/utils/tokens.test.ts | 7 ++++--- src/utils/tokens.ts | 31 ++++++++++++------------------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/__tests__/utils/tokens.test.ts b/src/__tests__/utils/tokens.test.ts index b441bfc..352c063 100644 --- a/src/__tests__/utils/tokens.test.ts +++ b/src/__tests__/utils/tokens.test.ts @@ -25,9 +25,9 @@ jest.mock('@patternfly/react-tokens', () => ({ }, // eslint-disable-next-line camelcase t_global_color_200: { - name: '--pf-v6-t-global--color--200', + name: '--pf-t-global--color--200', value: '#e0e0e0', - var: 'var(--pf-v6-t-global--color--200)', + var: 'var(--pf-t-global--color--200)', }, // eslint-disable-next-line camelcase chart_global_Fill: { @@ -149,7 +149,8 @@ describe('getTokensByCategory', () => { Object.entries(byCategory).forEach(([category, tokens]) => { tokens.forEach((token) => { - const prefix = token.name.split('-')[4] // --pf-v6-{prefix}-... + const isVersionedToken = /^--pf-v6/.test(token.name) + const prefix = token.name.split('-')[isVersionedToken ? 4 : 3] // --pf-v6-{prefix}- if versioned or --pf-{prefix}- if unversioned expect(prefix).toBe(category) }) }) diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index e1cc3e3..6e1429e 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -15,27 +15,20 @@ let cachedCategories: string[] | null = null let cachedTokensByCategory: TokensByCategory | null = null function getCategoryFromTokenName(tokenName: string): string { - // CSS variable format: --pf-v6-{category}-... - // Split by hyphen and get the category part (index 4 after splitting) - const parts = tokenName.split('-') - if (parts.length >= 5) { - // Handle multi-word categories like 'chart' - // Find where the component name starts (after category) - // Categories can be: c, t, l, chart, global, hidden, patternfly - const category = parts[4] - - // Check if this might be a multi-word category - // For patterns like: --pf-v6-chart-global-... - // we want to check if parts[4] + parts[5] forms a known longer category - if (parts.length >= 6 && parts[4] === 'chart' && parts[5] !== '') { - // For chart tokens, the category is just 'chart' - return 'chart' - } - - return category + const nameWithoutPfPrefix = tokenName.replace(/^--pf-/, '') + const parts = nameWithoutPfPrefix.split(/-+/) + if (/^v\d+/.test(parts[0])) { + return parts[1] } - return tokenName + return parts[0] + // --pf-[t|vX]- + // add test for nonversion token + // strip out leading --pf- + // check if 1st thing is version, strip out version and grab first part after, v6-chart-global + // if not version, put that thing in category + // CSS variable format: --pf-v6-{category}-... + // Split by hyphen and get the category part (index 4 after splitting) } export function getAllTokens(): Token[] { From 7cd647930be912e7b96cf40ae9c0f4b78786b385 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Fri, 23 Jan 2026 15:25:47 -0500 Subject: [PATCH 8/8] Removed comments --- src/utils/tokens.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 6e1429e..50aeda9 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -22,13 +22,6 @@ function getCategoryFromTokenName(tokenName: string): string { } return parts[0] - // --pf-[t|vX]- - // add test for nonversion token - // strip out leading --pf- - // check if 1st thing is version, strip out version and grab first part after, v6-chart-global - // if not version, put that thing in category - // CSS variable format: --pf-v6-{category}-... - // Split by hyphen and get the category part (index 4 after splitting) } export function getAllTokens(): Token[] {