@deckio/deck-engine 1.7.6 → 1.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/BottomBar.jsx +9 -9
- package/components/BottomBar.module.css +17 -17
- package/components/Navigation.jsx +155 -106
- package/components/Navigation.module.css +154 -145
- package/components/Slide.jsx +15 -15
- package/components/exportDeckPdf.js +133 -0
- package/context/SlideContext.jsx +171 -171
- package/index.js +5 -5
- package/instructions/AGENTS.md +26 -26
- package/instructions/deck-config.instructions.md +34 -34
- package/instructions/deck-project.instructions.md +34 -34
- package/instructions/slide-css.instructions.md +91 -91
- package/instructions/slide-jsx.instructions.md +34 -34
- package/package.json +49 -45
- package/scripts/capture-screen.mjs +110 -110
- package/scripts/export-pdf.mjs +287 -287
- package/scripts/generate-image.mjs +110 -110
- package/scripts/init-project.mjs +214 -214
- package/skills/deck-add-slide/SKILL.md +217 -217
- package/skills/deck-delete-slide/SKILL.md +51 -51
- package/skills/deck-generate-image/SKILL.md +85 -85
- package/skills/deck-inspect/SKILL.md +60 -60
- package/skills/deck-sketch/SKILL.md +91 -91
- package/skills/deck-validate-project/SKILL.md +80 -80
- package/slides/GenericThankYouSlide.jsx +31 -31
- package/slides/ThankYouSlide.module.css +131 -131
- package/styles/global.css +191 -191
- package/vite.js +26 -26
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Use when working in any deck-project folder. Defines the role, scope, and guardrails for Copilot in deck presentation projects."
|
|
3
|
-
applyTo: "deck-project-*/**"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Deck Project Scope
|
|
7
|
-
|
|
8
|
-
You are a **slide builder** for a presentation deck built with `@deckio/deck-engine`.
|
|
9
|
-
|
|
10
|
-
## Your role
|
|
11
|
-
|
|
12
|
-
- Create new slide components (JSX + CSS module pairs)
|
|
13
|
-
- Edit existing slides
|
|
14
|
-
- Manage data files in `src/data/`
|
|
15
|
-
- Register / reorder slides in `deck.config.js`
|
|
16
|
-
|
|
17
|
-
## Out of scope — do NOT modify
|
|
18
|
-
|
|
19
|
-
- `App.jsx`, `main.jsx` — these are generic, engine-driven, identical across projects
|
|
20
|
-
- `vite.config.js`, `package.json`, `index.html` — project scaffolding, don't touch
|
|
21
|
-
- Anything in `node_modules/` or the deck-engine package itself
|
|
22
|
-
|
|
23
|
-
## Project architecture
|
|
24
|
-
|
|
25
|
-
- `deck.config.js` — single source of truth: metadata + slide array
|
|
26
|
-
- `src/slides/` — one `PascalCase.jsx` + matching `.module.css` per slide
|
|
27
|
-
- `src/data/` — ESM exports for logos, speakers, opportunity data, governance
|
|
28
|
-
- The engine (`@deckio/deck-engine`) provides: `Slide`, `BottomBar`, `Navigation`, `SlideProvider`, `useSlides`, `GenericThankYouSlide`
|
|
29
|
-
|
|
30
|
-
## Data conventions
|
|
31
|
-
|
|
32
|
-
- Always use ESM imports for images: `import logo from '../data/logos/acme.png'`
|
|
33
|
-
- Export data as named exports or default objects
|
|
34
|
-
- Keep data files focused — one concern per file
|
|
1
|
+
---
|
|
2
|
+
description: "Use when working in any deck-project folder. Defines the role, scope, and guardrails for Copilot in deck presentation projects."
|
|
3
|
+
applyTo: "deck-project-*/**"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deck Project Scope
|
|
7
|
+
|
|
8
|
+
You are a **slide builder** for a presentation deck built with `@deckio/deck-engine`.
|
|
9
|
+
|
|
10
|
+
## Your role
|
|
11
|
+
|
|
12
|
+
- Create new slide components (JSX + CSS module pairs)
|
|
13
|
+
- Edit existing slides
|
|
14
|
+
- Manage data files in `src/data/`
|
|
15
|
+
- Register / reorder slides in `deck.config.js`
|
|
16
|
+
|
|
17
|
+
## Out of scope — do NOT modify
|
|
18
|
+
|
|
19
|
+
- `App.jsx`, `main.jsx` — these are generic, engine-driven, identical across projects
|
|
20
|
+
- `vite.config.js`, `package.json`, `index.html` — project scaffolding, don't touch
|
|
21
|
+
- Anything in `node_modules/` or the deck-engine package itself
|
|
22
|
+
|
|
23
|
+
## Project architecture
|
|
24
|
+
|
|
25
|
+
- `deck.config.js` — single source of truth: metadata + slide array
|
|
26
|
+
- `src/slides/` — one `PascalCase.jsx` + matching `.module.css` per slide
|
|
27
|
+
- `src/data/` — ESM exports for logos, speakers, opportunity data, governance
|
|
28
|
+
- The engine (`@deckio/deck-engine`) provides: `Slide`, `BottomBar`, `Navigation`, `SlideProvider`, `useSlides`, `GenericThankYouSlide`
|
|
29
|
+
|
|
30
|
+
## Data conventions
|
|
31
|
+
|
|
32
|
+
- Always use ESM imports for images: `import logo from '../data/logos/acme.png'`
|
|
33
|
+
- Export data as named exports or default objects
|
|
34
|
+
- Keep data files focused — one concern per file
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Use when creating or editing slide CSS modules in a deck project. Enforces required properties, orb positioning, and theme variables."
|
|
3
|
-
applyTo: "**/slides/**/*.module.css"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Slide CSS Module Conventions
|
|
7
|
-
|
|
8
|
-
## Required root class
|
|
9
|
-
|
|
10
|
-
```css
|
|
11
|
-
.mySlide {
|
|
12
|
-
background: var(--bg-deep);
|
|
13
|
-
flex-direction: column;
|
|
14
|
-
padding: 0 0 44px 0; /* reserve BottomBar height */
|
|
15
|
-
}
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Add `justify-content: center` for cover or thank-you slides.
|
|
19
|
-
|
|
20
|
-
## Orb positioning recipe
|
|
21
|
-
|
|
22
|
-
```css
|
|
23
|
-
.orb1 {
|
|
24
|
-
width: 420px; height: 420px;
|
|
25
|
-
top: -100px; right: -60px;
|
|
26
|
-
background: radial-gradient(circle at 40% 40%, var(--accent), var(--blue-glow) 50%, transparent 70%);
|
|
27
|
-
}
|
|
28
|
-
.orb2 {
|
|
29
|
-
width: 320px; height: 320px;
|
|
30
|
-
bottom: -40px; right: 100px;
|
|
31
|
-
background: radial-gradient(circle at 50% 50%, var(--purple-deep), rgba(110,64,201,0.25) 60%, transparent 75%);
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Body wrapper
|
|
36
|
-
|
|
37
|
-
```css
|
|
38
|
-
.body {
|
|
39
|
-
position: relative;
|
|
40
|
-
z-index: 10;
|
|
41
|
-
display: flex;
|
|
42
|
-
flex-direction: column;
|
|
43
|
-
justify-content: center;
|
|
44
|
-
flex: 1;
|
|
45
|
-
min-height: 0;
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Theme variables (always use these, never hard-code colors)
|
|
50
|
-
|
|
51
|
-
| Variable | Value |
|
|
52
|
-
|----------|-------|
|
|
53
|
-
| `--bg-deep` | `#080b10` |
|
|
54
|
-
| `--surface` | `#161b22` |
|
|
55
|
-
| `--border` | `#30363d` |
|
|
56
|
-
| `--text` | `#e6edf3` |
|
|
57
|
-
| `--text-muted` | `#8b949e` |
|
|
58
|
-
| `--accent` | project-specific |
|
|
59
|
-
| `--blue-glow` | `#1f6feb` |
|
|
60
|
-
| `--purple` | `#bc8cff` |
|
|
61
|
-
| `--purple-deep` | `#6e40c9` |
|
|
62
|
-
| `--pink` | `#f778ba` |
|
|
63
|
-
| `--cyan` | `#56d4dd` |
|
|
64
|
-
| `--green` | `#3fb950` |
|
|
65
|
-
| `--orange` | `#d29922` |
|
|
66
|
-
|
|
67
|
-
## Global classes (no import needed)
|
|
68
|
-
|
|
69
|
-
`accent-bar`, `orb`, `grid-dots`, `content-frame`, `content-gutter`
|
|
70
|
-
|
|
71
|
-
## Card pattern
|
|
72
|
-
|
|
73
|
-
```css
|
|
74
|
-
.card {
|
|
75
|
-
background: var(--surface);
|
|
76
|
-
border: 1px solid var(--border);
|
|
77
|
-
border-radius: 16px;
|
|
78
|
-
padding: 24px;
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Typography
|
|
83
|
-
|
|
84
|
-
| Element | Size | Weight | Spacing |
|
|
85
|
-
|---------|------|--------|---------|
|
|
86
|
-
| h1 | `clamp(42px, 5vw, 72px)` | 900 | `-2px` |
|
|
87
|
-
| h2 | `clamp(28px, 3.2vw, 36px)` | 700 | `-0.8px` |
|
|
88
|
-
| h3 | `16px–20px` | 700 | `-0.3px` |
|
|
89
|
-
| Subtitle | `17px` | 300–400 | — |
|
|
90
|
-
| Body | `13px–14px` | 400 | — |
|
|
91
|
-
| Badge | `10px–11px` | 600–700 | `1.5px` |
|
|
1
|
+
---
|
|
2
|
+
description: "Use when creating or editing slide CSS modules in a deck project. Enforces required properties, orb positioning, and theme variables."
|
|
3
|
+
applyTo: "**/slides/**/*.module.css"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Slide CSS Module Conventions
|
|
7
|
+
|
|
8
|
+
## Required root class
|
|
9
|
+
|
|
10
|
+
```css
|
|
11
|
+
.mySlide {
|
|
12
|
+
background: var(--bg-deep);
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
padding: 0 0 44px 0; /* reserve BottomBar height */
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Add `justify-content: center` for cover or thank-you slides.
|
|
19
|
+
|
|
20
|
+
## Orb positioning recipe
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
.orb1 {
|
|
24
|
+
width: 420px; height: 420px;
|
|
25
|
+
top: -100px; right: -60px;
|
|
26
|
+
background: radial-gradient(circle at 40% 40%, var(--accent), var(--blue-glow) 50%, transparent 70%);
|
|
27
|
+
}
|
|
28
|
+
.orb2 {
|
|
29
|
+
width: 320px; height: 320px;
|
|
30
|
+
bottom: -40px; right: 100px;
|
|
31
|
+
background: radial-gradient(circle at 50% 50%, var(--purple-deep), rgba(110,64,201,0.25) 60%, transparent 75%);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Body wrapper
|
|
36
|
+
|
|
37
|
+
```css
|
|
38
|
+
.body {
|
|
39
|
+
position: relative;
|
|
40
|
+
z-index: 10;
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
flex: 1;
|
|
45
|
+
min-height: 0;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Theme variables (always use these, never hard-code colors)
|
|
50
|
+
|
|
51
|
+
| Variable | Value |
|
|
52
|
+
|----------|-------|
|
|
53
|
+
| `--bg-deep` | `#080b10` |
|
|
54
|
+
| `--surface` | `#161b22` |
|
|
55
|
+
| `--border` | `#30363d` |
|
|
56
|
+
| `--text` | `#e6edf3` |
|
|
57
|
+
| `--text-muted` | `#8b949e` |
|
|
58
|
+
| `--accent` | project-specific |
|
|
59
|
+
| `--blue-glow` | `#1f6feb` |
|
|
60
|
+
| `--purple` | `#bc8cff` |
|
|
61
|
+
| `--purple-deep` | `#6e40c9` |
|
|
62
|
+
| `--pink` | `#f778ba` |
|
|
63
|
+
| `--cyan` | `#56d4dd` |
|
|
64
|
+
| `--green` | `#3fb950` |
|
|
65
|
+
| `--orange` | `#d29922` |
|
|
66
|
+
|
|
67
|
+
## Global classes (no import needed)
|
|
68
|
+
|
|
69
|
+
`accent-bar`, `orb`, `grid-dots`, `content-frame`, `content-gutter`
|
|
70
|
+
|
|
71
|
+
## Card pattern
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
.card {
|
|
75
|
+
background: var(--surface);
|
|
76
|
+
border: 1px solid var(--border);
|
|
77
|
+
border-radius: 16px;
|
|
78
|
+
padding: 24px;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Typography
|
|
83
|
+
|
|
84
|
+
| Element | Size | Weight | Spacing |
|
|
85
|
+
|---------|------|--------|---------|
|
|
86
|
+
| h1 | `clamp(42px, 5vw, 72px)` | 900 | `-2px` |
|
|
87
|
+
| h2 | `clamp(28px, 3.2vw, 36px)` | 700 | `-0.8px` |
|
|
88
|
+
| h3 | `16px–20px` | 700 | `-0.3px` |
|
|
89
|
+
| Subtitle | `17px` | 300–400 | — |
|
|
90
|
+
| Body | `13px–14px` | 400 | — |
|
|
91
|
+
| Badge | `10px–11px` | 600–700 | `1.5px` |
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Use when creating, editing, or reviewing slide JSX components in a deck project. Enforces the mandatory slide skeleton, imports, and anti-patterns."
|
|
3
|
-
applyTo: "**/slides/**/*.jsx"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Slide JSX Conventions
|
|
7
|
-
|
|
8
|
-
## Imports
|
|
9
|
-
|
|
10
|
-
```jsx
|
|
11
|
-
import { BottomBar, Slide } from '@deckio/deck-engine'
|
|
12
|
-
import styles from './MySlide.module.css'
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Mandatory skeleton (in order inside `<Slide>`)
|
|
16
|
-
|
|
17
|
-
1. `<div className="accent-bar" />` — always first
|
|
18
|
-
2. 2–4 orbs: `<div className={\`orb ${styles.orbN}\`} />`
|
|
19
|
-
3. Content wrapper: `<div className={\`${styles.body} content-frame content-gutter\`}>` — all visible content here
|
|
20
|
-
4. `<BottomBar text="..." />` — always last child
|
|
21
|
-
|
|
22
|
-
## Props
|
|
23
|
-
|
|
24
|
-
Every slide receives `{ index, project, title, subtitle }`. Pass `index` to `<Slide>`.
|
|
25
|
-
|
|
26
|
-
## Available engine exports
|
|
27
|
-
|
|
28
|
-
`Slide`, `BottomBar`, `Navigation`, `SlideProvider`, `useSlides`, `GenericThankYouSlide`
|
|
29
|
-
|
|
30
|
-
## Anti-patterns
|
|
31
|
-
|
|
32
|
-
- Never omit `accent-bar`, `content-frame content-gutter`, or `BottomBar`
|
|
33
|
-
- Never use string paths for images — always `import logo from '../data/...'`
|
|
34
|
-
- Never hardcode slide indices — use `useSlides().goTo()` for navigation
|
|
1
|
+
---
|
|
2
|
+
description: "Use when creating, editing, or reviewing slide JSX components in a deck project. Enforces the mandatory slide skeleton, imports, and anti-patterns."
|
|
3
|
+
applyTo: "**/slides/**/*.jsx"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Slide JSX Conventions
|
|
7
|
+
|
|
8
|
+
## Imports
|
|
9
|
+
|
|
10
|
+
```jsx
|
|
11
|
+
import { BottomBar, Slide } from '@deckio/deck-engine'
|
|
12
|
+
import styles from './MySlide.module.css'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Mandatory skeleton (in order inside `<Slide>`)
|
|
16
|
+
|
|
17
|
+
1. `<div className="accent-bar" />` — always first
|
|
18
|
+
2. 2–4 orbs: `<div className={\`orb ${styles.orbN}\`} />`
|
|
19
|
+
3. Content wrapper: `<div className={\`${styles.body} content-frame content-gutter\`}>` — all visible content here
|
|
20
|
+
4. `<BottomBar text="..." />` — always last child
|
|
21
|
+
|
|
22
|
+
## Props
|
|
23
|
+
|
|
24
|
+
Every slide receives `{ index, project, title, subtitle }`. Pass `index` to `<Slide>`.
|
|
25
|
+
|
|
26
|
+
## Available engine exports
|
|
27
|
+
|
|
28
|
+
`Slide`, `BottomBar`, `Navigation`, `SlideProvider`, `useSlides`, `GenericThankYouSlide`
|
|
29
|
+
|
|
30
|
+
## Anti-patterns
|
|
31
|
+
|
|
32
|
+
- Never omit `accent-bar`, `content-frame content-gutter`, or `BottomBar`
|
|
33
|
+
- Never use string paths for images — always `import logo from '../data/...'`
|
|
34
|
+
- Never hardcode slide indices — use `useSlides().goTo()` for navigation
|
package/package.json
CHANGED
|
@@ -1,45 +1,49 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@deckio/deck-engine",
|
|
3
|
-
"version": "1.7.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"publishConfig": {
|
|
6
|
-
"registry": "https://registry.npmjs.org",
|
|
7
|
-
"access": "public"
|
|
8
|
-
},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "https://github.com/
|
|
12
|
-
"directory": "packages/deck-engine"
|
|
13
|
-
},
|
|
14
|
-
"main": "./index.js",
|
|
15
|
-
"module": "./index.js",
|
|
16
|
-
"exports": {
|
|
17
|
-
".": "./index.js",
|
|
18
|
-
"./slides/*": "./slides/*.jsx",
|
|
19
|
-
"./components/*": "./components/*.jsx",
|
|
20
|
-
"./styles/*": "./styles/*",
|
|
21
|
-
"./vite": "./vite.js"
|
|
22
|
-
},
|
|
23
|
-
"files": [
|
|
24
|
-
"index.js",
|
|
25
|
-
"vite.js",
|
|
26
|
-
"components",
|
|
27
|
-
"context",
|
|
28
|
-
"slides",
|
|
29
|
-
"styles",
|
|
30
|
-
"scripts",
|
|
31
|
-
"skills",
|
|
32
|
-
"instructions"
|
|
33
|
-
],
|
|
34
|
-
"sideEffects": [
|
|
35
|
-
"**/*.css"
|
|
36
|
-
],
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
},
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
}
|
|
45
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@deckio/deck-engine",
|
|
3
|
+
"version": "1.7.8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org",
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/deckio-art/deck-engine.git",
|
|
12
|
+
"directory": "packages/deck-engine"
|
|
13
|
+
},
|
|
14
|
+
"main": "./index.js",
|
|
15
|
+
"module": "./index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./index.js",
|
|
18
|
+
"./slides/*": "./slides/*.jsx",
|
|
19
|
+
"./components/*": "./components/*.jsx",
|
|
20
|
+
"./styles/*": "./styles/*",
|
|
21
|
+
"./vite": "./vite.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"index.js",
|
|
25
|
+
"vite.js",
|
|
26
|
+
"components",
|
|
27
|
+
"context",
|
|
28
|
+
"slides",
|
|
29
|
+
"styles",
|
|
30
|
+
"scripts",
|
|
31
|
+
"skills",
|
|
32
|
+
"instructions"
|
|
33
|
+
],
|
|
34
|
+
"sideEffects": [
|
|
35
|
+
"**/*.css"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"jspdf": "^3.0.3",
|
|
39
|
+
"modern-screenshot": "^4.6.8"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": "^19.1.0",
|
|
43
|
+
"react-dom": "^19.1.0"
|
|
44
|
+
},
|
|
45
|
+
"optionalDependencies": {
|
|
46
|
+
"puppeteer": "^24.38.0",
|
|
47
|
+
"puppeteer-core": "^24.38.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Capture a screenshot of the deck app via headless Edge.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* node scripts/capture-screen.mjs # project + port from state.md
|
|
7
|
-
* node scripts/capture-screen.mjs --project dev-plan # specific project
|
|
8
|
-
* node scripts/capture-screen.mjs --slide 3 # specific slide (1-based)
|
|
9
|
-
* node scripts/capture-screen.mjs --project dev-plan --slide 2 --label "cover-check"
|
|
10
|
-
*/
|
|
11
|
-
import puppeteer from 'puppeteer-core'
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync } from 'fs'
|
|
13
|
-
import path from 'path'
|
|
14
|
-
|
|
15
|
-
const EDGE = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'
|
|
16
|
-
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
17
|
-
const args = process.argv.slice(2)
|
|
18
|
-
const arg = (name, fb) => {
|
|
19
|
-
const i = args.indexOf(`--${name}`)
|
|
20
|
-
return i !== -1 && args[i + 1] ? args[i + 1] : fb
|
|
21
|
-
}
|
|
22
|
-
const numArg = (name, fb) => {
|
|
23
|
-
const v = arg(name, null)
|
|
24
|
-
return v != null ? Number(v) : fb
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const root = path.resolve(arg('root', process.cwd()))
|
|
28
|
-
const eyesDir = path.join(root, '.github', 'eyes')
|
|
29
|
-
const stateFile = path.join(root, '.github', 'memory', 'state.md')
|
|
30
|
-
|
|
31
|
-
const state = readState()
|
|
32
|
-
const PROJECT = arg('project', state.project || null)
|
|
33
|
-
const PORT = arg('port', state.port || '5175')
|
|
34
|
-
const SLIDE = numArg('slide', null)
|
|
35
|
-
const LABEL = arg('label', null)
|
|
36
|
-
const URL = `http://localhost:${PORT}/#/${PROJECT}`
|
|
37
|
-
|
|
38
|
-
if (!PROJECT) {
|
|
39
|
-
console.error('❌ No project. Use --project <id> or set it in .github/memory/state.md')
|
|
40
|
-
process.exit(1)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function readState() {
|
|
44
|
-
if (!existsSync(stateFile)) return {}
|
|
45
|
-
const c = readFileSync(stateFile, 'utf-8')
|
|
46
|
-
return {
|
|
47
|
-
project: c.match(/^project:\s*(.+)$/m)?.[1]?.trim() || '',
|
|
48
|
-
port: c.match(/^port:\s*(\d+)$/m)?.[1]?.trim() || '5175',
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function main() {
|
|
53
|
-
mkdirSync(eyesDir, { recursive: true })
|
|
54
|
-
console.log(`👁️ ${PROJECT} ${URL}${SLIDE ? ` slide ${SLIDE}` : ''}`)
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const response = await fetch(`http://localhost:${PORT}`, { signal: AbortSignal.timeout(3000) })
|
|
58
|
-
if (!response.ok) throw new Error('Dev server unavailable')
|
|
59
|
-
} catch {
|
|
60
|
-
console.error(`❌ Dev server not reachable on port ${PORT}`)
|
|
61
|
-
process.exit(1)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const browser = await puppeteer.launch({
|
|
65
|
-
executablePath: EDGE,
|
|
66
|
-
headless: 'new',
|
|
67
|
-
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
|
68
|
-
defaultViewport: { width: 1920, height: 1080, deviceScaleFactor: 2 },
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
const page = await browser.newPage()
|
|
72
|
-
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 15000 })
|
|
73
|
-
await page.waitForFunction(() => document.querySelectorAll('.slide').length > 0, { timeout: 10000 })
|
|
74
|
-
await sleep(1500)
|
|
75
|
-
|
|
76
|
-
if (SLIDE && SLIDE > 1) {
|
|
77
|
-
const total = await page.evaluate(() => document.querySelectorAll('.slide').length)
|
|
78
|
-
for (let i = 0; i < total; i++) {
|
|
79
|
-
await page.keyboard.press('ArrowLeft')
|
|
80
|
-
await sleep(50)
|
|
81
|
-
}
|
|
82
|
-
for (let i = 0; i < SLIDE - 1; i++) {
|
|
83
|
-
await page.keyboard.press('ArrowRight')
|
|
84
|
-
await sleep(100)
|
|
85
|
-
}
|
|
86
|
-
await sleep(800)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
await page.evaluate(() => {
|
|
90
|
-
document.querySelectorAll('[class*="nav"],[class*="Nav"],[class*="progress"],[class*="Progress"],[class*="hint"],[class*="Hint"],[class*="bottomBar"],[class*="BottomBar"]')
|
|
91
|
-
.forEach(el => {
|
|
92
|
-
if (el.tagName !== 'BODY' && el.tagName !== 'HTML') el.style.display = 'none'
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
await sleep(300)
|
|
96
|
-
|
|
97
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
98
|
-
const name = [PROJECT, SLIDE && `slide${SLIDE}`, LABEL, ts].filter(Boolean).join('-') + '.png'
|
|
99
|
-
const filepath = path.join(eyesDir, name)
|
|
100
|
-
await page.screenshot({ path: filepath, type: 'png' })
|
|
101
|
-
await browser.close()
|
|
102
|
-
|
|
103
|
-
const rel = path.relative(root, filepath).replace(/\\/g, '/')
|
|
104
|
-
console.log(`📸 ${rel}`)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
main().catch(err => {
|
|
108
|
-
console.error('❌', err?.message || err)
|
|
109
|
-
process.exit(1)
|
|
110
|
-
})
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Capture a screenshot of the deck app via headless Edge.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/capture-screen.mjs # project + port from state.md
|
|
7
|
+
* node scripts/capture-screen.mjs --project dev-plan # specific project
|
|
8
|
+
* node scripts/capture-screen.mjs --slide 3 # specific slide (1-based)
|
|
9
|
+
* node scripts/capture-screen.mjs --project dev-plan --slide 2 --label "cover-check"
|
|
10
|
+
*/
|
|
11
|
+
import puppeteer from 'puppeteer-core'
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync } from 'fs'
|
|
13
|
+
import path from 'path'
|
|
14
|
+
|
|
15
|
+
const EDGE = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'
|
|
16
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
17
|
+
const args = process.argv.slice(2)
|
|
18
|
+
const arg = (name, fb) => {
|
|
19
|
+
const i = args.indexOf(`--${name}`)
|
|
20
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : fb
|
|
21
|
+
}
|
|
22
|
+
const numArg = (name, fb) => {
|
|
23
|
+
const v = arg(name, null)
|
|
24
|
+
return v != null ? Number(v) : fb
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const root = path.resolve(arg('root', process.cwd()))
|
|
28
|
+
const eyesDir = path.join(root, '.github', 'eyes')
|
|
29
|
+
const stateFile = path.join(root, '.github', 'memory', 'state.md')
|
|
30
|
+
|
|
31
|
+
const state = readState()
|
|
32
|
+
const PROJECT = arg('project', state.project || null)
|
|
33
|
+
const PORT = arg('port', state.port || '5175')
|
|
34
|
+
const SLIDE = numArg('slide', null)
|
|
35
|
+
const LABEL = arg('label', null)
|
|
36
|
+
const URL = `http://localhost:${PORT}/#/${PROJECT}`
|
|
37
|
+
|
|
38
|
+
if (!PROJECT) {
|
|
39
|
+
console.error('❌ No project. Use --project <id> or set it in .github/memory/state.md')
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readState() {
|
|
44
|
+
if (!existsSync(stateFile)) return {}
|
|
45
|
+
const c = readFileSync(stateFile, 'utf-8')
|
|
46
|
+
return {
|
|
47
|
+
project: c.match(/^project:\s*(.+)$/m)?.[1]?.trim() || '',
|
|
48
|
+
port: c.match(/^port:\s*(\d+)$/m)?.[1]?.trim() || '5175',
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
mkdirSync(eyesDir, { recursive: true })
|
|
54
|
+
console.log(`👁️ ${PROJECT} ${URL}${SLIDE ? ` slide ${SLIDE}` : ''}`)
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const response = await fetch(`http://localhost:${PORT}`, { signal: AbortSignal.timeout(3000) })
|
|
58
|
+
if (!response.ok) throw new Error('Dev server unavailable')
|
|
59
|
+
} catch {
|
|
60
|
+
console.error(`❌ Dev server not reachable on port ${PORT}`)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const browser = await puppeteer.launch({
|
|
65
|
+
executablePath: EDGE,
|
|
66
|
+
headless: 'new',
|
|
67
|
+
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
|
68
|
+
defaultViewport: { width: 1920, height: 1080, deviceScaleFactor: 2 },
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const page = await browser.newPage()
|
|
72
|
+
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 15000 })
|
|
73
|
+
await page.waitForFunction(() => document.querySelectorAll('.slide').length > 0, { timeout: 10000 })
|
|
74
|
+
await sleep(1500)
|
|
75
|
+
|
|
76
|
+
if (SLIDE && SLIDE > 1) {
|
|
77
|
+
const total = await page.evaluate(() => document.querySelectorAll('.slide').length)
|
|
78
|
+
for (let i = 0; i < total; i++) {
|
|
79
|
+
await page.keyboard.press('ArrowLeft')
|
|
80
|
+
await sleep(50)
|
|
81
|
+
}
|
|
82
|
+
for (let i = 0; i < SLIDE - 1; i++) {
|
|
83
|
+
await page.keyboard.press('ArrowRight')
|
|
84
|
+
await sleep(100)
|
|
85
|
+
}
|
|
86
|
+
await sleep(800)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await page.evaluate(() => {
|
|
90
|
+
document.querySelectorAll('[class*="nav"],[class*="Nav"],[class*="progress"],[class*="Progress"],[class*="hint"],[class*="Hint"],[class*="bottomBar"],[class*="BottomBar"]')
|
|
91
|
+
.forEach(el => {
|
|
92
|
+
if (el.tagName !== 'BODY' && el.tagName !== 'HTML') el.style.display = 'none'
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
await sleep(300)
|
|
96
|
+
|
|
97
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
98
|
+
const name = [PROJECT, SLIDE && `slide${SLIDE}`, LABEL, ts].filter(Boolean).join('-') + '.png'
|
|
99
|
+
const filepath = path.join(eyesDir, name)
|
|
100
|
+
await page.screenshot({ path: filepath, type: 'png' })
|
|
101
|
+
await browser.close()
|
|
102
|
+
|
|
103
|
+
const rel = path.relative(root, filepath).replace(/\\/g, '/')
|
|
104
|
+
console.log(`📸 ${rel}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main().catch(err => {
|
|
108
|
+
console.error('❌', err?.message || err)
|
|
109
|
+
process.exit(1)
|
|
110
|
+
})
|