Skip to content

Commit 5d7d5b3

Browse files
author
matthew.hardern
committed
feat(pie-token-map): CACT-4721 add pie-token-map package
Adds a new tool package providing programmatic lookup of PIE design tokens by name, returning CSS variable references. Supports color, typography, radius, spacing, elevation, gradient, motion, breakpoint, and z-index categories. Includes platform-agnostic color aliases for Android PIE token map parity.
1 parent 715efb5 commit 5d7d5b3

20 files changed

+1800
-1
lines changed

.changeset/token-map-initial.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@justeattakeaway/pie-token-map": minor
3+
---
4+
5+
[Added] - New `pie-token-map` package providing programmatic lookup of PIE design tokens by name, returning CSS variable references (`var(--dt-*)`). Supports color, typography, radius, spacing, elevation, gradient, motion, breakpoint, and z-index token categories.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.turbo
2+
dist
3+
node_modules
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@justeattakeaway/pie-token-map",
3+
"version": "0.1.0",
4+
"description": "Programmatic lookup of PIE design tokens by name, returning CSS variable references.",
5+
"type": "module",
6+
"main": "dist/index.js",
7+
"module": "dist/index.js",
8+
"types": "dist/index.d.ts",
9+
"files": [
10+
"src",
11+
"dist",
12+
"**/*.d.ts"
13+
],
14+
"publishConfig": {
15+
"access": "public"
16+
},
17+
"license": "Apache-2.0",
18+
"sideEffects": false,
19+
"repository": {
20+
"type": "git",
21+
"url": "https://github.com/justeattakeaway/pie.git"
22+
},
23+
"pieMetadata": {
24+
"componentStatus": "alpha"
25+
},
26+
"scripts": {
27+
"generate": "run -T ts-node --esm ./scripts/generate-token-map.ts",
28+
"build": "yarn generate && run -T vite build",
29+
"test": "run -T vitest run",
30+
"lint:scripts": "run -T eslint .",
31+
"lint:scripts:fix": "yarn lint:scripts --fix",
32+
"watch": "run -T vite build --watch"
33+
},
34+
"devDependencies": {
35+
"@justeat/pie-design-tokens": "7.11.1",
36+
"@justeattakeaway/pie-components-config": "*"
37+
}
38+
}
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/**
2+
* Reads @justeat/pie-design-tokens/dist/tokens.json and generates
3+
* typed token maps in src/generated/.
4+
*
5+
* Run: yarn generate
6+
*/
7+
8+
import { createRequire } from 'node:module';
9+
import { writeFileSync, mkdirSync } from 'node:fs';
10+
import { resolve, dirname } from 'node:path';
11+
import { fileURLToPath } from 'node:url';
12+
13+
const require = createRequire(import.meta.url);
14+
15+
const __filename = fileURLToPath(import.meta.url);
16+
const __dirname = dirname(__filename);
17+
18+
const GENERATED_DIR = resolve(__dirname, '../src/generated');
19+
20+
// eslint-disable-next-line @typescript-eslint/no-var-requires
21+
const tokens = require('@justeat/pie-design-tokens/dist/tokens.json');
22+
// eslint-disable-next-line @typescript-eslint/no-var-requires
23+
const tokensPkg = require('@justeat/pie-design-tokens/package.json');
24+
25+
const HEADER = '// Auto-generated by scripts/generate-token-map.ts — do not edit manually.\n';
26+
27+
/**
28+
* Returns true if a property key needs quoting (contains hyphens, starts with a digit, etc.).
29+
*/
30+
function needsQuotes (key: string): boolean {
31+
return !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key);
32+
}
33+
34+
/**
35+
* Format a property key, quoting only when necessary.
36+
*/
37+
function formatKey (key: string): string {
38+
return needsQuotes(key) ? `'${key}'` : key;
39+
}
40+
41+
function writeFile (filePath: string, content: string): void {
42+
writeFileSync(filePath, content, 'utf-8');
43+
// eslint-disable-next-line no-console
44+
console.info(` Generated ${filePath}`);
45+
}
46+
47+
/**
48+
* Generate a simple map where each token name maps to a single CSS var ref.
49+
*/
50+
function generateSimpleMap (
51+
names: string[],
52+
prefix: string,
53+
typeName: string,
54+
mapName: string,
55+
namesArrayName: string,
56+
): string {
57+
const sorted = [...names].sort();
58+
const namesArr = sorted.map((n) => ` '${n}',`).join('\n');
59+
const entries = sorted.map((n) => ` ${formatKey(n)}: 'var(--dt-${prefix}-${n})',`).join('\n');
60+
61+
return `${HEADER}import type { CssVarRef } from '../types.js';
62+
63+
export const ${namesArrayName} = [
64+
${namesArr}
65+
] as const;
66+
67+
export type ${typeName} = typeof ${namesArrayName}[number];
68+
69+
export const ${mapName}: Record<${typeName}, CssVarRef> = {
70+
${entries}
71+
};
72+
`;
73+
}
74+
75+
// ---------------------------------------------------------------------------
76+
// Color map
77+
// ---------------------------------------------------------------------------
78+
79+
/**
80+
* Platform-agnostic aliases for Android parity.
81+
* tokens.json uses `web-container-*` but the server sends `container-*`.
82+
*/
83+
const COLOR_ALIASES: Record<string, string> = {
84+
'container-base': 'web-container-base',
85+
'container-base-dark': 'web-container-base-dark',
86+
'container-neutral': 'web-container-neutral',
87+
'container-prominent': 'web-container-prominent',
88+
};
89+
90+
function generateColorMap (): void {
91+
const colorNames: string[] = Object.keys(tokens.theme.jet.color.alias.default);
92+
93+
// Add platform-agnostic alias names
94+
const aliasNames = Object.keys(COLOR_ALIASES).filter((a) => !colorNames.includes(a));
95+
const allNames = [...colorNames, ...aliasNames];
96+
97+
const sorted = [...allNames].sort();
98+
const namesArr = sorted.map((n) => ` '${n}',`).join('\n');
99+
const entries = sorted.map((n) => {
100+
// Aliases point to the web-prefixed CSS var
101+
const cssName = COLOR_ALIASES[n] || n;
102+
return ` ${formatKey(n)}: 'var(--dt-color-${cssName})',`;
103+
}).join('\n');
104+
105+
const content = `${HEADER}import type { CssVarRef } from '../types.js';
106+
107+
export const colorTokenNames = [
108+
${namesArr}
109+
] as const;
110+
111+
export type ColorTokenName = typeof colorTokenNames[number];
112+
113+
export const colorMap: Record<ColorTokenName, CssVarRef> = {
114+
${entries}
115+
};
116+
`;
117+
writeFile(resolve(GENERATED_DIR, 'color-map.ts'), content);
118+
}
119+
120+
// ---------------------------------------------------------------------------
121+
// Typography map
122+
// ---------------------------------------------------------------------------
123+
function generateTypographyMap (): void {
124+
const fontTokens = tokens.theme.jet.font.alias as Record<string, Record<string, unknown>>;
125+
const names = Object.keys(fontTokens).sort();
126+
127+
const namesArr = names.map((n) => ` '${n}',`).join('\n');
128+
129+
const entries = names.map((name) => {
130+
const token = fontTokens[name];
131+
const isResponsive = 'size--wide' in token;
132+
const hasStyle = 'font-style' in token;
133+
const hasDecoration = 'text-decoration' in token;
134+
const hasParagraph = 'paragraph' in token;
135+
136+
const prefix = `--dt-font-${name}`;
137+
const props: string[] = [
138+
` family: 'var(${prefix}-family)',`,
139+
` weight: 'var(${prefix}-weight)',`,
140+
];
141+
142+
if (isResponsive) {
143+
props.push(` sizeWide: 'var(${prefix}-size--wide)',`);
144+
props.push(` lineHeightWide: 'var(${prefix}-line-height--wide)',`);
145+
props.push(` sizeNarrow: 'var(${prefix}-size--narrow)',`);
146+
props.push(` lineHeightNarrow: 'var(${prefix}-line-height--narrow)',`);
147+
} else {
148+
props.push(` size: 'var(${prefix}-size)',`);
149+
props.push(` lineHeight: 'var(${prefix}-line-height)',`);
150+
}
151+
152+
if (hasStyle) {
153+
props.push(` fontStyle: 'var(${prefix}-font-style)',`);
154+
}
155+
if (hasDecoration) {
156+
props.push(` textDecoration: 'var(${prefix}-text-decoration)',`);
157+
}
158+
if (hasParagraph) {
159+
props.push(` paragraph: 'var(${prefix}-paragraph)',`);
160+
}
161+
162+
return ` ${formatKey(name)}: {\n${props.join('\n')}\n },`;
163+
}).join('\n');
164+
165+
const content = `${HEADER}import type { TypographyTokenValue } from '../types.js';
166+
167+
export const typographyTokenNames = [
168+
${namesArr}
169+
] as const;
170+
171+
export type TypographyTokenName = typeof typographyTokenNames[number];
172+
173+
export const typographyMap: Record<TypographyTokenName, TypographyTokenValue> = {
174+
${entries}
175+
};
176+
`;
177+
178+
writeFile(resolve(GENERATED_DIR, 'typography-map.ts'), content);
179+
}
180+
181+
// ---------------------------------------------------------------------------
182+
// Radius map
183+
// ---------------------------------------------------------------------------
184+
function generateRadiusMap (): void {
185+
const names: string[] = Object.keys(tokens.theme.jet.radius.alias);
186+
const content = generateSimpleMap(names, 'radius', 'RadiusTokenName', 'radiusMap', 'radiusTokenNames');
187+
writeFile(resolve(GENERATED_DIR, 'radius-map.ts'), content);
188+
}
189+
190+
// ---------------------------------------------------------------------------
191+
// Spacing map
192+
// ---------------------------------------------------------------------------
193+
function generateSpacingMap (): void {
194+
const names: string[] = Object.keys(tokens.theme.jet.spacing.alias);
195+
const content = generateSimpleMap(names, 'spacing', 'SpacingTokenName', 'spacingMap', 'spacingTokenNames');
196+
writeFile(resolve(GENERATED_DIR, 'spacing-map.ts'), content);
197+
}
198+
199+
// ---------------------------------------------------------------------------
200+
// Elevation map
201+
// ---------------------------------------------------------------------------
202+
function generateElevationMap (): void {
203+
const names: string[] = Object.keys(tokens.theme.jet.elevation.alias.default);
204+
const content = generateSimpleMap(names, 'elevation', 'ElevationTokenName', 'elevationMap', 'elevationTokenNames');
205+
writeFile(resolve(GENERATED_DIR, 'elevation-map.ts'), content);
206+
}
207+
208+
// ---------------------------------------------------------------------------
209+
// Gradient map
210+
// ---------------------------------------------------------------------------
211+
function generateGradientMap (): void {
212+
const names: string[] = Object.keys(tokens.theme.jet.gradient.alias.default);
213+
const content = generateSimpleMap(names, 'gradient', 'GradientTokenName', 'gradientMap', 'gradientTokenNames');
214+
writeFile(resolve(GENERATED_DIR, 'gradient-map.ts'), content);
215+
}
216+
217+
// ---------------------------------------------------------------------------
218+
// Motion map
219+
// ---------------------------------------------------------------------------
220+
function generateMotionMap (): void {
221+
const names: string[] = Object.keys(tokens.theme.jet.motion.global);
222+
const content = generateSimpleMap(names, 'motion', 'MotionTokenName', 'motionMap', 'motionTokenNames');
223+
writeFile(resolve(GENERATED_DIR, 'motion-map.ts'), content);
224+
}
225+
226+
// ---------------------------------------------------------------------------
227+
// Breakpoint map
228+
// ---------------------------------------------------------------------------
229+
function generateBreakpointMap (): void {
230+
const names: string[] = Object.keys(tokens.theme.jet.breakpoint.alias);
231+
const content = generateSimpleMap(names, 'breakpoint', 'BreakpointTokenName', 'breakpointMap', 'breakpointTokenNames');
232+
writeFile(resolve(GENERATED_DIR, 'breakpoint-map.ts'), content);
233+
}
234+
235+
// ---------------------------------------------------------------------------
236+
// Z-index map (hand-written — values from pie-css/css/input.css, not tokens.json)
237+
// ---------------------------------------------------------------------------
238+
function generateZIndexMap (): void {
239+
const names = [
240+
'base',
241+
'bottom-sheet',
242+
'cookie-banner',
243+
'dropdown',
244+
'fab',
245+
'modal',
246+
'popover',
247+
'side-sheet',
248+
'toast',
249+
'tooltip',
250+
];
251+
const content = generateSimpleMap(names, 'z-index', 'ZIndexTokenName', 'zIndexMap', 'zIndexTokenNames');
252+
writeFile(resolve(GENERATED_DIR, 'z-index-map.ts'), content);
253+
}
254+
255+
// ---------------------------------------------------------------------------
256+
// Version
257+
// ---------------------------------------------------------------------------
258+
function generateVersion (): void {
259+
const content = `${HEADER}export const tokensVersion = '${tokensPkg.version}';\n`;
260+
writeFile(resolve(GENERATED_DIR, 'version.ts'), content);
261+
}
262+
263+
// ---------------------------------------------------------------------------
264+
// Main
265+
// ---------------------------------------------------------------------------
266+
function main (): void {
267+
mkdirSync(GENERATED_DIR, { recursive: true });
268+
269+
// eslint-disable-next-line no-console
270+
console.info('Generating token maps...');
271+
272+
generateColorMap();
273+
generateTypographyMap();
274+
generateRadiusMap();
275+
generateSpacingMap();
276+
generateElevationMap();
277+
generateGradientMap();
278+
generateMotionMap();
279+
generateBreakpointMap();
280+
generateZIndexMap();
281+
generateVersion();
282+
283+
// eslint-disable-next-line no-console
284+
console.info('Done.');
285+
}
286+
287+
main();
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Auto-generated by scripts/generate-token-map.ts — do not edit manually.
2+
import type { CssVarRef } from '../types.js';
3+
4+
export const breakpointTokenNames = [
5+
'lg',
6+
'md',
7+
'sm',
8+
'xl',
9+
'xs',
10+
'xxl',
11+
] as const;
12+
13+
export type BreakpointTokenName = typeof breakpointTokenNames[number];
14+
15+
export const breakpointMap: Record<BreakpointTokenName, CssVarRef> = {
16+
lg: 'var(--dt-breakpoint-lg)',
17+
md: 'var(--dt-breakpoint-md)',
18+
sm: 'var(--dt-breakpoint-sm)',
19+
xl: 'var(--dt-breakpoint-xl)',
20+
xs: 'var(--dt-breakpoint-xs)',
21+
xxl: 'var(--dt-breakpoint-xxl)',
22+
};

0 commit comments

Comments
 (0)