mdeck 3.0.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 +17 -1
- package/assets/base.css +24 -0
- package/assets/{packs/lagoon → extensions/palettes}/lagoon/extension.toml +3 -0
- package/assets/{packs/aurora → extensions/palettes}/neon/extension.toml +3 -0
- package/assets/{packs/academic → extensions/palettes}/nordic/extension.toml +3 -0
- package/assets/{packs/minimal → extensions/palettes}/paper/extension.toml +3 -0
- package/assets/{packs/neue → extensions/palettes}/swiss/extension.toml +3 -0
- package/assets/{packs/academic → extensions/themes}/academic/extension.toml +3 -0
- package/assets/{packs/aurora → extensions/themes}/aurora/extension.toml +3 -0
- package/assets/{packs/minimal → extensions/themes}/minimal/extension.toml +3 -0
- package/assets/{packs/neue → extensions/themes}/neue/extension.toml +3 -0
- package/docs/reference/authoring.md +16 -3
- package/docs/reference/extensions.md +42 -35
- package/docs/reference/palettes.md +1 -1
- package/docs/reference/themes.md +3 -3
- package/docs/site/content/appearance.md +11 -9
- package/docs/site/content/commands.md +2 -1
- package/docs/site/content/theme-authoring.md +1 -1
- package/docs/site/content/theme-repository.md +36 -28
- package/package.json +2 -2
- package/skills/write-slides/SKILL.md +1 -1
- package/src/build/editorPlugin.js +22 -17
- package/src/build/themeCheck.js +14 -3
- package/src/cli/args.js +2 -1
- package/src/cli/index.js +7 -6
- package/src/cli/themes.js +99 -92
- package/src/editor/api.js +11 -10
- package/src/editor/components/DesignApp.jsx +9 -11
- package/src/editor/components/ExtensionEditor.jsx +10 -11
- package/src/editor/components/ExtensionsPanel.jsx +24 -28
- package/src/extensions/discover.js +28 -35
- package/src/extensions/installed.js +73 -48
- package/src/extensions/manifest.js +20 -3
- package/src/extensions/moved.js +14 -7
- package/src/extensions/packages.js +318 -0
- package/src/paths.js +0 -2
- package/src/runtime/markedSetup.js +7 -0
- package/assets/packs/academic/pack.toml +0 -8
- package/assets/packs/aurora/pack.toml +0 -8
- package/assets/packs/lagoon/pack.toml +0 -8
- package/assets/packs/minimal/pack.toml +0 -8
- package/assets/packs/neue/pack.toml +0 -8
- package/src/extensions/packs.js +0 -357
- /package/assets/{packs/academic → extensions/themes}/academic/styles.css +0 -0
- /package/assets/{packs/aurora → extensions/themes}/aurora/styles.css +0 -0
- /package/assets/{packs/minimal → extensions/themes}/minimal/styles.css +0 -0
- /package/assets/{packs/neue → extensions/themes}/neue/styles.css +0 -0
package/src/build/themeCheck.js
CHANGED
|
@@ -89,7 +89,8 @@ export function buildSampleDeck({ extensions = {}, outDir }) {
|
|
|
89
89
|
try {
|
|
90
90
|
writeFileSync(resolve(temp, 'slides.md'), sampleDeck())
|
|
91
91
|
writeFileSync(resolve(temp, 'picture.svg'), SAMPLE_PICTURE)
|
|
92
|
-
|
|
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 })
|
|
93
94
|
mkdirSync(resolve(temp, 'home'))
|
|
94
95
|
execFileSync(process.execPath, [resolve(frameworkRoot, 'bin/mdeck.js'), 'build', resolve(temp, 'slides.md'), '-o', resolve(outDir, 'index.html')],
|
|
95
96
|
{ cwd: temp, stdio: 'pipe', env: { ...process.env, MDECK_HOME: resolve(temp, 'home') } })
|
|
@@ -146,15 +147,25 @@ export function checkLooks(looks, { extensions = {} } = {}) {
|
|
|
146
147
|
})
|
|
147
148
|
}
|
|
148
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
|
+
|
|
149
154
|
// Pictures of looks on the sample deck, as WebP: { file, theme, palette,
|
|
150
|
-
// 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.
|
|
151
158
|
export function renderPreviews(shots, { extensions = {}, width = 960, height = 540 } = {}) {
|
|
152
159
|
return withSampleDeck({ extensions }, async ({ open }) => {
|
|
153
|
-
for (const { file, theme, palette = '', slide = 1 } of shots) {
|
|
160
|
+
for (const { file, theme, palette = '', slide = 1, icon = null } of shots) {
|
|
154
161
|
const page = await open({ theme, palette })
|
|
155
162
|
if (slide > 1) { await page.evaluate(`document.querySelector('deck-stage').goTo(${slide - 1})`); await settle(page) }
|
|
156
163
|
mkdirSync(resolve(file, '..'), { recursive: true })
|
|
157
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
|
+
}
|
|
158
169
|
await page.close()
|
|
159
170
|
}
|
|
160
171
|
})
|
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: { '--local': flag, '--
|
|
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
|
-
update [
|
|
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,93 +1,96 @@
|
|
|
1
|
-
// `mdeck themes
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
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'
|
|
9
11
|
import { createRequire } from 'node:module'
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { target,
|
|
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'
|
|
13
15
|
import { palettesFor } from '../extensions/tokens.js'
|
|
14
16
|
import { ManifestError } from '../extensions/manifest.js'
|
|
15
|
-
import { buildRepository, compareVersions,
|
|
17
|
+
import { buildRepository, compareVersions, findEntry, installedPackages, keyOf, loadCatalogue, PackageError } from '../extensions/packages.js'
|
|
16
18
|
|
|
17
19
|
const VERSION = createRequire(import.meta.url)('../../package.json').version
|
|
18
20
|
|
|
19
|
-
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]' : ''}`
|
|
20
22
|
|
|
21
23
|
// Every theme with the palettes it offers, and every palette's colours, each
|
|
22
|
-
// marked
|
|
23
|
-
function looksOf(registry,
|
|
24
|
-
const
|
|
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')))
|
|
25
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]))
|
|
26
30
|
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
31
|
return {
|
|
29
32
|
mdeck: VERSION,
|
|
30
|
-
themes: Object.values(manifestsOf(registry, 'theme')).map(theme => ({ id: theme.id, title: theme.title, description: theme.description ?? '',
|
|
31
|
-
palette: theme.palette, appearance: theme.appearance ?? 'light', palettes: palettesFor(theme, palettes).map(p => p.id)
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
themes: Object.values(manifestsOf(registry, 'theme')).map(theme => ({ id: theme.id, title: theme.title, description: theme.description ?? '', builtIn: builtIn.has(theme.id + ':t'),
|
|
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) } : {}) })),
|
|
36
|
+
palettes: Object.values(palettes).map(palette => ({ id: palette.id, title: palette.title, description: palette.description ?? '', builtIn: builtIn.has(palette.id + ':p'),
|
|
37
|
+
...(palette.theme ? { theme: palette.theme } : {}), light: colours(palette.light), dark: colours(palette.dark) })),
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
export async function
|
|
41
|
+
export async function runPackages({ kind, positionals, flag, output, ui: { ok, err, tip, c } }) {
|
|
42
|
+
const kinds = `${kind}s`
|
|
38
43
|
const [sub = 'search', ...rest] = positionals
|
|
39
44
|
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
|
|
41
|
-
const
|
|
45
|
+
const warnOffline = catalogue => { if (catalogue.offline) tip(`The theme repository could not be reached (${catalogue.offline}); showing the ${kinds} that come with mdeck.`) }
|
|
46
|
+
const label = step => `the ${step.kind} ${step.id}`
|
|
42
47
|
try {
|
|
43
48
|
if (sub === 'search') {
|
|
44
|
-
const catalogue = await loadCatalogue(
|
|
49
|
+
const catalogue = await loadCatalogue()
|
|
45
50
|
warnOffline(catalogue)
|
|
46
51
|
const words = rest.map(word => word.toLowerCase())
|
|
47
|
-
const
|
|
48
|
-
const found = catalogue.
|
|
49
|
-
if (!found.length) return tip(`No
|
|
50
|
-
for (const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
:
|
|
55
|
-
:
|
|
56
|
-
console.log(`\n ${c.cyan}${
|
|
57
|
-
if (
|
|
58
|
-
|
|
52
|
+
const forEvery = installedPackages(userExtensionsDir()), here = installedPackages(resolve('extensions')), hidden = removedBuiltIns()
|
|
53
|
+
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)))
|
|
54
|
+
if (!found.length) return tip(`No ${kind} matches${words.length ? ` "${words.join(' ')}"` : ''}`)
|
|
55
|
+
for (const entry of found) {
|
|
56
|
+
const key = keyOf(kind, entry.id)
|
|
57
|
+
const have = here[key] ?? forEvery[key]
|
|
58
|
+
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}`)
|
|
59
|
+
: entry.builtIn ? (hidden.includes(key) ? ` ${c.dim}(comes with mdeck, removed)${c.reset}` : ` ${c.green}(comes with mdeck)${c.reset}`)
|
|
60
|
+
: entry.builtInVersion ? ` ${c.yellow}(comes with mdeck as ${entry.builtInVersion}, update available)${c.reset}` : ''
|
|
61
|
+
console.log(`\n ${c.cyan}${entry.id}${c.reset} ${entry.version} — ${entry.title}${state}`)
|
|
62
|
+
if (entry.description) console.log(` ${entry.description}`)
|
|
63
|
+
const facts = [kind === 'theme' ? `palette: ${entry.palette}` : entry.theme ? `only for the theme ${entry.theme}` : '', entry.author ? `by ${entry.author}` : '', entry.license].filter(Boolean)
|
|
64
|
+
if (facts.length) console.log(` ${c.dim}${facts.join(' · ')}${c.reset}`)
|
|
59
65
|
}
|
|
60
66
|
console.log()
|
|
61
|
-
return tip(
|
|
67
|
+
return tip(`Install one for every deck with: mdeck ${kinds} install <${kind}> (beside a deck: add slides.md or --local)`)
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
if (sub === 'install') {
|
|
65
71
|
const [id, where] = rest
|
|
66
|
-
if (!id) { err(
|
|
72
|
+
if (!id) { err(`Name a ${kind}.`); return tip(`Usage: mdeck ${kinds} install <${kind}> [slides.md or folder] [--local]; mdeck ${kinds} search lists them.`) }
|
|
67
73
|
const at = target(where, local)
|
|
68
|
-
const catalogue = await loadCatalogue(
|
|
74
|
+
const catalogue = await loadCatalogue()
|
|
69
75
|
warnOffline(catalogue)
|
|
70
|
-
|
|
71
|
-
for (const step of await installHere(catalogue, id, at, { mdeckVersion: VERSION, force })) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else
|
|
76
|
+
findEntry(catalogue, kind, id)
|
|
77
|
+
for (const step of await installHere(catalogue, kind, id, at, { mdeckVersion: VERSION, force })) {
|
|
78
|
+
const what = step.requested ? label(step) : `${label(step)}, which it needs,`
|
|
79
|
+
if (step.kept) tip(`${what[0].toUpperCase() + what.slice(1)} is there already`)
|
|
80
|
+
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
|
|
81
|
+
else ok(`${step.replaced ? `Updated ${label(step)} from ${step.replaced} to ${step.version}` : `Installed ${label(step)} ${step.version}`} ${at.label}`)
|
|
75
82
|
}
|
|
76
|
-
|
|
77
|
-
|
|
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.`)
|
|
80
|
-
return tip('Look at it on a sample deck: mdeck design')
|
|
83
|
+
tip(`Use it with "${kind}: ${id}" at the top of a slide file. Look at it on a sample deck: mdeck design`)
|
|
84
|
+
return
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
if (sub === 'remove') {
|
|
84
88
|
const [id, where] = rest
|
|
85
|
-
if (!id) { err(
|
|
89
|
+
if (!id) { err(`Name a ${kind}.`); return tip(`Usage: mdeck ${kinds} remove <${kind}> [slides.md or folder] [--local]`) }
|
|
86
90
|
const at = target(where, local)
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (removed.hidden) ok(`${id} comes with mdeck and is no longer offered; mdeck themes install ${id} brings it back`)
|
|
91
|
+
for (const step of removeHere(kind, id, at, { force })) {
|
|
92
|
+
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}`)
|
|
93
|
+
}
|
|
91
94
|
return
|
|
92
95
|
}
|
|
93
96
|
|
|
@@ -95,19 +98,18 @@ export async function runThemes({ positionals, flag, output, ui: { ok, err, tip,
|
|
|
95
98
|
const [first, second] = rest
|
|
96
99
|
const named = first && !/\.md$/i.test(first) && !(existsSync(first) && statSync(first).isDirectory()) ? first : null
|
|
97
100
|
const at = target(named ? second : first, local)
|
|
98
|
-
const catalogue = await loadCatalogue(
|
|
101
|
+
const catalogue = await loadCatalogue()
|
|
99
102
|
warnOffline(catalogue)
|
|
100
|
-
const installed =
|
|
101
|
-
const ids = named ? [named] :
|
|
102
|
-
if (!ids.length) return tip(`No
|
|
103
|
+
const installed = Object.values(installedPackages(at.root)).filter(item => item.kind === kind)
|
|
104
|
+
const ids = named ? [named] : installed.map(item => item.id)
|
|
105
|
+
if (!ids.length) return tip(`No ${kinds} installed ${at.label}`)
|
|
103
106
|
for (const id of ids) {
|
|
104
|
-
const have = installed
|
|
105
|
-
if (!have) throw new
|
|
106
|
-
const entry =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
ok(`Updated ${id} ${have.version ?? entry.bundledVersion} → ${steps.at(-1).version}`)
|
|
107
|
+
const have = installed.find(item => item.id === id)
|
|
108
|
+
if (!have) throw new PackageError(`The ${kind} ${id} is not installed ${at.label}`)
|
|
109
|
+
const entry = findEntry(catalogue, kind, id)
|
|
110
|
+
if (entry.builtIn || compareVersions(entry.version, have.version ?? '0.0.0') <= 0) { tip(`The ${kind} ${id} ${have.version} is up to date`); continue }
|
|
111
|
+
await installHere(catalogue, kind, id, at, { mdeckVersion: VERSION, force })
|
|
112
|
+
ok(`Updated the ${kind} ${id} ${have.version} → ${entry.version}`)
|
|
111
113
|
}
|
|
112
114
|
loadRegistry(at.deck)
|
|
113
115
|
return
|
|
@@ -115,54 +117,59 @@ export async function runThemes({ positionals, flag, output, ui: { ok, err, tip,
|
|
|
115
117
|
|
|
116
118
|
if (sub === 'list') {
|
|
117
119
|
const [where] = rest
|
|
118
|
-
const hidden =
|
|
120
|
+
const hidden = removedBuiltIns()
|
|
121
|
+
const catalogue = await loadCatalogue({ online: false })
|
|
119
122
|
console.log(`\n ${c.bold}Come with mdeck${c.reset}`)
|
|
120
|
-
for (const
|
|
123
|
+
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
124
|
const sections = [['For every deck', userExtensionsDir()], ...(where || local ? [['Beside the deck', target(where, true).root]] : [])]
|
|
122
|
-
for (const [
|
|
123
|
-
const
|
|
124
|
-
console.log(`\n ${c.bold}${
|
|
125
|
-
if (!
|
|
126
|
-
for (const
|
|
125
|
+
for (const [title, root] of sections) {
|
|
126
|
+
const items = Object.values(installedPackages(root)).filter(item => item.kind === kind)
|
|
127
|
+
console.log(`\n ${c.bold}${title}${c.reset} ${c.dim}${root}${c.reset}`)
|
|
128
|
+
if (!items.length) console.log(` ${c.dim}none${c.reset}`)
|
|
129
|
+
for (const item of items) console.log(` ${c.cyan}${item.id}${c.reset} ${item.version}`)
|
|
127
130
|
}
|
|
128
131
|
console.log()
|
|
129
132
|
return
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
if (sub === 'build') {
|
|
133
|
-
const [
|
|
135
|
+
if (sub === 'build' && kind === 'theme') {
|
|
136
|
+
const [repoDir] = rest
|
|
134
137
|
const out = output()
|
|
135
|
-
if (!
|
|
136
|
-
const
|
|
137
|
-
const index = buildRepository(resolve(packsDir), resolve(out), { bundled })
|
|
138
|
+
if (!repoDir || !out) { err('Name the repository folder and the output folder.'); return tip('Usage: mdeck themes build <repository folder> -o <output folder> [--check]') }
|
|
139
|
+
const { catalogue, dirs } = buildRepository(resolve(repoDir), resolve(out))
|
|
138
140
|
// The repository's own themes and palettes go beside the sample deck;
|
|
139
|
-
// the
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
const paletteTheme = id => /^theme\s*=\s*"([^"]+)"/m.exec(readFileSync(resolve(dirOf(id), 'extension.toml'), 'utf8'))?.[1] ?? 'neue'
|
|
141
|
+
// the built-in ones are in it already.
|
|
142
|
+
const repo = [...catalogue.themes.map(e => ({ ...e, kind: 'theme' })), ...catalogue.palettes.map(e => ({ ...e, kind: 'palette' }))].filter(entry => !entry.builtIn)
|
|
143
|
+
const extensions = Object.fromEntries(repo.map(entry => [keyOf(entry.kind, entry.id), dirs[keyOf(entry.kind, entry.id)]]))
|
|
143
144
|
const { checkLooks, renderPreviews, buildSampleDeck } = await import('../build/themeCheck.js')
|
|
144
|
-
const looks =
|
|
145
|
+
const looks = [...catalogue.themes, ...catalogue.palettes.map(p => ({ ...p, isPalette: true }))].map(entry => entry.isPalette
|
|
146
|
+
? { key: `palettes/${entry.id}`, theme: entry.theme ?? 'neue', palette: entry.id }
|
|
147
|
+
: { key: `themes/${entry.id}`, theme: entry.id })
|
|
145
148
|
if (flag('--check')) {
|
|
146
149
|
const failures = await checkLooks(looks, { extensions })
|
|
147
150
|
if (failures.length) { err(`The theme check found ${failures.length} problem(s):`); for (const line of failures) console.error(` ${line}`); process.exitCode = 1; return }
|
|
148
|
-
ok(`Checked ${
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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` })
|
|
159
|
+
for (const entry of catalogue.palettes) entry.preview = `previews/palettes/${entry.id}.webp`
|
|
160
|
+
writeFileSync(resolve(out, 'catalogue.json'), JSON.stringify(catalogue, null, 2) + '\n')
|
|
154
161
|
// For the repository's theme browser: the sample deck with every theme
|
|
155
162
|
// and palette, and what each theme offers.
|
|
156
163
|
const registry = buildSampleDeck({ extensions, outDir: resolve(out, 'preview') })
|
|
157
|
-
writeFileSync(resolve(out, 'looks.json'), JSON.stringify(looksOf(registry,
|
|
158
|
-
return ok(`Built ${
|
|
164
|
+
writeFileSync(resolve(out, 'looks.json'), JSON.stringify(looksOf(registry, catalogue), null, 2) + '\n')
|
|
165
|
+
return ok(`Built ${catalogue.themes.length} themes and ${catalogue.palettes.length} palettes into ${resolve(out)}`)
|
|
159
166
|
}
|
|
160
167
|
|
|
161
|
-
err(`Unknown: mdeck
|
|
162
|
-
tip(`Use: mdeck
|
|
168
|
+
err(`Unknown: mdeck ${kinds} ${sub}`)
|
|
169
|
+
tip(`Use: mdeck ${kinds} ${usage(kind)}`)
|
|
163
170
|
process.exitCode = 1
|
|
164
171
|
} catch (error) {
|
|
165
|
-
if (!(error instanceof
|
|
172
|
+
if (!(error instanceof PackageError || error instanceof ManifestError)) throw error
|
|
166
173
|
err(error.message)
|
|
167
174
|
process.exitCode = 1
|
|
168
175
|
}
|
package/src/editor/api.js
CHANGED
|
@@ -52,24 +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
|
-
// new registry.
|
|
64
|
-
export async function
|
|
65
|
-
const response = await fetch('/__mdeck/themes/install', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) })
|
|
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 }) })
|
|
66
67
|
if (!response.ok) throw await failure(response)
|
|
67
68
|
return response.json()
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
// Removes a
|
|
71
|
-
export async function
|
|
72
|
-
const response = await fetch('/__mdeck/themes/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) })
|
|
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 }) })
|
|
73
74
|
if (!response.ok) throw await failure(response)
|
|
74
75
|
return response.json()
|
|
75
76
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
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,
|
|
4
|
+
import { loadExtension, saveExtension, deleteExtension, installPackage, removePackage } from '../api.js'
|
|
5
5
|
import { starterFiles, parseManifest, toModel, toRuntimeManifest, MANIFEST } from '../extensions.js'
|
|
6
6
|
import { sampleDeck, sampleDataUrl } from '../sampleDeck.js'
|
|
7
7
|
import { parseTarget, launchPageUrl } from '../designLink.js'
|
|
@@ -118,18 +118,16 @@ export function DesignApp() {
|
|
|
118
118
|
} catch (caught) { setError(caught.message) }
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
// A
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const result = await installThemePack(pack.id)
|
|
121
|
+
// A theme or palette installed for every deck, then opened.
|
|
122
|
+
const installOnline = async (kind, id) => {
|
|
123
|
+
const result = await installPackage(kind, id)
|
|
125
124
|
dispatch({ type: 'setRegistry', registry: result.registry })
|
|
126
|
-
|
|
127
|
-
if (id) await select(kind, id)
|
|
125
|
+
await select(kind, id)
|
|
128
126
|
}
|
|
129
127
|
|
|
130
|
-
// Removes a
|
|
131
|
-
const removeOnline = async
|
|
132
|
-
const result = await
|
|
128
|
+
// Removes a theme or palette for every deck; the deck's look is not changed.
|
|
129
|
+
const removeOnline = async (kind, id) => {
|
|
130
|
+
const result = await removePackage(kind, id)
|
|
133
131
|
dispatch({ type: 'setRegistry', registry: result.registry })
|
|
134
132
|
setCurrent(null)
|
|
135
133
|
}
|
|
@@ -253,7 +251,7 @@ export function DesignApp() {
|
|
|
253
251
|
? <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>
|
|
254
252
|
: current.source === 'local'
|
|
255
253
|
? <>{usage}<ExtensionEditor extension={current} palettes={manifests.palettes} status={status} error={error} note={note} onFiles={onFiles} onDelete={remove} onCopy={() => copyDialog(current.kind, current.id)} /></>
|
|
256
|
-
: <BuiltInCard extension={current} manifest={manifests[`${current.kind}s`][current.id]} onCopy={() => copyDialog(current.kind, current.id)}
|
|
254
|
+
: <BuiltInCard extension={current} manifest={manifests[`${current.kind}s`][current.id]} onCopy={() => copyDialog(current.kind, current.id)} onRemove={removeOnline} />}
|
|
257
255
|
</aside>
|
|
258
256
|
</div>
|
|
259
257
|
{dialog && <NewExtensionDialog kind={dialog.kind} initial={dialog.initial ?? null} registry={state.registry} existing={existing} onCreate={create} onClose={() => setDialog(null)} />}
|
|
@@ -45,30 +45,29 @@ export function ExtensionEditor({ extension, palettes = {}, status, error, note,
|
|
|
45
45
|
</>
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// A built-in or installed
|
|
49
|
-
// it
|
|
50
|
-
export function BuiltInCard({ extension, manifest, onCopy,
|
|
48
|
+
// A built-in or installed theme or palette: what it is, the one way to
|
|
49
|
+
// change it (a copy in the deck's extensions folder), and removing it.
|
|
50
|
+
export function BuiltInCard({ extension, manifest, onCopy, onRemove }) {
|
|
51
51
|
const { kind, id, files } = extension
|
|
52
52
|
const user = extension.source === 'user'
|
|
53
|
-
const pack = manifest?.pack ?? null
|
|
54
53
|
const [removing, setRemoving] = useState(null)
|
|
55
54
|
return <div class="form">
|
|
56
|
-
<p class="section-title">{user ? `Installed for every deck` : `Comes with mdeck`}{
|
|
55
|
+
<p class="section-title">{user ? `Installed for every deck` : `Comes with mdeck`}{manifest?.version ? ` · ${manifest.version}` : ''}</p>
|
|
57
56
|
<h2 class="builtin-title">{manifest?.title ?? id} <span class="muted">{id}</span></h2>
|
|
58
57
|
{manifest?.description && <p class="builtin-description">{manifest.description}</p>}
|
|
59
58
|
{kind === 'palette' && manifest && ['light', 'dark'].map(mode => <Field key={mode} label={mode === 'light' ? 'Light' : 'Dark'}><Swatches tokens={manifest[mode] ?? {}} /></Field>)}
|
|
60
59
|
{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>}
|
|
61
|
-
<p class="note">{user ? `Installed ${kind}s change only when they are updated (mdeck
|
|
60
|
+
<p class="note">{user ? `Installed ${kind}s change only when they are updated (mdeck ${kind}s 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
61
|
<div class="row" style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
|
63
62
|
<button class="btn is-primary" onClick={onCopy}>Copy to customise</button>
|
|
64
|
-
{
|
|
65
|
-
? <><span class="muted" style={{ alignSelf: 'center' }}>Remove the
|
|
66
|
-
<button class="btn" onClick={async () => { setRemoving('busy'); try { await
|
|
63
|
+
{onRemove && (removing === 'confirm'
|
|
64
|
+
? <><span class="muted" style={{ alignSelf: 'center' }}>Remove the {kind} {id} for every deck?</span>
|
|
65
|
+
<button class="btn" onClick={async () => { setRemoving('busy'); try { await onRemove(kind, id); setRemoving(null) } catch (error) { setRemoving({ error: error.message }) } }}>Remove</button>
|
|
67
66
|
<button class="btn" onClick={() => setRemoving(null)}>Keep</button></>
|
|
68
|
-
: <button class="btn" disabled={removing === 'busy'} onClick={() => setRemoving('confirm')}>Remove
|
|
67
|
+
: <button class="btn" disabled={removing === 'busy'} onClick={() => setRemoving('confirm')}>Remove {kind}</button>)}
|
|
69
68
|
</div>
|
|
70
69
|
{removing?.error && <p class="online-error">{removing.error}</p>}
|
|
71
|
-
{
|
|
70
|
+
{!user && <p class="hint-text">Removing a {kind} that comes with mdeck hides it; install it again from “More to install”.</p>}
|
|
72
71
|
<details class="builtin-source">
|
|
73
72
|
<summary>{MANIFEST}</summary>
|
|
74
73
|
<pre>{files?.[MANIFEST] ?? ''}</pre>
|
|
@@ -3,13 +3,13 @@ import { useEffect, useState } from 'preact/hooks'
|
|
|
3
3
|
import { ID_RE } from '../extensions.js'
|
|
4
4
|
import { Icon } from '../../components/Icon.jsx'
|
|
5
5
|
import { LookThumbnail } from './LookThumbnail.jsx'
|
|
6
|
-
import {
|
|
6
|
+
import { loadCatalogue } from '../api.js'
|
|
7
7
|
|
|
8
8
|
const KINDS = [['theme', 'Themes'], ['palette', 'Palettes']]
|
|
9
9
|
|
|
10
10
|
// The deck's own themes or palettes, to change; those installed for every
|
|
11
|
-
// deck;
|
|
12
|
-
//
|
|
11
|
+
// deck; the built-in ones; and those to install from the theme repository.
|
|
12
|
+
// A new theme always starts as a copy; a palette can also start from
|
|
13
13
|
// readable plain colours. The pictures show the sample deck as `view` says:
|
|
14
14
|
// palettes in the preview's theme where it offers them, themes with its
|
|
15
15
|
// palette where they offer it, both in its light or dark.
|
|
@@ -44,24 +44,24 @@ export function ExtensionsPanel({ registry, view = {}, owner = "This deck's", se
|
|
|
44
44
|
</>}
|
|
45
45
|
<div class="panel-section-head"><p class="section-title">Come with mdeck</p></div>
|
|
46
46
|
<ol class="outline-list">{builtIn.map(row)}</ol>
|
|
47
|
-
<
|
|
47
|
+
<OnlinePackages key="online" kind={kind} onInstall={onInstall} />
|
|
48
48
|
</aside>
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
function
|
|
51
|
+
// Themes or palettes not available yet: from the theme repository, and
|
|
52
|
+
// built-in ones that were removed. Loaded on request, since it needs the
|
|
53
|
+
// network. Installing makes one available to every deck; a theme brings its
|
|
54
|
+
// default palette along.
|
|
55
|
+
function OnlinePackages({ kind, onInstall }) {
|
|
56
56
|
const [state, setState] = useState({ status: 'idle' })
|
|
57
57
|
const [busy, setBusy] = useState(null)
|
|
58
58
|
const load = () => {
|
|
59
|
-
setState(prev => ({ ...prev, status: prev.
|
|
60
|
-
|
|
59
|
+
setState(prev => ({ ...prev, status: prev.catalogue ? 'ready' : 'loading' }))
|
|
60
|
+
loadCatalogue().then(catalogue => setState({ status: 'ready', catalogue }), error => setState({ status: 'error', error: error.message }))
|
|
61
61
|
}
|
|
62
|
-
const install = async
|
|
63
|
-
setBusy(
|
|
64
|
-
try { await onInstall(
|
|
62
|
+
const install = async entry => {
|
|
63
|
+
setBusy(entry.id)
|
|
64
|
+
try { await onInstall(entry.kind, entry.id); load() } catch (error) { setState(prev => ({ ...prev, error: error.message })) }
|
|
65
65
|
setBusy(null)
|
|
66
66
|
}
|
|
67
67
|
const head = <div class="panel-section-head"><p class="section-title">More to install</p></div>
|
|
@@ -69,22 +69,18 @@ function OnlinePacks({ kind, onInstall }) {
|
|
|
69
69
|
{state.status === 'loading' ? 'Loading…' : <button class="btn is-small" onClick={load}>Show what you can install</button>}
|
|
70
70
|
{state.error && <p class="online-error">{state.error}</p>}
|
|
71
71
|
</div></>
|
|
72
|
-
const {
|
|
73
|
-
const
|
|
72
|
+
const { catalogue } = state
|
|
73
|
+
const entries = catalogue.entries.filter(entry => entry.kind === kind && !entry.available)
|
|
74
74
|
return <>{head}
|
|
75
75
|
{state.error && <p class="panel-hint online-error">{state.error}</p>}
|
|
76
|
-
{
|
|
77
|
-
{!
|
|
78
|
-
<ol class="outline-list">{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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>
|
|
86
|
-
</li>
|
|
87
|
-
})}</ol>
|
|
76
|
+
{catalogue.offline && <p class="panel-hint">The theme repository could not be reached; showing what comes with mdeck.</p>}
|
|
77
|
+
{!entries.length && <p class="panel-hint">Every {kind} there is, is installed.</p>}
|
|
78
|
+
<ol class="outline-list">{entries.map(entry => <li key={entry.id} class="outline-item has-thumb online-pack">
|
|
79
|
+
{entry.preview && <div class="thumb"><img src={entry.preview} alt="" loading="lazy" /></div>}
|
|
80
|
+
<span><span class="title" style={{ display: 'block' }}>{entry.title} <span class="muted">{entry.version}</span></span>
|
|
81
|
+
<span class="online-meta">{entry.removed ? 'comes with mdeck, removed' : entry.author}{kind === 'theme' ? ` · with ${entry.palette}` : entry.theme ? ` · for ${entry.theme}` : ''}</span></span>
|
|
82
|
+
<button class="btn is-small" disabled={busy === entry.id} onClick={() => install(entry)}>{busy === entry.id ? 'Installing…' : 'Install'}</button>
|
|
83
|
+
</li>)}</ol>
|
|
88
84
|
</>
|
|
89
85
|
}
|
|
90
86
|
|