Skip to content

Commit 76ea4ca

Browse files
omachalaOndrej Machala
andauthored
feat: add snippet command for GitHub README/Wiki integration (#32)
## Summary - Add `heroshot snippet [pattern]` command for generating markdown/HTML snippets - Pattern matches against screenshot `id` and `name` (case-insensitive) - Generates `<picture>` elements with `prefers-color-scheme` media queries for light/dark mode support - Supports custom path prefix via `--path-prefix` flag ## Usage ```bash # Generate snippets for all screenshots heroshot snippet # Filter by pattern (matches id or name) heroshot snippet dashboard # Custom image path prefix heroshot snippet --path-prefix ./images/screenshots/ ``` ## Example Output ```html <!-- heroshot: Dashboard (abc123) --> <picture> <source srcset="./heroshots/dashboard-dark.png" media="(prefers-color-scheme: dark)"> <img src="./heroshots/dashboard-light.png" alt="Dashboard"> </picture> ``` ## Test plan - [x] Unit tests for snippet generation logic (12 tests) - [x] CLI integration tests (8 tests) - [x] Lint passes - [x] TypeScript compiles --------- Co-authored-by: Ondrej Machala <[email protected]>
1 parent 1e83923 commit 76ea4ca

File tree

7 files changed

+530
-0
lines changed

7 files changed

+530
-0
lines changed

.changeset/snippet-command.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'heroshot': minor
3+
---
4+
5+
Add `heroshot snippet [pattern]` command for generating markdown/HTML snippets for GitHub README and Wiki integration. Generates `<picture>` elements with `prefers-color-scheme` media queries for automatic light/dark mode support.

docs/docs/cli.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,43 @@ Or set it in your config to use by default:
211211
Each worker captures a portion of your screenshots concurrently. More workers = faster captures, but requires more system resources. Start with 2-4 and adjust based on your machine.
212212
:::
213213

214+
### `heroshot snippet [pattern]`
215+
216+
Generate `<picture>` HTML snippets for use in GitHub READMEs, wikis, or any plain Markdown file. This saves you from manually writing the light/dark mode markup.
217+
218+
```bash
219+
heroshot snippet # Generate snippets for all screenshots
220+
heroshot snippet dashboard # Only screenshots matching "dashboard"
221+
heroshot snippet --path-prefix ./img/ # Custom image path prefix
222+
```
223+
224+
| Option | Description |
225+
| ------------------------ | ------------------------------------------- |
226+
| `--path-prefix <prefix>` | Image path prefix (default: `./heroshots/`) |
227+
228+
The pattern matches against screenshot **id** and **name** (case-insensitive). More matches = more snippets output.
229+
230+
**Example output:**
231+
232+
```html
233+
<!-- heroshot: Dashboard (abc123) -->
234+
<picture>
235+
<source srcset="./heroshots/dashboard-dark.png" media="(prefers-color-scheme: dark)" />
236+
<img src="./heroshots/dashboard-light.png" alt="Dashboard" />
237+
</picture>
238+
```
239+
240+
Copy-paste this into your README.md and GitHub will automatically show the right variant based on the user's color scheme preference.
241+
242+
::: tip Single Color Scheme
243+
If your config has `browser.colorScheme` set to `light` or `dark` (not both), the snippet outputs simple Markdown instead:
244+
245+
```md
246+
![Dashboard](./heroshots/dashboard-light.png)
247+
```
248+
249+
:::
250+
214251
### `heroshot session-key`
215252

216253
When you log into a site during `heroshot config`, that session is saved encrypted. The encryption key is machine-specific by default, which is fine for local use.
@@ -262,6 +299,10 @@ heroshot session-key
262299
# Parallel capture with 4 workers
263300
heroshot --workers 4
264301

302+
# Generate README snippets for GitHub
303+
heroshot snippet
304+
heroshot snippet dashboard --path-prefix ./images/
305+
265306
# Verbose output (works with any command)
266307
heroshot -v
267308
heroshot config -v

docs/docs/integrations/markdown.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,27 @@ For plain Markdown files (GitHub READMEs, GitLab wikis, or any Markdown renderer
88

99
This works on GitHub, GitLab, and anywhere that respects `prefers-color-scheme` media queries.
1010

11+
## Generate Snippets Automatically
12+
13+
Use the `heroshot snippet` command to generate the `<picture>` markup automatically:
14+
15+
```bash
16+
heroshot snippet # All screenshots
17+
heroshot snippet dashboard # Filter by name/id
18+
```
19+
20+
Output:
21+
22+
```html
23+
<!-- heroshot: Dashboard (abc123) -->
24+
<picture>
25+
<source srcset="./heroshots/dashboard-dark.png" media="(prefers-color-scheme: dark)" />
26+
<img src="./heroshots/dashboard-light.png" alt="Dashboard" />
27+
</picture>
28+
```
29+
30+
Copy-paste into your README. See [CLI Reference](/docs/cli#heroshot-snippet-pattern) for all options.
31+
1132
## Light/Dark Mode Images
1233

1334
Use the `<picture>` element with `prefers-color-scheme` media queries:

src/cli/cli.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Command } from 'commander';
44
import type { ShotCommandOptions } from '../types';
55
import { intro, setVerbose } from '../ui';
66
import { configAction, sessionKeyAction, shotAction } from './handlers';
7+
import { type SnippetOptions, snippetAction } from './snippet';
78
import type { ConfigActionOptions, GlobalOptions } from './types';
89

910
// Version is injected at build time for standalone binaries, or read from package.json for development
@@ -94,4 +95,14 @@ program
9495
if (!success) process.exitCode = 1;
9596
});
9697

98+
program
99+
.command('snippet [pattern]')
100+
.description('Generate markdown/HTML snippets for GitHub README and Wiki')
101+
.option('--path-prefix <prefix>', 'Image path prefix (default: ./heroshots/)')
102+
.action((pattern?: string, options?: SnippetOptions) => {
103+
const globalOptions = program.opts<GlobalOptions>();
104+
const success = snippetAction(pattern, options ?? {}, globalOptions.config);
105+
if (!success) process.exitCode = 1;
106+
});
107+
97108
program.parse();

src/cli/snippet.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* Generate markdown/HTML snippets for GitHub README and Wiki.
3+
*
4+
* Generates `<picture>` elements with prefers-color-scheme media queries
5+
* for light/dark mode support in pure markdown environments.
6+
*/
7+
8+
import { existsSync } from 'node:fs';
9+
import path from 'node:path';
10+
import { getConfigPath, loadConfig } from '../configFile';
11+
import { filterScreenshots } from '../sync/configHelpers';
12+
import type { Config, Screenshot } from '../types';
13+
import { error, log } from '../ui';
14+
import { generateScreenshotFilename } from '../utils/screenshotPath';
15+
16+
export type SnippetOptions = {
17+
/** Output format */
18+
format?: 'html' | 'markdown';
19+
/** Custom path prefix for images (default: ./heroshots/) */
20+
pathPrefix?: string;
21+
};
22+
23+
type SnippetResult = {
24+
screenshot: Screenshot;
25+
snippet: string;
26+
};
27+
28+
/**
29+
* Check if a screenshot has light/dark variants (colorScheme not set = both).
30+
*/
31+
function hasColorSchemeVariants(config: Config): boolean {
32+
return config.browser?.colorScheme === undefined;
33+
}
34+
35+
/**
36+
* Generate a single snippet for a screenshot.
37+
*/
38+
function generateSnippet(screenshot: Screenshot, config: Config, options: SnippetOptions): string {
39+
const { pathPrefix = './heroshots/' } = options;
40+
const format = config.outputFormat ?? 'png';
41+
const hasVariants = hasColorSchemeVariants(config);
42+
const viewports = screenshot.viewports ?? [];
43+
44+
// For simplicity, use the first viewport or no viewport suffix
45+
const viewport = viewports.length > 0 ? viewports[0] : undefined;
46+
47+
if (hasVariants) {
48+
// Generate <picture> with prefers-color-scheme
49+
const lightFile = generateScreenshotFilename({
50+
name: screenshot.name,
51+
viewport,
52+
colorScheme: 'light',
53+
format,
54+
});
55+
const darkFile = generateScreenshotFilename({
56+
name: screenshot.name,
57+
viewport,
58+
colorScheme: 'dark',
59+
format,
60+
});
61+
62+
const lightPath = `${pathPrefix}${lightFile}`;
63+
const darkPath = `${pathPrefix}${darkFile}`;
64+
65+
return `<picture>
66+
<source srcset="${darkPath}" media="(prefers-color-scheme: dark)">
67+
<img src="${lightPath}" alt="${screenshot.name}">
68+
</picture>`;
69+
}
70+
71+
// Single color scheme - just an img tag
72+
const filename = generateScreenshotFilename({
73+
name: screenshot.name,
74+
viewport,
75+
colorScheme: config.browser?.colorScheme,
76+
format,
77+
});
78+
79+
return `![${screenshot.name}](${pathPrefix}${filename})`;
80+
}
81+
82+
/**
83+
* Generate snippets for screenshots matching pattern.
84+
*/
85+
export function generateSnippets(
86+
config: Config,
87+
pattern?: string,
88+
options: SnippetOptions = {}
89+
): SnippetResult[] {
90+
const filtered = filterScreenshots(config.screenshots, pattern);
91+
92+
return filtered.map(screenshot => ({
93+
screenshot,
94+
snippet: generateSnippet(screenshot, config, options),
95+
}));
96+
}
97+
98+
/**
99+
* Snippet command handler.
100+
*/
101+
export function snippetAction(
102+
pattern: string | undefined,
103+
options: SnippetOptions,
104+
configPath?: string
105+
): boolean {
106+
const resolvedConfigPath = configPath ? path.resolve(configPath) : getConfigPath();
107+
108+
if (!existsSync(resolvedConfigPath)) {
109+
error(`Config file not found: ${resolvedConfigPath}`);
110+
error('Run "heroshot config" to create one.');
111+
return false;
112+
}
113+
114+
const config = loadConfig(resolvedConfigPath);
115+
116+
if (config.screenshots.length === 0) {
117+
error('No screenshots defined in config.');
118+
return false;
119+
}
120+
121+
const results = generateSnippets(config, pattern, options);
122+
123+
if (results.length === 0) {
124+
error(`No screenshots matching "${pattern ?? ''}"`);
125+
return false;
126+
}
127+
128+
// Output snippets
129+
for (const { screenshot, snippet } of results) {
130+
log(`\n<!-- heroshot: ${screenshot.name} (${screenshot.id}) -->`);
131+
log(snippet);
132+
}
133+
134+
log('');
135+
136+
return true;
137+
}

0 commit comments

Comments
 (0)