@interactivethings/scripts 0.0.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/README.md +28 -0
- package/mui-tokens-studio.ts +226 -0
- package/package.json +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ixt-scripts
|
|
2
|
+
|
|
3
|
+
Scripts shared across projects
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Edit code
|
|
9
|
+
yarn publish
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
yarn add @interactivethings/scripts
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
In the following the steps, bun is used but you can also use ts-node as
|
|
21
|
+
a Typescript runtime.
|
|
22
|
+
|
|
23
|
+
### Tokens studio JSON to MUI JSON
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Add this command to package.json scripts
|
|
27
|
+
bun ./node_modules/@interactivethings/scripts/mui-tokens-studio.ts src/styles/tokens.studio.json src/styles/tokens.mui.json
|
|
28
|
+
```
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This script transforms tokens exported with tokens-studio in Figma into
|
|
3
|
+
* a format that is easier to work with when using MUI.
|
|
4
|
+
* You can run it via `pnpm run design:tokens`
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
|
|
9
|
+
import { ArgumentParser } from "argparse";
|
|
10
|
+
import { mapValues, pick } from "remeda";
|
|
11
|
+
import { $IntentionalAny } from "src/utils/types";
|
|
12
|
+
|
|
13
|
+
const simplifyValues = (
|
|
14
|
+
x: { value: unknown } | string | null
|
|
15
|
+
): $IntentionalAny => {
|
|
16
|
+
if (typeof x === "string") {
|
|
17
|
+
return x;
|
|
18
|
+
} else if (typeof x === "object" && !!x) {
|
|
19
|
+
if ("value" in x) {
|
|
20
|
+
return x.value;
|
|
21
|
+
} else {
|
|
22
|
+
return mapValues(x, simplifyValues);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const kebabCase = (x: string) => {
|
|
28
|
+
return x
|
|
29
|
+
.split(" ")
|
|
30
|
+
.map((t, i) => {
|
|
31
|
+
if (i === 0) {
|
|
32
|
+
return t.toLowerCase();
|
|
33
|
+
} else {
|
|
34
|
+
return `${t[0].toUpperCase()}${t.substring(1).toLowerCase()}`;
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
.join("");
|
|
38
|
+
};
|
|
39
|
+
const renameColorKeys = (k: string) => {
|
|
40
|
+
return kebabCase(
|
|
41
|
+
k
|
|
42
|
+
.replace(/-[a-zA-Z0-9]+/, "")
|
|
43
|
+
.replace(" - ", " ")
|
|
44
|
+
.replace(",", "")
|
|
45
|
+
.replace(/^\d+\s+/, "")
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const renameColorKeysEntries = (obj: $IntentionalAny) => {
|
|
50
|
+
const visitor = (k: string, v: $IntentionalAny) => {
|
|
51
|
+
if (typeof v === "object") {
|
|
52
|
+
return [renameColorKeys(k), mapEntries(v, visitor)] as [
|
|
53
|
+
string,
|
|
54
|
+
$IntentionalAny
|
|
55
|
+
];
|
|
56
|
+
} else {
|
|
57
|
+
return [renameColorKeys(k), v] as [string, $IntentionalAny];
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return mapEntries(obj, visitor);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const mapEntries = (
|
|
64
|
+
x: $IntentionalAny,
|
|
65
|
+
mapper: (k: string, v: $IntentionalAny) => [string, $IntentionalAny]
|
|
66
|
+
) => {
|
|
67
|
+
return Object.fromEntries(Object.entries(x).map(([k, v]) => mapper(k, v)));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getPalette = (tokensData: $IntentionalAny) => {
|
|
71
|
+
const data = tokensData.global;
|
|
72
|
+
const colorKeys = ["Base", "Functional"];
|
|
73
|
+
|
|
74
|
+
let palette = pick(data, colorKeys);
|
|
75
|
+
palette = mapValues(palette, simplifyValues);
|
|
76
|
+
palette = renameColorKeysEntries(palette);
|
|
77
|
+
palette = {
|
|
78
|
+
...palette.base,
|
|
79
|
+
...pick(palette, ["functional"]),
|
|
80
|
+
};
|
|
81
|
+
return palette;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const getTypography = (tokensData: $IntentionalAny) => {
|
|
85
|
+
const sizes = ["Desktop", "Mobile"];
|
|
86
|
+
const res = {} as Record<string, $IntentionalAny>;
|
|
87
|
+
const index = {} as Record<string, $IntentionalAny>;
|
|
88
|
+
for (const k of [
|
|
89
|
+
"fontFamilies",
|
|
90
|
+
"lineHeights",
|
|
91
|
+
"fontWeights",
|
|
92
|
+
"fontSize",
|
|
93
|
+
"letterSpacing",
|
|
94
|
+
"textDecoration",
|
|
95
|
+
]) {
|
|
96
|
+
for (const [vName, value] of Object.entries(tokensData.global[k])) {
|
|
97
|
+
index[`${k}.${vName}`] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const maybeParseToNumber = (x: string | number) => {
|
|
102
|
+
const parsed = Number(x);
|
|
103
|
+
if (Number.isNaN(x)) {
|
|
104
|
+
return x;
|
|
105
|
+
} else {
|
|
106
|
+
return parsed;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const maybeParseToPx = (x: string | number) => {
|
|
111
|
+
if (typeof x === "number") {
|
|
112
|
+
return `${x}px`;
|
|
113
|
+
} else {
|
|
114
|
+
return x;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const cleanupFns = {
|
|
119
|
+
fontWeight: (x: string) => {
|
|
120
|
+
const lowered = x.toLowerCase();
|
|
121
|
+
if (lowered == "regular") {
|
|
122
|
+
return 400;
|
|
123
|
+
}
|
|
124
|
+
return lowered;
|
|
125
|
+
},
|
|
126
|
+
fontSize: maybeParseToNumber,
|
|
127
|
+
lineHeight: (x: string | number) => maybeParseToPx(maybeParseToNumber(x)),
|
|
128
|
+
paragraphSpacing: maybeParseToNumber,
|
|
129
|
+
letterSpacing: maybeParseToNumber,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const cleanup = (k: string, v: $IntentionalAny) => {
|
|
133
|
+
if (k in cleanupFns) {
|
|
134
|
+
return [k, cleanupFns[k as keyof typeof cleanupFns](v)] as [
|
|
135
|
+
string,
|
|
136
|
+
$IntentionalAny
|
|
137
|
+
];
|
|
138
|
+
} else {
|
|
139
|
+
return [k, v] as [string, $IntentionalAny];
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const resolve = (str: string) => {
|
|
144
|
+
if (str[0] === "{" && str[str.length - 1] === "}") {
|
|
145
|
+
const path = str.substring(1, str.length - 1);
|
|
146
|
+
return index[path]?.value;
|
|
147
|
+
} else {
|
|
148
|
+
return str;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
for (const size of sizes) {
|
|
152
|
+
const typographies = tokensData.global[size];
|
|
153
|
+
for (const [typo, typoDataRaw] of Object.entries(typographies)) {
|
|
154
|
+
const typoData = typoDataRaw as { value: $IntentionalAny };
|
|
155
|
+
const typoKey = kebabCase(typo.toLowerCase());
|
|
156
|
+
res[typoKey] = res[typoKey] || {};
|
|
157
|
+
res[typoKey][size.toLowerCase()] = mapEntries(
|
|
158
|
+
mapValues(typoData.value, resolve),
|
|
159
|
+
cleanup
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return res;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const getShadows = (tokenData: TokenData) => {
|
|
167
|
+
const transformShadow = (shadowData: ShadowValue) => {
|
|
168
|
+
const { color, x, y, blur, spread } = shadowData;
|
|
169
|
+
return `${x}px ${y}px ${blur}px ${spread}px ${color}`;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const shadows = mapEntries(tokenData.global.Elevation, (k, v) => [
|
|
173
|
+
`${Number(k.replace("dp", "").replace("pd", ""))}`,
|
|
174
|
+
Array.isArray(v.value)
|
|
175
|
+
? v.value.map(transformShadow).join(", ")
|
|
176
|
+
: transformShadow(v.value),
|
|
177
|
+
]);
|
|
178
|
+
return shadows;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
type ShadowValue = {
|
|
182
|
+
color: string;
|
|
183
|
+
x: string;
|
|
184
|
+
y: string;
|
|
185
|
+
blur: string;
|
|
186
|
+
spread: string;
|
|
187
|
+
};
|
|
188
|
+
type ShadowRecord = Record<
|
|
189
|
+
string,
|
|
190
|
+
{ value: ShadowValue } | { value: ShadowValue[] }
|
|
191
|
+
>;
|
|
192
|
+
|
|
193
|
+
type TokenData = {
|
|
194
|
+
global: {
|
|
195
|
+
Base: $IntentionalAny;
|
|
196
|
+
Function: $IntentionalAny;
|
|
197
|
+
Elevation: ShadowRecord;
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const transform = (tokenData: TokenData) => {
|
|
202
|
+
const palette = getPalette(tokenData);
|
|
203
|
+
const typography = getTypography(tokenData);
|
|
204
|
+
const shadows = getShadows(tokenData);
|
|
205
|
+
return {
|
|
206
|
+
palette,
|
|
207
|
+
typography,
|
|
208
|
+
shadows,
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const main = () => {
|
|
213
|
+
const parser = new ArgumentParser();
|
|
214
|
+
parser.add_argument("input");
|
|
215
|
+
parser.add_argument("output");
|
|
216
|
+
const args = parser.parse_args();
|
|
217
|
+
const content = JSON.parse(fs.readFileSync(args.input).toString());
|
|
218
|
+
const transformed = transform(content);
|
|
219
|
+
|
|
220
|
+
fs.writeFileSync(
|
|
221
|
+
args.output === "-" ? process.stdout.fd : args.output,
|
|
222
|
+
JSON.stringify(transformed, null, 2)
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
main();
|