@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
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Action, LiveTokens } from "./penpot.js";
|
|
2
|
+
export declare function actionSatisfied(action: Action, live: LiveTokens): boolean;
|
|
3
|
+
/** Only old or explicitly desired states can be our partial write; a third value needs owner reconciliation. */
|
|
4
|
+
export declare function reconcileAction(action: Action, before: LiveTokens, current: LiveTokens): "complete" | "pending" | "conflict";
|
|
5
|
+
/** Recompute residual work from actual state so completed token replacements are never removed/recreated again. */
|
|
6
|
+
export declare function remainingAction(action: Action, current: LiveTokens): Action | undefined;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { normalizeValue } from "./tokens.js";
|
|
2
|
+
const set = (live, name) => live.sets.filter((entry) => entry.name === name);
|
|
3
|
+
const theme = (live, group, name) => live.themes.filter((entry) => entry.group === group && entry.name === name);
|
|
4
|
+
const tokenState = (token) => token
|
|
5
|
+
? JSON.stringify([
|
|
6
|
+
token.type,
|
|
7
|
+
normalizeValue(token.type, token.value),
|
|
8
|
+
token.description || "",
|
|
9
|
+
])
|
|
10
|
+
: null;
|
|
11
|
+
export function actionSatisfied(action, live) {
|
|
12
|
+
switch (action.kind) {
|
|
13
|
+
case "createSet":
|
|
14
|
+
return set(live, action.set).length === 1;
|
|
15
|
+
case "addTokens":
|
|
16
|
+
case "updateTokens":
|
|
17
|
+
case "replaceTokens": {
|
|
18
|
+
const sets = set(live, action.set);
|
|
19
|
+
return (sets.length === 1 &&
|
|
20
|
+
action.tokens.every((wanted) => {
|
|
21
|
+
const actual = sets[0].tokens.filter((token) => token.name === wanted.name);
|
|
22
|
+
return actual.length === 1 && tokenState(actual[0]) === tokenState(wanted);
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
case "removeTokens":
|
|
26
|
+
return (set(live, action.set).length === 1 &&
|
|
27
|
+
action.names.every((name) => !set(live, action.set)[0].tokens.some((token) => token.name === name)));
|
|
28
|
+
case "createTheme": {
|
|
29
|
+
const found = theme(live, action.group, action.name);
|
|
30
|
+
return (found.length === 1 &&
|
|
31
|
+
found[0].sets.length === action.sets.length &&
|
|
32
|
+
action.sets.every((name) => found[0].sets.includes(name)));
|
|
33
|
+
}
|
|
34
|
+
case "syncThemeSets": {
|
|
35
|
+
const found = theme(live, action.group, action.name);
|
|
36
|
+
return (found.length === 1 &&
|
|
37
|
+
action.add.every((name) => found[0].sets.includes(name)) &&
|
|
38
|
+
action.remove.every((name) => !found[0].sets.includes(name)));
|
|
39
|
+
}
|
|
40
|
+
case "activateTheme":
|
|
41
|
+
return (theme(live, action.group, action.name).some((entry) => entry.active) &&
|
|
42
|
+
live.themes.every((entry) => !entry.active || (entry.group === action.group && entry.name === action.name)));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Only old or explicitly desired states can be our partial write; a third value needs owner reconciliation. */
|
|
46
|
+
export function reconcileAction(action, before, current) {
|
|
47
|
+
if (actionSatisfied(action, current)) {
|
|
48
|
+
return "complete";
|
|
49
|
+
}
|
|
50
|
+
switch (action.kind) {
|
|
51
|
+
case "createSet":
|
|
52
|
+
return set(current, action.set).length === 0 ? "pending" : "conflict";
|
|
53
|
+
case "addTokens":
|
|
54
|
+
case "updateTokens":
|
|
55
|
+
case "replaceTokens": {
|
|
56
|
+
const oldSets = set(before, action.set);
|
|
57
|
+
const nowSets = set(current, action.set);
|
|
58
|
+
if (nowSets.length !== 1 || oldSets.length > 1) {
|
|
59
|
+
return "conflict";
|
|
60
|
+
}
|
|
61
|
+
for (const wanted of action.tokens) {
|
|
62
|
+
const oldTokens = oldSets[0]?.tokens.filter((token) => token.name === wanted.name) ?? [];
|
|
63
|
+
const nowTokens = nowSets[0].tokens.filter((token) => token.name === wanted.name);
|
|
64
|
+
if (oldTokens.length > 1 || nowTokens.length > 1) {
|
|
65
|
+
return "conflict";
|
|
66
|
+
}
|
|
67
|
+
const old = tokenState(oldTokens[0]);
|
|
68
|
+
const actual = tokenState(nowTokens[0]);
|
|
69
|
+
const desired = tokenState(wanted);
|
|
70
|
+
if (action.kind === "addTokens" && old !== null && old !== desired) {
|
|
71
|
+
return "conflict";
|
|
72
|
+
}
|
|
73
|
+
if (action.kind !== "addTokens" && old === null) {
|
|
74
|
+
return "conflict";
|
|
75
|
+
}
|
|
76
|
+
if (actual !== old &&
|
|
77
|
+
actual !== desired &&
|
|
78
|
+
!(action.kind === "replaceTokens" && actual === null)) {
|
|
79
|
+
return "conflict";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return "pending";
|
|
83
|
+
}
|
|
84
|
+
case "removeTokens": {
|
|
85
|
+
const oldSets = set(before, action.set);
|
|
86
|
+
const nowSets = set(current, action.set);
|
|
87
|
+
if (oldSets.length !== 1 || nowSets.length !== 1) {
|
|
88
|
+
return "conflict";
|
|
89
|
+
}
|
|
90
|
+
return action.names.every((name) => {
|
|
91
|
+
const old = oldSets[0].tokens.filter((token) => token.name === name);
|
|
92
|
+
const actual = nowSets[0].tokens.filter((token) => token.name === name);
|
|
93
|
+
return (actual.length <= 1 &&
|
|
94
|
+
(actual.length === 0 ||
|
|
95
|
+
(old.length === 1 && tokenState(old[0]) === tokenState(actual[0]))));
|
|
96
|
+
})
|
|
97
|
+
? "pending"
|
|
98
|
+
: "conflict";
|
|
99
|
+
}
|
|
100
|
+
case "createTheme": {
|
|
101
|
+
const old = theme(before, action.group, action.name);
|
|
102
|
+
const actual = theme(current, action.group, action.name);
|
|
103
|
+
return old.length === 0 &&
|
|
104
|
+
actual.length <= 1 &&
|
|
105
|
+
(actual.length === 0 || actual[0].sets.every((name) => action.sets.includes(name)))
|
|
106
|
+
? "pending"
|
|
107
|
+
: "conflict";
|
|
108
|
+
}
|
|
109
|
+
case "syncThemeSets": {
|
|
110
|
+
const old = theme(before, action.group, action.name);
|
|
111
|
+
const actual = theme(current, action.group, action.name);
|
|
112
|
+
if (old.length !== 1 || actual.length !== 1) {
|
|
113
|
+
return "conflict";
|
|
114
|
+
}
|
|
115
|
+
const touched = new Set([
|
|
116
|
+
...action.add,
|
|
117
|
+
...action.remove,
|
|
118
|
+
]);
|
|
119
|
+
const unchanged = (sets) => sets
|
|
120
|
+
.filter((name) => !touched.has(name))
|
|
121
|
+
.sort()
|
|
122
|
+
.join("\n");
|
|
123
|
+
return unchanged(old[0].sets) === unchanged(actual[0].sets) ? "pending" : "conflict";
|
|
124
|
+
}
|
|
125
|
+
case "activateTheme": {
|
|
126
|
+
const active = (live) => live.themes
|
|
127
|
+
.filter((entry) => entry.active)
|
|
128
|
+
.map((entry) => JSON.stringify([
|
|
129
|
+
entry.group,
|
|
130
|
+
entry.name,
|
|
131
|
+
]))
|
|
132
|
+
.sort()
|
|
133
|
+
.join("\n");
|
|
134
|
+
return active(current) === active(before) || active(current) === "" ? "pending" : "conflict";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Recompute residual work from actual state so completed token replacements are never removed/recreated again. */
|
|
139
|
+
export function remainingAction(action, current) {
|
|
140
|
+
if (actionSatisfied(action, current)) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
if ("tokens" in action) {
|
|
144
|
+
const actual = set(current, action.set)[0];
|
|
145
|
+
const tokens = action.tokens.filter((wanted) => tokenState(actual?.tokens.find((token) => token.name === wanted.name)) !==
|
|
146
|
+
tokenState(wanted));
|
|
147
|
+
return tokens.length
|
|
148
|
+
? {
|
|
149
|
+
...action,
|
|
150
|
+
tokens,
|
|
151
|
+
}
|
|
152
|
+
: undefined;
|
|
153
|
+
}
|
|
154
|
+
if (action.kind === "removeTokens") {
|
|
155
|
+
return {
|
|
156
|
+
...action,
|
|
157
|
+
names: action.names.filter((name) => set(current, action.set)[0]?.tokens.some((token) => token.name === name)),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (action.kind === "createTheme" && theme(current, action.group, action.name).length === 1) {
|
|
161
|
+
const actual = theme(current, action.group, action.name)[0];
|
|
162
|
+
return {
|
|
163
|
+
kind: "syncThemeSets",
|
|
164
|
+
group: action.group,
|
|
165
|
+
name: action.name,
|
|
166
|
+
add: action.sets.filter((name) => !actual.sets.includes(name)),
|
|
167
|
+
remove: [],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (action.kind === "syncThemeSets") {
|
|
171
|
+
const actual = theme(current, action.group, action.name)[0];
|
|
172
|
+
return {
|
|
173
|
+
...action,
|
|
174
|
+
add: action.add.filter((name) => !actual.sets.includes(name)),
|
|
175
|
+
remove: action.remove.filter((name) => actual.sets.includes(name)),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return action;
|
|
179
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type TokenSnapshot } from "./tokens.js";
|
|
2
|
+
import { type ResolvedDeclaredToken } from "@penpair/core";
|
|
3
|
+
export { numericExpression } from "@penpair/core";
|
|
4
|
+
export type ResolvedExpectation = ResolvedDeclaredToken;
|
|
5
|
+
export declare function expectedResolved(snapshot: TokenSnapshot, theme: {
|
|
6
|
+
sets: string[];
|
|
7
|
+
}): ResolvedExpectation[];
|
|
8
|
+
export declare function compareResolved(wanted: ResolvedExpectation, actual: unknown): {
|
|
9
|
+
equal?: boolean;
|
|
10
|
+
unknown?: string;
|
|
11
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { canonical, normalizeValue, toHexColor } from "./tokens.js";
|
|
2
|
+
import { numericExpression, resolveDeclaredTokens, } from "@penpair/core";
|
|
3
|
+
export { numericExpression } from "@penpair/core";
|
|
4
|
+
const numericTypes = new Set([
|
|
5
|
+
"borderRadius",
|
|
6
|
+
"dimension",
|
|
7
|
+
"fontSizes",
|
|
8
|
+
"letterSpacing",
|
|
9
|
+
"number",
|
|
10
|
+
"opacity",
|
|
11
|
+
"rotation",
|
|
12
|
+
"sizing",
|
|
13
|
+
"spacing",
|
|
14
|
+
"borderWidth",
|
|
15
|
+
]);
|
|
16
|
+
export function expectedResolved(snapshot, theme) {
|
|
17
|
+
return resolveDeclaredTokens(snapshot, theme.sets);
|
|
18
|
+
}
|
|
19
|
+
export function compareResolved(wanted, actual) {
|
|
20
|
+
if (wanted.unknown) {
|
|
21
|
+
return {
|
|
22
|
+
unknown: wanted.unknown,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (actual === undefined || actual === null) {
|
|
26
|
+
return {
|
|
27
|
+
unknown: "API resolvedValue is unavailable",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
if (numericTypes.has(wanted.type)) {
|
|
32
|
+
const value = typeof actual === "number" ? actual : numericExpression(String(actual));
|
|
33
|
+
return {
|
|
34
|
+
equal: typeof wanted.value === "number" &&
|
|
35
|
+
Math.abs(value - wanted.value) <= 1e-9 * Math.max(1, Math.abs(wanted.value)),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (wanted.type === "color") {
|
|
39
|
+
const color = (value) => {
|
|
40
|
+
if (typeof value !== "string") {
|
|
41
|
+
throw new Error("Unsupported resolved color");
|
|
42
|
+
}
|
|
43
|
+
const expanded = /^#[0-9a-f]{3,4}$/i.test(value)
|
|
44
|
+
? "#" +
|
|
45
|
+
value
|
|
46
|
+
.slice(1)
|
|
47
|
+
.split("")
|
|
48
|
+
.map((c) => c + c)
|
|
49
|
+
.join("")
|
|
50
|
+
: value;
|
|
51
|
+
const hex = toHexColor(expanded);
|
|
52
|
+
return hex.length === 9 && hex.endsWith("FF") ? hex.slice(0, -2) : hex;
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
equal: color(wanted.value) === color(actual),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (wanted.type === "fontFamilies" &&
|
|
59
|
+
Array.isArray(actual) &&
|
|
60
|
+
!actual.every((value) => typeof value === "string")) {
|
|
61
|
+
return {
|
|
62
|
+
unknown: "Unsupported nested font resolution",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (wanted.type === "shadow") {
|
|
66
|
+
return {
|
|
67
|
+
unknown: "Composite shadow resolution has no supported comparison model",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (wanted.type === "typography") {
|
|
71
|
+
const normalize = (input) => {
|
|
72
|
+
const object = Array.isArray(input) ? (input.length === 1 ? input[0] : undefined) : input;
|
|
73
|
+
if (!object || typeof object !== "object" || Array.isArray(object)) {
|
|
74
|
+
throw new Error("Unsupported typography resolution");
|
|
75
|
+
}
|
|
76
|
+
const aliases = {
|
|
77
|
+
fontFamilies: "fontFamily",
|
|
78
|
+
fontSizes: "fontSize",
|
|
79
|
+
fontWeights: "fontWeight",
|
|
80
|
+
};
|
|
81
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => {
|
|
82
|
+
key = aliases[key] ?? key;
|
|
83
|
+
if (key === "fontFamily") {
|
|
84
|
+
const family = Array.isArray(value)
|
|
85
|
+
? value
|
|
86
|
+
: [
|
|
87
|
+
value,
|
|
88
|
+
];
|
|
89
|
+
if (!family.every((v) => typeof v === "string")) {
|
|
90
|
+
throw new Error("Unsupported nested font resolution");
|
|
91
|
+
}
|
|
92
|
+
return [
|
|
93
|
+
key,
|
|
94
|
+
family,
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
return [
|
|
98
|
+
key,
|
|
99
|
+
[
|
|
100
|
+
"fontSize",
|
|
101
|
+
"letterSpacing",
|
|
102
|
+
"lineHeight",
|
|
103
|
+
].includes(key)
|
|
104
|
+
? String(numericExpression(String(value)))
|
|
105
|
+
: String(value),
|
|
106
|
+
];
|
|
107
|
+
}));
|
|
108
|
+
};
|
|
109
|
+
const expected = normalize(wanted.value);
|
|
110
|
+
const observed = normalize(actual);
|
|
111
|
+
return {
|
|
112
|
+
equal: Object.keys(expected).every((key) => canonical(expected[key]) === canonical(observed[key])),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
equal: normalizeValue(wanted.type, wanted.value) === normalizeValue(wanted.type, actual),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
return {
|
|
121
|
+
unknown: String(error.message),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
package/dist/snapshot-file.d.ts
CHANGED
|
@@ -41,3 +41,48 @@ export interface SnapshotDiff {
|
|
|
41
41
|
export declare function diffSnapshots(before: TokenSnapshot | undefined, after: TokenSnapshot): SnapshotDiff;
|
|
42
42
|
/** A short, readable account of a diff for the terminal; long lists are cut with a count. */
|
|
43
43
|
export declare function formatDiff(diff: SnapshotDiff, limit?: number): string[];
|
|
44
|
+
/** Explicitly classify every legacy set; names cannot establish ownership. Original fields survive migration. */
|
|
45
|
+
export declare function migrateTokenSnapshot(raw: unknown, ownership: Record<string, "adapter" | "project">): TokenSnapshot;
|
|
46
|
+
export declare const tokenSnapshotSchema: {
|
|
47
|
+
readonly $schema: "https://json-schema.org/draft/2020-12/schema";
|
|
48
|
+
readonly $id: "urn:penpair:schema:v2:adapter-token-snapshot";
|
|
49
|
+
readonly type: "object";
|
|
50
|
+
readonly required: readonly ["schemaVersion", "ownership", "marwesVersion", "generatedBy", "project", "hash", "sets", "themes", "unmapped"];
|
|
51
|
+
readonly properties: {
|
|
52
|
+
readonly schemaVersion: {
|
|
53
|
+
readonly const: 2;
|
|
54
|
+
};
|
|
55
|
+
readonly ownership: {
|
|
56
|
+
readonly type: "object";
|
|
57
|
+
readonly additionalProperties: {
|
|
58
|
+
readonly enum: readonly ["adapter", "project"];
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
readonly marwesVersion: {
|
|
62
|
+
readonly type: "string";
|
|
63
|
+
};
|
|
64
|
+
readonly generatedBy: {
|
|
65
|
+
readonly type: "string";
|
|
66
|
+
};
|
|
67
|
+
readonly project: {
|
|
68
|
+
readonly type: "string";
|
|
69
|
+
};
|
|
70
|
+
readonly hash: {
|
|
71
|
+
readonly type: "string";
|
|
72
|
+
readonly pattern: "^[a-f0-9]{64}$";
|
|
73
|
+
};
|
|
74
|
+
readonly sets: {
|
|
75
|
+
readonly type: "array";
|
|
76
|
+
};
|
|
77
|
+
readonly themes: {
|
|
78
|
+
readonly type: "array";
|
|
79
|
+
};
|
|
80
|
+
readonly unmapped: {
|
|
81
|
+
readonly type: "array";
|
|
82
|
+
readonly items: {
|
|
83
|
+
readonly type: "string";
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
readonly additionalProperties: true;
|
|
88
|
+
};
|