Skip to content
Open
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
12 changes: 8 additions & 4 deletions packages/theme/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Generate a Slidev deck with `slidev-theme-tahta`. **No CSS, `<style>`, grids, or
21. Mark tangents with `aside: true` (or `aside: "label"`): an optional deep-dive / 'under the hood' detour gets a left accent rail + a corner tag so the audience knows it's off the main spine. Use it for the curiosity slides, not the core argument.
22. Math: write `$inline$` and `$$block$$` LaTeX in a slide BODY (default/statement/two-cols/columns markdown) — Slidev renders it with KaTeX in the variant's type. Frontmatter title/field text is injected as HTML and does NOT run KaTeX, with one exception: the `define` layout renders math in its `definition`/`points`. So for a 'term = formula' slide use `define`; for heavier math, put it in a body.
23. Sections give you wayfinding for free: when a deck has `layout: section` dividers, a thin progress rail at the top edge shows the audience which part they're in. Group a longer talk into 3–6 sections so the rail (and the deck) reads as a structured arc.
24. Treat technical diagram images as content, not photography. Put a full-canvas diagram in `layout: diagram` with `<Figure src="..." />` so the entire image stays visible. If a screenshot or diagram must sit beside text in `image` or `showcase`, set `fit: contain`; those layouts otherwise use `cover` and intentionally crop.

## Deck header (first slide)
```yaml
Expand Down Expand Up @@ -87,8 +88,8 @@ Per-slide frontmatter available on every layout.
| `logos` | A trust / logo wall. | kicker, title, columns, logos* |
| `code` | Code, optionally Magic Move. Body is a fenced code block. | kicker, title |
| `two-cols` | Generic split; left = body, right = after ::right::. Each column is a canvas — compose components in either. | kicker, title |
| `image` | Text + a side image (markdown body is the text column). | kicker, title, image*, side |
| `showcase` | Asymmetric image hero (fixed 43/57; deterministic). | kicker, title, subtitle, image*, side |
| `image` | Text + a side image (markdown body is the text column). Photos default to cover; use fit: contain for screenshots or diagrams. | kicker, title, image*, fit, side |
| `showcase` | Asymmetric image hero (fixed 43/57; deterministic). Photos default to cover; use fit: contain for screenshots or diagrams. | kicker, title, subtitle, image*, fit, side |
| `bleed` | Full-bleed image hero with overlaid text. | image*, kicker, stat, title, subtitle, duotone |
| `embed` | Video or iframe. | kicker, title, video, iframe |
| `end` | Closing slide. | title, subtitle, contact |
Expand Down Expand Up @@ -382,18 +383,20 @@ title: Before vs after
```

### `image`
Text + a side image (markdown body is the text column).
Text + a side image (markdown body is the text column). Photos default to cover; use fit: contain for screenshots or diagrams.

- `kicker` (string, optional)
- `title` (string, optional)
- `image` (string, **required**) — Path in the deck's public/ folder, e.g. /shot.png.
- `fit` (enum, optional) — How the image fits its pane. Defaults to cover; use contain when every edge and label must remain visible. — one of `cover | contain`
- `side` (enum, optional) — one of `left | right`

```yaml
---
layout: image
side: right
image: /shot.png
fit: contain
title: The product
---

Expand All @@ -402,12 +405,13 @@ title: The product
```

### `showcase`
Asymmetric image hero (fixed 43/57; deterministic).
Asymmetric image hero (fixed 43/57; deterministic). Photos default to cover; use fit: contain for screenshots or diagrams.

- `kicker` (string, optional)
- `title` (string, optional)
- `subtitle` (string, optional)
- `image` (string, **required**)
- `fit` (enum, optional) — How the image fits its pane. Defaults to cover; use contain when every edge and label must remain visible. — one of `cover | contain`
- `side` (enum, optional) — one of `left | right`

```yaml
Expand Down
1 change: 1 addition & 0 deletions packages/theme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ Variant / accent / lang are applied at runtime by `global-bottom.vue`; defaults

## Gotchas baked into the contract
- Images for `bleed`/`image`/`showcase` (and `bg: /x.jpg`) go in the deck's `public/`, referenced as `/name.jpg` — use the layout's `image:` field, don't `<img>` them.
- `image` and `showcase` crop with `cover` by default. Set `fit: contain` for screenshots and diagrams, or use `diagram` with `<Figure>` when the visual should own the slide.
- In YAML flow rows `{ ... }`, quote any value containing a comma/colon (`"$4,200"`).
- `cover`/`section`/`statement`/`end` take the title from frontmatter — leave the body empty.

Expand Down
11 changes: 7 additions & 4 deletions packages/theme/layouts.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"Add speaker notes as an HTML comment that is the LAST block of a slide's markdown body: `<!-- what you'll say out loud -->`. It never renders on the slide; tela surfaces it as the presenter note. Author notes for any talk you'll actually present — the deck is the slides plus what you say.",
"Mark tangents with `aside: true` (or `aside: \"label\"`): an optional deep-dive / 'under the hood' detour gets a left accent rail + a corner tag so the audience knows it's off the main spine. Use it for the curiosity slides, not the core argument.",
"Math: write `$inline$` and `$$block$$` LaTeX in a slide BODY (default/statement/two-cols/columns markdown) — Slidev renders it with KaTeX in the variant's type. Frontmatter title/field text is injected as HTML and does NOT run KaTeX, with one exception: the `define` layout renders math in its `definition`/`points`. So for a 'term = formula' slide use `define`; for heavier math, put it in a body.",
"Sections give you wayfinding for free: when a deck has `layout: section` dividers, a thin progress rail at the top edge shows the audience which part they're in. Group a longer talk into 3–6 sections so the rail (and the deck) reads as a structured arc."
"Sections give you wayfinding for free: when a deck has `layout: section` dividers, a thin progress rail at the top edge shows the audience which part they're in. Group a longer talk into 3–6 sections so the rail (and the deck) reads as a structured arc.",
"Treat technical diagram images as content, not photography. Put a full-canvas diagram in `layout: diagram` with `<Figure src=\"...\" />` so the entire image stays visible. If a screenshot or diagram must sit beside text in `image` or `showcase`, set `fit: contain`; those layouts otherwise use `cover` and intentionally crop."
],
"layouts": [
{ "id": "cover", "useFor": "Title / opening slide.", "fields": [
Expand Down Expand Up @@ -126,17 +127,19 @@
{ "name": "kicker", "type": "string", "required": false },
{ "name": "title", "type": "string", "required": false } ],
"example": "layout: two-cols\ntitle: Before vs after\n---\n\n**Before** — one region.\n\n::right::\n\n**After** — 18 regions." },
{ "id": "image", "useFor": "Text + a side image (markdown body is the text column).", "fields": [
{ "id": "image", "useFor": "Text + a side image (markdown body is the text column). Photos default to cover; use fit: contain for screenshots or diagrams.", "fields": [
{ "name": "kicker", "type": "string", "required": false },
{ "name": "title", "type": "string", "required": false },
{ "name": "image", "type": "string", "required": true, "description": "Path in the deck's public/ folder, e.g. /shot.png." },
{ "name": "fit", "type": "enum", "required": false, "enum": ["cover", "contain"], "description": "How the image fits its pane. Defaults to cover; use contain when every edge and label must remain visible." },
{ "name": "side", "type": "enum", "required": false, "enum": ["left", "right"] } ],
"example": "layout: image\nside: right\nimage: /shot.png\ntitle: The product\n---\n\n- A media column plus a text column" },
{ "id": "showcase", "useFor": "Asymmetric image hero (fixed 43/57; deterministic).", "fields": [
"example": "layout: image\nside: right\nimage: /shot.png\nfit: contain\ntitle: The product\n---\n\n- A media column plus a text column" },
{ "id": "showcase", "useFor": "Asymmetric image hero (fixed 43/57; deterministic). Photos default to cover; use fit: contain for screenshots or diagrams.", "fields": [
{ "name": "kicker", "type": "string", "required": false },
{ "name": "title", "type": "string", "required": false },
{ "name": "subtitle", "type": "string", "required": false },
{ "name": "image", "type": "string", "required": true },
{ "name": "fit", "type": "enum", "required": false, "enum": ["cover", "contain"], "description": "How the image fits its pane. Defaults to cover; use contain when every edge and label must remain visible." },
{ "name": "side", "type": "enum", "required": false, "enum": ["left", "right"] } ],
"example": "layout: showcase\nside: right\nimage: /shot.png\nkicker: The product\ntitle: Finally <span class=\"accent2\">visible</span>\nsubtitle: One line of config." },
{ "id": "bleed", "useFor": "Full-bleed image hero with overlaid text.", "fields": [
Expand Down
16 changes: 12 additions & 4 deletions packages/theme/layouts/image.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,25 @@ import { computed } from 'vue'
import { useSlideContext } from '@slidev/client'
const { $frontmatter } = useSlideContext()
const side = computed(() => $frontmatter.side || 'right')
const bg = computed(() => ({ backgroundImage: `url(${$frontmatter.image})` }))
const fit = computed(() => $frontmatter.fit === 'contain' ? 'contain' : 'cover')
const bg = computed(() => ({
backgroundImage: `url(${$frontmatter.image})`,
backgroundSize: fit.value,
}))
</script>

<template>
<div :class="['slidev-layout', 'l-split', side === 'left' ? 'split left' : 'split']">
<SlideBg />
<div v-if="side === 'left'" class="split-media" :style="bg" />
<div class="split-body">
<div v-if="$frontmatter.kicker" class="kicker">{{ $frontmatter.kicker }}</div>
<h2 v-if="$frontmatter.title" class="fs-h2 mb-4" v-html="$frontmatter.title" />
<div class="fs-body dim"><slot /></div>
<Fit>
<div>
<div v-if="$frontmatter.kicker" class="kicker">{{ $frontmatter.kicker }}</div>
<h2 v-if="$frontmatter.title" class="fs-h2 mb-4" v-html="$frontmatter.title" />
<div class="fs-body dim"><slot /></div>
</div>
</Fit>
</div>
<div v-if="side !== 'left'" class="split-media" :style="bg" />
<Foot />
Expand Down
6 changes: 5 additions & 1 deletion packages/theme/layouts/showcase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { computed } from 'vue'
import { useSlideContext } from '@slidev/client'
const { $frontmatter } = useSlideContext()
const side = computed(() => $frontmatter.side || 'right')
const bg = computed(() => ({ backgroundImage: `url(${$frontmatter.image})` }))
const fit = computed(() => $frontmatter.fit === 'contain' ? 'contain' : 'cover')
const bg = computed(() => ({
backgroundImage: `url(${$frontmatter.image})`,
backgroundSize: fit.value,
}))
</script>
<template>
<div :class="['slidev-layout', 'l-split', side === 'left' ? 'showcase flip' : 'showcase']">
Expand Down
11 changes: 7 additions & 4 deletions packages/theme/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
/* ===== showcase (deliberate asymmetric hero — fixed ratio, won't reflow with content) ===== */
.showcase { display: grid; grid-template-columns: 43% 57%; height: 100%; }
.showcase.flip { grid-template-columns: 57% 43%; }
.showcase-media { position: relative; background-size: cover; background-position: center; }
.showcase-media { position: relative; background-size: cover; background-position: center; background-repeat: no-repeat; }
.showcase-media::after { content: ""; position: absolute; inset: 0; box-shadow: inset 0 0 160px rgba(0,0,0,0.45); }
.showcase-body { display: flex; flex-direction: column; justify-content: center; padding: var(--pad-y) var(--pad-x); }

Expand Down Expand Up @@ -258,7 +258,7 @@
/* ===== image split ===== */
.split { display: grid; grid-template-columns: 1fr 1fr; height: 100%; }
.split.left { grid-template-columns: 0.9fr 1.1fr; }
.split-media { background-size: cover; background-position: center; position: relative;
.split-media { background-size: cover; background-position: center; background-repeat: no-repeat; position: relative;
/* feather the seam edge so the image dissolves into the body ground instead of a hard cut */
--feather: 13%;
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--feather)); mask-image: linear-gradient(to right, transparent 0, #000 var(--feather)); }
Expand Down Expand Up @@ -410,8 +410,8 @@
.filetree .ft-guide { color: color-mix(in oklab, var(--fg-dim) 55%, transparent); }

/* ----- Figure (image / diagram + caption + credit) ----- */
.tahta-figure { margin: 0; }
.tahta-figure img { display: block; width: 100%; border-radius: var(--radius-sm); border: var(--surface-border); }
.tahta-figure { margin: 0; max-width: 100%; max-height: 100%; }
.tahta-figure img { display: block; width: 100%; max-width: 100%; height: auto; max-height: 100%; object-fit: contain; border-radius: var(--radius-sm); border: var(--surface-border); }
.tahta-figure figcaption { margin-top: var(--sp-3); font-size: 0.92rem; color: var(--fg-dim); display: flex; justify-content: space-between; gap: var(--sp-4); align-items: baseline; }
.tahta-figure .tf-credit { font: 500 0.76rem/1.4 var(--font-mono); opacity: 0.8; white-space: nowrap; }

Expand Down Expand Up @@ -448,6 +448,9 @@
derived from these same tokens. Here we only size the light-DOM host to the stage. */
.diagram-stage .mermaid { display: flex; align-items: center; justify-content: center; max-width: 100%; max-height: 60vh; }
.diagram-stage .mermaid svg { max-width: 100%; max-height: 60vh; height: auto; }
.diagram-stage .tahta-figure { width: 100%; height: 100%; min-width: 0; min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.diagram-stage .tahta-figure img { width: auto; }
.diagram-stage .tahta-figure:has(figcaption) img { max-height: calc(100% - 2.5rem); }
.diagram-note { margin: var(--sp-4) auto 0; max-width: 60ch; text-align: center; font-size: 0.95rem; }

/* ===== Grid — cell layout for memory/byte/row-vs-column/matrix visuals ===== */
Expand Down