mdeck 2.3.0 → 3.0.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 +12 -1
- package/README.md +9 -9
- package/assets/base.css +4 -1
- package/assets/packs/academic/pack.toml +8 -0
- package/assets/packs/aurora/pack.toml +8 -0
- package/assets/packs/lagoon/pack.toml +8 -0
- package/assets/packs/minimal/minimal/extension.toml +45 -0
- package/assets/packs/minimal/minimal/styles.css +342 -0
- package/assets/packs/minimal/pack.toml +8 -0
- package/assets/packs/minimal/paper/extension.toml +27 -0
- package/assets/packs/neue/pack.toml +8 -0
- package/docs/reference/authoring.md +13 -18
- package/docs/reference/claude-skill.md +1 -1
- package/docs/reference/extensions.md +29 -20
- package/docs/reference/palettes.md +8 -12
- package/docs/reference/themes.md +8 -7
- package/docs/site/content/appearance.md +33 -49
- package/docs/site/content/commands.md +2 -2
- package/docs/site/content/theme-authoring.md +1 -1
- package/docs/site/content/theme-repository.md +19 -15
- 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 +1 -1
- package/skills/write-slides/SKILL.md +2 -2
- package/src/build/check.js +0 -7
- package/src/build/editorPlugin.js +19 -11
- package/src/build/themeCheck.js +15 -5
- package/src/cli/args.js +1 -1
- package/src/cli/index.js +3 -3
- package/src/cli/themes.js +93 -64
- package/src/core/validateDeck.js +4 -2
- package/src/editor/api.js +9 -1
- package/src/editor/components/App.jsx +4 -2
- package/src/editor/components/DesignApp.jsx +12 -3
- package/src/editor/components/ExtensionEditor.jsx +15 -4
- package/src/editor/components/ExtensionsPanel.jsx +19 -21
- 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 +56 -16
- package/src/extensions/installed.js +63 -0
- package/src/extensions/moved.js +13 -0
- package/src/extensions/packs.js +135 -46
- package/src/home/Home.jsx +12 -6
- package/src/layouts/layoutApi.jsx +10 -2
- package/src/paths.js +2 -0
- 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/assets/{extensions/themes → packs/academic}/academic/extension.toml +0 -0
- /package/assets/{extensions/themes → packs/academic}/academic/styles.css +0 -0
- /package/assets/{extensions/palettes → packs/academic}/nordic/extension.toml +0 -0
- /package/assets/{extensions/themes → packs/aurora}/aurora/extension.toml +0 -0
- /package/assets/{extensions/themes → packs/aurora}/aurora/styles.css +0 -0
- /package/assets/{extensions/palettes → packs/aurora}/neon/extension.toml +0 -0
- /package/assets/{extensions/palettes → packs/lagoon}/lagoon/extension.toml +0 -0
- /package/assets/{extensions/themes → packs/neue}/neue/extension.toml +0 -0
- /package/assets/{extensions/themes → packs/neue}/neue/styles.css +0 -0
- /package/assets/{extensions/palettes → packs/neue}/swiss/extension.toml +0 -0
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,27 @@ 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
|
for (const [id, dir] of Object.entries(extensions)) cpSync(dir, resolve(temp, 'extensions', id), { recursive: true })
|
|
92
93
|
mkdirSync(resolve(temp, 'home'))
|
|
93
|
-
execFileSync(process.execPath, [resolve(frameworkRoot, 'bin/mdeck.js'), 'build', resolve(temp, 'slides.md'), '-o', resolve(
|
|
94
|
+
execFileSync(process.execPath, [resolve(frameworkRoot, 'bin/mdeck.js'), 'build', resolve(temp, 'slides.md'), '-o', resolve(outDir, 'index.html')],
|
|
94
95
|
{ cwd: temp, stdio: 'pipe', env: { ...process.env, MDECK_HOME: resolve(temp, 'home') } })
|
|
96
|
+
return loadRegistry(resolve(temp, 'slides.md'), { userRoot: null })
|
|
97
|
+
} finally { rmSync(temp, { recursive: true, force: true }) }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The sample deck built with `extensions`, opened in Chrome for `use({ open })`.
|
|
101
|
+
export async function withSampleDeck({ extensions = {} } = {}, use) {
|
|
102
|
+
const temp = mkdtempSync(resolve(tmpdir(), 'mdeck-sample-'))
|
|
103
|
+
try {
|
|
104
|
+
buildSampleDeck({ extensions, outDir: resolve(temp, 'out') })
|
|
95
105
|
const browser = await launchChrome({ dir: resolve(temp, 'out') })
|
|
96
106
|
try {
|
|
97
107
|
// A page with the deck in a look, recording what it reports.
|
package/src/cli/args.js
CHANGED
|
@@ -7,7 +7,7 @@ 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: { '--global': flag, '--force': flag, '--check': flag, '--output': value, '-o': '--output', max: 4 },
|
|
10
|
+
themes: { '--local': flag, '--global': flag, '--force': flag, '--check': flag, '--output': value, '-o': '--output', max: 4 },
|
|
11
11
|
build: { ...output, '--single-file': flag, '--launchers': flag, '--reader': flag, '--notes': flag, max: 1 },
|
|
12
12
|
send: { ...output, '--notes': flag, '--no-pdf': flag, max: 1 },
|
|
13
13
|
pdf: { ...output, max: 1 },
|
package/src/cli/index.js
CHANGED
|
@@ -313,9 +313,9 @@ 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 and palettes
|
|
317
|
-
install <pack>
|
|
318
|
-
|
|
316
|
+
${c.green}mdeck themes${c.reset} search [words] Themes and palettes: those that come with mdeck and the online repository
|
|
317
|
+
install <pack>, remove <pack> for every deck; add slides.md or --local for one deck only
|
|
318
|
+
update [pack], list --force replaces files changed by hand
|
|
319
319
|
|
|
320
320
|
${c.dim}Make something to hand out${c.reset}
|
|
321
321
|
${c.green}mdeck build${c.reset} [slides.md] [-o dir/index.html] A folder to host
|
package/src/cli/themes.js
CHANGED
|
@@ -1,103 +1,129 @@
|
|
|
1
|
-
// `mdeck themes`:
|
|
2
|
-
//
|
|
3
|
-
// (~/.mdeck/extensions)
|
|
4
|
-
//
|
|
1
|
+
// `mdeck themes`: every theme and palette comes in a pack. Some packs come
|
|
2
|
+
// with mdeck (the starter set), the others from the theme repository.
|
|
3
|
+
// Packs are installed for every deck (~/.mdeck/extensions) unless a deck or
|
|
4
|
+
// folder is named, or --local is given: then into the extensions folder
|
|
5
|
+
// beside it, so the slide folder carries them. A pack that comes with mdeck
|
|
6
|
+
// is removed by hiding it, and installed again from mdeck's own copy, so
|
|
7
|
+
// that works offline. `build` is for the repository itself.
|
|
5
8
|
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
6
9
|
import { createRequire } from 'node:module'
|
|
7
10
|
import { dirname, resolve } from 'node:path'
|
|
8
|
-
import {
|
|
11
|
+
import { bundledPacks, loadRegistry, manifestsOf, removedPacks, userExtensionsDir } from '../extensions/discover.js'
|
|
12
|
+
import { target, packsAt, installHere, removeHere } from '../extensions/installed.js'
|
|
13
|
+
import { palettesFor } from '../extensions/tokens.js'
|
|
9
14
|
import { ManifestError } from '../extensions/manifest.js'
|
|
10
|
-
import { buildRepository, compareVersions,
|
|
15
|
+
import { buildRepository, compareVersions, findPack, installedPacks, installWithRequirements, loadCatalogue, PackError } from '../extensions/packs.js'
|
|
11
16
|
|
|
12
17
|
const VERSION = createRequire(import.meta.url)('../../package.json').version
|
|
13
18
|
|
|
14
|
-
const USAGE = 'search [words] | install <pack> | remove <pack> | update [pack] | list,
|
|
19
|
+
const USAGE = 'search [words] | install <pack> | remove <pack> | update [pack] | list — for every deck, or beside a deck with [slides.md or folder] or --local; build <packs> -o <out> [--check]'
|
|
15
20
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
// Every theme with the palettes it offers, and every palette's colours, each
|
|
22
|
+
// marked with its pack and whether it comes with mdeck.
|
|
23
|
+
function looksOf(registry, index) {
|
|
24
|
+
const packOf = Object.fromEntries(index.packs.flatMap(p => [...p.themes, ...p.palettes].map(id => [id, p])))
|
|
25
|
+
const palettes = manifestsOf(registry, 'palette')
|
|
26
|
+
const colours = tokens => Object.fromEntries(['--bg', '--surface', '--ink', '--accent', '--accent-2'].map(key => [key, tokens?.[key] ?? null]))
|
|
27
|
+
const origin = id => ({ pack: packOf[id]?.id ?? null, bundled: Boolean(packOf[id]?.bundled) })
|
|
28
|
+
return {
|
|
29
|
+
mdeck: VERSION,
|
|
30
|
+
themes: Object.values(manifestsOf(registry, 'theme')).map(theme => ({ id: theme.id, title: theme.title, description: theme.description ?? '', ...origin(theme.id),
|
|
31
|
+
palette: theme.palette, appearance: theme.appearance ?? 'light', palettes: palettesFor(theme, palettes).map(p => p.id) })),
|
|
32
|
+
palettes: Object.values(palettes).map(palette => ({ id: palette.id, title: palette.title, description: palette.description ?? '', ...origin(palette.id),
|
|
33
|
+
light: colours(palette.light), dark: colours(palette.dark) })),
|
|
34
|
+
}
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
const install = (index, entry, { root, force }) => installFromIndex(index, entry, { root, known: builtIns(), mdeckVersion: VERSION, force })
|
|
32
|
-
|
|
33
37
|
export async function runThemes({ positionals, flag, output, ui: { ok, err, tip, c } }) {
|
|
34
38
|
const [sub = 'search', ...rest] = positionals
|
|
35
|
-
const
|
|
39
|
+
const local = flag('--local'), force = flag('--force')
|
|
40
|
+
const warnOffline = catalogue => { if (catalogue.offline) tip(`The theme repository could not be reached (${catalogue.offline}); showing the packs that come with mdeck.`) }
|
|
41
|
+
const what = step => [step.themes?.length ? `themes ${step.themes.join(', ')}` : '', step.palettes?.length ? `palettes ${step.palettes.join(', ')}` : ''].filter(Boolean).join('; ')
|
|
36
42
|
try {
|
|
37
43
|
if (sub === 'search') {
|
|
38
|
-
const
|
|
44
|
+
const catalogue = await loadCatalogue({ bundled: bundledPacks({ all: true }) })
|
|
45
|
+
warnOffline(catalogue)
|
|
39
46
|
const words = rest.map(word => word.toLowerCase())
|
|
40
|
-
const
|
|
41
|
-
const found =
|
|
42
|
-
if (!found.length) return tip(`No pack matches${words.length ? ` "${words.join(' ')}"` : ''}
|
|
47
|
+
const mine = packsAt(target(null, false)), here = installedPacks(target('.', true).root), hidden = removedPacks()
|
|
48
|
+
const found = catalogue.packs.filter(pack => words.every(word => [pack.id, pack.title, pack.description, ...pack.themes, ...pack.palettes].join(' ').toLowerCase().includes(word)))
|
|
49
|
+
if (!found.length) return tip(`No pack matches${words.length ? ` "${words.join(' ')}"` : ''}`)
|
|
43
50
|
for (const pack of found) {
|
|
44
|
-
const have =
|
|
45
|
-
const state =
|
|
51
|
+
const have = here[pack.id] ?? mine[pack.id]
|
|
52
|
+
const state = hidden.includes(pack.id) && !have ? ` ${c.dim}(comes with mdeck, removed)${c.reset}`
|
|
53
|
+
: !have ? (pack.bundled || pack.bundledVersion ? ` ${c.dim}(comes with mdeck)${c.reset}` : '')
|
|
54
|
+
: have.bundled ? (pack.bundled ? ` ${c.green}(comes with mdeck)${c.reset}` : ` ${c.yellow}(comes with mdeck as ${pack.bundledVersion}, update available)${c.reset}`)
|
|
55
|
+
: compareVersions(pack.version, have.version) > 0 ? ` ${c.yellow}(${have.version} installed, update available)${c.reset}` : ` ${c.green}(installed)${c.reset}`
|
|
46
56
|
console.log(`\n ${c.cyan}${pack.id}${c.reset} ${pack.version} — ${pack.title}${state}`)
|
|
47
57
|
if (pack.description) console.log(` ${pack.description}`)
|
|
48
|
-
console.log(` ${c.dim}${[pack.themes.length ? `themes: ${pack.themes.join(', ')}` : '', pack.palettes.length ? `palettes: ${pack.palettes.join(', ')}` : ''].filter(Boolean).join(' · ')} · by ${pack.author}, ${pack.license}${c.reset}`)
|
|
58
|
+
console.log(` ${c.dim}${[pack.themes.length ? `themes: ${pack.themes.join(', ')}` : '', pack.palettes.length ? `palettes: ${pack.palettes.join(', ')}` : '', pack.requires?.length ? `requires: ${pack.requires.join(', ')}` : ''].filter(Boolean).join(' · ')} · by ${pack.author}, ${pack.license}${c.reset}`)
|
|
49
59
|
}
|
|
50
60
|
console.log()
|
|
51
|
-
return tip('Install one with: mdeck themes install <pack>
|
|
61
|
+
return tip('Install one for every deck with: mdeck themes install <pack> (beside a deck: add slides.md or --local)')
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
if (sub === 'install') {
|
|
55
65
|
const [id, where] = rest
|
|
56
|
-
if (!id) { err('Name a pack.'); return tip('Usage: mdeck themes install <pack> [slides.md or folder] [--
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
if (!id) { err('Name a pack.'); return tip('Usage: mdeck themes install <pack> [slides.md or folder] [--local]; mdeck themes search lists them.') }
|
|
67
|
+
const at = target(where, local)
|
|
68
|
+
const catalogue = await loadCatalogue({ bundled: bundledPacks({ all: true }) })
|
|
69
|
+
warnOffline(catalogue)
|
|
70
|
+
findPack(catalogue, id)
|
|
71
|
+
for (const step of await installHere(catalogue, id, at, { mdeckVersion: VERSION, force })) {
|
|
72
|
+
if (step.kept) tip(`${step.id} ${step.version} is there already`)
|
|
73
|
+
else if (step.bundled) (step.wasRemoved ? ok(`${step.id} comes with mdeck and is offered again ${at.label}`) : tip(`${step.id} comes with mdeck and is installed ${at.label} already`))
|
|
74
|
+
else ok(`${step.replaced ? `Updated ${step.id} from ${step.replaced} to ${step.version}` : `Installed ${step.id} ${step.version}`} ${at.label}${what(step) ? `: ${what(step)}` : ''}`)
|
|
75
|
+
}
|
|
76
|
+
loadRegistry(at.deck)
|
|
77
|
+
const pack = findPack(catalogue, id)
|
|
78
|
+
if (pack.themes.length) tip(`Use it with "theme: ${pack.themes[0]}" at the top of a slide file.`)
|
|
79
|
+
else if (pack.palettes.length) tip(`Use it with "palette: ${pack.palettes[0]}" at the top of a slide file.`)
|
|
64
80
|
return tip('Look at it on a sample deck: mdeck design')
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
if (sub === 'remove') {
|
|
68
84
|
const [id, where] = rest
|
|
69
|
-
if (!id) { err('Name a pack.'); return tip('Usage: mdeck themes remove <pack> [slides.md or folder] [--
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
85
|
+
if (!id) { err('Name a pack.'); return tip('Usage: mdeck themes remove <pack> [slides.md or folder] [--local]') }
|
|
86
|
+
const at = target(where, local)
|
|
87
|
+
const catalogue = await loadCatalogue({ bundled: bundledPacks({ all: true }), online: false })
|
|
88
|
+
const removed = removeHere(catalogue, id, at, { force })
|
|
89
|
+
if (removed.folders.length) ok(`Removed ${id} ${at.label} (${removed.folders.join(', ')})`)
|
|
90
|
+
if (removed.hidden) ok(`${id} comes with mdeck and is no longer offered; mdeck themes install ${id} brings it back`)
|
|
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
|
|
97
|
+
const at = target(named ? second : first, local)
|
|
98
|
+
const catalogue = await loadCatalogue({ bundled: bundledPacks({ all: true }) })
|
|
99
|
+
warnOffline(catalogue)
|
|
100
|
+
const installed = packsAt(at)
|
|
80
101
|
const ids = named ? [named] : Object.keys(installed)
|
|
81
|
-
if (!ids.length) return tip(`No packs installed ${label}`)
|
|
82
|
-
const index = await fetchIndex()
|
|
102
|
+
if (!ids.length) return tip(`No packs installed ${at.label}`)
|
|
83
103
|
for (const id of ids) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
const have = installed[id]
|
|
105
|
+
if (!have) throw new PackError(`The pack ${id} is not installed ${at.label}`)
|
|
106
|
+
const entry = findPack(catalogue, id)
|
|
107
|
+
// A bundled pack is up to date until the repository has a newer one.
|
|
108
|
+
if (have.bundled ? entry.bundled : compareVersions(entry.version, have.version) <= 0) { tip(`${id} ${have.version ?? entry.version} is up to date`); continue }
|
|
109
|
+
const steps = await installWithRequirements(catalogue, id, { root: at.root, mdeckVersion: VERSION, force, has: pack => pack !== id && Object.hasOwn(packsAt(at), pack) })
|
|
110
|
+
ok(`Updated ${id} ${have.version ?? entry.bundledVersion} → ${steps.at(-1).version}`)
|
|
89
111
|
}
|
|
90
|
-
loadRegistry(deck)
|
|
112
|
+
loadRegistry(at.deck)
|
|
91
113
|
return
|
|
92
114
|
}
|
|
93
115
|
|
|
94
116
|
if (sub === 'list') {
|
|
95
117
|
const [where] = rest
|
|
96
|
-
|
|
118
|
+
const hidden = removedPacks()
|
|
119
|
+
console.log(`\n ${c.bold}Come with mdeck${c.reset}`)
|
|
120
|
+
for (const id of Object.keys(bundledPacks({ all: true }))) console.log(` ${c.cyan}${id}${c.reset}${hidden.includes(id) ? ` ${c.dim}(removed; mdeck themes install ${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 [label, root] of sections) {
|
|
97
123
|
const packs = installedPacks(root)
|
|
98
124
|
console.log(`\n ${c.bold}${label}${c.reset} ${c.dim}${root}${c.reset}`)
|
|
99
125
|
if (!Object.keys(packs).length) console.log(` ${c.dim}none${c.reset}`)
|
|
100
|
-
for (const [id, pack] of Object.entries(packs)) console.log(` ${c.cyan}${id}${c.reset} ${pack.version} — ${pack.folders.join(', ')}`)
|
|
126
|
+
for (const [id, pack] of Object.entries(packs)) console.log(` ${c.cyan}${id}${c.reset} ${pack.version} — ${pack.folders.join(', ')}${pack.requires.length ? ` ${c.dim}(requires ${pack.requires.join(', ')})${c.reset}` : ''}`)
|
|
101
127
|
}
|
|
102
128
|
console.log()
|
|
103
129
|
return
|
|
@@ -107,14 +133,14 @@ export async function runThemes({ positionals, flag, output, ui: { ok, err, tip,
|
|
|
107
133
|
const [packsDir] = rest
|
|
108
134
|
const out = output()
|
|
109
135
|
if (!packsDir || !out) { err('Name the packs folder and the output folder.'); return tip('Usage: mdeck themes build <packs folder> -o <output folder> [--check]') }
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const { checkLooks, renderPreviews } = await import('../build/themeCheck.js')
|
|
136
|
+
const bundled = bundledPacks({ all: true })
|
|
137
|
+
const index = buildRepository(resolve(packsDir), resolve(out), { bundled })
|
|
138
|
+
// The repository's own themes and palettes go beside the sample deck;
|
|
139
|
+
// the bundled ones are in it already.
|
|
140
|
+
const extensions = Object.fromEntries(index.packs.filter(pack => !pack.bundled).flatMap(pack => [...pack.themes, ...pack.palettes].map(id => [id, resolve(packsDir, pack.id, id)])))
|
|
141
|
+
const dirOf = id => extensions[id] ?? resolve(bundled[index.packs.find(pack => [...pack.themes, ...pack.palettes].includes(id)).id], id)
|
|
142
|
+
const paletteTheme = id => /^theme\s*=\s*"([^"]+)"/m.exec(readFileSync(resolve(dirOf(id), 'extension.toml'), 'utf8'))?.[1] ?? 'neue'
|
|
143
|
+
const { checkLooks, renderPreviews, buildSampleDeck } = await import('../build/themeCheck.js')
|
|
118
144
|
const looks = index.packs.flatMap(pack => [...pack.themes.map(theme => ({ id: theme, theme })), ...pack.palettes.map(palette => ({ id: palette, theme: paletteTheme(palette), palette }))])
|
|
119
145
|
if (flag('--check')) {
|
|
120
146
|
const failures = await checkLooks(looks, { extensions })
|
|
@@ -125,6 +151,10 @@ export async function runThemes({ positionals, flag, output, ui: { ok, err, tip,
|
|
|
125
151
|
await renderPreviews(looks.map(look => ({ file: resolve(out, 'previews', `${look.id}.webp`), theme: look.theme, palette: look.palette ?? '', slide: look.palette ? 3 : 1 })), { extensions })
|
|
126
152
|
for (const pack of index.packs) pack.previews = Object.fromEntries([...pack.themes, ...pack.palettes].map(id => [id, `previews/${id}.webp`]))
|
|
127
153
|
writeFileSync(resolve(out, 'index.json'), JSON.stringify(index, null, 2) + '\n')
|
|
154
|
+
// For the repository's theme browser: the sample deck with every theme
|
|
155
|
+
// and palette, and what each theme offers.
|
|
156
|
+
const registry = buildSampleDeck({ extensions, outDir: resolve(out, 'preview') })
|
|
157
|
+
writeFileSync(resolve(out, 'looks.json'), JSON.stringify(looksOf(registry, index), null, 2) + '\n')
|
|
128
158
|
return ok(`Built ${index.packs.length} pack(s) into ${resolve(out)}`)
|
|
129
159
|
}
|
|
130
160
|
|
|
@@ -137,4 +167,3 @@ export async function runThemes({ positionals, flag, output, ui: { ok, err, tip,
|
|
|
137
167
|
process.exitCode = 1
|
|
138
168
|
}
|
|
139
169
|
}
|
|
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
|
@@ -59,9 +59,17 @@ export async function loadThemeIndex() {
|
|
|
59
59
|
return response.json()
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// Installs a pack
|
|
62
|
+
// Installs a pack for every deck, with the packs it requires. Returns the
|
|
63
|
+
// new registry.
|
|
63
64
|
export async function installThemePack(id) {
|
|
64
65
|
const response = await fetch('/__mdeck/themes/install', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) })
|
|
65
66
|
if (!response.ok) throw await failure(response)
|
|
66
67
|
return response.json()
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
// Removes a pack for every deck (one that comes with mdeck is hidden).
|
|
71
|
+
export async function removeThemePack(id) {
|
|
72
|
+
const response = await fetch('/__mdeck/themes/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) })
|
|
73
|
+
if (!response.ok) throw await failure(response)
|
|
74
|
+
return response.json()
|
|
75
|
+
}
|
|
@@ -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>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { h } from 'preact'
|
|
2
2
|
import { useEffect, useRef, useState } from 'preact/hooks'
|
|
3
3
|
import { useDeckDocument } from '../useDeckDocument.js'
|
|
4
|
-
import { loadExtension, saveExtension, deleteExtension, installThemePack } from '../api.js'
|
|
4
|
+
import { loadExtension, saveExtension, deleteExtension, installThemePack, removeThemePack } from '../api.js'
|
|
5
5
|
import { starterFiles, parseManifest, toModel, toRuntimeManifest, MANIFEST } from '../extensions.js'
|
|
6
6
|
import { sampleDeck, sampleDataUrl } from '../sampleDeck.js'
|
|
7
|
-
import { parseTarget } from '../designLink.js'
|
|
7
|
+
import { parseTarget, launchPageUrl } from '../designLink.js'
|
|
8
8
|
import { palettesFor } from '../../extensions/tokens.js'
|
|
9
9
|
import { parseSlides } from '../../core/parseSlides.js'
|
|
10
10
|
import { setDeckConfig } from '../../core/editDeck.js'
|
|
@@ -36,6 +36,7 @@ export function DesignApp() {
|
|
|
36
36
|
const [slide, setSlide] = useState(0)
|
|
37
37
|
const [inspectorWidth, setInspectorWidth] = useState(readInspectorWidth)
|
|
38
38
|
const resize = width => { setInspectorWidth(width); storeInspectorWidth(width) }
|
|
39
|
+
const [home] = useState(launchPageUrl)
|
|
39
40
|
const timer = useRef(null)
|
|
40
41
|
const currentRef = useRef(current)
|
|
41
42
|
currentRef.current = current
|
|
@@ -126,6 +127,13 @@ export function DesignApp() {
|
|
|
126
127
|
if (id) await select(kind, id)
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
// Removes a pack for every deck; the deck's look is not changed.
|
|
131
|
+
const removeOnline = async pack => {
|
|
132
|
+
const result = await removeThemePack(pack)
|
|
133
|
+
dispatch({ type: 'setRegistry', registry: result.registry })
|
|
134
|
+
setCurrent(null)
|
|
135
|
+
}
|
|
136
|
+
|
|
129
137
|
const remove = async () => {
|
|
130
138
|
const target = current
|
|
131
139
|
try {
|
|
@@ -232,6 +240,7 @@ export function DesignApp() {
|
|
|
232
240
|
{state.designOnly
|
|
233
241
|
? <span class="muted" title={`${state.path}/extensions`}>Saved in {state.name}/extensions</span>
|
|
234
242
|
: <a class="btn is-small" href="/editor.html">Back to the slides</a>}
|
|
243
|
+
{home && <a class="btn is-small" href={home}>Launch page</a>}
|
|
235
244
|
</header>
|
|
236
245
|
{state.status === 'conflict' && <ConflictBanner onReload={() => resolve('reload')} onOverwrite={() => resolve('overwrite')} />}
|
|
237
246
|
<div class="editor-main" style={{ gridTemplateColumns: `290px minmax(0, 1fr) ${inspectorWidth}px` }}>
|
|
@@ -244,7 +253,7 @@ export function DesignApp() {
|
|
|
244
253
|
? <div class="form"><p class="empty">Choose one of {state.designOnly ? "this folder's" : "this deck's"} themes and palettes on the left to change it, or start from a built-in one. What you change is saved as you type, in the <code>extensions</code> folder {state.designOnly ? 'of this folder, where decks in it find them' : "beside the deck. The deck's look is chosen in the editor's Deck settings"}.{error ? ` ${error}` : ''}</p></div>
|
|
245
254
|
: current.source === 'local'
|
|
246
255
|
? <>{usage}<ExtensionEditor extension={current} palettes={manifests.palettes} status={status} error={error} note={note} onFiles={onFiles} onDelete={remove} onCopy={() => copyDialog(current.kind, current.id)} /></>
|
|
247
|
-
: <BuiltInCard extension={current} manifest={manifests[`${current.kind}s`][current.id]} onCopy={() => copyDialog(current.kind, current.id)} />}
|
|
256
|
+
: <BuiltInCard extension={current} manifest={manifests[`${current.kind}s`][current.id]} onCopy={() => copyDialog(current.kind, current.id)} onRemovePack={removeOnline} />}
|
|
248
257
|
</aside>
|
|
249
258
|
</div>
|
|
250
259
|
{dialog && <NewExtensionDialog kind={dialog.kind} initial={dialog.initial ?? null} registry={state.registry} existing={existing} onCreate={create} onClose={() => setDialog(null)} />}
|
|
@@ -47,17 +47,28 @@ export function ExtensionEditor({ extension, palettes = {}, status, error, note,
|
|
|
47
47
|
|
|
48
48
|
// A built-in or installed extension: what it is, and the one way to change
|
|
49
49
|
// it, a copy in the deck's extensions folder.
|
|
50
|
-
export function BuiltInCard({ extension, manifest, onCopy }) {
|
|
50
|
+
export function BuiltInCard({ extension, manifest, onCopy, onRemovePack }) {
|
|
51
51
|
const { kind, id, files } = extension
|
|
52
52
|
const user = extension.source === 'user'
|
|
53
|
+
const pack = manifest?.pack ?? null
|
|
54
|
+
const [removing, setRemoving] = useState(null)
|
|
53
55
|
return <div class="form">
|
|
54
|
-
<p class="section-title">{user ? `Installed
|
|
56
|
+
<p class="section-title">{user ? `Installed for every deck` : `Comes with mdeck`}{pack ? ` · pack ${pack}` : ''}</p>
|
|
55
57
|
<h2 class="builtin-title">{manifest?.title ?? id} <span class="muted">{id}</span></h2>
|
|
56
58
|
{manifest?.description && <p class="builtin-description">{manifest.description}</p>}
|
|
57
59
|
{kind === 'palette' && manifest && ['light', 'dark'].map(mode => <Field key={mode} label={mode === 'light' ? 'Light' : 'Dark'}><Swatches tokens={manifest[mode] ?? {}} /></Field>)}
|
|
58
60
|
{kind === 'theme' && manifest?.palette && <p class="builtin-description">Its default palette is <code>{manifest.palette}</code>{manifest.palettes?.length ? `; it offers only ${manifest.palettes.join(', ')}` : ''}.</p>}
|
|
59
|
-
<p class="note">{user ? `Installed ${kind}s
|
|
60
|
-
<div class="row" style={{ display: 'flex', gap: '8px'
|
|
61
|
+
<p class="note">{user ? `Installed ${kind}s change only when they are updated (mdeck themes update).` : `This ${kind} cannot be changed where it is.`} Copy this one into the deck's <code>extensions</code> folder and change the copy. The preview shows every change as you make it.</p>
|
|
62
|
+
<div class="row" style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
|
63
|
+
<button class="btn is-primary" onClick={onCopy}>Copy to customise</button>
|
|
64
|
+
{pack && onRemovePack && (removing === 'confirm'
|
|
65
|
+
? <><span class="muted" style={{ alignSelf: 'center' }}>Remove the pack {pack} for every deck?</span>
|
|
66
|
+
<button class="btn" onClick={async () => { setRemoving('busy'); try { await onRemovePack(pack); setRemoving(null) } catch (error) { setRemoving({ error: error.message }) } }}>Remove</button>
|
|
67
|
+
<button class="btn" onClick={() => setRemoving(null)}>Keep</button></>
|
|
68
|
+
: <button class="btn" disabled={removing === 'busy'} onClick={() => setRemoving('confirm')}>Remove pack</button>)}
|
|
69
|
+
</div>
|
|
70
|
+
{removing?.error && <p class="online-error">{removing.error}</p>}
|
|
71
|
+
{pack && !user && <p class="hint-text">Removing a pack that comes with mdeck hides it; install it again from “More to install”.</p>}
|
|
61
72
|
<details class="builtin-source">
|
|
62
73
|
<summary>{MANIFEST}</summary>
|
|
63
74
|
<pre>{files?.[MANIFEST] ?? ''}</pre>
|
|
@@ -37,54 +37,52 @@ export function ExtensionsPanel({ registry, view = {}, owner = "This deck's", se
|
|
|
37
37
|
</div>
|
|
38
38
|
{own.length
|
|
39
39
|
? <ol class="outline-list">{own.map(row)}</ol>
|
|
40
|
-
: <p class="panel-hint">None yet.
|
|
40
|
+
: <p class="panel-hint">None yet. Copy one below to start{kind === 'palette' ? ', or with + from plain colours' : ''}.</p>}
|
|
41
41
|
{installed.length > 0 && <>
|
|
42
42
|
<div class="panel-section-head"><p class="section-title">Installed for every deck</p></div>
|
|
43
43
|
<ol class="outline-list">{installed.map(row)}</ol>
|
|
44
44
|
</>}
|
|
45
|
-
<
|
|
46
|
-
<div class="panel-section-head"><p class="section-title">Built in, to start from</p></div>
|
|
45
|
+
<div class="panel-section-head"><p class="section-title">Come with mdeck</p></div>
|
|
47
46
|
<ol class="outline-list">{builtIn.map(row)}</ol>
|
|
47
|
+
<OnlinePacks key="online" kind={kind} onInstall={onInstall} />
|
|
48
48
|
</aside>
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// Packs
|
|
52
|
-
//
|
|
53
|
-
|
|
51
|
+
// Packs not installed yet that have this kind of extension: from the theme
|
|
52
|
+
// repository, and those that come with mdeck but were removed. Loaded on
|
|
53
|
+
// request, since it needs the network. Installing makes a pack available to
|
|
54
|
+
// every deck.
|
|
55
|
+
function OnlinePacks({ kind, onInstall }) {
|
|
54
56
|
const [state, setState] = useState({ status: 'idle' })
|
|
55
57
|
const [busy, setBusy] = useState(null)
|
|
56
58
|
const load = () => {
|
|
57
|
-
setState({ status: 'loading' })
|
|
59
|
+
setState(prev => ({ ...prev, status: prev.index ? 'ready' : 'loading' }))
|
|
58
60
|
loadThemeIndex().then(index => setState({ status: 'ready', index }), error => setState({ status: 'error', error: error.message }))
|
|
59
61
|
}
|
|
60
62
|
const install = async pack => {
|
|
61
63
|
setBusy(pack.id)
|
|
62
|
-
try {
|
|
63
|
-
await onInstall(pack, kind)
|
|
64
|
-
setState(prev => ({ ...prev, index: { ...prev.index, installed: { ...prev.index.installed, [pack.id]: { version: pack.version } } } }))
|
|
65
|
-
} catch (error) { setState(prev => ({ ...prev, error: error.message })) }
|
|
64
|
+
try { await onInstall(pack, kind); load() } catch (error) { setState(prev => ({ ...prev, error: error.message })) }
|
|
66
65
|
setBusy(null)
|
|
67
66
|
}
|
|
68
|
-
const head = <div class="panel-section-head"><p class="section-title">
|
|
67
|
+
const head = <div class="panel-section-head"><p class="section-title">More to install</p></div>
|
|
69
68
|
if (state.status !== 'ready') return <>{head}<div class="panel-hint">
|
|
70
|
-
{state.status === 'loading' ? 'Loading…' : <button class="btn is-small" onClick={load}>Show what
|
|
69
|
+
{state.status === 'loading' ? 'Loading…' : <button class="btn is-small" onClick={load}>Show what you can install</button>}
|
|
71
70
|
{state.error && <p class="online-error">{state.error}</p>}
|
|
72
71
|
</div></>
|
|
73
72
|
const { index } = state
|
|
74
|
-
const packs = index.packs.filter(pack => pack[`${kind}s`].length)
|
|
73
|
+
const packs = index.packs.filter(pack => pack[`${kind}s`].length && !index.installed?.[pack.id])
|
|
75
74
|
return <>{head}
|
|
76
75
|
{state.error && <p class="panel-hint online-error">{state.error}</p>}
|
|
77
|
-
{
|
|
76
|
+
{index.offline && <p class="panel-hint">The theme repository could not be reached; showing what comes with mdeck.</p>}
|
|
77
|
+
{!packs.length && <p class="panel-hint">Every {kind} there is, is installed.</p>}
|
|
78
78
|
<ol class="outline-list">{packs.map(pack => {
|
|
79
79
|
const id = pack[`${kind}s`][0]
|
|
80
|
-
const
|
|
80
|
+
const previews = pack.previews ?? pack.repository?.previews
|
|
81
81
|
return <li key={pack.id} class="outline-item has-thumb online-pack">
|
|
82
|
-
{
|
|
82
|
+
{previews?.[id] && <div class="thumb"><img src={new URL(previews[id], index.url).href} alt="" loading="lazy" /></div>}
|
|
83
83
|
<span><span class="title" style={{ display: 'block' }}>{pack.title} <span class="muted">{pack.version}</span></span>
|
|
84
|
-
<span class="online-meta">{[...pack.themes, ...pack.palettes].join(', ')} · {pack.author}</span></span>
|
|
85
|
-
{
|
|
86
|
-
? <span class="muted">{`Installed${have.version !== pack.version ? ` (${have.version})` : ''} in ${owner === "This folder's" ? 'this folder' : 'this deck'}`}</span>
|
|
87
|
-
: <button class="btn is-small" disabled={busy === pack.id} onClick={() => install(pack)}>{busy === pack.id ? 'Installing…' : 'Install'}</button>}
|
|
84
|
+
<span class="online-meta">{[...pack.themes, ...pack.palettes].join(', ')}{pack.removed ? ' · comes with mdeck, removed' : ` · ${pack.author}`}{pack.requires?.length ? ` · needs ${pack.requires.join(', ')}` : ''}</span></span>
|
|
85
|
+
<button class="btn is-small" disabled={busy === pack.id} onClick={() => install(pack)}>{busy === pack.id ? 'Installing…' : 'Install'}</button>
|
|
88
86
|
</li>
|
|
89
87
|
})}</ol>
|
|
90
88
|
</>
|
package/src/editor/designLink.js
CHANGED
|
@@ -4,11 +4,15 @@ export const DESIGN_URL = '/design.html'
|
|
|
4
4
|
export const DESIGN_TAB = 'mdeck-design'
|
|
5
5
|
|
|
6
6
|
// `copy` starts a copy of a built-in extension to change; `open` opens one of
|
|
7
|
-
// the deck's own. Both are `kind:id`.
|
|
7
|
+
// the deck's own. Both are `kind:id`. The launch page's address, if this
|
|
8
|
+
// editor came from it, is passed on.
|
|
8
9
|
export function designUrl({ copy = null, open = null } = {}) {
|
|
9
10
|
const query = new URLSearchParams()
|
|
10
11
|
if (copy) query.set('copy', copy)
|
|
11
12
|
if (open) query.set('open', open)
|
|
13
|
+
// A new tab does not share this one's memory: the way back goes along.
|
|
14
|
+
const home = typeof location === 'undefined' ? null : launchPageUrl()
|
|
15
|
+
if (home) query.set('home', home)
|
|
12
16
|
return query.size ? `${DESIGN_URL}?${query}` : DESIGN_URL
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -17,3 +21,16 @@ export function parseTarget(value) {
|
|
|
17
21
|
const match = /^(theme|palette|layout):([a-z][a-z0-9-]*)$/.exec(value ?? '')
|
|
18
22
|
return match ? { kind: match[1], id: match[2] } : null
|
|
19
23
|
}
|
|
24
|
+
|
|
25
|
+
// The launch page of `mdeck run` that opened this editor, if one did: it
|
|
26
|
+
// passes its address as ?home=…, kept for this tab so that switching
|
|
27
|
+
// between the editor and the design page keeps the way back.
|
|
28
|
+
const HOME_KEY = 'mdeck-launch-page'
|
|
29
|
+
export function launchPageUrl() {
|
|
30
|
+
let home = new URLSearchParams(location.search).get('home')
|
|
31
|
+
try {
|
|
32
|
+
if (home) sessionStorage.setItem(HOME_KEY, home)
|
|
33
|
+
else home = sessionStorage.getItem(HOME_KEY)
|
|
34
|
+
} catch {}
|
|
35
|
+
try { return home && /^https?:$/.test(new URL(home).protocol) ? home : null } catch { return null }
|
|
36
|
+
}
|
package/src/editor/sampleDeck.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// ordinary amounts. The design page previews themes and palettes on it, and
|
|
3
3
|
// `npm run test:themes` checks every theme against it.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// A drawing in the palette's colours (with plain colours elsewhere): a chart
|
|
6
|
+
// over hills under a sun, for every layout that shows a picture.
|
|
7
|
+
export const SAMPLE_PICTURE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 500"><rect width="800" height="500" fill="var(--surface, #eef1f4)"/><g stroke="var(--rule, #d5dbe1)" stroke-width="2"><path d="M0 100H800M0 200H800M0 300H800M0 400H800M100 0V500M200 0V500M300 0V500M400 0V500M500 0V500M600 0V500M700 0V500"/></g><circle cx="580" cy="165" r="128" fill="none" stroke="var(--accent, #3d5a73)" stroke-width="4" opacity="0.35"/><circle cx="580" cy="165" r="88" fill="var(--accent, #3d5a73)"/><path d="M0 360C140 292 262 300 384 338S628 424 800 330V500H0Z" fill="var(--accent-2, #c2410c)"/><path d="M0 418C168 366 330 392 470 422S690 474 800 434V500H0Z" fill="var(--accent, #3d5a73)" opacity="0.6"/><polyline points="80,262 170,214 260,236 350,170 440,196" fill="none" stroke="var(--ink, #1b2733)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><g fill="var(--bg, #ffffff)" stroke="var(--ink, #1b2733)" stroke-width="5"><circle cx="80" cy="262" r="10"/><circle cx="170" cy="214" r="10"/><circle cx="260" cy="236" r="10"/><circle cx="350" cy="170" r="10"/><circle cx="440" cy="196" r="10"/></g></svg>'
|
|
6
8
|
|
|
7
9
|
// `picture` is where the slides find the picture: a file beside the deck, or
|
|
8
10
|
// a data URL where there is no such file.
|