framer-export 5.0.0-beta.1 → 5.0.0-beta.2

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/dist/cli/index.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "framer-export",
18
- version: "5.0.0-beta.1",
18
+ version: "5.0.0-beta.2",
19
19
  description: "Export 25+ website platforms (Framer, Webflow, Wix, Shopify, Notion, WordPress, Squarespace, Ghost, and more) into a fully working local mirror. Downloads all assets, strips badges, rewrites URLs, and pretty-prints JS.",
20
20
  type: "module",
21
21
  main: "dist/cli/index.js",
@@ -35,6 +35,7 @@ var init_package = __esm({
35
35
  start: "tsx src/cli/index.ts",
36
36
  dev: "tsx src/cli/index.ts",
37
37
  build: "tsup",
38
+ test: "node --import tsx --test tests/unit/*.test.ts",
38
39
  typecheck: "tsc --noEmit",
39
40
  format: 'prettier --write "src/**/*.ts"',
40
41
  prepublishOnly: "npm run build",
@@ -75,10 +76,8 @@ var init_package = __esm({
75
76
  },
76
77
  dependencies: {
77
78
  chalk: "^5.3.0",
78
- ora: "^8.0.0",
79
79
  prettier: "^3.2.0",
80
- puppeteer: "^22.0.0",
81
- "readline-promise": "^1.0.5"
80
+ puppeteer: "^22.0.0"
82
81
  },
83
82
  devDependencies: {
84
83
  "@types/node": "^22.0.0",
@@ -108,9 +107,6 @@ function softGradient(text) {
108
107
  function chip(label) {
109
108
  return `${ui.border("[")}${ui.primary(label)}${ui.border("]")}`;
110
109
  }
111
- function bullet(label = "\u2022") {
112
- return ui.primary(label);
113
- }
114
110
  function centerText(text, width) {
115
111
  const visible = stripAnsi(text).length;
116
112
  if (visible >= width) return text;
@@ -239,10 +235,27 @@ var init_framer = __esm({
239
235
  ],
240
236
  stripPatterns: [
241
237
  /<div id="__framer-badge-container"[^>]*><\/div>/g,
242
- /<script>try\{if\(localStorage\.get\("__framer_force_showing_editorbar_since"\)\)[^<]*<\/script>/g
238
+ /<script>try\{if\(localStorage\.getItem\("__framer_force_showing_editorbar_since"\)\)[^<]*<\/script>/g
243
239
  ],
244
240
  hydrationTimeout: 1e4,
245
241
  needsHydrationCheck: true,
242
+ skipAssetUrls: [
243
+ /^https:\/\/(?:www\.)?framer\.com\/edit(?:[/?]|$)/,
244
+ /^https:\/\/(?:[^/]+\.)*(?:framer\.com|framerstatic\.com|framerusercontent\.com|framercanvas\.com)\/[^?]*\/init\.mjs(?:\?|$)/,
245
+ /^https:\/\/(?:[^/]+\.)*(?:framer\.com|framerstatic\.com|framerusercontent\.com|framercanvas\.com)\/[^?]*\/editorbar\.[^/]*\.mjs(?:\?|$)/,
246
+ /^https:\/\/(?:[^/]+\.)*(?:framer\.com|framerstatic\.com|framerusercontent\.com|framercanvas\.com)\/[^?]*\/EditButton-[^/]*\.mjs(?:\?|$)/
247
+ ],
248
+ lazyChunkDirs: ["scripts/vendor", "scripts/modules"],
249
+ rewritePatterns: [
250
+ {
251
+ from: /https:\/\/(?:www\.)?framer\.com\/edit\/init\.mjs/g,
252
+ to: "data:text/javascript,export%20const%20createEditorBar=()=>()=>null"
253
+ },
254
+ {
255
+ from: /\(function\(\)\{[\w$]+&&[\w$]+\(\(\)=>\{[\s\S]{0,200}?__framer-badge-container[\s\S]{0,300}?\}\)\}\)\(\)/g,
256
+ to: ""
257
+ }
258
+ ],
246
259
  mapAssetDir(host, pathname, ext) {
247
260
  if (host.includes("framerusercontent.com")) {
248
261
  if (pathname.startsWith("/images/")) return "assets/images";
@@ -363,9 +376,7 @@ var init_wix = __esm({
363
376
  "panorama.wixapps.net"
364
377
  ],
365
378
  stripSelectors: ["#WIX_ADS", ".wix-ads", "#wix-badge", "#SCROLL_TO_TOP"],
366
- stripPatterns: [
367
- /<div[^>]*class="[^"]*wix-ads[^"]*"[^>]*>[\s\S]*?<\/div>/g
368
- ],
379
+ stripPatterns: [/<div[^>]*class="[^"]*wix-ads[^"]*"[^>]*>[\s\S]*?<\/div>/g],
369
380
  hydrationTimeout: 3e3,
370
381
  needsHydrationCheck: false,
371
382
  mapAssetDir(host, pathname, ext) {
@@ -412,17 +423,9 @@ var init_bubble = __esm({
412
423
  detectByHtml(html) {
413
424
  return html.includes("window.appquery = make_proxy(") || html.includes("window._bubble_page_load_data") || html.includes("window.bubble_session_uid") || html.includes("/package/pre_run_jquery_js/");
414
425
  },
415
- // Telemetry only (Plausible-based, on standard-plan apps). NEVER the asset CDN.
416
426
  stripDomains: ["pluginpul.se"],
417
- // Post-process removal of any telemetry <script> that survived into the DOM.
418
427
  stripSelectors: ['script[src*="pluginpul.se"]'],
419
428
  stripPatterns: [
420
- // "Made with Bubble" badge — injected at runtime by run.js and gated by
421
- // window._p.no_branding, so it never appears in the raw SSR HTML (only in
422
- // the captured rendered DOM). Best-effort strip anchored on its bubble.io
423
- // target: matches an <a> pointing at the bubble.io ROOT (optionally with a
424
- // tracking query), which is tight enough to spare normal deep-links to
425
- // bubble.io content pages.
426
429
  /<a\b[^>]*href="https?:\/\/(?:www\.)?bubble\.io\/?(?:\?[^"]*)?"[^>]*>[\s\S]*?<\/a>/gi
427
430
  ],
428
431
  needsHydrationCheck: true,
@@ -476,34 +479,14 @@ var init_carrd = __esm({
476
479
  return /\.carrd\.co/i.test(url);
477
480
  },
478
481
  detectByHtml(html) {
479
- return (
480
- // Hallmark inline runtime IIFE, present on every Carrd site (new and old).
481
- html.includes("$inner = $('.inner')") || // Free-plan "( Made with Carrd )" badge anchor.
482
- html.includes('href="https://carrd.co"') || // Carrd's component-class scheme (…-component). Webflow uses w-* prefixes and
483
- // Wix uses random ids, so this family does not collide with the frozen trio.
484
- /class="[^"]*\b(?:text|image|container|buttons|icons|divider|links)-component\b/.test(html) || // Current-gen structural nesting: .site-wrapper > .site-main (both required, so
485
- // neither generic-sounding class triggers detection on its own).
486
- html.includes('class="site-wrapper"') && html.includes('class="site-main"')
487
- );
482
+ return html.includes("$inner = $('.inner')") || html.includes('href="https://carrd.co"') || /class="[^"]*\b(?:text|image|container|buttons|icons|divider|links)-component\b/.test(html) || html.includes('class="site-wrapper"') && html.includes('class="site-main"');
488
483
  },
489
- // No third-party analytics/telemetry is injected by default (profile
490
- // analyticsScripts is empty), and fonts.googleapis.com is a needed asset host,
491
- // so nothing is blocked during capture.
492
484
  stripDomains: [],
493
- // The badge id sits directly on the <p> (there is no wrapper div and no
494
- // class="credits"); removing #credits drops the whole "Made with Carrd" line.
495
485
  stripSelectors: ["#credits"],
496
486
  stripPatterns: [
497
- // Free-plan badge paragraph: <p id="credits"><a href="https://carrd.co">…</a></p>
498
487
  /<p[^>]*id="credits"[^>]*>[\s\S]*?<\/p>/gi,
499
- // Belt-and-suspenders: the branded badge anchor itself (anchored on the exact
500
- // "( Made with Carrd )" text so a genuine user link to carrd.co is never removed).
501
488
  /<a[^>]*href="https:\/\/carrd\.co"[^>]*>\s*\(\s*Made with Carrd\s*\)\s*<\/a>/gi
502
489
  ],
503
- // ssr-static: the raw SSR HTML is complete, so no hydration wait is required
504
- // (profile needsHydrationCheck=false, hydrationTimeoutMs=0). antiBot is Cloudflare
505
- // but the pages fetched cleanly in research; per the authoring guide we keep a
506
- // small capture buffer instead of the literal 0 ms.
507
490
  hydrationTimeout: 2e3,
508
491
  needsHydrationCheck: false,
509
492
  mapAssetDir(host, pathname, ext) {
@@ -514,7 +497,8 @@ var init_carrd = __esm({
514
497
  if (VIDEO_EXTS4.includes(ext)) return "assets/videos";
515
498
  if (pathname.includes("/assets/images/") || IMG_EXTS4.includes(ext)) return "assets/images";
516
499
  if (pathname.includes("/assets/css/") || ext === ".css") return "styles";
517
- if (pathname.includes("/assets/js/") || ext === ".js" || ext === ".mjs") return "scripts/vendor";
500
+ if (pathname.includes("/assets/js/") || ext === ".js" || ext === ".mjs")
501
+ return "scripts/vendor";
518
502
  if (FONT_EXTS5.includes(ext)) return "assets/fonts";
519
503
  return "assets/misc";
520
504
  }
@@ -541,18 +525,11 @@ var init_duda = __esm({
541
525
  return /\.(?:multiscreensite|dudaone)\.com/i.test(url);
542
526
  },
543
527
  detectByHtml(html) {
544
- return html.includes("atob('RFVEQU9ORQ==')") || // window.Parameters SiteType → 'DUDAONE'
545
- html.includes('id="dmRoot"') || html.includes("DM_DIRECT") || // window.Parameters productId
546
- html.includes("d_track_campaign");
528
+ return html.includes("atob('RFVEQU9ORQ==')") || html.includes('id="dmRoot"') || html.includes("DM_DIRECT") || html.includes("d_track_campaign");
547
529
  },
548
- // No external telemetry/analytics hosts on Duda (tracking is inline, first-party).
549
530
  stripDomains: [],
550
- // Inline first-party tracking <script id="..."> elements (removed by id).
551
531
  stripSelectors: ["#d_track_campaign", "#d_track_personalization", "#d_track_sp"],
552
- stripPatterns: [
553
- // "Made with Duda" badge → https://www.duda.co/inspiration
554
- /<a[^>]*href="[^"]*duda\.co\/inspiration[^"]*"[^>]*>[\s\S]*?<\/a>/g
555
- ],
532
+ stripPatterns: [/<a[^>]*href="[^"]*duda\.co\/inspiration[^"]*"[^>]*>[\s\S]*?<\/a>/g],
556
533
  hydrationTimeout: 0,
557
534
  needsHydrationCheck: false,
558
535
  mapAssetDir(host, pathname, ext) {
@@ -591,9 +568,7 @@ var init_squarespace = __esm({
591
568
  detectByHtml(html) {
592
569
  return html.includes("<!-- This is Squarespace. -->") || html.includes("Static.SQUARESPACE_CONTEXT") || html.includes("<!-- End of Squarespace Headers -->") || html.includes("images.squarespace-cdn.com");
593
570
  },
594
- // First-party analytics only; no default third-party telemetry host to block.
595
571
  stripDomains: [],
596
- // No editor chrome / badge in public SSR HTML on paid or template sites.
597
572
  stripSelectors: [],
598
573
  stripPatterns: [],
599
574
  hydrationTimeout: 3e3,
@@ -656,21 +631,14 @@ var init_strikingly = __esm({
656
631
  detectByHtml(html) {
657
632
  return html.includes("strikingly-host-suffix") || html.includes('data-hypernova-key="SiteBootstrapper"') || html.includes('id="s-page-client-container"') || html.includes("strikinglycdn.com");
658
633
  },
659
- // Telemetry / error-monitoring only — NEVER the strikinglycdn asset CDN.
660
634
  stripDomains: [
661
635
  "google-analytics.com",
662
636
  "www.google-analytics.com",
663
637
  "notify.bugsnag.com",
664
638
  "sessions.bugsnag.com"
665
639
  ],
666
- // The "Powered by Strikingly" footer section carries the unique class
667
- // `.show-strikingly-logo` (inside #s-footer-section-container).
668
640
  stripSelectors: [".show-strikingly-logo"],
669
- stripPatterns: [
670
- // Hydration-rendered badge anchor → https://www.strikingly.com/?ref=logo…&utm_campaign=footer_pbs
671
- /<a[^>]*href="[^"]*strikingly\.com\/\?ref=logo[^"]*"[^>]*>[\s\S]*?<\/a>/gi
672
- ],
673
- // renderStrategy: react-hydration → use the rendered DOM as the HTML source.
641
+ stripPatterns: [/<a[^>]*href="[^"]*strikingly\.com\/\?ref=logo[^"]*"[^>]*>[\s\S]*?<\/a>/gi],
674
642
  hydrationTimeout: 3e3,
675
643
  needsHydrationCheck: false,
676
644
  hydrationSelector: "#s-page-client-container",
@@ -727,15 +695,9 @@ var init_tilda = __esm({
727
695
  detectByHtml(html) {
728
696
  return html.includes("data-tilda-project-id") || html.includes("data-tilda-page-id") || html.includes("tildacdn") || html.includes('id="allrecords"');
729
697
  },
730
- // Third-party telemetry only. NEVER the tildacdn asset hosts (they carry the
731
- // page's real images/CSS/JS); the Tilda stat script is stripped by selector.
732
698
  stripDomains: ["www.google-analytics.com", "google-analytics.com"],
733
699
  stripSelectors: ["#tildacopy", ".t-tildalabel", 'script[src*="tilda-stat"]'],
734
- stripPatterns: [
735
- // "Made on Tilda" badge link (free-plan only). No nested <a>, so the
736
- // non-greedy </a> match is safe even though the anchor wraps inner <div>s.
737
- /<a[^>]*href="[^"]*tilda\.cc[^"]*"[^>]*>[\s\S]*?<\/a>/g
738
- ],
700
+ stripPatterns: [/<a[^>]*href="[^"]*tilda\.cc[^"]*"[^>]*>[\s\S]*?<\/a>/g],
739
701
  hydrationTimeout: 0,
740
702
  needsHydrationCheck: false,
741
703
  mapAssetDir(host, pathname, ext) {
@@ -776,15 +738,8 @@ var init_weebly = __esm({
776
738
  detectByHtml(html) {
777
739
  return html.includes("Weebly.footer.setupContainer") || html.includes("weebly-footer-signup-container-v3") || html.includes("wsite-content") || html.includes("window._W");
778
740
  },
779
- // Third-party telemetry only. NEVER list editmysite.com here — that host serves
780
- // the platform CSS/JS/images (cdn1/cdn2/cdn11) and blocking it deletes assets.
781
- // The Weebly _W.Analytics trackers (main.js, stl.js) also live on that CDN, so
782
- // they are intentionally not blocked (blocking the host would kill assets too).
783
741
  stripDomains: ["google-analytics.com"],
784
- // Footer "Made with Weebly" signup badge: unique container id (empty in SSR
785
- // HTML, populated client-side) plus the script that injects it.
786
742
  stripSelectors: ["#weebly-footer-signup-container-v3", 'script[src*="footerSignup.js"]'],
787
- // Best-effort removal of the rendered badge anchor should it appear inline.
788
743
  stripPatterns: [/<a[^>]*href="[^"]*weebly\.com\/signup[^"]*"[^>]*>[\s\S]*?<\/a>/g],
789
744
  hydrationTimeout: 0,
790
745
  needsHydrationCheck: false,
@@ -822,21 +777,9 @@ var init_clickfunnels = __esm({
822
777
  detectByHtml(html) {
823
778
  return html.includes("cf-head-scripts") || html.includes("window.cfRootDomain") || html.includes("cf-lander-serialized-custom-fonts") || html.includes("data-page-element");
824
779
  },
825
- // Telemetry / analytics hosts blocked during capture. NOT the asset CDNs
826
- // (statics.myclickfunnels.com / images.clickfunnels.com) and NOT the functional
827
- // third-party libs (cdnjs / code.jquery.com / fontawesome) or Turnstile.
828
780
  stripDomains: ["bam.nr-data.net", "js-agent.newrelic.com", "events.myclickfunnels.com"],
829
- // No CF-specific badge/editor container has a static id/class in the delivered
830
- // HTML, so there is nothing selector-strippable here.
831
781
  stripSelectors: [],
832
- // BADGE: The "Powered by ClickFunnels" badge is default-on per docs but was
833
- // toggled OFF/HIDDEN on both verified samples (0 occurrences in the SSR HTML),
834
- // and this handler does not capture the rendered DOM. No reliable static markup
835
- // exists to anchor a regex on, so no badge stripPattern is emitted (a loose match
836
- // on clickfunnels.com would risk removing legitimate links). If a future sample
837
- // exposes the badge anchor, add a tight /<a[^>]*href="[^"]*clickfunnels\.com[^"]*"...>/ here.
838
782
  stripPatterns: [],
839
- // ssr-static: SSR body is complete, no hydration wait needed.
840
783
  hydrationTimeout: 0,
841
784
  needsHydrationCheck: false,
842
785
  mapAssetDir(host, pathname, ext) {
@@ -873,30 +816,15 @@ var init_elementor = __esm({
873
816
  displayName: "Elementor",
874
817
  category: "landing",
875
818
  priority: 42,
876
- // Only Elementor Cloud/Hosting uses a platform subdomain; self-hosted sites live
877
- // on the owner's own domain and are caught by detectByHtml / detectGenerator.
878
819
  detectByUrl(url) {
879
820
  return /\.elementor\.cloud/i.test(url);
880
821
  },
881
- // OR of four independent, Elementor-specific markers (none match Framer/Webflow/Wix,
882
- // and #2/#3 survive optimization plugins that strip the generator meta):
883
- // - Elementor plugin asset directory
884
- // - Elementor "kit" body class
885
- // - root wrapper data-attribute
886
- // - frontend bootstrap JS global
887
822
  detectByHtml(html) {
888
823
  return html.includes("/wp-content/plugins/elementor/") || html.includes("elementor-kit-") || html.includes("data-elementor-type=") || html.includes("elementorFrontendConfig");
889
824
  },
890
- // Matched against the <meta name="generator"> content only, e.g.
891
- // "Elementor 3.31.3; features: ...". Anchored so it fires solely on Elementor's own
892
- // generator string, never on the "WordPress 6.7.5" / "Site Kit by Google" siblings.
893
825
  detectGenerator: /^Elementor\s+\d+\.\d+/i,
894
- // No default third-party telemetry: googletagmanager / cdn.usefathom.com hosts seen
895
- // on sample sites are site-owner additions, not an Elementor platform default.
896
826
  stripDomains: [],
897
- // No front-end editor chrome or badge element to remove.
898
827
  stripSelectors: [],
899
- // Verified: no "Made with Elementor" badge in the front-end DOM → nothing to strip.
900
828
  stripPatterns: [],
901
829
  hydrationTimeout: 0,
902
830
  needsHydrationCheck: false,
@@ -935,8 +863,6 @@ var init_instapage = __esm({
935
863
  detectByHtml(html) {
936
864
  return html.includes("instapageSp") || html.includes("_instapageSnowplow") || html.includes("anthill.instapage.com") || html.includes("pageserver.page2");
937
865
  },
938
- // Telemetry / analytics hosts only — blocked during capture. The fastcdn.co
939
- // asset CDN is deliberately NOT listed here (those assets are mirrored).
940
866
  stripDomains: [
941
867
  "anthill.instapage.com",
942
868
  "cdn.instapagemetrics.com",
@@ -948,11 +874,7 @@ var init_instapage = __esm({
948
874
  'script[src*="anthill.instapage.com"]',
949
875
  'script[src*="heatmap.services"]'
950
876
  ],
951
- stripPatterns: [
952
- // Hidden local-storage helper iframe (runtime-injected on live pages; stripped
953
- // here best-effort in case it appears in the captured markup).
954
- /<iframe[^>]*d3mwhxgzltpnyp\.cloudfront\.net[^>]*>[\s\S]*?<\/iframe>/g
955
- ],
877
+ stripPatterns: [/<iframe[^>]*d3mwhxgzltpnyp\.cloudfront\.net[^>]*>[\s\S]*?<\/iframe>/g],
956
878
  hydrationTimeout: 0,
957
879
  needsHydrationCheck: false,
958
880
  mapAssetDir(host, pathname, ext) {
@@ -997,14 +919,10 @@ var init_systemeio = __esm({
997
919
  detectByHtml(html) {
998
920
  return html.includes("<!-- Created with https://systeme.io -->") || html.includes("d3fit27i5nzkqh.cloudfront.net") || html.includes("systeme.io/?sa=") || html.includes("d3syewzhvzylbl.cloudfront.net");
999
921
  },
1000
- // First-party click/funnel telemetry — blocked during capture, never downloaded.
1001
922
  stripDomains: ["log.systeme.io"],
1002
923
  stripSelectors: ['script[src*="log.systeme.io"]'],
1003
924
  stripPatterns: [
1004
- // Footer credit/affiliate badge: <a href="https://systeme.io/?sa=<code>" ...>SITE CREDIT</a>.
1005
- // Match on the `systeme.io/?sa=` href affiliate param (visible label is owner-editable).
1006
925
  /<a[^>]*href="[^"]*systeme\.io\/\?sa=[^"]*"[^>]*>[\s\S]*?<\/a>/g,
1007
- // Branding comment injected right after <!DOCTYPE html>.
1008
926
  /<!--\s*Created with https:\/\/systeme\.io\s*-->/g
1009
927
  ],
1010
928
  hydrationTimeout: 0,
@@ -1066,18 +984,9 @@ var init_unbounce = __esm({
1066
984
  detectByHtml(html) {
1067
985
  return html.includes("lp-pom-root") || html.includes("window.ub.page") || html.includes("builder-assets.unbounce.com") || html.includes("ubembed.com");
1068
986
  },
1069
- // No first-party telemetry/analytics hosts to block (profile analyticsScripts: []).
1070
- // The asset CDNs (builder-assets / cloudfront / ubembed) must NOT be listed here.
1071
987
  stripDomains: [],
1072
- // The badge selector (#lp-pom-root a[href*="unbounce.com"]) is a descendant
1073
- // combinator, unsupported by stripSelectors — handled via stripPatterns below.
1074
988
  stripSelectors: [],
1075
- stripPatterns: [
1076
- // Best-effort removal of the "Made with Unbounce" branded footer link.
1077
- // Anchored on the exact brand host so it never matches *.unbouncepages.com /
1078
- // *.ubpages.com hosting links. Markup is approximate (badge markup UNVERIFIED).
1079
- /<a[^>]*href="https?:\/\/(?:www\.)?unbounce\.com[^"]*"[^>]*>[\s\S]*?<\/a>/gi
1080
- ],
989
+ stripPatterns: [/<a[^>]*href="https?:\/\/(?:www\.)?unbounce\.com[^"]*"[^>]*>[\s\S]*?<\/a>/gi],
1081
990
  hydrationTimeout: 0,
1082
991
  needsHydrationCheck: false,
1083
992
  mapAssetDir(host, pathname, ext) {
@@ -1131,25 +1040,12 @@ var init_ghost = __esm({
1131
1040
  detectByHtml(html) {
1132
1041
  return html.includes("/assets/built/screen.css") || html.includes("/public/cards.min.css") || html.includes("cdn.jsdelivr.net/ghost/") || html.includes("gh-powered-by");
1133
1042
  },
1134
- // Matched against <meta name="generator"> content only. Version drifts per site
1135
- // (observed 6.51 and 6.39), so match the pattern rather than a literal version.
1136
1043
  detectGenerator: /Ghost [0-9.]+/i,
1137
- // First-party ghost-stats is the only analytics; it POSTs to the site's own
1138
- // origin, so there is no third-party telemetry host to block during capture.
1139
1044
  stripDomains: [],
1140
- // The "Powered by Ghost" badge's unique class sits on the WRAPPER <div>, whose
1141
- // only child is the <a>. The post-processor's naive `.class` stripper is
1142
- // non-greedy to the first close tag, so `.gh-powered-by` would delete only up to
1143
- // the inner </a> and orphan a dangling </div>. Since selectors run before
1144
- // patterns, that orphan cannot be cleaned up afterwards. The badge is therefore
1145
- // removed cleanly (both tags) by the wrapper-div stripPattern below instead.
1146
1045
  stripSelectors: [],
1147
1046
  stripPatterns: [
1148
- // <div class="gh-powered-by"><a href="https://ghost.org/" ...>Powered by Ghost</a></div>
1149
1047
  /<div[^>]*class="[^"]*gh-powered-by[^"]*"[^>]*>[\s\S]*?<\/div>/g,
1150
- // Bare <a href="…ghost.org…">Powered by Ghost</a> fallback if the wrapper drops.
1151
1048
  /<a[^>]*href="[^"]*ghost\.org[^"]*"[^>]*>[\s\S]*?<\/a>/g,
1152
- // Identifying <meta name="generator" content="Ghost X.Y">.
1153
1049
  /<meta[^>]*name="generator"[^>]*content="Ghost[^"]*"[^>]*>/gi
1154
1050
  ],
1155
1051
  hydrationTimeout: 0,
@@ -1196,18 +1092,9 @@ var init_notion = __esm({
1196
1092
  detectByHtml(html) {
1197
1093
  return html.includes('data-notion-html="web"') || html.includes('id="notion-app"') || html.includes("__notion_html_async") || html.includes("window.__notion_boot_data");
1198
1094
  },
1199
- // Notion's own Splunk telemetry collector — blocked during capture. NEVER add
1200
- // the asset hosts (www.notion.so / *.amazonaws.com) here; they serve real files.
1201
1095
  stripDomains: ["http-inputs-notion.splunkcloud.com"],
1202
- // Pre-hydration loading chrome. captureRenderedDom normally replaces these once
1203
- // #notion-app hydrates; stripping them is a defensive no-content cleanup in case
1204
- // hydration is partial.
1205
1096
  stripSelectors: ["#initial-loading-spinner", "#skeleton", "#skeleton-sidebar"],
1206
1097
  stripPatterns: [
1207
- // "Built with Notion" watermark is React-injected post-hydration (ABSENT from
1208
- // raw HTML — selector/href unverified). Best-effort removal from the captured
1209
- // rendered DOM: an <a> pointing at notion.so/notion.com that also carries the
1210
- // badge's signature text. Both conditions keep this from matching real content.
1211
1098
  /<a[^>]*href="[^"]*notion\.(so|com)[^"]*"[^>]*>[\s\S]*?Built with Notion[\s\S]*?<\/a>/gi
1212
1099
  ],
1213
1100
  hydrationTimeout: 8e3,
@@ -1262,15 +1149,9 @@ var init_wordpress = __esm({
1262
1149
  detectByHtml(html) {
1263
1150
  return html.includes("/wp-includes/") || html.includes("/wp-content/") || html.includes("api.w.org") || html.includes("_wpemojiSettings");
1264
1151
  },
1265
- // Matched against the <meta name="generator"> content only. Covers both
1266
- // "WordPress <version>" (self-hosted) and exact "WordPress.com" (hosted).
1267
1152
  detectGenerator: /WordPress/i,
1268
- // Jetpack / WP.com Stats trackers only — never the asset CDN.
1269
1153
  stripDomains: ["stats.wp.com", "pixel.wp.com"],
1270
- // Remove the leftover tracker <script> tags after their requests are blocked.
1271
1154
  stripSelectors: ['script[src*="stats.wp.com"]', 'script[src*="pixel.wp.com"]'],
1272
- // No badge to strip; drop the generator meta as an attribution cleanup
1273
- // (shape mirrors webflow.ts). Matches "WordPress 6.9.4" and "WordPress.com".
1274
1155
  stripPatterns: [
1275
1156
  /<meta[^>]*name=["']generator["'][^>]*content=["'][^"']*WordPress[^"']*["'][^>]*>/gi
1276
1157
  ],
@@ -1318,37 +1199,17 @@ var init_kajabi = __esm({
1318
1199
  detectByUrl(url) {
1319
1200
  return /\.mykajabi\.com/i.test(url);
1320
1201
  },
1321
- // Four Kajabi-only, tier-independent tells. Badge markers are deliberately NOT
1322
- // used here (the "Powered by Kajabi" badge is absent on paid plans). None of
1323
- // these strings appear on Framer / Webflow / Wix output.
1324
1202
  detectByHtml(html) {
1325
- return html.includes("var Kajabi = Kajabi") || // inline bootstrap: `var Kajabi = Kajabi || {};`
1326
- html.includes("Kajabi.currentSiteUser") || // inline theme bootstrap global
1327
- html.includes("kjb-settings-id=") || // Kajabi theme-editor data attribute
1328
- html.includes("kajabi-cdn.com");
1203
+ return html.includes("var Kajabi = Kajabi") || html.includes("Kajabi.currentSiteUser") || html.includes("kjb-settings-id=") || html.includes("kajabi-cdn.com");
1329
1204
  },
1330
- // Telemetry / analytics hosts only — NEVER the kajabi-cdn.com asset hosts.
1331
1205
  stripDomains: ["rs-dp.kajabi.com", "cdn.rudderlabs.com"],
1332
- // The "Powered by Kajabi" badge wrapper carries a unique class — remove it via
1333
- // selector (first-match). stripPatterns below covers repeats / rendered markup.
1334
1206
  stripSelectors: [".footer__powered-by"],
1335
1207
  stripPatterns: [
1336
- // Whole badge wrapper: <div class="footer__powered-by"> … </div>
1337
- // (real markup has indentation/newlines between the div and the anchor).
1338
1208
  /<div[^>]*class="[^"]*footer__powered-by[^"]*"[^>]*>[\s\S]*?<\/div>/g,
1339
- // Belt-and-suspenders: the branded referral anchor itself.
1340
1209
  /<a[^>]*href="[^"]*app\.kajabi\.com\/r\/[^"]*powered_by=true[^"]*"[^>]*>[\s\S]*?<\/a>/g
1341
1210
  ],
1342
- // ssr-static: the raw SSR HTML is complete, so no hydration wait is needed
1343
- // (profile needsHydrationCheck=false, hydrationTimeoutMs=0). antiBot is
1344
- // Cloudflare but the pages fetched cleanly in research; per the authoring
1345
- // guide we keep a small capture buffer instead of the literal 0 ms.
1346
1211
  hydrationTimeout: 2e3,
1347
1212
  needsHydrationCheck: false,
1348
- // Both cdnDomains live under *.kajabi-cdn.com
1349
- // - kajabi-storefronts-production.kajabi-cdn.com
1350
- // - kajabi-app-assets.kajabi-cdn.com
1351
- // so a single host match covers every declared CDN host; classify by ext/path.
1352
1213
  mapAssetDir(host, pathname, ext) {
1353
1214
  if (host.includes("kajabi-cdn.com")) {
1354
1215
  if (VIDEO_EXTS18.includes(ext)) return "assets/videos";
@@ -1382,28 +1243,15 @@ var init_podia = __esm({
1382
1243
  return /\.podia\.com/i.test(url);
1383
1244
  },
1384
1245
  detectByHtml(html) {
1385
- return (
1386
- // Constant Turnstile site-key present on every Podia storefront (incl. custom domains).
1387
- html.includes("0x4AAAAAAAJ5kwYW6AH1ybLx") || // Shared framework asset host (storefront-<hash>.css/.js, user-site-<hash>.js).
1388
- html.includes("cdn.podia.com") || // React section-adapter islands unique to Podia's creator UI.
1389
- html.includes('data-react-component="creator_ui/section_adapters/') || // Stimulus controller wiring the editable page sections.
1390
- html.includes('data-controller="editor--page-section"')
1391
- );
1246
+ return html.includes("0x4AAAAAAAJ5kwYW6AH1ybLx") || html.includes("cdn.podia.com") || html.includes('data-react-component="creator_ui/section_adapters/') || html.includes('data-controller="editor--page-section"');
1392
1247
  },
1393
- // Telemetry only — NEVER the asset CDN. Podia's default analytics is Google Tag Manager.
1394
1248
  stripDomains: ["www.googletagmanager.com"],
1395
1249
  stripSelectors: ['script[src*="googletagmanager.com"]'],
1396
1250
  stripPatterns: [
1397
- // "Powered by Podia" footer badge — React-rendered anchor in the captured DOM.
1398
- // Its href is https://www.podia.com?utm_medium=poweredby&utm_source=footer
1399
- // (the "&" may serialize as "&amp;"). Anchor on the stable brand host + the
1400
- // badge-unique utm_medium=poweredby token; tolerate single/double quotes and any
1401
- // chars between host and token (e.g. a "/" before the query string).
1402
1251
  /<a[^>]*href=['"][^'"]*podia\.com[^'"]*utm_medium=poweredby[^'"]*['"][^>]*>[\s\S]*?<\/a>/gi
1403
1252
  ],
1404
1253
  hydrationTimeout: 4e3,
1405
1254
  needsHydrationCheck: true,
1406
- // Content lives in these React islands rather than a #main root.
1407
1255
  hydrationSelector: ".react-page-section",
1408
1256
  captureRenderedDom: true,
1409
1257
  mapAssetDir(host, pathname, ext) {
@@ -1452,17 +1300,9 @@ var init_teachable = __esm({
1452
1300
  "www.recaptcha.net",
1453
1301
  "fast.wistia.com"
1454
1302
  ],
1455
- stripSelectors: [
1456
- // Analytics <script src> tags left in the HTML (hosts also blocked at capture
1457
- // via stripDomains). The powered-by badge is handled by stripPatterns instead
1458
- // — a `.class` selector can't match its single-quoted, multi-child anchor.
1459
- 'script[src*="heapanalytics.com"]',
1460
- 'script[src*="nr-data.net"]'
1461
- ],
1303
+ stripSelectors: ['script[src*="heapanalytics.com"]', 'script[src*="nr-data.net"]'],
1462
1304
  stripPatterns: [
1463
- // Footer "Teach Online with" badge — anchored on its unique powered-by class.
1464
1305
  /<a[^>]*class=['"][^'"]*powered-by[^'"]*['"][^>]*>[\s\S]*?<\/a>/gi,
1465
- // Fallback: anchored on the teachable.com ?src=school_footer badge href.
1466
1306
  /<a[^>]*href=['"][^'"]*teachable\.com[^'"]*src=school_footer[^'"]*['"][^>]*>[\s\S]*?<\/a>/gi
1467
1307
  ],
1468
1308
  hydrationTimeout: 0,
@@ -1501,49 +1341,17 @@ var init_thinkific = __esm({
1501
1341
  detectByUrl(url) {
1502
1342
  return /\.thinkific\.com/i.test(url);
1503
1343
  },
1504
- // Four Thinkific-only tells that hold on both subdomain and custom-domain
1505
- // sites. The "Powered by Thinkific" badge is deliberately NOT used here (it is
1506
- // removable on paid plans). None of these strings appear on Framer / Webflow /
1507
- // Wix output.
1508
1344
  detectByHtml(html) {
1509
- return html.includes("window.Thinkific") || // inline bootstrap: `window.Thinkific = window.Thinkific || {}`
1510
- html.includes("assets.thinkific.com") || // theme CSS/JS bundle host
1511
- html.includes("cdn.thinkific.com") || // toga-css icon-font host
1512
- html.includes("thnc.current_user-initialized");
1345
+ return html.includes("window.Thinkific") || html.includes("assets.thinkific.com") || html.includes("cdn.thinkific.com") || html.includes("thnc.current_user-initialized");
1513
1346
  },
1514
- // Telemetry / third-party embed hosts only (profile.analyticsScripts) — NEVER
1515
- // the *.thinkific.com asset CDN. These are matched against the request HOSTNAME
1516
- // only (capture.ts: `host.includes(d)`), so each entry MUST be a bare hostname —
1517
- // a path such as "www.google.com/recaptcha" can never match and would be dead.
1518
- // `fast.wistia.net` = Wistia video player; `www.google.com` = sign-in reCAPTCHA
1519
- // (api.js served from www.google.com/recaptcha; the whole host is safe to block
1520
- // on a static mirror — no legit Thinkific asset is served from it).
1521
1347
  stripDomains: ["fast.wistia.net", "www.google.com"],
1522
- // The badge is removed via stripPatterns (below), NOT a `.class` stripSelector.
1523
- // The class-based stripper (output.ts) is a non-greedy `…>[\s\S]*?<\/[^>]*>` that
1524
- // stops at the FIRST closing tag, so on the nested badge markup
1525
- // (<div class="footer__white-label"><a><svg><title>…</title>…</svg></a></div>) it
1526
- // would cut at </title>, leaving orphan </svg></a></div> AND deleting the opening
1527
- // <div>/<a> tags that the stripPatterns regexes anchor on (stripSelectors run
1528
- // before stripPatterns). So no stripSelectors are used here.
1529
1348
  stripSelectors: [],
1530
1349
  stripPatterns: [
1531
- // Whole badge wrapper: <div class="footer__white-label"> … </div>
1532
- // (inner anchor + inline SVG titled "Teach online with Thinkific").
1533
1350
  /<div[^>]*class="[^"]*footer__white-label[^"]*"[^>]*>[\s\S]*?<\/div>/g,
1534
- // Belt-and-suspenders: the branded powered-by referral anchor itself.
1535
1351
  /<a[^>]*href="[^"]*thinkific\.com[^"]*utm_medium=powered-by[^"]*"[^>]*>[\s\S]*?<\/a>/g
1536
1352
  ],
1537
- // ssr-static: the raw SSR HTML is complete, so no hydration wait is needed
1538
- // (profile needsHydrationCheck=false, hydrationTimeoutMs=0). antiBot is
1539
- // Cloudflare (rocket-loader observed) but pages fetched cleanly in research;
1540
- // per the authoring guide we keep a small capture buffer instead of literal 0.
1541
1353
  hydrationTimeout: 2e3,
1542
1354
  needsHydrationCheck: false,
1543
- // cdnDomains: assets.thinkific.com, cdn.thinkific.com, cdn-themes.thinkific.com,
1544
- // import.cdn.thinkific.com, files.cdn.thinkific.com (all *.thinkific.com), plus
1545
- // the bare s3.amazonaws.com upload host. Theme bundles/fonts live on the
1546
- // thinkific.com hosts; uploaded media on import.cdn / files.cdn (front S3).
1547
1355
  mapAssetDir(host, pathname, ext) {
1548
1356
  if (host.includes("thinkific.com")) {
1549
1357
  if (VIDEO_EXTS21.includes(ext)) return "assets/videos";
@@ -1587,25 +1395,18 @@ var init_gumroad = __esm({
1587
1395
  detectByHtml(html) {
1588
1396
  return html.includes("cable.gumroad.com") || html.includes("assets.gumroad.com") || html.includes("ns/fb/gumroad#") || html.includes("gumroad:product");
1589
1397
  },
1590
- // Telemetry / analytics hosts only — never the asset CDN.
1591
- // gumroad-analytics.com is Gumroad's first-party analytics proxy
1592
- // (domain_settings.third_party_analytics_domain); GA / FB Pixel / TikTok Pixel
1593
- // are injected client-side only when the creator enables them.
1594
1398
  stripDomains: [
1595
1399
  "gumroad-analytics.com",
1596
1400
  "www.googletagmanager.com",
1597
1401
  "connect.facebook.net",
1598
1402
  "analytics.tiktok.com"
1599
1403
  ],
1600
- // Defensively drop any surviving analytics <script> tags from the rendered DOM
1601
- // (stripDomains already blocks them at the network layer during capture).
1602
1404
  stripSelectors: [
1603
1405
  'script[src*="gumroad-analytics.com"]',
1604
1406
  'script[src*="googletagmanager.com"]',
1605
1407
  'script[src*="connect.facebook.net"]',
1606
1408
  'script[src*="analytics.tiktok.com"]'
1607
1409
  ],
1608
- // No server-rendered badge to remove (badgePresent = false, confirmed).
1609
1410
  stripPatterns: [],
1610
1411
  hydrationTimeout: 8e3,
1611
1412
  needsHydrationCheck: true,
@@ -1646,19 +1447,10 @@ var init_shopify = __esm({
1646
1447
  detectByHtml(html) {
1647
1448
  return html.includes("cdn.shopify.com") || html.includes("/cdn/shop/") || html.includes("Shopify.shop") || html.includes("ShopifyAnalytics");
1648
1449
  },
1649
- // Telemetry only — never the asset CDN.
1650
1450
  stripDomains: ["monorail-edge.shopifysvc.com"],
1651
- // The "Powered by Shopify" badge is a NESTED <span class="shopify-powered-by">
1652
- // <a …>…</a></span>. The post-processor's `.class` strip only deletes up to the
1653
- // FIRST closing tag (the inner </a>), which would leave an orphan </span>; worse,
1654
- // stripSelectors run BEFORE stripPatterns, so it would consume the opening <span>
1655
- // and stop the clean span regex below from matching. So the badge is removed
1656
- // purely via stripPatterns — no `.class` selector.
1657
1451
  stripSelectors: [],
1658
1452
  stripPatterns: [
1659
- // Whole badge container: <span class="shopify-powered-by" data-shopify="powered-by"><a href="…shopify.com?utm_campaign=poweredby…">Powered by Shopify</a></span>
1660
1453
  /<span[^>]*class="[^"]*shopify-powered-by[^"]*"[^>]*>[\s\S]*?<\/span>/gi,
1661
- // Fallback: the brand anchor itself (href carries ?utm_campaign=poweredby), in case the <span> wrapper is absent or customised.
1662
1454
  /<a[^>]*href="[^"]*shopify\.com\?utm_campaign=poweredby[^"]*"[^>]*>[\s\S]*?<\/a>/gi
1663
1455
  ],
1664
1456
  hydrationTimeout: 3500,
@@ -1711,9 +1503,7 @@ var init_gamma = __esm({
1711
1503
  detectByHtml(html) {
1712
1504
  return html.includes("assets.gammahosted.com") || html.includes("imgproxy.gamma.app") || html.includes("cdn.gamma.app") || html.includes("gamma-moveable-wrapper");
1713
1505
  },
1714
- // No telemetry/analytics hosts observed in the profile.
1715
1506
  stripDomains: [],
1716
- // No site badge and no editor chrome to strip on published Gamma sites.
1717
1507
  stripSelectors: [],
1718
1508
  stripPatterns: [],
1719
1509
  hydrationTimeout: 8e3,
@@ -1766,7 +1556,10 @@ function platformsByCategory() {
1766
1556
  }
1767
1557
  return out;
1768
1558
  }
1769
- var PLATFORM_REGISTRY, CATEGORY_LABELS, CATEGORY_ORDER;
1559
+ function isBetaPlatform(name) {
1560
+ return !STABLE_PLATFORMS.has(name);
1561
+ }
1562
+ var PLATFORM_REGISTRY, CATEGORY_LABELS, CATEGORY_ORDER, STABLE_PLATFORMS;
1770
1563
  var init_registry = __esm({
1771
1564
  "src/platforms/registry.ts"() {
1772
1565
  "use strict";
@@ -1796,11 +1589,9 @@ var init_registry = __esm({
1796
1589
  init_shopify();
1797
1590
  init_gamma();
1798
1591
  PLATFORM_REGISTRY = [
1799
- // frozen trio first (highest priority, unchanged behaviour)
1800
1592
  framer,
1801
1593
  webflow,
1802
1594
  wix,
1803
- // builders
1804
1595
  bubble,
1805
1596
  carrd,
1806
1597
  duda,
@@ -1808,25 +1599,20 @@ var init_registry = __esm({
1808
1599
  strikingly,
1809
1600
  tilda,
1810
1601
  weebly,
1811
- // landing
1812
1602
  clickfunnels,
1813
1603
  elementor,
1814
1604
  instapage,
1815
1605
  systemeio,
1816
1606
  unbounce,
1817
- // cms
1818
1607
  ghost,
1819
1608
  notion,
1820
1609
  wordpress,
1821
- // course
1822
1610
  kajabi,
1823
1611
  podia,
1824
1612
  teachable,
1825
1613
  thinkific,
1826
- // ecommerce
1827
1614
  gumroad,
1828
1615
  shopify,
1829
- // ai
1830
1616
  gamma
1831
1617
  ];
1832
1618
  CATEGORY_LABELS = {
@@ -1845,6 +1631,7 @@ var init_registry = __esm({
1845
1631
  "ecommerce",
1846
1632
  "ai"
1847
1633
  ];
1634
+ STABLE_PLATFORMS = /* @__PURE__ */ new Set(["framer", "webflow", "wix"]);
1848
1635
  }
1849
1636
  });
1850
1637
 
@@ -1938,6 +1725,7 @@ __export(platforms_exports, {
1938
1725
  CATEGORY_LABELS: () => CATEGORY_LABELS,
1939
1726
  CATEGORY_ORDER: () => CATEGORY_ORDER,
1940
1727
  PLATFORM_REGISTRY: () => PLATFORM_REGISTRY,
1728
+ STABLE_PLATFORMS: () => STABLE_PLATFORMS,
1941
1729
  detectByDom: () => detectByDom,
1942
1730
  detectByGenerator: () => detectByGenerator,
1943
1731
  detectByHtml: () => detectByHtml,
@@ -1945,6 +1733,7 @@ __export(platforms_exports, {
1945
1733
  detectPlatform: () => detectPlatform,
1946
1734
  framer: () => framer,
1947
1735
  getPlatformByName: () => getPlatformByName,
1736
+ isBetaPlatform: () => isBetaPlatform,
1948
1737
  platformsByCategory: () => platformsByCategory,
1949
1738
  readGenerator: () => readGenerator,
1950
1739
  sortedByPriority: () => sortedByPriority,
@@ -2163,36 +1952,19 @@ function boxTop(w) {
2163
1952
  const inner = w - 4;
2164
1953
  return " " + ui.border("\u256D\u2500") + ui.border("\u2500".repeat(inner)) + ui.border("\u2500\u256E");
2165
1954
  }
2166
- function panelTop(w) {
2167
- const inner = w - 4;
2168
- return ui.border("\u256D\u2500") + ui.border("\u2500".repeat(inner)) + ui.border("\u2500\u256E");
2169
- }
2170
1955
  function boxBot(w) {
2171
1956
  const inner = w - 4;
2172
1957
  return " " + ui.border("\u2570\u2500") + ui.border("\u2500".repeat(inner)) + ui.border("\u2500\u256F");
2173
1958
  }
2174
- function panelBot(w) {
2175
- const inner = w - 4;
2176
- return ui.border("\u2570\u2500") + ui.border("\u2500".repeat(inner)) + ui.border("\u2500\u256F");
2177
- }
2178
1959
  function boxLine(w, text) {
2179
1960
  const inner = w - 4;
2180
1961
  const padded = padRight(text, inner);
2181
1962
  return " " + ui.border("\u2502 ") + padded + ui.border(" \u2502");
2182
1963
  }
2183
- function panelLine(w, text) {
2184
- const inner = w - 4;
2185
- const padded = padRight(text, inner);
2186
- return ui.border("\u2502 ") + padded + ui.border(" \u2502");
2187
- }
2188
1964
  function boxSep(w) {
2189
1965
  const inner = w - 4;
2190
1966
  return " " + ui.border("\u251C\u2500") + ui.border("\u2500".repeat(inner)) + ui.border("\u2500\u2524");
2191
1967
  }
2192
- function panelSep(w) {
2193
- const inner = w - 4;
2194
- return ui.border("\u251C\u2500") + ui.border("\u2500".repeat(inner)) + ui.border("\u2500\u2524");
2195
- }
2196
1968
  function boxRow(w, label, value) {
2197
1969
  const inner = w - 4;
2198
1970
  const labelPlain = stripAnsi(chalk3.bold(label));
@@ -2208,18 +1980,116 @@ var init_box = __esm({
2208
1980
  }
2209
1981
  });
2210
1982
 
1983
+ // src/cli/backdrop.ts
1984
+ import chalk4 from "chalk";
1985
+ import { stdout } from "process";
1986
+ function hash(x, y, frame) {
1987
+ let h = x * 374761393 + y * 668265263 + frame * 2246822519;
1988
+ h = (h ^ h >> 13) * 1274126177;
1989
+ h = h ^ h >> 16;
1990
+ return Math.abs(h) % 1e3;
1991
+ }
1992
+ var GLYPHS, COLORS, FRAME_MS, DENSITY, Backdrop;
1993
+ var init_backdrop = __esm({
1994
+ "src/cli/backdrop.ts"() {
1995
+ "use strict";
1996
+ GLYPHS = ["\xB7", "\xB7", "\u2022", "\u25AA"];
1997
+ COLORS = ["#3d332a", "#4a3b2d", "#584535", "#33302c"];
1998
+ FRAME_MS = 160;
1999
+ DENSITY = 28;
2000
+ Backdrop = class {
2001
+ timer = null;
2002
+ frame = 0;
2003
+ prev = [];
2004
+ exclude = null;
2005
+ constructor() {
2006
+ if (process.env.FRAMER_EXPORT_NO_BG) this.frame = -1;
2007
+ }
2008
+ setExclude(rect) {
2009
+ this.exclude = rect;
2010
+ }
2011
+ start() {
2012
+ if (this.frame === -1 || !stdout.isTTY || this.timer) return;
2013
+ this.paint();
2014
+ this.timer = setInterval(() => this.paint(), FRAME_MS);
2015
+ }
2016
+ stop() {
2017
+ if (this.timer) clearInterval(this.timer);
2018
+ this.timer = null;
2019
+ this.erasePrev();
2020
+ this.prev = [];
2021
+ }
2022
+ inExclude(x, y) {
2023
+ const r = this.exclude;
2024
+ if (!r) return false;
2025
+ return y >= r.top && y < r.top + r.height && x >= r.left && x < r.left + r.width;
2026
+ }
2027
+ erasePrev() {
2028
+ if (this.prev.length === 0) return;
2029
+ let out = "";
2030
+ for (const [x, y] of this.prev) {
2031
+ out += `\x1B[${y};${x}H `;
2032
+ }
2033
+ stdout.write(out);
2034
+ }
2035
+ paint() {
2036
+ const rows = stdout.rows || 24;
2037
+ const columns = stdout.columns || 80;
2038
+ this.frame++;
2039
+ let out = "";
2040
+ for (const [x, y] of this.prev) {
2041
+ if (!this.inExclude(x, y)) out += `\x1B[${y};${x}H `;
2042
+ }
2043
+ const next = [];
2044
+ for (let y = 1; y <= rows; y += 2) {
2045
+ for (let x = 1; x <= columns; x += 2) {
2046
+ const h = hash(x, y, Math.floor(this.frame / 3) + Math.floor(x / 24) + Math.floor(y / 12));
2047
+ if (h >= DENSITY) continue;
2048
+ if (this.inExclude(x, y)) continue;
2049
+ const glyph = GLYPHS[h % GLYPHS.length];
2050
+ const color = COLORS[(h >> 2) % COLORS.length];
2051
+ out += `\x1B[${y};${x}H${chalk4.hex(color)(glyph)}`;
2052
+ next.push([x, y]);
2053
+ }
2054
+ }
2055
+ this.prev = next;
2056
+ stdout.write(out);
2057
+ }
2058
+ };
2059
+ }
2060
+ });
2061
+
2211
2062
  // src/cli/select.ts
2212
2063
  import readline from "readline";
2213
- import { stdin, stdout } from "process";
2064
+ import chalk5 from "chalk";
2065
+ import { stdin, stdout as stdout2 } from "process";
2066
+ function panelRow(width, content = "") {
2067
+ const visible = stripAnsi(content).length;
2068
+ const pad = Math.max(0, width - visible);
2069
+ return chalk5.bgHex(THEME.panel)(content + " ".repeat(pad));
2070
+ }
2071
+ function selectedRow(width, content) {
2072
+ const visible = content.length;
2073
+ const pad = Math.max(0, width - visible);
2074
+ return chalk5.bgHex(THEME.primary).hex(THEME.background).bold(content + " ".repeat(pad));
2075
+ }
2076
+ function titleRow(width, title) {
2077
+ const left = ` ${ui.text.bold(title)}`;
2078
+ const right = `${ui.muted("esc")} `;
2079
+ const visible = stripAnsi(left).length + stripAnsi(right).length;
2080
+ const gap = Math.max(1, width - visible);
2081
+ return chalk5.bgHex(THEME.panel)(left + " ".repeat(gap) + right);
2082
+ }
2214
2083
  async function select(question, options, defaultIndex = 0, config = {}) {
2215
- const isTTY = stdin.isTTY && stdout.isTTY;
2084
+ const isTTY = stdin.isTTY && stdout2.isTTY;
2216
2085
  if (!isTTY) {
2217
- return fallbackPrompt(question, options, defaultIndex, config);
2086
+ const flat = options.map((o) => o.heading ? { ...o, disabled: true } : o);
2087
+ return fallbackPrompt(question, flat, defaultIndex, config);
2218
2088
  }
2219
2089
  return arrowSelect(question, options, defaultIndex, config);
2220
2090
  }
2221
2091
  async function promptInput(question, defaultValue = "", config = {}) {
2222
- if (!stdin.isTTY || !stdout.isTTY) {
2092
+ if (!stdin.isTTY || !stdout2.isTTY) {
2223
2093
  return fallbackInput(question, defaultValue);
2224
2094
  }
2225
2095
  return fullscreenInput(question, defaultValue, config);
@@ -2227,73 +2097,154 @@ async function promptInput(question, defaultValue = "", config = {}) {
2227
2097
  async function arrowSelect(question, options, defaultIndex, config) {
2228
2098
  const actions = config.actions ?? [];
2229
2099
  const headerLines = config.headerLines ?? [];
2230
- const width = Math.max(44, Math.min(maxWidth(), 72));
2100
+ const searchable = config.searchable === true;
2101
+ const width = Math.max(50, Math.min(maxWidth(), 64));
2231
2102
  const inner = width - 4;
2232
- const actionLineOffset = 2 + headerLines.length;
2233
2103
  const hasActions = actions.length > 0;
2234
- const optionStartOffset = 3 + headerLines.length + (hasActions ? 1 : 0);
2235
- const lineCount = options.length + 4 + headerLines.length + (hasActions ? 1 : 0);
2104
+ const headerCount = headerLines.length;
2236
2105
  const rows = process.stdout.rows || 24;
2237
2106
  const columns = process.stdout.columns || 80;
2107
+ const searchRows = searchable ? 1 : 0;
2108
+ const optionStartOffset = 3 + searchRows + headerCount + (headerCount > 0 ? 1 : 0);
2109
+ const chromeLines = optionStartOffset + (hasActions ? 3 : 1) + 2;
2110
+ const maxVisible = Math.max(4, rows - chromeLines);
2111
+ const visibleCount = Math.min(options.length, maxVisible);
2112
+ const actionLineOffset = optionStartOffset + visibleCount + 1;
2113
+ const lineCount = actionLineOffset + (hasActions ? 2 : 1);
2238
2114
  const panelTopRow = Math.max(2, Math.floor((rows - lineCount) / 2) + 1);
2239
2115
  const panelLeftCol = Math.max(1, Math.floor((columns - width) / 2) + 1);
2240
2116
  const footerRow = Math.min(rows, panelTopRow + lineCount + 1);
2241
2117
  return new Promise((resolve) => {
2242
- const firstEnabled = options.findIndex((option) => !option.disabled);
2243
- let selected = options[defaultIndex]?.disabled ? firstEnabled : defaultIndex;
2118
+ let query = "";
2119
+ const buildView = () => {
2120
+ if (!query) return options.map((_, i) => i);
2121
+ const q = query.toLowerCase();
2122
+ const matched = /* @__PURE__ */ new Set();
2123
+ options.forEach((option, i) => {
2124
+ if (!option.heading && stripAnsi(option.label).toLowerCase().includes(q)) matched.add(i);
2125
+ });
2126
+ const view2 = [];
2127
+ for (let i = 0; i < options.length; i++) {
2128
+ if (options[i].heading) {
2129
+ for (let j = i + 1; j < options.length && !options[j].heading; j++) {
2130
+ if (matched.has(j)) {
2131
+ view2.push(i);
2132
+ break;
2133
+ }
2134
+ }
2135
+ } else if (matched.has(i)) {
2136
+ view2.push(i);
2137
+ }
2138
+ }
2139
+ return view2;
2140
+ };
2141
+ let view = buildView();
2142
+ const selectableAt = (pos) => {
2143
+ const option = options[view[pos]];
2144
+ return !option.disabled && !option.heading;
2145
+ };
2146
+ const firstSelectable = () => {
2147
+ for (let pos = 0; pos < view.length; pos++) if (selectableAt(pos)) return pos;
2148
+ return 0;
2149
+ };
2150
+ let selected = defaultIndex >= 0 && defaultIndex < options.length && view.indexOf(defaultIndex) >= 0 ? view.indexOf(defaultIndex) : firstSelectable();
2151
+ if (!view.length || !selectableAt(Math.min(selected, Math.max(0, view.length - 1)))) {
2152
+ selected = firstSelectable();
2153
+ }
2244
2154
  let selectedAction = null;
2245
- if (selected < 0) selected = 0;
2155
+ let scrollOffset = 0;
2246
2156
  const move = (direction) => {
2247
2157
  let next = selected + direction;
2248
- while (next >= 0 && next < options.length) {
2249
- if (!options[next].disabled) {
2158
+ while (next >= 0 && next < view.length) {
2159
+ if (selectableAt(next)) {
2250
2160
  selected = next;
2251
2161
  return;
2252
2162
  }
2253
2163
  next += direction;
2254
2164
  }
2255
2165
  };
2256
- const render = (initial = false) => {
2257
- if (!initial) {
2258
- stdout.write("\x1B[2J");
2166
+ const syncScroll = () => {
2167
+ if (view.length <= visibleCount) {
2168
+ scrollOffset = 0;
2169
+ return;
2259
2170
  }
2171
+ const anchor = selected > 0 && options[view[selected - 1]].heading ? selected - 1 : selected;
2172
+ if (anchor < scrollOffset) scrollOffset = anchor;
2173
+ if (selected >= scrollOffset + visibleCount) scrollOffset = selected - visibleCount + 1;
2174
+ scrollOffset = Math.max(0, Math.min(scrollOffset, view.length - visibleCount));
2175
+ };
2176
+ const backdrop = new Backdrop();
2177
+ backdrop.setExclude({
2178
+ top: panelTopRow - 1,
2179
+ left: panelLeftCol - 2,
2180
+ width: width + 4,
2181
+ height: lineCount + 3
2182
+ });
2183
+ const searchRow = () => {
2184
+ const shown = query ? `${chalk5.hex(THEME.text)(query)}${chalk5.hex(THEME.primary)("\u258C")}` : ui.muted("type to search");
2185
+ const content = ` ${chalk5.hex(THEME.text)("\u258F")} ${shown}`;
2186
+ const visible = stripAnsi(content).length;
2187
+ return chalk5.bgHex(THEME.element)(content + " ".repeat(Math.max(0, width - visible)));
2188
+ };
2189
+ const render = () => {
2260
2190
  const lines = [];
2261
- lines.push(panelTop(width));
2262
- lines.push(
2263
- panelLine(width, centerText(`${ui.primary("\u25CF")} ${ui.text.bold(question)}`, inner))
2264
- );
2191
+ lines.push(panelRow(width));
2192
+ lines.push(titleRow(width, question));
2193
+ if (searchable) lines.push(searchRow());
2194
+ lines.push(panelRow(width));
2265
2195
  for (const header of headerLines) {
2266
- lines.push(panelLine(width, centerText(ui.muted(header), inner)));
2267
- }
2268
- if (hasActions) {
2269
- lines.push(
2270
- panelLine(width, centerText(renderActions(actions, selectedAction, inner), inner))
2271
- );
2196
+ lines.push(panelRow(width, ` ${ui.muted(truncatePlain(stripAnsi(header), inner))}`));
2272
2197
  }
2273
- lines.push(panelSep(width));
2274
- for (let i = 0; i < options.length; i++) {
2275
- lines.push(
2276
- panelLine(
2277
- width,
2278
- centerText(
2279
- renderOption(options[i], selectedAction === null && i === selected, inner),
2198
+ if (headerCount > 0) lines.push(panelRow(width));
2199
+ syncScroll();
2200
+ for (let pos = scrollOffset; pos < scrollOffset + visibleCount; pos++) {
2201
+ if (pos < view.length) {
2202
+ lines.push(
2203
+ renderOption(
2204
+ options[view[pos]],
2205
+ selectedAction === null && pos === selected,
2206
+ view[pos] === defaultIndex,
2207
+ width,
2280
2208
  inner
2281
2209
  )
2282
- )
2283
- );
2210
+ );
2211
+ } else {
2212
+ lines.push(panelRow(width));
2213
+ }
2214
+ }
2215
+ if (view.length === 0) {
2216
+ lines[optionStartOffset] = panelRow(width, ` ${ui.muted("No results found")}`);
2217
+ }
2218
+ lines.push(
2219
+ view.length > visibleCount ? panelRow(width, ` ${ui.muted(`${selected + 1}/${view.length} scroll for more`)}`) : panelRow(width)
2220
+ );
2221
+ if (hasActions) {
2222
+ lines.push(panelRow(width, renderActions(actions, selectedAction)));
2223
+ lines.push(panelRow(width));
2284
2224
  }
2285
- lines.push(panelBot(width));
2286
2225
  lines.forEach((line, index) => writeAt(panelTopRow + index, panelLeftCol, line));
2287
2226
  writeAt(
2288
2227
  footerRow,
2289
2228
  panelLeftCol,
2290
2229
  centerText(
2291
- ui.muted(config.footer || "\u2191\u2193 move \xB7 enter select \xB7 mouse hover/click \xB7 esc close"),
2230
+ ui.muted(
2231
+ config.footer || (searchable ? "type to search \u2191\u2193 move enter select esc close" : "\u2191\u2193 move enter select mouse click esc close")
2232
+ ),
2292
2233
  width
2293
2234
  )
2294
2235
  );
2295
2236
  };
2296
- const choose = (value = options[selected].value) => {
2237
+ const refilter = () => {
2238
+ view = buildView();
2239
+ selected = firstSelectable();
2240
+ scrollOffset = 0;
2241
+ render();
2242
+ };
2243
+ const choose = (value) => {
2244
+ if (value === void 0) {
2245
+ if (!view.length || !selectableAt(selected)) return;
2246
+ value = options[view[selected]].value;
2247
+ }
2297
2248
  cleanup();
2298
2249
  console.log(
2299
2250
  ` ${ui.success("\u2713")} ${ui.text.bold(question)} ${ui.primary(labelForValue(value, options, actions))}
@@ -2324,10 +2275,12 @@ async function arrowSelect(question, options, defaultIndex, config) {
2324
2275
  if (mouse.kind === "click") choose(actions[actionIdx].value);
2325
2276
  return;
2326
2277
  }
2327
- const idx = mouse.y - panelTopRow - optionStartOffset;
2328
- if (idx < 0 || idx >= options.length || options[idx].disabled) return;
2329
- if (selected !== idx) {
2330
- selected = idx;
2278
+ const relative = mouse.y - panelTopRow - optionStartOffset;
2279
+ if (relative < 0 || relative >= visibleCount) return;
2280
+ const pos = scrollOffset + relative;
2281
+ if (pos >= view.length || !selectableAt(pos)) return;
2282
+ if (selected !== pos) {
2283
+ selected = pos;
2331
2284
  selectedAction = null;
2332
2285
  render();
2333
2286
  }
@@ -2336,6 +2289,7 @@ async function arrowSelect(question, options, defaultIndex, config) {
2336
2289
  }
2337
2290
  };
2338
2291
  const cleanup = () => {
2292
+ backdrop.stop();
2339
2293
  leaveInteractiveScreen();
2340
2294
  stdin.setRawMode(false);
2341
2295
  stdin.removeListener("keypress", onKeypress);
@@ -2345,14 +2299,15 @@ async function arrowSelect(question, options, defaultIndex, config) {
2345
2299
  readline.emitKeypressEvents(stdin);
2346
2300
  stdin.setRawMode(true);
2347
2301
  enterInteractiveScreen(true);
2348
- render(true);
2349
- const onKeypress = (_str, key) => {
2302
+ render();
2303
+ backdrop.start();
2304
+ const onKeypress = (str, key) => {
2350
2305
  if (!key) return;
2351
- if (key.name === "up" && selected > 0) {
2306
+ if (key.name === "up") {
2352
2307
  move(-1);
2353
2308
  selectedAction = null;
2354
2309
  render();
2355
- } else if (key.name === "down" && selected < options.length - 1) {
2310
+ } else if (key.name === "down") {
2356
2311
  move(1);
2357
2312
  selectedAction = null;
2358
2313
  render();
@@ -2360,10 +2315,25 @@ async function arrowSelect(question, options, defaultIndex, config) {
2360
2315
  selectedAction = selectedAction === null ? 0 : null;
2361
2316
  render();
2362
2317
  } else if (key.name === "return") {
2363
- choose(selectedAction === null ? options[selected].value : actions[selectedAction].value);
2364
- } else if (key.ctrl && key.name === "c" || key.name === "escape") {
2318
+ if (selectedAction !== null) choose(actions[selectedAction].value);
2319
+ else choose();
2320
+ } else if (key.ctrl && key.name === "c") {
2365
2321
  cleanup();
2366
2322
  process.exit(0);
2323
+ } else if (key.name === "escape") {
2324
+ if (searchable && query) {
2325
+ query = "";
2326
+ refilter();
2327
+ return;
2328
+ }
2329
+ cleanup();
2330
+ process.exit(0);
2331
+ } else if (searchable && key.name === "backspace") {
2332
+ query = query.slice(0, -1);
2333
+ refilter();
2334
+ } else if (searchable && str && !key.ctrl && !key.meta && str >= " " && !isTerminalSequence(str, key)) {
2335
+ query += cleanInputValue(str.replace(/[\r\n]/g, ""));
2336
+ refilter();
2367
2337
  }
2368
2338
  };
2369
2339
  stdin.resume();
@@ -2401,7 +2371,7 @@ async function fallbackPrompt(question, options, defaultIndex, config) {
2401
2371
  }
2402
2372
  }
2403
2373
  return new Promise((resolve) => {
2404
- const rl = readline.createInterface({ input: stdin, output: stdout });
2374
+ const rl = readline.createInterface({ input: stdin, output: stdout2 });
2405
2375
  const firstEnabled = options.findIndex((option) => !option.disabled);
2406
2376
  const enabledDefault = options[defaultIndex]?.disabled ? firstEnabled : defaultIndex;
2407
2377
  printFallbackOptions(question, options, enabledDefault, config);
@@ -2451,9 +2421,10 @@ async function fallbackPrompt(question, options, defaultIndex, config) {
2451
2421
  }
2452
2422
  function fullscreenInput(question, defaultValue, config) {
2453
2423
  const headerLines = config.headerLines ?? [];
2454
- const width = Math.max(44, Math.min(maxWidth(), 72));
2424
+ const width = Math.max(50, Math.min(maxWidth(), 64));
2455
2425
  const inner = width - 4;
2456
- const lineCount = 6 + headerLines.length;
2426
+ const headerCount = headerLines.length;
2427
+ const lineCount = 3 + headerCount + (headerCount > 0 ? 1 : 0) + 2;
2457
2428
  const rows = process.stdout.rows || 24;
2458
2429
  const columns = process.stdout.columns || 80;
2459
2430
  const panelTopRow = Math.max(2, Math.floor((rows - lineCount) / 2) + 1);
@@ -2461,30 +2432,40 @@ function fullscreenInput(question, defaultValue, config) {
2461
2432
  const footerRow = Math.min(rows, panelTopRow + lineCount + 1);
2462
2433
  return new Promise((resolve) => {
2463
2434
  let value = defaultValue;
2435
+ const backdrop = new Backdrop();
2436
+ backdrop.setExclude({
2437
+ top: panelTopRow - 1,
2438
+ left: panelLeftCol - 2,
2439
+ width: width + 4,
2440
+ height: lineCount + 3
2441
+ });
2464
2442
  const render = () => {
2465
- stdout.write("\x1B[2J");
2466
2443
  const shown = value || "";
2467
- const clipped = truncatePlain(shown, Math.max(12, inner - 10));
2468
- const input = `${ui.primary(">")} ${ui.text(clipped)}${ui.primary("\u258C")}`;
2469
- const lines = [];
2470
- lines.push(panelTop(width));
2471
- lines.push(
2472
- panelLine(width, centerText(`${ui.primary("\u25CF")} ${ui.text.bold(question)}`, inner))
2444
+ const clipped = truncatePlain(shown, Math.max(12, inner - 6));
2445
+ const inputContent = ` ${chalk5.hex(THEME.text)("\u258F")} ${chalk5.hex(THEME.text)(clipped)}${chalk5.hex(THEME.primary)("\u258C")}`;
2446
+ const inputVisible = stripAnsi(inputContent).length;
2447
+ const inputRow = chalk5.bgHex(THEME.element)(
2448
+ inputContent + " ".repeat(Math.max(0, width - inputVisible))
2473
2449
  );
2450
+ const lines = [];
2451
+ lines.push(panelRow(width));
2452
+ lines.push(titleRow(width, question));
2453
+ lines.push(panelRow(width));
2474
2454
  for (const header of headerLines) {
2475
- lines.push(panelLine(width, centerText(ui.muted(header), inner)));
2455
+ lines.push(panelRow(width, ` ${ui.muted(truncatePlain(stripAnsi(header), inner))}`));
2476
2456
  }
2477
- lines.push(panelSep(width));
2478
- lines.push(panelLine(width, centerText(input, inner)));
2479
- lines.push(panelBot(width));
2457
+ if (headerCount > 0) lines.push(panelRow(width));
2458
+ lines.push(inputRow);
2459
+ lines.push(panelRow(width));
2480
2460
  lines.forEach((line, index) => writeAt(panelTopRow + index, panelLeftCol, line));
2481
2461
  writeAt(
2482
2462
  footerRow,
2483
2463
  panelLeftCol,
2484
- centerText(ui.muted(config.footer || "type value \xB7 enter confirm \xB7 esc close"), width)
2464
+ centerText(ui.muted(config.footer || "type value enter confirm esc close"), width)
2485
2465
  );
2486
2466
  };
2487
2467
  const cleanup = () => {
2468
+ backdrop.stop();
2488
2469
  leaveInteractiveScreen();
2489
2470
  stdin.setRawMode(false);
2490
2471
  stdin.removeListener("keypress", onKeypress);
@@ -2523,6 +2504,7 @@ function fullscreenInput(question, defaultValue, config) {
2523
2504
  stdin.setRawMode(true);
2524
2505
  enterInteractiveScreen(false);
2525
2506
  render();
2507
+ backdrop.start();
2526
2508
  stdin.resume();
2527
2509
  stdin.on("keypress", onKeypress);
2528
2510
  });
@@ -2535,7 +2517,7 @@ async function fallbackInput(question, defaultValue) {
2535
2517
  return cleanInputValue(answer.trim() || defaultValue);
2536
2518
  }
2537
2519
  return new Promise((resolve) => {
2538
- const rl = readline.createInterface({ input: stdin, output: stdout });
2520
+ const rl = readline.createInterface({ input: stdin, output: stdout2 });
2539
2521
  const suffix = defaultValue ? ui.muted(` (${defaultValue})`) : "";
2540
2522
  rl.question(` ${ui.primary(">")} ${ui.text.bold(question)}${suffix}: `, (answer) => {
2541
2523
  rl.close();
@@ -2582,17 +2564,15 @@ function readPipedLine() {
2582
2564
  }
2583
2565
  return pipedLinesPromise.then((lines) => lines[pipedLineIndex++] ?? "");
2584
2566
  }
2585
- function renderActions(actions, active, width) {
2586
- return actions.map((action, index) => {
2587
- const label = truncatePlain(
2588
- stripAnsi(action.label),
2589
- Math.max(8, Math.floor(width / actions.length) - 8)
2590
- );
2591
- if (action.disabled) return `${ui.border("[")} ${ui.muted(label)} ${ui.border("]")}`;
2567
+ function renderActions(actions, active) {
2568
+ const rendered = actions.map((action, index) => {
2569
+ const label = stripAnsi(action.label);
2570
+ if (action.disabled) return ui.muted(label);
2592
2571
  if (active === index)
2593
- return `${ui.primary("\u203A")} ${ui.primary("[")} ${ui.text.bold(label)} ${ui.primary("]")} ${ui.primary("\u2039")}`;
2594
- return `${ui.border("[")} ${ui.primary(label)} ${ui.border("]")}`;
2595
- }).join(` ${ui.muted("\xB7")} `);
2572
+ return chalk5.bgHex(THEME.primary).hex(THEME.background).bold(` ${label} `);
2573
+ return ui.text(label);
2574
+ }).join(" ");
2575
+ return ` ${rendered}`;
2596
2576
  }
2597
2577
  function actionIndexAtX(actions, mouseX, panelLeftCol, inner) {
2598
2578
  if (actions.length === 0) return null;
@@ -2606,24 +2586,30 @@ function labelForValue(value, options, actions) {
2606
2586
  options.find((option) => option.value === value)?.label || actions.find((action) => action.value === value)?.label || value
2607
2587
  );
2608
2588
  }
2609
- function renderOption(option, selected, width) {
2610
- const plain = truncatePlain(stripAnsi(option.label), Math.max(10, width - 12));
2589
+ function renderOption(option, isSelected, isDefault, width, inner) {
2590
+ const plain = truncatePlain(stripAnsi(option.label), Math.max(10, inner - 2));
2591
+ if (option.heading) {
2592
+ return panelRow(width, ` ${ui.accent.bold(plain)}`);
2593
+ }
2594
+ if (isSelected && !option.disabled) {
2595
+ return selectedRow(width, ` ${isDefault ? "\u25CF" : " "} ${plain}`);
2596
+ }
2611
2597
  if (option.disabled) {
2612
- return `${ui.border("[")} ${ui.muted(plain)} ${ui.border("]")}`;
2598
+ return panelRow(width, ` ${ui.muted(plain)}`);
2613
2599
  }
2614
- if (selected) {
2615
- return `${ui.primary("\u203A")} ${ui.primary("[")} ${ui.text.bold(plain)} ${ui.primary("]")} ${ui.primary("\u2039")}`;
2600
+ if (isDefault) {
2601
+ return panelRow(width, ` ${ui.primary("\u25CF")} ${ui.primary(plain)}`);
2616
2602
  }
2617
- return `${ui.muted(" ")} ${ui.border("[")} ${ui.muted(plain)} ${ui.border("]")} ${ui.muted(" ")}`;
2603
+ return panelRow(width, ` ${ui.text(plain)}`);
2618
2604
  }
2619
2605
  function enterInteractiveScreen(enableMouse) {
2620
- stdout.write("\x1B[?1049h\x1B[2J\x1B[H\x1B[?25l");
2606
+ stdout2.write("\x1B[?1049h\x1B[2J\x1B[H\x1B[?25l");
2621
2607
  if (enableMouse) {
2622
- stdout.write("\x1B[?1006h\x1B[?1000h\x1B[?1002h\x1B[?1003h");
2608
+ stdout2.write("\x1B[?1006h\x1B[?1000h\x1B[?1002h\x1B[?1003h");
2623
2609
  }
2624
2610
  }
2625
2611
  function leaveInteractiveScreen() {
2626
- stdout.write(
2612
+ stdout2.write(
2627
2613
  "\x1B[?1003l\x1B[?1002l\x1B[?1000l\x1B[?1006l\x1B[?25h\x1B[?1049l"
2628
2614
  );
2629
2615
  }
@@ -2634,10 +2620,10 @@ function parseMouseEvent(chunk) {
2634
2620
  const code = Number(match[1]);
2635
2621
  const x = Number(match[2]);
2636
2622
  const y = Number(match[3]);
2637
- const state = match[4];
2623
+ const state2 = match[4];
2638
2624
  if (code === 64) return { kind: "wheel-up", x, y };
2639
2625
  if (code === 65) return { kind: "wheel-down", x, y };
2640
- if (state === "m") return { kind: "click", x, y };
2626
+ if (state2 === "m") return { kind: "click", x, y };
2641
2627
  if ((code & 32) === 32 || code === 35) return { kind: "hover", x, y };
2642
2628
  if ((code & 3) === 0) return { kind: "hover", x, y };
2643
2629
  return null;
@@ -2654,8 +2640,8 @@ function parseLegacyMouseEvent(text) {
2654
2640
  if ((code & 32) === 32) return { kind: "hover", x, y };
2655
2641
  return { kind: "hover", x, y };
2656
2642
  }
2657
- function writeAt(row, col, text) {
2658
- stdout.write(`\x1B[${row};${col}H${text}`);
2643
+ function writeAt(row2, col, text) {
2644
+ stdout2.write(`\x1B[${row2};${col}H${text}`);
2659
2645
  }
2660
2646
  function isTerminalSequence(str, key) {
2661
2647
  return str.includes("\x1B") || !!key.sequence?.includes("\x1B") || /^(?:\d+;){2}\d+[mM]$/.test(str);
@@ -2668,16 +2654,1012 @@ var init_select = __esm({
2668
2654
  "src/cli/select.ts"() {
2669
2655
  "use strict";
2670
2656
  init_box();
2657
+ init_backdrop();
2671
2658
  init_theme();
2672
2659
  pipedLinesPromise = null;
2673
2660
  pipedLineIndex = 0;
2674
2661
  }
2675
2662
  });
2676
2663
 
2664
+ // src/ui/page.ts
2665
+ var UI_HTML;
2666
+ var init_page = __esm({
2667
+ "src/ui/page.ts"() {
2668
+ "use strict";
2669
+ UI_HTML = `<!doctype html>
2670
+ <html lang="en">
2671
+ <head>
2672
+ <meta charset="utf-8" />
2673
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2674
+ <title>Framer Export</title>
2675
+ <style>
2676
+ :root {
2677
+ --bg: #0a0a0a;
2678
+ --bg-panel: #141414;
2679
+ --bg-element: #1e1e1e;
2680
+ --bg-hover: #232323;
2681
+ --text: #eeeeee;
2682
+ --muted: #808080;
2683
+ --primary: #fab283;
2684
+ --secondary: #5c9cf5;
2685
+ --accent: #9d7cd8;
2686
+ --success: #7fd88f;
2687
+ --warning: #f5a742;
2688
+ --error: #e06c75;
2689
+ --info: #56b6c2;
2690
+ --border: rgba(255, 255, 255, 0.10);
2691
+ --border-strong: rgba(255, 255, 255, 0.20);
2692
+ --radius: 5px;
2693
+ --font: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
2694
+ }
2695
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2696
+ html, body { height: 100%; }
2697
+ body {
2698
+ background: var(--bg);
2699
+ color: var(--text);
2700
+ font-family: var(--font);
2701
+ font-size: 14px;
2702
+ line-height: 1.55;
2703
+ overflow-x: hidden;
2704
+ }
2705
+ ::selection { background: #f2f7c8; color: #131010; }
2706
+ #bg {
2707
+ position: fixed;
2708
+ inset: 0;
2709
+ width: 100%;
2710
+ height: 100%;
2711
+ z-index: 0;
2712
+ opacity: 0.5;
2713
+ pointer-events: none;
2714
+ }
2715
+ .shell { position: relative; z-index: 1; min-height: 100%; display: flex; flex-direction: column; }
2716
+ header {
2717
+ position: sticky;
2718
+ top: 0;
2719
+ z-index: 5;
2720
+ display: flex;
2721
+ align-items: center;
2722
+ justify-content: space-between;
2723
+ height: 64px;
2724
+ padding: 0 2.5rem;
2725
+ background: rgba(10, 10, 10, 0.82);
2726
+ backdrop-filter: blur(10px);
2727
+ border-bottom: 1px solid var(--border);
2728
+ }
2729
+ .brand { font-size: 15px; }
2730
+ .brand .dim { color: var(--muted); }
2731
+ .brand .strong { color: var(--text); font-weight: 700; }
2732
+ .status { color: var(--muted); font-size: 12px; display: flex; align-items: center; gap: 8px; }
2733
+ .status .dot { color: var(--muted); }
2734
+ .status.running .dot { color: var(--warning); }
2735
+ .status.done .dot { color: var(--success); }
2736
+ .status.error .dot { color: var(--error); }
2737
+ main { flex: 1; padding: 2.5rem; max-width: 1180px; width: 100%; margin: 0 auto; }
2738
+ .screen { display: none; }
2739
+ .screen.active { display: block; }
2740
+ h1 { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.01em; }
2741
+ .sub { color: var(--muted); margin-top: 0.4rem; margin-bottom: 2rem; }
2742
+ .sub .star { color: var(--primary); }
2743
+ .grid {
2744
+ display: grid;
2745
+ grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
2746
+ gap: 1rem;
2747
+ padding-bottom: 3rem;
2748
+ }
2749
+ .card {
2750
+ position: relative;
2751
+ background: rgba(20, 20, 20, 0.86);
2752
+ border: 1px solid var(--border);
2753
+ border-radius: var(--radius);
2754
+ padding: 1.1rem 1.2rem 1rem;
2755
+ cursor: pointer;
2756
+ transition: border-color .15s ease, background .15s ease, transform .1s ease;
2757
+ }
2758
+ .card:hover { border-color: var(--border-strong); background: rgba(30, 30, 30, 0.92); }
2759
+ .card:active { transform: scale(0.98); }
2760
+ .card .cat { font-size: 11px; color: var(--muted); display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
2761
+ .card .cat .dot { font-size: 12px; }
2762
+ .card .name { font-weight: 700; font-size: 15px; display: flex; align-items: center; gap: 8px; }
2763
+ .badge-beta {
2764
+ font-size: 10px;
2765
+ font-weight: 700;
2766
+ color: var(--warning);
2767
+ border: 1px solid var(--warning);
2768
+ border-radius: 3px;
2769
+ padding: 1px 5px;
2770
+ letter-spacing: 0.04em;
2771
+ }
2772
+ .search-wrap { max-width: 420px; margin-bottom: 1.6rem; }
2773
+ .card .hint { font-size: 12px; color: var(--muted); margin-top: 8px; opacity: 0; transition: opacity .15s ease; }
2774
+ .card:hover .hint { opacity: 1; }
2775
+ .card.auto { border-style: dashed; }
2776
+ .cat-heading {
2777
+ grid-column: 1 / -1;
2778
+ margin-top: 1.2rem;
2779
+ color: var(--accent);
2780
+ font-weight: 700;
2781
+ font-size: 13px;
2782
+ }
2783
+ .cat-heading:first-child { margin-top: 0; }
2784
+ .panel {
2785
+ background: rgba(20, 20, 20, 0.9);
2786
+ border: 1px solid var(--border);
2787
+ border-radius: var(--radius);
2788
+ padding: 2rem;
2789
+ max-width: 640px;
2790
+ }
2791
+ .field-label { color: var(--muted); font-size: 12px; margin-bottom: 8px; }
2792
+ input[type="text"] {
2793
+ width: 100%;
2794
+ background: var(--bg-element);
2795
+ color: var(--text);
2796
+ border: 1px solid var(--border);
2797
+ border-left: 3px solid var(--primary);
2798
+ border-radius: 3px;
2799
+ font-family: var(--font);
2800
+ font-size: 14px;
2801
+ padding: 12px 14px;
2802
+ outline: none;
2803
+ }
2804
+ input[type="text"]:focus { border-color: var(--border-strong); border-left-color: var(--primary); }
2805
+ .error-line { color: var(--error); font-size: 12px; min-height: 18px; margin-top: 8px; }
2806
+ .row { display: flex; align-items: center; gap: 12px; margin-top: 1.6rem; }
2807
+ .btn {
2808
+ font-family: var(--font);
2809
+ font-size: 13px;
2810
+ padding: 9px 18px;
2811
+ border-radius: 4px;
2812
+ border: none;
2813
+ cursor: pointer;
2814
+ background: var(--bg-element);
2815
+ color: var(--text);
2816
+ box-shadow: 0 0 0 1px rgba(255,255,255,.10);
2817
+ transition: all .2s ease;
2818
+ }
2819
+ .btn:hover { box-shadow: 0 0 0 1px rgba(255,255,255,.22); }
2820
+ .btn:active { transform: scale(0.98); }
2821
+ .btn.primary { background: #f0efef; color: #131010; font-weight: 700; box-shadow: none; }
2822
+ .btn.primary:hover { background: #ffffff; }
2823
+ .btn.ghost { background: transparent; color: var(--muted); box-shadow: none; }
2824
+ .btn.ghost:hover { color: var(--text); }
2825
+ .chip {
2826
+ display: inline-flex;
2827
+ align-items: center;
2828
+ gap: 8px;
2829
+ background: var(--bg-element);
2830
+ border: 1px solid var(--border);
2831
+ border-radius: 3px;
2832
+ padding: 4px 10px;
2833
+ font-size: 12px;
2834
+ color: var(--muted);
2835
+ margin-bottom: 1.4rem;
2836
+ }
2837
+ .chip b { color: var(--primary); font-weight: 700; }
2838
+ .opt {
2839
+ display: flex;
2840
+ align-items: center;
2841
+ gap: 12px;
2842
+ padding: 13px 4px;
2843
+ border-bottom: 1px solid var(--border);
2844
+ cursor: pointer;
2845
+ user-select: none;
2846
+ }
2847
+ .opt:last-of-type { border-bottom: none; }
2848
+ .opt .box {
2849
+ width: 18px;
2850
+ height: 18px;
2851
+ flex: none;
2852
+ border: 1.5px solid var(--border-strong);
2853
+ border-radius: 3px;
2854
+ display: grid;
2855
+ place-items: center;
2856
+ color: transparent;
2857
+ font-size: 13px;
2858
+ font-weight: 700;
2859
+ transition: all .15s ease;
2860
+ }
2861
+ .opt.on .box { background: var(--primary); border-color: var(--primary); color: #131010; }
2862
+ .opt .label b { display: block; font-size: 14px; font-weight: 600; }
2863
+ .opt .label span { font-size: 12px; color: var(--muted); }
2864
+ .seg { display: inline-flex; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
2865
+ .seg button {
2866
+ font-family: var(--font);
2867
+ font-size: 12px;
2868
+ background: transparent;
2869
+ color: var(--muted);
2870
+ border: none;
2871
+ padding: 7px 16px;
2872
+ cursor: pointer;
2873
+ }
2874
+ .seg button.on { background: var(--primary); color: #131010; font-weight: 700; }
2875
+ .export-layout { display: flex; gap: 1.25rem; align-items: stretch; min-height: 62vh; }
2876
+ .term {
2877
+ flex: 1;
2878
+ min-width: 0;
2879
+ background: rgba(20, 20, 20, 0.94);
2880
+ border: 1px solid var(--border);
2881
+ border-left: 3px solid var(--primary);
2882
+ border-radius: var(--radius);
2883
+ padding: 1rem 1.2rem;
2884
+ overflow-y: auto;
2885
+ max-height: 72vh;
2886
+ font-size: 12.5px;
2887
+ line-height: 1.7;
2888
+ }
2889
+ .term .ln { white-space: pre-wrap; word-break: break-all; }
2890
+ .term .t { color: var(--muted); }
2891
+ .term .lv-log { color: var(--primary); }
2892
+ .term .lv-info { color: var(--info); font-weight: 700; }
2893
+ .term .lv-warn { color: var(--warning); font-weight: 700; }
2894
+ .term .lv-ok { color: var(--success); }
2895
+ .term .lv-error { color: var(--error); font-weight: 700; }
2896
+ .term .msg { color: var(--text); }
2897
+ .side {
2898
+ width: 300px;
2899
+ flex: none;
2900
+ background: rgba(20, 20, 20, 0.94);
2901
+ border: 1px solid var(--border);
2902
+ border-radius: var(--radius);
2903
+ padding: 1.2rem 1.3rem;
2904
+ align-self: flex-start;
2905
+ position: sticky;
2906
+ top: 84px;
2907
+ }
2908
+ .side h3 { font-size: 13px; font-weight: 700; margin-bottom: 4px; }
2909
+ .side .sec { margin-bottom: 1.3rem; }
2910
+ .side .m { color: var(--muted); font-size: 12px; }
2911
+ .side .m.err { color: var(--error); }
2912
+ .side .file { color: var(--muted); font-size: 11.5px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; direction: rtl; text-align: left; }
2913
+ .side .sumrow { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); }
2914
+ .side .sumrow b { color: var(--text); font-weight: 600; }
2915
+ .donebar { display: none; margin-top: 1.1rem; gap: 10px; flex-wrap: wrap; }
2916
+ .donebar.visible { display: flex; }
2917
+ .toast {
2918
+ position: fixed;
2919
+ top: 76px;
2920
+ right: 20px;
2921
+ z-index: 20;
2922
+ background: var(--bg-panel);
2923
+ border-left: 3px solid var(--success);
2924
+ border-right: 3px solid var(--success);
2925
+ padding: 10px 16px;
2926
+ font-size: 13px;
2927
+ border-radius: 3px;
2928
+ opacity: 0;
2929
+ transition: opacity .2s ease;
2930
+ pointer-events: none;
2931
+ }
2932
+ .toast.show { opacity: 1; }
2933
+ footer {
2934
+ padding: 1.2rem 2.5rem;
2935
+ border-top: 1px solid var(--border);
2936
+ color: var(--muted);
2937
+ font-size: 12px;
2938
+ display: flex;
2939
+ justify-content: space-between;
2940
+ background: rgba(10, 10, 10, 0.82);
2941
+ }
2942
+ @media (max-width: 860px) {
2943
+ main { padding: 1.4rem; }
2944
+ .export-layout { flex-direction: column; }
2945
+ .side { width: 100%; position: static; }
2946
+ }
2947
+ </style>
2948
+ </head>
2949
+ <body>
2950
+ <canvas id="bg"></canvas>
2951
+ <div class="shell">
2952
+ <header>
2953
+ <div class="brand"><span class="dim">framer</span><span class="strong">export</span></div>
2954
+ <div class="status" id="status"><span class="dot">\u25CF</span><span id="statusText">idle</span></div>
2955
+ </header>
2956
+ <main>
2957
+ <section class="screen active" id="screen-gallery">
2958
+ <h1>Select a tool</h1>
2959
+ <div class="sub"><span class="star">[*]</span> 25+ platforms supported. Click one, paste the URL, export.</div>
2960
+ <div class="search-wrap">
2961
+ <input type="text" id="searchInput" placeholder="search a platform..." spellcheck="false" autocomplete="off" />
2962
+ </div>
2963
+ <div class="grid" id="gallery"></div>
2964
+ </section>
2965
+
2966
+ <section class="screen" id="screen-url">
2967
+ <div class="chip">tool <b id="urlTool"></b></div>
2968
+ <div class="panel">
2969
+ <h1>Site URL</h1>
2970
+ <div class="sub">The public URL of the site to export.</div>
2971
+ <div class="field-label">URL</div>
2972
+ <input type="text" id="urlInput" placeholder="https://mysite.framer.app" spellcheck="false" autocomplete="off" />
2973
+ <div class="error-line" id="urlError"></div>
2974
+ <div class="row">
2975
+ <button class="btn primary" id="urlNext">Continue</button>
2976
+ <button class="btn ghost" id="urlBack">Back</button>
2977
+ </div>
2978
+ </div>
2979
+ </section>
2980
+
2981
+ <section class="screen" id="screen-options">
2982
+ <div class="chip">tool <b id="optTool"></b>&nbsp;&nbsp;url <b id="optUrl"></b></div>
2983
+ <div class="panel">
2984
+ <h1>Options</h1>
2985
+ <div class="sub">Everything has a sensible default.</div>
2986
+ <div class="opt on" id="optPretty">
2987
+ <div class="box">\u2713</div>
2988
+ <div class="label"><b>Pretty-print JS files</b><span>Reformat downloaded scripts for readability</span></div>
2989
+ </div>
2990
+ <div class="opt" id="optSubpages">
2991
+ <div class="box">\u2713</div>
2992
+ <div class="label"><b>Export sub-pages</b><span>Crawl internal links and mirror every page</span></div>
2993
+ </div>
2994
+ <div class="opt" style="cursor: default;">
2995
+ <div class="label" style="flex:1;"><b>Download concurrency</b><span>Parallel downloads</span></div>
2996
+ <div class="seg" id="optConcurrency">
2997
+ <button data-v="6">6</button>
2998
+ <button data-v="12" class="on">12</button>
2999
+ <button data-v="20">20</button>
3000
+ </div>
3001
+ </div>
3002
+ <div style="margin-top: 1.4rem;">
3003
+ <div class="field-label">Output directory</div>
3004
+ <input type="text" id="outInput" spellcheck="false" autocomplete="off" />
3005
+ </div>
3006
+ <div class="row">
3007
+ <button class="btn primary" id="startBtn">Start export</button>
3008
+ <button class="btn ghost" id="optBack">Back</button>
3009
+ </div>
3010
+ </div>
3011
+ </section>
3012
+
3013
+ <section class="screen" id="screen-export">
3014
+ <div class="chip">tool <b id="runTool"></b>&nbsp;&nbsp;url <b id="runUrl"></b></div>
3015
+ <div class="export-layout">
3016
+ <div class="term" id="term"></div>
3017
+ <aside class="side">
3018
+ <div class="sec">
3019
+ <h3>Export</h3>
3020
+ <div class="m" id="sidePhase">starting</div>
3021
+ <div class="m" id="sideElapsed">0:00 elapsed</div>
3022
+ </div>
3023
+ <div class="sec">
3024
+ <h3>Assets</h3>
3025
+ <div class="m" id="sideAssets">0 downloaded</div>
3026
+ <div class="m err" id="sideFailed" style="display:none;"></div>
3027
+ <div class="m" id="sideWritten">0 files written</div>
3028
+ </div>
3029
+ <div class="sec">
3030
+ <h3>Files</h3>
3031
+ <div id="sideFiles"><div class="m">waiting for output...</div></div>
3032
+ </div>
3033
+ <div class="sec" id="sideSummarySec" style="display:none;">
3034
+ <h3>Summary</h3>
3035
+ <div id="sideSummary"></div>
3036
+ </div>
3037
+ <div class="donebar" id="donebar">
3038
+ <button class="btn" id="copyLogs">Copy logs</button>
3039
+ <button class="btn" id="copyServe">Copy serve command</button>
3040
+ <button class="btn ghost" id="newExport">New export</button>
3041
+ </div>
3042
+ </aside>
3043
+ </div>
3044
+ </section>
3045
+ </main>
3046
+ <footer>
3047
+ <span id="footLeft">framer-export</span>
3048
+ <span>[*] served locally \xB7 nothing leaves your machine</span>
3049
+ </footer>
3050
+ </div>
3051
+ <div class="toast" id="toast"></div>
3052
+ <script src="/pixel-blast.js"></script>
3053
+ <script src="/app.js"></script>
3054
+ </body>
3055
+ </html>
3056
+ `;
3057
+ }
3058
+ });
3059
+
3060
+ // src/ui/app-js.ts
3061
+ var APP_JS;
3062
+ var init_app_js = __esm({
3063
+ "src/ui/app-js.ts"() {
3064
+ "use strict";
3065
+ APP_JS = `(function () {
3066
+ var CAT_COLORS = {
3067
+ builder: 'var(--primary)',
3068
+ landing: 'var(--warning)',
3069
+ cms: 'var(--info)',
3070
+ course: 'var(--accent)',
3071
+ ecommerce: 'var(--success)',
3072
+ ai: 'var(--secondary)'
3073
+ };
3074
+ var state = {
3075
+ tool: null,
3076
+ toolLabel: 'Auto-detect',
3077
+ url: '',
3078
+ pretty: true,
3079
+ subpages: false,
3080
+ concurrency: 12,
3081
+ outDir: '',
3082
+ running: false,
3083
+ serveCommand: '',
3084
+ startedAt: 0
3085
+ };
3086
+ var es = null;
3087
+ var elapsedTimer = null;
3088
+
3089
+ function $(id) { return document.getElementById(id); }
3090
+ function show(name) {
3091
+ var screens = document.querySelectorAll('.screen');
3092
+ for (var i = 0; i < screens.length; i++) screens[i].classList.remove('active');
3093
+ $('screen-' + name).classList.add('active');
3094
+ window.scrollTo(0, 0);
3095
+ }
3096
+ function setStatus(kind, text) {
3097
+ var el = $('status');
3098
+ el.className = 'status ' + kind;
3099
+ $('statusText').textContent = text;
3100
+ }
3101
+ function toast(msg) {
3102
+ var el = $('toast');
3103
+ el.textContent = msg;
3104
+ el.classList.add('show');
3105
+ setTimeout(function () { el.classList.remove('show'); }, 1800);
3106
+ }
3107
+ function copyText(text, okMsg) {
3108
+ function fallback() {
3109
+ var ta = document.createElement('textarea');
3110
+ ta.value = text;
3111
+ document.body.appendChild(ta);
3112
+ ta.select();
3113
+ try { document.execCommand('copy'); toast(okMsg); } catch (e) { toast('copy failed'); }
3114
+ document.body.removeChild(ta);
3115
+ }
3116
+ if (navigator.clipboard && navigator.clipboard.writeText) {
3117
+ navigator.clipboard.writeText(text).then(function () { toast(okMsg); }, fallback);
3118
+ } else fallback();
3119
+ }
3120
+
3121
+ if (window.PixelBlast) {
3122
+ window.PixelBlast.mount($('bg'), {
3123
+ variant: 'circle',
3124
+ pixelSize: 5,
3125
+ color: '#FAB283',
3126
+ patternScale: 3,
3127
+ patternDensity: 1.1,
3128
+ pixelSizeJitter: 0.4,
3129
+ rippleIntensityScale: 1.4,
3130
+ rippleSpeed: 0.4,
3131
+ rippleThickness: 0.12,
3132
+ speed: 0.5,
3133
+ edgeFade: 0.18
3134
+ });
3135
+ }
3136
+
3137
+ var galleryData = null;
3138
+
3139
+ function renderGallery(filter) {
3140
+ if (!galleryData) return;
3141
+ var q = (filter || '').trim().toLowerCase();
3142
+ var grid = $('gallery');
3143
+ grid.innerHTML = '';
3144
+ if (!q) {
3145
+ var auto = document.createElement('div');
3146
+ auto.className = 'card auto';
3147
+ auto.innerHTML = '<div class="cat"><span class="dot" style="color: var(--muted)">\u25CF</span>smart</div>' +
3148
+ '<div class="name">Auto-detect</div>' +
3149
+ '<div class="hint">figure out the platform from the URL</div>';
3150
+ auto.onclick = function () { pickTool(null, 'Auto-detect'); };
3151
+ grid.appendChild(auto);
3152
+ }
3153
+ var any = false;
3154
+ galleryData.categories.forEach(function (cat) {
3155
+ var matches = cat.platforms.filter(function (p) {
3156
+ return !q || p.displayName.toLowerCase().indexOf(q) !== -1 || cat.label.toLowerCase().indexOf(q) !== -1;
3157
+ });
3158
+ if (!matches.length) return;
3159
+ any = true;
3160
+ var heading = document.createElement('div');
3161
+ heading.className = 'cat-heading';
3162
+ heading.textContent = cat.label;
3163
+ grid.appendChild(heading);
3164
+ matches.forEach(function (p) {
3165
+ var card = document.createElement('div');
3166
+ card.className = 'card';
3167
+ var color = CAT_COLORS[cat.id] || 'var(--muted)';
3168
+ var badge = p.beta ? '<span class="badge-beta">beta</span>' : '';
3169
+ card.innerHTML = '<div class="cat"><span class="dot" style="color: ' + color + '">\u25CF</span>' + cat.label + '</div>' +
3170
+ '<div class="name">' + p.displayName + badge + '</div>' +
3171
+ '<div class="hint">export a ' + p.displayName + ' site</div>';
3172
+ card.onclick = function () { pickTool(p.name, p.displayName); };
3173
+ grid.appendChild(card);
3174
+ });
3175
+ });
3176
+ if (!any && q) {
3177
+ var empty = document.createElement('div');
3178
+ empty.className = 'cat-heading';
3179
+ empty.style.color = 'var(--muted)';
3180
+ empty.textContent = 'No results found';
3181
+ grid.appendChild(empty);
3182
+ }
3183
+ }
3184
+
3185
+ function loadGallery() {
3186
+ fetch('/api/platforms').then(function (r) { return r.json(); }).then(function (data) {
3187
+ galleryData = data;
3188
+ renderGallery('');
3189
+ });
3190
+ }
3191
+
3192
+ $('searchInput').addEventListener('input', function () {
3193
+ renderGallery(this.value);
3194
+ });
3195
+
3196
+ function pickTool(name, label) {
3197
+ state.tool = name;
3198
+ state.toolLabel = label;
3199
+ $('urlTool').textContent = label;
3200
+ $('urlError').textContent = '';
3201
+ show('url');
3202
+ $('urlInput').focus();
3203
+ }
3204
+
3205
+ function validUrl(value) {
3206
+ try { new URL(value); return true; } catch (e) { return false; }
3207
+ }
3208
+
3209
+ $('urlBack').onclick = function () { show('gallery'); };
3210
+ $('urlNext').onclick = function () {
3211
+ var value = $('urlInput').value.trim();
3212
+ if (!validUrl(value)) {
3213
+ $('urlError').textContent = 'Invalid URL. Enter a valid URL (https://...)';
3214
+ return;
3215
+ }
3216
+ state.url = value;
3217
+ $('optTool').textContent = state.toolLabel;
3218
+ $('optUrl').textContent = value;
3219
+ var q = '/api/derive?url=' + encodeURIComponent(value) + (state.tool ? '&platform=' + encodeURIComponent(state.tool) : '');
3220
+ fetch(q).then(function (r) { return r.json(); }).then(function (d) {
3221
+ $('outInput').value = './' + d.name;
3222
+ state.outDir = './' + d.name;
3223
+ });
3224
+ show('options');
3225
+ };
3226
+ $('urlInput').addEventListener('keydown', function (e) {
3227
+ if (e.key === 'Enter') $('urlNext').click();
3228
+ });
3229
+
3230
+ $('optPretty').onclick = function () {
3231
+ state.pretty = !state.pretty;
3232
+ this.classList.toggle('on', state.pretty);
3233
+ };
3234
+ $('optSubpages').onclick = function () {
3235
+ state.subpages = !state.subpages;
3236
+ this.classList.toggle('on', state.subpages);
3237
+ };
3238
+ var segButtons = $('optConcurrency').querySelectorAll('button');
3239
+ for (var i = 0; i < segButtons.length; i++) {
3240
+ segButtons[i].onclick = function () {
3241
+ for (var j = 0; j < segButtons.length; j++) segButtons[j].classList.remove('on');
3242
+ this.classList.add('on');
3243
+ state.concurrency = parseInt(this.getAttribute('data-v'), 10);
3244
+ };
3245
+ }
3246
+ $('optBack').onclick = function () { show('url'); };
3247
+
3248
+ $('startBtn').onclick = function () {
3249
+ if (state.running) return;
3250
+ state.outDir = $('outInput').value.trim() || state.outDir;
3251
+ state.running = true;
3252
+ state.startedAt = Date.now();
3253
+ $('runTool').textContent = state.toolLabel;
3254
+ $('runUrl').textContent = state.url;
3255
+ $('term').innerHTML = '';
3256
+ $('donebar').classList.remove('visible');
3257
+ $('sideSummarySec').style.display = 'none';
3258
+ setStatus('running', 'exporting');
3259
+ show('export');
3260
+ startElapsed();
3261
+ connectEvents();
3262
+ fetch('/api/export', {
3263
+ method: 'POST',
3264
+ headers: { 'Content-Type': 'application/json' },
3265
+ body: JSON.stringify({
3266
+ url: state.url,
3267
+ platform: state.tool,
3268
+ outDir: state.outDir,
3269
+ subpages: state.subpages,
3270
+ prettyPrint: state.pretty,
3271
+ concurrency: state.concurrency
3272
+ })
3273
+ }).then(function (r) {
3274
+ if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'export failed'); });
3275
+ }).catch(function (e) {
3276
+ appendLog({ time: '', level: 'error', message: e.message });
3277
+ setStatus('error', 'error');
3278
+ state.running = false;
3279
+ stopElapsed();
3280
+ });
3281
+ };
3282
+
3283
+ function startElapsed() {
3284
+ stopElapsed();
3285
+ elapsedTimer = setInterval(function () {
3286
+ var s = Math.floor((Date.now() - state.startedAt) / 1000);
3287
+ var m = Math.floor(s / 60);
3288
+ var r = s % 60;
3289
+ $('sideElapsed').textContent = m + ':' + (r < 10 ? '0' : '') + r + ' elapsed';
3290
+ }, 1000);
3291
+ }
3292
+ function stopElapsed() {
3293
+ if (elapsedTimer) clearInterval(elapsedTimer);
3294
+ elapsedTimer = null;
3295
+ }
3296
+
3297
+ function appendLog(rec) {
3298
+ var term = $('term');
3299
+ var nearBottom = term.scrollTop + term.clientHeight >= term.scrollHeight - 60;
3300
+ var line = document.createElement('div');
3301
+ line.className = 'ln';
3302
+ var t = rec.time ? '[' + rec.time + '] ' : '';
3303
+ line.innerHTML = '<span class="t">' + t + '</span>' +
3304
+ '<span class="lv-' + rec.level + '">[' + rec.level + ']</span> ' +
3305
+ '<span class="msg"></span>';
3306
+ line.querySelector('.msg').textContent = rec.message;
3307
+ term.appendChild(line);
3308
+ while (term.childNodes.length > 3000) term.removeChild(term.firstChild);
3309
+ if (nearBottom) term.scrollTop = term.scrollHeight;
3310
+ }
3311
+
3312
+ function renderProgress(p) {
3313
+ $('sidePhase').textContent = p.phase || 'starting';
3314
+ $('sideAssets').textContent = p.downloaded + (p.totalAssets ? '/' + p.totalAssets : '') + ' downloaded';
3315
+ if (p.failed > 0) {
3316
+ $('sideFailed').style.display = 'block';
3317
+ $('sideFailed').textContent = p.failed + ' failed';
3318
+ } else {
3319
+ $('sideFailed').style.display = 'none';
3320
+ }
3321
+ var written = p.written + ' files written';
3322
+ if (p.subpages > 0) written += ' \xB7 ' + p.subpages + ' sub-pages';
3323
+ $('sideWritten').textContent = written;
3324
+ var files = $('sideFiles');
3325
+ files.innerHTML = '';
3326
+ if (!p.recentFiles || p.recentFiles.length === 0) {
3327
+ files.innerHTML = '<div class="m">waiting for output...</div>';
3328
+ } else {
3329
+ p.recentFiles.forEach(function (f) {
3330
+ var d = document.createElement('div');
3331
+ d.className = 'file';
3332
+ d.textContent = f;
3333
+ files.appendChild(d);
3334
+ });
3335
+ }
3336
+ }
3337
+
3338
+ function renderSummary(entries) {
3339
+ var el = $('sideSummary');
3340
+ el.innerHTML = '';
3341
+ entries.forEach(function (e) {
3342
+ if (!e.count) return;
3343
+ var row = document.createElement('div');
3344
+ row.className = 'sumrow';
3345
+ row.innerHTML = '<span>' + e.dir + '</span><b>' + e.count + ' ' + e.kind + '</b>';
3346
+ el.appendChild(row);
3347
+ });
3348
+ $('sideSummarySec').style.display = 'block';
3349
+ }
3350
+
3351
+ function connectEvents() {
3352
+ if (es) return;
3353
+ es = new EventSource('/api/events');
3354
+ es.addEventListener('log', function (e) { appendLog(JSON.parse(e.data)); });
3355
+ es.addEventListener('progress', function (e) { renderProgress(JSON.parse(e.data)); });
3356
+ es.addEventListener('status', function (e) {
3357
+ var d = JSON.parse(e.data);
3358
+ if (d.state === 'done') {
3359
+ state.running = false;
3360
+ state.serveCommand = d.serveCommand || '';
3361
+ setStatus('done', 'done');
3362
+ stopElapsed();
3363
+ if (d.summary) renderSummary(d.summary);
3364
+ $('donebar').classList.add('visible');
3365
+ $('footLeft').textContent = d.outDir || 'framer-export';
3366
+ } else if (d.state === 'error') {
3367
+ state.running = false;
3368
+ setStatus('error', 'error');
3369
+ stopElapsed();
3370
+ appendLog({ time: '', level: 'error', message: d.error || 'export failed' });
3371
+ $('donebar').classList.add('visible');
3372
+ } else if (d.state === 'running') {
3373
+ setStatus('running', 'exporting');
3374
+ }
3375
+ });
3376
+ }
3377
+
3378
+ $('copyLogs').onclick = function () {
3379
+ fetch('/api/log').then(function (r) { return r.text(); }).then(function (text) {
3380
+ copyText(text, 'logs copied');
3381
+ });
3382
+ };
3383
+ $('copyServe').onclick = function () {
3384
+ copyText(state.serveCommand || 'node serve.js', 'serve command copied');
3385
+ };
3386
+ $('newExport').onclick = function () {
3387
+ setStatus('idle', 'idle');
3388
+ show('gallery');
3389
+ };
3390
+
3391
+ loadGallery();
3392
+ })();
3393
+ `;
3394
+ }
3395
+ });
3396
+
3397
+ // src/ui/pixel-blast.ts
3398
+ var FRAGMENT_LINES, VERTEX_LINES, CLIENT_LINES, PIXEL_BLAST_JS;
3399
+ var init_pixel_blast = __esm({
3400
+ "src/ui/pixel-blast.ts"() {
3401
+ "use strict";
3402
+ FRAGMENT_LINES = [
3403
+ "#version 300 es",
3404
+ "precision highp float;",
3405
+ "uniform vec3 uColor;",
3406
+ "uniform vec2 uResolution;",
3407
+ "uniform float uTime;",
3408
+ "uniform float uPixelSize;",
3409
+ "uniform float uScale;",
3410
+ "uniform float uDensity;",
3411
+ "uniform float uPixelJitter;",
3412
+ "uniform int uEnableRipples;",
3413
+ "uniform float uRippleSpeed;",
3414
+ "uniform float uRippleThickness;",
3415
+ "uniform float uRippleIntensity;",
3416
+ "uniform float uEdgeFade;",
3417
+ "uniform int uShapeType;",
3418
+ "const int SHAPE_SQUARE = 0;",
3419
+ "const int SHAPE_CIRCLE = 1;",
3420
+ "const int SHAPE_TRIANGLE = 2;",
3421
+ "const int SHAPE_DIAMOND = 3;",
3422
+ "const int MAX_CLICKS = 10;",
3423
+ "uniform vec2 uClickPos [MAX_CLICKS];",
3424
+ "uniform float uClickTimes[MAX_CLICKS];",
3425
+ "out vec4 fragColor;",
3426
+ "float Bayer2(vec2 a) { a = floor(a); return fract(a.x / 2. + a.y * a.y * .75); }",
3427
+ "#define Bayer4(a) (Bayer2(.5*(a))*0.25 + Bayer2(a))",
3428
+ "#define Bayer8(a) (Bayer4(.5*(a))*0.25 + Bayer2(a))",
3429
+ "#define FBM_OCTAVES 5",
3430
+ "#define FBM_LACUNARITY 1.25",
3431
+ "#define FBM_GAIN 1.0",
3432
+ "float hash11(float n){ return fract(sin(n)*43758.5453); }",
3433
+ "float vnoise(vec3 p){",
3434
+ " vec3 ip = floor(p);",
3435
+ " vec3 fp = fract(p);",
3436
+ " float n000 = hash11(dot(ip + vec3(0.0,0.0,0.0), vec3(1.0,57.0,113.0)));",
3437
+ " float n100 = hash11(dot(ip + vec3(1.0,0.0,0.0), vec3(1.0,57.0,113.0)));",
3438
+ " float n010 = hash11(dot(ip + vec3(0.0,1.0,0.0), vec3(1.0,57.0,113.0)));",
3439
+ " float n110 = hash11(dot(ip + vec3(1.0,1.0,0.0), vec3(1.0,57.0,113.0)));",
3440
+ " float n001 = hash11(dot(ip + vec3(0.0,0.0,1.0), vec3(1.0,57.0,113.0)));",
3441
+ " float n101 = hash11(dot(ip + vec3(1.0,0.0,1.0), vec3(1.0,57.0,113.0)));",
3442
+ " float n011 = hash11(dot(ip + vec3(0.0,1.0,1.0), vec3(1.0,57.0,113.0)));",
3443
+ " float n111 = hash11(dot(ip + vec3(1.0,1.0,1.0), vec3(1.0,57.0,113.0)));",
3444
+ " vec3 w = fp*fp*fp*(fp*(fp*6.0-15.0)+10.0);",
3445
+ " float x00 = mix(n000, n100, w.x);",
3446
+ " float x10 = mix(n010, n110, w.x);",
3447
+ " float x01 = mix(n001, n101, w.x);",
3448
+ " float x11 = mix(n011, n111, w.x);",
3449
+ " float y0 = mix(x00, x10, w.y);",
3450
+ " float y1 = mix(x01, x11, w.y);",
3451
+ " return mix(y0, y1, w.z) * 2.0 - 1.0;",
3452
+ "}",
3453
+ "float fbm2(vec2 uv, float t){",
3454
+ " vec3 p = vec3(uv * uScale, t);",
3455
+ " float amp = 1.0;",
3456
+ " float freq = 1.0;",
3457
+ " float sum = 1.0;",
3458
+ " for (int i = 0; i < FBM_OCTAVES; ++i){",
3459
+ " sum += amp * vnoise(p * freq);",
3460
+ " freq *= FBM_LACUNARITY;",
3461
+ " amp *= FBM_GAIN;",
3462
+ " }",
3463
+ " return sum * 0.5 + 0.5;",
3464
+ "}",
3465
+ "float maskCircle(vec2 p, float cov){",
3466
+ " float r = sqrt(cov) * .25;",
3467
+ " float d = length(p - 0.5) - r;",
3468
+ " float aa = 0.5 * fwidth(d);",
3469
+ " return cov * (1.0 - smoothstep(-aa, aa, d * 2.0));",
3470
+ "}",
3471
+ "float maskTriangle(vec2 p, vec2 id, float cov){",
3472
+ " bool flip = mod(id.x + id.y, 2.0) > 0.5;",
3473
+ " if (flip) p.x = 1.0 - p.x;",
3474
+ " float r = sqrt(cov);",
3475
+ " float d = p.y - r*(1.0 - p.x);",
3476
+ " float aa = fwidth(d);",
3477
+ " return cov * clamp(0.5 - d/aa, 0.0, 1.0);",
3478
+ "}",
3479
+ "float maskDiamond(vec2 p, float cov){",
3480
+ " float r = sqrt(cov) * 0.564;",
3481
+ " return step(abs(p.x - 0.49) + abs(p.y - 0.49), r);",
3482
+ "}",
3483
+ "void main(){",
3484
+ " float pixelSize = uPixelSize;",
3485
+ " vec2 fragCoord = gl_FragCoord.xy - uResolution * .5;",
3486
+ " float aspectRatio = uResolution.x / uResolution.y;",
3487
+ " vec2 pixelId = floor(fragCoord / pixelSize);",
3488
+ " vec2 pixelUV = fract(fragCoord / pixelSize);",
3489
+ " float cellPixelSize = 8.0 * pixelSize;",
3490
+ " vec2 cellId = floor(fragCoord / cellPixelSize);",
3491
+ " vec2 cellCoord = cellId * cellPixelSize;",
3492
+ " vec2 uv = cellCoord / uResolution * vec2(aspectRatio, 1.0);",
3493
+ " float base = fbm2(uv, uTime * 0.05);",
3494
+ " base = base * 0.5 - 0.65;",
3495
+ " float feed = base + (uDensity - 0.5) * 0.3;",
3496
+ " float speed = uRippleSpeed;",
3497
+ " float thickness = uRippleThickness;",
3498
+ " const float dampT = 1.0;",
3499
+ " const float dampR = 10.0;",
3500
+ " if (uEnableRipples == 1) {",
3501
+ " for (int i = 0; i < MAX_CLICKS; ++i){",
3502
+ " vec2 pos = uClickPos[i];",
3503
+ " if (pos.x < 0.0) continue;",
3504
+ " vec2 cuv = (((pos - uResolution * .5 - cellPixelSize * .5) / (uResolution))) * vec2(aspectRatio, 1.0);",
3505
+ " float t = max(uTime - uClickTimes[i], 0.0);",
3506
+ " float r = distance(uv, cuv);",
3507
+ " float waveR = speed * t;",
3508
+ " float ring = exp(-pow((r - waveR) / thickness, 2.0));",
3509
+ " float atten = exp(-dampT * t) * exp(-dampR * r);",
3510
+ " feed = max(feed, ring * atten * uRippleIntensity);",
3511
+ " }",
3512
+ " }",
3513
+ " float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;",
3514
+ " float bw = step(0.5, feed + bayer);",
3515
+ " float h = fract(sin(dot(floor(fragCoord / uPixelSize), vec2(127.1, 311.7))) * 43758.5453);",
3516
+ " float jitterScale = 1.0 + (h - 0.5) * uPixelJitter;",
3517
+ " float coverage = bw * jitterScale;",
3518
+ " float M;",
3519
+ " if (uShapeType == SHAPE_CIRCLE) M = maskCircle (pixelUV, coverage);",
3520
+ " else if (uShapeType == SHAPE_TRIANGLE) M = maskTriangle(pixelUV, pixelId, coverage);",
3521
+ " else if (uShapeType == SHAPE_DIAMOND) M = maskDiamond(pixelUV, coverage);",
3522
+ " else M = coverage;",
3523
+ " if (uEdgeFade > 0.0) {",
3524
+ " vec2 norm = gl_FragCoord.xy / uResolution;",
3525
+ " float edge = min(min(norm.x, norm.y), min(1.0 - norm.x, 1.0 - norm.y));",
3526
+ " float fade = smoothstep(0.0, uEdgeFade, edge);",
3527
+ " M *= fade;",
3528
+ " }",
3529
+ " vec3 color = uColor;",
3530
+ " vec3 srgbColor = mix(",
3531
+ " color * 12.92,",
3532
+ " 1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055,",
3533
+ " step(0.0031308, color)",
3534
+ " );",
3535
+ " fragColor = vec4(srgbColor, M);",
3536
+ "}"
3537
+ ];
3538
+ VERTEX_LINES = [
3539
+ "#version 300 es",
3540
+ "in vec2 position;",
3541
+ "void main() {",
3542
+ " gl_Position = vec4(position, 0.0, 1.0);",
3543
+ "}"
3544
+ ];
3545
+ CLIENT_LINES = [
3546
+ "(function () {",
3547
+ " var SHAPES = { square: 0, circle: 1, triangle: 2, diamond: 3 };",
3548
+ " var MAX_CLICKS = 10;",
3549
+ " function hexToRgb(hex) {",
3550
+ " var value = hex.replace('#', '');",
3551
+ " var n = parseInt(value, 16);",
3552
+ " return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];",
3553
+ " }",
3554
+ " function compile(gl, type, source) {",
3555
+ " var shader = gl.createShader(type);",
3556
+ " gl.shaderSource(shader, source);",
3557
+ " gl.compileShader(shader);",
3558
+ " if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {",
3559
+ ' throw new Error(gl.getShaderInfoLog(shader) || "shader compile failed");',
3560
+ " }",
3561
+ " return shader;",
3562
+ " }",
3563
+ " function mount(canvas, opts) {",
3564
+ " opts = opts || {};",
3565
+ ' var gl = canvas.getContext("webgl2", { alpha: true, antialias: false });',
3566
+ " if (!gl) return null;",
3567
+ " var program = gl.createProgram();",
3568
+ " gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, window.__PB_VERT__));",
3569
+ " gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, window.__PB_FRAG__));",
3570
+ " gl.linkProgram(program);",
3571
+ " if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {",
3572
+ ' throw new Error(gl.getProgramInfoLog(program) || "link failed");',
3573
+ " }",
3574
+ " gl.useProgram(program);",
3575
+ " var buffer = gl.createBuffer();",
3576
+ " gl.bindBuffer(gl.ARRAY_BUFFER, buffer);",
3577
+ " gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);",
3578
+ ' var loc = gl.getAttribLocation(program, "position");',
3579
+ " gl.enableVertexAttribArray(loc);",
3580
+ " gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);",
3581
+ " gl.enable(gl.BLEND);",
3582
+ " gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);",
3583
+ " var U = {};",
3584
+ ' ["uColor","uResolution","uTime","uPixelSize","uScale","uDensity","uPixelJitter","uEnableRipples","uRippleSpeed","uRippleThickness","uRippleIntensity","uEdgeFade","uShapeType","uClickPos","uClickTimes"].forEach(function (name) {',
3585
+ " U[name] = gl.getUniformLocation(program, name);",
3586
+ " });",
3587
+ ' var color = hexToRgb(opts.color || "#FAB283");',
3588
+ " var pixelSize = opts.pixelSize || 5;",
3589
+ " var dpr = Math.min(window.devicePixelRatio || 1, 2);",
3590
+ " var clicks = new Float32Array(MAX_CLICKS * 2).fill(-1);",
3591
+ " var clickTimes = new Float32Array(MAX_CLICKS);",
3592
+ " var clickIx = 0;",
3593
+ " var timeOffset = Math.random() * 1000;",
3594
+ " var speed = opts.speed || 0.5;",
3595
+ " var start = performance.now();",
3596
+ " function resize() {",
3597
+ " var w = canvas.clientWidth || window.innerWidth;",
3598
+ " var h = canvas.clientHeight || window.innerHeight;",
3599
+ " canvas.width = Math.floor(w * dpr);",
3600
+ " canvas.height = Math.floor(h * dpr);",
3601
+ " gl.viewport(0, 0, canvas.width, canvas.height);",
3602
+ " }",
3603
+ " resize();",
3604
+ ' window.addEventListener("resize", resize);',
3605
+ ' document.addEventListener("pointerdown", function (e) {',
3606
+ " var rect = canvas.getBoundingClientRect();",
3607
+ " var fx = (e.clientX - rect.left) * (canvas.width / rect.width);",
3608
+ " var fy = (rect.height - (e.clientY - rect.top)) * (canvas.height / rect.height);",
3609
+ " clicks[clickIx * 2] = fx;",
3610
+ " clicks[clickIx * 2 + 1] = fy;",
3611
+ " clickTimes[clickIx] = timeOffset + ((performance.now() - start) / 1000) * speed;",
3612
+ " clickIx = (clickIx + 1) % MAX_CLICKS;",
3613
+ " }, { passive: true });",
3614
+ " function frame() {",
3615
+ " var t = timeOffset + ((performance.now() - start) / 1000) * speed;",
3616
+ " gl.uniform3f(U.uColor, color[0], color[1], color[2]);",
3617
+ " gl.uniform2f(U.uResolution, canvas.width, canvas.height);",
3618
+ " gl.uniform1f(U.uTime, t);",
3619
+ " gl.uniform1f(U.uPixelSize, pixelSize * dpr);",
3620
+ " gl.uniform1f(U.uScale, opts.patternScale || 3);",
3621
+ " gl.uniform1f(U.uDensity, opts.patternDensity || 1.2);",
3622
+ " gl.uniform1f(U.uPixelJitter, opts.pixelSizeJitter || 0.5);",
3623
+ " gl.uniform1i(U.uEnableRipples, opts.enableRipples === false ? 0 : 1);",
3624
+ " gl.uniform1f(U.uRippleSpeed, opts.rippleSpeed || 0.4);",
3625
+ " gl.uniform1f(U.uRippleThickness, opts.rippleThickness || 0.12);",
3626
+ " gl.uniform1f(U.uRippleIntensity, opts.rippleIntensityScale || 1.5);",
3627
+ " gl.uniform1f(U.uEdgeFade, opts.edgeFade == null ? 0.25 : opts.edgeFade);",
3628
+ ' gl.uniform1i(U.uShapeType, SHAPES[opts.variant || "circle"] || 0);',
3629
+ " gl.uniform2fv(U.uClickPos, clicks);",
3630
+ " gl.uniform1fv(U.uClickTimes, clickTimes);",
3631
+ " gl.clearColor(0, 0, 0, 0);",
3632
+ " gl.clear(gl.COLOR_BUFFER_BIT);",
3633
+ " gl.drawArrays(gl.TRIANGLES, 0, 3);",
3634
+ " requestAnimationFrame(frame);",
3635
+ " }",
3636
+ " requestAnimationFrame(frame);",
3637
+ " return { gl: gl };",
3638
+ " }",
3639
+ " window.PixelBlast = { mount: mount };",
3640
+ "})();"
3641
+ ];
3642
+ PIXEL_BLAST_JS = [
3643
+ "window.__PB_VERT__ = " + JSON.stringify(VERTEX_LINES.join("\n")) + ";",
3644
+ "window.__PB_FRAG__ = " + JSON.stringify(FRAGMENT_LINES.join("\n")) + ";",
3645
+ CLIENT_LINES.join("\n")
3646
+ ].join("\n");
3647
+ }
3648
+ });
3649
+
2677
3650
  // src/assets/asset-map.ts
2678
3651
  import path from "path";
2679
3652
  import crypto from "crypto";
2680
3653
  import { URL as URL2 } from "url";
3654
+ function replaceUrl(text, url, rel) {
3655
+ const parts = text.split(url);
3656
+ if (parts.length === 1) return text;
3657
+ let out = parts[0];
3658
+ for (let i = 1; i < parts.length; i++) {
3659
+ out += (parts[i].startsWith("/") ? url : rel) + parts[i];
3660
+ }
3661
+ return out;
3662
+ }
2681
3663
  var AssetMap;
2682
3664
  var init_asset_map = __esm({
2683
3665
  "src/assets/asset-map.ts"() {
@@ -2705,12 +3687,12 @@ var init_asset_map = __esm({
2705
3687
  }
2706
3688
  let filename;
2707
3689
  const baseName = path.basename(pathname.split("?")[0]);
2708
- const hash = crypto.createHash("md5").update(urlStr).digest("hex").slice(0, 6);
3690
+ const hash2 = crypto.createHash("md5").update(urlStr).digest("hex").slice(0, 6);
2709
3691
  if (baseName && baseName.length > 1 && baseName !== "/") {
2710
3692
  const clean = baseName.replace(/[^a-zA-Z0-9._-]/g, "_");
2711
- filename = clean.includes(".") ? clean : `${clean}-${hash}`;
3693
+ filename = clean.includes(".") ? clean : `${clean}-${hash2}`;
2712
3694
  } else {
2713
- filename = `asset-${hash}${ext || ""}`;
3695
+ filename = `asset-${hash2}${ext || ""}`;
2714
3696
  }
2715
3697
  if (ext === ".mjs" || ext === ".js") {
2716
3698
  filename = baseName.replace(/[^a-zA-Z0-9._-]/g, "_");
@@ -2727,10 +3709,11 @@ var init_asset_map = __esm({
2727
3709
  const sorted = [...this.entries.entries()].sort((a, b) => b[0].length - a[0].length);
2728
3710
  let out = text;
2729
3711
  for (const [url, { localPath }] of sorted) {
2730
- const rel = fromDir ? path.posix.relative(fromDir, localPath) : localPath;
2731
- out = out.split(url).join(rel);
3712
+ let rel = fromDir ? path.posix.relative(fromDir, localPath) : localPath;
3713
+ if (fromDir && !rel.startsWith(".")) rel = "./" + rel;
3714
+ out = replaceUrl(out, url, rel);
2732
3715
  if (url.includes("&")) {
2733
- out = out.split(url.replace(/&/g, "&amp;")).join(rel);
3716
+ out = replaceUrl(out, url.replace(/&/g, "&amp;"), rel);
2734
3717
  }
2735
3718
  }
2736
3719
  return out;
@@ -2755,7 +3738,7 @@ var init_config = __esm({
2755
3738
  "src/config/index.ts"() {
2756
3739
  "use strict";
2757
3740
  CFG = {
2758
- viewport: { width: 1440, height: 900 },
3741
+ viewport: { width: 1440, height: 900, deviceScaleFactor: 1 },
2759
3742
  timeout: 9e4,
2760
3743
  scrollStep: 250,
2761
3744
  scrollDelay: 60,
@@ -2810,7 +3793,7 @@ var init_download = __esm({
2810
3793
  });
2811
3794
 
2812
3795
  // src/logger/index.ts
2813
- import chalk4 from "chalk";
3796
+ import chalk6 from "chalk";
2814
3797
  function setCooking(spinner) {
2815
3798
  _cooking = spinner;
2816
3799
  }
@@ -2818,69 +3801,268 @@ function trunc(text, max) {
2818
3801
  if (text.length <= max) return text;
2819
3802
  return text.slice(0, max - 2) + "..";
2820
3803
  }
2821
- function output(line) {
3804
+ function onLog(listener) {
3805
+ _listeners.add(listener);
3806
+ return () => _listeners.delete(listener);
3807
+ }
3808
+ function getLogHistory() {
3809
+ return _history;
3810
+ }
3811
+ function clearLogHistory() {
3812
+ _history.length = 0;
3813
+ }
3814
+ function setMessageWidth(width) {
3815
+ _messageWidth = width ?? 120;
3816
+ }
3817
+ function setRenderHook(hook) {
3818
+ _renderHook = hook;
3819
+ }
3820
+ function record(level, message) {
3821
+ const entry = { time: T(), level, message };
3822
+ _history.push(entry);
3823
+ if (_history.length > HISTORY_LIMIT) _history.shift();
3824
+ for (const listener of _listeners) listener(entry);
3825
+ return entry;
3826
+ }
3827
+ function output(line, channel = "log") {
2822
3828
  if (_cooking) _cooking.log(line);
3829
+ else if (channel === "warn") console.warn(line);
3830
+ else if (channel === "error") console.error(line);
2823
3831
  else console.log(line);
3832
+ _renderHook?.();
2824
3833
  }
2825
- var _cooking, T, LOG_PALETTE, _li, log, INFO_PALETTE, _ii, info, warn, success;
3834
+ var _cooking, T, _history, _listeners, HISTORY_LIMIT, _messageWidth, _renderHook, LOG_PALETTE, _li, log, INFO_PALETTE, _ii, info, warn, success;
2826
3835
  var init_logger = __esm({
2827
3836
  "src/logger/index.ts"() {
2828
3837
  "use strict";
2829
3838
  init_theme();
2830
3839
  _cooking = null;
2831
3840
  T = () => (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
3841
+ _history = [];
3842
+ _listeners = /* @__PURE__ */ new Set();
3843
+ HISTORY_LIMIT = 5e3;
3844
+ _messageWidth = 120;
3845
+ _renderHook = null;
2832
3846
  LOG_PALETTE = [
2833
- (s) => chalk4.hex(THEME.primary)(s),
2834
- (s) => chalk4.hex(THEME.primarySoft)(s),
2835
- (s) => chalk4.hex(THEME.secondary)(s),
2836
- (s) => chalk4.hex(THEME.accent)(s),
2837
- (s) => chalk4.hex(THEME.info)(s),
2838
- (s) => chalk4.hex(THEME.text)(s)
3847
+ (s) => chalk6.hex(THEME.primary)(s),
3848
+ (s) => chalk6.hex(THEME.primarySoft)(s),
3849
+ (s) => chalk6.hex(THEME.secondary)(s),
3850
+ (s) => chalk6.hex(THEME.accent)(s),
3851
+ (s) => chalk6.hex(THEME.info)(s),
3852
+ (s) => chalk6.hex(THEME.text)(s)
2839
3853
  ];
2840
3854
  _li = 0;
2841
3855
  log = (m) => {
3856
+ const { time } = record("log", m);
2842
3857
  _li++;
2843
3858
  const c = LOG_PALETTE[_li % LOG_PALETTE.length];
2844
3859
  output(
2845
- `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.primary)("[log]")} ${c(trunc(m, 120))}`
3860
+ `${chalk6.hex(THEME.muted)(`[${time}]`)} ${chalk6.hex(THEME.primary)("[log]")} ${c(trunc(m, _messageWidth))}`
2846
3861
  );
2847
3862
  };
2848
3863
  INFO_PALETTE = [
2849
- (s) => chalk4.hex(THEME.info)(s),
2850
- (s) => chalk4.hex(THEME.secondary)(s),
2851
- (s) => chalk4.hex(THEME.primarySoft)(s)
3864
+ (s) => chalk6.hex(THEME.info)(s),
3865
+ (s) => chalk6.hex(THEME.secondary)(s),
3866
+ (s) => chalk6.hex(THEME.primarySoft)(s)
2852
3867
  ];
2853
3868
  _ii = 0;
2854
3869
  info = (m) => {
3870
+ const { time } = record("info", m);
2855
3871
  _ii++;
2856
3872
  const c = INFO_PALETTE[_ii % INFO_PALETTE.length];
2857
3873
  output(
2858
- `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.info).bold("[info]")} ${c(trunc(m, 120))}`
3874
+ `${chalk6.hex(THEME.muted)(`[${time}]`)} ${chalk6.hex(THEME.info).bold("[info]")} ${c(trunc(m, _messageWidth))}`
2859
3875
  );
2860
3876
  };
2861
3877
  warn = (m) => {
2862
- const colors = [
2863
- (s) => chalk4.hex(THEME.warning)(s),
2864
- (s) => chalk4.hex(THEME.primary)(s)
2865
- ];
2866
- const c = colors[Math.floor(Math.random() * colors.length)];
2867
- const line = `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.warning).bold("[warn]")} ${c(trunc(m, 120))}`;
2868
- if (_cooking) _cooking.log(line);
2869
- else console.warn(line);
3878
+ const { time } = record("warn", m);
3879
+ const line = `${chalk6.hex(THEME.muted)(`[${time}]`)} ${chalk6.hex(THEME.warning).bold("[warn]")} ${chalk6.hex(THEME.warning)(trunc(m, _messageWidth))}`;
3880
+ output(line, "warn");
2870
3881
  };
2871
3882
  success = (m) => {
2872
- const colors = [
2873
- (s) => chalk4.hex(THEME.success)(s),
2874
- (s) => chalk4.hex(THEME.info)(s)
2875
- ];
2876
- const c = colors[Math.floor(Math.random() * colors.length)];
3883
+ const { time } = record("ok", m);
2877
3884
  output(
2878
- `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.success)("[ok]")} ${c(trunc(m, 120))}`
3885
+ `${chalk6.hex(THEME.muted)(`[${time}]`)} ${chalk6.hex(THEME.success)("[ok]")} ${chalk6.hex(THEME.success)(trunc(m, _messageWidth))}`
2879
3886
  );
2880
3887
  };
2881
3888
  }
2882
3889
  });
2883
3890
 
3891
+ // src/exporter/progress.ts
3892
+ function emit() {
3893
+ for (const listener of listeners) listener(state);
3894
+ }
3895
+ function onProgress(listener) {
3896
+ listeners.add(listener);
3897
+ return () => listeners.delete(listener);
3898
+ }
3899
+ function getProgress() {
3900
+ return state;
3901
+ }
3902
+ function resetProgress() {
3903
+ state.phase = "";
3904
+ state.startedAt = Date.now();
3905
+ state.totalAssets = 0;
3906
+ state.downloaded = 0;
3907
+ state.failed = 0;
3908
+ state.written = 0;
3909
+ state.subpages = 0;
3910
+ state.recentFiles = [];
3911
+ emit();
3912
+ }
3913
+ function setPhase(phase) {
3914
+ state.phase = phase;
3915
+ emit();
3916
+ }
3917
+ function setTotalAssets(total) {
3918
+ state.totalAssets = total;
3919
+ emit();
3920
+ }
3921
+ function noteDownload(ok) {
3922
+ if (ok) state.downloaded++;
3923
+ else state.failed++;
3924
+ emit();
3925
+ }
3926
+ function noteFile(relPath) {
3927
+ state.written++;
3928
+ state.recentFiles.push(relPath.replace(/\\/g, "/"));
3929
+ if (state.recentFiles.length > RECENT_LIMIT) state.recentFiles.shift();
3930
+ emit();
3931
+ }
3932
+ function noteSubpage() {
3933
+ state.subpages++;
3934
+ emit();
3935
+ }
3936
+ var RECENT_LIMIT, state, listeners;
3937
+ var init_progress = __esm({
3938
+ "src/exporter/progress.ts"() {
3939
+ "use strict";
3940
+ RECENT_LIMIT = 10;
3941
+ state = {
3942
+ phase: "",
3943
+ startedAt: 0,
3944
+ totalAssets: 0,
3945
+ downloaded: 0,
3946
+ failed: 0,
3947
+ written: 0,
3948
+ subpages: 0,
3949
+ recentFiles: []
3950
+ };
3951
+ listeners = /* @__PURE__ */ new Set();
3952
+ }
3953
+ });
3954
+
3955
+ // src/cli/sidebar.ts
3956
+ import chalk7 from "chalk";
3957
+ import { stdout as stdout3 } from "process";
3958
+ function leftTruncate(text, max) {
3959
+ if (text.length <= max) return text;
3960
+ return "\u2026" + text.slice(-(max - 1));
3961
+ }
3962
+ function row(content = "") {
3963
+ const visible = stripAnsi(content).length;
3964
+ const pad = Math.max(0, SIDEBAR_WIDTH - visible);
3965
+ return chalk7.bgHex(THEME.panel)(content + " ".repeat(pad));
3966
+ }
3967
+ function formatElapsed(startedAt) {
3968
+ const seconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
3969
+ const m = Math.floor(seconds / 60);
3970
+ const s = seconds % 60;
3971
+ return `${m}:${String(s).padStart(2, "0")}`;
3972
+ }
3973
+ var SIDEBAR_WIDTH, MIN_COLUMNS, TOP_ROW, ExportSidebar;
3974
+ var init_sidebar = __esm({
3975
+ "src/cli/sidebar.ts"() {
3976
+ "use strict";
3977
+ init_theme();
3978
+ init_progress();
3979
+ init_logger();
3980
+ SIDEBAR_WIDTH = 40;
3981
+ MIN_COLUMNS = 100;
3982
+ TOP_ROW = 2;
3983
+ ExportSidebar = class {
3984
+ active = false;
3985
+ unsubscribeProgress = null;
3986
+ timer = null;
3987
+ lastDraw = 0;
3988
+ height = 0;
3989
+ start() {
3990
+ const columns = stdout3.columns || 0;
3991
+ if (!stdout3.isTTY || columns < MIN_COLUMNS) return;
3992
+ this.active = true;
3993
+ setMessageWidth(Math.max(40, columns - SIDEBAR_WIDTH - 22));
3994
+ setRenderHook(() => this.draw(true));
3995
+ this.unsubscribeProgress = onProgress(() => this.draw(false));
3996
+ this.timer = setInterval(() => this.draw(true), 1e3);
3997
+ this.draw(true);
3998
+ }
3999
+ stop() {
4000
+ if (!this.active) return;
4001
+ this.active = false;
4002
+ setMessageWidth(null);
4003
+ setRenderHook(null);
4004
+ this.unsubscribeProgress?.();
4005
+ if (this.timer) clearInterval(this.timer);
4006
+ this.timer = null;
4007
+ this.clear();
4008
+ }
4009
+ draw(force) {
4010
+ if (!this.active) return;
4011
+ const now = Date.now();
4012
+ if (!force && now - this.lastDraw < 80) return;
4013
+ this.lastDraw = now;
4014
+ const columns = stdout3.columns || 0;
4015
+ if (columns < MIN_COLUMNS) return;
4016
+ const col = columns - SIDEBAR_WIDTH + 1;
4017
+ const p = getProgress();
4018
+ const lines = [];
4019
+ lines.push(row());
4020
+ lines.push(row(` ${ui.text.bold("Export")}`));
4021
+ lines.push(row(` ${ui.muted(leftTruncate(p.phase || "starting", SIDEBAR_WIDTH - 4))}`));
4022
+ lines.push(row(` ${ui.muted(formatElapsed(p.startedAt) + " elapsed")}`));
4023
+ lines.push(row());
4024
+ lines.push(row(` ${ui.text.bold("Assets")}`));
4025
+ lines.push(
4026
+ row(
4027
+ ` ${ui.muted(`${p.downloaded}${p.totalAssets ? "/" + p.totalAssets : ""} downloaded`)}` + (p.failed > 0 ? ` ${ui.error(`${p.failed} failed`)}` : "")
4028
+ )
4029
+ );
4030
+ lines.push(row(` ${ui.muted(`${p.written} files written`)}`));
4031
+ if (p.subpages > 0) lines.push(row(` ${ui.muted(`${p.subpages} sub-pages`)}`));
4032
+ lines.push(row());
4033
+ lines.push(row(` ${ui.text.bold("Files")}`));
4034
+ if (p.recentFiles.length === 0) {
4035
+ lines.push(row(` ${ui.muted("waiting for output...")}`));
4036
+ } else {
4037
+ for (const file of p.recentFiles) {
4038
+ lines.push(row(` ${ui.muted(leftTruncate(file, SIDEBAR_WIDTH - 4))}`));
4039
+ }
4040
+ }
4041
+ lines.push(row());
4042
+ this.height = Math.max(this.height, lines.length);
4043
+ stdout3.write("\x1B7");
4044
+ lines.forEach((line, index) => {
4045
+ stdout3.write(`\x1B[${TOP_ROW + index};${col}H${line}`);
4046
+ });
4047
+ for (let extra = lines.length; extra < this.height; extra++) {
4048
+ stdout3.write(`\x1B[${TOP_ROW + extra};${col}H${" ".repeat(SIDEBAR_WIDTH)}`);
4049
+ }
4050
+ stdout3.write("\x1B8");
4051
+ }
4052
+ clear() {
4053
+ const columns = stdout3.columns || 0;
4054
+ if (columns < MIN_COLUMNS) return;
4055
+ const col = columns - SIDEBAR_WIDTH + 1;
4056
+ stdout3.write("\x1B7");
4057
+ for (let index = 0; index < this.height; index++) {
4058
+ stdout3.write(`\x1B[${TOP_ROW + index};${col}H${" ".repeat(SIDEBAR_WIDTH)}`);
4059
+ }
4060
+ stdout3.write("\x1B8");
4061
+ }
4062
+ };
4063
+ }
4064
+ });
4065
+
2884
4066
  // src/exporter/anti-bot.ts
2885
4067
  var anti_bot_exports = {};
2886
4068
  __export(anti_bot_exports, {
@@ -2973,34 +4155,42 @@ async function launchAndCapture(exporter) {
2973
4155
  });
2974
4156
  success("Chromium launched");
2975
4157
  exporter.page = await exporter.browser.newPage();
2976
- await exporter.page.setViewport(CFG.viewport);
2977
- log("Viewport set to " + CFG.viewport.width + "x" + CFG.viewport.height);
4158
+ const viewport = {
4159
+ ...CFG.viewport,
4160
+ deviceScaleFactor: exporter.deviceScaleFactor ?? CFG.viewport.deviceScaleFactor ?? 1
4161
+ };
4162
+ await exporter.page.setViewport(viewport);
4163
+ log(
4164
+ "Viewport set to " + viewport.width + "x" + viewport.height + " at " + viewport.deviceScaleFactor + "x DPR"
4165
+ );
2978
4166
  await exporter.page.setUserAgent(
2979
4167
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
2980
4168
  );
2981
4169
  log("User agent set to Chrome 131");
2982
- const allStripDomains = [
2983
- ...CFG.sharedStripDomains,
2984
- ...exporter.platform.stripDomains
2985
- ];
4170
+ const allStripDomains = [...CFG.sharedStripDomains, ...exporter.platform.stripDomains];
2986
4171
  log("Blocking " + allStripDomains.length + " tracking domains:");
2987
4172
  for (const domain of allStripDomains) {
2988
4173
  log(" - " + domain);
2989
4174
  }
2990
4175
  let intercepted = 0;
2991
- let blocked = 0;
4176
+ let trackingBlocked = 0;
4177
+ let platformSkipped = 0;
2992
4178
  exporter.page.on("response", async (res) => {
2993
4179
  const url = res.url();
2994
4180
  if (url.startsWith("data:") || url.startsWith("blob:")) return;
2995
4181
  try {
2996
4182
  const host = new URL(url).hostname;
2997
4183
  if (allStripDomains.some((d) => host.includes(d))) {
2998
- blocked++;
4184
+ trackingBlocked++;
2999
4185
  return;
3000
4186
  }
3001
4187
  } catch {
3002
4188
  return;
3003
4189
  }
4190
+ if (exporter.platform.skipAssetUrls?.some((re) => re.test(url))) {
4191
+ platformSkipped++;
4192
+ return;
4193
+ }
3004
4194
  exporter.assets.localPathFor(url, exporter.platform);
3005
4195
  try {
3006
4196
  exporter.assets.buffers.set(url, await res.buffer());
@@ -3016,11 +4206,15 @@ async function launchAndCapture(exporter) {
3016
4206
  timeout: CFG.timeout
3017
4207
  });
3018
4208
  success("Page loaded (networkidle2)");
3019
- log("Intercepted " + intercepted + " resources, blocked " + blocked + " tracking requests");
4209
+ log(
4210
+ "Intercepted " + intercepted + " resources, blocked " + trackingBlocked + " tracking requests, skipped " + platformSkipped + " platform assets"
4211
+ );
3020
4212
  log("Checking DOM-based platform detection...");
3021
4213
  const domDetected = await detectByDom(exporter.page);
3022
4214
  if (domDetected && domDetected.name !== exporter.platform.name) {
3023
- log("Platform refined from DOM: " + domDetected.displayName + " (override: " + exporter.platform.name + ")");
4215
+ log(
4216
+ "Platform refined from DOM: " + domDetected.displayName + " (override: " + exporter.platform.name + ")"
4217
+ );
3024
4218
  exporter.platform = domDetected;
3025
4219
  }
3026
4220
  const antiBot = await detectAntiBotPage(exporter.page);
@@ -3065,13 +4259,17 @@ async function launchAndCapture(exporter) {
3065
4259
  log("Scroll skipped (scrollStrategy: none)");
3066
4260
  } else {
3067
4261
  exporter.cooking?.update("Scrolling page...");
3068
- log("Starting full-page scroll (step: " + CFG.scrollStep + "px, delay: " + CFG.scrollDelay + "ms)");
4262
+ log(
4263
+ "Starting full-page scroll (step: " + CFG.scrollStep + "px, delay: " + CFG.scrollDelay + "ms)"
4264
+ );
3069
4265
  const scrollStep = CFG.scrollStep;
3070
4266
  const scrollDelay = CFG.scrollDelay;
3071
4267
  const pageHeight = await exporter.page.evaluate(`
3072
4268
  Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)
3073
4269
  `);
3074
- log("Page height: " + pageHeight + "px (" + Math.ceil(pageHeight / scrollStep) + " scroll steps)");
4270
+ log(
4271
+ "Page height: " + pageHeight + "px (" + Math.ceil(pageHeight / scrollStep) + " scroll steps)"
4272
+ );
3075
4273
  const passes = scrollStrategy === "infinite" ? 3 : 1;
3076
4274
  for (let pass = 0; pass < passes; pass++) {
3077
4275
  await exporter.page.evaluate(`
@@ -3109,7 +4307,9 @@ async function launchAndCapture(exporter) {
3109
4307
  );
3110
4308
  if (rendered && rendered.length > 200) {
3111
4309
  exporter.ssrHTML = rendered;
3112
- log("Captured rendered DOM as HTML source (" + (rendered.length / 1024).toFixed(1) + " KB)");
4310
+ log(
4311
+ "Captured rendered DOM as HTML source (" + (rendered.length / 1024).toFixed(1) + " KB)"
4312
+ );
3113
4313
  }
3114
4314
  } catch (e) {
3115
4315
  log("Rendered DOM capture failed (keeping SSR HTML): " + e.message);
@@ -3117,11 +4317,21 @@ async function launchAndCapture(exporter) {
3117
4317
  }
3118
4318
  const totalCaptured = exporter.assets.buffers.size;
3119
4319
  success("Captured " + totalCaptured + " network resources total");
3120
- const cssCount = [...exporter.assets.entries.values()].filter((e) => e.localPath.endsWith(".css")).length;
3121
- const jsCount = [...exporter.assets.entries.values()].filter((e) => e.localPath.endsWith(".js") || e.localPath.endsWith(".mjs")).length;
3122
- const imgCount = [...exporter.assets.entries.values()].filter((e) => e.localPath.startsWith("assets/images")).length;
3123
- const fontCount = [...exporter.assets.entries.values()].filter((e) => e.localPath.startsWith("assets/fonts")).length;
3124
- log(" CSS: " + cssCount + " | JS: " + jsCount + " | Images: " + imgCount + " | Fonts: " + fontCount);
4320
+ const cssCount = [...exporter.assets.entries.values()].filter(
4321
+ (e) => e.localPath.endsWith(".css")
4322
+ ).length;
4323
+ const jsCount = [...exporter.assets.entries.values()].filter(
4324
+ (e) => e.localPath.endsWith(".js") || e.localPath.endsWith(".mjs")
4325
+ ).length;
4326
+ const imgCount = [...exporter.assets.entries.values()].filter(
4327
+ (e) => e.localPath.startsWith("assets/images")
4328
+ ).length;
4329
+ const fontCount = [...exporter.assets.entries.values()].filter(
4330
+ (e) => e.localPath.startsWith("assets/fonts")
4331
+ ).length;
4332
+ log(
4333
+ " CSS: " + cssCount + " | JS: " + jsCount + " | Images: " + imgCount + " | Fonts: " + fontCount
4334
+ );
3125
4335
  }
3126
4336
  async function closeBrowser(exporter) {
3127
4337
  if (exporter.browser) {
@@ -3134,8 +4344,7 @@ async function captureSubpage(page, url, platform) {
3134
4344
  log(" Navigating to sub-page: " + url);
3135
4345
  await page.goto(url, { waitUntil: "networkidle2", timeout: CFG.timeout });
3136
4346
  if (platform.needsHydrationCheck) {
3137
- await page.evaluate(
3138
- `new Promise(function(r) {
4347
+ await page.evaluate(`new Promise(function(r) {
3139
4348
  var t = Date.now();
3140
4349
  (function tick() {
3141
4350
  var m = document.getElementById('main') || document.body;
@@ -3143,12 +4352,13 @@ async function captureSubpage(page, url, platform) {
3143
4352
  else if (Date.now() - t > ${platform.hydrationTimeout}) r();
3144
4353
  else setTimeout(tick, 200);
3145
4354
  })();
3146
- })`
3147
- );
4355
+ })`);
3148
4356
  } else {
3149
4357
  await new Promise((r) => setTimeout(r, 1e3));
3150
4358
  }
3151
- const html = await page.evaluate(() => document.documentElement.outerHTML || document.body.innerHTML);
4359
+ const html = await page.evaluate(
4360
+ () => document.documentElement.outerHTML || document.body.innerHTML
4361
+ );
3152
4362
  log(" Sub-page fetched: " + (html.length / 1024).toFixed(1) + " KB");
3153
4363
  return html;
3154
4364
  }
@@ -3165,13 +4375,13 @@ var init_capture = __esm({
3165
4375
  // src/network/pool.ts
3166
4376
  async function pool(tasks, n) {
3167
4377
  let i = 0;
3168
- const run = async () => {
4378
+ const run2 = async () => {
3169
4379
  while (i < tasks.length) {
3170
4380
  const j = i++;
3171
4381
  await tasks[j]();
3172
4382
  }
3173
4383
  };
3174
- await Promise.all(Array.from({ length: Math.min(n, tasks.length) }, () => run()));
4384
+ await Promise.all(Array.from({ length: Math.min(n, tasks.length) }, () => run2()));
3175
4385
  }
3176
4386
  var init_pool = __esm({
3177
4387
  "src/network/pool.ts"() {
@@ -3191,6 +4401,7 @@ async function downloadAll(exporter) {
3191
4401
  toDownload.push({ url, localPath });
3192
4402
  }
3193
4403
  const total = toDownload.length;
4404
+ setTotalAssets(total);
3194
4405
  log("Starting download of " + total + " unique assets");
3195
4406
  log("Concurrency: " + CFG.concurrency + " parallel downloads");
3196
4407
  log("Retry policy: " + CFG.retries + " attempts, " + CFG.dlTimeout + "ms timeout");
@@ -3214,8 +4425,11 @@ async function downloadAll(exporter) {
3214
4425
  await fs.writeFile(dest, data);
3215
4426
  ok++;
3216
4427
  }
4428
+ noteDownload(true);
4429
+ noteFile(localPath);
3217
4430
  } catch (e) {
3218
4431
  fail++;
4432
+ noteDownload(false);
3219
4433
  if (!url.includes("framer.com/edit") && !url.includes("framerstatic.com/editorbar")) {
3220
4434
  warn("Download failed: " + path2.basename(localPath) + " - " + e.message);
3221
4435
  }
@@ -3230,19 +4444,77 @@ async function downloadAll(exporter) {
3230
4444
  }
3231
4445
  );
3232
4446
  await pool(tasks, CFG.concurrency);
3233
- success("Downloads complete: " + ok + " succeeded, " + cached + " from cache, " + fail + " failed");
4447
+ success(
4448
+ "Downloads complete: " + ok + " succeeded, " + cached + " from cache, " + fail + " failed"
4449
+ );
3234
4450
  const totalBytes = [...exporter.assets.entries.values()].length;
3235
4451
  log("Total unique assets written to disk: " + totalBytes);
3236
4452
  exporter.assets.buffers.clear();
3237
4453
  log("Network buffer cache cleared");
3238
4454
  }
4455
+ async function downloadLazyChunks(exporter) {
4456
+ const chunkDirs = exporter.platform.lazyChunkDirs;
4457
+ if (!chunkDirs?.length) return;
4458
+ const sourceOf = /* @__PURE__ */ new Map();
4459
+ for (const [url, { localPath }] of exporter.assets.entries) {
4460
+ if (!sourceOf.has(localPath)) sourceOf.set(localPath, url);
4461
+ }
4462
+ let frontier = [...sourceOf.keys()].filter(
4463
+ (p) => chunkDirs.some((dir) => p.startsWith(dir + "/"))
4464
+ );
4465
+ let added = 0;
4466
+ for (let depth = 0; depth < MAX_CHUNK_DEPTH && frontier.length > 0; depth++) {
4467
+ const next = [];
4468
+ const tasks = frontier.map((localPath) => async () => {
4469
+ let code;
4470
+ try {
4471
+ code = await fs.readFile(path2.join(exporter.outDir, localPath), "utf-8");
4472
+ } catch {
4473
+ return;
4474
+ }
4475
+ for (const match of code.matchAll(SIBLING_IMPORT)) {
4476
+ let chunkUrl;
4477
+ try {
4478
+ chunkUrl = new URL("./" + match[1], sourceOf.get(localPath)).href;
4479
+ } catch {
4480
+ continue;
4481
+ }
4482
+ if (exporter.assets.entries.has(chunkUrl)) continue;
4483
+ const dest = exporter.assets.localPathFor(chunkUrl, exporter.platform);
4484
+ if (!dest) continue;
4485
+ sourceOf.set(dest, chunkUrl);
4486
+ try {
4487
+ await fs.writeFile(path2.join(exporter.outDir, dest), await dlBuffer(chunkUrl));
4488
+ next.push(dest);
4489
+ added++;
4490
+ noteDownload(true);
4491
+ noteFile(dest);
4492
+ } catch (e) {
4493
+ warn("Lazy chunk failed: " + match[1] + " - " + e.message);
4494
+ }
4495
+ }
4496
+ });
4497
+ await pool(tasks, CFG.concurrency);
4498
+ frontier = next;
4499
+ }
4500
+ if (frontier.length > 0) {
4501
+ warn(
4502
+ "Lazy chunk traversal stopped at depth " + MAX_CHUNK_DEPTH + " with " + frontier.length + " chunks left to inspect"
4503
+ );
4504
+ }
4505
+ if (added > 0) success("Lazy-loaded chunks resolved: " + added);
4506
+ }
4507
+ var SIBLING_IMPORT, MAX_CHUNK_DEPTH;
3239
4508
  var init_download2 = __esm({
3240
4509
  "src/exporter/download.ts"() {
3241
4510
  "use strict";
3242
4511
  init_config();
3243
4512
  init_logger();
4513
+ init_progress();
3244
4514
  init_download();
3245
4515
  init_pool();
4516
+ SIBLING_IMPORT = /(?:import|from)\s*\(?\s*["'`]\.\/([A-Za-z0-9_.-]+\.m?js)["'`]/g;
4517
+ MAX_CHUNK_DEPTH = 5;
3246
4518
  }
3247
4519
  });
3248
4520
 
@@ -3320,13 +4592,10 @@ const server = http.createServer((req, res) => {
3320
4592
  return;
3321
4593
  }
3322
4594
 
3323
- const serveFile = (pathToFile) => {
4595
+ const serveFile = (pathToFile, onMissing) => {
3324
4596
  fs.readFile(pathToFile, (err, data) => {
3325
4597
  if (err) {
3326
- // SPA Fallback: if it's not a file, serve index.html
3327
- if (url !== '/index.html' && !path.extname(url)) {
3328
- return serveFile(path.join(ROOT, 'index.html'));
3329
- }
4598
+ if (onMissing) return onMissing();
3330
4599
  res.writeHead(404);
3331
4600
  res.end('Not found');
3332
4601
  return;
@@ -3342,6 +4611,15 @@ const server = http.createServer((req, res) => {
3342
4611
  });
3343
4612
  };
3344
4613
 
4614
+ if (url !== '/index.html' && !path.extname(url)) {
4615
+ const slug = url.replace(/^\\/+|\\/+$/g, '').replace(/[^a-zA-Z0-9_-]/g, '_').replace(/_+/g, '_');
4616
+ const serveIndex = () => serveFile(path.join(ROOT, 'index.html'));
4617
+ if (slug) {
4618
+ return serveFile(path.join(ROOT, 'subpages', slug + '.html'), serveIndex);
4619
+ }
4620
+ return serveIndex();
4621
+ }
4622
+
3345
4623
  serveFile(filePath);
3346
4624
  });
3347
4625
 
@@ -3535,94 +4813,134 @@ function stripSrcsetCdnUrls(html) {
3535
4813
  });
3536
4814
  return html;
3537
4815
  }
3538
- async function buildOutput(exporter) {
3539
- exporter.cooking?.update("Stripping platform badges...");
3540
- log("Starting HTML post-processing...");
3541
- log("HTML size: " + (exporter.ssrHTML.length / 1024).toFixed(1) + " KB");
3542
- let html = exporter.ssrHTML;
3543
- if (!html) {
3544
- warn("No SSR HTML available, cannot build output");
3545
- return;
3546
- }
3547
- exporter.cooking?.update("Removing integrity checks...");
3548
- const beforeIntegrity = html.length;
3549
- html = stripIntegrityAndCors(html);
3550
- log("Stripped integrity/crossorigin/preconnect (" + (beforeIntegrity - html.length) + " chars)");
3551
- success("Integrity and CORS restrictions removed");
3552
- if (typeof processSEO === "function") {
3553
- exporter.cooking?.update("Optimizing SEO...");
3554
- html = processSEO(html, exporter.siteUrl);
3555
- success("SEO meta tags optimized");
3556
- }
3557
- log("Stripping " + exporter.platform.stripSelectors.length + " selectors:");
3558
- for (const sel of exporter.platform.stripSelectors) {
3559
- const before = html.length;
3560
- html = stripBySelector(html, sel);
3561
- const removed = before - html.length;
3562
- if (removed > 0) {
3563
- log(" Stripped " + sel + " (" + removed + " chars removed)");
4816
+ function rewritePatterns(exporter) {
4817
+ return exporter.platform.rewritePatterns ?? exporter.platform.rewriteUrlPatterns ?? [];
4818
+ }
4819
+ function applyRewritePatterns(text, report) {
4820
+ report.patterns.forEach(({ from, to }, index) => {
4821
+ const pattern = new RegExp(from.source, from.flags);
4822
+ if (new RegExp(from.source, from.flags).test(text)) report.matched.add(index);
4823
+ text = text.replace(pattern, to);
4824
+ });
4825
+ return text;
4826
+ }
4827
+ function subpageRoutes(exporter) {
4828
+ const routes = /* @__PURE__ */ new Map();
4829
+ for (const [url, filename] of exporter.subpages) {
4830
+ try {
4831
+ routes.set(new URL(url).pathname.replace(/\/+$/, ""), filename);
4832
+ } catch {
3564
4833
  }
3565
4834
  }
3566
- log("Applying " + exporter.platform.stripPatterns.length + " regex patterns...");
4835
+ return routes;
4836
+ }
4837
+ function rewriteInternalLinks(html, siteUrl, routes, fromSubpages) {
4838
+ const source = new URL(siteUrl);
4839
+ const sourceHost = source.hostname.replace(/^www\./, "");
4840
+ const rootPath = source.pathname.replace(/\/+$/, "") || "/";
4841
+ return html.replace(
4842
+ /<(?!link\b)([^>]*?\bhref\s*=\s*)(["'])(.*?)\2/gi,
4843
+ (match, prefix, quote, href) => {
4844
+ if (/^(javascript:|mailto:|tel:|#|data:)/i.test(href)) return match;
4845
+ let target;
4846
+ try {
4847
+ target = new URL(href, siteUrl);
4848
+ } catch {
4849
+ return match;
4850
+ }
4851
+ if (target.hostname.replace(/^www\./, "") !== sourceHost) return match;
4852
+ const targetPath = target.pathname.replace(/\/+$/, "") || "/";
4853
+ if (fromSubpages && targetPath === rootPath) {
4854
+ return `<${prefix}${quote}../index.html${target.hash}${quote}`;
4855
+ }
4856
+ const filename = routes.get(targetPath);
4857
+ if (!filename) return match;
4858
+ const local = (fromSubpages ? filename : "subpages/" + filename) + target.hash;
4859
+ return `<${prefix}${quote}${local}${quote}`;
4860
+ }
4861
+ );
4862
+ }
4863
+ function processHtml(html, exporter, pageUrl, fromDir, routes, fromSubpages, report) {
4864
+ html = stripIntegrityAndCors(html);
4865
+ html = processSEO(html, pageUrl);
4866
+ for (const sel of exporter.platform.stripSelectors) html = stripBySelector(html, sel);
3567
4867
  for (const pattern of exporter.platform.stripPatterns) {
3568
- const before = html.length;
3569
4868
  html = html.replace(new RegExp(pattern.source, pattern.flags), "");
3570
- const removed = before - html.length;
3571
- if (removed > 0) {
3572
- log(" Pattern removed " + removed + " chars");
3573
- }
3574
4869
  }
3575
- if (exporter.platform.stripScripts && exporter.platform.stripScripts.length > 0) {
3576
- log("Applying " + exporter.platform.stripScripts.length + " script-strip patterns...");
3577
- for (const pattern of exporter.platform.stripScripts) {
3578
- const before = html.length;
3579
- html = html.replace(new RegExp(pattern.source, pattern.flags), "");
3580
- const removed = before - html.length;
3581
- if (removed > 0) log(" Script pattern removed " + removed + " chars");
3582
- }
4870
+ for (const pattern of exporter.platform.stripScripts || []) {
4871
+ html = html.replace(new RegExp(pattern.source, pattern.flags), "");
3583
4872
  }
3584
- success("Platform badges and tracking stripped");
3585
4873
  if (exporter.platform.postCapture) {
3586
4874
  try {
3587
- const before = html.length;
3588
4875
  html = exporter.platform.postCapture(html, exporter);
3589
- log("postCapture hook applied (delta " + (html.length - before) + " chars)");
3590
4876
  } catch (e) {
3591
- warn("postCapture hook failed: " + e.message);
4877
+ warn("postCapture hook failed for " + pageUrl + ": " + e.message);
3592
4878
  }
3593
4879
  }
3594
- exporter.cooking?.update("Rewriting asset URLs...");
3595
- log("Rewriting " + exporter.assets.entries.size + " CDN URLs to local paths...");
3596
- const beforeRewrite = html.length;
3597
- html = exporter.assets.rewrite(html, "");
3598
- log("HTML rewrite delta: " + (html.length - beforeRewrite) + " chars");
3599
- if (exporter.platform.rewriteUrlPatterns && exporter.platform.rewriteUrlPatterns.length > 0) {
3600
- for (const { from, to } of exporter.platform.rewriteUrlPatterns) {
3601
- html = html.replace(new RegExp(from.source, from.flags), to);
4880
+ html = exporter.assets.rewrite(html, fromDir);
4881
+ html = applyRewritePatterns(html, report);
4882
+ html = stripSrcsetCdnUrls(html);
4883
+ return rewriteInternalLinks(html, exporter.siteUrl, routes, fromSubpages);
4884
+ }
4885
+ async function buildSubpages(exporter, routes, report) {
4886
+ if (exporter.subpages.size === 0) return;
4887
+ let built = 0;
4888
+ for (const [pageUrl, filename] of exporter.subpages) {
4889
+ const filePath = path3.join(exporter.outDir, "subpages", filename);
4890
+ try {
4891
+ let html = await fs2.readFile(filePath, "utf-8");
4892
+ html = processHtml(html, exporter, pageUrl, "subpages", routes, true, report);
4893
+ await fs2.writeFile(filePath, html, "utf-8");
4894
+ built++;
4895
+ } catch (e) {
4896
+ warn("Sub-page post-processing skipped: " + filename + " - " + e.message);
3602
4897
  }
3603
- log("Applied " + exporter.platform.rewriteUrlPatterns.length + " custom URL rewrites");
3604
4898
  }
3605
- exporter.cooking?.update("Cleaning srcset references...");
3606
- html = stripSrcsetCdnUrls(html);
3607
- log("Cleaned remaining CDN URLs from srcset attributes");
3608
- await rewriteDownloadedFiles(exporter);
4899
+ success("Sub-pages linked to local assets: " + built + "/" + exporter.subpages.size);
4900
+ }
4901
+ async function buildOutput(exporter) {
4902
+ exporter.cooking?.update("Stripping platform badges...");
4903
+ log("Starting HTML post-processing...");
4904
+ log("HTML size: " + (exporter.ssrHTML.length / 1024).toFixed(1) + " KB");
4905
+ let html = exporter.ssrHTML;
4906
+ if (!html) {
4907
+ warn("No SSR HTML available, cannot build output");
4908
+ return;
4909
+ }
4910
+ const routes = subpageRoutes(exporter);
4911
+ const report = { patterns: rewritePatterns(exporter), matched: /* @__PURE__ */ new Set() };
4912
+ exporter.cooking?.update("Processing exported HTML...");
4913
+ html = processHtml(html, exporter, exporter.siteUrl, "", routes, false, report);
4914
+ success("Index HTML pipeline complete");
4915
+ await rewriteDownloadedFiles(exporter, report);
4916
+ await buildSubpages(exporter, routes, report);
4917
+ report.patterns.forEach(({ from }, index) => {
4918
+ if (!report.matched.has(index)) warn("Rewrite pattern matched nothing: " + from.toString());
4919
+ });
3609
4920
  success("All URLs rewritten to local paths");
3610
- exporter.cooking?.update("Pretty-printing JS files...");
3611
- await prettifyDownloadedJS(exporter);
4921
+ if (exporter.prettyPrint !== false) {
4922
+ exporter.cooking?.update("Pretty-printing JS files...");
4923
+ await prettifyDownloadedJS(exporter);
4924
+ } else {
4925
+ log("Pretty-print disabled, keeping JS files as downloaded");
4926
+ }
3612
4927
  exporter.cooking?.update("Writing final output...");
3613
4928
  log("Writing index.html (" + (html.length / 1024).toFixed(1) + " KB)...");
3614
4929
  await fs2.writeFile(path3.join(exporter.outDir, "index.html"), html);
4930
+ noteFile("index.html");
3615
4931
  success("index.html written");
3616
4932
  await fs2.writeFile(path3.join(exporter.outDir, "serve.js"), SERVE_SCRIPT);
4933
+ noteFile("serve.js");
3617
4934
  log("serve.js written");
3618
4935
  await fs2.writeFile(
3619
4936
  path3.join(exporter.outDir, "package.json"),
3620
4937
  JSON.stringify({ type: "module", scripts: { serve: "node serve.js" } }, null, 2) + "\n"
3621
4938
  );
4939
+ noteFile("package.json");
3622
4940
  log("package.json written for serve.js");
3623
4941
  success("Output build complete");
3624
4942
  }
3625
- async function rewriteDownloadedFiles(exporter) {
4943
+ async function rewriteDownloadedFiles(exporter, report) {
3626
4944
  const dirs = ["scripts/vendor", "scripts/modules", "styles"];
3627
4945
  let rewritten = 0;
3628
4946
  for (const dir of dirs) {
@@ -3641,6 +4959,7 @@ async function rewriteDownloadedFiles(exporter) {
3641
4959
  let content = await fs2.readFile(filePath, "utf-8");
3642
4960
  const before = content;
3643
4961
  content = exporter.assets.rewrite(content, dir);
4962
+ content = applyRewritePatterns(content, report);
3644
4963
  if (content !== before) {
3645
4964
  await fs2.writeFile(filePath, content);
3646
4965
  rewritten++;
@@ -3695,6 +5014,7 @@ var init_output = __esm({
3695
5014
  "src/exporter/output.ts"() {
3696
5015
  "use strict";
3697
5016
  init_logger();
5017
+ init_progress();
3698
5018
  init_prettify();
3699
5019
  init_template();
3700
5020
  }
@@ -3703,12 +5023,10 @@ var init_output = __esm({
3703
5023
  // src/exporter/summary.ts
3704
5024
  import fs3 from "fs/promises";
3705
5025
  import path4 from "path";
3706
- async function printSummary(exporter) {
3707
- const w = maxWidth();
3708
- const isSmall = w < 50;
5026
+ async function collectSummary(outDir) {
3709
5027
  const count = async (d) => {
3710
5028
  try {
3711
- return (await fs3.readdir(path4.join(exporter.outDir, d))).length;
5029
+ return (await fs3.readdir(path4.join(outDir, d))).length;
3712
5030
  } catch {
3713
5031
  return 0;
3714
5032
  }
@@ -3724,6 +5042,25 @@ async function printSummary(exporter) {
3724
5042
  count("data"),
3725
5043
  count("subpages")
3726
5044
  ]);
5045
+ return [
5046
+ { dir: "styles/", count: styles, kind: "CSS" },
5047
+ { dir: "scripts/vendor/", count: vendor, kind: "JS vendor" },
5048
+ { dir: "scripts/modules/", count: scripts, kind: "JS modules" },
5049
+ { dir: "assets/images/", count: imgs, kind: "images" },
5050
+ { dir: "assets/videos/", count: videos, kind: "videos" },
5051
+ { dir: "assets/fonts/", count: fonts, kind: "fonts" },
5052
+ { dir: "assets/misc/", count: misc, kind: "misc" },
5053
+ { dir: "data/", count: data, kind: "data" },
5054
+ { dir: "subpages/", count: subpages, kind: "pages" }
5055
+ ];
5056
+ }
5057
+ async function printSummary(exporter) {
5058
+ const w = maxWidth();
5059
+ const isSmall = w < 50;
5060
+ const collected = await collectSummary(exporter.outDir);
5061
+ const [styles, vendor, scripts, imgs, videos, fonts, misc, data, subpages] = collected.map(
5062
+ (e) => e.count
5063
+ );
3727
5064
  const G = ui.primary;
3728
5065
  const G2 = ui.primarySoft;
3729
5066
  const C2 = ui.secondary;
@@ -3798,10 +5135,10 @@ var init_summary = __esm({
3798
5135
  // src/ai/prompt-assistant.ts
3799
5136
  import fs4 from "fs/promises";
3800
5137
  import path5 from "path";
3801
- import { stdin as stdin2, stdout as stdout2 } from "process";
5138
+ import { stdin as stdin2, stdout as stdout4 } from "process";
3802
5139
  import { spawn } from "child_process";
3803
5140
  async function runAiPromptAssistant(exporter) {
3804
- if (!stdin2.isTTY || !stdout2.isTTY) return;
5141
+ if (!stdin2.isTTY || !stdout4.isTTY) return;
3805
5142
  const serveCommand = buildServeCommand(exporter);
3806
5143
  const shouldConvert = await runExportCompletePrompt(exporter, serveCommand);
3807
5144
  if (!shouldConvert) return;
@@ -3887,17 +5224,12 @@ async function countEntries(dir) {
3887
5224
  function buttonLabel(label) {
3888
5225
  return `${ui.border("[")} ${ui.text.bold(label)} ${ui.border("]")}`;
3889
5226
  }
3890
- function centerText2(text, width) {
3891
- const visible = stripAnsi(text).length;
3892
- if (visible >= width) return text;
3893
- const left = Math.floor((width - visible) / 2);
3894
- return " ".repeat(left) + text + " ".repeat(width - visible - left);
3895
- }
3896
5227
  function buildServeCommand(exporter) {
3897
5228
  return `cd ${path5.basename(exporter.outDir)} && node serve.js`;
3898
5229
  }
3899
5230
  async function runExportCompletePrompt(exporter, serveCommand) {
3900
5231
  let copyServeCommand = true;
5232
+ let copyExportLog = false;
3901
5233
  while (true) {
3902
5234
  const action = await select(
3903
5235
  "Export complete",
@@ -3906,26 +5238,35 @@ async function runExportCompletePrompt(exporter, serveCommand) {
3906
5238
  label: `${checkboxLabel(copyServeCommand)} Copy serve command when finishing`,
3907
5239
  value: "toggle-copy"
3908
5240
  },
5241
+ {
5242
+ label: `${checkboxLabel(copyExportLog)} Copy full export log when finishing`,
5243
+ value: "toggle-log"
5244
+ },
3909
5245
  {
3910
5246
  label: `${buttonLabel("Convert to AI code")} ${ui.error("BETA")}`,
3911
5247
  value: "convert"
3912
5248
  },
3913
5249
  { label: buttonLabel("Finish"), value: "finish" }
3914
5250
  ],
3915
- 2,
5251
+ 3,
3916
5252
  {
3917
5253
  headerLines: [
3918
5254
  `Output: ${path5.basename(exporter.outDir)}`,
3919
5255
  `Run: ${serveCommand}`,
5256
+ `Log: ${path5.basename(exporter.outDir)}/export.log`,
3920
5257
  "Use Convert to open the AI conversion assistant."
3921
5258
  ],
3922
- footer: "enter select \xB7 checkbox toggles copy \xB7 mouse hover/click"
5259
+ footer: "enter select checkbox toggles mouse click"
3923
5260
  }
3924
5261
  );
3925
5262
  if (action === "toggle-copy") {
3926
5263
  copyServeCommand = !copyServeCommand;
3927
5264
  continue;
3928
5265
  }
5266
+ if (action === "toggle-log") {
5267
+ copyExportLog = !copyExportLog;
5268
+ continue;
5269
+ }
3929
5270
  if (action === "finish") {
3930
5271
  if (copyServeCommand) {
3931
5272
  try {
@@ -3935,6 +5276,19 @@ async function runExportCompletePrompt(exporter, serveCommand) {
3935
5276
  } catch (error) {
3936
5277
  console.log(
3937
5278
  ` ${ui.warning("!")} ${ui.warning("Clipboard copy unavailable:")} ${ui.muted(error.message)}
5279
+ `
5280
+ );
5281
+ }
5282
+ }
5283
+ if (copyExportLog) {
5284
+ try {
5285
+ const logText = getLogHistory().map((r) => `[${r.time}] [${r.level}] ${r.message}`).join("\n");
5286
+ await copyToClipboard(logText);
5287
+ console.log(` ${ui.success("\u2713")} ${ui.text.bold("Export log copied")}
5288
+ `);
5289
+ } catch (error) {
5290
+ console.log(
5291
+ ` ${ui.warning("!")} ${ui.warning("Clipboard copy unavailable:")} ${ui.muted(error.message)}
3938
5292
  `
3939
5293
  );
3940
5294
  }
@@ -3983,10 +5337,10 @@ function printAssistantModal(title, lines) {
3983
5337
  const inner = w - 4;
3984
5338
  console.log("");
3985
5339
  console.log(boxTop(w));
3986
- console.log(boxLine(w, centerText2(`${ui.primary("\u25C6")} ${title}`, inner)));
5340
+ console.log(boxLine(w, centerText(`${ui.primary("\u25C6")} ${title}`, inner)));
3987
5341
  console.log(boxSep(w));
3988
5342
  for (const line of lines) {
3989
- console.log(boxLine(w, centerText2(ui.muted(line), inner)));
5343
+ console.log(boxLine(w, centerText(ui.muted(line), inner)));
3990
5344
  }
3991
5345
  console.log(boxBot(w));
3992
5346
  console.log("");
@@ -4173,6 +5527,7 @@ var init_prompt_assistant = __esm({
4173
5527
  init_select();
4174
5528
  init_box();
4175
5529
  init_theme();
5530
+ init_logger();
4176
5531
  IMPORTANT_DIRS = [
4177
5532
  "index.html",
4178
5533
  "styles",
@@ -4300,7 +5655,7 @@ __export(exporter_exports, {
4300
5655
  });
4301
5656
  import fs5 from "fs/promises";
4302
5657
  import path6 from "path";
4303
- import chalk5 from "chalk";
5658
+ import chalk8 from "chalk";
4304
5659
  function deriveOutputName(url, platformName) {
4305
5660
  try {
4306
5661
  const parsed = new URL(url);
@@ -4337,6 +5692,8 @@ var init_exporter = __esm({
4337
5692
  init_asset_map();
4338
5693
  init_download();
4339
5694
  init_logger();
5695
+ init_progress();
5696
+ init_sidebar();
4340
5697
  init_capture();
4341
5698
  init_anti_bot();
4342
5699
  init_download2();
@@ -4354,9 +5711,12 @@ var init_exporter = __esm({
4354
5711
  page;
4355
5712
  ssrHTML;
4356
5713
  prettyPrint;
5714
+ interactive;
4357
5715
  platform;
4358
5716
  cooking;
4359
- constructor(siteUrl, outDir, platformOverride) {
5717
+ deviceScaleFactor;
5718
+ subpages = /* @__PURE__ */ new Map();
5719
+ constructor(siteUrl, outDir, platformOverride, deviceScaleFactor) {
4360
5720
  this.siteUrl = siteUrl;
4361
5721
  this.outDir = outDir;
4362
5722
  this.assets = new AssetMap();
@@ -4364,6 +5724,8 @@ var init_exporter = __esm({
4364
5724
  this.page = null;
4365
5725
  this.ssrHTML = "";
4366
5726
  this.prettyPrint = true;
5727
+ this.interactive = true;
5728
+ this.deviceScaleFactor = deviceScaleFactor;
4367
5729
  if (platformOverride && platformOverride !== "unknown") {
4368
5730
  this.platform = getPlatformByName(platformOverride);
4369
5731
  } else {
@@ -4376,15 +5738,20 @@ var init_exporter = __esm({
4376
5738
  ${ui.text.bold("Framer Export")} ${chip("mirror")} ${ui.muted("v4 pipeline")}
4377
5739
  `
4378
5740
  );
4379
- info("Source : " + chalk5.underline(this.siteUrl));
5741
+ info("Source : " + chalk8.underline(this.siteUrl));
4380
5742
  info("Output : " + ui.primary(this.outDir));
4381
5743
  info("Platform : " + ui.primary(this.platform.displayName));
5744
+ info("DPR : " + ui.primary(String(this.deviceScaleFactor || 1)));
4382
5745
  if (includeSubpages) {
4383
5746
  info("Subpages : " + ui.success("enabled"));
4384
5747
  }
4385
5748
  console.log("");
4386
5749
  this.cooking = new CookingSpinner();
4387
5750
  setCooking(this.cooking);
5751
+ resetProgress();
5752
+ const sidebar = new ExportSidebar();
5753
+ sidebar.start();
5754
+ this.phase("Preparing directories...");
4388
5755
  this.cooking.start("Preparing directories...");
4389
5756
  for (const d of [
4390
5757
  "",
@@ -4401,6 +5768,7 @@ var init_exporter = __esm({
4401
5768
  await fs5.mkdir(path6.join(this.outDir, d), { recursive: true });
4402
5769
  }
4403
5770
  log("Output directory structure created");
5771
+ this.phase("Fetching SSR HTML...");
4404
5772
  this.cooking.update("Fetching SSR HTML...");
4405
5773
  log("Fetching SSR HTML from " + this.siteUrl);
4406
5774
  try {
@@ -4408,7 +5776,7 @@ var init_exporter = __esm({
4408
5776
  this.ssrHTML = buf.toString("utf-8");
4409
5777
  success("SSR HTML fetched (" + (this.ssrHTML.length / 1024).toFixed(1) + " KB)");
4410
5778
  } catch (e) {
4411
- log(chalk5.red("Could not fetch SSR HTML: " + e.message));
5779
+ log(chalk8.red("Could not fetch SSR HTML: " + e.message));
4412
5780
  }
4413
5781
  const htmlDetected = detectPlatform(this.siteUrl, this.ssrHTML);
4414
5782
  if (htmlDetected.name !== this.platform.name) {
@@ -4416,16 +5784,23 @@ var init_exporter = __esm({
4416
5784
  log("Platform refined: " + ui.primary(this.platform.displayName) + " (from HTML analysis)");
4417
5785
  }
4418
5786
  try {
5787
+ this.phase("Capturing rendered page...");
4419
5788
  await launchAndCapture(this);
4420
5789
  if (includeSubpages && this.page) {
4421
5790
  await this.crawlSubpages();
4422
5791
  }
4423
5792
  await closeBrowser(this);
5793
+ this.phase("Downloading assets...");
4424
5794
  this.cooking.update("Downloading assets...");
4425
5795
  await downloadAll(this);
5796
+ this.phase("Resolving lazy-loaded chunks...");
5797
+ this.cooking.update("Resolving lazy-loaded chunks...");
5798
+ await downloadLazyChunks(this);
5799
+ this.phase("Building output...");
4426
5800
  this.cooking.update("Building output...");
4427
5801
  await buildOutput(this);
4428
5802
  if (this.platform.postProcess) {
5803
+ this.phase("Post-processing...");
4429
5804
  this.cooking.update("Running " + this.platform.displayName + " post-processing...");
4430
5805
  try {
4431
5806
  await this.platform.postProcess(this);
@@ -4435,6 +5810,7 @@ var init_exporter = __esm({
4435
5810
  }
4436
5811
  }
4437
5812
  } catch (e) {
5813
+ sidebar.stop();
4438
5814
  this.cooking?.stop();
4439
5815
  setCooking(null);
4440
5816
  await closeBrowser(this);
@@ -4444,14 +5820,32 @@ var init_exporter = __esm({
4444
5820
  }
4445
5821
  throw e;
4446
5822
  }
5823
+ this.phase("Done");
5824
+ sidebar.stop();
4447
5825
  this.cooking.stop();
4448
5826
  setCooking(null);
4449
5827
  console.log("");
4450
5828
  success("Export complete!");
5829
+ await this.writeExportLog();
4451
5830
  await printSummary(this);
4452
- await runAiPromptAssistant(this);
5831
+ if (this.interactive) {
5832
+ await runAiPromptAssistant(this);
5833
+ }
5834
+ }
5835
+ phase(label) {
5836
+ setPhase(label);
5837
+ }
5838
+ async writeExportLog() {
5839
+ const lines = getLogHistory().map((r) => `[${r.time}] [${r.level}] ${r.message}`);
5840
+ const dest = path6.join(this.outDir, "export.log");
5841
+ try {
5842
+ await fs5.writeFile(dest, lines.join("\n") + "\n", "utf-8");
5843
+ log("Full export log written: export.log (" + lines.length + " lines)");
5844
+ } catch {
5845
+ }
4453
5846
  }
4454
5847
  async crawlSubpages() {
5848
+ this.phase("Crawling sub-pages...");
4455
5849
  this.cooking?.update("Discovering sub-pages...");
4456
5850
  log("Scanning page for internal links...");
4457
5851
  const page = this.page;
@@ -4475,8 +5869,20 @@ var init_exporter = __esm({
4475
5869
  }
4476
5870
  return Array.from(hrefs);
4477
5871
  }, baseHost);
4478
- log("Found " + links.length + " sub-page links");
4479
- const uniqueLinks = [...new Set(links)].slice(0, 50);
5872
+ const ssrLinks = [];
5873
+ for (const m of this.ssrHTML.matchAll(/<a\b[^>]*\shref=["']([^"']+)["']/gi)) {
5874
+ const href = m[1];
5875
+ if (/^(javascript:|mailto:|tel:|#)/i.test(href)) continue;
5876
+ try {
5877
+ const u = new URL(href, this.siteUrl);
5878
+ if (u.protocol.startsWith("http") && u.hostname.replace(/^www\./, "") === baseHost && u.pathname !== "/" && u.pathname !== "") {
5879
+ ssrLinks.push(u.href.split("#")[0]);
5880
+ }
5881
+ } catch {
5882
+ }
5883
+ }
5884
+ const uniqueLinks = [.../* @__PURE__ */ new Set([...links, ...ssrLinks])].slice(0, 50);
5885
+ log("Found " + uniqueLinks.length + " sub-page links");
4480
5886
  if (uniqueLinks.length === 0) {
4481
5887
  log("No sub-pages to crawl");
4482
5888
  return;
@@ -4493,6 +5899,9 @@ var init_exporter = __esm({
4493
5899
  const filename = slug + ".html";
4494
5900
  const filepath = path6.join(this.outDir, "subpages", filename);
4495
5901
  await fs5.writeFile(filepath, html, "utf-8");
5902
+ this.subpages.set(link, filename);
5903
+ noteSubpage();
5904
+ noteFile("subpages/" + filename);
4496
5905
  log(" Saved: subpages/" + filename);
4497
5906
  } catch (e) {
4498
5907
  log(" Skipped " + link + ": " + e.message);
@@ -4514,67 +5923,269 @@ var init_exporter = __esm({
4514
5923
  }
4515
5924
  });
4516
5925
 
4517
- // src/cli/select-grouped.ts
4518
- import chalk6 from "chalk";
4519
- async function selectPlatform(siteUrl) {
4520
- const grouped = platformsByCategory();
4521
- const detected = detectByUrl(siteUrl);
4522
- if (detected) {
4523
- const choice = await select(
4524
- "Platform detected",
4525
- [
4526
- { label: `Use ${detected.displayName}`, value: USE_DETECTED },
4527
- { label: "Browse all platforms", value: BROWSE }
4528
- ],
4529
- 0,
4530
- {
4531
- headerLines: [`URL: ${siteUrl}`, `Detected: ${detected.displayName}`],
4532
- actions: [{ label: "Modify URL", value: MODIFY_URL }],
4533
- footer: "tab focus button \xB7 enter select \xB7 mouse hover/click"
4534
- }
4535
- );
4536
- if (choice === MODIFY_URL) return { modifyUrl: true };
4537
- if (choice === USE_DETECTED) return { platform: detected.name };
5926
+ // src/ui/server.ts
5927
+ var server_exports = {};
5928
+ __export(server_exports, {
5929
+ startUiServer: () => startUiServer
5930
+ });
5931
+ import http2 from "http";
5932
+ import path7 from "path";
5933
+ import { URL as URL4 } from "url";
5934
+ function isAllowedHost(req) {
5935
+ const host = (req.headers.host || "").toLowerCase();
5936
+ return /^(localhost|127\.0\.0\.1)(:\d+)?$/.test(host);
5937
+ }
5938
+ function isSameOrigin(req) {
5939
+ const origin = req.headers.origin;
5940
+ if (!origin) return true;
5941
+ try {
5942
+ const parsed = new URL4(origin);
5943
+ return (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") && parsed.port === (req.headers.host || "").split(":")[1];
5944
+ } catch {
5945
+ return false;
4538
5946
  }
4539
- let categoryDefault = detected ? Math.max(0, CATEGORY_ORDER.indexOf(detected.category)) : 0;
4540
- while (true) {
4541
- const categoryOptions = CATEGORY_ORDER.map((cat2) => ({
4542
- label: `${CATEGORY_LABELS[cat2]} ${chalk6.gray(`(${grouped[cat2].length})`)}`,
4543
- value: cat2
4544
- }));
4545
- const category = await select("Select a category", categoryOptions, categoryDefault, {
4546
- headerLines: [`URL: ${siteUrl}`],
4547
- actions: [{ label: "Modify URL", value: MODIFY_URL }],
4548
- footer: "tab focus button \xB7 enter next \xB7 mouse hover/click"
5947
+ }
5948
+ function resolveOutDir(requested, fallbackName) {
5949
+ const cwd = process.cwd();
5950
+ const target = path7.resolve(cwd, requested || "./" + fallbackName);
5951
+ if (target !== cwd && !target.startsWith(cwd + path7.sep)) return null;
5952
+ return target;
5953
+ }
5954
+ function quoteForShell(name) {
5955
+ return '"' + name.replace(/["`$\\\r\n]/g, "") + '"';
5956
+ }
5957
+ function sse(event, data) {
5958
+ const payload = `event: ${event}
5959
+ data: ${JSON.stringify(data)}
5960
+
5961
+ `;
5962
+ for (const client of clients) client.write(payload);
5963
+ }
5964
+ function json(res, code, data) {
5965
+ const body = JSON.stringify(data);
5966
+ res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
5967
+ res.end(body);
5968
+ }
5969
+ function readBody(req) {
5970
+ return new Promise((resolve, reject) => {
5971
+ let data = "";
5972
+ req.on("data", (chunk) => {
5973
+ data += chunk;
5974
+ if (data.length > 1e6) reject(new Error("body too large"));
4549
5975
  });
4550
- if (category === MODIFY_URL) return { modifyUrl: true };
4551
- const cat = category;
4552
- categoryDefault = CATEGORY_ORDER.indexOf(cat);
4553
- const handlers = grouped[cat];
4554
- const platformOptions = handlers.map((handler) => ({
4555
- label: detected && handler.name === detected.name ? `${handler.displayName}${chalk6.gray(" (detected)")}` : handler.displayName,
4556
- value: handler.name
4557
- }));
4558
- const platformDefault = detected && detected.category === cat ? Math.max(0, handlers.findIndex((handler) => handler.name === detected.name)) : 0;
4559
- const platform = await select("Select platform", platformOptions, platformDefault, {
4560
- headerLines: [`Category: ${CATEGORY_LABELS[cat]}`],
4561
- actions: [{ label: "Back to categories", value: BACK }],
4562
- footer: "tab focus button \xB7 enter select \xB7 mouse hover/click"
5976
+ req.on("end", () => resolve(data));
5977
+ req.on("error", reject);
5978
+ });
5979
+ }
5980
+ function platformsPayload() {
5981
+ const grouped = platformsByCategory();
5982
+ return {
5983
+ categories: CATEGORY_ORDER.map((cat) => ({
5984
+ id: cat,
5985
+ label: CATEGORY_LABELS[cat],
5986
+ platforms: grouped[cat].map((h) => ({
5987
+ name: h.name,
5988
+ displayName: h.displayName,
5989
+ beta: isBetaPlatform(h.name)
5990
+ }))
5991
+ }))
5992
+ };
5993
+ }
5994
+ async function startExport(body, outDir) {
5995
+ run = { state: "running" };
5996
+ clearLogHistory();
5997
+ sse("status", run);
5998
+ const { CFG: CFG2 } = await Promise.resolve().then(() => (init_config(), config_exports));
5999
+ CFG2.concurrency = body.concurrency || 12;
6000
+ const exporter = new FramerExporter(
6001
+ body.url,
6002
+ outDir,
6003
+ body.platform || void 0
6004
+ );
6005
+ exporter.prettyPrint = body.prettyPrint !== false;
6006
+ exporter.interactive = false;
6007
+ try {
6008
+ await exporter.run(body.subpages === true);
6009
+ run = {
6010
+ state: "done",
6011
+ outDir,
6012
+ serveCommand: `cd ${quoteForShell(path7.basename(outDir))} && node serve.js`,
6013
+ summary: await collectSummary(outDir)
6014
+ };
6015
+ } catch (e) {
6016
+ run = { state: "error", error: e.message };
6017
+ }
6018
+ sse("status", run);
6019
+ }
6020
+ function startUiServer(port) {
6021
+ const unsubscribeLog = onLog((record2) => sse("log", record2));
6022
+ const unsubscribeProgress = onProgress((progress) => sse("progress", progress));
6023
+ const server = http2.createServer(async (req, res) => {
6024
+ const parsed = new URL4(req.url || "/", "http://localhost");
6025
+ const route = parsed.pathname;
6026
+ if (!isAllowedHost(req) || !isSameOrigin(req)) {
6027
+ json(res, 403, { error: "forbidden" });
6028
+ return;
6029
+ }
6030
+ if (route === "/" && req.method === "GET") {
6031
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
6032
+ res.end(UI_HTML);
6033
+ return;
6034
+ }
6035
+ if (route === "/app.js" && req.method === "GET") {
6036
+ res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8" });
6037
+ res.end(APP_JS);
6038
+ return;
6039
+ }
6040
+ if (route === "/pixel-blast.js" && req.method === "GET") {
6041
+ res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8" });
6042
+ res.end(PIXEL_BLAST_JS);
6043
+ return;
6044
+ }
6045
+ if (route === "/api/platforms" && req.method === "GET") {
6046
+ json(res, 200, platformsPayload());
6047
+ return;
6048
+ }
6049
+ if (route === "/api/derive" && req.method === "GET") {
6050
+ const url = parsed.searchParams.get("url") || "";
6051
+ const platform = parsed.searchParams.get("platform") || null;
6052
+ try {
6053
+ const { detectPlatform: detectPlatform2 } = await Promise.resolve().then(() => (init_platforms(), platforms_exports));
6054
+ const name = deriveOutputName(url, platform || detectPlatform2(url).name);
6055
+ json(res, 200, { name });
6056
+ } catch {
6057
+ json(res, 400, { error: "invalid url" });
6058
+ }
6059
+ return;
6060
+ }
6061
+ if (route === "/api/status" && req.method === "GET") {
6062
+ json(res, 200, { run, progress: getProgress() });
6063
+ return;
6064
+ }
6065
+ if (route === "/api/log" && req.method === "GET") {
6066
+ const text = getLogHistory().map((r) => `[${r.time}] [${r.level}] ${r.message}`).join("\n");
6067
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
6068
+ res.end(text + "\n");
6069
+ return;
6070
+ }
6071
+ if (route === "/api/events" && req.method === "GET") {
6072
+ res.writeHead(200, {
6073
+ "Content-Type": "text/event-stream",
6074
+ "Cache-Control": "no-cache",
6075
+ Connection: "keep-alive"
6076
+ });
6077
+ res.write(`event: status
6078
+ data: ${JSON.stringify(run)}
6079
+
6080
+ `);
6081
+ res.write(`event: progress
6082
+ data: ${JSON.stringify(getProgress())}
6083
+
6084
+ `);
6085
+ clients.add(res);
6086
+ req.on("close", () => clients.delete(res));
6087
+ return;
6088
+ }
6089
+ if (route === "/api/export" && req.method === "POST") {
6090
+ if (run.state === "running") {
6091
+ json(res, 409, { error: "an export is already running" });
6092
+ return;
6093
+ }
6094
+ let body;
6095
+ try {
6096
+ body = JSON.parse(await readBody(req));
6097
+ new URL4(body.url);
6098
+ } catch {
6099
+ json(res, 400, { error: "invalid request body" });
6100
+ return;
6101
+ }
6102
+ const outDir = resolveOutDir(body.outDir, deriveOutputName(body.url, "framer"));
6103
+ if (!outDir) {
6104
+ json(res, 400, { error: "output directory must stay inside the working directory" });
6105
+ return;
6106
+ }
6107
+ json(res, 202, { started: true });
6108
+ void startExport(body, outDir);
6109
+ return;
6110
+ }
6111
+ res.writeHead(404, { "Content-Type": "application/json" });
6112
+ res.end('{"error":"not found"}');
6113
+ });
6114
+ server.on("close", () => {
6115
+ unsubscribeLog();
6116
+ unsubscribeProgress();
6117
+ });
6118
+ return new Promise((resolve, reject) => {
6119
+ server.on("error", reject);
6120
+ server.listen(port, "127.0.0.1", () => {
6121
+ const address = server.address();
6122
+ const actualPort = typeof address === "object" && address ? address.port : port;
6123
+ console.log("");
6124
+ console.log(` ${ui.text.bold("Framer Export UI")}`);
6125
+ console.log(` ${ui.muted("Local")} ${ui.primary(`http://localhost:${actualPort}`)}`);
6126
+ console.log(` ${ui.muted("Stop")} ${ui.primary("ctrl+c")}`);
6127
+ console.log("");
6128
+ resolve({
6129
+ port: actualPort,
6130
+ close: () => new Promise((done) => {
6131
+ for (const client of clients) client.end();
6132
+ clients.clear();
6133
+ server.close(() => done());
6134
+ })
6135
+ });
4563
6136
  });
4564
- if (platform === BACK) continue;
4565
- return { platform };
6137
+ });
6138
+ }
6139
+ var clients, run;
6140
+ var init_server = __esm({
6141
+ "src/ui/server.ts"() {
6142
+ "use strict";
6143
+ init_page();
6144
+ init_app_js();
6145
+ init_pixel_blast();
6146
+ init_exporter();
6147
+ init_summary();
6148
+ init_platforms();
6149
+ init_logger();
6150
+ init_progress();
6151
+ init_theme();
6152
+ clients = /* @__PURE__ */ new Set();
6153
+ run = { state: "idle" };
6154
+ }
6155
+ });
6156
+
6157
+ // src/cli/select-grouped.ts
6158
+ async function selectTool() {
6159
+ const grouped = platformsByCategory();
6160
+ const options = [{ label: "Auto-detect from URL", value: AUTO_DETECT }];
6161
+ for (const cat of CATEGORY_ORDER) {
6162
+ options.push({ label: CATEGORY_LABELS[cat], value: `__heading_${cat}`, heading: true });
6163
+ for (const handler of grouped[cat]) {
6164
+ options.push({
6165
+ label: isBetaPlatform(handler.name) ? `${handler.displayName} [beta]` : handler.displayName,
6166
+ value: handler.name
6167
+ });
6168
+ }
4566
6169
  }
6170
+ const choice = await select("Select a tool to export", options, 0, {
6171
+ headerLines: ["25+ platforms supported, type to search the list."],
6172
+ searchable: true,
6173
+ footer: "type to search \u2191\u2193 scroll enter select esc close"
6174
+ });
6175
+ if (choice === AUTO_DETECT) return { auto: true };
6176
+ return { platform: choice };
6177
+ }
6178
+ function resolveDetected(siteUrl) {
6179
+ const detected = detectByUrl(siteUrl);
6180
+ return detected ? detected.name : null;
4567
6181
  }
4568
- var MODIFY_URL, BROWSE, USE_DETECTED, BACK;
6182
+ var AUTO_DETECT;
4569
6183
  var init_select_grouped = __esm({
4570
6184
  "src/cli/select-grouped.ts"() {
4571
6185
  "use strict";
4572
6186
  init_select();
4573
6187
  init_platforms();
4574
- MODIFY_URL = "__modify_url__";
4575
- BROWSE = "__browse__";
4576
- USE_DETECTED = "__use_detected__";
4577
- BACK = "__back__";
6188
+ AUTO_DETECT = "__auto__";
4578
6189
  }
4579
6190
  });
4580
6191
 
@@ -4584,21 +6195,12 @@ __export(setup_exports, {
4584
6195
  runSetup: () => runSetup
4585
6196
  });
4586
6197
  import readline2 from "readline/promises";
4587
- import { stdin as stdin3, stdout as stdout3 } from "process";
4588
- import path7 from "path";
4589
- import { URL as URL4 } from "url";
4590
- import chalk7 from "chalk";
4591
- function drawHeader(title) {
4592
- const w = maxWidth();
4593
- if (w < 50) {
4594
- console.log(`
4595
- ${bullet("\u25CF")} ${ui.text.bold(title)}`);
4596
- return;
4597
- }
4598
- console.log(boxTop(w));
4599
- console.log(boxLine(w, ui.text.bold(" " + title)));
4600
- console.log(boxBot(w));
4601
- console.log("");
6198
+ import { stdin as stdin3, stdout as stdout5 } from "process";
6199
+ import path8 from "path";
6200
+ import { URL as URL5 } from "url";
6201
+ import chalk9 from "chalk";
6202
+ function checkbox(checked) {
6203
+ return checked ? "[\u2713]" : "[ ]";
4602
6204
  }
4603
6205
  async function runSetup(legacyMode = false) {
4604
6206
  showBanner();
@@ -4607,269 +6209,183 @@ async function runSetup(legacyMode = false) {
4607
6209
  ` ${ui.muted("Export 25+ website platforms into a clean, self-hosted local mirror.")}
4608
6210
  `
4609
6211
  );
4610
- const rl = legacyMode ? readline2.createInterface({ input: stdin3, output: stdout3 }) : null;
4611
- const ask = async (question, defaultVal, headerLines = []) => {
4612
- if (!legacyMode) {
4613
- return promptInput(question, defaultVal || "", { headerLines });
4614
- }
4615
- const suffix = defaultVal ? chalk7.gray(` (${defaultVal})`) : "";
4616
- const prompt = ` ${ui.primary("\u25CF")} ${ui.text.bold(question)}${suffix} ${ui.muted(">")} `;
4617
- const answer = await rl.question(prompt);
4618
- return answer.trim() || defaultVal || "";
4619
- };
4620
- if (legacyMode) drawHeader("Step 1 : Site URL");
6212
+ if (legacyMode) {
6213
+ return runLegacySetup();
6214
+ }
6215
+ let platformName = null;
6216
+ let autoDetect = false;
6217
+ const tool = await selectTool();
6218
+ if ("auto" in tool) autoDetect = true;
6219
+ else platformName = tool.platform;
4621
6220
  let siteUrl = "";
4622
6221
  let urlError = "";
4623
6222
  while (!siteUrl) {
4624
- const input = await ask(
4625
- "Enter the site URL",
4626
- "",
4627
- ["Step 1 : Site URL", urlError].filter(Boolean)
4628
- );
6223
+ const headerLines = [
6224
+ autoDetect ? "Tool: auto-detect from URL" : `Tool: ${getPlatformByName(platformName).displayName}`,
6225
+ urlError
6226
+ ].filter(Boolean);
6227
+ const input = await promptInput("Enter the site URL", "", { headerLines });
4629
6228
  try {
4630
- new URL4(input);
6229
+ new URL5(input);
4631
6230
  siteUrl = input;
4632
6231
  urlError = "";
4633
6232
  } catch {
4634
6233
  urlError = "Invalid URL. Enter a valid URL (https://...)";
4635
- if (legacyMode) {
4636
- console.log(` ${ui.error("\u2717")} ${ui.error(urlError)}
4637
- `);
4638
- }
4639
6234
  }
4640
6235
  }
4641
- if (legacyMode) {
4642
- console.log(` ${ui.success("\u2713")} ${ui.success("URL:")} ${chalk7.underline(siteUrl)}
4643
- `);
4644
- }
4645
- let platformName = null;
4646
- if (legacyMode) {
4647
- drawHeader("Step 2 : Platform");
4648
- const detected = detectPlatform(siteUrl);
4649
- console.log(` ${ui.info("i")} Auto-detected: ${ui.primary(detected.displayName)}`);
4650
- const platformInput = await ask("Platform name", detected.name);
4651
- const known = PLATFORM_REGISTRY.some((platform) => platform.name === platformInput);
4652
- platformName = known ? platformInput : detected.name;
4653
- console.log(` ${ui.success("\u2713")} ${ui.success("Platform:")} ${ui.primary(platformName)}
4654
- `);
6236
+ if (autoDetect) {
6237
+ platformName = detectPlatform(siteUrl).name;
4655
6238
  } else {
4656
- while (!platformName) {
4657
- const selection = await selectPlatform(siteUrl);
4658
- if ("modifyUrl" in selection) {
4659
- siteUrl = "";
4660
- urlError = "";
4661
- while (!siteUrl) {
4662
- const input = await ask(
4663
- "Modify site URL",
4664
- "",
4665
- ["Step 1 : Site URL", urlError].filter(Boolean)
4666
- );
4667
- try {
4668
- new URL4(input);
4669
- siteUrl = input;
4670
- urlError = "";
4671
- } catch {
4672
- urlError = "Invalid URL. Enter a valid URL (https://...)";
4673
- }
4674
- }
4675
- continue;
4676
- }
4677
- platformName = selection.platform;
6239
+ const detected = resolveDetected(siteUrl);
6240
+ if (detected && detected !== platformName) {
6241
+ const detectedName = getPlatformByName(detected).displayName;
6242
+ const chosenName = getPlatformByName(platformName).displayName;
6243
+ const answer = await select(
6244
+ "URL looks like a different platform",
6245
+ [
6246
+ { label: `Switch to ${detectedName} (detected)`, value: "switch" },
6247
+ { label: `Keep ${chosenName}`, value: "keep" }
6248
+ ],
6249
+ 0,
6250
+ { headerLines: [`URL: ${siteUrl}`] }
6251
+ );
6252
+ if (answer === "switch") platformName = detected;
4678
6253
  }
4679
6254
  }
4680
- if (!platformName) {
4681
- throw new Error("Platform selection failed");
4682
- }
4683
- const rl2 = legacyMode ? rl : null;
4684
- const ask2 = async (question, defaultVal, headerLines = []) => {
4685
- if (!legacyMode) {
4686
- return promptInput(question, defaultVal || "", { headerLines });
4687
- }
4688
- const suffix = defaultVal ? chalk7.gray(` (${defaultVal})`) : "";
4689
- const prompt = ` ${ui.primary("\u25CF")} ${ui.text.bold(question)}${suffix} ${ui.muted(">")} `;
4690
- const answer = await rl2.question(prompt);
4691
- return answer.trim() || defaultVal || "";
4692
- };
4693
6255
  const derivedName = deriveOutputName(siteUrl, platformName);
4694
6256
  let outDir = "./" + derivedName;
4695
- if (legacyMode) {
4696
- drawHeader("Step 3 : Output Directory");
4697
- outDir = await ask2("Output directory", outDir, [
4698
- "Step 3 : Output Directory",
4699
- `URL: ${siteUrl}`,
4700
- `Platform: ${platformName}`
4701
- ]);
4702
- console.log(` ${ui.success("\u2713")} ${ui.success("Output:")} ${ui.primary(outDir)}
4703
- `);
4704
- drawHeader("Step 4 : Options");
4705
- } else {
4706
- outDir = await ask2("Output directory", outDir, [
4707
- "Step 3 : Output Directory",
4708
- `URL: ${siteUrl}`,
4709
- `Platform: ${platformName}`
4710
- ]);
4711
- }
6257
+ outDir = await promptInput("Output directory", outDir, {
6258
+ headerLines: [`URL: ${siteUrl}`, `Tool: ${getPlatformByName(platformName).displayName}`]
6259
+ });
4712
6260
  let prettyPrint = true;
4713
6261
  let concurrency = 12;
4714
6262
  let includeSubpages = false;
4715
- let startExport = false;
4716
- const summaryRows = () => [
4717
- ["URL", siteUrl],
4718
- ["Platform", platformName],
4719
- ["Output", path7.resolve(outDir)],
4720
- ["Pretty-print", prettyPrint ? "yes" : "no"],
4721
- ["Sub-pages", includeSubpages ? "yes" : "no"],
4722
- ["Concurrency", String(concurrency)]
4723
- ];
4724
- const summaryLines = () => summaryRows().map(([label, value]) => `${label}: ${value}`);
4725
- const printSummary2 = () => {
4726
- const w = maxWidth();
4727
- const isSmall = w < 50;
4728
- console.log("");
4729
- if (!isSmall) {
4730
- console.log(boxTop(w));
4731
- console.log(boxLine(w, ui.text.bold(" Summary")));
4732
- console.log(boxSep(w));
4733
- } else {
4734
- console.log(ui.text.bold(" Summary:"));
4735
- }
4736
- for (const [label, value] of summaryRows()) {
4737
- console.log(boxRow(w, label, value));
4738
- }
4739
- if (!isSmall) {
4740
- console.log(boxBot(w));
4741
- }
4742
- console.log("");
4743
- };
4744
- if (legacyMode) {
4745
- const prettyAnswer = await ask2("Pretty-print JS files? (y/n)", "y");
4746
- prettyPrint = prettyAnswer.toLowerCase().startsWith("y");
4747
- console.log(
4748
- ` ${ui.success("\u2713")} Pretty-print: ${prettyPrint ? ui.success("yes") : ui.error("no")}
4749
- `
4750
- );
4751
- const subpagesAnswer = await ask2("Export sub-pages? (y/n)", "n");
4752
- includeSubpages = subpagesAnswer.toLowerCase().startsWith("y");
4753
- console.log(
4754
- ` ${ui.success("\u2713")} Sub-pages: ${includeSubpages ? ui.success("yes") : ui.error("no")}
4755
- `
4756
- );
4757
- const concurrencyAnswer = await ask2("Download concurrency", "12");
4758
- concurrency = parseInt(concurrencyAnswer, 10) || 12;
4759
- console.log(` ${ui.success("\u2713")} Concurrency: ${ui.primary(String(concurrency))}
4760
- `);
4761
- printSummary2();
4762
- const confirm = await ask2("Start export? (y/n)", "y");
4763
- startExport = confirm.toLowerCase().startsWith("y");
4764
- rl2.close();
4765
- } else {
4766
- let optionStep = 0;
4767
- while (true) {
4768
- while (optionStep < 3) {
4769
- if (optionStep === 0) {
4770
- const prettyVal = await select(
4771
- "Pretty-print JS files?",
4772
- [
4773
- { label: "Yes", value: "yes" },
4774
- { label: "No", value: "no" }
4775
- ],
4776
- prettyPrint ? 0 : 1,
4777
- {
4778
- headerLines: ["Step 4 : Options"],
4779
- actions: [{ label: "Previous: output directory", value: "previous" }],
4780
- footer: "previous go back \xB7 enter next \xB7 mouse hover/click"
4781
- }
4782
- );
4783
- if (prettyVal === "previous") {
4784
- outDir = await ask2("Output directory", outDir, [
4785
- "Step 3 : Output Directory",
4786
- `URL: ${siteUrl}`,
4787
- `Platform: ${platformName}`
4788
- ]);
4789
- optionStep = 0;
4790
- continue;
4791
- }
4792
- prettyPrint = prettyVal === "yes";
4793
- optionStep = 1;
4794
- continue;
4795
- }
4796
- if (optionStep === 1) {
4797
- const subpagesVal = await select(
4798
- "Export sub-pages?",
4799
- [
4800
- { label: "No", value: "no" },
4801
- { label: "Yes, crawl and export", value: "yes" }
4802
- ],
4803
- includeSubpages ? 1 : 0,
4804
- {
4805
- headerLines: ["Step 4 : Options"],
4806
- actions: [{ label: "Previous: pretty-print", value: "previous" }],
4807
- footer: "previous go back \xB7 enter next \xB7 mouse hover/click"
4808
- }
4809
- );
4810
- if (subpagesVal === "previous") {
4811
- optionStep = 0;
4812
- continue;
4813
- }
4814
- includeSubpages = subpagesVal === "yes";
4815
- optionStep = 2;
4816
- continue;
4817
- }
4818
- const concurrencyVal = await select(
4819
- "Download concurrency",
4820
- [
4821
- { label: "6 (slow connection)", value: "6" },
4822
- { label: "12 (default)", value: "12" },
4823
- { label: "20 (fast connection)", value: "20" }
4824
- ],
4825
- concurrency === 6 ? 0 : concurrency === 20 ? 2 : 1,
4826
- {
4827
- headerLines: ["Step 4 : Options"],
4828
- actions: [{ label: "Previous: sub-pages", value: "previous" }],
4829
- footer: "previous go back \xB7 enter next \xB7 mouse hover/click"
4830
- }
4831
- );
4832
- if (concurrencyVal === "previous") {
4833
- optionStep = 1;
4834
- continue;
6263
+ let startExport2 = false;
6264
+ let focusIndex = 0;
6265
+ options: while (true) {
6266
+ const summary = [
6267
+ `URL ${siteUrl}`,
6268
+ `Tool ${getPlatformByName(platformName).displayName}`,
6269
+ `Output ${path8.resolve(outDir)}`
6270
+ ];
6271
+ const choice = await select(
6272
+ "Export options",
6273
+ [
6274
+ { label: `${checkbox(prettyPrint)} Pretty-print JS files`, value: "pretty" },
6275
+ { label: `${checkbox(includeSubpages)} Export sub-pages`, value: "subpages" },
6276
+ {
6277
+ label: `[${String(concurrency).padStart(2)}] Download concurrency`,
6278
+ value: "concurrency"
4835
6279
  }
4836
- concurrency = parseInt(concurrencyVal, 10);
4837
- optionStep = 3;
4838
- }
4839
- const confirmVal = await select(
4840
- "Ready to export?",
4841
- [
4842
- { label: "Previous: edit options", value: "previous" },
4843
- { label: "Next: start now", value: "next" },
6280
+ ],
6281
+ focusIndex,
6282
+ {
6283
+ headerLines: summary,
6284
+ actions: [
6285
+ { label: "Start export", value: "start" },
6286
+ { label: "Back", value: "back" },
4844
6287
  { label: "Cancel", value: "cancel" }
4845
6288
  ],
4846
- 1,
4847
- {
4848
- headerLines: ["Summary", ...summaryLines()],
4849
- footer: "previous edit options \xB7 next start export \xB7 mouse hover/click"
4850
- }
4851
- );
4852
- if (confirmVal === "previous") {
4853
- optionStep = 2;
6289
+ footer: "enter toggle tab actions mouse click esc close"
6290
+ }
6291
+ );
6292
+ switch (choice) {
6293
+ case "pretty":
6294
+ prettyPrint = !prettyPrint;
6295
+ focusIndex = 0;
6296
+ continue;
6297
+ case "subpages":
6298
+ includeSubpages = !includeSubpages;
6299
+ focusIndex = 1;
6300
+ continue;
6301
+ case "concurrency": {
6302
+ const idx = CONCURRENCY_STEPS.indexOf(concurrency);
6303
+ concurrency = CONCURRENCY_STEPS[(idx + 1) % CONCURRENCY_STEPS.length];
6304
+ focusIndex = 2;
4854
6305
  continue;
4855
6306
  }
4856
- startExport = confirmVal === "next";
4857
- break;
6307
+ case "back":
6308
+ outDir = await promptInput("Output directory", outDir, {
6309
+ headerLines: [`URL: ${siteUrl}`]
6310
+ });
6311
+ focusIndex = 0;
6312
+ continue;
6313
+ case "start":
6314
+ startExport2 = true;
6315
+ break options;
6316
+ default:
6317
+ startExport2 = false;
6318
+ break options;
6319
+ }
6320
+ }
6321
+ if (!startExport2) {
6322
+ console.log(`
6323
+ ${ui.warning("Export cancelled.")}
6324
+ `);
6325
+ return;
6326
+ }
6327
+ console.log("");
6328
+ await launchExport(siteUrl, outDir, platformName, {
6329
+ prettyPrint,
6330
+ concurrency,
6331
+ includeSubpages
6332
+ });
6333
+ }
6334
+ async function runLegacySetup() {
6335
+ const rl = readline2.createInterface({ input: stdin3, output: stdout5 });
6336
+ const ask = async (question, defaultVal) => {
6337
+ const suffix = defaultVal ? chalk9.gray(` (${defaultVal})`) : "";
6338
+ const prompt = ` ${ui.primary("\u25CF")} ${ui.text.bold(question)}${suffix} ${ui.muted(">")} `;
6339
+ const answer = await rl.question(prompt);
6340
+ return answer.trim() || defaultVal || "";
6341
+ };
6342
+ const platformInput = await ask('Tool (platform id, or "auto")', "auto");
6343
+ const known = PLATFORM_REGISTRY.some((platform) => platform.name === platformInput);
6344
+ let siteUrl = "";
6345
+ while (!siteUrl) {
6346
+ const input = await ask("Enter the site URL");
6347
+ try {
6348
+ new URL5(input);
6349
+ siteUrl = input;
6350
+ } catch {
6351
+ console.log(
6352
+ ` ${ui.error("\u2717")} ${ui.error("Invalid URL. Enter a valid URL (https://...)")}
6353
+ `
6354
+ );
4858
6355
  }
4859
6356
  }
4860
- if (!startExport) {
6357
+ const platformName = known ? platformInput : detectPlatform(siteUrl).name;
6358
+ console.log(` ${ui.success("\u2713")} ${ui.success("Tool:")} ${ui.primary(platformName)}
6359
+ `);
6360
+ const outDir = await ask("Output directory", "./" + deriveOutputName(siteUrl, platformName));
6361
+ const prettyAnswer = await ask("Pretty-print JS files? (y/n)", "y");
6362
+ const prettyPrint = prettyAnswer.toLowerCase().startsWith("y");
6363
+ const subpagesAnswer = await ask("Export sub-pages? (y/n)", "n");
6364
+ const includeSubpages = subpagesAnswer.toLowerCase().startsWith("y");
6365
+ const concurrencyAnswer = await ask("Download concurrency", "12");
6366
+ const concurrency = parseInt(concurrencyAnswer, 10) || 12;
6367
+ const confirm = await ask("Start export? (y/n)", "y");
6368
+ rl.close();
6369
+ if (!confirm.toLowerCase().startsWith("y")) {
4861
6370
  console.log(`
4862
6371
  ${ui.warning("Export cancelled.")}
4863
6372
  `);
4864
6373
  return;
4865
6374
  }
4866
6375
  console.log("");
6376
+ await launchExport(siteUrl, outDir, platformName, {
6377
+ prettyPrint,
6378
+ concurrency,
6379
+ includeSubpages
6380
+ });
6381
+ }
6382
+ async function launchExport(siteUrl, outDir, platformName, opts) {
4867
6383
  const { CFG: CFG2 } = await Promise.resolve().then(() => (init_config(), config_exports));
4868
- CFG2.concurrency = concurrency;
4869
- const exporter = new FramerExporter(siteUrl, path7.resolve(outDir), platformName);
4870
- exporter.prettyPrint = prettyPrint;
6384
+ CFG2.concurrency = opts.concurrency;
6385
+ const exporter = new FramerExporter(siteUrl, path8.resolve(outDir), platformName);
6386
+ exporter.prettyPrint = opts.prettyPrint;
4871
6387
  try {
4872
- await exporter.run(includeSubpages);
6388
+ await exporter.run(opts.includeSubpages);
4873
6389
  } catch (e) {
4874
6390
  const { AntiBotError: AntiBotError2, formatAntiBotError: formatAntiBotError2 } = await Promise.resolve().then(() => (init_anti_bot(), anti_bot_exports));
4875
6391
  if (e instanceof AntiBotError2) {
@@ -4879,6 +6395,7 @@ async function runSetup(legacyMode = false) {
4879
6395
  throw e;
4880
6396
  }
4881
6397
  }
6398
+ var CONCURRENCY_STEPS;
4882
6399
  var init_setup = __esm({
4883
6400
  "src/cli/setup.ts"() {
4884
6401
  "use strict";
@@ -4887,15 +6404,15 @@ var init_setup = __esm({
4887
6404
  init_platforms();
4888
6405
  init_select();
4889
6406
  init_select_grouped();
4890
- init_box();
4891
6407
  init_theme();
6408
+ CONCURRENCY_STEPS = [6, 12, 20];
4892
6409
  }
4893
6410
  });
4894
6411
 
4895
6412
  // src/cli/index.ts
4896
6413
  init_package();
4897
- import path8 from "path";
4898
- import { URL as URL5 } from "url";
6414
+ import path9 from "path";
6415
+ import { URL as URL6 } from "url";
4899
6416
  import { spawnSync } from "child_process";
4900
6417
 
4901
6418
  // src/cli/help.ts
@@ -4912,13 +6429,20 @@ function showHelp() {
4912
6429
  console.log(
4913
6430
  ` ${ui.primary("fexport")} ${ui.warning("<url>")} ${ui.muted("[output-dir]")}`
4914
6431
  );
6432
+ console.log(
6433
+ ` ${ui.primary("framer-export")} ${ui.success("ui")} ${ui.muted("[--port <n>] [--no-open]")}`
6434
+ );
4915
6435
  console.log("");
4916
6436
  console.log(ui.text.bold(" OPTIONS\n"));
4917
6437
  const opts = [
6438
+ ["ui", "Launch the web interface (gallery, options, live logs)"],
4918
6439
  ["--setup", "Launch the interactive setup assistant"],
4919
6440
  ["--platform <p>", "Force platform by id (e.g. framer, shopify, notion)"],
4920
6441
  ["--subpages", "Crawl and export sub-pages"],
4921
- ["--legacy-mode", "Use text input instead of arrow selection"],
6442
+ ["--dpr <number>", "Capture device pixel ratio (default: 1)"],
6443
+ ["--legacy-mode", "With --setup: use text input instead of arrow selection"],
6444
+ ["--about", "Show version and package information"],
6445
+ ["--version, -v", "Show the version number"],
4922
6446
  ["--help, -h", "Show this help message"]
4923
6447
  ];
4924
6448
  for (const [flag, desc] of opts) {
@@ -4928,9 +6452,10 @@ function showHelp() {
4928
6452
  console.log(ui.text.bold(" SUPPORTED PLATFORMS\n"));
4929
6453
  const grouped = platformsByCategory();
4930
6454
  for (const cat of CATEGORY_ORDER) {
4931
- const names = grouped[cat].map((handler) => handler.displayName).join(", ");
6455
+ const names = grouped[cat].map((handler) => handler.displayName + (isBetaPlatform(handler.name) ? "*" : "")).join(", ");
4932
6456
  console.log(` ${ui.primary(CATEGORY_LABELS[cat].padEnd(22))} ${ui.muted(names)}`);
4933
6457
  }
6458
+ console.log(` ${ui.muted("* beta support, Framer, Webflow and Wix are stable")}`);
4934
6459
  console.log("");
4935
6460
  console.log(ui.text.bold(" EXAMPLES\n"));
4936
6461
  console.log(
@@ -4971,8 +6496,8 @@ async function checkForUpdates(currentVersion) {
4971
6496
  res.on("data", (chunk) => data += chunk);
4972
6497
  res.on("end", () => {
4973
6498
  try {
4974
- const json = JSON.parse(data);
4975
- const latest = json.version;
6499
+ const json2 = JSON.parse(data);
6500
+ const latest = json2.version;
4976
6501
  if (!latest) return resolve(null);
4977
6502
  if (isNewerVersion(latest, currentVersion)) return resolve(latest);
4978
6503
  resolve(null);
@@ -5078,20 +6603,20 @@ async function main() {
5078
6603
  process.exit(0);
5079
6604
  }
5080
6605
  if (args.includes("--about")) {
5081
- const chalk8 = (await import("chalk")).default;
6606
+ const chalk10 = (await import("chalk")).default;
5082
6607
  showBanner();
5083
6608
  console.log(` ${ui.text.bold("Framer Export")} ${ui.muted(`v${package_default.version}`)}`);
5084
6609
  console.log(` ${ui.text(package_default.description)}
5085
6610
  `);
5086
6611
  console.log(` ${ui.text.bold("Author")} ${ui.primary("Dany (danbenba)")}`);
5087
6612
  console.log(
5088
- ` ${ui.text.bold("Portfolio")} ${chalk8.underline.hex("#FAB283")("https://github.com/danbenba")}`
6613
+ ` ${ui.text.bold("Portfolio")} ${chalk10.underline.hex("#FAB283")("https://github.com/danbenba")}`
5089
6614
  );
5090
6615
  console.log(
5091
- ` ${ui.text.bold("GitHub")} ${chalk8.underline.hex("#FAB283")(package_default.repository.url.replace("git+", "").replace(".git", ""))}`
6616
+ ` ${ui.text.bold("GitHub")} ${chalk10.underline.hex("#FAB283")(package_default.repository.url.replace("git+", "").replace(".git", ""))}`
5092
6617
  );
5093
6618
  console.log(
5094
- ` ${ui.text.bold("npm")} ${chalk8.underline.hex("#FAB283")(`https://www.npmjs.com/package/${package_default.name}`)}`
6619
+ ` ${ui.text.bold("npm")} ${chalk10.underline.hex("#FAB283")(`https://www.npmjs.com/package/${package_default.name}`)}`
5095
6620
  );
5096
6621
  console.log(` ${ui.text.bold("License")} ${ui.success(package_default.license)}`);
5097
6622
  console.log(` ${ui.text.bold("Node")} ${ui.muted(`>=${package_default.engines.node}`)}`);
@@ -5102,6 +6627,27 @@ async function main() {
5102
6627
  showHelp();
5103
6628
  process.exit(0);
5104
6629
  }
6630
+ if (args[0] === "ui") {
6631
+ args.shift();
6632
+ const portValue = extractFlag(args, "--port");
6633
+ const port = portValue === null ? 4400 : Number(portValue);
6634
+ if (!Number.isFinite(port) || port < 0 || port > 65535) {
6635
+ console.log(
6636
+ ` ${ui.error("\u2717")} ${ui.error.bold("Invalid port:")} ${ui.text(portValue || "")}
6637
+ `
6638
+ );
6639
+ process.exit(1);
6640
+ }
6641
+ showBanner();
6642
+ const { startUiServer: startUiServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
6643
+ const handle = await startUiServer2(port);
6644
+ if (!hasFlag(args, "--no-open")) {
6645
+ const target = `http://localhost:${handle.port}`;
6646
+ const opener = process.platform === "win32" ? { cmd: "cmd", args: ["/c", "start", "", target] } : process.platform === "darwin" ? { cmd: "open", args: [target] } : { cmd: "xdg-open", args: [target] };
6647
+ spawnSync(opener.cmd, opener.args, { stdio: "ignore" });
6648
+ }
6649
+ return;
6650
+ }
5105
6651
  await showLoadingIntro(VERSION);
5106
6652
  await showUpdateNotice();
5107
6653
  if (args.includes("--setup")) {
@@ -5117,11 +6663,20 @@ async function main() {
5117
6663
  return;
5118
6664
  }
5119
6665
  const platformOverride = extractFlag(args, "--platform");
6666
+ const hasDprFlag = args.includes("--dpr");
6667
+ const dprValue = extractFlag(args, "--dpr");
5120
6668
  const includeSubpages = hasFlag(args, "--subpages");
6669
+ const deviceScaleFactor = dprValue === null ? 1 : Number(dprValue);
6670
+ if (hasDprFlag && dprValue === null || !Number.isFinite(deviceScaleFactor) || deviceScaleFactor <= 0) {
6671
+ console.log(` ${ui.error("\u2717")} ${ui.error.bold("Invalid DPR:")} ${ui.text(dprValue || "")}`);
6672
+ console.log(` ${ui.muted("Expected a positive number, for example: --dpr 2")}
6673
+ `);
6674
+ process.exit(1);
6675
+ }
5121
6676
  showBanner();
5122
6677
  const url = args[0];
5123
6678
  try {
5124
- new URL5(url);
6679
+ new URL6(url);
5125
6680
  } catch {
5126
6681
  console.log(` ${ui.error("\u2717")} ${ui.error.bold("Invalid URL:")} ${ui.text(url)}`);
5127
6682
  console.log(` ${ui.muted("Expected: https://yoursite.framer.app")}
@@ -5134,21 +6689,24 @@ async function main() {
5134
6689
  const defaultDir = deriveOutputName2(url, detected);
5135
6690
  const out = args[1] || `./${defaultDir}`;
5136
6691
  try {
5137
- await new FramerExporter2(url, path8.resolve(out), platformOverride || void 0).run(
5138
- includeSubpages
5139
- );
6692
+ await new FramerExporter2(
6693
+ url,
6694
+ path9.resolve(out),
6695
+ platformOverride || void 0,
6696
+ deviceScaleFactor
6697
+ ).run(includeSubpages);
5140
6698
  } catch (e) {
5141
6699
  const { AntiBotError: AntiBotError2, formatAntiBotError: formatAntiBotError2 } = await Promise.resolve().then(() => (init_anti_bot(), anti_bot_exports));
5142
6700
  if (e instanceof AntiBotError2) {
5143
6701
  console.log(formatAntiBotError2(e));
5144
6702
  process.exit(1);
5145
6703
  }
5146
- const chalk8 = (await import("chalk")).default;
6704
+ const chalk10 = (await import("chalk")).default;
5147
6705
  console.log(
5148
6706
  `
5149
6707
  ${ui.error("\u2717")} ${ui.error.bold("FAILED:")} ${ui.text(e.message)}`
5150
6708
  );
5151
- console.log(chalk8.gray(e.stack || ""));
6709
+ console.log(chalk10.gray(e.stack || ""));
5152
6710
  process.exit(1);
5153
6711
  }
5154
6712
  }