@saeroon/cli 0.2.6 → 0.2.7

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 +263 -48
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -3940,10 +3940,12 @@ var EXTRACTION_SCRIPT = `
3940
3940
 
3941
3941
  function rgbToHex(rgb) {
3942
3942
  if (!rgb || rgb === 'transparent') return 'transparent';
3943
- if (rgb.startsWith('#')) return rgb;
3943
+ if (rgb.startsWith('#')) return rgb.length === 7 ? rgb : null;
3944
+ // rgb()/rgba()\uB9CC \uCC98\uB9AC, oklch/lab/color() \uB4F1 \uCD5C\uC2E0 \uD3EC\uB9F7\uC740 \uAC74\uB108\uB700
3945
+ if (!rgb.startsWith('rgb')) return null;
3944
3946
  const match = rgb.match(/\\d+/g);
3945
- if (!match || match.length < 3) return rgb;
3946
- const [r, g, b] = match.map(Number);
3947
+ if (!match || match.length < 3) return null;
3948
+ const [r, g, b] = match.map(n => Math.min(255, Math.max(0, Number(n))));
3947
3949
  return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
3948
3950
  }
3949
3951
 
@@ -3968,18 +3970,18 @@ var EXTRACTION_SCRIPT = `
3968
3970
 
3969
3971
  if (tag === 'header' || tag === 'nav') return 'header';
3970
3972
  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';
3973
+ if (/hero|banner|jumbotron|splash|main-?visual|\uBA54\uC778s?\uBE44\uC8FC\uC5BC|\uD788\uC5B4\uB85C/.test(combined)) return 'hero';
3974
+ 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';
3975
+ if (/testimonial|review|feedback|client|customer|\uD6C4\uAE30|\uB9AC\uBDF0|\uC218\uAC15s?\uD6C4\uAE30|\uACE0\uAC1Ds?\uD6C4\uAE30/.test(combined)) return 'testimonials';
3976
+ if (/faq|accordion|question|q-?and-?a|\uC790\uC8FCs?\uBB3B\uB294|\uC9C8\uBB38/.test(combined)) return 'faq';
3977
+ 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';
3978
+ if (/pricing|plan|package|\uAC00\uACA9|\uC694\uAE08|\uD50C\uB79C/.test(combined)) return 'pricing';
3979
+ if (/contact|inquiry|form|cta|call-?to-?action|get-?started|\uC0C1\uB2F4|\uBB38\uC758|\uC608\uC57D|\uC2E0\uCCAD/.test(combined)) return 'cta';
3980
+ if (/gallery|portfolio|work|project|showcase|\uAC24\uB7EC\uB9AC|\uB458\uB7EC\uBCF4\uAE30|\uC2DC\uC124|\uB0B4\uBD80/.test(combined)) return 'gallery';
3981
+ if (/blog|news|article|post|\uBE14\uB85C\uADF8|\uB274\uC2A4|\uC18C\uC2DD|\uCE7C\uB7FC/.test(combined)) return 'blog';
3982
+ if (/partner|client|logo|brand|trust|\uD30C\uD2B8\uB108|\uC81C\uD734/.test(combined)) return 'partners';
3983
+ if (/map|location|address|direction|\uC624\uC2DC\uB294s?\uAE38|\uCC3E\uC544\uC624\uC2DC\uB294s?\uAE38|\uC9C0\uB3C4|\uC704\uCE58/.test(combined)) return 'map';
3984
+ if (/stat|counter|number|achievement|\uC2E4\uC801|\uC131\uACFC|\uC218\uCE58/.test(combined)) return 'stats';
3983
3985
  return 'section';
3984
3986
  }
3985
3987
 
@@ -4019,11 +4021,68 @@ var EXTRACTION_SCRIPT = `
4019
4021
  // \u2500\u2500 1. Structure \u2500\u2500
4020
4022
  const sectionSelectors = 'body > header, body > footer, body > nav, body > main, body > section, body > div, body > article, body > aside';
4021
4023
  const topLevelEls = document.querySelectorAll(sectionSelectors);
4024
+
4025
+ // wrapper \uAC10\uC9C0: body \uC9C1\uACC4 \uC790\uC2DD\uC5D0\uC11C \uC2DC\uC791\uD558\uC5EC wrapper(\uC790\uC2DD 1\uAC1C) \uD480\uAE30
4026
+ let sectionEls = Array.from(topLevelEls).filter(el => el.getBoundingClientRect().height >= 10);
4027
+
4028
+ // \uC758\uBBF8 \uC788\uB294 \uC139\uC158\uC744 \uCC3E\uC744 \uB54C\uAE4C\uC9C0 wrapper\uB97C \uD480\uC5B4\uB098\uAC10 (\uCD5C\uB300 5\uB2E8\uACC4)
4029
+ function unwrapSections(els, depth) {
4030
+ if (depth <= 0) return els;
4031
+ const result = [];
4032
+ let expanded = false;
4033
+ els.forEach(el => {
4034
+ const tag = el.tagName.toLowerCase();
4035
+ // header, footer, nav, section, article \uB4F1 \uC2DC\uB9E8\uD2F1 \uD0DC\uADF8\uB294 \uC720\uC9C0
4036
+ if (['header', 'footer', 'nav', 'section', 'article', 'aside', 'main'].includes(tag)) {
4037
+ // main\uC740 \uADF8 \uC548\uC758 \uC790\uC2DD\uC744 \uD3BC\uCE68
4038
+ if (tag === 'main') {
4039
+ const children = Array.from(el.children).filter(c => c.getBoundingClientRect().height >= 10);
4040
+ if (children.length > 1) {
4041
+ result.push(...children);
4042
+ expanded = true;
4043
+ } else {
4044
+ result.push(el);
4045
+ }
4046
+ } else {
4047
+ result.push(el);
4048
+ }
4049
+ return;
4050
+ }
4051
+ // div \uB798\uD37C \uBC97\uAE30\uAE30
4052
+ const children = Array.from(el.children).filter(c => c.getBoundingClientRect().height >= 10);
4053
+ if (children.length === 1) {
4054
+ // \uB2E8\uC77C \uC790\uC2DD wrapper \u2192 \uBC97\uAE40
4055
+ result.push(...children);
4056
+ expanded = true;
4057
+ } else if (children.length > 1 && depth >= 4) {
4058
+ // \uAE4A\uC774 \uC0C1\uC704 2\uB2E8\uACC4(depth 5\u21924)\uC5D0\uC11C\uB9CC multi-child \uD655\uC7A5
4059
+ result.push(...children);
4060
+ expanded = true;
4061
+ } else {
4062
+ result.push(el);
4063
+ }
4064
+ });
4065
+ if (!expanded) return result;
4066
+ return unwrapSections(result, depth - 1);
4067
+ }
4068
+
4069
+ sectionEls = unwrapSections(sectionEls, 5);
4070
+
4071
+ // leaf \uC694\uC18C \uD544\uD130 \u2014 \uC139\uC158\uC774 \uB420 \uC218 \uC5C6\uB294 \uD0DC\uADF8 \uC81C\uAC70
4072
+ 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']);
4073
+ sectionEls = sectionEls.filter(el => {
4074
+ const tag = el.tagName.toLowerCase();
4075
+ // heading(h1-h6)\uC740 \uC139\uC158\uC774 \uC544\uB2CC heading \u2014 \uD56D\uC0C1 \uC81C\uC678
4076
+ if (/^h[1-6]$/.test(tag)) return false;
4077
+ // li\uB294 \uB9AC\uC2A4\uD2B8 \uC544\uC774\uD15C\uC774\uC9C0 \uC139\uC158\uC774 \uC544\uB2D8
4078
+ if (tag === 'li') return false;
4079
+ return !leafTags.has(tag);
4080
+ });
4081
+
4022
4082
  const sections = [];
4023
4083
  let maxDepth = 0;
4024
4084
 
4025
- topLevelEls.forEach(el => {
4026
- // \uC228\uACA8\uC9C4 \uC694\uC18C \uB610\uB294 \uB108\uBE44 0\uC778 \uC694\uC18C \uC81C\uC678
4085
+ sectionEls.forEach(el => {
4027
4086
  const rect = el.getBoundingClientRect();
4028
4087
  if (rect.height < 10) return;
4029
4088
 
@@ -4050,10 +4109,28 @@ var EXTRACTION_SCRIPT = `
4050
4109
  if (depth > maxDepth) maxDepth = depth;
4051
4110
  });
4052
4111
 
4053
- // heading hierarchy
4112
+ // heading hierarchy (\uC2DC\uB9E8\uD2F1 + \uB300\uD615 \uD3F0\uD2B8 pseudo-heading)
4054
4113
  const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
4055
4114
  const headingHierarchy = headings.map(h => h.tagName.toLowerCase() + ': ' + (h.textContent || '').trim().slice(0, 80));
4056
4115
 
4116
+ // \uC2DC\uB9E8\uD2F1 heading\uC774 \uBD80\uC871\uD558\uBA74 \uD070 \uD3F0\uD2B8 \uC694\uC18C\uB97C pseudo-heading\uC73C\uB85C \uBCF4\uC644
4117
+ if (headingHierarchy.length < 3) {
4118
+ const largeFontEls = [];
4119
+ document.querySelectorAll('p, span, div, a, strong, em').forEach(el => {
4120
+ const fs = parsePixel(getComputedProp(el, 'font-size'));
4121
+ const text = (el.textContent || '').trim();
4122
+ if (fs >= 24 && text.length > 0 && text.length < 100 && el.children.length <= 2) {
4123
+ largeFontEls.push({ el, fs, text });
4124
+ }
4125
+ });
4126
+ // \uD070 \uC21C\uC11C \uC815\uB82C \uD6C4 \uC0C1\uC704 20\uAC1C
4127
+ largeFontEls.sort((a, b) => b.fs - a.fs);
4128
+ largeFontEls.slice(0, 20).forEach(({ fs, text }) => {
4129
+ const level = fs >= 60 ? 'pseudo-h1' : fs >= 36 ? 'pseudo-h2' : 'pseudo-h3';
4130
+ headingHierarchy.push(level + ' (' + fs + 'px): ' + text.slice(0, 80));
4131
+ });
4132
+ }
4133
+
4057
4134
  // \u2500\u2500 2. Design Tokens \u2500\u2500
4058
4135
 
4059
4136
  // 2a. Colors \u2014 \uBAA8\uB4E0 \uC694\uC18C\uC758 color, backgroundColor \uC218\uC9D1
@@ -4074,11 +4151,61 @@ var EXTRACTION_SCRIPT = `
4074
4151
  const sortedColors = Object.entries(colorMap).sort((a, b) => b[1] - a[1]);
4075
4152
  const sortedBgs = Object.entries(bgColorMap).sort((a, b) => b[1] - a[1]);
4076
4153
 
4077
- // primary = body\uB098 heading\uC758 \uAC00\uC7A5 \uBE48\uBC88\uD55C \uD14D\uC2A4\uD2B8 \uC0C9\uC0C1\uC774 \uC544\uB2CC \uC0C9 \uC911 1\uC704
4078
4154
  const bodyColor = rgbToHex(getComputedProp(document.body, 'color'));
4079
4155
  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);
4156
+ // \uAE30\uBCF8 \uB9C1\uD06C\uC0C9(#0000ee), \uD770/\uAC80 \uC81C\uC678
4157
+ const defaultLinkColors = ['#0000ee', '#0000ff', '#551a8b'];
4158
+ const nonBodyColors = sortedColors.filter(([c]) =>
4159
+ c !== bodyColor && c !== '#ffffff' && c !== '#000000' && !defaultLinkColors.includes(c)
4160
+ );
4161
+
4162
+ // heading \uC0C9\uC0C1 \uC218\uC9D1 \u2014 h1-h3 + \uB300\uD615 \uD3F0\uD2B8(24px+) \uC694\uC18C \uD3EC\uD568
4163
+ const headingColorMap = {};
4164
+ document.querySelectorAll('h1, h2, h3, h4').forEach(h => {
4165
+ const c = rgbToHex(getComputedProp(h, 'color'));
4166
+ if (c && c !== 'transparent' && c !== '#ffffff' && c !== '#000000' && !defaultLinkColors.includes(c)) {
4167
+ headingColorMap[c] = (headingColorMap[c] || 0) + 1;
4168
+ }
4169
+ });
4170
+ // \uC2DC\uB9E8\uD2F1 heading\uC774 \uC5C6\uC73C\uBA74 \uB300\uD615 \uD3F0\uD2B8 \uC694\uC18C\uC5D0\uC11C \uC0C9\uC0C1 \uCD94\uCD9C
4171
+ if (Object.keys(headingColorMap).length === 0) {
4172
+ document.querySelectorAll('p, span, div, strong, a').forEach(el => {
4173
+ const fs = parsePixel(getComputedProp(el, 'font-size'));
4174
+ if (fs >= 24 && (el.textContent || '').trim().length > 0 && (el.textContent || '').trim().length < 100) {
4175
+ const c = rgbToHex(getComputedProp(el, 'color'));
4176
+ if (c && c !== 'transparent' && c !== '#ffffff' && c !== '#000000' && !defaultLinkColors.includes(c)) {
4177
+ headingColorMap[c] = (headingColorMap[c] || 0) + 1;
4178
+ }
4179
+ }
4180
+ });
4181
+ }
4182
+ const topHeadingColor = Object.entries(headingColorMap).sort((a, b) => b[1] - a[1])[0];
4183
+
4184
+ const allPalette = [...new Set([...sortedColors.map(c => c[0]), ...sortedBgs.map(c => c[0])])].filter(c => !defaultLinkColors.includes(c)).slice(0, 20);
4185
+
4186
+ // accent \uD6C4\uBCF4: \uCC44\uB3C4\uAC00 \uB192\uC740 \uBE44-\uC911\uB9BD \uC0C9\uC0C1 (\uAC15\uC870/CTA\uC6A9)
4187
+ function hexToHsl(hex) {
4188
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
4189
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
4190
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
4191
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
4192
+ const l = (max + min) / 2;
4193
+ if (max === min) return { h: 0, s: 0, l };
4194
+ const d = max - min;
4195
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
4196
+ let h = 0;
4197
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
4198
+ else if (max === g) h = ((b - r) / d + 2) / 6;
4199
+ else h = ((r - g) / d + 4) / 6;
4200
+ return { h, s, l };
4201
+ }
4202
+
4203
+ const accentFromPalette = allPalette
4204
+ .filter(c => c.length === 7 && c.startsWith('#'))
4205
+ .map(c => ({ color: c, ...hexToHsl(c) }))
4206
+ .filter(c => c.s > 0.3 && c.l > 0.15 && c.l < 0.85)
4207
+ .sort((a, b) => b.s - a.s);
4208
+ const topAccent = accentFromPalette.length > 0 ? [accentFromPalette[0].color] : null;
4082
4209
 
4083
4210
  // 2b. Typography
4084
4211
  const typographyScale = {};
@@ -4171,13 +4298,30 @@ var EXTRACTION_SCRIPT = `
4171
4298
  document.querySelector('details, [class*=accordion], [class*=collapse], [data-toggle=collapse]')
4172
4299
  );
4173
4300
  const hasModal = !!(
4174
- document.querySelector('dialog, [class*=modal], [class*=lightbox], [role=dialog]')
4301
+ document.querySelector('dialog, [class*=modal], [role=dialog], [class*=popup]')
4302
+ );
4303
+ const hasTab = !!(
4304
+ 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')
4305
+ );
4306
+ const hasLightbox = !!(
4307
+ document.querySelector('[class*=lightbox], [data-lightbox], [class*=fancybox], [data-fancybox], [class*=glightbox], [data-gallery], [data-elementor-open-lightbox]')
4175
4308
  );
4176
4309
  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';
4310
+ // \uC2DC\uB9E8\uD2F1 \uD0DC\uADF8 \uC6B0\uC120
4311
+ const header = document.querySelector('header, [class*=header], nav, [class*=gnb], [class*=lnb], [id*=header], [id*=gnb]');
4312
+ if (header) {
4313
+ const pos = getComputedProp(header, 'position');
4314
+ if (pos === 'sticky' || pos === 'fixed') return true;
4315
+ }
4316
+ // \uC0C1\uB2E8 fixed/sticky \uC694\uC18C \uD0D0\uC0C9 (\uBE44\uC2DC\uB9E8\uD2F1 \uB9C8\uD06C\uC5C5 \uB300\uC751)
4317
+ const topEls = document.querySelectorAll('body > div, body > div > div');
4318
+ for (const el of topEls) {
4319
+ const pos = getComputedProp(el, 'position');
4320
+ if ((pos === 'sticky' || pos === 'fixed') && el.getBoundingClientRect().top <= 0 && el.getBoundingClientRect().height < 200) {
4321
+ return true;
4322
+ }
4323
+ }
4324
+ return false;
4181
4325
  })();
4182
4326
  const hasScrollAnimations = !!(
4183
4327
  document.querySelector('[class*=aos], [data-aos], [class*=wow], [class*=scroll-animate], [class*=animate-on-scroll]') ||
@@ -4199,6 +4343,7 @@ var EXTRACTION_SCRIPT = `
4199
4343
  // \u2500\u2500 4. Images \u2500\u2500
4200
4344
  const imgEls = document.querySelectorAll('img, picture source, [style*="background-image"]');
4201
4345
  const images = [];
4346
+ const seenImageSrcs = new Set();
4202
4347
 
4203
4348
  imgEls.forEach(el => {
4204
4349
  let src = '';
@@ -4220,6 +4365,9 @@ var EXTRACTION_SCRIPT = `
4220
4365
  }
4221
4366
 
4222
4367
  if (!src || src.startsWith('data:image/svg') || src.includes('.svg')) return;
4368
+ // src \uC911\uBCF5 \uC81C\uAC70
4369
+ if (seenImageSrcs.has(src)) return;
4370
+ seenImageSrcs.add(src);
4223
4371
 
4224
4372
  const rect = el.getBoundingClientRect();
4225
4373
  if (!width) width = Math.round(rect.width);
@@ -4271,20 +4419,21 @@ var EXTRACTION_SCRIPT = `
4271
4419
  src = el.src || el.dataset.src || '';
4272
4420
  if (/youtube.com|youtu.be/.test(src)) {
4273
4421
  platform = 'youtube';
4274
- type = 'embed';
4275
4422
  // YouTube autoplay \uD30C\uB77C\uBBF8\uD130 \uAC10\uC9C0
4276
4423
  autoplay = /autoplay=1/.test(src);
4277
4424
  muted = /mute=1/.test(src);
4278
4425
  loop = /loop=1/.test(src);
4426
+ // autoplay+muted \u2192 background \uBE44\uB514\uC624\uB85C \uBD84\uB958 (YouTube \uBC30\uACBD \uC601\uC0C1 \uD328\uD134)
4427
+ type = (autoplay && muted) ? 'background' : 'embed';
4279
4428
  // YouTube thumbnail \uCD94\uCD9C
4280
4429
  const ytMatch = src.match(/(?:embed\\/|v=|youtu\\.be\\/)([a-zA-Z0-9_-]{11})/);
4281
4430
  if (ytMatch) posterSrc = 'https://img.youtube.com/vi/' + ytMatch[1] + '/hqdefault.jpg';
4282
4431
  } else if (/vimeo.com/.test(src)) {
4283
4432
  platform = 'vimeo';
4284
- type = 'embed';
4285
4433
  autoplay = /autoplay=1/.test(src);
4286
4434
  muted = /muted=1/.test(src);
4287
4435
  loop = /loop=1/.test(src);
4436
+ type = (autoplay && muted) ? 'background' : 'embed';
4288
4437
  }
4289
4438
  }
4290
4439
 
@@ -4375,11 +4524,11 @@ var EXTRACTION_SCRIPT = `
4375
4524
  },
4376
4525
  designTokens: {
4377
4526
  colors: {
4378
- primary: (nonBodyColors[0] || sortedColors[0] || ['#000000'])[0],
4379
- secondary: (nonBodyColors[1] || sortedColors[1] || ['#666666'])[0],
4527
+ primary: topHeadingColor ? topHeadingColor[0] : (nonBodyColors[0] || sortedColors[0] || ['#000000'])[0],
4528
+ secondary: (nonBodyColors[0] || sortedColors[0] || ['#666666'])[0],
4380
4529
  background: bodyBg || '#ffffff',
4381
4530
  text: bodyColor || '#000000',
4382
- accent: (nonBodyColors[2] || sortedColors[2] || ['#0066ff'])[0],
4531
+ accent: topAccent ? topAccent[0] : (nonBodyColors[1] || sortedColors[1] || ['#0066ff'])[0],
4383
4532
  palette: allPalette,
4384
4533
  },
4385
4534
  typography: {
@@ -4404,6 +4553,8 @@ var EXTRACTION_SCRIPT = `
4404
4553
  hasCarousel,
4405
4554
  hasAccordion,
4406
4555
  hasModal,
4556
+ hasTab,
4557
+ hasLightbox,
4407
4558
  hasStickyHeader,
4408
4559
  hasParallax,
4409
4560
  detectedAnimations: [...animationNames].slice(0, 20),
@@ -4441,26 +4592,81 @@ async function analyzeReference(options) {
4441
4592
  await writeFile7(analysisPath, JSON.stringify(analysis, null, 2), "utf-8");
4442
4593
  return analysis;
4443
4594
  }
4595
+ var POPUP_DISMISS_SCRIPT = `
4596
+ // 1. HTML dialog \uB2EB\uAE30
4597
+ document.querySelectorAll('dialog[open]').forEach(d => d.close());
4598
+ // 2. \uC77C\uBC18 \uD31D\uC5C5/\uBAA8\uB2EC \uB2EB\uAE30 \uBC84\uD2BC \uD074\uB9AD
4599
+ const closeSelectors = [
4600
+ '[class*=popup] [class*=close]', '[class*=modal] [class*=close]',
4601
+ '[class*=popup] [class*=btn-close]', '[class*=modal] [class*=btn-close]',
4602
+ '[aria-label*=close]', '[aria-label*="\uB2EB\uAE30"]', '[aria-label*=Close]',
4603
+ '.popup-close', '.modal-close', '.btn-close',
4604
+ '[class*=overlay] [class*=close]',
4605
+ ];
4606
+ for (const sel of closeSelectors) {
4607
+ document.querySelectorAll(sel).forEach(btn => {
4608
+ try { btn.click(); } catch {}
4609
+ });
4610
+ }
4611
+ // 3. \uC624\uBC84\uB808\uC774/\uB524 \uB808\uC774\uC5B4 \uC81C\uAC70
4612
+ document.querySelectorAll('[class*=overlay], [class*=dimmed], .modal-backdrop, [class*=popup-bg]').forEach(el => {
4613
+ if (el.getBoundingClientRect().width >= window.innerWidth * 0.8) {
4614
+ el.style.display = 'none';
4615
+ }
4616
+ });
4617
+ // 4. body overflow \uBCF5\uC6D0
4618
+ document.body.style.overflow = '';
4619
+ document.body.style.position = '';
4620
+ document.documentElement.style.overflow = '';
4621
+ `;
4444
4622
  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
- ], {
4623
+ const scriptContent = `
4624
+ const { chromium } = require('playwright');
4625
+ (async () => {
4626
+ const browser = await chromium.launch({ headless: true });
4627
+ const context = await browser.newContext({
4628
+ viewport: { width: ${width}, height: ${height} },
4629
+ 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',
4630
+ });
4631
+ const page = await context.newPage();
4632
+ await page.goto(${JSON.stringify(url)}, { waitUntil: 'domcontentloaded', timeout: ${timeout} });
4633
+ await page.waitForTimeout(3000);
4634
+ // \uD31D\uC5C5 \uC790\uB3D9 \uB2EB\uAE30
4635
+ await page.evaluate(() => { ${POPUP_DISMISS_SCRIPT} });
4636
+ await page.waitForTimeout(500);
4637
+ await page.screenshot({ path: ${JSON.stringify(outputPath)}, fullPage: true });
4638
+ await browser.close();
4639
+ })().catch(e => {
4640
+ process.stderr.write(e.message);
4641
+ process.exit(1);
4642
+ });
4643
+ `;
4644
+ const result = spawnSync("node", ["-e", scriptContent], {
4458
4645
  stdio: "pipe",
4459
- timeout: timeout + 15e3
4646
+ timeout: timeout + 3e4,
4647
+ env: { ...process.env }
4460
4648
  });
4461
4649
  if (result.status !== 0) {
4462
4650
  const stderr = result.stderr?.toString() ?? "";
4463
- throw new Error(`\uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC2E4\uD328 (${width}px): ${stderr || "unknown error"}`);
4651
+ const fallbackResult = spawnSync("npx", [
4652
+ "playwright",
4653
+ "screenshot",
4654
+ "--browser",
4655
+ "chromium",
4656
+ "--viewport-size",
4657
+ `${width},${height}`,
4658
+ "--wait-for-timeout",
4659
+ "3000",
4660
+ "--full-page",
4661
+ url,
4662
+ outputPath
4663
+ ], {
4664
+ stdio: "pipe",
4665
+ timeout: timeout + 15e3
4666
+ });
4667
+ if (fallbackResult.status !== 0) {
4668
+ throw new Error(`\uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC2E4\uD328 (${width}px): ${stderr || "unknown error"}`);
4669
+ }
4464
4670
  }
4465
4671
  }
4466
4672
  async function extractPageData(url, timeout) {
@@ -4473,8 +4679,13 @@ async function extractPageData(url, timeout) {
4473
4679
  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
4680
  });
4475
4681
  const page = await context.newPage();
4476
- await page.goto(${JSON.stringify(url)}, { waitUntil: 'networkidle', timeout: ${timeout} });
4477
- await page.waitForTimeout(2000);
4682
+ await page.goto(${JSON.stringify(url)}, { waitUntil: 'domcontentloaded', timeout: ${timeout} });
4683
+ await page.waitForTimeout(5000);
4684
+
4685
+ // \uD31D\uC5C5/\uBAA8\uB2EC/\uC624\uBC84\uB808\uC774 \uC790\uB3D9 \uB2EB\uAE30
4686
+ await page.evaluate(() => { ${POPUP_DISMISS_SCRIPT} });
4687
+ await page.waitForTimeout(500);
4688
+
4478
4689
  const data = await page.evaluate(${JSON.stringify(EXTRACTION_SCRIPT)});
4479
4690
  await browser.close();
4480
4691
  process.stdout.write(JSON.stringify(data));
@@ -4536,6 +4747,8 @@ function createFallbackData() {
4536
4747
  hasCarousel: false,
4537
4748
  hasAccordion: false,
4538
4749
  hasModal: false,
4750
+ hasTab: false,
4751
+ hasLightbox: false,
4539
4752
  hasStickyHeader: false,
4540
4753
  hasParallax: false,
4541
4754
  detectedAnimations: [],
@@ -4738,6 +4951,8 @@ async function commandAnalyze(url, options) {
4738
4951
  interactions.hasCarousel && "Carousel",
4739
4952
  interactions.hasAccordion && "Accordion",
4740
4953
  interactions.hasModal && "Modal",
4954
+ interactions.hasTab && "Tab",
4955
+ interactions.hasLightbox && "Lightbox",
4741
4956
  interactions.hasParallax && "Parallax",
4742
4957
  interactions.hasHoverEffects && "Hover Effects"
4743
4958
  ].filter(Boolean);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saeroon/cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
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
  },