@stacksjs/defaults 0.72.50 → 0.72.54

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.
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Commands:
10
10
  * bun browse.ts navigate <url>
11
- * bun browse.ts screenshot <url> [--viewport WxH] [--full] [--element SEL] [--scale N] [--out PATH]
11
+ * bun browse.ts screenshot <url> [--viewport WxH] [--full | --element SEL | --scroll-y N] [--scale N] [--out PATH]
12
12
  * bun browse.ts responsive <url> [--out-dir DIR]
13
13
  * bun browse.ts monitor <url> [--ms 5000]
14
14
  * bun browse.ts snapshot <url>
@@ -403,7 +403,39 @@ async function title(cdp: Cdp): Promise<string> {
403
403
  return r.result?.value ?? ''
404
404
  }
405
405
 
406
- async function captureScreenshot(cdp: Cdp, opts: { full?: boolean, element?: string } = {}): Promise<Buffer> {
406
+ /**
407
+ * Scroll the real page to `y` and let one frame settle before the caller
408
+ * screenshots it.
409
+ *
410
+ * `--full` captures the whole document through `captureBeyondViewport`,
411
+ * which paints content at its full-page layout position without ever
412
+ * moving `scrollY` - correct for a top-to-bottom review, but useless for
413
+ * checking anything that depends on an actual mid-scroll viewport: a
414
+ * `position: sticky` nav overlapping content beneath it, a CSS
415
+ * `animation-timeline: view()` reveal mid-transition, or simply "what does
416
+ * the fold look like 800px down". `window.scrollTo(..., { behavior:
417
+ * 'instant' })` forces the jump synchronously even when the page sets
418
+ * `scroll-behavior: smooth`, which would otherwise animate the scroll and
419
+ * leave a screenshot taken immediately after mid-flight. The two rAF
420
+ * round-trips that follow give the compositor one full frame to repaint
421
+ * sticky/fixed layers and any scroll-driven animation at the new offset
422
+ * before `Page.captureScreenshot` reads back pixels, which
423
+ * `Runtime.evaluate` returning does not by itself guarantee.
424
+ */
425
+ async function scrollTo(cdp: Cdp, y: number): Promise<void> {
426
+ const r = await cdp.send('Runtime.evaluate', {
427
+ expression: `(() => {
428
+ window.scrollTo({ top: ${y}, left: 0, behavior: 'instant' })
429
+ return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(window.scrollY))))
430
+ })()`,
431
+ returnByValue: true,
432
+ awaitPromise: true,
433
+ })
434
+ if (r.exceptionDetails)
435
+ throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text || 'scrollTo failed')
436
+ }
437
+
438
+ async function captureScreenshot(cdp: Cdp, opts: { full?: boolean, element?: string, scrollY?: number } = {}): Promise<Buffer> {
407
439
  let clip: any
408
440
  let captureBeyondViewport = false
409
441
 
@@ -423,6 +455,9 @@ async function captureScreenshot(cdp: Cdp, opts: { full?: boolean, element?: str
423
455
  clip = { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height), scale: 1 }
424
456
  captureBeyondViewport = true
425
457
  }
458
+ else if (opts.scrollY != null) {
459
+ await scrollTo(cdp, opts.scrollY)
460
+ }
426
461
 
427
462
  const r = await cdp.send('Page.captureScreenshot', { format: 'png', ...(clip ? { clip, captureBeyondViewport } : {}) })
428
463
  return Buffer.from(r.data, 'base64')
@@ -523,6 +558,22 @@ function parseViewport(value: string | undefined): { w: number, h: number } {
523
558
  return { w, h }
524
559
  }
525
560
 
561
+ /** `--scroll-y` on `screenshot`: an explicit document Y to jump to before
562
+ * capturing just the viewport, for content a full-page capture cannot show
563
+ * (sticky/fixed elements, scroll-linked animation) at a specific fold. */
564
+ function parseScrollY(value: string | boolean | Array<string | boolean> | undefined): number | null {
565
+ if (value === undefined)
566
+ return null
567
+ if (typeof value !== 'string')
568
+ throw new TypeError('--scroll-y requires a value, for example --scroll-y 800.')
569
+
570
+ const y = Number(value)
571
+ if (!Number.isFinite(y) || y < 0)
572
+ throw new TypeError(`Invalid --scroll-y "${value}". Expected a non-negative number of pixels.`)
573
+
574
+ return y
575
+ }
576
+
526
577
  type ScenarioAction = 'assert' | 'click' | 'evaluate' | 'fill' | 'focus' | 'press' | 'wait'
527
578
 
528
579
  interface ScenarioStep {
@@ -703,6 +754,10 @@ async function main() {
703
754
  console.log(' --cookie "name=value" repeatable; pre-seeds cookies (e.g. coming-soon bypass)')
704
755
  console.log(' --settle 1500 ms to wait after load before acting (default 700; stretch for entrance animations)')
705
756
  console.log(' --scheme dark emulate prefers-color-scheme (light|dark) for QA of theme-aware pages')
757
+ console.log(' --scroll-y 800 screenshot: jump to this document Y first, then capture just the viewport')
758
+ console.log(' (--full renders beyond-viewport without ever scrolling, so it cannot show a')
759
+ console.log(' position:sticky/fixed element overlapping content, or a scroll-linked')
760
+ console.log(' animation mid-transition, at a specific fold - --scroll-y can)')
706
761
  process.exit(url ? 0 : 1)
707
762
  }
708
763
 
@@ -733,12 +788,16 @@ async function main() {
733
788
  const cdp = await openPage(session.port)
734
789
  const viewport = parseViewport(typeof flags.viewport === 'string' ? flags.viewport : undefined)
735
790
  const scale = flags.scale ? Number(flags.scale) : 1
791
+ const scrollY = parseScrollY(flags['scroll-y'])
792
+ const modes = [flags.full && 'full', flags.element && 'element', scrollY != null && 'scroll-y'].filter(Boolean)
793
+ if (modes.length > 1)
794
+ throw new TypeError(`--full, --element, and --scroll-y are mutually exclusive; got ${modes.join(', ')}.`)
736
795
  const out = (flags.out as string) || `storage/framework/runtime/shots/${new URL(url).pathname.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '') || 'home'}.png`
737
796
  mkdirSync(out.split('/').slice(0, -1).join('/') || '.', { recursive: true })
738
797
  const state = await gotoAndInstrument(cdp, url, { viewport, scale, cookies, settleMs, scheme })
739
- const png = await captureScreenshot(cdp, { full: !!flags.full, element: flags.element as string | undefined })
798
+ const png = await captureScreenshot(cdp, { full: !!flags.full, element: flags.element as string | undefined, scrollY: scrollY ?? undefined })
740
799
  await Bun.write(out, png)
741
- console.log(JSON.stringify({ url, out, viewport: `${viewport.w}x${viewport.h}`, scale, full: !!flags.full, element: flags.element ?? null, bytes: png.length }, null, 2))
800
+ console.log(JSON.stringify({ url, out, viewport: `${viewport.w}x${viewport.h}`, scale, full: !!flags.full, element: flags.element ?? null, scrollY: scrollY ?? null, bytes: png.length }, null, 2))
742
801
  state.dispose()
743
802
  cdp.close()
744
803
  }
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.72.50",
5
+ "version": "0.72.54",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,12 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.50",
5
+ "version": "0.72.54",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/stacksjs/stacks.git",
9
+ "directory": "./storage/framework/core/defaults"
10
+ },
6
11
  "description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
7
12
  "author": "Chris Breuer",
8
13
  "license": "MIT",
@@ -46,7 +51,7 @@
46
51
  "dependencies": {
47
52
  "@iconify-json/f7": "^1.2.2",
48
53
  "@iconify-json/hugeicons": "^1.2.27",
49
- "@stacksjs/mobile": "^0.72.50",
54
+ "@stacksjs/mobile": "^0.72.54",
50
55
  "@stacksjs/sanitizer": "^0.2.113"
51
56
  },
52
57
  "scripts": {
@@ -26,7 +26,9 @@ const backgroundImage = props.backgroundImage || '/svgs/404.svg'
26
26
  Sorry, we couldn't find the page you're looking for.
27
27
  </p>
28
28
 
29
- <a href="/" class="inline-block mt-10 px-4 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded">
29
+ <!-- Same deep rose as the illustration beside it. White on #b63e59 is
30
+ 5.49:1 and on the #a22a50 hover 7.04:1, so both states clear AA. -->
31
+ <a href="/" class="inline-block mt-10 px-4 py-2 text-white bg-[#b63e59] hover:bg-[#a22a50] rounded">
30
32
  Go back home
31
33
  </a>
32
34
  </main>
@@ -10,6 +10,8 @@
10
10
  // served from public/svgs.
11
11
  </script>
12
12
 
13
+ @title('Page not found')
14
+
13
15
  <div class="flex flex-col md:flex-row min-h-screen bg-neutral-50 dark:bg-neutral-900">
14
16
  <div class="overflow-hidden md:order-2 h-56 sm:h-72 md:h-auto md:w-1/2 bg-neutral-200 dark:bg-neutral-800">
15
17
  <img src="/svgs/404.svg" alt="" class="object-cover h-full w-full">
@@ -18,7 +20,11 @@
18
20
  <section class="flex md:order-1 flex-1 flex-col items-start justify-center md:flex-none px-8 py-12 md:px-16 md:w-1/2">
19
21
  <div class="font-extrabold text-6xl text-neutral-950 tracking-tight md:text-8xl dark:text-white">404</div>
20
22
 
21
- <div class="my-6 h-1 w-14 bg-blue-600 dark:bg-blue-500 rounded-full"></div>
23
+ <!-- The rule takes the illustration's own deep rose rather than a
24
+ framework accent: it sits inches from the artwork, and a blue mark
25
+ against a warm dusk scene reads as a mistake in any app's brand. The
26
+ dark value is the same ramp's coral, from the scene's night variant. -->
27
+ <div class="my-6 h-1 w-14 bg-[#b63e59] dark:bg-[#ee7566] rounded-full"></div>
22
28
 
23
29
  <h1 class="mb-2 font-semibold text-2xl text-neutral-950 dark:text-white">Page not found</h1>
24
30
 
@@ -9,6 +9,9 @@ on:
9
9
  branches:
10
10
  - main
11
11
 
12
+ # So a maintainer can ship without pushing an empty commit.
13
+ workflow_dispatch:
14
+
12
15
  concurrency:
13
16
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
14
17
  cancel-in-progress: true
@@ -119,3 +122,87 @@ jobs:
119
122
 
120
123
  - name: Test Suite
121
124
  run: bun run test
125
+
126
+ # Push to deploy: `buddy deploy` on every green push to main.
127
+ #
128
+ # A fresh app has nothing to deploy to, and red CI on day one is worse than
129
+ # no deploy, so this checks for its secrets first and finishes green with a
130
+ # notice when they are missing. Add them under
131
+ # Settings > Secrets and variables > Actions to turn it on:
132
+ #
133
+ # DOTENV_PRIVATE_KEY_PRODUCTION decrypts .env.production for the deploy
134
+ # DEPLOY_SSH_KEY private key for the target server
135
+ #
136
+ # Cloud tokens are optional. Without them the release still ships and DNS is
137
+ # still published; only the provider-specific parts are skipped.
138
+ deploy:
139
+ if: >-
140
+ github.event_name == 'workflow_dispatch' ||
141
+ (github.event_name == 'push' && github.ref == 'refs/heads/main')
142
+ needs: [lint, typecheck, test]
143
+ runs-on: ubuntu-latest
144
+ timeout-minutes: 30
145
+ permissions:
146
+ contents: read
147
+ deployments: write
148
+
149
+ steps:
150
+ - name: Checkout Code
151
+ uses: actions/checkout@v4
152
+
153
+ - name: Is deployment configured?
154
+ id: check
155
+ env:
156
+ DOTENV_PRIVATE_KEY_PRODUCTION: ${{ secrets.DOTENV_PRIVATE_KEY_PRODUCTION }}
157
+ DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
158
+ run: |
159
+ if [ -n "$DOTENV_PRIVATE_KEY_PRODUCTION" ] && [ -n "$DEPLOY_SSH_KEY" ]; then
160
+ echo 'configured=true' >> "$GITHUB_OUTPUT"
161
+ else
162
+ echo 'configured=false' >> "$GITHUB_OUTPUT"
163
+ echo '::notice::Deployment is not configured, so this step did nothing. Add DOTENV_PRIVATE_KEY_PRODUCTION and DEPLOY_SSH_KEY to deploy on every push to main.'
164
+ fi
165
+
166
+ - name: Setup Pantry
167
+ if: steps.check.outputs.configured == 'true'
168
+ uses: pantry-pm/pantry/packages/action@c1417d98f5613290bc3b1f96d35086b834684659 # v0.10.47 + the socket-hang-up retry fix
169
+ with:
170
+ install: 'false'
171
+
172
+ - name: Use cached node_modules
173
+ if: steps.check.outputs.configured == 'true'
174
+ uses: actions/cache@v4
175
+ with:
176
+ path: node_modules
177
+ key: node-modules-${{ hashFiles('**/bun.lock') }}
178
+ restore-keys: |
179
+ node-modules-
180
+
181
+ - name: Install Dependencies
182
+ if: steps.check.outputs.configured == 'true'
183
+ run: bun install
184
+
185
+ - name: Prepare Deploy SSH Key
186
+ if: steps.check.outputs.configured == 'true'
187
+ env:
188
+ DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
189
+ run: |
190
+ mkdir -p ~/.ssh
191
+ chmod 700 ~/.ssh
192
+ printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
193
+ chmod 600 ~/.ssh/id_ed25519
194
+ ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub
195
+ chmod 644 ~/.ssh/id_ed25519.pub
196
+
197
+ - name: Deploy production
198
+ if: steps.check.outputs.configured == 'true'
199
+ run: bun buddy deploy --yes
200
+ env:
201
+ APP_ENV: production
202
+ DOTENV_PRIVATE_KEY_PRODUCTION: ${{ secrets.DOTENV_PRIVATE_KEY_PRODUCTION }}
203
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
204
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
205
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
206
+ # macOS resource forks in a tarball built on a developer machine make
207
+ # the extraction on the server noisy and occasionally fatal.
208
+ COPYFILE_DISABLE: '1'