@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.
@@ -1,104 +1,179 @@
1
1
  import { normalizeValue } from "./tokens.js";
2
2
  const set = (live, name) => live.sets.filter((entry) => entry.name === name);
3
3
  const theme = (live, group, name) => live.themes.filter((entry) => entry.group === group && entry.name === name);
4
- const tokenState = (token) => token ? JSON.stringify([token.type, normalizeValue(token.type, token.value), token.description || ""]) : null;
4
+ const tokenState = (token) => token
5
+ ? JSON.stringify([
6
+ token.type,
7
+ normalizeValue(token.type, token.value),
8
+ token.description || "",
9
+ ])
10
+ : null;
5
11
  export function actionSatisfied(action, live) {
6
12
  switch (action.kind) {
7
- case "createSet": return set(live, action.set).length === 1;
13
+ case "createSet":
14
+ return set(live, action.set).length === 1;
8
15
  case "addTokens":
9
16
  case "updateTokens":
10
17
  case "replaceTokens": {
11
18
  const sets = set(live, action.set);
12
- return sets.length === 1 && action.tokens.every((wanted) => {
13
- const actual = sets[0].tokens.filter((token) => token.name === wanted.name);
14
- return actual.length === 1 && tokenState(actual[0]) === tokenState(wanted);
15
- });
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
+ }));
16
24
  }
17
- case "removeTokens": return set(live, action.set).length === 1 && action.names.every((name) => !set(live, action.set)[0].tokens.some((token) => token.name === name));
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)));
18
28
  case "createTheme": {
19
29
  const found = theme(live, action.group, action.name);
20
- return found.length === 1 && found[0].sets.length === action.sets.length && action.sets.every((name) => found[0].sets.includes(name));
30
+ return (found.length === 1 &&
31
+ found[0].sets.length === action.sets.length &&
32
+ action.sets.every((name) => found[0].sets.includes(name)));
21
33
  }
22
34
  case "syncThemeSets": {
23
35
  const found = theme(live, action.group, action.name);
24
- return found.length === 1 && action.add.every((name) => found[0].sets.includes(name)) && action.remove.every((name) => !found[0].sets.includes(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)));
25
39
  }
26
- case "activateTheme": return theme(live, action.group, action.name).some((entry) => entry.active) && live.themes.every((entry) => !entry.active || entry.group === action.group && entry.name === action.name);
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)));
27
43
  }
28
44
  }
29
45
  /** Only old or explicitly desired states can be our partial write; a third value needs owner reconciliation. */
30
46
  export function reconcileAction(action, before, current) {
31
- if (actionSatisfied(action, current))
47
+ if (actionSatisfied(action, current)) {
32
48
  return "complete";
49
+ }
33
50
  switch (action.kind) {
34
- case "createSet": return set(current, action.set).length === 0 ? "pending" : "conflict";
51
+ case "createSet":
52
+ return set(current, action.set).length === 0 ? "pending" : "conflict";
35
53
  case "addTokens":
36
54
  case "updateTokens":
37
55
  case "replaceTokens": {
38
- const oldSets = set(before, action.set), nowSets = set(current, action.set);
39
- if (nowSets.length !== 1 || oldSets.length > 1)
56
+ const oldSets = set(before, action.set);
57
+ const nowSets = set(current, action.set);
58
+ if (nowSets.length !== 1 || oldSets.length > 1) {
40
59
  return "conflict";
60
+ }
41
61
  for (const wanted of action.tokens) {
42
62
  const oldTokens = oldSets[0]?.tokens.filter((token) => token.name === wanted.name) ?? [];
43
63
  const nowTokens = nowSets[0].tokens.filter((token) => token.name === wanted.name);
44
- if (oldTokens.length > 1 || nowTokens.length > 1)
64
+ if (oldTokens.length > 1 || nowTokens.length > 1) {
45
65
  return "conflict";
46
- const old = tokenState(oldTokens[0]), actual = tokenState(nowTokens[0]), desired = tokenState(wanted);
47
- if (action.kind === "addTokens" && old !== null && old !== desired)
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) {
48
71
  return "conflict";
49
- if (action.kind !== "addTokens" && old === null)
72
+ }
73
+ if (action.kind !== "addTokens" && old === null) {
50
74
  return "conflict";
51
- if (actual !== old && actual !== desired && !(action.kind === "replaceTokens" && actual === null))
75
+ }
76
+ if (actual !== old &&
77
+ actual !== desired &&
78
+ !(action.kind === "replaceTokens" && actual === null)) {
52
79
  return "conflict";
80
+ }
53
81
  }
54
82
  return "pending";
55
83
  }
56
84
  case "removeTokens": {
57
- const oldSets = set(before, action.set), nowSets = set(current, action.set);
58
- if (oldSets.length !== 1 || nowSets.length !== 1)
85
+ const oldSets = set(before, action.set);
86
+ const nowSets = set(current, action.set);
87
+ if (oldSets.length !== 1 || nowSets.length !== 1) {
59
88
  return "conflict";
89
+ }
60
90
  return action.names.every((name) => {
61
91
  const old = oldSets[0].tokens.filter((token) => token.name === name);
62
92
  const actual = nowSets[0].tokens.filter((token) => token.name === name);
63
- return actual.length <= 1 && (actual.length === 0 || old.length === 1 && tokenState(old[0]) === tokenState(actual[0]));
64
- }) ? "pending" : "conflict";
93
+ return (actual.length <= 1 &&
94
+ (actual.length === 0 ||
95
+ (old.length === 1 && tokenState(old[0]) === tokenState(actual[0]))));
96
+ })
97
+ ? "pending"
98
+ : "conflict";
65
99
  }
66
100
  case "createTheme": {
67
- const old = theme(before, action.group, action.name), actual = theme(current, action.group, action.name);
68
- return old.length === 0 && actual.length <= 1 && (actual.length === 0 || actual[0].sets.every((name) => action.sets.includes(name))) ? "pending" : "conflict";
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";
69
108
  }
70
109
  case "syncThemeSets": {
71
- const old = theme(before, action.group, action.name), actual = theme(current, action.group, action.name);
72
- if (old.length !== 1 || actual.length !== 1)
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) {
73
113
  return "conflict";
74
- const touched = new Set([...action.add, ...action.remove]);
75
- const unchanged = (sets) => sets.filter((name) => !touched.has(name)).sort().join("\n");
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");
76
123
  return unchanged(old[0].sets) === unchanged(actual[0].sets) ? "pending" : "conflict";
77
124
  }
78
125
  case "activateTheme": {
79
- const active = (live) => live.themes.filter((entry) => entry.active).map((entry) => JSON.stringify([entry.group, entry.name])).sort().join("\n");
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");
80
134
  return active(current) === active(before) || active(current) === "" ? "pending" : "conflict";
81
135
  }
82
136
  }
83
137
  }
84
138
  /** Recompute residual work from actual state so completed token replacements are never removed/recreated again. */
85
139
  export function remainingAction(action, current) {
86
- if (actionSatisfied(action, current))
140
+ if (actionSatisfied(action, current)) {
87
141
  return undefined;
142
+ }
88
143
  if ("tokens" in action) {
89
144
  const actual = set(current, action.set)[0];
90
- const tokens = action.tokens.filter((wanted) => tokenState(actual?.tokens.find((token) => token.name === wanted.name)) !== tokenState(wanted));
91
- return tokens.length ? { ...action, tokens } : undefined;
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
+ };
92
159
  }
93
- if (action.kind === "removeTokens")
94
- return { ...action, names: action.names.filter((name) => set(current, action.set)[0]?.tokens.some((token) => token.name === name)) };
95
160
  if (action.kind === "createTheme" && theme(current, action.group, action.name).length === 1) {
96
161
  const actual = theme(current, action.group, action.name)[0];
97
- return { kind: "syncThemeSets", group: action.group, name: action.name, add: action.sets.filter((name) => !actual.sets.includes(name)), remove: [] };
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
+ };
98
169
  }
99
170
  if (action.kind === "syncThemeSets") {
100
171
  const actual = theme(current, action.group, action.name)[0];
101
- return { ...action, add: action.add.filter((name) => !actual.sets.includes(name)), remove: action.remove.filter((name) => actual.sets.includes(name)) };
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
+ };
102
177
  }
103
178
  return action;
104
179
  }
@@ -1,12 +1,7 @@
1
1
  import { type TokenSnapshot } from "./tokens.js";
2
- export interface ResolvedExpectation {
3
- name: string;
4
- type: string;
5
- value?: unknown;
6
- unknown?: string;
7
- }
8
- /** A small arithmetic grammar, never JavaScript evaluation. Unsupported units/functions are explicit unknowns. */
9
- export declare function numericExpression(source: string): number;
2
+ import { type ResolvedDeclaredToken } from "@penpair/core";
3
+ export { numericExpression } from "@penpair/core";
4
+ export type ResolvedExpectation = ResolvedDeclaredToken;
10
5
  export declare function expectedResolved(snapshot: TokenSnapshot, theme: {
11
6
  sets: string[];
12
7
  }): ResolvedExpectation[];
@@ -1,128 +1,124 @@
1
1
  import { canonical, normalizeValue, toHexColor } from "./tokens.js";
2
- /** A small arithmetic grammar, never JavaScript evaluation. Unsupported units/functions are explicit unknowns. */
3
- export function numericExpression(source) {
4
- if (source.length > 2000)
5
- throw new Error("Expression budget exceeded");
6
- const tokens = source.match(/(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|[()+*/-]/gi) ?? [];
7
- if (tokens.join("") !== source.replace(/\s+/g, "") || tokens.length > 500)
8
- throw new Error("Unsupported numeric expression");
9
- let index = 0, depth = 0;
10
- const atom = () => {
11
- if (++depth > 64)
12
- throw new Error("Expression depth exceeded");
13
- const token = tokens[index++];
14
- let value;
15
- if (token === "(") {
16
- value = sum();
17
- if (tokens[index++] !== ")")
18
- throw new Error("Unclosed expression");
19
- }
20
- else if (token === "-")
21
- value = -atom();
22
- else if (token === "+")
23
- value = atom();
24
- else {
25
- if (!token || !/^[\d.]/.test(token))
26
- throw new Error("Missing number");
27
- value = Number(token);
28
- }
29
- depth--;
30
- if (!Number.isFinite(value))
31
- throw new Error("Nonfinite expression");
32
- return value;
33
- };
34
- const product = () => { let value = atom(); while (["*", "/"].includes(tokens[index] ?? "")) {
35
- const op = tokens[index++], right = atom();
36
- value = op === "*" ? value * right : value / right;
37
- } return value; };
38
- const sum = () => { let value = product(); while (["+", "-"].includes(tokens[index] ?? "")) {
39
- const op = tokens[index++], right = product();
40
- value = op === "+" ? value + right : value - right;
41
- } return value; };
42
- const value = sum();
43
- if (index !== tokens.length || !Number.isFinite(value))
44
- throw new Error("Invalid numeric expression");
45
- return value;
46
- }
47
- const numericTypes = new Set(["borderRadius", "dimension", "fontSizes", "letterSpacing", "number", "opacity", "rotation", "sizing", "spacing", "borderWidth"]);
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
+ ]);
48
16
  export function expectedResolved(snapshot, theme) {
49
- const effective = new Map(snapshot.sets.filter(set => theme.sets.includes(set.name)).flatMap(set => set.tokens.map(token => [token.name, token])));
50
- const resolve = (name, visiting = new Set()) => {
51
- if (visiting.has(name) || visiting.size >= 64)
52
- throw new Error("Cyclic or over-deep reference");
53
- const token = effective.get(name);
54
- if (!token)
55
- throw new Error("Missing referenced token: " + name);
56
- const next = new Set(visiting);
57
- next.add(name);
58
- const value = (input) => {
59
- if (typeof input === "string") {
60
- const alias = /^\{([^{}]+)\}$/.exec(input);
61
- if (alias)
62
- return resolve(alias[1], next);
63
- return input.replace(/\{([^{}]+)\}/g, (_match, reference) => {
64
- const resolved = resolve(reference, next);
65
- if (typeof resolved !== "number" && typeof resolved !== "string")
66
- throw new Error("Composite interpolation unsupported");
67
- return String(resolved);
68
- });
69
- }
70
- if (input && typeof input === "object" && !Array.isArray(input))
71
- return Object.fromEntries(Object.entries(input).map(([key, entry]) => [key, value(entry)]));
72
- return input;
73
- };
74
- const resolved = value(token.value);
75
- return numericTypes.has(token.type) ? typeof resolved === "number" ? resolved : numericExpression(String(resolved)) : resolved;
76
- };
77
- return [...effective.values()].map(token => { try {
78
- return { name: token.name, type: token.type, value: resolve(token.name) };
79
- }
80
- catch (error) {
81
- return { name: token.name, type: token.type, unknown: String(error.message) };
82
- } });
17
+ return resolveDeclaredTokens(snapshot, theme.sets);
83
18
  }
84
19
  export function compareResolved(wanted, actual) {
85
- if (wanted.unknown)
86
- return { unknown: wanted.unknown };
87
- if (actual === undefined || actual === null)
88
- return { unknown: "API resolvedValue is unavailable" };
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
+ }
89
30
  try {
90
31
  if (numericTypes.has(wanted.type)) {
91
32
  const value = typeof actual === "number" ? actual : numericExpression(String(actual));
92
- return { equal: typeof wanted.value === "number" && Math.abs(value - wanted.value) <= 1e-9 * Math.max(1, Math.abs(wanted.value)) };
33
+ return {
34
+ equal: typeof wanted.value === "number" &&
35
+ Math.abs(value - wanted.value) <= 1e-9 * Math.max(1, Math.abs(wanted.value)),
36
+ };
93
37
  }
94
38
  if (wanted.type === "color") {
95
- const color = (value) => { if (typeof value !== "string")
96
- throw new Error("Unsupported resolved color"); const expanded = /^#[0-9a-f]{3,4}$/i.test(value) ? "#" + value.slice(1).split("").map(c => c + c).join("") : value; const hex = toHexColor(expanded); return hex.length === 9 && hex.endsWith("FF") ? hex.slice(0, -2) : hex; };
97
- return { equal: color(wanted.value) === color(actual) };
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
+ };
98
69
  }
99
- if (wanted.type === "fontFamilies" && Array.isArray(actual) && !actual.every(value => typeof value === "string"))
100
- return { unknown: "Unsupported nested font resolution" };
101
- if (wanted.type === "shadow")
102
- return { unknown: "Composite shadow resolution has no supported comparison model" };
103
70
  if (wanted.type === "typography") {
104
71
  const normalize = (input) => {
105
- const object = Array.isArray(input) ? input.length === 1 ? input[0] : undefined : input;
106
- if (!object || typeof object !== "object" || Array.isArray(object))
72
+ const object = Array.isArray(input) ? (input.length === 1 ? input[0] : undefined) : input;
73
+ if (!object || typeof object !== "object" || Array.isArray(object)) {
107
74
  throw new Error("Unsupported typography resolution");
108
- const aliases = { fontFamilies: "fontFamily", fontSizes: "fontSize", fontWeights: "fontWeight" };
75
+ }
76
+ const aliases = {
77
+ fontFamilies: "fontFamily",
78
+ fontSizes: "fontSize",
79
+ fontWeights: "fontWeight",
80
+ };
109
81
  return Object.fromEntries(Object.entries(object).map(([key, value]) => {
110
82
  key = aliases[key] ?? key;
111
83
  if (key === "fontFamily") {
112
- const family = Array.isArray(value) ? value : [value];
113
- if (!family.every(v => typeof v === "string"))
84
+ const family = Array.isArray(value)
85
+ ? value
86
+ : [
87
+ value,
88
+ ];
89
+ if (!family.every((v) => typeof v === "string")) {
114
90
  throw new Error("Unsupported nested font resolution");
115
- return [key, family];
91
+ }
92
+ return [
93
+ key,
94
+ family,
95
+ ];
116
96
  }
117
- return [key, ["fontSize", "letterSpacing", "lineHeight"].includes(key) ? String(numericExpression(String(value))) : String(value)];
97
+ return [
98
+ key,
99
+ [
100
+ "fontSize",
101
+ "letterSpacing",
102
+ "lineHeight",
103
+ ].includes(key)
104
+ ? String(numericExpression(String(value)))
105
+ : String(value),
106
+ ];
118
107
  }));
119
108
  };
120
- const expected = normalize(wanted.value), observed = normalize(actual);
121
- return { equal: Object.keys(expected).every(key => canonical(expected[key]) === canonical(observed[key])) };
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
+ };
122
114
  }
123
- return { equal: normalizeValue(wanted.type, wanted.value) === normalizeValue(wanted.type, actual) };
115
+ return {
116
+ equal: normalizeValue(wanted.type, wanted.value) === normalizeValue(wanted.type, actual),
117
+ };
124
118
  }
125
119
  catch (error) {
126
- return { unknown: String(error.message) };
120
+ return {
121
+ unknown: String(error.message),
122
+ };
127
123
  }
128
124
  }