@interactivethings/scripts 2.2.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/README.md +62 -0
- package/examples/custom-transform.js +41 -0
- package/examples/ixt.config.example.ts +33 -0
- package/examples/mui-transform.ts +158 -0
- package/examples/tailwind-transform.ts +167 -0
- package/package.json +3 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Tokens Studio Transform Examples
|
|
2
|
+
|
|
3
|
+
This directory contains example transformer handlers that demonstrate how to use the `ixt tokens-studio transform` command with custom transformation logic.
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
When working in this repository during development, the TypeScript path mappings are configured to resolve `@interactivethings/scripts/*` imports to the local source files.
|
|
8
|
+
|
|
9
|
+
To compile and check the examples:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx tsc --noEmit --project tsconfig.examples.json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### MUI Transform Example
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
ixt tokens-studio transform --handler ./examples/mui-transform.ts --input ./tokens --output mui-theme.json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Tailwind Transform Example
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ixt tokens-studio transform --handler ./examples/tailwind-transform.ts --input ./tokens --output tailwind-theme.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Custom Transform Example
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
ixt tokens-studio transform --handler ./examples/custom-transform.js --input ./tokens --output custom-theme.json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Creating Your Own Transformer
|
|
36
|
+
|
|
37
|
+
1. Copy one of the example files as a starting point
|
|
38
|
+
2. Modify the `transform` function to suit your needs
|
|
39
|
+
3. The `transform` function receives: `{ metadata, tokenData }`
|
|
40
|
+
4. Return the transformed data object
|
|
41
|
+
|
|
42
|
+
### Using in Production
|
|
43
|
+
|
|
44
|
+
When using these examples in your own project after installing `@interactivethings/scripts`:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import {
|
|
48
|
+
kebabCase,
|
|
49
|
+
mapEntries /* ... */,
|
|
50
|
+
} from "@interactivethings/scripts/tokens-studio";
|
|
51
|
+
import { $IntentionalAny } from "@interactivethings/scripts/types";
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or using the alias:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import {
|
|
58
|
+
kebabCase,
|
|
59
|
+
mapEntries /* ... */,
|
|
60
|
+
} from "@interactivethings/scripts/tokens-studio";
|
|
61
|
+
import { $IntentionalAny } from "@interactivethings/scripts/types";
|
|
62
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example custom transformer handler
|
|
3
|
+
* This file demonstrates how to create a completely custom transformer
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ixt tokens-studio transform --handler ./examples/custom-transform.js --input ./tokens --output theme.json
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
transform: (input) => {
|
|
11
|
+
const { metadata, tokenData } = input;
|
|
12
|
+
|
|
13
|
+
// Example: Extract only colors and convert to CSS custom properties
|
|
14
|
+
const colors = tokenData.core || {};
|
|
15
|
+
|
|
16
|
+
const cssVariables = {};
|
|
17
|
+
|
|
18
|
+
// Recursively convert tokens to CSS custom property format
|
|
19
|
+
const convertToCssVars = (obj, prefix = "") => {
|
|
20
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
21
|
+
if (value && typeof value === "object" && "value" in value) {
|
|
22
|
+
// This is a token
|
|
23
|
+
cssVariables[`--${prefix}${key}`] = value.value;
|
|
24
|
+
} else if (value && typeof value === "object") {
|
|
25
|
+
// This is a nested object
|
|
26
|
+
convertToCssVars(value, `${prefix}${key}-`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
convertToCssVars(colors, "color-");
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
cssVariables,
|
|
35
|
+
metadata: {
|
|
36
|
+
generatedAt: new Date().toISOString(),
|
|
37
|
+
tokenSetOrder: metadata.tokenSetOrder,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineConfig } from '@interactivethings/scripts';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
figma: {
|
|
5
|
+
// Figma API token (can also be set via FIGMA_TOKEN environment variable)
|
|
6
|
+
token: process.env.FIGMA_TOKEN,
|
|
7
|
+
|
|
8
|
+
// Assets to download from Figma
|
|
9
|
+
assets: [
|
|
10
|
+
{
|
|
11
|
+
name: "icons",
|
|
12
|
+
url: "https://www.figma.com/design/ElWWZIcOGFhiT06rzfIwRO/Design-System?node-id=11861-10071",
|
|
13
|
+
output: "src/assets/icons"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "illustrations",
|
|
17
|
+
url: "https://www.figma.com/design/ElWWZIcOGFhiT06rzfIwRO/Design-System?node-id=12345-67890",
|
|
18
|
+
output: "src/assets/illustrations"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
tokensStudio: {
|
|
24
|
+
// Input directory containing design tokens
|
|
25
|
+
input: "./design-tokens",
|
|
26
|
+
|
|
27
|
+
// Default output file for transformations
|
|
28
|
+
output: "src/theme/tokens.json",
|
|
29
|
+
|
|
30
|
+
// Default transformer handler
|
|
31
|
+
handler: "./transforms/mui-transform.ts"
|
|
32
|
+
}
|
|
33
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MUI transformer for tokens-studio
|
|
3
|
+
* Transforms tokens exported with tokens-studio in Figma into
|
|
4
|
+
* a format that is easier to work with when using MUI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mapValues, pick } from "remeda";
|
|
8
|
+
|
|
9
|
+
import { $IntentionalAny } from "@interactivethings/scripts/types";
|
|
10
|
+
import {
|
|
11
|
+
simplifyValues,
|
|
12
|
+
kebabCase,
|
|
13
|
+
mapEntries,
|
|
14
|
+
maybeParseToNumber,
|
|
15
|
+
maybeParseToPx,
|
|
16
|
+
makeResolver,
|
|
17
|
+
defineTransform,
|
|
18
|
+
type TokenStudioShadowValue,
|
|
19
|
+
mapKeysRecursively,
|
|
20
|
+
} from "@interactivethings/scripts/tokens-studio";
|
|
21
|
+
|
|
22
|
+
// Ideally those types should be imported from your theme's type definitions
|
|
23
|
+
// You should change those lines to match your actual theme structure
|
|
24
|
+
// @example
|
|
25
|
+
// ```
|
|
26
|
+
// import tokensStudioTokens from '../path/to/your/tokens-studio/tokens.json';
|
|
27
|
+
// type MUITokenData = typeof tokensStudioTokens
|
|
28
|
+
// ```
|
|
29
|
+
type MUITokenData = {
|
|
30
|
+
Base: $IntentionalAny;
|
|
31
|
+
Functional: $IntentionalAny;
|
|
32
|
+
Elevation: Record<
|
|
33
|
+
string,
|
|
34
|
+
{ value: TokenStudioShadowValue | TokenStudioShadowValue[] }
|
|
35
|
+
>;
|
|
36
|
+
Desktop: $IntentionalAny;
|
|
37
|
+
Mobile: $IntentionalAny;
|
|
38
|
+
fontFamilies: $IntentionalAny;
|
|
39
|
+
lineHeights: $IntentionalAny;
|
|
40
|
+
fontWeights: $IntentionalAny;
|
|
41
|
+
fontSize: $IntentionalAny;
|
|
42
|
+
letterSpacing: $IntentionalAny;
|
|
43
|
+
textDecoration: $IntentionalAny;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const renameColorKeys = (k: string) => {
|
|
47
|
+
return kebabCase(
|
|
48
|
+
k
|
|
49
|
+
.replace(/-[a-zA-Z0-9]+/, "")
|
|
50
|
+
.replace(" - ", " ")
|
|
51
|
+
.replace(",", "")
|
|
52
|
+
.replace(/^\d+\s+/, "")
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const getPalette = (tokensData: MUITokenData) => {
|
|
57
|
+
const data = tokensData;
|
|
58
|
+
const colorKeys = ["Base", "Functional"] as const;
|
|
59
|
+
|
|
60
|
+
let palette = pick(data, colorKeys);
|
|
61
|
+
type Palette = typeof palette;
|
|
62
|
+
palette = mapValues(palette, simplifyValues);
|
|
63
|
+
const tpalette = mapKeysRecursively(palette, renameColorKeys) as unknown as {
|
|
64
|
+
base: Palette["Base"];
|
|
65
|
+
functional: Palette["Functional"];
|
|
66
|
+
};
|
|
67
|
+
palette = {
|
|
68
|
+
...tpalette.base,
|
|
69
|
+
...pick(tpalette, ["functional"]),
|
|
70
|
+
};
|
|
71
|
+
return palette;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getTypography = (tokensData: MUITokenData) => {
|
|
75
|
+
const sizes = ["Desktop", "Mobile"] as const;
|
|
76
|
+
const res = {} as Record<string, $IntentionalAny>;
|
|
77
|
+
const resolve = makeResolver(tokensData, [
|
|
78
|
+
"fontFamilies",
|
|
79
|
+
"lineHeights",
|
|
80
|
+
"fontWeights",
|
|
81
|
+
"fontSize",
|
|
82
|
+
"letterSpacing",
|
|
83
|
+
"textDecoration",
|
|
84
|
+
] as const);
|
|
85
|
+
|
|
86
|
+
const cleanupTypographyFns = {
|
|
87
|
+
fontWeight: (x: string) => {
|
|
88
|
+
const lowered = x.toLowerCase();
|
|
89
|
+
if (lowered == "regular") {
|
|
90
|
+
return 400;
|
|
91
|
+
}
|
|
92
|
+
return lowered;
|
|
93
|
+
},
|
|
94
|
+
fontSize: maybeParseToNumber,
|
|
95
|
+
lineHeight: (x: string | number) => maybeParseToPx(maybeParseToNumber(x)),
|
|
96
|
+
paragraphSpacing: maybeParseToNumber,
|
|
97
|
+
letterSpacing: maybeParseToNumber,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const cleanupTypographyValue = (k: string, v: $IntentionalAny) => {
|
|
101
|
+
if (k in cleanupTypographyFns) {
|
|
102
|
+
return [
|
|
103
|
+
k,
|
|
104
|
+
cleanupTypographyFns[k as keyof typeof cleanupTypographyFns](v),
|
|
105
|
+
] as [string, $IntentionalAny];
|
|
106
|
+
} else {
|
|
107
|
+
return [k, v] as [string, $IntentionalAny];
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
for (const size of sizes) {
|
|
112
|
+
const typographies = tokensData[size];
|
|
113
|
+
for (const [typo, typoDataRaw] of Object.entries(typographies)) {
|
|
114
|
+
const typoData = typoDataRaw as { value: $IntentionalAny };
|
|
115
|
+
const typoKey = kebabCase(typo.toLowerCase());
|
|
116
|
+
res[typoKey] = res[typoKey] || {};
|
|
117
|
+
res[typoKey][size.toLowerCase()] = mapEntries(
|
|
118
|
+
mapValues(typoData.value, resolve),
|
|
119
|
+
cleanupTypographyValue
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return res;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const getShadows = (tokenData: MUITokenData) => {
|
|
127
|
+
const transformShadow = (shadowData: TokenStudioShadowValue) => {
|
|
128
|
+
const { color, x, y, blur, spread } = shadowData;
|
|
129
|
+
return `${x}px ${y}px ${blur}px ${spread}px ${color}`;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const shadows = mapEntries(tokenData.Elevation, (k, v) => {
|
|
133
|
+
const shadowEntry = v as
|
|
134
|
+
| { value: TokenStudioShadowValue }
|
|
135
|
+
| { value: TokenStudioShadowValue[] };
|
|
136
|
+
return [
|
|
137
|
+
`${Number((k as string).replace("dp", "").replace("pd", ""))}`,
|
|
138
|
+
Array.isArray(shadowEntry.value)
|
|
139
|
+
? (shadowEntry.value as TokenStudioShadowValue[])
|
|
140
|
+
.map(transformShadow)
|
|
141
|
+
.join(", ")
|
|
142
|
+
: transformShadow(shadowEntry.value as TokenStudioShadowValue),
|
|
143
|
+
];
|
|
144
|
+
});
|
|
145
|
+
return shadows;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const transform = defineTransform<MUITokenData>((input) => {
|
|
149
|
+
const palette = getPalette(input.tokenData);
|
|
150
|
+
const typography = getTypography(input.tokenData);
|
|
151
|
+
const shadows = getShadows(input.tokenData);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
palette,
|
|
155
|
+
typography,
|
|
156
|
+
shadows,
|
|
157
|
+
};
|
|
158
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind transformer for tokens-studio
|
|
3
|
+
* Transforms tokens exported with tokens-studio in Figma into
|
|
4
|
+
* a format that is compatible with Tailwind CSS.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mapValues, pick } from "remeda";
|
|
8
|
+
import {
|
|
9
|
+
resolveReferences,
|
|
10
|
+
mapEntries,
|
|
11
|
+
flattenTokens,
|
|
12
|
+
defineTransform,
|
|
13
|
+
} from "@interactivethings/scripts/tokens-studio";
|
|
14
|
+
import { $IntentionalAny } from "../dist/types";
|
|
15
|
+
|
|
16
|
+
// Ideally those types should be imported from your theme's type definitions
|
|
17
|
+
// You should change those lines to match your actual theme structure
|
|
18
|
+
// @example
|
|
19
|
+
// ```
|
|
20
|
+
// import coreTokens from '../path/to/your/core/tokens.json';
|
|
21
|
+
// import functionalTokens from '../path/to/your/functional/tokens.json';
|
|
22
|
+
// type TailwindTokenData = {
|
|
23
|
+
// core: typeof coreTokens;
|
|
24
|
+
// functional: typeof functionalTokens;
|
|
25
|
+
// };
|
|
26
|
+
// ```
|
|
27
|
+
type TailwindTokenData = {
|
|
28
|
+
core: $IntentionalAny;
|
|
29
|
+
functional: $IntentionalAny;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const transform = defineTransform<TailwindTokenData>((input) => {
|
|
33
|
+
const mergedTokens = input.tokenData;
|
|
34
|
+
|
|
35
|
+
const valueMap = new Map<string, string>();
|
|
36
|
+
resolveReferences(mergedTokens, valueMap);
|
|
37
|
+
|
|
38
|
+
const theme = {
|
|
39
|
+
colors: {
|
|
40
|
+
...pick(mergedTokens.core, [
|
|
41
|
+
"amber",
|
|
42
|
+
"blue",
|
|
43
|
+
"brand",
|
|
44
|
+
"cyan",
|
|
45
|
+
"emerald",
|
|
46
|
+
"fuchsia",
|
|
47
|
+
"green",
|
|
48
|
+
"indigo",
|
|
49
|
+
"light-blue",
|
|
50
|
+
"lime",
|
|
51
|
+
"midnight",
|
|
52
|
+
"monochrome",
|
|
53
|
+
"orange",
|
|
54
|
+
"pink",
|
|
55
|
+
"purple",
|
|
56
|
+
"red",
|
|
57
|
+
"rose",
|
|
58
|
+
"teal",
|
|
59
|
+
"violet",
|
|
60
|
+
"yellow",
|
|
61
|
+
]),
|
|
62
|
+
...pick(mergedTokens.functional, [
|
|
63
|
+
"primary",
|
|
64
|
+
"secondary",
|
|
65
|
+
"tertiary",
|
|
66
|
+
"error",
|
|
67
|
+
"success",
|
|
68
|
+
"warning",
|
|
69
|
+
"link",
|
|
70
|
+
"surface",
|
|
71
|
+
"on-surface",
|
|
72
|
+
"surface-inverted",
|
|
73
|
+
"on-surface-inverted",
|
|
74
|
+
"border",
|
|
75
|
+
"input",
|
|
76
|
+
"accent",
|
|
77
|
+
"qualitative",
|
|
78
|
+
"pathways",
|
|
79
|
+
]),
|
|
80
|
+
},
|
|
81
|
+
borderWidth: {
|
|
82
|
+
...mergedTokens.functional["border-width"],
|
|
83
|
+
},
|
|
84
|
+
borderRadius: {
|
|
85
|
+
...mergedTokens.functional["border-radius"],
|
|
86
|
+
},
|
|
87
|
+
fontSize: mergedTokens.core["font-size"],
|
|
88
|
+
fontWeight: mergedTokens.core["font-weight"],
|
|
89
|
+
lineHeight: mergedTokens.core["line-height"],
|
|
90
|
+
spacing: mergedTokens.functional.spacing,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const transformKey = (key: string, depth: number) => {
|
|
94
|
+
if (key.startsWith("on-")) {
|
|
95
|
+
if (depth === 1) {
|
|
96
|
+
return key.replace("on-", "foreground-");
|
|
97
|
+
} else {
|
|
98
|
+
return "foreground";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (key === "default") {
|
|
102
|
+
return "DEFAULT";
|
|
103
|
+
}
|
|
104
|
+
return key;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const flattenedTheme = mapValues(theme, (x) =>
|
|
108
|
+
flattenTokens(x, 0, transformKey)
|
|
109
|
+
) as {
|
|
110
|
+
colors: Record<string, string>;
|
|
111
|
+
borderWidth: Record<string, string>;
|
|
112
|
+
borderRadius: Record<string, string>;
|
|
113
|
+
fontSize: Record<
|
|
114
|
+
"mobile-large" | "mobile-xxxlarge",
|
|
115
|
+
Record<string, number>
|
|
116
|
+
>;
|
|
117
|
+
fontWeight: Record<string, string>;
|
|
118
|
+
lineHeight: Record<
|
|
119
|
+
"mobile-large" | "mobile-xxxlarge",
|
|
120
|
+
Record<string, number>
|
|
121
|
+
>;
|
|
122
|
+
spacing: Record<string, string>;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const lineHeights = flattenedTheme.lineHeight;
|
|
126
|
+
const fontWeights = flattenedTheme.fontWeight;
|
|
127
|
+
|
|
128
|
+
const finalTheme = {
|
|
129
|
+
...flattenedTheme,
|
|
130
|
+
borderWidth: mapValues(flattenedTheme.borderWidth, (x) => `${x}px`),
|
|
131
|
+
borderRadius: mapValues(flattenedTheme.borderRadius, (x) => `${x}px`),
|
|
132
|
+
fontSize: mapEntries(
|
|
133
|
+
flattenedTheme.fontSize["mobile-large"],
|
|
134
|
+
(k: string, value) => {
|
|
135
|
+
return [
|
|
136
|
+
k.replace(",", ""),
|
|
137
|
+
[
|
|
138
|
+
`${value}px`,
|
|
139
|
+
{
|
|
140
|
+
lineHeight: lineHeights["mobile-large"][k]
|
|
141
|
+
? `${lineHeights["mobile-large"][k]}px`
|
|
142
|
+
: undefined,
|
|
143
|
+
fontWeight: fontWeights[k],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
),
|
|
149
|
+
fontWeight: flattenedTheme.fontWeight,
|
|
150
|
+
lineHeight: mapEntries(
|
|
151
|
+
flattenedTheme.lineHeight["mobile-large"],
|
|
152
|
+
(k: string, value) => {
|
|
153
|
+
return [k.replace(",", ""), `${value}px`];
|
|
154
|
+
}
|
|
155
|
+
),
|
|
156
|
+
backgroundImage: {
|
|
157
|
+
"gradient-2":
|
|
158
|
+
"var(--Gradient-2, linear-gradient(135deg, var(--core-brand-electricblue500, #48A1C7) 20.83%, var(--core-brand-nocturne700, #336476) 87.96%))",
|
|
159
|
+
},
|
|
160
|
+
spacing: mapEntries(flattenedTheme.spacing, (k, v) => [
|
|
161
|
+
k.replace("spacing-", ""),
|
|
162
|
+
`${v}px`,
|
|
163
|
+
]),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return finalTheme;
|
|
167
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interactivethings/scripts",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.js",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
|
38
38
|
"README.md",
|
|
39
|
-
"codemods"
|
|
39
|
+
"codemods",
|
|
40
|
+
"examples"
|
|
40
41
|
],
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@next/env": "^15.5.4",
|