designlang 10.1.0 → 10.2.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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [10.2.0] — 2026-04-22
4
+
5
+ **Dark mode pairing + responsive screenshots.** Joins the light & dark extractor passes into semantic pairs, and adds full-page captures at 4 breakpoints × (light, dark).
6
+
7
+ ### Added
8
+
9
+ - **`src/extractors/dark-mode-pair.js`** — pure function that maps light ↔ dark pairs for primary/secondary/accent/background/text roles and every CSS variable that actually differs between themes. Emits a drop-in Tailwind `darkMode: 'class'` config plus an audit (tokens missing from either pass).
10
+ - **`src/extractors/responsive-screenshots.js`** — full-page PNGs at mobile / tablet / desktop / wide × (light, dark). Writes to `screenshots/responsive/<breakpoint>-<scheme>.png` with an index.
11
+ - New flag `--responsive-shots`. Auto-on with `--full`.
12
+ - New outputs: `*-dark-mode.json`, `*-responsive.json`.
13
+
14
+ ### Changed
15
+
16
+ - CLI version test now reads from `package.json` instead of a hardcoded string — no per-release test churn going forward.
17
+
3
18
  ## [10.1.0] — 2026-04-22
4
19
 
5
20
  **Component screenshots.** The existing `--screenshots` flag now emits cluster-aware, retina (2×), multi-variant PNGs instead of five hardcoded selectors and a full-page image.
@@ -10,6 +10,8 @@ import { refineWithSmart } from '../src/classifiers/smart.js';
10
10
  import { crawlCanonicalPages } from '../src/multipage.js';
11
11
  import { extractLogo } from '../src/extractors/logo.js';
12
12
  import { captureComponentScreenshotsV10 } from '../src/extractors/component-screenshots.js';
13
+ import { pairDarkMode } from '../src/extractors/dark-mode-pair.js';
14
+ import { captureResponsiveScreenshots } from '../src/extractors/responsive-screenshots.js';
13
15
  import { buildPromptPack } from '../src/formatters/prompt-pack.js';
14
16
  import { formatMarkdown } from '../src/formatters/markdown.js';
15
17
  import { formatTokens } from '../src/formatters/tokens.js';
@@ -53,7 +55,7 @@ const program = new Command();
53
55
  program
54
56
  .name('designlang')
55
57
  .description('Extract the complete design language from any website')
56
- .version('10.1.0');
58
+ .version('10.2.0');
57
59
 
58
60
  // ── Main command: extract ──────────────────────────────────────
59
61
  program
@@ -85,6 +87,7 @@ program
85
87
  .option('--smart', 'use optional LLM fallback when heuristic classifiers have low confidence (needs OPENAI_API_KEY or ANTHROPIC_API_KEY)')
86
88
  .option('--pages <n>', 'crawl N canonical pages (pricing/docs/blog/about/product) in addition to the homepage', parseInt)
87
89
  .option('--no-prompts', 'skip writing the prompt-pack directory')
90
+ .option('--responsive-shots', 'capture full-page PNGs at 4 breakpoints × (light,dark)')
88
91
  .option('--json', 'output raw JSON to stdout (for CI/CD)')
89
92
  .option('--json-pretty', 'output formatted JSON to stdout')
90
93
  .option('--no-history', 'skip saving to history')
@@ -233,6 +236,20 @@ program
233
236
  } catch (e) { design.componentScreenshots = { error: e.message }; }
234
237
  }
235
238
 
239
+ // v10.2: dark-mode pairing (pure, based on already-extracted data).
240
+ design.darkModePaired = pairDarkMode(design);
241
+
242
+ // v10.2: responsive screenshots at 4 breakpoints × (light, dark).
243
+ if (merged.full || merged.responsiveShots) {
244
+ spinner.text = 'Capturing responsive screenshots...';
245
+ try {
246
+ design.responsiveShots = await captureResponsiveScreenshots(url, outDir, {
247
+ includeDark: merged.dark || merged.full,
248
+ channel: merged.systemChrome ? 'chrome' : undefined,
249
+ });
250
+ } catch (e) { design.responsiveShots = { error: e.message }; }
251
+ }
252
+
236
253
  // v10: multi-page canonical crawl (pricing/docs/blog/about/product).
237
254
  const pagesArg = merged.pages != null ? merged.pages : (merged.full ? 5 : 0);
238
255
  if (pagesArg > 0) {
@@ -322,6 +339,12 @@ program
322
339
  if (design.componentScreenshots && (design.componentScreenshots.components || []).length) {
323
340
  files.push({ name: `${prefix}-screenshots.json`, content: JSON.stringify(design.componentScreenshots, null, 2), label: 'Component Screenshots index' });
324
341
  }
342
+ if (design.darkModePaired && design.darkModePaired.available) {
343
+ files.push({ name: `${prefix}-dark-mode.json`, content: JSON.stringify(design.darkModePaired, null, 2), label: 'Dark Mode Pairing' });
344
+ }
345
+ if (design.responsiveShots && Array.isArray(design.responsiveShots.shots) && design.responsiveShots.shots.length) {
346
+ files.push({ name: `${prefix}-responsive.json`, content: JSON.stringify(design.responsiveShots, null, 2), label: 'Responsive Screenshots index' });
347
+ }
325
348
  if (merged.prompts !== false) {
326
349
  const pack = buildPromptPack(design);
327
350
  const promptsDir = join(outDir, `${prefix}-prompts`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "designlang",
3
- "version": "10.1.0",
3
+ "version": "10.2.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,96 @@
1
+ // v10.2 — Dark Mode Pairing
2
+ //
3
+ // When the user passes --dark the crawler runs a second pass under
4
+ // `color-scheme: dark` and the main extractor emits `design.darkMode` with
5
+ // parallel colors + CSS variable maps. This module joins the two halves into
6
+ // *semantic pairs* — same role in light mode ↔ same role in dark mode — so
7
+ // downstream consumers (prompt pack, Tailwind darkMode config, agents) can
8
+ // author a single `[data-theme="dark"]` override block without guessing.
9
+ //
10
+ // Pure function. No Page handle, no side effects — takes the finished design
11
+ // object and returns a plain JSON structure.
12
+
13
+ function hexOf(c) {
14
+ if (!c) return null;
15
+ if (typeof c === 'string') return c.toLowerCase();
16
+ return (c.hex || '').toLowerCase();
17
+ }
18
+
19
+ function pairRoleColors(light = {}, dark = {}) {
20
+ const pairs = {};
21
+ for (const role of ['primary', 'secondary', 'accent']) {
22
+ const l = hexOf(light[role]);
23
+ const d = hexOf(dark[role]);
24
+ if (l || d) pairs[role] = { light: l, dark: d };
25
+ }
26
+ const bgPair = {
27
+ light: (light.backgrounds?.[0]?.hex || null),
28
+ dark: (dark.backgrounds?.[0]?.hex || null),
29
+ };
30
+ if (bgPair.light || bgPair.dark) pairs.background = bgPair;
31
+ const textPair = {
32
+ light: (light.text?.[0]?.hex || null),
33
+ dark: (dark.text?.[0]?.hex || null),
34
+ };
35
+ if (textPair.light || textPair.dark) pairs.text = textPair;
36
+ return pairs;
37
+ }
38
+
39
+ function pairVariables(lightVars = {}, darkVars = {}) {
40
+ // Walk the union of keys; emit light/dark only when values differ.
41
+ const keys = new Set([...Object.keys(lightVars || {}), ...Object.keys(darkVars || {})]);
42
+ const out = {};
43
+ for (const k of keys) {
44
+ const l = lightVars[k];
45
+ const d = darkVars[k];
46
+ if (l == null && d == null) continue;
47
+ if (typeof l === 'string' && typeof d === 'string' && l === d) continue;
48
+ out[k] = { light: l ?? null, dark: d ?? null };
49
+ }
50
+ return out;
51
+ }
52
+
53
+ export function pairDarkMode(design = {}) {
54
+ if (!design.darkMode) {
55
+ return { available: false, reason: 'no --dark pass captured' };
56
+ }
57
+ const lightColors = design.colors || {};
58
+ const darkColors = design.darkMode.colors || {};
59
+ const roles = pairRoleColors(lightColors, darkColors);
60
+ const variables = pairVariables(design.variables || {}, design.darkMode.variables || {});
61
+ const pairedVarCount = Object.keys(variables).length;
62
+
63
+ // Light colors that don't appear in dark (and vice versa) signal tokens the
64
+ // site forgot to theme — useful for an audit.
65
+ const lightSet = new Set((lightColors.all || []).map(c => (c.hex || '').toLowerCase()).filter(Boolean));
66
+ const darkSet = new Set((darkColors.all || []).map(c => (c.hex || '').toLowerCase()).filter(Boolean));
67
+ const missingInDark = [...lightSet].filter(x => !darkSet.has(x)).slice(0, 20);
68
+ const missingInLight = [...darkSet].filter(x => !lightSet.has(x)).slice(0, 20);
69
+
70
+ // Tailwind-ready config snippet.
71
+ const tailwind = {
72
+ darkMode: 'class',
73
+ theme: {
74
+ extend: {
75
+ colors: Object.fromEntries(
76
+ Object.entries(roles)
77
+ .filter(([, v]) => v.light && v.dark)
78
+ .map(([role, v]) => [role, { DEFAULT: v.light, dark: v.dark }]),
79
+ ),
80
+ },
81
+ },
82
+ };
83
+
84
+ return {
85
+ available: true,
86
+ roles,
87
+ variables,
88
+ pairedVarCount,
89
+ audit: {
90
+ missingInDark,
91
+ missingInLight,
92
+ coverage: pairedVarCount > 0 ? 'paired' : 'light-only-vars',
93
+ },
94
+ tailwind,
95
+ };
96
+ }
@@ -0,0 +1,55 @@
1
+ // v10.2 — Responsive Screenshots
2
+ //
3
+ // Full-page PNGs at each breakpoint × (light, dark). Lives alongside the
4
+ // component screenshots dir so output stays organised. Writes to
5
+ // `screenshots/responsive/<breakpoint>-<scheme>.png` and returns an index.
6
+
7
+ import { chromium } from 'playwright';
8
+ import { mkdirSync } from 'fs';
9
+ import { join } from 'path';
10
+
11
+ const BREAKPOINTS = [
12
+ { slug: 'mobile', width: 375, height: 812 },
13
+ { slug: 'tablet', width: 768, height: 1024 },
14
+ { slug: 'desktop', width: 1280, height: 800 },
15
+ { slug: 'wide', width: 1920, height: 1080 },
16
+ ];
17
+
18
+ async function captureAt(url, dir, bp, scheme, channel) {
19
+ const browser = await chromium.launch({ headless: true, ...(channel && { channel }) });
20
+ try {
21
+ const ctx = await browser.newContext({
22
+ viewport: { width: bp.width, height: bp.height },
23
+ deviceScaleFactor: bp.slug === 'mobile' ? 2 : 1,
24
+ colorScheme: scheme,
25
+ });
26
+ const page = await ctx.newPage();
27
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {});
28
+ await page.waitForLoadState('networkidle').catch(() => {});
29
+ await page.evaluate(() => document.fonts.ready).catch(() => {});
30
+ const file = `${bp.slug}-${scheme}.png`;
31
+ const path = join(dir, file);
32
+ await page.screenshot({ path, fullPage: true });
33
+ return { breakpoint: bp.slug, scheme, width: bp.width, path: `screenshots/responsive/${file}` };
34
+ } finally {
35
+ await browser.close();
36
+ }
37
+ }
38
+
39
+ export async function captureResponsiveScreenshots(url, outDir, { includeDark = true, channel } = {}) {
40
+ const dir = join(outDir, 'screenshots', 'responsive');
41
+ mkdirSync(dir, { recursive: true });
42
+ const out = [];
43
+ const schemes = includeDark ? ['light', 'dark'] : ['light'];
44
+ for (const bp of BREAKPOINTS) {
45
+ for (const scheme of schemes) {
46
+ try {
47
+ const row = await captureAt(url, dir, bp, scheme, channel);
48
+ out.push(row);
49
+ } catch (e) {
50
+ out.push({ breakpoint: bp.slug, scheme, error: e.message });
51
+ }
52
+ }
53
+ }
54
+ return { count: out.filter(r => !r.error).length, shots: out };
55
+ }
package/src/index.js CHANGED
@@ -203,6 +203,8 @@ export { extractMaterialLanguage } from './extractors/material-language.js';
203
203
  export { extractImageryStyle } from './extractors/imagery-style.js';
204
204
  export { extractLogo } from './extractors/logo.js';
205
205
  export { captureComponentScreenshotsV10 } from './extractors/component-screenshots.js';
206
+ export { pairDarkMode } from './extractors/dark-mode-pair.js';
207
+ export { captureResponsiveScreenshots } from './extractors/responsive-screenshots.js';
206
208
  export { refineWithSmart } from './classifiers/smart.js';
207
209
  export { crawlCanonicalPages, computeCrossPageConsistency, discoverCanonicalPages } from './multipage.js';
208
210
  export { buildPromptPack, formatV0Prompt, formatLovablePrompt, formatCursorPrompt, formatClaudeArtifactPrompt } from './formatters/prompt-pack.js';