designlang 10.4.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 +13 -0
- package/bin/design-extract.js +3 -0
- package/package.json +1 -1
- package/src/extractors/form-states.js +109 -0
- package/src/index.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
3
16
|
## [10.4.0] — 2026-04-22
|
|
4
17
|
|
|
5
18
|
**Identification trio: icon system, background patterns, stack intel.**
|
package/bin/design-extract.js
CHANGED
|
@@ -376,6 +376,9 @@ program
|
|
|
376
376
|
if (design.stackIntel) {
|
|
377
377
|
files.push({ name: `${prefix}-stack-intel.json`, content: JSON.stringify(design.stackIntel, null, 2), label: 'Stack Intel (CMS/analytics/experimentation)' });
|
|
378
378
|
}
|
|
379
|
+
if (design.formStates) {
|
|
380
|
+
files.push({ name: `${prefix}-form-states.json`, content: JSON.stringify(design.formStates, null, 2), label: 'Forms + States' });
|
|
381
|
+
}
|
|
379
382
|
if (merged.prompts !== false) {
|
|
380
383
|
const pack = buildPromptPack(design);
|
|
381
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,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
|
+
}
|
package/src/index.js
CHANGED
|
@@ -37,6 +37,7 @@ import { extractSeo } from './extractors/seo.js';
|
|
|
37
37
|
import { extractIconSystem } from './extractors/icon-system.js';
|
|
38
38
|
import { extractBackgroundPatterns } from './extractors/background-patterns.js';
|
|
39
39
|
import { extractStackIntel } from './extractors/stack-intel.js';
|
|
40
|
+
import { extractFormStates } from './extractors/form-states.js';
|
|
40
41
|
import { formatDtcgTokens } from './formatters/dtcg-tokens.js';
|
|
41
42
|
import { formatMotionTokens } from './formatters/motion-tokens.js';
|
|
42
43
|
|
|
@@ -145,6 +146,7 @@ export async function extractDesignLanguage(url, options = {}) {
|
|
|
145
146
|
design.iconSystem = safeExtract(extractIconSystem, rawData.light?.icons || []) || { library: 'unknown', confidence: 0, stats: {}, signals: [], icons: [] };
|
|
146
147
|
design.backgroundPatterns = safeExtract(extractBackgroundPatterns, rawData) || { labels: ['plain'], counts: {}, gradientTotals: {}, samples: [] };
|
|
147
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: [] };
|
|
148
150
|
// Stash raw crawler output so downstream orchestration (multipage, smart)
|
|
149
151
|
// can rebuild the digest without re-crawling.
|
|
150
152
|
design._raw = rawData;
|
|
@@ -218,6 +220,7 @@ export { extractSeo } from './extractors/seo.js';
|
|
|
218
220
|
export { extractIconSystem } from './extractors/icon-system.js';
|
|
219
221
|
export { extractBackgroundPatterns } from './extractors/background-patterns.js';
|
|
220
222
|
export { extractStackIntel } from './extractors/stack-intel.js';
|
|
223
|
+
export { extractFormStates } from './extractors/form-states.js';
|
|
221
224
|
export { refineWithSmart } from './classifiers/smart.js';
|
|
222
225
|
export { crawlCanonicalPages, computeCrossPageConsistency, discoverCanonicalPages } from './multipage.js';
|
|
223
226
|
export { buildPromptPack, formatV0Prompt, formatLovablePrompt, formatCursorPrompt, formatClaudeArtifactPrompt } from './formatters/prompt-pack.js';
|