@saeroon/cli 0.2.6 → 0.2.8

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.
Files changed (2) hide show
  1. package/dist/index.js +425 -66
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -2514,19 +2514,27 @@ var ELEMENT_TYPE_PROPS = {
2514
2514
  "pre",
2515
2515
  "code",
2516
2516
  "progress",
2517
- "meter"
2517
+ "meter",
2518
+ "output",
2519
+ "label",
2520
+ "search",
2521
+ "time",
2522
+ "mark",
2523
+ "abbr"
2518
2524
  ]),
2519
2525
  coerce: () => null
2520
2526
  }
2521
2527
  };
2522
2528
  var VALID_BLOCK_TYPES = /* @__PURE__ */ new Set([
2523
- // Primitives (11)
2529
+ // Primitives (13)
2524
2530
  "container",
2525
2531
  "text-block",
2532
+ "rich-text-block",
2526
2533
  "heading-block",
2527
2534
  "button-block",
2528
2535
  "image-block",
2529
2536
  "video-block",
2537
+ "audio-block",
2530
2538
  "embed-block",
2531
2539
  "icon-block",
2532
2540
  "input-block",
@@ -3940,10 +3948,12 @@ var EXTRACTION_SCRIPT = `
3940
3948
 
3941
3949
  function rgbToHex(rgb) {
3942
3950
  if (!rgb || rgb === 'transparent') return 'transparent';
3943
- if (rgb.startsWith('#')) return rgb;
3951
+ if (rgb.startsWith('#')) return rgb.length === 7 ? rgb : null;
3952
+ // rgb()/rgba()\uB9CC \uCC98\uB9AC, oklch/lab/color() \uB4F1 \uCD5C\uC2E0 \uD3EC\uB9F7\uC740 \uAC74\uB108\uB700
3953
+ if (!rgb.startsWith('rgb')) return null;
3944
3954
  const match = rgb.match(/\\d+/g);
3945
- if (!match || match.length < 3) return rgb;
3946
- const [r, g, b] = match.map(Number);
3955
+ if (!match || match.length < 3) return null;
3956
+ const [r, g, b] = match.map(n => Math.min(255, Math.max(0, Number(n))));
3947
3957
  return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
3948
3958
  }
3949
3959
 
@@ -3968,18 +3978,18 @@ var EXTRACTION_SCRIPT = `
3968
3978
 
3969
3979
  if (tag === 'header' || tag === 'nav') return 'header';
3970
3980
  if (tag === 'footer') return 'footer';
3971
- if (/hero|banner|jumbotron|splash|main-?visual/.test(combined)) return 'hero';
3972
- if (/feature|service|benefit|what-?we/.test(combined)) return 'features';
3973
- if (/testimonial|review|feedback|client|customer/.test(combined)) return 'testimonials';
3974
- if (/faq|accordion|question|q-?and-?a/.test(combined)) return 'faq';
3975
- if (/team|staff|member|about-?us|who-?we/.test(combined)) return 'team';
3976
- if (/pricing|plan|package/.test(combined)) return 'pricing';
3977
- if (/contact|inquiry|form|cta|call-?to-?action|get-?started/.test(combined)) return 'cta';
3978
- if (/gallery|portfolio|work|project|showcase/.test(combined)) return 'gallery';
3979
- if (/blog|news|article|post/.test(combined)) return 'blog';
3980
- if (/partner|client|logo|brand|trust/.test(combined)) return 'partners';
3981
- if (/map|location|address|direction/.test(combined)) return 'map';
3982
- if (/stat|counter|number|achievement/.test(combined)) return 'stats';
3981
+ if (/hero|banner|jumbotron|splash|main-?visual|\uBA54\uC778s?\uBE44\uC8FC\uC5BC|\uD788\uC5B4\uB85C/.test(combined)) return 'hero';
3982
+ if (/feature|service|benefit|what-?we|\uC5C5\uBB34s?\uC601\uC5ED|\uC11C\uBE44\uC2A4|\uC9C4\uB8CCs?\uACFC\uBAA9|\uCE58\uB8CCs?\uBC29\uBC95|\uBC95\uB960s?\uC11C\uBE44\uC2A4/.test(combined)) return 'features';
3983
+ if (/testimonial|review|feedback|client|customer|\uD6C4\uAE30|\uB9AC\uBDF0|\uC218\uAC15s?\uD6C4\uAE30|\uACE0\uAC1Ds?\uD6C4\uAE30/.test(combined)) return 'testimonials';
3984
+ if (/faq|accordion|question|q-?and-?a|\uC790\uC8FCs?\uBB3B\uB294|\uC9C8\uBB38/.test(combined)) return 'faq';
3985
+ if (/team|staff|member|about-?us|who-?we|\uC758\uB8CC\uC9C4|\uC6D0\uC7A5|\uBCC0\uD638\uC0AC|\uAC15\uC0AC|\uC18C\uAC1C|\uC778\uC0AC\uB9D0/.test(combined)) return 'team';
3986
+ if (/pricing|plan|package|\uAC00\uACA9|\uC694\uAE08|\uD50C\uB79C/.test(combined)) return 'pricing';
3987
+ if (/contact|inquiry|form|cta|call-?to-?action|get-?started|\uC0C1\uB2F4|\uBB38\uC758|\uC608\uC57D|\uC2E0\uCCAD/.test(combined)) return 'cta';
3988
+ if (/gallery|portfolio|work|project|showcase|\uAC24\uB7EC\uB9AC|\uB458\uB7EC\uBCF4\uAE30|\uC2DC\uC124|\uB0B4\uBD80/.test(combined)) return 'gallery';
3989
+ if (/blog|news|article|post|\uBE14\uB85C\uADF8|\uB274\uC2A4|\uC18C\uC2DD|\uCE7C\uB7FC/.test(combined)) return 'blog';
3990
+ if (/partner|client|logo|brand|trust|\uD30C\uD2B8\uB108|\uC81C\uD734/.test(combined)) return 'partners';
3991
+ if (/map|location|address|direction|\uC624\uC2DC\uB294s?\uAE38|\uCC3E\uC544\uC624\uC2DC\uB294s?\uAE38|\uC9C0\uB3C4|\uC704\uCE58/.test(combined)) return 'map';
3992
+ if (/stat|counter|number|achievement|\uC2E4\uC801|\uC131\uACFC|\uC218\uCE58/.test(combined)) return 'stats';
3983
3993
  return 'section';
3984
3994
  }
3985
3995
 
@@ -4019,11 +4029,79 @@ var EXTRACTION_SCRIPT = `
4019
4029
  // \u2500\u2500 1. Structure \u2500\u2500
4020
4030
  const sectionSelectors = 'body > header, body > footer, body > nav, body > main, body > section, body > div, body > article, body > aside';
4021
4031
  const topLevelEls = document.querySelectorAll(sectionSelectors);
4032
+
4033
+ // wrapper \uAC10\uC9C0: body \uC9C1\uACC4 \uC790\uC2DD\uC5D0\uC11C \uC2DC\uC791\uD558\uC5EC wrapper(\uC790\uC2DD 1\uAC1C) \uD480\uAE30
4034
+ let sectionEls = Array.from(topLevelEls).filter(el => el.getBoundingClientRect().height >= 10);
4035
+
4036
+ // \uC758\uBBF8 \uC788\uB294 \uC139\uC158\uC744 \uCC3E\uC744 \uB54C\uAE4C\uC9C0 wrapper\uB97C \uD480\uC5B4\uB098\uAC10 (\uCD5C\uB300 5\uB2E8\uACC4)
4037
+ function unwrapSections(els, depth) {
4038
+ if (depth <= 0) return els;
4039
+ const result = [];
4040
+ let expanded = false;
4041
+ els.forEach(el => {
4042
+ const tag = el.tagName.toLowerCase();
4043
+ // \uC2DC\uB9E8\uD2F1 \uD0DC\uADF8: header/footer/nav/section/article/aside\uB294 \uC139\uC158\uC73C\uB85C \uC720\uC9C0
4044
+ const keepTags = new Set(['header', 'footer', 'nav', 'section', 'article', 'aside']);
4045
+ // main/form\uC740 \uB798\uD37C \uC5ED\uD560\uC774 \uB9CE\uC74C \u2014 \uC790\uC2DD\uC774 \uC5EC\uB7EC \uAC1C\uBA74 \uD3BC\uCE68
4046
+ const unwrapTags = new Set(['main', 'form']);
4047
+
4048
+ if (keepTags.has(tag)) {
4049
+ result.push(el);
4050
+ return;
4051
+ }
4052
+
4053
+ const children = Array.from(el.children).filter(c => c.getBoundingClientRect().height >= 10);
4054
+
4055
+ if (unwrapTags.has(tag)) {
4056
+ // main/form: \uC790\uC2DD\uC774 \uC5EC\uB7EC \uAC1C\uBA74 \uD3BC\uCE68
4057
+ if (children.length > 1) {
4058
+ result.push(...children);
4059
+ expanded = true;
4060
+ } else {
4061
+ result.push(el);
4062
+ }
4063
+ return;
4064
+ }
4065
+
4066
+ // div \uB4F1 \uBE44\uC2DC\uB9E8\uD2F1 \uB798\uD37C \uBC97\uAE30\uAE30
4067
+ if (children.length <= 1 && children.length > 0) {
4068
+ result.push(...children);
4069
+ expanded = true;
4070
+ } else if (children.length > 1) {
4071
+ // \uD398\uC774\uC9C0 \uB192\uC774 \uB300\uBD80\uBD84\uC744 \uCC28\uC9C0\uD558\uB294 \uB798\uD37C\uB294 \uD3BC\uCE68
4072
+ const elHeight = el.getBoundingClientRect().height;
4073
+ const pageHeight = document.body.scrollHeight || 1;
4074
+ if (elHeight > pageHeight * 0.5) {
4075
+ result.push(...children);
4076
+ expanded = true;
4077
+ } else {
4078
+ result.push(el);
4079
+ }
4080
+ } else {
4081
+ result.push(el);
4082
+ }
4083
+ });
4084
+ if (!expanded) return result;
4085
+ return unwrapSections(result, depth - 1);
4086
+ }
4087
+
4088
+ sectionEls = unwrapSections(sectionEls, 5);
4089
+
4090
+ // leaf \uC694\uC18C \uD544\uD130 \u2014 \uC139\uC158\uC774 \uB420 \uC218 \uC5C6\uB294 \uD0DC\uADF8 \uC81C\uAC70
4091
+ const leafTags = new Set(['img', 'br', 'hr', 'span', 'a', 'input', 'button', 'label', 'iframe', 'video', 'source', 'svg', 'canvas', 'p', 'strong', 'em', 'b', 'i', 'small', 'path', 'circle', 'rect', 'line', 'polygon', 'polyline', 'ellipse', 'g', 'use', 'defs', 'symbol']);
4092
+ sectionEls = sectionEls.filter(el => {
4093
+ const tag = el.tagName.toLowerCase();
4094
+ // heading(h1-h6)\uC740 \uC139\uC158\uC774 \uC544\uB2CC heading \u2014 \uD56D\uC0C1 \uC81C\uC678
4095
+ if (/^h[1-6]$/.test(tag)) return false;
4096
+ // li\uB294 \uB9AC\uC2A4\uD2B8 \uC544\uC774\uD15C\uC774\uC9C0 \uC139\uC158\uC774 \uC544\uB2D8
4097
+ if (tag === 'li') return false;
4098
+ return !leafTags.has(tag);
4099
+ });
4100
+
4022
4101
  const sections = [];
4023
4102
  let maxDepth = 0;
4024
4103
 
4025
- topLevelEls.forEach(el => {
4026
- // \uC228\uACA8\uC9C4 \uC694\uC18C \uB610\uB294 \uB108\uBE44 0\uC778 \uC694\uC18C \uC81C\uC678
4104
+ sectionEls.forEach(el => {
4027
4105
  const rect = el.getBoundingClientRect();
4028
4106
  if (rect.height < 10) return;
4029
4107
 
@@ -4050,10 +4128,39 @@ var EXTRACTION_SCRIPT = `
4050
4128
  if (depth > maxDepth) maxDepth = depth;
4051
4129
  });
4052
4130
 
4053
- // heading hierarchy
4054
- const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
4131
+ // heading hierarchy (\uC2DC\uB9E8\uD2F1 + \uB300\uD615 \uD3F0\uD2B8 pseudo-heading)
4132
+ // \uD31D\uC5C5/\uBAA8\uB2EC/dialog \uB0B4\uBD80 heading \uC81C\uC678
4133
+ const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')).filter(h => {
4134
+ const popup = h.closest('dialog, [class*=popup], [class*=modal], [class*=layer], [role=dialog], [style*="display: none"], [style*="display:none"]');
4135
+ if (popup) {
4136
+ const rect = popup.getBoundingClientRect();
4137
+ // \uD654\uBA74 \uBC16\uC774\uAC70\uB098 display:none\uC774\uBA74 \uD31D\uC5C5 heading
4138
+ if (rect.width === 0 || rect.height === 0) return false;
4139
+ // \uD31D\uC5C5\uC774 \uD654\uBA74 \uAC00\uC6B4\uB370 \uB5A0 \uC788\uC73C\uBA74 (\uC804\uCCB4 \uB108\uBE44\uC758 90% \uBBF8\uB9CC) \uD31D\uC5C5 heading
4140
+ if (rect.width < window.innerWidth * 0.9) return false;
4141
+ }
4142
+ return true;
4143
+ });
4055
4144
  const headingHierarchy = headings.map(h => h.tagName.toLowerCase() + ': ' + (h.textContent || '').trim().slice(0, 80));
4056
4145
 
4146
+ // \uC2DC\uB9E8\uD2F1 heading\uC774 \uBD80\uC871\uD558\uBA74 \uD070 \uD3F0\uD2B8 \uC694\uC18C\uB97C pseudo-heading\uC73C\uB85C \uBCF4\uC644
4147
+ if (headingHierarchy.length < 3) {
4148
+ const largeFontEls = [];
4149
+ document.querySelectorAll('p, span, div, a, strong, em').forEach(el => {
4150
+ const fs = parsePixel(getComputedProp(el, 'font-size'));
4151
+ const text = (el.textContent || '').trim();
4152
+ if (fs >= 24 && text.length > 0 && text.length < 100 && el.children.length <= 2) {
4153
+ largeFontEls.push({ el, fs, text });
4154
+ }
4155
+ });
4156
+ // \uD070 \uC21C\uC11C \uC815\uB82C \uD6C4 \uC0C1\uC704 20\uAC1C
4157
+ largeFontEls.sort((a, b) => b.fs - a.fs);
4158
+ largeFontEls.slice(0, 20).forEach(({ fs, text }) => {
4159
+ const level = fs >= 60 ? 'pseudo-h1' : fs >= 36 ? 'pseudo-h2' : 'pseudo-h3';
4160
+ headingHierarchy.push(level + ' (' + fs + 'px): ' + text.slice(0, 80));
4161
+ });
4162
+ }
4163
+
4057
4164
  // \u2500\u2500 2. Design Tokens \u2500\u2500
4058
4165
 
4059
4166
  // 2a. Colors \u2014 \uBAA8\uB4E0 \uC694\uC18C\uC758 color, backgroundColor \uC218\uC9D1
@@ -4074,11 +4181,61 @@ var EXTRACTION_SCRIPT = `
4074
4181
  const sortedColors = Object.entries(colorMap).sort((a, b) => b[1] - a[1]);
4075
4182
  const sortedBgs = Object.entries(bgColorMap).sort((a, b) => b[1] - a[1]);
4076
4183
 
4077
- // primary = body\uB098 heading\uC758 \uAC00\uC7A5 \uBE48\uBC88\uD55C \uD14D\uC2A4\uD2B8 \uC0C9\uC0C1\uC774 \uC544\uB2CC \uC0C9 \uC911 1\uC704
4078
4184
  const bodyColor = rgbToHex(getComputedProp(document.body, 'color'));
4079
4185
  const bodyBg = rgbToHex(getComputedProp(document.body, 'background-color'));
4080
- const nonBodyColors = sortedColors.filter(([c]) => c !== bodyColor && c !== '#ffffff' && c !== '#000000');
4081
- const allPalette = [...new Set([...sortedColors.map(c => c[0]), ...sortedBgs.map(c => c[0])])].slice(0, 20);
4186
+ // \uAE30\uBCF8 \uB9C1\uD06C\uC0C9(#0000ee), \uD770/\uAC80 \uC81C\uC678
4187
+ const defaultLinkColors = ['#0000ee', '#0000ff', '#551a8b'];
4188
+ const nonBodyColors = sortedColors.filter(([c]) =>
4189
+ c !== bodyColor && c !== '#ffffff' && c !== '#000000' && !defaultLinkColors.includes(c)
4190
+ );
4191
+
4192
+ // heading \uC0C9\uC0C1 \uC218\uC9D1 \u2014 h1-h3 + \uB300\uD615 \uD3F0\uD2B8(24px+) \uC694\uC18C \uD3EC\uD568
4193
+ const headingColorMap = {};
4194
+ document.querySelectorAll('h1, h2, h3, h4').forEach(h => {
4195
+ const c = rgbToHex(getComputedProp(h, 'color'));
4196
+ if (c && c !== 'transparent' && c !== '#ffffff' && c !== '#000000' && !defaultLinkColors.includes(c)) {
4197
+ headingColorMap[c] = (headingColorMap[c] || 0) + 1;
4198
+ }
4199
+ });
4200
+ // \uC2DC\uB9E8\uD2F1 heading\uC774 \uC5C6\uC73C\uBA74 \uB300\uD615 \uD3F0\uD2B8 \uC694\uC18C\uC5D0\uC11C \uC0C9\uC0C1 \uCD94\uCD9C
4201
+ if (Object.keys(headingColorMap).length === 0) {
4202
+ document.querySelectorAll('p, span, div, strong, a').forEach(el => {
4203
+ const fs = parsePixel(getComputedProp(el, 'font-size'));
4204
+ if (fs >= 24 && (el.textContent || '').trim().length > 0 && (el.textContent || '').trim().length < 100) {
4205
+ const c = rgbToHex(getComputedProp(el, 'color'));
4206
+ if (c && c !== 'transparent' && c !== '#ffffff' && c !== '#000000' && !defaultLinkColors.includes(c)) {
4207
+ headingColorMap[c] = (headingColorMap[c] || 0) + 1;
4208
+ }
4209
+ }
4210
+ });
4211
+ }
4212
+ const topHeadingColor = Object.entries(headingColorMap).sort((a, b) => b[1] - a[1])[0];
4213
+
4214
+ const allPalette = [...new Set([...sortedColors.map(c => c[0]), ...sortedBgs.map(c => c[0])])].filter(c => !defaultLinkColors.includes(c)).slice(0, 20);
4215
+
4216
+ // accent \uD6C4\uBCF4: \uCC44\uB3C4\uAC00 \uB192\uC740 \uBE44-\uC911\uB9BD \uC0C9\uC0C1 (\uAC15\uC870/CTA\uC6A9)
4217
+ function hexToHsl(hex) {
4218
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
4219
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
4220
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
4221
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
4222
+ const l = (max + min) / 2;
4223
+ if (max === min) return { h: 0, s: 0, l };
4224
+ const d = max - min;
4225
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
4226
+ let h = 0;
4227
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
4228
+ else if (max === g) h = ((b - r) / d + 2) / 6;
4229
+ else h = ((r - g) / d + 4) / 6;
4230
+ return { h, s, l };
4231
+ }
4232
+
4233
+ const accentFromPalette = allPalette
4234
+ .filter(c => c.length === 7 && c.startsWith('#'))
4235
+ .map(c => ({ color: c, ...hexToHsl(c) }))
4236
+ .filter(c => c.s > 0.3 && c.l > 0.15 && c.l < 0.85)
4237
+ .sort((a, b) => b.s - a.s);
4238
+ const topAccent = accentFromPalette.length > 0 ? [accentFromPalette[0].color] : null;
4082
4239
 
4083
4240
  // 2b. Typography
4084
4241
  const typographyScale = {};
@@ -4171,13 +4328,30 @@ var EXTRACTION_SCRIPT = `
4171
4328
  document.querySelector('details, [class*=accordion], [class*=collapse], [data-toggle=collapse]')
4172
4329
  );
4173
4330
  const hasModal = !!(
4174
- document.querySelector('dialog, [class*=modal], [class*=lightbox], [role=dialog]')
4331
+ document.querySelector('dialog, [class*=modal], [role=dialog], [class*=popup]')
4332
+ );
4333
+ const hasTab = !!(
4334
+ document.querySelector('[role=tablist], [role=tab], [class*=tab-content], [class*=tab-pane], [data-toggle=tab], .elementor-tab-title, .e-n-tabs, .e-n-tab-title')
4335
+ );
4336
+ const hasLightbox = !!(
4337
+ document.querySelector('[class*=lightbox], [data-lightbox], [class*=fancybox], [data-fancybox], [class*=glightbox], [data-gallery], [data-elementor-open-lightbox]')
4175
4338
  );
4176
4339
  const hasStickyHeader = (() => {
4177
- const header = document.querySelector('header, [class*=header], nav');
4178
- if (!header) return false;
4179
- const pos = getComputedProp(header, 'position');
4180
- return pos === 'sticky' || pos === 'fixed';
4340
+ // \uC2DC\uB9E8\uD2F1 \uD0DC\uADF8 \uC6B0\uC120
4341
+ const header = document.querySelector('header, [class*=header], nav, [class*=gnb], [class*=lnb], [id*=header], [id*=gnb]');
4342
+ if (header) {
4343
+ const pos = getComputedProp(header, 'position');
4344
+ if (pos === 'sticky' || pos === 'fixed') return true;
4345
+ }
4346
+ // \uC0C1\uB2E8 fixed/sticky \uC694\uC18C \uD0D0\uC0C9 (\uBE44\uC2DC\uB9E8\uD2F1 \uB9C8\uD06C\uC5C5 \uB300\uC751)
4347
+ const topEls = document.querySelectorAll('body > div, body > div > div');
4348
+ for (const el of topEls) {
4349
+ const pos = getComputedProp(el, 'position');
4350
+ if ((pos === 'sticky' || pos === 'fixed') && el.getBoundingClientRect().top <= 0 && el.getBoundingClientRect().height < 200) {
4351
+ return true;
4352
+ }
4353
+ }
4354
+ return false;
4181
4355
  })();
4182
4356
  const hasScrollAnimations = !!(
4183
4357
  document.querySelector('[class*=aos], [data-aos], [class*=wow], [class*=scroll-animate], [class*=animate-on-scroll]') ||
@@ -4197,8 +4371,39 @@ var EXTRACTION_SCRIPT = `
4197
4371
  const hasHoverEffects = transitionProps.size > 0;
4198
4372
 
4199
4373
  // \u2500\u2500 4. Images \u2500\u2500
4374
+ // inline style + \uC8FC\uC694 \uCEE8\uD14C\uC774\uB108\uC758 computed background-image \uD0D0\uC0C9
4200
4375
  const imgEls = document.querySelectorAll('img, picture source, [style*="background-image"]');
4201
4376
  const images = [];
4377
+ const seenImageSrcs = new Set();
4378
+
4379
+ // computed background-image\uAC00 \uC788\uB294 \uCEE8\uD14C\uC774\uB108 \uC694\uC18C \uCD94\uAC00 \uD0D0\uC0C9
4380
+ const bgCandidates = document.querySelectorAll('div, section, header, footer, main, article, aside, figure, span, a');
4381
+ const bgCandidateArr = Array.from(bgCandidates).filter((_, i) => i % Math.max(1, Math.ceil(bgCandidates.length / 300)) === 0);
4382
+ bgCandidateArr.forEach(el => {
4383
+ const bg = getComputedProp(el, 'background-image');
4384
+ if (bg && bg !== 'none') {
4385
+ const match = bg.match(/url\\(["']?(.+?)["']?\\)/);
4386
+ if (match && match[1] && !match[1].startsWith('data:') && !match[1].includes('.svg')) {
4387
+ const src = match[1];
4388
+ if (seenImageSrcs.has(src)) return;
4389
+ seenImageSrcs.add(src);
4390
+ const rect = el.getBoundingClientRect();
4391
+ if (rect.width < 20 || rect.height < 20) return;
4392
+ const parentSection = el.closest('section, header, footer, main, [class*=hero], [class*=banner]');
4393
+ const parentRole = parentSection ? inferSectionRole(parentSection) : 'unknown';
4394
+ images.push({
4395
+ src: src.slice(0, 500),
4396
+ alt: '',
4397
+ width: Math.round(rect.width),
4398
+ height: Math.round(rect.height),
4399
+ aspectRatio: guessAspectRatio(Math.round(rect.width), Math.round(rect.height)),
4400
+ role: rect.width > window.innerWidth * 0.8 && rect.height > 300 ? 'hero-bg' : 'background',
4401
+ dominantColor: '',
4402
+ position: parentRole,
4403
+ });
4404
+ }
4405
+ }
4406
+ });
4202
4407
 
4203
4408
  imgEls.forEach(el => {
4204
4409
  let src = '';
@@ -4220,6 +4425,9 @@ var EXTRACTION_SCRIPT = `
4220
4425
  }
4221
4426
 
4222
4427
  if (!src || src.startsWith('data:image/svg') || src.includes('.svg')) return;
4428
+ // src \uC911\uBCF5 \uC81C\uAC70
4429
+ if (seenImageSrcs.has(src)) return;
4430
+ seenImageSrcs.add(src);
4223
4431
 
4224
4432
  const rect = el.getBoundingClientRect();
4225
4433
  if (!width) width = Math.round(rect.width);
@@ -4271,20 +4479,21 @@ var EXTRACTION_SCRIPT = `
4271
4479
  src = el.src || el.dataset.src || '';
4272
4480
  if (/youtube.com|youtu.be/.test(src)) {
4273
4481
  platform = 'youtube';
4274
- type = 'embed';
4275
4482
  // YouTube autoplay \uD30C\uB77C\uBBF8\uD130 \uAC10\uC9C0
4276
4483
  autoplay = /autoplay=1/.test(src);
4277
4484
  muted = /mute=1/.test(src);
4278
4485
  loop = /loop=1/.test(src);
4486
+ // autoplay+muted \u2192 background \uBE44\uB514\uC624\uB85C \uBD84\uB958 (YouTube \uBC30\uACBD \uC601\uC0C1 \uD328\uD134)
4487
+ type = (autoplay && muted) ? 'background' : 'embed';
4279
4488
  // YouTube thumbnail \uCD94\uCD9C
4280
4489
  const ytMatch = src.match(/(?:embed\\/|v=|youtu\\.be\\/)([a-zA-Z0-9_-]{11})/);
4281
4490
  if (ytMatch) posterSrc = 'https://img.youtube.com/vi/' + ytMatch[1] + '/hqdefault.jpg';
4282
4491
  } else if (/vimeo.com/.test(src)) {
4283
4492
  platform = 'vimeo';
4284
- type = 'embed';
4285
4493
  autoplay = /autoplay=1/.test(src);
4286
4494
  muted = /muted=1/.test(src);
4287
4495
  loop = /loop=1/.test(src);
4496
+ type = (autoplay && muted) ? 'background' : 'embed';
4288
4497
  }
4289
4498
  }
4290
4499
 
@@ -4375,11 +4584,11 @@ var EXTRACTION_SCRIPT = `
4375
4584
  },
4376
4585
  designTokens: {
4377
4586
  colors: {
4378
- primary: (nonBodyColors[0] || sortedColors[0] || ['#000000'])[0],
4379
- secondary: (nonBodyColors[1] || sortedColors[1] || ['#666666'])[0],
4587
+ primary: topHeadingColor ? topHeadingColor[0] : (nonBodyColors[0] || sortedColors[0] || ['#000000'])[0],
4588
+ secondary: (nonBodyColors[0] || sortedColors[0] || ['#666666'])[0],
4380
4589
  background: bodyBg || '#ffffff',
4381
4590
  text: bodyColor || '#000000',
4382
- accent: (nonBodyColors[2] || sortedColors[2] || ['#0066ff'])[0],
4591
+ accent: topAccent ? topAccent[0] : (nonBodyColors[1] || sortedColors[1] || ['#0066ff'])[0],
4383
4592
  palette: allPalette,
4384
4593
  },
4385
4594
  typography: {
@@ -4404,6 +4613,8 @@ var EXTRACTION_SCRIPT = `
4404
4613
  hasCarousel,
4405
4614
  hasAccordion,
4406
4615
  hasModal,
4616
+ hasTab,
4617
+ hasLightbox,
4407
4618
  hasStickyHeader,
4408
4619
  hasParallax,
4409
4620
  detectedAnimations: [...animationNames].slice(0, 20),
@@ -4441,26 +4652,97 @@ async function analyzeReference(options) {
4441
4652
  await writeFile7(analysisPath, JSON.stringify(analysis, null, 2), "utf-8");
4442
4653
  return analysis;
4443
4654
  }
4655
+ var POPUP_DISMISS_SCRIPT = `
4656
+ // 1. HTML dialog \uB2EB\uAE30
4657
+ document.querySelectorAll('dialog[open]').forEach(d => d.close());
4658
+ // 2. \uC77C\uBC18 \uD31D\uC5C5/\uBAA8\uB2EC \uB2EB\uAE30 \uBC84\uD2BC \uD074\uB9AD
4659
+ const closeSelectors = [
4660
+ '[class*=popup] [class*=close]', '[class*=modal] [class*=close]',
4661
+ '[class*=popup] [class*=btn-close]', '[class*=modal] [class*=btn-close]',
4662
+ '[aria-label*=close]', '[aria-label*="\uB2EB\uAE30"]', '[aria-label*=Close]',
4663
+ '.popup-close', '.modal-close', '.btn-close',
4664
+ '[class*=overlay] [class*=close]',
4665
+ ];
4666
+ for (const sel of closeSelectors) {
4667
+ document.querySelectorAll(sel).forEach(btn => {
4668
+ try { btn.click(); } catch {}
4669
+ });
4670
+ }
4671
+ // 3. \uC624\uBC84\uB808\uC774/\uB524 \uB808\uC774\uC5B4 \uC81C\uAC70
4672
+ document.querySelectorAll('[class*=overlay], [class*=dimmed], .modal-backdrop, [class*=popup-bg]').forEach(el => {
4673
+ if (el.getBoundingClientRect().width >= window.innerWidth * 0.8) {
4674
+ el.style.display = 'none';
4675
+ }
4676
+ });
4677
+ // 4. body overflow \uBCF5\uC6D0
4678
+ document.body.style.overflow = '';
4679
+ document.body.style.position = '';
4680
+ document.documentElement.style.overflow = '';
4681
+ `;
4444
4682
  function captureScreenshot(url, outputPath, width, height, timeout) {
4445
- const result = spawnSync("npx", [
4446
- "playwright",
4447
- "screenshot",
4448
- "--browser",
4449
- "chromium",
4450
- "--viewport-size",
4451
- `${width},${height}`,
4452
- "--wait-for-timeout",
4453
- "3000",
4454
- "--full-page",
4455
- url,
4456
- outputPath
4457
- ], {
4683
+ const scriptContent = `
4684
+ const { chromium } = require('playwright');
4685
+ (async () => {
4686
+ const browser = await chromium.launch({ headless: true });
4687
+ const context = await browser.newContext({
4688
+ viewport: { width: ${width}, height: ${height} },
4689
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
4690
+ });
4691
+ const page = await context.newPage();
4692
+ await page.goto(${JSON.stringify(url)}, { waitUntil: 'domcontentloaded', timeout: ${timeout} });
4693
+ await page.waitForTimeout(3000);
4694
+ // \uD31D\uC5C5 \uC790\uB3D9 \uB2EB\uAE30
4695
+ await page.evaluate(() => { ${POPUP_DISMISS_SCRIPT} });
4696
+ await page.waitForTimeout(500);
4697
+ // scroll simulation \u2014 lazy-load/IntersectionObserver \uCF58\uD150\uCE20 \uD2B8\uB9AC\uAC70
4698
+ await page.evaluate(async () => {
4699
+ const delay = (ms) => new Promise(r => setTimeout(r, ms));
4700
+ const scrollHeight = document.body.scrollHeight;
4701
+ const viewportHeight = window.innerHeight;
4702
+ const step = Math.floor(viewportHeight * 0.7);
4703
+ for (let y = 0; y < scrollHeight; y += step) {
4704
+ window.scrollTo(0, y);
4705
+ await delay(300);
4706
+ }
4707
+ window.scrollTo(0, scrollHeight);
4708
+ await delay(500);
4709
+ window.scrollTo(0, 0);
4710
+ await delay(500);
4711
+ });
4712
+ await page.waitForTimeout(1000);
4713
+ await page.screenshot({ path: ${JSON.stringify(outputPath)}, fullPage: true });
4714
+ await browser.close();
4715
+ })().catch(e => {
4716
+ process.stderr.write(e.message);
4717
+ process.exit(1);
4718
+ });
4719
+ `;
4720
+ const result = spawnSync("node", ["-e", scriptContent], {
4458
4721
  stdio: "pipe",
4459
- timeout: timeout + 15e3
4722
+ timeout: timeout + 3e4,
4723
+ env: { ...process.env }
4460
4724
  });
4461
4725
  if (result.status !== 0) {
4462
4726
  const stderr = result.stderr?.toString() ?? "";
4463
- throw new Error(`\uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC2E4\uD328 (${width}px): ${stderr || "unknown error"}`);
4727
+ const fallbackResult = spawnSync("npx", [
4728
+ "playwright",
4729
+ "screenshot",
4730
+ "--browser",
4731
+ "chromium",
4732
+ "--viewport-size",
4733
+ `${width},${height}`,
4734
+ "--wait-for-timeout",
4735
+ "3000",
4736
+ "--full-page",
4737
+ url,
4738
+ outputPath
4739
+ ], {
4740
+ stdio: "pipe",
4741
+ timeout: timeout + 15e3
4742
+ });
4743
+ if (fallbackResult.status !== 0) {
4744
+ throw new Error(`\uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC2E4\uD328 (${width}px): ${stderr || "unknown error"}`);
4745
+ }
4464
4746
  }
4465
4747
  }
4466
4748
  async function extractPageData(url, timeout) {
@@ -4473,8 +4755,29 @@ async function extractPageData(url, timeout) {
4473
4755
  userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
4474
4756
  });
4475
4757
  const page = await context.newPage();
4476
- await page.goto(${JSON.stringify(url)}, { waitUntil: 'networkidle', timeout: ${timeout} });
4477
- await page.waitForTimeout(2000);
4758
+ await page.goto(${JSON.stringify(url)}, { waitUntil: 'domcontentloaded', timeout: ${timeout} });
4759
+ await page.waitForTimeout(5000);
4760
+
4761
+ // \uD31D\uC5C5/\uBAA8\uB2EC/\uC624\uBC84\uB808\uC774 \uC790\uB3D9 \uB2EB\uAE30
4762
+ await page.evaluate(() => { ${POPUP_DISMISS_SCRIPT} });
4763
+ await page.waitForTimeout(500);
4764
+
4765
+ // scroll simulation \u2014 lazy-load \uCF58\uD150\uCE20 \uD2B8\uB9AC\uAC70 (DOM \uCD94\uCD9C \uC804)
4766
+ await page.evaluate(async () => {
4767
+ const delay = (ms) => new Promise(r => setTimeout(r, ms));
4768
+ const scrollHeight = document.body.scrollHeight;
4769
+ const step = Math.floor(window.innerHeight * 0.7);
4770
+ for (let y = 0; y < scrollHeight; y += step) {
4771
+ window.scrollTo(0, y);
4772
+ await delay(300);
4773
+ }
4774
+ window.scrollTo(0, scrollHeight);
4775
+ await delay(500);
4776
+ window.scrollTo(0, 0);
4777
+ await delay(500);
4778
+ });
4779
+ await page.waitForTimeout(1000);
4780
+
4478
4781
  const data = await page.evaluate(${JSON.stringify(EXTRACTION_SCRIPT)});
4479
4782
  await browser.close();
4480
4783
  process.stdout.write(JSON.stringify(data));
@@ -4536,6 +4839,8 @@ function createFallbackData() {
4536
4839
  hasCarousel: false,
4537
4840
  hasAccordion: false,
4538
4841
  hasModal: false,
4842
+ hasTab: false,
4843
+ hasLightbox: false,
4539
4844
  hasStickyHeader: false,
4540
4845
  hasParallax: false,
4541
4846
  detectedAnimations: [],
@@ -4738,6 +5043,8 @@ async function commandAnalyze(url, options) {
4738
5043
  interactions.hasCarousel && "Carousel",
4739
5044
  interactions.hasAccordion && "Accordion",
4740
5045
  interactions.hasModal && "Modal",
5046
+ interactions.hasTab && "Tab",
5047
+ interactions.hasLightbox && "Lightbox",
4741
5048
  interactions.hasParallax && "Parallax",
4742
5049
  interactions.hasHoverEffects && "Hover Effects"
4743
5050
  ].filter(Boolean);
@@ -5103,25 +5410,77 @@ function checkCommand(cmd) {
5103
5410
  }
5104
5411
  }
5105
5412
  function captureScreenshot2(url, outputPath, width, height) {
5106
- const result = spawnSync2("npx", [
5107
- "playwright",
5108
- "screenshot",
5109
- "--browser",
5110
- "chromium",
5111
- "--viewport-size",
5112
- `${width},${height}`,
5113
- "--wait-for-timeout",
5114
- "3000",
5115
- "--full-page",
5116
- url,
5117
- outputPath
5118
- ], {
5413
+ const scriptContent = `
5414
+ const { chromium } = require('playwright');
5415
+ (async () => {
5416
+ const browser = await chromium.launch({ headless: true });
5417
+ const context = await browser.newContext({
5418
+ viewport: { width: ${width}, height: ${height} },
5419
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
5420
+ });
5421
+ const page = await context.newPage();
5422
+ await page.goto(${JSON.stringify(url)}, { waitUntil: 'domcontentloaded', timeout: 30000 });
5423
+ await page.waitForTimeout(3000);
5424
+ // \uD31D\uC5C5/\uBAA8\uB2EC \uC790\uB3D9 \uB2EB\uAE30
5425
+ await page.evaluate(() => {
5426
+ document.querySelectorAll('dialog[open]').forEach(d => d.close());
5427
+ ['[class*=popup] [class*=close]','[class*=modal] [class*=close]',
5428
+ '[aria-label*=close]','[aria-label*="\uB2EB\uAE30"]','[aria-label*=Close]',
5429
+ '.popup-close','.modal-close','.btn-close'].forEach(sel => {
5430
+ document.querySelectorAll(sel).forEach(btn => { try { btn.click(); } catch {} });
5431
+ });
5432
+ document.querySelectorAll('[class*=overlay],[class*=dimmed],.modal-backdrop').forEach(el => {
5433
+ if (el.getBoundingClientRect().width >= window.innerWidth * 0.8) el.style.display = 'none';
5434
+ });
5435
+ document.body.style.overflow = '';
5436
+ document.body.style.position = '';
5437
+ document.documentElement.style.overflow = '';
5438
+ });
5439
+ await page.waitForTimeout(500);
5440
+ // scroll simulation \u2014 lazy-load \uCF58\uD150\uCE20 \uD2B8\uB9AC\uAC70
5441
+ await page.evaluate(async () => {
5442
+ const delay = (ms) => new Promise(r => setTimeout(r, ms));
5443
+ const step = Math.floor(window.innerHeight * 0.7);
5444
+ for (let y = 0; y < document.body.scrollHeight; y += step) {
5445
+ window.scrollTo(0, y);
5446
+ await delay(300);
5447
+ }
5448
+ window.scrollTo(0, document.body.scrollHeight);
5449
+ await delay(500);
5450
+ window.scrollTo(0, 0);
5451
+ await delay(500);
5452
+ });
5453
+ await page.waitForTimeout(1000);
5454
+ await page.screenshot({ path: ${JSON.stringify(outputPath)}, fullPage: true });
5455
+ await browser.close();
5456
+ })().catch(e => {
5457
+ process.stderr.write(e.message);
5458
+ process.exit(1);
5459
+ });
5460
+ `;
5461
+ const result = spawnSync2("node", ["-e", scriptContent], {
5119
5462
  stdio: "pipe",
5120
- timeout: 6e4
5463
+ timeout: 9e4,
5464
+ env: { ...process.env }
5121
5465
  });
5122
5466
  if (result.status !== 0) {
5123
5467
  const stderr = result.stderr?.toString() ?? "";
5124
- throw new Error(`\uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC2E4\uD328: ${stderr || "unknown error"}`);
5468
+ const fallback = spawnSync2("npx", [
5469
+ "playwright",
5470
+ "screenshot",
5471
+ "--browser",
5472
+ "chromium",
5473
+ "--viewport-size",
5474
+ `${width},${height}`,
5475
+ "--wait-for-timeout",
5476
+ "3000",
5477
+ "--full-page",
5478
+ url,
5479
+ outputPath
5480
+ ], { stdio: "pipe", timeout: 6e4 });
5481
+ if (fallback.status !== 0) {
5482
+ throw new Error(`\uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC2E4\uD328: ${stderr || "unknown error"}`);
5483
+ }
5125
5484
  }
5126
5485
  }
5127
5486
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saeroon/cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Saeroon Hosting developer CLI — preview, validate, and deploy sites & templates",
5
5
  "private": false,
6
6
  "type": "module",
@@ -40,6 +40,7 @@
40
40
  "@saeroon/tsconfig": "workspace:*",
41
41
  "@types/node": "^25.0.3",
42
42
  "@types/ws": "^8.5.0",
43
+ "playwright": "^1.57.0",
43
44
  "tsup": "^8.3.5",
44
45
  "typescript": "~5.9.3"
45
46
  },