Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/more-layouts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@noctcore/showcase-kit": minor
---

More looks for the hero, the frames and the README snippet. Every new option defaults to the look you have today, so an existing config renders the same images byte for byte.

- **Hero layouts**: `hero.layout` picks the composition: `stack` (the default, as before), `spotlight` (one large straight window running off the edges), `split` (one window in perspective), `row` (up to four windows under centered text), `mosaic` (a tilted wall of windows) and `centered` (text first, one window rising from the bottom). `hero.shots` takes as many shots as the layout shows and defaults to that many of the first shots.
- **Frame styles**: `frame.style` also takes `browser` (a toolbar with an address bar, whose text is the new `frame.address`, `'{url}'` by default), `windows` (a Windows title bar) and `terminal` (a terminal tab; in tty mode the bar takes the terminal background). tty mode refuses `browser`, and cdp mode needs an `address` without `{url}`.
- **Backgrounds**: `frame.background` and `hero.background` also take `{ type: 'mesh', colors }`, `{ type: 'dots', color, dot, spacing }` and `{ type: 'noise', from, to, angle, amount }`, a gradient with a film grain that is the same on every run.
- **README layouts**: `showcase readme --layout <name>` prints `table` (the default, as before), `rows`, `featured`, `details` or `list`, all built from HTML that GitHub keeps in a README. `--cols` applies to `table` and `featured`. The library's `readmeSnippet` takes the same `layout` option.
21 changes: 16 additions & 5 deletions site/scripts/reference/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,19 @@ const NORMALIZED: Record<string, (literal: unknown) => unknown> = {
'terminal.theme': (literal) => (literal === 'dark' ? lib.DARK_THEME : lib.LIGHT_THEME),
};

/** Form fields with a default, and a value of that form without the field, to see the default filled in. */
/**
* Form fields with a default, keyed by path and the form's `type` (two forms can share a field name), and a value of
* that form without the field, to see the default filled in.
*/
const GRADIENT = { type: 'gradient', from: '#000000', to: '#ffffff' };
const NOISE = { type: 'noise', from: '#000000', to: '#ffffff' };
const DOTS = { type: 'dots', color: '#000000' };
const FORM_SAMPLES: Record<string, Record<string, unknown>> = {
'frame.background.angle': { type: 'gradient', from: '#000000', to: '#ffffff' },
'frame.background.angle in gradient': GRADIENT,
'frame.background.angle in noise': NOISE,
'frame.background.amount in noise': NOISE,
'frame.background.dot in dots': DOTS,
'frame.background.spacing in dots': DOTS,
};

/** Every default written as prose (it depends on other keys or on files), exactly. */
Expand Down Expand Up @@ -130,11 +140,12 @@ describe('(a) documented defaults', () => {
for (const field of form.fields ?? []) {
if (field.default?.kind !== 'literal') continue;
const path = `${table.path}.${field.key}`;
const sample = FORM_SAMPLES[path];
expect({ path, sample: sample !== undefined }).toEqual({ path, sample: true });
const key = `${path} in ${/type: '(\w+)'/.exec(form.type)?.[1] ?? form.type}`;
const sample = FORM_SAMPLES[key];
expect({ key, sample: sample !== undefined }).toEqual({ key, sample: true });
const config = setAt(fixture('url'), segmentsOf(table.path, table.array), sample);
expect(valueAt(lib.resolveConfig(config, FIXTURE_ROOT), path)).toEqual(parseLiteral(field.default.text));
seen.push(path);
seen.push(key);
}
}
}
Expand Down
47 changes: 44 additions & 3 deletions site/src/content/docs/guides/frames.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Frames
description: Turn raw captures into README images inside a window frame, on a solid, gradient or transparent background, as WebP or PNG.
description: Turn raw captures into README images inside a window, browser or terminal frame, on a solid, gradient, mesh, dotted, grainy or transparent background, as WebP or PNG.
---

import { Aside } from '@astrojs/starlight/components';
Expand Down Expand Up @@ -37,30 +37,71 @@ config reference under [`frame`](/showcase-kit/reference/config/#frame) and
| `'window'` (default) | 40 px, with the three traffic lights and the title centered. |
| `'minimal'` | A thin 28 px bar with the title, no lights. |
| `'none'` | No bar and no title: just the rounded screenshot. |
| `'browser'` | A 44 px browser toolbar: traffic lights, back, forward and reload, and an address bar showing `address`. |
| `'windows'` | A 32 px Windows title bar: the title on the left, minimize, maximize and close on the right. |
| `'terminal'` | A 34 px terminal bar: traffic lights and a tab with a prompt icon and the title in a monospace font. |

`theme` (`'dark'` by default, or `'light'`) sets the title bar's colors. Pick the one that matches the
app, so the bar reads as part of the window.

### The browser address bar

`address` is the text in the `browser` style's address bar, `'{url}'` by default. `{url}` is the page
the shot visits, without `http://` or `https://`: the target url, resolved with the shot's `nav` when
that is a path or a URL. A shot reached by a click keeps the target url, so most apps will want their
own text, such as their public address:

```js
frame: {
style: 'browser',
address: 'nightjar.app/{id}',
},
```

`address` also takes `{name}`, `{title}`, `{id}` and `{lang}`. The style has no title text, so `title`
does not show. In cdp mode the page is not known when the frame is drawn, so an `address` with `{url}`
is an error there; write the address instead. A terminal app has no address, so tty mode refuses the
`browser` style and suggests `terminal` or `window`.

### The terminal style

`terminal` works for web apps and terminal apps. In tty mode its bar takes the terminal theme's
background and drops the line under the bar, so the bar and the screen read as one terminal window. In
url and cdp mode the bar is a dark (or light) grey.

`title` is the title bar text, `'{name}'` by default. It takes the tokens `{name}` (the config's
`name`), `{title}` (the shot's title), `{id}` and `{lang}`, so `title: '{name}: {title}'` gives
"My App: Settings". `title: false` hides it and keeps the bar.

## Backgrounds

`background` takes four forms:
`background` takes these forms:

```js
background: '#1e1b4b', // any CSS color: a hex, a name, rgb(), hsl(), oklch(), color-mix() ...
background: { type: 'solid', color: '#1e1b4b' },
background: { type: 'gradient', from: '#0f766e', to: '#1e1b4b', angle: 135 }, // angle defaults to 135
background: { type: 'transparent' },
background: { type: 'mesh', colors: ['#0b1026', '#6d28d9', '#0e7490', '#1e3a8a', '#be185d'] },
background: { type: 'dots', color: '#0d1224', dot: 'rgba(255,255,255,0.14)', spacing: 24 },
background: { type: 'noise', from: '#2a1f6b', to: '#0b3b4c', angle: 135, amount: 0.2 },
```

- **mesh** takes two to five colors: the first is the base, and the others glow from the top left, top
right, bottom right and bottom left corners, in that order.
- **dots** draws a grid of small dots on `color`. `dot` is the dot color (a faint white by default: pick
a dark one on a light `color`) and `spacing` the distance between dots, 8 to 96 CSS pixels (default 24).
- **noise** is a gradient like `gradient`, with a film grain on top. `amount` (0 to 1, default 0.2) sets
how strong the grain is. The grain comes from a fixed seed, so it is the same on every run.

The hero's `background` takes the same forms.

Colors are checked when the config loads: only color syntax is accepted, so `url()` and other CSS
functions are rejected. A transparent background keeps the alpha channel in both WebP and PNG, so the
shadow falls on whatever page the image sits on.

The default is the teal to indigo gradient above.
The default is the teal to indigo gradient above. For [clips](/showcase-kit/guides/clips/), a solid
background keeps the file small: gradients, meshes, dots and grain do not compress.

## Spacing, corners and shadow

Expand Down
41 changes: 33 additions & 8 deletions site/src/content/docs/guides/hero.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
---
title: Hero banner
description: Render a banner for the top of a README, and a GitHub social preview, from your logo, name, tagline and up to three framed shots.
description: Render a banner for the top of a README, and a GitHub social preview, from your logo, name, tagline and framed shots, in one of six layouts.
---

import { Aside } from '@astrojs/starlight/components';

`showcase hero` composes a banner from the raw captures: your logo, name and tagline on the left, and
one to three framed shots stacked and tilted on the right. The default size, 1280x640, is GitHub's
`showcase hero` composes a banner from the raw captures: your logo, name and tagline, and framed shots
arranged by `layout`. The default, `stack`, puts the text on the left and one to three shots stacked and
tilted on the right. The default size, 1280x640, is GitHub's
social preview size, so the same file works at the top of the README and as the repository's preview
image. The `hero` block configures it; every key is optional.

Expand Down Expand Up @@ -35,18 +36,42 @@ npx showcase hero # writes assets/showcase/hero.webp
## Composition

- **Text.** The logo (fitted into a 96 px square at the default height), then `name`, then `tagline`,
vertically centered on the left.
- **Windows.** `shots` lists one to three shot ids, back to front, and defaults to the first three
shots. Each window is half the banner's width, tilted a few degrees, and framed with your `frame`
settings (style, theme, title, radius, shadow). The positions are fractions of the canvas, so a
different `size` keeps the composition.
vertically centered on the left. The `row` and `centered` layouts center it at the top instead.
- **Windows.** `shots` lists the shot ids to show, back to front, and defaults to as many of the first
shots as the layout shows. In `stack`, each window is half the banner's width and tilted a few
degrees. Every window is framed with your `frame` settings (style, theme, title, radius, shadow). The
positions are fractions of the canvas, so a different `size` keeps the composition.
- **Background.** `background` takes the same forms as `frame.background` and defaults to it.
- **Text color.** `theme` defaults to `frame.theme`: `'dark'` gives light text, `'light'` gives dark
text. Match it to the background, not the app.

The banner is rendered at twice its size and scaled down to exactly `size`, for sharper text and edges
on the tilted windows.

## Layouts

`layout` picks the composition. Every layout keeps `size`, `background`, `theme` and the frame look.

| `layout` | Shots | What it looks like |
| --- | --- | --- |
| `'stack'` (default) | 1 to 3 | Text on the left, the windows stacked and tilted on the right. |
| `'spotlight'` | 1 | Text on the left, one large straight window running off the right and bottom edges. |
| `'split'` | 1 | Text on the left, one window turned in perspective towards it. |
| `'row'` | 1 to 4 | Text centered at the top, the windows side by side under it. |
| `'mosaic'` | 1 to 4 | Text on the left, a tilted wall of the windows (repeated to fill it), fading out towards the text. |
| `'centered'` | 1 | Text centered at the top, one window rising from the bottom edge and leaning back. |

```js
hero: {
layout: 'spotlight',
shots: ['player'],
},
```

A `shots` list longer than the layout shows is an error that names the layout, so switching from
`stack` to a one-window layout means picking the one shot to show. Without `shots`, each layout takes the
first shots of the config.

## Logo, language and output

- `logo` is a PNG, SVG, WebP or JPEG file, relative to the config's directory. A missing file or another
Expand Down
41 changes: 39 additions & 2 deletions site/src/content/docs/guides/readme-table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ So a shot with a `title` and no `caption` gets its title as the caption, as `set

| Option | Default | What it does |
| --- | --- | --- |
| `--cols <n>` | 2 | Images per row, 1 to 6. Each cell is `100 / n` percent wide, rounded down. |
| `--layout <name>` | `table` | How the images are laid out: `table`, `rows`, `featured`, `details` or `list` (see [Layouts](#layouts)). |
| `--cols <n>` | 2 | Images per row, 1 to 6, in the `table` layout, and thumbnails per row in `featured`. Each cell is `100 / n` percent wide, rounded down. |
| `--lang <code>` | the first of `langs` | Which language's images to list. It must be one of `langs`. |
| `--base <dir>` | the config's directory | The directory your README is in. Image paths are written relative to it. |
| `--only <ids>` | every shot and clip | Comma-separated shot and clip ids. The table keeps config order; an unknown id is an error. |
Expand All @@ -65,10 +66,46 @@ run (`showcase frame`, or `showcase record` for a clip).

The [CLI reference](/showcase-kit/reference/cli/#readme) has the command with every option.

## Layouts

`--layout` picks another arrangement of the same images, captions and alt text. GitHub removes CSS and
most HTML attributes from a README, so every layout is built only from what it keeps: tables with
`width` and `align`, `<p align>`, `<h3>`, `<sub>`, `<br>`, `<details>` and `<summary>`. Each one was
checked through GitHub's own Markdown renderer.

| `--layout` | What it prints |
| --- | --- |
| `table` (default) | The table above. |
| `rows` | One small table per image: the image in a 60% cell, and the title (as `<h3>`) and caption in a 40% cell, alternating sides. A table each, because the columns of one table are shared and alternating cells would squeeze the images. |
| `featured` | The first image full width with its caption under it, then the others in a table of `--cols` per row. |
| `details` | One collapsible `<details>` per image, with the title and caption as its summary; the first starts open. |
| `list` | Every image full width, one under the other, each with its caption. |

`--cols` only means something for `table` and `featured`; with another layout it is an error.

```sh
npx showcase readme --layout featured --cols 3
```

```html title="output"
<p align="center">
<img width="100%" src="assets/showcase/en/home.webp" alt="My App: Home" />
<br /><sub>The home screen.</sub>
</p>
<table>
<tr>
<td width="33%"><img src="assets/showcase/en/settings.webp" alt="My App: Settings" /></td>
</tr>
<tr>
<td align="center"><sub>Settings</sub></td>
</tr>
</table>
```

## Clips in the table

In a terminal app config with [`clips`](/showcase-kit/guides/clips/), the clips follow the shots in the
same table:
same table (or in the same layout):

- a clip with a WebP or GIF gets an `<img>` of the first of those in its `formats`, which GitHub plays
inline;
Expand Down
4 changes: 2 additions & 2 deletions src/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { isTtyConfig } from './config/resolve.js';
import type { CdpTarget, ResolvedConfig, ResolvedWebConfig, ResolvedWebShot, UrlTarget } from './config/types.js';
import { ShowcaseError } from './errors.js';
import { log } from './log.js';
import { navUrl, outputPath, select } from './paths.js';
import { isGotoNav, navUrl, outputPath, select } from './paths.js';
import { answers, startCommand, waitForUrl, type StartedProcess } from './process.js';
import { trimTrailing } from './text.js';

Expand Down Expand Up @@ -307,7 +307,7 @@ async function navigate(session: Session, page: Page, shot: ResolvedWebShot): Pr
let goto: string | undefined;
let click: string | undefined;
if (typeof nav === 'string') {
if (/^https?:\/\//.test(nav) || (nav.startsWith('/') && !nav.startsWith('//'))) goto = nav;
if (isGotoNav(nav)) goto = nav;
else click = nav;
} else if ('goto' in nav) {
goto = nav.goto;
Expand Down
8 changes: 6 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { generateIcons, ICON_PRESETS, type IconPreset } from './icons.js';
import { init } from './init.js';
import { log } from './log.js';
import { exportPortfolio } from './portfolio.js';
import { readmeSnippet } from './readme.js';
import { readmeSnippet, type ReadmeLayout } from './readme.js';
import { record, splitIds } from './record.js';

const HELP = `showcase: capture, frame and export showcase images of your app
Expand All @@ -38,6 +38,8 @@ Options:
readme, record, all)
--langs <codes> Comma-separated languages (capture, frame, record, all)
--lang <code> Language for readme (default: the first in langs)
--layout <name> Layout for readme: table, rows, featured, details or list
(default table)
--cols <n> Images per row for readme (default 2)
--base <dir> Directory the README is in, for relative image paths (readme)
--source <png> Square source image, 1024px or larger (icons)
Expand Down Expand Up @@ -75,6 +77,7 @@ async function main(argv: string[]): Promise<void> {
only: { type: 'string' },
langs: { type: 'string' },
lang: { type: 'string' },
layout: { type: 'string' },
cols: { type: 'string' },
base: { type: 'string' },
source: { type: 'string' },
Expand Down Expand Up @@ -137,7 +140,8 @@ async function main(argv: string[]): Promise<void> {
},
readme: async config => {
const cols = values.cols === undefined ? undefined : Number(values.cols);
console.log(readmeSnippet(config, { lang: values.lang, cols, base: values.base, only: list(values.only) }));
const layout = values.layout as ReadmeLayout | undefined;
console.log(readmeSnippet(config, { lang: values.lang, layout, cols, base: values.base, only: list(values.only) }));
},
all: async config => {
const { shots: only, clips } = splitIds(config, list(values.only));
Expand Down
Loading
Loading