dembrandt 0.6.1 → 0.7.1
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/README.md +45 -1
- package/index.js +41 -2
- package/lib/extractors.js +186 -10
- package/lib/pdf.js +970 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -6,7 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
Extract any website’s design system into design tokens in a few seconds: logo, colors, typography, borders, and more. One command.
|
|
8
8
|
|
|
9
|
-

|
|
10
|
+
|
|
11
|
+
**CLI output**
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
**Brand Guide PDF**
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
**Local UI**
|
|
20
|
+
|
|
21
|
+

|
|
10
22
|
|
|
11
23
|
## Install
|
|
12
24
|
|
|
@@ -41,6 +53,7 @@ dembrandt bmw.de --dtcg # Export in W3C Design Tokens (DTCG) format (
|
|
|
41
53
|
dembrandt bmw.de --dark-mode # Extract colors from dark mode variant
|
|
42
54
|
dembrandt bmw.de --mobile # Use mobile viewport (390x844, iPhone 12/13/14/15) for responsive analysis
|
|
43
55
|
dembrandt bmw.de --slow # 3x longer timeouts (24s hydration) for JavaScript-heavy sites
|
|
56
|
+
dembrandt bmw.de --brand-guide # Generate a brand guide PDF
|
|
44
57
|
dembrandt bmw.de --no-sandbox # Disable Chromium sandbox (required for Docker/CI)
|
|
45
58
|
dembrandt bmw.de --browser=firefox # Use Firefox instead of Chromium (better for Cloudflare bypass)
|
|
46
59
|
```
|
|
@@ -82,6 +95,37 @@ dembrandt stripe.com --dtcg
|
|
|
82
95
|
|
|
83
96
|
The DTCG format is an industry-standard JSON schema that can be consumed by design tools and token transformation libraries like [Style Dictionary](https://styledictionary.com).
|
|
84
97
|
|
|
98
|
+
## Local UI
|
|
99
|
+
|
|
100
|
+
Browse your extracted brands in a visual interface.
|
|
101
|
+
|
|
102
|
+
### Setup
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
cd local-ui
|
|
106
|
+
npm install
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Running
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm start
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Opens http://localhost:5173 with API on port 3002.
|
|
116
|
+
|
|
117
|
+
### Features
|
|
118
|
+
|
|
119
|
+
- Visual grid of all extracted brands
|
|
120
|
+
- Color palettes with click-to-copy
|
|
121
|
+
- Typography specimens
|
|
122
|
+
- Spacing, shadows, border radius visualization
|
|
123
|
+
- Button and link component previews
|
|
124
|
+
- Dark/light theme toggle
|
|
125
|
+
- Section nav links on extraction pages — jump directly to Colors, Typography, Shadows, etc. via a sticky sidebar
|
|
126
|
+
|
|
127
|
+
Extractions are performed via CLI (`dembrandt <url> --save-output`) and automatically appear in the UI.
|
|
128
|
+
|
|
85
129
|
## Use Cases
|
|
86
130
|
|
|
87
131
|
- Brand audits & competitive analysis
|
package/index.js
CHANGED
|
@@ -14,13 +14,14 @@ import { chromium, firefox } from "playwright-core";
|
|
|
14
14
|
import { extractBranding } from "./lib/extractors.js";
|
|
15
15
|
import { displayResults } from "./lib/display.js";
|
|
16
16
|
import { toW3CFormat } from "./lib/w3c-exporter.js";
|
|
17
|
+
import { generatePDF } from "./lib/pdf.js";
|
|
17
18
|
import { writeFileSync, mkdirSync } from "fs";
|
|
18
19
|
import { join } from "path";
|
|
19
20
|
|
|
20
21
|
program
|
|
21
22
|
.name("dembrandt")
|
|
22
23
|
.description("Extract design tokens from any website")
|
|
23
|
-
.version("0.
|
|
24
|
+
.version("0.7.1")
|
|
24
25
|
.argument("<url>")
|
|
25
26
|
.option("--browser <type>", "Browser to use (chromium|firefox)", "chromium")
|
|
26
27
|
.option("--json-only", "Output raw JSON")
|
|
@@ -29,7 +30,10 @@ program
|
|
|
29
30
|
.option("--dark-mode", "Extract colors from dark mode")
|
|
30
31
|
.option("--mobile", "Extract from mobile viewport")
|
|
31
32
|
.option("--slow", "3x longer timeouts for slow-loading sites")
|
|
33
|
+
.option("--brand-guide", "Export a brand guide PDF")
|
|
32
34
|
.option("--no-sandbox", "Disable browser sandbox (needed for Docker/CI)")
|
|
35
|
+
.option("--raw-colors", "Include pre-filter raw colors in JSON output")
|
|
36
|
+
.option("--screenshot <path>", "Save a screenshot of the page")
|
|
33
37
|
.action(async (input, opts) => {
|
|
34
38
|
let url = input;
|
|
35
39
|
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
@@ -68,6 +72,7 @@ program
|
|
|
68
72
|
darkMode: opts.darkMode,
|
|
69
73
|
mobile: opts.mobile,
|
|
70
74
|
slow: opts.slow,
|
|
75
|
+
screenshotPath: opts.screenshot,
|
|
71
76
|
});
|
|
72
77
|
break;
|
|
73
78
|
} catch (err) {
|
|
@@ -95,11 +100,16 @@ program
|
|
|
95
100
|
|
|
96
101
|
console.log();
|
|
97
102
|
|
|
103
|
+
// Strip raw colors unless --raw-colors flag is set
|
|
104
|
+
if (!opts.rawColors && result.colors && result.colors.rawColors) {
|
|
105
|
+
delete result.colors.rawColors;
|
|
106
|
+
}
|
|
107
|
+
|
|
98
108
|
// Convert to W3C format if requested
|
|
99
109
|
const outputData = opts.dtcg ? toW3CFormat(result) : result;
|
|
100
110
|
|
|
101
111
|
// Save JSON output if --save-output or --dtcg is specified
|
|
102
|
-
if (
|
|
112
|
+
if (opts.saveOutput || opts.dtcg) {
|
|
103
113
|
try {
|
|
104
114
|
const domain = new URL(url).hostname.replace("www.", "");
|
|
105
115
|
const timestamp = new Date()
|
|
@@ -129,6 +139,35 @@ program
|
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
141
|
|
|
142
|
+
// Generate PDF brand guide
|
|
143
|
+
if (opts.brandGuide) {
|
|
144
|
+
try {
|
|
145
|
+
const pdfDomain = new URL(url).hostname.replace("www.", "");
|
|
146
|
+
const now = new Date();
|
|
147
|
+
const pdfDate = now.toISOString().slice(0, 10);
|
|
148
|
+
const pdfTime = `${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}`;
|
|
149
|
+
const pdfDir = join(process.cwd(), "output", pdfDomain);
|
|
150
|
+
mkdirSync(pdfDir, { recursive: true });
|
|
151
|
+
const pdfFilename = `${pdfDomain}-brand-guide-${pdfDate}-${pdfTime}.pdf`;
|
|
152
|
+
const pdfPath = join(pdfDir, pdfFilename);
|
|
153
|
+
spinner.start("Generating PDF brand guide...");
|
|
154
|
+
await generatePDF(result, pdfPath, browser);
|
|
155
|
+
spinner.stop();
|
|
156
|
+
console.log(
|
|
157
|
+
chalk.dim(
|
|
158
|
+
`PDF saved to: ${chalk.hex('#8BE9FD')(
|
|
159
|
+
`output/${pdfDomain}/${pdfFilename}`
|
|
160
|
+
)}`
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
spinner.stop();
|
|
165
|
+
console.log(
|
|
166
|
+
chalk.hex('#FFB86C')(`Could not generate PDF: ${err.message}`)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
132
171
|
// Output to terminal
|
|
133
172
|
if (opts.jsonOnly) {
|
|
134
173
|
console.log(JSON.stringify(outputData, null, 2));
|
package/lib/extractors.js
CHANGED
|
@@ -206,7 +206,7 @@ export async function extractBranding(
|
|
|
206
206
|
spinner.stop();
|
|
207
207
|
console.log(chalk.hex('#8BE9FD')("\n Extracting design tokens...\n"));
|
|
208
208
|
|
|
209
|
-
spinner.start("Analyzing design system (
|
|
209
|
+
spinner.start("Analyzing design system (15 parallel tasks)...");
|
|
210
210
|
const [
|
|
211
211
|
{ logo, favicons },
|
|
212
212
|
colors,
|
|
@@ -222,6 +222,7 @@ export async function extractBranding(
|
|
|
222
222
|
breakpoints,
|
|
223
223
|
iconSystem,
|
|
224
224
|
frameworks,
|
|
225
|
+
siteName,
|
|
225
226
|
] = await Promise.all([
|
|
226
227
|
extractLogo(page, url),
|
|
227
228
|
extractColors(page),
|
|
@@ -237,6 +238,7 @@ export async function extractBranding(
|
|
|
237
238
|
extractBreakpoints(page),
|
|
238
239
|
detectIconSystem(page),
|
|
239
240
|
detectFrameworks(page),
|
|
241
|
+
extractSiteName(page),
|
|
240
242
|
]);
|
|
241
243
|
|
|
242
244
|
spinner.stop();
|
|
@@ -584,6 +586,7 @@ export async function extractBranding(
|
|
|
584
586
|
const result = {
|
|
585
587
|
url: page.url(),
|
|
586
588
|
extractedAt: new Date().toISOString(),
|
|
589
|
+
siteName,
|
|
587
590
|
logo,
|
|
588
591
|
favicons,
|
|
589
592
|
colors,
|
|
@@ -616,6 +619,18 @@ export async function extractBranding(
|
|
|
616
619
|
result.isCanvasOnly = true;
|
|
617
620
|
}
|
|
618
621
|
|
|
622
|
+
// Save page screenshot if requested
|
|
623
|
+
if (options.screenshotPath) {
|
|
624
|
+
try {
|
|
625
|
+
const { mkdirSync } = await import("fs");
|
|
626
|
+
const { dirname } = await import("path");
|
|
627
|
+
mkdirSync(dirname(options.screenshotPath), { recursive: true });
|
|
628
|
+
await page.screenshot({ path: options.screenshotPath, fullPage: false });
|
|
629
|
+
} catch (err) {
|
|
630
|
+
console.error(chalk.dim(` ↳ Screenshot failed: ${err.message}`));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
619
634
|
return result;
|
|
620
635
|
} catch (error) {
|
|
621
636
|
spinner.fail("Extraction failed");
|
|
@@ -626,6 +641,47 @@ export async function extractBranding(
|
|
|
626
641
|
}
|
|
627
642
|
}
|
|
628
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Extract the site/company name from meta tags, title, or structured data
|
|
646
|
+
*/
|
|
647
|
+
async function extractSiteName(page) {
|
|
648
|
+
return await page.evaluate(() => {
|
|
649
|
+
// 1. og:site_name - most reliable
|
|
650
|
+
const ogSiteName = document.querySelector('meta[property="og:site_name"]');
|
|
651
|
+
if (ogSiteName?.content?.trim()) return ogSiteName.content.trim();
|
|
652
|
+
|
|
653
|
+
// 2. application-name
|
|
654
|
+
const appName = document.querySelector('meta[name="application-name"]');
|
|
655
|
+
if (appName?.content?.trim()) return appName.content.trim();
|
|
656
|
+
|
|
657
|
+
// 3. JSON-LD structured data
|
|
658
|
+
const ldScripts = document.querySelectorAll('script[type="application/ld+json"]');
|
|
659
|
+
for (const s of ldScripts) {
|
|
660
|
+
try {
|
|
661
|
+
const data = JSON.parse(s.textContent);
|
|
662
|
+
const items = Array.isArray(data) ? data : data?.['@graph'] || [data];
|
|
663
|
+
for (const obj of items) {
|
|
664
|
+
if (obj?.name && typeof obj.name === 'string') return obj.name;
|
|
665
|
+
if (obj?.organization?.name) return obj.organization.name;
|
|
666
|
+
}
|
|
667
|
+
} catch {}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// 4. Title tag - take part before separator
|
|
671
|
+
const title = document.title?.trim();
|
|
672
|
+
if (title) {
|
|
673
|
+
const sep = title.match(/(.+?)\s*[|\-–—:]\s*/);
|
|
674
|
+
if (sep && sep[1].length > 1 && sep[1].length < 40) return sep[1].trim();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// 5. Logo alt text
|
|
678
|
+
const logoImg = document.querySelector('img[class*="logo"], img[id*="logo"], a[class*="logo"] img');
|
|
679
|
+
if (logoImg?.alt?.trim() && logoImg.alt.length < 40) return logoImg.alt.trim();
|
|
680
|
+
|
|
681
|
+
return null;
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
629
685
|
/**
|
|
630
686
|
* Extract logo information from the page
|
|
631
687
|
* Looks for common logo patterns: img with logo in class/id, SVG logos, etc.
|
|
@@ -668,13 +724,91 @@ async function extractLogo(page, url) {
|
|
|
668
724
|
}
|
|
669
725
|
}
|
|
670
726
|
|
|
727
|
+
// Include header SVGs/images that link to homepage (likely site logo)
|
|
728
|
+
const inHeader = el.closest('header, nav, [role="banner"], [class*="header"], [class*="Header"], [id*="header"]');
|
|
729
|
+
if (inHeader) {
|
|
730
|
+
const parentLink = el.closest('a');
|
|
731
|
+
if (parentLink) {
|
|
732
|
+
const href = parentLink.getAttribute('href') || '';
|
|
733
|
+
const ariaLabel = (parentLink.getAttribute('aria-label') || '').toLowerCase();
|
|
734
|
+
// Check for homepage link patterns (including /country/ paths like /fi/, /en-us/)
|
|
735
|
+
if (href === '/' || href === baseUrl || href === baseUrl + '/' ||
|
|
736
|
+
href.match(/^https?:\/\/[^/]+\/?$/) ||
|
|
737
|
+
href.match(/^https?:\/\/[^/]+\/[a-z]{2}(-[a-z]{2})?\/?$/) ||
|
|
738
|
+
ariaLabel.includes('homepage') || ariaLabel.includes('home page')) {
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
671
744
|
return false;
|
|
672
745
|
}
|
|
673
746
|
);
|
|
674
747
|
|
|
675
748
|
let logoData = null;
|
|
676
749
|
if (candidates.length > 0) {
|
|
677
|
-
|
|
750
|
+
// Extract site domain for matching
|
|
751
|
+
const siteDomain = new URL(baseUrl).hostname.replace('www.', '').split('.')[0].toLowerCase();
|
|
752
|
+
|
|
753
|
+
// Score each candidate
|
|
754
|
+
const scored = candidates.map(el => {
|
|
755
|
+
let score = 0;
|
|
756
|
+
const rect = el.getBoundingClientRect();
|
|
757
|
+
const parentLink = el.closest('a');
|
|
758
|
+
const linkHref = parentLink?.getAttribute('href') || '';
|
|
759
|
+
const imgSrc = el.tagName === 'IMG' ? (el.src || '') : '';
|
|
760
|
+
const altText = (el.getAttribute('alt') || '').toLowerCase();
|
|
761
|
+
const className = (typeof el.className === 'string' ? el.className : el.className.baseVal || '').toLowerCase();
|
|
762
|
+
|
|
763
|
+
// 1. Prioritize logos in header/nav (+50)
|
|
764
|
+
const inHeader = el.closest('header, nav, [role="banner"], [class*="header"], [class*="nav"], [id*="header"], [id*="nav"]');
|
|
765
|
+
if (inHeader) score += 50;
|
|
766
|
+
|
|
767
|
+
// 2. URL/alt contains site domain (+40)
|
|
768
|
+
if (imgSrc.toLowerCase().includes(siteDomain) || altText.includes(siteDomain) || className.includes(siteDomain)) {
|
|
769
|
+
score += 40;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// 3. Links to homepage (+30)
|
|
773
|
+
if (parentLink) {
|
|
774
|
+
const href = linkHref.toLowerCase();
|
|
775
|
+
if (href === '/' || href === baseUrl || href === baseUrl + '/' || href.endsWith('://' + new URL(baseUrl).hostname + '/') || href.endsWith('://' + new URL(baseUrl).hostname)) {
|
|
776
|
+
score += 30;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// 4. Position scoring - top-left bias (+20 max)
|
|
781
|
+
// Logos in top 200px get bonus, left 400px gets bonus
|
|
782
|
+
if (rect.top < 200) score += 10;
|
|
783
|
+
if (rect.left < 400) score += 10;
|
|
784
|
+
|
|
785
|
+
// Penalty for logos far down the page
|
|
786
|
+
if (rect.top > 600) score -= 20;
|
|
787
|
+
|
|
788
|
+
// Penalty for very small or very large images (likely not logos)
|
|
789
|
+
const width = el.naturalWidth || el.width?.baseVal?.value || rect.width;
|
|
790
|
+
const height = el.naturalHeight || el.height?.baseVal?.value || rect.height;
|
|
791
|
+
if (width < 20 || height < 20) score -= 30;
|
|
792
|
+
if (width > 500 || height > 300) score -= 40;
|
|
793
|
+
|
|
794
|
+
// Penalty for descriptive alt text (likely content images, not logos)
|
|
795
|
+
if (altText.length > 50) score -= 30;
|
|
796
|
+
if (altText.includes(' the ') || altText.includes(' a ') || altText.includes(' of ')) score -= 20;
|
|
797
|
+
|
|
798
|
+
// Bonus for typical logo dimensions (width > height, reasonable size)
|
|
799
|
+
if (width > height && width < 300 && width > 40 && height > 15 && height < 100) score += 15;
|
|
800
|
+
|
|
801
|
+
// Strong penalty if not in header and no domain match
|
|
802
|
+
if (!inHeader && !imgSrc.toLowerCase().includes(siteDomain) && !altText.includes(siteDomain)) {
|
|
803
|
+
score -= 30;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return { el, score };
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// Sort by score descending and pick the best
|
|
810
|
+
scored.sort((a, b) => b.score - a.score);
|
|
811
|
+
const logo = scored[0].el;
|
|
678
812
|
const computed = window.getComputedStyle(logo);
|
|
679
813
|
const parent = logo.parentElement;
|
|
680
814
|
const parentComputed = parent ? window.getComputedStyle(parent) : null;
|
|
@@ -695,6 +829,18 @@ async function extractLogo(page, url) {
|
|
|
695
829
|
(parentComputed ? parseFloat(parentComputed.paddingLeft) : 0),
|
|
696
830
|
};
|
|
697
831
|
|
|
832
|
+
// Walk up the DOM to find the actual rendered background behind the logo
|
|
833
|
+
let logoBg = null;
|
|
834
|
+
let walker = logo;
|
|
835
|
+
while (walker) {
|
|
836
|
+
const bg = window.getComputedStyle(walker).backgroundColor;
|
|
837
|
+
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
|
|
838
|
+
logoBg = bg;
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
walker = walker.parentElement;
|
|
842
|
+
}
|
|
843
|
+
|
|
698
844
|
if (logo.tagName === "IMG") {
|
|
699
845
|
logoData = {
|
|
700
846
|
source: "img",
|
|
@@ -703,6 +849,7 @@ async function extractLogo(page, url) {
|
|
|
703
849
|
height: logo.naturalHeight || logo.height,
|
|
704
850
|
alt: logo.alt,
|
|
705
851
|
safeZone: safeZone,
|
|
852
|
+
background: logoBg,
|
|
706
853
|
};
|
|
707
854
|
} else {
|
|
708
855
|
// SVG logo - try to get the parent link or closest anchor
|
|
@@ -713,6 +860,7 @@ async function extractLogo(page, url) {
|
|
|
713
860
|
width: logo.width?.baseVal?.value,
|
|
714
861
|
height: logo.height?.baseVal?.value,
|
|
715
862
|
safeZone: safeZone,
|
|
863
|
+
background: logoBg,
|
|
716
864
|
};
|
|
717
865
|
}
|
|
718
866
|
}
|
|
@@ -1124,6 +1272,17 @@ async function extractColors(page) {
|
|
|
1124
1272
|
return Math.sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB);
|
|
1125
1273
|
}
|
|
1126
1274
|
|
|
1275
|
+
// Capture all colors before filtering (for QA/debugging)
|
|
1276
|
+
const rawColors = Array.from(colorMap.entries())
|
|
1277
|
+
.map(([normalizedColor, data]) => ({
|
|
1278
|
+
color: data.original,
|
|
1279
|
+
normalized: normalizedColor,
|
|
1280
|
+
count: data.count,
|
|
1281
|
+
score: data.score,
|
|
1282
|
+
sources: Array.from(data.sources).slice(0, 3),
|
|
1283
|
+
}))
|
|
1284
|
+
.sort((a, b) => b.count - a.count);
|
|
1285
|
+
|
|
1127
1286
|
const palette = Array.from(colorMap.entries())
|
|
1128
1287
|
.filter(([normalizedColor, data]) => {
|
|
1129
1288
|
// Filter out colors below threshold
|
|
@@ -1212,6 +1371,7 @@ async function extractColors(page) {
|
|
|
1212
1371
|
return {
|
|
1213
1372
|
semantic: semanticColors,
|
|
1214
1373
|
palette: perceptuallyDeduped,
|
|
1374
|
+
rawColors,
|
|
1215
1375
|
cssVariables: filteredCssVariables,
|
|
1216
1376
|
};
|
|
1217
1377
|
});
|
|
@@ -1630,7 +1790,7 @@ async function extractButtonStyles(page) {
|
|
|
1630
1790
|
color: computed.color,
|
|
1631
1791
|
padding: computed.padding,
|
|
1632
1792
|
borderRadius: computed.borderRadius,
|
|
1633
|
-
border: computed.border
|
|
1793
|
+
border: computed.border || `${computed.borderWidth} ${computed.borderStyle} ${computed.borderColor}`,
|
|
1634
1794
|
boxShadow: computed.boxShadow,
|
|
1635
1795
|
outline: computed.outline,
|
|
1636
1796
|
transform: computed.transform,
|
|
@@ -1653,11 +1813,25 @@ async function extractButtonStyles(page) {
|
|
|
1653
1813
|
const bg = computed.backgroundColor;
|
|
1654
1814
|
const border = computed.border;
|
|
1655
1815
|
const borderWidth = computed.borderWidth;
|
|
1656
|
-
const
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1816
|
+
const borderColor = computed.borderColor;
|
|
1817
|
+
const boxShadow = computed.boxShadow;
|
|
1818
|
+
|
|
1819
|
+
const hasBorder = borderWidth &&
|
|
1820
|
+
parseFloat(borderWidth) > 0 &&
|
|
1821
|
+
border !== 'none' &&
|
|
1822
|
+
borderColor !== 'rgba(0, 0, 0, 0)' &&
|
|
1823
|
+
borderColor !== 'transparent';
|
|
1824
|
+
|
|
1825
|
+
const hasBackground = bg &&
|
|
1826
|
+
bg !== 'rgba(0, 0, 0, 0)' &&
|
|
1827
|
+
bg !== 'transparent';
|
|
1828
|
+
|
|
1829
|
+
const hasShadow = boxShadow &&
|
|
1830
|
+
boxShadow !== 'none' &&
|
|
1831
|
+
boxShadow !== 'rgba(0, 0, 0, 0)';
|
|
1832
|
+
|
|
1833
|
+
// Skip only if background, border, and shadow are all missing/transparent
|
|
1834
|
+
if (!hasBackground && !hasBorder && !hasShadow) {
|
|
1661
1835
|
return;
|
|
1662
1836
|
}
|
|
1663
1837
|
|
|
@@ -1755,7 +1929,9 @@ async function extractButtonStyles(page) {
|
|
|
1755
1929
|
const seen = new Set();
|
|
1756
1930
|
|
|
1757
1931
|
for (const btn of buttonStyles) {
|
|
1758
|
-
const
|
|
1932
|
+
const s = btn.states.default;
|
|
1933
|
+
// Key includes background, border (width/style), and shadow to distinguish variants
|
|
1934
|
+
const key = `${s.backgroundColor}|${s.border}|${s.boxShadow}`;
|
|
1759
1935
|
if (!seen.has(key)) {
|
|
1760
1936
|
seen.add(key);
|
|
1761
1937
|
uniqueButtons.push(btn);
|
|
@@ -1825,7 +2001,7 @@ async function extractInputStyles(page) {
|
|
|
1825
2001
|
const defaultState = {
|
|
1826
2002
|
backgroundColor: computed.backgroundColor,
|
|
1827
2003
|
color: computed.color,
|
|
1828
|
-
border: computed.border
|
|
2004
|
+
border: computed.border || `${computed.borderWidth} ${computed.borderStyle} ${computed.borderColor}`,
|
|
1829
2005
|
borderRadius: computed.borderRadius,
|
|
1830
2006
|
padding: computed.padding,
|
|
1831
2007
|
boxShadow: computed.boxShadow,
|