@jay-framework/design-system-validator 0.25.4 → 0.26.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.
@@ -0,0 +1,63 @@
1
+ interface TypographyToken {
2
+ fontFamily?: string;
3
+ fontSize?: string;
4
+ fontWeight?: string | number;
5
+ lineHeight?: string | number;
6
+ letterSpacing?: string;
7
+ }
8
+ interface ComponentSpec {
9
+ [property: string]: string;
10
+ }
11
+ interface AnimationPreset {
12
+ duration?: string;
13
+ easing?: string;
14
+ }
15
+ interface DesignRules {
16
+ 'max-font-weights'?: number;
17
+ 'max-primary-buttons'?: number;
18
+ 'require-contrast-aa'?: boolean;
19
+ }
20
+ interface DesignTokens {
21
+ name?: string;
22
+ colors: Record<string, string>;
23
+ typography: Record<string, TypographyToken>;
24
+ spacing: Record<string, string>;
25
+ rounded: Record<string, string>;
26
+ breakpoints: Record<string, string>;
27
+ animations: Record<string, AnimationPreset>;
28
+ components: Record<string, ComponentSpec>;
29
+ rawComponents: Record<string, ComponentSpec>;
30
+ rules: DesignRules;
31
+ }
32
+ declare function parseDesignMd(content: string): DesignTokens | null;
33
+ interface FoundDesignMd {
34
+ tokens: DesignTokens;
35
+ designMdPath: string;
36
+ }
37
+ declare function findDesignMd(filePath: string, projectRoot: string): FoundDesignMd | null;
38
+
39
+ interface MatchResult {
40
+ matches: boolean;
41
+ suggestion?: string;
42
+ }
43
+ declare function isColorProperty(property: string): boolean;
44
+ declare function isSpacingProperty(property: string): boolean;
45
+ declare function isRadiusProperty(property: string): boolean;
46
+ declare function isTypographyProperty(property: string): boolean;
47
+ declare function isAnimationDurationProperty(property: string): boolean;
48
+ declare function isAnimationEasingProperty(property: string): boolean;
49
+ declare function matchColor(value: string, colorTokens: Record<string, string>): MatchResult;
50
+ declare function matchSpacing(value: string, spacingTokens: Record<string, string>): MatchResult;
51
+ declare function matchRounded(value: string, roundedTokens: Record<string, string>): MatchResult;
52
+ declare function matchTypographyProperty(property: string, value: string, typographyTokens: Record<string, TypographyToken>): MatchResult;
53
+ declare function matchAnimationDuration(value: string, animationTokens: Record<string, AnimationPreset>): MatchResult;
54
+ declare function matchAnimationEasing(value: string, animationTokens: Record<string, AnimationPreset>): MatchResult;
55
+ interface ComponentMismatch {
56
+ cssProp: string;
57
+ expected: string;
58
+ expectedRaw?: string;
59
+ actual: string;
60
+ }
61
+ declare function matchComponent(elementStyles: Record<string, string>, componentSpec: ComponentSpec, rawSpec?: ComponentSpec): ComponentMismatch[];
62
+
63
+ export { type AnimationPreset, type ComponentSpec, type DesignRules, type DesignTokens, type FoundDesignMd, type MatchResult, type TypographyToken, findDesignMd, isAnimationDurationProperty, isAnimationEasingProperty, isColorProperty, isRadiusProperty, isSpacingProperty, isTypographyProperty, matchAnimationDuration, matchAnimationEasing, matchColor, matchComponent, matchRounded, matchSpacing, matchTypographyProperty, parseDesignMd };
@@ -0,0 +1,18 @@
1
+ import { f, i, a, b, c, d, e, m, g, h, j, k, l, n, p } from "./parse-design-md-C2uZEN8Y.js";
2
+ export {
3
+ f as findDesignMd,
4
+ i as isAnimationDurationProperty,
5
+ a as isAnimationEasingProperty,
6
+ b as isColorProperty,
7
+ c as isRadiusProperty,
8
+ d as isSpacingProperty,
9
+ e as isTypographyProperty,
10
+ m as matchAnimationDuration,
11
+ g as matchAnimationEasing,
12
+ h as matchColor,
13
+ j as matchComponent,
14
+ k as matchRounded,
15
+ l as matchSpacing,
16
+ n as matchTypographyProperty,
17
+ p as parseDesignMd
18
+ };
@@ -0,0 +1,413 @@
1
+ import yaml from "js-yaml";
2
+ import fs__default from "node:fs";
3
+ import path__default from "node:path";
4
+ function normalizeHex(color) {
5
+ const hex = color.trim().toLowerCase();
6
+ if (!/^#[0-9a-f]{3,8}$/.test(hex)) return null;
7
+ if (hex.length === 4) {
8
+ return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
9
+ }
10
+ return hex.length === 7 ? hex : null;
11
+ }
12
+ function parseRgb(value) {
13
+ const match = value.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
14
+ if (!match) return null;
15
+ const r = parseInt(match[1], 10);
16
+ const g = parseInt(match[2], 10);
17
+ const b = parseInt(match[3], 10);
18
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
19
+ }
20
+ function normalizeColor(value) {
21
+ const trimmed = value.trim().toLowerCase();
22
+ if (trimmed.startsWith("#")) return normalizeHex(trimmed);
23
+ if (trimmed.startsWith("rgb")) return parseRgb(trimmed);
24
+ return null;
25
+ }
26
+ const COLOR_PROPERTIES = /* @__PURE__ */ new Set([
27
+ "color",
28
+ "background-color",
29
+ "border-color",
30
+ "border-top-color",
31
+ "border-right-color",
32
+ "border-bottom-color",
33
+ "border-left-color",
34
+ "outline-color",
35
+ "text-decoration-color",
36
+ "fill",
37
+ "stroke"
38
+ ]);
39
+ const SPACING_PROPERTIES = /* @__PURE__ */ new Set([
40
+ "padding",
41
+ "padding-top",
42
+ "padding-right",
43
+ "padding-bottom",
44
+ "padding-left",
45
+ "margin",
46
+ "margin-top",
47
+ "margin-right",
48
+ "margin-bottom",
49
+ "margin-left",
50
+ "gap",
51
+ "row-gap",
52
+ "column-gap",
53
+ "top",
54
+ "right",
55
+ "bottom",
56
+ "left"
57
+ ]);
58
+ const RADIUS_PROPERTIES = /* @__PURE__ */ new Set([
59
+ "border-radius",
60
+ "border-top-left-radius",
61
+ "border-top-right-radius",
62
+ "border-bottom-left-radius",
63
+ "border-bottom-right-radius"
64
+ ]);
65
+ function isColorProperty(property) {
66
+ return COLOR_PROPERTIES.has(property);
67
+ }
68
+ function isSpacingProperty(property) {
69
+ return SPACING_PROPERTIES.has(property);
70
+ }
71
+ function isRadiusProperty(property) {
72
+ return RADIUS_PROPERTIES.has(property);
73
+ }
74
+ function isTypographyProperty(property) {
75
+ return property === "font-size" || property === "font-weight" || property === "line-height" || property === "letter-spacing" || property === "font-family";
76
+ }
77
+ function isAnimationDurationProperty(property) {
78
+ return property === "transition-duration" || property === "animation-duration";
79
+ }
80
+ function isAnimationEasingProperty(property) {
81
+ return property === "transition-timing-function" || property === "animation-timing-function";
82
+ }
83
+ function matchColor(value, colorTokens) {
84
+ if (value.startsWith("var(")) return { matches: true };
85
+ if (value === "transparent" || value === "inherit" || value === "currentColor" || value === "initial" || value === "unset") {
86
+ return { matches: true };
87
+ }
88
+ const normalized = normalizeColor(value);
89
+ if (!normalized) return { matches: true };
90
+ const tokenEntries = Object.entries(colorTokens);
91
+ for (const [name, tokenValue] of tokenEntries) {
92
+ const normalizedToken = normalizeColor(tokenValue);
93
+ if (normalizedToken === normalized) return { matches: true };
94
+ }
95
+ let closestName = "";
96
+ let closestDist = Infinity;
97
+ for (const [name, tokenValue] of tokenEntries) {
98
+ const nt = normalizeColor(tokenValue);
99
+ if (!nt) continue;
100
+ const dist = colorDistance(normalized, nt);
101
+ if (dist < closestDist) {
102
+ closestDist = dist;
103
+ closestName = name;
104
+ }
105
+ }
106
+ const suggestion = closestName ? `Use token {colors.${closestName}} ("${colorTokens[closestName]}")` : "Add this color to DESIGN.md colors section";
107
+ return { matches: false, suggestion };
108
+ }
109
+ function colorDistance(a, b) {
110
+ const ar = parseInt(a.slice(1, 3), 16);
111
+ const ag = parseInt(a.slice(3, 5), 16);
112
+ const ab = parseInt(a.slice(5, 7), 16);
113
+ const br = parseInt(b.slice(1, 3), 16);
114
+ const bg = parseInt(b.slice(3, 5), 16);
115
+ const bb = parseInt(b.slice(5, 7), 16);
116
+ return Math.sqrt((ar - br) ** 2 + (ag - bg) ** 2 + (ab - bb) ** 2);
117
+ }
118
+ function matchSpacing(value, spacingTokens) {
119
+ if (value.startsWith("var(") || value === "auto" || value === "0" || value === "inherit" || value === "initial" || value === "unset") {
120
+ return { matches: true };
121
+ }
122
+ const parts = value.split(/\s+/);
123
+ const tokenValues = new Set(Object.values(spacingTokens));
124
+ tokenValues.add("0");
125
+ for (const part of parts) {
126
+ if (part === "0" || part === "auto" || part.startsWith("var(")) continue;
127
+ if (!tokenValues.has(part)) {
128
+ return {
129
+ matches: false,
130
+ suggestion: `Use a DESIGN.md spacing token`
131
+ };
132
+ }
133
+ }
134
+ return { matches: true };
135
+ }
136
+ function matchRounded(value, roundedTokens) {
137
+ if (value.startsWith("var(") || value === "0" || value === "inherit" || value === "initial" || value === "unset") {
138
+ return { matches: true };
139
+ }
140
+ const tokenValues = new Set(Object.values(roundedTokens));
141
+ tokenValues.add("0");
142
+ if (!tokenValues.has(value)) {
143
+ return {
144
+ matches: false,
145
+ suggestion: `Use a DESIGN.md rounded token`
146
+ };
147
+ }
148
+ return { matches: true };
149
+ }
150
+ function matchTypographyProperty(property, value, typographyTokens) {
151
+ if (value.startsWith("var(") || value === "inherit" || value === "initial" || value === "unset") {
152
+ return { matches: true };
153
+ }
154
+ const tokenValues = /* @__PURE__ */ new Set();
155
+ for (const token of Object.values(typographyTokens)) {
156
+ if (property === "font-size" && token.fontSize) tokenValues.add(token.fontSize);
157
+ if (property === "font-weight" && token.fontWeight != null)
158
+ tokenValues.add(String(token.fontWeight));
159
+ if (property === "line-height" && token.lineHeight != null)
160
+ tokenValues.add(String(token.lineHeight));
161
+ if (property === "letter-spacing" && token.letterSpacing)
162
+ tokenValues.add(token.letterSpacing);
163
+ if (property === "font-family" && token.fontFamily) tokenValues.add(token.fontFamily);
164
+ }
165
+ if (tokenValues.size === 0) return { matches: true };
166
+ if (tokenValues.has(value)) return { matches: true };
167
+ if (property === "font-family") {
168
+ const normalizedValue = value.replace(/['"]/g, "").trim();
169
+ const primaryFont = normalizedValue.split(",")[0].trim();
170
+ for (const tv of tokenValues) {
171
+ if (tv.replace(/['"]/g, "").trim() === primaryFont) return { matches: true };
172
+ }
173
+ }
174
+ return {
175
+ matches: false,
176
+ suggestion: `Use a DESIGN.md typography token`
177
+ };
178
+ }
179
+ function matchAnimationDuration(value, animationTokens) {
180
+ if (value.startsWith("var(") || value === "0s" || value === "0ms" || value === "inherit" || value === "initial" || value === "unset") {
181
+ return { matches: true };
182
+ }
183
+ const durations = /* @__PURE__ */ new Set();
184
+ for (const preset of Object.values(animationTokens)) {
185
+ if (preset.duration) durations.add(preset.duration);
186
+ }
187
+ if (durations.size === 0) return { matches: true };
188
+ if (durations.has(value)) return { matches: true };
189
+ return {
190
+ matches: false,
191
+ suggestion: `Use a DESIGN.md animation preset duration`
192
+ };
193
+ }
194
+ function matchAnimationEasing(value, animationTokens) {
195
+ if (value.startsWith("var(") || value === "inherit" || value === "initial" || value === "unset") {
196
+ return { matches: true };
197
+ }
198
+ const easings = /* @__PURE__ */ new Set();
199
+ for (const preset of Object.values(animationTokens)) {
200
+ if (preset.easing) easings.add(preset.easing);
201
+ }
202
+ if (easings.size === 0) return { matches: true };
203
+ if (easings.has(value)) return { matches: true };
204
+ const normalizedValue = value.replace(/\s/g, "");
205
+ for (const e of easings) {
206
+ if (e.replace(/\s/g, "") === normalizedValue) return { matches: true };
207
+ }
208
+ return {
209
+ matches: false,
210
+ suggestion: `Use a DESIGN.md animation preset easing`
211
+ };
212
+ }
213
+ function matchComponent(elementStyles, componentSpec, rawSpec) {
214
+ const mismatches = [];
215
+ const propertyMapping = {
216
+ backgroundColor: "background-color",
217
+ textColor: "color",
218
+ rounded: "border-radius",
219
+ padding: "padding",
220
+ height: "height",
221
+ width: "width"
222
+ };
223
+ for (const [specProp, expectedValue] of Object.entries(componentSpec)) {
224
+ if (specProp === "typography") continue;
225
+ const cssProp = propertyMapping[specProp] || specProp;
226
+ const actualValue = elementStyles[cssProp];
227
+ if (actualValue && actualValue !== expectedValue) {
228
+ const rawValue = rawSpec?.[specProp];
229
+ mismatches.push({
230
+ cssProp,
231
+ expected: expectedValue,
232
+ expectedRaw: rawValue !== expectedValue ? rawValue : void 0,
233
+ actual: actualValue
234
+ });
235
+ }
236
+ }
237
+ return mismatches;
238
+ }
239
+ function formatComponentMismatches(componentName, mismatches, elementDescription) {
240
+ const specPath = `DESIGN.md components.${componentName}`;
241
+ const header = elementDescription ? `${specPath} on ${elementDescription}:` : `${specPath}:`;
242
+ const lines = [header];
243
+ for (let i = 0; i < mismatches.length; i++) {
244
+ const m = mismatches[i];
245
+ const expectedDisplay = m.expectedRaw ? `${m.expectedRaw} (${m.expected})` : `"${m.expected}"`;
246
+ lines.push(` ${i + 1}. ${m.cssProp} should be ${expectedDisplay}, found "${m.actual}"`);
247
+ }
248
+ return lines.join("\n");
249
+ }
250
+ function isBackgroundShorthand(property) {
251
+ return property === "background";
252
+ }
253
+ function splitTopLevelCommas(value) {
254
+ const parts = [];
255
+ let depth = 0;
256
+ let current = "";
257
+ for (const ch of value) {
258
+ if (ch === "(" || ch === "[") depth++;
259
+ else if (ch === ")" || ch === "]") depth--;
260
+ if (ch === "," && depth === 0) {
261
+ parts.push(current.trim());
262
+ current = "";
263
+ } else {
264
+ current += ch;
265
+ }
266
+ }
267
+ if (current.trim()) parts.push(current.trim());
268
+ return parts;
269
+ }
270
+ function extractBackgroundColors(value) {
271
+ if (value.startsWith("var(") || value === "transparent" || value === "none" || value === "inherit" || value === "initial" || value === "unset") {
272
+ return [];
273
+ }
274
+ const layers = splitTopLevelCommas(value);
275
+ const colors = [];
276
+ for (const layer of layers) {
277
+ if (/^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(layer)) {
278
+ continue;
279
+ }
280
+ const hexMatch = layer.match(/(^|\s)(#[0-9a-fA-F]{3,8})(\s|$)/);
281
+ if (hexMatch) {
282
+ colors.push(hexMatch[2]);
283
+ continue;
284
+ }
285
+ const rgbMatch = layer.match(/(^|\s)(rgba?\s*\([^)]+\))/);
286
+ if (rgbMatch) {
287
+ colors.push(rgbMatch[2]);
288
+ }
289
+ }
290
+ return colors;
291
+ }
292
+ function hexToRgbValues(hex) {
293
+ const normalized = normalizeHex(hex);
294
+ if (!normalized) return null;
295
+ return [
296
+ parseInt(normalized.slice(1, 3), 16),
297
+ parseInt(normalized.slice(3, 5), 16),
298
+ parseInt(normalized.slice(5, 7), 16)
299
+ ];
300
+ }
301
+ function relativeLuminance(r, g, b) {
302
+ const [rs, gs, bs] = [r / 255, g / 255, b / 255].map(
303
+ (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
304
+ );
305
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
306
+ }
307
+ function contrastRatio(l1, l2) {
308
+ const lighter = Math.max(l1, l2);
309
+ const darker = Math.min(l1, l2);
310
+ return (lighter + 0.05) / (darker + 0.05);
311
+ }
312
+ function extractFrontmatter(content) {
313
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
314
+ return match ? match[1] : null;
315
+ }
316
+ function resolveReferences(tokens) {
317
+ const resolve = (value) => {
318
+ return value.replace(/\{([^}]+)\}/g, (_, refPath) => {
319
+ const parts = refPath.split(".");
320
+ let current = tokens;
321
+ for (const part of parts) {
322
+ if (current == null || typeof current !== "object") return `{${refPath}}`;
323
+ current = current[part];
324
+ }
325
+ if (typeof current === "string") return resolve(current);
326
+ if (typeof current === "number") return String(current);
327
+ return `{${refPath}}`;
328
+ });
329
+ };
330
+ if (tokens.components) {
331
+ for (const [name, spec] of Object.entries(tokens.components)) {
332
+ for (const [prop, value] of Object.entries(spec)) {
333
+ if (typeof value === "string" && value.includes("{")) {
334
+ tokens.components[name][prop] = resolve(value);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ }
340
+ function normalizeStringValues(map) {
341
+ if (!map) return {};
342
+ const result = {};
343
+ for (const [key, value] of Object.entries(map)) {
344
+ result[key] = String(value);
345
+ }
346
+ return result;
347
+ }
348
+ function parseDesignMd(content) {
349
+ const frontmatter = extractFrontmatter(content);
350
+ if (!frontmatter) return null;
351
+ const raw = yaml.load(frontmatter);
352
+ if (!raw || typeof raw !== "object") return null;
353
+ const rawComponents = {};
354
+ if (raw.components) {
355
+ for (const [name, spec] of Object.entries(raw.components)) {
356
+ rawComponents[name] = { ...spec };
357
+ }
358
+ }
359
+ resolveReferences(raw);
360
+ return {
361
+ name: raw.name,
362
+ colors: raw.colors || {},
363
+ typography: raw.typography || {},
364
+ spacing: normalizeStringValues(raw.spacing),
365
+ rounded: normalizeStringValues(raw.rounded),
366
+ breakpoints: normalizeStringValues(raw.breakpoints),
367
+ animations: raw.animations || {},
368
+ components: raw.components || {},
369
+ rawComponents,
370
+ rules: raw.rules || {}
371
+ };
372
+ }
373
+ function findDesignMd(filePath, projectRoot) {
374
+ let dir = path__default.dirname(path__default.resolve(projectRoot, filePath));
375
+ const root = path__default.resolve(projectRoot);
376
+ while (dir.startsWith(root)) {
377
+ const candidate = path__default.join(dir, "DESIGN.md");
378
+ if (fs__default.existsSync(candidate)) {
379
+ const content = fs__default.readFileSync(candidate, "utf-8");
380
+ const tokens = parseDesignMd(content);
381
+ if (!tokens) return null;
382
+ const designMdPath = path__default.relative(projectRoot, candidate);
383
+ return { tokens, designMdPath };
384
+ }
385
+ const parent = path__default.dirname(dir);
386
+ if (parent === dir) break;
387
+ dir = parent;
388
+ }
389
+ return null;
390
+ }
391
+ export {
392
+ isAnimationEasingProperty as a,
393
+ isColorProperty as b,
394
+ isRadiusProperty as c,
395
+ isSpacingProperty as d,
396
+ isTypographyProperty as e,
397
+ findDesignMd as f,
398
+ matchAnimationEasing as g,
399
+ matchColor as h,
400
+ isAnimationDurationProperty as i,
401
+ matchComponent as j,
402
+ matchRounded as k,
403
+ matchSpacing as l,
404
+ matchAnimationDuration as m,
405
+ matchTypographyProperty as n,
406
+ isBackgroundShorthand as o,
407
+ parseDesignMd as p,
408
+ extractBackgroundColors as q,
409
+ formatComponentMismatches as r,
410
+ hexToRgbValues as s,
411
+ relativeLuminance as t,
412
+ contrastRatio as u
413
+ };
package/dist/tools.js CHANGED
@@ -1,94 +1,16 @@
1
1
  import postcss from "postcss";
2
- import yaml, { load, dump } from "js-yaml";
3
- import * as fs from "node:fs";
4
- import fs__default, { promises } from "node:fs";
5
- import * as path from "node:path";
6
- import path__default from "node:path";
2
+ import { f as findDesignMd, b as isColorProperty, h as matchColor, d as isSpacingProperty, l as matchSpacing, c as isRadiusProperty, k as matchRounded, e as isTypographyProperty, n as matchTypographyProperty, i as isAnimationDurationProperty, m as matchAnimationDuration, a as isAnimationEasingProperty, g as matchAnimationEasing, o as isBackgroundShorthand, q as extractBackgroundColors, j as matchComponent, r as formatComponentMismatches, s as hexToRgbValues, t as relativeLuminance, u as contrastRatio, p as parseDesignMd } from "./parse-design-md-C2uZEN8Y.js";
7
3
  import selectorParser from "postcss-selector-parser";
8
4
  import { selectorSpecificity } from "@csstools/selector-specificity";
9
5
  import { walkElements, JAY_EXTENSION } from "@jay-framework/compiler-shared";
6
+ import * as fs from "node:fs";
7
+ import { promises } from "node:fs";
8
+ import * as path from "node:path";
9
+ import path__default from "node:path";
10
+ import { load, dump } from "js-yaml";
10
11
  import { fileURLToPath } from "node:url";
11
12
  import { makeJayQuery, makeJayAction, makeJayStackComponent, phaseOutput } from "@jay-framework/fullstack-component";
12
13
  import { parseJayFile, JAY_IMPORT_RESOLVER } from "@jay-framework/compiler-jay-html";
13
- function extractFrontmatter(content) {
14
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
15
- return match ? match[1] : null;
16
- }
17
- function resolveReferences(tokens) {
18
- const resolve = (value) => {
19
- return value.replace(/\{([^}]+)\}/g, (_, refPath) => {
20
- const parts = refPath.split(".");
21
- let current = tokens;
22
- for (const part of parts) {
23
- if (current == null || typeof current !== "object") return `{${refPath}}`;
24
- current = current[part];
25
- }
26
- if (typeof current === "string") return resolve(current);
27
- if (typeof current === "number") return String(current);
28
- return `{${refPath}}`;
29
- });
30
- };
31
- if (tokens.components) {
32
- for (const [name, spec] of Object.entries(tokens.components)) {
33
- for (const [prop, value] of Object.entries(spec)) {
34
- if (typeof value === "string" && value.includes("{")) {
35
- tokens.components[name][prop] = resolve(value);
36
- }
37
- }
38
- }
39
- }
40
- }
41
- function normalizeStringValues(map) {
42
- if (!map) return {};
43
- const result = {};
44
- for (const [key, value] of Object.entries(map)) {
45
- result[key] = String(value);
46
- }
47
- return result;
48
- }
49
- function parseDesignMd(content) {
50
- const frontmatter = extractFrontmatter(content);
51
- if (!frontmatter) return null;
52
- const raw = yaml.load(frontmatter);
53
- if (!raw || typeof raw !== "object") return null;
54
- const rawComponents = {};
55
- if (raw.components) {
56
- for (const [name, spec] of Object.entries(raw.components)) {
57
- rawComponents[name] = { ...spec };
58
- }
59
- }
60
- resolveReferences(raw);
61
- return {
62
- name: raw.name,
63
- colors: raw.colors || {},
64
- typography: raw.typography || {},
65
- spacing: normalizeStringValues(raw.spacing),
66
- rounded: normalizeStringValues(raw.rounded),
67
- breakpoints: normalizeStringValues(raw.breakpoints),
68
- animations: raw.animations || {},
69
- components: raw.components || {},
70
- rawComponents,
71
- rules: raw.rules || {}
72
- };
73
- }
74
- function findDesignMd(filePath, projectRoot) {
75
- let dir = path__default.dirname(path__default.resolve(projectRoot, filePath));
76
- const root = path__default.resolve(projectRoot);
77
- while (dir.startsWith(root)) {
78
- const candidate = path__default.join(dir, "DESIGN.md");
79
- if (fs__default.existsSync(candidate)) {
80
- const content = fs__default.readFileSync(candidate, "utf-8");
81
- const tokens = parseDesignMd(content);
82
- if (!tokens) return null;
83
- const designMdPath = path__default.relative(projectRoot, candidate);
84
- return { tokens, designMdPath };
85
- }
86
- const parent = path__default.dirname(dir);
87
- if (parent === dir) break;
88
- dir = parent;
89
- }
90
- return null;
91
- }
92
14
  const ALLOW_COMMENT$1 = "design-system: allow";
93
15
  function computeSpecificity(selector) {
94
16
  let result = [0, 0, 0];
@@ -333,314 +255,6 @@ function resolveCascadeByBreakpoint(cssSources, root) {
333
255
  }
334
256
  return result;
335
257
  }
336
- function normalizeHex(color) {
337
- const hex = color.trim().toLowerCase();
338
- if (!/^#[0-9a-f]{3,8}$/.test(hex)) return null;
339
- if (hex.length === 4) {
340
- return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
341
- }
342
- return hex.length === 7 ? hex : null;
343
- }
344
- function parseRgb(value) {
345
- const match = value.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
346
- if (!match) return null;
347
- const r = parseInt(match[1], 10);
348
- const g = parseInt(match[2], 10);
349
- const b = parseInt(match[3], 10);
350
- return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
351
- }
352
- function normalizeColor(value) {
353
- const trimmed = value.trim().toLowerCase();
354
- if (trimmed.startsWith("#")) return normalizeHex(trimmed);
355
- if (trimmed.startsWith("rgb")) return parseRgb(trimmed);
356
- return null;
357
- }
358
- const COLOR_PROPERTIES = /* @__PURE__ */ new Set([
359
- "color",
360
- "background-color",
361
- "border-color",
362
- "border-top-color",
363
- "border-right-color",
364
- "border-bottom-color",
365
- "border-left-color",
366
- "outline-color",
367
- "text-decoration-color",
368
- "fill",
369
- "stroke"
370
- ]);
371
- const SPACING_PROPERTIES = /* @__PURE__ */ new Set([
372
- "padding",
373
- "padding-top",
374
- "padding-right",
375
- "padding-bottom",
376
- "padding-left",
377
- "margin",
378
- "margin-top",
379
- "margin-right",
380
- "margin-bottom",
381
- "margin-left",
382
- "gap",
383
- "row-gap",
384
- "column-gap",
385
- "top",
386
- "right",
387
- "bottom",
388
- "left"
389
- ]);
390
- const RADIUS_PROPERTIES = /* @__PURE__ */ new Set([
391
- "border-radius",
392
- "border-top-left-radius",
393
- "border-top-right-radius",
394
- "border-bottom-left-radius",
395
- "border-bottom-right-radius"
396
- ]);
397
- function isColorProperty(property) {
398
- return COLOR_PROPERTIES.has(property);
399
- }
400
- function isSpacingProperty(property) {
401
- return SPACING_PROPERTIES.has(property);
402
- }
403
- function isRadiusProperty(property) {
404
- return RADIUS_PROPERTIES.has(property);
405
- }
406
- function isTypographyProperty(property) {
407
- return property === "font-size" || property === "font-weight" || property === "line-height" || property === "letter-spacing" || property === "font-family";
408
- }
409
- function isAnimationDurationProperty(property) {
410
- return property === "transition-duration" || property === "animation-duration";
411
- }
412
- function isAnimationEasingProperty(property) {
413
- return property === "transition-timing-function" || property === "animation-timing-function";
414
- }
415
- function matchColor(value, colorTokens) {
416
- if (value.startsWith("var(")) return { matches: true };
417
- if (value === "transparent" || value === "inherit" || value === "currentColor" || value === "initial" || value === "unset") {
418
- return { matches: true };
419
- }
420
- const normalized = normalizeColor(value);
421
- if (!normalized) return { matches: true };
422
- const tokenEntries = Object.entries(colorTokens);
423
- for (const [name, tokenValue] of tokenEntries) {
424
- const normalizedToken = normalizeColor(tokenValue);
425
- if (normalizedToken === normalized) return { matches: true };
426
- }
427
- let closestName = "";
428
- let closestDist = Infinity;
429
- for (const [name, tokenValue] of tokenEntries) {
430
- const nt = normalizeColor(tokenValue);
431
- if (!nt) continue;
432
- const dist = colorDistance(normalized, nt);
433
- if (dist < closestDist) {
434
- closestDist = dist;
435
- closestName = name;
436
- }
437
- }
438
- const suggestion = closestName ? `Use token {colors.${closestName}} ("${colorTokens[closestName]}")` : "Add this color to DESIGN.md colors section";
439
- return { matches: false, suggestion };
440
- }
441
- function colorDistance(a, b) {
442
- const ar = parseInt(a.slice(1, 3), 16);
443
- const ag = parseInt(a.slice(3, 5), 16);
444
- const ab = parseInt(a.slice(5, 7), 16);
445
- const br = parseInt(b.slice(1, 3), 16);
446
- const bg = parseInt(b.slice(3, 5), 16);
447
- const bb = parseInt(b.slice(5, 7), 16);
448
- return Math.sqrt((ar - br) ** 2 + (ag - bg) ** 2 + (ab - bb) ** 2);
449
- }
450
- function matchSpacing(value, spacingTokens) {
451
- if (value.startsWith("var(") || value === "auto" || value === "0" || value === "inherit" || value === "initial" || value === "unset") {
452
- return { matches: true };
453
- }
454
- const parts = value.split(/\s+/);
455
- const tokenValues = new Set(Object.values(spacingTokens));
456
- tokenValues.add("0");
457
- for (const part of parts) {
458
- if (part === "0" || part === "auto" || part.startsWith("var(")) continue;
459
- if (!tokenValues.has(part)) {
460
- return {
461
- matches: false,
462
- suggestion: `Use a DESIGN.md spacing token`
463
- };
464
- }
465
- }
466
- return { matches: true };
467
- }
468
- function matchRounded(value, roundedTokens) {
469
- if (value.startsWith("var(") || value === "0" || value === "inherit" || value === "initial" || value === "unset") {
470
- return { matches: true };
471
- }
472
- const tokenValues = new Set(Object.values(roundedTokens));
473
- tokenValues.add("0");
474
- if (!tokenValues.has(value)) {
475
- return {
476
- matches: false,
477
- suggestion: `Use a DESIGN.md rounded token`
478
- };
479
- }
480
- return { matches: true };
481
- }
482
- function matchTypographyProperty(property, value, typographyTokens) {
483
- if (value.startsWith("var(") || value === "inherit" || value === "initial" || value === "unset") {
484
- return { matches: true };
485
- }
486
- const tokenValues = /* @__PURE__ */ new Set();
487
- for (const token of Object.values(typographyTokens)) {
488
- if (property === "font-size" && token.fontSize) tokenValues.add(token.fontSize);
489
- if (property === "font-weight" && token.fontWeight != null)
490
- tokenValues.add(String(token.fontWeight));
491
- if (property === "line-height" && token.lineHeight != null)
492
- tokenValues.add(String(token.lineHeight));
493
- if (property === "letter-spacing" && token.letterSpacing)
494
- tokenValues.add(token.letterSpacing);
495
- if (property === "font-family" && token.fontFamily) tokenValues.add(token.fontFamily);
496
- }
497
- if (tokenValues.size === 0) return { matches: true };
498
- if (tokenValues.has(value)) return { matches: true };
499
- if (property === "font-family") {
500
- const normalizedValue = value.replace(/['"]/g, "").trim();
501
- const primaryFont = normalizedValue.split(",")[0].trim();
502
- for (const tv of tokenValues) {
503
- if (tv.replace(/['"]/g, "").trim() === primaryFont) return { matches: true };
504
- }
505
- }
506
- return {
507
- matches: false,
508
- suggestion: `Use a DESIGN.md typography token`
509
- };
510
- }
511
- function matchAnimationDuration(value, animationTokens) {
512
- if (value.startsWith("var(") || value === "0s" || value === "0ms" || value === "inherit" || value === "initial" || value === "unset") {
513
- return { matches: true };
514
- }
515
- const durations = /* @__PURE__ */ new Set();
516
- for (const preset of Object.values(animationTokens)) {
517
- if (preset.duration) durations.add(preset.duration);
518
- }
519
- if (durations.size === 0) return { matches: true };
520
- if (durations.has(value)) return { matches: true };
521
- return {
522
- matches: false,
523
- suggestion: `Use a DESIGN.md animation preset duration`
524
- };
525
- }
526
- function matchAnimationEasing(value, animationTokens) {
527
- if (value.startsWith("var(") || value === "inherit" || value === "initial" || value === "unset") {
528
- return { matches: true };
529
- }
530
- const easings = /* @__PURE__ */ new Set();
531
- for (const preset of Object.values(animationTokens)) {
532
- if (preset.easing) easings.add(preset.easing);
533
- }
534
- if (easings.size === 0) return { matches: true };
535
- if (easings.has(value)) return { matches: true };
536
- const normalizedValue = value.replace(/\s/g, "");
537
- for (const e of easings) {
538
- if (e.replace(/\s/g, "") === normalizedValue) return { matches: true };
539
- }
540
- return {
541
- matches: false,
542
- suggestion: `Use a DESIGN.md animation preset easing`
543
- };
544
- }
545
- function matchComponent(elementStyles, componentSpec, rawSpec) {
546
- const mismatches = [];
547
- const propertyMapping = {
548
- backgroundColor: "background-color",
549
- textColor: "color",
550
- rounded: "border-radius",
551
- padding: "padding",
552
- height: "height",
553
- width: "width"
554
- };
555
- for (const [specProp, expectedValue] of Object.entries(componentSpec)) {
556
- if (specProp === "typography") continue;
557
- const cssProp = propertyMapping[specProp] || specProp;
558
- const actualValue = elementStyles[cssProp];
559
- if (actualValue && actualValue !== expectedValue) {
560
- const rawValue = rawSpec?.[specProp];
561
- mismatches.push({
562
- cssProp,
563
- expected: expectedValue,
564
- expectedRaw: rawValue !== expectedValue ? rawValue : void 0,
565
- actual: actualValue
566
- });
567
- }
568
- }
569
- return mismatches;
570
- }
571
- function formatComponentMismatches(componentName, mismatches, elementDescription) {
572
- const specPath = `DESIGN.md components.${componentName}`;
573
- const header = elementDescription ? `${specPath} on ${elementDescription}:` : `${specPath}:`;
574
- const lines = [header];
575
- for (let i = 0; i < mismatches.length; i++) {
576
- const m = mismatches[i];
577
- const expectedDisplay = m.expectedRaw ? `${m.expectedRaw} (${m.expected})` : `"${m.expected}"`;
578
- lines.push(` ${i + 1}. ${m.cssProp} should be ${expectedDisplay}, found "${m.actual}"`);
579
- }
580
- return lines.join("\n");
581
- }
582
- function isBackgroundShorthand(property) {
583
- return property === "background";
584
- }
585
- function splitTopLevelCommas(value) {
586
- const parts = [];
587
- let depth = 0;
588
- let current = "";
589
- for (const ch of value) {
590
- if (ch === "(" || ch === "[") depth++;
591
- else if (ch === ")" || ch === "]") depth--;
592
- if (ch === "," && depth === 0) {
593
- parts.push(current.trim());
594
- current = "";
595
- } else {
596
- current += ch;
597
- }
598
- }
599
- if (current.trim()) parts.push(current.trim());
600
- return parts;
601
- }
602
- function extractBackgroundColors(value) {
603
- if (value.startsWith("var(") || value === "transparent" || value === "none" || value === "inherit" || value === "initial" || value === "unset") {
604
- return [];
605
- }
606
- const layers = splitTopLevelCommas(value);
607
- const colors = [];
608
- for (const layer of layers) {
609
- if (/^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(layer)) {
610
- continue;
611
- }
612
- const hexMatch = layer.match(/(^|\s)(#[0-9a-fA-F]{3,8})(\s|$)/);
613
- if (hexMatch) {
614
- colors.push(hexMatch[2]);
615
- continue;
616
- }
617
- const rgbMatch = layer.match(/(^|\s)(rgba?\s*\([^)]+\))/);
618
- if (rgbMatch) {
619
- colors.push(rgbMatch[2]);
620
- }
621
- }
622
- return colors;
623
- }
624
- function hexToRgbValues(hex) {
625
- const normalized = normalizeHex(hex);
626
- if (!normalized) return null;
627
- return [
628
- parseInt(normalized.slice(1, 3), 16),
629
- parseInt(normalized.slice(3, 5), 16),
630
- parseInt(normalized.slice(5, 7), 16)
631
- ];
632
- }
633
- function relativeLuminance(r, g, b) {
634
- const [rs, gs, bs] = [r / 255, g / 255, b / 255].map(
635
- (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
636
- );
637
- return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
638
- }
639
- function contrastRatio(l1, l2) {
640
- const lighter = Math.max(l1, l2);
641
- const darker = Math.min(l1, l2);
642
- return (lighter + 0.05) / (darker + 0.05);
643
- }
644
258
  const GUIDE_SUGGESTION$2 = "See design-system-validator agent-kit/designer/design-system.md for usage guide";
645
259
  function elementHint(el) {
646
260
  const tag = el.rawTagName?.toLowerCase() || "element";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/design-system-validator",
3
- "version": "0.25.4",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "description": "Design system validation plugin for Jay Framework — validates jay-html CSS against DESIGN.md tokens",
6
6
  "license": "Apache-2.0",
@@ -10,10 +10,18 @@
10
10
  "plugin.yaml",
11
11
  "agent-kit"
12
12
  ],
13
+ "typesVersions": {
14
+ "*": {
15
+ "design-tokens": [
16
+ "dist/design-tokens.d.ts"
17
+ ]
18
+ }
19
+ },
13
20
  "exports": {
14
21
  ".": "./dist/index.js",
15
22
  "./client": "./dist/index.client.js",
16
23
  "./tools": "./dist/tools.js",
24
+ "./design-tokens": "./dist/design-tokens.js",
17
25
  "./plugin.yaml": "./plugin.yaml",
18
26
  "./dist/pages/settings/page.jay-html": "./dist/pages/settings/page.jay-html",
19
27
  "./font-fallback.jay-action": "./dist/actions/font-fallback.jay-action"
@@ -26,7 +34,7 @@
26
34
  "build:client": "vite build",
27
35
  "validate": "jay-stack-cli validate-plugin",
28
36
  "build:server": "vite build --ssr",
29
- "build:types": "tsup lib/index.ts lib/index.client.ts lib/tools.ts --dts-only --format esm",
37
+ "build:types": "tsup lib/index.ts lib/index.client.ts lib/tools.ts lib/design-tokens.ts --dts-only --format esm",
30
38
  "build:check-types": "tsc",
31
39
  "clean": "rimraf dist",
32
40
  "confirm": "npm run clean && npm run build && npm run test",
@@ -37,32 +45,32 @@
37
45
  "@capsizecss/metrics": "^4.2.0",
38
46
  "@capsizecss/unpack": "^4.0.1",
39
47
  "@csstools/selector-specificity": "^5.0.0",
40
- "@jay-framework/component": "^0.25.4",
41
- "@jay-framework/fullstack-component": "^0.25.4",
42
- "@jay-framework/plugin-validator": "^0.25.4",
43
- "@jay-framework/stack-server-runtime": "^0.25.4",
48
+ "@jay-framework/component": "^0.26.0",
49
+ "@jay-framework/fullstack-component": "^0.26.0",
50
+ "@jay-framework/plugin-validator": "^0.26.0",
51
+ "@jay-framework/stack-server-runtime": "^0.26.0",
44
52
  "js-yaml": "^4.1.0",
45
53
  "postcss": "^8.5.6",
46
54
  "postcss-selector-parser": "^7.1.0"
47
55
  },
48
56
  "peerDependencies": {
49
- "@jay-framework/compiler-jay-html": "^0.25.4",
50
- "@jay-framework/compiler-shared": "^0.25.4"
57
+ "@jay-framework/compiler-jay-html": "^0.26.0",
58
+ "@jay-framework/compiler-shared": "^0.26.0"
51
59
  },
52
60
  "devDependencies": {
53
- "@jay-framework/compiler-jay-html": "^0.25.4",
54
- "@jay-framework/compiler-jay-stack": "^0.25.4",
55
- "@jay-framework/compiler-shared": "^0.25.4",
56
- "@jay-framework/dev-environment": "^0.25.4",
57
- "@jay-framework/jay-cli": "^0.25.4",
58
- "@jay-framework/jay-stack-cli": "^0.25.4",
61
+ "@jay-framework/compiler-jay-html": "^0.26.0",
62
+ "@jay-framework/compiler-jay-stack": "^0.26.0",
63
+ "@jay-framework/compiler-shared": "^0.26.0",
64
+ "@jay-framework/dev-environment": "^0.26.0",
65
+ "@jay-framework/jay-cli": "^0.26.0",
66
+ "@jay-framework/jay-stack-cli": "^0.26.0",
59
67
  "@types/js-yaml": "^4.0.9",
60
68
  "@types/node": "^22.15.21",
61
69
  "node-html-parser": "^6.1.0",
62
70
  "rimraf": "^5.0.5",
63
71
  "tsup": "^8.0.1",
64
72
  "typescript": "^5.3.3",
65
- "vite": "^5.0.11",
66
- "vitest": "^1.2.1"
73
+ "vite": "^6.4.4",
74
+ "vitest": "^4.1.11"
67
75
  }
68
76
  }