mdeck 2.3.0 → 3.1.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 +22 -1
- package/README.md +9 -9
- package/assets/base.css +4 -1
- package/assets/extensions/palettes/lagoon/extension.toml +3 -0
- package/assets/extensions/palettes/neon/extension.toml +3 -0
- package/assets/extensions/palettes/nordic/extension.toml +3 -0
- package/assets/extensions/palettes/paper/extension.toml +30 -0
- package/assets/extensions/palettes/swiss/extension.toml +3 -0
- package/assets/extensions/themes/academic/extension.toml +3 -0
- package/assets/extensions/themes/aurora/extension.toml +3 -0
- package/assets/extensions/themes/minimal/extension.toml +48 -0
- package/assets/extensions/themes/minimal/styles.css +342 -0
- package/assets/extensions/themes/neue/extension.toml +3 -0
- package/docs/reference/authoring.md +13 -18
- package/docs/reference/claude-skill.md +1 -1
- package/docs/reference/extensions.md +40 -26
- package/docs/reference/palettes.md +8 -12
- package/docs/reference/themes.md +8 -7
- package/docs/site/content/appearance.md +35 -49
- package/docs/site/content/commands.md +3 -2
- package/docs/site/content/theme-authoring.md +1 -1
- package/docs/site/content/theme-repository.md +44 -32
- package/docs/site/examples/first-talk.md +1 -1
- package/docs/site/images/themes/academic-dark.webp +0 -0
- package/docs/site/images/themes/academic-light.webp +0 -0
- package/docs/site/images/themes/academic.webp +0 -0
- package/docs/site/images/themes/aurora-dark.webp +0 -0
- package/docs/site/images/themes/aurora-light.webp +0 -0
- package/docs/site/images/themes/aurora.webp +0 -0
- package/docs/site/images/themes/minimal-dark.webp +0 -0
- package/docs/site/images/themes/minimal-light.webp +0 -0
- package/docs/site/images/themes/minimal.webp +0 -0
- package/docs/site/images/themes/neue-dark.webp +0 -0
- package/docs/site/images/themes/neue-light.webp +0 -0
- package/docs/site/images/themes/neue.webp +0 -0
- package/docs/site/pages.js +1 -1
- package/examples/custom-layouts/slides.md +1 -1
- package/examples/poll/slides.md +1 -1
- package/examples/python/slides.md +2 -2
- package/examples/showcase/slides.md +3 -3
- package/package.json +2 -2
- package/skills/write-slides/SKILL.md +2 -2
- package/src/build/check.js +0 -7
- package/src/build/editorPlugin.js +26 -13
- package/src/build/themeCheck.js +17 -6
- package/src/cli/args.js +2 -1
- package/src/cli/index.js +7 -6
- package/src/cli/themes.js +117 -87
- package/src/core/validateDeck.js +4 -2
- package/src/editor/api.js +15 -6
- package/src/editor/components/App.jsx +4 -2
- package/src/editor/components/DesignApp.jsx +16 -9
- package/src/editor/components/ExtensionEditor.jsx +16 -6
- package/src/editor/components/ExtensionsPanel.jsx +28 -34
- package/src/editor/designLink.js +18 -1
- package/src/editor/sampleDeck.js +3 -1
- package/src/extensions/appearance.js +2 -1
- package/src/extensions/discover.js +44 -11
- package/src/extensions/installed.js +88 -0
- package/src/extensions/manifest.js +20 -3
- package/src/extensions/moved.js +20 -0
- package/src/extensions/packages.js +318 -0
- package/src/home/Home.jsx +12 -6
- package/src/layouts/layoutApi.jsx +10 -2
- package/src/runtime/themeLoader.js +5 -2
- package/assets/extensions/palettes/brand/extension.toml +0 -29
- package/assets/extensions/palettes/cobalt/extension.toml +0 -27
- package/assets/extensions/palettes/ember/extension.toml +0 -27
- package/assets/extensions/palettes/forest/extension.toml +0 -27
- package/assets/extensions/palettes/graphite/extension.toml +0 -27
- package/assets/extensions/palettes/pastel/extension.toml +0 -30
- package/assets/extensions/palettes/phosphor/extension.toml +0 -27
- package/assets/extensions/palettes/terra/extension.toml +0 -27
- package/assets/extensions/themes/duet/extension.toml +0 -46
- package/assets/extensions/themes/duet/styles.css +0 -573
- package/assets/extensions/themes/editorial/extension.toml +0 -46
- package/assets/extensions/themes/editorial/styles.css +0 -574
- package/assets/extensions/themes/fhnw/extension.toml +0 -61
- package/assets/extensions/themes/fhnw/styles.css +0 -604
- package/assets/extensions/themes/sketch/extension.toml +0 -48
- package/assets/extensions/themes/sketch/styles.css +0 -633
- package/assets/extensions/themes/terminal/extension.toml +0 -38
- package/assets/extensions/themes/terminal/styles.css +0 -550
- package/docs/site/images/themes/duet-dark.webp +0 -0
- package/docs/site/images/themes/duet-light.webp +0 -0
- package/docs/site/images/themes/duet.webp +0 -0
- package/docs/site/images/themes/editorial-dark.webp +0 -0
- package/docs/site/images/themes/editorial-light.webp +0 -0
- package/docs/site/images/themes/editorial.webp +0 -0
- package/docs/site/images/themes/fhnw.webp +0 -0
- package/docs/site/images/themes/sketch-dark.webp +0 -0
- package/docs/site/images/themes/sketch-light.webp +0 -0
- package/docs/site/images/themes/sketch.webp +0 -0
- package/docs/site/images/themes/terminal-dark.webp +0 -0
- package/docs/site/images/themes/terminal-light.webp +0 -0
- package/docs/site/images/themes/terminal.webp +0 -0
- package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T08-36-47Z.json +0 -19
- package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T11-27-30Z.json +0 -6
- package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T08-08-30Z.json +0 -10
- package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T11-41-35Z.json +0 -13
- package/src/extensions/packs.js +0 -268
package/src/build/check.js
CHANGED
|
@@ -26,13 +26,6 @@ export function checkDeck(slidesPath, registry, source = readFileSync(slidesPath
|
|
|
26
26
|
if (record.source !== 'local') continue
|
|
27
27
|
for (const problem of paletteProblems(record.manifest)) diagnostics.push({ severity: 'warning', code: 'palette-contrast', message: `Palette "${record.id}", ${problem}`, line: 1, column: 1 })
|
|
28
28
|
}
|
|
29
|
-
// A theme or palette installed for this user only: the slide file needs it
|
|
30
|
-
// beside it to work on another computer.
|
|
31
|
-
const config = deck.deckConfig ?? {}
|
|
32
|
-
for (const [kind, id] of [['theme', config.theme], ['palette', config.palette]]) {
|
|
33
|
-
const record = id && registry[`${kind}s`][id]
|
|
34
|
-
if (record?.source === 'user') diagnostics.push({ severity: 'warning', code: 'user-extension', message: `The ${kind} "${id}" is installed only on this computer (${dirname(record.dir)}); copy it into the deck's extensions folder, for example with mdeck themes install, so the slide file works elsewhere`, line: 1, column: 1 })
|
|
35
|
-
}
|
|
36
29
|
// The deck's drawings file: readable, and only for slides that still exist.
|
|
37
30
|
const inkPath = inkFileFor(resolve(slidesPath))
|
|
38
31
|
const renamed = drawingsDiagnostic(slidesPath)
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
import { createHash } from 'node:crypto'
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, unlinkSync } from 'node:fs'
|
|
7
7
|
import { resolve, basename, dirname, relative } from 'node:path'
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { loadRegistry, removedBuiltIns, serializeRegistry, userExtensionsDir } from '../extensions/discover.js'
|
|
9
|
+
import { findEntry, installedPackages, keyOf, loadCatalogue, PackageError } from '../extensions/packages.js'
|
|
10
|
+
import { installHere, removeHere, target } from '../extensions/installed.js'
|
|
10
11
|
import { createRequire } from 'node:module'
|
|
11
12
|
import { ManifestError, KINDS, ID_RE, MANIFEST_FILENAME, parseManifestText, validateManifest } from '../extensions/manifest.js'
|
|
12
13
|
|
|
@@ -180,7 +181,6 @@ export function createExtensionStore(deckDir, { registryFor }) {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
export function editorMiddleware(deckFile, { registryFor, extensions = createExtensionStore(dirname(deckFile.path), { registryFor }) }) {
|
|
183
|
-
const extensionsDir = resolve(dirname(deckFile.path), 'extensions')
|
|
184
184
|
return async (request, response, next) => {
|
|
185
185
|
const pathname = new URL(request.url, 'http://localhost').pathname
|
|
186
186
|
if (!isAllowedRequest(request)) return send(response, 403, { error: 'Requests are only accepted from this editor on this computer' })
|
|
@@ -201,18 +201,31 @@ export function editorMiddleware(deckFile, { registryFor, extensions = createExt
|
|
|
201
201
|
response.setHeader('Allow', 'GET, PUT, DELETE')
|
|
202
202
|
return send(response, 405, { error: 'Method not allowed' })
|
|
203
203
|
}
|
|
204
|
-
//
|
|
205
|
-
// deck
|
|
206
|
-
if (pathname === '/themes/
|
|
207
|
-
const
|
|
208
|
-
|
|
204
|
+
// Themes and palettes: what mdeck and the repository offer, and
|
|
205
|
+
// installing or removing one for every deck (as mdeck themes does).
|
|
206
|
+
if (pathname === '/themes/catalogue' && request.method === 'GET') {
|
|
207
|
+
const catalogue = await loadCatalogue()
|
|
208
|
+
const hidden = removedBuiltIns()
|
|
209
|
+
const installed = installedPackages(userExtensionsDir())
|
|
210
|
+
const entries = catalogue.entries.map(({ dir, ...entry }) => {
|
|
211
|
+
const key = keyOf(entry.kind, entry.id)
|
|
212
|
+
const removed = entry.builtIn && hidden.includes(key)
|
|
213
|
+
return { ...entry, removed, available: Boolean(installed[key]) || (entry.builtIn && !removed), ...(entry.preview ? { preview: new URL(entry.preview, catalogue.url).href } : {}) }
|
|
214
|
+
})
|
|
215
|
+
return send(response, 200, { url: catalogue.url, offline: catalogue.offline, entries })
|
|
209
216
|
}
|
|
210
217
|
if (pathname === '/themes/install' && request.method === 'POST') {
|
|
211
218
|
const body = await readJson(request)
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
const kind = String(body?.kind ?? ''), id = String(body?.id ?? '')
|
|
220
|
+
const catalogue = await loadCatalogue()
|
|
221
|
+
findEntry(catalogue, kind, id)
|
|
222
|
+
const steps = await installHere(catalogue, kind, id, target(null, false), { mdeckVersion: VERSION })
|
|
223
|
+
return send(response, 200, { steps, registry: serializeRegistry(registryFor()) })
|
|
224
|
+
}
|
|
225
|
+
if (pathname === '/themes/remove' && request.method === 'POST') {
|
|
226
|
+
const body = await readJson(request)
|
|
227
|
+
const removed = removeHere(String(body?.kind ?? ''), String(body?.id ?? ''), target(null, false))
|
|
228
|
+
return send(response, 200, { removed, registry: serializeRegistry(registryFor()) })
|
|
216
229
|
}
|
|
217
230
|
if (pathname === '/deck' && request.method === 'GET') {
|
|
218
231
|
const registry = registryFor()
|
|
@@ -230,7 +243,7 @@ export function editorMiddleware(deckFile, { registryFor, extensions = createExt
|
|
|
230
243
|
if (['/deck', '/source'].includes(pathname)) { response.setHeader('Allow', pathname === '/deck' ? 'GET' : 'POST'); return send(response, 405, { error: 'Method not allowed' }) }
|
|
231
244
|
return next()
|
|
232
245
|
} catch (error) {
|
|
233
|
-
if (error instanceof
|
|
246
|
+
if (error instanceof PackageError) return send(response, 400, { error: error.message })
|
|
234
247
|
if (error instanceof ManifestError) return send(response, 500, { error: error.message })
|
|
235
248
|
return send(response, error.status ?? 500, { error: error.message })
|
|
236
249
|
}
|
package/src/build/themeCheck.js
CHANGED
|
@@ -11,6 +11,7 @@ import { resolve } from 'node:path'
|
|
|
11
11
|
import { launchChrome } from './chrome.js'
|
|
12
12
|
import { frameworkRoot } from '../paths.js'
|
|
13
13
|
import { sampleDeck, SAMPLE_PICTURE } from '../editor/sampleDeck.js'
|
|
14
|
+
import { loadRegistry } from '../extensions/discover.js'
|
|
14
15
|
|
|
15
16
|
// Runs in the page before the deck: keeps what the page reports.
|
|
16
17
|
const RECORDER = `window.__report = [];
|
|
@@ -80,18 +81,28 @@ const MEASURE = `(() => {
|
|
|
80
81
|
return problems
|
|
81
82
|
})()`
|
|
82
83
|
|
|
83
|
-
// Builds the sample deck with `extensions` ({ id: folder }) beside it
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
export
|
|
84
|
+
// Builds the sample deck with `extensions` ({ id: folder }) beside it into
|
|
85
|
+
// `outDir`, as a folder to host. Installed user extensions are left out, so
|
|
86
|
+
// the result depends only on what is given. Returns the registry it used.
|
|
87
|
+
export function buildSampleDeck({ extensions = {}, outDir }) {
|
|
87
88
|
const temp = mkdtempSync(resolve(tmpdir(), 'mdeck-sample-'))
|
|
88
89
|
try {
|
|
89
90
|
writeFileSync(resolve(temp, 'slides.md'), sampleDeck())
|
|
90
91
|
writeFileSync(resolve(temp, 'picture.svg'), SAMPLE_PICTURE)
|
|
91
|
-
|
|
92
|
+
// Keyed by id, or by "kind:id" to keep a theme and a palette of one name apart.
|
|
93
|
+
for (const [key, dir] of Object.entries(extensions)) cpSync(dir, resolve(temp, 'extensions', ...key.split(':').map((part, i, all) => all.length > 1 && i === 0 ? `${part}s` : part)), { recursive: true })
|
|
92
94
|
mkdirSync(resolve(temp, 'home'))
|
|
93
|
-
execFileSync(process.execPath, [resolve(frameworkRoot, 'bin/mdeck.js'), 'build', resolve(temp, 'slides.md'), '-o', resolve(
|
|
95
|
+
execFileSync(process.execPath, [resolve(frameworkRoot, 'bin/mdeck.js'), 'build', resolve(temp, 'slides.md'), '-o', resolve(outDir, 'index.html')],
|
|
94
96
|
{ cwd: temp, stdio: 'pipe', env: { ...process.env, MDECK_HOME: resolve(temp, 'home') } })
|
|
97
|
+
return loadRegistry(resolve(temp, 'slides.md'), { userRoot: null })
|
|
98
|
+
} finally { rmSync(temp, { recursive: true, force: true }) }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The sample deck built with `extensions`, opened in Chrome for `use({ open })`.
|
|
102
|
+
export async function withSampleDeck({ extensions = {} } = {}, use) {
|
|
103
|
+
const temp = mkdtempSync(resolve(tmpdir(), 'mdeck-sample-'))
|
|
104
|
+
try {
|
|
105
|
+
buildSampleDeck({ extensions, outDir: resolve(temp, 'out') })
|
|
95
106
|
const browser = await launchChrome({ dir: resolve(temp, 'out') })
|
|
96
107
|
try {
|
|
97
108
|
// A page with the deck in a look, recording what it reports.
|
package/src/cli/args.js
CHANGED
|
@@ -7,7 +7,8 @@ const commands = {
|
|
|
7
7
|
run: { ...common, '--network': flag, '--server': optional, max: 1 },
|
|
8
8
|
edit: { ...common, max: 1 },
|
|
9
9
|
design: { ...common, max: 1 },
|
|
10
|
-
themes: { '--
|
|
10
|
+
themes: { '--local': flag, '--force': flag, '--check': flag, '--output': value, '-o': '--output', max: 4 },
|
|
11
|
+
palettes: { '--local': flag, '--force': flag, max: 4 },
|
|
11
12
|
build: { ...output, '--single-file': flag, '--launchers': flag, '--reader': flag, '--notes': flag, max: 1 },
|
|
12
13
|
send: { ...output, '--notes': flag, '--no-pdf': flag, max: 1 },
|
|
13
14
|
pdf: { ...output, max: 1 },
|
package/src/cli/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { createPairing, pairingPlugin } from '../build/pairing.js'
|
|
|
19
19
|
import { isAllowedRequest } from '../build/editorPlugin.js'
|
|
20
20
|
import { renderPdf, attachPdf, findChrome } from '../build/pdf.js'
|
|
21
21
|
import { installSkill, readSkill, TARGETS } from './skill.js'
|
|
22
|
-
import {
|
|
22
|
+
import { runPackages } from './themes.js'
|
|
23
23
|
import { ManifestError, KINDS } from '../extensions/manifest.js'
|
|
24
24
|
|
|
25
25
|
import { frameworkRoot } from '../paths.js'
|
|
@@ -313,9 +313,10 @@ const HELP = `
|
|
|
313
313
|
${c.green}mdeck edit${c.reset} [slides.md] Edit slides in the browser; saves to the file
|
|
314
314
|
${c.green}mdeck design${c.reset} [folder | slides.md] Look at and fine-tune themes and palettes on a sample deck;
|
|
315
315
|
saves to the folder's extensions/ (default: this folder)
|
|
316
|
-
${c.green}mdeck themes${c.reset} search [words] Themes
|
|
317
|
-
install <
|
|
318
|
-
|
|
316
|
+
${c.green}mdeck themes${c.reset} search [words] Themes from mdeck and the theme repository; install <theme>
|
|
317
|
+
install <theme>, remove <theme> brings its default palette along; for every deck, or add
|
|
318
|
+
update [theme], list slides.md or --local for one deck; --force replaces edited files
|
|
319
|
+
${c.green}mdeck palettes${c.reset} search | install | remove | update | list The same for palettes
|
|
319
320
|
|
|
320
321
|
${c.dim}Make something to hand out${c.reset}
|
|
321
322
|
${c.green}mdeck build${c.reset} [slides.md] [-o dir/index.html] A folder to host
|
|
@@ -613,8 +614,8 @@ if (command === 'new') {
|
|
|
613
614
|
tip(`Changes are saved to the file as you type. Before the first change, a copy goes to ${BACKUP_DIR}/ beside it.\n`)
|
|
614
615
|
|
|
615
616
|
// ── themes ────────────────────────────────────────────────────────────────────
|
|
616
|
-
} else if (command === 'themes') {
|
|
617
|
-
await
|
|
617
|
+
} else if (command === 'themes' || command === 'palettes') {
|
|
618
|
+
await runPackages({ kind: command.slice(0, -1), positionals: positionals(), flag: name => hasFlag(name), output: outputOption, ui: { ok, err, tip, c } })
|
|
618
619
|
|
|
619
620
|
// ── design ────────────────────────────────────────────────────────────────────
|
|
620
621
|
// The design page: with a deck, beside its editor; with a folder (or nothing),
|
package/src/cli/themes.js
CHANGED
|
@@ -1,140 +1,170 @@
|
|
|
1
|
-
// `mdeck themes
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
1
|
+
// `mdeck themes` and `mdeck palettes`: install, update and remove themes and
|
|
2
|
+
// palettes, each on its own. Some come with mdeck, the others from the theme
|
|
3
|
+
// repository. A theme brings its default palette along, a palette that
|
|
4
|
+
// belongs to one theme brings that theme. They are installed for every deck
|
|
5
|
+
// (~/.mdeck/extensions) unless a deck or folder is named, or --local is
|
|
6
|
+
// given: then into the extensions folder beside it, so the slide folder
|
|
7
|
+
// carries them. A built-in one is removed by hiding it and installed again
|
|
8
|
+
// from mdeck's own copy, so that works offline. `mdeck themes build` is for
|
|
9
|
+
// the repository itself.
|
|
10
|
+
import { existsSync, statSync, writeFileSync } from 'node:fs'
|
|
6
11
|
import { createRequire } from 'node:module'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
12
|
+
import { resolve } from 'node:path'
|
|
13
|
+
import { loadRegistry, manifestsOf, removedBuiltIns, userExtensionsDir } from '../extensions/discover.js'
|
|
14
|
+
import { target, installHere, removeHere } from '../extensions/installed.js'
|
|
15
|
+
import { palettesFor } from '../extensions/tokens.js'
|
|
9
16
|
import { ManifestError } from '../extensions/manifest.js'
|
|
10
|
-
import { buildRepository, compareVersions,
|
|
17
|
+
import { buildRepository, compareVersions, findEntry, installedPackages, keyOf, loadCatalogue, PackageError } from '../extensions/packages.js'
|
|
11
18
|
|
|
12
19
|
const VERSION = createRequire(import.meta.url)('../../package.json').version
|
|
13
20
|
|
|
14
|
-
const
|
|
21
|
+
const usage = kind => `search [words] | install <${kind}> | remove <${kind}> | update [${kind}] | list — for every deck, or beside a deck with [slides.md or folder] or --local${kind === 'theme' ? '; build <repository> -o <out> [--check]' : ''}`
|
|
15
22
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
// Every theme with the palettes it offers, and every palette's colours, each
|
|
24
|
+
// marked as built in or from the repository: for the theme browser.
|
|
25
|
+
function looksOf(registry, catalogue) {
|
|
26
|
+
const builtIn = new Set([...catalogue.themes, ...catalogue.palettes].filter(entry => entry.builtIn).map(entry => entry.id + (catalogue.themes.includes(entry) ? ':t' : ':p')))
|
|
27
|
+
const palettes = manifestsOf(registry, 'palette')
|
|
28
|
+
const colours = tokens => Object.fromEntries(['--bg', '--surface', '--ink', '--accent', '--accent-2'].map(key => [key, tokens?.[key] ?? null]))
|
|
29
|
+
return {
|
|
30
|
+
mdeck: VERSION,
|
|
31
|
+
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) })),
|
|
33
|
+
palettes: Object.values(palettes).map(palette => ({ id: palette.id, title: palette.title, description: palette.description ?? '', builtIn: builtIn.has(palette.id + ':p'),
|
|
34
|
+
...(palette.theme ? { theme: palette.theme } : {}), light: colours(palette.light), dark: colours(palette.dark) })),
|
|
35
|
+
}
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
export async function runThemes({ positionals, flag, output, ui: { ok, err, tip, c } }) {
|
|
38
|
+
export async function runPackages({ kind, positionals, flag, output, ui: { ok, err, tip, c } }) {
|
|
39
|
+
const kinds = `${kind}s`
|
|
34
40
|
const [sub = 'search', ...rest] = positionals
|
|
35
|
-
const
|
|
41
|
+
const local = flag('--local'), force = flag('--force')
|
|
42
|
+
const warnOffline = catalogue => { if (catalogue.offline) tip(`The theme repository could not be reached (${catalogue.offline}); showing the ${kinds} that come with mdeck.`) }
|
|
43
|
+
const label = step => `the ${step.kind} ${step.id}`
|
|
36
44
|
try {
|
|
37
45
|
if (sub === 'search') {
|
|
38
|
-
const
|
|
46
|
+
const catalogue = await loadCatalogue()
|
|
47
|
+
warnOffline(catalogue)
|
|
39
48
|
const words = rest.map(word => word.toLowerCase())
|
|
40
|
-
const
|
|
41
|
-
const found =
|
|
42
|
-
if (!found.length) return tip(`No
|
|
43
|
-
for (const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
const forEvery = installedPackages(userExtensionsDir()), here = installedPackages(resolve('extensions')), hidden = removedBuiltIns()
|
|
50
|
+
const found = catalogue.entries.filter(entry => entry.kind === kind && words.every(word => [entry.id, entry.title, entry.description, entry.palette ?? '', entry.theme ?? ''].join(' ').toLowerCase().includes(word)))
|
|
51
|
+
if (!found.length) return tip(`No ${kind} matches${words.length ? ` "${words.join(' ')}"` : ''}`)
|
|
52
|
+
for (const entry of found) {
|
|
53
|
+
const key = keyOf(kind, entry.id)
|
|
54
|
+
const have = here[key] ?? forEvery[key]
|
|
55
|
+
const state = have ? (compareVersions(entry.version, have.version ?? '0.0.0') > 0 ? ` ${c.yellow}(${have.version} installed, update available)${c.reset}` : ` ${c.green}(installed)${c.reset}`)
|
|
56
|
+
: entry.builtIn ? (hidden.includes(key) ? ` ${c.dim}(comes with mdeck, removed)${c.reset}` : ` ${c.green}(comes with mdeck)${c.reset}`)
|
|
57
|
+
: entry.builtInVersion ? ` ${c.yellow}(comes with mdeck as ${entry.builtInVersion}, update available)${c.reset}` : ''
|
|
58
|
+
console.log(`\n ${c.cyan}${entry.id}${c.reset} ${entry.version} — ${entry.title}${state}`)
|
|
59
|
+
if (entry.description) console.log(` ${entry.description}`)
|
|
60
|
+
const facts = [kind === 'theme' ? `palette: ${entry.palette}` : entry.theme ? `only for the theme ${entry.theme}` : '', entry.author ? `by ${entry.author}` : '', entry.license].filter(Boolean)
|
|
61
|
+
if (facts.length) console.log(` ${c.dim}${facts.join(' · ')}${c.reset}`)
|
|
49
62
|
}
|
|
50
63
|
console.log()
|
|
51
|
-
return tip(
|
|
64
|
+
return tip(`Install one for every deck with: mdeck ${kinds} install <${kind}> (beside a deck: add slides.md or --local)`)
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
if (sub === 'install') {
|
|
55
68
|
const [id, where] = rest
|
|
56
|
-
if (!id) { err(
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
if (!id) { err(`Name a ${kind}.`); return tip(`Usage: mdeck ${kinds} install <${kind}> [slides.md or folder] [--local]; mdeck ${kinds} search lists them.`) }
|
|
70
|
+
const at = target(where, local)
|
|
71
|
+
const catalogue = await loadCatalogue()
|
|
72
|
+
warnOffline(catalogue)
|
|
73
|
+
findEntry(catalogue, kind, id)
|
|
74
|
+
for (const step of await installHere(catalogue, kind, id, at, { mdeckVersion: VERSION, force })) {
|
|
75
|
+
const what = step.requested ? label(step) : `${label(step)}, which it needs,`
|
|
76
|
+
if (step.kept) tip(`${what[0].toUpperCase() + what.slice(1)} is there already`)
|
|
77
|
+
else if (step.builtIn) step.wasRemoved || step.requested ? ok(`${label(step)[0].toUpperCase() + label(step).slice(1)} comes with mdeck${step.wasRemoved ? ' and is offered again' : ''}`) : null
|
|
78
|
+
else ok(`${step.replaced ? `Updated ${label(step)} from ${step.replaced} to ${step.version}` : `Installed ${label(step)} ${step.version}`} ${at.label}`)
|
|
79
|
+
}
|
|
80
|
+
tip(`Use it with "${kind}: ${id}" at the top of a slide file. Look at it on a sample deck: mdeck design`)
|
|
81
|
+
return
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
if (sub === 'remove') {
|
|
68
85
|
const [id, where] = rest
|
|
69
|
-
if (!id) { err(
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
86
|
+
if (!id) { err(`Name a ${kind}.`); return tip(`Usage: mdeck ${kinds} remove <${kind}> [slides.md or folder] [--local]`) }
|
|
87
|
+
const at = target(where, local)
|
|
88
|
+
for (const step of removeHere(kind, id, at, { force })) {
|
|
89
|
+
ok(step.hidden ? `${label(step)[0].toUpperCase() + label(step).slice(1)} comes with mdeck and is no longer offered; mdeck ${step.kind}s install ${step.id} brings it back` : `Removed ${label(step)} ${at.label}`)
|
|
90
|
+
}
|
|
91
|
+
return
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
if (sub === 'update') {
|
|
76
95
|
const [first, second] = rest
|
|
77
96
|
const named = first && !/\.md$/i.test(first) && !(existsSync(first) && statSync(first).isDirectory()) ? first : null
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
97
|
+
const at = target(named ? second : first, local)
|
|
98
|
+
const catalogue = await loadCatalogue()
|
|
99
|
+
warnOffline(catalogue)
|
|
100
|
+
const installed = Object.values(installedPackages(at.root)).filter(item => item.kind === kind)
|
|
101
|
+
const ids = named ? [named] : installed.map(item => item.id)
|
|
102
|
+
if (!ids.length) return tip(`No ${kinds} installed ${at.label}`)
|
|
83
103
|
for (const id of ids) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
const have = installed.find(item => item.id === id)
|
|
105
|
+
if (!have) throw new PackageError(`The ${kind} ${id} is not installed ${at.label}`)
|
|
106
|
+
const entry = findEntry(catalogue, kind, id)
|
|
107
|
+
if (entry.builtIn || compareVersions(entry.version, have.version ?? '0.0.0') <= 0) { tip(`The ${kind} ${id} ${have.version} is up to date`); continue }
|
|
108
|
+
await installHere(catalogue, kind, id, at, { mdeckVersion: VERSION, force })
|
|
109
|
+
ok(`Updated the ${kind} ${id} ${have.version} → ${entry.version}`)
|
|
89
110
|
}
|
|
90
|
-
loadRegistry(deck)
|
|
111
|
+
loadRegistry(at.deck)
|
|
91
112
|
return
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
if (sub === 'list') {
|
|
95
116
|
const [where] = rest
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
const hidden = removedBuiltIns()
|
|
118
|
+
const catalogue = await loadCatalogue({ online: false })
|
|
119
|
+
console.log(`\n ${c.bold}Come with mdeck${c.reset}`)
|
|
120
|
+
for (const entry of catalogue.entries.filter(entry => entry.kind === kind)) console.log(` ${c.cyan}${entry.id}${c.reset}${hidden.includes(keyOf(kind, entry.id)) ? ` ${c.dim}(removed; mdeck ${kinds} install ${entry.id} brings it back)${c.reset}` : ''}`)
|
|
121
|
+
const sections = [['For every deck', userExtensionsDir()], ...(where || local ? [['Beside the deck', target(where, true).root]] : [])]
|
|
122
|
+
for (const [title, root] of sections) {
|
|
123
|
+
const items = Object.values(installedPackages(root)).filter(item => item.kind === kind)
|
|
124
|
+
console.log(`\n ${c.bold}${title}${c.reset} ${c.dim}${root}${c.reset}`)
|
|
125
|
+
if (!items.length) console.log(` ${c.dim}none${c.reset}`)
|
|
126
|
+
for (const item of items) console.log(` ${c.cyan}${item.id}${c.reset} ${item.version}`)
|
|
101
127
|
}
|
|
102
128
|
console.log()
|
|
103
129
|
return
|
|
104
130
|
}
|
|
105
131
|
|
|
106
|
-
if (sub === 'build') {
|
|
107
|
-
const [
|
|
132
|
+
if (sub === 'build' && kind === 'theme') {
|
|
133
|
+
const [repoDir] = rest
|
|
108
134
|
const out = output()
|
|
109
|
-
if (!
|
|
110
|
-
const
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
if (!repoDir || !out) { err('Name the repository folder and the output folder.'); return tip('Usage: mdeck themes build <repository folder> -o <output folder> [--check]') }
|
|
136
|
+
const { catalogue, dirs } = buildRepository(resolve(repoDir), resolve(out))
|
|
137
|
+
// The repository's own themes and palettes go beside the sample deck;
|
|
138
|
+
// the built-in ones are in it already.
|
|
139
|
+
const repo = [...catalogue.themes.map(e => ({ ...e, kind: 'theme' })), ...catalogue.palettes.map(e => ({ ...e, kind: 'palette' }))].filter(entry => !entry.builtIn)
|
|
140
|
+
const extensions = Object.fromEntries(repo.map(entry => [keyOf(entry.kind, entry.id), dirs[keyOf(entry.kind, entry.id)]]))
|
|
141
|
+
const { checkLooks, renderPreviews, buildSampleDeck } = await import('../build/themeCheck.js')
|
|
142
|
+
const looks = [...catalogue.themes, ...catalogue.palettes.map(p => ({ ...p, isPalette: true }))].map(entry => entry.isPalette
|
|
143
|
+
? { key: `palettes/${entry.id}`, theme: entry.theme ?? 'neue', palette: entry.id }
|
|
144
|
+
: { key: `themes/${entry.id}`, theme: entry.id })
|
|
119
145
|
if (flag('--check')) {
|
|
120
146
|
const failures = await checkLooks(looks, { extensions })
|
|
121
147
|
if (failures.length) { err(`The theme check found ${failures.length} problem(s):`); for (const line of failures) console.error(` ${line}`); process.exitCode = 1; return }
|
|
122
|
-
ok(`Checked ${
|
|
148
|
+
ok(`Checked ${catalogue.themes.length} themes and ${catalogue.palettes.length} palettes, light and dark, on every kind of slide`)
|
|
123
149
|
}
|
|
124
150
|
// Themes on the title slide, palettes on the chapter slide (the third).
|
|
125
|
-
await renderPreviews(looks.map(look => ({ file: resolve(out, 'previews', `${look.
|
|
126
|
-
for (const
|
|
127
|
-
|
|
128
|
-
|
|
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`
|
|
153
|
+
for (const entry of catalogue.palettes) entry.preview = `previews/palettes/${entry.id}.webp`
|
|
154
|
+
writeFileSync(resolve(out, 'catalogue.json'), JSON.stringify(catalogue, null, 2) + '\n')
|
|
155
|
+
// For the repository's theme browser: the sample deck with every theme
|
|
156
|
+
// and palette, and what each theme offers.
|
|
157
|
+
const registry = buildSampleDeck({ extensions, outDir: resolve(out, 'preview') })
|
|
158
|
+
writeFileSync(resolve(out, 'looks.json'), JSON.stringify(looksOf(registry, catalogue), null, 2) + '\n')
|
|
159
|
+
return ok(`Built ${catalogue.themes.length} themes and ${catalogue.palettes.length} palettes into ${resolve(out)}`)
|
|
129
160
|
}
|
|
130
161
|
|
|
131
|
-
err(`Unknown: mdeck
|
|
132
|
-
tip(`Use: mdeck
|
|
162
|
+
err(`Unknown: mdeck ${kinds} ${sub}`)
|
|
163
|
+
tip(`Use: mdeck ${kinds} ${usage(kind)}`)
|
|
133
164
|
process.exitCode = 1
|
|
134
165
|
} catch (error) {
|
|
135
|
-
if (!(error instanceof
|
|
166
|
+
if (!(error instanceof PackageError || error instanceof ManifestError)) throw error
|
|
136
167
|
err(error.message)
|
|
137
168
|
process.exitCode = 1
|
|
138
169
|
}
|
|
139
170
|
}
|
|
140
|
-
|
package/src/core/validateDeck.js
CHANGED
|
@@ -3,6 +3,7 @@ import { isPlainObject, RENAMED_DECK_KEYS } from './parseSlides.js'
|
|
|
3
3
|
import { palettesFor } from '../extensions/tokens.js'
|
|
4
4
|
import { LABEL_KEYS, CALLOUT_TYPES } from './labels.js'
|
|
5
5
|
import { resolveLayoutProps, propertyErrors } from '../layouts/layoutProps.js'
|
|
6
|
+
import { movedHint } from '../extensions/moved.js'
|
|
6
7
|
|
|
7
8
|
// `layouts`, `themes` and `palettes` are manifest maps from the extension
|
|
8
9
|
// registry (see src/extensions/discover.js). Checks for a kind are skipped when
|
|
@@ -49,11 +50,12 @@ export function validateDeck(deck, { layouts = null, themes = null, palettes = n
|
|
|
49
50
|
if (config.show[key] != null && !values.includes(config.show[key])) add('invalid-config', `show.${key} must be one of: ${values.join(', ')}`, deck.configSource?.start)
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
|
-
if (themes && config.theme != null && !Object.hasOwn(themes, config.theme)) add('unknown-theme', `Unknown theme "${config.theme}"; available: ${Object.keys(themes).join(', ')}`, deck.configSource?.start)
|
|
53
|
+
if (themes && config.theme != null && !Object.hasOwn(themes, config.theme)) add('unknown-theme', movedHint('theme', config.theme) ?? `Unknown theme "${config.theme}"; available: ${Object.keys(themes).join(', ') || 'none'}. mdeck themes search lists more to install`, deck.configSource?.start)
|
|
54
|
+
if (themes && config.theme == null && !Object.hasOwn(themes, 'neue')) add('unknown-theme', `This deck sets no theme, so it uses neue, which is not installed: run mdeck themes install neue, or set a theme${Object.keys(themes).length ? ` (${Object.keys(themes).join(', ')})` : ''}`, deck.configSource?.start ?? 0)
|
|
53
55
|
if (palettes && themes && config.palette != null) {
|
|
54
56
|
const theme = themes[config.theme ?? 'neue']
|
|
55
57
|
const offered = theme ? palettesFor(theme, palettes).map(palette => palette.id) : Object.keys(palettes)
|
|
56
|
-
if (!Object.hasOwn(palettes, config.palette)) add('unknown-palette', `Unknown palette "${config.palette}"; available: ${offered.join(', ')}`, deck.configSource?.start)
|
|
58
|
+
if (!Object.hasOwn(palettes, config.palette)) add('unknown-palette', movedHint('palette', config.palette) ?? `Unknown palette "${config.palette}"; available: ${offered.join(', ')}`, deck.configSource?.start)
|
|
57
59
|
else if (theme && !offered.includes(config.palette)) add('unknown-palette', `The theme "${theme.id}" does not offer the palette "${config.palette}"; available: ${offered.join(', ')}`, deck.configSource?.start)
|
|
58
60
|
}
|
|
59
61
|
if (themes && config.params != null && isPlainObject(config.params) && Object.hasOwn(themes, config.theme ?? 'neue')) {
|
package/src/editor/api.js
CHANGED
|
@@ -52,16 +52,25 @@ export async function deleteExtension(kind, id) {
|
|
|
52
52
|
return response.json()
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
// Every theme and palette mdeck and the theme repository offer, each marked
|
|
56
|
+
// whether it is available to every deck already.
|
|
57
|
+
export async function loadCatalogue() {
|
|
58
|
+
const response = await fetch('/__mdeck/themes/catalogue', { headers: { Accept: 'application/json' } })
|
|
58
59
|
if (!response.ok) throw await failure(response)
|
|
59
60
|
return response.json()
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
// Installs a
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// Installs a theme (with its default palette) or a palette for every deck.
|
|
64
|
+
// Returns the steps and the new registry.
|
|
65
|
+
export async function installPackage(kind, id) {
|
|
66
|
+
const response = await fetch('/__mdeck/themes/install', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind, id }) })
|
|
67
|
+
if (!response.ok) throw await failure(response)
|
|
68
|
+
return response.json()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Removes a theme or palette for every deck (a built-in one is hidden).
|
|
72
|
+
export async function removePackage(kind, id) {
|
|
73
|
+
const response = await fetch('/__mdeck/themes/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind, id }) })
|
|
65
74
|
if (!response.ok) throw await failure(response)
|
|
66
75
|
return response.json()
|
|
67
76
|
}
|
|
@@ -10,7 +10,7 @@ import { DeckSettings } from './DeckSettings.jsx'
|
|
|
10
10
|
import { LayoutPicker } from './LayoutPicker.jsx'
|
|
11
11
|
import { ConflictBanner } from './ConflictBanner.jsx'
|
|
12
12
|
import { Resizer, readInspectorWidth, storeInspectorWidth } from './Resizer.jsx'
|
|
13
|
-
import {
|
|
13
|
+
import { designUrl, DESIGN_TAB, launchPageUrl } from '../designLink.js'
|
|
14
14
|
import markUrl from '../../../assets/logo/mark.svg'
|
|
15
15
|
|
|
16
16
|
const STATUS = { saved: 'Saved', unsaved: 'Unsaved changes', saving: 'Saving…', conflict: 'Conflict', error: 'Could not save' }
|
|
@@ -20,6 +20,7 @@ export function App() {
|
|
|
20
20
|
const [picker, setPicker] = useState(false)
|
|
21
21
|
const [inspectorWidth, setInspectorWidth] = useState(readInspectorWidth)
|
|
22
22
|
const resize = width => { setInspectorWidth(width); storeInspectorWidth(width) }
|
|
23
|
+
const [home] = useState(launchPageUrl)
|
|
23
24
|
|
|
24
25
|
const select = index => dispatch({ type: 'select', index })
|
|
25
26
|
const actions = {
|
|
@@ -50,7 +51,8 @@ export function App() {
|
|
|
50
51
|
<span class="spacer" />
|
|
51
52
|
<button class="btn is-small" onClick={actions.undo} disabled={!state.history.past.length} title="Undo (⌘Z)">Undo</button>
|
|
52
53
|
<button class="btn is-small" onClick={actions.redo} disabled={!state.history.future.length} title="Redo (⇧⌘Z)">Redo</button>
|
|
53
|
-
<a class="btn is-small" href={
|
|
54
|
+
{home && <a class="btn is-small" href={home}>Launch page</a>}
|
|
55
|
+
<a class="btn is-small" href={designUrl()} target={DESIGN_TAB} title="Make and change themes, palettes and layouts, on a sample deck">Design themes</a>
|
|
54
56
|
<a class="btn is-small" href="/?view=presenter" target="_blank" rel="noopener">Present</a>
|
|
55
57
|
<a class="btn is-small" href="/" target="_blank" rel="noopener">Open deck</a>
|
|
56
58
|
</header>
|