release: WRNexusJS 0.4.0
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
export interface CssTokenAudit {
|
||||
declared: string[];
|
||||
used: string[];
|
||||
missing: string[];
|
||||
unused: string[];
|
||||
}
|
||||
|
||||
/** Audit framework design-token declarations and var() references. */
|
||||
export function auditWireTokens(css: string): CssTokenAudit {
|
||||
const declared = new Set<string>();
|
||||
const used = new Set<string>();
|
||||
for (const match of css.matchAll(/(--wire-[A-Za-z0-9_-]+)\s*:/g)) declared.add(match[1]!);
|
||||
for (const match of css.matchAll(/var\(\s*(--wire-[A-Za-z0-9_-]+)/g)) used.add(match[1]!);
|
||||
return {
|
||||
declared: [...declared].sort(),
|
||||
used: [...used].sort(),
|
||||
missing: [...used].filter((token) => !declared.has(token)).sort(),
|
||||
unused: [...declared].filter((token) => !used.has(token)).sort(),
|
||||
};
|
||||
}
|
||||
|
||||
export interface StyleSource {
|
||||
path: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** Normalize/dedupe Tailwind scan sources without allowing line injection. */
|
||||
export function normalizeStyleSources(values: readonly (string | StyleSource)[]): StyleSource[] {
|
||||
const result = new Map<string, StyleSource>();
|
||||
for (const value of values) {
|
||||
const source = typeof value === "string" ? { path: value } : value;
|
||||
const path = source.path.trim();
|
||||
if (!path || /[\r\n]/.test(path)) continue;
|
||||
result.set(path, { path, reason: source.reason });
|
||||
}
|
||||
return [...result.values()];
|
||||
}
|
||||
|
||||
export function tailwindSourceDirectives(values: readonly (string | StyleSource)[]): string {
|
||||
return normalizeStyleSources(values)
|
||||
.map(({ path }) => `@source ${JSON.stringify(path)};`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export interface ContrastResult {
|
||||
ratio: number;
|
||||
level: "fail" | "aa-large" | "aa" | "aaa";
|
||||
}
|
||||
|
||||
function hexRgb(value: string): [number, number, number] | null {
|
||||
const hex = value.trim().replace(/^#/, "");
|
||||
const normalized = hex.length === 3 ? [...hex].map((part) => part + part).join("") : hex;
|
||||
if (!/^[0-9a-f]{6}$/i.test(normalized)) return null;
|
||||
return [0, 2, 4].map((offset) => Number.parseInt(normalized.slice(offset, offset + 2), 16)) as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
];
|
||||
}
|
||||
|
||||
function luminance(value: string): number | null {
|
||||
const rgb = hexRgb(value);
|
||||
if (!rgb) return null;
|
||||
const channels = rgb.map((channel) => {
|
||||
const scaled = channel / 255;
|
||||
return scaled <= 0.03928 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return 0.2126 * channels[0]! + 0.7152 * channels[1]! + 0.0722 * channels[2]!;
|
||||
}
|
||||
|
||||
export function contrast(foreground: string, background: string): ContrastResult | null {
|
||||
const left = luminance(foreground);
|
||||
const right = luminance(background);
|
||||
if (left === null || right === null) return null;
|
||||
const ratio = (Math.max(left, right) + 0.05) / (Math.min(left, right) + 0.05);
|
||||
return {
|
||||
ratio: Math.round(ratio * 100) / 100,
|
||||
level: ratio >= 7 ? "aaa" : ratio >= 4.5 ? "aa" : ratio >= 3 ? "aa-large" : "fail",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user