@domandigital/craft 0.10.0 → 0.12.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/CHANGELOG.md +35 -0
- package/CHARACTER.md +9 -1
- package/COPY.md +6 -2
- package/README.md +40 -0
- package/dist/{audit-EGDNWSQB.js → audit-5QWFLZ6L.js} +6 -3
- package/dist/audit.cjs +279 -16
- package/dist/audit.d.cts +81 -4
- package/dist/audit.d.ts +81 -4
- package/dist/audit.js +278 -16
- package/dist/{chunk-3NGWXZZE.js → chunk-AL6MRG6V.js} +40 -3
- package/dist/{chunk-2NQFIDLE.js → chunk-CDAJUS5U.js} +13 -11
- package/dist/{chunk-JKIDWHG3.js → chunk-J3QUROJ5.js} +299 -32
- package/dist/chunk-JYNXGCYZ.js +75 -0
- package/dist/{chunk-APO4BT2F.js → chunk-LHGY43LL.js} +11 -7
- package/dist/chunk-PU5G5DUC.js +206 -0
- package/dist/{chunk-QMYERCBB.js → chunk-PZ4MF2VV.js} +19 -7
- package/dist/{chunk-JWHC67ZS.js → chunk-S3NZAVU6.js} +121 -1
- package/dist/{chunk-TSKRBIOC.js → chunk-ULPI335S.js} +181 -13
- package/dist/chunk-ZKVJ7K6X.js +19 -0
- package/dist/cli-2DY3V3SF.js +17 -0
- package/dist/{cli-PRWTNFVP.js → cli-C3JLFXMG.js} +5 -4
- package/dist/cli-QM2AQQMF.js +19 -0
- package/dist/{cli-ADQQURNR.js → cli-ZB2VJ7CY.js} +25 -17
- package/dist/{cli-2WBGTBKD.js → cli-ZPJIWVEP.js} +15 -80
- package/dist/cli.js +2 -2
- package/dist/index.cjs +486 -9
- package/dist/index.d.cts +364 -6
- package/dist/index.d.ts +364 -6
- package/dist/index.js +462 -9
- package/licences.json +104 -0
- package/package.json +15 -7
- package/word-lists.json +1 -1
- package/dist/chunk-U2JTFQKO.js +0 -112
- package/dist/cli-EK3GSAOW.js +0 -15
- package/dist/cli-VQUNNIZM.js +0 -16
package/dist/audit.js
CHANGED
|
@@ -1535,6 +1535,7 @@ var REFLEX_FONTS_2 = [
|
|
|
1535
1535
|
"Plus Jakarta Sans",
|
|
1536
1536
|
"Outfit",
|
|
1537
1537
|
"Geist",
|
|
1538
|
+
"Cal Sans",
|
|
1538
1539
|
"Sora",
|
|
1539
1540
|
"Instrument Sans",
|
|
1540
1541
|
"Instrument Serif",
|
|
@@ -1756,7 +1757,7 @@ function iconTiles(file) {
|
|
|
1756
1757
|
return sized && has(unit, /^rounded-(?:md|lg|xl|2xl|full)$/) && has(unit, /^(?:items-center|place-items-center|place-content-center)$/) && has(unit, /^bg-/);
|
|
1757
1758
|
});
|
|
1758
1759
|
}
|
|
1759
|
-
var ICON_LIBRARY = /from\s+["'](?:lucide-react|@heroicons\/react[^"']*|react-icons[^"']*|@phosphor-icons\/react|@tabler\/icons-react|@radix-ui\/react-icons)["']/;
|
|
1760
|
+
var ICON_LIBRARY = /from\s+["'](?:lucide-react|@heroicons\/react[^"']*|react-icons[^"']*|@phosphor-icons\/react|@tabler\/icons-react|@radix-ui\/react-icons|@iconscout\/react-unicons[^"']*|@mui\/icons-material[^"']*|react-material-symbols[^"']*)["']/;
|
|
1760
1761
|
function iconTileStackHits(ctx) {
|
|
1761
1762
|
const hits = [];
|
|
1762
1763
|
for (const file of ctx.files) {
|
|
@@ -1903,6 +1904,12 @@ function shadcnHits(ctx) {
|
|
|
1903
1904
|
const detail = unused.length > 0 ? `; ${unused.length} imported nowhere` : "";
|
|
1904
1905
|
return [{ path: ui[0].path, offset: 0, message: `${ui.length} stock shadcn primitives${detail}` }];
|
|
1905
1906
|
}
|
|
1907
|
+
function shadcnCardHits(ctx) {
|
|
1908
|
+
return eachUnit(
|
|
1909
|
+
ctx,
|
|
1910
|
+
(unit) => has(unit, /^bg-card$/) && has(unit, /^text-card-foreground$/) && has(unit, /^border$/) && has(unit, /^rounded-(?:lg|xl)$/) && has(unit, /^shadow(?:-sm)?$/) ? "the registry Card recipe, unchanged" : null
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1906
1913
|
function marqueeHits(ctx) {
|
|
1907
1914
|
const hits = [];
|
|
1908
1915
|
const pattern = /@keyframes\s+[\w-]*marquee|animate-(?:marquee|scroll-x|infinite-scroll)\b|<Marquee\b|["']react-fast-marquee["']|ui\/marquee["']|animation(?:-name)?\s*:\s*[^;]*marquee/i;
|
|
@@ -2120,6 +2127,23 @@ const inter = Inter({ subsets: ["latin"] });`),
|
|
|
2120
2127
|
pass: [f3("src/components/ui/button.tsx", `export function Button() { return null; }`)]
|
|
2121
2128
|
}
|
|
2122
2129
|
},
|
|
2130
|
+
{
|
|
2131
|
+
id: "shadcn-card-stock",
|
|
2132
|
+
name: "Stock shadcn Card",
|
|
2133
|
+
generation: 1,
|
|
2134
|
+
severity: "warn",
|
|
2135
|
+
surface: "source",
|
|
2136
|
+
why: "The registry Card (bg-card, a plain border, rounded-lg or rounded-xl, shadow-sm) is the container every generated page is built from. Left as shipped, every card on the site looks like every other shadcn site's.",
|
|
2137
|
+
fix: "Give the card this site's radius and shadow from the restraint budget, or drop the shadow and let a visible border carry it.",
|
|
2138
|
+
detect: shadcnCardHits,
|
|
2139
|
+
fixtures: {
|
|
2140
|
+
flag: [
|
|
2141
|
+
f3("components/ui/card.tsx", `className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}`),
|
|
2142
|
+
f3("components/ui/card.tsx", `className={cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className)}`)
|
|
2143
|
+
],
|
|
2144
|
+
pass: [f3("components/ui/card.tsx", `className={cn("rounded-sm border-2 border-ink bg-card text-card-foreground", className)}`)]
|
|
2145
|
+
}
|
|
2146
|
+
},
|
|
2123
2147
|
{
|
|
2124
2148
|
id: "cream-palette",
|
|
2125
2149
|
name: "Cream ground",
|
|
@@ -2363,7 +2387,7 @@ function excerptAt(text, offset, max = 140) {
|
|
|
2363
2387
|
}
|
|
2364
2388
|
|
|
2365
2389
|
// src/snapshot/types.ts
|
|
2366
|
-
var SNAPSHOT_VERSION =
|
|
2390
|
+
var SNAPSHOT_VERSION = 2;
|
|
2367
2391
|
|
|
2368
2392
|
// src/snapshot/fixture.ts
|
|
2369
2393
|
function makeSnapshot(patch = {}) {
|
|
@@ -2697,7 +2721,7 @@ var RENDERED_PATHS = {
|
|
|
2697
2721
|
};
|
|
2698
2722
|
|
|
2699
2723
|
// src/character/check.ts
|
|
2700
|
-
var CATALOGUE_VERSION = "2026.09.
|
|
2724
|
+
var CATALOGUE_VERSION = "2026.09.9";
|
|
2701
2725
|
var CATALOGUE = [...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map(
|
|
2702
2726
|
(t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t
|
|
2703
2727
|
);
|
|
@@ -2795,7 +2819,7 @@ function auditSnapshot(snapshot, options = {}) {
|
|
|
2795
2819
|
}
|
|
2796
2820
|
|
|
2797
2821
|
// src/fingerprint/index.ts
|
|
2798
|
-
var FINGERPRINT_VERSION =
|
|
2822
|
+
var FINGERPRINT_VERSION = 2;
|
|
2799
2823
|
var CHROMATIC = 0.03;
|
|
2800
2824
|
var MIN_ACCENT_AREA = 2e4;
|
|
2801
2825
|
var oklch = (value) => {
|
|
@@ -2850,7 +2874,8 @@ function fingerprint(s) {
|
|
|
2850
2874
|
roundness: median(s.controls.map((c) => Math.min(0.5, c.radiusPx / Math.max(1, c.heightPx)))),
|
|
2851
2875
|
motion: below ? Math.min(1, s.motion.hiddenSections / below) : 0,
|
|
2852
2876
|
effects: effects.sort(),
|
|
2853
|
-
layout: s.sections.map((x) => x.kind)
|
|
2877
|
+
layout: s.sections.map((x) => x.kind),
|
|
2878
|
+
...s.sections.length > 0 && s.sections.every((x) => x.role && x.geometry) ? { sections: s.sections.map((x) => ({ role: x.role, centred: x.geometry.centredShare, width: x.geometry.contentWidthRatio })) } : {}
|
|
2854
2879
|
};
|
|
2855
2880
|
}
|
|
2856
2881
|
|
|
@@ -2951,18 +2976,26 @@ function collectInPage() {
|
|
|
2951
2976
|
top: Math.round(box(h).top)
|
|
2952
2977
|
};
|
|
2953
2978
|
});
|
|
2979
|
+
const isButtonEl = (el) => el.tagName === "BUTTON" || el.tagName === "INPUT" && /^(submit|button)$/i.test(el.type);
|
|
2980
|
+
const isControl = (el) => {
|
|
2981
|
+
const isButton = isButtonEl(el);
|
|
2982
|
+
if (!isButton && el.tagName !== "A") return false;
|
|
2983
|
+
if (!shown(el)) return false;
|
|
2984
|
+
const cs = style(el);
|
|
2985
|
+
const b = box(el);
|
|
2986
|
+
const label = (el.textContent ?? el.value ?? "").replace(/\s+/g, " ").trim();
|
|
2987
|
+
if (!label || b.height < 24 || b.height > 90) return false;
|
|
2988
|
+
if (!isButton && !(painted(cs.backgroundColor) || px(cs.borderTopWidth) >= 1)) return false;
|
|
2989
|
+
if (!isButton && px(cs.paddingLeft) < 8) return false;
|
|
2990
|
+
return true;
|
|
2991
|
+
};
|
|
2954
2992
|
const controls = [];
|
|
2955
2993
|
for (const el of all) {
|
|
2956
|
-
|
|
2957
|
-
const isButton =
|
|
2958
|
-
if (!isButton && tag !== "A") continue;
|
|
2959
|
-
if (!shown(el)) continue;
|
|
2994
|
+
if (!isControl(el)) continue;
|
|
2995
|
+
const isButton = isButtonEl(el);
|
|
2960
2996
|
const cs = style(el);
|
|
2961
2997
|
const b = box(el);
|
|
2962
2998
|
const label = (el.textContent ?? el.value ?? "").replace(/\s+/g, " ").trim();
|
|
2963
|
-
if (!label || b.height < 24 || b.height > 90) continue;
|
|
2964
|
-
if (!isButton && !(painted(cs.backgroundColor) || px(cs.borderTopWidth) >= 1)) continue;
|
|
2965
|
-
if (!isButton && px(cs.paddingLeft) < 8) continue;
|
|
2966
2999
|
const r = cs.borderTopLeftRadius;
|
|
2967
3000
|
const radius = r.endsWith("%") ? parseFloat(r) / 100 * b.height : px(r);
|
|
2968
3001
|
controls.push({
|
|
@@ -3042,7 +3075,80 @@ function collectInPage() {
|
|
|
3042
3075
|
}
|
|
3043
3076
|
return best;
|
|
3044
3077
|
};
|
|
3045
|
-
const
|
|
3078
|
+
const backgroundOf = (el) => {
|
|
3079
|
+
for (let n = el; n && n !== document.documentElement; n = n.parentElement) {
|
|
3080
|
+
const c = style(n).backgroundColor;
|
|
3081
|
+
if (painted(c)) return c;
|
|
3082
|
+
}
|
|
3083
|
+
return ground;
|
|
3084
|
+
};
|
|
3085
|
+
const geometryOf = (el, b) => {
|
|
3086
|
+
const leaves = Array.from(el.querySelectorAll("*")).slice(0, 1500).filter((n) => (ownText(n).length > 1 || /^(IMG|SVG|VIDEO|PICTURE|BUTTON|INPUT|TEXTAREA|SELECT)$/i.test(n.tagName)) && shown(n)).slice(0, 400);
|
|
3087
|
+
const centreX = b.left + b.width / 2;
|
|
3088
|
+
let left = 0;
|
|
3089
|
+
let right = 0;
|
|
3090
|
+
let covered = 0;
|
|
3091
|
+
let minX = Infinity;
|
|
3092
|
+
let maxX = -Infinity;
|
|
3093
|
+
let textBlocks = 0;
|
|
3094
|
+
let centred = 0;
|
|
3095
|
+
for (const n of leaves) {
|
|
3096
|
+
const r = box(n);
|
|
3097
|
+
const area = r.width * r.height;
|
|
3098
|
+
covered += area;
|
|
3099
|
+
minX = Math.min(minX, r.left);
|
|
3100
|
+
maxX = Math.max(maxX, r.left + r.width);
|
|
3101
|
+
const split = Math.max(0, Math.min(r.width, centreX - r.left));
|
|
3102
|
+
left += split * r.height;
|
|
3103
|
+
right += (r.width - split) * r.height;
|
|
3104
|
+
if (ownText(n).length > 1) {
|
|
3105
|
+
textBlocks += 1;
|
|
3106
|
+
const mid = r.left + r.width / 2;
|
|
3107
|
+
if (style(n).textAlign === "center" || Math.abs(mid - centreX) < vw * 0.03 && r.width < b.width * 0.8) centred += 1;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
const round = (v) => Math.round(v * 1e3) / 1e3;
|
|
3111
|
+
return {
|
|
3112
|
+
centredShare: textBlocks ? round(centred / textBlocks) : 0,
|
|
3113
|
+
mirrorSymmetry: left + right > 0 ? round(1 - Math.abs(left - right) / (left + right)) : 1,
|
|
3114
|
+
whitespaceRatio: round(Math.max(0, 1 - Math.min(1, covered / Math.max(1, b.width * b.height)))),
|
|
3115
|
+
contentWidthRatio: leaves.length ? round(Math.min(1, (maxX - minX) / vw)) : 0,
|
|
3116
|
+
background: backgroundOf(el),
|
|
3117
|
+
controls: Array.from(el.querySelectorAll("a, button, input")).slice(0, 200).filter(isControl).length
|
|
3118
|
+
};
|
|
3119
|
+
};
|
|
3120
|
+
const CURRENCY = /[£$€]\s?\d/;
|
|
3121
|
+
const roleOf = (el, kind, geo, b, cards, iconCards, last) => {
|
|
3122
|
+
if (kind === "marquee" || kind === "hero") return kind;
|
|
3123
|
+
const heading = (el.querySelector("h1, h2, h3")?.textContent ?? "").toLowerCase();
|
|
3124
|
+
const leaves = Array.from(el.querySelectorAll("*")).slice(0, 1500);
|
|
3125
|
+
const own = leaves.map(ownText);
|
|
3126
|
+
const questions = own.filter((t) => /\?$/.test(t) && t.length > 8).length;
|
|
3127
|
+
if (el.querySelectorAll("details").length >= 3 || el.querySelectorAll("[aria-expanded]").length >= 3 || /\bfaq|frequently asked|questions\b/.test(heading) && questions >= 3) return "faq";
|
|
3128
|
+
const prices = own.filter((t) => CURRENCY.test(t) && t.length < 40).length;
|
|
3129
|
+
if (prices >= 2 && (cards >= 2 || /pric|plan|package/.test(heading))) return "pricing";
|
|
3130
|
+
const quotes = el.querySelectorAll("blockquote, q").length;
|
|
3131
|
+
if (quotes >= 2 || /testimonial|review|what (?:our )?(?:clients|customers|people) say/.test(heading)) return "testimonials";
|
|
3132
|
+
const steps = Math.max(0, ...Array.from(el.querySelectorAll("ol")).map((ol) => ol.children.length));
|
|
3133
|
+
const numbered = own.filter((t) => /^(?:step\s*)?0?[1-9][.:]?$/i.test(t)).length;
|
|
3134
|
+
if (steps >= 3 || numbered >= 3 || /how it works|our process|\bsteps?\b/.test(heading)) return "process";
|
|
3135
|
+
const form = el.querySelector("form");
|
|
3136
|
+
const fields = form ? form.querySelectorAll("input:not([type=hidden]):not([type=submit]), textarea, select").length : 0;
|
|
3137
|
+
if (fields >= 2 || /contact|get in touch/.test(heading)) return "contact";
|
|
3138
|
+
const portraits = Array.from(el.querySelectorAll("img")).filter((i) => {
|
|
3139
|
+
const r = box(i);
|
|
3140
|
+
return r.width >= 80 && r.width <= 420 && Math.abs(r.width - r.height) < r.width * 0.25;
|
|
3141
|
+
}).length;
|
|
3142
|
+
if (portraits >= 3 && /team|people|meet|who we are/.test(heading)) return "team";
|
|
3143
|
+
if (kind === "stats" || kind === "logos") return kind;
|
|
3144
|
+
const words = (el.textContent ?? "").replace(/\s+/g, " ").trim().length;
|
|
3145
|
+
const setApart = geo.background !== ground || geo.centredShare >= 0.6;
|
|
3146
|
+
if (setApart && heading && b.height <= vh * 0.9 && geo.controls >= 1 && geo.controls <= 2 && words < 400 && cards < 3) return last ? "footer-cta" : "cta-band";
|
|
3147
|
+
if (cards >= 3 && iconCards >= 2) return "features";
|
|
3148
|
+
return kind;
|
|
3149
|
+
};
|
|
3150
|
+
const kept = blocks.slice(0, 40);
|
|
3151
|
+
const sections = kept.map((el, index) => {
|
|
3046
3152
|
const b = box(el);
|
|
3047
3153
|
const heading = el.querySelector("h1, h2, h3");
|
|
3048
3154
|
const label = (heading?.textContent ?? el.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 60);
|
|
@@ -3056,6 +3162,7 @@ function collectInPage() {
|
|
|
3056
3162
|
else if (images.length >= 4 && textChars < 300) kind = "logos";
|
|
3057
3163
|
else if (numbers >= 3 && textChars < 600) kind = "stats";
|
|
3058
3164
|
else if (cards >= 3) kind = "cards";
|
|
3165
|
+
const geometry = geometryOf(el, b);
|
|
3059
3166
|
return {
|
|
3060
3167
|
top: Math.round(b.top),
|
|
3061
3168
|
height: Math.round(b.height),
|
|
@@ -3063,7 +3170,9 @@ function collectInPage() {
|
|
|
3063
3170
|
label,
|
|
3064
3171
|
cards,
|
|
3065
3172
|
iconCards,
|
|
3066
|
-
hiddenAtLoad: b.top > vh && hiddenAtLoad(el)
|
|
3173
|
+
hiddenAtLoad: b.top > vh && hiddenAtLoad(el),
|
|
3174
|
+
role: roleOf(el, kind, geometry, b, cards, iconCards, index === kept.length - 1),
|
|
3175
|
+
geometry
|
|
3067
3176
|
};
|
|
3068
3177
|
});
|
|
3069
3178
|
if (header && sections.length === 0) {
|
|
@@ -3156,6 +3265,13 @@ function collectInPage() {
|
|
|
3156
3265
|
headings: headings2,
|
|
3157
3266
|
controls,
|
|
3158
3267
|
sections,
|
|
3268
|
+
rhythmVariance: (() => {
|
|
3269
|
+
const hs = sections.map((x) => x.height);
|
|
3270
|
+
if (hs.length < 2) return null;
|
|
3271
|
+
const mean = hs.reduce((t, v) => t + v, 0) / hs.length;
|
|
3272
|
+
const sd = Math.sqrt(hs.reduce((t, v) => t + (v - mean) ** 2, 0) / hs.length);
|
|
3273
|
+
return mean > 0 ? Math.round(sd / mean * 1e3) / 1e3 : null;
|
|
3274
|
+
})(),
|
|
3159
3275
|
effects,
|
|
3160
3276
|
motion: {
|
|
3161
3277
|
hiddenSections: sections.filter((s) => s.hiddenAtLoad).length,
|
|
@@ -3189,7 +3305,139 @@ function overlayInPage() {
|
|
|
3189
3305
|
return false;
|
|
3190
3306
|
}
|
|
3191
3307
|
|
|
3308
|
+
// src/direction/png.ts
|
|
3309
|
+
import { inflateSync } from "zlib";
|
|
3310
|
+
function decodePng(buf) {
|
|
3311
|
+
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
3312
|
+
if (!sig.every((b, i) => buf[i] === b)) throw new Error("not a PNG");
|
|
3313
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
3314
|
+
let pos = 8;
|
|
3315
|
+
let width = 0;
|
|
3316
|
+
let height = 0;
|
|
3317
|
+
let depth = 0;
|
|
3318
|
+
let colour = 0;
|
|
3319
|
+
let interlace = 0;
|
|
3320
|
+
const idat = [];
|
|
3321
|
+
while (pos < buf.length) {
|
|
3322
|
+
const len = view.getUint32(pos);
|
|
3323
|
+
const type = String.fromCharCode(...buf.subarray(pos + 4, pos + 8));
|
|
3324
|
+
const data = buf.subarray(pos + 8, pos + 8 + len);
|
|
3325
|
+
if (type === "IHDR") {
|
|
3326
|
+
width = view.getUint32(pos + 8);
|
|
3327
|
+
height = view.getUint32(pos + 12);
|
|
3328
|
+
depth = data[8];
|
|
3329
|
+
colour = data[9];
|
|
3330
|
+
interlace = data[12];
|
|
3331
|
+
} else if (type === "IDAT") idat.push(data);
|
|
3332
|
+
else if (type === "IEND") break;
|
|
3333
|
+
pos += 12 + len;
|
|
3334
|
+
}
|
|
3335
|
+
const channels = { 0: 1, 2: 3, 4: 2, 6: 4 }[colour];
|
|
3336
|
+
if (depth !== 8 || !channels || interlace !== 0) throw new Error(`unsupported PNG (depth ${depth}, colour type ${colour}${interlace ? ", interlaced" : ""}); export it as 8-bit RGB`);
|
|
3337
|
+
const raw = inflateSync(Buffer.concat(idat.map((d) => Buffer.from(d))));
|
|
3338
|
+
const stride = width * channels;
|
|
3339
|
+
const out = new Uint8Array(width * height * 4);
|
|
3340
|
+
const prev = new Uint8Array(stride);
|
|
3341
|
+
const line = new Uint8Array(stride);
|
|
3342
|
+
for (let y = 0; y < height; y += 1) {
|
|
3343
|
+
const filter = raw[y * (stride + 1)];
|
|
3344
|
+
const src = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
3345
|
+
for (let x = 0; x < stride; x += 1) {
|
|
3346
|
+
const a = x >= channels ? line[x - channels] : 0;
|
|
3347
|
+
const b = prev[x];
|
|
3348
|
+
const c = x >= channels ? prev[x - channels] : 0;
|
|
3349
|
+
let v = src[x];
|
|
3350
|
+
if (filter === 1) v += a;
|
|
3351
|
+
else if (filter === 2) v += b;
|
|
3352
|
+
else if (filter === 3) v += a + b >> 1;
|
|
3353
|
+
else if (filter === 4) {
|
|
3354
|
+
const p = a + b - c;
|
|
3355
|
+
const pa = Math.abs(p - a);
|
|
3356
|
+
const pb = Math.abs(p - b);
|
|
3357
|
+
const pc = Math.abs(p - c);
|
|
3358
|
+
v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
3359
|
+
}
|
|
3360
|
+
line[x] = v & 255;
|
|
3361
|
+
}
|
|
3362
|
+
for (let x = 0; x < width; x += 1) {
|
|
3363
|
+
const i = x * channels;
|
|
3364
|
+
const o = (y * width + x) * 4;
|
|
3365
|
+
if (channels >= 3) {
|
|
3366
|
+
out[o] = line[i];
|
|
3367
|
+
out[o + 1] = line[i + 1];
|
|
3368
|
+
out[o + 2] = line[i + 2];
|
|
3369
|
+
out[o + 3] = channels === 4 ? line[i + 3] : 255;
|
|
3370
|
+
} else {
|
|
3371
|
+
out[o] = out[o + 1] = out[o + 2] = line[i];
|
|
3372
|
+
out[o + 3] = channels === 2 ? line[i + 1] : 255;
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
prev.set(line);
|
|
3376
|
+
}
|
|
3377
|
+
return { width, height, rgba: out };
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
// src/snapshot/visual.ts
|
|
3381
|
+
var EDGE_STEP = 32;
|
|
3382
|
+
function visualMeasures(img, maxSamples = 1e6) {
|
|
3383
|
+
const { width: w, height: h, rgba } = img;
|
|
3384
|
+
if (w < 2 || h < 2 || rgba.length < w * h * 4) throw new Error(`visualMeasures: ${w}x${h} image with ${rgba.length} bytes`);
|
|
3385
|
+
const step = Math.max(1, Math.ceil(Math.sqrt(w * h / maxSamples)));
|
|
3386
|
+
const lum = (x, y) => {
|
|
3387
|
+
const i = (y * w + x) * 4;
|
|
3388
|
+
return 0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2];
|
|
3389
|
+
};
|
|
3390
|
+
let n = 0;
|
|
3391
|
+
let sumRg = 0;
|
|
3392
|
+
let sumYb = 0;
|
|
3393
|
+
let sumRg2 = 0;
|
|
3394
|
+
let sumYb2 = 0;
|
|
3395
|
+
let edges = 0;
|
|
3396
|
+
let edgeN = 0;
|
|
3397
|
+
let asym = 0;
|
|
3398
|
+
let asymN = 0;
|
|
3399
|
+
for (let y = 0; y < h; y += step) {
|
|
3400
|
+
for (let x = 0; x < w; x += step) {
|
|
3401
|
+
const i = (y * w + x) * 4;
|
|
3402
|
+
const r = rgba[i];
|
|
3403
|
+
const g = rgba[i + 1];
|
|
3404
|
+
const b = rgba[i + 2];
|
|
3405
|
+
const rg = r - g;
|
|
3406
|
+
const yb = 0.5 * (r + g) - b;
|
|
3407
|
+
sumRg += rg;
|
|
3408
|
+
sumYb += yb;
|
|
3409
|
+
sumRg2 += rg * rg;
|
|
3410
|
+
sumYb2 += yb * yb;
|
|
3411
|
+
n += 1;
|
|
3412
|
+
const l = lum(x, y);
|
|
3413
|
+
if (x + step < w && y + step < h) {
|
|
3414
|
+
if (Math.abs(lum(x + step, y) - l) + Math.abs(lum(x, y + step) - l) > EDGE_STEP) edges += 1;
|
|
3415
|
+
edgeN += 1;
|
|
3416
|
+
}
|
|
3417
|
+
if (x < w / 2) {
|
|
3418
|
+
asym += Math.abs(l - lum(w - 1 - x, y));
|
|
3419
|
+
asymN += 1;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
const meanRg = sumRg / n;
|
|
3424
|
+
const meanYb = sumYb / n;
|
|
3425
|
+
const sdRg = Math.sqrt(Math.max(0, sumRg2 / n - meanRg * meanRg));
|
|
3426
|
+
const sdYb = Math.sqrt(Math.max(0, sumYb2 / n - meanYb * meanYb));
|
|
3427
|
+
const colourfulness = Math.sqrt(sdRg ** 2 + sdYb ** 2) + 0.3 * Math.sqrt(meanRg ** 2 + meanYb ** 2);
|
|
3428
|
+
const round = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
|
|
3429
|
+
return {
|
|
3430
|
+
colourfulness: round(colourfulness, 1),
|
|
3431
|
+
edgeDensity: round(edgeN ? edges / edgeN : 0),
|
|
3432
|
+
symmetry: round(asymN ? 1 - asym / asymN / 255 : 1),
|
|
3433
|
+
width: w,
|
|
3434
|
+
height: h,
|
|
3435
|
+
sampled: n
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3192
3439
|
// src/audit/index.ts
|
|
3440
|
+
var VISUAL_MAX_HEIGHT = 6e3;
|
|
3193
3441
|
async function loadPlaywright() {
|
|
3194
3442
|
for (const name of ["playwright", "playwright-core"]) {
|
|
3195
3443
|
try {
|
|
@@ -3207,12 +3455,23 @@ async function snapshotPage(page, url, options = {}) {
|
|
|
3207
3455
|
await page.waitForTimeout(options.introWindowMs ?? 5e3);
|
|
3208
3456
|
const late = await page.evaluate(overlayInPage).catch(() => false);
|
|
3209
3457
|
const data = await page.evaluate(collectInPage);
|
|
3458
|
+
let visual;
|
|
3459
|
+
if (page.screenshot && options.visual !== false) {
|
|
3460
|
+
try {
|
|
3461
|
+
const height = Math.min(data.pageHeight, VISUAL_MAX_HEIGHT);
|
|
3462
|
+
const png = await page.screenshot({ fullPage: true, type: "png", clip: { x: 0, y: 0, width: data.viewport.width, height } });
|
|
3463
|
+
visual = visualMeasures(decodePng(png));
|
|
3464
|
+
} catch {
|
|
3465
|
+
visual = void 0;
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3210
3468
|
return {
|
|
3211
3469
|
version: SNAPSHOT_VERSION,
|
|
3212
3470
|
url,
|
|
3213
3471
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3214
3472
|
...data,
|
|
3215
|
-
motion: { ...data.motion, introOverlay: early && !late }
|
|
3473
|
+
motion: { ...data.motion, introOverlay: early && !late },
|
|
3474
|
+
...visual ? { visual } : {}
|
|
3216
3475
|
};
|
|
3217
3476
|
}
|
|
3218
3477
|
async function launch(pw, url, options) {
|
|
@@ -3238,7 +3497,9 @@ async function snapshotUrls(urls, options = {}) {
|
|
|
3238
3497
|
for (const url of urls) {
|
|
3239
3498
|
const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
|
|
3240
3499
|
try {
|
|
3241
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
3500
|
+
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
3501
|
+
const status = typeof response?.status === "function" ? response.status() : 200;
|
|
3502
|
+
if (status >= 400) throw new Error(`HTTP ${status}`);
|
|
3242
3503
|
out.push({ url, snapshot: await snapshotPage(page, url, options) });
|
|
3243
3504
|
} catch (error) {
|
|
3244
3505
|
out.push({ url, error: error.message });
|
|
@@ -3256,6 +3517,7 @@ async function audit(url, options = {}) {
|
|
|
3256
3517
|
return { snapshot, report: auditSnapshot(snapshot, options), fingerprint: fingerprint(snapshot) };
|
|
3257
3518
|
}
|
|
3258
3519
|
export {
|
|
3520
|
+
VISUAL_MAX_HEIGHT,
|
|
3259
3521
|
audit,
|
|
3260
3522
|
collectInPage,
|
|
3261
3523
|
loadPlaywright,
|
|
@@ -330,7 +330,7 @@ function extractStrings(file) {
|
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
// src/snapshot/types.ts
|
|
333
|
-
var SNAPSHOT_VERSION =
|
|
333
|
+
var SNAPSHOT_VERSION = 2;
|
|
334
334
|
|
|
335
335
|
// src/character/tells/copy.ts
|
|
336
336
|
var f = (path, text) => ({ path, text });
|
|
@@ -1902,6 +1902,7 @@ var REFLEX_FONTS_2 = [
|
|
|
1902
1902
|
"Plus Jakarta Sans",
|
|
1903
1903
|
"Outfit",
|
|
1904
1904
|
"Geist",
|
|
1905
|
+
"Cal Sans",
|
|
1905
1906
|
"Sora",
|
|
1906
1907
|
"Instrument Sans",
|
|
1907
1908
|
"Instrument Serif",
|
|
@@ -2123,7 +2124,7 @@ function iconTiles(file) {
|
|
|
2123
2124
|
return sized && has(unit, /^rounded-(?:md|lg|xl|2xl|full)$/) && has(unit, /^(?:items-center|place-items-center|place-content-center)$/) && has(unit, /^bg-/);
|
|
2124
2125
|
});
|
|
2125
2126
|
}
|
|
2126
|
-
var ICON_LIBRARY = /from\s+["'](?:lucide-react|@heroicons\/react[^"']*|react-icons[^"']*|@phosphor-icons\/react|@tabler\/icons-react|@radix-ui\/react-icons)["']/;
|
|
2127
|
+
var ICON_LIBRARY = /from\s+["'](?:lucide-react|@heroicons\/react[^"']*|react-icons[^"']*|@phosphor-icons\/react|@tabler\/icons-react|@radix-ui\/react-icons|@iconscout\/react-unicons[^"']*|@mui\/icons-material[^"']*|react-material-symbols[^"']*)["']/;
|
|
2127
2128
|
function iconTileStackHits(ctx) {
|
|
2128
2129
|
const hits = [];
|
|
2129
2130
|
for (const file of ctx.files) {
|
|
@@ -2270,6 +2271,12 @@ function shadcnHits(ctx) {
|
|
|
2270
2271
|
const detail = unused.length > 0 ? `; ${unused.length} imported nowhere` : "";
|
|
2271
2272
|
return [{ path: ui[0].path, offset: 0, message: `${ui.length} stock shadcn primitives${detail}` }];
|
|
2272
2273
|
}
|
|
2274
|
+
function shadcnCardHits(ctx) {
|
|
2275
|
+
return eachUnit(
|
|
2276
|
+
ctx,
|
|
2277
|
+
(unit) => has(unit, /^bg-card$/) && has(unit, /^text-card-foreground$/) && has(unit, /^border$/) && has(unit, /^rounded-(?:lg|xl)$/) && has(unit, /^shadow(?:-sm)?$/) ? "the registry Card recipe, unchanged" : null
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2273
2280
|
function marqueeHits(ctx) {
|
|
2274
2281
|
const hits = [];
|
|
2275
2282
|
const pattern = /@keyframes\s+[\w-]*marquee|animate-(?:marquee|scroll-x|infinite-scroll)\b|<Marquee\b|["']react-fast-marquee["']|ui\/marquee["']|animation(?:-name)?\s*:\s*[^;]*marquee/i;
|
|
@@ -2487,6 +2494,23 @@ const inter = Inter({ subsets: ["latin"] });`),
|
|
|
2487
2494
|
pass: [f3("src/components/ui/button.tsx", `export function Button() { return null; }`)]
|
|
2488
2495
|
}
|
|
2489
2496
|
},
|
|
2497
|
+
{
|
|
2498
|
+
id: "shadcn-card-stock",
|
|
2499
|
+
name: "Stock shadcn Card",
|
|
2500
|
+
generation: 1,
|
|
2501
|
+
severity: "warn",
|
|
2502
|
+
surface: "source",
|
|
2503
|
+
why: "The registry Card (bg-card, a plain border, rounded-lg or rounded-xl, shadow-sm) is the container every generated page is built from. Left as shipped, every card on the site looks like every other shadcn site's.",
|
|
2504
|
+
fix: "Give the card this site's radius and shadow from the restraint budget, or drop the shadow and let a visible border carry it.",
|
|
2505
|
+
detect: shadcnCardHits,
|
|
2506
|
+
fixtures: {
|
|
2507
|
+
flag: [
|
|
2508
|
+
f3("components/ui/card.tsx", `className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}`),
|
|
2509
|
+
f3("components/ui/card.tsx", `className={cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className)}`)
|
|
2510
|
+
],
|
|
2511
|
+
pass: [f3("components/ui/card.tsx", `className={cn("rounded-sm border-2 border-ink bg-card text-card-foreground", className)}`)]
|
|
2512
|
+
}
|
|
2513
|
+
},
|
|
2490
2514
|
{
|
|
2491
2515
|
id: "cream-palette",
|
|
2492
2516
|
name: "Cream ground",
|
|
@@ -2713,6 +2737,17 @@ var HOUSE = {
|
|
|
2713
2737
|
function houseRule(id, name = id) {
|
|
2714
2738
|
return HOUSE[id] ?? { tier: "review", label: name };
|
|
2715
2739
|
}
|
|
2740
|
+
var HOUSE_LOCKED = "the house blocking tier cannot be lowered by a repo. A genuine one-off takes copy-ok on its line; a rule that is wrong is changed in craft's house.ts and COPY.md";
|
|
2741
|
+
function holdHouseBlocks(exceptions, severity) {
|
|
2742
|
+
const blocks = (tell) => HOUSE[tell]?.tier === "block";
|
|
2743
|
+
const refusedSeverity = Object.entries(severity).filter(([tell, o]) => blocks(tell) && o.level !== "block").map(([tell]) => tell);
|
|
2744
|
+
return {
|
|
2745
|
+
exceptions: exceptions.filter((e) => !blocks(e.tell)),
|
|
2746
|
+
refused: exceptions.filter((e) => blocks(e.tell)),
|
|
2747
|
+
severity: Object.fromEntries(Object.entries(severity).filter(([tell]) => !refusedSeverity.includes(tell))),
|
|
2748
|
+
refusedSeverity
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2716
2751
|
|
|
2717
2752
|
// src/snapshot/fixture.ts
|
|
2718
2753
|
function makeSnapshot(patch = {}) {
|
|
@@ -3046,7 +3081,7 @@ var RENDERED_PATHS = {
|
|
|
3046
3081
|
};
|
|
3047
3082
|
|
|
3048
3083
|
// src/character/check.ts
|
|
3049
|
-
var CATALOGUE_VERSION = "2026.09.
|
|
3084
|
+
var CATALOGUE_VERSION = "2026.09.9";
|
|
3050
3085
|
var CATALOGUE = [...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map(
|
|
3051
3086
|
(t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t
|
|
3052
3087
|
);
|
|
@@ -3171,6 +3206,8 @@ export {
|
|
|
3171
3206
|
REFLEX_FONTS_1,
|
|
3172
3207
|
REFLEX_FONTS_2,
|
|
3173
3208
|
houseRule,
|
|
3209
|
+
HOUSE_LOCKED,
|
|
3210
|
+
holdHouseBlocks,
|
|
3174
3211
|
fileKind,
|
|
3175
3212
|
extractCopy,
|
|
3176
3213
|
extractStrings,
|
|
@@ -3,18 +3,21 @@ import {
|
|
|
3
3
|
hueName,
|
|
4
4
|
loadNull,
|
|
5
5
|
shapeName
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-LHGY43LL.js";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
readSnapshot
|
|
9
|
+
} from "./chunk-ZKVJ7K6X.js";
|
|
10
|
+
import {
|
|
11
|
+
parseFlags,
|
|
12
|
+
toJson
|
|
13
|
+
} from "./chunk-J3QUROJ5.js";
|
|
10
14
|
import {
|
|
11
15
|
fingerprint,
|
|
12
16
|
fingerprintDistance
|
|
13
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-PZ4MF2VV.js";
|
|
14
18
|
import {
|
|
15
|
-
SNAPSHOT_VERSION,
|
|
16
19
|
auditSnapshot
|
|
17
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-AL6MRG6V.js";
|
|
18
21
|
|
|
19
22
|
// src/estate/cli.ts
|
|
20
23
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -87,12 +90,11 @@ function loadEstate(path) {
|
|
|
87
90
|
async function snapshotOf(target, cwd) {
|
|
88
91
|
const local = resolve(cwd, target);
|
|
89
92
|
if (/\.json$/i.test(target) && existsSync(local)) {
|
|
90
|
-
const snap = JSON.parse(readFileSync(local, "utf8"));
|
|
91
|
-
if (snap.version !== SNAPSHOT_VERSION) throw new Error(`${target} is not a snapshot`);
|
|
93
|
+
const snap = readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
|
|
92
94
|
return snap;
|
|
93
95
|
}
|
|
94
96
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
|
|
95
|
-
const { snapshotUrl } = await import("./audit-
|
|
97
|
+
const { snapshotUrl } = await import("./audit-5QWFLZ6L.js");
|
|
96
98
|
return snapshotUrl(target);
|
|
97
99
|
}
|
|
98
100
|
var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
|
|
@@ -137,13 +139,13 @@ async function runEstate(args, io) {
|
|
|
137
139
|
if (sub === "compare") {
|
|
138
140
|
if (!target) {
|
|
139
141
|
const pairs = estatePairs(register, at);
|
|
140
|
-
io.out(flags.json ?
|
|
142
|
+
io.out(flags.json ? toJson({ siblingAt: at, pairs }) : formatPairs(pairs, at, register.sites.length));
|
|
141
143
|
return flags.strict && pairs.some((p) => p.sibling) ? 1 : 0;
|
|
142
144
|
}
|
|
143
145
|
const known = register.sites.find((s) => s.id === target);
|
|
144
146
|
const fp = known ? known.fingerprint : fingerprint(await snapshotOf(target, io.cwd));
|
|
145
147
|
const matches = compareToEstate(fp, register, { exclude: known?.id, siblingAt: at });
|
|
146
|
-
io.out(flags.json ?
|
|
148
|
+
io.out(flags.json ? toJson({ siblingAt: at, matches }) : formatMatches(known?.id ?? target, matches, at));
|
|
147
149
|
return flags.strict && matches.some((m) => m.sibling) ? 1 : 0;
|
|
148
150
|
}
|
|
149
151
|
throw new Error(USAGE);
|