designlang 10.3.0 → 10.4.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [10.4.0] — 2026-04-22
4
+
5
+ **Identification trio: icon system, background patterns, stack intel.**
6
+
7
+ ### Added
8
+
9
+ - **`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.
10
+ - **`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`.
11
+ - **`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.
12
+ - Bin reads its own version from `package.json` — no more per-release version drift in the CLI.
13
+ - New outputs: `*-icon-system.json`, `*-stack-intel.json`.
14
+
3
15
  ## [10.3.0] — 2026-04-22
4
16
 
5
17
  **Perf + SEO.** designlang now doubles as a lightweight auditor.
@@ -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('10.3.0');
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,12 @@ 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
+ }
369
379
  if (merged.prompts !== false) {
370
380
  const pack = buildPromptPack(design);
371
381
  const promptsDir = join(outDir, `${prefix}-prompts`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "designlang",
3
- "version": "10.3.0",
3
+ "version": "10.4.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,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,9 @@ 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';
37
40
  import { formatDtcgTokens } from './formatters/dtcg-tokens.js';
38
41
  import { formatMotionTokens } from './formatters/motion-tokens.js';
39
42
 
@@ -139,6 +142,9 @@ export async function extractDesignLanguage(url, options = {}) {
139
142
  design.materialLanguage = safeExtract(extractMaterialLanguage, design) || { label: 'flat', confidence: 0, signals: [], metrics: {} };
140
143
  design.imageryStyle = safeExtract(extractImageryStyle, rawData.light?.images || []) || { label: 'none', confidence: 0, counts: {}, signals: [] };
141
144
  design.seo = safeExtract(extractSeo, rawData) || { openGraph: {}, twitter: {}, structuredData: [], score: {} };
145
+ design.iconSystem = safeExtract(extractIconSystem, rawData.light?.icons || []) || { library: 'unknown', confidence: 0, stats: {}, signals: [], icons: [] };
146
+ design.backgroundPatterns = safeExtract(extractBackgroundPatterns, rawData) || { labels: ['plain'], counts: {}, gradientTotals: {}, samples: [] };
147
+ design.stackIntel = safeExtract(extractStackIntel, rawData.light?.stack || {}) || { cms: [], analytics: [], experimentation: [] };
142
148
  // Stash raw crawler output so downstream orchestration (multipage, smart)
143
149
  // can rebuild the digest without re-crawling.
144
150
  design._raw = rawData;
@@ -209,6 +215,9 @@ export { pairDarkMode } from './extractors/dark-mode-pair.js';
209
215
  export { captureResponsiveScreenshots } from './extractors/responsive-screenshots.js';
210
216
  export { captureCoreWebVitals, extractFontLoading } from './extractors/perf.js';
211
217
  export { extractSeo } from './extractors/seo.js';
218
+ export { extractIconSystem } from './extractors/icon-system.js';
219
+ export { extractBackgroundPatterns } from './extractors/background-patterns.js';
220
+ export { extractStackIntel } from './extractors/stack-intel.js';
212
221
  export { refineWithSmart } from './classifiers/smart.js';
213
222
  export { crawlCanonicalPages, computeCrossPageConsistency, discoverCanonicalPages } from './multipage.js';
214
223
  export { buildPromptPack, formatV0Prompt, formatLovablePrompt, formatCursorPrompt, formatClaudeArtifactPrompt } from './formatters/prompt-pack.js';