diff --git a/README.md b/README.md index 6393fd5..2eb9814 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +

+ A banner showcase-kit rendered for Nightjar, the made-up observing planner in its docs gallery: the crescent logo, the name and the tagline on the left, and the Gear, Log and Tonight windows stacked and tilted on the right. +
A banner the kit rendered for Nightjar, the fixture app of the docs gallery. +

+ # @noctcore/showcase-kit Capture, frame and export showcase images of desktop and web apps for READMEs and portfolios. @@ -11,13 +16,41 @@ Protocol, or a terminal app in a pseudo terminal) and captures the same pixels o From one config it produces: - **raw captures**: pixel-exact PNGs at a fixed viewport and device scale factor, in every UI language; -- **framed README images**: each capture in a window frame on a solid or gradient background, as WebP or PNG; +- **framed README images**: each capture in a window, browser or terminal frame (six styles) on a solid, gradient, + mesh, dotted, grainy or transparent background, as WebP or PNG; - **portfolio images**: exact-size 16:9 images with the framed window contained, a thumbnail and a gallery JSON; -- **a README table**: an HTML table of the framed images with captions; -- **a hero banner**: logo, name, tagline and a stack of tilted framed shots; +- **a README snippet**: the framed images with captions as HTML, in one of five layouts (a table, rows, a featured + image, collapsible details or a list); +- **a hero banner**: logo, name, tagline and framed shots in one of six layouts (stack, spotlight, split, row, + mosaic or centered); - **terminal clips**: animated WebP and GIF recordings of a terminal app (MP4 opt-in); - **app icons**: web, Electron or Tauri icon sets from one square image. +## Layouts + +The [gallery](https://noctcore.github.io/showcase-kit/gallery/) shows every hero layout, frame style, background and +README layout, each with the config that made it. A few of them below, for Nightjar, the made-up app the gallery +captures (not the kit's own UI). The table is what `showcase readme` printed for them. + + + + + + + + + + + + + + + + + + +
A banner for Nightjar, the made-up app in showcase-kit's docs gallery, in the spotlight layout: the logo, name and tagline on the left, one large Tonight window running off the right edge.The Nightjar banner in the row layout: the logo, name and tagline centered at the top, the Gear, Log and Tonight windows side by side under them.
Hero layout spotlight, for Nightjar (the gallery fixture app)Hero layout row
The Nightjar banner in the mosaic layout: the text on the left, a tilted wall of Nightjar windows fading out towards it.Nightjar's Tonight view in the browser frame: a toolbar with back, forward and reload, and nightjar.app/tonight in the address bar.
Hero layout mosaicFrame style browser, with an address bar
+ ## Install ```sh @@ -67,9 +100,9 @@ export default defineConfig({ | [Terminal determinism](https://noctcore.github.io/showcase-kit/guides/terminal-determinism/) | What the kit pins down, and what a TUI should offer | | [Clips](https://noctcore.github.io/showcase-kit/guides/clips/) | Animated recordings of a terminal app | | [Frames](https://noctcore.github.io/showcase-kit/guides/frames/) | Window styles, backgrounds, formats and sizes | -| [README table](https://noctcore.github.io/showcase-kit/guides/readme-table/) | `showcase readme` and its options | +| [README table](https://noctcore.github.io/showcase-kit/guides/readme-table/) | `showcase readme`, its options and its five layouts | | [Portfolio](https://noctcore.github.io/showcase-kit/guides/portfolio/) | Exact-size images, the thumbnail and the gallery JSON | -| [Hero banner](https://noctcore.github.io/showcase-kit/guides/hero/) | The README banner and social preview | +| [Hero banner](https://noctcore.github.io/showcase-kit/guides/hero/) | The README banner and social preview, in six layouts | | [Icons](https://noctcore.github.io/showcase-kit/guides/icons/) | Web, Electron and Tauri icon sets | | [Config reference](https://noctcore.github.io/showcase-kit/reference/config/) | Every config key, its type and default | | [CLI reference](https://noctcore.github.io/showcase-kit/reference/cli/) | Every command and option | diff --git a/site/gallery/gallery.manifest.json b/site/gallery/gallery.manifest.json index 25eae9d..1e0b2dd 100644 --- a/site/gallery/gallery.manifest.json +++ b/site/gallery/gallery.manifest.json @@ -27,6 +27,120 @@ "width": 512, "height": 512 }, + "layouts/hero-centered.webp": { + "bytes": 23008, + "sha256": "fd4d3067b1e991726205757055fbd8062288f8a191a9050fddd28928906c9822", + "width": 1280, + "height": 640 + }, + "layouts/hero-dots.webp": { + "bytes": 46254, + "sha256": "3a9fd1b1b5fe8664e4d6fc61855710a586aa8bae2765631ceaf3c04690227c3c", + "width": 1280, + "height": 640 + }, + "layouts/hero-mesh.webp": { + "bytes": 41004, + "sha256": "6c63e699bd1fd0c2bef1b7ab9890bc5fc007bd258d1f15be2093e38db45be069", + "width": 1280, + "height": 640 + }, + "layouts/hero-mosaic.webp": { + "bytes": 62384, + "sha256": "31aefc7c1d14f7c0e742688d7677fe774c2415cfe8cdd342b404432278c20614", + "width": 1280, + "height": 640 + }, + "layouts/hero-noise.webp": { + "bytes": 46084, + "sha256": "6d3df9f322a13ad1938a3397b0764a1879c13c6bbe3546cc72e04060a70591fc", + "width": 1280, + "height": 640 + }, + "layouts/hero-row.webp": { + "bytes": 30680, + "sha256": "8ca0d9fa2c00abfff28f4b77e9de0a864bba47f89b9e03aa91089870d1a1b7f1", + "width": 1280, + "height": 640 + }, + "layouts/hero-split.webp": { + "bytes": 27188, + "sha256": "68357ce8b5f8c7a89d2db20859fda6c597f83f77c8a08fb3e9cb8180cae57760", + "width": 1280, + "height": 640 + }, + "layouts/hero-spotlight.webp": { + "bytes": 29646, + "sha256": "32bdc538ce116559e75dfb71605b2b07920ac410bf9362fd943590f798774452", + "width": 1280, + "height": 640 + }, + "layouts/queue-terminal.webp": { + "bytes": 82066, + "sha256": "768e2f7d9782f6a2f696fc062fbbcc1d59888dce2b78a8801568d390b00d6d06", + "width": 2040, + "height": 1188 + }, + "layouts/queue-windows.webp": { + "bytes": 81074, + "sha256": "0865d57c918ab5a498d580f538005d58aec33e6760d762b69d2917c0f463386d", + "width": 2040, + "height": 1184 + }, + "layouts/tonight-browser.webp": { + "bytes": 33656, + "sha256": "421c7299e80c128681da0863a84959beb200d937a09d4545506f2c81c2435e57", + "width": 1200, + "height": 760 + }, + "layouts/tonight-dots.webp": { + "bytes": 37050, + "sha256": "710ef31572a5e574178130d43ef7b2a0800acfda8898389dac7b5941d8929d2e", + "width": 1200, + "height": 757 + }, + "layouts/tonight-mesh.webp": { + "bytes": 33804, + "sha256": "31bccf9ac03db192d9b183ab70f59727ed702de5978d09a1bd566fa3c4edb09a", + "width": 1200, + "height": 757 + }, + "layouts/tonight-minimal.webp": { + "bytes": 32324, + "sha256": "909fb7793bef805aa89c3fa549aecbe12ee20dc5f92aa49f794af1ae40c19961", + "width": 1200, + "height": 747 + }, + "layouts/tonight-noise.webp": { + "bytes": 32900, + "sha256": "3603caac55b081a56b4e2c9df0e81ed5748c7cce4156574f03b04991121b1a8e", + "width": 1200, + "height": 757 + }, + "layouts/tonight-none.webp": { + "bytes": 31926, + "sha256": "331238da379691695edd84441d63cba1380b4d659d004531185410628639253b", + "width": 1200, + "height": 723 + }, + "layouts/tonight-solid.webp": { + "bytes": 32280, + "sha256": "98ba29628b99b180aba967d118d24ed2b3b7411dcedeb3e559c15a6573726fe1", + "width": 1200, + "height": 757 + }, + "layouts/tonight-terminal.webp": { + "bytes": 33216, + "sha256": "deaa48f6b326cca377e7432f118a70ee8198548e09b40c3d7b25291f12ce877b", + "width": 1200, + "height": 752 + }, + "layouts/tonight-windows.webp": { + "bytes": 32514, + "sha256": "5f1dc938990152803661f8eb1b5e896787a46d5b281a108ac7785bc6a2ce7fc9", + "width": 1200, + "height": 750 + }, "portfolio/gear.webp": { "bytes": 39490, "sha256": "06265706035004f4b97fc384c0a298fbebaad0f4fbae62b228c0a5e3d130fc13", diff --git a/site/gallery/readme.config.mjs b/site/gallery/readme.config.mjs new file mode 100644 index 0000000..3ee1e58 --- /dev/null +++ b/site/gallery/readme.config.mjs @@ -0,0 +1,34 @@ +// Not a capture config: the kit's own README shows a few gallery images, and `showcase readme` prints the table +// for them. That command only reads `shots` and `outputs.readme`, so each shot here names an image one of the +// configs in variants/ wrote. Nothing is captured with this file and its target never starts. +export default { + name: 'Nightjar', + target: { mode: 'url', url: 'http://127.0.0.1:47219/' }, + shots: [ + { + id: 'hero-spotlight', + title: 'Hero: spotlight', + caption: 'Hero layout spotlight, for Nightjar (the gallery fixture app)', + alt: "A banner for Nightjar, the made-up app in showcase-kit's docs gallery, in the spotlight layout: the logo, name and tagline on the left, one large Tonight window running off the right edge.", + }, + { + id: 'hero-row', + title: 'Hero: row', + caption: 'Hero layout row', + alt: "The Nightjar banner in the row layout: the logo, name and tagline centered at the top, the Gear, Log and Tonight windows side by side under them.", + }, + { + id: 'hero-mosaic', + title: 'Hero: mosaic', + caption: 'Hero layout mosaic', + alt: 'The Nightjar banner in the mosaic layout: the text on the left, a tilted wall of Nightjar windows fading out towards it.', + }, + { + id: 'tonight-browser', + title: 'Frame: browser', + caption: 'Frame style browser, with an address bar', + alt: "Nightjar's Tonight view in the browser frame: a toolbar with back, forward and reload, and nightjar.app/tonight in the address bar.", + }, + ], + outputs: { readme: '../public/gallery/layouts/{id}.webp' }, +}; diff --git a/site/gallery/readme/details.html b/site/gallery/readme/details.html new file mode 100644 index 0000000..90cd72f --- /dev/null +++ b/site/gallery/readme/details.html @@ -0,0 +1,12 @@ +
+ Tonight: Plan the night: conditions, targets and a sky chart. +

Nightjar's Tonight view: four condition cards, a table of six targets with altitude bars, and a sky chart.

+
+
+ Log: Every session, with seeing and notes. +

Nightjar's Log view: session totals and a table of eight observations with seeing dots, star ratings and notes.

+
+
+ Gear: The kit that goes in the car. +

Nightjar's Gear view: six equipment cards with their specs, and a packing checklist.

+
diff --git a/site/gallery/readme/featured.html b/site/gallery/readme/featured.html new file mode 100644 index 0000000..30cd74b --- /dev/null +++ b/site/gallery/readme/featured.html @@ -0,0 +1,14 @@ +

+ Nightjar's Tonight view: four condition cards, a table of six targets with altitude bars, and a sky chart. +
Plan the night: conditions, targets and a sky chart. +

+ + + + + + + + + +
Nightjar's Log view: session totals and a table of eight observations with seeing dots, star ratings and notes.Nightjar's Gear view: six equipment cards with their specs, and a packing checklist.
Every session, with seeing and notes.The kit that goes in the car.
diff --git a/site/gallery/readme/kit-readme.html b/site/gallery/readme/kit-readme.html new file mode 100644 index 0000000..9dc8388 --- /dev/null +++ b/site/gallery/readme/kit-readme.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + +
A banner for Nightjar, the made-up app in showcase-kit's docs gallery, in the spotlight layout: the logo, name and tagline on the left, one large Tonight window running off the right edge.The Nightjar banner in the row layout: the logo, name and tagline centered at the top, the Gear, Log and Tonight windows side by side under them.
Hero layout spotlight, for Nightjar (the gallery fixture app)Hero layout row
The Nightjar banner in the mosaic layout: the text on the left, a tilted wall of Nightjar windows fading out towards it.Nightjar's Tonight view in the browser frame: a toolbar with back, forward and reload, and nightjar.app/tonight in the address bar.
Hero layout mosaicFrame style browser, with an address bar
diff --git a/site/gallery/readme/list.html b/site/gallery/readme/list.html new file mode 100644 index 0000000..182e09c --- /dev/null +++ b/site/gallery/readme/list.html @@ -0,0 +1,12 @@ +

+ Nightjar's Tonight view: four condition cards, a table of six targets with altitude bars, and a sky chart. +
Plan the night: conditions, targets and a sky chart. +

+

+ Nightjar's Log view: session totals and a table of eight observations with seeing dots, star ratings and notes. +
Every session, with seeing and notes. +

+

+ Nightjar's Gear view: six equipment cards with their specs, and a packing checklist. +
The kit that goes in the car. +

diff --git a/site/gallery/readme/rows.html b/site/gallery/readme/rows.html new file mode 100644 index 0000000..0101b44 --- /dev/null +++ b/site/gallery/readme/rows.html @@ -0,0 +1,18 @@ + + + + + +
Nightjar's Tonight view: four condition cards, a table of six targets with altitude bars, and a sky chart.

Tonight

Plan the night: conditions, targets and a sky chart.

+ + + + + +

Log

Every session, with seeing and notes.

Nightjar's Log view: session totals and a table of eight observations with seeing dots, star ratings and notes.
+ + + + + +
Nightjar's Gear view: six equipment cards with their specs, and a packing checklist.

Gear

The kit that goes in the car.

diff --git a/site/gallery/readme/table.html b/site/gallery/readme/table.html new file mode 100644 index 0000000..8431512 --- /dev/null +++ b/site/gallery/readme/table.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + +
Nightjar's Tonight view: four condition cards, a table of six targets with altitude bars, and a sky chart.Nightjar's Log view: session totals and a table of eight observations with seeing dots, star ratings and notes.
Plan the night: conditions, targets and a sky chart.Every session, with seeing and notes.
Nightjar's Gear view: six equipment cards with their specs, and a packing checklist.
The kit that goes in the car.
diff --git a/site/gallery/variants/background-dots.mjs b/site/gallery/variants/background-dots.mjs new file mode 100644 index 0000000..32d5593 --- /dev/null +++ b/site/gallery/variants/background-dots.mjs @@ -0,0 +1,20 @@ +// The web config's README image in another frame, for the gallery's frame section. `frame` renders from the +// raw captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` +// keeps every path resolving from site/gallery/. One language, sized for a docs tile rather than a README. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + langs: ['en'], + frame: { + ...web.frame, + background: { type: 'dots', color: '#0d1224' }, + maxWidth: 1200, + quality: 80, + }, + outputs: { + raw: web.outputs.raw, + readme: '../public/gallery/layouts/{id}-dots.webp', + }, +}; diff --git a/site/gallery/variants/background-mesh.mjs b/site/gallery/variants/background-mesh.mjs new file mode 100644 index 0000000..8830b98 --- /dev/null +++ b/site/gallery/variants/background-mesh.mjs @@ -0,0 +1,20 @@ +// The web config's README image in another frame, for the gallery's frame section. `frame` renders from the +// raw captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` +// keeps every path resolving from site/gallery/. One language, sized for a docs tile rather than a README. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + langs: ['en'], + frame: { + ...web.frame, + background: { type: 'mesh', colors: ['#0b1026', '#6d28d9', '#0e7490', '#1e3a8a', '#be185d'] }, + maxWidth: 1200, + quality: 80, + }, + outputs: { + raw: web.outputs.raw, + readme: '../public/gallery/layouts/{id}-mesh.webp', + }, +}; diff --git a/site/gallery/variants/background-noise.mjs b/site/gallery/variants/background-noise.mjs new file mode 100644 index 0000000..0b38095 --- /dev/null +++ b/site/gallery/variants/background-noise.mjs @@ -0,0 +1,20 @@ +// The web config's README image in another frame, for the gallery's frame section. `frame` renders from the +// raw captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` +// keeps every path resolving from site/gallery/. One language, sized for a docs tile rather than a README. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + langs: ['en'], + frame: { + ...web.frame, + background: { type: 'noise', from: '#2a1f6b', to: '#0b3b4c' }, + maxWidth: 1200, + quality: 80, + }, + outputs: { + raw: web.outputs.raw, + readme: '../public/gallery/layouts/{id}-noise.webp', + }, +}; diff --git a/site/gallery/variants/background-solid.mjs b/site/gallery/variants/background-solid.mjs new file mode 100644 index 0000000..ab2b1d2 --- /dev/null +++ b/site/gallery/variants/background-solid.mjs @@ -0,0 +1,20 @@ +// The web config's README image in another frame, for the gallery's frame section. `frame` renders from the +// raw captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` +// keeps every path resolving from site/gallery/. One language, sized for a docs tile rather than a README. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + langs: ['en'], + frame: { + ...web.frame, + background: '#141a3a', + maxWidth: 1200, + quality: 80, + }, + outputs: { + raw: web.outputs.raw, + readme: '../public/gallery/layouts/{id}-solid.webp', + }, +}; diff --git a/site/gallery/variants/frame-browser.mjs b/site/gallery/variants/frame-browser.mjs new file mode 100644 index 0000000..be23f50 --- /dev/null +++ b/site/gallery/variants/frame-browser.mjs @@ -0,0 +1,21 @@ +// The web config's README image in another frame, for the gallery's frame section. `frame` renders from the +// raw captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` +// keeps every path resolving from site/gallery/. One language, sized for a docs tile rather than a README. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + langs: ['en'], + frame: { + ...web.frame, + style: 'browser', + address: 'nightjar.app/{id}', + maxWidth: 1200, + quality: 80, + }, + outputs: { + raw: web.outputs.raw, + readme: '../public/gallery/layouts/{id}-browser.webp', + }, +}; diff --git a/site/gallery/variants/frame-minimal.mjs b/site/gallery/variants/frame-minimal.mjs new file mode 100644 index 0000000..b98a383 --- /dev/null +++ b/site/gallery/variants/frame-minimal.mjs @@ -0,0 +1,20 @@ +// The web config's README image in another frame, for the gallery's frame section. `frame` renders from the +// raw captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` +// keeps every path resolving from site/gallery/. One language, sized for a docs tile rather than a README. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + langs: ['en'], + frame: { + ...web.frame, + style: 'minimal', + maxWidth: 1200, + quality: 80, + }, + outputs: { + raw: web.outputs.raw, + readme: '../public/gallery/layouts/{id}-minimal.webp', + }, +}; diff --git a/site/gallery/variants/frame-none.mjs b/site/gallery/variants/frame-none.mjs new file mode 100644 index 0000000..f4057fe --- /dev/null +++ b/site/gallery/variants/frame-none.mjs @@ -0,0 +1,20 @@ +// The web config's README image in another frame, for the gallery's frame section. `frame` renders from the +// raw captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` +// keeps every path resolving from site/gallery/. One language, sized for a docs tile rather than a README. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + langs: ['en'], + frame: { + ...web.frame, + style: 'none', + maxWidth: 1200, + quality: 80, + }, + outputs: { + raw: web.outputs.raw, + readme: '../public/gallery/layouts/{id}-none.webp', + }, +}; diff --git a/site/gallery/variants/frame-terminal.mjs b/site/gallery/variants/frame-terminal.mjs new file mode 100644 index 0000000..495715d --- /dev/null +++ b/site/gallery/variants/frame-terminal.mjs @@ -0,0 +1,20 @@ +// The web config's README image in another frame, for the gallery's frame section. `frame` renders from the +// raw captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` +// keeps every path resolving from site/gallery/. One language, sized for a docs tile rather than a README. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + langs: ['en'], + frame: { + ...web.frame, + style: 'terminal', + maxWidth: 1200, + quality: 80, + }, + outputs: { + raw: web.outputs.raw, + readme: '../public/gallery/layouts/{id}-terminal.webp', + }, +}; diff --git a/site/gallery/variants/frame-windows.mjs b/site/gallery/variants/frame-windows.mjs new file mode 100644 index 0000000..419240c --- /dev/null +++ b/site/gallery/variants/frame-windows.mjs @@ -0,0 +1,20 @@ +// The web config's README image in another frame, for the gallery's frame section. `frame` renders from the +// raw captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` +// keeps every path resolving from site/gallery/. One language, sized for a docs tile rather than a README. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + langs: ['en'], + frame: { + ...web.frame, + style: 'windows', + maxWidth: 1200, + quality: 80, + }, + outputs: { + raw: web.outputs.raw, + readme: '../public/gallery/layouts/{id}-windows.webp', + }, +}; diff --git a/site/gallery/variants/hero-centered.mjs b/site/gallery/variants/hero-centered.mjs new file mode 100644 index 0000000..421319d --- /dev/null +++ b/site/gallery/variants/hero-centered.mjs @@ -0,0 +1,15 @@ +// The web config's hero in another look, for the gallery's hero section. `hero` renders from the raw +// captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` keeps +// every path resolving from site/gallery/, like that config. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + hero: { + ...web.hero, + layout: 'centered', + shots: ['tonight'], + output: '../public/gallery/layouts/hero-centered.webp', + }, +}; diff --git a/site/gallery/variants/hero-dots.mjs b/site/gallery/variants/hero-dots.mjs new file mode 100644 index 0000000..aac3b41 --- /dev/null +++ b/site/gallery/variants/hero-dots.mjs @@ -0,0 +1,14 @@ +// The web config's hero in another look, for the gallery's hero section. `hero` renders from the raw +// captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` keeps +// every path resolving from site/gallery/, like that config. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + hero: { + ...web.hero, + background: { type: 'dots', color: '#0d1224' }, + output: '../public/gallery/layouts/hero-dots.webp', + }, +}; diff --git a/site/gallery/variants/hero-mesh.mjs b/site/gallery/variants/hero-mesh.mjs new file mode 100644 index 0000000..40a05e9 --- /dev/null +++ b/site/gallery/variants/hero-mesh.mjs @@ -0,0 +1,14 @@ +// The web config's hero in another look, for the gallery's hero section. `hero` renders from the raw +// captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` keeps +// every path resolving from site/gallery/, like that config. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + hero: { + ...web.hero, + background: { type: 'mesh', colors: ['#0b1026', '#6d28d9', '#0e7490', '#1e3a8a', '#be185d'] }, + output: '../public/gallery/layouts/hero-mesh.webp', + }, +}; diff --git a/site/gallery/variants/hero-mosaic.mjs b/site/gallery/variants/hero-mosaic.mjs new file mode 100644 index 0000000..c9b3d02 --- /dev/null +++ b/site/gallery/variants/hero-mosaic.mjs @@ -0,0 +1,14 @@ +// The web config's hero in another look, for the gallery's hero section. `hero` renders from the raw +// captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` keeps +// every path resolving from site/gallery/, like that config. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + hero: { + ...web.hero, + layout: 'mosaic', + output: '../public/gallery/layouts/hero-mosaic.webp', + }, +}; diff --git a/site/gallery/variants/hero-noise.mjs b/site/gallery/variants/hero-noise.mjs new file mode 100644 index 0000000..9af27de --- /dev/null +++ b/site/gallery/variants/hero-noise.mjs @@ -0,0 +1,14 @@ +// The web config's hero in another look, for the gallery's hero section. `hero` renders from the raw +// captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` keeps +// every path resolving from site/gallery/, like that config. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + hero: { + ...web.hero, + background: { type: 'noise', from: '#2a1f6b', to: '#0b3b4c' }, + output: '../public/gallery/layouts/hero-noise.webp', + }, +}; diff --git a/site/gallery/variants/hero-row.mjs b/site/gallery/variants/hero-row.mjs new file mode 100644 index 0000000..80b5e41 --- /dev/null +++ b/site/gallery/variants/hero-row.mjs @@ -0,0 +1,14 @@ +// The web config's hero in another look, for the gallery's hero section. `hero` renders from the raw +// captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` keeps +// every path resolving from site/gallery/, like that config. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + hero: { + ...web.hero, + layout: 'row', + output: '../public/gallery/layouts/hero-row.webp', + }, +}; diff --git a/site/gallery/variants/hero-split.mjs b/site/gallery/variants/hero-split.mjs new file mode 100644 index 0000000..f29e463 --- /dev/null +++ b/site/gallery/variants/hero-split.mjs @@ -0,0 +1,15 @@ +// The web config's hero in another look, for the gallery's hero section. `hero` renders from the raw +// captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` keeps +// every path resolving from site/gallery/, like that config. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + hero: { + ...web.hero, + layout: 'split', + shots: ['tonight'], + output: '../public/gallery/layouts/hero-split.webp', + }, +}; diff --git a/site/gallery/variants/hero-spotlight.mjs b/site/gallery/variants/hero-spotlight.mjs new file mode 100644 index 0000000..8551b7f --- /dev/null +++ b/site/gallery/variants/hero-spotlight.mjs @@ -0,0 +1,15 @@ +// The web config's hero in another look, for the gallery's hero section. `hero` renders from the raw +// captures `showcase all` wrote for showcase.config.mjs, so nothing is captured again; `root: '..'` keeps +// every path resolving from site/gallery/, like that config. +import web from '../showcase.config.mjs'; + +export default { + ...web, + root: '..', + hero: { + ...web.hero, + layout: 'spotlight', + shots: ['tonight'], + output: '../public/gallery/layouts/hero-spotlight.webp', + }, +}; diff --git a/site/gallery/variants/tty-terminal.mjs b/site/gallery/variants/tty-terminal.mjs new file mode 100644 index 0000000..c08e3c6 --- /dev/null +++ b/site/gallery/variants/tty-terminal.mjs @@ -0,0 +1,17 @@ +// The terminal shot in another frame, for the gallery's frame section. `frame` renders from the raw capture +// `showcase all` wrote for showcase.tty.config.mjs, so the app does not run again; `root: '..'` keeps every +// path resolving from site/gallery/. +import tty from '../showcase.tty.config.mjs'; + +export default { + ...tty, + root: '..', + frame: { + ...tty.frame, + style: 'terminal', + }, + outputs: { + raw: tty.outputs.raw, + readme: '../public/gallery/layouts/{id}-terminal.webp', + }, +}; diff --git a/site/gallery/variants/tty-windows.mjs b/site/gallery/variants/tty-windows.mjs new file mode 100644 index 0000000..4706813 --- /dev/null +++ b/site/gallery/variants/tty-windows.mjs @@ -0,0 +1,17 @@ +// The terminal shot in another frame, for the gallery's frame section. `frame` renders from the raw capture +// `showcase all` wrote for showcase.tty.config.mjs, so the app does not run again; `root: '..'` keeps every +// path resolving from site/gallery/. +import tty from '../showcase.tty.config.mjs'; + +export default { + ...tty, + root: '..', + frame: { + ...tty.frame, + style: 'windows', + }, + outputs: { + raw: tty.outputs.raw, + readme: '../public/gallery/layouts/{id}-windows.webp', + }, +}; diff --git a/site/public/gallery/layouts/hero-centered.webp b/site/public/gallery/layouts/hero-centered.webp new file mode 100644 index 0000000..b0627cf Binary files /dev/null and b/site/public/gallery/layouts/hero-centered.webp differ diff --git a/site/public/gallery/layouts/hero-dots.webp b/site/public/gallery/layouts/hero-dots.webp new file mode 100644 index 0000000..069ee9d Binary files /dev/null and b/site/public/gallery/layouts/hero-dots.webp differ diff --git a/site/public/gallery/layouts/hero-mesh.webp b/site/public/gallery/layouts/hero-mesh.webp new file mode 100644 index 0000000..306064d Binary files /dev/null and b/site/public/gallery/layouts/hero-mesh.webp differ diff --git a/site/public/gallery/layouts/hero-mosaic.webp b/site/public/gallery/layouts/hero-mosaic.webp new file mode 100644 index 0000000..60981ef Binary files /dev/null and b/site/public/gallery/layouts/hero-mosaic.webp differ diff --git a/site/public/gallery/layouts/hero-noise.webp b/site/public/gallery/layouts/hero-noise.webp new file mode 100644 index 0000000..3c403d8 Binary files /dev/null and b/site/public/gallery/layouts/hero-noise.webp differ diff --git a/site/public/gallery/layouts/hero-row.webp b/site/public/gallery/layouts/hero-row.webp new file mode 100644 index 0000000..85e19c7 Binary files /dev/null and b/site/public/gallery/layouts/hero-row.webp differ diff --git a/site/public/gallery/layouts/hero-split.webp b/site/public/gallery/layouts/hero-split.webp new file mode 100644 index 0000000..ba928df Binary files /dev/null and b/site/public/gallery/layouts/hero-split.webp differ diff --git a/site/public/gallery/layouts/hero-spotlight.webp b/site/public/gallery/layouts/hero-spotlight.webp new file mode 100644 index 0000000..df528b2 Binary files /dev/null and b/site/public/gallery/layouts/hero-spotlight.webp differ diff --git a/site/public/gallery/layouts/queue-terminal.webp b/site/public/gallery/layouts/queue-terminal.webp new file mode 100644 index 0000000..0f24c2b Binary files /dev/null and b/site/public/gallery/layouts/queue-terminal.webp differ diff --git a/site/public/gallery/layouts/queue-windows.webp b/site/public/gallery/layouts/queue-windows.webp new file mode 100644 index 0000000..17d4f24 Binary files /dev/null and b/site/public/gallery/layouts/queue-windows.webp differ diff --git a/site/public/gallery/layouts/tonight-browser.webp b/site/public/gallery/layouts/tonight-browser.webp new file mode 100644 index 0000000..36de1d8 Binary files /dev/null and b/site/public/gallery/layouts/tonight-browser.webp differ diff --git a/site/public/gallery/layouts/tonight-dots.webp b/site/public/gallery/layouts/tonight-dots.webp new file mode 100644 index 0000000..4396db5 Binary files /dev/null and b/site/public/gallery/layouts/tonight-dots.webp differ diff --git a/site/public/gallery/layouts/tonight-mesh.webp b/site/public/gallery/layouts/tonight-mesh.webp new file mode 100644 index 0000000..5fa14e4 Binary files /dev/null and b/site/public/gallery/layouts/tonight-mesh.webp differ diff --git a/site/public/gallery/layouts/tonight-minimal.webp b/site/public/gallery/layouts/tonight-minimal.webp new file mode 100644 index 0000000..3fdefc7 Binary files /dev/null and b/site/public/gallery/layouts/tonight-minimal.webp differ diff --git a/site/public/gallery/layouts/tonight-noise.webp b/site/public/gallery/layouts/tonight-noise.webp new file mode 100644 index 0000000..031ec71 Binary files /dev/null and b/site/public/gallery/layouts/tonight-noise.webp differ diff --git a/site/public/gallery/layouts/tonight-none.webp b/site/public/gallery/layouts/tonight-none.webp new file mode 100644 index 0000000..ec4d627 Binary files /dev/null and b/site/public/gallery/layouts/tonight-none.webp differ diff --git a/site/public/gallery/layouts/tonight-solid.webp b/site/public/gallery/layouts/tonight-solid.webp new file mode 100644 index 0000000..e617caf Binary files /dev/null and b/site/public/gallery/layouts/tonight-solid.webp differ diff --git a/site/public/gallery/layouts/tonight-terminal.webp b/site/public/gallery/layouts/tonight-terminal.webp new file mode 100644 index 0000000..94b75ca Binary files /dev/null and b/site/public/gallery/layouts/tonight-terminal.webp differ diff --git a/site/public/gallery/layouts/tonight-windows.webp b/site/public/gallery/layouts/tonight-windows.webp new file mode 100644 index 0000000..a8ec335 Binary files /dev/null and b/site/public/gallery/layouts/tonight-windows.webp differ diff --git a/site/scripts/gallery.test.ts b/site/scripts/gallery.test.ts index cb67ef5..4335404 100644 --- a/site/scripts/gallery.test.ts +++ b/site/scripts/gallery.test.ts @@ -1,9 +1,10 @@ import { describe, expect, test } from 'bun:test'; import { createHash } from 'node:crypto'; -import { existsSync, mkdtempSync, readFileSync } from 'node:fs'; +import { existsSync, mkdtempSync, readdirSync, readFileSync } from 'node:fs'; import { mkdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { join, relative, sep } from 'node:path'; +import { pathToFileURL } from 'node:url'; import sharp from 'sharp'; import { configSection } from '../src/components/GallerySection.ts'; import { @@ -15,12 +16,19 @@ import { missingBuild, OUTPUT_DIR, overBudget, + README_CONFIG, + README_LAYOUTS, + SNIPPET_STEPS, STEPS, TTY_CONFIG, + VARIANT_STEPS, + VARIANTS, + VARIANTS_DIR, + variantConfig, WEB_CONFIG, type Manifest, } from './gallery.ts'; -import { SITE_BASE } from './site.ts'; +import { SITE_BASE, SITE_DIR } from './site.ts'; const tmp = () => mkdtempSync(join(tmpdir(), 'gallery-test-')); const solid = (width: number, height: number, r = 20) => @@ -89,6 +97,70 @@ describe('the steps', () => { }); }); +describe('the variants', () => { + test('every config in variants/ runs, and every step has its config', () => { + const files = readdirSync(VARIANTS_DIR).filter(name => name.endsWith('.mjs')); + expect(files.map(name => name.slice(0, -'.mjs'.length)).sort()).toEqual([...VARIANTS].sort()); + for (const name of VARIANTS) expect(existsSync(variantConfig(name))).toBe(true); + }); + + test('a hero- config runs hero, a web one frames only tonight, a tty one frames its only shot', () => { + VARIANTS.forEach((name, index) => { + const step = VARIANT_STEPS[index]; + if (name.startsWith('hero-')) expect(step).toEqual(['hero', '--config', variantConfig(name)]); + else if (name.startsWith('tty-')) expect(step).toEqual(['frame', '--config', variantConfig(name)]); + else expect(step).toEqual(['frame', '--config', variantConfig(name), '--only', 'tonight']); + }); + }); + + test('each one writes a file of its own under layouts/, named after the config', async () => { + const outputs = new Set(); + for (const name of VARIANTS) { + const config = ((await import(pathToFileURL(variantConfig(name)).href)) as { default: Record }).default; + const option = name.slice(name.indexOf('-') + 1); + if (name.startsWith('hero-')) { + expect(config.hero.output).toBe(`../public/gallery/layouts/${name}.webp`); + outputs.add(config.hero.output); + } else { + // The web variants frame `tonight`; a tty variant frames its config's only shot. + const shots: { id: string }[] = config.shots; + const id = name.startsWith('tty-') ? shots[0]?.id : 'tonight'; + if (name.startsWith('tty-')) expect(shots.length).toBe(1); + expect(config.outputs.readme).toBe(`../public/gallery/layouts/{id}-${option}.webp`); + outputs.add(config.outputs.readme.replace('{id}', String(id))); + } + } + expect(outputs.size).toBe(VARIANTS.length); + }); +}); + +describe('the README snippets', () => { + test('one run per layout with the web config, then the kit README table, each to its own file', () => { + const layoutOf = (args: readonly string[]) => (args.includes('--layout') ? args[args.indexOf('--layout') + 1] : undefined); + expect(SNIPPET_STEPS.map(step => layoutOf(step.args))).toEqual([...README_LAYOUTS, undefined]); + expect(SNIPPET_STEPS.slice(0, -1).every(step => step.args.includes(WEB_CONFIG))).toBe(true); + expect(SNIPPET_STEPS.at(-1)?.args).toContain(README_CONFIG); + for (const { args, out } of SNIPPET_STEPS) { + expect(args[0]).toBe('readme'); + expect(args.slice(-2)).toEqual(['--base', '../..']); + expect(existsSync(out)).toBe(true); + } + expect(new Set(SNIPPET_STEPS.map(step => step.out)).size).toBe(SNIPPET_STEPS.length); + }); + + test('every image a snippet lists is a gallery file in the manifest', () => { + const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8')) as Manifest; + for (const { out } of SNIPPET_STEPS) { + const srcs = [...readFileSync(out, 'utf8').matchAll(/src="([^"]*)"/g)].map(match => match[1] ?? ''); + expect(srcs.length).toBeGreaterThan(0); + for (const src of srcs) { + expect(src.startsWith('site/public/gallery/')).toBe(true); + expect(manifest[src.slice('site/public/gallery/'.length)]).toBeDefined(); + } + } + }); +}); + describe('the committed gallery', () => { test('matches its manifest byte for byte', async () => { const onDisk = await buildManifest(OUTPUT_DIR); @@ -98,9 +170,13 @@ describe('the committed gallery', () => { test('fits the budget', async () => { const manifest = await buildManifest(OUTPUT_DIR); + const snippets = Object.fromEntries( + SNIPPET_STEPS.map(({ out }) => [relative(SITE_DIR, out).split(sep).join('/'), readFileSync(out).length]), + ); const { total } = budgetLines(manifest, { json: readFileSync(GALLERY_JSON).length, manifest: readFileSync(MANIFEST).length, + ...snippets, }); expect(overBudget(total)).toBeUndefined(); }); diff --git a/site/scripts/gallery.ts b/site/scripts/gallery.ts index a7f997b..0f03726 100644 --- a/site/scripts/gallery.ts +++ b/site/scripts/gallery.ts @@ -4,9 +4,12 @@ * Regenerates everything the /gallery/ page shows by running the built CLI * against the fixture app in site/gallery/: README frames, portfolio images and * their gallery JSON, the hero banner, a terminal shot and clip, and an icon - * set. It starts from empty output folders, so a run leaves exactly what the - * configs produce, then writes a manifest (size, pixel size, hash of every - * file) that the page reads for image dimensions. + * set. Then the variant configs in site/gallery/variants/ render every hero + * layout, frame style and background from the same raw captures, and + * `showcase readme` prints each README layout to a file the pages import. It + * starts from empty output folders, so a run leaves exactly what the configs + * produce, then writes a manifest (size, pixel size, hash of every file) that + * the page reads for image dimensions. * * bun run docs:gallery (from the repo root; needs `bun run build` first) */ @@ -29,6 +32,12 @@ export const GALLERY_JSON = join(GALLERY_DIR, 'showcase.gallery.json'); export const MANIFEST = join(GALLERY_DIR, 'gallery.manifest.json'); export const WEB_CONFIG = join(GALLERY_DIR, 'showcase.config.mjs'); export const TTY_CONFIG = join(GALLERY_DIR, 'showcase.tty.config.mjs'); +/** One config per hero layout, frame style and background, each a small change to the web or tty config. */ +export const VARIANTS_DIR = join(GALLERY_DIR, 'variants'); +/** Lists gallery images for the kit's own README; only `showcase readme` reads it. */ +export const README_CONFIG = join(GALLERY_DIR, 'readme.config.mjs'); +/** What `showcase readme` printed, one file per run: committed, because the pages import them. */ +export const SNIPPET_DIR = join(GALLERY_DIR, 'readme'); export const CLI = join(REPO_ROOT, 'dist', 'cli.js'); const MARK = join(SITE_DIR, 'src', 'assets', 'mark.svg'); const ICON_SOURCE = join(RAW_DIR, 'icon-source.png'); @@ -59,6 +68,61 @@ export const STEPS: readonly (readonly string[])[] = [ ['icons', '--source', ICON_SOURCE, '--preset', 'web', '--out', join(OUTPUT_DIR, 'icons')], ]; +/** + * The variant configs, by file name without `.mjs`. A `hero-` config runs `hero`; the others run `frame`, and a web + * one frames only `tonight`. None of them captures: they read the raw captures the steps above wrote. + */ +export const VARIANTS: readonly string[] = [ + 'hero-spotlight', + 'hero-split', + 'hero-row', + 'hero-mosaic', + 'hero-centered', + 'hero-mesh', + 'hero-dots', + 'hero-noise', + 'frame-minimal', + 'frame-none', + 'frame-browser', + 'frame-windows', + 'frame-terminal', + 'tty-windows', + 'tty-terminal', + 'background-solid', + 'background-mesh', + 'background-dots', + 'background-noise', +]; + +export const variantConfig = (name: string): string => join(VARIANTS_DIR, `${name}.mjs`); + +export const VARIANT_STEPS: readonly (readonly string[])[] = VARIANTS.map(name => + name.startsWith('hero-') + ? ['hero', '--config', variantConfig(name)] + : ['frame', '--config', variantConfig(name), ...(name.startsWith('tty-') ? [] : ['--only', 'tonight'])], +); + +/** The layouts `showcase readme --layout` takes, in the order the CLI lists them. */ +export const README_LAYOUTS = ['table', 'rows', 'featured', 'details', 'list'] as const; + +/** A `showcase readme` run and the file its standard output goes to. */ +export interface SnippetStep { + args: readonly string[]; + out: string; +} + +/** + * Each README layout of the web config's English images, then the table in the kit's own README. `--base ../..` is + * the repo root, where a README would sit, so every image path starts with `site/public/gallery/`. + */ +export const SNIPPET_STEPS: readonly SnippetStep[] = [ + ...README_LAYOUTS.map(layout => ({ + args: ['readme', '--config', WEB_CONFIG, '--layout', layout, '--base', '../..'], + out: join(SNIPPET_DIR, `${layout}.html`), + })), + { args: ['readme', '--config', README_CONFIG, '--base', '../..'], out: join(SNIPPET_DIR, 'kit-readme.html') }, +]; + /** Why the gallery cannot run yet, or undefined when the built CLI is there. */ export function missingBuild(cli: string = CLI): string | undefined { if (existsSync(cli)) return undefined; @@ -122,17 +186,32 @@ function portAnswers(port: number): Promise { let current: ReturnType | undefined; -/** Run the built CLI under Node (tty mode refuses the Bun runtime) and wait for it. */ -function runCli(args: readonly string[]): Promise { - console.log(`\n$ node dist/cli.js ${args.map(arg => (arg.startsWith(REPO_ROOT) ? relative(REPO_ROOT, arg) : arg)).join(' ')}`); +/** + * Run the built CLI under Node (tty mode refuses the Bun runtime) and wait for it. With `out`, its standard output + * goes to that file instead of the console, byte for byte. + */ +function runCli(args: readonly string[], out?: string): Promise { + const shown = args.map(arg => (arg.startsWith(REPO_ROOT) ? relative(REPO_ROOT, arg) : arg)).join(' '); + console.log(`\n$ node dist/cli.js ${shown}${out ? ` > ${relative(REPO_ROOT, out)}` : ''}`); return new Promise((resolve, reject) => { - const child = spawn('node', [CLI, ...args], { cwd: REPO_ROOT, stdio: 'inherit', windowsHide: true }); + const child = spawn('node', [CLI, ...args], { + cwd: REPO_ROOT, + stdio: ['inherit', out ? 'pipe' : 'inherit', 'inherit'], + windowsHide: true, + }); current = child; + const chunks: Buffer[] = []; + child.stdout?.on('data', (chunk: Buffer) => chunks.push(chunk)); child.once('error', reject); - child.once('exit', (code, signal) => { + child.once('close', (code, signal) => { current = undefined; - if (code === 0) resolve(); - else reject(new Error(`gallery: \`showcase ${args[0] ?? ''}\` failed (${signal ?? `exit code ${String(code)}`})`)); + if (code !== 0) { + reject(new Error(`gallery: \`showcase ${args[0] ?? ''}\` failed (${signal ?? `exit code ${String(code)}`})`)); + } else if (out) { + writeFile(out, Buffer.concat(chunks)).then(resolve, reject); + } else { + resolve(); + } }); }); } @@ -159,18 +238,22 @@ async function main(): Promise { // reaches this process, so pass it on. process.on('SIGTERM', () => current?.kill('SIGTERM')); - for (const path of [OUTPUT_DIR, RAW_DIR, GALLERY_JSON, MANIFEST]) await rm(path, { recursive: true, force: true }); + for (const path of [OUTPUT_DIR, RAW_DIR, GALLERY_JSON, MANIFEST, SNIPPET_DIR]) await rm(path, { recursive: true, force: true }); await renderIconSource(); for (const args of STEPS) await runCli(args); + for (const args of VARIANT_STEPS) await runCli(args); + await mkdir(SNIPPET_DIR, { recursive: true }); + for (const { args, out } of SNIPPET_STEPS) await runCli(args, out); if (await portAnswers(port)) throw new Error(`gallery: the fixture server is still running on 127.0.0.1:${String(port)}.`); const manifest = await buildManifest(OUTPUT_DIR); await writeFile(MANIFEST, `${JSON.stringify(manifest, null, 2)}\n`); - const extra = { + const extra: Record = { 'gallery/showcase.gallery.json': (await readFile(GALLERY_JSON)).length, 'gallery/gallery.manifest.json': (await readFile(MANIFEST)).length, }; + for (const { out } of SNIPPET_STEPS) extra[relative(SITE_DIR, out).split(sep).join('/')] = (await readFile(out)).length; const { lines, total } = budgetLines(manifest, extra); console.log('\nCommitted gallery files (under site/):'); for (const [path, bytes] of lines) console.log(` ${kb(bytes).padStart(10)} ${path}`); diff --git a/site/scripts/readme-images.test.ts b/site/scripts/readme-images.test.ts new file mode 100644 index 0000000..cdad8a1 --- /dev/null +++ b/site/scripts/readme-images.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from 'bun:test'; +import { mkdtempSync, readFileSync } from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { SNIPPET_DIR } from './gallery.ts'; +import { missingPublicImages, PUBLIC_URL, publicImageUrls, withPublicUrls } from './readme-images.ts'; +import { REPO_ROOT, SITE_DIR } from './site.ts'; + +const README = readFileSync(join(REPO_ROOT, 'README.md'), 'utf8'); +const PUBLIC_DIR = join(SITE_DIR, 'public'); + +describe('the repo README', () => { + test('shows gallery images, and every one is a file the site serves', () => { + expect(publicImageUrls(README).length).toBeGreaterThan(0); + expect(missingPublicImages(README, PUBLIC_DIR)).toEqual([]); + }); + + test('has the layouts table exactly as `showcase readme` printed it, with the served URLs', () => { + const snippet = readFileSync(join(SNIPPET_DIR, 'kit-readme.html'), 'utf8'); + expect(README).toContain(withPublicUrls(snippet).trimEnd()); + }); +}); + +describe('publicImageUrls', () => { + test('reads src attributes and Markdown images under the site, and nothing else', () => { + const markdown = [ + ``, + `![banner](${PUBLIC_URL}gallery/a.webp "title")`, + `![angle](<${PUBLIC_URL}gallery/b.webp>)`, + '', + `[a link, not an image](${PUBLIC_URL}gallery/c.webp)`, + ].join('\n'); + expect(publicImageUrls(markdown)).toEqual([ + `${PUBLIC_URL}gallery/hero.webp`, + `${PUBLIC_URL}gallery/a.webp`, + `${PUBLIC_URL}gallery/b.webp`, + ]); + }); +}); + +describe('missingPublicImages', () => { + test('names the URLs with no file under the public folder', async () => { + const dir = mkdtempSync(join(tmpdir(), 'readme-images-')); + await mkdir(join(dir, 'gallery'), { recursive: true }); + await writeFile(join(dir, 'gallery', 'there.webp'), ''); + const markdown = [ + ``, + ``, + ``, + ].join('\n'); + expect(missingPublicImages(markdown, dir)).toEqual([`${PUBLIC_URL}gallery/gone.webp`, PUBLIC_URL]); + }); +}); + +describe('withPublicUrls', () => { + test('turns repo-root paths under site/public/ into served URLs', () => { + expect(withPublicUrls('X')).toBe( + `X`, + ); + }); + + test('refuses a path the site does not serve', () => { + expect(() => withPublicUrls('')).toThrow('not under site/public/'); + }); +}); diff --git a/site/scripts/readme-images.ts b/site/scripts/readme-images.ts new file mode 100644 index 0000000..ad36753 --- /dev/null +++ b/site/scripts/readme-images.ts @@ -0,0 +1,42 @@ +/** + * The repo's README.md shows gallery images by absolute URL, so they render on + * npm as well as on GitHub. These checks keep every such URL pointing at a file + * the site serves, and keep the README's layouts table the kit's own output. + */ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { SITE_BASE, SITE_ORIGIN } from './site.ts'; + +/** Where site/public/ is served: `https://noctcore.github.io/showcase-kit/`. */ +export const PUBLIC_URL = `${SITE_ORIGIN}${SITE_BASE}/`; + +/** Where `showcase readme --base ../..` puts the gallery images, relative to the repo root. */ +const SNIPPET_PREFIX = 'site/public/'; + +/** Every `src="..."` and Markdown image URL in `markdown` that is served from PUBLIC_URL, in order. */ +export function publicImageUrls(markdown: string): string[] { + const urls = [ + ...markdown.matchAll(/\bsrc="([^"]*)"/g), + ...markdown.matchAll(/!\[[^\]]*\]\(\s*]+)/g), + ].map(match => match[1] ?? ''); + return urls.filter(url => url.startsWith(PUBLIC_URL)); +} + +/** The URLs among `publicImageUrls(markdown)` with no file under `publicDir`. */ +export function missingPublicImages(markdown: string, publicDir: string): string[] { + return publicImageUrls(markdown).filter(url => { + const path = decodeURI(url.slice(PUBLIC_URL.length).replace(/[?#].*$/, '')); + return path === '' || !existsSync(join(publicDir, ...path.split('/'))); + }); +} + +/** + * A snippet `showcase readme --base ../..` printed for a README at the repo root, with its image paths turned into + * the URLs the site serves them from. A path outside site/public/ is an error: the site would not serve it. + */ +export function withPublicUrls(snippet: string): string { + return snippet.replace(/\bsrc="([^"]*)"/g, (_match, src: string) => { + if (!src.startsWith(SNIPPET_PREFIX)) throw new Error(`readme: "${src}" is not under ${SNIPPET_PREFIX}, so the site does not serve it`); + return `src="${PUBLIC_URL}${src.slice(SNIPPET_PREFIX.length)}"`; + }); +} diff --git a/site/scripts/site.ts b/site/scripts/site.ts index d03ca5f..8d5d57d 100644 --- a/site/scripts/site.ts +++ b/site/scripts/site.ts @@ -49,6 +49,7 @@ export const STUB_MARKER = 'STUB(wave-2)'; * points at a page, without a fragment. */ export const CONTRACT_ANCHORS: Readonly> = { + 'gallery/': ['hero-banner', 'hero-layouts', 'frame-styles-and-backgrounds', 'readme-layouts'], 'reference/config/': ['top-level', 'target', 'shots', 'frame', 'outputs', 'hero', 'terminal', 'clips'], 'reference/cli/': ['options', 'capture', 'frame', 'portfolio', 'readme', 'record', 'all', 'hero', 'icons', 'init'], }; diff --git a/site/src/components/GalleryOption.astro b/site/src/components/GalleryOption.astro new file mode 100644 index 0000000..a22bee3 --- /dev/null +++ b/site/src/components/GalleryOption.astro @@ -0,0 +1,65 @@ +--- +/** + * One option in the gallery: its name, the image it produced, and the block of + * the config that produced it, cut from the file on disk. A `variant` is a file + * in site/gallery/variants/; the defaults pass the main config's `code` and `file`. + */ +import GalleryConfig from './GalleryConfig.astro'; +import GalleryImage from './GalleryImage.astro'; +import { variantConfig } from './GalleryVariants'; + +interface Props { + /** The option's value as a config writes it, for example `'spotlight'`. */ + name: string; + /** Path under site/public/gallery/. */ + file: string; + alt: string; + /** One line on what the option does. */ + caption?: string; + /** Marks the option the kit uses when the key is left out. */ + isDefault?: boolean; + /** A config in site/gallery/variants/, by name without `.mjs`. */ + variant?: string; + /** Otherwise the config's text (imported with `?raw`) and its path. */ + code?: string; + config?: string; + /** The config block to show, such as `hero` or `frame`. */ + section: string; +} + +const { name, file, alt, caption, isDefault = false, variant, section } = Astro.props; +const source = variant === undefined ? { code: Astro.props.code, file: Astro.props.config } : variantConfig(variant); +if (source.code === undefined || source.file === undefined) { + throw new Error(`gallery: the option "${name}" needs a variant, or code and config`); +} +--- + + + + diff --git a/site/src/components/GalleryReadmeLayout.astro b/site/src/components/GalleryReadmeLayout.astro new file mode 100644 index 0000000..5d3d0d3 --- /dev/null +++ b/site/src/components/GalleryReadmeLayout.astro @@ -0,0 +1,17 @@ +--- +/** One README layout: the snippet `showcase readme` printed, as a live preview and as the HTML itself. */ +import { Code } from '@astrojs/starlight/components'; +import { readmeSnippet } from './GalleryVariants'; +import ReadmePreview from './ReadmePreview.astro'; + +interface Props { + /** The `--layout` value, which is also the snippet's file name in site/gallery/readme/. */ + layout: string; +} + +const { layout } = Astro.props; +const snippet = readmeSnippet(layout); +--- + + + diff --git a/site/src/components/GalleryThumbs.astro b/site/src/components/GalleryThumbs.astro new file mode 100644 index 0000000..69a00d9 --- /dev/null +++ b/site/src/components/GalleryThumbs.astro @@ -0,0 +1,67 @@ +--- +/** + * A compact grid of gallery images for a guide: each one opens the full file + * (or `href`), with a short label under it. The gallery page has the configs. + */ +import { galleryFile } from './GalleryData'; + +interface Props { + items: { file: string; alt: string; label: string; href?: string }[]; + /** Images across on wide screens; two on a phone. */ + cols?: number; +} + +const { items, cols = 3 } = Astro.props; +const shown = items.map(item => ({ ...item, image: galleryFile(item.file) })); +--- + + + + diff --git a/site/src/components/GalleryVariants.ts b/site/src/components/GalleryVariants.ts new file mode 100644 index 0000000..86d13b7 --- /dev/null +++ b/site/src/components/GalleryVariants.ts @@ -0,0 +1,41 @@ +/** + * The variant configs in site/gallery/variants/ and the README snippets in + * site/gallery/readme/, as the files on disk. The page shows the config that + * made an image and the snippet the kit printed, never a copy of either. + */ +import { galleryFile, GALLERY_URL } from './GalleryData'; + +const variants = import.meta.glob('../../gallery/variants/*.mjs', { query: '?raw', import: 'default', eager: true }); +const snippets = import.meta.glob('../../gallery/readme/*.html', { query: '?raw', import: 'default', eager: true }); + +const REGENERATE = 'Run `bun run docs:gallery` from the repo root.'; + +/** A variant config's text and its path from the repo root, by file name without `.mjs`. */ +export function variantConfig(name: string): { code: string; file: string } { + const code = variants[`../../gallery/variants/${name}.mjs`]; + if (code === undefined) throw new Error(`gallery: no site/gallery/variants/${name}.mjs`); + return { code, file: `site/gallery/variants/${name}.mjs` }; +} + +/** What `showcase readme` printed to site/gallery/readme/.html. */ +export function readmeSnippet(name: string): { code: string; file: string } { + const code = snippets[`../../gallery/readme/${name}.html`]; + if (code === undefined) throw new Error(`gallery: no site/gallery/readme/${name}.html. ${REGENERATE}`); + return { code, file: `site/gallery/readme/${name}.html` }; +} + +/** Where `--base ../..` (the repo root) puts the gallery's files in a snippet. */ +const SNIPPET_GALLERY = 'site/public/gallery/'; + +/** + * A snippet with its image paths served from this site, for a live preview. Every image must be a gallery file in + * the manifest, so a snippet that points anywhere else fails the build. + */ +export function servedSnippet(code: string): string { + return code.replace(/\bsrc="([^"]*)"/g, (_match, src: string) => { + if (!src.startsWith(SNIPPET_GALLERY)) throw new Error(`gallery: a README snippet lists "${src}", outside ${SNIPPET_GALLERY}`); + const path = src.slice(SNIPPET_GALLERY.length); + galleryFile(path); + return `src="${GALLERY_URL}${path}"`; + }); +} diff --git a/site/src/components/GuideOutputs.astro b/site/src/components/GuideOutputs.astro index 30b21f9..4a52beb 100644 --- a/site/src/components/GuideOutputs.astro +++ b/site/src/components/GuideOutputs.astro @@ -14,13 +14,13 @@ const outputs = [ }, { name: 'Framed README images', - desc: 'Each capture in a window frame (title bar, rounded corners, soft shadow) on a solid or gradient background, as WebP or PNG.', + desc: 'Each capture in a window, browser or terminal frame on a solid, gradient, mesh, dotted or grainy background, as WebP or PNG.', command: 'showcase frame', guide: 'guides/frames/', }, { - name: 'README table', - desc: 'An HTML table of the framed images with captions, printed for you to paste into a README.', + name: 'README snippet', + desc: 'The framed images with captions as HTML to paste into a README: a table, rows, a featured image, collapsible details or a list.', command: 'showcase readme', guide: 'guides/readme-table/', }, @@ -32,7 +32,7 @@ const outputs = [ }, { name: 'Hero banner', - desc: 'Logo, name, tagline and up to three tilted framed shots, 1280x640 by default.', + desc: 'Logo, name, tagline and framed shots in one of six layouts, 1280x640 by default.', command: 'showcase hero', guide: 'guides/hero/', }, diff --git a/site/src/components/ReadmePreview.astro b/site/src/components/ReadmePreview.astro new file mode 100644 index 0000000..5643a88 --- /dev/null +++ b/site/src/components/ReadmePreview.astro @@ -0,0 +1,149 @@ +--- +/** + * A README snippet the kit printed, rendered live in a box styled like a + * GitHub README (GitHub's own table, `` and `
` rules, in its + * light and dark colors), with its image paths pointing at this gallery. + * `compact` shrinks the type for a guide. + */ +import { servedSnippet } from './GalleryVariants'; + +interface Props { + /** The snippet as printed (imported from site/gallery/readme/). */ + code: string; + /** Accessible name of the box, for example "Preview of the table layout". */ + label: string; + compact?: boolean; +} + +const { code, label, compact = false } = Astro.props; +const html = servedSnippet(code); +--- + +
+ +
+
+ + diff --git a/site/src/content/docs/gallery.mdx b/site/src/content/docs/gallery.mdx index effb401..a364870 100644 --- a/site/src/content/docs/gallery.mdx +++ b/site/src/content/docs/gallery.mdx @@ -7,7 +7,9 @@ import GalleryConfig from '../../components/GalleryConfig.astro'; import GalleryFiles from '../../components/GalleryFiles.astro'; import GalleryGrid from '../../components/GalleryGrid.astro'; import GalleryImage from '../../components/GalleryImage.astro'; +import GalleryOption from '../../components/GalleryOption.astro'; import GalleryPortfolio from '../../components/GalleryPortfolio.astro'; +import GalleryReadmeLayout from '../../components/GalleryReadmeLayout.astro'; import web from '../../../gallery/showcase.config.mjs'; import tty from '../../../gallery/showcase.tty.config.mjs'; import webCode from '../../../gallery/showcase.config.mjs?raw'; @@ -72,8 +74,8 @@ imports it: ## Hero banner -`showcase hero` stacks up to three framed shots, tilted, next to the logo, the name and the tagline, at the 1280x640 -GitHub social preview size. It renders from the raw captures, like the portfolio. The logo is this site's own mark. +`showcase hero` in its default `stack` layout stacks up to three framed shots, tilted, next to the logo, the name and +the tagline, at the 1280x640 GitHub social preview size. It renders from the raw captures, like the portfolio. The logo is this site's own mark. See the [hero guide](/showcase-kit/guides/hero/). +## Hero layouts + +The same banner in each of the six `hero.layout` values. Every image from here down comes from a variant config in +`site/gallery/variants/`: the web config above with one block changed, and `root: '..'` so its paths still resolve +from `site/gallery/`. `showcase hero` and `showcase frame` render from the raw captures, so the variants capture +nothing again. The block under each image is cut from the file that made it. The layouts that show one window +also pick the one shot, since the config lists three. See [layouts in the hero guide](/showcase-kit/guides/hero/). + + + + + + + + + + +### Hero backgrounds + +`hero.background` takes every form `frame.background` does and defaults to it, which is the gradient in the banner +above. Here is the default `stack` on the three newer types. + + + + + + + +## Frame styles and backgrounds + +`frame.style` sets the window chrome and `frame.background` what it sits on, for the README images, the portfolio, +the hero and clips alike. These variants frame only the English `tonight` shot, at `maxWidth: 1200` and +`quality: 80` for a tile here (the README images above are 1600 pixels wide). See the +[frames guide](/showcase-kit/guides/frames/). + +### Web app + + + + + + + + + + +### Terminal app + +The terminal shot from the tty config, framed three ways. `browser` is not among them: a terminal app has no +address, so a tty config with `style: 'browser'` fails to load with `"browser" needs a page with an address; a +terminal app has none (use "terminal" or "window")`. + + + + + + + +### Backgrounds + +The web config's own background is a gradient, the same type as the kit's default (which is teal to indigo). The +other forms are below; `transparent` is left out, since here it would only show this page's background. + + + + + + + + + +## README layouts + +`showcase readme --layout` prints the same images, captions and alt text in five arrangements. The gallery script +runs it once per layout with the web config, from the repo root: + +```sh wrap +showcase readme --config site/gallery/showcase.config.mjs --layout --base ../.. +``` + +Each box below renders the snippet the kit printed, committed as it came out under `site/gallery/readme/`, with its +image paths pointing at this gallery's files. The box is styled like a GitHub README, and the HTML is under it. +GitHub strips CSS and most attributes from a README, so every layout uses only the tags and attributes it keeps. +Each layout was checked through GitHub's own Markdown renderer (its `markdown` API, in `gfm` mode) when it was added, +and again for this page: every tag and attribute survives. See [layouts in the README guide](/showcase-kit/guides/readme-table/). + +### `table` (default) + +A row of images and a row of `` captions under it, two per row unless `--cols` says otherwise. + + + +### `rows` + +One small table per image: the image in a 60% cell beside its title (as `

`) and caption, alternating sides. + + + +### `featured` + +The first image full width with its caption, then the others in a table, two per row unless `--cols` says +otherwise. + + + +### `details` + +One collapsible `
` 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. + + + ## Terminal shot and clip A second config runs `tui.mjs` in a pseudo terminal at 100x22 cells. The shot presses `j` once and waits for the @@ -130,7 +418,11 @@ under Node with an argument list (no shell), one command at a time: 2. `hero` with the web config; 3. `all --only queue` with the tty config: the terminal shot; 4. `record` with the tty config: the clip; -5. `icons` with the `web` preset. +5. `icons` with the `web` preset; +6. `hero` or `frame` with each config in `site/gallery/variants/`, from the raw captures of steps 1 and 3; +7. `readme --layout ` with the web config, once per layout, each written to `site/gallery/readme/.html`; +8. `readme` with `site/gallery/readme.config.mjs`, a config that only lists gallery images: the table in the kit's + own README, written to `site/gallery/readme/kit-readme.html`. The kit starts the fixture server itself and stops its whole process tree when a command ends, and the script checks that the port is free again afterwards. Last, it writes `site/gallery/gallery.manifest.json` (the size, pixel size and diff --git a/site/src/content/docs/getting-started.mdx b/site/src/content/docs/getting-started.mdx index 3b5483a..380a179 100644 --- a/site/src/content/docs/getting-started.mdx +++ b/site/src/content/docs/getting-started.mdx @@ -105,8 +105,9 @@ The CLI is called `showcase`. The commands below use `npx showcase`; `pnpm exec npx showcase readme ``` - It prints an HTML table of the framed images with their captions. Paste it into your README, or - redirect it to a file: the table goes to standard output and warnings to standard error. + It prints an HTML table of the framed images with their captions (`--layout` picks one of four + other arrangements). Paste it into your README, or redirect it to a file: the table goes to + standard output and warnings to standard error. diff --git a/site/src/content/docs/guides/frames.mdx b/site/src/content/docs/guides/frames.mdx index 664b6e7..8fed1df 100644 --- a/site/src/content/docs/guides/frames.mdx +++ b/site/src/content/docs/guides/frames.mdx @@ -4,6 +4,7 @@ description: Turn raw captures into README images inside a window, browser or te --- import { Aside } from '@astrojs/starlight/components'; +import GalleryThumbs from '../../../components/GalleryThumbs.astro'; `showcase frame` puts each raw capture inside a window frame (a title bar, rounded corners, a soft shadow) on a background, and writes it to `outputs.readme`. `showcase all` runs it after the capture. @@ -41,6 +42,20 @@ config reference under [`frame`](/showcase-kit/reference/config/#frame) and | `'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. | +Each style on Nightjar, the fixture app of the docs gallery. Select one to open the file; the +[gallery](/showcase-kit/gallery/#frame-styles-and-backgrounds) has the config that made each. + + + `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. @@ -69,6 +84,16 @@ is an error there; write the address instead. A terminal app has no address, so 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. +The gallery's terminal app in `window` (the default), `terminal` and `windows`: + + + `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. @@ -94,6 +119,19 @@ background: { type: 'noise', from: '#2a1f6b', to: '#0b3b4c', angle: 135, amount: - **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. +Each form on Nightjar, the fixture app of the docs gallery; the +[gallery](/showcase-kit/gallery/#frame-styles-and-backgrounds) has the config that made each: + + + 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 diff --git a/site/src/content/docs/guides/hero.mdx b/site/src/content/docs/guides/hero.mdx index 48f8e0b..89110d7 100644 --- a/site/src/content/docs/guides/hero.mdx +++ b/site/src/content/docs/guides/hero.mdx @@ -4,6 +4,7 @@ description: Render a banner for the top of a README, and a GitHub social previe --- import { Aside } from '@astrojs/starlight/components'; +import GalleryThumbs from '../../../components/GalleryThumbs.astro'; `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 @@ -45,6 +46,17 @@ npx showcase hero # writes assets/showcase/hero.webp - **Text color.** `theme` defaults to `frame.theme`: `'dark'` gives light text, `'light'` gives dark text. Match it to the background, not the app. +The default `stack` on a `mesh`, a `dots` and a `noise` background, from the docs +[gallery](/showcase-kit/gallery/#hero-layouts), which has the config of each. Select one to open the file. + + + The banner is rendered at twice its size and scaled down to exactly `size`, for sharper text and edges on the tilted windows. @@ -61,6 +73,20 @@ on the tilted windows. | `'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. | +Each layout with Nightjar, the fixture app of the docs gallery. Select one to open the file; the +[gallery](/showcase-kit/gallery/#hero-layouts) has the config that made each. + + + ```js hero: { layout: 'spotlight', diff --git a/site/src/content/docs/guides/readme-table.mdx b/site/src/content/docs/guides/readme-table.mdx index 35c3e4c..74a1790 100644 --- a/site/src/content/docs/guides/readme-table.mdx +++ b/site/src/content/docs/guides/readme-table.mdx @@ -1,11 +1,16 @@ --- title: README table -description: Print an HTML table of the framed images with captions, ready to paste into a README. +description: Print the framed images with captions as README HTML, a table or one of four other layouts, ready to paste into a README. --- +import { TabItem, Tabs } from '@astrojs/starlight/components'; +import { readmeSnippet } from '../../../components/GalleryVariants'; +import ReadmePreview from '../../../components/ReadmePreview.astro'; + `showcase readme` prints an HTML table of the framed images, a row of images and a row of `` captions under it, with image paths relative to your README. Run it after `showcase all` (or -`showcase frame`) and paste the output into the README. +`showcase frame`) and paste the output into the README. `--layout` picks one of four other +arrangements of the same images (see [Layouts](#layouts)). ## A minimal run @@ -81,6 +86,28 @@ checked through GitHub's own Markdown renderer. | `details` | One collapsible `
` 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. | +Each layout as the kit printed it for Nightjar, the fixture app of the docs gallery, rendered here the +way a GitHub README shows it. The [gallery](/showcase-kit/gallery/#readme-layouts) has the HTML of each +and the command that printed it. + + + + + + + + + + + + + + + + + + + `--cols` only means something for `table` and `featured`; with another layout it is an error. ```sh diff --git a/site/src/content/docs/index.mdx b/site/src/content/docs/index.mdx index 8d09e93..0545abe 100644 --- a/site/src/content/docs/index.mdx +++ b/site/src/content/docs/index.mdx @@ -20,6 +20,7 @@ hero: variant: minimal --- +import GalleryThumbs from '../../components/GalleryThumbs.astro'; import GuideOutputs from '../../components/GuideOutputs.astro';
@@ -50,14 +51,27 @@ or newer and Playwright's Chromium. DevTools Protocol either, so there you capture the app's web build instead ([Tauri](/showcase-kit/guides/tauri/) explains the trade-off). The kit also cannot make a changing app look the same twice on its own: if every run shows new data, the images change with it until you give -the app a fixture mode. And clips (animated recordings) are for terminal apps only in 0.2.0. +the app a fixture mode. And clips (animated recordings) are for terminal apps only, so far. ## What one config produces `showcase all` runs capture, frame, then the portfolio export when it is configured, then the clips when -there are any. The README table, the hero banner and the icons are their own commands. +there are any. The README snippet, the hero banner and the icons are their own commands. + +Six hero layouts, six frame styles, six kinds of background and five README layouts, all shown in the +[gallery](/showcase-kit/gallery/#hero-layouts) with the config that made each. Four of the hero layouts, for Nightjar: + + ## A minimal config diff --git a/site/src/content/docs/reference/cli.mdx b/site/src/content/docs/reference/cli.mdx index d665988..005caac 100644 --- a/site/src/content/docs/reference/cli.mdx +++ b/site/src/content/docs/reference/cli.mdx @@ -63,8 +63,8 @@ Exports `outputs.portfolio`: one image per shot, `thumbnail.`, and the g -Prints an HTML table of the framed images, with the captions under them in ``, to paste into a README. Clips -follow the shots. Image paths are relative to `--base`, which defaults to the config root. +Prints the framed images as HTML to paste into a README: by default a table, with the captions under them in +``, or with `--layout` the `rows`, `featured`, `details` or `list` layout. Clips follow the shots. Image paths are relative to `--base`, which defaults to the config root. ## record