voxflow 1.15.5 → 1.16.0
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/README.md +2 -2
- package/dist/index.js +1 -1
- package/lib/commands/skills.js +3 -3
- package/lib/commands/slice-fork.js +4 -4
- package/lib/commands/slice.js +1 -1
- package/package.json +1 -1
- package/skills/.claude-plugin/marketplace.json +2 -2
- package/skills/.claude-plugin/plugin.json +3 -2
- package/skills/README.md +9 -7
- package/skills/card/SKILL.md +410 -0
- package/skills/card/references/design-languages/cinematic-still.md +57 -0
- package/skills/card/references/design-languages/data-poster.md +57 -0
- package/skills/card/references/design-languages/editorial-artifact.md +58 -0
- package/skills/card/references/design-languages/field-notes.md +58 -0
- package/skills/card/references/design-languages/image-led-magazine.md +57 -0
- package/skills/card/references/design-languages/newsroom-poster.md +58 -0
- package/skills/card/references/design-languages/product-catalog.md +57 -0
- package/skills/card/references/design-languages/swiss-poster.md +60 -0
- package/skills/card/references/design-languages.md +166 -0
- package/skills/card/references/layouts/card-layouts.md +268 -0
- package/skills/card/references/magazine-card-adaptations.md +154 -0
- package/skills/card/references/taste.md +121 -0
- package/skills/card/references/themes/presets.md +314 -0
- package/skills/card/scripts/render-cards.mjs +216 -0
- package/skills/voxflow-slice/SKILL.md +0 -415
- package/skills/voxflow-slice/examples/article.md +0 -13
- package/skills/voxflow-slice/examples/expected-deck.json +0 -39
- package/skills/voxflow-slice/examples/validate.mjs +0 -46
- /package/skills/{voxflow-slice → slice}/templates/data-finding/deck.json +0 -0
- /package/skills/{voxflow-slice → slice}/templates/founder-lesson/deck.json +0 -0
- /package/skills/{voxflow-slice → slice}/templates/incident-review/deck.json +0 -0
- /package/skills/{voxflow-slice → slice}/templates/manifest.json +0 -0
- /package/skills/{voxflow-slice → slice}/templates/product-launch/deck.json +0 -0
- /package/skills/{voxflow-slice → slice}/templates/quiet-essay/deck.json +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Theme Presets
|
|
2
|
+
|
|
3
|
+
Use these presets as palette and composition starting points for card sets. The default taste should be closer to magazines, reports, field notes, product manuals, and printed matter than to generic AI SaaS visuals.
|
|
4
|
+
|
|
5
|
+
## Anti-AI-Template Rule
|
|
6
|
+
|
|
7
|
+
When style is ambiguous, avoid the usual AI-card signals:
|
|
8
|
+
|
|
9
|
+
- neon blue/purple gradients, aurora blobs, glowing dark dashboards, terminal green, cyberpunk grids, fake glass panels, and excessive black backgrounds.
|
|
10
|
+
- generic "AI future" imagery, robot hands, abstract brain networks, floating chips, holographic UI, and meaningless data particles.
|
|
11
|
+
- one-note blue/purple palettes or pitch-deck gradients unless the user explicitly asks for them.
|
|
12
|
+
|
|
13
|
+
Prefer restrained paper color, readable ink, real editorial hierarchy, purposeful whitespace, and one or two quiet accent colors.
|
|
14
|
+
|
|
15
|
+
## Anti-Plain-Template Rule
|
|
16
|
+
|
|
17
|
+
Removing AI cliches is not enough. Do not replace them with a plain system of hairlines, coordinate grids, and repeated boxes.
|
|
18
|
+
|
|
19
|
+
- Do not use a full-card grid background as the default texture.
|
|
20
|
+
- Do not make every content item a bordered rectangle.
|
|
21
|
+
- Do not rely on thin rules and small mono labels as the main source of "design".
|
|
22
|
+
- Prefer large filled fields, cropped objects, image-led composition, typographic scale contrast, torn-paper shapes, stamps, labels, bands, and asymmetric layouts.
|
|
23
|
+
- Use lines and borders sparingly, as accents around a stronger visual anchor.
|
|
24
|
+
|
|
25
|
+
## Token Model
|
|
26
|
+
|
|
27
|
+
Every card set should define a compact theme token block before component styling:
|
|
28
|
+
|
|
29
|
+
- `--bg`, `--bg-soft`
|
|
30
|
+
- `--surface`, `--surface-2`
|
|
31
|
+
- `--border`, `--border-strong`
|
|
32
|
+
- `--text-1`, `--text-2`, `--text-3`
|
|
33
|
+
- `--accent`, `--accent-2`, `--accent-3`
|
|
34
|
+
- `--good`, `--warn`, `--bad`
|
|
35
|
+
- `--grad`, `--grad-soft`
|
|
36
|
+
- `--radius`, `--radius-sm`, `--radius-lg`
|
|
37
|
+
- `--shadow`, `--shadow-lg`
|
|
38
|
+
- `--font-sans`, `--font-serif`, `--font-mono`, `--font-display`
|
|
39
|
+
|
|
40
|
+
Use tokens in component CSS. Avoid scattering literal colors outside the token block unless a one-off asset or data visualization genuinely requires it.
|
|
41
|
+
|
|
42
|
+
## Primary Preset Catalog
|
|
43
|
+
|
|
44
|
+
### `magazine-eink`
|
|
45
|
+
|
|
46
|
+
Ink-black on warm paper, serif/display headlines, sans-serif body, mono metadata, minimal borders.
|
|
47
|
+
|
|
48
|
+
Use for editorial carousels, talk summaries, longform explainers, essays, culture, creator narratives, and premium text-led cards.
|
|
49
|
+
|
|
50
|
+
Suggested tokens:
|
|
51
|
+
|
|
52
|
+
- `--bg: #f1efea`
|
|
53
|
+
- `--bg-soft: #e8e5de`
|
|
54
|
+
- `--surface: #fbfaf6`
|
|
55
|
+
- `--surface-2: #f4f1ea`
|
|
56
|
+
- `--border: rgba(10, 10, 11, 0.18)`
|
|
57
|
+
- `--border-strong: #0a0a0b`
|
|
58
|
+
- `--text-1: #0a0a0b`
|
|
59
|
+
- `--text-2: #3f3d39`
|
|
60
|
+
- `--text-3: #777169`
|
|
61
|
+
- `--accent: #18181a`
|
|
62
|
+
- `--accent-2: #8f5b34`
|
|
63
|
+
- `--accent-3: #b8aa8c`
|
|
64
|
+
|
|
65
|
+
### `porcelain-research`
|
|
66
|
+
|
|
67
|
+
Deep indigo ink on porcelain white, cool but not neon, with precise rules and quiet contrast.
|
|
68
|
+
|
|
69
|
+
Use for technology, research, data analysis, academic-feeling explainers, serious product narratives, and technical topics that should not look like a hacker poster.
|
|
70
|
+
|
|
71
|
+
Suggested tokens:
|
|
72
|
+
|
|
73
|
+
- `--bg: #f1f3f5`
|
|
74
|
+
- `--bg-soft: #e4e8ec`
|
|
75
|
+
- `--surface: #fbfcfd`
|
|
76
|
+
- `--surface-2: #edf2f6`
|
|
77
|
+
- `--border: rgba(10, 31, 61, 0.18)`
|
|
78
|
+
- `--border-strong: #0a1f3d`
|
|
79
|
+
- `--text-1: #0a1f3d`
|
|
80
|
+
- `--text-2: #3b526d`
|
|
81
|
+
- `--text-3: #76879a`
|
|
82
|
+
- `--accent: #174f9a`
|
|
83
|
+
- `--accent-2: #6f8fb4`
|
|
84
|
+
- `--accent-3: #c2a35f`
|
|
85
|
+
|
|
86
|
+
### `newsroom-paper`
|
|
87
|
+
|
|
88
|
+
White and light gray editorial base, black text, measured red accent, hard information blocks.
|
|
89
|
+
|
|
90
|
+
Use for news, incidents, releases, comparisons, urgency, public statements, and factual explainers.
|
|
91
|
+
|
|
92
|
+
Suggested tokens:
|
|
93
|
+
|
|
94
|
+
- `--bg: #f7f7f5`
|
|
95
|
+
- `--bg-soft: #ececea`
|
|
96
|
+
- `--surface: #ffffff`
|
|
97
|
+
- `--surface-2: #f0f0ee`
|
|
98
|
+
- `--border: rgba(20, 22, 24, 0.16)`
|
|
99
|
+
- `--border-strong: #141618`
|
|
100
|
+
- `--text-1: #141618`
|
|
101
|
+
- `--text-2: #4b5055`
|
|
102
|
+
- `--text-3: #7b8085`
|
|
103
|
+
- `--accent: #c9342c`
|
|
104
|
+
- `--accent-2: #2f5f86`
|
|
105
|
+
- `--accent-3: #c9a451`
|
|
106
|
+
|
|
107
|
+
### `quiet-report`
|
|
108
|
+
|
|
109
|
+
Clean report style with off-white background, charcoal/navy text, modest blue accent, and conservative spacing.
|
|
110
|
+
|
|
111
|
+
Use for B2B, operations, finance, board updates, enterprise product messaging, and formal summaries.
|
|
112
|
+
|
|
113
|
+
Suggested tokens:
|
|
114
|
+
|
|
115
|
+
- `--bg: #f6f7f4`
|
|
116
|
+
- `--bg-soft: #ebeee9`
|
|
117
|
+
- `--surface: #ffffff`
|
|
118
|
+
- `--surface-2: #eef2f3`
|
|
119
|
+
- `--border: rgba(26, 39, 52, 0.15)`
|
|
120
|
+
- `--border-strong: #1a2734`
|
|
121
|
+
- `--text-1: #1a2734`
|
|
122
|
+
- `--text-2: #4a5a66`
|
|
123
|
+
- `--text-3: #7d8a92`
|
|
124
|
+
- `--accent: #2d647f`
|
|
125
|
+
- `--accent-2: #7d8b68`
|
|
126
|
+
- `--accent-3: #c0a062`
|
|
127
|
+
|
|
128
|
+
### `product-manual`
|
|
129
|
+
|
|
130
|
+
Instruction-manual feel: warm white, graphite text, practical diagrams, object labels, stamps, and large artifact metaphors.
|
|
131
|
+
|
|
132
|
+
Use for product notes, how-to guides, API explainers, workflows, checklists, and developer education.
|
|
133
|
+
|
|
134
|
+
Suggested tokens:
|
|
135
|
+
|
|
136
|
+
- `--bg: #f4f1ea`
|
|
137
|
+
- `--bg-soft: #e7e2d8`
|
|
138
|
+
- `--surface: #fffdf8`
|
|
139
|
+
- `--surface-2: #ece8df`
|
|
140
|
+
- `--border: rgba(32, 38, 42, 0.18)`
|
|
141
|
+
- `--border-strong: #20262a`
|
|
142
|
+
- `--text-1: #20262a`
|
|
143
|
+
- `--text-2: #4e565b`
|
|
144
|
+
- `--text-3: #7f8587`
|
|
145
|
+
- `--accent: #3b6f75`
|
|
146
|
+
- `--accent-2: #9a6b37`
|
|
147
|
+
- `--accent-3: #c7b474`
|
|
148
|
+
|
|
149
|
+
### `engineering-paper`
|
|
150
|
+
|
|
151
|
+
White engineering paper, navy ink, sparse mono labels, precise diagrams, and strong technical artifacts. Do not default to full-canvas coordinate grids.
|
|
152
|
+
|
|
153
|
+
Use for architecture, system design, infrastructure, API flows, technical documentation, and engineering explainers.
|
|
154
|
+
|
|
155
|
+
Suggested tokens:
|
|
156
|
+
|
|
157
|
+
- `--bg: #f7f8f3`
|
|
158
|
+
- `--bg-soft: #eef3f6`
|
|
159
|
+
- `--surface: #ffffff`
|
|
160
|
+
- `--surface-2: #edf3f8`
|
|
161
|
+
- `--border: rgba(17, 36, 58, 0.16)`
|
|
162
|
+
- `--border-strong: #11243a`
|
|
163
|
+
- `--text-1: #102033`
|
|
164
|
+
- `--text-2: #3f566b`
|
|
165
|
+
- `--text-3: #708292`
|
|
166
|
+
- `--accent: #1468a8`
|
|
167
|
+
- `--accent-2: #8a7044`
|
|
168
|
+
- `--accent-3: #c9a24e`
|
|
169
|
+
|
|
170
|
+
### `field-notes`
|
|
171
|
+
|
|
172
|
+
Ivory paper, forest ink, muted green and clay accents, calm non-fiction tone.
|
|
173
|
+
|
|
174
|
+
Use for sustainability, culture, public-interest explainers, field research, community, and reflective narratives.
|
|
175
|
+
|
|
176
|
+
Suggested tokens:
|
|
177
|
+
|
|
178
|
+
- `--bg: #f5f1e8`
|
|
179
|
+
- `--bg-soft: #ece7da`
|
|
180
|
+
- `--surface: #fffaf0`
|
|
181
|
+
- `--surface-2: #eee8d9`
|
|
182
|
+
- `--border: rgba(26, 46, 31, 0.18)`
|
|
183
|
+
- `--border-strong: #1a2e1f`
|
|
184
|
+
- `--text-1: #1a2e1f`
|
|
185
|
+
- `--text-2: #485849`
|
|
186
|
+
- `--text-3: #7f8979`
|
|
187
|
+
- `--accent: #2f6d4f`
|
|
188
|
+
- `--accent-2: #8b6b3e`
|
|
189
|
+
- `--accent-3: #c9b276`
|
|
190
|
+
|
|
191
|
+
### `kraft-editorial`
|
|
192
|
+
|
|
193
|
+
Warm kraft paper, dark brown ink, archival texture, independent-magazine warmth.
|
|
194
|
+
|
|
195
|
+
Use for history, reading notes, handmade brands, personal essays, retro product stories, and literary cards.
|
|
196
|
+
|
|
197
|
+
Suggested tokens:
|
|
198
|
+
|
|
199
|
+
- `--bg: #eedfc7`
|
|
200
|
+
- `--bg-soft: #e0d0b6`
|
|
201
|
+
- `--surface: #f8ecd8`
|
|
202
|
+
- `--surface-2: #ead8bb`
|
|
203
|
+
- `--border: rgba(42, 30, 19, 0.2)`
|
|
204
|
+
- `--border-strong: #2a1e13`
|
|
205
|
+
- `--text-1: #2a1e13`
|
|
206
|
+
- `--text-2: #604c38`
|
|
207
|
+
- `--text-3: #8e7b64`
|
|
208
|
+
- `--accent: #8f4b28`
|
|
209
|
+
- `--accent-2: #2f5d50`
|
|
210
|
+
- `--accent-3: #b9894a`
|
|
211
|
+
|
|
212
|
+
### `dune-gallery`
|
|
213
|
+
|
|
214
|
+
Charcoal ink on sand paper, restrained gallery tone, neutral premium feel.
|
|
215
|
+
|
|
216
|
+
Use for design, art, architecture, brand studies, portfolio stories, and aesthetic-first cards.
|
|
217
|
+
|
|
218
|
+
Suggested tokens:
|
|
219
|
+
|
|
220
|
+
- `--bg: #f0e6d2`
|
|
221
|
+
- `--bg-soft: #e3d7bf`
|
|
222
|
+
- `--surface: #fbf3e3`
|
|
223
|
+
- `--surface-2: #eadcc4`
|
|
224
|
+
- `--border: rgba(31, 26, 20, 0.18)`
|
|
225
|
+
- `--border-strong: #1f1a14`
|
|
226
|
+
- `--text-1: #1f1a14`
|
|
227
|
+
- `--text-2: #51483d`
|
|
228
|
+
- `--text-3: #817768`
|
|
229
|
+
- `--accent: #6f6255`
|
|
230
|
+
- `--accent-2: #a06445`
|
|
231
|
+
- `--accent-3: #c9b171`
|
|
232
|
+
|
|
233
|
+
### `social-notebook`
|
|
234
|
+
|
|
235
|
+
Warm white, soft red/clay accents, light paper shadows, approachable but still editorial.
|
|
236
|
+
|
|
237
|
+
Use for Xiaohongshu, creator notes, consumer product tips, lightweight education, and friendly explainers.
|
|
238
|
+
|
|
239
|
+
Suggested tokens:
|
|
240
|
+
|
|
241
|
+
- `--bg: #fbf7f2`
|
|
242
|
+
- `--bg-soft: #f2e9e2`
|
|
243
|
+
- `--surface: #ffffff`
|
|
244
|
+
- `--surface-2: #fff2ef`
|
|
245
|
+
- `--border: rgba(86, 54, 46, 0.14)`
|
|
246
|
+
- `--border-strong: #56362e`
|
|
247
|
+
- `--text-1: #302421`
|
|
248
|
+
- `--text-2: #6a5650`
|
|
249
|
+
- `--text-3: #9a8982`
|
|
250
|
+
- `--accent: #c75f55`
|
|
251
|
+
- `--accent-2: #7a8b62`
|
|
252
|
+
- `--accent-3: #d7b77a`
|
|
253
|
+
|
|
254
|
+
### `bold-editorial`
|
|
255
|
+
|
|
256
|
+
Cream paper, black editorial type, one warm spot color, strong cover energy without synthetic gradients.
|
|
257
|
+
|
|
258
|
+
Use for opinion pieces, columns, hot takes, cultural topics, strong hooks, and shareable argument cards.
|
|
259
|
+
|
|
260
|
+
Suggested tokens:
|
|
261
|
+
|
|
262
|
+
- `--bg: #f3eadb`
|
|
263
|
+
- `--bg-soft: #e7dac7`
|
|
264
|
+
- `--surface: #fff7ea`
|
|
265
|
+
- `--surface-2: #eadcc8`
|
|
266
|
+
- `--border: rgba(15, 14, 13, 0.22)`
|
|
267
|
+
- `--border-strong: #0f0e0d`
|
|
268
|
+
- `--text-1: #0f0e0d`
|
|
269
|
+
- `--text-2: #4b4037`
|
|
270
|
+
- `--text-3: #807266`
|
|
271
|
+
- `--accent: #d65f2d`
|
|
272
|
+
- `--accent-2: #2f5d6b`
|
|
273
|
+
- `--accent-3: #d5aa4b`
|
|
274
|
+
|
|
275
|
+
## Legacy Motifs
|
|
276
|
+
|
|
277
|
+
These names may appear in older card sets. Do not recommend them by default because they often create a generic AI-template feel.
|
|
278
|
+
|
|
279
|
+
- `tokyo-night`: only use if the user explicitly asks for a dark code-editor mood.
|
|
280
|
+
- `terminal-green`: only use for command-reference cards or intentionally retro terminal artifacts.
|
|
281
|
+
- `neo-brutalism`: only use if the user explicitly wants a loud youth-campaign look.
|
|
282
|
+
- `blueprint`: prefer `engineering-paper` unless the user asks for a dark blueprint.
|
|
283
|
+
- `pitch-deck-vc`: prefer `quiet-report` or `product-manual` unless the user asks for startup deck styling.
|
|
284
|
+
- `soft-pastel`: prefer `social-notebook` unless the user asks for soft consumer colors.
|
|
285
|
+
- `xiaohongshu-white`: prefer `social-notebook`; keep this as a platform-specific alias.
|
|
286
|
+
- `corporate-clean`: prefer `quiet-report`; keep this as a conservative-report alias.
|
|
287
|
+
- `news-broadcast`: prefer `newsroom-paper`; keep this as a higher-urgency alias.
|
|
288
|
+
- `engineering-whiteprint`: prefer `engineering-paper`; keep this as a technical-paper alias.
|
|
289
|
+
- `magazine-bold`: prefer `bold-editorial`; keep this as an older alias.
|
|
290
|
+
- `editorial-serif`: prefer `magazine-eink`; keep this as an older alias.
|
|
291
|
+
|
|
292
|
+
## Scenario Mapping
|
|
293
|
+
|
|
294
|
+
When the style is ambiguous, recommend 2-3 user-facing directions and map them internally to presets:
|
|
295
|
+
|
|
296
|
+
- Business/report: `quiet-report`, `product-manual`, `porcelain-research`.
|
|
297
|
+
- Engineering/API/system: `engineering-paper`, `product-manual`, `porcelain-research`.
|
|
298
|
+
- Social/Xiaohongshu: `social-notebook`, `magazine-eink`, `bold-editorial`.
|
|
299
|
+
- News/update/incident: `newsroom-paper`, `quiet-report`, `engineering-paper`.
|
|
300
|
+
- Bold opinion: `bold-editorial`, `magazine-eink`, `newsroom-paper`.
|
|
301
|
+
- Editorial/magazine: `magazine-eink`, `dune-gallery`, `field-notes`, `kraft-editorial`.
|
|
302
|
+
- Design/art/brand: `dune-gallery`, `magazine-eink`, `bold-editorial`.
|
|
303
|
+
- Culture/history/reading: `kraft-editorial`, `field-notes`, `magazine-eink`.
|
|
304
|
+
- Technical CLI/security: `engineering-paper`, `product-manual`, `newsroom-paper`; use `terminal-green` only on explicit request.
|
|
305
|
+
|
|
306
|
+
## Guardrails
|
|
307
|
+
|
|
308
|
+
- Keep letter spacing at `0` unless uppercase micro-label tracking is necessary. Do not use negative letter spacing.
|
|
309
|
+
- Avoid one-note palettes unless the user explicitly asks for a monochrome look.
|
|
310
|
+
- Use dark themes sparingly. If a card needs dark contrast, make it feel like black ink, night photography, or print inversion rather than neon code wallpaper.
|
|
311
|
+
- For technical topics, prefer `engineering-paper`, `product-manual`, or `porcelain-research` before any dark developer motif.
|
|
312
|
+
- Prefer texture through typography, cropped shapes, stamps, labels, image/object anchors, captions, paper color, and real images instead of abstract glowing decoration.
|
|
313
|
+
- Do not make fine-line grids, rectangular panels, or hairline diagrams the primary visual language unless the user explicitly asks for blueprint/spec-sheet aesthetics.
|
|
314
|
+
- For final exports, keep source inspiration in `sources.md` or `credits.md` when relevant.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const RATIOS = {
|
|
7
|
+
'1:1': { width: 1080, height: 1080 },
|
|
8
|
+
'3:4': { width: 1080, height: 1440 },
|
|
9
|
+
'4:5': { width: 1080, height: 1350 },
|
|
10
|
+
'9:16': { width: 1080, height: 1920 },
|
|
11
|
+
'16:9': { width: 1920, height: 1080 },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function usage() {
|
|
15
|
+
console.log(`Usage:
|
|
16
|
+
node render-cards.mjs --input <html-dir> --output <png-dir> [--ratio 1:1|3:4|9:16] [--size 1080x1920] [--selector .card] [--scale 2]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--input Directory containing card-*.html files, or any .html files.
|
|
20
|
+
--output Directory for PNG exports and audit.json.
|
|
21
|
+
--ratio Aspect ratio. Defaults to 1:1. Supports known ratios or custom a:b.
|
|
22
|
+
--size Exact viewport size in WIDTHxHEIGHT. Overrides --ratio.
|
|
23
|
+
--selector Element to screenshot when present. Defaults to .card.
|
|
24
|
+
--scale deviceScaleFactor. Defaults to 2 for retina-quality exports.
|
|
25
|
+
--wait Milliseconds to wait after load. Defaults to 250.
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const args = {
|
|
31
|
+
ratio: '1:1',
|
|
32
|
+
selector: '.card',
|
|
33
|
+
scale: 2,
|
|
34
|
+
wait: 250,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
38
|
+
const token = argv[i];
|
|
39
|
+
if (token === '--help' || token === '-h') {
|
|
40
|
+
args.help = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!token.startsWith('--')) {
|
|
44
|
+
throw new Error(`Unexpected argument: ${token}`);
|
|
45
|
+
}
|
|
46
|
+
const key = token.slice(2);
|
|
47
|
+
const value = argv[i + 1];
|
|
48
|
+
if (!value || value.startsWith('--')) {
|
|
49
|
+
throw new Error(`Missing value for ${token}`);
|
|
50
|
+
}
|
|
51
|
+
args[key] = value;
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return args;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function viewportFromArgs(args) {
|
|
59
|
+
if (args.size) {
|
|
60
|
+
const match = String(args.size).match(/^(\d+)x(\d+)$/i);
|
|
61
|
+
if (!match) throw new Error('--size must be WIDTHxHEIGHT, for example 1080x1920');
|
|
62
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (RATIOS[args.ratio]) return RATIOS[args.ratio];
|
|
66
|
+
|
|
67
|
+
const match = String(args.ratio).match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
|
|
68
|
+
if (!match) throw new Error(`Unsupported ratio: ${args.ratio}`);
|
|
69
|
+
|
|
70
|
+
const a = Number(match[1]);
|
|
71
|
+
const b = Number(match[2]);
|
|
72
|
+
if (a <= 0 || b <= 0) throw new Error(`Invalid ratio: ${args.ratio}`);
|
|
73
|
+
|
|
74
|
+
const width = 1080;
|
|
75
|
+
return { width, height: Math.round((width * b) / a) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function htmlFiles(inputDir) {
|
|
79
|
+
const entries = await readdir(inputDir, { withFileTypes: true });
|
|
80
|
+
return entries
|
|
81
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.html'))
|
|
82
|
+
.map((entry) => path.join(inputDir, entry.name))
|
|
83
|
+
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function auditPage(page) {
|
|
87
|
+
return page.evaluate(() => {
|
|
88
|
+
const viewport = {
|
|
89
|
+
width: window.innerWidth,
|
|
90
|
+
height: window.innerHeight,
|
|
91
|
+
scrollWidth: document.documentElement.scrollWidth,
|
|
92
|
+
scrollHeight: document.documentElement.scrollHeight,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const visibleElements = Array.from(document.querySelectorAll('body *')).filter((element) => {
|
|
96
|
+
const style = window.getComputedStyle(element);
|
|
97
|
+
const rect = element.getBoundingClientRect();
|
|
98
|
+
return style.visibility !== 'hidden' && style.display !== 'none' && rect.width > 0 && rect.height > 0;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const outOfBounds = [];
|
|
102
|
+
const overflow = [];
|
|
103
|
+
|
|
104
|
+
for (const element of visibleElements) {
|
|
105
|
+
const rect = element.getBoundingClientRect();
|
|
106
|
+
const label = element.id ? `#${element.id}` : element.className ? `.${String(element.className).trim().split(/\s+/).join('.')}` : element.tagName.toLowerCase();
|
|
107
|
+
|
|
108
|
+
if (rect.left < -1 || rect.top < -1 || rect.right > window.innerWidth + 1 || rect.bottom > window.innerHeight + 1) {
|
|
109
|
+
outOfBounds.push({
|
|
110
|
+
element: label,
|
|
111
|
+
rect: {
|
|
112
|
+
left: Math.round(rect.left),
|
|
113
|
+
top: Math.round(rect.top),
|
|
114
|
+
right: Math.round(rect.right),
|
|
115
|
+
bottom: Math.round(rect.bottom),
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (element.scrollWidth > element.clientWidth + 1 || element.scrollHeight > element.clientHeight + 1) {
|
|
121
|
+
overflow.push({
|
|
122
|
+
element: label,
|
|
123
|
+
scrollWidth: element.scrollWidth,
|
|
124
|
+
clientWidth: element.clientWidth,
|
|
125
|
+
scrollHeight: element.scrollHeight,
|
|
126
|
+
clientHeight: element.clientHeight,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
viewport,
|
|
133
|
+
hasHorizontalScroll: viewport.scrollWidth > viewport.width + 1,
|
|
134
|
+
hasVerticalScroll: viewport.scrollHeight > viewport.height + 1,
|
|
135
|
+
outOfBounds,
|
|
136
|
+
overflow,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function main() {
|
|
142
|
+
const args = parseArgs(process.argv.slice(2));
|
|
143
|
+
if (args.help) {
|
|
144
|
+
usage();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!args.input || !args.output) {
|
|
149
|
+
usage();
|
|
150
|
+
throw new Error('--input and --output are required');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const viewport = viewportFromArgs(args);
|
|
154
|
+
const inputDir = path.resolve(args.input);
|
|
155
|
+
const outputDir = path.resolve(args.output);
|
|
156
|
+
const files = await htmlFiles(inputDir);
|
|
157
|
+
if (files.length === 0) throw new Error(`No .html files found in ${inputDir}`);
|
|
158
|
+
|
|
159
|
+
let chromium;
|
|
160
|
+
try {
|
|
161
|
+
({ chromium } = await import('playwright'));
|
|
162
|
+
} catch {
|
|
163
|
+
throw new Error('Playwright is not installed. Run: npm install -D playwright && npx playwright install chromium');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await mkdir(outputDir, { recursive: true });
|
|
167
|
+
|
|
168
|
+
const browser = await chromium.launch();
|
|
169
|
+
const audit = [];
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
173
|
+
const file = files[index];
|
|
174
|
+
const page = await browser.newPage({
|
|
175
|
+
viewport,
|
|
176
|
+
deviceScaleFactor: Number(args.scale),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await page.goto(pathToFileURL(file).href, { waitUntil: 'load' });
|
|
180
|
+
await page.waitForTimeout(Number(args.wait));
|
|
181
|
+
|
|
182
|
+
const handle = await page.$(args.selector);
|
|
183
|
+
const baseName = path.basename(file, path.extname(file));
|
|
184
|
+
const outputPath = path.join(outputDir, `${baseName}.png`);
|
|
185
|
+
|
|
186
|
+
if (handle) {
|
|
187
|
+
await handle.screenshot({ path: outputPath });
|
|
188
|
+
} else {
|
|
189
|
+
await page.screenshot({ path: outputPath, fullPage: false });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const pageAudit = await auditPage(page);
|
|
193
|
+
audit.push({
|
|
194
|
+
file,
|
|
195
|
+
output: outputPath,
|
|
196
|
+
selector: handle ? args.selector : null,
|
|
197
|
+
viewport,
|
|
198
|
+
...pageAudit,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await page.close();
|
|
202
|
+
console.log(`Rendered ${index + 1}/${files.length}: ${outputPath}`);
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
await browser.close();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const auditPath = path.join(outputDir, 'audit.json');
|
|
209
|
+
await writeFile(auditPath, `${JSON.stringify(audit, null, 2)}\n`);
|
|
210
|
+
console.log(`Audit: ${auditPath}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main().catch((error) => {
|
|
214
|
+
console.error(error.message);
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
});
|