designlang 10.3.0 → 10.5.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 +25 -0
- package/bin/design-extract.js +17 -4
- package/package.json +1 -1
- package/src/extractors/background-patterns.js +72 -0
- package/src/extractors/form-states.js +109 -0
- package/src/extractors/icon-system.js +110 -0
- package/src/extractors/stack-intel.js +73 -0
- package/src/index.js +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [10.5.0] — 2026-04-22
|
|
4
|
+
|
|
5
|
+
**The states LLMs always botch.**
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`src/extractors/form-states.js`** — surfaces forms (input count + style families), modal/dialog/sheet containers, skeleton and spinner loading indicators, empty-state and error-state placeholders, and detects which toast library is on the page (Sonner, react-hot-toast, react-toastify, Radix Toast, Chakra Toast, Notistack).
|
|
10
|
+
- New output: `*-form-states.json`.
|
|
11
|
+
|
|
12
|
+
Closes the v10.x minor-release series started in v10.1. Everything the
|
|
13
|
+
v10 spec deferred for v11 is now shipped as minor releases — with no
|
|
14
|
+
breaking changes along the way.
|
|
15
|
+
|
|
16
|
+
## [10.4.0] — 2026-04-22
|
|
17
|
+
|
|
18
|
+
**Identification trio: icon system, background patterns, stack intel.**
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`src/extractors/icon-system.js`** — fingerprints the icon library (Lucide / Heroicons outline+solid / Phosphor / Tabler / Feather / Remix / Material) from stroke vs fill dominance, stroke width, grid size, and rounded-caps presence. Emits per-icon hints agents can act on.
|
|
23
|
+
- **`src/extractors/background-patterns.js`** — classifies noise / dot-grid / line-grid / gradient-mesh / svg-pattern / plain from computed `background-image` values. Merged into `*-visual-dna.json`.
|
|
24
|
+
- **`src/extractors/stack-intel.js`** — extends the existing stack-fingerprint with 12 CMSs (Webflow, Framer, Shopify, Ghost, Sanity, Contentful, Wix, Squarespace, WordPress, Hashnode, Notion, Bubble), 13 analytics platforms, and 7 experimentation platforms.
|
|
25
|
+
- Bin reads its own version from `package.json` — no more per-release version drift in the CLI.
|
|
26
|
+
- New outputs: `*-icon-system.json`, `*-stack-intel.json`.
|
|
27
|
+
|
|
3
28
|
## [10.3.0] — 2026-04-22
|
|
4
29
|
|
|
5
30
|
**Perf + SEO.** designlang now doubles as a lightweight auditor.
|
package/bin/design-extract.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
5
|
-
import { resolve, join } from 'path';
|
|
4
|
+
import { mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
5
|
+
import { resolve, join, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PKG_VERSION = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8')).version;
|
|
6
10
|
import chalk from 'chalk';
|
|
7
11
|
import ora from 'ora';
|
|
8
12
|
import { extractDesignLanguage } from '../src/index.js';
|
|
@@ -56,7 +60,7 @@ const program = new Command();
|
|
|
56
60
|
program
|
|
57
61
|
.name('designlang')
|
|
58
62
|
.description('Extract the complete design language from any website')
|
|
59
|
-
.version(
|
|
63
|
+
.version(PKG_VERSION);
|
|
60
64
|
|
|
61
65
|
// ── Main command: extract ──────────────────────────────────────
|
|
62
66
|
program
|
|
@@ -343,7 +347,7 @@ program
|
|
|
343
347
|
|
|
344
348
|
// v10: page intent + section roles + visual DNA + component library + multi-page + prompt pack.
|
|
345
349
|
files.push({ name: `${prefix}-intent.json`, content: JSON.stringify({ pageIntent: design.pageIntent, sectionRoles: design.sectionRoles }, null, 2), label: 'Page Intent + Section Roles' });
|
|
346
|
-
files.push({ name: `${prefix}-visual-dna.json`, content: JSON.stringify({ materialLanguage: design.materialLanguage, imageryStyle: design.imageryStyle }, null, 2), label: 'Visual DNA' });
|
|
350
|
+
files.push({ name: `${prefix}-visual-dna.json`, content: JSON.stringify({ materialLanguage: design.materialLanguage, imageryStyle: design.imageryStyle, backgroundPatterns: design.backgroundPatterns }, null, 2), label: 'Visual DNA' });
|
|
347
351
|
files.push({ name: `${prefix}-library.json`, content: JSON.stringify(design.componentLibrary || {}, null, 2), label: 'Component Library Detection' });
|
|
348
352
|
if (design.logo && design.logo.found) {
|
|
349
353
|
files.push({ name: `${prefix}-logo.json`, content: JSON.stringify(design.logo, null, 2), label: 'Logo Metadata' });
|
|
@@ -366,6 +370,15 @@ program
|
|
|
366
370
|
if (design.perf && !design.perf.error) {
|
|
367
371
|
files.push({ name: `${prefix}-perf.json`, content: JSON.stringify(design.perf, null, 2), label: 'Perf + Bundle' });
|
|
368
372
|
}
|
|
373
|
+
if (design.iconSystem && (design.iconSystem.icons || []).length) {
|
|
374
|
+
files.push({ name: `${prefix}-icon-system.json`, content: JSON.stringify(design.iconSystem, null, 2), label: 'Icon System' });
|
|
375
|
+
}
|
|
376
|
+
if (design.stackIntel) {
|
|
377
|
+
files.push({ name: `${prefix}-stack-intel.json`, content: JSON.stringify(design.stackIntel, null, 2), label: 'Stack Intel (CMS/analytics/experimentation)' });
|
|
378
|
+
}
|
|
379
|
+
if (design.formStates) {
|
|
380
|
+
files.push({ name: `${prefix}-form-states.json`, content: JSON.stringify(design.formStates, null, 2), label: 'Forms + States' });
|
|
381
|
+
}
|
|
369
382
|
if (merged.prompts !== false) {
|
|
370
383
|
const pack = buildPromptPack(design);
|
|
371
384
|
const promptsDir = join(outDir, `${prefix}-prompts`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "designlang",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.5.0",
|
|
4
4
|
"description": "Extract the complete design language from any website — colors, typography, spacing, shadows, motion, component anatomy, brand voice, page intent, section roles, material language, component library, imagery style, and logo. Outputs AI-optimized markdown, W3C design tokens, motion tokens, typed component stubs, Tailwind config, and ready-to-paste v0 / Lovable / Cursor / Claude-Artifacts prompts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// v10.4 — Background Patterns
|
|
2
|
+
//
|
|
3
|
+
// Classifies the visual backgrounds on a site from computed-style evidence:
|
|
4
|
+
// noise (repeated grain PNG/SVG), dot-grid, line-grid, gradient-mesh (multiple
|
|
5
|
+
// radial gradients), chequer, diagonal stripes, SVG patterns, or plain.
|
|
6
|
+
//
|
|
7
|
+
// Pure function — reads `rawData.light.computedStyles`, which every extractor
|
|
8
|
+
// already has access to, plus the `modernColors` and any collected svgs.
|
|
9
|
+
|
|
10
|
+
function looksLikeDotGrid(image) {
|
|
11
|
+
return /radial-gradient\(.*\)/i.test(image) && /repeat/i.test(image) && /(\d+px\s*\d+px)/.test(image);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function looksLikeLineGrid(image) {
|
|
15
|
+
// repeating-linear-gradient with a narrow colored band.
|
|
16
|
+
return /repeating-linear-gradient/i.test(image);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function looksLikeNoise(image) {
|
|
20
|
+
// data URI SVG with feTurbulence filter, or a well-known noise png path.
|
|
21
|
+
return /feTurbulence|data:image\/svg.+fractalNoise/i.test(image) || /noise\.(png|svg|webp)/i.test(image);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function countRadialGradients(image) {
|
|
25
|
+
return (image.match(/radial-gradient\(/gi) || []).length;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function countLinearGradients(image) {
|
|
29
|
+
return (image.match(/linear-gradient\(/gi) || []).length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function detectSvgPattern(image) {
|
|
33
|
+
return /url\("data:image\/svg/i.test(image) && !looksLikeNoise(image);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function extractBackgroundPatterns(rawData = {}) {
|
|
37
|
+
const styles = (rawData.light?.computedStyles) || [];
|
|
38
|
+
let dotGrid = 0, lineGrid = 0, noise = 0, svgPattern = 0, radialSum = 0, linearSum = 0, meshCount = 0, plain = 0;
|
|
39
|
+
const samples = [];
|
|
40
|
+
|
|
41
|
+
for (const s of styles) {
|
|
42
|
+
const bg = s.backgroundImage || s['background-image'] || '';
|
|
43
|
+
if (!bg || bg === 'none') { plain++; continue; }
|
|
44
|
+
const radial = countRadialGradients(bg);
|
|
45
|
+
const linear = countLinearGradients(bg);
|
|
46
|
+
radialSum += radial;
|
|
47
|
+
linearSum += linear;
|
|
48
|
+
let tag = null;
|
|
49
|
+
if (looksLikeNoise(bg)) { noise++; tag = 'noise'; }
|
|
50
|
+
else if (looksLikeDotGrid(bg)) { dotGrid++; tag = 'dot-grid'; }
|
|
51
|
+
else if (looksLikeLineGrid(bg)) { lineGrid++; tag = 'line-grid'; }
|
|
52
|
+
else if (radial >= 2) { meshCount++; tag = 'gradient-mesh'; }
|
|
53
|
+
else if (detectSvgPattern(bg)) { svgPattern++; tag = 'svg-pattern'; }
|
|
54
|
+
if (tag && samples.length < 8) samples.push({ tag, value: bg.slice(0, 200) });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const total = styles.length || 1;
|
|
58
|
+
const labels = [];
|
|
59
|
+
if (noise / total > 0.002) labels.push('noise');
|
|
60
|
+
if (dotGrid / total > 0.002) labels.push('dot-grid');
|
|
61
|
+
if (lineGrid / total > 0.002) labels.push('line-grid');
|
|
62
|
+
if (meshCount > 0) labels.push('gradient-mesh');
|
|
63
|
+
if (svgPattern > 0) labels.push('svg-pattern');
|
|
64
|
+
if (!labels.length) labels.push('plain');
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
labels,
|
|
68
|
+
counts: { noise, dotGrid, lineGrid, meshCount, svgPattern },
|
|
69
|
+
gradientTotals: { radial: radialSum, linear: linearSum },
|
|
70
|
+
samples,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// v10.5 — Form & State Capture
|
|
2
|
+
//
|
|
3
|
+
// The states LLM agents always botch when rebuilding: form fields (styling
|
|
4
|
+
// per type), validation hints, modal containers (backdrop + panel geometry),
|
|
5
|
+
// empty / loading / error placeholders, skeleton shapes, and which toast
|
|
6
|
+
// library (if any) is on the page. Pure function — reads the crawler's
|
|
7
|
+
// existing computedStyles + sections + componentCandidates.
|
|
8
|
+
|
|
9
|
+
const TOAST_LIBS = [
|
|
10
|
+
{ id: 'sonner', re: /\bsonner\b|sonner-toast/i },
|
|
11
|
+
{ id: 'react-hot-toast', re: /react-hot-toast/i },
|
|
12
|
+
{ id: 'react-toastify', re: /react-toastify/i },
|
|
13
|
+
{ id: 'radix-toast', re: /radix-toast|data-radix-toast/i },
|
|
14
|
+
{ id: 'chakra-toast', re: /chakra-toast/i },
|
|
15
|
+
{ id: 'notistack', re: /notistack/i },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const SKELETON_CLASS_RE = /\b(skeleton|placeholder-loading|shimmer|pulse-loading|animate-pulse)\b/i;
|
|
19
|
+
const SPINNER_CLASS_RE = /\b(spinner|loading-indicator|loader)\b/i;
|
|
20
|
+
const EMPTY_STATE_RE = /\b(empty-state|no-results|no-data|nothing-here)\b/i;
|
|
21
|
+
const ERROR_STATE_RE = /\b(error-state|error-message|alert-error|form-error|invalid)\b/i;
|
|
22
|
+
|
|
23
|
+
function summarizeInputs(styles = []) {
|
|
24
|
+
const types = {};
|
|
25
|
+
for (const s of styles) {
|
|
26
|
+
if (!s.tag || !/^(input|textarea|select)$/i.test(s.tag)) continue;
|
|
27
|
+
const t = (s.type || s.inputType || s.tag).toLowerCase();
|
|
28
|
+
types[t] = (types[t] || 0) + 1;
|
|
29
|
+
}
|
|
30
|
+
return types;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function detectToastLib(stack = {}) {
|
|
34
|
+
const haystack = [
|
|
35
|
+
...(stack.scripts || []),
|
|
36
|
+
...(stack.classNameSample || []),
|
|
37
|
+
].join(' ');
|
|
38
|
+
return TOAST_LIBS.filter(t => t.re.test(haystack)).map(t => t.id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function detectModals(sections = []) {
|
|
42
|
+
return sections.filter(s => {
|
|
43
|
+
const blob = `${s.className || ''} ${s.role || ''}`.toLowerCase();
|
|
44
|
+
return /\bmodal\b|dialog|overlay|drawer|sheet/.test(blob) || s.role === 'dialog';
|
|
45
|
+
}).map(s => ({
|
|
46
|
+
role: s.role || null,
|
|
47
|
+
className: (s.className || '').slice(0, 80),
|
|
48
|
+
bounds: s.bounds || null,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function classBasedScan(classSample = []) {
|
|
53
|
+
let skeleton = 0, spinner = 0, emptyState = 0, errorState = 0;
|
|
54
|
+
for (const c of classSample) {
|
|
55
|
+
if (SKELETON_CLASS_RE.test(c)) skeleton++;
|
|
56
|
+
if (SPINNER_CLASS_RE.test(c)) spinner++;
|
|
57
|
+
if (EMPTY_STATE_RE.test(c)) emptyState++;
|
|
58
|
+
if (ERROR_STATE_RE.test(c)) errorState++;
|
|
59
|
+
}
|
|
60
|
+
return { skeleton, spinner, emptyState, errorState };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function summarizeFormFields(candidates = []) {
|
|
64
|
+
const inputs = candidates.filter(c => c.kind === 'input');
|
|
65
|
+
if (!inputs.length) return { count: 0, families: {} };
|
|
66
|
+
const families = {};
|
|
67
|
+
for (const inp of inputs) {
|
|
68
|
+
const key = [
|
|
69
|
+
inp.css?.borderRadius || '',
|
|
70
|
+
inp.css?.padding || '',
|
|
71
|
+
inp.css?.border || '',
|
|
72
|
+
].join('|');
|
|
73
|
+
families[key] = (families[key] || 0) + 1;
|
|
74
|
+
}
|
|
75
|
+
return { count: inputs.length, families: Object.values(families).slice(0, 6) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function extractFormStates(rawData = {}, design = {}) {
|
|
79
|
+
const light = rawData.light || {};
|
|
80
|
+
const stack = light.stack || {};
|
|
81
|
+
const sections = light.sections || [];
|
|
82
|
+
const candidates = light.componentCandidates || [];
|
|
83
|
+
|
|
84
|
+
const toastLibs = detectToastLib(stack);
|
|
85
|
+
const modals = detectModals(sections);
|
|
86
|
+
const classScan = classBasedScan(stack.classNameSample || []);
|
|
87
|
+
const form = summarizeFormFields(candidates);
|
|
88
|
+
const inputTypes = summarizeInputs(light.computedStyles || []);
|
|
89
|
+
|
|
90
|
+
const flags = [];
|
|
91
|
+
if (classScan.skeleton) flags.push('skeleton-loading');
|
|
92
|
+
if (classScan.spinner) flags.push('spinner-loading');
|
|
93
|
+
if (classScan.emptyState) flags.push('empty-state');
|
|
94
|
+
if (classScan.errorState) flags.push('error-state');
|
|
95
|
+
if (modals.length) flags.push('modal');
|
|
96
|
+
if (toastLibs.length) flags.push('toast-library');
|
|
97
|
+
if (form.count) flags.push('forms');
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
flags,
|
|
101
|
+
forms: form,
|
|
102
|
+
inputTypesSeen: inputTypes,
|
|
103
|
+
modals,
|
|
104
|
+
toastLibraries: toastLibs,
|
|
105
|
+
loading: { skeleton: classScan.skeleton, spinner: classScan.spinner },
|
|
106
|
+
empty: { count: classScan.emptyState },
|
|
107
|
+
error: { count: classScan.errorState },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// v10.4 — Icon System fingerprint
|
|
2
|
+
//
|
|
3
|
+
// Pure extractor — operates on the icon payload the crawler already collects.
|
|
4
|
+
// We can't reliably match against Lucide/Phosphor/Heroicons path-data without
|
|
5
|
+
// shipping the full libraries, so this extractor does the next-best thing:
|
|
6
|
+
// infers the *system* an icon set came from (stroke vs fill, stroke width,
|
|
7
|
+
// corner style, grid size, viewBox convention) and emits guidance any LLM can
|
|
8
|
+
// act on ("use Lucide @ 1.5 stroke, 24px grid").
|
|
9
|
+
|
|
10
|
+
const LIBRARY_HINTS = [
|
|
11
|
+
{ id: 'lucide', match: (ctx) => ctx.strokeDominant && ctx.avgWeight > 1.3 && ctx.avgWeight < 1.7 && ctx.grid24 && !ctx.roundedCaps, score: 0.8 },
|
|
12
|
+
{ id: 'heroicons-outline', match: (ctx) => ctx.strokeDominant && ctx.avgWeight >= 1.8 && ctx.avgWeight <= 2.2 && ctx.grid24, score: 0.8 },
|
|
13
|
+
{ id: 'heroicons-solid', match: (ctx) => ctx.fillDominant && ctx.grid24, score: 0.55 },
|
|
14
|
+
{ id: 'phosphor', match: (ctx) => ctx.strokeDominant && ctx.roundedCaps && ctx.grid24, score: 0.7 },
|
|
15
|
+
{ id: 'tabler', match: (ctx) => ctx.strokeDominant && ctx.avgWeight > 1.9 && ctx.grid24, score: 0.6 },
|
|
16
|
+
{ id: 'feather', match: (ctx) => ctx.strokeDominant && ctx.avgWeight > 1.8 && ctx.roundedCaps && ctx.grid24, score: 0.7 },
|
|
17
|
+
{ id: 'remix', match: (ctx) => ctx.mixedFillStroke && ctx.grid24, score: 0.45 },
|
|
18
|
+
{ id: 'material', match: (ctx) => ctx.fillDominant && ctx.grid24, score: 0.4 },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function parseStroke(v) {
|
|
22
|
+
if (!v) return 0;
|
|
23
|
+
const n = parseFloat(v);
|
|
24
|
+
return Number.isFinite(n) ? n : 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function viewBoxGrid(vb) {
|
|
28
|
+
if (!vb) return null;
|
|
29
|
+
const parts = vb.trim().split(/\s+/).map(Number);
|
|
30
|
+
if (parts.length !== 4 || parts.some(n => !Number.isFinite(n))) return null;
|
|
31
|
+
const w = parts[2], h = parts[3];
|
|
32
|
+
if (w === h && [16, 20, 24, 32, 48, 64].includes(w)) return w;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function detectRoundedCaps(svg) {
|
|
37
|
+
// Look for `stroke-linecap="round"` or `stroke-linejoin="round"` as a
|
|
38
|
+
// proxy for Phosphor/Feather-style rounded terminals.
|
|
39
|
+
return /stroke-linecap="round"|stroke-linejoin="round"/i.test(svg || '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function extractIconSystem(icons = []) {
|
|
43
|
+
if (!icons.length) {
|
|
44
|
+
return { library: 'unknown', confidence: 0, stats: {}, signals: [], icons: [] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let strokeCount = 0, fillCount = 0, mixed = 0, weights = [], gridHits = {};
|
|
48
|
+
let rounded = 0;
|
|
49
|
+
const perIconHints = [];
|
|
50
|
+
|
|
51
|
+
for (const icon of icons) {
|
|
52
|
+
const svg = icon.svg || '';
|
|
53
|
+
const stroke = icon.stroke || (svg.match(/stroke="([^"]+)"/i) || [])[1];
|
|
54
|
+
const fill = icon.fill || (svg.match(/fill="([^"]+)"/i) || [])[1];
|
|
55
|
+
const strokeWidthMatch = svg.match(/stroke-width="([0-9.]+)"/i);
|
|
56
|
+
const sw = strokeWidthMatch ? parseStroke(strokeWidthMatch[1]) : 0;
|
|
57
|
+
|
|
58
|
+
const hasStroke = !!(stroke && stroke !== 'none');
|
|
59
|
+
const hasFill = !!(fill && fill !== 'none');
|
|
60
|
+
if (hasStroke && !hasFill) strokeCount++;
|
|
61
|
+
else if (hasFill && !hasStroke) fillCount++;
|
|
62
|
+
else if (hasStroke && hasFill) mixed++;
|
|
63
|
+
if (sw > 0) weights.push(sw);
|
|
64
|
+
const grid = viewBoxGrid(icon.viewBox);
|
|
65
|
+
if (grid) gridHits[grid] = (gridHits[grid] || 0) + 1;
|
|
66
|
+
if (detectRoundedCaps(svg)) rounded++;
|
|
67
|
+
|
|
68
|
+
perIconHints.push({
|
|
69
|
+
class: (icon.classList || '').slice(0, 80),
|
|
70
|
+
grid,
|
|
71
|
+
strokeWidth: sw || null,
|
|
72
|
+
style: hasStroke && !hasFill ? 'stroke' : hasFill && !hasStroke ? 'fill' : 'mixed',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const avgWeight = weights.length ? weights.reduce((a, b) => a + b, 0) / weights.length : 0;
|
|
77
|
+
const total = icons.length;
|
|
78
|
+
const ctx = {
|
|
79
|
+
strokeDominant: strokeCount / total > 0.55,
|
|
80
|
+
fillDominant: fillCount / total > 0.55,
|
|
81
|
+
mixedFillStroke: mixed / total > 0.3,
|
|
82
|
+
avgWeight,
|
|
83
|
+
roundedCaps: rounded / total > 0.4,
|
|
84
|
+
grid24: gridHits[24] ? gridHits[24] / total > 0.5 : false,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const scored = LIBRARY_HINTS
|
|
88
|
+
.map(lib => ({ id: lib.id, score: lib.match(ctx) ? lib.score : 0 }))
|
|
89
|
+
.filter(x => x.score > 0)
|
|
90
|
+
.sort((a, b) => b.score - a.score);
|
|
91
|
+
|
|
92
|
+
const primary = scored[0] || { id: 'unknown', score: 0 };
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
library: primary.id,
|
|
96
|
+
confidence: Number(primary.score.toFixed(3)),
|
|
97
|
+
alternates: scored.slice(1, 4),
|
|
98
|
+
stats: {
|
|
99
|
+
count: total,
|
|
100
|
+
strokeOnly: strokeCount,
|
|
101
|
+
fillOnly: fillCount,
|
|
102
|
+
mixed,
|
|
103
|
+
avgStrokeWidth: Number(avgWeight.toFixed(2)),
|
|
104
|
+
gridDistribution: gridHits,
|
|
105
|
+
roundedCapsFraction: Number((rounded / total).toFixed(2)),
|
|
106
|
+
},
|
|
107
|
+
signals: Object.entries(ctx).filter(([, v]) => v === true).map(([k]) => k),
|
|
108
|
+
icons: perIconHints.slice(0, 30),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// v10.4 — Stack Intel
|
|
2
|
+
//
|
|
3
|
+
// Extends stack-fingerprint.js with detectors for CMS platforms (Webflow,
|
|
4
|
+
// Framer, Shopify, Ghost, Sanity, Contentful, Wix, Squarespace, WordPress),
|
|
5
|
+
// analytics (GA, Segment, Mixpanel, PostHog, Amplitude, Heap), and
|
|
6
|
+
// experimentation platforms (Optimizely, Statsig, GrowthBook, LaunchDarkly,
|
|
7
|
+
// Split, Eppo). All signals come from script URLs + meta + known globals.
|
|
8
|
+
|
|
9
|
+
const CMS = [
|
|
10
|
+
{ id: 'webflow', re: /webflow\.com|wf-|\.webflow\./i },
|
|
11
|
+
{ id: 'framer', re: /framer\.(?:com|website)|__framer|framer-motion\b/i },
|
|
12
|
+
{ id: 'shopify', re: /cdn\.shopify|shopify\.com|x-shopify/i },
|
|
13
|
+
{ id: 'ghost', re: /ghost\.io|__ghost_|ghost-url/i },
|
|
14
|
+
{ id: 'sanity', re: /cdn\.sanity\.io|sanity-studio/i },
|
|
15
|
+
{ id: 'contentful', re: /cdn\.contentful\.com|ctfassets\.net/i },
|
|
16
|
+
{ id: 'wix', re: /parastorage\.com|\.wix\.com/i },
|
|
17
|
+
{ id: 'squarespace', re: /squarespace\.com|sqspcdn\.com|squarespace-cdn/i },
|
|
18
|
+
{ id: 'wordpress', re: /wp-content|wp-includes|wordpress/i },
|
|
19
|
+
{ id: 'hashnode', re: /hashnode\.com/i },
|
|
20
|
+
{ id: 'notion', re: /notion\.so\/image|notion-static/i },
|
|
21
|
+
{ id: 'bubble', re: /bubble\.io|bubble-cdn/i },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const ANALYTICS = [
|
|
25
|
+
{ id: 'google-analytics', re: /google-analytics\.com|googletagmanager\.com|gtag\(/ },
|
|
26
|
+
{ id: 'segment', re: /segment\.com\/analytics|cdn\.segment\.io/i },
|
|
27
|
+
{ id: 'mixpanel', re: /cdn\.mxpnl\.com|mixpanel\.com\/lib/i },
|
|
28
|
+
{ id: 'amplitude', re: /amplitude\.com|cdn\.amplitude\.com/i },
|
|
29
|
+
{ id: 'posthog', re: /posthog\.com|ph\.posthog\.com/i },
|
|
30
|
+
{ id: 'heap', re: /heapanalytics\.com/i },
|
|
31
|
+
{ id: 'fullstory', re: /fullstory\.com/i },
|
|
32
|
+
{ id: 'hotjar', re: /static\.hotjar\.com|hj\.contentsquare/i },
|
|
33
|
+
{ id: 'vercel-analytics', re: /_vercel\/insights|vercel\/analytics/i },
|
|
34
|
+
{ id: 'plausible', re: /plausible\.io\/js|plausible\.io\/api/i },
|
|
35
|
+
{ id: 'fathom', re: /usefathom\.com/i },
|
|
36
|
+
{ id: 'sentry', re: /sentry\.io|sentry-cdn/i },
|
|
37
|
+
{ id: 'datadog', re: /datadoghq\.com|datadog-rum/i },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const EXPERIMENTATION = [
|
|
41
|
+
{ id: 'optimizely', re: /optimizely\.com|cdn\.optimizely\./i },
|
|
42
|
+
{ id: 'statsig', re: /statsig\.com/i },
|
|
43
|
+
{ id: 'growthbook', re: /growthbook\.io/i },
|
|
44
|
+
{ id: 'launchdarkly', re: /launchdarkly\.com/i },
|
|
45
|
+
{ id: 'split', re: /split\.io|sdk\.split\.io/i },
|
|
46
|
+
{ id: 'eppo', re: /eppo\.cloud/i },
|
|
47
|
+
{ id: 'vercel-flags', re: /vercel\/flags|flags\.sdk/i },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function fingerprint(haystack, list) {
|
|
51
|
+
const hits = [];
|
|
52
|
+
for (const entry of list) {
|
|
53
|
+
if (entry.re.test(haystack)) hits.push(entry.id);
|
|
54
|
+
}
|
|
55
|
+
return hits;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function extractStackIntel(stack = {}) {
|
|
59
|
+
const scripts = (stack.scripts || []).join(' \n');
|
|
60
|
+
const metas = (stack.metas || []).map(m => `${m.name || ''} ${m.content || ''}`).join(' ');
|
|
61
|
+
const classes = (stack.classNameSample || []).join(' ');
|
|
62
|
+
const haystack = `${scripts}\n${metas}\n${classes}`;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
cms: fingerprint(haystack, CMS),
|
|
66
|
+
analytics: fingerprint(haystack, ANALYTICS),
|
|
67
|
+
experimentation: fingerprint(haystack, EXPERIMENTATION),
|
|
68
|
+
signals: {
|
|
69
|
+
scriptCount: (stack.scripts || []).length,
|
|
70
|
+
metaCount: (stack.metas || []).length,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/index.js
CHANGED
|
@@ -34,6 +34,10 @@ import { extractComponentLibrary } from './extractors/component-library.js';
|
|
|
34
34
|
import { extractMaterialLanguage } from './extractors/material-language.js';
|
|
35
35
|
import { extractImageryStyle } from './extractors/imagery-style.js';
|
|
36
36
|
import { extractSeo } from './extractors/seo.js';
|
|
37
|
+
import { extractIconSystem } from './extractors/icon-system.js';
|
|
38
|
+
import { extractBackgroundPatterns } from './extractors/background-patterns.js';
|
|
39
|
+
import { extractStackIntel } from './extractors/stack-intel.js';
|
|
40
|
+
import { extractFormStates } from './extractors/form-states.js';
|
|
37
41
|
import { formatDtcgTokens } from './formatters/dtcg-tokens.js';
|
|
38
42
|
import { formatMotionTokens } from './formatters/motion-tokens.js';
|
|
39
43
|
|
|
@@ -139,6 +143,10 @@ export async function extractDesignLanguage(url, options = {}) {
|
|
|
139
143
|
design.materialLanguage = safeExtract(extractMaterialLanguage, design) || { label: 'flat', confidence: 0, signals: [], metrics: {} };
|
|
140
144
|
design.imageryStyle = safeExtract(extractImageryStyle, rawData.light?.images || []) || { label: 'none', confidence: 0, counts: {}, signals: [] };
|
|
141
145
|
design.seo = safeExtract(extractSeo, rawData) || { openGraph: {}, twitter: {}, structuredData: [], score: {} };
|
|
146
|
+
design.iconSystem = safeExtract(extractIconSystem, rawData.light?.icons || []) || { library: 'unknown', confidence: 0, stats: {}, signals: [], icons: [] };
|
|
147
|
+
design.backgroundPatterns = safeExtract(extractBackgroundPatterns, rawData) || { labels: ['plain'], counts: {}, gradientTotals: {}, samples: [] };
|
|
148
|
+
design.stackIntel = safeExtract(extractStackIntel, rawData.light?.stack || {}) || { cms: [], analytics: [], experimentation: [] };
|
|
149
|
+
design.formStates = safeExtract(extractFormStates, rawData, design) || { flags: [], forms: { count: 0, families: [] }, modals: [], toastLibraries: [] };
|
|
142
150
|
// Stash raw crawler output so downstream orchestration (multipage, smart)
|
|
143
151
|
// can rebuild the digest without re-crawling.
|
|
144
152
|
design._raw = rawData;
|
|
@@ -209,6 +217,10 @@ export { pairDarkMode } from './extractors/dark-mode-pair.js';
|
|
|
209
217
|
export { captureResponsiveScreenshots } from './extractors/responsive-screenshots.js';
|
|
210
218
|
export { captureCoreWebVitals, extractFontLoading } from './extractors/perf.js';
|
|
211
219
|
export { extractSeo } from './extractors/seo.js';
|
|
220
|
+
export { extractIconSystem } from './extractors/icon-system.js';
|
|
221
|
+
export { extractBackgroundPatterns } from './extractors/background-patterns.js';
|
|
222
|
+
export { extractStackIntel } from './extractors/stack-intel.js';
|
|
223
|
+
export { extractFormStates } from './extractors/form-states.js';
|
|
212
224
|
export { refineWithSmart } from './classifiers/smart.js';
|
|
213
225
|
export { crawlCanonicalPages, computeCrossPageConsistency, discoverCanonicalPages } from './multipage.js';
|
|
214
226
|
export { buildPromptPack, formatV0Prompt, formatLovablePrompt, formatCursorPrompt, formatClaudeArtifactPrompt } from './formatters/prompt-pack.js';
|