launchframe 0.2.4 → 0.2.6

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.
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Reads URL from launchframe.config.json (or --url). Writes:
6
6
  * - docs/design-references/playwright-<host>-<w>px.png (full page)
7
- * - docs/research/computed-snapshot.json (styles + asset inventory + bot-wall hint)
7
+ * - docs/research/computed-snapshot.json (styles + merged asset inventory + bot-wall hint)
8
8
  * - docs/research/MEDIA_MANIFEST.md (table of discovered media URLs)
9
9
  *
10
10
  * Usage:
@@ -52,27 +52,91 @@ function safeHost(url) {
52
52
  }
53
53
  }
54
54
 
55
- /** Scroll to bottom slowly to trigger lazy-loaded media */
55
+ /**
56
+ * Scroll the full document depth slowly so lazy-loaded media and below-fold DOM resolve.
57
+ * Repeats until scrollHeight stabilizes (handles incremental infinite layouts up to max rounds).
58
+ */
56
59
  async function scrollFullPage(page) {
57
60
  await page.evaluate(async () => {
58
61
  const delay = (ms) => new Promise((r) => setTimeout(r, ms));
59
- const step = Math.max(1, Math.floor(window.innerHeight * 0.85));
60
- let y = 0;
61
- const max = document.documentElement.scrollHeight;
62
- while (y < max) {
63
- y = Math.min(y + step, max);
64
- window.scrollTo(0, y);
65
- await delay(200);
62
+ const stepSize = () => Math.max(1, Math.floor(window.innerHeight * 0.85));
63
+
64
+ let unchangedStreak = 0;
65
+ let prevTotal = -1;
66
+
67
+ for (let round = 0; round < 12; round++) {
68
+ let y = 0;
69
+ let limit = document.documentElement.scrollHeight;
70
+
71
+ while (y < limit) {
72
+ y = Math.min(y + stepSize(), limit);
73
+ window.scrollTo(0, y);
74
+ await delay(130);
75
+ limit = document.documentElement.scrollHeight;
76
+ }
77
+
78
+ window.scrollTo(0, document.documentElement.scrollHeight);
79
+ await delay(420);
80
+
81
+ const h = document.documentElement.scrollHeight;
82
+
83
+ window.scrollTo(0, 0);
84
+ await delay(220);
85
+
86
+ if (h === prevTotal) unchangedStreak++;
87
+ else unchangedStreak = 0;
88
+ prevTotal = h;
89
+
90
+ if (unchangedStreak >= 2) break;
66
91
  }
67
- window.scrollTo(0, 0);
68
- await delay(300);
69
92
  });
70
93
  }
71
94
 
95
+ function mergeAssetInventory(base, extra) {
96
+ if (!extra) return base;
97
+ const imgKey = (i) => i.src || "";
98
+ const seenImg = new Set(base.images.map(imgKey));
99
+ const images = [...base.images];
100
+ for (const i of extra.images) {
101
+ const k = imgKey(i);
102
+ if (k && !seenImg.has(k)) {
103
+ seenImg.add(k);
104
+ images.push(i);
105
+ }
106
+ }
107
+
108
+ const vidKey = (v) => `${v.src || ""}|${v.poster || ""}`;
109
+ const seenVid = new Set(base.videos.map(vidKey));
110
+ const videos = [...base.videos];
111
+ for (const v of extra.videos) {
112
+ const k = vidKey(v);
113
+ if ((v.src || v.poster) && !seenVid.has(k)) {
114
+ seenVid.add(k);
115
+ videos.push(v);
116
+ }
117
+ }
118
+
119
+ const seenBg = new Set(base.backgroundImages.map((b) => b.url));
120
+ const backgroundImages = [...base.backgroundImages];
121
+ for (const b of extra.backgroundImages) {
122
+ if (b.url && !seenBg.has(b.url)) {
123
+ seenBg.add(b.url);
124
+ backgroundImages.push(b);
125
+ }
126
+ }
127
+
128
+ return {
129
+ images,
130
+ videos,
131
+ backgroundImages,
132
+ svgCount: Math.max(base.svgCount ?? 0, extra.svgCount ?? 0),
133
+ };
134
+ }
135
+
72
136
  async function gatherPageData(page) {
73
137
  return page.evaluate(() => {
74
138
  const title = document.title || "";
75
- const text = (document.body?.innerText || "").slice(0, 5000);
139
+ const text = (document.body?.innerText || "").slice(0, 80000);
76
140
  const likelyBotWall =
77
141
  /just a moment/i.test(title) ||
78
142
  /checking your browser/i.test(text) ||
@@ -145,7 +209,7 @@ async function gatherPageData(page) {
145
209
  return {
146
210
  title,
147
211
  likelyBotWall,
148
- textSample: text.slice(0, 1200),
212
+ textSample: text.slice(0, 24000),
149
213
  assetInventory: {
150
214
  images: imgs.filter((i) => i.src),
151
215
  videos,
@@ -272,23 +336,32 @@ async function main() {
272
336
  await new Promise((r) => setTimeout(r, 2500));
273
337
  snapshot.finalUrl = page.url();
274
338
 
339
+ let mergedInventory = null;
340
+ let desktopLandmarks = null;
341
+
275
342
  for (const w of [1440, 390]) {
276
343
  await page.setViewportSize({ width: w, height: w === 1440 ? 900 : 844 });
277
- if (w === 1440) await scrollFullPage(page);
344
+ await scrollFullPage(page);
278
345
 
279
346
  const shotPath = join(ROOT, "docs", "design-references", `playwright-${host}-${w}px.png`);
280
347
  await page.screenshot({ path: shotPath, fullPage: true });
281
348
  snapshot.viewports[w] = { screenshot: `docs/design-references/playwright-${host}-${w}px.png` };
282
349
 
350
+ const data = await gatherPageData(page);
351
+
283
352
  if (w === 1440) {
284
- const data = await gatherPageData(page);
285
353
  snapshot.title = data.title;
286
354
  snapshot.likelyBotWall = data.likelyBotWall;
287
- snapshot.assetInventory = data.assetInventory;
288
- snapshot.computedLandmarks = data.computedLandmarks;
355
+ mergedInventory = data.assetInventory;
356
+ desktopLandmarks = data.computedLandmarks;
289
357
  snapshot.textSample = data.textSample;
358
+ } else {
359
+ mergedInventory = mergeAssetInventory(mergedInventory, data.assetInventory);
290
360
  }
291
361
  }
362
+
363
+ snapshot.assetInventory = mergedInventory;
364
+ snapshot.computedLandmarks = desktopLandmarks;
292
365
  } finally {
293
366
  await browser.close();
294
367
  }
@@ -46,6 +46,9 @@
46
46
  --radius-2xl: calc(var(--radius) * 1.8);
47
47
  --radius-3xl: calc(var(--radius) * 2.2);
48
48
  --radius-4xl: calc(var(--radius) * 2.6);
49
+ --color-brand: var(--brand);
50
+ --color-brand-foreground: var(--brand-foreground);
51
+ --color-brand-muted: var(--brand-muted);
49
52
  }
50
53
 
51
54
  :root {
@@ -73,6 +76,10 @@
73
76
  --chart-4: oklch(0.371 0 0);
74
77
  --chart-5: oklch(0.269 0 0);
75
78
  --radius: 0.625rem;
79
+ /* Scribewise brand accents (ink + notebook blue) */
80
+ --brand: oklch(0.52 0.14 252);
81
+ --brand-foreground: oklch(0.985 0 0);
82
+ --brand-muted: oklch(0.94 0.03 252);
76
83
  --sidebar: oklch(0.985 0 0);
77
84
  --sidebar-foreground: oklch(0.145 0 0);
78
85
  --sidebar-primary: oklch(0.205 0 0);
@@ -84,6 +91,9 @@
84
91
  }
85
92
 
86
93
  .dark {
94
+ --brand: oklch(0.62 0.12 252);
95
+ --brand-foreground: oklch(0.145 0 0);
96
+ --brand-muted: oklch(0.26 0.04 252);
87
97
  --background: oklch(0.145 0 0);
88
98
  --foreground: oklch(0.985 0 0);
89
99
  --card: oklch(0.205 0 0);
@@ -117,6 +127,88 @@
117
127
  --sidebar-ring: oklch(0.556 0 0);
118
128
  }
119
129
 
130
+ @keyframes scribewise-marquee {
131
+ from {
132
+ transform: translateX(0);
133
+ }
134
+ to {
135
+ transform: translateX(-50%);
136
+ }
137
+ }
138
+
139
+ @keyframes scribewise-float {
140
+ 0%,
141
+ 100% {
142
+ transform: translateY(0);
143
+ }
144
+ 50% {
145
+ transform: translateY(-8px);
146
+ }
147
+ }
148
+
149
+ @keyframes scribewise-pixel-blink {
150
+ 0%,
151
+ 49% {
152
+ opacity: 1;
153
+ }
154
+ 50%,
155
+ 100% {
156
+ opacity: 0.25;
157
+ }
158
+ }
159
+
160
+ @keyframes scribewise-grid-shift {
161
+ 0% {
162
+ transform: translate(0, 0);
163
+ }
164
+ 100% {
165
+ transform: translate(-24px, -24px);
166
+ }
167
+ }
168
+
169
+ .scribewise-marquee-track {
170
+ display: flex;
171
+ width: max-content;
172
+ gap: 3rem;
173
+ animation: scribewise-marquee 42s linear infinite;
174
+ }
175
+
176
+ .scribewise-bg-grid {
177
+ animation: scribewise-grid-shift 18s linear infinite;
178
+ }
179
+
180
+ @media (prefers-reduced-motion: reduce) {
181
+ .scribewise-marquee-track,
182
+ .scribewise-bg-grid {
183
+ animation: none !important;
184
+ }
185
+
186
+ .scribewise-pixel-brain rect,
187
+ .scribewise-soft-float {
188
+ animation: none !important;
189
+ }
190
+ }
191
+
192
+ .scribewise-pixel-brain rect {
193
+ animation: scribewise-pixel-blink 1s steps(1, end) infinite;
194
+ }
195
+
196
+ .scribewise-pixel-brain rect:nth-child(1) {
197
+ animation-delay: 0s;
198
+ }
199
+
200
+ .scribewise-pixel-brain rect:nth-child(2) {
201
+ animation-delay: 0.12s;
202
+ }
203
+
204
+ .scribewise-pixel-brain rect:nth-child(3) {
205
+ animation-delay: 0.24s;
206
+ }
207
+
208
+ .scribewise-pixel-brain rect:nth-child(4) {
209
+ animation-delay: 0.36s;
210
+ }
211
+
120
212
  @layer base {
121
213
  * {
122
214
  @apply border-border outline-ring/50;
@@ -127,4 +219,4 @@
127
219
  html {
128
220
  @apply font-sans;
129
221
  }
130
- }
222
+ }
@@ -13,8 +13,9 @@ const geistMono = Geist_Mono({
13
13
  });
14
14
 
15
15
  export const metadata: Metadata = {
16
- title: "Website Clone",
17
- description: "Pixel-perfect website clone",
16
+ title: "Scribewise — AI workspace for structured notes",
17
+ description:
18
+ "Capture voice, screenshots, and messy thoughts into structured notes synced everywhere.",
18
19
  };
19
20
 
20
21
  export default function RootLayout({
@@ -1,9 +1,5 @@
1
+ import { ScribewiseLanding } from "@/components/marketing/scribewise-landing";
2
+
1
3
  export default function Home() {
2
- return (
3
- <main className="flex min-h-screen items-center justify-center">
4
- <p className="text-muted-foreground">
5
- Clone target not yet built. Run <code className="font-mono text-foreground">/clone-website</code> to start.
6
- </p>
7
- </main>
8
- );
4
+ return <ScribewiseLanding />;
9
5
  }
@@ -0,0 +1,34 @@
1
+ import Link from "next/link";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ export function ScribewiseLanding() {
6
+ return (
7
+ <main className="relative flex flex-1 flex-col items-center justify-center gap-8 overflow-hidden px-6 py-24 text-center">
8
+ <div
9
+ aria-hidden
10
+ className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,color-mix(in_oklab,var(--primary)_28%,transparent),transparent)]"
11
+ />
12
+ <p className="text-muted-foreground text-xs font-medium tracking-[0.2em] uppercase">
13
+ Launchframe template
14
+ </p>
15
+ <div className="max-w-2xl space-y-4">
16
+ <h1 className="text-balance font-semibold text-4xl tracking-tight sm:text-5xl">
17
+ Ready for `/clone-website`
18
+ </h1>
19
+ <p className="text-muted-foreground text-balance text-lg">
20
+ Add your target URL and SaaS idea via <code className="text-foreground">launchframe.config.json</code>,
21
+ run recon if needed, then let your agent ship the full landing.
22
+ </p>
23
+ </div>
24
+ <div className="flex flex-wrap items-center justify-center gap-3">
25
+ <Button nativeButton={false} render={<Link href="https://github.com/evangruhlkey/launchframe" />}>
26
+ Launchframe docs
27
+ </Button>
28
+ <Button variant="outline" nativeButton={false} render={<Link href="/" />}>
29
+ Replace this page
30
+ </Button>
31
+ </div>
32
+ </main>
33
+ );
34
+ }