mdeck 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.2.0 — 2026-10-07
4
+
5
+ - Pictures written in the text fit on the slide in every theme: a paragraph of nothing but pictures shrinks into the room below the heading instead of running off the slide, keeping its proportions (small pictures keep their size), and several pictures in one paragraph sit side by side. Such paragraphs have the class `pictures`, for themes that set them their own way; a picture in a sentence is left alone.
6
+ - Tables have room below them in every theme, so a sentence after a table no longer sits right against it.
7
+ - `mdeck themes build` also renders an icon for each theme: its title slide at 96×54 (drawn at twice that, about 3 KB), listed as `icon` in `catalogue.json` and `looks.json`. The theme repository's gallery shows it in its list instead of the palette's colours, which said little about a theme.
8
+
3
9
  ## 3.1.0 — 2026-10-07
4
10
 
5
11
  - Packs are gone: every theme and palette is installed on its own. `mdeck themes install duet` installs the theme with its default palette (`cobalt`), and `mdeck palettes install solarized` a palette; both have `search`, `remove`, `update` and `list`. A palette made for one theme (FHNW's `brand`) brings that theme along and goes with it. A palette an installed theme uses by default is not removed while that theme is there. Installs go into `themes/<id>/` and `palettes/<id>/` of the extensions folder, so a theme and a palette may share a name.
package/assets/base.css CHANGED
@@ -162,11 +162,35 @@ deck-stage[data-ink-hidden] .slide-ink { display: none; }
162
162
  /* ── Code blocks ─────────────────────────────────────────────────────────── */
163
163
  .code-block { white-space: pre-wrap; overflow-x: hidden; }
164
164
 
165
+ /* ── Pictures written in the text ───────────────────────────────────────── */
166
+ /* A paragraph of nothing but pictures (marked in markedSetup.js) fits into
167
+ the room the slide has left: a large picture shrinks, keeping its
168
+ proportions, and never runs off the slide; a small one keeps its size.
169
+ Several sit side by side. Themes may set them differently (a grid, cropped
170
+ to fill); a picture in a sentence is left alone. */
171
+ .slide-body p.pictures {
172
+ display: flex;
173
+ align-items: center;
174
+ gap: 16px;
175
+ flex: 0 1 auto;
176
+ min-height: 0;
177
+ }
178
+ .slide-body p.pictures > :is(img, .themed-picture) {
179
+ flex: 0 1 auto;
180
+ min-width: 0;
181
+ min-height: 0;
182
+ max-width: 100%;
183
+ max-height: 100%;
184
+ object-fit: contain;
185
+ }
186
+
165
187
  /* ── Tables ──────────────────────────────────────────────────────────────── */
166
188
  .slide-body table {
167
189
  border-collapse: collapse;
168
190
  width: 100%;
169
191
  font-size: var(--fs-body);
192
+ /* Room before what follows, such as a sentence about the numbers. */
193
+ margin-bottom: calc(var(--gap-item, 24px) * 1.5);
170
194
  }
171
195
  .slide-body th,
172
196
  .slide-body td {
@@ -594,6 +594,19 @@ Reference images with standard Markdown syntax or the `image:` frontmatter field
594
594
  ![Alt text](./img/diagram.png)
595
595
  ```
596
596
 
597
+ A paragraph of nothing but pictures fits into the room the slide has left: a
598
+ large picture shrinks, keeping its proportions, and a small one keeps its
599
+ size. Several pictures in one paragraph sit side by side:
600
+
601
+ ```markdown
602
+ # Three mornings
603
+
604
+ ![Fjord](./img/fjord.jpg) ![Mountains](./img/mountains.jpg) ![Snow](./img/snow.jpg)
605
+ ```
606
+
607
+ A picture inside a sentence stays in the sentence. Themes may set picture
608
+ paragraphs their own way (Lightbox fills a grid edge to edge).
609
+
597
610
  A local SVG whose source contains `var(--…)` is drawn inline instead of as an
598
611
  `<img>`, both in slide text (`![…](./img/drawing.svg)` or `<img src>`) and as a
599
612
  layout's `image` (`title`, `chapter`, `image-text`, `full-bleed-image`, and
@@ -246,7 +246,9 @@ touch files changed since then unless given `--force`.
246
246
 
247
247
  The repository serves `catalogue.json` (`{ schema: 2, themes: [...],
248
248
  palettes: [...] }`, each with its id, version, default palette or theme,
249
- `url`, `sha256`, `preview`, and `builtIn` for those that come with mdeck) and
249
+ `url`, `sha256`, `preview`, for a theme an `icon` (its title slide at
250
+ 96×54, drawn at twice that for sharp screens), and `builtIn` for those that
251
+ come with mdeck) and
250
252
  one JSON file per version holding the text of its files.
251
253
  `mdeck themes build <repository> -o <out> [--check]` writes both from a
252
254
  folder with `themes/` and `palettes/`; `MDECK_THEMES_URL` points mdeck at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdeck",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "type": "module",
5
5
  "description": "Write slides as a plain text file, present them in the browser, share them as one HTML file or PDF.",
6
6
  "license": "MIT",
@@ -147,15 +147,25 @@ export function checkLooks(looks, { extensions = {} } = {}) {
147
147
  })
148
148
  }
149
149
 
150
+ // The size of a theme's icon in lists, in CSS pixels; drawn at twice that
151
+ // for sharp screens.
152
+ export const ICON = { width: 96, height: 54, scale: 2 }
153
+
150
154
  // Pictures of looks on the sample deck, as WebP: { file, theme, palette,
151
- // slide } each, slide counting from 1.
155
+ // slide, icon } each, slide counting from 1. `icon`, if given, is a second
156
+ // file with the same slide at icon size: small enough for a list, and still
157
+ // showing the theme's type, colours and shapes.
152
158
  export function renderPreviews(shots, { extensions = {}, width = 960, height = 540 } = {}) {
153
159
  return withSampleDeck({ extensions }, async ({ open }) => {
154
- for (const { file, theme, palette = '', slide = 1 } of shots) {
160
+ for (const { file, theme, palette = '', slide = 1, icon = null } of shots) {
155
161
  const page = await open({ theme, palette })
156
162
  if (slide > 1) { await page.evaluate(`document.querySelector('deck-stage').goTo(${slide - 1})`); await settle(page) }
157
163
  mkdirSync(resolve(file, '..'), { recursive: true })
158
164
  writeFileSync(file, await page.screenshot({ width, height, format: 'webp' }))
165
+ if (icon) {
166
+ mkdirSync(resolve(icon, '..'), { recursive: true })
167
+ writeFileSync(icon, await page.screenshot({ ...ICON, format: 'webp' }))
168
+ }
159
169
  await page.close()
160
170
  }
161
171
  })
package/src/cli/themes.js CHANGED
@@ -25,11 +25,14 @@ const usage = kind => `search [words] | install <${kind}> | remove <${kind}> | u
25
25
  function looksOf(registry, catalogue) {
26
26
  const builtIn = new Set([...catalogue.themes, ...catalogue.palettes].filter(entry => entry.builtIn).map(entry => entry.id + (catalogue.themes.includes(entry) ? ':t' : ':p')))
27
27
  const palettes = manifestsOf(registry, 'palette')
28
+ // Each theme's icon for the gallery's list (see renderPreviews).
29
+ const icons = new Map(catalogue.themes.map(entry => [entry.id, entry.icon]))
28
30
  const colours = tokens => Object.fromEntries(['--bg', '--surface', '--ink', '--accent', '--accent-2'].map(key => [key, tokens?.[key] ?? null]))
29
31
  return {
30
32
  mdeck: VERSION,
31
33
  themes: Object.values(manifestsOf(registry, 'theme')).map(theme => ({ id: theme.id, title: theme.title, description: theme.description ?? '', builtIn: builtIn.has(theme.id + ':t'),
32
- palette: theme.palette, appearance: theme.appearance ?? 'light', palettes: palettesFor(theme, palettes).map(p => p.id) })),
34
+ palette: theme.palette, appearance: theme.appearance ?? 'light', palettes: palettesFor(theme, palettes).map(p => p.id),
35
+ ...(icons.get(theme.id) ? { icon: icons.get(theme.id) } : {}) })),
33
36
  palettes: Object.values(palettes).map(palette => ({ id: palette.id, title: palette.title, description: palette.description ?? '', builtIn: builtIn.has(palette.id + ':p'),
34
37
  ...(palette.theme ? { theme: palette.theme } : {}), light: colours(palette.light), dark: colours(palette.dark) })),
35
38
  }
@@ -148,8 +151,11 @@ export async function runPackages({ kind, positionals, flag, output, ui: { ok, e
148
151
  ok(`Checked ${catalogue.themes.length} themes and ${catalogue.palettes.length} palettes, light and dark, on every kind of slide`)
149
152
  }
150
153
  // Themes on the title slide, palettes on the chapter slide (the third).
151
- await renderPreviews(looks.map(look => ({ file: resolve(out, 'previews', `${look.key}.webp`), theme: look.theme, palette: look.palette ?? '', slide: look.palette ? 3 : 1 })), { extensions })
152
- for (const entry of catalogue.themes) entry.preview = `previews/themes/${entry.id}.webp`
154
+ // Themes also get an icon of their title slide for lists; a palette's
155
+ // colours are its icon.
156
+ await renderPreviews(looks.map(look => ({ file: resolve(out, 'previews', `${look.key}.webp`), theme: look.theme, palette: look.palette ?? '', slide: look.palette ? 3 : 1,
157
+ ...(look.palette ? {} : { icon: resolve(out, 'previews', `${look.key}-icon.webp`) }) })), { extensions })
158
+ for (const entry of catalogue.themes) Object.assign(entry, { preview: `previews/themes/${entry.id}.webp`, icon: `previews/themes/${entry.id}-icon.webp` })
153
159
  for (const entry of catalogue.palettes) entry.preview = `previews/palettes/${entry.id}.webp`
154
160
  writeFileSync(resolve(out, 'catalogue.json'), JSON.stringify(catalogue, null, 2) + '\n')
155
161
  // For the repository's theme browser: the sample deck with every theme
@@ -143,5 +143,12 @@ marked.use({
143
143
  .replace(/>/g, '&gt;')
144
144
  return `<codeblock lang="${lang}"${attrs ? ' ' + attrs : ''}>${encoded}</codeblock>`
145
145
  },
146
+ // A paragraph of nothing but pictures (one, or several side by side) is
147
+ // marked, so themes can fit it into the slide or set it as a row; a
148
+ // sentence with an icon in it stays a sentence.
149
+ paragraph({ tokens }) {
150
+ const pictures = tokens.some(t => t.type === 'image') && tokens.every(t => t.type === 'image' || t.type === 'br' || (t.type === 'text' && !t.raw.trim()))
151
+ return `<p${pictures ? ' class="pictures"' : ''}>${this.parser.parseInline(tokens)}</p>\n`
152
+ },
146
153
  },
147
154
  })