mdeck 3.0.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 +11 -1
- 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 +3 -3
- package/docs/reference/extensions.md +40 -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 +2 -1
- package/src/cli/args.js +2 -1
- package/src/cli/index.js +7 -6
- package/src/cli/themes.js +92 -91
- 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/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/cli/themes.js
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
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')
|
|
26
28
|
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
29
|
return {
|
|
29
30
|
mdeck: VERSION,
|
|
30
|
-
themes: Object.values(manifestsOf(registry, 'theme')).map(theme => ({ id: theme.id, title: theme.title, description: theme.description ?? '',
|
|
31
|
+
themes: Object.values(manifestsOf(registry, 'theme')).map(theme => ({ id: theme.id, title: theme.title, description: theme.description ?? '', builtIn: builtIn.has(theme.id + ':t'),
|
|
31
32
|
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 ?? '',
|
|
33
|
-
light: colours(palette.light), dark: colours(palette.dark) })),
|
|
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) })),
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export async function
|
|
38
|
+
export async function runPackages({ kind, positionals, flag, output, ui: { ok, err, tip, c } }) {
|
|
39
|
+
const kinds = `${kind}s`
|
|
38
40
|
const [sub = 'search', ...rest] = positionals
|
|
39
41
|
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
|
|
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}`
|
|
42
44
|
try {
|
|
43
45
|
if (sub === 'search') {
|
|
44
|
-
const catalogue = await loadCatalogue(
|
|
46
|
+
const catalogue = await loadCatalogue()
|
|
45
47
|
warnOffline(catalogue)
|
|
46
48
|
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
|
-
|
|
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}`)
|
|
59
62
|
}
|
|
60
63
|
console.log()
|
|
61
|
-
return tip(
|
|
64
|
+
return tip(`Install one for every deck with: mdeck ${kinds} install <${kind}> (beside a deck: add slides.md or --local)`)
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
if (sub === 'install') {
|
|
65
68
|
const [id, where] = rest
|
|
66
|
-
if (!id) { err(
|
|
69
|
+
if (!id) { err(`Name a ${kind}.`); return tip(`Usage: mdeck ${kinds} install <${kind}> [slides.md or folder] [--local]; mdeck ${kinds} search lists them.`) }
|
|
67
70
|
const at = target(where, local)
|
|
68
|
-
const catalogue = await loadCatalogue(
|
|
71
|
+
const catalogue = await loadCatalogue()
|
|
69
72
|
warnOffline(catalogue)
|
|
70
|
-
|
|
71
|
-
for (const step of await installHere(catalogue, id, at, { mdeckVersion: VERSION, force })) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else
|
|
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}`)
|
|
75
79
|
}
|
|
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')
|
|
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
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
if (sub === 'remove') {
|
|
84
85
|
const [id, where] = rest
|
|
85
|
-
if (!id) { err(
|
|
86
|
+
if (!id) { err(`Name a ${kind}.`); return tip(`Usage: mdeck ${kinds} remove <${kind}> [slides.md or folder] [--local]`) }
|
|
86
87
|
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`)
|
|
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
91
|
return
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -95,19 +95,18 @@ export async function runThemes({ positionals, flag, output, ui: { ok, err, tip,
|
|
|
95
95
|
const [first, second] = rest
|
|
96
96
|
const named = first && !/\.md$/i.test(first) && !(existsSync(first) && statSync(first).isDirectory()) ? first : null
|
|
97
97
|
const at = target(named ? second : first, local)
|
|
98
|
-
const catalogue = await loadCatalogue(
|
|
98
|
+
const catalogue = await loadCatalogue()
|
|
99
99
|
warnOffline(catalogue)
|
|
100
|
-
const installed =
|
|
101
|
-
const ids = named ? [named] :
|
|
102
|
-
if (!ids.length) return tip(`No
|
|
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}`)
|
|
103
103
|
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}`)
|
|
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}`)
|
|
111
110
|
}
|
|
112
111
|
loadRegistry(at.deck)
|
|
113
112
|
return
|
|
@@ -115,54 +114,56 @@ export async function runThemes({ positionals, flag, output, ui: { ok, err, tip,
|
|
|
115
114
|
|
|
116
115
|
if (sub === 'list') {
|
|
117
116
|
const [where] = rest
|
|
118
|
-
const hidden =
|
|
117
|
+
const hidden = removedBuiltIns()
|
|
118
|
+
const catalogue = await loadCatalogue({ online: false })
|
|
119
119
|
console.log(`\n ${c.bold}Come with mdeck${c.reset}`)
|
|
120
|
-
for (const
|
|
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
121
|
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
|
|
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}`)
|
|
127
127
|
}
|
|
128
128
|
console.log()
|
|
129
129
|
return
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
if (sub === 'build') {
|
|
133
|
-
const [
|
|
132
|
+
if (sub === 'build' && kind === 'theme') {
|
|
133
|
+
const [repoDir] = rest
|
|
134
134
|
const out = output()
|
|
135
|
-
if (!
|
|
136
|
-
const
|
|
137
|
-
const index = buildRepository(resolve(packsDir), resolve(out), { bundled })
|
|
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))
|
|
138
137
|
// 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'
|
|
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)]]))
|
|
143
141
|
const { checkLooks, renderPreviews, buildSampleDeck } = await import('../build/themeCheck.js')
|
|
144
|
-
const looks =
|
|
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 })
|
|
145
145
|
if (flag('--check')) {
|
|
146
146
|
const failures = await checkLooks(looks, { extensions })
|
|
147
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 }
|
|
148
|
-
ok(`Checked ${
|
|
148
|
+
ok(`Checked ${catalogue.themes.length} themes and ${catalogue.palettes.length} palettes, light and dark, on every kind of slide`)
|
|
149
149
|
}
|
|
150
150
|
// Themes on the title slide, palettes on the chapter slide (the third).
|
|
151
|
-
await renderPreviews(looks.map(look => ({ file: resolve(out, 'previews', `${look.
|
|
152
|
-
for (const
|
|
153
|
-
|
|
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')
|
|
154
155
|
// For the repository's theme browser: the sample deck with every theme
|
|
155
156
|
// and palette, and what each theme offers.
|
|
156
157
|
const registry = buildSampleDeck({ extensions, outDir: resolve(out, 'preview') })
|
|
157
|
-
writeFileSync(resolve(out, 'looks.json'), JSON.stringify(looksOf(registry,
|
|
158
|
-
return ok(`Built ${
|
|
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)}`)
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
err(`Unknown: mdeck
|
|
162
|
-
tip(`Use: mdeck
|
|
162
|
+
err(`Unknown: mdeck ${kinds} ${sub}`)
|
|
163
|
+
tip(`Use: mdeck ${kinds} ${usage(kind)}`)
|
|
163
164
|
process.exitCode = 1
|
|
164
165
|
} catch (error) {
|
|
165
|
-
if (!(error instanceof
|
|
166
|
+
if (!(error instanceof PackageError || error instanceof ManifestError)) throw error
|
|
166
167
|
err(error.message)
|
|
167
168
|
process.exitCode = 1
|
|
168
169
|
}
|
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
|
|