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