@penpair/marwes-ui 0.2.0 → 0.3.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/dist/build-kit.js +24 -6
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +500 -94
- package/dist/components.js +375 -74
- package/dist/config.js +53 -16
- package/dist/index.js +21 -3
- package/dist/marwes.js +45 -12
- package/dist/mode-trial.js +325 -75
- package/dist/ownership.js +162 -37
- package/dist/penpot.js +447 -114
- package/dist/reconciliation.js +112 -37
- package/dist/resolved-values.d.ts +3 -8
- package/dist/resolved-values.js +99 -103
- package/dist/snapshot-file.js +255 -50
- package/dist/theme-input.js +12 -4
- package/dist/theme-lint.js +113 -27
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +207 -52
- package/package.json +2 -2
package/dist/tokens.js
CHANGED
|
@@ -6,21 +6,24 @@ export const themeGroup = "Mode";
|
|
|
6
6
|
const hex = (value) => Math.round(value).toString(16).padStart(2, "0").toUpperCase();
|
|
7
7
|
/** Penpot colour tokens are hex; Marwes also uses rgba() and the word transparent. */
|
|
8
8
|
export function toHexColor(value) {
|
|
9
|
-
if (value === "transparent")
|
|
9
|
+
if (value === "transparent") {
|
|
10
10
|
return "#00000000";
|
|
11
|
+
}
|
|
11
12
|
const rgba = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/.exec(value);
|
|
12
13
|
if (rgba) {
|
|
13
14
|
const alpha = rgba[4] === undefined ? 1 : Number(rgba[4]);
|
|
14
15
|
return `#${hex(Number(rgba[1]))}${hex(Number(rgba[2]))}${hex(Number(rgba[3]))}${alpha < 1 ? hex(alpha * 255) : ""}`;
|
|
15
16
|
}
|
|
16
|
-
if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(value))
|
|
17
|
+
if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(value)) {
|
|
17
18
|
return value.toUpperCase();
|
|
19
|
+
}
|
|
18
20
|
throw new Error(`Unsupported colour value: ${value}`);
|
|
19
21
|
}
|
|
20
22
|
const px = (value) => {
|
|
21
23
|
const match = /^(-?[\d.]+)px$/.exec(value);
|
|
22
|
-
if (!match)
|
|
24
|
+
if (!match) {
|
|
23
25
|
throw new Error(`Expected a px value, got ${value}`);
|
|
26
|
+
}
|
|
24
27
|
return String(Number(match[1]));
|
|
25
28
|
};
|
|
26
29
|
const firstFamily = (stack) => stack
|
|
@@ -38,25 +41,36 @@ function typographyTokens(vars, fontFamily, describe) {
|
|
|
38
41
|
const variants = new Map();
|
|
39
42
|
for (const [variable, value] of Object.entries(vars)) {
|
|
40
43
|
const match = /^--mw-typography-(.+)-(font-size|line-height|font-weight|letter-spacing|transform)$/.exec(variable);
|
|
41
|
-
if (!match)
|
|
44
|
+
if (!match) {
|
|
42
45
|
continue;
|
|
43
|
-
|
|
46
|
+
}
|
|
47
|
+
const entry = variants.get(match[1]) ?? {
|
|
48
|
+
fontFamilies: fontFamily,
|
|
49
|
+
};
|
|
44
50
|
const key = typographyKeys[match[2]];
|
|
45
51
|
if (match[2] === "transform") {
|
|
46
|
-
if (value !== "none")
|
|
52
|
+
if (value !== "none") {
|
|
47
53
|
entry[key] = value;
|
|
54
|
+
}
|
|
48
55
|
}
|
|
49
|
-
else
|
|
56
|
+
else {
|
|
50
57
|
entry[key] = match[2] === "line-height" || match[2] === "font-weight" ? value : px(value);
|
|
58
|
+
}
|
|
51
59
|
variants.set(match[1], entry);
|
|
52
60
|
}
|
|
53
|
-
return [
|
|
61
|
+
return [
|
|
62
|
+
...variants.entries(),
|
|
63
|
+
]
|
|
54
64
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
55
65
|
.map(([variant, value]) => ({
|
|
56
66
|
name: `mw-typography-${variant}`,
|
|
57
67
|
type: "typography",
|
|
58
68
|
// The paragraph variants in Marwes define only size and line height; weight and spacing fall back to the normal values, as a browser does.
|
|
59
|
-
value: {
|
|
69
|
+
value: {
|
|
70
|
+
fontWeight: "400",
|
|
71
|
+
letterSpacing: "0",
|
|
72
|
+
...value,
|
|
73
|
+
},
|
|
60
74
|
description: describe(`--mw-typography-${variant}-*`),
|
|
61
75
|
}));
|
|
62
76
|
}
|
|
@@ -65,34 +79,77 @@ const byName = (left, right) => left.name.localeCompare(right.name);
|
|
|
65
79
|
export function tokensFromVars(vars, describe) {
|
|
66
80
|
const colors = Object.entries(vars)
|
|
67
81
|
.filter(([variable]) => variable.startsWith("--mw-color-"))
|
|
68
|
-
.map(([variable, value]) => ({
|
|
82
|
+
.map(([variable, value]) => ({
|
|
83
|
+
name: variable.slice(2),
|
|
84
|
+
type: "color",
|
|
85
|
+
value: toHexColor(value),
|
|
86
|
+
description: describe(variable),
|
|
87
|
+
}))
|
|
69
88
|
.sort(byName);
|
|
70
89
|
const density = Object.entries(vars)
|
|
71
90
|
.filter(([variable, value]) => variable.startsWith("--mw-density-") && value.endsWith("px"))
|
|
72
|
-
.map(([variable, value]) => ({
|
|
91
|
+
.map(([variable, value]) => ({
|
|
92
|
+
name: variable.slice(2),
|
|
93
|
+
type: "sizing",
|
|
94
|
+
value: px(value),
|
|
95
|
+
description: describe(variable),
|
|
96
|
+
}))
|
|
73
97
|
.sort(byName);
|
|
74
|
-
const radii = [
|
|
98
|
+
const radii = [
|
|
99
|
+
"--mw-ui-radius",
|
|
100
|
+
"--mw-ui-radius-button",
|
|
101
|
+
]
|
|
75
102
|
.filter((variable) => vars[variable] !== undefined)
|
|
76
|
-
.map((variable) => ({
|
|
77
|
-
|
|
103
|
+
.map((variable) => ({
|
|
104
|
+
name: variable.slice(2),
|
|
105
|
+
type: "borderRadius",
|
|
106
|
+
value: px(vars[variable]),
|
|
107
|
+
description: describe(variable),
|
|
108
|
+
}));
|
|
109
|
+
const fonts = [
|
|
110
|
+
"--mw-font-primary",
|
|
111
|
+
"--mw-font-secondary",
|
|
112
|
+
"--mw-font-mono",
|
|
113
|
+
]
|
|
78
114
|
.filter((variable) => vars[variable] !== undefined)
|
|
79
|
-
.map((variable) => ({
|
|
115
|
+
.map((variable) => ({
|
|
116
|
+
name: variable.slice(2),
|
|
117
|
+
type: "fontFamilies",
|
|
118
|
+
value: firstFamily(vars[variable]),
|
|
119
|
+
description: describe(variable),
|
|
120
|
+
}));
|
|
80
121
|
const primary = vars["--mw-font-primary"];
|
|
81
122
|
const typography = primary === undefined ? [] : typographyTokens(vars, firstFamily(primary), describe);
|
|
82
|
-
return [
|
|
123
|
+
return [
|
|
124
|
+
...colors,
|
|
125
|
+
...density,
|
|
126
|
+
...radii,
|
|
127
|
+
...fonts,
|
|
128
|
+
...typography,
|
|
129
|
+
];
|
|
83
130
|
}
|
|
84
131
|
/** The tokens of `of` that `base` does not already provide with the same value: what is left to say on top of it. */
|
|
85
132
|
export function tokensAbove(base, of) {
|
|
86
|
-
const known = new Map(base.map((token) => [
|
|
133
|
+
const known = new Map(base.map((token) => [
|
|
134
|
+
token.name,
|
|
135
|
+
canonical(token.value),
|
|
136
|
+
]));
|
|
87
137
|
return of.filter((token) => known.get(token.name) !== canonical(token.value));
|
|
88
138
|
}
|
|
89
139
|
/** JSON with sorted keys, so the same data always gives the same text. */
|
|
90
140
|
export function canonical(value) {
|
|
91
141
|
const sort = (input) => {
|
|
92
|
-
if (Array.isArray(input))
|
|
142
|
+
if (Array.isArray(input)) {
|
|
93
143
|
return input.map(sort);
|
|
94
|
-
|
|
95
|
-
|
|
144
|
+
}
|
|
145
|
+
if (typeof input === "object" && input !== null) {
|
|
146
|
+
return Object.fromEntries(Object.entries(input)
|
|
147
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
148
|
+
.map(([k, v]) => [
|
|
149
|
+
k,
|
|
150
|
+
sort(v),
|
|
151
|
+
]));
|
|
152
|
+
}
|
|
96
153
|
return input;
|
|
97
154
|
};
|
|
98
155
|
return JSON.stringify(sort(value));
|
|
@@ -100,23 +157,34 @@ export function canonical(value) {
|
|
|
100
157
|
const typographyVariable = /^--mw-typography-(.+)-(font-size|line-height|font-weight|letter-spacing|transform)$/;
|
|
101
158
|
/** Whether a Marwes variable becomes a token (see tokensFromVars). Everything else is reported, never silently dropped. */
|
|
102
159
|
export function isMapped(variable, value) {
|
|
103
|
-
if (variable.startsWith("--mw-color-"))
|
|
160
|
+
if (variable.startsWith("--mw-color-")) {
|
|
104
161
|
return true;
|
|
105
|
-
|
|
162
|
+
}
|
|
163
|
+
if (variable.startsWith("--mw-density-")) {
|
|
106
164
|
return value.endsWith("px");
|
|
107
|
-
|
|
165
|
+
}
|
|
166
|
+
if (variable === "--mw-ui-radius" || variable === "--mw-ui-radius-button") {
|
|
108
167
|
return true;
|
|
109
|
-
|
|
168
|
+
}
|
|
169
|
+
if (variable === "--mw-font-primary" ||
|
|
170
|
+
variable === "--mw-font-secondary" ||
|
|
171
|
+
variable === "--mw-font-mono") {
|
|
110
172
|
return true;
|
|
173
|
+
}
|
|
111
174
|
return typographyVariable.test(variable);
|
|
112
175
|
}
|
|
113
176
|
export function unmappedVariables(...themes) {
|
|
114
177
|
const found = new Set();
|
|
115
|
-
for (const vars of themes)
|
|
116
|
-
for (const [variable, value] of Object.entries(vars))
|
|
117
|
-
if (!isMapped(variable, value))
|
|
178
|
+
for (const vars of themes) {
|
|
179
|
+
for (const [variable, value] of Object.entries(vars)) {
|
|
180
|
+
if (!isMapped(variable, value)) {
|
|
118
181
|
found.add(variable);
|
|
119
|
-
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return [
|
|
186
|
+
...found,
|
|
187
|
+
].sort();
|
|
120
188
|
}
|
|
121
189
|
const hashOf = (parts) => createHash("sha256").update(canonical(parts)).digest("hex");
|
|
122
190
|
/**
|
|
@@ -126,7 +194,9 @@ const hashOf = (parts) => createHash("sha256").update(canonical(parts)).digest("
|
|
|
126
194
|
*/
|
|
127
195
|
export function buildSnapshot(input) {
|
|
128
196
|
const { core, version, name } = input;
|
|
129
|
-
const mode = (which, theme) => core.themeToCSSVars(core.resolveThemeInput(theme ?? {
|
|
197
|
+
const mode = (which, theme) => core.themeToCSSVars(core.resolveThemeInput(theme ?? {
|
|
198
|
+
mode: which,
|
|
199
|
+
}));
|
|
130
200
|
const marwes = (variable) => `${variable} (Marwes ${version})`;
|
|
131
201
|
const marwesDark = (variable) => `${variable} in dark mode (Marwes ${version})`;
|
|
132
202
|
const overlay = (which) => (variable) => `${variable}, ${name} ${which} theme (Marwes ${version})`;
|
|
@@ -137,71 +207,156 @@ export function buildSnapshot(input) {
|
|
|
137
207
|
const spacing = Object.values(core.mwThemeVars.spacing)
|
|
138
208
|
.map((reference) => /var\((--mw-spacing-sp-(\d+))\)/.exec(reference))
|
|
139
209
|
.filter((match) => match !== null)
|
|
140
|
-
.map((match) => ({
|
|
210
|
+
.map((match) => ({
|
|
211
|
+
name: match[1].slice(2),
|
|
212
|
+
type: "spacing",
|
|
213
|
+
value: match[2],
|
|
214
|
+
description: marwes(match[1]),
|
|
215
|
+
}))
|
|
141
216
|
.sort((left, right) => Number(left.value) - Number(right.value));
|
|
142
217
|
const unmapped = unmappedVariables(lightDefaults, darkDefaults);
|
|
143
218
|
const sets = [
|
|
144
|
-
{
|
|
145
|
-
|
|
219
|
+
{
|
|
220
|
+
name: coreSetName,
|
|
221
|
+
tokens: [
|
|
222
|
+
...lightBase,
|
|
223
|
+
...spacing,
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: darkSetName,
|
|
228
|
+
tokens: tokensAbove(lightBase, darkBase),
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
const lightSets = [
|
|
232
|
+
coreSetName,
|
|
233
|
+
];
|
|
234
|
+
const darkSets = [
|
|
235
|
+
coreSetName,
|
|
236
|
+
darkSetName,
|
|
146
237
|
];
|
|
147
|
-
const lightSets = [coreSetName];
|
|
148
|
-
const darkSets = [coreSetName, darkSetName];
|
|
149
238
|
if (input.project?.light) {
|
|
150
239
|
const tokens = tokensAbove(lightBase, tokensFromVars(mode("light", input.project.light), overlay("light")));
|
|
151
240
|
if (tokens.length > 0) {
|
|
152
|
-
sets.push({
|
|
241
|
+
sets.push({
|
|
242
|
+
name: `${name} · Light`,
|
|
243
|
+
tokens,
|
|
244
|
+
});
|
|
153
245
|
lightSets.push(`${name} · Light`);
|
|
154
246
|
}
|
|
155
247
|
}
|
|
156
248
|
if (input.project?.dark) {
|
|
157
249
|
const tokens = tokensAbove(darkBase, tokensFromVars(mode("dark", input.project.dark), overlay("dark")));
|
|
158
250
|
if (tokens.length > 0) {
|
|
159
|
-
sets.push({
|
|
251
|
+
sets.push({
|
|
252
|
+
name: `${name} · Dark`,
|
|
253
|
+
tokens,
|
|
254
|
+
});
|
|
160
255
|
darkSets.push(`${name} · Dark`);
|
|
161
256
|
}
|
|
162
257
|
}
|
|
163
|
-
const ownership = Object.fromEntries(sets.map(set => [
|
|
164
|
-
|
|
258
|
+
const ownership = Object.fromEntries(sets.map((set) => [
|
|
259
|
+
set.name,
|
|
260
|
+
"adapter",
|
|
261
|
+
]));
|
|
262
|
+
const seenSets = new Set(sets.map((set) => set.name));
|
|
165
263
|
for (const extra of input.extras ?? []) {
|
|
166
|
-
if (seenSets.has(extra.name))
|
|
264
|
+
if (seenSets.has(extra.name)) {
|
|
167
265
|
throw new Error("Project extra collides with generated set: " + extra.name);
|
|
168
|
-
|
|
266
|
+
}
|
|
267
|
+
if (typeof extra.name !== "string" ||
|
|
268
|
+
!extra.name.trim() ||
|
|
269
|
+
extra.name.length > 200 ||
|
|
270
|
+
!Array.isArray(extra.tokens)) {
|
|
169
271
|
throw new Error("Invalid project extra set");
|
|
272
|
+
}
|
|
170
273
|
const tokenNames = new Set();
|
|
171
274
|
for (const token of extra.tokens) {
|
|
172
275
|
validateEditableToken(token);
|
|
173
|
-
if (tokenNames.has(token.name))
|
|
276
|
+
if (tokenNames.has(token.name)) {
|
|
174
277
|
throw new Error("Duplicate project extra token: " + token.name);
|
|
278
|
+
}
|
|
175
279
|
tokenNames.add(token.name);
|
|
176
280
|
}
|
|
177
281
|
seenSets.add(extra.name);
|
|
178
|
-
sets.push({
|
|
179
|
-
|
|
282
|
+
sets.push({
|
|
283
|
+
name: extra.name,
|
|
284
|
+
tokens: extra.tokens
|
|
285
|
+
.map((token) => validateEditableToken(token))
|
|
286
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
287
|
+
});
|
|
288
|
+
Object.defineProperty(ownership, extra.name, {
|
|
289
|
+
value: "project",
|
|
290
|
+
enumerable: true,
|
|
291
|
+
configurable: true,
|
|
292
|
+
writable: true,
|
|
293
|
+
});
|
|
180
294
|
lightSets.push(extra.name);
|
|
181
295
|
darkSets.push(extra.name);
|
|
182
296
|
}
|
|
183
297
|
const themes = [
|
|
184
|
-
{
|
|
185
|
-
|
|
298
|
+
{
|
|
299
|
+
group: themeGroup,
|
|
300
|
+
name: "Light",
|
|
301
|
+
sets: lightSets,
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
group: themeGroup,
|
|
305
|
+
name: "Dark",
|
|
306
|
+
sets: darkSets,
|
|
307
|
+
},
|
|
186
308
|
];
|
|
187
|
-
return {
|
|
309
|
+
return {
|
|
310
|
+
schemaVersion: 2,
|
|
311
|
+
ownership,
|
|
312
|
+
marwesVersion: version,
|
|
313
|
+
generatedBy: input.generatedBy,
|
|
314
|
+
project: name,
|
|
315
|
+
hash: hashOf({
|
|
316
|
+
marwesVersion: version,
|
|
317
|
+
project: name,
|
|
318
|
+
sets,
|
|
319
|
+
themes,
|
|
320
|
+
unmapped,
|
|
321
|
+
ownership,
|
|
322
|
+
}),
|
|
323
|
+
unmapped,
|
|
324
|
+
sets,
|
|
325
|
+
themes,
|
|
326
|
+
};
|
|
188
327
|
}
|
|
189
328
|
/** The value of a token as it compares in Penpot: strings are exact, font families and typography are normalised the way Penpot stores them. */
|
|
190
329
|
export function normalizeValue(type, value) {
|
|
191
330
|
if (type === "typography" && typeof value === "object" && value !== null) {
|
|
192
331
|
const entry = {};
|
|
193
332
|
for (const [key, raw] of Object.entries(value)) {
|
|
194
|
-
if (key === "fontFamilies" || key === "fontFamily")
|
|
195
|
-
entry.fontFamily = (Array.isArray(raw)
|
|
196
|
-
|
|
333
|
+
if (key === "fontFamilies" || key === "fontFamily") {
|
|
334
|
+
entry.fontFamily = (Array.isArray(raw)
|
|
335
|
+
? raw
|
|
336
|
+
: [
|
|
337
|
+
raw,
|
|
338
|
+
])
|
|
339
|
+
.map(String)
|
|
340
|
+
.join(",");
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
197
343
|
entry[key] = String(raw);
|
|
344
|
+
}
|
|
198
345
|
}
|
|
199
346
|
return canonical(entry);
|
|
200
347
|
}
|
|
201
|
-
if (type === "color" && typeof value === "string" && /^#[0-9a-f]+$/i.test(value))
|
|
348
|
+
if (type === "color" && typeof value === "string" && /^#[0-9a-f]+$/i.test(value)) {
|
|
202
349
|
return value.toUpperCase();
|
|
350
|
+
}
|
|
203
351
|
// Penpot stores a font family token as a list (["Inter"]) whatever it was given.
|
|
204
|
-
if (type === "fontFamilies" || type === "fontFamily")
|
|
205
|
-
return (Array.isArray(value)
|
|
352
|
+
if (type === "fontFamilies" || type === "fontFamily") {
|
|
353
|
+
return (Array.isArray(value)
|
|
354
|
+
? value
|
|
355
|
+
: [
|
|
356
|
+
value,
|
|
357
|
+
])
|
|
358
|
+
.map(String)
|
|
359
|
+
.join(",");
|
|
360
|
+
}
|
|
206
361
|
return typeof value === "string" ? value : canonical(value);
|
|
207
362
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@penpair/marwes-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "The Marwes UI adapter for penpair: Marwes tokens (and your own theme) in your Penpot file, with drift checks against the installed Marwes: snapshot, check, diff, push.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"jiti": "^2.4.2",
|
|
47
|
-
"@penpair/core": "0.
|
|
47
|
+
"@penpair/core": "0.3.1"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsc -p tsconfig.json",
|